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
|
@@ -23,7 +23,7 @@ class ReadOnlyStorage extends ReadableStorage {
|
|
|
23
23
|
* @returns {boolean}
|
|
24
24
|
*/
|
|
25
25
|
storageFilesFilter(filename) {
|
|
26
|
-
return filename.
|
|
26
|
+
return !filename.endsWith('.branch') && filename.substring(0, this.storageFile.length) === this.storageFile;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
@@ -46,17 +46,17 @@ class ReadOnlyStorage extends ReadableStorage {
|
|
|
46
46
|
* @param {string} filename
|
|
47
47
|
*/
|
|
48
48
|
onStorageFileChanged(filename) {
|
|
49
|
-
if (filename.
|
|
50
|
-
const indexName = filename.
|
|
49
|
+
if (filename.endsWith('.index')) {
|
|
50
|
+
const indexName = filename.substring(this.storageFile.length + 1, filename.length - 6);
|
|
51
51
|
// New indexes are not automatically opened in the reader
|
|
52
52
|
this.emit('index-created', indexName);
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
const partitionId = ReadablePartition.idFor(filename);
|
|
57
|
-
if (!this.partitions
|
|
57
|
+
if (!this.partitions.has(partitionId)) {
|
|
58
58
|
const partition = this.createPartition(filename, this.partitionConfig);
|
|
59
|
-
this.partitions
|
|
59
|
+
this.partitions.add(partition.id, partition);
|
|
60
60
|
this.emit('partition-created', partition.id);
|
|
61
61
|
}
|
|
62
62
|
}
|
|
@@ -3,22 +3,29 @@ import path from 'path';
|
|
|
3
3
|
import events from 'events';
|
|
4
4
|
import Partition, { ReadOnly as ReadOnlyPartition } from '../Partition.js';
|
|
5
5
|
import Index, { ReadOnly as ReadOnlyIndex } from '../Index.js';
|
|
6
|
-
import { assert, wrapAndCheck, kWayMerge } from '../util.js';
|
|
7
|
-
import {
|
|
6
|
+
import { assert, wrapAndCheck, iterate, kWayMerge } from '../utils/util.js';
|
|
7
|
+
import { scanForFiles } from '../utils/fsUtil.js';
|
|
8
|
+
import { createHmac, matches, buildMetadataForMatcher } from '../utils/metadataUtil.js';
|
|
9
|
+
import IndexMatcher from '../IndexMatcher.js';
|
|
10
|
+
import PartitionPool from '../PartitionPool.js';
|
|
8
11
|
|
|
9
12
|
const DEFAULT_READ_BUFFER_SIZE = 4 * 1024;
|
|
13
|
+
const NDJSON_NEWLINE = Buffer.from('\n');
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
16
|
+
* Default ordered list of document property paths used as discriminant keys when
|
|
17
|
+
* classifying object matchers into the fast-lookup table. Each path may use
|
|
18
|
+
* dot-notation for nested access (e.g. `'payload.type'`). The first path that
|
|
19
|
+
* resolves to a scalar value in a given matcher wins; remaining paths are not
|
|
20
|
+
* examined for that matcher.
|
|
15
21
|
*/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
const DEFAULT_MATCHER_PROPERTIES = ['stream', 'payload.type'];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Default maximum number of partition file descriptors kept open simultaneously.
|
|
26
|
+
* Partitions beyond this limit are evicted using LRU order. 0 disables the limit.
|
|
27
|
+
*/
|
|
28
|
+
const DEFAULT_MAX_OPEN_PARTITIONS = 1024;
|
|
22
29
|
|
|
23
30
|
/**
|
|
24
31
|
* @typedef {object|function(object):boolean} Matcher
|
|
@@ -43,6 +50,13 @@ class ReadableStorage extends events.EventEmitter {
|
|
|
43
50
|
* @param {object} [config.indexOptions] An options object that should be passed to all indexes on construction.
|
|
44
51
|
* @param {string} [config.hmacSecret] A private key that is used to verify matchers retrieved from indexes.
|
|
45
52
|
* @param {object} [config.metadata] A metadata object to be stored in all partitions belonging to this storage.
|
|
53
|
+
* @param {string[]} [config.matcherProperties] Ordered list of document property paths (dot-notation) used as
|
|
54
|
+
* discriminant keys for the fast secondary-index lookup table. Only the first property that resolves to a scalar
|
|
55
|
+
* value inside a given object matcher is used; the rest are checked via the full `matches()` fallback.
|
|
56
|
+
* Default: `['stream', 'payload.type']`.
|
|
57
|
+
* @param {number} [config.maxOpenPartitions] Maximum number of partition file descriptors kept open at one time.
|
|
58
|
+
* When the limit is reached the least-recently-used partition is closed to make room. 0 disables the limit.
|
|
59
|
+
* Default: 1024.
|
|
46
60
|
*/
|
|
47
61
|
constructor(storageName = 'storage', config = {}) {
|
|
48
62
|
super();
|
|
@@ -58,7 +72,9 @@ class ReadableStorage extends events.EventEmitter {
|
|
|
58
72
|
indexFile: this.storageFile + '.index',
|
|
59
73
|
indexOptions: {},
|
|
60
74
|
hmacSecret: '',
|
|
61
|
-
metadata: {}
|
|
75
|
+
metadata: {},
|
|
76
|
+
matcherProperties: DEFAULT_MATCHER_PROPERTIES,
|
|
77
|
+
maxOpenPartitions: DEFAULT_MAX_OPEN_PARTITIONS
|
|
62
78
|
};
|
|
63
79
|
config = Object.assign(defaults, config);
|
|
64
80
|
this.serializer = config.serializer;
|
|
@@ -67,7 +83,13 @@ class ReadableStorage extends events.EventEmitter {
|
|
|
67
83
|
|
|
68
84
|
this.dataDirectory = path.resolve(config.dataDirectory);
|
|
69
85
|
|
|
70
|
-
|
|
86
|
+
const partitionDefaults = { readBufferSize: DEFAULT_READ_BUFFER_SIZE };
|
|
87
|
+
this.partitionConfig = Object.assign(partitionDefaults, config);
|
|
88
|
+
this.partitions = new PartitionPool(config.maxOpenPartitions);
|
|
89
|
+
|
|
90
|
+
// initialized: null = not started (or scan cancelled), false = in progress, true = done
|
|
91
|
+
this.initialized = null;
|
|
92
|
+
|
|
71
93
|
this.initializeIndexes(config);
|
|
72
94
|
}
|
|
73
95
|
|
|
@@ -111,6 +133,9 @@ class ReadableStorage extends events.EventEmitter {
|
|
|
111
133
|
this.index = index;
|
|
112
134
|
this.secondaryIndexes = {};
|
|
113
135
|
this.readonlyIndexes = {};
|
|
136
|
+
|
|
137
|
+
/** Fast secondary-index lookup — classifies matchers for O(1) candidate resolution on write. */
|
|
138
|
+
this.indexMatcher = new IndexMatcher(config.matcherProperties);
|
|
114
139
|
}
|
|
115
140
|
|
|
116
141
|
/**
|
|
@@ -122,56 +147,91 @@ class ReadableStorage extends events.EventEmitter {
|
|
|
122
147
|
}
|
|
123
148
|
|
|
124
149
|
/**
|
|
125
|
-
* Scan
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* @private
|
|
129
|
-
* @param {object} config The configuration object containing options for the partitions.
|
|
130
|
-
* @returns void
|
|
150
|
+
* Scan partitions and secondary index files; emit 'index-created' for each found index.
|
|
151
|
+
* @param {function} done Called when both scans finish.
|
|
131
152
|
*/
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
153
|
+
scanFiles(done) {
|
|
154
|
+
const escaped = this.storageFile.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
155
|
+
const partitionPattern = new RegExp(`^(${escaped}.*)$`);
|
|
156
|
+
scanForFiles(this.dataDirectory, partitionPattern, (file) => {
|
|
157
|
+
if (file.endsWith('.index') || file.endsWith('.branch') || file.endsWith('.lock')) return;
|
|
158
|
+
const partition = this.createPartition(file, this.partitionConfig);
|
|
159
|
+
this.partitions.add(partition.id, partition);
|
|
160
|
+
}, (partErr) => {
|
|
161
|
+
/* istanbul ignore if */
|
|
162
|
+
if (partErr) throw partErr;
|
|
138
163
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (file.substr(-6) === '.index') continue;
|
|
142
|
-
if (file.substr(-7) === '.branch') continue;
|
|
143
|
-
if (file.substr(-5) === '.lock') continue;
|
|
144
|
-
if (file.substr(0, this.storageFile.length) !== this.storageFile) continue;
|
|
164
|
+
// Scan was cancelled by close() between the two scan phases.
|
|
165
|
+
if (this.initialized === null) return;
|
|
145
166
|
|
|
146
|
-
|
|
147
|
-
this.
|
|
148
|
-
|
|
167
|
+
// No secondary indexes exist yet — nothing to scan.
|
|
168
|
+
if (!fs.existsSync(this.indexDirectory)) {
|
|
169
|
+
return done();
|
|
170
|
+
}
|
|
171
|
+
const indexPattern = new RegExp(`^${escaped}\\.(.+)\\.index$`);
|
|
172
|
+
scanForFiles(this.indexDirectory, indexPattern, (name) => {
|
|
173
|
+
this.emit('index-created', name);
|
|
174
|
+
}, (indexErr) => {
|
|
175
|
+
// The directory could disappear between existsSync and readdir (e.g. test cleanup).
|
|
176
|
+
/* istanbul ignore if */
|
|
177
|
+
if (indexErr && indexErr.code !== 'ENOENT') throw indexErr;
|
|
178
|
+
done();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
149
181
|
}
|
|
150
182
|
|
|
151
183
|
/**
|
|
152
|
-
*
|
|
153
|
-
* Will emit an 'opened' event if finished.
|
|
184
|
+
* Only the primary index is opened eagerly; secondary indexes open on demand.
|
|
154
185
|
*
|
|
155
|
-
* @
|
|
156
|
-
* @returns {boolean}
|
|
186
|
+
* @protected
|
|
157
187
|
*/
|
|
158
|
-
|
|
188
|
+
openIndexes() {
|
|
159
189
|
this.index.open();
|
|
190
|
+
}
|
|
160
191
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Open the storage; scans existing partitions and indexes asynchronously on first open.
|
|
194
|
+
* Re-opens after `close()` are synchronous.
|
|
195
|
+
* Will emit an `'opened'` event when finished.
|
|
196
|
+
*
|
|
197
|
+
* @api
|
|
198
|
+
* @param {function(): void} [callback] Called after indexes open, before `'opened'` is emitted.
|
|
199
|
+
* Can be used as a synchronous alternative to listening to the `'opened'` event.
|
|
200
|
+
* @returns {boolean}
|
|
201
|
+
*/
|
|
202
|
+
open(callback) {
|
|
203
|
+
if (this.initialized === true) {
|
|
204
|
+
this.openIndexes();
|
|
205
|
+
callback?.();
|
|
206
|
+
this.emit('opened');
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
if (this.initialized === false) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
this.initialized = false;
|
|
213
|
+
this.scanFiles(() => {
|
|
214
|
+
// Guard: close() while scanning resets initialized to null.
|
|
215
|
+
if (this.initialized === null) return;
|
|
216
|
+
this.initialized = true;
|
|
217
|
+
this.openIndexes();
|
|
218
|
+
callback?.();
|
|
219
|
+
this.emit('opened');
|
|
220
|
+
});
|
|
164
221
|
return true;
|
|
165
222
|
}
|
|
166
223
|
|
|
167
224
|
/**
|
|
168
|
-
* Close the storage and
|
|
225
|
+
* Close the storage and free up all resources.
|
|
169
226
|
* Will emit a 'closed' event when finished.
|
|
170
227
|
*
|
|
171
228
|
* @api
|
|
172
|
-
* @returns void
|
|
173
229
|
*/
|
|
174
230
|
close() {
|
|
231
|
+
// Cancel in-progress scan so the callback does not re-open after an explicit close.
|
|
232
|
+
if (this.initialized === false) {
|
|
233
|
+
this.initialized = null;
|
|
234
|
+
}
|
|
175
235
|
this.index.close();
|
|
176
236
|
this.forEachSecondaryIndex(index => index.close());
|
|
177
237
|
for (let index of Object.values(this.readonlyIndexes)) {
|
|
@@ -182,20 +242,17 @@ class ReadableStorage extends events.EventEmitter {
|
|
|
182
242
|
}
|
|
183
243
|
|
|
184
244
|
/**
|
|
185
|
-
* Get a partition
|
|
186
|
-
* If a partition with the given name does not exist, a new one will be created.
|
|
245
|
+
* Get a partition by its id.
|
|
187
246
|
* If a partition with the given id does not exist, an error is thrown.
|
|
188
247
|
*
|
|
189
248
|
* @protected
|
|
190
|
-
* @param {string
|
|
249
|
+
* @param {number|string} partitionIdentifier The partition Id
|
|
191
250
|
* @returns {ReadablePartition}
|
|
192
|
-
* @throws {Error} If
|
|
251
|
+
* @throws {Error} If no such partition exists.
|
|
193
252
|
*/
|
|
194
253
|
getPartition(partitionIdentifier) {
|
|
195
|
-
assert(
|
|
196
|
-
|
|
197
|
-
this.partitions[partitionIdentifier].open();
|
|
198
|
-
return this.partitions[partitionIdentifier];
|
|
254
|
+
assert(this.partitions.has(partitionIdentifier), `Partition #${partitionIdentifier} does not exist.`);
|
|
255
|
+
return this.partitions.open(partitionIdentifier);
|
|
199
256
|
}
|
|
200
257
|
|
|
201
258
|
/**
|
|
@@ -216,16 +273,19 @@ class ReadableStorage extends events.EventEmitter {
|
|
|
216
273
|
* @param {number} partitionId The partition to read from.
|
|
217
274
|
* @param {number} position The file position to read from.
|
|
218
275
|
* @param {number} [size] The expected byte size of the document at the given position.
|
|
219
|
-
* @
|
|
276
|
+
* @param {boolean} [raw] Whether to return raw buffers instead of deserialized objects. Default false.
|
|
277
|
+
* @param {boolean} [backwardsHint] If set to true, will optimize buffering for backwards reading.
|
|
278
|
+
* @returns {object|{ buffer: Buffer, time64: number, sequenceNumber: number }} The document stored at the given position.
|
|
220
279
|
* @throws {Error} if the document at the given position can not be deserialized.
|
|
221
280
|
*/
|
|
222
|
-
readFrom(partitionId, position, size) {
|
|
281
|
+
readFrom(partitionId, position, size, raw = false, backwardsHint = false) {
|
|
223
282
|
const partition = this.getPartition(partitionId);
|
|
224
283
|
if (this.listenerCount('preRead') > 0) {
|
|
225
284
|
this.emit('preRead', position, partition.metadata);
|
|
226
285
|
}
|
|
227
|
-
const
|
|
228
|
-
|
|
286
|
+
const headerOut = {};
|
|
287
|
+
const buffer = partition.readFrom(position, size, headerOut, backwardsHint);
|
|
288
|
+
return raw ? { buffer, time64: headerOut.time64, sequenceNumber: headerOut.sequenceNumber } : this.serializer.deserialize(buffer.toString('utf8'));
|
|
229
289
|
}
|
|
230
290
|
|
|
231
291
|
/**
|
|
@@ -238,10 +298,7 @@ class ReadableStorage extends events.EventEmitter {
|
|
|
238
298
|
*/
|
|
239
299
|
read(number, index) {
|
|
240
300
|
index = index || this.index;
|
|
241
|
-
|
|
242
|
-
if (!index.isOpen()) {
|
|
243
|
-
index.open();
|
|
244
|
-
}
|
|
301
|
+
index.open();
|
|
245
302
|
|
|
246
303
|
const entry = index.get(number);
|
|
247
304
|
if (entry === false) {
|
|
@@ -261,30 +318,22 @@ class ReadableStorage extends events.EventEmitter {
|
|
|
261
318
|
* @param {ReadableIndex|false} [index] The index to use for finding the documents in the range.
|
|
262
319
|
* Pass `false` to skip the global index and iterate all partitions directly in sequenceNumber order
|
|
263
320
|
* (useful when the global index is unavailable or corrupted).
|
|
321
|
+
* @param {boolean} [raw] Whether to return raw buffers instead of deserialized objects. Default false.
|
|
264
322
|
* @returns {Generator<object>} A generator that will read each document in the range one by one.
|
|
265
323
|
*/
|
|
266
|
-
*readRange(from, until = -1, index = null) {
|
|
267
|
-
|
|
268
|
-
if (
|
|
269
|
-
|
|
324
|
+
*readRange(from, until = -1, index = null, raw = false) {
|
|
325
|
+
let length = Number.MAX_SAFE_INTEGER;
|
|
326
|
+
if (index !== false) {
|
|
327
|
+
index = index || this.index;
|
|
328
|
+
index.open();
|
|
329
|
+
length = index.length;
|
|
270
330
|
}
|
|
271
331
|
|
|
272
|
-
const readFrom = wrapAndCheck(from,
|
|
273
|
-
const readUntil = wrapAndCheck(until,
|
|
332
|
+
const readFrom = wrapAndCheck(from, length);
|
|
333
|
+
const readUntil = wrapAndCheck(until, length);
|
|
274
334
|
assert(readFrom > 0 && readUntil > 0, `Range scan error for range ${from} - ${until}.`);
|
|
275
335
|
|
|
276
|
-
|
|
277
|
-
const batchSize = 10;
|
|
278
|
-
let batchUntil = readFrom;
|
|
279
|
-
while (batchUntil >= readUntil) {
|
|
280
|
-
const batchFrom = Math.max(readUntil, batchUntil - batchSize);
|
|
281
|
-
yield* reverse(this.iterateRange(batchFrom, batchUntil, index));
|
|
282
|
-
batchUntil = batchFrom - 1;
|
|
283
|
-
}
|
|
284
|
-
return undefined;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
yield* this.iterateRange(readFrom, readUntil, index);
|
|
336
|
+
yield* this.iterateRange(readFrom, readUntil, index, raw);
|
|
288
337
|
}
|
|
289
338
|
|
|
290
339
|
/**
|
|
@@ -294,23 +343,25 @@ class ReadableStorage extends events.EventEmitter {
|
|
|
294
343
|
* @param {number} from
|
|
295
344
|
* @param {number} until
|
|
296
345
|
* @param {ReadableIndex|false|null} index
|
|
346
|
+
* @param {boolean} [raw] Whether to return raw buffers instead of deserialized objects. Default false.
|
|
297
347
|
* @returns {Generator<object>}
|
|
298
348
|
*/
|
|
299
|
-
*iterateRange(from, until, index) {
|
|
349
|
+
*iterateRange(from, until, index, raw = false) {
|
|
300
350
|
if (index === false) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
for (const entry of this.iterateDocumentsNoIndex(from - 1, until - 1)) {
|
|
304
|
-
yield entry.document;
|
|
351
|
+
for (const { document } of this.iterateDocumentsNoIndex(from - 1, until - 1)) {
|
|
352
|
+
yield document;
|
|
305
353
|
}
|
|
306
354
|
return;
|
|
307
355
|
}
|
|
308
356
|
|
|
309
357
|
const idx = index || this.index;
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
358
|
+
const forwards = from <= until;
|
|
359
|
+
const lo = Math.min(from, until);
|
|
360
|
+
const hi = Math.max(from, until);
|
|
361
|
+
const entries = idx.range(lo, hi);
|
|
362
|
+
if (!entries) return;
|
|
363
|
+
for (const entry of iterate(entries, forwards)) {
|
|
364
|
+
yield this.readFrom(entry.partition, entry.position, entry.size, raw, !forwards);
|
|
314
365
|
}
|
|
315
366
|
}
|
|
316
367
|
|
|
@@ -359,66 +410,71 @@ class ReadableStorage extends events.EventEmitter {
|
|
|
359
410
|
const metadata = buildMetadataForMatcher(matcher, this.hmac);
|
|
360
411
|
let { index } = this.secondaryIndexes[name] = this.createIndex(indexName, Object.assign({}, this.indexOptions, { metadata }));
|
|
361
412
|
|
|
413
|
+
// Register the actual stored matcher (may have been reconstructed from metadata by WritableStorage.createIndex).
|
|
414
|
+
this.indexMatcher.add(name, this.secondaryIndexes[name].matcher);
|
|
415
|
+
|
|
362
416
|
index.open();
|
|
363
417
|
return index;
|
|
364
418
|
}
|
|
365
419
|
|
|
366
420
|
/**
|
|
367
|
-
*
|
|
368
|
-
* Opens any closed partition automatically.
|
|
421
|
+
* Remove a secondary index from the write path and the matcher lookup table.
|
|
369
422
|
*
|
|
370
|
-
* @
|
|
371
|
-
* @param {
|
|
372
|
-
* @param {number} [until=Number.MAX_SAFE_INTEGER] The 0-based sequenceNumber to read until (inclusive).
|
|
373
|
-
* @returns {Generator<{document: object, sequenceNumber: number, partitionName: string, position: number, size: number, partition: number}>}
|
|
423
|
+
* @api
|
|
424
|
+
* @param {string} name The secondary index name to remove.
|
|
374
425
|
*/
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const found = partition.findDocument(from);
|
|
384
|
-
if (found && found.headerOut.sequenceNumber <= until) {
|
|
385
|
-
const nextPosition = found.headerOut.position + partition.documentWriteSize(found.headerOut.dataSize);
|
|
386
|
-
const reader = partition.readAll(nextPosition, found.headerOut);
|
|
387
|
-
streams.push({ ...found, reader, partition: partition.id, partitionName: partition.name });
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
const items = [];
|
|
392
|
-
kWayMerge(
|
|
393
|
-
streams,
|
|
394
|
-
stream => stream.headerOut.sequenceNumber,
|
|
395
|
-
stream => {
|
|
396
|
-
const next = stream.reader.next();
|
|
397
|
-
if (!next.done && stream.headerOut.sequenceNumber <= until) {
|
|
398
|
-
stream.data = next.value;
|
|
399
|
-
return true;
|
|
400
|
-
}
|
|
401
|
-
return false;
|
|
402
|
-
},
|
|
403
|
-
stream => items.push({
|
|
404
|
-
document: this.serializer.deserialize(stream.data),
|
|
405
|
-
sequenceNumber: stream.headerOut.sequenceNumber,
|
|
406
|
-
partitionName: stream.partitionName,
|
|
407
|
-
position: stream.headerOut.position,
|
|
408
|
-
size: stream.headerOut.dataSize,
|
|
409
|
-
partition: stream.partition,
|
|
410
|
-
})
|
|
411
|
-
);
|
|
412
|
-
|
|
413
|
-
yield* items;
|
|
426
|
+
removeSecondaryIndex(name) {
|
|
427
|
+
const entry = this.secondaryIndexes[name];
|
|
428
|
+
if (entry) {
|
|
429
|
+
this.indexMatcher.remove(name);
|
|
430
|
+
delete this.secondaryIndexes[name];
|
|
431
|
+
}
|
|
414
432
|
}
|
|
415
433
|
|
|
434
|
+
/**
|
|
435
|
+
* Build the standard document result entry from a readRange yield.
|
|
436
|
+
* @private
|
|
437
|
+
* @param {{ data: Buffer, entry: { number: number, position: number, size: number, partition: number } }} [readItem]
|
|
438
|
+
*/
|
|
439
|
+
buildDocumentEntry(readItem) {
|
|
440
|
+
return {
|
|
441
|
+
document: this.serializer.deserialize(readItem.data.toString('utf8')),
|
|
442
|
+
// Replicate the index entry structure here, so iteration can be used easily to reindex
|
|
443
|
+
entry: readItem.entry
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Iterate documents across all partitions in sequenceNumber order using a k-way merge.
|
|
449
|
+
* Opens any closed partition automatically.
|
|
450
|
+
*
|
|
451
|
+
* @protected
|
|
452
|
+
* @param {number} [from=0] The 0-based sequenceNumber to start from (inclusive).
|
|
453
|
+
* @param {number} [until=Number.MAX_SAFE_INTEGER] The 0-based sequenceNumber to read until (inclusive).
|
|
454
|
+
* @returns {Generator<{document: object, entry: { sequenceNumber: number, position: number, size: number, partition: number }}>}
|
|
455
|
+
*/
|
|
456
|
+
*iterateDocumentsNoIndex(from = 0, until = Number.MAX_SAFE_INTEGER) {
|
|
457
|
+
const forwards = from <= until;
|
|
458
|
+
const partitions = [];
|
|
459
|
+
this.forEachPartition(partition => {
|
|
460
|
+
partition.open();
|
|
461
|
+
partitions.push(partition.readRange(from, until));
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
yield* kWayMerge(
|
|
465
|
+
partitions,
|
|
466
|
+
item => item.entry.number,
|
|
467
|
+
forwards,
|
|
468
|
+
item => this.buildDocumentEntry(item)
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
416
472
|
/**
|
|
417
473
|
* Helper method to iterate over all documents, invoking a callback for each one.
|
|
418
474
|
* Pass `noIndex = true` to iterate all partitions directly in sequenceNumber order
|
|
419
475
|
* (useful when the global index is unavailable or corrupted).
|
|
420
476
|
* When `noIndex` is false the second callback argument is the raw index `EntryInterface`.
|
|
421
|
-
* When `noIndex` is true the second callback argument has `{ partition, position, size, sequenceNumber
|
|
477
|
+
* When `noIndex` is true the second callback argument has `{ partition, position, size, sequenceNumber }`.
|
|
422
478
|
*
|
|
423
479
|
* @protected
|
|
424
480
|
* @param {function(object, object): void} iterationHandler
|
|
@@ -431,8 +487,8 @@ class ReadableStorage extends events.EventEmitter {
|
|
|
431
487
|
}
|
|
432
488
|
|
|
433
489
|
if (noIndex) {
|
|
434
|
-
for (const { document,
|
|
435
|
-
iterationHandler(document,
|
|
490
|
+
for (const { document, entry } of this.iterateDocumentsNoIndex()) {
|
|
491
|
+
iterationHandler(document, entry);
|
|
436
492
|
}
|
|
437
493
|
return;
|
|
438
494
|
}
|
|
@@ -448,6 +504,9 @@ class ReadableStorage extends events.EventEmitter {
|
|
|
448
504
|
/**
|
|
449
505
|
* Helper method to iterate over all secondary indexes.
|
|
450
506
|
*
|
|
507
|
+
* When `matchDocument` is provided, `this.indexMatcher.forEachMatch()` is used to
|
|
508
|
+
* efficiently find only the matching indexes via the discriminant lookup table.
|
|
509
|
+
*
|
|
451
510
|
* @protected
|
|
452
511
|
* @param {function(ReadableIndex, string)} iterationHandler
|
|
453
512
|
* @param {object} [matchDocument] If supplied, only indexes the document matches on will be iterated.
|
|
@@ -458,11 +517,17 @@ class ReadableStorage extends events.EventEmitter {
|
|
|
458
517
|
return;
|
|
459
518
|
}
|
|
460
519
|
|
|
461
|
-
|
|
462
|
-
|
|
520
|
+
if (!matchDocument) {
|
|
521
|
+
// No document filter: iterate all secondary indexes unconditionally.
|
|
522
|
+
for (const indexName of Object.keys(this.secondaryIndexes)) {
|
|
463
523
|
iterationHandler(this.secondaryIndexes[indexName].index, indexName);
|
|
464
524
|
}
|
|
525
|
+
return;
|
|
465
526
|
}
|
|
527
|
+
|
|
528
|
+
this.indexMatcher.forEachMatch(matchDocument, indexName => {
|
|
529
|
+
iterationHandler(this.secondaryIndexes[indexName].index, indexName);
|
|
530
|
+
});
|
|
466
531
|
}
|
|
467
532
|
|
|
468
533
|
/**
|
|
@@ -477,9 +542,7 @@ class ReadableStorage extends events.EventEmitter {
|
|
|
477
542
|
return;
|
|
478
543
|
}
|
|
479
544
|
|
|
480
|
-
|
|
481
|
-
iterationHandler(this.partitions[partition]);
|
|
482
|
-
}
|
|
545
|
+
this.partitions.forEach(iterationHandler);
|
|
483
546
|
}
|
|
484
547
|
|
|
485
548
|
}
|