@heroku/sdk 0.1.0-alpha.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/LICENSE.txt +207 -0
- package/README.md +94 -0
- package/dist/compositions/add-on.d.ts +7 -0
- package/dist/compositions/add-on.js +6 -0
- package/dist/compositions/app.d.ts +8 -0
- package/dist/compositions/app.js +11 -0
- package/dist/compositions/dyno.d.ts +17 -0
- package/dist/compositions/dyno.js +23 -0
- package/dist/compositions/pg.d.ts +15 -0
- package/dist/compositions/pg.js +56 -0
- package/dist/compositions/pipeline.d.ts +33 -0
- package/dist/compositions/pipeline.js +146 -0
- package/dist/core/create-client.d.ts +32 -0
- package/dist/core/create-client.js +48 -0
- package/dist/core/dispatcher.d.ts +3 -0
- package/dist/core/dispatcher.js +80 -0
- package/dist/core/extend-resource.d.ts +24 -0
- package/dist/core/extend-resource.js +3 -0
- package/dist/core/extensions-proxy.d.ts +2 -0
- package/dist/core/extensions-proxy.js +56 -0
- package/dist/core/heroku-sdk.d.ts +14 -0
- package/dist/core/heroku-sdk.js +54 -0
- package/dist/core/interpolate-path.d.ts +3 -0
- package/dist/core/interpolate-path.js +13 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/resources/data/backup.d.ts +9 -0
- package/dist/resources/data/backup.js +10 -0
- package/dist/resources/data/database.d.ts +54 -0
- package/dist/resources/data/database.js +44 -0
- package/dist/resources/data/internal/resolve-addon-id.d.ts +2 -0
- package/dist/resources/data/internal/resolve-addon-id.js +13 -0
- package/dist/resources/data/internal/resolve-pg-database.d.ts +23 -0
- package/dist/resources/data/internal/resolve-pg-database.js +52 -0
- package/dist/resources/data/maintenance.d.ts +9 -0
- package/dist/resources/data/maintenance.js +10 -0
- package/dist/resources/data/postgres-credential.d.ts +11 -0
- package/dist/resources/data/postgres-credential.js +16 -0
- package/dist/resources/data/postgres-database.d.ts +32 -0
- package/dist/resources/data/postgres-database.js +14 -0
- package/dist/resources/data/postgres.d.ts +9 -0
- package/dist/resources/data/postgres.js +10 -0
- package/dist/resources/data/transfer-schedule.d.ts +9 -0
- package/dist/resources/data/transfer-schedule.js +10 -0
- package/dist/resources/data/transfer.d.ts +49 -0
- package/dist/resources/data/transfer.js +14 -0
- package/dist/resources/extensions/data.d.ts +4 -0
- package/dist/resources/extensions/data.js +4 -0
- package/dist/resources/extensions/platform.d.ts +8 -0
- package/dist/resources/extensions/platform.js +8 -0
- package/dist/resources/platform/add-on/create-and-wait.d.ts +28 -0
- package/dist/resources/platform/add-on/create-and-wait.js +63 -0
- package/dist/resources/platform/add-on/debug.d.ts +2 -0
- package/dist/resources/platform/add-on/debug.js +2 -0
- package/dist/resources/platform/add-on/describe.d.ts +20 -0
- package/dist/resources/platform/add-on/describe.js +51 -0
- package/dist/resources/platform/add-on/errors.d.ts +51 -0
- package/dist/resources/platform/add-on/errors.js +61 -0
- package/dist/resources/platform/add-on/index.d.ts +23 -0
- package/dist/resources/platform/add-on/index.js +25 -0
- package/dist/resources/platform/add-on/list-plans.d.ts +34 -0
- package/dist/resources/platform/add-on/list-plans.js +53 -0
- package/dist/resources/platform/add-on/pricing.d.ts +72 -0
- package/dist/resources/platform/add-on/pricing.js +100 -0
- package/dist/resources/platform/add-on/resolve.d.ts +43 -0
- package/dist/resources/platform/add-on/resolve.js +103 -0
- package/dist/resources/platform/add-on/types.d.ts +49 -0
- package/dist/resources/platform/add-on/types.js +1 -0
- package/dist/resources/platform/add-on/upgrade.d.ts +21 -0
- package/dist/resources/platform/add-on/upgrade.js +36 -0
- package/dist/resources/platform/add-on.d.ts +211 -0
- package/dist/resources/platform/add-on.js +354 -0
- package/dist/resources/platform/app/index.d.ts +58 -0
- package/dist/resources/platform/app/index.js +53 -0
- package/dist/resources/platform/app/info.d.ts +30 -0
- package/dist/resources/platform/app/info.js +27 -0
- package/dist/resources/platform/app.d.ts +56 -0
- package/dist/resources/platform/app.js +50 -0
- package/dist/resources/platform/dyno/index.d.ts +45 -0
- package/dist/resources/platform/dyno/index.js +26 -0
- package/dist/resources/platform/dyno.d.ts +45 -0
- package/dist/resources/platform/dyno.js +26 -0
- package/dist/resources/platform/log-session/index.d.ts +102 -0
- package/dist/resources/platform/log-session/index.js +223 -0
- package/dist/resources/platform/log-session/parser.d.ts +74 -0
- package/dist/resources/platform/log-session/parser.js +71 -0
- package/dist/resources/platform/log-session.d.ts +81 -0
- package/dist/resources/platform/log-session.js +167 -0
- package/dist/resources/platform/pipeline/coupling.d.ts +27 -0
- package/dist/resources/platform/pipeline/coupling.js +55 -0
- package/dist/resources/platform/pipeline/index.d.ts +53 -0
- package/dist/resources/platform/pipeline/index.js +75 -0
- package/dist/resources/platform/pipeline/promotion.d.ts +21 -0
- package/dist/resources/platform/pipeline/promotion.js +109 -0
- package/dist/resources/platform/pipeline-coupling.d.ts +27 -0
- package/dist/resources/platform/pipeline-coupling.js +55 -0
- package/dist/resources/platform/pipeline-promotion.d.ts +21 -0
- package/dist/resources/platform/pipeline-promotion.js +128 -0
- package/dist/resources/platform/pipeline.d.ts +53 -0
- package/dist/resources/platform/pipeline.js +75 -0
- package/dist/resources/platform/shield.d.ts +2 -0
- package/dist/resources/platform/shield.js +6 -0
- package/dist/services/data.d.ts +5 -0
- package/dist/services/data.js +5 -0
- package/dist/services/platform.d.ts +5 -0
- package/dist/services/platform.js +5 -0
- package/dist/utils/wait.d.ts +16 -0
- package/dist/utils/wait.js +35 -0
- package/package.json +72 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { HerokuApiClient } from '@heroku/heroku-fetch';
|
|
2
|
+
import createDebug from 'debug';
|
|
3
|
+
import { extendResource } from '../../core/extend-resource.js';
|
|
4
|
+
const debug = createDebug('heroku:sdk:resources:pipeline-promotion');
|
|
5
|
+
const DEFAULT_INTERVAL_MS = 1000;
|
|
6
|
+
const DEFAULT_RELEASE_STREAM_MAX_ATTEMPTS = 100;
|
|
7
|
+
export async function promotePipeline(ctx, body, options = {}) {
|
|
8
|
+
const { intervalMs = DEFAULT_INTERVAL_MS, onReleaseStream, releaseStreamMaxAttempts = DEFAULT_RELEASE_STREAM_MAX_ATTEMPTS, signal, timeoutMs, } = options;
|
|
9
|
+
debug('promote source=%s targets=%d', body.source?.app?.id ?? '<unknown>', body.targets?.length ?? 0);
|
|
10
|
+
const promotion = await ctx.platform.pipelinePromotion.create(body);
|
|
11
|
+
if (!promotion.id) {
|
|
12
|
+
throw new Error('Pipeline promotion response did not include an id');
|
|
13
|
+
}
|
|
14
|
+
debug('promote created promotion=%s intervalMs=%d timeoutMs=%s', promotion.id, intervalMs, timeoutMs ?? '<none>');
|
|
15
|
+
const deadline = timeoutMs === undefined ? undefined : Date.now() + timeoutMs;
|
|
16
|
+
const streamRunner = onReleaseStream
|
|
17
|
+
? createReleaseStreamRunner(ctx, onReleaseStream, releaseStreamMaxAttempts, intervalMs, signal)
|
|
18
|
+
: undefined;
|
|
19
|
+
let poll = 0;
|
|
20
|
+
while (true) {
|
|
21
|
+
signal?.throwIfAborted();
|
|
22
|
+
poll++;
|
|
23
|
+
// eslint-disable-next-line no-await-in-loop
|
|
24
|
+
const targets = await ctx.platform.pipelinePromotionTarget.list(promotion.id);
|
|
25
|
+
debug('promote poll=%d promotion=%s statuses=%o', poll, promotion.id, targets.map(target => target.status));
|
|
26
|
+
if (targets.every(target => target.status !== 'pending')) {
|
|
27
|
+
debug('promote terminal promotion=%s polls=%d', promotion.id, poll);
|
|
28
|
+
return { promotion, targets };
|
|
29
|
+
}
|
|
30
|
+
// eslint-disable-next-line no-await-in-loop
|
|
31
|
+
await streamRunner?.(targets);
|
|
32
|
+
if (deadline !== undefined && Date.now() >= deadline) {
|
|
33
|
+
throw new Error(`Pipeline promotion ${promotion.id} did not reach a terminal state within ${timeoutMs}ms`);
|
|
34
|
+
}
|
|
35
|
+
// eslint-disable-next-line no-await-in-loop
|
|
36
|
+
await wait(intervalMs, signal);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Returns a function that, on first call where `targets` is a single
|
|
41
|
+
* promotion target with `release.id` and `app.id`, fetches the release's
|
|
42
|
+
* output stream and hands it to `onReleaseStream`. Subsequent calls are
|
|
43
|
+
* no-ops; the stream is one-shot per promotion.
|
|
44
|
+
*/
|
|
45
|
+
function createReleaseStreamRunner(ctx, onReleaseStream, releaseStreamMaxAttempts, intervalMs, signal) {
|
|
46
|
+
let handled = false;
|
|
47
|
+
return async (targets) => {
|
|
48
|
+
if (handled || targets.length !== 1)
|
|
49
|
+
return;
|
|
50
|
+
const target = targets[0];
|
|
51
|
+
if (!target.release?.id || !target.app?.id)
|
|
52
|
+
return;
|
|
53
|
+
handled = true;
|
|
54
|
+
debug('promote release-stream lookup app=%s release=%s', target.app.id, target.release.id);
|
|
55
|
+
const release = await ctx.platform.release.info(target.app.id, target.release.id);
|
|
56
|
+
if (!release.output_stream_url) {
|
|
57
|
+
debug('promote release-stream skipped reason=no-output-stream-url');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
debug('promote release-stream fetching url=%s maxAttempts=%d', release.output_stream_url, releaseStreamMaxAttempts);
|
|
61
|
+
const stream = await fetchReleaseOutput(release.output_stream_url, releaseStreamMaxAttempts, intervalMs, signal);
|
|
62
|
+
await onReleaseStream({ stream, target });
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export const pipelinePromotionExtensions = extendResource('platform', 'pipelinePromotion', ctx => ({
|
|
66
|
+
promote: (body, options) => promotePipeline(ctx, body, options),
|
|
67
|
+
}));
|
|
68
|
+
async function fetchReleaseOutput(url, maxAttempts, intervalMs, signal) {
|
|
69
|
+
const parsed = new URL(url);
|
|
70
|
+
const path = `${parsed.pathname}${parsed.search}`;
|
|
71
|
+
// The release output URL points at a third-party stream host (e.g. busl).
|
|
72
|
+
// Use a custom-service client so the platform prefixUrl and bearer token
|
|
73
|
+
// don't leak across origins.
|
|
74
|
+
const buslClient = new HerokuApiClient({
|
|
75
|
+
baseUrl: parsed.origin,
|
|
76
|
+
service: 'custom',
|
|
77
|
+
token: '',
|
|
78
|
+
});
|
|
79
|
+
let attempt = 0;
|
|
80
|
+
while (true) {
|
|
81
|
+
signal?.throwIfAborted();
|
|
82
|
+
attempt++;
|
|
83
|
+
let response;
|
|
84
|
+
try {
|
|
85
|
+
// eslint-disable-next-line no-await-in-loop
|
|
86
|
+
response = await buslClient.stream(path);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Treat HTTP errors thrown by the api-client as a retryable miss.
|
|
90
|
+
response = undefined;
|
|
91
|
+
}
|
|
92
|
+
if (response?.ok && response.body) {
|
|
93
|
+
debug('release-stream attempt=%d status=%d ok stream=true', attempt, response.status);
|
|
94
|
+
return response.body;
|
|
95
|
+
}
|
|
96
|
+
debug('release-stream attempt=%d status=%s ok=false', attempt, response?.status ?? '<error>');
|
|
97
|
+
if (response?.body) {
|
|
98
|
+
// Drain so the connection can be reused.
|
|
99
|
+
// eslint-disable-next-line no-await-in-loop
|
|
100
|
+
await response.body.cancel().catch(() => { });
|
|
101
|
+
}
|
|
102
|
+
if (attempt >= maxAttempts) {
|
|
103
|
+
throw new Error('stream release output not available');
|
|
104
|
+
}
|
|
105
|
+
// eslint-disable-next-line no-await-in-loop
|
|
106
|
+
await wait(intervalMs, signal);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function wait(ms, signal) {
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
function onAbort() {
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
reject(signal.reason ?? new Error('Aborted'));
|
|
114
|
+
}
|
|
115
|
+
const timer = setTimeout(() => {
|
|
116
|
+
signal?.removeEventListener('abort', onAbort);
|
|
117
|
+
resolve();
|
|
118
|
+
}, ms);
|
|
119
|
+
if (signal) {
|
|
120
|
+
if (signal.aborted) {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
reject(signal.reason ?? new Error('Aborted'));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Pipeline } from '@heroku/types/3.sdk';
|
|
2
|
+
import { type HerokuApiClientOptions } from '@heroku/heroku-fetch';
|
|
3
|
+
import type { ResourceCtx } from '../../core/extend-resource.js';
|
|
4
|
+
export type ResolvePipelineOptions = {
|
|
5
|
+
clientOptions?: HerokuApiClientOptions;
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
};
|
|
8
|
+
export declare class PipelineNotFoundError extends Error {
|
|
9
|
+
readonly identity: string;
|
|
10
|
+
readonly id = "not_found";
|
|
11
|
+
readonly statusCode = 404;
|
|
12
|
+
constructor(identity: string);
|
|
13
|
+
get body(): {
|
|
14
|
+
id: string;
|
|
15
|
+
message: string;
|
|
16
|
+
resource: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export declare class PipelineAmbiguousError extends Error {
|
|
20
|
+
readonly identity: string;
|
|
21
|
+
readonly matches: Pipeline[];
|
|
22
|
+
readonly id = "multiple_matches";
|
|
23
|
+
readonly statusCode = 422;
|
|
24
|
+
constructor(identity: string, matches: Pipeline[]);
|
|
25
|
+
get body(): {
|
|
26
|
+
id: string;
|
|
27
|
+
message: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a Pipeline by identity.
|
|
32
|
+
*
|
|
33
|
+
* The identity may be:
|
|
34
|
+
* - a UUID — fetched directly via `pipeline.info`.
|
|
35
|
+
* - a name — looked up via `GET /pipelines?eq[name]=<name>`. Pipeline
|
|
36
|
+
* names are not unique platform-wide, so a name lookup may return
|
|
37
|
+
* zero, one, or many matches.
|
|
38
|
+
*
|
|
39
|
+
* The name path uses a raw `HerokuApiClient` because the route
|
|
40
|
+
* registry's `pipeline.list` doesn't accept query params, and we don't
|
|
41
|
+
* want to bake those params into the routes-generated client (where
|
|
42
|
+
* they would leak across unrelated calls).
|
|
43
|
+
*
|
|
44
|
+
* Errors:
|
|
45
|
+
* - throws `PipelineNotFoundError` when no pipeline matches the name.
|
|
46
|
+
* - throws `PipelineAmbiguousError` when more than one does. Callers
|
|
47
|
+
* that want to prompt the user should catch this and inspect
|
|
48
|
+
* `error.matches`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolvePipeline(ctx: Pick<ResourceCtx, 'platform'>, identity: string, options?: ResolvePipelineOptions): Promise<Pipeline>;
|
|
51
|
+
export declare const pipelineExtensions: import("../../index.js").ResourceExtension<"platform", "pipeline", {
|
|
52
|
+
resolve: (identity: string, options?: ResolvePipelineOptions) => Promise<Pipeline>;
|
|
53
|
+
}>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { HerokuApiClient } from '@heroku/heroku-fetch';
|
|
2
|
+
import { extendResource } from '../../core/extend-resource.js';
|
|
3
|
+
export class PipelineNotFoundError extends Error {
|
|
4
|
+
identity;
|
|
5
|
+
id = 'not_found';
|
|
6
|
+
statusCode = 404;
|
|
7
|
+
constructor(identity) {
|
|
8
|
+
super(`Couldn't find a pipeline matching '${identity}'.`);
|
|
9
|
+
this.identity = identity;
|
|
10
|
+
this.name = 'PipelineNotFoundError';
|
|
11
|
+
}
|
|
12
|
+
get body() {
|
|
13
|
+
return { id: this.id, message: this.message, resource: 'pipeline' };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class PipelineAmbiguousError extends Error {
|
|
17
|
+
identity;
|
|
18
|
+
matches;
|
|
19
|
+
id = 'multiple_matches';
|
|
20
|
+
statusCode = 422;
|
|
21
|
+
constructor(identity, matches) {
|
|
22
|
+
super(`Ambiguous identifier; multiple pipelines named '${identity}' found (${matches.length} matches).`);
|
|
23
|
+
this.identity = identity;
|
|
24
|
+
this.matches = matches;
|
|
25
|
+
this.name = 'PipelineAmbiguousError';
|
|
26
|
+
}
|
|
27
|
+
get body() {
|
|
28
|
+
return { id: this.id, message: this.message };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const UUID_RE = /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i;
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a Pipeline by identity.
|
|
34
|
+
*
|
|
35
|
+
* The identity may be:
|
|
36
|
+
* - a UUID — fetched directly via `pipeline.info`.
|
|
37
|
+
* - a name — looked up via `GET /pipelines?eq[name]=<name>`. Pipeline
|
|
38
|
+
* names are not unique platform-wide, so a name lookup may return
|
|
39
|
+
* zero, one, or many matches.
|
|
40
|
+
*
|
|
41
|
+
* The name path uses a raw `HerokuApiClient` because the route
|
|
42
|
+
* registry's `pipeline.list` doesn't accept query params, and we don't
|
|
43
|
+
* want to bake those params into the routes-generated client (where
|
|
44
|
+
* they would leak across unrelated calls).
|
|
45
|
+
*
|
|
46
|
+
* Errors:
|
|
47
|
+
* - throws `PipelineNotFoundError` when no pipeline matches the name.
|
|
48
|
+
* - throws `PipelineAmbiguousError` when more than one does. Callers
|
|
49
|
+
* that want to prompt the user should catch this and inspect
|
|
50
|
+
* `error.matches`.
|
|
51
|
+
*/
|
|
52
|
+
export async function resolvePipeline(ctx, identity, options = {}) {
|
|
53
|
+
options.signal?.throwIfAborted();
|
|
54
|
+
if (UUID_RE.test(identity)) {
|
|
55
|
+
return ctx.platform.pipeline.info(identity);
|
|
56
|
+
}
|
|
57
|
+
const apiClient = new HerokuApiClient({
|
|
58
|
+
...options.clientOptions,
|
|
59
|
+
service: 'platform',
|
|
60
|
+
});
|
|
61
|
+
const response = await apiClient.get('/pipelines', {
|
|
62
|
+
searchParams: { 'eq[name]': identity },
|
|
63
|
+
});
|
|
64
|
+
const matches = (await response.json());
|
|
65
|
+
if (matches.length === 0) {
|
|
66
|
+
throw new PipelineNotFoundError(identity);
|
|
67
|
+
}
|
|
68
|
+
if (matches.length > 1) {
|
|
69
|
+
throw new PipelineAmbiguousError(identity, matches);
|
|
70
|
+
}
|
|
71
|
+
return matches[0];
|
|
72
|
+
}
|
|
73
|
+
export const pipelineExtensions = extendResource('platform', 'pipeline', ctx => ({
|
|
74
|
+
resolve: (identity, options) => resolvePipeline(ctx, identity, options),
|
|
75
|
+
}));
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { HerokuApiClientOptions } from '@heroku/heroku-fetch';
|
|
2
|
+
import type { HerokuClient } from '@heroku/types/data';
|
|
3
|
+
import type { RoutesClientExtras } from '../core/create-client.js';
|
|
4
|
+
export type DataClient = HerokuClient & RoutesClientExtras<HerokuClient>;
|
|
5
|
+
export declare function createDataClient(options?: HerokuApiClientOptions): DataClient;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { HerokuApiClientOptions } from '@heroku/heroku-fetch';
|
|
2
|
+
import type { HerokuClient } from '@heroku/types/3.sdk';
|
|
3
|
+
import type { RoutesClientExtras } from '../core/create-client.js';
|
|
4
|
+
export type PlatformClient = HerokuClient & RoutesClientExtras<HerokuClient>;
|
|
5
|
+
export declare function createPlatformClient(options?: HerokuApiClientOptions): PlatformClient;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Promise-based delay that respects an optional AbortSignal.
|
|
3
|
+
*
|
|
4
|
+
* Resolves after `ms` milliseconds. If `signal` is provided and is
|
|
5
|
+
* (or becomes) aborted, the promise rejects with `signal.reason`
|
|
6
|
+
* (or `new Error('Aborted')` if `reason` is unset) and clears the
|
|
7
|
+
* pending timer.
|
|
8
|
+
*
|
|
9
|
+
* Used by polling loops in resource extensions (e.g. `addOn.createAndWait`,
|
|
10
|
+
* `pipelinePromotion.promote`) to space out poll requests while staying
|
|
11
|
+
* responsive to cancellation.
|
|
12
|
+
*
|
|
13
|
+
* @param ms Milliseconds to wait before resolving.
|
|
14
|
+
* @param signal Optional AbortSignal to cancel the wait early.
|
|
15
|
+
*/
|
|
16
|
+
export declare function wait(ms: number, signal?: AbortSignal): Promise<void>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Promise-based delay that respects an optional AbortSignal.
|
|
3
|
+
*
|
|
4
|
+
* Resolves after `ms` milliseconds. If `signal` is provided and is
|
|
5
|
+
* (or becomes) aborted, the promise rejects with `signal.reason`
|
|
6
|
+
* (or `new Error('Aborted')` if `reason` is unset) and clears the
|
|
7
|
+
* pending timer.
|
|
8
|
+
*
|
|
9
|
+
* Used by polling loops in resource extensions (e.g. `addOn.createAndWait`,
|
|
10
|
+
* `pipelinePromotion.promote`) to space out poll requests while staying
|
|
11
|
+
* responsive to cancellation.
|
|
12
|
+
*
|
|
13
|
+
* @param ms Milliseconds to wait before resolving.
|
|
14
|
+
* @param signal Optional AbortSignal to cancel the wait early.
|
|
15
|
+
*/
|
|
16
|
+
export function wait(ms, signal) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
function onAbort() {
|
|
19
|
+
clearTimeout(timer);
|
|
20
|
+
reject(signal.reason ?? new Error('Aborted'));
|
|
21
|
+
}
|
|
22
|
+
const timer = setTimeout(() => {
|
|
23
|
+
signal?.removeEventListener('abort', onAbort);
|
|
24
|
+
resolve();
|
|
25
|
+
}, ms);
|
|
26
|
+
if (signal) {
|
|
27
|
+
if (signal.aborted) {
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
reject(signal.reason ?? new Error('Aborted'));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@heroku/sdk",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "Heroku SDK",
|
|
5
|
+
"author": "Heroku",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./platform": {
|
|
14
|
+
"types": "./dist/services/platform.d.ts",
|
|
15
|
+
"default": "./dist/services/platform.js"
|
|
16
|
+
},
|
|
17
|
+
"./data": {
|
|
18
|
+
"types": "./dist/services/data.d.ts",
|
|
19
|
+
"default": "./dist/services/data.js"
|
|
20
|
+
},
|
|
21
|
+
"./extensions/platform": {
|
|
22
|
+
"types": "./dist/resources/extensions/platform.d.ts",
|
|
23
|
+
"default": "./dist/resources/extensions/platform.js"
|
|
24
|
+
},
|
|
25
|
+
"./extensions/data": {
|
|
26
|
+
"types": "./dist/resources/extensions/data.d.ts",
|
|
27
|
+
"default": "./dist/resources/extensions/data.js"
|
|
28
|
+
},
|
|
29
|
+
"./resources/*": {
|
|
30
|
+
"types": "./dist/resources/*.d.ts",
|
|
31
|
+
"default": "./dist/resources/*.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"sideEffects": false,
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/heroku/heroku-sdk"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc",
|
|
41
|
+
"coverage": "vitest --run --coverage",
|
|
42
|
+
"example": "tsx",
|
|
43
|
+
"lint": "eslint",
|
|
44
|
+
"lint:fix": "eslint --fix",
|
|
45
|
+
"prepare": "npm run build",
|
|
46
|
+
"test": "vitest --run"
|
|
47
|
+
},
|
|
48
|
+
"license": "Apache-2.0",
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=20"
|
|
51
|
+
},
|
|
52
|
+
"files": [
|
|
53
|
+
"dist"
|
|
54
|
+
],
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@heroku-cli/test-utils": "^0.3.0",
|
|
57
|
+
"@types/debug": "^4.1.12",
|
|
58
|
+
"@types/node": "^25.6.2",
|
|
59
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
60
|
+
"eslint": "^9.39.4",
|
|
61
|
+
"eslint-config-oclif": "^6.0.167",
|
|
62
|
+
"eslint-plugin-vitest": "0.5.4",
|
|
63
|
+
"tsx": "^4.22.4",
|
|
64
|
+
"typescript": "^6.0.2",
|
|
65
|
+
"vitest": "^4.1.4"
|
|
66
|
+
},
|
|
67
|
+
"dependencies": {
|
|
68
|
+
"@heroku/heroku-fetch": "^0.1.1-beta",
|
|
69
|
+
"@heroku/types": "^3.0.0-beta",
|
|
70
|
+
"debug": "^4.4.0"
|
|
71
|
+
}
|
|
72
|
+
}
|