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

rokucommunity / brighterscript / #12930

13 Aug 2024 05:02PM UTC coverage: 86.193% (-1.7%) from 87.933%
#12930

push

web-flow
Merge 58ad447a2 into 0e968f1c3

10630 of 13125 branches covered (80.99%)

Branch coverage included in aggregate %.

6675 of 7284 new or added lines in 99 files covered. (91.64%)

84 existing lines in 18 files now uncovered.

12312 of 13492 relevant lines covered (91.25%)

26865.48 hits per line

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

86.96
/src/DependencyGraph.ts
1
import { EventEmitter } from 'eventemitter3';
1✔
2
/**
3
 * A graph of files and their dependencies.
4
 * Each file will only contain nodes that they directly reference (i.e. script imports, inheritance, etc)
5
 */
6
export class DependencyGraph {
1✔
7
    /**
8
     * A dictionary of all unique nodes in the entire graph
9
     */
10
    public nodes = {} as Record<string, Node>;
1,575✔
11

12
    /**
13
     * An internal event emitter for when keys have changed.
14
     */
15
    private onchangeEmitter = new EventEmitter<string, DependencyChangedEvent>();
1,575✔
16

17
    /**
18
     * Add a node to the graph.
19
     */
20
    public addOrReplace(key: string, dependencies?: string[]) {
21
        //sort the dependencies
22
        dependencies = dependencies?.sort() ?? [];
4,284✔
23

24
        //dispose any existing node
25
        this.nodes[key]?.dispose();
4,284✔
26

27
        //create a new dependency node
28
        let node = new Node(key, dependencies, this);
4,284✔
29
        this.nodes[key] = node;
4,284✔
30
        this.emit(key, { sourceKey: key, notifiedKeys: new Set() });
4,284✔
31
    }
32

33
    /**
34
     * Add a new dependency to an existing node (or create a new node if the node doesn't exist
35
     */
36
    public addDependency(key: string, dependencyKey: string) {
37
        let existingNode = this.nodes[key];
1,506✔
38
        if (existingNode) {
1,506✔
39
            let dependencies = existingNode.dependencies.includes(dependencyKey) ? existingNode.dependencies : [dependencyKey, ...existingNode.dependencies];
276✔
40
            this.addOrReplace(key, dependencies);
276✔
41
        } else {
42
            this.addOrReplace(key, [dependencyKey]);
1,230✔
43
        }
44
    }
45

46
    /**
47
     * Remove a dependency from an existing node.
48
     * Do nothing if the node does not have that dependency.
49
     * Do nothing if that node does not exist
50
     */
51
    public removeDependency(key: string, dependencyKey: string) {
52
        let existingNode = this.nodes[key];
129✔
53
        let idx = (existingNode?.dependencies ?? []).indexOf(dependencyKey);
129✔
54
        if (existingNode && idx > -1) {
129✔
55
            existingNode.dependencies.splice(idx, 1);
128✔
56
            this.addOrReplace(key, existingNode.dependencies);
128✔
57
        }
58
    }
59

60
    /**
61
     * Get a list of the dependencies for the given key, recursively.
62
     * @param keys the key (or keys) for which to get the dependencies
63
     * @param exclude a list of keys to exclude from traversal. Anytime one of these nodes is encountered, it is skipped.
64
     */
65
    public getAllDependencies(keys: string | string[], exclude?: string[]) {
66
        if (typeof keys === 'string') {
3,686✔
67
            return this.nodes[keys]?.getAllDependencies(exclude) ?? [];
3,685✔
68
        } else {
69
            const set = new Set<string>();
1✔
70
            for (const key of keys) {
1✔
71
                const dependencies = this.getAllDependencies(key, exclude);
2✔
72
                for (const dependency of dependencies) {
2✔
73
                    set.add(dependency);
5✔
74
                }
75
            }
76
            return [...set];
1✔
77
        }
78
    }
79

80
    /**
81
     * Get a list of the immediate dependencies for the given key.
82
     */
83
    public getImmediateDependencies(keys: string | string[], exclude?: string[]) {
NEW
84
        if (typeof keys === 'string') {
×
NEW
85
            return this.nodes[keys]?.dependencies ?? [];
×
86
        } else {
NEW
87
            const set = new Set<string>();
×
NEW
88
            for (const key of keys) {
×
NEW
89
                const dependencies = this.getImmediateDependencies(key, exclude);
×
NEW
90
                for (const dependency of dependencies) {
×
NEW
91
                    set.add(dependency);
×
92
                }
93
            }
NEW
94
            return [...set];
×
95
        }
96
    }
97

98
    /**
99
     * Remove the item. This will emit an onchange event for all dependent nodes
100
     */
101
    public remove(key: string) {
102
        this.nodes[key]?.dispose();
165✔
103
        delete this.nodes[key];
165✔
104
        this.emit(key, { sourceKey: key, notifiedKeys: new Set() });
165✔
105
    }
106

107
    /**
108
     * Emit event that this item has changed
109
     */
110
    public emit(key: string, event: DependencyChangedEvent) {
111
        //prevent infinite event loops by skipping already-notified keys
112
        if (!event.notifiedKeys.has(key)) {
5,672✔
113
            event.notifiedKeys.add(key);
5,648✔
114
            this.onchangeEmitter.emit(key, event);
5,648✔
115
        }
116
    }
117

118
    /**
119
     * Listen for any changes to dependencies with the given key.
120
     * @param key the name of the dependency
121
     * @param handler a function called anytime changes occur
122
     */
123
    public onchange(key: string, handler: (event: DependencyChangedEvent) => void) {
124
        this.onchangeEmitter.on(key, handler);
10,090✔
125
        return () => {
10,090✔
126
            this.onchangeEmitter.off(key, handler);
8,880✔
127
        };
128
    }
129

130
    public dispose() {
131
        for (let key in this.nodes) {
1,427✔
132
            let node = this.nodes[key];
3,436✔
133
            node.dispose();
3,436✔
134
        }
135
        this.onchangeEmitter.removeAllListeners();
1,427✔
136
    }
137
}
138

139
export interface DependencyChangedEvent {
140
    /**
141
     * The key that was the initiator of this event. Child keys will emit this same event object, but this key will remain the same
142
     */
143
    sourceKey: string;
144
    /**
145
     * A set of keys that have already been notified of this change. Used to prevent circular reference notification cycles
146
     */
147
    notifiedKeys: Set<string>;
148
}
149

150
export class Node {
1✔
151
    public constructor(
152
        public key: string,
4,284✔
153
        public dependencies: string[],
4,284✔
154
        public graph: DependencyGraph
4,284✔
155
    ) {
156
        if (dependencies.length > 0) {
4,284✔
157
            this.subscriptions = [];
2,888✔
158

159
            for (let dependency of this.dependencies) {
2,888✔
160
                let sub = this.graph.onchange(dependency, (event) => {
4,621✔
161
                    //notify the graph that we changed since one of our dependencies changed
162
                    this.graph.emit(this.key, event);
1,223✔
163
                });
164

165
                this.subscriptions.push(sub);
4,621✔
166
            }
167
        }
168
    }
169

170
    private subscriptions: Array<() => void> | undefined;
171

172
    /**
173
     * Return the full list of unique dependencies for this node by traversing all descendents
174
     * @param exclude a list of keys to exclude from traversal. Anytime one of these nodes is encountered, it is skipped.
175
     */
176
    public getAllDependencies(exclude: string[] = []) {
1,618✔
177
        let dependencyMap = {};
2,091✔
178
        let dependencyStack = [...this.dependencies];
2,091✔
179
        //keep walking the dependency graph until we run out of unseen dependencies
180
        while (dependencyStack.length > 0) {
2,091✔
181
            let dependency = dependencyStack.pop();
5,615✔
182

183
            //if this is a new dependency and we aren't supposed to skip it
184
            if (dependency && !dependencyMap[dependency] && !exclude.includes(dependency)) {
5,615✔
185
                dependencyMap[dependency] = true;
4,852✔
186

187
                //get the node for this dependency
188
                let node = this.graph.nodes[dependency];
4,852✔
189
                if (node) {
4,852✔
190
                    dependencyStack.push(...node.dependencies);
2,851✔
191
                }
192
            }
193
        }
194
        return Object.keys(dependencyMap);
2,091✔
195
    }
196

197
    public dispose() {
198
        for (let unsubscribe of this.subscriptions ?? []) {
4,009✔
199
            unsubscribe();
4,271✔
200
        }
201
    }
202
}
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

© 2025 Coveralls, Inc