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.
- package/README.md +49 -4
- package/index.js +1 -0
- package/package.json +4 -5
- package/src/Consumer.js +16 -20
- package/src/EventStore.js +176 -118
- package/src/EventStream.js +56 -38
- package/src/Index/ReadOnlyIndex.js +1 -1
- package/src/Index/ReadableIndex.js +9 -9
- package/src/Index/WritableIndex.js +6 -10
- package/src/IndexMatcher.js +2 -2
- package/src/JoinEventStream.js +33 -59
- package/src/Partition/ReadOnlyPartition.js +1 -1
- package/src/Partition/ReadablePartition.js +158 -90
- package/src/Partition/WritablePartition.js +38 -29
- package/src/Storage/ReadOnlyStorage.js +4 -4
- package/src/Storage/ReadableStorage.js +81 -113
- package/src/Storage/WritableStorage.js +52 -37
- package/src/Watcher.js +1 -1
- package/src/utils/apiHelpers.js +123 -0
- package/src/{fsUtil.js → utils/fsUtil.js} +27 -23
- package/src/utils/jsonUtil.js +302 -0
- package/src/utils/metadataUtil.js +517 -0
- package/src/{util.js → utils/util.js} +69 -31
- package/src/metadataUtil.js +0 -126
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
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';
|
|
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
|
+
*/
|
|
26
|
+
function isPlainObject(value) {
|
|
27
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
28
|
+
}
|
|
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
|
+
*/
|
|
47
|
+
function propertyMatchesValue(documentValue, matcherValue) {
|
|
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
|
+
}
|
|
55
|
+
return matches(documentValue, matcherValue);
|
|
56
|
+
}
|
|
57
|
+
return typeof matcherValue === 'undefined' || documentValue === matcherValue;
|
|
58
|
+
}
|
|
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
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Build a buffer containing the file magic header and a JSON stringified metadata block, padded to be a multiple of 16 bytes long.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} magic
|
|
134
|
+
* @param {object} metadata
|
|
135
|
+
* @returns {Buffer} A buffer containing the header data
|
|
136
|
+
*/
|
|
137
|
+
function buildMetadataHeader(magic, metadata) {
|
|
138
|
+
assertEqual(magic.length, 8, 'The header magic bytes length is wrong.');
|
|
139
|
+
let metadataString = JSON.stringify(metadata);
|
|
140
|
+
let metadataSize = Buffer.byteLength(metadataString, 'utf8');
|
|
141
|
+
// 8 byte MAGIC, 4 byte metadata size, 1 byte line break
|
|
142
|
+
const pad = (16 - ((8 + 4 + metadataSize + 1) % 16)) % 16;
|
|
143
|
+
metadataString += ' '.repeat(pad) + "\n";
|
|
144
|
+
metadataSize += pad + 1;
|
|
145
|
+
const metadataBuffer = Buffer.allocUnsafe(8 + 4 + metadataSize);
|
|
146
|
+
metadataBuffer.write(magic, 0, 8, 'utf8');
|
|
147
|
+
metadataBuffer.writeUInt32BE(metadataSize, 8);
|
|
148
|
+
metadataBuffer.write(metadataString, 8 + 4, metadataSize, 'utf8');
|
|
149
|
+
return metadataBuffer;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @param {string} secret The secret to use for calculating further HMACs
|
|
154
|
+
* @returns {function(string)} A function that calculates the HMAC for a given string
|
|
155
|
+
*/
|
|
156
|
+
const createHmac = secret => string => {
|
|
157
|
+
const hmac = crypto.createHmac('sha256', secret);
|
|
158
|
+
hmac.update(string);
|
|
159
|
+
return hmac.digest('hex');
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @typedef {object|function(object):boolean} Matcher
|
|
164
|
+
*/
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @param {object} document The document to check against the matcher.
|
|
168
|
+
* @param {Matcher} matcher An object of properties and their values that need to match in the object or a function that checks if the document matches.
|
|
169
|
+
* @returns {boolean} True if the document matches the matcher or false otherwise.
|
|
170
|
+
*/
|
|
171
|
+
function matches(document, matcher) {
|
|
172
|
+
if (typeof document === 'undefined') return false;
|
|
173
|
+
if (typeof matcher === 'undefined') return true;
|
|
174
|
+
|
|
175
|
+
if (typeof matcher === 'function') return matcher(document);
|
|
176
|
+
|
|
177
|
+
for (let prop of Object.getOwnPropertyNames(matcher)) {
|
|
178
|
+
if (!propertyMatchesValue(document[prop], matcher[prop])) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @param {Matcher} matcher The matcher object or function that should be serialized.
|
|
187
|
+
* @param {function(string)} hmac A function that calculates a HMAC of the given string.
|
|
188
|
+
* @returns {{matcher: string|object, hmac?: string}}
|
|
189
|
+
*/
|
|
190
|
+
function buildMetadataForMatcher(matcher, hmac) {
|
|
191
|
+
/* c8 ignore next 2 */
|
|
192
|
+
if (!matcher) {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
if (typeof matcher === 'object') {
|
|
196
|
+
return {matcher};
|
|
197
|
+
}
|
|
198
|
+
const matcherString = matcher.toString();
|
|
199
|
+
return {matcher: matcherString, hmac: hmac(matcherString)};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @param {{matcher: string|object, hmac: string}} matcherMetadata The serialized matcher and its HMAC
|
|
204
|
+
* @param {function(string)} hmac A function that calculates a HMAC of the given string.
|
|
205
|
+
* @returns {Matcher} The matcher object or function.
|
|
206
|
+
*/
|
|
207
|
+
function buildMatcherFromMetadata(matcherMetadata, hmac) {
|
|
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.');
|
|
214
|
+
|
|
215
|
+
matcher = eval('(' + matcherMetadata.matcher + ')').bind({}); // jshint ignore:line
|
|
216
|
+
}
|
|
217
|
+
return matcher;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Builds a factory function that, given a type string, returns an object matcher for
|
|
222
|
+
* documents whose payload contains that type at the given dot-notation path.
|
|
223
|
+
*
|
|
224
|
+
* @param {string} payloadPath Dot-notation path relative to the event payload (e.g. `'type'`, `'meta.kind'`).
|
|
225
|
+
* @returns {function(string): object} A function `(typeValue) => objectMatcher`.
|
|
226
|
+
*/
|
|
227
|
+
function buildTypeMatcherFn(payloadPath) {
|
|
228
|
+
const parts = payloadPath.split('.');
|
|
229
|
+
return function (typeValue) {
|
|
230
|
+
let obj = typeValue;
|
|
231
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
232
|
+
obj = {[parts[i]]: obj};
|
|
233
|
+
}
|
|
234
|
+
return {payload: obj};
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Compile an object matcher into a raw-buffer predicate so raw-mode reads can filter compact
|
|
240
|
+
* JSON without parsing every document first.
|
|
241
|
+
*
|
|
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.
|
|
245
|
+
*/
|
|
246
|
+
function buildRawBufferMatcher(matcher = {}, options = {}) {
|
|
247
|
+
assert(isPlainObject(matcher), 'Matcher must be an object.', TypeError);
|
|
248
|
+
const enableOperatorBufferMatcher = options.enableOperatorBufferMatcher !== false;
|
|
249
|
+
|
|
250
|
+
const root = buildMatcherTree(matcher, enableOperatorBufferMatcher);
|
|
251
|
+
/* c8 ignore next 3 */
|
|
252
|
+
if (root.children.length === 0) {
|
|
253
|
+
return () => true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return function matchesRawBuffer(buffer) {
|
|
257
|
+
if (!isOpeningObject(buffer[0])) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
if (!preCheck(buffer, 1, root)) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
return matchesNode(buffer, 1, root);
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
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.
|
|
274
|
+
*/
|
|
275
|
+
function buildMatcherTree(matcher) {
|
|
276
|
+
const fast = [];
|
|
277
|
+
const slow = [];
|
|
278
|
+
|
|
279
|
+
for (const [key, value] of Object.entries(matcher)) {
|
|
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);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return {children: [...fast, ...slow]};
|
|
286
|
+
}
|
|
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
|
+
*/
|
|
296
|
+
function buildMatcherTreeChild(key, value) {
|
|
297
|
+
const keyPrefix = Buffer.from(`${JSON.stringify(key)}:`, 'utf8');
|
|
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);
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
child.pattern = buildKeyValuePattern(keyPrefix, value);
|
|
339
|
+
}
|
|
340
|
+
return child;
|
|
341
|
+
}
|
|
342
|
+
|
|
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) {
|
|
349
|
+
return Buffer.concat([keyPrefix, Buffer.from(JSON.stringify(value), 'utf8')]);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
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.
|
|
382
|
+
*/
|
|
383
|
+
function matchesNode(buffer, startOffset, node) {
|
|
384
|
+
for (const child of node.children) {
|
|
385
|
+
const matchPosition = indexOfSameLevel(buffer, child.pattern, startOffset, child.lastMatch, child.isKeyPattern);
|
|
386
|
+
if (matchPosition === -1) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const valueStart = matchPosition + child.pattern.length;
|
|
391
|
+
if (child.matches && !child.matches(buffer, valueStart)) {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
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
|
+
|
|
509
|
+
export {
|
|
510
|
+
createHmac,
|
|
511
|
+
matches,
|
|
512
|
+
buildMetadataHeader,
|
|
513
|
+
buildMetadataForMatcher,
|
|
514
|
+
buildMatcherFromMetadata,
|
|
515
|
+
buildTypeMatcherFn,
|
|
516
|
+
buildRawBufferMatcher
|
|
517
|
+
};
|
|
@@ -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
|
}
|
|
@@ -113,26 +115,61 @@ function wrapAndCheck(index, length) {
|
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
/**
|
|
116
|
-
*
|
|
117
|
-
* Each stream object is mutated in place by the `advance` function.
|
|
118
|
+
* Iterate an array-like list in forward or reverse order.
|
|
118
119
|
*
|
|
119
|
-
* @param {
|
|
120
|
-
* @param {
|
|
121
|
-
* @
|
|
122
|
-
* Returns true if the stream has more items within range, false if exhausted.
|
|
123
|
-
* @param {function(object): void} visit Called for each stream state in merged order.
|
|
120
|
+
* @param {Iterable|ArrayLike<any>} entries Entries to iterate.
|
|
121
|
+
* @param {boolean} forwards Iteration direction.
|
|
122
|
+
* @returns {Generator<any>}
|
|
124
123
|
*/
|
|
125
|
-
function
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
124
|
+
function* iterate(entries, forwards) {
|
|
125
|
+
if (forwards) {
|
|
126
|
+
yield* entries;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
131
|
+
yield entries[i];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Perform a k-way merge over multiple iterables in sort-key order.
|
|
137
|
+
*
|
|
138
|
+
* Each iterable is primed by calling `.next()` once at startup. On each merge step the iterable
|
|
139
|
+
* with the best current value is advanced and its value is yielded (after passing through `visit`).
|
|
140
|
+
* An iterable is dropped once its iterator reports `done`.
|
|
141
|
+
*
|
|
142
|
+
* @param {Iterable[]|Iterator[]} iterables Iterables or bare iterators to merge.
|
|
143
|
+
* @param {function(*): number} getSortKey Extracts the numeric sort key from an iterable's current value.
|
|
144
|
+
* @param {boolean} [ascending=true] When true, yields items in ascending key order (min-merge).
|
|
145
|
+
* When false, yields in descending key order (max-merge).
|
|
146
|
+
* @param {function(*): *} [visit] Optional extractor for the yielded value. Defaults to identity.
|
|
147
|
+
* @returns {Generator<*>} Merged sequence in key order.
|
|
148
|
+
*/
|
|
149
|
+
function *kWayMerge(iterables, getSortKey, ascending = true, visit = v => v) {
|
|
150
|
+
const states = [];
|
|
151
|
+
for (const iterable of iterables) {
|
|
152
|
+
const iterator = typeof iterable[Symbol.iterator] === 'function' ? iterable[Symbol.iterator]() : iterable;
|
|
153
|
+
const { value, done } = iterator.next();
|
|
154
|
+
if (!done) {
|
|
155
|
+
states.push({ iterator, current: value });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
while (states.length > 0) {
|
|
160
|
+
let bestIdx = 0;
|
|
161
|
+
for (let i = 1; i < states.length; i++) {
|
|
162
|
+
const better = ascending
|
|
163
|
+
? getSortKey(states[i].current) < getSortKey(states[bestIdx].current)
|
|
164
|
+
: getSortKey(states[i].current) > getSortKey(states[bestIdx].current);
|
|
165
|
+
if (better) bestIdx = i;
|
|
132
166
|
}
|
|
133
|
-
visit(
|
|
134
|
-
|
|
135
|
-
|
|
167
|
+
yield visit(states[bestIdx].current);
|
|
168
|
+
const { value, done } = states[bestIdx].iterator.next();
|
|
169
|
+
if (done) {
|
|
170
|
+
states.splice(bestIdx, 1);
|
|
171
|
+
} else {
|
|
172
|
+
states[bestIdx].current = value;
|
|
136
173
|
}
|
|
137
174
|
}
|
|
138
175
|
}
|
|
@@ -141,9 +178,9 @@ function kWayMerge(streams, getKey, advance, visit) {
|
|
|
141
178
|
* Read a scalar value at a dot-notation path from an object.
|
|
142
179
|
* Returns `undefined` if any path segment is absent or an intermediate value is not an object.
|
|
143
180
|
*
|
|
144
|
-
* @param {object} obj
|
|
181
|
+
* @param {object} obj Source object.
|
|
145
182
|
* @param {string} dotPath Dot-separated property path, e.g. `'payload.type'`.
|
|
146
|
-
* @returns {*}
|
|
183
|
+
* @returns {*} Value at the path or `undefined`.
|
|
147
184
|
*/
|
|
148
185
|
function getPropertyAtPath(obj, dotPath) {
|
|
149
186
|
let current = obj;
|
|
@@ -160,6 +197,7 @@ export {
|
|
|
160
197
|
assertEqual,
|
|
161
198
|
hash,
|
|
162
199
|
wrapAndCheck,
|
|
200
|
+
iterate,
|
|
163
201
|
binarySearch,
|
|
164
202
|
alignTo,
|
|
165
203
|
kWayMerge,
|