event-storage 1.0.0 → 1.1.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 +3 -0
- package/index.js +1 -1
- package/package.json +2 -1
- package/src/Consumer.js +2 -1
- package/src/EventStore.js +212 -38
- package/src/EventStream.js +28 -4
- package/src/Index/WritableIndex.js +3 -1
- package/src/IndexMatcher.js +205 -0
- package/src/JoinEventStream.js +35 -21
- package/src/Partition/WritablePartition.js +3 -1
- package/src/PartitionPool.js +149 -0
- package/src/Storage/ReadOnlyStorage.js +2 -2
- package/src/Storage/ReadableStorage.js +138 -46
- package/src/Storage/WritableStorage.js +45 -27
- package/src/fsUtil.js +123 -0
- package/src/metadataUtil.js +49 -2
- package/src/util.js +15 -66
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { getPropertyAtPath } from './util.js';
|
|
2
|
+
import { matches } from './metadataUtil.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {object|function(object):boolean} Matcher
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Classifies secondary-index matchers into a fast lookup table keyed on a
|
|
10
|
+
* configurable ordered list of "discriminant" property paths. This enables O(1)
|
|
11
|
+
* candidate resolution on write instead of evaluating every registered matcher.
|
|
12
|
+
*
|
|
13
|
+
* Object matchers that contain at least one of the discriminant property paths
|
|
14
|
+
* (in priority order) are stored in a nested Map keyed first by property path and
|
|
15
|
+
* then by the scalar value found at that path in the matcher. Function matchers
|
|
16
|
+
* and object matchers whose discriminant properties all resolve to undefined/object
|
|
17
|
+
* are kept in separate fallback sets and are always evaluated in full.
|
|
18
|
+
*
|
|
19
|
+
* When the discriminant property list is empty, `forEachMatch()` falls back to a
|
|
20
|
+
* full O(N) scan over all registered indexes.
|
|
21
|
+
*/
|
|
22
|
+
class IndexMatcher {
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string[]} [properties] Ordered list of document property paths (dot-notation)
|
|
26
|
+
* used as discriminant keys. The first path that resolves to a non-null scalar inside
|
|
27
|
+
* a given object matcher is used as the key; remaining paths are ignored for that matcher.
|
|
28
|
+
* Pass an empty array (the default) to disable the fast path entirely.
|
|
29
|
+
*/
|
|
30
|
+
constructor(properties = []) {
|
|
31
|
+
this.properties = properties;
|
|
32
|
+
/** Map<indexName, Matcher> — stores every registered matcher for full match verification. */
|
|
33
|
+
this.matchers = new Map();
|
|
34
|
+
/**
|
|
35
|
+
* Nested lookup table: Map<propPath, Map<discriminantValue, Set<indexName>>>.
|
|
36
|
+
* Populated only for object matchers that contain at least one discriminant property.
|
|
37
|
+
*/
|
|
38
|
+
this.table = new Map();
|
|
39
|
+
/** Set of index names whose matchers are functions (always evaluated in full). */
|
|
40
|
+
this.functionMatchers = new Set();
|
|
41
|
+
/**
|
|
42
|
+
* Set of index names whose object matchers contain none of the configured
|
|
43
|
+
* discriminant properties (evaluated in full against every incoming document).
|
|
44
|
+
*/
|
|
45
|
+
this.unclassifiedMatchers = new Set();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Register an index name and its matcher in the lookup structures.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} indexName
|
|
52
|
+
* @param {Matcher} matcher
|
|
53
|
+
*/
|
|
54
|
+
add(indexName, matcher) {
|
|
55
|
+
this.matchers.set(indexName, matcher);
|
|
56
|
+
if (typeof matcher === 'function') {
|
|
57
|
+
this.functionMatchers.add(indexName);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (!matcher || typeof matcher !== 'object') {
|
|
61
|
+
this.unclassifiedMatchers.add(indexName);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const discriminant = this.findDiscriminant(matcher);
|
|
66
|
+
if (!discriminant) {
|
|
67
|
+
this.unclassifiedMatchers.add(indexName);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let propMap = this.table.get(discriminant.propPath);
|
|
72
|
+
if (!propMap) {
|
|
73
|
+
propMap = new Map();
|
|
74
|
+
this.table.set(discriminant.propPath, propMap);
|
|
75
|
+
}
|
|
76
|
+
for (const v of discriminant.values) {
|
|
77
|
+
let indexSet = propMap.get(v);
|
|
78
|
+
if (!indexSet) {
|
|
79
|
+
indexSet = new Set();
|
|
80
|
+
propMap.set(v, indexSet);
|
|
81
|
+
}
|
|
82
|
+
indexSet.add(indexName);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Remove an index name from the lookup structures.
|
|
88
|
+
* The matcher is retrieved from the internal registry, so only the index name
|
|
89
|
+
* is required.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} indexName
|
|
92
|
+
*/
|
|
93
|
+
remove(indexName) {
|
|
94
|
+
if (!this.matchers.has(indexName)) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const matcher = this.matchers.get(indexName);
|
|
98
|
+
this.matchers.delete(indexName);
|
|
99
|
+
if (typeof matcher === 'function') {
|
|
100
|
+
this.functionMatchers.delete(indexName);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (!matcher || typeof matcher !== 'object') {
|
|
104
|
+
this.unclassifiedMatchers.delete(indexName);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const discriminant = this.findDiscriminant(matcher);
|
|
109
|
+
if (!discriminant) {
|
|
110
|
+
this.unclassifiedMatchers.delete(indexName);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const v of discriminant.values) {
|
|
115
|
+
this.table.get(discriminant.propPath)?.get(v)?.delete(indexName);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Iterate over every registered index whose matcher matches `document`, calling
|
|
121
|
+
* `iterationHandler` with the index name for each match.
|
|
122
|
+
*
|
|
123
|
+
* When `this.properties` is non-empty, an O(1) discriminant lookup narrows the
|
|
124
|
+
* candidate set before the full `matches()` check is applied. Each candidate's
|
|
125
|
+
* matcher is still verified with `matches()` to handle multi-property matchers
|
|
126
|
+
* where only the first property was used as the discriminant.
|
|
127
|
+
*
|
|
128
|
+
* When `this.properties` is empty the method falls back to a full O(N) scan.
|
|
129
|
+
*
|
|
130
|
+
* @param {object} document
|
|
131
|
+
* @param {function(string): void} iterationHandler Called with the index name for each match.
|
|
132
|
+
*/
|
|
133
|
+
forEachMatch(document, iterationHandler) {
|
|
134
|
+
if (this.properties.length === 0) {
|
|
135
|
+
// Fast path disabled: full O(N) scan.
|
|
136
|
+
for (const [indexName, matcher] of this.matchers) {
|
|
137
|
+
if (matches(document, matcher)) {
|
|
138
|
+
iterationHandler(indexName);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const propPath of this.properties) {
|
|
145
|
+
const docValue = getPropertyAtPath(document, propPath);
|
|
146
|
+
if (docValue === undefined || docValue === null || typeof docValue === 'object') {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const indexSet = this.table.get(propPath)?.get(String(docValue));
|
|
151
|
+
if (!indexSet) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const indexName of indexSet) {
|
|
156
|
+
if (matches(document, this.matchers.get(indexName))) {
|
|
157
|
+
iterationHandler(indexName);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (const indexName of this.unclassifiedMatchers) {
|
|
163
|
+
if (matches(document, this.matchers.get(indexName))) {
|
|
164
|
+
iterationHandler(indexName);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for (const indexName of this.functionMatchers) {
|
|
169
|
+
if (matches(document, this.matchers.get(indexName))) {
|
|
170
|
+
iterationHandler(indexName);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Find the first usable discriminant for an object matcher.
|
|
177
|
+
* Returns `{ propPath, values }` for the first entry in `this.properties` that
|
|
178
|
+
* resolves to a non-null, non-object scalar (or a non-empty array of scalars) inside
|
|
179
|
+
* `matcher`, or `null` if none.
|
|
180
|
+
*
|
|
181
|
+
* For a scalar discriminant `values` contains exactly one element.
|
|
182
|
+
* For an array-valued discriminant `values` contains all elements of the array.
|
|
183
|
+
*
|
|
184
|
+
* @param {object} matcher
|
|
185
|
+
* @returns {{ propPath: string, values: string[] }|null}
|
|
186
|
+
*/
|
|
187
|
+
findDiscriminant(matcher) {
|
|
188
|
+
for (const propPath of this.properties) {
|
|
189
|
+
const value = getPropertyAtPath(matcher, propPath);
|
|
190
|
+
if (value !== undefined && value !== null) {
|
|
191
|
+
if (typeof value !== 'object') {
|
|
192
|
+
return { propPath, values: [String(value)] };
|
|
193
|
+
}
|
|
194
|
+
if (Array.isArray(value) && value.length > 0 &&
|
|
195
|
+
value.every(v => v !== null && v !== undefined && typeof v !== 'object')) {
|
|
196
|
+
return { propPath, values: value.map(String) };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export default IndexMatcher;
|
package/src/JoinEventStream.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import EventStream from './EventStream.js';
|
|
2
2
|
import { wrapAndCheck } from './util.js';
|
|
3
3
|
|
|
4
|
+
/** Reusable sentinel used for missing or empty per-stream iterators. */
|
|
5
|
+
const emptyIterator = Object.freeze({ next() { return { done: true }; } });
|
|
6
|
+
|
|
4
7
|
/**
|
|
5
8
|
* Calculate the actual version number from a possibly relative (negative) version number.
|
|
6
9
|
*
|
|
@@ -24,9 +27,11 @@ class JoinEventStream extends EventStream {
|
|
|
24
27
|
* @param {EventStore} eventStore The event store to get the stream from.
|
|
25
28
|
* @param {number} [minRevision] The 1-based minimum revision to include in the events (inclusive).
|
|
26
29
|
* @param {number} [maxRevision] The 1-based maximum revision to include in the events (inclusive).
|
|
30
|
+
* @param {function(object, object): boolean|null} [predicate] An optional filter function
|
|
31
|
+
* `(payload, metadata) => boolean`. Only events for which this returns truthy are yielded.
|
|
27
32
|
*/
|
|
28
|
-
constructor(name, streams, eventStore, minRevision = 1, maxRevision = -1) {
|
|
29
|
-
super(name, eventStore, minRevision, maxRevision);
|
|
33
|
+
constructor(name, streams, eventStore, minRevision = 1, maxRevision = -1, predicate = null) {
|
|
34
|
+
super(name, eventStore, minRevision, maxRevision, predicate);
|
|
30
35
|
if (!(streams instanceof Array) || streams.length === 0) {
|
|
31
36
|
throw new Error(`Invalid list of streams supplied to JoinStream ${name}.`);
|
|
32
37
|
}
|
|
@@ -38,12 +43,17 @@ class JoinEventStream extends EventStream {
|
|
|
38
43
|
this.fetch = function() {
|
|
39
44
|
this._next = new Array(streams.length).fill(undefined);
|
|
40
45
|
return streams.map(streamName => {
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
const streamIndex = eventStore.streams[streamName]?.index;
|
|
47
|
+
if (!streamIndex || streamIndex.length === 0) {
|
|
48
|
+
return emptyIterator;
|
|
43
49
|
}
|
|
44
|
-
const streamIndex = eventStore.streams[streamName].index;
|
|
45
50
|
const from = streamIndex.find(this.minRevision, this.minRevision <= this.maxRevision);
|
|
46
51
|
const until = streamIndex.find(this.maxRevision, this.minRevision > this.maxRevision);
|
|
52
|
+
if (from === 0 || until === 0) {
|
|
53
|
+
// find() returns 0 when the requested revision is outside the stream's range
|
|
54
|
+
// (e.g. minRevision > all entries, or maxRevision < all entries).
|
|
55
|
+
return emptyIterator;
|
|
56
|
+
}
|
|
47
57
|
return eventStore.storage.readRange(from, until, streamIndex);
|
|
48
58
|
});
|
|
49
59
|
}
|
|
@@ -78,25 +88,29 @@ class JoinEventStream extends EventStream {
|
|
|
78
88
|
if (!this._iterator) {
|
|
79
89
|
this._iterator = this.fetch();
|
|
80
90
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
while (true) {
|
|
92
|
+
let nextIndex = -1;
|
|
93
|
+
this._next.forEach((value, index) => {
|
|
94
|
+
if (typeof value === 'undefined') {
|
|
95
|
+
value = this._next[index] = this.getValue(index);
|
|
96
|
+
}
|
|
97
|
+
if (value === false) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (nextIndex === -1 || this.follows(this._next[nextIndex].metadata.commitId, value.metadata.commitId)) {
|
|
101
|
+
nextIndex = index;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (nextIndex === -1) {
|
|
106
|
+
return false;
|
|
88
107
|
}
|
|
89
|
-
|
|
90
|
-
|
|
108
|
+
const next = this._next[nextIndex];
|
|
109
|
+
this._next[nextIndex] = undefined;
|
|
110
|
+
if (!this.predicate || this.predicate(next.payload, next.metadata)) {
|
|
111
|
+
return next;
|
|
91
112
|
}
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
if (nextIndex === -1) {
|
|
95
|
-
return false;
|
|
96
113
|
}
|
|
97
|
-
const next = this._next[nextIndex];
|
|
98
|
-
this._next[nextIndex] = undefined;
|
|
99
|
-
return next;
|
|
100
114
|
}
|
|
101
115
|
|
|
102
116
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import ReadablePartition, { CorruptFileError, HEADER_MAGIC, DOCUMENT_ALIGNMENT, DOCUMENT_SEPARATOR, DOCUMENT_HEADER_SIZE, DOCUMENT_FOOTER_SIZE } from './ReadablePartition.js';
|
|
3
|
-
import { assert,
|
|
3
|
+
import { assert, alignTo } from '../util.js';
|
|
4
|
+
import { buildMetadataHeader } from '../metadataUtil.js';
|
|
5
|
+
import { ensureDirectory } from '../fsUtil.js';
|
|
4
6
|
import Clock from '../Clock.js';
|
|
5
7
|
|
|
6
8
|
const DEFAULT_WRITE_BUFFER_SIZE = 16 * 1024;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A fixed-capacity registry of partitions with LRU eviction of open file handles.
|
|
3
|
+
*
|
|
4
|
+
* All partitions are stored by their numeric id and may be queried at any time.
|
|
5
|
+
* The pool additionally tracks which partitions currently have an open file descriptor
|
|
6
|
+
* in LRU (least-recently-used) order. When the pool is asked to open a partition and
|
|
7
|
+
* doing so would exceed the configured cap, the least-recently-used open partition is
|
|
8
|
+
* closed first to stay within the limit.
|
|
9
|
+
*
|
|
10
|
+
* Setting the cap to 0 disables eviction: all partitions are allowed to remain open
|
|
11
|
+
* simultaneously, which matches the uncapped behaviour of the original implementation.
|
|
12
|
+
*/
|
|
13
|
+
class PartitionPool {
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {number} [maxOpen=0] Maximum number of simultaneously open partition file
|
|
17
|
+
* handles. 0 disables the limit (no eviction).
|
|
18
|
+
*/
|
|
19
|
+
constructor(maxOpen = 0) {
|
|
20
|
+
this.maxOpen = maxOpen;
|
|
21
|
+
/** Registry of all known partitions keyed by id. */
|
|
22
|
+
this.registry = Object.create(null);
|
|
23
|
+
/**
|
|
24
|
+
* Insertion-order map used for LRU tracking of open file handles.
|
|
25
|
+
* Key = partition id, value = true.
|
|
26
|
+
* Oldest (least-recently-used) entry is first; newest (most-recently-used) is last.
|
|
27
|
+
*/
|
|
28
|
+
this.handles = new Map();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Register a partition under the given id.
|
|
33
|
+
*
|
|
34
|
+
* @param {number|string} id
|
|
35
|
+
* @param {object} partition
|
|
36
|
+
*/
|
|
37
|
+
add(id, partition) {
|
|
38
|
+
this.registry[id] = partition;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Retrieve a registered partition without opening it.
|
|
43
|
+
*
|
|
44
|
+
* @param {number|string} id
|
|
45
|
+
* @returns {object|undefined}
|
|
46
|
+
*/
|
|
47
|
+
get(id) {
|
|
48
|
+
return this.registry[id];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check whether a partition with the given id is registered in the pool.
|
|
53
|
+
*
|
|
54
|
+
* @param {number|string} id
|
|
55
|
+
* @returns {boolean}
|
|
56
|
+
*/
|
|
57
|
+
has(id) {
|
|
58
|
+
return id in this.registry;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Open the partition with the given id, applying LRU eviction if necessary.
|
|
63
|
+
*
|
|
64
|
+
* If the partition is not yet open and adding it would exceed `maxOpen`, the
|
|
65
|
+
* least-recently-used open partition is closed first. Stale entries (partitions
|
|
66
|
+
* that were closed externally) are discarded from the LRU map as they are
|
|
67
|
+
* encountered; if all tracked entries turn out to be stale the loop exits without
|
|
68
|
+
* closing any partition — the handle count stays temporarily inflated (bounded by
|
|
69
|
+
* the number of external closes since the last `open()` call) but correctness is
|
|
70
|
+
* preserved.
|
|
71
|
+
*
|
|
72
|
+
* @param {number|string} id
|
|
73
|
+
* @returns {object} The opened partition.
|
|
74
|
+
*/
|
|
75
|
+
open(id) {
|
|
76
|
+
const partition = this.registry[id];
|
|
77
|
+
|
|
78
|
+
if (this.maxOpen > 0) {
|
|
79
|
+
// Remove id first — this may already bring the handle count below the cap.
|
|
80
|
+
this.handles.delete(id);
|
|
81
|
+
if (this.handles.size >= this.maxOpen) {
|
|
82
|
+
for (const [lruId] of this.handles) {
|
|
83
|
+
this.handles.delete(lruId);
|
|
84
|
+
const lruPartition = this.registry[lruId];
|
|
85
|
+
if (lruPartition && lruPartition.isOpen()) {
|
|
86
|
+
lruPartition.close();
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// (Re-)add id at the MRU end of the map.
|
|
92
|
+
this.handles.set(id, true);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
partition.open();
|
|
96
|
+
return partition;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Invoke `callback` for every registered partition.
|
|
101
|
+
*
|
|
102
|
+
* @param {function(object): void} callback
|
|
103
|
+
*/
|
|
104
|
+
forEach(callback) {
|
|
105
|
+
for (const id of Object.keys(this.registry)) {
|
|
106
|
+
callback(this.registry[id]);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Yield every registered partition object.
|
|
112
|
+
*
|
|
113
|
+
* @returns {Generator<object>}
|
|
114
|
+
*/
|
|
115
|
+
*values() {
|
|
116
|
+
for (const id of Object.keys(this.registry)) {
|
|
117
|
+
yield this.registry[id];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The total number of registered partitions.
|
|
123
|
+
* @returns {number}
|
|
124
|
+
*/
|
|
125
|
+
get count() {
|
|
126
|
+
return Object.keys(this.registry).length;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The number of open partition file handles currently tracked by the pool.
|
|
131
|
+
* @returns {number}
|
|
132
|
+
*/
|
|
133
|
+
get openCount() {
|
|
134
|
+
return this.handles.size;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Reset the open-handle tracking without closing any partitions.
|
|
139
|
+
*
|
|
140
|
+
* Call this after externally closing all partitions (e.g. after
|
|
141
|
+
* `checkTornWrites`) to keep the pool's LRU state consistent with reality.
|
|
142
|
+
*/
|
|
143
|
+
clearOpenHandles() {
|
|
144
|
+
this.handles.clear();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export default PartitionPool;
|
|
@@ -54,9 +54,9 @@ class ReadOnlyStorage extends ReadableStorage {
|
|
|
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
|
}
|