event-storage 1.0.0 → 1.2.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 +24 -0
- package/index.js +1 -1
- package/package.json +3 -4
- package/src/Consumer.js +9 -6
- package/src/EventStore.js +336 -80
- package/src/EventStream.js +73 -11
- package/src/Index/ReadableIndex.js +7 -5
- package/src/Index/WritableIndex.js +4 -4
- package/src/IndexMatcher.js +205 -0
- package/src/JoinEventStream.js +44 -46
- package/src/Partition/ReadablePartition.js +153 -85
- package/src/Partition/WritablePartition.js +37 -26
- package/src/PartitionPool.js +149 -0
- package/src/Storage/ReadOnlyStorage.js +5 -5
- package/src/Storage/ReadableStorage.js +203 -140
- package/src/Storage/WritableStorage.js +81 -45
- package/src/Watcher.js +1 -1
- package/src/utils/fsUtil.js +123 -0
- package/src/utils/jsonUtil.js +87 -0
- package/src/utils/metadataUtil.js +247 -0
- package/src/utils/util.js +202 -0
- package/src/metadataUtil.js +0 -79
- package/src/util.js +0 -218
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import events from 'events';
|
|
4
|
-
import { assert, alignTo, hash, binarySearch } from '../util.js';
|
|
4
|
+
import { assert, alignTo, hash, binarySearch } from '../utils/util.js';
|
|
5
|
+
|
|
6
|
+
|
|
5
7
|
|
|
6
8
|
const DEFAULT_READ_BUFFER_SIZE = 64 * 1024;
|
|
7
9
|
const DOCUMENT_HEADER_SIZE = 16;
|
|
@@ -114,10 +116,10 @@ class ReadablePartition extends events.EventEmitter {
|
|
|
114
116
|
fs.readSync(this.fd, headerBuffer, 0, 8 + 4, 0);
|
|
115
117
|
const headerMagic = headerBuffer.toString('utf8', 0, 8);
|
|
116
118
|
|
|
117
|
-
assert(headerMagic.
|
|
119
|
+
assert(headerMagic.substring(0, 6) === HEADER_MAGIC.substring(0, 6), `Invalid file header in partition ${this.name}.`);
|
|
118
120
|
|
|
119
121
|
this.header = headerMagic;
|
|
120
|
-
assert(headerMagic === HEADER_MAGIC, `Invalid file version. The partition ${this.name} was created with a different library version (${headerMagic.
|
|
122
|
+
assert(headerMagic === HEADER_MAGIC, `Invalid file version. The partition ${this.name} was created with a different library version (${headerMagic.substring(6)}).`);
|
|
121
123
|
|
|
122
124
|
const metadataSize = headerBuffer.readUInt32BE(8);
|
|
123
125
|
assert(metadataSize > 2 && metadataSize <= 4096, 'Invalid metadata size.');
|
|
@@ -200,51 +202,35 @@ class ReadablePartition extends events.EventEmitter {
|
|
|
200
202
|
const dataSize = buffer.readUInt32BE(offset + 0);
|
|
201
203
|
assert(dataSize > 0 && dataSize <= 64 * 1024 * 1024, `Error reading document size from ${position}, got ${dataSize}.`);
|
|
202
204
|
|
|
203
|
-
|
|
204
|
-
throw new InvalidDataSizeError(`Invalid document size ${dataSize} at position ${position}, expected ${size}.`);
|
|
205
|
-
}
|
|
205
|
+
assert(!size || dataSize === size, `Invalid document size ${dataSize} at position ${position}, expected ${size}.`, InvalidDataSizeError);
|
|
206
206
|
|
|
207
207
|
const sequenceNumber = buffer.readUInt32BE(offset + 4);
|
|
208
208
|
const time64 = buffer.readDoubleBE(offset + 8);
|
|
209
209
|
return ({ dataSize, sequenceNumber, time64 });
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
*
|
|
215
|
-
* @protected
|
|
216
|
-
* @param {number} position The position in the file to prepare the read buffer for reading from.
|
|
217
|
-
* @returns {{ buffer: Buffer|null, cursor: number, length: number }} A reader object with properties `buffer`, `cursor` and `length`.
|
|
218
|
-
*/
|
|
219
|
-
prepareReadBuffer(position) {
|
|
220
|
-
if (position + DOCUMENT_HEADER_SIZE >= this.size) {
|
|
221
|
-
return ({ buffer: null, cursor: 0, length: 0 });
|
|
222
|
-
}
|
|
223
|
-
let bufferCursor = position - this.readBufferPos;
|
|
224
|
-
if (this.readBufferPos < 0 || bufferCursor < 0 || bufferCursor + DOCUMENT_HEADER_SIZE + DOCUMENT_ALIGNMENT > this.readBufferLength) {
|
|
225
|
-
this.fillBuffer(position);
|
|
226
|
-
bufferCursor = 0;
|
|
227
|
-
}
|
|
228
|
-
return ({ buffer: this.readBuffer, cursor: bufferCursor, length: this.readBufferLength });
|
|
212
|
+
resolveIterationPosition(position) {
|
|
213
|
+
return position < 0 ? this.size + position + 1 : position;
|
|
229
214
|
}
|
|
230
215
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
* @returns {{ buffer: Buffer|null, cursor: number, length: number }} A reader object with properties `buffer`, `cursor` and `length`.
|
|
237
|
-
*/
|
|
238
|
-
prepareReadBufferBackwards(position) {
|
|
239
|
-
if (position < 0) {
|
|
240
|
-
return ({ buffer: null, cursor: 0, length: 0 });
|
|
216
|
+
selectReader(position, size, backwardsHint) {
|
|
217
|
+
if (size > 0 && backwardsHint) {
|
|
218
|
+
const bufferOffset = DOCUMENT_HEADER_SIZE + size;
|
|
219
|
+
const reader = this.prepareReadBufferBackwards(position + bufferOffset, bufferOffset);
|
|
220
|
+
return { reader, bufferOffset };
|
|
241
221
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
222
|
+
return { reader: this.prepareReadBuffer(position), bufferOffset: 0 };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
assignHeaderOutput(headerOut, header) {
|
|
226
|
+
if (headerOut === null) {
|
|
227
|
+
return;
|
|
246
228
|
}
|
|
247
|
-
|
|
229
|
+
headerOut.dataSize = header.dataSize;
|
|
230
|
+
headerOut.sequenceNumber = header.sequenceNumber;
|
|
231
|
+
// Denormalize time64 relative to this partition's epoch so callers can compare
|
|
232
|
+
// timestamps across partitions without needing to know the epoch value.
|
|
233
|
+
headerOut.time64 = this.metadata.epoch + header.time64;
|
|
248
234
|
}
|
|
249
235
|
|
|
250
236
|
/**
|
|
@@ -255,39 +241,37 @@ class ReadablePartition extends events.EventEmitter {
|
|
|
255
241
|
* @param {number} [size] The expected byte size of the document at the given position.
|
|
256
242
|
* @param {object|null} [headerOut] Optional object to populate with the document header fields
|
|
257
243
|
* (`dataSize`, `sequenceNumber`, `time64`). Pass an existing object to avoid extra allocation.
|
|
258
|
-
* @
|
|
244
|
+
* @param {boolean} [backwardsHint] If set to true, will optimize buffering for backwards reads.
|
|
245
|
+
* @returns {Buffer|boolean} The data stored at the given position or false if no data could be read.
|
|
259
246
|
* @throws {Error} if the storage entry at the given position is corrupted.
|
|
260
247
|
* @throws {InvalidDataSizeError} if the document size at the given position does not match the provided size.
|
|
261
248
|
* @throws {CorruptFileError} if the document at the given position can not be read completely.
|
|
262
249
|
*/
|
|
263
|
-
readFrom(position, size = 0, headerOut = null) {
|
|
250
|
+
readFrom(position, size = 0, headerOut = null, backwardsHint = false) {
|
|
264
251
|
assert(this.fd, 'Partition is not opened.');
|
|
265
252
|
assert((position % DOCUMENT_ALIGNMENT) === 0, `Invalid read position ${position}. Needs to be a multiple of ${DOCUMENT_ALIGNMENT}.`);
|
|
266
253
|
|
|
267
|
-
const reader = this.
|
|
268
|
-
if (reader.length <
|
|
254
|
+
const { reader, bufferOffset } = this.selectReader(position, size, backwardsHint);
|
|
255
|
+
if (reader.length < DOCUMENT_HEADER_SIZE) {
|
|
269
256
|
return false;
|
|
270
257
|
}
|
|
271
258
|
|
|
259
|
+
// prepareReadBufferBackwards positions the cursor at position + bufferOffset (the end of
|
|
260
|
+
// the document data), so the previous document in file order lands inside the buffer on the next
|
|
261
|
+
// backwards read. Adjust the cursor back to the document header start before reading.
|
|
262
|
+
reader.cursor -= bufferOffset;
|
|
272
263
|
let dataPosition = reader.cursor + DOCUMENT_HEADER_SIZE;
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
headerOut.sequenceNumber = sequenceNumber;
|
|
277
|
-
headerOut.time64 = time64;
|
|
278
|
-
}
|
|
264
|
+
const header = this.readDocumentHeader(reader.buffer, reader.cursor, position, size);
|
|
265
|
+
const dataSize = header.dataSize;
|
|
266
|
+
this.assignHeaderOutput(headerOut, header);
|
|
279
267
|
|
|
280
|
-
// TODO: This should only be checked on opening
|
|
281
268
|
const writeSize = this.documentWriteSize(dataSize);
|
|
282
|
-
|
|
283
|
-
throw new CorruptFileError(`Invalid document at position ${position}. This may be caused by an unfinished write.`);
|
|
284
|
-
}
|
|
269
|
+
assert(position + writeSize <= this.size, `Invalid document at position ${position}. This may be caused by an unfinished write.`, CorruptFileError);
|
|
285
270
|
|
|
286
271
|
if (dataSize + DOCUMENT_HEADER_SIZE > reader.buffer.byteLength) {
|
|
287
|
-
//console.log('sync read for large document size', dataLength, 'at position', position);
|
|
288
272
|
const tempReadBuffer = Buffer.allocUnsafe(dataSize);
|
|
289
273
|
fs.readSync(this.fd, tempReadBuffer, 0, dataSize, this.headerSize + position + DOCUMENT_HEADER_SIZE);
|
|
290
|
-
return tempReadBuffer
|
|
274
|
+
return tempReadBuffer;
|
|
291
275
|
}
|
|
292
276
|
|
|
293
277
|
if (reader.cursor > 0 && dataPosition + dataSize > reader.length) {
|
|
@@ -295,7 +279,48 @@ class ReadablePartition extends events.EventEmitter {
|
|
|
295
279
|
dataPosition = DOCUMENT_HEADER_SIZE;
|
|
296
280
|
}
|
|
297
281
|
|
|
298
|
-
|
|
282
|
+
// reader.buffer is a shared buffer filled by fillBuffer; callers must consume the returned
|
|
283
|
+
// view before the next readFrom call (which may fill the same buffer region).
|
|
284
|
+
return reader.buffer.subarray(dataPosition, dataPosition + dataSize);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Prepare the read buffer for reading from the specified position.
|
|
289
|
+
*
|
|
290
|
+
* @protected
|
|
291
|
+
* @param {number} position The position in the file to prepare the read buffer for reading from.
|
|
292
|
+
* @returns {{ buffer: Buffer|null, cursor: number, length: number }} A reader object with properties `buffer`, `cursor` and `length`.
|
|
293
|
+
*/
|
|
294
|
+
prepareReadBuffer(position) {
|
|
295
|
+
if (position + DOCUMENT_HEADER_SIZE >= this.size) {
|
|
296
|
+
return ({ buffer: null, cursor: 0, length: 0 });
|
|
297
|
+
}
|
|
298
|
+
let bufferCursor = position - this.readBufferPos;
|
|
299
|
+
if (this.readBufferPos < 0 || bufferCursor < 0 || bufferCursor + DOCUMENT_HEADER_SIZE + DOCUMENT_ALIGNMENT > this.readBufferLength) {
|
|
300
|
+
this.fillBuffer(position);
|
|
301
|
+
bufferCursor = 0;
|
|
302
|
+
}
|
|
303
|
+
return ({ buffer: this.readBuffer, cursor: bufferCursor, length: this.readBufferLength });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Prepare the read buffer for reading *before* the specified position. Don't try to read *after* the returned cursor.
|
|
308
|
+
*
|
|
309
|
+
* @protected
|
|
310
|
+
* @param {number} position The position in the file to prepare the read buffer for reading before.
|
|
311
|
+
* @param {number} [size] The amount of bytes that need to be buffered before position. By default, only guarantees that the document footer can be read.
|
|
312
|
+
* @returns {{ buffer: Buffer|null, cursor: number, length: number }} A reader object with properties `buffer`, `cursor` and `length`.
|
|
313
|
+
*/
|
|
314
|
+
prepareReadBufferBackwards(position, size = 0) {
|
|
315
|
+
if (position < 0) {
|
|
316
|
+
return ({ buffer: null, cursor: 0, length: 0 });
|
|
317
|
+
}
|
|
318
|
+
let bufferCursor = position - this.readBufferPos;
|
|
319
|
+
if (this.readBufferPos < 0 || (this.readBufferPos > 0 && bufferCursor < size + DOCUMENT_FOOTER_SIZE) || bufferCursor > this.readBufferLength) {
|
|
320
|
+
this.fillBuffer(Math.max(position - this.readBuffer.byteLength, 0));
|
|
321
|
+
bufferCursor = position - this.readBufferPos;
|
|
322
|
+
}
|
|
323
|
+
return ({ buffer: this.readBuffer, cursor: bufferCursor, length: this.readBufferLength });
|
|
299
324
|
}
|
|
300
325
|
|
|
301
326
|
/**
|
|
@@ -307,7 +332,7 @@ class ReadablePartition extends events.EventEmitter {
|
|
|
307
332
|
*/
|
|
308
333
|
findDocumentPositionBefore(position) {
|
|
309
334
|
assert(this.fd, 'Partition is not opened.');
|
|
310
|
-
position -= (position % DOCUMENT_ALIGNMENT);
|
|
335
|
+
if (position > 0) position -= (position % DOCUMENT_ALIGNMENT);
|
|
311
336
|
if (position <= 0) {
|
|
312
337
|
return false;
|
|
313
338
|
}
|
|
@@ -373,49 +398,49 @@ class ReadablePartition extends events.EventEmitter {
|
|
|
373
398
|
}
|
|
374
399
|
|
|
375
400
|
/**
|
|
376
|
-
* Find the
|
|
401
|
+
* Find a document around the target sequence number using one of two binary-search modes.
|
|
377
402
|
* Uses readLast() to short-circuit when the partition contains no such document.
|
|
378
403
|
* Uses a binary search over file positions via readDocumentBefore() to locate the
|
|
379
|
-
* document
|
|
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.
|
|
404
|
+
* document and tracks nearest candidates on both sides of the target.
|
|
383
405
|
*
|
|
384
406
|
* @api
|
|
385
407
|
* @param {number} sequenceNumber The 0-based sequence number to search for.
|
|
386
|
-
* @
|
|
387
|
-
*
|
|
408
|
+
* @param {boolean} [min=true] When true, returns the first document with sequenceNumber >= target.
|
|
409
|
+
* When false, returns the last document with sequenceNumber <= target.
|
|
410
|
+
* @returns {{ data: Buffer, header: object, position: number }|null}
|
|
411
|
+
* The matched document with its header and position, or null if no such document exists.
|
|
388
412
|
*/
|
|
389
|
-
findDocument(sequenceNumber) {
|
|
413
|
+
findDocument(sequenceNumber, min = true) {
|
|
390
414
|
const last = this.readLast();
|
|
391
|
-
if (!last
|
|
415
|
+
if (!last) {
|
|
392
416
|
return null;
|
|
393
417
|
}
|
|
394
418
|
|
|
395
|
-
|
|
396
|
-
|
|
419
|
+
if (min && last.header.sequenceNumber < sequenceNumber) {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const [low, high] = binarySearch(
|
|
397
424
|
sequenceNumber,
|
|
398
425
|
this.size,
|
|
399
426
|
(pos) => {
|
|
400
427
|
const doc = this.readDocumentBefore(pos);
|
|
401
|
-
|
|
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;
|
|
428
|
+
return doc ? doc.header.sequenceNumber : Number.MIN_SAFE_INTEGER;
|
|
408
429
|
}
|
|
409
430
|
);
|
|
410
431
|
|
|
411
|
-
const
|
|
412
|
-
|
|
432
|
+
const position = this.findDocumentPositionBefore(min ? low : high);
|
|
433
|
+
if (position === false || position < 0) {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const header = {};
|
|
438
|
+
const data = this.readFrom(position, 0, header);
|
|
413
439
|
/* istanbul ignore if */
|
|
414
440
|
if (data === false) {
|
|
415
441
|
return null;
|
|
416
442
|
}
|
|
417
|
-
|
|
418
|
-
return { headerOut, data };
|
|
443
|
+
return { data, header, position };
|
|
419
444
|
}
|
|
420
445
|
|
|
421
446
|
/**
|
|
@@ -424,10 +449,10 @@ class ReadablePartition extends events.EventEmitter {
|
|
|
424
449
|
* @param {object|null} [headerOut] Optional object to populate with document header fields
|
|
425
450
|
* (`dataSize`, `sequenceNumber`, `time64`, `position`) on each yield. Pass an existing object
|
|
426
451
|
* to avoid extra allocation. The object is mutated in place before each yield.
|
|
427
|
-
* @returns {Generator<
|
|
452
|
+
* @returns {Generator<Buffer>} A generator that returns all documents in this partition.
|
|
428
453
|
*/
|
|
429
454
|
*readAll(after = 0, headerOut = null) {
|
|
430
|
-
let position =
|
|
455
|
+
let position = this.resolveIterationPosition(after);
|
|
431
456
|
const internalHeader = headerOut !== null ? headerOut : {};
|
|
432
457
|
let data;
|
|
433
458
|
while ((data = this.readFrom(position, 0, internalHeader)) !== false) {
|
|
@@ -442,16 +467,59 @@ class ReadablePartition extends events.EventEmitter {
|
|
|
442
467
|
/**
|
|
443
468
|
* @api
|
|
444
469
|
* @param {number} [before] The document position to start reading backward from.
|
|
445
|
-
* @
|
|
470
|
+
* @param {object|null} [headerOut] Optional object to populate with document header fields
|
|
471
|
+
* (`dataSize`, `sequenceNumber`, `time64`, `position`) on each yield.
|
|
472
|
+
* @returns {Generator<Buffer>} A generator that returns all documents in this partition in reverse order.
|
|
446
473
|
*/
|
|
447
|
-
*readAllBackwards(before = -1) {
|
|
448
|
-
let position =
|
|
474
|
+
*readAllBackwards(before = -1, headerOut = null) {
|
|
475
|
+
let position = this.resolveIterationPosition(before);
|
|
476
|
+
const internalHeader = headerOut !== null ? headerOut : {};
|
|
449
477
|
while ((position = this.findDocumentPositionBefore(position)) !== false) {
|
|
450
|
-
const data = this.readFrom(position);
|
|
478
|
+
const data = this.readFrom(position, 0, internalHeader, true);
|
|
479
|
+
if (headerOut !== null) {
|
|
480
|
+
headerOut.position = position;
|
|
481
|
+
}
|
|
451
482
|
yield data;
|
|
452
483
|
}
|
|
453
484
|
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Read documents with sequenceNumber in the inclusive range [from, until].
|
|
488
|
+
* If from > until, documents are yielded in reverse order.
|
|
489
|
+
*
|
|
490
|
+
* @api
|
|
491
|
+
* @param {number} [from=0]
|
|
492
|
+
* @param {number} [until=Number.MAX_SAFE_INTEGER]
|
|
493
|
+
* @returns {Generator<{ data: Buffer, header: object, entry: object }>}
|
|
494
|
+
*/
|
|
495
|
+
*readRange(from = 0, until = Number.MAX_SAFE_INTEGER) {
|
|
496
|
+
const forwards = from <= until;
|
|
497
|
+
const lo = Math.min(from, until);
|
|
498
|
+
const hi = Math.max(from, until);
|
|
499
|
+
const found = this.findDocument(forwards ? lo : hi, forwards);
|
|
500
|
+
if (!found || found.header.sequenceNumber < lo || found.header.sequenceNumber > hi) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const entry = { partition: this.id };
|
|
505
|
+
const header = {};
|
|
506
|
+
const iterator = forwards
|
|
507
|
+
? this.readAll(found.position, header)
|
|
508
|
+
: this.readAllBackwards(found.position + this.documentWriteSize(found.header.dataSize), header);
|
|
509
|
+
|
|
510
|
+
for (const data of iterator) {
|
|
511
|
+
if (header.sequenceNumber < lo || header.sequenceNumber > hi) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
entry.number = header.sequenceNumber;
|
|
515
|
+
entry.position = header.position;
|
|
516
|
+
entry.size = header.dataSize;
|
|
517
|
+
yield { data, header, entry };
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
|
|
454
522
|
}
|
|
455
523
|
|
|
456
524
|
export default ReadablePartition;
|
|
457
|
-
export { CorruptFileError, InvalidDataSizeError, HEADER_MAGIC, DOCUMENT_SEPARATOR, DOCUMENT_ALIGNMENT, DOCUMENT_HEADER_SIZE, DOCUMENT_FOOTER_SIZE };
|
|
525
|
+
export { CorruptFileError, InvalidDataSizeError, HEADER_MAGIC, DOCUMENT_SEPARATOR, DOCUMENT_ALIGNMENT, DOCUMENT_HEADER_SIZE, DOCUMENT_FOOTER_SIZE };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import ReadablePartition, { CorruptFileError, HEADER_MAGIC, DOCUMENT_ALIGNMENT, DOCUMENT_SEPARATOR, DOCUMENT_HEADER_SIZE, DOCUMENT_FOOTER_SIZE } from './ReadablePartition.js';
|
|
3
|
-
import { assert,
|
|
3
|
+
import { assert, alignTo } from '../utils/util.js';
|
|
4
|
+
import { buildMetadataHeader } from '../utils/metadataUtil.js';
|
|
5
|
+
import { ensureDirectory } from '../utils/fsUtil.js';
|
|
4
6
|
import Clock from '../Clock.js';
|
|
5
7
|
|
|
6
8
|
const DEFAULT_WRITE_BUFFER_SIZE = 16 * 1024;
|
|
@@ -175,22 +177,34 @@ class WritablePartition extends ReadablePartition {
|
|
|
175
177
|
* @returns {number} The size of the document header
|
|
176
178
|
*/
|
|
177
179
|
writeDocumentHeader(buffer, offset, dataSize, sequenceNumber = null, time64 = null) {
|
|
178
|
-
|
|
179
|
-
sequenceNumber = 0;
|
|
180
|
-
}
|
|
181
|
-
if (time64 === null) {
|
|
182
|
-
time64 = this.clock.time();
|
|
183
|
-
}
|
|
180
|
+
({ sequenceNumber, time64 } = this.normalizeWriteMetadata(sequenceNumber, time64));
|
|
184
181
|
/* istanbul ignore if */
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
182
|
+
assert(time64 >= 0, 'Time may not be negative!');
|
|
183
|
+
|
|
188
184
|
buffer.writeUInt32BE(dataSize, offset);
|
|
189
185
|
buffer.writeUInt32BE(sequenceNumber, offset + 4);
|
|
190
186
|
buffer.writeDoubleBE(time64, offset + 8);
|
|
191
187
|
return DOCUMENT_HEADER_SIZE;
|
|
192
188
|
}
|
|
193
189
|
|
|
190
|
+
normalizeWriteMetadata(sequenceNumber, time64) {
|
|
191
|
+
return {
|
|
192
|
+
sequenceNumber: sequenceNumber === null ? 0 : sequenceNumber,
|
|
193
|
+
time64: time64 === null ? this.clock.time() : time64
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
normalizeWriteArguments(sequenceNumber, callback) {
|
|
198
|
+
if (typeof sequenceNumber === 'function') {
|
|
199
|
+
return { sequenceNumber: null, callback: sequenceNumber };
|
|
200
|
+
}
|
|
201
|
+
return { sequenceNumber, callback };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
shouldWriteUnbuffered(dataSize) {
|
|
205
|
+
return dataSize + DOCUMENT_HEADER_SIZE >= this.writeBuffer.byteLength * 4 / 5;
|
|
206
|
+
}
|
|
207
|
+
|
|
194
208
|
/**
|
|
195
209
|
* Write the given data to the partition without buffering.
|
|
196
210
|
* @private
|
|
@@ -208,7 +222,7 @@ class WritablePartition extends ReadablePartition {
|
|
|
208
222
|
bytesWritten += fs.writeSync(this.fd, this.writeMetaBuffer, 0, DOCUMENT_HEADER_SIZE);
|
|
209
223
|
bytesWritten += fs.writeSync(this.fd, data);
|
|
210
224
|
const padSize = alignTo(dataSize + DOCUMENT_FOOTER_SIZE, DOCUMENT_ALIGNMENT);
|
|
211
|
-
bytesWritten += fs.writeSync(this.fd, DOCUMENT_PAD.
|
|
225
|
+
bytesWritten += fs.writeSync(this.fd, DOCUMENT_PAD.substring(0, padSize));
|
|
212
226
|
this.writeMetaBuffer.writeUInt32BE(dataSize, 0);
|
|
213
227
|
bytesWritten += fs.writeSync(this.fd, this.writeMetaBuffer, 0, 4);
|
|
214
228
|
bytesWritten += fs.writeSync(this.fd, DOCUMENT_SEPARATOR);
|
|
@@ -235,7 +249,7 @@ class WritablePartition extends ReadablePartition {
|
|
|
235
249
|
bytesWritten += this.writeDocumentHeader(this.writeBuffer, this.writeBufferCursor, dataSize, sequenceNumber);
|
|
236
250
|
bytesWritten += this.writeBuffer.write(data, this.writeBufferCursor + bytesWritten, 'utf8');
|
|
237
251
|
const padSize = alignTo(dataSize + DOCUMENT_FOOTER_SIZE, DOCUMENT_ALIGNMENT);
|
|
238
|
-
bytesWritten += this.writeBuffer.write(DOCUMENT_PAD.
|
|
252
|
+
bytesWritten += this.writeBuffer.write(DOCUMENT_PAD.substring(0, padSize), this.writeBufferCursor + bytesWritten, 'utf8');
|
|
239
253
|
this.writeBuffer.writeUInt32BE(dataSize, this.writeBufferCursor + bytesWritten);
|
|
240
254
|
bytesWritten += 4;
|
|
241
255
|
bytesWritten += this.writeBuffer.write(DOCUMENT_SEPARATOR, this.writeBufferCursor + bytesWritten, 'utf8');
|
|
@@ -257,15 +271,12 @@ class WritablePartition extends ReadablePartition {
|
|
|
257
271
|
*/
|
|
258
272
|
write(data, sequenceNumber, callback) {
|
|
259
273
|
assert(this.fd, 'Partition is not opened.');
|
|
260
|
-
|
|
261
|
-
callback = sequenceNumber;
|
|
262
|
-
sequenceNumber = null;
|
|
263
|
-
}
|
|
274
|
+
({ sequenceNumber, callback } = this.normalizeWriteArguments(sequenceNumber, callback));
|
|
264
275
|
const dataSize = Buffer.byteLength(data, 'utf8');
|
|
265
276
|
assert(dataSize <= 64 * 1024 * 1024, 'Document is too large! Maximum is 64 MB');
|
|
266
277
|
|
|
267
278
|
const dataPosition = this.size;
|
|
268
|
-
if (
|
|
279
|
+
if (this.shouldWriteUnbuffered(dataSize)) {
|
|
269
280
|
this.size += this.writeUnbuffered(data, dataSize, sequenceNumber, callback);
|
|
270
281
|
} else {
|
|
271
282
|
this.size += this.writeBuffered(data, dataSize, sequenceNumber, callback);
|
|
@@ -298,15 +309,16 @@ class WritablePartition extends ReadablePartition {
|
|
|
298
309
|
*
|
|
299
310
|
* @protected
|
|
300
311
|
* @param {number} position The position in the file to prepare the read buffer for reading before.
|
|
312
|
+
* @param {number} [size] The amount of bytes that need to be buffered before position. By default, only guarantees that the document footer can be read.
|
|
301
313
|
* @returns {object} A reader object with properties `buffer`, `cursor` and `length`.
|
|
302
314
|
*/
|
|
303
|
-
prepareReadBufferBackwards(position) {
|
|
315
|
+
prepareReadBufferBackwards(position, size = 0) {
|
|
304
316
|
const bufferPos = this.size - this.writeBufferCursor;
|
|
305
317
|
// Handle the case when data that is still in write buffer is supposed to be read backwards
|
|
306
318
|
if (this.dirtyReads && this.writeBufferCursor > 0 && position > bufferPos) {
|
|
307
319
|
return { buffer: this.writeBuffer, cursor: position - bufferPos, length: this.writeBufferCursor };
|
|
308
320
|
}
|
|
309
|
-
return super.prepareReadBufferBackwards(position);
|
|
321
|
+
return super.prepareReadBufferBackwards(position, size);
|
|
310
322
|
}
|
|
311
323
|
|
|
312
324
|
/**
|
|
@@ -315,16 +327,17 @@ class WritablePartition extends ReadablePartition {
|
|
|
315
327
|
*
|
|
316
328
|
* @api
|
|
317
329
|
* @param {number} [before] The document position to start reading backward from.
|
|
318
|
-
* @
|
|
330
|
+
* @param {object|null} [headerOut] Optional object to populate with document header fields on each yield.
|
|
331
|
+
* @returns {Generator<Buffer>} A generator that returns all documents in this partition in reverse order.
|
|
319
332
|
*/
|
|
320
|
-
*readAllBackwards(before = -1) {
|
|
333
|
+
*readAllBackwards(before = -1, headerOut = null) {
|
|
321
334
|
if (!this.dirtyReads && this.writeBufferCursor > 0) {
|
|
322
335
|
const flushedSize = this.size - this.writeBufferCursor;
|
|
323
336
|
const clampedBefore = before < 0 ? flushedSize : Math.min(before, flushedSize);
|
|
324
|
-
yield* super.readAllBackwards(clampedBefore);
|
|
337
|
+
yield* super.readAllBackwards(clampedBefore, headerOut);
|
|
325
338
|
return;
|
|
326
339
|
}
|
|
327
|
-
yield* super.readAllBackwards(before);
|
|
340
|
+
yield* super.readAllBackwards(before, headerOut);
|
|
328
341
|
}
|
|
329
342
|
|
|
330
343
|
/**
|
|
@@ -386,9 +399,7 @@ class WritablePartition extends ReadablePartition {
|
|
|
386
399
|
try {
|
|
387
400
|
this.readFrom(after);
|
|
388
401
|
} catch (e) {
|
|
389
|
-
|
|
390
|
-
throw new Error('Can only truncate on valid document boundaries.');
|
|
391
|
-
}
|
|
402
|
+
assert(e instanceof CorruptFileError, 'Can only truncate on valid document boundaries.');
|
|
392
403
|
}
|
|
393
404
|
|
|
394
405
|
fs.truncateSync(this.fileName, this.headerSize + after);
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A fixed-capacity registry of partitions with LRU eviction of open file handles.
|
|
3
|
+
*
|
|
4
|
+
* All partitions are stored by their numeric id and may be queried at any time.
|
|
5
|
+
* The pool additionally tracks which partitions currently have an open file descriptor
|
|
6
|
+
* in LRU (least-recently-used) order. When the pool is asked to open a partition and
|
|
7
|
+
* doing so would exceed the configured cap, the least-recently-used open partition is
|
|
8
|
+
* closed first to stay within the limit.
|
|
9
|
+
*
|
|
10
|
+
* Setting the cap to 0 disables eviction: all partitions are allowed to remain open
|
|
11
|
+
* simultaneously, which matches the uncapped behaviour of the original implementation.
|
|
12
|
+
*/
|
|
13
|
+
class PartitionPool {
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {number} [maxOpen=0] Maximum number of simultaneously open partition file
|
|
17
|
+
* handles. 0 disables the limit (no eviction).
|
|
18
|
+
*/
|
|
19
|
+
constructor(maxOpen = 0) {
|
|
20
|
+
this.maxOpen = maxOpen;
|
|
21
|
+
/** Registry of all known partitions keyed by id. */
|
|
22
|
+
this.registry = Object.create(null);
|
|
23
|
+
/**
|
|
24
|
+
* Insertion-order map used for LRU tracking of open file handles.
|
|
25
|
+
* Key = partition id, value = true.
|
|
26
|
+
* Oldest (least-recently-used) entry is first; newest (most-recently-used) is last.
|
|
27
|
+
*/
|
|
28
|
+
this.handles = new Map();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Register a partition under the given id.
|
|
33
|
+
*
|
|
34
|
+
* @param {number|string} id
|
|
35
|
+
* @param {object} partition
|
|
36
|
+
*/
|
|
37
|
+
add(id, partition) {
|
|
38
|
+
this.registry[id] = partition;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Retrieve a registered partition without opening it.
|
|
43
|
+
*
|
|
44
|
+
* @param {number|string} id
|
|
45
|
+
* @returns {object|undefined}
|
|
46
|
+
*/
|
|
47
|
+
get(id) {
|
|
48
|
+
return this.registry[id];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check whether a partition with the given id is registered in the pool.
|
|
53
|
+
*
|
|
54
|
+
* @param {number|string} id
|
|
55
|
+
* @returns {boolean}
|
|
56
|
+
*/
|
|
57
|
+
has(id) {
|
|
58
|
+
return id in this.registry;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Open the partition with the given id, applying LRU eviction if necessary.
|
|
63
|
+
*
|
|
64
|
+
* If the partition is not yet open and adding it would exceed `maxOpen`, the
|
|
65
|
+
* least-recently-used open partition is closed first. Stale entries (partitions
|
|
66
|
+
* that were closed externally) are discarded from the LRU map as they are
|
|
67
|
+
* encountered; if all tracked entries turn out to be stale the loop exits without
|
|
68
|
+
* closing any partition — the handle count stays temporarily inflated (bounded by
|
|
69
|
+
* the number of external closes since the last `open()` call) but correctness is
|
|
70
|
+
* preserved.
|
|
71
|
+
*
|
|
72
|
+
* @param {number|string} id
|
|
73
|
+
* @returns {object} The opened partition.
|
|
74
|
+
*/
|
|
75
|
+
open(id) {
|
|
76
|
+
const partition = this.registry[id];
|
|
77
|
+
|
|
78
|
+
if (this.maxOpen > 0) {
|
|
79
|
+
// Remove id first — this may already bring the handle count below the cap.
|
|
80
|
+
this.handles.delete(id);
|
|
81
|
+
if (this.handles.size >= this.maxOpen) {
|
|
82
|
+
for (const [lruId] of this.handles) {
|
|
83
|
+
this.handles.delete(lruId);
|
|
84
|
+
const lruPartition = this.registry[lruId];
|
|
85
|
+
if (lruPartition && lruPartition.isOpen()) {
|
|
86
|
+
lruPartition.close();
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// (Re-)add id at the MRU end of the map.
|
|
92
|
+
this.handles.set(id, true);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
partition.open();
|
|
96
|
+
return partition;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Invoke `callback` for every registered partition.
|
|
101
|
+
*
|
|
102
|
+
* @param {function(object): void} callback
|
|
103
|
+
*/
|
|
104
|
+
forEach(callback) {
|
|
105
|
+
for (const id of Object.keys(this.registry)) {
|
|
106
|
+
callback(this.registry[id]);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Yield every registered partition object.
|
|
112
|
+
*
|
|
113
|
+
* @returns {Generator<object>}
|
|
114
|
+
*/
|
|
115
|
+
*values() {
|
|
116
|
+
for (const id of Object.keys(this.registry)) {
|
|
117
|
+
yield this.registry[id];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The total number of registered partitions.
|
|
123
|
+
* @returns {number}
|
|
124
|
+
*/
|
|
125
|
+
get count() {
|
|
126
|
+
return Object.keys(this.registry).length;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The number of open partition file handles currently tracked by the pool.
|
|
131
|
+
* @returns {number}
|
|
132
|
+
*/
|
|
133
|
+
get openCount() {
|
|
134
|
+
return this.handles.size;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Reset the open-handle tracking without closing any partitions.
|
|
139
|
+
*
|
|
140
|
+
* Call this after externally closing all partitions (e.g. after
|
|
141
|
+
* `checkTornWrites`) to keep the pool's LRU state consistent with reality.
|
|
142
|
+
*/
|
|
143
|
+
clearOpenHandles() {
|
|
144
|
+
this.handles.clear();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export default PartitionPool;
|