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.
- package/README.md +50 -541
- package/package.json +22 -21
- package/src/Clock.js +20 -8
- package/src/EventStore.js +244 -51
- package/src/EventStream.js +169 -16
- package/src/Index/ReadableIndex.js +9 -3
- package/src/Index/WritableIndex.js +10 -4
- package/src/JoinEventStream.js +32 -30
- package/src/Partition/ReadablePartition.js +105 -46
- package/src/Partition/WritablePartition.js +73 -14
- package/src/Storage/ReadableStorage.js +136 -13
- package/src/Storage/WritableStorage.js +169 -24
- package/src/Watcher.js +1 -0
- package/src/WatchesFile.js +6 -5
- package/src/metadataUtil.js +79 -0
- package/src/util.js +71 -70
- 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
package/src/EventStream.js
CHANGED
|
@@ -2,16 +2,14 @@ const stream = require('stream');
|
|
|
2
2
|
const { assert } = require('./util');
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Calculate the actual version number from a possibly relative (negative) version number.
|
|
6
6
|
*
|
|
7
|
-
* @param {number}
|
|
8
|
-
* @
|
|
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
|
|
11
|
-
|
|
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 =
|
|
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
|
-
|
|
47
|
-
this.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
this.
|
|
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.
|
|
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.
|
|
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
|
|
285
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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[
|
|
199
|
+
this.data[dataLen] = entry;
|
|
194
200
|
|
|
195
201
|
if (this.writeBufferCursor === 0) {
|
|
196
202
|
this.flushTimeout = setTimeout(() => this.flush(), this.flushDelay);
|
package/src/JoinEventStream.js
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
const EventStream = require('./EventStream');
|
|
2
|
+
const { wrapAndCheck } = require('./util');
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
+
* Calculate the actual version number from a possibly relative (negative) version number.
|
|
5
6
|
*
|
|
6
|
-
* @param {number}
|
|
7
|
-
* @param {number} length The
|
|
8
|
-
* @returns {number} The
|
|
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
|
|
11
|
-
|
|
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 =
|
|
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 =
|
|
40
|
-
maxRevision =
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
438
|
+
position += this.documentWriteSize(internalHeader.dataSize);
|
|
380
439
|
}
|
|
381
440
|
}
|
|
382
441
|
|