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

snatalenko / node-cqrs / 21717407497

05 Feb 2026 03:26PM UTC coverage: 84.53% (-9.9%) from 94.396%
21717407497

Pull #28

github

web-flow
Merge 025edb883 into 828e39903
Pull Request #28: TypeScript and event dispatching pipeline refactoring

611 of 939 branches covered (65.07%)

819 of 934 new or added lines in 65 files covered. (87.69%)

59 existing lines in 13 files now uncovered.

1213 of 1435 relevant lines covered (84.53%)

28.39 hits per line

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

77.78
/src/in-memory/InMemoryView.ts
1
import { InMemoryLock } from './InMemoryLock.ts';
34✔
2
import type { IViewLocker, Identifier, IObjectStorage } from '../interfaces/index.ts';
3
import { nextCycle } from './utils/index.ts';
34✔
4

5
/**
6
 * Update given value with an update Cb and return updated value.
7
 * Wrapper is needed for backward compatibility with update methods that were modifying the passed in objects directly
8
 */
9
function applyUpdate<T>(view: T, update: (r: T) => T): T {
10
        const valueReturnedByUpdate = update(view);
40✔
11
        return valueReturnedByUpdate === undefined ?
40✔
12
                view :
13
                valueReturnedByUpdate;
14
}
15

16
/**
17
 * In-memory Projection View, which suspends get()'s until it is ready
18
 */
19
export class InMemoryView<TRecord> implements IViewLocker, IObjectStorage<TRecord> {
34✔
20

21
        static factory<TView>(): TView {
22
                return (new InMemoryView() as unknown) as TView;
×
23
        }
24

25
        protected _map: Map<Identifier, TRecord> = new Map();
72✔
26

27
        #lock: InMemoryLock;
28

29
        /** Whether the view is restored */
30
        get ready(): boolean {
31
                return !this.#lock.locked;
48✔
32
        }
33

34
        /** Number of records in the View */
35
        get size(): number {
36
                return this._map.size;
12✔
37
        }
38

39
        constructor() {
40
                this.#lock = new InMemoryLock();
72✔
41

42
                // explicitly bind the `get` method to this object for easier using in Promises
43
                Object.defineProperty(this, this.get.name, {
72✔
44
                        value: this.get.bind(this)
45
                });
46
        }
47

48
        /** Lock the view to prevent concurrent modifications */
49
        async lock(): Promise<boolean> {
50
                await this.#lock.lock();
24✔
51
                return this.#lock.locked;
24✔
52
        }
53

54
        /** Release the lock */
55
        async unlock(): Promise<void> {
56
                return this.#lock.unlock();
50✔
57
        }
58

59
        /** Create a Promise which will resolve to a first emitted event of a given type */
60
        once(eventType: 'ready'): Promise<any> {
61
                if (eventType !== 'ready')
6!
62
                        throw new TypeError(`Unexpected event type: ${eventType}`);
×
63

64
                return this.#lock.once('unlocked');
6✔
65
        }
66

67
        /**
68
         * Check if view contains a record with a given key.
69
         * This is the only synchronous method, so make sure to check the `ready` flag, if necessary
70
         *
71
         * @deprecated Use `async get()` instead
72
         */
73
        has(key: Identifier): boolean {
74
                return this._map.has(key);
4✔
75
        }
76

77
        /** Get record with a given key; await until the view is restored */
78
        async get(key: Identifier, options?: { nowait?: boolean }): Promise<TRecord | undefined> {
79
                if (!key)
38!
80
                        throw new TypeError('key argument required');
×
81

82
                if (!this.ready && !options?.nowait)
38✔
83
                        await this.once('ready');
2✔
84

85
                await nextCycle();
38✔
86

87
                return this._map.get(key);
38✔
88
        }
89

90
        /**
91
         * Get record with a given key synchronously
92
         */
93
        getSync(key: Identifier): TRecord | undefined {
94
                if (!key)
×
95
                        throw new TypeError('key argument required');
×
96

97
                return this._map.get(key);
×
98
        }
99

100
        /** Get all records matching an optional filter */
101
        async getAll(filter?: (r: TRecord | undefined, i: Identifier) => boolean):
102
                Promise<Array<[Identifier, TRecord | undefined]>> {
103
                if (filter && typeof filter !== 'function')
6✔
104
                        throw new TypeError('filter argument, when defined, must be a Function');
2✔
105

106
                if (!this.ready)
4✔
107
                        await this.once('ready');
2✔
108

109
                await nextCycle();
4✔
110

111
                const r: Array<[Identifier, TRecord | undefined]> = [];
4✔
112
                for (const entry of this._map.entries()) {
4✔
113
                        if (!filter || filter(entry[1], entry[0]))
10✔
114
                                r.push(entry);
6✔
115
                }
116

117
                return r;
4✔
118
        }
119

120
        /** Create record with a given key and value */
121
        async create(key: Identifier, value: TRecord = {} as TRecord) {
×
122
                if (!key)
72!
123
                        throw new TypeError('key argument required');
×
124
                if (typeof value === 'function')
72✔
125
                        throw new TypeError('value argument must be an instance of an Object');
2✔
126

127
                if (this._map.has(key))
70✔
128
                        throw new Error(`Key '${key}' already exists`);
2✔
129

130
                this._map.set(key, value);
68✔
131
        }
132

133
        /** Update existing view record */
134
        async update(key: Identifier, update: (r: TRecord) => TRecord) {
135
                if (!key)
2!
136
                        throw new TypeError('key argument required');
×
137
                if (typeof update !== 'function')
2!
138
                        throw new TypeError('update argument must be a Function');
×
139

140
                if (!this._map.has(key))
2✔
141
                        throw new Error(`Key '${key}' does not exist`);
2✔
142

143
                return this._update(key, update);
×
144
        }
145

146
        /** Update existing view record or create new */
147
        async updateEnforcingNew(key: Identifier, update: (r?: TRecord) => TRecord) {
148
                if (!key)
38!
149
                        throw new TypeError('key argument required');
×
150
                if (typeof update !== 'function')
38!
151
                        throw new TypeError('update argument must be a Function');
×
152

153
                if (!this._map.has(key))
38✔
154
                        return this.create(key, applyUpdate(undefined, update));
24✔
155

156
                return this._update(key, update);
14✔
157
        }
158

159
        /** Update all records that match filter criteria */
160
        async updateAll(filter: (r: TRecord) => boolean, update: (r: TRecord) => TRecord) {
161
                if (filter && typeof filter !== 'function')
2!
162
                        throw new TypeError('filter argument, when specified, must be a Function');
×
163
                if (typeof update !== 'function')
2!
164
                        throw new TypeError('update argument must be a Function');
×
165

166
                for (const [key, value] of this._map) {
2✔
167
                        if (!filter || filter(value))
4✔
168
                                await this._update(key, update);
2✔
169
                }
170
        }
171

172
        /** Update existing record */
173
        private async _update(key: Identifier, update: (r: TRecord) => TRecord) {
174
                const value = this._map.get(key);
16✔
175
                if (!value)
16!
NEW
176
                        throw new Error(`Key '${key}' does not exist`);
×
177

178
                const updatedValue = applyUpdate(value, update);
16✔
179
                if (updatedValue === undefined)
16!
180
                        return;
×
181

182
                this._map.set(key, updatedValue);
16✔
183
        }
184

185
        /** Delete record */
186
        async delete(key: Identifier) {
187
                if (!key)
6!
188
                        throw new TypeError('key argument required');
×
189

190
                this._map.delete(key);
6✔
191
        }
192

193
        /** Delete all records that match filter criteria */
194
        async deleteAll(filter: (r?: TRecord) => boolean) {
195
                if (filter && typeof filter !== 'function')
2!
196
                        throw new TypeError('filter argument, when specified, must be a Function');
×
197

198
                for (const [key, value] of this._map) {
2✔
199
                        if (!filter || filter(value))
4✔
200
                                await this.delete(key);
2✔
201
                }
202
        }
203

204
        /** Get view summary as string */
205
        toString(): string {
206
                return `${this.size} record${this.size !== 1 ? 's' : ''}`;
4✔
207
        }
208
}
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