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

snatalenko / node-cqrs / 11544751153

27 Oct 2024 11:37PM UTC coverage: 95.151% (+1.4%) from 93.73%
11544751153

push

github

snatalenko
1.0.0-rc.5

563 of 866 branches covered (65.01%)

2394 of 2516 relevant lines covered (95.15%)

21.46 hits per line

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

96.38
/src/infrastructure/InMemoryView.ts
1
import { InMemoryLock } from './InMemoryLock';
4✔
2
import { IProjectionView, Identifier } from "../interfaces";
4✔
3
import { nextCycle } from './utils';
4✔
4

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

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

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

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

42✔
27
        #lock: InMemoryLock;
42✔
28

42✔
29
        #asyncWrites: boolean;
42✔
30

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

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

42✔
41
        constructor(options?: {
42✔
42
                /** Indicates if writes should be submitted asynchronously */
64✔
43
                asyncWrites?: boolean
64✔
44
        }) {
64✔
45
                this.#asyncWrites = options?.asyncWrites ?? false;
64!
46

64✔
47
                this.#lock = new InMemoryLock();
64✔
48

64✔
49
                // explicitly bind the `get` method to this object for easier using in Promises
64✔
50
                Object.defineProperty(this, this.get.name, {
64✔
51
                        value: this.get.bind(this)
64✔
52
                });
64✔
53
        }
64✔
54

42✔
55
        /** Lock the view to prevent concurrent modifications */
42✔
56
        async lock(): Promise<boolean> {
42✔
57
                await this.#lock.lock();
22✔
58
                return this.#lock.locked;
22✔
59
        }
22✔
60

42✔
61
        /** Release the lock */
42✔
62
        async unlock(): Promise<void> {
42✔
63
                return this.#lock.unlock();
46✔
64
        }
46✔
65

42✔
66
        /** Create a Promise which will resolve to a first emitted event of a given type */
42✔
67
        once(eventType: 'ready'): Promise<any> {
42✔
68
                if (eventType !== 'ready')
6✔
69
                        throw new TypeError(`Unexpected event type: ${eventType}`);
6!
70

6✔
71
                return this.#lock.once('unlocked');
6✔
72
        }
6✔
73

42✔
74
        /**
42✔
75
         * Check if view contains a record with a given key.
42✔
76
         * This is the only synchronous method, so make sure to check the `ready` flag, if necessary
42✔
77
         *
42✔
78
         * @deprecated Use `async get()` instead
42✔
79
         */
42✔
80
        has(key: Identifier): boolean {
42✔
81
                return this._map.has(key);
4✔
82
        }
4✔
83

42✔
84
        /** Get record with a given key; await until the view is restored */
42✔
85
        async get(key: Identifier, options?: { nowait?: boolean }): Promise<TRecord | undefined> {
42✔
86
                if (!key)
38✔
87
                        throw new TypeError('key argument required');
38!
88

38✔
89
                if (!this.ready && !options?.nowait)
38✔
90
                        await this.once('ready');
38✔
91

38✔
92
                await nextCycle();
38✔
93

38✔
94
                return this._map.get(key);
38✔
95
        }
38✔
96

42✔
97
        /**
42✔
98
         * Get record with a given key synchronously
42✔
99
         */
42✔
100
        getSync(key: Identifier): TRecord | undefined {
42✔
101
                if (!key)
×
102
                        throw new TypeError('key argument required');
×
103

×
104
                return this._map.get(key);
×
105
        }
×
106

42✔
107
        /** Get all records matching an optional filter */
42✔
108
        async getAll(filter?: (r: TRecord | undefined, i: Identifier) => boolean):
42✔
109
                Promise<Array<[Identifier, TRecord | undefined]>> {
6✔
110
                if (filter && typeof filter !== 'function')
6✔
111
                        throw new TypeError('filter argument, when defined, must be a Function');
6✔
112

4✔
113
                if (!this.ready)
4✔
114
                        await this.once('ready');
6✔
115

4✔
116
                await nextCycle();
4✔
117

4✔
118
                const r: Array<[Identifier, TRecord | undefined]> = [];
4✔
119
                for (const entry of this._map.entries()) {
6✔
120
                        if (!filter || filter(entry[1], entry[0]))
10✔
121
                                r.push(entry);
10✔
122
                }
10✔
123

4✔
124
                return r;
4✔
125
        }
4✔
126

42✔
127
        /** Create record with a given key and value */
42✔
128
        async create(key: Identifier, value: TRecord = {} as TRecord) {
42✔
129
                if (!key)
72✔
130
                        throw new TypeError('key argument required');
72!
131
                if (typeof value === 'function')
72✔
132
                        throw new TypeError('value argument must be an instance of an Object');
72✔
133

70✔
134
                if (this.#asyncWrites)
70✔
135
                        await nextCycle();
72!
136

70✔
137
                if (this._map.has(key))
70✔
138
                        throw new Error(`Key '${key}' already exists`);
72✔
139

68✔
140
                this._map.set(key, value);
68✔
141
        }
68✔
142

42✔
143
        /** Update existing view record */
42✔
144
        async update(key: Identifier, update: (r: TRecord) => TRecord) {
42✔
145
                if (!key)
2✔
146
                        throw new TypeError('key argument required');
2!
147
                if (typeof update !== 'function')
2✔
148
                        throw new TypeError('update argument must be a Function');
2!
149

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

×
153
                return this._update(key, update);
×
154
        }
×
155

42✔
156
        /** Update existing view record or create new */
42✔
157
        async updateEnforcingNew(key: Identifier, update: (r?: TRecord) => TRecord) {
42✔
158
                if (!key)
38✔
159
                        throw new TypeError('key argument required');
38!
160
                if (typeof update !== 'function')
38✔
161
                        throw new TypeError('update argument must be a Function');
38!
162

38✔
163
                if (!this._map.has(key))
38✔
164
                        return this.create(key, applyUpdate(undefined, update));
38✔
165

14✔
166
                return this._update(key, update);
14✔
167
        }
14✔
168

42✔
169
        /** Update all records that match filter criteria */
42✔
170
        async updateAll(filter: (r: TRecord) => boolean, update: (r: TRecord) => TRecord) {
42✔
171
                if (filter && typeof filter !== 'function')
2✔
172
                        throw new TypeError('filter argument, when specified, must be a Function');
2!
173
                if (typeof update !== 'function')
2✔
174
                        throw new TypeError('update argument must be a Function');
2!
175

2✔
176
                for (const [key, value] of this._map) {
2✔
177
                        if (!filter || filter(value))
4✔
178
                                await this._update(key, update);
4✔
179
                }
4✔
180
        }
2✔
181

42✔
182
        /** Update existing record */
42✔
183
        private async _update(key: Identifier, update: (r?: TRecord) => TRecord) {
42✔
184
                const value = this._map.get(key);
16✔
185
                const updatedValue = applyUpdate(value, update);
16✔
186
                if (updatedValue === undefined)
16✔
187
                        return;
16!
188

16✔
189
                if (this.#asyncWrites)
16✔
190
                        await nextCycle();
16!
191

16✔
192
                this._map.set(key, updatedValue);
16✔
193
        }
16✔
194

42✔
195
        /** Delete record */
42✔
196
        async delete(key: Identifier) {
42✔
197
                if (!key)
6✔
198
                        throw new TypeError('key argument required');
6!
199

6✔
200
                if (this.#asyncWrites)
6✔
201
                        await nextCycle();
6!
202

6✔
203
                this._map.delete(key);
6✔
204
        }
6✔
205

42✔
206
        /** Delete all records that match filter criteria */
42✔
207
        async deleteAll(filter: (r?: TRecord) => boolean) {
42✔
208
                if (filter && typeof filter !== 'function')
2✔
209
                        throw new TypeError('filter argument, when specified, must be a Function');
2!
210

2✔
211
                for (const [key, value] of this._map) {
2✔
212
                        if (!filter || filter(value))
4✔
213
                                await this.delete(key);
4✔
214
                }
4✔
215
        }
2✔
216

42✔
217
        /** Get view summary as string */
42✔
218
        toString(): string {
42✔
219
                return `${this.size} record${this.size !== 1 ? 's' : ''}`;
4✔
220
        }
4✔
221
}
42✔
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