event-storage 1.2.0 → 1.3.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 +28 -4
- package/index.js +1 -0
- package/package.json +2 -1
- package/src/Consumer.js +7 -13
- package/src/EventStore.js +28 -52
- package/src/EventStream.js +8 -28
- package/src/Index/ReadOnlyIndex.js +1 -1
- package/src/Index/ReadableIndex.js +2 -4
- package/src/Index/WritableIndex.js +2 -4
- package/src/JoinEventStream.js +3 -13
- package/src/Partition/ReadOnlyPartition.js +1 -1
- package/src/Partition/ReadablePartition.js +5 -5
- package/src/Partition/WritablePartition.js +1 -1
- package/src/Storage/ReadOnlyStorage.js +1 -1
- package/src/Storage/ReadableStorage.js +9 -12
- package/src/Storage/WritableStorage.js +9 -12
- package/src/utils/apiHelpers.js +123 -0
- package/src/utils/fsUtil.js +27 -23
- package/src/utils/jsonUtil.js +257 -42
- package/src/utils/metadataUtil.js +357 -87
- package/src/utils/util.js +20 -17
|
@@ -6,6 +6,7 @@ import ReadableStorage from './ReadableStorage.js';
|
|
|
6
6
|
import { assert } from '../utils/util.js';
|
|
7
7
|
import { ensureDirectory } from '../utils/fsUtil.js';
|
|
8
8
|
import { matches, buildMetadataForMatcher, buildMatcherFromMetadata } from '../utils/metadataUtil.js';
|
|
9
|
+
import { normalizeNamedCtorArgs } from '../utils/apiHelpers.js';
|
|
9
10
|
|
|
10
11
|
const DEFAULT_WRITE_BUFFER_SIZE = 16 * 1024;
|
|
11
12
|
|
|
@@ -44,10 +45,7 @@ class WritableStorage extends ReadableStorage {
|
|
|
44
45
|
* @param {number} [config.lock] One of LOCK_* constants that defines how an existing lock should be handled.
|
|
45
46
|
*/
|
|
46
47
|
constructor(storageName = 'storage', config = {}) {
|
|
47
|
-
|
|
48
|
-
config = storageName;
|
|
49
|
-
storageName = undefined;
|
|
50
|
-
}
|
|
48
|
+
({ name: storageName, options: config } = normalizeNamedCtorArgs(storageName, config));
|
|
51
49
|
const defaults = {
|
|
52
50
|
partitioner: (document, number) => '',
|
|
53
51
|
writeBufferSize: DEFAULT_WRITE_BUFFER_SIZE,
|
|
@@ -100,7 +98,7 @@ class WritableStorage extends ReadableStorage {
|
|
|
100
98
|
*/
|
|
101
99
|
forEachWritableSecondaryIndex(iterationHandler, matchDocument) {
|
|
102
100
|
this.forEachSecondaryIndex((index, name) => {
|
|
103
|
-
/*
|
|
101
|
+
/* c8 ignore next */
|
|
104
102
|
if (!(index instanceof WritableIndex)) return;
|
|
105
103
|
const wasOpen = index.isOpen();
|
|
106
104
|
if (!wasOpen) index.open();
|
|
@@ -123,7 +121,7 @@ class WritableStorage extends ReadableStorage {
|
|
|
123
121
|
this.forEachPartition(partition => {
|
|
124
122
|
partition.open();
|
|
125
123
|
const last = partition.readLast();
|
|
126
|
-
/*
|
|
124
|
+
/* c8 ignore next */
|
|
127
125
|
if (!last) return;
|
|
128
126
|
const { header: { sequenceNumber, dataSize }, position } = last;
|
|
129
127
|
if (position + partition.documentWriteSize(dataSize) > partition.size) {
|
|
@@ -165,7 +163,7 @@ class WritableStorage extends ReadableStorage {
|
|
|
165
163
|
// Truncate all indexes to the torn-write boundary.
|
|
166
164
|
this.index.open();
|
|
167
165
|
this.index.truncate(lastValidSequenceNumber);
|
|
168
|
-
/*
|
|
166
|
+
/* c8 ignore next */
|
|
169
167
|
this.forEachWritableSecondaryIndex(index => {
|
|
170
168
|
index.truncate(index.find(lastValidSequenceNumber));
|
|
171
169
|
});
|
|
@@ -231,7 +229,7 @@ class WritableStorage extends ReadableStorage {
|
|
|
231
229
|
fs.mkdirSync(this.lockFile);
|
|
232
230
|
this.locked = true;
|
|
233
231
|
} catch (e) {
|
|
234
|
-
/*
|
|
232
|
+
/* c8 ignore next */
|
|
235
233
|
assert(e.code === 'EEXIST', `Error creating lock for storage ${this.storageFile}: ` + e.message)
|
|
236
234
|
|
|
237
235
|
throw new StorageLockedError(`Storage ${this.storageFile} is locked by another process`);
|
|
@@ -290,7 +288,7 @@ class WritableStorage extends ReadableStorage {
|
|
|
290
288
|
const entry = new WritableIndexEntry(this.index.length + 1, position, size, partitionId);
|
|
291
289
|
this.index.add(entry, (indexPosition) => {
|
|
292
290
|
this.emit('wrote', document, entry, indexPosition);
|
|
293
|
-
/*
|
|
291
|
+
/* c8 ignore next 3 */
|
|
294
292
|
if (typeof callback === 'function') {
|
|
295
293
|
return callback(indexPosition);
|
|
296
294
|
}
|
|
@@ -509,9 +507,8 @@ class WritableStorage extends ReadableStorage {
|
|
|
509
507
|
2) truncate all partitions accordingly
|
|
510
508
|
3) truncate/rewrite all indexes
|
|
511
509
|
*/
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
}
|
|
510
|
+
this.index.open();
|
|
511
|
+
|
|
515
512
|
if (after < 0) {
|
|
516
513
|
after += this.index.length;
|
|
517
514
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize commit overloads into a single argument object.
|
|
3
|
+
*
|
|
4
|
+
* @param {object|object[]} events Event or event list.
|
|
5
|
+
* @param {number|object|function} expectedVersion Expected version, CommitCondition, or already metadata/callback.
|
|
6
|
+
* @param {object|function} metadata Commit metadata or callback.
|
|
7
|
+
* @param {function} callback Completion callback.
|
|
8
|
+
* @param {number} ExpectedVersionAny Fallback value for "any" expectedVersion.
|
|
9
|
+
* @param {Function} CommitConditionClass Class used for CommitCondition checks.
|
|
10
|
+
* @returns {{events: object[], expectedVersion: number|object, metadata: object, callback: function}} Normalized commit arguments.
|
|
11
|
+
*/
|
|
12
|
+
function fixCommitArgumentTypes(events, expectedVersion, metadata, callback, ExpectedVersionAny, CommitConditionClass) {
|
|
13
|
+
if (!(events instanceof Array)) {
|
|
14
|
+
events = [events];
|
|
15
|
+
}
|
|
16
|
+
if (typeof expectedVersion !== 'number' && !(expectedVersion instanceof CommitConditionClass)) {
|
|
17
|
+
callback = metadata;
|
|
18
|
+
metadata = expectedVersion;
|
|
19
|
+
expectedVersion = ExpectedVersionAny;
|
|
20
|
+
}
|
|
21
|
+
if (typeof metadata !== 'object') {
|
|
22
|
+
callback = metadata;
|
|
23
|
+
metadata = {};
|
|
24
|
+
}
|
|
25
|
+
if (typeof callback !== 'function') {
|
|
26
|
+
callback = () => {};
|
|
27
|
+
}
|
|
28
|
+
return { events, expectedVersion, metadata, callback };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Derive the stream name from an index name.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} indexName Index file/index name.
|
|
35
|
+
* @returns {string} Corresponding stream name.
|
|
36
|
+
*/
|
|
37
|
+
function parseStreamFromIndexName(indexName) {
|
|
38
|
+
if (indexName === '_all') {
|
|
39
|
+
return '_all';
|
|
40
|
+
}
|
|
41
|
+
if (indexName.startsWith('stream-')) {
|
|
42
|
+
return indexName.slice(7);
|
|
43
|
+
}
|
|
44
|
+
return indexName;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Support predicate/raw shorthand (`predicate=true`).
|
|
49
|
+
*
|
|
50
|
+
* @param {object|function|boolean|null} predicate Filter predicate or raw shorthand.
|
|
51
|
+
* @param {boolean} raw Raw flag from the call signature.
|
|
52
|
+
* @returns {{predicate: object|function|null, raw: boolean}} Normalized predicate/raw pair.
|
|
53
|
+
*/
|
|
54
|
+
function normalizePredicateRaw(predicate, raw) {
|
|
55
|
+
if (typeof predicate === 'boolean' && raw === false) {
|
|
56
|
+
return { predicate: null, raw: predicate };
|
|
57
|
+
}
|
|
58
|
+
return { predicate, raw };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Support constructor overloads with optional `name`.
|
|
63
|
+
*
|
|
64
|
+
* @param {string|object} name Name or already the options object.
|
|
65
|
+
* @param {object} options Options object when `name` is provided.
|
|
66
|
+
* @param {string|undefined} [fallbackName=undefined] Fallback name when no explicit `name` is passed.
|
|
67
|
+
* @returns {{name: string|undefined, options: object}} Normalized name/options pair.
|
|
68
|
+
*/
|
|
69
|
+
function normalizeNamedCtorArgs(name, options, fallbackName = undefined) {
|
|
70
|
+
if (typeof name !== 'string') {
|
|
71
|
+
return { name: fallbackName, options: name };
|
|
72
|
+
}
|
|
73
|
+
return { name, options };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Normalize negative revisions relative to stream length.
|
|
78
|
+
*
|
|
79
|
+
* @param {number} version Requested revision.
|
|
80
|
+
* @param {number} length Current stream length.
|
|
81
|
+
* @returns {number} Resolved revision.
|
|
82
|
+
*/
|
|
83
|
+
function normalizeRevision(version, length) {
|
|
84
|
+
return version < 0 ? version + length + 1 : version;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Clamp and normalize maxRevision, including negative values.
|
|
89
|
+
*
|
|
90
|
+
* @param {number} length Current stream length.
|
|
91
|
+
* @param {number} maxRevision Requested max revision.
|
|
92
|
+
* @returns {number} Effective max revision in valid range.
|
|
93
|
+
*/
|
|
94
|
+
function normalizeMaxRevision(length, maxRevision) {
|
|
95
|
+
return Math.min(length, maxRevision < 0 ? length + maxRevision + 1 : maxRevision);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Support the consumer overload where the first argument is a numeric start offset.
|
|
100
|
+
*
|
|
101
|
+
* @param {object|number} initialState Initial state or numeric start offset.
|
|
102
|
+
* @param {number} startFrom Start offset from the call signature.
|
|
103
|
+
* @returns {{initialState: object, startFrom: number}} Normalized consumer initialization values.
|
|
104
|
+
*/
|
|
105
|
+
function normalizeConsumerStateArgs(initialState, startFrom) {
|
|
106
|
+
if (typeof initialState === 'number') {
|
|
107
|
+
return { initialState: {}, startFrom: initialState };
|
|
108
|
+
}
|
|
109
|
+
return { initialState, startFrom };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export {
|
|
113
|
+
fixCommitArgumentTypes,
|
|
114
|
+
parseStreamFromIndexName,
|
|
115
|
+
normalizePredicateRaw,
|
|
116
|
+
normalizeNamedCtorArgs,
|
|
117
|
+
normalizeRevision,
|
|
118
|
+
normalizeMaxRevision,
|
|
119
|
+
normalizeConsumerStateArgs
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
package/src/utils/fsUtil.js
CHANGED
|
@@ -4,8 +4,8 @@ import { mkdirpSync } from 'mkdirp';
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Ensure that the given directory exists.
|
|
7
|
-
* @param {string} dirName
|
|
8
|
-
* @
|
|
7
|
+
* @param {string} dirName Target directory.
|
|
8
|
+
* @returns {boolean} True when the directory already existed.
|
|
9
9
|
*/
|
|
10
10
|
function ensureDirectory(dirName) {
|
|
11
11
|
if (!fs.existsSync(dirName)) {
|
|
@@ -21,9 +21,10 @@ function ensureDirectory(dirName) {
|
|
|
21
21
|
/**
|
|
22
22
|
* Invoke `onEach` if `relativePath` matches `regexPattern`, passing the first capture group or the full match.
|
|
23
23
|
*
|
|
24
|
-
* @param {string} relativePath
|
|
25
|
-
* @param {RegExp} regexPattern
|
|
26
|
-
* @param {function(string)} onEach
|
|
24
|
+
* @param {string} relativePath Relative file path.
|
|
25
|
+
* @param {RegExp} regexPattern Regex used for path matching.
|
|
26
|
+
* @param {function(string): void} onEach Callback invoked per match.
|
|
27
|
+
* @returns {void}
|
|
27
28
|
*/
|
|
28
29
|
function visitMatchingPath(relativePath, regexPattern, onEach) {
|
|
29
30
|
const match = relativePath.match(regexPattern);
|
|
@@ -35,11 +36,11 @@ function visitMatchingPath(relativePath, regexPattern, onEach) {
|
|
|
35
36
|
/**
|
|
36
37
|
* Classify `entries` into matching files (visited via `onEach`) and subdirectory names (returned).
|
|
37
38
|
*
|
|
38
|
-
* @param {fs.Dirent[]} entries
|
|
39
|
-
* @param {string} relativePrefix
|
|
40
|
-
* @param {RegExp} regexPattern
|
|
41
|
-
* @param {function(string)} onEach
|
|
42
|
-
* @returns {string[]}
|
|
39
|
+
* @param {fs.Dirent[]} entries Directory entries from one level.
|
|
40
|
+
* @param {string} relativePrefix Relative prefix for child entries.
|
|
41
|
+
* @param {RegExp} regexPattern Regex for file paths.
|
|
42
|
+
* @param {function(string): void} onEach Callback for matching files.
|
|
43
|
+
* @returns {string[]} Names of subdirectory entries.
|
|
43
44
|
*/
|
|
44
45
|
function classifyEntries(entries, relativePrefix, regexPattern, onEach) {
|
|
45
46
|
const subdirs = [];
|
|
@@ -56,12 +57,13 @@ function classifyEntries(entries, relativePrefix, regexPattern, onEach) {
|
|
|
56
57
|
/**
|
|
57
58
|
* Sequentially scan each name in `subdirs`, calling `done` when all are complete or on first error.
|
|
58
59
|
*
|
|
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
|
|
60
|
+
* @param {string[]} subdirs Subdirectory names.
|
|
61
|
+
* @param {string} dir Absolute parent path.
|
|
62
|
+
* @param {string} relativePrefix Relative prefix used during recursion.
|
|
63
|
+
* @param {RegExp} regexPattern Regex for file paths.
|
|
64
|
+
* @param {function(string): void} onEach Callback for matching files.
|
|
65
|
+
* @param {function(Error=): void} done Completion callback.
|
|
66
|
+
* @returns {void}
|
|
65
67
|
*/
|
|
66
68
|
function scanSubdirs(subdirs, dir, relativePrefix, regexPattern, onEach, done) {
|
|
67
69
|
let i = 0;
|
|
@@ -79,17 +81,18 @@ function scanSubdirs(subdirs, dir, relativePrefix, regexPattern, onEach, done) {
|
|
|
79
81
|
/**
|
|
80
82
|
* Asynchronously scan one directory level, then recurse into subdirectories sequentially.
|
|
81
83
|
*
|
|
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
|
|
84
|
+
* @param {string} dir Absolute directory path.
|
|
85
|
+
* @param {string} relativePrefix Relative prefix for match paths.
|
|
86
|
+
* @param {boolean} isRoot True for the initial call.
|
|
87
|
+
* @param {RegExp} regexPattern Regex for file paths.
|
|
88
|
+
* @param {function(string): void} onEach Callback for matching files.
|
|
89
|
+
* @param {function(Error=): void} done Completion callback.
|
|
90
|
+
* @returns {void}
|
|
88
91
|
*/
|
|
89
92
|
function scanDir(dir, relativePrefix, isRoot, regexPattern, onEach, done) {
|
|
90
93
|
fs.readdir(dir, { withFileTypes: true }, (err, entries) => {
|
|
91
94
|
if (err) {
|
|
92
|
-
/*
|
|
95
|
+
/* c8 ignore next */
|
|
93
96
|
if (!isRoot && err.code === 'ENOENT') return done(null);
|
|
94
97
|
return done(err);
|
|
95
98
|
}
|
|
@@ -112,6 +115,7 @@ function scanDir(dir, relativePrefix, isRoot, regexPattern, onEach, done) {
|
|
|
112
115
|
* @param {RegExp} regexPattern The pattern to match relative file paths against.
|
|
113
116
|
* @param {function(string)} onEach Called with the first capturing group (or full match) for each matching path.
|
|
114
117
|
* @param {function(Error?)} onDone Called when the scan is complete, or with an error if one occurred.
|
|
118
|
+
* @returns {void}
|
|
115
119
|
*/
|
|
116
120
|
function scanForFiles(directory, regexPattern, onEach, onDone) {
|
|
117
121
|
scanDir(directory, '', true, regexPattern, onEach, onDone);
|
package/src/utils/jsonUtil.js
CHANGED
|
@@ -5,76 +5,138 @@ const BYTE_CLOSE_OBJECT = 0x7d;
|
|
|
5
5
|
const BYTE_OPEN_ARRAY = 0x5b;
|
|
6
6
|
const BYTE_CLOSE_ARRAY = 0x5d;
|
|
7
7
|
const BYTE_COMMA = 0x2c;
|
|
8
|
+
const BYTE_SIGN_MINUS = 0x2d;
|
|
9
|
+
const BYTE_DECIMAL_SEP = 0x2e;
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* Advance past a JSON string whose opening `"` is at `i`.
|
|
13
|
+
* Returns the position after the closing `"`, or -1 if the string is unterminated.
|
|
14
|
+
*
|
|
15
|
+
* @param {Buffer} buffer Source JSON buffer.
|
|
16
|
+
* @param {number} i Offset of the opening quote.
|
|
17
|
+
* @returns {number} Offset after the closing quote, or -1 if unterminated.
|
|
16
18
|
*/
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
function skipString(buffer, i) {
|
|
20
|
+
let j = i + 1;
|
|
21
|
+
while (j < buffer.length) {
|
|
22
|
+
if (buffer[j] === BYTE_ESCAPE) {
|
|
23
|
+
j += 2;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (buffer[j] === BYTE_QUOTE) {
|
|
27
|
+
return j + 1;
|
|
28
|
+
}
|
|
29
|
+
j++;
|
|
22
30
|
}
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
/* c8 ignore next */
|
|
32
|
+
return -1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if a character byte is a valid JSON value delimiter (comma, closing brace, or closing bracket).
|
|
37
|
+
* @param {number} char Byte value to test.
|
|
38
|
+
* @returns {boolean} True when `char` is `,`, `}` or `]`.
|
|
39
|
+
*/
|
|
40
|
+
function isDelimiter(char) {
|
|
41
|
+
return (char === BYTE_COMMA || char === BYTE_CLOSE_OBJECT || char === BYTE_CLOSE_ARRAY);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {number} char Byte value to test.
|
|
46
|
+
* @returns {boolean} True when `char` opens an object or array.
|
|
47
|
+
*/
|
|
48
|
+
function isOpeningBracket(char) {
|
|
49
|
+
return char === BYTE_OPEN_OBJECT || char === BYTE_OPEN_ARRAY;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {number} char Byte value to test.
|
|
54
|
+
* @returns {boolean} True when `char` closes an object or array.
|
|
55
|
+
*/
|
|
56
|
+
function isClosingBracket(char) {
|
|
57
|
+
return char === BYTE_CLOSE_OBJECT || char === BYTE_CLOSE_ARRAY;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {number} char Byte value to test.
|
|
62
|
+
* @returns {boolean} True when `char` is `{`.
|
|
63
|
+
*/
|
|
64
|
+
function isOpeningObject(char) {
|
|
65
|
+
return char === BYTE_OPEN_OBJECT;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {Buffer} buffer Source JSON buffer.
|
|
70
|
+
* @param {Buffer} pattern Pattern to find.
|
|
71
|
+
* @param {number} startOffset Search start offset.
|
|
72
|
+
* @param {number|undefined} lastMatchPosition Optional cached candidate position.
|
|
73
|
+
* @returns {number} Match position or -1.
|
|
74
|
+
*/
|
|
75
|
+
function nextIndexOf(buffer, pattern, startOffset, lastMatchPosition) {
|
|
76
|
+
if (lastMatchPosition === undefined || lastMatchPosition < startOffset) {
|
|
77
|
+
return buffer.indexOf(pattern, startOffset);
|
|
25
78
|
}
|
|
26
|
-
|
|
79
|
+
return lastMatchPosition;
|
|
80
|
+
}
|
|
27
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Find the position of `pattern` within `buffer` at depth 0 (the top-level object), starting
|
|
84
|
+
* from `startOffset`. Tracks JSON nesting depth and skips over string contents entirely.
|
|
85
|
+
* If `matchPosition` arrives at depth > 0 it means the pattern is inside a nested
|
|
86
|
+
* object/array, so the scan continues searching for the next candidate at depth 0.
|
|
87
|
+
*
|
|
88
|
+
* For value patterns (`"key":value`) it validates the trailing delimiter to avoid prefix matches.
|
|
89
|
+
* For key patterns (`"key":`) pass `isKeyPattern=true` to skip that trailing delimiter check.
|
|
90
|
+
* Returns -1 when no such position exists before the end of the buffer or when a closing brace
|
|
91
|
+
* reduces depth below zero (the top-level object has ended).
|
|
92
|
+
*
|
|
93
|
+
* @param {Buffer} buffer Source JSON buffer.
|
|
94
|
+
* @param {Buffer} pattern Serialized key/value pattern.
|
|
95
|
+
* @param {number} [startOffset=0] Offset where scanning begins.
|
|
96
|
+
* @param {number|undefined} [matchPosition=undefined] Optional cached candidate position.
|
|
97
|
+
* @param {boolean} [isKeyPattern=false] Skip value-delimiter validation for key-only patterns.
|
|
98
|
+
* @returns {number} Match position at the same JSON depth, or -1.
|
|
99
|
+
*/
|
|
100
|
+
function indexOfSameLevel(buffer, pattern, startOffset = 0, matchPosition = undefined, isKeyPattern = false) {
|
|
28
101
|
let depth = 0;
|
|
29
|
-
let inString = false;
|
|
30
102
|
let i = startOffset;
|
|
31
103
|
|
|
32
104
|
while (i < buffer.length) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
if (buffer[i] === BYTE_QUOTE) { // '"'
|
|
39
|
-
inString = false;
|
|
40
|
-
}
|
|
41
|
-
i++;
|
|
42
|
-
continue;
|
|
105
|
+
matchPosition = nextIndexOf(buffer, pattern, i, matchPosition);
|
|
106
|
+
if (matchPosition === -1) {
|
|
107
|
+
return -1;
|
|
43
108
|
}
|
|
44
|
-
|
|
45
109
|
const ch = buffer[i];
|
|
46
|
-
|
|
110
|
+
|
|
111
|
+
if (isOpeningBracket(ch)) {
|
|
47
112
|
depth++;
|
|
48
113
|
i++;
|
|
49
114
|
continue;
|
|
50
|
-
}
|
|
115
|
+
}
|
|
116
|
+
if (isClosingBracket(ch)) {
|
|
51
117
|
depth--;
|
|
52
|
-
|
|
53
118
|
if (depth < 0) {
|
|
54
119
|
return -1;
|
|
55
120
|
}
|
|
56
|
-
|
|
57
121
|
i++;
|
|
58
122
|
continue;
|
|
59
|
-
} else if (ch === BYTE_QUOTE) { // '"'
|
|
60
|
-
inString = true;
|
|
61
123
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const end = i + pattern.length;
|
|
66
|
-
if (pattern[pattern.length - 1] === BYTE_OPEN_OBJECT) { // '{'
|
|
124
|
+
if (ch === BYTE_QUOTE) {
|
|
125
|
+
if (i === matchPosition && depth === 0) {
|
|
126
|
+
if (isKeyPattern || isOpeningObject(pattern[pattern.length - 1])) {
|
|
67
127
|
return i;
|
|
68
128
|
}
|
|
69
|
-
|
|
129
|
+
const end = i + pattern.length;
|
|
130
|
+
if (isDelimiter(buffer[end])) {
|
|
70
131
|
return i;
|
|
71
132
|
}
|
|
72
133
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (
|
|
134
|
+
i = skipString(buffer, i);
|
|
135
|
+
/* c8 ignore next 3 */
|
|
136
|
+
if (i === -1) {
|
|
76
137
|
return -1;
|
|
77
138
|
}
|
|
139
|
+
continue;
|
|
78
140
|
}
|
|
79
141
|
|
|
80
142
|
i++;
|
|
@@ -84,4 +146,157 @@ function indexOfSameLevel(buffer, pattern, startOffset = 0, matchPosition) {
|
|
|
84
146
|
return -1;
|
|
85
147
|
}
|
|
86
148
|
|
|
87
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Find the end of a scalar JSON value so operator matching can parse only the relevant slice.
|
|
151
|
+
*
|
|
152
|
+
* @param {Buffer} buffer Source JSON buffer.
|
|
153
|
+
* @param {number} offset Offset where the scalar starts.
|
|
154
|
+
* @returns {number} End offset (exclusive), or -1 for invalid start.
|
|
155
|
+
*/
|
|
156
|
+
function findJsonValueEnd(buffer, offset) {
|
|
157
|
+
/* c8 ignore next 3 */
|
|
158
|
+
if (offset >= buffer.length) {
|
|
159
|
+
return -1;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (buffer[offset] === BYTE_QUOTE) {
|
|
163
|
+
return skipString(buffer, offset);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Number, boolean, or null: scan until delimiter (,}])
|
|
167
|
+
let i = offset;
|
|
168
|
+
while (i < buffer.length && !isDelimiter(buffer[i])) i++;
|
|
169
|
+
return i;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Parse one scalar JSON slice only after a byte-level match has already narrowed the candidate.
|
|
174
|
+
*
|
|
175
|
+
* @param {Buffer} buffer Source JSON buffer.
|
|
176
|
+
* @param {number} startOffset Start offset (inclusive).
|
|
177
|
+
* @param {number} endOffset End offset (exclusive).
|
|
178
|
+
* @returns {string|number|boolean|null|undefined} Parsed scalar, or `undefined` on parse failure.
|
|
179
|
+
*/
|
|
180
|
+
function parseJsonValue(buffer, startOffset, endOffset) {
|
|
181
|
+
try {
|
|
182
|
+
const valueStr = buffer.toString('utf8', startOffset, endOffset);
|
|
183
|
+
return JSON.parse(valueStr);
|
|
184
|
+
} catch {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {number} byte
|
|
191
|
+
* @returns {boolean} True when `byte` is an ASCII digit.
|
|
192
|
+
*/
|
|
193
|
+
function isAsciiDigit(byte) {
|
|
194
|
+
return byte >= 0x30 && byte <= 0x39;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Compare a contiguous ASCII digit sequence in `buffer` against expected digits.
|
|
199
|
+
* For JSON.stringify()-normalized numbers this is enough for both integer and fraction parts.
|
|
200
|
+
* Returns only the ordering; when ordering === 0, callers can compute the consumed length as startOffset + expectedDigits.length.
|
|
201
|
+
*
|
|
202
|
+
* @param {Buffer} buffer
|
|
203
|
+
* @param {number} startOffset
|
|
204
|
+
* @param {string} expectedDigits
|
|
205
|
+
* @returns {-1|0|1}
|
|
206
|
+
*/
|
|
207
|
+
function compareDigits(buffer, startOffset, expectedDigits) {
|
|
208
|
+
let index = startOffset;
|
|
209
|
+
let position = 0;
|
|
210
|
+
let ordering = 0;
|
|
211
|
+
const expectedLength = expectedDigits.length;
|
|
212
|
+
|
|
213
|
+
while (index < buffer.length && isAsciiDigit(buffer[index])) {
|
|
214
|
+
if (ordering === 0 && position < expectedLength) {
|
|
215
|
+
const expectedByte = expectedDigits.charCodeAt(position);
|
|
216
|
+
if (buffer[index] !== expectedByte) {
|
|
217
|
+
ordering = buffer[index] < expectedByte ? -1 : 1;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
position++;
|
|
221
|
+
index++;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (position !== expectedLength) {
|
|
225
|
+
ordering = position > expectedLength ? 1 : -1;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return ordering;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Compare a compact JSON numeric token in `buffer` against a precompiled expected number,
|
|
233
|
+
* using one linear pass over the buffer slice and no `parseJsonValue` call.
|
|
234
|
+
*
|
|
235
|
+
* @param {Buffer} buffer
|
|
236
|
+
* @param {number} startOffset
|
|
237
|
+
* @param {{isNegative: boolean, integerPart: string, fractionPart: string}} expected
|
|
238
|
+
* @returns {-1|0|1|null} Ordering (`actual` vs `expected`) or null for invalid/non-numeric token.
|
|
239
|
+
*/
|
|
240
|
+
function compareNumeric(buffer, startOffset, expected) {
|
|
241
|
+
let index = startOffset;
|
|
242
|
+
const firstByte = buffer[index];
|
|
243
|
+
if (firstByte !== BYTE_SIGN_MINUS && !isAsciiDigit(firstByte)) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const isNegative = firstByte === BYTE_SIGN_MINUS;
|
|
248
|
+
if (isNegative !== expected.isNegative) {
|
|
249
|
+
return isNegative ? -1 : 1;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (isNegative) {
|
|
253
|
+
index++;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let result = compareDigits(buffer, index, expected.integerPart);
|
|
257
|
+
if (result === 0) {
|
|
258
|
+
index += expected.integerPart.length;
|
|
259
|
+
|
|
260
|
+
const hasFraction = index < buffer.length && buffer[index] === BYTE_DECIMAL_SEP;
|
|
261
|
+
const expectedHasFraction = expected.fractionPart.length > 0;
|
|
262
|
+
if (hasFraction !== expectedHasFraction) {
|
|
263
|
+
result = hasFraction ? 1 : -1;
|
|
264
|
+
} else if (hasFraction) {
|
|
265
|
+
result = compareDigits(buffer, index + 1, expected.fractionPart);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return isNegative ? -result : result;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Compare a matched key's scalar value against pre-serialized candidates without reparsing JSON.
|
|
273
|
+
*
|
|
274
|
+
* @param {Buffer} buffer Source JSON buffer.
|
|
275
|
+
* @param {number} valueStart Offset where the candidate scalar begins.
|
|
276
|
+
* @param {Buffer[]} patterns Pre-serialized scalar candidates.
|
|
277
|
+
* @returns {boolean} True when any candidate matches exactly and is delimiter-terminated.
|
|
278
|
+
*/
|
|
279
|
+
function matchesAnyValuePattern(buffer, valueStart, patterns) {
|
|
280
|
+
for (const pattern of patterns) {
|
|
281
|
+
const valueEnd = valueStart + pattern.length;
|
|
282
|
+
if (valueEnd > buffer.length) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
let matches = true;
|
|
286
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
287
|
+
if (buffer[valueStart + i] !== pattern[i]) {
|
|
288
|
+
matches = false;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (!matches) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (isDelimiter(buffer[valueEnd])) {
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export { isOpeningObject, indexOfSameLevel, findJsonValueEnd, parseJsonValue, compareNumeric, matchesAnyValuePattern };
|