@heroku/sdk 0.11.1 → 0.12.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/dist/core/create-client.d.ts +7 -2
- package/dist/core/create-client.js +5 -2
- package/dist/core/extend-resource.d.ts +52 -2
- package/dist/core/extend-resource.js +27 -0
- package/dist/core/extensions-proxy.js +51 -1
- package/dist/core/heroku-sdk.js +44 -1
- package/dist/core/with-signal.d.ts +19 -0
- package/dist/core/with-signal.js +20 -0
- package/dist/resources/data/database.js +19 -6
- package/dist/resources/data/internal/resolve-pg-database.js +4 -7
- package/dist/resources/data/maintenance.js +4 -1
- package/dist/resources/data/postgres-database.js +4 -1
- package/dist/resources/data/redis/resolve-by-app.js +2 -1
- package/dist/resources/data/redis/wait-for-ready.d.ts +1 -1
- package/dist/resources/data/redis/wait-for-ready.js +3 -2
- package/dist/resources/data/transfer/capture-and-wait.js +4 -1
- package/dist/resources/data/transfer/restore-and-wait.js +4 -1
- package/dist/resources/data/transfer/wait-for-transfer.js +8 -1
- package/dist/resources/data/transfer-schedule.js +10 -3
- package/dist/resources/extensions/platform.d.ts +3 -0
- package/dist/resources/extensions/platform.js +3 -0
- package/dist/resources/platform/account.d.ts +22 -0
- package/dist/resources/platform/account.js +34 -0
- package/dist/resources/platform/add-on/create-and-wait.js +5 -1
- package/dist/resources/platform/add-on/describe.js +5 -2
- package/dist/resources/platform/add-on/destroy-and-wait.js +2 -3
- package/dist/resources/platform/add-on/list-plans.js +8 -2
- package/dist/resources/platform/add-on/resolve.js +5 -1
- package/dist/resources/platform/add-on/upgrade.js +5 -2
- package/dist/resources/platform/add-on/wait-for-provisioning.js +4 -1
- package/dist/resources/platform/add-on-attachment/resolve.js +2 -3
- package/dist/resources/platform/app/create-and-setup.d.ts +3 -4
- package/dist/resources/platform/app/create-and-setup.js +5 -5
- package/dist/resources/platform/app/diff.js +2 -3
- package/dist/resources/platform/app/index.js +9 -3
- package/dist/resources/platform/app/info.d.ts +4 -0
- package/dist/resources/platform/app/info.js +21 -7
- package/dist/resources/platform/app/transfer.js +2 -1
- package/dist/resources/platform/app/wait-for-acm-certificates.js +2 -1
- package/dist/resources/platform/container/ensure-container-stack.js +2 -1
- package/dist/resources/platform/container/release-images.js +2 -1
- package/dist/resources/platform/container/remove-process-types.js +2 -1
- package/dist/resources/platform/delinquency.d.ts +23 -0
- package/dist/resources/platform/delinquency.js +41 -0
- package/dist/resources/platform/domain/clear-custom-domains.js +2 -1
- package/dist/resources/platform/domain/create-and-wait.js +2 -1
- package/dist/resources/platform/domain/wait-for-ready.js +2 -1
- package/dist/resources/platform/dyno/exec.d.ts +12 -2
- package/dist/resources/platform/dyno/exec.js +27 -12
- package/dist/resources/platform/dyno/index.js +9 -6
- package/dist/resources/platform/dyno/list-extended.d.ts +7 -1
- package/dist/resources/platform/dyno/list-extended.js +8 -3
- package/dist/resources/platform/dyno/run.d.ts +1 -1
- package/dist/resources/platform/dyno/run.js +5 -3
- package/dist/resources/platform/dyno/wait-for-info.js +2 -1
- package/dist/resources/platform/dyno/wait-for-release.js +2 -1
- package/dist/resources/platform/log-session/index.d.ts +1 -1
- package/dist/resources/platform/log-session/index.js +12 -11
- package/dist/resources/platform/pipeline/coupling.js +5 -2
- package/dist/resources/platform/pipeline/index.d.ts +2 -2
- package/dist/resources/platform/pipeline/index.js +6 -4
- package/dist/resources/platform/pipeline/promotion.d.ts +1 -1
- package/dist/resources/platform/pipeline/promotion.js +22 -18
- package/dist/resources/platform/review-app-config/resolve-repo-name.js +4 -3
- package/dist/resources/platform/sni-endpoint/create-and-associate.js +2 -1
- package/dist/resources/platform/space/wait-for-allocated.js +2 -4
- package/dist/resources/platform/team.d.ts +22 -0
- package/dist/resources/platform/team.js +34 -0
- package/dist/resources/platform/telemetry-drain/remove-drains-for-target.js +2 -1
- package/dist/resources/platform/test-run/wait-for-state.js +2 -1
- package/dist/resources/platform/vpn-connection/wait-for-active.js +2 -1
- package/dist/utils/is-abort-error.d.ts +11 -0
- package/dist/utils/is-abort-error.js +14 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +2 -0
- package/package.json +9 -4
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { NotFoundError } from '@heroku/heroku-fetch';
|
|
2
|
+
import { makeCustomHostClient } from '../../../core/extend-resource.js';
|
|
3
|
+
import { withSignal } from '../../../core/with-signal.js';
|
|
2
4
|
import { wait } from '../../../utils/wait.js';
|
|
3
5
|
import { waitForInfo } from './wait-for-info.js';
|
|
4
6
|
/** The app feature that gates Heroku Exec. */
|
|
@@ -20,7 +22,7 @@ const DEFAULT_SETTLE_MS = 2000;
|
|
|
20
22
|
*/
|
|
21
23
|
export async function execPrereqs(ctx, appIdentity, options = {}) {
|
|
22
24
|
options.signal?.throwIfAborted();
|
|
23
|
-
const platform =
|
|
25
|
+
const platform = withSignal(ctx.platform, options.signal);
|
|
24
26
|
const [app, buildpacks, configVars, feature] = await Promise.all([
|
|
25
27
|
platform.app.info(appIdentity),
|
|
26
28
|
platform.buildpackInstallation.list(appIdentity),
|
|
@@ -51,7 +53,7 @@ export async function execPrereqs(ctx, appIdentity, options = {}) {
|
|
|
51
53
|
*/
|
|
52
54
|
export async function enableExec(ctx, appIdentity, options = {}) {
|
|
53
55
|
options.signal?.throwIfAborted();
|
|
54
|
-
const platform =
|
|
56
|
+
const platform = withSignal(ctx.platform, options.signal);
|
|
55
57
|
await platform.appFeature.update(appIdentity, EXEC_FEATURE, { enabled: true });
|
|
56
58
|
}
|
|
57
59
|
/**
|
|
@@ -82,7 +84,7 @@ export async function restartForExec(ctx, appIdentity, dynoIdentity, options = {
|
|
|
82
84
|
const { settleMs = DEFAULT_SETTLE_MS, ...waitOptions } = options;
|
|
83
85
|
const { signal } = waitOptions;
|
|
84
86
|
signal?.throwIfAborted();
|
|
85
|
-
const platform =
|
|
87
|
+
const platform = withSignal(ctx.platform, signal);
|
|
86
88
|
await wait(settleMs, signal);
|
|
87
89
|
await platform.dyno.restartAll(appIdentity);
|
|
88
90
|
return waitForInfo(ctx, appIdentity, dynoIdentity, {
|
|
@@ -106,11 +108,16 @@ export async function restartForExec(ctx, appIdentity, dynoIdentity, options = {
|
|
|
106
108
|
* `Authorization` header survive). The caller feeds the returned info,
|
|
107
109
|
* plus the private key it holds, to its own SSH transport; the SDK does
|
|
108
110
|
* not open the connection.
|
|
111
|
+
*
|
|
112
|
+
* @param ctx - Internal: supplied by the SDK's dyno extension to thread the
|
|
113
|
+
* resolved client identity and cancellation signal into the custom-host
|
|
114
|
+
* client. Standalone callers omit it, and the client falls back to the
|
|
115
|
+
* pre-threading bare construction.
|
|
109
116
|
*/
|
|
110
|
-
export async function exchangeExecCredentials(appIdentity, options) {
|
|
117
|
+
export async function exchangeExecCredentials(appIdentity, options, ctx = {}) {
|
|
111
118
|
options.signal?.throwIfAborted();
|
|
112
119
|
const endpoint = resolveExecEndpoint(appIdentity, options.apiKey, options.configVars);
|
|
113
|
-
const client = newExecClient(endpoint.baseUrl, options.clientOptions);
|
|
120
|
+
const client = newExecClient(endpoint.baseUrl, options.clientOptions, ctx);
|
|
114
121
|
const path = `${endpoint.apiPath}/${encodeURIComponent(options.dyno)}`;
|
|
115
122
|
const response = await client.put(path, { client_key: options.publicKey }, {
|
|
116
123
|
headers: {
|
|
@@ -129,11 +136,16 @@ export async function exchangeExecCredentials(appIdentity, options) {
|
|
|
129
136
|
* custom-host Basic-auth setup as {@link exchangeExecCredentials}. The
|
|
130
137
|
* caller correlates each reservation's `dyno_name` with live dyno state
|
|
131
138
|
* for display.
|
|
139
|
+
*
|
|
140
|
+
* @param ctx - Internal: supplied by the SDK's dyno extension to thread the
|
|
141
|
+
* resolved client identity and cancellation signal into the custom-host
|
|
142
|
+
* client. Standalone callers omit it, and the client falls back to the
|
|
143
|
+
* pre-threading bare construction.
|
|
132
144
|
*/
|
|
133
|
-
export async function execStatus(appIdentity, options) {
|
|
145
|
+
export async function execStatus(appIdentity, options, ctx = {}) {
|
|
134
146
|
options.signal?.throwIfAborted();
|
|
135
147
|
const endpoint = resolveExecEndpoint(appIdentity, options.apiKey, options.configVars);
|
|
136
|
-
const client = newExecClient(endpoint.baseUrl, options.clientOptions);
|
|
148
|
+
const client = newExecClient(endpoint.baseUrl, options.clientOptions, ctx);
|
|
137
149
|
const response = await client.get(endpoint.apiPath, {
|
|
138
150
|
headers: { ...options.headers, Authorization: endpoint.authorization },
|
|
139
151
|
signal: options.signal,
|
|
@@ -168,10 +180,13 @@ function resolveExecEndpoint(appIdentity, apiKey, configVars) {
|
|
|
168
180
|
baseUrl: `${url.protocol}//${url.host}`,
|
|
169
181
|
};
|
|
170
182
|
}
|
|
171
|
-
function newExecClient(baseUrl, clientOptions) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
183
|
+
function newExecClient(baseUrl, clientOptions, ctx = {}) {
|
|
184
|
+
// exec-manager is a custom host with Basic auth embedded per request. The
|
|
185
|
+
// custom-host client forces `token: ''` and inherits only the SDK's identity
|
|
186
|
+
// (User-Agent) and signal — never `reauthorize`/headers, so a bad-Basic 401
|
|
187
|
+
// can't drive a spurious `heroku login`. The caller's `clientOptions` still
|
|
188
|
+
// apply on top.
|
|
189
|
+
return makeCustomHostClient(ctx, baseUrl, clientOptions);
|
|
175
190
|
}
|
|
176
191
|
function toBasicAuth(username, password) {
|
|
177
192
|
return `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { extendResource } from '../../../core/extend-resource.js';
|
|
2
|
+
import { withSignal } from '../../../core/with-signal.js';
|
|
2
3
|
import { enableExec, exchangeExecCredentials, execPrereqs, execStatus, restartForExec, } from './exec.js';
|
|
3
4
|
import { listExtended } from './list-extended.js';
|
|
4
5
|
import { runDyno } from './run.js';
|
|
@@ -20,22 +21,24 @@ export async function scaleDynos(ctx, appIdentity, updates, options = {}) {
|
|
|
20
21
|
}
|
|
21
22
|
export async function restartDynos(ctx, appIdentity, target, options = {}) {
|
|
22
23
|
options.signal?.throwIfAborted();
|
|
24
|
+
// Bind the signal so the request aborts in flight.
|
|
25
|
+
const platform = withSignal(ctx.platform, options.signal);
|
|
23
26
|
if (!target) {
|
|
24
|
-
await
|
|
27
|
+
await platform.dyno.restartAll(appIdentity);
|
|
25
28
|
return;
|
|
26
29
|
}
|
|
27
30
|
if ('dyno' in target) {
|
|
28
|
-
await
|
|
31
|
+
await platform.dyno.restart(appIdentity, target.dyno);
|
|
29
32
|
return;
|
|
30
33
|
}
|
|
31
|
-
await
|
|
34
|
+
await platform.dyno.restartFormation(appIdentity, target.type);
|
|
32
35
|
}
|
|
33
36
|
export const dynoExtensions = extendResource('platform', 'dyno', ctx => ({
|
|
34
37
|
enableExec: (appIdentity, options) => (enableExec(ctx, appIdentity, options)),
|
|
35
|
-
exchangeExecCredentials: (appIdentity, options) => (exchangeExecCredentials(appIdentity, options)),
|
|
38
|
+
exchangeExecCredentials: (appIdentity, options) => (exchangeExecCredentials(appIdentity, options, ctx)),
|
|
36
39
|
execPrereqs: (appIdentity, options) => (execPrereqs(ctx, appIdentity, options)),
|
|
37
|
-
execStatus: (appIdentity, options) => (execStatus(appIdentity, options)),
|
|
38
|
-
listExtended: (appIdentity, options) => (listExtended(appIdentity, options)),
|
|
40
|
+
execStatus: (appIdentity, options) => (execStatus(appIdentity, options, ctx)),
|
|
41
|
+
listExtended: (appIdentity, options) => (listExtended(appIdentity, options, ctx)),
|
|
39
42
|
restart: (appIdentity, target, options) => (restartDynos(ctx, appIdentity, target, options)),
|
|
40
43
|
restartForExec: (appIdentity, dynoIdentity, options) => (restartForExec(ctx, appIdentity, dynoIdentity, options)),
|
|
41
44
|
run: (appIdentity, command, options) => (runDyno(ctx, appIdentity, command, options)),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { HerokuApiClientOptions } from '@heroku/heroku-fetch';
|
|
2
2
|
import type { Dyno } from '@heroku/types/3.sdk';
|
|
3
|
+
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
3
4
|
/**
|
|
4
5
|
* Operational detail the platform attaches to each dyno only when the
|
|
5
6
|
* `?extended=true` query is used with the `version=3.sdk` Accept
|
|
@@ -46,5 +47,10 @@ export type ListExtendedOptions = {
|
|
|
46
47
|
*
|
|
47
48
|
* Unlike the routes client, this does not auto-paginate — it issues a
|
|
48
49
|
* single GET, matching the historical CLI behavior.
|
|
50
|
+
*
|
|
51
|
+
* @param ctx - Internal: supplied by the SDK's dyno extension to thread the
|
|
52
|
+
* resolved client identity and cancellation signal into the bare client.
|
|
53
|
+
* Standalone callers omit it, and the client falls back to the pre-threading
|
|
54
|
+
* bare construction.
|
|
49
55
|
*/
|
|
50
|
-
export declare function listExtended(appIdentity: string, options?: ListExtendedOptions): Promise<DynoExtended[]>;
|
|
56
|
+
export declare function listExtended(appIdentity: string, options?: ListExtendedOptions, ctx?: Pick<ResourceCtx, 'makeClient'>): Promise<DynoExtended[]>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { makeCtxClient } from '../../../core/extend-resource.js';
|
|
2
2
|
const ACCEPT_EXTENDED = 'application/vnd.heroku+json; version=3.sdk';
|
|
3
3
|
/**
|
|
4
4
|
* List an app's dynos with the extended operational detail block.
|
|
@@ -12,10 +12,15 @@ const ACCEPT_EXTENDED = 'application/vnd.heroku+json; version=3.sdk';
|
|
|
12
12
|
*
|
|
13
13
|
* Unlike the routes client, this does not auto-paginate — it issues a
|
|
14
14
|
* single GET, matching the historical CLI behavior.
|
|
15
|
+
*
|
|
16
|
+
* @param ctx - Internal: supplied by the SDK's dyno extension to thread the
|
|
17
|
+
* resolved client identity and cancellation signal into the bare client.
|
|
18
|
+
* Standalone callers omit it, and the client falls back to the pre-threading
|
|
19
|
+
* bare construction.
|
|
15
20
|
*/
|
|
16
|
-
export async function listExtended(appIdentity, options = {}) {
|
|
21
|
+
export async function listExtended(appIdentity, options = {}, ctx = {}) {
|
|
17
22
|
options.signal?.throwIfAborted();
|
|
18
|
-
const apiClient =
|
|
23
|
+
const apiClient = makeCtxClient(ctx, 'platform', {
|
|
19
24
|
...options.clientOptions,
|
|
20
25
|
service: 'platform',
|
|
21
26
|
});
|
|
@@ -73,4 +73,4 @@ export type RunDynoOptions = {
|
|
|
73
73
|
* open their own interactive session; the SDK does not establish the
|
|
74
74
|
* session, spawn ssh, or manage TTY state.
|
|
75
75
|
*/
|
|
76
|
-
export declare function runDyno(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, command: string, options?: RunDynoOptions): Promise<Dyno>;
|
|
76
|
+
export declare function runDyno(ctx: Pick<ResourceCtx, 'makeClient' | 'platform'>, appIdentity: string, command: string, options?: RunDynoOptions): Promise<Dyno>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HerokuApiError } from '@heroku/heroku-fetch';
|
|
2
|
+
import { makeCtxClient } from '../../../core/extend-resource.js';
|
|
3
|
+
import { withSignal } from '../../../core/with-signal.js';
|
|
2
4
|
const DEFAULT_RUN_RETRIES = 2;
|
|
3
5
|
const ACCEPT_RUN_INSIDE = 'application/vnd.heroku+json; version=3.sdk';
|
|
4
6
|
/**
|
|
@@ -58,7 +60,7 @@ export async function runDyno(ctx, appIdentity, command, options = {}) {
|
|
|
58
60
|
}
|
|
59
61
|
async function postRun(ctx, appIdentity, body, options) {
|
|
60
62
|
if (options.dyno) {
|
|
61
|
-
const apiClient =
|
|
63
|
+
const apiClient = makeCtxClient(ctx, 'platform', {
|
|
62
64
|
...options.clientOptions,
|
|
63
65
|
service: 'platform',
|
|
64
66
|
});
|
|
@@ -69,6 +71,6 @@ async function postRun(ctx, appIdentity, body, options) {
|
|
|
69
71
|
});
|
|
70
72
|
return (await response.json());
|
|
71
73
|
}
|
|
72
|
-
const platform =
|
|
74
|
+
const platform = withSignal(ctx.platform, options.signal);
|
|
73
75
|
return platform.dyno.create(appIdentity, body);
|
|
74
76
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { HerokuApiError, NotFoundError, RateLimitError } from '@heroku/heroku-fetch';
|
|
2
|
+
import { withSignal } from '../../../core/with-signal.js';
|
|
2
3
|
import { wait } from '../../../utils/wait.js';
|
|
3
4
|
const DEFAULT_ATTEMPTS = 20;
|
|
4
5
|
const DEFAULT_DELAY_MS = 1000;
|
|
@@ -62,7 +63,7 @@ export async function waitForInfo(ctx, appIdentity, dynoIdentity, options = {})
|
|
|
62
63
|
signal?.throwIfAborted();
|
|
63
64
|
// Scope the in-flight info call to the caller's signal so an abort
|
|
64
65
|
// takes effect during the API call, not just during the backoff.
|
|
65
|
-
const platform =
|
|
66
|
+
const platform = withSignal(ctx.platform, signal);
|
|
66
67
|
let lastError;
|
|
67
68
|
let lastDyno;
|
|
68
69
|
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withSignal } from '../../../core/with-signal.js';
|
|
1
2
|
import { wait } from '../../../utils/wait.js';
|
|
2
3
|
const DEFAULT_DELAY_MS = 10_000;
|
|
3
4
|
const LATEST_RELEASE_RANGE = 'version ..; max=1, order=desc';
|
|
@@ -45,7 +46,7 @@ export class ReleaseNotConvergedError extends Error {
|
|
|
45
46
|
export async function waitForRelease(ctx, appIdentity, options = {}) {
|
|
46
47
|
const { attempts, delayMs = DEFAULT_DELAY_MS, onPoll, signal, type, withRun, } = options;
|
|
47
48
|
signal?.throwIfAborted();
|
|
48
|
-
const platform =
|
|
49
|
+
const platform = withSignal(ctx.platform, signal);
|
|
49
50
|
const releases = await platform
|
|
50
51
|
.withHeaders({ Range: LATEST_RELEASE_RANGE })
|
|
51
52
|
.release.list(appIdentity);
|
|
@@ -95,7 +95,7 @@ export type StreamLogsOptions = {
|
|
|
95
95
|
* - non-2xx response from the logplex URL
|
|
96
96
|
* - errors thrown by `logSession.create` (auth, app-not-found, etc.)
|
|
97
97
|
*/
|
|
98
|
-
export declare function streamLogs(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, options?: StreamLogsOptions): AsyncGenerator<string, void, unknown>;
|
|
98
|
+
export declare function streamLogs(ctx: Pick<ResourceCtx, 'makeClient' | 'platform'>, appIdentity: string, options?: StreamLogsOptions): AsyncGenerator<string, void, unknown>;
|
|
99
99
|
export declare const logSessionExtensions: import("../../../core/extend-resource.js").ResourceExtension<"platform", "logSession", {
|
|
100
100
|
parseHerokuLogLine: typeof parseHerokuLogLine;
|
|
101
101
|
streamLogs: (appIdentity: string, options?: StreamLogsOptions) => AsyncGenerator<string, void, unknown>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { HerokuApiClient } from '@heroku/heroku-fetch';
|
|
2
1
|
import createDebug from 'debug';
|
|
3
|
-
import { extendResource } from '../../../core/extend-resource.js';
|
|
2
|
+
import { extendResource, makeCustomHostClient } from '../../../core/extend-resource.js';
|
|
3
|
+
import { withSignal } from '../../../core/with-signal.js';
|
|
4
4
|
import { getGeneration } from '../app/index.js';
|
|
5
5
|
import { parseHerokuLogLine } from './parser.js';
|
|
6
6
|
export { parseHerokuLogLine, } from './parser.js';
|
|
@@ -37,6 +37,9 @@ const TRANSPORT_RETRY_MAX_DELAY_MS = 5000;
|
|
|
37
37
|
export async function* streamLogs(ctx, appIdentity, options = {}) {
|
|
38
38
|
const { dyno, lines, maxConsecutiveTransportErrors = MAX_CONSECUTIVE_TRANSPORT_ERRORS, maxRetryDelayMs = TRANSPORT_RETRY_MAX_DELAY_MS, retryBaseDelayMs = TRANSPORT_RETRY_BASE_DELAY_MS, sessionTimeoutMs = DEFAULT_FIR_SESSION_TIMEOUT_MS, signal, source, tail = false, type, } = options;
|
|
39
39
|
signal?.throwIfAborted();
|
|
40
|
+
// Bind the caller's signal to the platform client so the session-create
|
|
41
|
+
// request below aborts in flight, not merely between reads of the stream.
|
|
42
|
+
const platform = withSignal(ctx.platform, signal);
|
|
40
43
|
const generation = await getGeneration(ctx, appIdentity, { signal });
|
|
41
44
|
const isFir = generation === 'fir';
|
|
42
45
|
// Fir doesn't support a bounded session; the platform always streams.
|
|
@@ -52,7 +55,7 @@ export async function* streamLogs(ctx, appIdentity, options = {}) {
|
|
|
52
55
|
let consecutiveTransportErrors = 0;
|
|
53
56
|
while (true) {
|
|
54
57
|
debug('streamLogs creating session app=%s generation=%s tail=%s', appIdentity, generation ?? '<unknown>', effectiveTail);
|
|
55
|
-
const session = await
|
|
58
|
+
const session = await platform.logSession.create(appIdentity, createOpts);
|
|
56
59
|
if (!session.logplex_url) {
|
|
57
60
|
throw new Error('Log session response did not include a logplex_url.');
|
|
58
61
|
}
|
|
@@ -64,15 +67,12 @@ export async function* streamLogs(ctx, appIdentity, options = {}) {
|
|
|
64
67
|
try {
|
|
65
68
|
// Logplex URLs hit a region-specific stream host (e.g.
|
|
66
69
|
// virginia.sessions.logs.heroku.com). The token is embedded in
|
|
67
|
-
// the URL, so we use a custom-
|
|
68
|
-
//
|
|
69
|
-
// env-var proxy
|
|
70
|
+
// the URL, so we use a custom-host client with no auth and a
|
|
71
|
+
// per-call baseUrl, inheriting only the SDK's identity and
|
|
72
|
+
// cancellation; this also picks up heroku-fetch's env-var proxy
|
|
73
|
+
// support.
|
|
70
74
|
const parsed = new URL(session.logplex_url);
|
|
71
|
-
const logplexClient =
|
|
72
|
-
baseUrl: parsed.origin,
|
|
73
|
-
service: 'custom',
|
|
74
|
-
token: '',
|
|
75
|
-
});
|
|
75
|
+
const logplexClient = makeCustomHostClient(ctx, parsed.origin);
|
|
76
76
|
// Fir's telemetry-proxy sends newline-delimited plain text by
|
|
77
77
|
// default, but concatenates records with zero delimiter bytes —
|
|
78
78
|
// splitting on '\n' yields nothing. Requesting SSE flips the
|
|
@@ -81,6 +81,7 @@ export async function* streamLogs(ctx, appIdentity, options = {}) {
|
|
|
81
81
|
// header and keeps sending `text/plain`).
|
|
82
82
|
const response = await logplexClient.stream(`${parsed.pathname}${parsed.search}`, {
|
|
83
83
|
headers: { Accept: 'text/event-stream' },
|
|
84
|
+
signal,
|
|
84
85
|
});
|
|
85
86
|
if (!response.body) {
|
|
86
87
|
throw new Error('Logplex stream returned no body.');
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import createDebug from 'debug';
|
|
2
2
|
import { extendResource } from '../../../core/extend-resource.js';
|
|
3
|
+
import { withSignal } from '../../../core/with-signal.js';
|
|
3
4
|
const debug = createDebug('heroku:sdk:resources:pipeline-coupling');
|
|
4
5
|
const APPS_FILTER_LIMIT = 1000;
|
|
5
6
|
/**
|
|
@@ -13,7 +14,9 @@ const APPS_FILTER_LIMIT = 1000;
|
|
|
13
14
|
*/
|
|
14
15
|
export async function listPipelineApps(ctx, pipelineId, options = {}) {
|
|
15
16
|
options.signal?.throwIfAborted();
|
|
16
|
-
|
|
17
|
+
// Bind the signal so both requests abort in flight, not just at the checkpoints.
|
|
18
|
+
const platform = withSignal(ctx.platform, options.signal);
|
|
19
|
+
const allCouplings = await platform.pipelineCoupling.listByPipeline(pipelineId);
|
|
17
20
|
// Drop malformed couplings (no app id) before issuing the bulk filter call.
|
|
18
21
|
const couplings = allCouplings.filter(coupling => coupling.app?.id);
|
|
19
22
|
const skipped = allCouplings.length - couplings.length;
|
|
@@ -35,7 +38,7 @@ export async function listPipelineApps(ctx, pipelineId, options = {}) {
|
|
|
35
38
|
}
|
|
36
39
|
options.signal?.throwIfAborted();
|
|
37
40
|
const ids = couplingsToResolve.map(coupling => coupling.app.id);
|
|
38
|
-
const apps = await
|
|
41
|
+
const apps = await platform
|
|
39
42
|
.withHeaders({ Range: `id ..; max=${APPS_FILTER_LIMIT};` })
|
|
40
43
|
.filterApps.apps({ in: { id: ids } });
|
|
41
44
|
if (apps.length !== ids.length) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import type { HerokuApiClientOptions } from '@heroku/heroku-fetch';
|
|
1
2
|
import type { Pipeline } from '@heroku/types/3.sdk';
|
|
2
|
-
import { type HerokuApiClientOptions } from '@heroku/heroku-fetch';
|
|
3
3
|
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
4
4
|
export type ResolvePipelineOptions = {
|
|
5
5
|
clientOptions?: HerokuApiClientOptions;
|
|
@@ -47,7 +47,7 @@ export declare class PipelineAmbiguousError extends Error {
|
|
|
47
47
|
* that want to prompt the user should catch this and inspect
|
|
48
48
|
* `error.matches`.
|
|
49
49
|
*/
|
|
50
|
-
export declare function resolvePipeline(ctx: Pick<ResourceCtx, 'platform'>, identity: string, options?: ResolvePipelineOptions): Promise<Pipeline>;
|
|
50
|
+
export declare function resolvePipeline(ctx: Pick<ResourceCtx, 'makeClient' | 'platform'>, identity: string, options?: ResolvePipelineOptions): Promise<Pipeline>;
|
|
51
51
|
export declare const pipelineExtensions: import("../../../core/extend-resource.js").ResourceExtension<"platform", "pipeline", {
|
|
52
52
|
resolve: (identity: string, options?: ResolvePipelineOptions) => Promise<Pipeline>;
|
|
53
53
|
}>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { extendResource, makeCtxClient } from '../../../core/extend-resource.js';
|
|
2
|
+
import { withSignal } from '../../../core/with-signal.js';
|
|
3
3
|
export class PipelineNotFoundError extends Error {
|
|
4
4
|
identity;
|
|
5
5
|
id = 'not_found';
|
|
@@ -52,14 +52,16 @@ const UUID_RE = /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i;
|
|
|
52
52
|
export async function resolvePipeline(ctx, identity, options = {}) {
|
|
53
53
|
options.signal?.throwIfAborted();
|
|
54
54
|
if (UUID_RE.test(identity)) {
|
|
55
|
-
|
|
55
|
+
// Bind the signal so the lookup aborts in flight.
|
|
56
|
+
return withSignal(ctx.platform, options.signal).pipeline.info(identity);
|
|
56
57
|
}
|
|
57
|
-
const apiClient =
|
|
58
|
+
const apiClient = makeCtxClient(ctx, 'platform', {
|
|
58
59
|
...options.clientOptions,
|
|
59
60
|
service: 'platform',
|
|
60
61
|
});
|
|
61
62
|
const response = await apiClient.get('/pipelines', {
|
|
62
63
|
searchParams: { 'eq[name]': identity },
|
|
64
|
+
signal: options.signal,
|
|
63
65
|
});
|
|
64
66
|
const matches = (await response.json());
|
|
65
67
|
if (matches.length === 0) {
|
|
@@ -15,7 +15,7 @@ export type PromotePipelineResult = {
|
|
|
15
15
|
promotion: PipelinePromotion;
|
|
16
16
|
targets: PipelinePromotionTarget[];
|
|
17
17
|
};
|
|
18
|
-
export declare function promotePipeline(ctx: Pick<ResourceCtx, 'platform'>, body: PipelinePromotionCreateOpts, options?: PromotePipelineOptions): Promise<PromotePipelineResult>;
|
|
18
|
+
export declare function promotePipeline(ctx: Pick<ResourceCtx, 'makeClient' | 'platform'>, body: PipelinePromotionCreateOpts, options?: PromotePipelineOptions): Promise<PromotePipelineResult>;
|
|
19
19
|
export declare const pipelinePromotionExtensions: import("../../../core/extend-resource.js").ResourceExtension<"platform", "pipelinePromotion", {
|
|
20
20
|
promote: (body: PipelinePromotionCreateOpts, options?: PromotePipelineOptions) => Promise<PromotePipelineResult>;
|
|
21
21
|
}>;
|
|
@@ -1,28 +1,32 @@
|
|
|
1
|
-
import { HerokuApiClient } from '@heroku/heroku-fetch';
|
|
2
1
|
import createDebug from 'debug';
|
|
3
|
-
import { extendResource } from '../../../core/extend-resource.js';
|
|
2
|
+
import { extendResource, makeCustomHostClient } from '../../../core/extend-resource.js';
|
|
3
|
+
import { withSignal } from '../../../core/with-signal.js';
|
|
4
|
+
import { isAbortError } from '../../../utils/is-abort-error.js';
|
|
4
5
|
import { wait } from '../../../utils/wait.js';
|
|
5
6
|
const debug = createDebug('heroku:sdk:resources:pipeline-promotion');
|
|
6
7
|
const DEFAULT_INTERVAL_MS = 1000;
|
|
7
8
|
const DEFAULT_RELEASE_STREAM_MAX_ATTEMPTS = 100;
|
|
8
9
|
export async function promotePipeline(ctx, body, options = {}) {
|
|
9
10
|
const { intervalMs = DEFAULT_INTERVAL_MS, onReleaseStream, releaseStreamMaxAttempts = DEFAULT_RELEASE_STREAM_MAX_ATTEMPTS, signal, timeoutMs, } = options;
|
|
11
|
+
// Bind the signal to the client so the create/poll/release-info requests
|
|
12
|
+
// abort in flight, not just the delay between polls.
|
|
13
|
+
const platform = withSignal(ctx.platform, signal);
|
|
10
14
|
debug('promote source=%s targets=%d', body.source?.app?.id ?? '<unknown>', body.targets?.length ?? 0);
|
|
11
|
-
const promotion = await
|
|
15
|
+
const promotion = await platform.pipelinePromotion.create(body);
|
|
12
16
|
if (!promotion.id) {
|
|
13
17
|
throw new Error('Pipeline promotion response did not include an id');
|
|
14
18
|
}
|
|
15
19
|
debug('promote created promotion=%s intervalMs=%d timeoutMs=%s', promotion.id, intervalMs, timeoutMs ?? '<none>');
|
|
16
20
|
const deadline = timeoutMs === undefined ? undefined : Date.now() + timeoutMs;
|
|
17
21
|
const streamRunner = onReleaseStream
|
|
18
|
-
? createReleaseStreamRunner(ctx, onReleaseStream, releaseStreamMaxAttempts, intervalMs, signal)
|
|
22
|
+
? createReleaseStreamRunner(ctx, platform, onReleaseStream, releaseStreamMaxAttempts, intervalMs, signal)
|
|
19
23
|
: undefined;
|
|
20
24
|
let poll = 0;
|
|
21
25
|
while (true) {
|
|
22
26
|
signal?.throwIfAborted();
|
|
23
27
|
poll++;
|
|
24
28
|
// eslint-disable-next-line no-await-in-loop
|
|
25
|
-
const targets = await
|
|
29
|
+
const targets = await platform.pipelinePromotionTarget.list(promotion.id);
|
|
26
30
|
debug('promote poll=%d promotion=%s statuses=%o', poll, promotion.id, targets.map(target => target.status));
|
|
27
31
|
if (targets.every(target => target.status !== 'pending')) {
|
|
28
32
|
debug('promote terminal promotion=%s polls=%d', promotion.id, poll);
|
|
@@ -43,7 +47,7 @@ export async function promotePipeline(ctx, body, options = {}) {
|
|
|
43
47
|
* output stream and hands it to `onReleaseStream`. Subsequent calls are
|
|
44
48
|
* no-ops; the stream is one-shot per promotion.
|
|
45
49
|
*/
|
|
46
|
-
function createReleaseStreamRunner(ctx, onReleaseStream, releaseStreamMaxAttempts, intervalMs, signal) {
|
|
50
|
+
function createReleaseStreamRunner(ctx, platform, onReleaseStream, releaseStreamMaxAttempts, intervalMs, signal) {
|
|
47
51
|
let handled = false;
|
|
48
52
|
return async (targets) => {
|
|
49
53
|
if (handled || targets.length !== 1)
|
|
@@ -53,30 +57,27 @@ function createReleaseStreamRunner(ctx, onReleaseStream, releaseStreamMaxAttempt
|
|
|
53
57
|
return;
|
|
54
58
|
handled = true;
|
|
55
59
|
debug('promote release-stream lookup app=%s release=%s', target.app.id, target.release.id);
|
|
56
|
-
const release = await
|
|
60
|
+
const release = await platform.release.info(target.app.id, target.release.id);
|
|
57
61
|
if (!release.output_stream_url) {
|
|
58
62
|
debug('promote release-stream skipped reason=no-output-stream-url');
|
|
59
63
|
return;
|
|
60
64
|
}
|
|
61
65
|
debug('promote release-stream fetching url=%s maxAttempts=%d', release.output_stream_url, releaseStreamMaxAttempts);
|
|
62
|
-
const stream = await fetchReleaseOutput(release.output_stream_url, releaseStreamMaxAttempts, intervalMs, signal);
|
|
66
|
+
const stream = await fetchReleaseOutput(ctx, release.output_stream_url, releaseStreamMaxAttempts, intervalMs, signal);
|
|
63
67
|
await onReleaseStream({ stream, target });
|
|
64
68
|
};
|
|
65
69
|
}
|
|
66
70
|
export const pipelinePromotionExtensions = extendResource('platform', 'pipelinePromotion', ctx => ({
|
|
67
71
|
promote: (body, options) => promotePipeline(ctx, body, options),
|
|
68
72
|
}));
|
|
69
|
-
async function fetchReleaseOutput(url, maxAttempts, intervalMs, signal) {
|
|
73
|
+
async function fetchReleaseOutput(ctx, url, maxAttempts, intervalMs, signal) {
|
|
70
74
|
const parsed = new URL(url);
|
|
71
75
|
const path = `${parsed.pathname}${parsed.search}`;
|
|
72
76
|
// The release output URL points at a third-party stream host (e.g. busl).
|
|
73
|
-
// Use a custom-
|
|
74
|
-
// don't leak across origins
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
service: 'custom',
|
|
78
|
-
token: '',
|
|
79
|
-
});
|
|
77
|
+
// Use a custom-host client so the platform prefixUrl, bearer token, and
|
|
78
|
+
// other auth/routing options don't leak across origins, while still
|
|
79
|
+
// inheriting the SDK's identity (User-Agent) and cancellation signal.
|
|
80
|
+
const buslClient = makeCustomHostClient(ctx, parsed.origin);
|
|
80
81
|
let attempt = 0;
|
|
81
82
|
while (true) {
|
|
82
83
|
signal?.throwIfAborted();
|
|
@@ -84,9 +85,12 @@ async function fetchReleaseOutput(url, maxAttempts, intervalMs, signal) {
|
|
|
84
85
|
let response;
|
|
85
86
|
try {
|
|
86
87
|
// eslint-disable-next-line no-await-in-loop
|
|
87
|
-
response = await buslClient.stream(path);
|
|
88
|
+
response = await buslClient.stream(path, { signal });
|
|
88
89
|
}
|
|
89
|
-
catch {
|
|
90
|
+
catch (error) {
|
|
91
|
+
// An abort is terminal, not a retryable miss — let it propagate.
|
|
92
|
+
if (signal?.aborted || isAbortError(error))
|
|
93
|
+
throw error;
|
|
90
94
|
// Treat HTTP errors thrown by the api-client as a retryable miss.
|
|
91
95
|
response = undefined;
|
|
92
96
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withSignal } from '../../../core/with-signal.js';
|
|
1
2
|
import { debug } from './debug.js';
|
|
2
3
|
/**
|
|
3
4
|
* Resolves the GitHub repository name for a canonical pipeline ID.
|
|
@@ -9,7 +10,7 @@ import { debug } from './debug.js';
|
|
|
9
10
|
export async function resolveRepoName(ctx, pipelineId, options = {}) {
|
|
10
11
|
const { signal } = options;
|
|
11
12
|
signal?.throwIfAborted();
|
|
12
|
-
const platform =
|
|
13
|
+
const platform = withSignal(ctx.platform, signal);
|
|
13
14
|
let useRepositoriesApi;
|
|
14
15
|
try {
|
|
15
16
|
const feature = await platform.accountFeature.info('dashboard-repositories-api');
|
|
@@ -22,7 +23,7 @@ export async function resolveRepoName(ctx, pipelineId, options = {}) {
|
|
|
22
23
|
}
|
|
23
24
|
if (useRepositoriesApi) {
|
|
24
25
|
try {
|
|
25
|
-
const repositoriesApi =
|
|
26
|
+
const repositoriesApi = withSignal(ctx.repositoriesApi, signal);
|
|
26
27
|
const repository = await repositoriesApi.githubRepository.info(pipelineId);
|
|
27
28
|
signal?.throwIfAborted();
|
|
28
29
|
const repositoryName = repository.full_name?.trim();
|
|
@@ -40,7 +41,7 @@ export async function resolveRepoName(ctx, pipelineId, options = {}) {
|
|
|
40
41
|
signal?.throwIfAborted();
|
|
41
42
|
debug('repositories API disabled; falling back');
|
|
42
43
|
}
|
|
43
|
-
const repositories =
|
|
44
|
+
const repositories = withSignal(ctx.repositories, signal);
|
|
44
45
|
const pipelineRepository = await repositories.pipelineRepository.info(pipelineId);
|
|
45
46
|
signal?.throwIfAborted();
|
|
46
47
|
const repositoryName = pipelineRepository.repository.name.trim();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withSignal } from '../../../core/with-signal.js';
|
|
1
2
|
import { waitForReady } from '../domain/wait-for-ready.js';
|
|
2
3
|
const DEFAULT_WAIT_INTERVAL_MS = 1000;
|
|
3
4
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
@@ -35,7 +36,7 @@ const DEFAULT_TIMEOUT_MS = 30_000;
|
|
|
35
36
|
export async function createAndAssociate(ctx, appIdentity, certificateChain, privateKey, options) {
|
|
36
37
|
const { domainPoller, intervalMs = DEFAULT_WAIT_INTERVAL_MS, resolveDomains, signal, timeoutMs = DEFAULT_TIMEOUT_MS, } = options;
|
|
37
38
|
signal?.throwIfAborted();
|
|
38
|
-
const platform =
|
|
39
|
+
const platform = withSignal(ctx.platform, signal);
|
|
39
40
|
const sniEndpoint = await platform.sniEndpoint.create(appIdentity, {
|
|
40
41
|
certificate_chain: certificateChain,
|
|
41
42
|
private_key: privateKey,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withSignal } from '../../../core/with-signal.js';
|
|
1
2
|
import { wait } from '../../../utils/wait.js';
|
|
2
3
|
import { SpaceNotAllocatedError, SpaceNotReadyError } from './errors.js';
|
|
3
4
|
const DEFAULT_INTERVAL_MS = 5000;
|
|
@@ -22,10 +23,7 @@ const DEFAULT_TIMEOUT_MS = 20 * 60 * 1000;
|
|
|
22
23
|
export async function waitForAllocated(ctx, name, options = {}) {
|
|
23
24
|
const { includeNat, intervalMs = DEFAULT_INTERVAL_MS, signal, timeoutMs = DEFAULT_TIMEOUT_MS, } = options;
|
|
24
25
|
signal?.throwIfAborted();
|
|
25
|
-
|
|
26
|
-
if (signal) {
|
|
27
|
-
platform = platform.withOptions({ signal });
|
|
28
|
-
}
|
|
26
|
+
const platform = withSignal(ctx.platform.withHeaders({ 'Accept-Expansion': 'region' }), signal);
|
|
29
27
|
const deadline = Date.now() + timeoutMs;
|
|
30
28
|
let space = await platform.space.info(name);
|
|
31
29
|
while (space.state === 'allocating') {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Team, TeamDelinquency } from '@heroku/types/3.sdk';
|
|
2
|
+
import type { ResourceCtx } from '../../core/extend-resource.js';
|
|
3
|
+
export type GetTeamWithDelinquencyOptions = {
|
|
4
|
+
signal?: AbortSignal;
|
|
5
|
+
};
|
|
6
|
+
export type TeamWithDelinquency = {
|
|
7
|
+
delinquency: null | TeamDelinquency;
|
|
8
|
+
team: Team;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Fetch a team together with its delinquency status.
|
|
12
|
+
*
|
|
13
|
+
* Runs the two reads concurrently. The team lookup hard-fails — there is no
|
|
14
|
+
* result without it. The delinquency lookup soft-fails to `null`: a hiccup on
|
|
15
|
+
* that secondary call must not sink an otherwise-successful team fetch. Callers
|
|
16
|
+
* decide whether and how to surface a warning from `delinquency` —
|
|
17
|
+
* `formatDelinquencyWarning` renders the sentence.
|
|
18
|
+
*/
|
|
19
|
+
export declare function getTeamWithDelinquency(ctx: Pick<ResourceCtx, 'platform'>, teamIdentity: string, options?: GetTeamWithDelinquencyOptions): Promise<TeamWithDelinquency>;
|
|
20
|
+
export declare const teamExtensions: import("../../core/extend-resource.js").ResourceExtension<"platform", "team", {
|
|
21
|
+
getWithDelinquency: (teamIdentity: string, options?: GetTeamWithDelinquencyOptions) => Promise<TeamWithDelinquency>;
|
|
22
|
+
}>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import createDebug from 'debug';
|
|
2
|
+
import { extendResource } from '../../core/extend-resource.js';
|
|
3
|
+
const debug = createDebug('heroku:sdk:resources:team');
|
|
4
|
+
/**
|
|
5
|
+
* Fetch a team together with its delinquency status.
|
|
6
|
+
*
|
|
7
|
+
* Runs the two reads concurrently. The team lookup hard-fails — there is no
|
|
8
|
+
* result without it. The delinquency lookup soft-fails to `null`: a hiccup on
|
|
9
|
+
* that secondary call must not sink an otherwise-successful team fetch. Callers
|
|
10
|
+
* decide whether and how to surface a warning from `delinquency` —
|
|
11
|
+
* `formatDelinquencyWarning` renders the sentence.
|
|
12
|
+
*/
|
|
13
|
+
export async function getTeamWithDelinquency(ctx, teamIdentity, options = {}) {
|
|
14
|
+
options.signal?.throwIfAborted();
|
|
15
|
+
const platform = options.signal
|
|
16
|
+
? ctx.platform.withOptions({ signal: options.signal })
|
|
17
|
+
: ctx.platform;
|
|
18
|
+
const [team, delinquency] = await Promise.all([
|
|
19
|
+
platform.team.info(teamIdentity),
|
|
20
|
+
platform.teamDelinquency.info(teamIdentity).catch((error) => {
|
|
21
|
+
// Soft-fail a secondary-call hiccup to null, but rethrow a cancellation:
|
|
22
|
+
// if the caller aborted, returning null would misreport a delinquent team
|
|
23
|
+
// as current. Anything else is logged and swallowed.
|
|
24
|
+
if (options.signal?.aborted)
|
|
25
|
+
throw error;
|
|
26
|
+
debug('team delinquency lookup failed; treating as not delinquent: %O', error);
|
|
27
|
+
return null;
|
|
28
|
+
}),
|
|
29
|
+
]);
|
|
30
|
+
return { delinquency, team };
|
|
31
|
+
}
|
|
32
|
+
export const teamExtensions = extendResource('platform', 'team', ctx => ({
|
|
33
|
+
getWithDelinquency: (teamIdentity, options) => getTeamWithDelinquency(ctx, teamIdentity, options),
|
|
34
|
+
}));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withSignal } from '../../../core/with-signal.js';
|
|
1
2
|
/**
|
|
2
3
|
* Remove all telemetry drains for a given app or space.
|
|
3
4
|
*
|
|
@@ -11,7 +12,7 @@ export async function removeDrainsForTarget(ctx, target, options = {}) {
|
|
|
11
12
|
const { signal } = options;
|
|
12
13
|
const { app, space } = target;
|
|
13
14
|
signal?.throwIfAborted();
|
|
14
|
-
const platform =
|
|
15
|
+
const platform = withSignal(ctx.platform, signal);
|
|
15
16
|
let drains = [];
|
|
16
17
|
if (app) {
|
|
17
18
|
drains = await platform.telemetryDrain.listByApp(app);
|