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

albe / node-event-storage / 23711535156

29 Mar 2026 02:43PM UTC coverage: 97.02% (-1.0%) from 98.004%
23711535156

Pull #277

github

web-flow
Merge 1b1c84f1a into de1c33ad1
Pull Request #277: Refactor torn write repair: extract `findTornWriteBoundary` and `truncateAfterSequence`, restore index-based `truncatePartitions`

701 of 746 branches covered (93.97%)

Branch coverage included in aggregate %.

34 of 48 new or added lines in 3 files covered. (70.83%)

7 existing lines in 1 file now uncovered.

1578 of 1603 relevant lines covered (98.44%)

1316.18 hits per line

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

93.36
/src/Storage/WritableStorage.js
1
const fs = require('fs');
4✔
2
const path = require('path');
4✔
3
const WritablePartition = require('../Partition/WritablePartition');
4✔
4
const WritableIndex = require('../Index/WritableIndex');
4✔
5
const ReadableStorage = require('./ReadableStorage');
4✔
6
const { assert, matches, buildMetadataForMatcher, buildMatcherFromMetadata, ensureDirectory } = require('../util');
4✔
7

8
const DEFAULT_WRITE_BUFFER_SIZE = 16 * 1024;
4✔
9

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

13
class StorageLockedError extends Error {}
14

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

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

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

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

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

83
    /**
84
     * Scan every partition's last document to detect torn writes inline.
85
     * A document is torn when its expected end exceeds the actual file size:
86
     *   position + documentWriteSize(dataSize) > partition.size
87
     *
88
     * @private
89
     * @returns {{ lastValidSequenceNumber: number, maxPartitionSequenceNumber: number }}
90
     */
91
    findTornWriteBoundary() {
92
        let lastValidSequenceNumber = Number.MAX_SAFE_INTEGER;
28✔
93
        let maxPartitionSequenceNumber = -1;
28✔
94
        this.forEachPartition(partition => {
28✔
95
            partition.open();
32✔
96
            if (partition.size === 0) return;
32!
97
            const position = partition.findDocumentPositionBefore(partition.size);
32✔
98
            if (position === false || position < 0) return;
32!
99
            const reader = partition.prepareReadBufferBackwards(position);
32✔
100
            if (!reader.buffer) return;
32!
101
            const { sequenceNumber, dataSize } = partition.readDocumentHeader(reader.buffer, reader.cursor, position);
32✔
102
            if (position + partition.documentWriteSize(dataSize) > partition.size) {
32✔
103
                // Torn write: the document extends beyond the end of the file.
104
                lastValidSequenceNumber = Math.min(lastValidSequenceNumber, sequenceNumber);
16✔
105
            } else {
106
                maxPartitionSequenceNumber = Math.max(maxPartitionSequenceNumber, sequenceNumber);
16✔
107
            }
108
        });
109
        return { lastValidSequenceNumber, maxPartitionSequenceNumber };
28✔
110
    }
111

112
    /**
113
     * Check all partitions for torn writes, physically repair each partition, truncate all indexes
114
     * to the torn-write boundary, and then reindex to rebuild any missing index entries.
115
     *
116
     * A document is torn when the partition file ends before the document's expected end position
117
     * (i.e. position + documentWriteSize(dataSize) > partition.size).  Detected inline in
118
     * findTornWriteBoundary(), without any checkTornWrite() call.
119
     *
120
     * Repair flow:
121
     * 1. findTornWriteBoundary() reads the last document of every partition and finds the global
122
     *    torn-write boundary (minimum torn sequence number across all partitions).
123
     * 2. If torn writes were found, truncateAfterSequence() removes all documents at or beyond
124
     *    the boundary from each partition.
125
     * 3. Truncate all indexes to the torn-write boundary, then reindex to fill any lagging entries.
126
     * 4. If no torn writes were found but the index is lagging, reindex directly.
127
     */
128
    checkTornWrites() {
129
        const { lastValidSequenceNumber, maxPartitionSequenceNumber } = this.findTornWriteBoundary();
28✔
130

131
        if (lastValidSequenceNumber < Number.MAX_SAFE_INTEGER) {
28✔
132
            // Phase 2: remove all documents at or beyond the torn-write boundary from each partition.
133
            this.forEachPartition(partition => {
16✔
134
                partition.open();
20✔
135
                partition.truncateAfterSequence(lastValidSequenceNumber - 1);
20✔
136
            });
137

138
            // Truncate all indexes to the torn-write boundary.
139
            this.index.open();
16✔
140
            this.index.truncate(lastValidSequenceNumber);
16✔
141
            this.forEachSecondaryIndex(index => {
16✔
142
                /* istanbul ignore if */
NEW
143
                if (!(index instanceof WritableIndex)) {
×
144
                    return;
145
                }
NEW
146
                let closeIndex = false;
×
NEW
147
                if (!index.isOpen()) {
×
NEW
148
                    index.open();
×
NEW
149
                    closeIndex = true;
×
150
                }
NEW
151
                index.truncate(index.find(lastValidSequenceNumber));
×
NEW
152
                if (closeIndex) {
×
NEW
153
                    index.close();
×
154
                }
155
            });
156

157
            // Reindex to fill in any missing complete-document entries.
158
            this.reindex(this.index.length);
16✔
159
        } else if (maxPartitionSequenceNumber >= 0 && maxPartitionSequenceNumber + 1 > this.index.length) {
12✔
160
            // No torn writes, but the index is lagging — repair it.
161
            this.reindex(this.index.length);
8✔
162
        }
163

164
        this.forEachPartition(partition => partition.close());
32✔
165
    }
166

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

182
        // Truncate all loaded secondary indexes to match the new primary length.
183
        this.forEachSecondaryIndex(index => {
48✔
184
            /* istanbul ignore if */
185
            if (!(index instanceof WritableIndex)) {
8✔
186
                return;
187
            }
188
            // find(0) returns 0, so truncate(0) will remove all entries when fromSequenceNumber===0
189
            index.truncate(fromSequenceNumber === 0 ? 0 : index.find(fromSequenceNumber));
8✔
190
        });
191

192
        // Ensure all partitions are open so iteratePartitionsBySequenceNumber can read them.
193
        this.forEachPartition(partition => {
48✔
194
            if (!partition.isOpen()) {
60✔
195
                partition.open();
4✔
196
            }
197
        });
198

199
        // Scan partitions in sequence-number order and rebuild index entries.
200
        for (const { document, partitionId, position, size } of
48✔
201
            this.iteratePartitionsBySequenceNumber(fromSequenceNumber, Number.MAX_SAFE_INTEGER)) {
202
            const newEntry = new WritableIndex.Entry(this.index.length + 1, position, size, partitionId);
112✔
203
            this.index.add(newEntry);
112✔
204

205
            this.forEachSecondaryIndex((secIndex, name) => {
112✔
206
                /* istanbul ignore if */
207
                if (!(secIndex instanceof WritableIndex)) {
36✔
208
                    return;
209
                }
210
                /* istanbul ignore if */
211
                if (!secIndex.isOpen()) {
36✔
212
                    secIndex.open();
213
                }
214
                const { matcher } = this.secondaryIndexes[name];
36✔
215
                if (matches(document, matcher)) {
36✔
216
                    secIndex.add(newEntry);
20✔
217
                }
218
            });
219
        }
220

221
        this.flush();
48✔
222
    }
223

224
    /**
225
     * Attempt to lock this storage by means of a lock directory.
226
     * @returns {boolean} True if the lock was created or false if the lock is already in place.
227
     * @throws {StorageLockedError} If this storage is already locked by another process.
228
     * @throws {Error} If the lock could not be created.
229
     */
230
    lock() {
231
        if (this.locked) {
864✔
232
            return false;
4✔
233
        }
234
        try {
860✔
235
            fs.mkdirSync(this.lockFile);
860✔
236
            this.locked = true;
856✔
237
        } catch (e) {
238
            /* istanbul ignore if */
239
            if (e.code !== 'EEXIST') {
4✔
240
                throw new Error(`Error creating lock for storage ${this.storageFile}: ` + e.message);
241
            }
242
            throw new StorageLockedError(`Storage ${this.storageFile} is locked by another process`);
4✔
243
        }
244
        return true;
856✔
245
    }
246

247
    /**
248
     * Unlock this storage, no matter if it was previously locked by this writer.
249
     * Only use this if you are sure there is no other process still having a writer open.
250
     * Current implementation just deletes a lock file that is named like the storage.
251
     */
252
    unlock() {
253
        if (fs.existsSync(this.lockFile)) {
864✔
254
            if (!this.locked) {
856✔
255
                this.checkTornWrites();
12✔
256
            }
257
            fs.rmdirSync(this.lockFile);
856✔
258
        }
259
        this.locked = false;
864✔
260
    }
261

262
    /**
263
     * @inheritDoc
264
     */
265
    close() {
266
        if (this.locked) {
1,420✔
267
            this.unlock();
852✔
268
        }
269
        super.close();
1,420✔
270
    }
271

272
    /**
273
     * Add an index entry for the given document at the position and size.
274
     *
275
     * @private
276
     * @param {number} partitionId The partition where the document is stored.
277
     * @param {number} position The file offset where the document is stored.
278
     * @param {number} size The size of the stored document.
279
     * @param {object} document The document to add to the index.
280
     * @param {function} [callback] The callback to call when the index is written to disk.
281
     * @returns {EntryInterface} The index entry item.
282
     */
283
    addIndex(partitionId, position, size, document, callback) {
284
        if (!this.index.isOpen()) {
2,940✔
285
            this.index.open();
4✔
286
        }
287

288
        /*if (this.index.lastEntry.position + this.index.lastEntry.size !== position) {
289
         this.emit('index-corrupted');
290
         throw new Error('Corrupted index, needs to be rebuilt!');
291
         }*/
292

293
        const entry = new WritableIndex.Entry(this.index.length + 1, position, size, partitionId);
2,940✔
294
        this.index.add(entry, (indexPosition) => {
2,940✔
295
            this.emit('wrote', document, entry, indexPosition);
2,940✔
296
            /* istanbul ignore if  */
297
            if (typeof callback === 'function') {
2,940✔
298
                return callback(indexPosition);
299
            }
300
        });
301
        return entry;
2,940✔
302
    }
303

304
    /**
305
     * Register a handler that is called before a document is written to storage.
306
     * The handler receives the document and the partition metadata and may throw to abort the write.
307
     * Multiple handlers can be registered; all run on every write in registration order.
308
     * Equivalent to `storage.on('preCommit', hook)`.
309
     *
310
     * @api
311
     * @param {function(object, object): void} hook A function receiving (document, partitionMetadata).
312
     */
313
    preCommit(hook) {
314
        this.on('preCommit', hook);
16✔
315
    }
316

317
    /**
318
     * Get a partition either by name or by id.
319
     * If a partition with the given name does not exist, a new one will be created.
320
     * If a partition with the given id does not exist, an error is thrown.
321
     *
322
     * @protected
323
     * @param {string|number} partitionIdentifier The partition name or the partition Id
324
     * @returns {ReadablePartition}
325
     * @throws {Error} If an id is given and no such partition exists.
326
     */
327
    getPartition(partitionIdentifier) {
328
        if (typeof partitionIdentifier === 'string') {
7,100✔
329
            const partitionShortName = partitionIdentifier;
2,964✔
330
            const partitionName = this.storageFile + (partitionIdentifier.length ? '.' + partitionIdentifier : '');
2,964✔
331
            partitionIdentifier = WritablePartition.idFor(partitionName);
2,964✔
332
            if (!this.partitions[partitionIdentifier]) {
2,964✔
333
                const partitionConfig = typeof this.partitionConfig.metadata === 'function'
900✔
334
                    ? { ...this.partitionConfig, metadata: this.partitionConfig.metadata(partitionShortName) }
335
                    : this.partitionConfig;
336
                this.partitions[partitionIdentifier] = this.createPartition(partitionName, partitionConfig);
900✔
337
                this.emit('partition-created', partitionIdentifier);
900✔
338
            }
339
            this.partitions[partitionIdentifier].open();
2,964✔
340
            return this.partitions[partitionIdentifier];
2,964✔
341
        }
342
        return super.getPartition(partitionIdentifier);
4,136✔
343
    }
344

345
    /**
346
     * @api
347
     * @param {object} document The document to write to storage.
348
     * @param {function} [callback] A function that will be called when the document is written to disk.
349
     * @returns {number} The 1-based document sequence number in the storage.
350
     */
351
    write(document, callback) {
352
        const data = this.serializer.serialize(document).toString();
2,948✔
353
        const dataSize = Buffer.byteLength(data, 'utf8');
2,948✔
354

355
        const partitionName = this.partitioner(document, this.index.length + 1);
2,948✔
356
        const partition = this.getPartition(partitionName);
2,948✔
357
        if (this.listenerCount('preCommit') > 0) {
2,948✔
358
            this.emit('preCommit', document, partition.metadata);
56✔
359
        }
360
        const position = partition.write(data, this.length, callback);
2,940✔
361

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

364
        const indexEntry = this.addIndex(partition.id, position, dataSize, document);
2,940✔
365
        this.forEachSecondaryIndex((index, name) => {
2,940✔
366
            if (!index.isOpen()) {
1,504✔
367
                index.open();
4✔
368
            }
369
            index.add(indexEntry);
1,504✔
370
            this.emit('index-add', name, index.length, document);
1,504✔
371
        }, document);
372

373
        return this.index.length;
2,940✔
374
    }
375

376
    /**
377
     * Ensure that an index with the given name and document matcher exists.
378
     * Will create the index if it doesn't exist, otherwise return the existing index.
379
     *
380
     * @api
381
     * @param {string} name The index name.
382
     * @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.
383
     * @returns {ReadableIndex} The index containing all documents that match the query.
384
     * @throws {Error} if the index doesn't exist yet and no matcher was specified.
385
     */
386
    ensureIndex(name, matcher) {
387
        if (name === '_all') {
868✔
388
            return this.index;
4✔
389
        }
390
        if (name in this.secondaryIndexes) {
864✔
391
            return this.secondaryIndexes[name].index;
4✔
392
        }
393

394
        const indexName = this.storageFile + '.' + name + '.index';
860✔
395
        if (fs.existsSync(path.join(this.indexDirectory, indexName))) {
860✔
396
            return this.openIndex(name, matcher);
16✔
397
        }
398

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

401
        const metadata = buildMetadataForMatcher(matcher, this.hmac);
840✔
402
        const { index } = this.createIndex(indexName, Object.assign({}, this.indexOptions, { metadata }));
840✔
403
        try {
840✔
404
            this.forEachDocument((document, indexEntry) => {
840✔
405
                if (matches(document, matcher)) {
1,752✔
406
                    index.add(indexEntry);
16✔
407
                }
408
            });
409
        } catch (e) {
410
            index.destroy();
4✔
411
            throw e;
4✔
412
        }
413

414
        this.secondaryIndexes[name] = { index, matcher };
836✔
415
        this.emit('index-created', name);
836✔
416
        return index;
836✔
417
    }
418

419
    /**
420
     * Flush all write buffers to disk.
421
     * This is a sync method and will invoke all previously registered flush callbacks.
422
     *
423
     * @api
424
     * @returns {boolean} Returns true if a flush on any partition or the main index was executed.
425
     */
426
    flush() {
427
        let result = this.index.flush();
152✔
428
        this.forEachPartition(partition => result = result | partition.flush());
168✔
429
        this.forEachSecondaryIndex(index => index.flush());
152✔
430
        return result;
152✔
431
    }
432

433
    /**
434
     * Iterate all distinct partitions in which the given iterable list of entries are stored.
435
     * @param {Iterable<Index.Entry>} entries
436
     * @param {function(Index.Entry)} iterationHandler
437
     */
438
    forEachDistinctPartitionOf(entries, iterationHandler) {
439
        const partitions = [];
36✔
440
        const numPartitions = Object.keys(this.partitions).length;
36✔
441
        for (let entry of entries) {
36✔
442
            if (partitions.indexOf(entry.partition) >= 0) {
56✔
443
                continue;
16✔
444
            }
445
            partitions.push(entry.partition);
40✔
446
            iterationHandler(entry);
40✔
447
            if (partitions.length === numPartitions) {
40✔
448
                break;
24✔
449
            }
450
        }
451
    }
452

453
    /**
454
     * Truncate all partitions after the given (global) sequence number.
455
     *
456
     * Assumes the primary index is fully consistent with the partition data. Looks up the first
457
     * index entry after `after` for each affected partition and truncates the partition file
458
     * at that entry's byte position.
459
     *
460
     * @private
461
     * @param {number} after The document sequence number to truncate after.
462
     */
463
    truncatePartitions(after) {
464
        if (after === 0) {
48✔
465
            this.forEachPartition(partition => partition.truncate(0));
8✔
466
            return;
8✔
467
        }
468

469
        const entries = this.index.range(after + 1);  // We need the first entry that is cut off
40✔
470
        if (entries === false || entries.length === 0) {
40✔
471
            return;
4✔
472
        }
473

474
        this.forEachDistinctPartitionOf(entries, entry => this.getPartition(entry.partition).truncate(entry.position));
40✔
475
    }
476

477
    /**
478
     * Truncate the storage after the given sequence number.
479
     *
480
     * @param {number} after The document sequence number to truncate after.
481
     */
482
    truncate(after) {
483
        /*
484
         To truncate the store following steps need to be done:
485

486
         1) find all partition positions after which their files should be truncated
487
         2) truncate all partitions accordingly
488
         3) truncate/rewrite all indexes
489
         */
490
        if (!this.index.isOpen()) {
48✔
491
            this.index.open();
4✔
492
        }
493
        if (after < 0) {
48✔
494
            after += this.index.length;
4✔
495
        }
496

497
        this.truncatePartitions(after);
48✔
498

499
        this.index.truncate(after);
48✔
500
        this.forEachSecondaryIndex(index => {
48✔
501
            /* istanbul ignore if */
502
            if (!(index instanceof WritableIndex)) {
32✔
503
                return;
504
            }
505
            let closeIndex = false;
32✔
506
            if (!index.isOpen()) {
32✔
507
                index.open();
4✔
508
                closeIndex = true;
4✔
509
            }
510
            index.truncate(index.find(after));
32✔
511
            if (closeIndex) {
32✔
512
                index.close();
4✔
513
            }
514
        });
515
    }
516

517
    /**
518
     * @inheritDoc
519
     * Open an existing secondary index and repair any stale entries beyond the current primary
520
     * index length. Stale entries can be present when checkTornWrites() truncated the primary
521
     * index before this secondary index was loaded into memory.
522
     */
523
    openIndex(name, matcher) {
524
        const index = super.openIndex(name, matcher);
704✔
525
        const lastEntry = index.lastEntry;
688✔
526
        if (lastEntry !== false && lastEntry.number > this.index.length) {
688✔
527
            // Secondary index is ahead of primary: truncate stale entries.
528
            index.truncate(index.find(this.index.length));
8✔
529
        }
530
        return index;
688✔
531
    }
532

533
    /**
534
     * @protected
535
     * @param {string} name
536
     * @param {object} [options]
537
     * @returns {{ index: WritableIndex, matcher: Matcher }}
538
     */
539
    createIndex(name, options = {}) {
×
540
        const index = new WritableIndex(name, options);
1,852✔
541
        let matcher;
542

543
        // If the index contains a matcher (possibly a serialized function) we check HMAC
544
        // to prevent evaluating unknown code.
545
        if (index.metadata.matcher) {
1,844✔
546
            try {
936✔
547
                matcher = buildMatcherFromMetadata(index.metadata, this.hmac);
936✔
548
            } catch (e) {
549
                index.destroy();
4✔
550
                throw e;
4✔
551
            }
552
        }
553

554
        return { index, matcher };
1,840✔
555
    }
556

557
    /**
558
     * @protected
559
     * @param {string} name
560
     * @param {object} [config]
561
     * @returns {WritablePartition}
562
     */
563
    createPartition(name, config = {}) {
×
564
        return new WritablePartition(name, config);
1,000✔
565
    }
566

567
}
568

569
module.exports = WritableStorage;
4✔
570
module.exports.StorageLockedError = StorageLockedError;
4✔
571
module.exports.CorruptFileError = ReadableStorage.CorruptFileError;
4✔
572
module.exports.LOCK_THROW = LOCK_THROW;
4✔
573
module.exports.LOCK_RECLAIM = 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