@usestatemachines/sdk 0.1.1 → 0.3.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.
- package/README.md +265 -75
- package/agents/SKILL.md +33 -46
- package/agents/index.js +7 -7
- package/agents/llms.txt +25 -40
- package/agents/prompt.md +11 -34
- package/esm/extensions/apps.d.ts +18 -0
- package/esm/extensions/apps.js +39 -0
- package/esm/extensions/client.d.ts +66 -3
- package/esm/extensions/client.js +236 -35
- package/esm/extensions/errors.d.ts +50 -6
- package/esm/extensions/errors.js +37 -3
- package/esm/extensions/index.d.ts +5 -3
- package/esm/extensions/index.js +2 -1
- package/esm/extensions/running-environment.d.ts +32 -0
- package/esm/extensions/running-environment.js +44 -0
- package/esm/extensions/signal.d.ts +1 -2
- package/esm/extensions/signal.js +9 -4
- package/esm/extensions/wait.d.ts +11 -3
- package/esm/extensions/wait.js +23 -13
- package/esm/funcs/create-environment-definition.d.ts +11 -0
- package/esm/funcs/create-environment-definition.js +94 -0
- package/esm/funcs/environments-list.js +1 -0
- package/esm/funcs/snapshots-capture.d.ts +11 -0
- package/esm/funcs/snapshots-capture.js +94 -0
- package/esm/funcs/snapshots-delete.d.ts +11 -0
- package/esm/funcs/snapshots-delete.js +92 -0
- package/esm/funcs/snapshots-get.d.ts +11 -0
- package/esm/funcs/snapshots-get.js +92 -0
- package/esm/funcs/snapshots-list.d.ts +11 -0
- package/esm/funcs/snapshots-list.js +98 -0
- package/esm/funcs/snapshots-update.d.ts +11 -0
- package/esm/funcs/snapshots-update.js +93 -0
- package/esm/lib/config.d.ts +2 -2
- package/esm/lib/config.js +2 -2
- package/esm/models/app-instance-snapshot.d.ts +15 -0
- package/esm/models/app-instance-snapshot.js +19 -0
- package/esm/models/connection-secret.d.ts +16 -0
- package/esm/models/connection-secret.js +13 -0
- package/esm/models/connection.d.ts +14 -0
- package/esm/models/connection.js +14 -0
- package/esm/models/create-environment-input.d.ts +6 -2
- package/esm/models/create-environment-input.js +3 -1
- package/esm/models/environment-app.d.ts +1 -0
- package/esm/models/environment-app.js +1 -0
- package/esm/models/environment-snapshot.d.ts +39 -0
- package/esm/models/environment-snapshot.js +41 -0
- package/esm/models/environment.d.ts +1 -0
- package/esm/models/environment.js +1 -0
- package/esm/models/error-response.d.ts +5 -0
- package/esm/models/error-response.js +5 -0
- package/esm/models/index.d.ts +2 -0
- package/esm/models/index.js +2 -0
- package/esm/models/operations/capture-snapshot.d.ts +41 -0
- package/esm/models/operations/capture-snapshot.js +36 -0
- package/esm/models/operations/create-environment-definition.d.ts +143 -0
- package/esm/models/operations/create-environment-definition.js +90 -0
- package/esm/models/operations/delete-snapshot.d.ts +23 -0
- package/esm/models/operations/delete-snapshot.js +20 -0
- package/esm/models/operations/get-snapshot.d.ts +23 -0
- package/esm/models/operations/get-snapshot.js +20 -0
- package/esm/models/operations/index.d.ts +6 -0
- package/esm/models/operations/index.js +6 -0
- package/esm/models/operations/list-environments.d.ts +2 -0
- package/esm/models/operations/list-environments.js +1 -0
- package/esm/models/operations/list-snapshots.d.ts +30 -0
- package/esm/models/operations/list-snapshots.js +25 -0
- package/esm/models/operations/reveal-connection.d.ts +26 -4
- package/esm/models/operations/reveal-connection.js +28 -2
- package/esm/models/operations/update-snapshot.d.ts +37 -0
- package/esm/models/operations/update-snapshot.js +29 -0
- package/esm/sdk/sdk.d.ts +6 -1
- package/esm/sdk/sdk.js +10 -0
- package/esm/sdk/snapshots.d.ts +9 -0
- package/esm/sdk/snapshots.js +27 -0
- package/openapi.json +1471 -144
- package/package.json +10 -11
|
@@ -2,15 +2,21 @@ import { type Fetcher, HTTPClient } from '../lib/http.js';
|
|
|
2
2
|
import type { Catalog } from '../models/catalog.js';
|
|
3
3
|
import type { Connection as ConnectionDetails } from '../models/connection.js';
|
|
4
4
|
import type { ConnectionSecret } from '../models/connection-secret.js';
|
|
5
|
-
import type { CreateEnvironmentInput } from '../models/create-environment-input.js';
|
|
6
5
|
import type { Credential } from '../models/credential.js';
|
|
7
6
|
import type { Environment } from '../models/environment.js';
|
|
7
|
+
import type { EnvironmentSnapshot } from '../models/environment-snapshot.js';
|
|
8
8
|
import type { EventPage } from '../models/event-page.js';
|
|
9
|
+
import type { CaptureSnapshotRequestBody } from '../models/operations/capture-snapshot.js';
|
|
9
10
|
import type { ListEnvironmentsRequest } from '../models/operations/list-environments.js';
|
|
11
|
+
import type { ListSnapshotsRequest } from '../models/operations/list-snapshots.js';
|
|
10
12
|
import type { ListWorkspacesRequest } from '../models/operations/list-workspaces.js';
|
|
13
|
+
import type { UpdateSnapshotRequestBody } from '../models/operations/update-snapshot.js';
|
|
11
14
|
import type { Release } from '../models/release.js';
|
|
15
|
+
import type { ReleaseActor } from '../models/release-actor.js';
|
|
12
16
|
import type { Workspace } from '../models/workspace.js';
|
|
17
|
+
import { type EnvironmentSource } from './apps.js';
|
|
13
18
|
import { Connection } from './connection.js';
|
|
19
|
+
import { RunningEnvironment } from './running-environment.js';
|
|
14
20
|
import { type WaitOptions } from './wait.js';
|
|
15
21
|
export interface StateMachinesOptions {
|
|
16
22
|
/** Workspace API key. Defaults to `STATE_API_KEY` where a `process` exists. */
|
|
@@ -38,8 +44,20 @@ export interface Page<T> {
|
|
|
38
44
|
data: T[];
|
|
39
45
|
nextCursor: string | null;
|
|
40
46
|
}
|
|
47
|
+
export interface StartOptions extends CreateOptions {
|
|
48
|
+
/** Deadline for the whole wait for readiness. Defaults to 10 minutes. */
|
|
49
|
+
readyTimeoutMs?: number;
|
|
50
|
+
}
|
|
51
|
+
export type CaptureInput = Omit<CaptureSnapshotRequestBody, 'environmentId'>;
|
|
52
|
+
/** `readyTimeoutMs` bounds the wait for `ready`, as in `start()`. */
|
|
53
|
+
export type CaptureOptions = StartOptions;
|
|
54
|
+
export type ListSnapshotsQuery = Omit<ListSnapshotsRequest, 'workspaceId'> & {
|
|
55
|
+
workspaceId?: string;
|
|
56
|
+
};
|
|
41
57
|
export interface ConnectOptions extends CallOptions {
|
|
58
|
+
/** Defaults to the actor of the environment's declared default connection, else the app's first actor. */
|
|
42
59
|
actorId?: string;
|
|
60
|
+
/** Required when the app has more than one API, as Salesforce does. */
|
|
43
61
|
apiId?: string;
|
|
44
62
|
}
|
|
45
63
|
export declare class StateMachines {
|
|
@@ -63,19 +81,64 @@ export declare class StateMachines {
|
|
|
63
81
|
readonly environments: {
|
|
64
82
|
list: (query?: ListEnvironmentsQuery, options?: CallOptions | undefined) => Promise<Page<Environment>>;
|
|
65
83
|
get: (environmentId: string, options?: CallOptions | undefined) => Promise<Environment>;
|
|
66
|
-
/**
|
|
67
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Creates an environment and waits until it's running. If the wait fails, stops the
|
|
86
|
+
* environment (unless the caller passed its own `idempotencyKey`), then rethrows the
|
|
87
|
+
* error with `environmentId`, `stopped` and `stopError`.
|
|
88
|
+
*/
|
|
89
|
+
start: (input: EnvironmentSource, options?: StartOptions) => Promise<RunningEnvironment>;
|
|
90
|
+
/**
|
|
91
|
+
* A string app is shorthand for `{ key: <the string>, appId: <the string> }`. Every
|
|
92
|
+
* failure carries the `idempotencyKey`; replaying the create with it is safe.
|
|
93
|
+
*/
|
|
94
|
+
create: (input: EnvironmentSource, options?: CreateOptions) => Promise<Environment>;
|
|
68
95
|
stop: (environmentId: string, options?: CallOptions | undefined) => Promise<Environment>;
|
|
69
96
|
events: (environmentId: string, query?: {
|
|
70
97
|
limit?: number;
|
|
71
98
|
cursor?: string;
|
|
72
99
|
}, options?: CallOptions | undefined) => Promise<EventPage>;
|
|
100
|
+
/** The app's users, in the release's order. */
|
|
101
|
+
actors: (environmentId: string, appKey: string, options?: CallOptions | undefined) => Promise<ReleaseActor[]>;
|
|
73
102
|
getConnection: (environmentId: string, appKey: string, options?: CallOptions | undefined) => Promise<ConnectionDetails>;
|
|
74
103
|
reveal: (environmentId: string, appKey: string, actorId: string, options?: CallOptions | undefined) => Promise<ConnectionSecret>;
|
|
75
104
|
/** Resolves when the environment is running. Never stops it. */
|
|
76
105
|
waitUntilReady: (environmentId: string, options?: WaitOptions | undefined) => Promise<Environment>;
|
|
77
106
|
/** Resolves when the environment ended and every sandbox is confirmed disposed. */
|
|
78
107
|
waitUntilStopped: (environmentId: string, options?: WaitOptions | undefined) => Promise<Environment>;
|
|
108
|
+
/**
|
|
109
|
+
* actorId defaults to the actor of the environment's declared default connection, or
|
|
110
|
+
* else the app's first actor (for Salesforce, the administrator). Pick another from
|
|
111
|
+
* actors() to act with that user's permissions.
|
|
112
|
+
*/
|
|
79
113
|
connect: (environmentId: string, appKey: string, options?: ConnectOptions) => Promise<Connection>;
|
|
80
114
|
};
|
|
115
|
+
readonly snapshots: {
|
|
116
|
+
/** Returns the API's answer at once: it may still be `capturing`. */
|
|
117
|
+
create: (environmentId: string, input?: CaptureInput, options?: CreateOptions) => Promise<EnvironmentSnapshot>;
|
|
118
|
+
/**
|
|
119
|
+
* Creates a snapshot and waits until it's `ready`. Needs `snapshots:create` and
|
|
120
|
+
* `snapshots:read`. A failed or timed-out snapshot is never deleted: the error
|
|
121
|
+
* carries `snapshotId` and `idempotencyKey`.
|
|
122
|
+
*/
|
|
123
|
+
capture: (environmentId: string, input?: CaptureInput, options?: StartOptions) => Promise<EnvironmentSnapshot>;
|
|
124
|
+
/** Resolves when the snapshot is `ready`. Never deletes it. */
|
|
125
|
+
waitUntilReady: (snapshotId: string, options?: WaitOptions | undefined) => Promise<EnvironmentSnapshot>;
|
|
126
|
+
/** Deleted snapshots too, with `deletedAt` set. */
|
|
127
|
+
get: (snapshotId: string, options?: CallOptions | undefined) => Promise<EnvironmentSnapshot>;
|
|
128
|
+
/** Newest first. Deleted snapshots are left out. */
|
|
129
|
+
list: (query?: ListSnapshotsQuery, options?: CallOptions | undefined) => Promise<Page<EnvironmentSnapshot>>;
|
|
130
|
+
/**
|
|
131
|
+
* `null` clears the name. A stale `expectedVersion`, or a deleted snapshot, is a 409
|
|
132
|
+
* `version-conflict`; so is a retry of a rename whose first answer was lost.
|
|
133
|
+
*/
|
|
134
|
+
rename: (snapshotId: string, body: UpdateSnapshotRequestBody, options?: CallOptions | undefined) => Promise<EnvironmentSnapshot>;
|
|
135
|
+
/** Marks the snapshot deleted and returns it; safe to repeat. A capturing snapshot is a 409 `not-ready`. */
|
|
136
|
+
delete: (snapshotId: string, options?: CallOptions | undefined) => Promise<EnvironmentSnapshot>;
|
|
137
|
+
};
|
|
81
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Names the call a request belongs to. Never sent: the attempt hook removes it.
|
|
141
|
+
* Needed because the generated loop clones each attempt, and after a garbage
|
|
142
|
+
* collection a clone's signal no longer follows the call's (Node 20 and 24).
|
|
143
|
+
*/
|
|
144
|
+
export declare const CALL_HEADER = "x-statemachines-call";
|
package/esm/extensions/client.js
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
import { SDK_METADATA } from '../lib/config.js';
|
|
2
2
|
import { HTTPClient } from '../lib/http.js';
|
|
3
3
|
import { StateMachines as Generated } from '../sdk/sdk.js';
|
|
4
|
+
import { expandSource } from './apps.js';
|
|
4
5
|
import { Connection } from './connection.js';
|
|
5
|
-
import { StateMachinesError, toStateMachinesError } from './errors.js';
|
|
6
|
+
import { CallerAbortedError, EnvironmentFailedError, SnapshotFailedError, SnapshotTimeoutError, StateMachinesError, TimeoutError, toStateMachinesError, } from './errors.js';
|
|
7
|
+
import { RunningEnvironment } from './running-environment.js';
|
|
6
8
|
import { linkSignal } from './signal.js';
|
|
7
|
-
import { disposed, poll, ready } from './wait.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
name = 'CallerAbortedError';
|
|
11
|
-
constructor(reason) {
|
|
12
|
-
super('The caller aborted the request.', { cause: reason });
|
|
13
|
-
}
|
|
14
|
-
}
|
|
9
|
+
import { backoff, defaultTimeoutMs, disposed, poll, pollUntil, ready, sleep, snapshotReady, } from './wait.js';
|
|
10
|
+
const stopAfterFailureMs = 30000;
|
|
11
|
+
const busyBudgetMs = 60000;
|
|
15
12
|
export class StateMachines {
|
|
16
13
|
#sdk;
|
|
17
14
|
#fetch;
|
|
18
15
|
#workspaceId;
|
|
16
|
+
#callSignals = new Map();
|
|
17
|
+
#lastCall = 0;
|
|
19
18
|
#credentialWorkspace;
|
|
20
19
|
constructor(options = {}) {
|
|
21
20
|
const apiKey = options.apiKey ?? environmentApiKey();
|
|
@@ -37,7 +36,7 @@ export class StateMachines {
|
|
|
37
36
|
}
|
|
38
37
|
const httpClient = options.httpClient?.clone() ??
|
|
39
38
|
new HTTPClient(fetcher ? { fetcher: (input) => fetcher(input) } : {});
|
|
40
|
-
addAttemptTimeout(httpClient, timeoutMs);
|
|
39
|
+
addAttemptTimeout(httpClient, timeoutMs, this.#callSignals);
|
|
41
40
|
this.#fetch = fetcher;
|
|
42
41
|
this.#workspaceId = options.workspaceId;
|
|
43
42
|
this.#sdk = new Generated({
|
|
@@ -66,13 +65,56 @@ export class StateMachines {
|
|
|
66
65
|
return this.#sdk.environments.list({ ...filters, workspaceId: resolved }, call);
|
|
67
66
|
}),
|
|
68
67
|
get: (environmentId, options) => this.#unwrap(options, (call) => this.#sdk.environments.get({ environmentId }, call)),
|
|
69
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Creates an environment and waits until it's running. If the wait fails, stops the
|
|
70
|
+
* environment (unless the caller passed its own `idempotencyKey`), then rethrows the
|
|
71
|
+
* error with `environmentId`, `stopped` and `stopError`.
|
|
72
|
+
*/
|
|
73
|
+
start: async (input, options = {}) => {
|
|
74
|
+
const { readyTimeoutMs, ...createOptions } = options;
|
|
75
|
+
const { idempotencyKey, signal } = options;
|
|
76
|
+
const timeoutMs = readyTimeoutMs ?? defaultTimeoutMs;
|
|
77
|
+
if (!(timeoutMs > 0)) {
|
|
78
|
+
throw new RangeError('readyTimeoutMs must be more than 0.');
|
|
79
|
+
}
|
|
80
|
+
const created = await this.environments.create(input, createOptions);
|
|
81
|
+
try {
|
|
82
|
+
const environment = await this.environments.waitUntilReady(created.id, {
|
|
83
|
+
timeoutMs,
|
|
84
|
+
...(signal ? { signal } : {}),
|
|
85
|
+
});
|
|
86
|
+
return new RunningEnvironment(this, environment);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
// The API replays a create by key, returning the same environment to another
|
|
90
|
+
// start() that may still be using it, so only the SDK's own key allows a stop.
|
|
91
|
+
const outcome = idempotencyKey === undefined
|
|
92
|
+
? await this.#stopAfterFailure(created.id)
|
|
93
|
+
: { environmentId: created.id, stopped: false };
|
|
94
|
+
const isCallerReason = signal?.aborted && Object.is(error, signal.reason);
|
|
95
|
+
if (isCallerReason) {
|
|
96
|
+
if (outcome.stopped) {
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
throw Object.assign(new StateMachinesError(new CallerAbortedError(error)), outcome);
|
|
100
|
+
}
|
|
101
|
+
if (isStartError(error) && Object.isExtensible(error)) {
|
|
102
|
+
Object.assign(error, outcome);
|
|
103
|
+
}
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
/**
|
|
108
|
+
* A string app is shorthand for `{ key: <the string>, appId: <the string> }`. Every
|
|
109
|
+
* failure carries the `idempotencyKey`; replaying the create with it is safe.
|
|
110
|
+
*/
|
|
70
111
|
create: async (input, options = {}) => {
|
|
71
112
|
const idempotencyKey = options.idempotencyKey ?? crypto.randomUUID();
|
|
72
113
|
try {
|
|
114
|
+
const body = expandSource(input);
|
|
73
115
|
return await this.#call(options, async (call) => {
|
|
74
116
|
const workspaceId = await this.#workspace(options.workspaceId, call.signal);
|
|
75
|
-
const { data } = await this.#sdk.environments.create({ workspaceId, idempotencyKey, body
|
|
117
|
+
const { data } = await this.#sdk.environments.create({ workspaceId, idempotencyKey, body }, call);
|
|
76
118
|
const [environment] = data;
|
|
77
119
|
if (!environment) {
|
|
78
120
|
throw new Error('Invalid response from the State API: the create returned no environment.');
|
|
@@ -87,23 +129,142 @@ export class StateMachines {
|
|
|
87
129
|
},
|
|
88
130
|
stop: (environmentId, options) => this.#unwrap(options, (call) => this.#sdk.environments.stop({ environmentId }, call)),
|
|
89
131
|
events: (environmentId, query = {}, options) => this.#call(options, (call) => this.#sdk.environments.events({ ...query, environmentId }, call)),
|
|
132
|
+
/** The app's users, in the release's order. */
|
|
133
|
+
actors: async (environmentId, appKey, options) => (await this.environments.getConnection(environmentId, appKey, options))
|
|
134
|
+
.actors,
|
|
90
135
|
getConnection: (environmentId, appKey, options) => this.#unwrap(options, (call) => this.#sdk.environments.getConnection({ environmentId, appKey }, call)),
|
|
91
|
-
reveal: (environmentId, appKey, actorId, options) => this.#
|
|
136
|
+
reveal: (environmentId, appKey, actorId, options) => this.#reveal(environmentId, appKey, { actorId }, options),
|
|
92
137
|
/** Resolves when the environment is running. Never stops it. */
|
|
93
138
|
waitUntilReady: (environmentId, options) => poll((signal) => this.#poll(environmentId, signal), ready, options),
|
|
94
139
|
/** Resolves when the environment ended and every sandbox is confirmed disposed. */
|
|
95
140
|
waitUntilStopped: (environmentId, options) => poll((signal) => this.#poll(environmentId, signal), disposed, options),
|
|
141
|
+
/**
|
|
142
|
+
* actorId defaults to the actor of the environment's declared default connection, or
|
|
143
|
+
* else the app's first actor (for Salesforce, the administrator). Pick another from
|
|
144
|
+
* actors() to act with that user's permissions.
|
|
145
|
+
*/
|
|
96
146
|
connect: async (environmentId, appKey, options = {}) => {
|
|
97
147
|
const details = await this.environments.getConnection(environmentId, appKey, options);
|
|
98
148
|
if (details.status !== 'ready' || !details.baseUrl) {
|
|
99
149
|
throw new TypeError(`App ${appKey} is not ready to connect (${details.status}).`);
|
|
100
150
|
}
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
151
|
+
// The API resolves { actorId } to that actor's first connection, not the declared one.
|
|
152
|
+
const body = options.actorId === undefined && details.defaultConnection
|
|
153
|
+
? { connectionKey: details.defaultConnection }
|
|
154
|
+
: { actorId: pickActor(appKey, details.actors, options.actorId).id };
|
|
155
|
+
const api = pickApi(appKey, details.apis, options.apiId);
|
|
156
|
+
const secret = await this.#reveal(environmentId, appKey, body, options);
|
|
104
157
|
return new Connection(details.baseUrl, api, secret, this.#fetch);
|
|
105
158
|
},
|
|
106
159
|
};
|
|
160
|
+
snapshots = {
|
|
161
|
+
/** Returns the API's answer at once: it may still be `capturing`. */
|
|
162
|
+
create: (environmentId, input = {}, options = {}) => this.#createSnapshot(environmentId, input, options, 0),
|
|
163
|
+
/**
|
|
164
|
+
* Creates a snapshot and waits until it's `ready`. Needs `snapshots:create` and
|
|
165
|
+
* `snapshots:read`. A failed or timed-out snapshot is never deleted: the error
|
|
166
|
+
* carries `snapshotId` and `idempotencyKey`.
|
|
167
|
+
*/
|
|
168
|
+
capture: async (environmentId, input = {}, options = {}) => {
|
|
169
|
+
const { readyTimeoutMs, ...createOptions } = options;
|
|
170
|
+
const { signal } = options;
|
|
171
|
+
const idempotencyKey = options.idempotencyKey ?? crypto.randomUUID();
|
|
172
|
+
const timeoutMs = readyTimeoutMs ?? defaultTimeoutMs;
|
|
173
|
+
if (!(timeoutMs > 0)) {
|
|
174
|
+
throw new RangeError('readyTimeoutMs must be more than 0.');
|
|
175
|
+
}
|
|
176
|
+
const created = await this.#createSnapshot(environmentId, input, { ...createOptions, idempotencyKey }, busyBudgetMs);
|
|
177
|
+
try {
|
|
178
|
+
signal?.throwIfAborted();
|
|
179
|
+
if (snapshotReady(created)) {
|
|
180
|
+
return created;
|
|
181
|
+
}
|
|
182
|
+
return await this.snapshots.waitUntilReady(created.id, {
|
|
183
|
+
timeoutMs,
|
|
184
|
+
...(signal ? { signal } : {}),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
const snapshotId = created.id;
|
|
189
|
+
if (signal?.aborted && Object.is(error, signal.reason)) {
|
|
190
|
+
throw Object.assign(new StateMachinesError(new CallerAbortedError(error), {
|
|
191
|
+
idempotencyKey,
|
|
192
|
+
}), { snapshotId });
|
|
193
|
+
}
|
|
194
|
+
if (error instanceof StateMachinesError ||
|
|
195
|
+
error instanceof SnapshotFailedError ||
|
|
196
|
+
error instanceof SnapshotTimeoutError) {
|
|
197
|
+
Object.assign(error, { snapshotId, idempotencyKey });
|
|
198
|
+
}
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
/** Resolves when the snapshot is `ready`. Never deletes it. */
|
|
203
|
+
waitUntilReady: (snapshotId, options) => pollUntil((signal) => this.#unwrap({ signal }, (call) => this.#sdk.snapshots.get({ snapshotId }, { ...call, retries: { strategy: 'none' } })), snapshotReady, options, (timeoutMs, last) => new SnapshotTimeoutError(timeoutMs, last, snapshotId)),
|
|
204
|
+
/** Deleted snapshots too, with `deletedAt` set. */
|
|
205
|
+
get: (snapshotId, options) => this.#unwrap(options, (call) => this.#sdk.snapshots.get({ snapshotId }, call)),
|
|
206
|
+
/** Newest first. Deleted snapshots are left out. */
|
|
207
|
+
list: (query = {}, options) => this.#call(options, async (call) => {
|
|
208
|
+
const { workspaceId, ...filters } = query;
|
|
209
|
+
const resolved = await this.#workspace(workspaceId, call.signal);
|
|
210
|
+
return this.#sdk.snapshots.list({ ...filters, workspaceId: resolved }, call);
|
|
211
|
+
}),
|
|
212
|
+
/**
|
|
213
|
+
* `null` clears the name. A stale `expectedVersion`, or a deleted snapshot, is a 409
|
|
214
|
+
* `version-conflict`; so is a retry of a rename whose first answer was lost.
|
|
215
|
+
*/
|
|
216
|
+
rename: (snapshotId, body, options) => this.#unwrap(options, (call) => this.#sdk.snapshots.update({ snapshotId, body }, call)),
|
|
217
|
+
/** Marks the snapshot deleted and returns it; safe to repeat. A capturing snapshot is a 409 `not-ready`. */
|
|
218
|
+
delete: (snapshotId, options) => this.#unwrap(options, (call) => this.#sdk.snapshots.delete({ snapshotId }, call)),
|
|
219
|
+
};
|
|
220
|
+
async #createSnapshot(environmentId, input, options, busyMs) {
|
|
221
|
+
const idempotencyKey = options.idempotencyKey ?? crypto.randomUUID();
|
|
222
|
+
try {
|
|
223
|
+
return await this.#call(options, async (call) => {
|
|
224
|
+
const workspaceId = await this.#workspace(options.workspaceId, call.signal);
|
|
225
|
+
const request = {
|
|
226
|
+
workspaceId,
|
|
227
|
+
idempotencyKey,
|
|
228
|
+
body: { ...input, environmentId },
|
|
229
|
+
};
|
|
230
|
+
const deadline = Date.now() + busyMs;
|
|
231
|
+
for (let attempt = 0;; attempt += 1) {
|
|
232
|
+
try {
|
|
233
|
+
return (await this.#sdk.snapshots.capture(request, call)).data;
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
const busy = toStateMachinesError(error);
|
|
237
|
+
const waitMs = Math.max(busy.retryAfter ?? 0, backoff(attempt));
|
|
238
|
+
if (busy.code !== 'snapshot-limit' ||
|
|
239
|
+
Date.now() + waitMs >= deadline) {
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
await sleep(waitMs, call.signal);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
throw new StateMachinesError(error, { idempotencyKey });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
#reveal(environmentId, appKey, body, options) {
|
|
252
|
+
return this.#unwrap(options, (call) => this.#sdk.environments.reveal({ environmentId, appKey, body }, call));
|
|
253
|
+
}
|
|
254
|
+
async #stopAfterFailure(environmentId) {
|
|
255
|
+
const budget = linkSignal(undefined, stopAfterFailureMs);
|
|
256
|
+
try {
|
|
257
|
+
// abortable() ends the wait at the budget even during a retry delay.
|
|
258
|
+
await abortable(this.environments.stop(environmentId, { signal: budget.signal }), budget.signal);
|
|
259
|
+
return { environmentId, stopped: true };
|
|
260
|
+
}
|
|
261
|
+
catch (stopError) {
|
|
262
|
+
return { environmentId, stopped: false, stopError };
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
budget.dispose();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
107
268
|
/** One read for the poller, which owns retries, timing and the caller's signal. */
|
|
108
269
|
#poll(environmentId, signal) {
|
|
109
270
|
return this.#unwrap({ signal }, (call) => this.#sdk.environments.get({ environmentId }, { ...call, retries: { strategy: 'none' } }));
|
|
@@ -141,8 +302,13 @@ export class StateMachines {
|
|
|
141
302
|
*/
|
|
142
303
|
async #call(options, run) {
|
|
143
304
|
const scope = linkSignal(options?.signal);
|
|
305
|
+
const id = String(++this.#lastCall);
|
|
306
|
+
this.#callSignals.set(id, scope.signal);
|
|
144
307
|
try {
|
|
145
|
-
return await run({
|
|
308
|
+
return await run({
|
|
309
|
+
signal: scope.signal,
|
|
310
|
+
headers: { [CALL_HEADER]: id },
|
|
311
|
+
});
|
|
146
312
|
}
|
|
147
313
|
catch (error) {
|
|
148
314
|
// A caller abort can surface as a raw TimeoutError while a body is read; it is never retryable.
|
|
@@ -151,35 +317,53 @@ export class StateMachines {
|
|
|
151
317
|
: error);
|
|
152
318
|
}
|
|
153
319
|
finally {
|
|
320
|
+
this.#callSignals.delete(id);
|
|
154
321
|
scope.close();
|
|
155
322
|
}
|
|
156
323
|
}
|
|
157
324
|
}
|
|
158
325
|
/**
|
|
159
|
-
*
|
|
326
|
+
* Names the call a request belongs to. Never sent: the attempt hook removes it.
|
|
327
|
+
* Needed because the generated loop clones each attempt, and after a garbage
|
|
328
|
+
* collection a clone's signal no longer follows the call's (Node 20 and 24).
|
|
329
|
+
*/
|
|
330
|
+
export const CALL_HEADER = 'x-statemachines-call';
|
|
331
|
+
/**
|
|
332
|
+
* Gives each attempt its own timeout, linked to its call's signal. The generated
|
|
160
333
|
* `timeoutMs` is never used: it is dropped when a caller passes a signal.
|
|
161
334
|
*/
|
|
162
|
-
function addAttemptTimeout(httpClient, timeoutMs) {
|
|
335
|
+
function addAttemptTimeout(httpClient, timeoutMs, calls) {
|
|
163
336
|
const callers = new WeakMap();
|
|
164
337
|
httpClient.addHook('beforeRequest', (request) => {
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
338
|
+
const headers = new Headers(request.headers);
|
|
339
|
+
const call = calls.get(headers.get(CALL_HEADER) ?? '');
|
|
340
|
+
// The request's own signal still counts: a caller's hook may have set it.
|
|
341
|
+
const sources = call ? [call, request.signal] : [request.signal];
|
|
342
|
+
const aborted = sources.find(({ aborted }) => aborted);
|
|
343
|
+
headers.delete(CALL_HEADER);
|
|
344
|
+
if (aborted) {
|
|
345
|
+
throw new CallerAbortedError(aborted.reason);
|
|
168
346
|
}
|
|
169
347
|
const attempt = new Request(request, {
|
|
170
|
-
|
|
348
|
+
headers,
|
|
349
|
+
signal: linkSignal(sources, timeoutMs).signal,
|
|
171
350
|
});
|
|
172
|
-
callers.set(attempt,
|
|
351
|
+
callers.set(attempt, sources);
|
|
173
352
|
return attempt;
|
|
174
353
|
});
|
|
175
354
|
// Without this, a timeout-shaped abort reason looks like a retryable timeout to the generated loop.
|
|
176
355
|
httpClient.addHook('requestError', (_error, request) => {
|
|
177
|
-
const
|
|
178
|
-
if (
|
|
179
|
-
throw new CallerAbortedError(
|
|
356
|
+
const aborted = callers.get(request)?.find(({ aborted }) => aborted);
|
|
357
|
+
if (aborted) {
|
|
358
|
+
throw new CallerAbortedError(aborted.reason);
|
|
180
359
|
}
|
|
181
360
|
});
|
|
182
361
|
}
|
|
362
|
+
function isStartError(error) {
|
|
363
|
+
return (error instanceof StateMachinesError ||
|
|
364
|
+
error instanceof EnvironmentFailedError ||
|
|
365
|
+
error instanceof TimeoutError);
|
|
366
|
+
}
|
|
183
367
|
function abortable(promise, signal) {
|
|
184
368
|
if (!signal) {
|
|
185
369
|
return promise;
|
|
@@ -196,16 +380,33 @@ function abortable(promise, signal) {
|
|
|
196
380
|
.finally(() => signal.removeEventListener('abort', onAbort));
|
|
197
381
|
});
|
|
198
382
|
}
|
|
199
|
-
function
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
383
|
+
function pickActor(appKey, actors, actorId) {
|
|
384
|
+
const [first] = actors;
|
|
385
|
+
if (!first) {
|
|
386
|
+
throw new TypeError(`App ${appKey} has no actors.`);
|
|
387
|
+
}
|
|
388
|
+
if (actorId === undefined) {
|
|
389
|
+
return first;
|
|
390
|
+
}
|
|
391
|
+
const actor = actors.find(({ id }) => id === actorId);
|
|
392
|
+
if (!actor) {
|
|
393
|
+
throw new TypeError(`Unknown actor ${actorId} for ${appKey}. Choose one of: ${ids(actors)}.`);
|
|
394
|
+
}
|
|
395
|
+
return actor;
|
|
396
|
+
}
|
|
397
|
+
function pickApi(appKey, apis, apiId) {
|
|
398
|
+
const api = apiId
|
|
399
|
+
? apis.find(({ id }) => id === apiId)
|
|
400
|
+
: apis.length === 1
|
|
401
|
+
? apis[0]
|
|
204
402
|
: undefined;
|
|
205
|
-
if (!
|
|
206
|
-
throw new TypeError(`Choose one ${
|
|
403
|
+
if (!api) {
|
|
404
|
+
throw new TypeError(`Choose one API for ${appKey}: ${ids(apis) || 'none available'}.`);
|
|
207
405
|
}
|
|
208
|
-
return
|
|
406
|
+
return api;
|
|
407
|
+
}
|
|
408
|
+
function ids(choices) {
|
|
409
|
+
return choices.map(({ id }) => id).join(', ');
|
|
209
410
|
}
|
|
210
411
|
function environmentApiKey() {
|
|
211
412
|
const runtime = globalThis;
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import type { Environment } from '../models/environment.js';
|
|
2
|
+
import type { EnvironmentSnapshot } from '../models/environment-snapshot.js';
|
|
2
3
|
import type { Code } from '../models/error-response.js';
|
|
4
|
+
/** What `environments.start()` adds to the error it rethrows after a failed wait. */
|
|
5
|
+
export interface StartFailure {
|
|
6
|
+
/** The environment the start created. */
|
|
7
|
+
readonly environmentId?: string;
|
|
8
|
+
/** Whether the stop request succeeded. Disposal may still be in progress. */
|
|
9
|
+
readonly stopped?: boolean;
|
|
10
|
+
/** Why the stop request failed, when it did. */
|
|
11
|
+
readonly stopError?: unknown;
|
|
12
|
+
}
|
|
3
13
|
interface Details {
|
|
4
14
|
readonly message: string;
|
|
5
15
|
readonly cause?: unknown;
|
|
@@ -12,11 +22,17 @@ interface Details {
|
|
|
12
22
|
readonly idempotencyKey?: string;
|
|
13
23
|
}
|
|
14
24
|
/**
|
|
15
|
-
* Any failed API call: an error response, an invalid response,
|
|
16
|
-
*
|
|
25
|
+
* Any failed API call: an error response, an invalid response, no response at all,
|
|
26
|
+
* or input the SDK rejected before sending (`status` null for the last three). The
|
|
27
|
+
* message never includes a response body.
|
|
17
28
|
*/
|
|
18
|
-
export declare class StateMachinesError extends Error implements Details {
|
|
29
|
+
export declare class StateMachinesError extends Error implements Details, StartFailure {
|
|
19
30
|
readonly name = "StateMachinesError";
|
|
31
|
+
readonly environmentId?: string;
|
|
32
|
+
readonly stopped?: boolean;
|
|
33
|
+
readonly stopError?: unknown;
|
|
34
|
+
/** Set when `snapshots.capture()` fails after the snapshot was created. */
|
|
35
|
+
readonly snapshotId?: string;
|
|
20
36
|
readonly status: number | null;
|
|
21
37
|
readonly code: Code | null;
|
|
22
38
|
readonly requestId: string | null;
|
|
@@ -25,7 +41,7 @@ export declare class StateMachinesError extends Error implements Details {
|
|
|
25
41
|
readonly retryAfter: number | null;
|
|
26
42
|
/** Transient: a 502, 503 or 504, or the request never got a response. */
|
|
27
43
|
readonly retryable: boolean;
|
|
28
|
-
/** Set on
|
|
44
|
+
/** Set on create and capture failures. A replay with it returns the same result, so use it when the outcome is unknown. */
|
|
29
45
|
readonly idempotencyKey?: string;
|
|
30
46
|
/** Wraps any thrown value. Wrapping a `StateMachinesError` copies it, keeping its cause. */
|
|
31
47
|
constructor(error: unknown, extra?: {
|
|
@@ -33,9 +49,12 @@ export declare class StateMachinesError extends Error implements Details {
|
|
|
33
49
|
});
|
|
34
50
|
}
|
|
35
51
|
export declare function toStateMachinesError(error: unknown): StateMachinesError;
|
|
36
|
-
export declare class EnvironmentFailedError extends Error {
|
|
52
|
+
export declare class EnvironmentFailedError extends Error implements StartFailure {
|
|
37
53
|
readonly environment: Environment;
|
|
38
54
|
readonly name = "EnvironmentFailedError";
|
|
55
|
+
readonly environmentId?: string;
|
|
56
|
+
readonly stopped?: boolean;
|
|
57
|
+
readonly stopError?: unknown;
|
|
39
58
|
constructor(environment: Environment);
|
|
40
59
|
}
|
|
41
60
|
export declare class DisposalIncompleteError extends Error {
|
|
@@ -43,10 +62,35 @@ export declare class DisposalIncompleteError extends Error {
|
|
|
43
62
|
readonly name = "DisposalIncompleteError";
|
|
44
63
|
constructor(environment: Environment);
|
|
45
64
|
}
|
|
46
|
-
export declare class TimeoutError extends Error {
|
|
65
|
+
export declare class TimeoutError extends Error implements StartFailure {
|
|
47
66
|
readonly timeoutMs: number;
|
|
48
67
|
readonly environment: Environment | null;
|
|
49
68
|
readonly name = "TimeoutError";
|
|
69
|
+
readonly environmentId?: string;
|
|
70
|
+
readonly stopped?: boolean;
|
|
71
|
+
readonly stopError?: unknown;
|
|
50
72
|
constructor(timeoutMs: number, environment: Environment | null);
|
|
51
73
|
}
|
|
74
|
+
/** Thrown from a hook so the generated retry loop stops, whatever the abort reason. */
|
|
75
|
+
export declare class CallerAbortedError extends Error {
|
|
76
|
+
readonly name = "CallerAbortedError";
|
|
77
|
+
constructor(reason: unknown);
|
|
78
|
+
}
|
|
79
|
+
/** A snapshot that ended `failed`, or was deleted, while the SDK waited for `ready`. */
|
|
80
|
+
export declare class SnapshotFailedError extends Error {
|
|
81
|
+
readonly snapshot: EnvironmentSnapshot;
|
|
82
|
+
readonly name = "SnapshotFailedError";
|
|
83
|
+
readonly snapshotId: string;
|
|
84
|
+
readonly idempotencyKey?: string;
|
|
85
|
+
constructor(snapshot: EnvironmentSnapshot);
|
|
86
|
+
}
|
|
87
|
+
/** The wait for `ready` passed its deadline. The capture may still finish on the server. */
|
|
88
|
+
export declare class SnapshotTimeoutError extends Error {
|
|
89
|
+
readonly timeoutMs: number;
|
|
90
|
+
readonly snapshot: EnvironmentSnapshot | null;
|
|
91
|
+
readonly snapshotId: string;
|
|
92
|
+
readonly name = "SnapshotTimeoutError";
|
|
93
|
+
readonly idempotencyKey?: string;
|
|
94
|
+
constructor(timeoutMs: number, snapshot: EnvironmentSnapshot | null, snapshotId: string);
|
|
95
|
+
}
|
|
52
96
|
export {};
|
package/esm/extensions/errors.js
CHANGED
|
@@ -3,8 +3,9 @@ import { ConnectionError, RequestTimeoutError, } from '../models/errors/http-cli
|
|
|
3
3
|
import { ResponseValidationError } from '../models/errors/response-validation-error.js';
|
|
4
4
|
import { StateMachinesHTTPError } from '../models/errors/state-machines-http-error.js';
|
|
5
5
|
/**
|
|
6
|
-
* Any failed API call: an error response, an invalid response,
|
|
7
|
-
*
|
|
6
|
+
* Any failed API call: an error response, an invalid response, no response at all,
|
|
7
|
+
* or input the SDK rejected before sending (`status` null for the last three). The
|
|
8
|
+
* message never includes a response body.
|
|
8
9
|
*/
|
|
9
10
|
export class StateMachinesError extends Error {
|
|
10
11
|
name = 'StateMachinesError';
|
|
@@ -16,7 +17,7 @@ export class StateMachinesError extends Error {
|
|
|
16
17
|
retryAfter;
|
|
17
18
|
/** Transient: a 502, 503 or 504, or the request never got a response. */
|
|
18
19
|
retryable;
|
|
19
|
-
/** Set on
|
|
20
|
+
/** Set on create and capture failures. A replay with it returns the same result, so use it when the outcome is unknown. */
|
|
20
21
|
idempotencyKey;
|
|
21
22
|
/** Wraps any thrown value. Wrapping a `StateMachinesError` copies it, keeping its cause. */
|
|
22
23
|
constructor(error, extra = {}) {
|
|
@@ -127,3 +128,36 @@ export class TimeoutError extends Error {
|
|
|
127
128
|
this.environment = environment;
|
|
128
129
|
}
|
|
129
130
|
}
|
|
131
|
+
/** Thrown from a hook so the generated retry loop stops, whatever the abort reason. */
|
|
132
|
+
export class CallerAbortedError extends Error {
|
|
133
|
+
name = 'CallerAbortedError';
|
|
134
|
+
constructor(reason) {
|
|
135
|
+
super('The caller aborted the request.', { cause: reason });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** A snapshot that ended `failed`, or was deleted, while the SDK waited for `ready`. */
|
|
139
|
+
export class SnapshotFailedError extends Error {
|
|
140
|
+
snapshot;
|
|
141
|
+
name = 'SnapshotFailedError';
|
|
142
|
+
snapshotId;
|
|
143
|
+
constructor(snapshot) {
|
|
144
|
+
super(snapshot.status === 'failed'
|
|
145
|
+
? `Snapshot ${snapshot.id} is failed (${snapshot.errorCode ?? 'no error code'}).`
|
|
146
|
+
: `Snapshot ${snapshot.id} was deleted.`);
|
|
147
|
+
this.snapshot = snapshot;
|
|
148
|
+
this.snapshotId = snapshot.id;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** The wait for `ready` passed its deadline. The capture may still finish on the server. */
|
|
152
|
+
export class SnapshotTimeoutError extends Error {
|
|
153
|
+
timeoutMs;
|
|
154
|
+
snapshot;
|
|
155
|
+
snapshotId;
|
|
156
|
+
name = 'SnapshotTimeoutError';
|
|
157
|
+
constructor(timeoutMs, snapshot, snapshotId) {
|
|
158
|
+
super(`Timed out after ${timeoutMs} ms waiting for snapshot ${snapshotId}.`);
|
|
159
|
+
this.timeoutMs = timeoutMs;
|
|
160
|
+
this.snapshot = snapshot;
|
|
161
|
+
this.snapshotId = snapshotId;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
export type { Fetcher, HTTPClientOptions } from '../lib/http.js';
|
|
2
2
|
export { HTTPClient } from '../lib/http.js';
|
|
3
3
|
export { ConnectionError, ErrorResponse, HTTPClientError, InvalidRequestError, RequestAbortedError, RequestTimeoutError, ResponseValidationError, SDKValidationError, StateMachinesHTTPError, UnexpectedClientError, UnexpectedResponseError, } from '../models/errors/index.js';
|
|
4
|
-
export type { ApiProfile, App, Catalog, Connection as ConnectionDetails, ConnectionSecret, CreateEnvironmentInput, Credential, Environment, EnvironmentApp, Event, EventPage, Release, ReleaseActor, Workspace, } from '../models/index.js';
|
|
4
|
+
export type { ApiProfile, App, AppInstanceSnapshot, Catalog, Connection as ConnectionDetails, ConnectionSecret, CreateEnvironmentInput, Credential, Environment, EnvironmentApp, EnvironmentSnapshot, Event, EventPage, Release, ReleaseActor, Workspace, } from '../models/index.js';
|
|
5
5
|
export { Code as ErrorCode, Disposal, EnvironmentStatus, StopReason, } from '../models/index.js';
|
|
6
6
|
export { Status as EnvironmentListStatus } from '../models/operations/list-environments.js';
|
|
7
|
-
export type {
|
|
7
|
+
export type { AppInput, EnvironmentInput, SnapshotEnvironmentInput, } from './apps.js';
|
|
8
|
+
export type { CallOptions, CaptureInput, CaptureOptions, ConnectOptions, CreateOptions, ListEnvironmentsQuery, ListSnapshotsQuery, Page, StartOptions, StateMachinesOptions, } from './client.js';
|
|
8
9
|
export { StateMachines } from './client.js';
|
|
9
10
|
export { Connection, formatActorHeader } from './connection.js';
|
|
10
|
-
export { DisposalIncompleteError, EnvironmentFailedError, StateMachinesError, TimeoutError, } from './errors.js';
|
|
11
|
+
export { DisposalIncompleteError, EnvironmentFailedError, SnapshotFailedError, SnapshotTimeoutError, StateMachinesError, TimeoutError, } from './errors.js';
|
|
12
|
+
export { RunningEnvironment } from './running-environment.js';
|
|
11
13
|
export type { WaitOptions } from './wait.js';
|
package/esm/extensions/index.js
CHANGED
|
@@ -4,4 +4,5 @@ export { Code as ErrorCode, Disposal, EnvironmentStatus, StopReason, } from '../
|
|
|
4
4
|
export { Status as EnvironmentListStatus } from '../models/operations/list-environments.js';
|
|
5
5
|
export { StateMachines } from './client.js';
|
|
6
6
|
export { Connection, formatActorHeader } from './connection.js';
|
|
7
|
-
export { DisposalIncompleteError, EnvironmentFailedError, StateMachinesError, TimeoutError, } from './errors.js';
|
|
7
|
+
export { DisposalIncompleteError, EnvironmentFailedError, SnapshotFailedError, SnapshotTimeoutError, StateMachinesError, TimeoutError, } from './errors.js';
|
|
8
|
+
export { RunningEnvironment } from './running-environment.js';
|