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
|
@@ -1,20 +1,132 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import {assert, assertEqual} from './util.js';
|
|
3
|
+
import {
|
|
4
|
+
indexOfSameLevel,
|
|
5
|
+
findJsonValueEnd,
|
|
6
|
+
parseJsonValue,
|
|
7
|
+
matchesAnyValuePattern,
|
|
8
|
+
isOpeningObject,
|
|
9
|
+
compareNumeric
|
|
10
|
+
} from './jsonUtil.js';
|
|
4
11
|
|
|
12
|
+
const compiledOperatorMatcherCache = new WeakMap();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {any} value Value to classify.
|
|
16
|
+
* @returns {boolean} True when `value` is a non-null object.
|
|
17
|
+
*/
|
|
18
|
+
function isObject(value) {
|
|
19
|
+
return value !== null && typeof value === 'object';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {any} value Value to classify.
|
|
24
|
+
* @returns {boolean} True when `value` is a non-array object.
|
|
25
|
+
*/
|
|
5
26
|
function isPlainObject(value) {
|
|
6
27
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
7
28
|
}
|
|
8
29
|
|
|
30
|
+
/**
|
|
31
|
+
* @param {object} obj Candidate matcher object.
|
|
32
|
+
* @returns {boolean} True when all keys are operator keys (`$...`).
|
|
33
|
+
*/
|
|
34
|
+
function isOperatorObject(obj) {
|
|
35
|
+
const keys = Object.keys(obj);
|
|
36
|
+
return keys.length > 0 && keys.every(key => key.startsWith('$'));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Dispatch between array (OR), operator object, nested object, and scalar equality matching
|
|
41
|
+
* so callers don't need to know the shape of `matcherValue`.
|
|
42
|
+
*
|
|
43
|
+
* @param {any} documentValue Value from the document.
|
|
44
|
+
* @param {any} matcherValue Value from the matcher definition.
|
|
45
|
+
* @returns {boolean} True when both values match under matcher semantics.
|
|
46
|
+
*/
|
|
9
47
|
function propertyMatchesValue(documentValue, matcherValue) {
|
|
10
|
-
if (
|
|
11
|
-
|
|
12
|
-
|
|
48
|
+
if (isObject(matcherValue)) {
|
|
49
|
+
if (Array.isArray(matcherValue)) {
|
|
50
|
+
return matcherValue.includes(documentValue);
|
|
51
|
+
} else if (isOperatorObject(matcherValue)) {
|
|
52
|
+
const operatorChecks = getCompiledOperatorChecks(matcherValue);
|
|
53
|
+
return matchesCompiledOperators(documentValue, operatorChecks);
|
|
54
|
+
}
|
|
13
55
|
return matches(documentValue, matcherValue);
|
|
14
56
|
}
|
|
15
57
|
return typeof matcherValue === 'undefined' || documentValue === matcherValue;
|
|
16
58
|
}
|
|
17
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Pre-compile an operator object into an array of comparison closures so the hot path avoids
|
|
62
|
+
* repeated `Object.entries` + switch dispatch per matched document.
|
|
63
|
+
*
|
|
64
|
+
* @param {object} operatorObj Object containing operator/value pairs.
|
|
65
|
+
* @returns {Array<function(any): boolean>} Compiled predicate checks in evaluation order.
|
|
66
|
+
*/
|
|
67
|
+
function buildOperatorChecks(operatorObj) {
|
|
68
|
+
const checks = [];
|
|
69
|
+
for (const [operator, expectedValue] of Object.entries(operatorObj)) {
|
|
70
|
+
switch (operator) {
|
|
71
|
+
case '$gt':
|
|
72
|
+
checks.push(value => value > expectedValue);
|
|
73
|
+
break;
|
|
74
|
+
case '$gte':
|
|
75
|
+
checks.push(value => value >= expectedValue);
|
|
76
|
+
break;
|
|
77
|
+
case '$lt':
|
|
78
|
+
checks.push(value => value < expectedValue);
|
|
79
|
+
break;
|
|
80
|
+
case '$lte':
|
|
81
|
+
checks.push(value => value <= expectedValue);
|
|
82
|
+
break;
|
|
83
|
+
case '$eq':
|
|
84
|
+
checks.push(value => value === expectedValue);
|
|
85
|
+
break;
|
|
86
|
+
case '$ne':
|
|
87
|
+
checks.push(value => value !== expectedValue);
|
|
88
|
+
break;
|
|
89
|
+
default:
|
|
90
|
+
throw new TypeError(`Unknown operator: ${operator}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return checks;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Return cached compiled checks for `operatorObj`, compiling and caching on first access.
|
|
98
|
+
* Using WeakMap keeps operator objects GC-eligible and avoids mutating user-supplied objects.
|
|
99
|
+
*
|
|
100
|
+
* @param {object} operatorObj Object containing operator/value pairs.
|
|
101
|
+
* @returns {Array<function(any): boolean>} Cached or newly compiled operator checks.
|
|
102
|
+
*/
|
|
103
|
+
function getCompiledOperatorChecks(operatorObj) {
|
|
104
|
+
const cachedChecks = compiledOperatorMatcherCache.get(operatorObj);
|
|
105
|
+
if (cachedChecks) {
|
|
106
|
+
return cachedChecks;
|
|
107
|
+
}
|
|
108
|
+
const checks = buildOperatorChecks(operatorObj);
|
|
109
|
+
compiledOperatorMatcherCache.set(operatorObj, checks);
|
|
110
|
+
return checks;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {any} documentValue Parsed scalar value from the document.
|
|
115
|
+
* @param {Array<function(any): boolean>} checks Compiled operator checks.
|
|
116
|
+
* @returns {boolean} True when all checks pass.
|
|
117
|
+
*/
|
|
118
|
+
function matchesCompiledOperators(documentValue, checks) {
|
|
119
|
+
if (documentValue === undefined) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
for (const check of checks) {
|
|
123
|
+
if (!check(documentValue)) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
18
130
|
/**
|
|
19
131
|
* Build a buffer containing the file magic header and a JSON stringified metadata block, padded to be a multiple of 16 bytes long.
|
|
20
132
|
*
|
|
@@ -42,10 +154,10 @@ function buildMetadataHeader(magic, metadata) {
|
|
|
42
154
|
* @returns {function(string)} A function that calculates the HMAC for a given string
|
|
43
155
|
*/
|
|
44
156
|
const createHmac = secret => string => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
157
|
+
const hmac = crypto.createHmac('sha256', secret);
|
|
158
|
+
hmac.update(string);
|
|
159
|
+
return hmac.digest('hex');
|
|
160
|
+
};
|
|
49
161
|
|
|
50
162
|
/**
|
|
51
163
|
* @typedef {object|function(object):boolean} Matcher
|
|
@@ -76,14 +188,15 @@ function matches(document, matcher) {
|
|
|
76
188
|
* @returns {{matcher: string|object, hmac?: string}}
|
|
77
189
|
*/
|
|
78
190
|
function buildMetadataForMatcher(matcher, hmac) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
191
|
+
/* c8 ignore next 2 */
|
|
192
|
+
if (!matcher) {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
82
195
|
if (typeof matcher === 'object') {
|
|
83
|
-
return {
|
|
196
|
+
return {matcher};
|
|
84
197
|
}
|
|
85
198
|
const matcherString = matcher.toString();
|
|
86
|
-
return {
|
|
199
|
+
return {matcher: matcherString, hmac: hmac(matcherString)};
|
|
87
200
|
}
|
|
88
201
|
|
|
89
202
|
/**
|
|
@@ -92,11 +205,12 @@ function buildMetadataForMatcher(matcher, hmac) {
|
|
|
92
205
|
* @returns {Matcher} The matcher object or function.
|
|
93
206
|
*/
|
|
94
207
|
function buildMatcherFromMetadata(matcherMetadata, hmac) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
208
|
+
let matcher;
|
|
209
|
+
if (typeof matcherMetadata.matcher === 'object') {
|
|
210
|
+
matcher = matcherMetadata.matcher;
|
|
211
|
+
} else {
|
|
212
|
+
/* c8 ignore next 1 */
|
|
213
|
+
assert(matcherMetadata.hmac === hmac(matcherMetadata.matcher), 'Invalid HMAC for matcher.');
|
|
100
214
|
|
|
101
215
|
matcher = eval('(' + matcherMetadata.matcher + ')').bind({}); // jshint ignore:line
|
|
102
216
|
}
|
|
@@ -112,33 +226,35 @@ function buildMatcherFromMetadata(matcherMetadata, hmac) {
|
|
|
112
226
|
*/
|
|
113
227
|
function buildTypeMatcherFn(payloadPath) {
|
|
114
228
|
const parts = payloadPath.split('.');
|
|
115
|
-
return function(typeValue) {
|
|
229
|
+
return function (typeValue) {
|
|
116
230
|
let obj = typeValue;
|
|
117
231
|
for (let i = parts.length - 1; i >= 0; i--) {
|
|
118
|
-
obj = {
|
|
232
|
+
obj = {[parts[i]]: obj};
|
|
119
233
|
}
|
|
120
|
-
return {
|
|
234
|
+
return {payload: obj};
|
|
121
235
|
};
|
|
122
236
|
}
|
|
123
237
|
|
|
124
238
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* and supports matcher objects with sub properties and multi-value matches (OR/any of).
|
|
239
|
+
* Compile an object matcher into a raw-buffer predicate so raw-mode reads can filter compact
|
|
240
|
+
* JSON without parsing every document first.
|
|
128
241
|
*
|
|
129
|
-
* @param {object} matcher Object matcher.
|
|
130
|
-
* @
|
|
242
|
+
* @param {object} matcher Object matcher to compile.
|
|
243
|
+
* @param {{enableOperatorBufferMatcher?: boolean}} [options] Raw matcher build options.
|
|
244
|
+
* @returns {function(Buffer): boolean} Predicate over compact JSON buffers.
|
|
131
245
|
*/
|
|
132
|
-
function buildRawBufferMatcher(matcher = {}) {
|
|
133
|
-
assert(
|
|
246
|
+
function buildRawBufferMatcher(matcher = {}, options = {}) {
|
|
247
|
+
assert(isPlainObject(matcher), 'Matcher must be an object.', TypeError);
|
|
248
|
+
const enableOperatorBufferMatcher = options.enableOperatorBufferMatcher !== false;
|
|
134
249
|
|
|
135
|
-
const root = buildMatcherTree(matcher);
|
|
250
|
+
const root = buildMatcherTree(matcher, enableOperatorBufferMatcher);
|
|
251
|
+
/* c8 ignore next 3 */
|
|
136
252
|
if (root.children.length === 0) {
|
|
137
253
|
return () => true;
|
|
138
254
|
}
|
|
139
255
|
|
|
140
256
|
return function matchesRawBuffer(buffer) {
|
|
141
|
-
if (buffer[0]
|
|
257
|
+
if (!isOpeningObject(buffer[0])) {
|
|
142
258
|
return false;
|
|
143
259
|
}
|
|
144
260
|
if (!preCheck(buffer, 1, root)) {
|
|
@@ -149,93 +265,247 @@ function buildRawBufferMatcher(matcher = {}) {
|
|
|
149
265
|
}
|
|
150
266
|
|
|
151
267
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
child.valueMatches[i] = buffer.indexOf(pattern, startOffset);
|
|
159
|
-
return child.valueMatches[i] !== -1;
|
|
160
|
-
})) {
|
|
161
|
-
return false;
|
|
162
|
-
}
|
|
163
|
-
if (child.objectPattern) {
|
|
164
|
-
const objectMatch = buffer.indexOf(child.objectPattern, startOffset);
|
|
165
|
-
if (objectMatch === -1) {
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
child.objMatch = objectMatch;
|
|
169
|
-
if (!preCheck(buffer, objectMatch, child.node)) {
|
|
170
|
-
return false;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return true;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Pre-compile a plain object matcher into a tree of byte patterns so `matchesNode` can scan
|
|
179
|
-
* raw JSON buffers without deserializing them.
|
|
268
|
+
* Compile a matcher object into a tree whose children each carry one primary byte pattern plus
|
|
269
|
+
* optional follow-up checks for nested objects, operators, or multi-value scalars.
|
|
270
|
+
* Fast scalar-equality children are placed first so preCheck and matchesNode short-circuit early.
|
|
271
|
+
*
|
|
272
|
+
* @param {object} matcher Matcher object for this tree level.
|
|
273
|
+
* @returns {{children: Array<object>}} Compiled child descriptors for this level.
|
|
180
274
|
*/
|
|
181
275
|
function buildMatcherTree(matcher) {
|
|
182
|
-
const
|
|
276
|
+
const fast = [];
|
|
277
|
+
const slow = [];
|
|
183
278
|
|
|
184
279
|
for (const [key, value] of Object.entries(matcher)) {
|
|
185
|
-
|
|
280
|
+
const child = buildMatcherTreeChild(key, value);
|
|
281
|
+
// A child with only one byte pattern and no follow-up matcher cannot be outperformed by any extra matcher logic.
|
|
282
|
+
(!child.matches ? fast : slow).push(child);
|
|
186
283
|
}
|
|
187
284
|
|
|
188
|
-
return
|
|
285
|
+
return {children: [...fast, ...slow]};
|
|
189
286
|
}
|
|
190
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Normalize one matcher property into the cheapest raw-buffer strategy for that value shape.
|
|
290
|
+
* Children that compile to a plain byte-equality pattern leave `matches` as null.
|
|
291
|
+
*
|
|
292
|
+
* @param {string} key Property name at this matcher level.
|
|
293
|
+
* @param {any} value Matcher value for `key`.
|
|
294
|
+
* @returns {{pattern: Buffer, isKeyPattern: boolean, matches: ((function(Buffer, number): boolean)|null), node: ({children: Array<object>}|null), lastMatch: number}} Compiled descriptor consumed by preCheck/matchesNode.
|
|
295
|
+
*/
|
|
191
296
|
function buildMatcherTreeChild(key, value) {
|
|
192
297
|
const keyPrefix = Buffer.from(`${JSON.stringify(key)}:`, 'utf8');
|
|
193
|
-
const child = {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
298
|
+
const child = {
|
|
299
|
+
pattern: null,
|
|
300
|
+
isKeyPattern: false,
|
|
301
|
+
matches: null,
|
|
302
|
+
node: null,
|
|
303
|
+
lastMatch: -1
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
if (isObject(value)) {
|
|
307
|
+
if (Array.isArray(value)) {
|
|
308
|
+
assert(!value.some(isObject), 'Array matcher values must be scalars.', TypeError);
|
|
309
|
+
if (value.length === 1) {
|
|
310
|
+
child.pattern = buildKeyValuePattern(keyPrefix, value[0]);
|
|
311
|
+
} else {
|
|
312
|
+
child.isKeyPattern = true;
|
|
313
|
+
child.pattern = keyPrefix;
|
|
314
|
+
const valuePatterns = value.map(item => Buffer.from(JSON.stringify(item), 'utf8'));
|
|
315
|
+
child.matches = (buffer, startOffset) => matchesAnyValuePattern(buffer, startOffset, valuePatterns);
|
|
316
|
+
}
|
|
317
|
+
} else if ('$eq' in value && Object.keys(value).length === 1) {
|
|
318
|
+
// A lone $eq is semantically identical to a scalar equality check — fold it into a
|
|
319
|
+
// value pattern at compile time so the buffer scan takes the same fast path as { key: value }.
|
|
320
|
+
child.pattern = buildKeyValuePattern(keyPrefix, value['$eq']);
|
|
321
|
+
} else if ('$ne' in value && Object.keys(value).length === 1) {
|
|
322
|
+
// A lone $ne is the logical negation of $eq: confirm the key exists at this level,
|
|
323
|
+
// then reject only when the value byte-matches the excluded pattern.
|
|
324
|
+
child.isKeyPattern = true;
|
|
325
|
+
child.pattern = keyPrefix;
|
|
326
|
+
const nePattern = [Buffer.from(JSON.stringify(value['$ne']), 'utf8')];
|
|
327
|
+
child.matches = (buffer, valueStart) => !matchesAnyValuePattern(buffer, valueStart, nePattern);
|
|
328
|
+
} else if (isOperatorObject(value)) {
|
|
329
|
+
child.isKeyPattern = true;
|
|
330
|
+
child.pattern = keyPrefix;
|
|
331
|
+
child.matches = buildOperatorBufferMatcher(value);
|
|
332
|
+
} else {
|
|
333
|
+
child.pattern = Buffer.concat([keyPrefix, Buffer.from('{', 'utf8')]);
|
|
334
|
+
child.node = buildMatcherTree(value);
|
|
335
|
+
child.matches = (buffer, startOffset) => matchesNode(buffer, startOffset, child.node);
|
|
197
336
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
} else if (value && typeof value === 'object') {
|
|
201
|
-
child.objectPattern = Buffer.concat([keyPrefix, Buffer.from('{', 'utf8')]);
|
|
202
|
-
child.node = buildMatcherTree(value);
|
|
203
|
-
return child;
|
|
337
|
+
} else {
|
|
338
|
+
child.pattern = buildKeyValuePattern(keyPrefix, value);
|
|
204
339
|
}
|
|
205
|
-
child.valuePatterns = [buildValuePattern(keyPrefix, value)];
|
|
206
340
|
return child;
|
|
207
341
|
}
|
|
208
342
|
|
|
209
|
-
|
|
343
|
+
/**
|
|
344
|
+
* @param {Buffer} keyPrefix Serialized key prefix (`"key":`).
|
|
345
|
+
* @param {any} value Scalar value to append.
|
|
346
|
+
* @returns {Buffer} Full serialized `"key":value` pattern.
|
|
347
|
+
*/
|
|
348
|
+
function buildKeyValuePattern(keyPrefix, value) {
|
|
210
349
|
return Buffer.concat([keyPrefix, Buffer.from(JSON.stringify(value), 'utf8')]);
|
|
211
350
|
}
|
|
212
351
|
|
|
213
352
|
/**
|
|
214
|
-
*
|
|
215
|
-
*
|
|
353
|
+
* Cheap pass: confirm each child's primary pattern exists somewhere and cache that position as a
|
|
354
|
+
* hint for the depth-aware pass that follows.
|
|
355
|
+
*
|
|
356
|
+
* @param {Buffer} buffer Compact JSON document buffer.
|
|
357
|
+
* @param {number} startOffset Start offset within `buffer`.
|
|
358
|
+
* @param {{children: Array<object>}} node Compiled matcher node for this level.
|
|
359
|
+
* @returns {boolean} True when every child pattern exists somewhere from `startOffset`.
|
|
360
|
+
*/
|
|
361
|
+
function preCheck(buffer, startOffset, node) {
|
|
362
|
+
for (const child of node.children) {
|
|
363
|
+
child.lastMatch = buffer.indexOf(child.pattern, startOffset);
|
|
364
|
+
if (child.lastMatch === -1) {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
if (child.node && !preCheck(buffer, child.lastMatch, child.node)) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Confirm that each prechecked child really matches at the requested JSON level and then run the
|
|
376
|
+
* optional value-specific follow-up checks from the compiled tree.
|
|
377
|
+
*
|
|
378
|
+
* @param {Buffer} buffer Compact JSON document buffer.
|
|
379
|
+
* @param {number} startOffset Start offset within `buffer`.
|
|
380
|
+
* @param {{children: Array<object>}} node Compiled matcher node for this level.
|
|
381
|
+
* @returns {boolean} True when all children match at the requested JSON level.
|
|
216
382
|
*/
|
|
217
383
|
function matchesNode(buffer, startOffset, node) {
|
|
218
384
|
for (const child of node.children) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
})) {
|
|
385
|
+
const matchPosition = indexOfSameLevel(buffer, child.pattern, startOffset, child.lastMatch, child.isKeyPattern);
|
|
386
|
+
if (matchPosition === -1) {
|
|
222
387
|
return false;
|
|
223
388
|
}
|
|
224
389
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return false;
|
|
229
|
-
}
|
|
230
|
-
if (!matchesNode(buffer, objectIndex + child.objectPattern.length, child.node)) {
|
|
231
|
-
return false;
|
|
232
|
-
}
|
|
390
|
+
const valueStart = matchPosition + child.pattern.length;
|
|
391
|
+
if (child.matches && !child.matches(buffer, valueStart)) {
|
|
392
|
+
return false;
|
|
233
393
|
}
|
|
234
394
|
}
|
|
235
395
|
|
|
236
396
|
return true;
|
|
237
397
|
}
|
|
238
398
|
|
|
399
|
+
/**
|
|
400
|
+
* Parse the scalar at a matched key position only when operator objects require a real JavaScript
|
|
401
|
+
* comparison instead of a pure byte-pattern check.
|
|
402
|
+
*
|
|
403
|
+
* @param {Buffer} buffer Compact JSON document buffer.
|
|
404
|
+
* @param {number} startOffset Offset of the scalar value to parse.
|
|
405
|
+
* @param {Array<function(any): boolean>} operatorChecks Compiled operator checks.
|
|
406
|
+
* @returns {boolean} True when the parsed scalar satisfies all operators.
|
|
407
|
+
*/
|
|
408
|
+
function matchesOperatorInBuffer(buffer, startOffset, operatorChecks) {
|
|
409
|
+
const valueStart = startOffset;
|
|
410
|
+
const valueEnd = findJsonValueEnd(buffer, valueStart);
|
|
411
|
+
/* c8 ignore next 2 */
|
|
412
|
+
if (valueEnd === -1 || valueEnd <= valueStart) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const parsedValue = parseJsonValue(buffer, valueStart, valueEnd);
|
|
417
|
+
|
|
418
|
+
return matchesCompiledOperators(parsedValue, operatorChecks);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Map pre-computed ordering information to operator semantics.
|
|
423
|
+
* ordering: -1 (actual < expected), 0 (equal), 1 (actual > expected)
|
|
424
|
+
*
|
|
425
|
+
* @param {string} operator
|
|
426
|
+
* @param {-1|0|1} ordering
|
|
427
|
+
* @returns {boolean}
|
|
428
|
+
*/
|
|
429
|
+
function matchesOrdering(operator, ordering) {
|
|
430
|
+
switch (operator) {
|
|
431
|
+
case '$gt':
|
|
432
|
+
return ordering === 1;
|
|
433
|
+
case '$gte':
|
|
434
|
+
return ordering >= 0;
|
|
435
|
+
case '$lt':
|
|
436
|
+
return ordering === -1;
|
|
437
|
+
case '$lte':
|
|
438
|
+
return ordering <= 0;
|
|
439
|
+
case '$eq':
|
|
440
|
+
return ordering === 0;
|
|
441
|
+
case '$ne':
|
|
442
|
+
return ordering !== 0;
|
|
443
|
+
/* c8 ignore next 1 */
|
|
444
|
+
default:
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* @param {Array<[string, any]>} entries
|
|
451
|
+
* @returns {Array<{operator: string, expectedNumeric: {isNegative: boolean, integerPart: string, fractionPart: string}}>|null}
|
|
452
|
+
*/
|
|
453
|
+
function buildNumericOperatorComparisons(entries) {
|
|
454
|
+
const comparisons = [];
|
|
455
|
+
for (const [operator, expectedValue] of entries) {
|
|
456
|
+
if (typeof expectedValue !== 'number') {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
const expectedStr = JSON.stringify(expectedValue);
|
|
460
|
+
const expectedIsNegative = expectedStr[0] === '-';
|
|
461
|
+
const intStart = expectedIsNegative ? 1 : 0;
|
|
462
|
+
const [expectedIntegerPart, expectedFractionPart = ''] = expectedStr.substring(intStart).split('.');
|
|
463
|
+
comparisons.push({
|
|
464
|
+
operator,
|
|
465
|
+
expectedNumeric: {
|
|
466
|
+
isNegative: expectedIsNegative,
|
|
467
|
+
integerPart: expectedIntegerPart,
|
|
468
|
+
fractionPart: expectedFractionPart
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
return comparisons;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Build a specialized buffer-based operator comparator by pre-compiling operator-specific
|
|
477
|
+
* byte shortcuts at matcher build time. This avoids runtime dispatch and enables aggressive
|
|
478
|
+
* short-circuit evaluation for sign mismatches and digit-count differences.
|
|
479
|
+
*
|
|
480
|
+
* Assumes no scientific notation in the JSON buffer.
|
|
481
|
+
*
|
|
482
|
+
* @param {object} operatorObj Operator object (e.g., { $gt: 100 }).
|
|
483
|
+
* @returns {function(Buffer, number): boolean} Predicate that reads the buffer at given offset.
|
|
484
|
+
*/
|
|
485
|
+
function buildOperatorBufferMatcher(operatorObj) {
|
|
486
|
+
const entries = Object.entries(operatorObj);
|
|
487
|
+
const numericComparisons = buildNumericOperatorComparisons(entries);
|
|
488
|
+
if (numericComparisons) {
|
|
489
|
+
return (buffer, startOffset) => {
|
|
490
|
+
for (const comparison of numericComparisons) {
|
|
491
|
+
const ordering = compareNumeric(buffer, startOffset, comparison.expectedNumeric);
|
|
492
|
+
/* c8 ignore next 2 */
|
|
493
|
+
if (ordering === null) {
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
if (!matchesOrdering(comparison.operator, ordering)) {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return true;
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Non-numeric expected value: use generic operator checks.
|
|
505
|
+
const operatorChecks = getCompiledOperatorChecks(operatorObj);
|
|
506
|
+
return (buffer, startOffset) => matchesOperatorInBuffer(buffer, startOffset, operatorChecks);
|
|
507
|
+
}
|
|
508
|
+
|
|
239
509
|
export {
|
|
240
510
|
createHmac,
|
|
241
511
|
matches,
|
package/src/utils/util.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Assert that actual and expected match or throw an Error with the given message appended by information about expected and actual value.
|
|
3
3
|
*
|
|
4
|
-
* @param {*} actual
|
|
5
|
-
* @param {*} expected
|
|
6
|
-
* @param {string} message
|
|
4
|
+
* @param {*} actual Actual value.
|
|
5
|
+
* @param {*} expected Expected value.
|
|
6
|
+
* @param {string} message Error message prefix.
|
|
7
|
+
* @returns {void}
|
|
7
8
|
*/
|
|
8
9
|
function assertEqual(actual, expected, message) {
|
|
9
10
|
if (actual !== expected) {
|
|
@@ -14,9 +15,10 @@ function assertEqual(actual, expected, message) {
|
|
|
14
15
|
/**
|
|
15
16
|
* Assert that the condition holds and if not, throw an error with the given message.
|
|
16
17
|
*
|
|
17
|
-
* @param {boolean} condition
|
|
18
|
-
* @param {string} message
|
|
19
|
-
* @param {typeof Error} ErrorType
|
|
18
|
+
* @param {boolean} condition Condition to verify.
|
|
19
|
+
* @param {string} message Error message when the condition fails.
|
|
20
|
+
* @param {typeof Error} ErrorType Error class to throw.
|
|
21
|
+
* @returns {void}
|
|
20
22
|
*/
|
|
21
23
|
function assert(condition, message, ErrorType = Error) {
|
|
22
24
|
if (!condition) {
|
|
@@ -27,9 +29,9 @@ function assert(condition, message, ErrorType = Error) {
|
|
|
27
29
|
/**
|
|
28
30
|
* Return the amount required to align value to the given alignment.
|
|
29
31
|
* It calculates the difference of the alignment and the modulo of value by alignment.
|
|
30
|
-
* @param {number} value
|
|
31
|
-
* @param {number} alignment
|
|
32
|
-
* @returns {number}
|
|
32
|
+
* @param {number} value Source value.
|
|
33
|
+
* @param {number} alignment Target alignment.
|
|
34
|
+
* @returns {number} Additional offset needed to reach the next aligned value.
|
|
33
35
|
*/
|
|
34
36
|
function alignTo(value, alignment) {
|
|
35
37
|
return (alignment - (value % alignment)) % alignment;
|
|
@@ -38,11 +40,11 @@ function alignTo(value, alignment) {
|
|
|
38
40
|
/**
|
|
39
41
|
* Method for hashing a string (e.g. a partition name) to a 32-bit unsigned integer.
|
|
40
42
|
*
|
|
41
|
-
* @param {string} str
|
|
42
|
-
* @returns {number}
|
|
43
|
+
* @param {string} str Input string.
|
|
44
|
+
* @returns {number} 32-bit unsigned hash value.
|
|
43
45
|
*/
|
|
44
46
|
function hash(str) {
|
|
45
|
-
/*
|
|
47
|
+
/* c8 ignore next 3 */
|
|
46
48
|
if (str.length === 0) {
|
|
47
49
|
return 0;
|
|
48
50
|
}
|
|
@@ -115,8 +117,9 @@ function wrapAndCheck(index, length) {
|
|
|
115
117
|
/**
|
|
116
118
|
* Iterate an array-like list in forward or reverse order.
|
|
117
119
|
*
|
|
118
|
-
* @param {Iterable} entries
|
|
119
|
-
* @param {boolean} forwards
|
|
120
|
+
* @param {Iterable|ArrayLike<any>} entries Entries to iterate.
|
|
121
|
+
* @param {boolean} forwards Iteration direction.
|
|
122
|
+
* @returns {Generator<any>}
|
|
120
123
|
*/
|
|
121
124
|
function* iterate(entries, forwards) {
|
|
122
125
|
if (forwards) {
|
|
@@ -141,7 +144,7 @@ function* iterate(entries, forwards) {
|
|
|
141
144
|
* @param {boolean} [ascending=true] When true, yields items in ascending key order (min-merge).
|
|
142
145
|
* When false, yields in descending key order (max-merge).
|
|
143
146
|
* @param {function(*): *} [visit] Optional extractor for the yielded value. Defaults to identity.
|
|
144
|
-
* @returns {Generator<*>}
|
|
147
|
+
* @returns {Generator<*>} Merged sequence in key order.
|
|
145
148
|
*/
|
|
146
149
|
function *kWayMerge(iterables, getSortKey, ascending = true, visit = v => v) {
|
|
147
150
|
const states = [];
|
|
@@ -175,9 +178,9 @@ function *kWayMerge(iterables, getSortKey, ascending = true, visit = v => v) {
|
|
|
175
178
|
* Read a scalar value at a dot-notation path from an object.
|
|
176
179
|
* Returns `undefined` if any path segment is absent or an intermediate value is not an object.
|
|
177
180
|
*
|
|
178
|
-
* @param {object} obj
|
|
181
|
+
* @param {object} obj Source object.
|
|
179
182
|
* @param {string} dotPath Dot-separated property path, e.g. `'payload.type'`.
|
|
180
|
-
* @returns {*}
|
|
183
|
+
* @returns {*} Value at the path or `undefined`.
|
|
181
184
|
*/
|
|
182
185
|
function getPropertyAtPath(obj, dotPath) {
|
|
183
186
|
let current = obj;
|