Skip to content

Commit 444fefb

Browse files
committed
feat(toolkit-lib): emit marker messages during actions
1 parent db60595 commit 444fefb

File tree

16 files changed

+406
-125
lines changed

16 files changed

+406
-125
lines changed

.projenrc.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,7 @@ const tmpToolkitHelpers = configureProject(
705705
'archiver',
706706
'glob',
707707
'semver',
708+
'uuid',
708709
'yaml@^1',
709710
],
710711
tsconfig: {

packages/@aws-cdk/tmp-toolkit-helpers/.projen/deps.json

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk/tmp-toolkit-helpers/.projen/tasks.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk/tmp-toolkit-helpers/package.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './io-helper';
22
export * from './level-priority';
3+
export * from './marker';
34
export * from './message-maker';
45
export * from './types';
Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,59 @@
11
import type { IIoHost } from '../io-host';
22
import type { IoMessage, IoRequest } from '../io-message';
33
import type { ToolkitAction } from '../toolkit-action';
4+
import type { MarkerEnd, MarkerStart, MarkerDefinition } from './marker';
5+
import { Marker } from './marker';
46

5-
export type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
6-
export type SimplifiedMessage<T> = Pick<IoMessage<T>, 'level' | 'code' | 'message' | 'data'>;
77
export type ActionLessMessage<T> = Omit<IoMessage<T>, 'action'>;
88
export type ActionLessRequest<T, U> = Omit<IoRequest<T, U>, 'action'>;
99

1010
/**
11-
* Helper for IO messaging.
12-
*
13-
* Wraps a client provided IoHost and provides additional features & services to toolkit internal classes.
11+
* A class containing helper tools to interact with IoHost
1412
*/
15-
export interface IoHelper extends IIoHost {
16-
notify<T>(msg: ActionLessMessage<T>): Promise<void>;
17-
requestResponse<T, U>(msg: ActionLessRequest<T, U>): Promise<U>;
13+
export class IoHelper implements IIoHost {
14+
public static fromIoHost(ioHost: IIoHost, action: ToolkitAction) {
15+
return new IoHelper(ioHost, action);
16+
}
17+
18+
private readonly ioHost: IIoHost;
19+
private readonly action: ToolkitAction;
20+
21+
private constructor(ioHost: IIoHost, action: ToolkitAction) {
22+
this.ioHost = ioHost;
23+
this.action = action;
24+
}
25+
26+
/**
27+
* Forward a message to the IoHost, while injection the current action
28+
*/
29+
public notify<T>(msg: ActionLessMessage<T>): Promise<void> {
30+
return this.ioHost.notify({
31+
...msg,
32+
action: this.action,
33+
});
34+
}
35+
36+
/**
37+
* Forward a request to the IoHost, while injection the current action
38+
*/
39+
public requestResponse<T, U>(msg: ActionLessRequest<T, U>): Promise<U> {
40+
return this.ioHost.requestResponse({
41+
...msg,
42+
action: this.action,
43+
});
44+
}
45+
46+
/**
47+
* Create a new marker from a given registry entry
48+
*/
49+
public marker<S extends MarkerStart, E extends MarkerEnd>(type: MarkerDefinition<S, E>): Marker<S, E> {
50+
return new Marker(this, type);
51+
}
1852
}
1953

2054
/**
2155
* Wraps an IoHost and creates an IoHelper from it
2256
*/
2357
export function asIoHelper(ioHost: IIoHost, action: ToolkitAction): IoHelper {
24-
return {
25-
notify: async <T>(msg: Omit<IoMessage<T>, 'action'>) => {
26-
await ioHost.notify({
27-
...msg,
28-
action,
29-
});
30-
},
31-
requestResponse: async <T, U>(msg: Omit<IoRequest<T, U>, 'action'>) => {
32-
return ioHost.requestResponse({
33-
...msg,
34-
action,
35-
});
36-
},
37-
};
58+
return IoHelper.fromIoHost(ioHost, action);
3859
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import * as util from 'node:util';
2+
import * as uuid from 'uuid';
3+
import type { IoHelper } from './io-helper';
4+
import type { IoMessageMaker } from './message-maker';
5+
import { formatTime } from '../../../util';
6+
7+
export interface MarkerStart {
8+
readonly marker: string;
9+
}
10+
11+
export interface MarkerEnd {
12+
readonly marker: string;
13+
readonly duration: number;
14+
}
15+
16+
/**
17+
* Describes a specific marker
18+
*
19+
* A marker definition is a pair of `IoMessageMaker`s to create a start and end message respectively
20+
* and a display name that is used to auto-generate messages.
21+
*/
22+
export interface MarkerDefinition<S extends MarkerStart, E extends MarkerEnd> {
23+
readonly name: string;
24+
readonly start: IoMessageMaker<S>;
25+
readonly end: IoMessageMaker<E>;
26+
}
27+
28+
/**
29+
* Used in conditional types to check if a type (e.g. after omitting fields) is an empty object
30+
* This is needed because counter-intuitive neither `object` nor `{}` represent that.
31+
*/
32+
type EmptyObject = {
33+
[index: string | number | symbol]: never;
34+
}
35+
36+
/**
37+
* Helper type to force a parameter to be not present of the computed type is an empty object
38+
*/
39+
type VoidWhenEmpty<T> = T extends EmptyObject ? void : T
40+
41+
/**
42+
* Helper type to force a parameter to be an empty object if the computed type is an empty object
43+
* This is weird, but some computed types (e.g. using `Omit`) don't end up enforcing this.
44+
*/
45+
type ForceEmpty<T> = T extends EmptyObject ? EmptyObject : T
46+
47+
/**
48+
* A minimal interface that provides a single function to end the marker
49+
*/
50+
interface CanEndMarker<E extends MarkerEnd> {
51+
end(payload: VoidWhenEmpty<Omit<E, keyof MarkerEnd>>): Promise<ObservedDuration>;
52+
end(message: string, payload: ForceEmpty<Omit<E, keyof MarkerEnd>>): Promise<ObservedDuration>;
53+
}
54+
55+
/**
56+
* Ending the marker returns the observed duration
57+
*/
58+
export interface ObservedDuration {
59+
readonly marker: string;
60+
readonly asMs: number;
61+
readonly asSec: number;
62+
}
63+
64+
/**
65+
* Helper class to mark task blocks
66+
*
67+
* Blocks will be enclosed by a start and end message.
68+
* Both messages share a unique id.
69+
* The end message contains the time passed between start and end.
70+
*/
71+
export class Marker<S extends MarkerStart, E extends MarkerEnd> {
72+
public readonly type: MarkerDefinition<S, E>;
73+
private readonly ioHelper: IoHelper;
74+
private startTime: number;
75+
private id: string;
76+
77+
public constructor(ioHelper: IoHelper, type: MarkerDefinition<S, E>) {
78+
this.type = type;
79+
this.ioHelper = ioHelper;
80+
this.startTime = new Date().getTime();
81+
this.id = uuid.v4();
82+
}
83+
84+
/**
85+
* Starts the marker and notifies the IoHost.
86+
* @returns an object that can end the marker
87+
*/
88+
public async start(payload: VoidWhenEmpty<Omit<S, keyof MarkerStart>>): Promise<CanEndMarker<E>>;
89+
public async start(message: string, payload: Omit<S, keyof MarkerStart>): Promise<CanEndMarker<E>>;
90+
public async start(first: any, second?: Omit<S, keyof MarkerStart>): Promise<CanEndMarker<E>> {
91+
this.startTime = new Date().getTime();
92+
93+
const msg = second ? first : 'Starting %s ...';
94+
const payload = second ?? first;
95+
96+
await this.ioHelper.notify(this.type.start.msg(
97+
util.format(msg, this.type.name), {
98+
marker: this.id,
99+
...payload,
100+
} as S));
101+
102+
return {
103+
end: (a: any, b?: Omit<E, keyof MarkerEnd>): Promise<ObservedDuration> => {
104+
return this.end(a, b);
105+
},
106+
} as CanEndMarker<E>;
107+
}
108+
109+
/**
110+
* Ends the current timer as a specified timing and notifies the IoHost.
111+
* @returns the elapsed time
112+
*/
113+
private async end(first: any | undefined, second?: Omit<E, keyof MarkerEnd>): Promise<ObservedDuration> {
114+
const duration = this.time();
115+
116+
const msg = second ? first : `\n✨ %s time: ${duration.asSec}s\n`;
117+
const payload = second ?? first;
118+
119+
await this.ioHelper.notify(this.type.end.msg(
120+
util.format(msg, this.type.name), {
121+
marker: this.id,
122+
duration: duration.asMs,
123+
...payload,
124+
} as E));
125+
126+
return duration;
127+
}
128+
129+
/**
130+
* Get the current timer for the marker
131+
* @returns the elapsed time
132+
*/
133+
private time(): ObservedDuration {
134+
const elapsedTime = new Date().getTime() - this.startTime;
135+
return {
136+
marker: this.id,
137+
asMs: elapsedTime,
138+
asSec: formatTime(elapsedTime),
139+
};
140+
}
141+
}

packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,12 @@ export class BootstrapSource {
229229
}
230230

231231
export interface BootstrapEnvironmentProgress {
232+
/**
233+
* Uniquely identifies this marker amongst concurrent messages
234+
*
235+
* This is an otherwise meaningless identifier.
236+
*/
237+
readonly marker: string;
232238
/**
233239
* The total number of environments being deployed
234240
*/

packages/@aws-cdk/toolkit-lib/lib/actions/deploy/index.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { CloudFormationStackArtifact } from '@aws-cdk/cx-api';
2+
import type { IManifestEntry } from 'cdk-assets';
23
import type { BaseDeployOptions } from './private/deploy-options';
34
import type { Tag } from '../../api/aws-cdk';
45
import type { ConfirmationRequest } from '../../toolkit/types';
@@ -219,6 +220,12 @@ export interface HotswapProperties {
219220
}
220221

221222
export interface StackDeployProgress {
223+
/**
224+
* Uniquely identifies this marker amongst concurrent messages
225+
*
226+
* This is an otherwise meaningless identifier.
227+
*/
228+
readonly marker: string;
222229
/**
223230
* The total number of stacks being deployed
224231
*/
@@ -245,3 +252,30 @@ export interface DeployConfirmationRequest extends ConfirmationRequest {
245252
*/
246253
readonly permissionChangeType: PermissionChangeType;
247254
}
255+
256+
export interface BuildAsset {
257+
/**
258+
* Uniquely identifies this marker amongst concurrent messages
259+
*
260+
* This is an otherwise meaningless identifier.
261+
*/
262+
readonly marker: string;
263+
/**
264+
* The asset that is build
265+
*/
266+
readonly asset: IManifestEntry;
267+
}
268+
269+
export interface PublishAsset {
270+
/**
271+
* Uniquely identifies this marker amongst concurrent messages
272+
*
273+
* This is an otherwise meaningless identifier.
274+
*/
275+
readonly marker: string;
276+
277+
/**
278+
* The asset that is published
279+
*/
280+
readonly asset: IManifestEntry;
281+
}

packages/@aws-cdk/toolkit-lib/lib/actions/destroy/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ export interface DestroyOptions {
2121
}
2222

2323
export interface StackDestroyProgress {
24+
/**
25+
* Uniquely identifies this marker amongst concurrent messages
26+
*
27+
* This is an otherwise meaningless identifier.
28+
*/
29+
readonly marker: string;
2430
/**
2531
* The total number of stacks being destroyed
2632
*/

0 commit comments

Comments
 (0)