event-storage 1.1.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.
@@ -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);
@@ -0,0 +1,302 @@
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
+ const BYTE_SIGN_MINUS = 0x2d;
9
+ const BYTE_DECIMAL_SEP = 0x2e;
10
+
11
+ /**
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.
18
+ */
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++;
30
+ }
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);
78
+ }
79
+ return lastMatchPosition;
80
+ }
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) {
101
+ let depth = 0;
102
+ let i = startOffset;
103
+
104
+ while (i < buffer.length) {
105
+ matchPosition = nextIndexOf(buffer, pattern, i, matchPosition);
106
+ if (matchPosition === -1) {
107
+ return -1;
108
+ }
109
+ const ch = buffer[i];
110
+
111
+ if (isOpeningBracket(ch)) {
112
+ depth++;
113
+ i++;
114
+ continue;
115
+ }
116
+ if (isClosingBracket(ch)) {
117
+ depth--;
118
+ if (depth < 0) {
119
+ return -1;
120
+ }
121
+ i++;
122
+ continue;
123
+ }
124
+ if (ch === BYTE_QUOTE) {
125
+ if (i === matchPosition && depth === 0) {
126
+ if (isKeyPattern || isOpeningObject(pattern[pattern.length - 1])) {
127
+ return i;
128
+ }
129
+ const end = i + pattern.length;
130
+ if (isDelimiter(buffer[end])) {
131
+ return i;
132
+ }
133
+ }
134
+ i = skipString(buffer, i);
135
+ /* c8 ignore next 3 */
136
+ if (i === -1) {
137
+ return -1;
138
+ }
139
+ continue;
140
+ }
141
+
142
+ i++;
143
+ }
144
+
145
+ /* c8 ignore next */
146
+ return -1;
147
+ }
148
+
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 };