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
|
@@ -3,8 +3,9 @@ import path from 'path';
|
|
|
3
3
|
import WritablePartition from '../Partition/WritablePartition.js';
|
|
4
4
|
import WritableIndex, { Entry as WritableIndexEntry } from '../Index/WritableIndex.js';
|
|
5
5
|
import ReadableStorage from './ReadableStorage.js';
|
|
6
|
-
import { assert
|
|
7
|
-
import {
|
|
6
|
+
import { assert } from '../utils/util.js';
|
|
7
|
+
import { ensureDirectory } from '../utils/fsUtil.js';
|
|
8
|
+
import { matches, buildMetadataForMatcher, buildMatcherFromMetadata } from '../utils/metadataUtil.js';
|
|
8
9
|
|
|
9
10
|
const DEFAULT_WRITE_BUFFER_SIZE = 16 * 1024;
|
|
10
11
|
|
|
@@ -61,24 +62,31 @@ class WritableStorage extends ReadableStorage {
|
|
|
61
62
|
super(storageName, config);
|
|
62
63
|
|
|
63
64
|
this.lockFile = path.resolve(this.dataDirectory, this.storageFile + '.lock');
|
|
64
|
-
|
|
65
|
-
this.unlock();
|
|
66
|
-
}
|
|
65
|
+
this._lockMode = config.lock;
|
|
67
66
|
this.partitioner = config.partitioner;
|
|
67
|
+
this.partitionIds = {};
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
71
|
* @inheritDoc
|
|
72
|
+
* Acquires the write lock synchronously.
|
|
73
|
+
* For LOCK_RECLAIM, removes any orphaned lock before trying to acquire our own; torn-write
|
|
74
|
+
* repair runs after the primary index is open, before `'opened'` is emitted.
|
|
75
|
+
*
|
|
72
76
|
* @returns {boolean}
|
|
73
77
|
* @throws {StorageLockedError} If this storage is locked by another process.
|
|
74
78
|
*/
|
|
75
|
-
open() {
|
|
79
|
+
open(callback) {
|
|
80
|
+
const needsRepair = this._lockMode === LOCK_RECLAIM && this.unlock();
|
|
81
|
+
|
|
76
82
|
if (!this.lock()) {
|
|
77
83
|
return true;
|
|
78
84
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
|
|
86
|
+
const onOpen = needsRepair
|
|
87
|
+
? () => { this.checkTornWrites(); callback?.(); }
|
|
88
|
+
: callback;
|
|
89
|
+
return super.open(onOpen);
|
|
82
90
|
}
|
|
83
91
|
|
|
84
92
|
/**
|
|
@@ -170,6 +178,8 @@ class WritableStorage extends ReadableStorage {
|
|
|
170
178
|
}
|
|
171
179
|
|
|
172
180
|
this.forEachPartition(partition => partition.close());
|
|
181
|
+
// Partitions were closed directly (bypassing the pool), so reset the open-handle tracking.
|
|
182
|
+
this.partitions.clearOpenHandles();
|
|
173
183
|
}
|
|
174
184
|
|
|
175
185
|
/**
|
|
@@ -195,8 +205,8 @@ class WritableStorage extends ReadableStorage {
|
|
|
195
205
|
|
|
196
206
|
// Scan partitions in sequence-number order and rebuild index entries.
|
|
197
207
|
// iterateDocumentsNoIndex opens any closed partitions automatically.
|
|
198
|
-
for (const { document,
|
|
199
|
-
const newEntry = new WritableIndexEntry(this.index.length + 1, position, size, partition);
|
|
208
|
+
for (const { document, entry } of this.iterateDocumentsNoIndex(fromSequenceNumber)) {
|
|
209
|
+
const newEntry = new WritableIndexEntry(this.index.length + 1, entry.position, entry.size, entry.partition);
|
|
200
210
|
this.index.add(newEntry);
|
|
201
211
|
|
|
202
212
|
this.forEachWritableSecondaryIndex((secIndex) => {
|
|
@@ -222,9 +232,8 @@ class WritableStorage extends ReadableStorage {
|
|
|
222
232
|
this.locked = true;
|
|
223
233
|
} catch (e) {
|
|
224
234
|
/* istanbul ignore if */
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
235
|
+
assert(e.code === 'EEXIST', `Error creating lock for storage ${this.storageFile}: ` + e.message)
|
|
236
|
+
|
|
228
237
|
throw new StorageLockedError(`Storage ${this.storageFile} is locked by another process`);
|
|
229
238
|
}
|
|
230
239
|
return true;
|
|
@@ -234,19 +243,21 @@ class WritableStorage extends ReadableStorage {
|
|
|
234
243
|
* Unlock this storage, no matter if it was previously locked by this writer.
|
|
235
244
|
* Only use this if you are sure there is no other process still having a writer open.
|
|
236
245
|
* Current implementation just deletes a lock file that is named like the storage.
|
|
246
|
+
* @returns {boolean} True if an orphaned lock from another process was removed.
|
|
237
247
|
*/
|
|
238
248
|
unlock() {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
249
|
+
const lockExists = fs.existsSync(this.lockFile);
|
|
250
|
+
const orphaned = lockExists && !this.locked;
|
|
251
|
+
if (lockExists) {
|
|
243
252
|
fs.rmdirSync(this.lockFile);
|
|
244
253
|
}
|
|
245
254
|
this.locked = false;
|
|
255
|
+
return orphaned;
|
|
246
256
|
}
|
|
247
257
|
|
|
248
258
|
/**
|
|
249
259
|
* @inheritDoc
|
|
260
|
+
* Unlocks the storage, then delegates to the parent close().
|
|
250
261
|
*/
|
|
251
262
|
close() {
|
|
252
263
|
if (this.locked) {
|
|
@@ -300,11 +311,43 @@ class WritableStorage extends ReadableStorage {
|
|
|
300
311
|
this.on('preCommit', hook);
|
|
301
312
|
}
|
|
302
313
|
|
|
314
|
+
getPartitionIdForName(partitionShortName, partitionName) {
|
|
315
|
+
const partitionId = this.partitionIds[partitionShortName] ?? WritablePartition.idFor(partitionName);
|
|
316
|
+
this.partitionIds[partitionShortName] = partitionId;
|
|
317
|
+
return partitionId;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
buildPartitionConfig(partitionShortName) {
|
|
321
|
+
if (typeof this.partitionConfig.metadata !== 'function') {
|
|
322
|
+
return this.partitionConfig;
|
|
323
|
+
}
|
|
324
|
+
return { ...this.partitionConfig, metadata: this.partitionConfig.metadata(partitionShortName) };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
ensurePartitionDirectory(partitionName) {
|
|
328
|
+
if (!partitionName.includes('/')) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
ensureDirectory(path.join(this.dataDirectory, path.dirname(partitionName)));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
createNamedPartition(partitionId, partitionName, partitionShortName) {
|
|
335
|
+
if (this.partitions.has(partitionId)) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const partitionConfig = this.buildPartitionConfig(partitionShortName);
|
|
339
|
+
this.ensurePartitionDirectory(partitionName);
|
|
340
|
+
this.partitions.add(partitionId, this.createPartition(partitionName, partitionConfig));
|
|
341
|
+
this.emit('partition-created', partitionId);
|
|
342
|
+
}
|
|
343
|
+
|
|
303
344
|
/**
|
|
304
345
|
* Get a partition either by name or by id.
|
|
305
346
|
* If a partition with the given name does not exist, a new one will be created.
|
|
306
347
|
* If a partition with the given id does not exist, an error is thrown.
|
|
307
348
|
*
|
|
349
|
+
* Partition opening and LRU tracking are delegated to `super.getPartition()`.
|
|
350
|
+
*
|
|
308
351
|
* @protected
|
|
309
352
|
* @param {string|number} partitionIdentifier The partition name or the partition Id
|
|
310
353
|
* @returns {ReadablePartition}
|
|
@@ -314,16 +357,8 @@ class WritableStorage extends ReadableStorage {
|
|
|
314
357
|
if (typeof partitionIdentifier === 'string') {
|
|
315
358
|
const partitionShortName = partitionIdentifier;
|
|
316
359
|
const partitionName = this.storageFile + (partitionIdentifier.length ? '.' + partitionIdentifier : '');
|
|
317
|
-
partitionIdentifier =
|
|
318
|
-
|
|
319
|
-
const partitionConfig = typeof this.partitionConfig.metadata === 'function'
|
|
320
|
-
? { ...this.partitionConfig, metadata: this.partitionConfig.metadata(partitionShortName) }
|
|
321
|
-
: this.partitionConfig;
|
|
322
|
-
this.partitions[partitionIdentifier] = this.createPartition(partitionName, partitionConfig);
|
|
323
|
-
this.emit('partition-created', partitionIdentifier);
|
|
324
|
-
}
|
|
325
|
-
this.partitions[partitionIdentifier].open();
|
|
326
|
-
return this.partitions[partitionIdentifier];
|
|
360
|
+
partitionIdentifier = this.getPartitionIdForName(partitionShortName, partitionName);
|
|
361
|
+
this.createNamedPartition(partitionIdentifier, partitionName, partitionShortName);
|
|
327
362
|
}
|
|
328
363
|
return super.getPartition(partitionIdentifier);
|
|
329
364
|
}
|
|
@@ -340,18 +375,15 @@ class WritableStorage extends ReadableStorage {
|
|
|
340
375
|
|
|
341
376
|
const partitionName = this.partitioner(document, this.index.length + 1);
|
|
342
377
|
const partition = this.getPartition(partitionName);
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
378
|
+
this.emit('preCommit', document, partition.metadata);
|
|
379
|
+
|
|
346
380
|
const position = partition.write(data, this.length, callback);
|
|
347
381
|
|
|
348
382
|
assert(position !== false, 'Error writing document.');
|
|
349
383
|
|
|
350
384
|
const indexEntry = this.addIndex(partition.id, position, dataSize, document);
|
|
351
385
|
this.forEachSecondaryIndex((index, name) => {
|
|
352
|
-
|
|
353
|
-
index.open();
|
|
354
|
-
}
|
|
386
|
+
index.open();
|
|
355
387
|
index.add(indexEntry);
|
|
356
388
|
this.emit('index-add', name, index.length, document);
|
|
357
389
|
}, document);
|
|
@@ -366,10 +398,11 @@ class WritableStorage extends ReadableStorage {
|
|
|
366
398
|
* @api
|
|
367
399
|
* @param {string} name The index name.
|
|
368
400
|
* @param {Matcher} [matcher] An object that describes the document properties that need to match to add it this index or a function that receives a document and returns true if the document should be indexed.
|
|
401
|
+
* @param {boolean} [reindex=true] Whether to scan existing documents and populate the new index. Set to false when it is known that no existing documents can match the matcher.
|
|
369
402
|
* @returns {ReadableIndex} The index containing all documents that match the query.
|
|
370
403
|
* @throws {Error} if the index doesn't exist yet and no matcher was specified.
|
|
371
404
|
*/
|
|
372
|
-
ensureIndex(name, matcher) {
|
|
405
|
+
ensureIndex(name, matcher, reindex = true) {
|
|
373
406
|
if (name === '_all') {
|
|
374
407
|
return this.index;
|
|
375
408
|
}
|
|
@@ -386,18 +419,21 @@ class WritableStorage extends ReadableStorage {
|
|
|
386
419
|
|
|
387
420
|
const metadata = buildMetadataForMatcher(matcher, this.hmac);
|
|
388
421
|
const { index } = this.createIndex(indexName, Object.assign({}, this.indexOptions, { metadata }));
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
422
|
+
if (reindex) {
|
|
423
|
+
try {
|
|
424
|
+
this.forEachDocument((document, indexEntry) => {
|
|
425
|
+
if (matches(document, matcher)) {
|
|
426
|
+
index.add(indexEntry);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
} catch (e) {
|
|
430
|
+
index.destroy();
|
|
431
|
+
throw e;
|
|
432
|
+
}
|
|
398
433
|
}
|
|
399
434
|
|
|
400
435
|
this.secondaryIndexes[name] = { index, matcher };
|
|
436
|
+
this.indexMatcher.add(name, matcher);
|
|
401
437
|
this.emit('index-created', name);
|
|
402
438
|
return index;
|
|
403
439
|
}
|
|
@@ -423,7 +459,7 @@ class WritableStorage extends ReadableStorage {
|
|
|
423
459
|
*/
|
|
424
460
|
forEachDistinctPartitionOf(entries, iterationHandler) {
|
|
425
461
|
const partitions = [];
|
|
426
|
-
const numPartitions =
|
|
462
|
+
const numPartitions = this.partitions.count;
|
|
427
463
|
for (let entry of entries) {
|
|
428
464
|
if (partitions.indexOf(entry.partition) >= 0) {
|
|
429
465
|
continue;
|
package/src/Watcher.js
CHANGED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { mkdirpSync } from 'mkdirp';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Ensure that the given directory exists.
|
|
7
|
+
* @param {string} dirName
|
|
8
|
+
* @return {boolean} true if the directory existed already
|
|
9
|
+
*/
|
|
10
|
+
function ensureDirectory(dirName) {
|
|
11
|
+
if (!fs.existsSync(dirName)) {
|
|
12
|
+
try {
|
|
13
|
+
mkdirpSync(dirName);
|
|
14
|
+
} catch (e) {
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Invoke `onEach` if `relativePath` matches `regexPattern`, passing the first capture group or the full match.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} relativePath
|
|
25
|
+
* @param {RegExp} regexPattern
|
|
26
|
+
* @param {function(string)} onEach
|
|
27
|
+
*/
|
|
28
|
+
function visitMatchingPath(relativePath, regexPattern, onEach) {
|
|
29
|
+
const match = relativePath.match(regexPattern);
|
|
30
|
+
if (match !== null) {
|
|
31
|
+
onEach(match[1] !== undefined ? match[1] : match[0]);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Classify `entries` into matching files (visited via `onEach`) and subdirectory names (returned).
|
|
37
|
+
*
|
|
38
|
+
* @param {fs.Dirent[]} entries
|
|
39
|
+
* @param {string} relativePrefix
|
|
40
|
+
* @param {RegExp} regexPattern
|
|
41
|
+
* @param {function(string)} onEach
|
|
42
|
+
* @returns {string[]} names of subdirectory entries
|
|
43
|
+
*/
|
|
44
|
+
function classifyEntries(entries, relativePrefix, regexPattern, onEach) {
|
|
45
|
+
const subdirs = [];
|
|
46
|
+
for (let entry of entries) {
|
|
47
|
+
if (entry.isDirectory()) {
|
|
48
|
+
subdirs.push(entry.name);
|
|
49
|
+
} else {
|
|
50
|
+
visitMatchingPath(relativePrefix + entry.name, regexPattern, onEach);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return subdirs;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Sequentially scan each name in `subdirs`, calling `done` when all are complete or on first error.
|
|
58
|
+
*
|
|
59
|
+
* @param {string[]} subdirs
|
|
60
|
+
* @param {string} dir
|
|
61
|
+
* @param {string} relativePrefix
|
|
62
|
+
* @param {RegExp} regexPattern
|
|
63
|
+
* @param {function(string)} onEach
|
|
64
|
+
* @param {function(Error?)} done
|
|
65
|
+
*/
|
|
66
|
+
function scanSubdirs(subdirs, dir, relativePrefix, regexPattern, onEach, done) {
|
|
67
|
+
let i = 0;
|
|
68
|
+
function next() {
|
|
69
|
+
if (i >= subdirs.length) return done(null);
|
|
70
|
+
const name = subdirs[i++];
|
|
71
|
+
scanDir(path.join(dir, name), relativePrefix + name + '/', false, regexPattern, onEach, (err) => {
|
|
72
|
+
if (err) return done(err);
|
|
73
|
+
next();
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
next();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Asynchronously scan one directory level, then recurse into subdirectories sequentially.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} dir
|
|
83
|
+
* @param {string} relativePrefix
|
|
84
|
+
* @param {boolean} isRoot
|
|
85
|
+
* @param {RegExp} regexPattern
|
|
86
|
+
* @param {function(string)} onEach
|
|
87
|
+
* @param {function(Error?)} done
|
|
88
|
+
*/
|
|
89
|
+
function scanDir(dir, relativePrefix, isRoot, regexPattern, onEach, done) {
|
|
90
|
+
fs.readdir(dir, { withFileTypes: true }, (err, entries) => {
|
|
91
|
+
if (err) {
|
|
92
|
+
/* istanbul ignore next */
|
|
93
|
+
if (!isRoot && err.code === 'ENOENT') return done(null);
|
|
94
|
+
return done(err);
|
|
95
|
+
}
|
|
96
|
+
const subdirs = classifyEntries(entries, relativePrefix, regexPattern, onEach);
|
|
97
|
+
scanSubdirs(subdirs, dir, relativePrefix, regexPattern, onEach, done);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Scan a directory (and its subdirectories) for files whose relative paths match a regex pattern,
|
|
103
|
+
* calling a callback for each match.
|
|
104
|
+
*
|
|
105
|
+
* The regex is matched against the **relative path from `directory`** (e.g. `eventstore.stream-x/foo.index`),
|
|
106
|
+
* so patterns that capture a path prefix work transparently for both flat and nested layouts.
|
|
107
|
+
*
|
|
108
|
+
* The `onEach` callback receives the first capturing group of the match (`match[1]`), or the full
|
|
109
|
+
* match (`match[0]`) when no capturing group is defined in the pattern.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} directory The root directory to scan.
|
|
112
|
+
* @param {RegExp} regexPattern The pattern to match relative file paths against.
|
|
113
|
+
* @param {function(string)} onEach Called with the first capturing group (or full match) for each matching path.
|
|
114
|
+
* @param {function(Error?)} onDone Called when the scan is complete, or with an error if one occurred.
|
|
115
|
+
*/
|
|
116
|
+
function scanForFiles(directory, regexPattern, onEach, onDone) {
|
|
117
|
+
scanDir(directory, '', true, regexPattern, onEach, onDone);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export {
|
|
121
|
+
ensureDirectory,
|
|
122
|
+
scanForFiles,
|
|
123
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const BYTE_QUOTE = 0x22;
|
|
2
|
+
const BYTE_ESCAPE = 0x5c;
|
|
3
|
+
const BYTE_OPEN_OBJECT = 0x7b;
|
|
4
|
+
const BYTE_CLOSE_OBJECT = 0x7d;
|
|
5
|
+
const BYTE_OPEN_ARRAY = 0x5b;
|
|
6
|
+
const BYTE_CLOSE_ARRAY = 0x5d;
|
|
7
|
+
const BYTE_COMMA = 0x2c;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Find the position of `pattern` within `buffer` at depth 0 (the top-level object), starting
|
|
11
|
+
* from `startOffset`. It scans character-by-character tracking JSON nesting depth and string
|
|
12
|
+
* quoting. If `matchPosition` arrives at depth > 0 it means the pattern is inside a nested
|
|
13
|
+
* object/array, so the scan continues searching for the next candidate at depth 0. Returns -1
|
|
14
|
+
* when no such position exists before the end of the buffer or when a closing brace reduces depth
|
|
15
|
+
* below zero (the top-level object has ended).
|
|
16
|
+
*/
|
|
17
|
+
function indexOfSameLevel(buffer, pattern, startOffset = 0, matchPosition) {
|
|
18
|
+
/* c8 ignore start */
|
|
19
|
+
// Defensive fallback: public call path precomputes an initial candidate in preCheck.
|
|
20
|
+
if (matchPosition === undefined) {
|
|
21
|
+
matchPosition = buffer.indexOf(pattern, startOffset);
|
|
22
|
+
}
|
|
23
|
+
if (matchPosition === -1) {
|
|
24
|
+
return -1;
|
|
25
|
+
}
|
|
26
|
+
/* c8 ignore stop */
|
|
27
|
+
|
|
28
|
+
let depth = 0;
|
|
29
|
+
let inString = false;
|
|
30
|
+
let i = startOffset;
|
|
31
|
+
|
|
32
|
+
while (i < buffer.length) {
|
|
33
|
+
if (inString) {
|
|
34
|
+
if (buffer[i] === BYTE_ESCAPE) { // '\\'
|
|
35
|
+
i += 2;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (buffer[i] === BYTE_QUOTE) { // '"'
|
|
39
|
+
inString = false;
|
|
40
|
+
}
|
|
41
|
+
i++;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ch = buffer[i];
|
|
46
|
+
if (ch === BYTE_OPEN_OBJECT || ch === BYTE_OPEN_ARRAY) { // '{' or '['
|
|
47
|
+
depth++;
|
|
48
|
+
i++;
|
|
49
|
+
continue;
|
|
50
|
+
} else if (ch === BYTE_CLOSE_OBJECT || ch === BYTE_CLOSE_ARRAY) { // '}' or ']'
|
|
51
|
+
depth--;
|
|
52
|
+
|
|
53
|
+
if (depth < 0) {
|
|
54
|
+
return -1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
i++;
|
|
58
|
+
continue;
|
|
59
|
+
} else if (ch === BYTE_QUOTE) { // '"'
|
|
60
|
+
inString = true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (i >= matchPosition) {
|
|
64
|
+
if (i === matchPosition && ch === BYTE_QUOTE && depth === 0) { // '"'
|
|
65
|
+
const end = i + pattern.length;
|
|
66
|
+
if (pattern[pattern.length - 1] === BYTE_OPEN_OBJECT) { // '{'
|
|
67
|
+
return i;
|
|
68
|
+
}
|
|
69
|
+
if (buffer[end] === BYTE_COMMA || buffer[end] === BYTE_CLOSE_OBJECT || buffer[end] === BYTE_CLOSE_ARRAY) { // ',' or '}' or ']'
|
|
70
|
+
return i;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
matchPosition = buffer.indexOf(pattern, matchPosition + 1);
|
|
75
|
+
if (matchPosition < 0) {
|
|
76
|
+
return -1;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
i++;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* c8 ignore next */
|
|
84
|
+
return -1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export { BYTE_OPEN_OBJECT, indexOfSameLevel };
|