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

streetsidesoftware / cspell / 21283815940

23 Jan 2026 10:59AM UTC coverage: 92.838% (+0.01%) from 92.825%
21283815940

Pull #8423

github

web-flow
Merge 74416f189 into 405411fa8
Pull Request #8423: fix: Add NotifyEmitter

8874 of 10620 branches covered (83.56%)

62 of 64 new or added lines in 3 files covered. (96.88%)

4 existing lines in 2 files now uncovered.

17734 of 19102 relevant lines covered (92.84%)

31510.24 hits per line

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

97.62
/packages/cspell-lib/src/rpc/notify.ts
1
import { AlreadyDisposedError } from './errors.js';
2

3
export type NotifyHandler<T> = (event: T) => void;
4

5
export type NotifyEvent<T> = (handler: NotifyHandler<T>) => Disposable;
6

7
/**
8
 * Used to have a type distinction between NotifyOnceEvents and NotifyEvents.
9
 * It is not used at runtime.
10
 */
11
const SymbolNotifyOnceEvent: symbol = Symbol('NotifyOnceEvent');
4✔
12
export type NotifyOnceEvent<T> = NotifyEvent<T> & { [SymbolNotifyOnceEvent]?: true };
13

14
/**
15
 * A Class used to emit notifications to registered handlers.
16
 */
17
export class NotifyEmitter<T> {
18
    #handlers: Map<NotifyHandler<T>, Disposable> = new Map();
34✔
19
    #disposed = false;
34✔
20

21
    /**
22
     * Registers a handler for the event. Multiple handlers can be added. The same handler will
23
     * not be added more than once. To add the same handler multiple times, use a wrapper function.
24
     *
25
     * Events are Async, so the handler will NOT be called during the registration.
26
     *
27
     * Note: This function can be used without needing to bind 'this'.
28
     * @param handler - the handler to add.
29
     * @returns a Disposable to remove the handler.
30
     */
31
    readonly onEvent: NotifyEvent<T> = (handler) => this.#onEvent(handler);
37✔
32

33
    /**
34
     * Notify all handlers of the event.
35
     *
36
     * If a handler throws an error, the error is not caught and will propagate up the call stack.
37
     *
38
     * Note: This function can be used without needing to bind 'this'.
39
     * @param value - The event value.
40
     */
41
    readonly notify: (value: T) => void = (value) => this.#notify(value);
34✔
42

43
    /**
44
     * A NotifyEvent that only fires once for each handler added.
45
     *
46
     * Multiple handlers can be added. The same handler can be added multiple times
47
     * and will be called once for each time it is added.
48
     *
49
     * Note: This property can be used without needing to bind 'this'.
50
     */
51
    readonly once: NotifyOnceEvent<T> = notifyEventOnce(this.onEvent);
34✔
52

53
    /**
54
     * Get a Promise that resolves with the next event.
55
     * @param signal - A signal to abort the wait.
56
     * @returns a Promise that will resolve with the next value emitted.
57
     */
58
    readonly awaitNext: (signal?: AbortSignal) => Promise<T> = (signal?: AbortSignal) =>
34✔
59
        notifyEventToPromise(this.onEvent, signal);
14✔
60

61
    /**
62
     * The number of registered handlers.
63
     */
64
    get size(): number {
65
        return this.#handlers.size;
33✔
66
    }
67

68
    /**
69
     * Removes all registered handlers.
70
     */
71
    clear(): void {
NEW
72
        this.#handlers.clear();
×
73
    }
74

75
    #onEvent(handler: NotifyHandler<T>): Disposable {
76
        if (this.#disposed) {
37✔
77
            throw new AlreadyDisposedError();
2✔
78
        }
79
        const found = this.#handlers.get(handler);
35✔
80
        if (found) {
35✔
81
            return found;
1✔
82
        }
83
        let disposed = false;
34✔
84
        const disposable = {
34✔
85
            [Symbol.dispose]: () => {
86
                if (disposed) return;
42✔
87
                disposed = true;
33✔
88
                this.#handlers.delete(handler);
33✔
89
            },
90
        };
91
        this.#handlers.set(handler, disposable);
34✔
92
        return disposable;
34✔
93
    }
94

95
    /**
96
     * Notify all handlers of the event.
97
     * @param value - The event value.
98
     */
99
    #notify(value: T): void {
100
        for (const handler of this.#handlers.keys()) {
31✔
101
            handler(value);
46✔
102
        }
103
    }
104

105
    [Symbol.dispose](): void {
106
        if (this.#disposed) return;
34✔
107
        this.#disposed = true;
33✔
108
        this.#handlers.clear();
33✔
109
    }
110
}
111

112
/**
113
 * Convert a NotifyEvent to a Promise.
114
 * @param event - The event to convert.
115
 * @param signal - Optional AbortSignal to cancel the subscription if the promise is abandoned.
116
 * @returns A Promise that resolves with the first value emitted by the event.
117
 */
118
export function notifyEventToPromise<T>(event: NotifyEvent<T>, signal?: AbortSignal): Promise<T> {
119
    const once = notifyEventOnce(event);
18✔
120
    return new Promise<T>((resolve, reject) => {
18✔
121
        signal?.throwIfAborted();
18✔
122

123
        const disposable = once((value) => {
17✔
124
            signal?.removeEventListener('abort', onAbort);
14✔
125
            resolve(value);
14✔
126
        });
127

128
        function onAbort() {
129
            disposable[Symbol.dispose]();
3✔
130
            signal?.removeEventListener('abort', onAbort);
3✔
131
            reject(signal?.reason);
3✔
132
        }
133

134
        signal?.addEventListener('abort', onAbort, { once: true });
17✔
135
    });
136
}
137

138
/**
139
 * Create a NotifyEvent that only fires once.
140
 *
141
 * The same handler can be added multiple times and will be called once for each time it is added.
142
 * This is different from a normal NotifyEvent where the same handler is only added once.
143
 *
144
 * @param event - The event to wrap.
145
 * @returns A NotifyOnceEvent that only fires once for the handlers added.
146
 */
147
export function notifyEventOnce<T>(event: NotifyEvent<T>): NotifyOnceEvent<T> {
148
    function notifyOnce(handler: NotifyHandler<T>): Disposable {
149
        const disposable = event((e) => {
23✔
150
            // A NotifyEvent should register a handler, but never call it immediately.
151
            // Therefor the disposable should always be defined here. A ReferenceError
152
            // would indicate a bug in NotifyEmitter or NotifyEvent implementation.
153
            disposable[Symbol.dispose]();
20✔
154
            handler(e);
20✔
155
        });
156
        return disposable;
23✔
157
    }
158

159
    return notifyOnce as NotifyOnceEvent<T>;
53✔
160
}
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