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

albe / node-event-storage / 23897478107

02 Apr 2026 11:08AM UTC coverage: 98.151% (+0.1%) from 98.054%
23897478107

push

github

web-flow
Merge pull request #257 from albe/copilot/v0x-move-to-node-module-api

Move to ES Modules (ESM)

890 of 929 branches covered (95.8%)

Branch coverage included in aggregate %.

118 of 118 new or added lines in 21 files covered. (100.0%)

55 existing lines in 12 files now uncovered.

4684 of 4750 relevant lines covered (98.61%)

787.62 hits per line

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

98.6
/src/Storage/WritableStorage.js
1
import fs from 'fs';
4✔
2
import path from 'path';
4✔
3
import WritablePartition from '../Partition/WritablePartition.js';
4✔
4
import WritableIndex, { Entry as WritableIndexEntry } from '../Index/WritableIndex.js';
4✔
5
import ReadableStorage from './ReadableStorage.js';
4✔
6
import { assert, ensureDirectory } from '../util.js';
4✔
7
import { matches, buildMetadataForMatcher, buildMatcherFromMetadata } from '../metadataUtil.js';
4✔
8

4✔
9
const DEFAULT_WRITE_BUFFER_SIZE = 16 * 1024;
4✔
10

4✔
11
const LOCK_RECLAIM = 0x1;
4✔
12
const LOCK_THROW = 0x2;
4✔
13

4✔
14
class StorageLockedError extends Error {}
4✔
15

4✔
16
/**
4✔
17
 * @typedef {object|function(object):boolean} Matcher
4✔
18
 */
4✔
19

4✔
20
/**
4✔
21
 * An append-only storage with highly performant positional range scans.
4✔
22
 * It's highly optimized for an event-store and hence does not support compaction or data-rewrite, nor any querying
4✔
23
 */
4✔
24
class WritableStorage extends ReadableStorage {
4✔
25

4✔
26
    /**
4✔
27
     * @param {string} [storageName] The name of the storage.
4✔
28
     * @param {object} [config] An object with storage parameters.
4✔
29
     * @param {object} [config.serializer] A serializer object with methods serialize(document) and deserialize(data).
4✔
30
     * @param {function(object): string} config.serializer.serialize Default is JSON.stringify.
4✔
31
     * @param {function(string): object} config.serializer.deserialize Default is JSON.parse.
4✔
32
     * @param {string} [config.dataDirectory] The path where the storage data should reside. Default '.'.
4✔
33
     * @param {string} [config.indexDirectory] The path where the indexes should be stored. Defaults to dataDirectory.
4✔
34
     * @param {string} [config.indexFile] The name of the primary index. Default '{storageName}.index'.
4✔
35
     * @param {number} [config.readBufferSize] Size of the read buffer in bytes. Default 4096.
4✔
36
     * @param {number} [config.writeBufferSize] Size of the write buffer in bytes. Default 16384.
4✔
37
     * @param {number} [config.maxWriteBufferDocuments] How many documents to have in the write buffer at max. 0 means as much as possible. Default 0.
4✔
38
     * @param {boolean} [config.syncOnFlush] If fsync should be called on write buffer flush. Set this if you need strict durability. Defaults to false.
4✔
39
     * @param {boolean} [config.dirtyReads] If dirty reads should be allowed. This means that writes that are in write buffer but not yet flushed can be read. Defaults to true.
4✔
40
     * @param {function(object, number): string} [config.partitioner] A function that takes a document and sequence number and returns a partition name that the document should be stored in. Defaults to write all documents to the primary partition.
4✔
41
     * @param {object} [config.indexOptions] An options object that should be passed to all indexes on construction.
4✔
42
     * @param {string} [config.hmacSecret] A private key that is used to verify matchers retrieved from indexes.
4✔
43
     * @param {number} [config.lock] One of LOCK_* constants that defines how an existing lock should be handled.
4✔
44
     */
4✔
45
    constructor(storageName = 'storage', config = {}) {
4✔
46
        if (typeof storageName !== 'string') {
908✔
47
            config = storageName;
536✔
48
            storageName = undefined;
536✔
49
        }
536✔
50
        const defaults = {
908✔
51
            partitioner: (document, number) => '',
908✔
52
            writeBufferSize: DEFAULT_WRITE_BUFFER_SIZE,
908✔
53
            maxWriteBufferDocuments: 0,
908✔
54
            syncOnFlush: false,
908✔
55
            dirtyReads: true,
908✔
56
            dataDirectory: '.'
908✔
57
        };
908✔
58
        config = Object.assign(defaults, config);
908✔
59
        config.indexOptions = Object.assign({ syncOnFlush: config.syncOnFlush }, config.indexOptions);
908✔
60
        ensureDirectory(config.dataDirectory);
908✔
61
        super(storageName, config);
908✔
62

908✔
63
        this.lockFile = path.resolve(this.dataDirectory, this.storageFile + '.lock');
908✔
64
        if (config.lock === LOCK_RECLAIM) {
908✔
65
            this.unlock();
12✔
66
        }
12✔
67
        this.partitioner = config.partitioner;
908✔
68
    }
908✔
69

4✔
70
    /**
4✔
71
     * @inheritDoc
4✔
72
     * @returns {boolean}
4✔
73
     * @throws {StorageLockedError} If this storage is locked by another process.
4✔
74
     */
4✔
75
    open() {
4✔
76
        if (!this.lock()) {
864✔
77
            return true;
4✔
78
        }
4✔
79
        const result = super.open();
856✔
80
        this.emit('ready');
856✔
81
        return result;
856✔
82
    }
864✔
83

4✔
84
    /**
4✔
85
     * Helper method to iterate over all writable secondary indexes.
4✔
86
     * Opens each index before calling the callback (passing the previous open status),
4✔
87
     * and closes it afterwards if it was not already open.
4✔
88
     *
4✔
89
     * @protected
4✔
90
     * @param {function(WritableIndex, string, boolean)} iterationHandler Called with (index, name, wasOpen).
4✔
91
     * @param {object} [matchDocument] If supplied, only indexes the document matches on will be iterated.
4✔
92
     */
4✔
93
    forEachWritableSecondaryIndex(iterationHandler, matchDocument) {
4✔
94
        this.forEachSecondaryIndex((index, name) => {
224✔
95
            /* istanbul ignore if */
60✔
96
            if (!(index instanceof WritableIndex)) return;
60!
97
            const wasOpen = index.isOpen();
60✔
98
            if (!wasOpen) index.open();
60✔
99
            iterationHandler(index, name, wasOpen);
60✔
100
            if (!wasOpen) index.close();
60✔
101
        }, matchDocument);
224✔
102
    }
224✔
103

4✔
104
    /**
4✔
105
     * Scan every partition's last document to detect torn writes inline.
4✔
106
     * A document is torn when its expected end exceeds the actual file size:
4✔
107
     *   position + documentWriteSize(dataSize) > partition.size
4✔
108
     *
4✔
109
     * @private
4✔
110
     * @returns {{ lastValidSequenceNumber: number, maxPartitionSequenceNumber: number }}
4✔
111
     */
4✔
112
    findTornWriteBoundary() {
4✔
113
        let lastValidSequenceNumber = Number.MAX_SAFE_INTEGER;
28✔
114
        let maxPartitionSequenceNumber = -1;
28✔
115
        this.forEachPartition(partition => {
28✔
116
            partition.open();
32✔
117
            const last = partition.readLast();
32✔
118
            /* istanbul ignore if */
32✔
119
            if (!last) return;
32!
120
            const { header: { sequenceNumber, dataSize }, position } = last;
32✔
121
            if (position + partition.documentWriteSize(dataSize) > partition.size) {
32✔
122
                // Torn write: the document extends beyond the end of the file.
16✔
123
                lastValidSequenceNumber = Math.min(lastValidSequenceNumber, sequenceNumber);
16✔
124
            } else {
16✔
125
                maxPartitionSequenceNumber = Math.max(maxPartitionSequenceNumber, sequenceNumber);
16✔
126
            }
16✔
127
        });
28✔
128
        return { lastValidSequenceNumber, maxPartitionSequenceNumber };
28✔
129
    }
28✔
130

4✔
131
    /**
4✔
132
     * Check all partitions for torn writes, physically repair each partition, truncate all indexes
4✔
133
     * to the torn-write boundary, and then reindex to rebuild any missing index entries.
4✔
134
     *
4✔
135
     * A document is torn when the partition file ends before the document's expected end position
4✔
136
     * (i.e. position + documentWriteSize(dataSize) > partition.size).  Detected inline in
4✔
137
     * findTornWriteBoundary(), without any checkTornWrite() call.
4✔
138
     *
4✔
139
     * Repair flow:
4✔
140
     * 1. findTornWriteBoundary() reads the last document of every partition and finds the global
4✔
141
     *    torn-write boundary (minimum torn sequence number across all partitions).
4✔
142
     * 2. If torn writes were found, truncateAfterSequence() removes all documents at or beyond
4✔
143
     *    the boundary from each partition.
4✔
144
     * 3. Truncate all indexes to the torn-write boundary, then reindex to fill any lagging entries.
4✔
145
     * 4. If no torn writes were found but the index is lagging, reindex directly.
4✔
146
     */
4✔
147
    checkTornWrites() {
4✔
148
        const { lastValidSequenceNumber, maxPartitionSequenceNumber } = this.findTornWriteBoundary();
28✔
149

28✔
150
        if (lastValidSequenceNumber < Number.MAX_SAFE_INTEGER) {
28✔
151
            // Phase 2: remove all documents at or beyond the torn-write boundary from each partition.
16✔
152
            this.forEachPartition(partition => {
16✔
153
                partition.open();
20✔
154
                partition.truncateAfterSequence(lastValidSequenceNumber - 1);
20✔
155
            });
16✔
156

16✔
157
            // Truncate all indexes to the torn-write boundary.
16✔
158
            this.index.open();
16✔
159
            this.index.truncate(lastValidSequenceNumber);
16✔
160
            /* istanbul ignore next */
16✔
161
            this.forEachWritableSecondaryIndex(index => {
16✔
UNCOV
162
                index.truncate(index.find(lastValidSequenceNumber));
×
163
            });
16✔
164

16✔
165
            // Reindex to fill in any missing complete-document entries.
16✔
166
            this.reindex(this.index.length);
16✔
167
        } else if (maxPartitionSequenceNumber >= 0 && maxPartitionSequenceNumber + 1 > this.index.length) {
28✔
168
            // No torn writes, but the index is lagging — repair it.
8✔
169
            this.reindex(this.index.length);
8✔
170
        }
8✔
171

28✔
172
        this.forEachPartition(partition => partition.close());
28✔
173
    }
28✔
174

4✔
175
    /**
4✔
176
     * Rebuild the primary index and all loaded secondary indexes starting from the given sequence
4✔
177
     * number by scanning the partition data directly.
4✔
178
     * This is the building block for both auto-repair (invoked automatically when the primary
4✔
179
     * index is found to be lagging in checkTornWrites()) and for user-driven re-indexing after
4✔
180
     * index corruption.
4✔
181
     *
4✔
182
     * @api
4✔
183
     * @param {number} [fromSequenceNumber=0] The number of primary index entries to keep intact.
4✔
184
     *   All index entries beyond this position will be removed and rebuilt from partition data.
4✔
185
     *   Defaults to 0, which rebuilds all indexes from scratch.
4✔
186
     */
4✔
187
    reindex(fromSequenceNumber = 0) {
4✔
188
        this.index.truncate(fromSequenceNumber);
48✔
189

48✔
190
        // Truncate all loaded secondary indexes to match the new primary length.
48✔
191
        this.forEachWritableSecondaryIndex(index => {
48✔
192
            // find(0) returns 0, so truncate(0) will remove all entries when fromSequenceNumber===0
8✔
193
            index.truncate(fromSequenceNumber === 0 ? 0 : index.find(fromSequenceNumber));
8✔
194
        });
48✔
195

48✔
196
        // Scan partitions in sequence-number order and rebuild index entries.
48✔
197
        // iterateDocumentsNoIndex opens any closed partitions automatically.
48✔
198
        for (const { document, partition, position, size } of this.iterateDocumentsNoIndex(fromSequenceNumber, Number.MAX_SAFE_INTEGER)) {
48✔
199
            const newEntry = new WritableIndexEntry(this.index.length + 1, position, size, partition);
112✔
200
            this.index.add(newEntry);
112✔
201

112✔
202
            this.forEachWritableSecondaryIndex((secIndex) => {
112✔
203
                secIndex.add(newEntry);
20✔
204
            }, document);
112✔
205
        }
112✔
206

48✔
207
        this.flush();
48✔
208
    }
48✔
209

4✔
210
    /**
4✔
211
     * Attempt to lock this storage by means of a lock directory.
4✔
212
     * @returns {boolean} True if the lock was created or false if the lock is already in place.
4✔
213
     * @throws {StorageLockedError} If this storage is already locked by another process.
4✔
214
     * @throws {Error} If the lock could not be created.
4✔
215
     */
4✔
216
    lock() {
4✔
217
        if (this.locked) {
864✔
218
            return false;
4✔
219
        }
4✔
220
        try {
860✔
221
            fs.mkdirSync(this.lockFile);
860✔
222
            this.locked = true;
860✔
223
        } catch (e) {
864✔
224
            /* istanbul ignore if */
4✔
225
            if (e.code !== 'EEXIST') {
4!
UNCOV
226
                throw new Error(`Error creating lock for storage ${this.storageFile}: ` + e.message);
×
UNCOV
227
            }
×
228
            throw new StorageLockedError(`Storage ${this.storageFile} is locked by another process`);
4✔
229
        }
4✔
230
        return true;
856✔
231
    }
864✔
232

4✔
233
    /**
4✔
234
     * Unlock this storage, no matter if it was previously locked by this writer.
4✔
235
     * Only use this if you are sure there is no other process still having a writer open.
4✔
236
     * Current implementation just deletes a lock file that is named like the storage.
4✔
237
     */
4✔
238
    unlock() {
4✔
239
        if (fs.existsSync(this.lockFile)) {
864✔
240
            if (!this.locked) {
856✔
241
                this.checkTornWrites();
12✔
242
            }
12✔
243
            fs.rmdirSync(this.lockFile);
856✔
244
        }
856✔
245
        this.locked = false;
864✔
246
    }
864✔
247

4✔
248
    /**
4✔
249
     * @inheritDoc
4✔
250
     */
4✔
251
    close() {
4✔
252
        if (this.locked) {
1,420✔
253
            this.unlock();
852✔
254
        }
852✔
255
        super.close();
1,420✔
256
    }
1,420✔
257

4✔
258
    /**
4✔
259
     * Add an index entry for the given document at the position and size.
4✔
260
     *
4✔
261
     * @private
4✔
262
     * @param {number} partitionId The partition where the document is stored.
4✔
263
     * @param {number} position The file offset where the document is stored.
4✔
264
     * @param {number} size The size of the stored document.
4✔
265
     * @param {object} document The document to add to the index.
4✔
266
     * @param {function} [callback] The callback to call when the index is written to disk.
4✔
267
     * @returns {EntryInterface} The index entry item.
4✔
268
     */
4✔
269
    addIndex(partitionId, position, size, document, callback) {
4✔
270
        if (!this.index.isOpen()) {
2,940✔
271
            this.index.open();
4✔
272
        }
4✔
273

2,940✔
274
        /*if (this.index.lastEntry.position + this.index.lastEntry.size !== position) {
2,940✔
275
         this.emit('index-corrupted');
2,940✔
276
         throw new Error('Corrupted index, needs to be rebuilt!');
2,940✔
277
         }*/
2,940✔
278

2,940✔
279
        const entry = new WritableIndexEntry(this.index.length + 1, position, size, partitionId);
2,940✔
280
        this.index.add(entry, (indexPosition) => {
2,940✔
281
            this.emit('wrote', document, entry, indexPosition);
2,940✔
282
            /* istanbul ignore if  */
2,940✔
283
            if (typeof callback === 'function') {
2,940!
UNCOV
284
                return callback(indexPosition);
×
UNCOV
285
            }
×
286
        });
2,940✔
287
        return entry;
2,940✔
288
    }
2,940✔
289

4✔
290
    /**
4✔
291
     * Register a handler that is called before a document is written to storage.
4✔
292
     * The handler receives the document and the partition metadata and may throw to abort the write.
4✔
293
     * Multiple handlers can be registered; all run on every write in registration order.
4✔
294
     * Equivalent to `storage.on('preCommit', hook)`.
4✔
295
     *
4✔
296
     * @api
4✔
297
     * @param {function(object, object): void} hook A function receiving (document, partitionMetadata).
4✔
298
     */
4✔
299
    preCommit(hook) {
4✔
300
        this.on('preCommit', hook);
16✔
301
    }
16✔
302

4✔
303
    /**
4✔
304
     * Get a partition either by name or by id.
4✔
305
     * If a partition with the given name does not exist, a new one will be created.
4✔
306
     * If a partition with the given id does not exist, an error is thrown.
4✔
307
     *
4✔
308
     * @protected
4✔
309
     * @param {string|number} partitionIdentifier The partition name or the partition Id
4✔
310
     * @returns {ReadablePartition}
4✔
311
     * @throws {Error} If an id is given and no such partition exists.
4✔
312
     */
4✔
313
    getPartition(partitionIdentifier) {
4✔
314
        if (typeof partitionIdentifier === 'string') {
7,100✔
315
            const partitionShortName = partitionIdentifier;
2,964✔
316
            const partitionName = this.storageFile + (partitionIdentifier.length ? '.' + partitionIdentifier : '');
2,964✔
317
            partitionIdentifier = WritablePartition.idFor(partitionName);
2,964✔
318
            if (!this.partitions[partitionIdentifier]) {
2,964✔
319
                const partitionConfig = typeof this.partitionConfig.metadata === 'function'
900✔
320
                    ? { ...this.partitionConfig, metadata: this.partitionConfig.metadata(partitionShortName) }
900✔
321
                    : this.partitionConfig;
900✔
322
                this.partitions[partitionIdentifier] = this.createPartition(partitionName, partitionConfig);
900✔
323
                this.emit('partition-created', partitionIdentifier);
900✔
324
            }
900✔
325
            this.partitions[partitionIdentifier].open();
2,964✔
326
            return this.partitions[partitionIdentifier];
2,964✔
327
        }
2,964✔
328
        return super.getPartition(partitionIdentifier);
4,136✔
329
    }
7,100✔
330

4✔
331
    /**
4✔
332
     * @api
4✔
333
     * @param {object} document The document to write to storage.
4✔
334
     * @param {function} [callback] A function that will be called when the document is written to disk.
4✔
335
     * @returns {number} The 1-based document sequence number in the storage.
4✔
336
     */
4✔
337
    write(document, callback) {
4✔
338
        const data = this.serializer.serialize(document).toString();
2,948✔
339
        const dataSize = Buffer.byteLength(data, 'utf8');
2,948✔
340

2,948✔
341
        const partitionName = this.partitioner(document, this.index.length + 1);
2,948✔
342
        const partition = this.getPartition(partitionName);
2,948✔
343
        if (this.listenerCount('preCommit') > 0) {
2,948✔
344
            this.emit('preCommit', document, partition.metadata);
56✔
345
        }
56✔
346
        const position = partition.write(data, this.length, callback);
2,940✔
347

2,940✔
348
        assert(position !== false, 'Error writing document.');
2,940✔
349

2,940✔
350
        const indexEntry = this.addIndex(partition.id, position, dataSize, document);
2,940✔
351
        this.forEachSecondaryIndex((index, name) => {
2,940✔
352
            if (!index.isOpen()) {
1,504✔
353
                index.open();
4✔
354
            }
4✔
355
            index.add(indexEntry);
1,504✔
356
            this.emit('index-add', name, index.length, document);
1,504✔
357
        }, document);
2,940✔
358

2,940✔
359
        return this.index.length;
2,940✔
360
    }
2,948✔
361

4✔
362
    /**
4✔
363
     * Ensure that an index with the given name and document matcher exists.
4✔
364
     * Will create the index if it doesn't exist, otherwise return the existing index.
4✔
365
     *
4✔
366
     * @api
4✔
367
     * @param {string} name The index name.
4✔
368
     * @param {Matcher} [matcher] An object that describes the document properties that need to match to add it this index or a function that receives a document and returns true if the document should be indexed.
4✔
369
     * @returns {ReadableIndex} The index containing all documents that match the query.
4✔
370
     * @throws {Error} if the index doesn't exist yet and no matcher was specified.
4✔
371
     */
4✔
372
    ensureIndex(name, matcher) {
4✔
373
        if (name === '_all') {
868✔
374
            return this.index;
4✔
375
        }
4✔
376
        if (name in this.secondaryIndexes) {
868✔
377
            return this.secondaryIndexes[name].index;
4✔
378
        }
4✔
379

860✔
380
        const indexName = this.storageFile + '.' + name + '.index';
860✔
381
        if (fs.existsSync(path.join(this.indexDirectory, indexName))) {
868✔
382
            return this.openIndex(name, matcher);
16✔
383
        }
16✔
384

844✔
385
        assert((typeof matcher === 'object' || typeof matcher === 'function') && matcher !== null, 'Need to specify a matcher.');
868✔
386

868✔
387
        const metadata = buildMetadataForMatcher(matcher, this.hmac);
868✔
388
        const { index } = this.createIndex(indexName, Object.assign({}, this.indexOptions, { metadata }));
868✔
389
        try {
868✔
390
            this.forEachDocument((document, indexEntry) => {
868✔
391
                if (matches(document, matcher)) {
1,752✔
392
                    index.add(indexEntry);
16✔
393
                }
16✔
394
            });
868✔
395
        } catch (e) {
868✔
396
            index.destroy();
4✔
397
            throw e;
4✔
398
        }
4✔
399

836✔
400
        this.secondaryIndexes[name] = { index, matcher };
836✔
401
        this.emit('index-created', name);
836✔
402
        return index;
836✔
403
    }
868✔
404

4✔
405
    /**
4✔
406
     * Flush all write buffers to disk.
4✔
407
     * This is a sync method and will invoke all previously registered flush callbacks.
4✔
408
     *
4✔
409
     * @api
4✔
410
     * @returns {boolean} Returns true if a flush on any partition or the main index was executed.
4✔
411
     */
4✔
412
    flush() {
4✔
413
        let result = this.index.flush();
152✔
414
        this.forEachPartition(partition => result = result | partition.flush());
152✔
415
        this.forEachSecondaryIndex(index => index.flush());
152✔
416
        return result;
152✔
417
    }
152✔
418

4✔
419
    /**
4✔
420
     * Iterate all distinct partitions in which the given iterable list of entries are stored.
4✔
421
     * @param {Iterable<Index.Entry>} entries
4✔
422
     * @param {function(Index.Entry)} iterationHandler
4✔
423
     */
4✔
424
    forEachDistinctPartitionOf(entries, iterationHandler) {
4✔
425
        const partitions = [];
36✔
426
        const numPartitions = Object.keys(this.partitions).length;
36✔
427
        for (let entry of entries) {
36✔
428
            if (partitions.indexOf(entry.partition) >= 0) {
56✔
429
                continue;
16✔
430
            }
16✔
431
            partitions.push(entry.partition);
40✔
432
            iterationHandler(entry);
40✔
433
            if (partitions.length === numPartitions) {
56✔
434
                break;
24✔
435
            }
24✔
436
        }
56✔
437
    }
36✔
438

4✔
439
    /**
4✔
440
     * Truncate all partitions after the given (global) sequence number.
4✔
441
     *
4✔
442
     * Assumes the primary index is fully consistent with the partition data. Looks up the first
4✔
443
     * index entry after `after` for each affected partition and truncates the partition file
4✔
444
     * at that entry's byte position.
4✔
445
     *
4✔
446
     * @private
4✔
447
     * @param {number} after The document sequence number to truncate after.
4✔
448
     */
4✔
449
    truncatePartitions(after) {
4✔
450
        if (after === 0) {
48✔
451
            this.forEachPartition(partition => partition.truncate(0));
8✔
452
            return;
8✔
453
        }
8✔
454

40✔
455
        const entries = this.index.range(after + 1);  // We need the first entry that is cut off
40✔
456
        if (entries === false || entries.length === 0) {
48✔
457
            return;
4✔
458
        }
4✔
459

36✔
460
        this.forEachDistinctPartitionOf(entries, entry => this.getPartition(entry.partition).truncate(entry.position));
36✔
461
    }
48✔
462

4✔
463
    /**
4✔
464
     * Truncate the storage after the given sequence number.
4✔
465
     *
4✔
466
     * @param {number} after The document sequence number to truncate after.
4✔
467
     */
4✔
468
    truncate(after) {
4✔
469
        /*
48✔
470
         To truncate the store following steps need to be done:
48✔
471

48✔
472
         1) find all partition positions after which their files should be truncated
48✔
473
         2) truncate all partitions accordingly
48✔
474
         3) truncate/rewrite all indexes
48✔
475
         */
48✔
476
        if (!this.index.isOpen()) {
48✔
477
            this.index.open();
4✔
478
        }
4✔
479
        if (after < 0) {
48✔
480
            after += this.index.length;
4✔
481
        }
4✔
482

48✔
483
        this.truncatePartitions(after);
48✔
484

48✔
485
        this.index.truncate(after);
48✔
486
        this.forEachWritableSecondaryIndex(index => {
48✔
487
            index.truncate(index.find(after));
32✔
488
        });
48✔
489
    }
48✔
490

4✔
491
    /**
4✔
492
     * @inheritDoc
4✔
493
     * Open an existing secondary index and repair any stale entries beyond the current primary
4✔
494
     * index length. Stale entries can be present when checkTornWrites() truncated the primary
4✔
495
     * index before this secondary index was loaded into memory.
4✔
496
     */
4✔
497
    openIndex(name, matcher) {
4✔
498
        const index = super.openIndex(name, matcher);
704✔
499
        const lastEntry = index.lastEntry;
704✔
500
        if (lastEntry !== false && lastEntry.number > this.index.length) {
704✔
501
            // Secondary index is ahead of primary: truncate stale entries.
8✔
502
            index.truncate(index.find(this.index.length));
8✔
503
        }
8✔
504
        return index;
688✔
505
    }
704✔
506

4✔
507
    /**
4✔
508
     * @protected
4✔
509
     * @param {string} name
4✔
510
     * @param {object} [options]
4✔
511
     * @returns {{ index: WritableIndex, matcher: Matcher }}
4✔
512
     */
4✔
513
    createIndex(name, options = {}) {
4✔
514
        const index = new WritableIndex(name, options);
1,852✔
515
        let matcher;
1,852✔
516

1,852✔
517
        // If the index contains a matcher (possibly a serialized function) we check HMAC
1,852✔
518
        // to prevent evaluating unknown code.
1,852✔
519
        if (index.metadata.matcher) {
1,852✔
520
            try {
936✔
521
                matcher = buildMatcherFromMetadata(index.metadata, this.hmac);
936✔
522
            } catch (e) {
936✔
523
                index.destroy();
4✔
524
                throw e;
4✔
525
            }
4✔
526
        }
936✔
527

1,840✔
528
        return { index, matcher };
1,840✔
529
    }
1,852✔
530

4✔
531
    /**
4✔
532
     * @protected
4✔
533
     * @param {string} name
4✔
534
     * @param {object} [config]
4✔
535
     * @returns {WritablePartition}
4✔
536
     */
4✔
537
    createPartition(name, config = {}) {
4✔
538
        return new WritablePartition(name, config);
1,000✔
539
    }
1,000✔
540

4✔
541
}
4✔
542

4✔
543
export default WritableStorage;
4✔
544
export { StorageLockedError, LOCK_THROW, LOCK_RECLAIM };
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