• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rokucommunity / brighterscript / #15903

11 May 2026 06:41PM UTC coverage: 86.896% (-2.2%) from 89.094%
#15903

push

web-flow
Merge 70dfd6181 into ce68f5cb7

15597 of 18958 branches covered (82.27%)

Branch coverage included in aggregate %.

9 of 9 new or added lines in 3 files covered. (100.0%)

955 existing lines in 53 files now uncovered.

16351 of 17808 relevant lines covered (91.82%)

27326.16 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.16
/src/common/Sequencer.ts
1
import type { CancellationToken } from 'vscode-languageserver-protocol';
2
import { util } from '../util';
1✔
3
import { EventEmitter } from 'eventemitter3';
1✔
4
import * as parseMilliseconds from 'parse-ms';
1✔
5
import { performance } from 'perf_hooks';
1✔
6

7
/**
8
 * Supports running a series of actions in sequence, either synchronously or asynchronously
9
 * @private
10
 */
11
export class Sequencer {
1✔
12
    constructor(
13
        private options?: {
2,183✔
14
            name?: string;
15
            cancellationToken?: CancellationToken;
16
            /**
17
             * The number of operations to run before registering a nexttick
18
             */
19
            minSyncDuration?: number;
20
        }
21
    ) {
22
    }
23

24
    private get minSyncDuration() {
25
        return this.options?.minSyncDuration ?? 150;
5,661✔
26
    }
27

28
    // eslint-disable-next-line @typescript-eslint/ban-types
29
    private actions: Array<{
2,183✔
30
        /**
31
         * Label for this action. Is used for logging and metrics
32
         */
33
        label: string;
34
        /**
35
         * If this action is a member of a group (i.e. forEach calls), this is the label for the group which will be logged in addition to the label
36
         */
37
        groupLabel?: string;
38
        args: any[];
39
        func: (...args: any[]) => any;
40
    }> = [];
41

42
    public forEach<T>(label: string, itemsOrFactory: Iterable<T> | (() => Iterable<T>), func: (item: T) => any) {
43
        //register a single action for now, we will fetch the full list and register their actions later
44
        const primaryAction = {
13,070✔
45
            args: [],
46
            label: label,
47
            func: (data) => {
48
                const items = typeof itemsOrFactory === 'function' ? itemsOrFactory() : itemsOrFactory;
12,616✔
49
                const actions: Sequencer['actions'] = [];
12,616✔
50
                let i = 0;
12,616✔
51
                for (const item of items) {
12,616✔
52
                    actions.push({
15,909✔
53
                        label: label + `[${i++}]`,
54
                        groupLabel: label,
55
                        args: [item],
56
                        func: func
57
                    });
58
                }
59
                let primaryActionIndex = this.actions.indexOf(primaryAction);
12,616✔
60
                //insert all of these item actions immediately after this action
61
                this.actions.splice(primaryActionIndex + 1, 0, ...actions);
12,616✔
62
            }
63
        };
64
        this.actions.push(primaryAction);
13,070✔
65
        return this;
13,070✔
66
    }
67

68
    private emitter = new EventEmitter();
2,183✔
69

70
    public onCancel(callback: () => void) {
71
        this.emitter.on('cancel', callback);
2,180✔
72
        return this;
2,180✔
73
    }
74

75
    public onComplete(callback: () => void) {
76
        this.emitter.on('complete', callback);
2,178✔
77
        return this;
2,178✔
78
    }
79

80
    public onSuccess(callback: () => void) {
81
        this.emitter.on('success', callback);
2,178✔
82
        return this;
2,178✔
83
    }
84

85
    public once(label: string, func: () => any) {
86
        this.actions.push({
19,605✔
87
            args: [],
88
            func: func,
89
            label: label
90
        });
91
        return this;
19,605✔
92
    }
93

94
    public async run() {
95
        try {
370✔
96
            let start = Date.now();
370✔
97
            for (const action of this.actions) {
370✔
98
                //register a very short timeout between every action so we don't hog the CPU
99
                if (Date.now() - start > this.minSyncDuration) {
5,661✔
100
                    await util.sleep(1);
1✔
101
                    start = Date.now();
1✔
102
                }
103

104
                //if the cancellation token has asked us to cancel, then stop processing now
105
                if (this.options?.cancellationToken?.isCancellationRequested) {
5,661!
UNCOV
106
                    return this.handleCancel();
×
107
                }
108

109
                await this.runActionAsync(action);
5,661✔
110

111
                //if the cancellation token has asked us to cancel, then stop processing now
112
                if (this.options?.cancellationToken?.isCancellationRequested) {
5,658✔
113
                    return this.handleCancel();
118✔
114
                }
115
            }
116
            this.emitter.emit('success');
249✔
117
        } catch (e) {
118
            this.handleCancel();
3✔
119
            throw e;
3✔
120
        } finally {
121
            this.emitter.emit('complete');
370✔
122
            this.dispose();
370✔
123
        }
124
    }
125

126
    public runSync() {
127
        try {
1,813✔
128
            for (const action of this.actions) {
1,813✔
129
                //if the cancellation token has asked us to cancel, then stop processing now
130
                if (this.options?.cancellationToken?.isCancellationRequested) {
42,039✔
131
                    return this.handleCancel();
1✔
132
                }
133

134
                const result = this.runActionSync(action);
42,038✔
135

136
                if (typeof result?.then === 'function') {
42,036✔
137
                    throw new Error(`Action returned a promise which is unsupported when running 'runSync'`);
1✔
138
                }
139
            }
140
            this.emitter.emit('success');
1,809✔
141
        } catch (e) {
142
            this.handleCancel();
3✔
143
            throw e;
3✔
144
        } finally {
145
            this.emitter.emit('complete');
1,813✔
146
            this.dispose();
1,813✔
147
        }
148
    }
149

150
    private async runActionAsync<T>(action: typeof this.actions[0]): Promise<T> {
151
        //record the start time for this action
152
        let perfBefore = performance.now();
5,661✔
153
        try {
5,661✔
154
            return await Promise.resolve(
5,661✔
155
                action.func(...action.args)
156
            );
157
        } finally {
158
            let perfAfter = performance.now();
5,661✔
159
            this.incrementMetric(action.label, perfAfter - perfBefore, 1, !!action.groupLabel);
5,661✔
160
            if (action.groupLabel) {
5,661✔
161
                this.incrementMetric(action.groupLabel, perfAfter - perfBefore, 1);
1,003✔
162
            }
163
        }
164
    }
165

166
    private runActionSync<T>(action: typeof this.actions[0]) {
167
        //record the start time for this action
168
        let perfBefore = performance.now();
42,038✔
169
        try {
42,038✔
170
            return action.func(...action.args);
42,038✔
171
        } finally {
172
            let perfAfter = performance.now();
42,038✔
173
            this.incrementMetric(action.label, perfAfter - perfBefore, 1, !!action.groupLabel);
42,038✔
174
            if (action.groupLabel) {
42,038✔
175
                this.incrementMetric(action.groupLabel, perfAfter - perfBefore, 1);
14,887✔
176
            }
177
        }
178
    }
179

180
    /**
181
     * An object that collects timing information for our actions
182
     */
183
    public metrics = new Map<string, {
2,183✔
184
        duration: number;
185
        callCount: number;
186
        isLoopIteration: boolean;
187
    }>();
188

189
    /**
190
     * Get all the metrics in a text-based format (useful for logging)
191
     */
192
    public formatMetrics(options: { header: string; includeLoopIterations: boolean }) {
193
        let results: Array<{ label: string; durationText: string; callText: string }> = [];
2,178✔
194
        let total = 0;
2,178✔
195
        for (const [label, metric] of this.metrics) {
2,178✔
196
            //skip loop iterations if options say they should be skipped
197
            if (metric.isLoopIteration && options?.includeLoopIterations !== true) {
47,689!
198
                continue;
15,885✔
199
            }
200
            //sum all the non-loop iterations (i.e. the one-off runs or grouped loop runs).
201
            //this represents the total time spend running these tasks
202
            if (!metric.isLoopIteration) {
31,804!
203
                total += metric.duration;
31,804✔
204
            }
205
            results.push({
31,804✔
206
                label: label,
207
                durationText: this.getDurationText(metric.duration),
208
                callText: `${metric.callCount} calls`
209
            });
210
        }
211

212
        let maxLabelLength = Math.max(...results.map(x => x.label.length));
31,804✔
213
        let maxCallTextLength = Math.max(...results.map(x => x.callText.length));
31,804✔
214
        let maxDurationLength = Math.max(...results.map(x => x.durationText.length));
31,804✔
215

216
        results.push(
2,178✔
217
            {
218
                label: 'Total',
219
                durationText: this.getDurationText(total),
220
                callText: ''
221
            }
222
        );
223

224
        return (options?.header ? options.header + '\n' : '') +
2,178!
225
            results.map(x => [
33,982✔
226
                '    ' + (x.label ?? '').padEnd(maxLabelLength + 2, '-') +
101,946!
227
                (' ' + (x.durationText ?? '')).padStart(maxDurationLength + 2, '-') + ', ' +
101,946!
228
                (x.callText ?? '').padStart(maxCallTextLength, ' ')
101,946!
229
            ].join(', ')).join('\n');
230

231
    }
232

233
    private getDurationText(duration: number) {
234
        let parts = parseMilliseconds(duration);
33,982✔
235
        let fractionalMilliseconds = parseInt(duration.toFixed(3).toString().split('.')[1]);
33,982✔
236
        let result = `${parts.minutes}m${parts.seconds.toString().padStart(2, '0')}s${parts.milliseconds.toString().padStart(3, '0')}.${fractionalMilliseconds.toString().padStart(3, '0')}ms`;
33,982✔
237
        //remove leading zeros for minutes and seconds
238
        result = result.replace(/^0+m/, '').replace(/^0+s/, '');
33,982✔
239
        return result;
33,982✔
240
    }
241

242
    /**
243
     * Bump the metric for the given key by the given duration and call count
244
     * @param label the label of the action to have its metric incremented
245
     * @param duration the amount of new duration to add to the existing duration
246
     * @param callCount the amount of new calls to add to the existing call count
247
     */
248
    private incrementMetric(label: string, duration: number, callCount: number, isLoopIteration = false) {
15,890✔
249
        let metric = this.metrics.get(label);
63,589✔
250
        if (!metric) {
63,589✔
251
            metric = {
47,699✔
252
                duration: 0,
253
                callCount: 0,
254
                isLoopIteration: false
255
            };
256
            this.metrics.set(label, metric);
47,699✔
257
        }
258
        metric.duration += duration;
63,589✔
259
        metric.callCount += callCount;
63,589✔
260
        metric.isLoopIteration = isLoopIteration;
63,589✔
261
    }
262

263

264
    private handleCancel() {
265
        console.log(`Cancelling sequence ${this.options?.name}`);
125✔
266
        this.emitter.emit('cancel');
125✔
267
    }
268

269
    private dispose() {
270
        this.emitter.removeAllListeners();
2,183✔
271
    }
272
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc