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

albe / node-event-storage / 25625880534

10 May 2026 10:02AM UTC coverage: 97.684% (-0.3%) from 98.015%
25625880534

push

github

web-flow
Merge pull request #303 from albe/copilot/sub-pr-301

feat(Storage): async partition + index scan in open() with 'opened' event and callback hook

1036 of 1086 branches covered (95.4%)

Branch coverage included in aggregate %.

100 of 102 new or added lines in 4 files covered. (98.04%)

21 existing lines in 2 files now uncovered.

5416 of 5519 relevant lines covered (98.13%)

808.45 hits per line

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

93.11
/src/util.js
1
import fs from 'fs';
4✔
2
import path from 'path';
4✔
3
import { mkdirpSync } from 'mkdirp';
4✔
4

4✔
5
/**
4✔
6
 * Assert that actual and expected match or throw an Error with the given message appended by information about expected and actual value.
4✔
7
 *
4✔
8
 * @param {*} actual
4✔
9
 * @param {*} expected
4✔
10
 * @param {string} message
4✔
11
 */
4✔
12
function assertEqual(actual, expected, message) {
19,212✔
13
    if (actual !== expected) {
19,212✔
14
        throw new Error(message + (message ? ' ' : '') + `Expected "${expected}" but got "${actual}".`);
8!
15
    }
8✔
16
}
19,212✔
17

4✔
18
/**
4✔
19
 * Assert that the condition holds and if not, throw an error with the given message.
4✔
20
 *
4✔
21
 * @param {boolean} condition
4✔
22
 * @param {string} message
4✔
23
 * @param {typeof Error} ErrorType
4✔
24
 */
4✔
25
function assert(condition, message, ErrorType = Error) {
100,768✔
26
    if (!condition) {
100,768✔
27
        throw new ErrorType(message);
200✔
28
    }
200✔
29
}
100,768✔
30

4✔
31
/**
4✔
32
 * Return the amount required to align value to the given alignment.
4✔
33
 * It calculates the difference of the alignment and the modulo of value by alignment.
4✔
34
 * @param {number} value
4✔
35
 * @param {number} alignment
4✔
36
 * @returns {number}
4✔
37
 */
4✔
38
function alignTo(value, alignment) {
36,020✔
39
    return (alignment - (value % alignment)) % alignment;
36,020✔
40
}
36,020✔
41

4✔
42
/**
4✔
43
 * Method for hashing a string (e.g. a partition name) to a 32-bit unsigned integer.
4✔
44
 *
4✔
45
 * @param {string} str
4✔
46
 * @returns {number}
4✔
47
 */
4✔
48
function hash(str) {
5,286✔
49
    /* istanbul ignore if */
5,286✔
50
    if (str.length === 0) {
5,286!
51
        return 0;
×
52
    }
×
53
    let hash = 5381,
5,286✔
54
        i    = str.length;
5,286✔
55

5,286✔
56
    while(i) {
5,286✔
57
        hash = ((hash << 5) + hash) ^ str.charCodeAt(--i); // jshint ignore:line
63,174✔
58
    }
63,174✔
59

5,286✔
60
    /* JavaScript does bitwise operations (like XOR, above) on 32-bit signed
5,286✔
61
     * integers. Since we want the results to be always positive, convert the
5,286✔
62
     * signed int to an unsigned by doing an unsigned bitshift. */
5,286✔
63
    return hash >>> 0; // jshint ignore:line
5,286✔
64
}
5,286✔
65

4✔
66
/**
4✔
67
 * Build a buffer containing the file magic header and a JSON stringified metadata block, padded to be a multiple of 16 bytes long.
4✔
68
 *
4✔
69
 * @param {string} magic
4✔
70
 * @param {object} metadata
4✔
71
 * @returns {Buffer} A buffer containing the header data
4✔
72
 */
4✔
73
function buildMetadataHeader(magic, metadata) {
3,984✔
74
    assertEqual(magic.length, 8, 'The header magic bytes length is wrong.');
3,984✔
75
    let metadataString = JSON.stringify(metadata);
3,984✔
76
    let metadataSize = Buffer.byteLength(metadataString, 'utf8');
3,984✔
77
    // 8 byte MAGIC, 4 byte metadata size, 1 byte line break
3,984✔
78
    const pad = (16 - ((8 + 4 + metadataSize + 1) % 16)) % 16;
3,984✔
79
    metadataString += ' '.repeat(pad) + "\n";
3,984✔
80
    metadataSize += pad + 1;
3,984✔
81
    const metadataBuffer = Buffer.allocUnsafe(8 + 4 + metadataSize);
3,984✔
82
    metadataBuffer.write(magic, 0, 8, 'utf8');
3,984✔
83
    metadataBuffer.writeUInt32BE(metadataSize, 8);
3,984✔
84
    metadataBuffer.write(metadataString, 8 + 4, metadataSize, 'utf8');
3,984✔
85
    return metadataBuffer;
3,984✔
86
}
3,984✔
87

4✔
88
/**
4✔
89
 * Do a binary search for number in the range 1-length with values retrieved via a provided getter.
4✔
90
 *
4✔
91
 * @param {number} number The value to search for
4✔
92
 * @param {number} length The upper position to search up to
4✔
93
 * @param {function(number)} get The getter function to retrieve the values at the specific position
4✔
94
 * @returns {Array<number>} An array of the low and high position that match the searched number
4✔
95
 */
4✔
96
function binarySearch(number, length, get) {
820✔
97
    let low = 1;
820✔
98
    let high = length;
820✔
99

820✔
100
    if (get(low) > number) {
820✔
101
        return [low, 0];
228✔
102
    }
228✔
103
    if (get(high) < number) {
820✔
104
        return [0, high];
216✔
105
    }
216✔
106

376✔
107
    while (low <= high) {
820✔
108
        const mid = low + ((high - low) >> 1);
872✔
109
        const value = get(mid);
872✔
110
        if (value === number) {
872✔
111
            return [mid, mid];
328✔
112
        }
328✔
113
        if (value < number) {
872✔
114
            low = mid + 1;
332✔
115
        } else {
872✔
116
            high = mid - 1;
212✔
117
        }
212✔
118
    }
872✔
119
    return [low, high];
48✔
120
}
820✔
121

4✔
122
/**
4✔
123
 * @param {number} index The 1-based index position to wrap around if < 0 and check against the bounds.
4✔
124
 * @param {number} length The length of the index and upper bound.
4✔
125
 * @returns {number} The wrapped index position or -1 if index out of bounds.
4✔
126
 */
4✔
127
function wrapAndCheck(index, length) {
5,248✔
128
    if (typeof index !== 'number') {
5,248✔
129
        return -1;
4✔
130
    }
4✔
131

5,244✔
132
    if (index < 0) {
5,248✔
133
        index += length + 1;
108✔
134
    }
108✔
135
    if (index < 1 || index > length) {
5,248✔
136
        return -1;
72✔
137
    }
72✔
138
    return index;
5,172✔
139
}
5,248✔
140

4✔
141
/**
4✔
142
 * Ensure that the given directory exists.
4✔
143
 * @param {string} dirName
4✔
144
 * @return {boolean} true if the directory existed already
4✔
145
 */
4✔
146
function ensureDirectory(dirName) {
6,010✔
147
    if (!fs.existsSync(dirName)) {
6,010✔
148
        try {
720✔
149
            mkdirpSync(dirName);
720✔
150
        } catch (e) {
720!
151
        }
×
152
        return false;
720✔
153
    }
720✔
154
    return true;
5,290✔
155
}
6,010✔
156

4✔
157
/**
4✔
158
 * Perform a k-way merge over multiple streams, invoking a callback for each item in ascending key order.
4✔
159
 * Each stream object is mutated in place by the `advance` function.
4✔
160
 *
4✔
161
 * @param {object[]} streams Array of stream state objects; entries are removed when exhausted.
4✔
162
 * @param {function(object): number} getKey Returns the current sort key for a stream state.
4✔
163
 * @param {function(object): boolean} advance Advances the stream to its next item.
4✔
164
 *   Returns true if the stream has more items within range, false if exhausted.
4✔
165
 * @param {function(object): void} visit Called for each stream state in merged order.
4✔
166
 */
4✔
167
function kWayMerge(streams, getKey, advance, visit) {
64✔
168
    while (streams.length > 0) {
64✔
169
        let minIdx = 0;
228✔
170
        for (let i = 1; i < streams.length; i++) {
228✔
171
            if (getKey(streams[i]) < getKey(streams[minIdx])) {
212✔
172
                minIdx = i;
104✔
173
            }
104✔
174
        }
212✔
175
        visit(streams[minIdx]);
228✔
176
        if (!advance(streams[minIdx])) {
228✔
177
            streams.splice(minIdx, 1);
92✔
178
        }
92✔
179
    }
228✔
180
}
64✔
181

4✔
182
/**
4✔
183
 * Scan a directory (and its subdirectories) for files whose relative paths match a regex pattern,
4✔
184
 * calling a callback for each match.
4✔
185
 *
4✔
186
 * The regex is matched against the **relative path from `directory`** (e.g. `eventstore.stream-x/foo.index`),
4✔
187
 * so patterns that capture a path prefix work transparently for both flat and nested layouts.
4✔
188
 *
4✔
189
 * The `onEach` callback receives the first capturing group of the match (`match[1]`), or the full
4✔
190
 * match (`match[0]`) when no capturing group is defined in the pattern.
4✔
191
 *
4✔
192
 * @param {string} directory The root directory to scan.
4✔
193
 * @param {RegExp} regexPattern The pattern to match relative file paths against.
4✔
194
 * @param {function(string)} onEach Called with the first capturing group (or full match) for each matching path.
4✔
195
 * @param {function(Error?)} onDone Called when the scan is complete, or with an error if one occurred.
4✔
196
 */
4✔
197
function scanForFiles(directory, regexPattern, onEach, onDone) {
1,527✔
198
    function scan(dir, relativePrefix, isRoot, done) {
1,527✔
199
        fs.readdir(dir, { withFileTypes: true }, (err, entries) => {
3,542✔
200
            if (err) {
3,542✔
201
                // For non-root subdirectories, silently skip if they have disappeared
239✔
202
                // (e.g. a transient lock directory that was removed while scanning).
239✔
203
                /* istanbul ignore next */
239✔
204
                if (!isRoot && err.code === 'ENOENT') return done(null);
239✔
205
                return done(err);
8✔
206
            }
8✔
207
            const subdirs = [];
3,303✔
208
            for (let entry of entries) {
3,542✔
209
                if (entry.isDirectory()) {
5,201✔
210
                    subdirs.push(entry.name);
2,015✔
211
                } else {
5,201✔
212
                    const relativePath = relativePrefix + entry.name;
3,186✔
213
                    const match = relativePath.match(regexPattern);
3,186✔
214
                    if (match !== null) {
3,186✔
215
                        onEach(match[1] !== undefined ? match[1] : match[0]);
1,101✔
216
                    }
1,101✔
217
                }
3,186✔
218
            }
5,201✔
219
            let i = 0;
3,303✔
220
            function next() {
3,303✔
221
                if (i >= subdirs.length) return done(null);
5,318✔
222
                const name = subdirs[i++];
2,015✔
223
                scan(path.join(dir, name), relativePrefix + name + '/', false, (err) => {
2,015✔
224
                    if (err) return done(err);
2,015!
225
                    next();
2,015✔
226
                });
2,015✔
227
            }
5,318✔
228
            next();
3,303✔
229
        });
3,542✔
230
    }
3,542✔
231

1,527✔
232
    scan(directory, '', true, onDone);
1,527✔
233
}
1,527✔
234

4✔
235

4✔
236
/**
4✔
237
 * Synchronously scan a directory (and its subdirectories) for files whose relative paths match a
4✔
238
 * regex pattern, calling a callback for each match.
4✔
239
 *
4✔
240
 * Behaves identically to {@link scanForFiles} but uses synchronous fs calls, making it suitable
4✔
241
 * for use during object construction.
4✔
242
 *
4✔
243
 * @param {string} directory The root directory to scan.
4✔
244
 * @param {RegExp} regexPattern The pattern to match relative file paths against.
4✔
245
 * @param {function(string)} onEach Called with the first capturing group (or full match) for each matching path.
4✔
246
 */
4✔
UNCOV
247
function scanForFilesSync(directory, regexPattern, onEach) {
×
UNCOV
248
    function scan(dir, relativePrefix) {
×
UNCOV
249
        const entries = fs.readdirSync(dir, { withFileTypes: true });
×
UNCOV
250
        for (let entry of entries) {
×
UNCOV
251
            if (entry.isDirectory()) {
×
UNCOV
252
                scan(path.join(dir, entry.name), relativePrefix + entry.name + '/');
×
UNCOV
253
            } else {
×
UNCOV
254
                const relativePath = relativePrefix + entry.name;
×
UNCOV
255
                const match = relativePath.match(regexPattern);
×
UNCOV
256
                if (match !== null) {
×
UNCOV
257
                    onEach(match[1] !== undefined ? match[1] : match[0]);
×
UNCOV
258
                }
×
UNCOV
259
            }
×
UNCOV
260
        }
×
UNCOV
261
    }
×
UNCOV
262

×
UNCOV
263
    scan(directory, '');
×
UNCOV
264
}
×
265

4✔
266

4✔
267
/**
4✔
268
 * Read a scalar value at a dot-notation path from an object.
4✔
269
 * Returns `undefined` if any path segment is absent or an intermediate value is not an object.
4✔
270
 *
4✔
271
 * @param {object} obj
4✔
272
 * @param {string} dotPath Dot-separated property path, e.g. `'payload.type'`.
4✔
273
 * @returns {*}
4✔
274
 */
4✔
275
function getPropertyAtPath(obj, dotPath) {
7,732✔
276
    let current = obj;
7,732✔
277
    const parts = dotPath.split('.');
7,732✔
278
    for (const part of parts) {
7,732✔
279
        if (current == null || typeof current !== 'object') return undefined;
11,176✔
280
        current = current[part];
8,988✔
281
    }
8,988✔
282
    return current;
5,544✔
283
}
7,732✔
284

4✔
285
export {
4✔
286
    assert,
4✔
287
    assertEqual,
4✔
288
    hash,
4✔
289
    wrapAndCheck,
4✔
290
    binarySearch,
4✔
291
    buildMetadataHeader,
4✔
292
    alignTo,
4✔
293
    ensureDirectory,
4✔
294
    scanForFiles,
4✔
295
    scanForFilesSync,
4✔
296
    kWayMerge,
4✔
297
    getPropertyAtPath
4✔
298
};
4✔
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