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

albe / node-event-storage / 27492865541

14 Jun 2026 08:10AM UTC coverage: 98.686% (-0.1%) from 98.787%
27492865541

Pull #322

github

web-flow
Merge 128175f25 into 388cb5d1f
Pull Request #322: fix: resolve race condition crashing read-only live-watch reader

1355 of 1394 branches covered (97.2%)

Branch coverage included in aggregate %.

132 of 139 new or added lines in 2 files covered. (94.96%)

29 existing lines in 5 files now uncovered.

6684 of 6752 relevant lines covered (98.99%)

799.85 hits per line

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

98.76
/src/Storage/ReadableStorage.js
1
import fs from 'fs';
4✔
2
import path from 'path';
4✔
3
import events from 'events';
4✔
4
import { ReadOnly as ReadOnlyPartition } from '../Partition.js';
4✔
5
import { ReadOnly as ReadOnlyIndex } from '../Index.js';
4✔
6
import { assert, wrapAndCheck, iterate, kWayMerge } from '../utils/util.js';
4✔
7
import { scanForFiles } from '../utils/fsUtil.js';
4✔
8
import { createHmac, matches, buildMetadataForMatcher } from '../utils/metadataUtil.js';
4✔
9
import { normalizeNamedCtorArgs } from '../utils/apiHelpers.js';
4✔
10
import IndexMatcher from '../IndexMatcher.js';
4✔
11
import PartitionPool from '../PartitionPool.js';
4✔
12

4✔
13
const DEFAULT_READ_BUFFER_SIZE = 4 * 1024;
4✔
14

4✔
15
/**
4✔
16
 * Default ordered list of document property paths used as discriminant keys when
4✔
17
 * classifying object matchers into the fast-lookup table.  Each path may use
4✔
18
 * dot-notation for nested access (e.g. `'payload.type'`).  The first path that
4✔
19
 * resolves to a scalar value in a given matcher wins; remaining paths are not
4✔
20
 * examined for that matcher.
4✔
21
 */
4✔
22
const DEFAULT_MATCHER_PROPERTIES = ['stream', 'payload.type'];
4✔
23

4✔
24
/**
4✔
25
 * Default maximum number of partition file descriptors kept open simultaneously.
4✔
26
 * Partitions beyond this limit are evicted using LRU order. 0 disables the limit.
4✔
27
 */
4✔
28
const DEFAULT_MAX_OPEN_PARTITIONS = 1024;
4✔
29

4✔
30
/**
4✔
31
 * @typedef {object|function(object):boolean} Matcher
4✔
32
 */
4✔
33

4✔
34
/**
4✔
35
 * An append-only storage with highly performant positional range scans.
4✔
36
 * It's highly optimized for an event-store and hence does not support compaction or data-rewrite, nor any querying
4✔
37
 */
4✔
38
class ReadableStorage extends events.EventEmitter {
4✔
39

4✔
40
    /**
4✔
41
     * @param {string} [storageName] The name of the storage.
4✔
42
     * @param {object} [config] An object with storage parameters.
4✔
43
     * @param {object} [config.serializer] A serializer object with methods serialize(document) and deserialize(data).
4✔
44
     * @param {function(object): string} config.serializer.serialize Default is JSON.stringify.
4✔
45
     * @param {function(string): object} config.serializer.deserialize Default is JSON.parse.
4✔
46
     * @param {string} [config.dataDirectory] The path where the storage data should reside. Default '.'.
4✔
47
     * @param {string} [config.indexDirectory] The path where the indexes should be stored. Defaults to dataDirectory.
4✔
48
     * @param {string} [config.indexFile] The name of the primary index. Default '{storageName}.index'.
4✔
49
     * @param {number} [config.readBufferSize] Size of the read buffer in bytes. Default 4096.
4✔
50
     * @param {object} [config.indexOptions] An options object that should be passed to all indexes on construction.
4✔
51
     * @param {string} [config.hmacSecret] A private key that is used to verify matchers retrieved from indexes.
4✔
52
     * @param {object} [config.metadata] A metadata object to be stored in all partitions belonging to this storage.
4✔
53
     * @param {string[]} [config.matcherProperties] Ordered list of document property paths (dot-notation) used as
4✔
54
     *   discriminant keys for the fast secondary-index lookup table. Only the first property that resolves to a scalar
4✔
55
     *   value inside a given object matcher is used; the rest are checked via the full `matches()` fallback.
4✔
56
     *   Default: `['stream', 'payload.type']`.
4✔
57
     * @param {number} [config.maxOpenPartitions] Maximum number of partition file descriptors kept open at one time.
4✔
58
     *   When the limit is reached the least-recently-used partition is closed to make room. 0 disables the limit.
4✔
59
     *   Default: 1024.
4✔
60
     */
4✔
61
    constructor(storageName = 'storage', config = {}) {
4✔
62
        super();
1,372✔
63
        ({ name: storageName, options: config } = normalizeNamedCtorArgs(storageName, config));
1,372✔
64

1,372✔
65
        this.storageFile = storageName || 'storage';
1,372✔
66
        const defaults = {
1,372✔
67
            serializer: { serialize: JSON.stringify, deserialize: JSON.parse },
1,372✔
68
            dataDirectory: '.',
1,372✔
69
            indexFile: this.storageFile + '.index',
1,372✔
70
            indexOptions: {},
1,372✔
71
            hmacSecret: '',
1,372✔
72
            metadata: {},
1,372✔
73
            matcherProperties: DEFAULT_MATCHER_PROPERTIES,
1,372✔
74
            maxOpenPartitions: DEFAULT_MAX_OPEN_PARTITIONS
1,372✔
75
        };
1,372✔
76
        config = Object.assign(defaults, config);
1,372✔
77
        this.serializer = config.serializer;
1,372✔
78

1,372✔
79
        this.hmac = createHmac(config.hmacSecret);
1,372✔
80

1,372✔
81
        this.dataDirectory = path.resolve(config.dataDirectory);
1,372✔
82

1,372✔
83
        const partitionDefaults = { readBufferSize: DEFAULT_READ_BUFFER_SIZE };
1,372✔
84
        this.partitionConfig = Object.assign(partitionDefaults, config);
1,372✔
85
        this.partitions = new PartitionPool(config.maxOpenPartitions);
1,372✔
86

1,372✔
87
        // initialized: null = not started (or scan cancelled), false = in progress, true = done
1,372✔
88
        this.initialized = null;
1,372✔
89

1,372✔
90
        this.initializeIndexes(config);
1,372✔
91
    }
1,372✔
92

4✔
93
    /**
4✔
94
     * @protected
4✔
95
     * @param {string} name
4✔
96
     * @param {object} [options]
4✔
97
     * @returns {{ index: ReadableIndex, matcher?: Matcher }}
4✔
98
     */
4✔
99
    createIndex(name, options = {}) {
4✔
100
        /** @type ReadableIndex */
176✔
101
        const index = new ReadOnlyIndex(name, options);
176✔
102
        return { index };
176✔
103
    }
176✔
104

4✔
105
    /**
4✔
106
     * @protected
4✔
107
     * @param {string} name
4✔
108
     * @param {object} [options]
4✔
109
     * @returns {ReadablePartition}
4✔
110
     */
4✔
111
    createPartition(name, options = {}) {
4✔
112
        return new ReadOnlyPartition(name, options);
72✔
113
    }
72✔
114

4✔
115
    /**
4✔
116
     * Create/open the primary index and build the base configuration for all secondary indexes.
4✔
117
     *
4✔
118
     * @private
4✔
119
     * @param {object} config The configuration object
4✔
120
     * @returns void
4✔
121
     */
4✔
122
    initializeIndexes(config) {
4✔
123
        this.indexDirectory = path.resolve(config.indexDirectory || this.dataDirectory);
1,372✔
124

1,372✔
125
        this.indexOptions = config.indexOptions;
1,372✔
126
        this.indexOptions.dataDirectory = this.indexDirectory;
1,372✔
127
        // Safety precaution to prevent accidentally restricting main index
1,372✔
128
        delete this.indexOptions.matcher;
1,372✔
129
        const { index } = this.createIndex(config.indexFile, this.indexOptions);
1,372✔
130
        this.index = index;
1,372✔
131
        this.secondaryIndexes = {};
1,372✔
132
        this.readonlyIndexes = {};
1,372✔
133

1,372✔
134
        /** Fast secondary-index lookup — classifies matchers for O(1) candidate resolution on write. */
1,372✔
135
        this.indexMatcher = new IndexMatcher(config.matcherProperties);
1,372✔
136
    }
1,372✔
137

4✔
138
    /**
4✔
139
     * The amount of documents in the storage.
4✔
140
     * @returns {number}
4✔
141
     */
4✔
142
    get length() {
4✔
143
        return this.index.length;
5,340✔
144
    }
5,340✔
145

4✔
146
    /**
4✔
147
     * Scan partitions and secondary index files; emit 'index-created' for each found index.
4✔
148
     * @param {function} done Called when both scans finish.
4✔
149
     */
4✔
150
    scanFiles(done) {
4✔
151
        const escaped = this.storageFile.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1,292✔
152
        const partitionPattern = new RegExp(`^(${escaped}.*)$`);
1,292✔
153
        scanForFiles(this.dataDirectory, partitionPattern, (file) => {
1,292✔
154
            if (file.endsWith('.index') || file.endsWith('.branch') || file.endsWith('.lock')) return;
959✔
155
            const partition = this.createPartition(file, this.partitionConfig);
307✔
156
            this.partitions.add(partition.id, partition);
307✔
157
        }, (partErr) => {
1,292✔
158
            /* c8 ignore next */
4✔
159
            if (partErr) throw partErr;
4✔
160

1,288✔
161
            // Scan was cancelled by close() between the two scan phases.
1,288✔
162
            if (this.initialized === null) return;
1,292✔
163

387✔
164
            // No secondary indexes exist yet — nothing to scan.
387✔
165
            if (!fs.existsSync(this.indexDirectory)) {
1,292!
UNCOV
166
                return done();
×
UNCOV
167
            }
×
168
            const indexPattern = new RegExp(`^${escaped}\\.(.+)\\.index$`);
387✔
169
            scanForFiles(this.indexDirectory, indexPattern, (name) => {
387✔
170
                this.emit('index-created', name);
164✔
171
            }, (indexErr) => {
387✔
172
                // The directory could disappear between existsSync and readdir (e.g. test cleanup).
387✔
173
                /* c8 ignore next */
4✔
174
                if (indexErr && indexErr.code !== 'ENOENT') throw indexErr;
4✔
175
                done();
387✔
176
            });
387✔
177
        });
1,292✔
178
    }
1,292✔
179

4✔
180
    /**
4✔
181
     * Only the primary index is opened eagerly; secondary indexes open on demand.
4✔
182
     *
4✔
183
     * @protected
4✔
184
     */
4✔
185
    openIndexes() {
4✔
186
        this.index.open();
254✔
187
    }
254✔
188

4✔
189
    /**
4✔
190
     * Open the storage; scans existing partitions and indexes asynchronously on first open.
4✔
191
     * Re-opens after `close()` are synchronous.
4✔
192
     * Will emit an `'opened'` event when finished.
4✔
193
     *
4✔
194
     * @api
4✔
195
     * @param {function(): void} [callback] Called after indexes open, before `'opened'` is emitted.
4✔
196
     *   Can be used as a synchronous alternative to listening to the `'opened'` event.
4✔
197
     * @returns {boolean}
4✔
198
     */
4✔
199
    open(callback) {
4✔
200
        if (this.initialized === true) {
1,304✔
201
            this.openIndexes();
8✔
202
            callback?.();
8✔
203
            this.emit('opened');
8✔
204
            return true;
8✔
205
        }
8✔
206
        if (this.initialized === false) {
1,304✔
207
            return true;
4✔
208
        }
4✔
209
        this.initialized = false;
1,292✔
210
        this.scanFiles(() => {
1,292✔
211
            // Guard: close() while scanning resets initialized to null.
387✔
212
            if (this.initialized === null) return;
387✔
213
            this.initialized = true;
246✔
214
            this.openIndexes();
246✔
215
            callback?.();
387✔
216
            this.emit('opened');
387✔
217
        });
1,292✔
218
        return true;
1,292✔
219
    }
1,304✔
220

4✔
221
    /**
4✔
222
     * Close the storage and free up all resources.
4✔
223
     * Will emit a 'closed' event when finished.
4✔
224
     *
4✔
225
     * @api
4✔
226
     */
4✔
227
    close() {
4✔
228
        // Cancel in-progress scan so the callback does not re-open after an explicit close.
1,996✔
229
        if (this.initialized === false) {
1,996✔
230
            this.initialized = null;
1,042✔
231
        }
1,042✔
232
        this.index.close();
1,996✔
233
        this.forEachSecondaryIndex(index => index.close());
1,996✔
234
        for (let index of Object.values(this.readonlyIndexes)) {
1,996✔
235
            index.close();
44✔
236
        }
44✔
237
        this.forEachPartition(partition => partition.close());
1,996✔
238
        this.emit('closed');
1,996✔
239
    }
1,996✔
240

4✔
241
    /**
4✔
242
     * Get a partition by its id.
4✔
243
     * If a partition with the given id does not exist, an error is thrown.
4✔
244
     *
4✔
245
     * @protected
4✔
246
     * @param {number|string} partitionIdentifier The partition Id
4✔
247
     * @returns {ReadablePartition}
4✔
248
     * @throws {Error} If no such partition exists.
4✔
249
     */
4✔
250
    getPartition(partitionIdentifier) {
4✔
251
        assert(this.partitions.has(partitionIdentifier), `Partition #${partitionIdentifier} does not exist.`);
6,172✔
252
        return this.partitions.open(partitionIdentifier);
6,172✔
253
    }
6,172✔
254

4✔
255
    /**
4✔
256
     * Register a handler that is called before a document is read from a partition.
4✔
257
     * The handler receives the position and the partition metadata and may throw to abort the read.
4✔
258
     * Multiple handlers can be registered; all run on every read in registration order.
4✔
259
     * Equivalent to `storage.on('preRead', hook)`.
4✔
260
     *
4✔
261
     * @api
4✔
262
     * @param {function(number, object): void} hook A function receiving (position, partitionMetadata).
4✔
263
     */
4✔
264
    preRead(hook) {
4✔
265
        this.on('preRead', hook);
12✔
266
    }
12✔
267

4✔
268
    /**
4✔
269
     * @protected
4✔
270
     * @param {number} partitionId The partition to read from.
4✔
271
     * @param {number} position The file position to read from.
4✔
272
     * @param {number} [size] The expected byte size of the document at the given position.
4✔
273
     * @param {boolean} [raw] Whether to return raw buffers instead of deserialized objects. Default false.
4✔
274
     * @param {boolean} [backwardsHint] If set to true, will optimize buffering for backwards reading.
4✔
275
     * @returns {object|{ buffer: Buffer, time64: number, sequenceNumber: number }} The document stored at the given position.
4✔
276
     * @throws {Error} if the document at the given position can not be deserialized.
4✔
277
     */
4✔
278
    readFrom(partitionId, position, size, raw = false, backwardsHint = false) {
4✔
279
        const partition = this.getPartition(partitionId);
2,620✔
280
        if (this.listenerCount('preRead') > 0) {
2,620✔
281
            this.emit('preRead', position, partition.metadata);
64✔
282
        }
64✔
283
        const headerOut = {};
2,612✔
284
        const buffer = partition.readFrom(position, size, headerOut, backwardsHint);
2,612✔
285
        return raw ? { buffer, time64: headerOut.time64, sequenceNumber: headerOut.sequenceNumber } : this.serializer.deserialize(buffer.toString('utf8'));
2,620✔
286
    }
2,620✔
287

4✔
288
    /**
4✔
289
     * Read a single document from the given position, in the full index or in the provided index.
4✔
290
     *
4✔
291
     * @api
4✔
292
     * @param {number} number The 1-based document number (inside the given index) to read.
4✔
293
     * @param {ReadableIndex} [index] The index to use for finding the document position.
4✔
294
     * @returns {object} The document at the given position inside the index.
4✔
295
     */
4✔
296
    read(number, index) {
4✔
297
        index = index || this.index;
412✔
298
        index.open();
412✔
299

412✔
300
        const entry = index.get(number);
412✔
301
        if (entry === false) {
412✔
302
            return false;
4✔
303
        }
4✔
304

408✔
305
        return this.readFrom(entry.partition, entry.position, entry.size);
408✔
306
    }
412✔
307

4✔
308
    /**
4✔
309
     * Read a range of documents from the given position range, in the full index or in the provided index.
4✔
310
     * Returns a generator in order to reduce memory usage and be able to read lots of documents with little latency.
4✔
311
     *
4✔
312
     * @api
4✔
313
     * @param {number} from The 1-based document number (inclusive) to start reading from.
4✔
314
     * @param {number} [until] The 1-based document number (inclusive) to read until. Defaults to index.length.
4✔
315
     * @param {ReadableIndex|false} [index] The index to use for finding the documents in the range.
4✔
316
     *   Pass `false` to skip the global index and iterate all partitions directly in sequenceNumber order
4✔
317
     *   (useful when the global index is unavailable or corrupted).
4✔
318
     * @param {boolean} [raw] Whether to return raw buffers instead of deserialized objects. Default false.
4✔
319
     * @returns {Generator<object>} A generator that will read each document in the range one by one.
4✔
320
     */
4✔
321
    *readRange(from, until = -1, index = null, raw = false) {
4✔
322
        let length = Number.MAX_SAFE_INTEGER;
824✔
323
        if (index !== false) {
824✔
324
            index = index || this.index;
812✔
325
            index.open();
812✔
326
            length = index.length;
812✔
327
        }
812✔
328

824✔
329
        const readFrom = wrapAndCheck(from, length);
824✔
330
        const readUntil = wrapAndCheck(until, length);
824✔
331
        assert(readFrom > 0 && readUntil > 0, `Range scan error for range ${from} - ${until}.`);
824✔
332

824✔
333
        yield* this.iterateRange(readFrom, readUntil, index, raw);
824✔
334
    }
824✔
335

4✔
336
    /**
4✔
337
     * Iterate all documents in this storage in range from to until inside the index.
4✔
338
     * If index is false, iterates all partitions directly in sequenceNumber order.
4✔
339
     * @private
4✔
340
     * @param {number} from
4✔
341
     * @param {number} until
4✔
342
     * @param {ReadableIndex|false|null} index
4✔
343
     * @param {boolean} [raw] Whether to return raw buffers instead of deserialized objects. Default false.
4✔
344
     * @returns {Generator<object>}
4✔
345
     */
4✔
346
    *iterateRange(from, until, index, raw = false) {
4✔
347
        if (index === false) {
808✔
348
            for (const { document } of this.iterateDocumentsNoIndex(from - 1, until - 1)) {
12✔
349
                yield document;
92✔
350
            }
92✔
351
            return;
12✔
352
        }
12✔
353

796✔
354
        const idx = index || this.index;
808!
355
        const forwards = from <= until;
808✔
356
        const lo = Math.min(from, until);
808✔
357
        const hi = Math.max(from, until);
808✔
358
        const entries = idx.range(lo, hi);
808✔
359
        if (!entries) return;
808!
360
        for (const entry of iterate(entries, forwards)) {
808✔
361
            yield this.readFrom(entry.partition, entry.position, entry.size, raw, !forwards);
2,152✔
362
        }
2,116✔
363
    }
808✔
364

4✔
365
    /**
4✔
366
     * Open an existing readonly index for reading, without registering it in the secondary indexes write path.
4✔
367
     * Use this for indexes whose files carry a status marker (e.g. `stream-foo.closed.index`).
4✔
368
     *
4✔
369
     * @api
4✔
370
     * @param {string} name The readonly index name (e.g. 'stream-foo.closed').
4✔
371
     * @returns {ReadableIndex}
4✔
372
     * @throws {Error} if the readonly index does not exist.
4✔
373
     */
4✔
374
    openReadonlyIndex(name) {
4✔
375
        if (name in this.readonlyIndexes) {
44!
UNCOV
376
            return this.readonlyIndexes[name];
×
UNCOV
377
        }
×
378
        const indexName = this.storageFile + '.' + name + '.index';
44✔
379
        assert(fs.existsSync(path.join(this.indexDirectory, indexName)), `Index "${name}" does not exist.`);
44✔
380
        const { index } = this.createIndex(indexName, Object.assign({}, this.indexOptions));
44✔
381
        index.open();
44✔
382
        this.readonlyIndexes[name] = index;
44✔
383
        return index;
44✔
384
    }
44✔
385

4✔
386
    /**
4✔
387
     * Open an existing index.
4✔
388
     *
4✔
389
     * @api
4✔
390
     * @param {string} name The index name.
4✔
391
     * @param {Matcher} [matcher] The matcher object or function that the index needs to have been defined with. If not given it will not be validated.
4✔
392
     * @returns {ReadableIndex}
4✔
393
     * @throws {Error} if the index with that name does not exist.
4✔
394
     * @throws {Error} if the HMAC for the matcher does not match.
4✔
395
     */
4✔
396
    openIndex(name, matcher) {
4✔
397
        if (name === '_all') {
1,256✔
398
            return this.index;
12✔
399
        }
12✔
400
        if (name in this.secondaryIndexes) {
1,256✔
401
            return this.secondaryIndexes[name].index;
1,116✔
402
        }
1,116✔
403

128✔
404
        const indexName = this.storageFile + '.' + name + '.index';
128✔
405
        assert(fs.existsSync(path.join(this.indexDirectory, indexName)), `Index "${name}" does not exist.`);
128✔
406

128✔
407
        const metadata = buildMetadataForMatcher(matcher, this.hmac);
128✔
408
        let { index } = this.secondaryIndexes[name] = this.createIndex(indexName, Object.assign({}, this.indexOptions, { metadata }));
128✔
409

128✔
410
        // Register the actual stored matcher (may have been reconstructed from metadata by WritableStorage.createIndex).
128✔
411
        this.indexMatcher.add(name, this.secondaryIndexes[name].matcher);
128✔
412

128✔
413
        index.open();
128✔
414
        return index;
128✔
415
    }
1,256✔
416

4✔
417
    /**
4✔
418
     * Remove a secondary index from the write path and the matcher lookup table.
4✔
419
     *
4✔
420
     * @api
4✔
421
     * @param {string} name The secondary index name to remove.
4✔
422
     */
4✔
423
    removeSecondaryIndex(name) {
4✔
424
        const entry = this.secondaryIndexes[name];
44✔
425
        if (entry) {
44✔
426
            this.indexMatcher.remove(name);
40✔
427
            delete this.secondaryIndexes[name];
40✔
428
        }
40✔
429
    }
44✔
430

4✔
431
     /**
4✔
432
      * Build the standard document result entry from a readRange yield.
4✔
433
      * @private
4✔
434
      * @param {{ data: Buffer, entry: { number: number, position: number, size: number, partition: number } }} [readItem]
4✔
435
      */
4✔
436
     buildDocumentEntry(readItem) {
4✔
437
         return {
228✔
438
             document: this.serializer.deserialize(readItem.data.toString('utf8')),
228✔
439
             // Replicate the index entry structure here, so iteration can be used easily to reindex
228✔
440
             entry: readItem.entry
228✔
441
         };
228✔
442
     }
228✔
443

4✔
444
     /**
4✔
445
      * Iterate documents across all partitions in sequenceNumber order using a k-way merge.
4✔
446
      * Opens any closed partition automatically.
4✔
447
      *
4✔
448
      * @protected
4✔
449
      * @param {number} [from=0] The 0-based sequenceNumber to start from (inclusive).
4✔
450
      * @param {number} [until=Number.MAX_SAFE_INTEGER] The 0-based sequenceNumber to read until (inclusive).
4✔
451
      * @returns {Generator<{document: object, entry: { sequenceNumber: number, position: number, size: number, partition: number }}>}
4✔
452
      */
4✔
453
     *iterateDocumentsNoIndex(from = 0, until = Number.MAX_SAFE_INTEGER) {
4✔
454
         const forwards = from <= until;
64✔
455
         const partitions = [];
64✔
456
         this.forEachPartition(partition => {
64✔
457
             partition.open();
108✔
458
             partitions.push(partition.readRange(from, until));
108✔
459
         });
64✔
460

64✔
461
         yield* kWayMerge(
64✔
462
             partitions,
64✔
463
             item => item.entry.number,
64✔
464
             forwards,
64✔
465
             item => this.buildDocumentEntry(item)
64✔
466
         );
64✔
467
     }
64✔
468

4✔
469
    /**
4✔
470
     * Helper method to iterate over all documents, invoking a callback for each one.
4✔
471
     * Pass `noIndex = true` to iterate all partitions directly in sequenceNumber order
4✔
472
     * (useful when the global index is unavailable or corrupted).
4✔
473
     * When `noIndex` is false the second callback argument is the raw index `EntryInterface`.
4✔
474
     * When `noIndex` is true the second callback argument has `{ partition, position, size, sequenceNumber }`.
4✔
475
     *
4✔
476
     * @protected
4✔
477
     * @param {function(object, object): void} iterationHandler
4✔
478
     * @param {boolean} [noIndex=false] When true, bypasses the index and iterates partitions directly.
4✔
479
     */
4✔
480
    forEachDocument(iterationHandler, noIndex = false) {
4✔
481
        /* c8 ignore next 3  */
4✔
482
        if (typeof iterationHandler !== 'function') {
4✔
483
            return;
4✔
484
        }
4✔
485

492✔
486
        if (noIndex) {
492✔
487
            for (const { document, entry } of this.iterateDocumentsNoIndex()) {
4✔
488
                iterationHandler(document, entry);
24✔
489
            }
24✔
490
            return;
4✔
491
        }
4✔
492

488✔
493
        const entries = this.index.all();
488✔
494

488✔
495
        for (let entry of entries) {
492✔
496
            const document = this.readFrom(entry.partition, entry.position, entry.size);
40✔
497
            iterationHandler(document, entry);
40✔
498
        }
40✔
499
    }
492✔
500

4✔
501
    /**
4✔
502
     * Helper method to iterate over all secondary indexes.
4✔
503
     *
4✔
504
     * When `matchDocument` is provided, `this.indexMatcher.forEachMatch()` is used to
4✔
505
     * efficiently find only the matching indexes via the discriminant lookup table.
4✔
506
     *
4✔
507
     * @protected
4✔
508
     * @param {function(ReadableIndex, string)} iterationHandler
4✔
509
     * @param {object} [matchDocument] If supplied, only indexes the document matches on will be iterated.
4✔
510
     */
4✔
511
    forEachSecondaryIndex(iterationHandler, matchDocument) {
4✔
512
        /* c8 ignore next 3  */
4✔
513
        if (typeof iterationHandler !== 'function') {
4✔
514
            return;
4✔
515
        }
4✔
516

5,868✔
517
        if (!matchDocument) {
5,868✔
518
            // No document filter: iterate all secondary indexes unconditionally.
2,292✔
519
            for (const indexName of Object.keys(this.secondaryIndexes)) {
2,292✔
520
                iterationHandler(this.secondaryIndexes[indexName].index, indexName);
1,768✔
521
            }
1,768✔
522
            return;
2,292✔
523
        }
2,292✔
524

3,576✔
525
        this.indexMatcher.forEachMatch(matchDocument, indexName => {
3,576✔
526
            iterationHandler(this.secondaryIndexes[indexName].index, indexName);
2,116✔
527
        });
3,576✔
528
    }
5,868✔
529

4✔
530
    /**
4✔
531
     * Helper method to iterate over all partitions.
4✔
532
     *
4✔
533
     * @protected
4✔
534
     * @param {function(ReadablePartition)} iterationHandler
4✔
535
     */
4✔
536
    forEachPartition(iterationHandler) {
4✔
537
        /* c8 ignore next 3  */
4✔
538
        if (typeof iterationHandler !== 'function') {
4✔
539
            return;
4✔
540
        }
4✔
541

2,328✔
542
        this.partitions.forEach(iterationHandler);
2,328✔
543
    }
2,328✔
544

4✔
545
}
4✔
546

4✔
547
export default ReadableStorage;
4✔
548
export { matches };
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