@oclif/multi-stage-output 0.7.10 → 0.7.12

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.
@@ -28,6 +28,10 @@ type Info<T extends Record<string, unknown>> = {
28
28
  * Set to `true` to prevent this key-value pair or message from being collapsed when the window is too short. Defaults to false.
29
29
  */
30
30
  neverCollapse?: boolean;
31
+ /**
32
+ * Set to `true` to only show this key-value pair or message at the very end of the CI output. Defaults to false.
33
+ */
34
+ onlyShowAtEndInCI?: boolean;
31
35
  };
32
36
  export type KeyValuePair<T extends Record<string, unknown>> = Info<T> & {
33
37
  /**
@@ -53,25 +53,44 @@ export type MultiStageOutputOptions<T extends Record<string, unknown>> = {
53
53
  readonly jsonEnabled: boolean;
54
54
  };
55
55
  declare class CIMultiStageOutput<T extends Record<string, unknown>> {
56
+ private readonly completedStages;
56
57
  private data?;
57
58
  private readonly design;
58
59
  private readonly hasElapsedTime?;
59
60
  private readonly hasStageTime?;
60
- private lastUpdateTime;
61
- private readonly messageTimeout;
61
+ /**
62
+ * Amount of time (in milliseconds) between heartbeat updates
63
+ */
64
+ private readonly heartbeat;
65
+ /**
66
+ * Time of the last heartbeat
67
+ */
68
+ private lastHeartbeatTime;
69
+ /**
70
+ * Map of the last time a specific piece of info was updated. This is used for throttling messages
71
+ */
72
+ private readonly lastUpdateByInfo;
62
73
  private readonly postStagesBlock?;
63
74
  private readonly preStagesBlock?;
64
- private readonly seenInfo;
65
- private readonly seenStages;
75
+ private readonly seenStrings;
66
76
  private readonly stages;
67
77
  private readonly stageSpecificBlock?;
68
78
  private readonly startTime;
69
79
  private readonly startTimes;
80
+ /**
81
+ * Amount of time (in milliseconds) between throttled updates
82
+ */
83
+ private readonly throttle;
70
84
  private readonly timerUnit;
85
+ /**
86
+ * Map of intervals used to trigger heartbeat updates
87
+ */
88
+ private readonly updateIntervals;
71
89
  constructor({ data, design, postStagesBlock, preStagesBlock, showElapsedTime, showStageTime, stageSpecificBlock, stages, timerUnit, title, }: MultiStageOutputOptions<T>);
72
90
  stop(stageTracker: StageTracker): void;
73
91
  update(stageTracker: StageTracker, data?: Partial<T>): void;
74
- private printInfo;
92
+ private maybePrintInfo;
93
+ private maybeStdout;
75
94
  }
76
95
  declare class MultiStageOutputBase<T extends Record<string, unknown>> implements Disposable {
77
96
  protected readonly ciInstance: CIMultiStageOutput<T> | undefined;
@@ -33,21 +33,39 @@ function shouldUseCIMode() {
33
33
  }
34
34
  const isInCi = shouldUseCIMode();
35
35
  class CIMultiStageOutput {
36
+ completedStages = new Set();
36
37
  data;
37
38
  design;
38
39
  hasElapsedTime;
39
40
  hasStageTime;
40
- lastUpdateTime;
41
- messageTimeout = Number.parseInt(env.SF_CI_MESSAGE_TIMEOUT ?? '5000', 10) ?? 5000;
41
+ /**
42
+ * Amount of time (in milliseconds) between heartbeat updates
43
+ */
44
+ heartbeat = Number.parseInt(env.OCLIF_CI_HEARTBEAT_FREQUENCY_MS ?? env.SF_CI_HEARTBEAT_FREQUENCY_MS ?? '300000', 10) ?? 300_000;
45
+ /**
46
+ * Time of the last heartbeat
47
+ */
48
+ lastHeartbeatTime;
49
+ /**
50
+ * Map of the last time a specific piece of info was updated. This is used for throttling messages
51
+ */
52
+ lastUpdateByInfo = new Map();
42
53
  postStagesBlock;
43
54
  preStagesBlock;
44
- seenInfo = new Set();
45
- seenStages = new Set();
55
+ seenStrings = new Set();
46
56
  stages;
47
57
  stageSpecificBlock;
48
58
  startTime;
49
59
  startTimes = new Map();
60
+ /**
61
+ * Amount of time (in milliseconds) between throttled updates
62
+ */
63
+ throttle = Number.parseInt(env.OCLIF_CI_UPDATE_FREQUENCY_MS ?? env.SF_CI_UPDATE_FREQUENCY_MS ?? '5000', 10) ?? 5000;
50
64
  timerUnit;
65
+ /**
66
+ * Map of intervals used to trigger heartbeat updates
67
+ */
68
+ updateIntervals = new Map();
51
69
  constructor({ data, design, postStagesBlock, preStagesBlock, showElapsedTime, showStageTime, stageSpecificBlock, stages, timerUnit, title, }) {
52
70
  this.design = constructDesignParams(design);
53
71
  this.stages = stages;
@@ -58,7 +76,7 @@ class CIMultiStageOutput {
58
76
  this.stageSpecificBlock = stageSpecificBlock;
59
77
  this.timerUnit = timerUnit ?? 'ms';
60
78
  this.data = data;
61
- this.lastUpdateTime = Date.now();
79
+ this.lastHeartbeatTime = Date.now();
62
80
  if (title)
63
81
  ux.stdout(`───── ${title} ─────`);
64
82
  ux.stdout('Stages:');
@@ -73,20 +91,24 @@ class CIMultiStageOutput {
73
91
  stop(stageTracker) {
74
92
  this.update(stageTracker);
75
93
  ux.stdout();
76
- this.printInfo(this.preStagesBlock, 0, true);
77
- this.printInfo(this.postStagesBlock, 0, true);
94
+ this.maybePrintInfo(this.preStagesBlock, 0, true);
95
+ this.maybePrintInfo(this.postStagesBlock, 0, true);
78
96
  if (this.startTime) {
79
97
  const elapsedTime = Date.now() - this.startTime;
80
98
  ux.stdout();
81
99
  const displayTime = readableTime(elapsedTime, this.timerUnit);
82
100
  ux.stdout(`Elapsed time: ${displayTime}`);
83
101
  }
102
+ for (const interval of this.updateIntervals.values()) {
103
+ clearInterval(interval);
104
+ }
84
105
  }
106
+ // eslint-disable-next-line complexity
85
107
  update(stageTracker, data) {
86
108
  this.data = { ...this.data, ...data };
87
109
  for (const [stage, status] of stageTracker.entries()) {
88
110
  // no need to re-render completed, failed, or skipped stages
89
- if (this.seenStages.has(stage))
111
+ if (this.completedStages.has(stage))
90
112
  continue;
91
113
  switch (status) {
92
114
  case 'pending': {
@@ -96,13 +118,33 @@ class CIMultiStageOutput {
96
118
  case 'current': {
97
119
  if (!this.startTimes.has(stage))
98
120
  this.startTimes.set(stage, Date.now());
99
- if (Date.now() - this.lastUpdateTime < this.messageTimeout)
100
- break;
101
- this.lastUpdateTime = Date.now();
102
- ux.stdout(`${this.design.icons.current.figure} ${stage}…`);
103
- this.printInfo(this.preStagesBlock, 3);
104
- this.printInfo(this.stageSpecificBlock?.filter((info) => info.stage === stage), 3);
105
- this.printInfo(this.postStagesBlock, 3);
121
+ const stageInfos = this.stageSpecificBlock?.filter((info) => info.stage === stage);
122
+ const iconAndStage = `${this.design.icons.current.figure} ${stage}…`;
123
+ if (Date.now() - this.lastHeartbeatTime < this.heartbeat) {
124
+ // only print if it hasn't been seen before
125
+ this.maybeStdout(iconAndStage);
126
+ this.maybePrintInfo(this.preStagesBlock, 3);
127
+ this.maybePrintInfo(stageInfos, 3);
128
+ this.maybePrintInfo(this.postStagesBlock, 3);
129
+ }
130
+ else {
131
+ // force a reprint if it's been too long
132
+ this.lastHeartbeatTime = Date.now();
133
+ if (stageInfos?.length) {
134
+ // only reprint the stage infos if it has them
135
+ this.maybePrintInfo(stageInfos, 3, true);
136
+ }
137
+ else {
138
+ // only reprint the stage
139
+ this.maybeStdout(iconAndStage, 0, true);
140
+ }
141
+ }
142
+ if (!this.updateIntervals.has(stage)) {
143
+ // set interval to update the stage message - this is used for long running stages in CI environments that timeout after a certain period without output
144
+ this.updateIntervals.set(stage, setInterval(() => {
145
+ this.update(stageTracker);
146
+ }, this.heartbeat));
147
+ }
106
148
  break;
107
149
  }
108
150
  case 'failed':
@@ -112,24 +154,35 @@ class CIMultiStageOutput {
112
154
  case 'async':
113
155
  case 'warning':
114
156
  case 'completed': {
115
- this.seenStages.add(stage);
157
+ // clear the heartbeat interval since it's no longer needed
158
+ const interval = this.updateIntervals.get(stage);
159
+ if (interval) {
160
+ clearInterval(interval);
161
+ this.updateIntervals.delete(stage);
162
+ }
163
+ // clear all throttled messages since the stage is done
164
+ for (const key of this.lastUpdateByInfo.keys()) {
165
+ this.lastUpdateByInfo.delete(key);
166
+ }
167
+ const stageInfos = this.stageSpecificBlock?.filter((info) => info.stage === stage);
168
+ this.completedStages.add(stage);
116
169
  if (this.hasStageTime && status !== 'skipped') {
117
170
  const startTime = this.startTimes.get(stage);
118
171
  const elapsedTime = startTime ? Date.now() - startTime : 0;
119
172
  const displayTime = readableTime(elapsedTime, this.timerUnit);
120
- ux.stdout(`${this.design.icons[status].figure} ${stage} (${displayTime})`);
121
- this.printInfo(this.preStagesBlock, 3);
122
- this.printInfo(this.stageSpecificBlock?.filter((info) => info.stage === stage), 3);
123
- this.printInfo(this.postStagesBlock, 3);
173
+ this.maybeStdout(`${this.design.icons[status].figure} ${stage} (${displayTime})`);
174
+ this.maybePrintInfo(this.preStagesBlock, 3);
175
+ this.maybePrintInfo(stageInfos, 3);
176
+ this.maybePrintInfo(this.postStagesBlock, 3);
124
177
  }
125
178
  else if (status === 'skipped') {
126
- ux.stdout(`${this.design.icons[status].figure} ${stage} - Skipped`);
179
+ this.maybeStdout(`${this.design.icons[status].figure} ${stage} - Skipped`);
127
180
  }
128
181
  else {
129
- ux.stdout(`${this.design.icons[status].figure} ${stage}`);
130
- this.printInfo(this.preStagesBlock, 3);
131
- this.printInfo(this.stageSpecificBlock?.filter((info) => info.stage === stage), 3);
132
- this.printInfo(this.postStagesBlock, 3);
182
+ this.maybeStdout(`${this.design.icons[status].figure} ${stage}`);
183
+ this.maybePrintInfo(this.preStagesBlock, 3);
184
+ this.maybePrintInfo(stageInfos, 3);
185
+ this.maybePrintInfo(this.postStagesBlock, 3);
133
186
  }
134
187
  break;
135
188
  }
@@ -138,21 +191,34 @@ class CIMultiStageOutput {
138
191
  }
139
192
  }
140
193
  }
141
- printInfo(infoBlock, indent = 0, force = false) {
142
- const spaces = ' '.repeat(indent);
194
+ maybePrintInfo(infoBlock, indent = 0, force = false) {
143
195
  if (infoBlock?.length) {
144
196
  for (const info of infoBlock) {
197
+ if (info.onlyShowAtEndInCI && !force)
198
+ continue;
145
199
  const formattedData = info.get ? info.get(this.data) : undefined;
146
200
  if (!formattedData)
147
201
  continue;
202
+ const key = info.type === 'message' ? formattedData : info.label;
148
203
  const str = info.type === 'message' ? formattedData : `${info.label}: ${formattedData}`;
149
- if (!force && this.seenInfo.has(str))
204
+ const lastUpdateTime = this.lastUpdateByInfo.get(key);
205
+ // Skip if the info has been printed before the throttle time
206
+ if (lastUpdateTime && Date.now() - lastUpdateTime < this.throttle && !force)
150
207
  continue;
151
- ux.stdout(`${spaces}${str}`);
152
- this.seenInfo.add(str);
208
+ const didPrint = this.maybeStdout(str, indent, force);
209
+ if (didPrint)
210
+ this.lastUpdateByInfo.set(key, Date.now());
153
211
  }
154
212
  }
155
213
  }
214
+ maybeStdout(str, indent = 0, force = false) {
215
+ const spaces = ' '.repeat(indent);
216
+ if (!force && this.seenStrings.has(str))
217
+ return false;
218
+ ux.stdout(`${spaces}${str}`);
219
+ this.seenStrings.add(str);
220
+ return true;
221
+ }
156
222
  }
157
223
  class MultiStageOutputBase {
158
224
  ciInstance;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oclif/multi-stage-output",
3
3
  "description": "Terminal output for oclif commands with multiple stages",
4
- "version": "0.7.10",
4
+ "version": "0.7.12",
5
5
  "author": "Salesforce",
6
6
  "bugs": "https://github.com/oclif/multi-stage-output/issues",
7
7
  "dependencies": {
@@ -33,7 +33,7 @@
33
33
  "husky": "^9.1.6",
34
34
  "ink-testing-library": "^4.0.0",
35
35
  "lint-staged": "^15",
36
- "mocha": "^10.7.3",
36
+ "mocha": "^10.8.2",
37
37
  "prettier": "^3.3.3",
38
38
  "shx": "^0.3.4",
39
39
  "sinon": "^18",