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.
@@ -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
- if (typeof storageName !== 'string') {
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
- /* istanbul ignore if */
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
- /* istanbul ignore if */
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
- /* istanbul ignore next */
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
- /* istanbul ignore if */
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
- /* istanbul ignore if */
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
- if (!this.index.isOpen()) {
513
- this.index.open();
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
+
@@ -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
- * @return {boolean} true if the directory existed already
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[]} names of subdirectory entries
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?)} done
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?)} done
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
- /* istanbul ignore next */
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);
@@ -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
- * 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).
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 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);
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
- if (matchPosition === -1) {
24
- return -1;
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
- /* c8 ignore stop */
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
- 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;
105
+ matchPosition = nextIndexOf(buffer, pattern, i, matchPosition);
106
+ if (matchPosition === -1) {
107
+ return -1;
43
108
  }
44
-
45
109
  const ch = buffer[i];
46
- if (ch === BYTE_OPEN_OBJECT || ch === BYTE_OPEN_ARRAY) { // '{' or '['
110
+
111
+ if (isOpeningBracket(ch)) {
47
112
  depth++;
48
113
  i++;
49
114
  continue;
50
- } else if (ch === BYTE_CLOSE_OBJECT || ch === BYTE_CLOSE_ARRAY) { // '}' or ']'
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
- 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) { // '{'
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
- if (buffer[end] === BYTE_COMMA || buffer[end] === BYTE_CLOSE_OBJECT || buffer[end] === BYTE_CLOSE_ARRAY) { // ',' or '}' or ']'
129
+ const end = i + pattern.length;
130
+ if (isDelimiter(buffer[end])) {
70
131
  return i;
71
132
  }
72
133
  }
73
-
74
- matchPosition = buffer.indexOf(pattern, matchPosition + 1);
75
- if (matchPosition < 0) {
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
- export { BYTE_OPEN_OBJECT, indexOfSameLevel };
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 };