event-storage 0.8.0 → 0.9.1

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.
@@ -2,16 +2,14 @@ const stream = require('stream');
2
2
  const { assert } = require('./util');
3
3
 
4
4
  /**
5
- * Adjusts a revision number from the EventStore/EventStream interface range into the underlying storage item number.
5
+ * Calculate the actual version number from a possibly relative (negative) version number.
6
6
  *
7
- * @param {number} rev A zero-based revision number, or a negative number to denote a "from the end" position
8
- * @returns {number} A one-based storage item number
7
+ * @param {number} version The version to normalize.
8
+ * @param {number} length The maximum version number
9
+ * @returns {number} The absolute version number.
9
10
  */
10
- function adjustedRevision(rev) {
11
- if (rev >= 0) {
12
- return rev + 1;
13
- }
14
- return rev;
11
+ function normalizeVersion(version, length) {
12
+ return version < 0 ? version + length + 1 : version;
15
13
  }
16
14
 
17
15
  /**
@@ -36,22 +34,163 @@ class EventStream extends stream.Readable {
36
34
  * @param {number} [minRevision] The minimum revision to include in the events (inclusive).
37
35
  * @param {number} [maxRevision] The maximum revision to include in the events (inclusive).
38
36
  */
39
- constructor(name, eventStore, minRevision = 0, maxRevision = -1) {
37
+ constructor(name, eventStore, minRevision = 1, maxRevision = -1) {
40
38
  super({ objectMode: true });
41
39
  assert(typeof name === 'string' && name !== '', 'Need to specify a stream name.');
42
40
  assert(typeof eventStore === 'object' && eventStore !== null, `Need to provide EventStore instance to create EventStream ${name}.`);
43
41
 
44
42
  this.name = name;
45
43
  if (eventStore.streams[name]) {
46
- const streamIndex = eventStore.streams[name].index;
47
- this.version = minVersion(streamIndex.length, maxRevision);
48
- minRevision = adjustedRevision(minRevision);
49
- maxRevision = adjustedRevision(maxRevision);
50
- this.iterator = eventStore.storage.readRange(minRevision, maxRevision, streamIndex);
44
+ this.streamIndex = eventStore.streams[name].index;
45
+ this.minRevision = normalizeVersion(minRevision, this.streamIndex.length);
46
+ this.maxRevision = normalizeVersion(maxRevision, this.streamIndex.length);
47
+ this.version = minVersion(this.streamIndex.length, maxRevision);
48
+ this._iterator = null;
49
+ this.fetch = function() {
50
+ return eventStore.storage.readRange(this.minRevision, this.maxRevision, this.streamIndex);
51
+ }
51
52
  } else {
53
+ this.streamIndex = { length: 0 };
52
54
  this.version = -1;
53
- this.iterator = { next() { return { done: true }; } };
55
+ this._iterator = { next() { return { done: true }; } };
56
+ }
57
+ }
58
+
59
+ /**
60
+ * @api
61
+ * @param {number} revision The event revision to start reading from (inclusive).
62
+ * @returns {EventStream}
63
+ */
64
+ from(revision) {
65
+ this.minRevision = normalizeVersion(revision, this.streamIndex.length);
66
+ return this;
67
+ }
68
+
69
+ /**
70
+ * @api
71
+ * @param {number} revision The event revision to read until (inclusive).
72
+ * @returns {EventStream}
73
+ */
74
+ until(revision) {
75
+ this.maxRevision = normalizeVersion(revision, this.streamIndex.length);
76
+ this.version = minVersion(this.streamIndex.length, this.maxRevision);
77
+ return this;
78
+ }
79
+
80
+ /**
81
+ * @api
82
+ * @param {number} amount The amount of events at the start of the stream to return in chronological order.
83
+ * @returns {EventStream}
84
+ */
85
+ first(amount) {
86
+ return this.fromStart().following(amount);
87
+ }
88
+
89
+ /**
90
+ * @api
91
+ * @param {number} amount The amount of events at the end of the stream to return in chronological order.
92
+ * @returns {EventStream}
93
+ */
94
+ last(amount) {
95
+ return this.fromEnd().previous(amount).forwards();
96
+ }
97
+
98
+ /**
99
+ * @api
100
+ * @returns {EventStream}
101
+ */
102
+ fromStart() {
103
+ this.minRevision = 1;
104
+ return this;
105
+ }
106
+
107
+ /**
108
+ * @api
109
+ * @returns {EventStream}
110
+ */
111
+ fromEnd() {
112
+ this.minRevision = this.streamIndex.length;
113
+ return this;
114
+ }
115
+
116
+ /**
117
+ * @param {number} amount The amount of events to return in reverse chronological order.
118
+ * @returns {EventStream}
119
+ */
120
+ previous(amount) {
121
+ this.maxRevision = Math.max(1, this.minRevision - amount + 1);
122
+ return this;
123
+ }
124
+
125
+ /**
126
+ * @param {number} amount The amount of events to return in chronological order.
127
+ * @returns {EventStream}
128
+ */
129
+ following(amount) {
130
+ this.maxRevision = Math.min(this.streamIndex.length, this.minRevision + amount - 1);
131
+ return this;
132
+ }
133
+
134
+ /**
135
+ * @api
136
+ * @returns {EventStream}
137
+ */
138
+ toEnd() {
139
+ this.maxRevision = this.version = this.streamIndex.length;
140
+ return this;
141
+ }
142
+
143
+ /**
144
+ * @api
145
+ * @returns {EventStream}
146
+ */
147
+ toStart() {
148
+ this.maxRevision = 1;
149
+ return this;
150
+ }
151
+
152
+ /**
153
+ * Reverse the current range of events, no matter which direction it currently has.
154
+ * @returns {EventStream}
155
+ */
156
+ reverse() {
157
+ let tmp = this.maxRevision;
158
+ this.maxRevision = this.minRevision;
159
+ this.minRevision = tmp;
160
+ this.version = minVersion(this.streamIndex.length, this.maxRevision);
161
+ return this;
162
+ }
163
+
164
+ /**
165
+ * Make the current range of events read in forward chronological order.
166
+ * @api
167
+ * @param {number} [amount] Amount of events to read forward. If not specified, will read forward until the previously set limit.
168
+ * @returns {EventStream}
169
+ */
170
+ forwards(amount = 0) {
171
+ if (amount > 0) {
172
+ this.following(amount);
173
+ }
174
+ if (this.maxRevision < this.minRevision) {
175
+ this.reverse();
54
176
  }
177
+ return this;
178
+ }
179
+
180
+ /**
181
+ * Make the current range of events read in backward chronological order.
182
+ * @api
183
+ * @param {number} [amount] Amount of events to read backward. If not specified, will read backward until the previously set limit.
184
+ * @returns {EventStream}
185
+ */
186
+ backwards(amount = 0) {
187
+ if (amount > 0) {
188
+ this.previous(amount);
189
+ }
190
+ if (this.maxRevision > this.minRevision) {
191
+ this.reverse();
192
+ }
193
+ return this;
55
194
  }
56
195
 
57
196
  /**
@@ -75,6 +214,7 @@ class EventStream extends stream.Readable {
75
214
  * Iterate over the events in this stream with a callback.
76
215
  * This method is useful to gain access to the event metadata.
77
216
  *
217
+ * @api
78
218
  * @param {function(object, object, string)} callback A callback function that will receive the event, the storage metadata and the original stream name for every event in this stream.
79
219
  */
80
220
  forEach(callback) {
@@ -94,13 +234,26 @@ class EventStream extends stream.Readable {
94
234
  }
95
235
  }
96
236
 
237
+ /**
238
+ * Reset this stream to the start so it can be iterated again.
239
+ * @returns {EventStream}
240
+ */
241
+ reset() {
242
+ this._iterator = null;
243
+ this._events = null;
244
+ return this;
245
+ }
246
+
97
247
  /**
98
248
  * @returns {object|boolean} The next event or false if no more events in the stream.
99
249
  */
100
250
  next() {
251
+ if (!this._iterator) {
252
+ this._iterator = this.fetch();
253
+ }
101
254
  let next;
102
255
  try {
103
- next = this.iterator.next();
256
+ next = this._iterator.next();
104
257
  } catch(e) {
105
258
  return false;
106
259
  }
@@ -72,7 +72,7 @@ class ReadableIndex extends events.EventEmitter {
72
72
  this.EntryClass = options.EntryClass;
73
73
  this.dataDirectory = options.dataDirectory;
74
74
  this.fileName = path.resolve(options.dataDirectory, this.name);
75
- this.readBuffer = Buffer.allocUnsafe(options.EntryClass.size);
75
+ this.readBuffer = Buffer.allocUnsafe(Math.max(options.EntryClass.size, options.writeBufferSize > 0 ? options.writeBufferSize : 4096));
76
76
 
77
77
  if (options.metadata) {
78
78
  this.metadata = Object.assign({entryClass: options.EntryClass.name, entrySize: options.EntryClass.size}, options.metadata);
@@ -281,8 +281,11 @@ class ReadableIndex extends events.EventEmitter {
281
281
  const readFrom = Math.max(this.readUntil + 1, from);
282
282
  const amount = (until - readFrom + 1);
283
283
 
284
- const readBuffer = Buffer.allocUnsafe(amount * this.EntryClass.size);
285
- let readSize = fs.readSync(this.fd, readBuffer, 0, readBuffer.byteLength, this.headerSize + readFrom * this.EntryClass.size);
284
+ const bufferSize = amount * this.EntryClass.size;
285
+ const readBuffer = bufferSize <= this.readBuffer.byteLength
286
+ ? this.readBuffer
287
+ : Buffer.allocUnsafe(bufferSize);
288
+ let readSize = fs.readSync(this.fd, readBuffer, 0, bufferSize, this.headerSize + readFrom * this.EntryClass.size);
286
289
  let index = 0;
287
290
  while (index < amount && readSize > 0) {
288
291
  this.data[index + readFrom] = this.EntryClass.fromBuffer(readBuffer, index * this.EntryClass.size);
@@ -388,6 +391,9 @@ class ReadableIndex extends events.EventEmitter {
388
391
  * @returns {number} The last index entry position that is lower than or equal to the `number`. Returns 0 if no index matches.
389
392
  */
390
393
  find(number, min = false) {
394
+ if (this.length < 1) {
395
+ return 0;
396
+ }
391
397
  // We only need to search until the searched number because entry.number is always >= position
392
398
  const [low, high] = binarySearch(number, Math.min(this.length, number), index => this.get(index).number);
393
399
  return min ? low : high;
@@ -156,8 +156,9 @@ class WritableIndex extends ReadableIndex {
156
156
  }
157
157
 
158
158
  this.writeBufferCursor = 0;
159
- this.flushCallbacks.forEach(callback => callback());
159
+ const callbacks = this.flushCallbacks;
160
160
  this.flushCallbacks = [];
161
+ for (let i = 0; i < callbacks.length; i += 2) callbacks[i](callbacks[i + 1]);
161
162
  return true;
162
163
  }
163
164
 
@@ -172,7 +173,7 @@ class WritableIndex extends ReadableIndex {
172
173
  if (typeof callback !== 'function') {
173
174
  return;
174
175
  }
175
- this.flushCallbacks.push(() => callback(position));
176
+ this.flushCallbacks.push(callback, position);
176
177
  }
177
178
 
178
179
  /**
@@ -187,10 +188,15 @@ class WritableIndex extends ReadableIndex {
187
188
  assertEqual(entry.constructor.name, this.EntryClass.name, `Wrong entry object.`);
188
189
  assertEqual(entry.constructor.size, this.EntryClass.size, `Invalid entry size.`);
189
190
 
190
- if (this.readUntil === this.data.length - 1) {
191
+ const dataLen = this.data.length;
192
+ if (dataLen > 0 && this.data[dataLen - 1].number >= entry.number) {
193
+ throw new Error('Consistency error. Tried to add an index that should come before existing last entry.');
194
+ }
195
+
196
+ if (this.readUntil === dataLen - 1) {
191
197
  this.readUntil++;
192
198
  }
193
- this.data[this.data.length] = entry;
199
+ this.data[dataLen] = entry;
194
200
 
195
201
  if (this.writeBufferCursor === 0) {
196
202
  this.flushTimeout = setTimeout(() => this.flush(), this.flushDelay);
@@ -1,18 +1,15 @@
1
1
  const EventStream = require('./EventStream');
2
+ const { wrapAndCheck } = require('./util');
2
3
 
3
4
  /**
4
- * Translates an EventStore revision number to an index sequence number and wraps it around the given length, if it's < 0
5
+ * Calculate the actual version number from a possibly relative (negative) version number.
5
6
  *
6
- * @param {number} rev The zero-based EventStore revision
7
- * @param {number} length The length of the store
8
- * @returns {number} The 1-based index sequence number
7
+ * @param {number} version The version to normalize.
8
+ * @param {number} length The maximum version number
9
+ * @returns {number} The absolute version number.
9
10
  */
10
- function wrapRevision(rev, length) {
11
- rev++;
12
- if (rev <= 0) {
13
- rev += length;
14
- }
15
- return rev;
11
+ function normalizeVersion(version, length) {
12
+ return version < 0 ? version + length + 1 : version;
16
13
  }
17
14
 
18
15
  /**
@@ -25,30 +22,32 @@ class JoinEventStream extends EventStream {
25
22
  * @param {string} name The name of the stream.
26
23
  * @param {Array<string>} streams The name of the streams to join together.
27
24
  * @param {EventStore} eventStore The event store to get the stream from.
28
- * @param {number} [minRevision] The minimum revision to include in the events (inclusive).
29
- * @param {number} [maxRevision] The maximum revision to include in the events (inclusive).
25
+ * @param {number} [minRevision] The 1-based minimum revision to include in the events (inclusive).
26
+ * @param {number} [maxRevision] The 1-based maximum revision to include in the events (inclusive).
30
27
  */
31
- constructor(name, streams, eventStore, minRevision = 0, maxRevision = -1) {
28
+ constructor(name, streams, eventStore, minRevision = 1, maxRevision = -1) {
32
29
  super(name, eventStore, minRevision, maxRevision);
33
30
  if (!(streams instanceof Array) || streams.length === 0) {
34
31
  throw new Error(`Invalid list of streams supplied to JoinStream ${name}.`);
35
32
  }
36
- this._next = new Array(streams.length).fill(undefined);
37
33
 
34
+ this.streamIndex = eventStore.storage.index;
38
35
  // Translate revisions to index numbers (1-based) and wrap around negatives
39
- minRevision = wrapRevision(minRevision, eventStore.length);
40
- maxRevision = wrapRevision(maxRevision, eventStore.length);
41
-
42
- this.reverse = minRevision > maxRevision;
43
- this.iterator = streams.map(streamName => {
44
- if (!eventStore.streams[streamName]) {
45
- return { next() { return { done: true }; } };
46
- }
47
- const streamIndex = eventStore.streams[streamName].index;
48
- const from = streamIndex.find(minRevision, !this.reverse);
49
- const until = streamIndex.find(maxRevision, this.reverse);
50
- return eventStore.storage.readRange(from || 1, until, streamIndex);
51
- });
36
+ this.minRevision = normalizeVersion(minRevision, eventStore.length);
37
+ this.maxRevision = normalizeVersion(maxRevision, eventStore.length);
38
+ this.fetch = function() {
39
+ this._next = new Array(streams.length).fill(undefined);
40
+ return streams.map(streamName => {
41
+ if (!eventStore.streams[streamName]) {
42
+ return { next() { return { done: true }; } };
43
+ }
44
+ const streamIndex = eventStore.streams[streamName].index;
45
+ const from = streamIndex.find(this.minRevision, this.minRevision <= this.maxRevision);
46
+ const until = streamIndex.find(this.maxRevision, this.minRevision > this.maxRevision);
47
+ return eventStore.storage.readRange(from, until, streamIndex);
48
+ });
49
+ }
50
+ this._iterator = null;
52
51
  }
53
52
 
54
53
  /**
@@ -57,7 +56,7 @@ class JoinEventStream extends EventStream {
57
56
  * @returns {*}
58
57
  */
59
58
  getValue(index) {
60
- const next = this.iterator[index].next();
59
+ const next = this._iterator[index].next();
61
60
  return next.done ? false : next.value;
62
61
  }
63
62
 
@@ -65,10 +64,10 @@ class JoinEventStream extends EventStream {
65
64
  * @private
66
65
  * @param {number} first
67
66
  * @param {number} second
68
- * @returns {boolean} If the first item follows after the second in the given read order determined by this.reverse flag.
67
+ * @returns {boolean} If the first item follows after the second in the given read order determined by this.minRevision and this.maxRevision.
69
68
  */
70
69
  follows(first, second) {
71
- return (this.reverse ? first < second : first > second);
70
+ return (this.minRevision > this.maxRevision ? first < second : first > second);
72
71
  }
73
72
 
74
73
  /**
@@ -76,6 +75,9 @@ class JoinEventStream extends EventStream {
76
75
  * @returns {object|boolean} The next event or false if no more events in the stream.
77
76
  */
78
77
  next() {
78
+ if (!this._iterator) {
79
+ this._iterator = this.fetch();
80
+ }
79
81
  let nextIndex = -1;
80
82
  this._next.forEach((value, index) => {
81
83
  if (typeof value === 'undefined') {
@@ -1,7 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const events = require('events');
4
- const { assert, alignTo } = require('../util');
4
+ const { assert, alignTo, hash, binarySearch } = require('../util');
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(Buffer.byteLength(data, 'utf8'));
438
+ position += this.documentWriteSize(internalHeader.dataSize);
380
439
  }
381
440
  }
382
441