@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.
Files changed (76) hide show
  1. package/README.md +265 -75
  2. package/agents/SKILL.md +33 -46
  3. package/agents/index.js +7 -7
  4. package/agents/llms.txt +25 -40
  5. package/agents/prompt.md +11 -34
  6. package/esm/extensions/apps.d.ts +18 -0
  7. package/esm/extensions/apps.js +39 -0
  8. package/esm/extensions/client.d.ts +66 -3
  9. package/esm/extensions/client.js +236 -35
  10. package/esm/extensions/errors.d.ts +50 -6
  11. package/esm/extensions/errors.js +37 -3
  12. package/esm/extensions/index.d.ts +5 -3
  13. package/esm/extensions/index.js +2 -1
  14. package/esm/extensions/running-environment.d.ts +32 -0
  15. package/esm/extensions/running-environment.js +44 -0
  16. package/esm/extensions/signal.d.ts +1 -2
  17. package/esm/extensions/signal.js +9 -4
  18. package/esm/extensions/wait.d.ts +11 -3
  19. package/esm/extensions/wait.js +23 -13
  20. package/esm/funcs/create-environment-definition.d.ts +11 -0
  21. package/esm/funcs/create-environment-definition.js +94 -0
  22. package/esm/funcs/environments-list.js +1 -0
  23. package/esm/funcs/snapshots-capture.d.ts +11 -0
  24. package/esm/funcs/snapshots-capture.js +94 -0
  25. package/esm/funcs/snapshots-delete.d.ts +11 -0
  26. package/esm/funcs/snapshots-delete.js +92 -0
  27. package/esm/funcs/snapshots-get.d.ts +11 -0
  28. package/esm/funcs/snapshots-get.js +92 -0
  29. package/esm/funcs/snapshots-list.d.ts +11 -0
  30. package/esm/funcs/snapshots-list.js +98 -0
  31. package/esm/funcs/snapshots-update.d.ts +11 -0
  32. package/esm/funcs/snapshots-update.js +93 -0
  33. package/esm/lib/config.d.ts +2 -2
  34. package/esm/lib/config.js +2 -2
  35. package/esm/models/app-instance-snapshot.d.ts +15 -0
  36. package/esm/models/app-instance-snapshot.js +19 -0
  37. package/esm/models/connection-secret.d.ts +16 -0
  38. package/esm/models/connection-secret.js +13 -0
  39. package/esm/models/connection.d.ts +14 -0
  40. package/esm/models/connection.js +14 -0
  41. package/esm/models/create-environment-input.d.ts +6 -2
  42. package/esm/models/create-environment-input.js +3 -1
  43. package/esm/models/environment-app.d.ts +1 -0
  44. package/esm/models/environment-app.js +1 -0
  45. package/esm/models/environment-snapshot.d.ts +39 -0
  46. package/esm/models/environment-snapshot.js +41 -0
  47. package/esm/models/environment.d.ts +1 -0
  48. package/esm/models/environment.js +1 -0
  49. package/esm/models/error-response.d.ts +5 -0
  50. package/esm/models/error-response.js +5 -0
  51. package/esm/models/index.d.ts +2 -0
  52. package/esm/models/index.js +2 -0
  53. package/esm/models/operations/capture-snapshot.d.ts +41 -0
  54. package/esm/models/operations/capture-snapshot.js +36 -0
  55. package/esm/models/operations/create-environment-definition.d.ts +143 -0
  56. package/esm/models/operations/create-environment-definition.js +90 -0
  57. package/esm/models/operations/delete-snapshot.d.ts +23 -0
  58. package/esm/models/operations/delete-snapshot.js +20 -0
  59. package/esm/models/operations/get-snapshot.d.ts +23 -0
  60. package/esm/models/operations/get-snapshot.js +20 -0
  61. package/esm/models/operations/index.d.ts +6 -0
  62. package/esm/models/operations/index.js +6 -0
  63. package/esm/models/operations/list-environments.d.ts +2 -0
  64. package/esm/models/operations/list-environments.js +1 -0
  65. package/esm/models/operations/list-snapshots.d.ts +30 -0
  66. package/esm/models/operations/list-snapshots.js +25 -0
  67. package/esm/models/operations/reveal-connection.d.ts +26 -4
  68. package/esm/models/operations/reveal-connection.js +28 -2
  69. package/esm/models/operations/update-snapshot.d.ts +37 -0
  70. package/esm/models/operations/update-snapshot.js +29 -0
  71. package/esm/sdk/sdk.d.ts +6 -1
  72. package/esm/sdk/sdk.js +10 -0
  73. package/esm/sdk/snapshots.d.ts +9 -0
  74. package/esm/sdk/snapshots.js +27 -0
  75. package/openapi.json +1471 -144
  76. 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
- /** Every failure carries the `idempotencyKey`; replaying the create with it is safe. */
67
- create: (input: CreateEnvironmentInput, options?: CreateOptions) => Promise<Environment>;
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";
@@ -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
- /** Thrown from a hook so the generated retry loop stops, whatever the abort reason. */
9
- class CallerAbortedError extends Error {
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
- /** Every failure carries the `idempotencyKey`; replaying the create with it is safe. */
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: input }, call);
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.#unwrap(options, (call) => this.#sdk.environments.reveal({ environmentId, appKey, body: { actorId } }, call)),
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
- const actor = pick('actor', details.actors, options.actorId);
102
- const api = pick('API', details.apis, options.apiId);
103
- const secret = await this.environments.reveal(environmentId, appKey, actor.id, options);
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({ signal: scope.signal });
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
- * Gives each attempt its own timeout, linked to the caller's signal. The generated
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 caller = request.signal;
166
- if (caller.aborted) {
167
- throw new CallerAbortedError(caller.reason);
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
- signal: linkSignal(caller, timeoutMs).signal,
348
+ headers,
349
+ signal: linkSignal(sources, timeoutMs).signal,
171
350
  });
172
- callers.set(attempt, caller);
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 caller = callers.get(request);
178
- if (caller?.aborted) {
179
- throw new CallerAbortedError(caller.reason);
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 pick(kind, choices, id) {
200
- const found = id
201
- ? choices.find((choice) => choice.id === id)
202
- : choices.length === 1
203
- ? choices[0]
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 (!found) {
206
- throw new TypeError(`Choose one ${kind}: ${choices.map((choice) => choice.id).join(', ') || 'none available'}.`);
403
+ if (!api) {
404
+ throw new TypeError(`Choose one API for ${appKey}: ${ids(apis) || 'none available'}.`);
207
405
  }
208
- return found;
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, or no response at
16
- * all (`status` null). The message never includes a response body.
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 `environments.create` failures; replay the create with it to recover. */
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 {};
@@ -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, or no response at
7
- * all (`status` null). The message never includes a response body.
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 `environments.create` failures; replay the create with it to recover. */
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 { CallOptions, ConnectOptions, CreateOptions, ListEnvironmentsQuery, Page, StateMachinesOptions, } from './client.js';
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';
@@ -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';