event-storage 0.8.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -540
- package/index.js +5 -6
- package/package.json +28 -31
- package/src/Clock.js +21 -9
- package/src/Consumer.js +6 -7
- package/src/EventStore.js +261 -67
- package/src/EventStream.js +172 -19
- package/src/Index/ReadOnlyIndex.js +3 -3
- package/src/Index/ReadableIndex.js +17 -13
- package/src/Index/WritableIndex.js +17 -11
- package/src/Index.js +7 -5
- package/src/IndexEntry.js +2 -3
- package/src/JoinEventStream.js +34 -32
- package/src/Partition/ReadOnlyPartition.js +3 -3
- package/src/Partition/ReadablePartition.js +110 -57
- package/src/Partition/WritablePartition.js +81 -23
- package/src/Partition.js +7 -4
- package/src/Storage/ReadOnlyStorage.js +4 -4
- package/src/Storage/ReadableStorage.js +144 -22
- package/src/Storage/WritableStorage.js +175 -33
- package/src/Storage.js +9 -4
- package/src/Watcher.js +6 -5
- package/src/WatchesFile.js +8 -7
- package/src/metadataUtil.js +79 -0
- package/src/util.js +74 -73
- package/test/Consumer.spec.js +0 -455
- package/test/EventStore.spec.js +0 -632
- package/test/EventStream.spec.js +0 -120
- package/test/Index.spec.js +0 -591
- package/test/JoinEventStream.spec.js +0 -113
- package/test/Partition.spec.js +0 -488
- package/test/Storage.spec.js +0 -1017
- package/test/Watcher.spec.js +0 -131
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import events from 'events';
|
|
4
|
+
import { assert, alignTo, hash, binarySearch } from '../util.js';
|
|
5
5
|
|
|
6
6
|
const DEFAULT_READ_BUFFER_SIZE = 64 * 1024;
|
|
7
7
|
const DOCUMENT_HEADER_SIZE = 16;
|
|
@@ -17,30 +17,6 @@ const NES_EPOCH = new Date('2020-01-01T00:00:00');
|
|
|
17
17
|
class CorruptFileError extends Error {}
|
|
18
18
|
class InvalidDataSizeError extends Error {}
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
* Method for hashing a string (partition name) to a 32-bit unsigned integer.
|
|
22
|
-
*
|
|
23
|
-
* @param {string} str
|
|
24
|
-
* @returns {number}
|
|
25
|
-
*/
|
|
26
|
-
function hash(str) {
|
|
27
|
-
/* istanbul ignore if */
|
|
28
|
-
if (str.length === 0) {
|
|
29
|
-
return 0;
|
|
30
|
-
}
|
|
31
|
-
let hash = 5381,
|
|
32
|
-
i = str.length;
|
|
33
|
-
|
|
34
|
-
while(i) {
|
|
35
|
-
hash = ((hash << 5) + hash) ^ str.charCodeAt(--i); // jshint ignore:line
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/* JavaScript does bitwise operations (like XOR, above) on 32-bit signed
|
|
39
|
-
* integers. Since we want the results to be always positive, convert the
|
|
40
|
-
* signed int to an unsigned by doing an unsigned bitshift. */
|
|
41
|
-
return hash >>> 0; // jshint ignore:line
|
|
42
|
-
}
|
|
43
|
-
|
|
44
20
|
/**
|
|
45
21
|
* A partition is a single file where the storage will write documents to depending on some partitioning rules.
|
|
46
22
|
* In the case of an event store, this is most likely the (write) streams.
|
|
@@ -123,21 +99,6 @@ class ReadablePartition extends events.EventEmitter {
|
|
|
123
99
|
return true;
|
|
124
100
|
}
|
|
125
101
|
|
|
126
|
-
/**
|
|
127
|
-
* @returns {number} -1 if the partition is ok and the sequence number of the broken document if a torn write was detected.
|
|
128
|
-
*/
|
|
129
|
-
checkTornWrite() {
|
|
130
|
-
const reader = this.prepareReadBufferBackwards(this.size);
|
|
131
|
-
const separator = reader.buffer.toString('ascii', reader.cursor - DOCUMENT_SEPARATOR.length, reader.cursor);
|
|
132
|
-
if (separator !== DOCUMENT_SEPARATOR) {
|
|
133
|
-
const position = this.findDocumentPositionBefore(this.size);
|
|
134
|
-
const reader = this.prepareReadBuffer(position);
|
|
135
|
-
const { sequenceNumber } = this.readDocumentHeader(reader.buffer, reader.cursor, position);
|
|
136
|
-
return sequenceNumber;
|
|
137
|
-
}
|
|
138
|
-
return -1;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
102
|
/**
|
|
142
103
|
* Read the partition metadata from the file.
|
|
143
104
|
*
|
|
@@ -279,7 +240,7 @@ class ReadablePartition extends events.EventEmitter {
|
|
|
279
240
|
return ({ buffer: null, cursor: 0, length: 0 });
|
|
280
241
|
}
|
|
281
242
|
let bufferCursor = position - this.readBufferPos;
|
|
282
|
-
if (this.readBufferPos < 0 || (this.readBufferPos > 0 && bufferCursor < DOCUMENT_FOOTER_SIZE)) {
|
|
243
|
+
if (this.readBufferPos < 0 || (this.readBufferPos > 0 && bufferCursor < DOCUMENT_FOOTER_SIZE) || bufferCursor > this.readBufferLength) {
|
|
283
244
|
this.fillBuffer(Math.max(position - this.readBuffer.byteLength, 0));
|
|
284
245
|
bufferCursor = position - this.readBufferPos;
|
|
285
246
|
}
|
|
@@ -292,12 +253,14 @@ class ReadablePartition extends events.EventEmitter {
|
|
|
292
253
|
* @api
|
|
293
254
|
* @param {number} position The file position to read from.
|
|
294
255
|
* @param {number} [size] The expected byte size of the document at the given position.
|
|
256
|
+
* @param {object|null} [headerOut] Optional object to populate with the document header fields
|
|
257
|
+
* (`dataSize`, `sequenceNumber`, `time64`). Pass an existing object to avoid extra allocation.
|
|
295
258
|
* @returns {string|boolean} The data stored at the given position or false if no data could be read.
|
|
296
259
|
* @throws {Error} if the storage entry at the given position is corrupted.
|
|
297
260
|
* @throws {InvalidDataSizeError} if the document size at the given position does not match the provided size.
|
|
298
261
|
* @throws {CorruptFileError} if the document at the given position can not be read completely.
|
|
299
262
|
*/
|
|
300
|
-
readFrom(position, size = 0) {
|
|
263
|
+
readFrom(position, size = 0, headerOut = null) {
|
|
301
264
|
assert(this.fd, 'Partition is not opened.');
|
|
302
265
|
assert((position % DOCUMENT_ALIGNMENT) === 0, `Invalid read position ${position}. Needs to be a multiple of ${DOCUMENT_ALIGNMENT}.`);
|
|
303
266
|
|
|
@@ -307,7 +270,12 @@ class ReadablePartition extends events.EventEmitter {
|
|
|
307
270
|
}
|
|
308
271
|
|
|
309
272
|
let dataPosition = reader.cursor + DOCUMENT_HEADER_SIZE;
|
|
310
|
-
const { dataSize } = this.readDocumentHeader(reader.buffer, reader.cursor, position, size);
|
|
273
|
+
const { dataSize, sequenceNumber, time64 } = this.readDocumentHeader(reader.buffer, reader.cursor, position, size);
|
|
274
|
+
if (headerOut !== null) {
|
|
275
|
+
headerOut.dataSize = dataSize;
|
|
276
|
+
headerOut.sequenceNumber = sequenceNumber;
|
|
277
|
+
headerOut.time64 = time64;
|
|
278
|
+
}
|
|
311
279
|
|
|
312
280
|
// TODO: This should only be checked on opening
|
|
313
281
|
const writeSize = this.documentWriteSize(dataSize);
|
|
@@ -366,17 +334,108 @@ class ReadablePartition extends events.EventEmitter {
|
|
|
366
334
|
return Math.max(0, position);
|
|
367
335
|
}
|
|
368
336
|
|
|
337
|
+
/**
|
|
338
|
+
* Find the document that starts immediately before `position`, fill the read buffer
|
|
339
|
+
* centered around that document's start, and return its file position and parsed header.
|
|
340
|
+
* The buffer is centered by calling `prepareReadBufferBackwards` with a position
|
|
341
|
+
* half a buffer-length ahead of the document start, clamped to file size, so the
|
|
342
|
+
* document start lands near the middle of the buffer.
|
|
343
|
+
*
|
|
344
|
+
* @private
|
|
345
|
+
* @param {number} position The file position to search before.
|
|
346
|
+
* @returns {{ header: {dataSize: number, sequenceNumber: number, time64: number}, position: number }|null}
|
|
347
|
+
* The document header and file position, or null if no document could be found.
|
|
348
|
+
*/
|
|
349
|
+
readDocumentBefore(position) {
|
|
350
|
+
const docPos = this.findDocumentPositionBefore(position);
|
|
351
|
+
/* istanbul ignore if */
|
|
352
|
+
if (docPos === false || docPos < 0) return null;
|
|
353
|
+
const reader = this.prepareReadBufferBackwards(Math.min(docPos + (this.readBuffer.byteLength >> 1), this.size));
|
|
354
|
+
/* istanbul ignore if */
|
|
355
|
+
if (!reader.buffer) return null;
|
|
356
|
+
const cursor = docPos - this.readBufferPos;
|
|
357
|
+
/* istanbul ignore if */
|
|
358
|
+
if (cursor < 0 || cursor + DOCUMENT_HEADER_SIZE > reader.length) return null;
|
|
359
|
+
const header = this.readDocumentHeader(reader.buffer, cursor, docPos);
|
|
360
|
+
return { header, position: docPos };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Read the header and file position of the last document in this partition.
|
|
365
|
+
*
|
|
366
|
+
* @api
|
|
367
|
+
* @returns {{ header: {dataSize: number, sequenceNumber: number, time64: number}, position: number } | null}
|
|
368
|
+
* The last document's header and its file position, or null if the partition is empty or unreadable.
|
|
369
|
+
*/
|
|
370
|
+
readLast() {
|
|
371
|
+
if (this.size === 0) return null;
|
|
372
|
+
return this.readDocumentBefore(this.size);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Find the first document whose sequenceNumber is >= the given value.
|
|
377
|
+
* Uses readLast() to short-circuit when the partition contains no such document.
|
|
378
|
+
* Uses a binary search over file positions via readDocumentBefore() to locate the
|
|
379
|
+
* document. The search tracks both the lower bound (position just after the last
|
|
380
|
+
* confirmed "< sequenceNumber" doc) and the upper bound (minimum position of any
|
|
381
|
+
* probed doc with sequenceNumber >= target). The upper bound, when available, is
|
|
382
|
+
* the exact target document, so no further linear scan is needed.
|
|
383
|
+
*
|
|
384
|
+
* @api
|
|
385
|
+
* @param {number} sequenceNumber The 0-based sequence number to search for.
|
|
386
|
+
* @returns {{ reader: Generator<string>, headerOut: object, data: string }|null}
|
|
387
|
+
* The matched document with its reader and shared headerOut, or null if no such document exists.
|
|
388
|
+
*/
|
|
389
|
+
findDocument(sequenceNumber) {
|
|
390
|
+
const last = this.readLast();
|
|
391
|
+
if (!last || last.header.sequenceNumber < sequenceNumber) {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
let startPosition = this.size;
|
|
396
|
+
binarySearch(
|
|
397
|
+
sequenceNumber,
|
|
398
|
+
this.size,
|
|
399
|
+
(pos) => {
|
|
400
|
+
const doc = this.readDocumentBefore(pos);
|
|
401
|
+
if (!doc) return sequenceNumber;
|
|
402
|
+
if (doc.header.sequenceNumber < sequenceNumber) {
|
|
403
|
+
startPosition = Math.max(startPosition, doc.position + this.documentWriteSize(doc.header.dataSize));
|
|
404
|
+
} else {
|
|
405
|
+
startPosition = Math.min(startPosition, doc.position);
|
|
406
|
+
}
|
|
407
|
+
return doc.header.sequenceNumber;
|
|
408
|
+
}
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
const headerOut = {};
|
|
412
|
+
const data = this.readFrom(startPosition, 0, headerOut);
|
|
413
|
+
/* istanbul ignore if */
|
|
414
|
+
if (data === false) {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
headerOut.position = startPosition;
|
|
418
|
+
return { headerOut, data };
|
|
419
|
+
}
|
|
420
|
+
|
|
369
421
|
/**
|
|
370
422
|
* @api
|
|
371
423
|
* @param {number} [after] The document position to start reading from.
|
|
424
|
+
* @param {object|null} [headerOut] Optional object to populate with document header fields
|
|
425
|
+
* (`dataSize`, `sequenceNumber`, `time64`, `position`) on each yield. Pass an existing object
|
|
426
|
+
* to avoid extra allocation. The object is mutated in place before each yield.
|
|
372
427
|
* @returns {Generator<string>} A generator that returns all documents in this partition.
|
|
373
428
|
*/
|
|
374
|
-
*readAll(after = 0) {
|
|
429
|
+
*readAll(after = 0, headerOut = null) {
|
|
375
430
|
let position = after < 0 ? this.size + after + 1 : after;
|
|
431
|
+
const internalHeader = headerOut !== null ? headerOut : {};
|
|
376
432
|
let data;
|
|
377
|
-
while ((data = this.readFrom(position)) !== false) {
|
|
433
|
+
while ((data = this.readFrom(position, 0, internalHeader)) !== false) {
|
|
434
|
+
if (headerOut !== null) {
|
|
435
|
+
headerOut.position = position;
|
|
436
|
+
}
|
|
378
437
|
yield data;
|
|
379
|
-
position += this.documentWriteSize(
|
|
438
|
+
position += this.documentWriteSize(internalHeader.dataSize);
|
|
380
439
|
}
|
|
381
440
|
}
|
|
382
441
|
|
|
@@ -394,11 +453,5 @@ class ReadablePartition extends events.EventEmitter {
|
|
|
394
453
|
}
|
|
395
454
|
}
|
|
396
455
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
module.exports.InvalidDataSizeError = InvalidDataSizeError;
|
|
400
|
-
module.exports.HEADER_MAGIC = HEADER_MAGIC;
|
|
401
|
-
module.exports.DOCUMENT_SEPARATOR = DOCUMENT_SEPARATOR;
|
|
402
|
-
module.exports.DOCUMENT_ALIGNMENT = DOCUMENT_ALIGNMENT;
|
|
403
|
-
module.exports.DOCUMENT_HEADER_SIZE = DOCUMENT_HEADER_SIZE;
|
|
404
|
-
module.exports.DOCUMENT_FOOTER_SIZE = DOCUMENT_FOOTER_SIZE;
|
|
456
|
+
export default ReadablePartition;
|
|
457
|
+
export { CorruptFileError, InvalidDataSizeError, HEADER_MAGIC, DOCUMENT_SEPARATOR, DOCUMENT_ALIGNMENT, DOCUMENT_HEADER_SIZE, DOCUMENT_FOOTER_SIZE };
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import ReadablePartition, { CorruptFileError, HEADER_MAGIC, DOCUMENT_ALIGNMENT, DOCUMENT_SEPARATOR, DOCUMENT_HEADER_SIZE, DOCUMENT_FOOTER_SIZE } from './ReadablePartition.js';
|
|
3
|
+
import { assert, buildMetadataHeader, alignTo, ensureDirectory } from '../util.js';
|
|
4
|
+
import Clock from '../Clock.js';
|
|
5
5
|
|
|
6
6
|
const DEFAULT_WRITE_BUFFER_SIZE = 16 * 1024;
|
|
7
|
-
const { DOCUMENT_ALIGNMENT, DOCUMENT_SEPARATOR, DOCUMENT_HEADER_SIZE, DOCUMENT_FOOTER_SIZE } = ReadablePartition;
|
|
8
7
|
const DOCUMENT_PAD = ' '.repeat(DOCUMENT_ALIGNMENT);
|
|
9
8
|
|
|
10
9
|
/**
|
|
@@ -77,6 +76,8 @@ class WritablePartition extends ReadablePartition {
|
|
|
77
76
|
// How many documents are currently in the write buffer
|
|
78
77
|
this.writeBufferDocuments = 0;
|
|
79
78
|
this.flushCallbacks = [];
|
|
79
|
+
// Pre-allocated buffer for document header (16 bytes) + size footer (4 bytes) used in writeUnbuffered
|
|
80
|
+
this.writeMetaBuffer = Buffer.allocUnsafe(DOCUMENT_HEADER_SIZE + 4);
|
|
80
81
|
|
|
81
82
|
this.clock = new this.ClockConstructor(this.metadata.epoch);
|
|
82
83
|
|
|
@@ -97,6 +98,7 @@ class WritablePartition extends ReadablePartition {
|
|
|
97
98
|
this.writeBuffer = null;
|
|
98
99
|
this.writeBufferCursor = 0;
|
|
99
100
|
this.writeBufferDocuments = 0;
|
|
101
|
+
this.writeMetaBuffer = null;
|
|
100
102
|
}
|
|
101
103
|
super.close();
|
|
102
104
|
}
|
|
@@ -108,7 +110,7 @@ class WritablePartition extends ReadablePartition {
|
|
|
108
110
|
* @returns void
|
|
109
111
|
*/
|
|
110
112
|
writeMetadata() {
|
|
111
|
-
const metadataBuffer = buildMetadataHeader(
|
|
113
|
+
const metadataBuffer = buildMetadataHeader(HEADER_MAGIC, this.metadata);
|
|
112
114
|
fs.writeFileSync(this.fileName, metadataBuffer);
|
|
113
115
|
this.headerSize = metadataBuffer.byteLength;
|
|
114
116
|
}
|
|
@@ -134,8 +136,9 @@ class WritablePartition extends ReadablePartition {
|
|
|
134
136
|
|
|
135
137
|
this.writeBufferCursor = 0;
|
|
136
138
|
this.writeBufferDocuments = 0;
|
|
137
|
-
this.flushCallbacks
|
|
139
|
+
const callbacks = this.flushCallbacks;
|
|
138
140
|
this.flushCallbacks = [];
|
|
141
|
+
for (let i = 0; i < callbacks.length; i++) callbacks[i]();
|
|
139
142
|
|
|
140
143
|
return true;
|
|
141
144
|
}
|
|
@@ -199,17 +202,15 @@ class WritablePartition extends ReadablePartition {
|
|
|
199
202
|
*/
|
|
200
203
|
writeUnbuffered(data, dataSize, sequenceNumber, callback) {
|
|
201
204
|
this.flush();
|
|
202
|
-
|
|
203
|
-
this.writeDocumentHeader(dataHeader, 0, dataSize, sequenceNumber);
|
|
205
|
+
this.writeDocumentHeader(this.writeMetaBuffer, 0, dataSize, sequenceNumber);
|
|
204
206
|
|
|
205
207
|
let bytesWritten = 0;
|
|
206
|
-
bytesWritten += fs.writeSync(this.fd,
|
|
208
|
+
bytesWritten += fs.writeSync(this.fd, this.writeMetaBuffer, 0, DOCUMENT_HEADER_SIZE);
|
|
207
209
|
bytesWritten += fs.writeSync(this.fd, data);
|
|
208
210
|
const padSize = alignTo(dataSize + DOCUMENT_FOOTER_SIZE, DOCUMENT_ALIGNMENT);
|
|
209
211
|
bytesWritten += fs.writeSync(this.fd, DOCUMENT_PAD.substr(0, padSize));
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
bytesWritten += fs.writeSync(this.fd, dataSizeBuffer);
|
|
212
|
+
this.writeMetaBuffer.writeUInt32BE(dataSize, 0);
|
|
213
|
+
bytesWritten += fs.writeSync(this.fd, this.writeMetaBuffer, 0, 4);
|
|
213
214
|
bytesWritten += fs.writeSync(this.fd, DOCUMENT_SEPARATOR);
|
|
214
215
|
if (typeof callback === 'function') {
|
|
215
216
|
process.nextTick(callback);
|
|
@@ -227,7 +228,7 @@ class WritablePartition extends ReadablePartition {
|
|
|
227
228
|
* @returns {number} Number of bytes written.
|
|
228
229
|
*/
|
|
229
230
|
writeBuffered(data, dataSize, sequenceNumber, callback) {
|
|
230
|
-
const bytesToWrite = this.documentWriteSize(
|
|
231
|
+
const bytesToWrite = this.documentWriteSize(dataSize);
|
|
231
232
|
this.flushIfWriteBufferTooSmall(bytesToWrite);
|
|
232
233
|
|
|
233
234
|
let bytesWritten = 0;
|
|
@@ -293,7 +294,40 @@ class WritablePartition extends ReadablePartition {
|
|
|
293
294
|
}
|
|
294
295
|
|
|
295
296
|
/**
|
|
296
|
-
*
|
|
297
|
+
* Prepare the read buffer for reading *before* the specified position. Don't try to read *after* the returned cursor.
|
|
298
|
+
*
|
|
299
|
+
* @protected
|
|
300
|
+
* @param {number} position The position in the file to prepare the read buffer for reading before.
|
|
301
|
+
* @returns {object} A reader object with properties `buffer`, `cursor` and `length`.
|
|
302
|
+
*/
|
|
303
|
+
prepareReadBufferBackwards(position) {
|
|
304
|
+
const bufferPos = this.size - this.writeBufferCursor;
|
|
305
|
+
// Handle the case when data that is still in write buffer is supposed to be read backwards
|
|
306
|
+
if (this.dirtyReads && this.writeBufferCursor > 0 && position > bufferPos) {
|
|
307
|
+
return { buffer: this.writeBuffer, cursor: position - bufferPos, length: this.writeBufferCursor };
|
|
308
|
+
}
|
|
309
|
+
return super.prepareReadBufferBackwards(position);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Read all documents in reverse write order, ignoring any unflushed write-buffer data when
|
|
314
|
+
* dirty reads are disabled.
|
|
315
|
+
*
|
|
316
|
+
* @api
|
|
317
|
+
* @param {number} [before] The document position to start reading backward from.
|
|
318
|
+
* @returns {Generator<string>} A generator that returns all documents in this partition in reverse order.
|
|
319
|
+
*/
|
|
320
|
+
*readAllBackwards(before = -1) {
|
|
321
|
+
if (!this.dirtyReads && this.writeBufferCursor > 0) {
|
|
322
|
+
const flushedSize = this.size - this.writeBufferCursor;
|
|
323
|
+
const clampedBefore = before < 0 ? flushedSize : Math.min(before, flushedSize);
|
|
324
|
+
yield* super.readAllBackwards(clampedBefore);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
yield* super.readAllBackwards(before);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
297
331
|
*
|
|
298
332
|
* @internal
|
|
299
333
|
* @param {number} after The byte position to truncate the read buffer after.
|
|
@@ -307,6 +341,31 @@ class WritablePartition extends ReadablePartition {
|
|
|
307
341
|
}
|
|
308
342
|
}
|
|
309
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Truncate the partition, removing all documents with sequenceNumber > after.
|
|
346
|
+
* Scans the partition file backwards to find the cutoff position.
|
|
347
|
+
*
|
|
348
|
+
* @api
|
|
349
|
+
* @param {number} after Keep all documents with sequenceNumber <= after. Documents with
|
|
350
|
+
* sequenceNumber > after (and any torn write at the end) are removed.
|
|
351
|
+
*/
|
|
352
|
+
truncateAfterSequence(after) {
|
|
353
|
+
let position = this.size;
|
|
354
|
+
let truncateAt = this.size; // default: nothing to truncate
|
|
355
|
+
while ((position = this.findDocumentPositionBefore(position)) !== false) {
|
|
356
|
+
const reader = this.prepareReadBufferBackwards(position);
|
|
357
|
+
if (!reader.buffer) break;
|
|
358
|
+
const { sequenceNumber } = this.readDocumentHeader(reader.buffer, reader.cursor, position);
|
|
359
|
+
if (sequenceNumber > after) {
|
|
360
|
+
// This document must be removed; record its start as the new cutoff.
|
|
361
|
+
truncateAt = position;
|
|
362
|
+
} else {
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
this.truncate(truncateAt);
|
|
367
|
+
}
|
|
368
|
+
|
|
310
369
|
/**
|
|
311
370
|
* Truncate the partition storage at the given position.
|
|
312
371
|
*
|
|
@@ -314,25 +373,24 @@ class WritablePartition extends ReadablePartition {
|
|
|
314
373
|
* @param {number} after The file position after which to truncate the partition.
|
|
315
374
|
*/
|
|
316
375
|
truncate(after) {
|
|
317
|
-
if (after
|
|
376
|
+
if (after >= this.size) {
|
|
318
377
|
return;
|
|
319
378
|
}
|
|
320
379
|
this.open();
|
|
321
380
|
after = Math.max(0, after);
|
|
322
381
|
this.flush();
|
|
323
382
|
|
|
383
|
+
// Always save the truncated part for manual recovery, even if it contains corrupted data
|
|
384
|
+
this.branchOff('truncated-' + Date.now(), after);
|
|
385
|
+
|
|
324
386
|
try {
|
|
325
387
|
this.readFrom(after);
|
|
326
388
|
} catch (e) {
|
|
327
|
-
if (!(e instanceof
|
|
389
|
+
if (!(e instanceof CorruptFileError)) {
|
|
328
390
|
throw new Error('Can only truncate on valid document boundaries.');
|
|
329
391
|
}
|
|
330
392
|
}
|
|
331
393
|
|
|
332
|
-
// copy all truncated documents to some delete log
|
|
333
|
-
const backupName = (new Date()).toISOString().substring(0,10);
|
|
334
|
-
this.branchOff(backupName, after);
|
|
335
|
-
|
|
336
394
|
fs.truncateSync(this.fileName, this.headerSize + after);
|
|
337
395
|
this.truncateReadBuffer(after);
|
|
338
396
|
this.size = after;
|
|
@@ -360,5 +418,5 @@ class WritablePartition extends ReadablePartition {
|
|
|
360
418
|
}
|
|
361
419
|
}
|
|
362
420
|
|
|
363
|
-
|
|
364
|
-
|
|
421
|
+
export default WritablePartition;
|
|
422
|
+
export { CorruptFileError };
|
package/src/Partition.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import WritablePartition, { CorruptFileError } from './Partition/WritablePartition.js';
|
|
2
|
+
import ReadOnlyPartition from './Partition/ReadOnlyPartition.js';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
WritablePartition.ReadOnly = ReadOnlyPartition;
|
|
5
|
+
WritablePartition.CorruptFileError = CorruptFileError;
|
|
6
|
+
|
|
7
|
+
export default WritablePartition;
|
|
8
|
+
export { ReadOnlyPartition as ReadOnly, CorruptFileError };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import ReadableStorage from './ReadableStorage.js';
|
|
2
|
+
import ReadablePartition from '../Partition/ReadablePartition.js';
|
|
3
|
+
import Watcher from '../Watcher.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* An append-only storage with highly performant positional range scans.
|
|
@@ -113,4 +113,4 @@ class ReadOnlyStorage extends ReadableStorage {
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
export default ReadOnlyStorage;
|