@phpsandbox/sdk 0.1.19 → 0.2.0

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.
@@ -0,0 +1,46 @@
1
+ import { RemoteError } from '../errors/index.js';
2
+ import { PublicationInstance } from './instance.js';
3
+ import { PublicationRun } from './run.js';
4
+ import { PublicationResources } from './resources.js';
5
+ export class NotebookPublication {
6
+ constructor(notebookId, client) {
7
+ this.notebookId = notebookId;
8
+ this.client = client;
9
+ this.resources = new PublicationResources(client, `${this.path}/publication/resources`);
10
+ }
11
+ get path() {
12
+ return `/notebook/${encodeURIComponent(this.notebookId)}`;
13
+ }
14
+ async current() {
15
+ try {
16
+ const response = await this.client.get(`${this.path}/publication`);
17
+ return new PublicationInstance(response.data, this.client, this.notebookId);
18
+ }
19
+ catch (error) {
20
+ if (RemoteError.is(error) && error.status === 404) {
21
+ return null;
22
+ }
23
+ throw error;
24
+ }
25
+ }
26
+ async readiness() {
27
+ return (await this.client.get(`${this.path}/publication/readiness`)).data;
28
+ }
29
+ /** Review a publishing plan without committing changes or creating resources. */
30
+ async prepare(input) {
31
+ return (await this.client.post(`${this.path}/publication/plan`, input)).data;
32
+ }
33
+ async catalog(provider, serverId) {
34
+ const query = new URLSearchParams({ provider });
35
+ if (serverId !== undefined) {
36
+ query.set('serverId', serverId);
37
+ }
38
+ return (await this.client.get(`${this.path}/publication/catalog?${query}`))
39
+ .data;
40
+ }
41
+ /** Core validates the plan, prepares source and executes publishing. */
42
+ async publish(input) {
43
+ const response = await this.client.post(`${this.path}/publication`, input);
44
+ return new PublicationRun(new PublicationInstance(response.data, this.client, this.notebookId), this.client).start();
45
+ }
46
+ }
@@ -0,0 +1,114 @@
1
+ export type LaravelCloudRegion = 'us-east-2' | 'us-east-1' | 'ca-central-1' | 'eu-central-1' | 'eu-west-1' | 'eu-west-2' | 'me-central-1' | 'ap-southeast-1' | 'ap-southeast-2' | 'ap-northeast-1';
2
+ export interface LaravelCloudResourceInput {
3
+ mode: 'none' | 'reuse' | 'create' | 'external';
4
+ id?: string;
5
+ type?: string;
6
+ version?: string;
7
+ size?: string;
8
+ config?: Record<string, string | number | boolean | null>;
9
+ }
10
+ export interface LaravelCloudSetupInput {
11
+ repositoryAccessConfirmed?: boolean;
12
+ database?: LaravelCloudResourceInput;
13
+ cache?: LaravelCloudResourceInput;
14
+ storage?: LaravelCloudResourceInput;
15
+ worker?: boolean;
16
+ scheduler?: boolean;
17
+ workerPlacement?: 'app' | 'separate';
18
+ workerSize?: string;
19
+ buildCommand?: string;
20
+ deployCommand?: string;
21
+ }
22
+ export interface LaravelCloudCatalogResource {
23
+ id: string;
24
+ name: string;
25
+ region: string | null;
26
+ type: string | null;
27
+ status: string | null;
28
+ clusterId?: string;
29
+ bucketId?: string;
30
+ keyName?: string;
31
+ }
32
+ export interface LaravelCloudConfigField {
33
+ name: string;
34
+ type: string;
35
+ required: boolean;
36
+ nullable?: boolean;
37
+ description?: string;
38
+ min?: number;
39
+ max?: number;
40
+ enum?: Array<string | number>;
41
+ example?: string | number;
42
+ }
43
+ export interface LaravelCloudDatabaseType {
44
+ type: string;
45
+ label: string;
46
+ versions: string[];
47
+ regions: string[];
48
+ config_schema: LaravelCloudConfigField[];
49
+ }
50
+ export interface LaravelCloudCatalog {
51
+ regions: Array<{
52
+ region: string;
53
+ label: string;
54
+ flag?: string;
55
+ }>;
56
+ databaseTypes: LaravelCloudDatabaseType[];
57
+ cacheTypes: Array<{
58
+ type: string;
59
+ label: string;
60
+ regions: string[];
61
+ sizes: Array<string | {
62
+ value?: string;
63
+ name?: string;
64
+ size?: string;
65
+ label?: string;
66
+ supports_hibernation?: boolean;
67
+ }>;
68
+ }>;
69
+ databases: LaravelCloudCatalogResource[];
70
+ caches: LaravelCloudCatalogResource[];
71
+ storage: LaravelCloudCatalogResource[];
72
+ pricingUrl: string;
73
+ }
74
+ export interface LaravelCloudSetupState {
75
+ applicationId?: string;
76
+ environmentId?: string;
77
+ applicationManaged?: boolean;
78
+ environmentManaged?: boolean;
79
+ databaseId?: string;
80
+ databaseIdManaged?: boolean;
81
+ cacheId?: string;
82
+ cacheIdManaged?: boolean;
83
+ storageId?: string;
84
+ storageIdManaged?: boolean;
85
+ workerId?: string;
86
+ backgroundInstanceId?: string;
87
+ backgroundProcessId?: string;
88
+ configuredAt?: string;
89
+ retainedResources?: Array<{
90
+ kind: string;
91
+ id: string;
92
+ providerId: string;
93
+ name: string | null;
94
+ }>;
95
+ pendingCreation?: {
96
+ resource: string;
97
+ name: string | null;
98
+ startedAt: string;
99
+ };
100
+ }
101
+ export interface LaravelCloudProviderData {
102
+ setup?: LaravelCloudSetupInput;
103
+ region?: LaravelCloudRegion;
104
+ repository?: string;
105
+ branch?: string;
106
+ expectedCommitSha?: string | null;
107
+ sourceControlProviderType?: 'github' | 'gitlab' | 'bitbucket';
108
+ }
109
+ export interface LaravelCloudProviderInput {
110
+ setup?: LaravelCloudSetupInput;
111
+ region: LaravelCloudRegion;
112
+ }
113
+ /** Keep retained resources when publishing again after removing the Cloud application. */
114
+ export declare function laravelCloudSetupForRepublish(setup?: LaravelCloudSetupInput, state?: LaravelCloudSetupState | null): LaravelCloudSetupInput;
@@ -0,0 +1,13 @@
1
+ /** Keep retained resources when publishing again after removing the Cloud application. */
2
+ export function laravelCloudSetupForRepublish(setup = {}, state = null) {
3
+ const next = { ...setup };
4
+ const ids = { database: state?.databaseId, cache: state?.cacheId, storage: state?.storageId };
5
+ const kinds = ['database', 'cache', 'storage'];
6
+ for (const kind of kinds) {
7
+ const id = ids[kind];
8
+ if (setup[kind]?.mode === 'create' && id) {
9
+ next[kind] = { mode: 'reuse', id };
10
+ }
11
+ }
12
+ return next;
13
+ }
@@ -0,0 +1,9 @@
1
+ import type { Client, PaginatedApiResponse } from '../index.js';
2
+ import type { PublicationManagedResource, PublicationResourceListOptions } from './types.js';
3
+ export declare class PublicationResources {
4
+ private readonly client;
5
+ private readonly path;
6
+ constructor(client: Client, path?: string);
7
+ inspect(id: string): Promise<PublicationManagedResource>;
8
+ list(options?: PublicationResourceListOptions): Promise<PaginatedApiResponse<PublicationManagedResource>>;
9
+ }
@@ -0,0 +1,19 @@
1
+ export class PublicationResources {
2
+ constructor(client, path = '/publication-resources') {
3
+ this.client = client;
4
+ this.path = path;
5
+ }
6
+ async inspect(id) {
7
+ return (await this.client.post(`/publication-resources/${encodeURIComponent(id)}/inspect`, {})).data;
8
+ }
9
+ async list(options = {}) {
10
+ const query = new URLSearchParams();
11
+ for (const [name, value] of Object.entries(options)) {
12
+ if (value !== undefined) {
13
+ query.set(name, String(value));
14
+ }
15
+ }
16
+ const suffix = query.size > 0 ? `?${query.toString()}` : '';
17
+ return this.client.get(`${this.path}${suffix}`);
18
+ }
19
+ }
@@ -0,0 +1,30 @@
1
+ import type { PublicationProviderName } from './types.js';
2
+ import type { Client } from '../index.js';
3
+ import type { PublishStreamEvent, PublishStreamResult } from '../schemas/publications.js';
4
+ import { PublicationInstance } from './instance.js';
5
+ export declare class PublicationRun<TProvider extends PublicationProviderName = PublicationProviderName> {
6
+ readonly initial: PublicationInstance<TProvider>;
7
+ private readonly client;
8
+ private readonly streamAbort;
9
+ private started;
10
+ private completed;
11
+ private finalError;
12
+ private finalResult;
13
+ private resultPromise;
14
+ private resolveResult;
15
+ private rejectResult;
16
+ private readonly bufferedEvents;
17
+ private readonly subscribers;
18
+ constructor(initial: PublicationInstance<TProvider>, client: Client);
19
+ events(): AsyncIterable<PublishStreamEvent>;
20
+ result(): Promise<PublishStreamResult>;
21
+ publication(): Promise<PublicationInstance<TProvider>>;
22
+ start(): this;
23
+ /** Stop observing this run. Once its stream has attached, the server-side publication continues. */
24
+ dispose(): void;
25
+ private subscribe;
26
+ private consumeStream;
27
+ private publish;
28
+ private closeSubscribers;
29
+ private fail;
30
+ }
@@ -1,34 +1,5 @@
1
- import { PublicationEventDataSchema, PublicationLogChunkDataSchema, PublishStreamSseEventNameSchema, PublishStreamSseEventSchema, } from './schemas/publications.js';
2
- export class PublicationDomains {
3
- constructor(client, publicationId) {
4
- this.client = client;
5
- this.publicationId = publicationId;
6
- }
7
- async list() {
8
- return (await this.client.get(this.path())).data;
9
- }
10
- async create(hostname) {
11
- return (await this.client.post(this.path(), { hostname })).data;
12
- }
13
- async refresh(id) {
14
- return (await this.client.post(`${this.path()}/${encodeURIComponent(id)}/refresh`)).data;
15
- }
16
- async delete(id) {
17
- await this.client.delete(`${this.path()}/${encodeURIComponent(id)}`);
18
- }
19
- path() {
20
- return `/publications/${encodeURIComponent(this.publicationId)}/domains`;
21
- }
22
- }
23
- export class PublicationApi {
24
- constructor(client) {
25
- this.client = client;
26
- }
27
- async get(id) {
28
- const response = await this.client.get(`/publications/${id}`);
29
- return new PublicationInstance(response.data, this.client);
30
- }
31
- }
1
+ import { PublishStreamSseEventNameSchema, PublishStreamSseEventSchema } from '../schemas/publications.js';
2
+ import { PHPSandboxError } from '../errors/index.js';
32
3
  export class PublicationRun {
33
4
  constructor(initial, client) {
34
5
  this.initial = initial;
@@ -59,6 +30,9 @@ export class PublicationRun {
59
30
  }
60
31
  result() {
61
32
  this.start();
33
+ if (!this.resultPromise) {
34
+ throw new PHPSandboxError('Publication stream could not start.');
35
+ }
62
36
  return this.resultPromise;
63
37
  }
64
38
  async publication() {
@@ -71,7 +45,7 @@ export class PublicationRun {
71
45
  }
72
46
  const eventStreamUrl = this.initial.data.eventStreamUrl;
73
47
  if (!eventStreamUrl) {
74
- throw new Error(`Publication ${this.initial.data.id} has no eventStreamUrl for SSE streaming`);
48
+ throw new PHPSandboxError(`Publication ${this.initial.data.id} has no eventStreamUrl for SSE streaming`);
75
49
  }
76
50
  this.started = true;
77
51
  this.resultPromise = new Promise((resolve, reject) => {
@@ -82,10 +56,11 @@ export class PublicationRun {
82
56
  void this.consumeStream(eventStreamUrl);
83
57
  return this;
84
58
  }
85
- /** Stop observing this run; the server-side publication continues. */
59
+ /** Stop observing this run. Once its stream has attached, the server-side publication continues. */
86
60
  dispose() {
87
- if (this.completed)
61
+ if (this.completed) {
88
62
  return;
63
+ }
89
64
  const error = new DOMException('Stopped observing publication; deployment continues.', 'AbortError');
90
65
  if (!this.started) {
91
66
  this.started = true;
@@ -123,12 +98,12 @@ export class PublicationRun {
123
98
  }
124
99
  }
125
100
  if (!this.finalResult) {
126
- throw new Error('Publish stream ended without result');
101
+ throw new PHPSandboxError('Publish stream ended without result');
127
102
  }
128
103
  this.closeSubscribers();
129
104
  }
130
105
  catch (err) {
131
- this.fail(err instanceof Error ? err : new Error(String(err)));
106
+ this.fail(err instanceof Error ? err : new PHPSandboxError(String(err), err));
132
107
  }
133
108
  }
134
109
  publish(event) {
@@ -154,79 +129,6 @@ export class PublicationRun {
154
129
  this.subscribers.clear();
155
130
  }
156
131
  }
157
- export class PublicationInstance {
158
- constructor(data, client, notebookId) {
159
- this.data = data;
160
- this.client = client;
161
- this.notebookId = notebookId;
162
- this.domains = new PublicationDomains(client, data.id);
163
- }
164
- async refresh() {
165
- const response = await this.client.get(`/publications/${this.data.id}`);
166
- return new PublicationInstance(response.data, this.client, this.notebookId);
167
- }
168
- async publish() {
169
- if (!this.notebookId) {
170
- return Promise.reject(new Error('Publishing requires a notebook-scoped publication instance.'));
171
- }
172
- const response = await this.client.post(`/notebook/${this.notebookId}/publication`);
173
- const run = new PublicationRun(new PublicationInstance(response.data, this.client, this.notebookId), this.client);
174
- run.start();
175
- return run;
176
- }
177
- async destroy() {
178
- await this.client.delete(`/publications/${this.data.id}`);
179
- }
180
- async wait(options = {}) {
181
- const intervalMs = options.intervalMs ?? 1000;
182
- const timeoutMs = options.timeoutMs ?? 300000;
183
- const startedAt = Date.now();
184
- let current = this;
185
- while (!publicationStatusIsTerminal(current.data.status)) {
186
- if (Date.now() - startedAt > timeoutMs) {
187
- throw new Error(`Timed out waiting for publication ${this.data.id}`);
188
- }
189
- await delay(intervalMs);
190
- current = await current.refresh();
191
- }
192
- return current;
193
- }
194
- async events() {
195
- return parseNdjsonStream(await this.client.stream(`/publications/${this.data.id}/events/stream`), PublicationEventDataSchema);
196
- }
197
- buildLogs(buildId = this.data.latestBuild?.id ?? '') {
198
- if (buildId === '') {
199
- return Promise.reject(new Error('Publication has no latest build id'));
200
- }
201
- return this.client.stream(`/publications/${this.data.id}/builds/${buildId}/logs/stream`)
202
- .then((stream) => parseNdjsonStream(stream, PublicationLogChunkDataSchema));
203
- }
204
- async logs() {
205
- return parseNdjsonStream(await this.client.stream(`/publications/${this.data.id}/logs/stream`), PublicationLogChunkDataSchema);
206
- }
207
- async setProtection(input) {
208
- const response = await this.client.put(`/publications/${this.data.id}/protection`, input);
209
- return new PublicationInstance(response.data, this.client, this.notebookId);
210
- }
211
- async protection() {
212
- const response = await this.client.get(`/publications/${this.data.id}/protection`);
213
- return response.data;
214
- }
215
- async createProtectionSession() {
216
- const response = await this.client.post(`/publications/${this.data.id}/protection/session`);
217
- return response.data;
218
- }
219
- async disableProtection() {
220
- const response = await this.client.delete(`/publications/${this.data.id}/protection`);
221
- return new PublicationInstance(response.data, this.client, this.notebookId);
222
- }
223
- }
224
- function publicationStatusIsTerminal(status) {
225
- return ['healthy', 'failed', 'stopped', 'deleted', 'delete_failed'].includes(status);
226
- }
227
- function delay(ms) {
228
- return new Promise((resolve) => setTimeout(resolve, ms));
229
- }
230
132
  class AsyncPublishEventQueue {
231
133
  constructor() {
232
134
  this.values = [];
@@ -363,27 +265,3 @@ function parseSSEBlock(block) {
363
265
  return { event: eventType, data };
364
266
  }
365
267
  }
366
- function parseNdjsonStream(input, schema) {
367
- const decoder = new TextDecoder();
368
- let buffer = '';
369
- return input.pipeThrough(new TransformStream({
370
- transform(chunk, controller) {
371
- buffer += decoder.decode(chunk, { stream: true });
372
- const lines = buffer.split('\n');
373
- buffer = lines.pop() ?? '';
374
- for (const line of lines) {
375
- const trimmed = line.trim();
376
- if (trimmed !== '') {
377
- controller.enqueue(schema.parse(JSON.parse(trimmed)));
378
- }
379
- }
380
- },
381
- flush(controller) {
382
- buffer += decoder.decode();
383
- const trimmed = buffer.trim();
384
- if (trimmed !== '') {
385
- controller.enqueue(schema.parse(JSON.parse(trimmed)));
386
- }
387
- },
388
- }));
389
- }