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.
@@ -23,7 +23,7 @@ class ReadOnlyStorage extends ReadableStorage {
23
23
  * @returns {boolean}
24
24
  */
25
25
  storageFilesFilter(filename) {
26
- return filename.substr(-7) !== '.branch' && filename.substr(0, this.storageFile.length) === this.storageFile;
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.substr(-6) === '.index') {
50
- const indexName = filename.substr(this.storageFile.length + 1, filename.length - this.storageFile.length - 7);
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[partitionId]) {
57
+ if (!this.partitions.has(partitionId)) {
58
58
  const partition = this.createPartition(filename, this.partitionConfig);
59
- this.partitions[partition.id] = partition;
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 { createHmac, matches, buildMetadataForMatcher } from '../metadataUtil.js';
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
- * Reverses the items of an iterable
13
- * @param {Generator|Iterable} iterator
14
- * @returns {Generator<*>}
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
- function *reverse(iterator) {
17
- const items = Array.from(iterator);
18
- for (let i = items.length - 1; i >= 0; i--) {
19
- yield items[i];
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
- this.scanPartitions(config);
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 the data directory for all existing partitions.
126
- * Every file beginning with the storageFile name is considered a partition.
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
- scanPartitions(config) {
133
- const defaults = {
134
- readBufferSize: DEFAULT_READ_BUFFER_SIZE
135
- };
136
- this.partitionConfig = Object.assign(defaults, config);
137
- this.partitions = Object.create(null);
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
- const files = fs.readdirSync(this.dataDirectory);
140
- for (let file of files) {
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
- const partition = this.createPartition(file, this.partitionConfig);
147
- this.partitions[partition.id] = partition;
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
- * Open the storage and indexes and create read and write buffers eagerly.
153
- * Will emit an 'opened' event if finished.
184
+ * Only the primary index is opened eagerly; secondary indexes open on demand.
154
185
  *
155
- * @api
156
- * @returns {boolean}
186
+ * @protected
157
187
  */
158
- open() {
188
+ openIndexes() {
159
189
  this.index.open();
190
+ }
160
191
 
161
- this.forEachSecondaryIndex(index => index.open());
162
-
163
- this.emit('opened');
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 frees up all resources.
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 either by name or by id.
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|number} partitionIdentifier The partition name or the partition Id
249
+ * @param {number|string} partitionIdentifier The partition Id
191
250
  * @returns {ReadablePartition}
192
- * @throws {Error} If an id is given and no such partition exists.
251
+ * @throws {Error} If no such partition exists.
193
252
  */
194
253
  getPartition(partitionIdentifier) {
195
- assert(partitionIdentifier in this.partitions, `Partition #${partitionIdentifier} does not exist.`);
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
- * @returns {object} The document stored at the given position.
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 data = partition.readFrom(position, size);
228
- return this.serializer.deserialize(data);
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
- const lengthSource = index || this.index;
268
- if (!lengthSource.isOpen()) {
269
- lengthSource.open();
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, lengthSource.length);
273
- const readUntil = wrapAndCheck(until, lengthSource.length);
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
- if (readFrom > readUntil) {
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
- // Explicitly disabled index: iterate all partitions and merge by sequenceNumber.
302
- // Document header sequenceNumber is 0-based; from/until are 1-based index positions.
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 entries = idx.range(from, until);
311
- for (let entry of entries) {
312
- const document = this.readFrom(entry.partition, entry.position, entry.size);
313
- yield document;
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
- * Iterate documents across all partitions in sequenceNumber order using a k-way merge.
368
- * Opens any closed partition automatically.
421
+ * Remove a secondary index from the write path and the matcher lookup table.
369
422
  *
370
- * @protected
371
- * @param {number} [from=0] The 0-based sequenceNumber to start from (inclusive).
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
- *iterateDocumentsNoIndex(from = 0, until = Number.MAX_SAFE_INTEGER) {
376
- const streams = [];
377
-
378
- this.forEachPartition(partition => {
379
- if (!partition.isOpen()) {
380
- partition.open();
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, partitionName }`.
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, ...entryInfo } of this.iterateDocumentsNoIndex()) {
435
- iterationHandler(document, entryInfo);
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
- for (let indexName of Object.keys(this.secondaryIndexes)) {
462
- if (!matchDocument || matches(matchDocument, this.secondaryIndexes[indexName].matcher)) {
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
- for (let partition of Object.keys(this.partitions)) {
481
- iterationHandler(this.partitions[partition]);
482
- }
545
+ this.partitions.forEach(iterationHandler);
483
546
  }
484
547
 
485
548
  }