event-storage 1.0.0 → 1.2.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.
@@ -0,0 +1,247 @@
1
+ import crypto from 'crypto';
2
+ import { assert, assertEqual } from './util.js';
3
+ import { BYTE_OPEN_OBJECT, indexOfSameLevel } from './jsonUtil.js';
4
+
5
+ function isPlainObject(value) {
6
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
7
+ }
8
+
9
+ function propertyMatchesValue(documentValue, matcherValue) {
10
+ if (Array.isArray(matcherValue)) {
11
+ return matcherValue.includes(documentValue);
12
+ } else if (matcherValue && typeof matcherValue === 'object') {
13
+ return matches(documentValue, matcherValue);
14
+ }
15
+ return typeof matcherValue === 'undefined' || documentValue === matcherValue;
16
+ }
17
+
18
+ /**
19
+ * Build a buffer containing the file magic header and a JSON stringified metadata block, padded to be a multiple of 16 bytes long.
20
+ *
21
+ * @param {string} magic
22
+ * @param {object} metadata
23
+ * @returns {Buffer} A buffer containing the header data
24
+ */
25
+ function buildMetadataHeader(magic, metadata) {
26
+ assertEqual(magic.length, 8, 'The header magic bytes length is wrong.');
27
+ let metadataString = JSON.stringify(metadata);
28
+ let metadataSize = Buffer.byteLength(metadataString, 'utf8');
29
+ // 8 byte MAGIC, 4 byte metadata size, 1 byte line break
30
+ const pad = (16 - ((8 + 4 + metadataSize + 1) % 16)) % 16;
31
+ metadataString += ' '.repeat(pad) + "\n";
32
+ metadataSize += pad + 1;
33
+ const metadataBuffer = Buffer.allocUnsafe(8 + 4 + metadataSize);
34
+ metadataBuffer.write(magic, 0, 8, 'utf8');
35
+ metadataBuffer.writeUInt32BE(metadataSize, 8);
36
+ metadataBuffer.write(metadataString, 8 + 4, metadataSize, 'utf8');
37
+ return metadataBuffer;
38
+ }
39
+
40
+ /**
41
+ * @param {string} secret The secret to use for calculating further HMACs
42
+ * @returns {function(string)} A function that calculates the HMAC for a given string
43
+ */
44
+ const createHmac = secret => string => {
45
+ const hmac = crypto.createHmac('sha256', secret);
46
+ hmac.update(string);
47
+ return hmac.digest('hex');
48
+ };
49
+
50
+ /**
51
+ * @typedef {object|function(object):boolean} Matcher
52
+ */
53
+
54
+ /**
55
+ * @param {object} document The document to check against the matcher.
56
+ * @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.
57
+ * @returns {boolean} True if the document matches the matcher or false otherwise.
58
+ */
59
+ function matches(document, matcher) {
60
+ if (typeof document === 'undefined') return false;
61
+ if (typeof matcher === 'undefined') return true;
62
+
63
+ if (typeof matcher === 'function') return matcher(document);
64
+
65
+ for (let prop of Object.getOwnPropertyNames(matcher)) {
66
+ if (!propertyMatchesValue(document[prop], matcher[prop])) {
67
+ return false;
68
+ }
69
+ }
70
+ return true;
71
+ }
72
+
73
+ /**
74
+ * @param {Matcher} matcher The matcher object or function that should be serialized.
75
+ * @param {function(string)} hmac A function that calculates a HMAC of the given string.
76
+ * @returns {{matcher: string|object, hmac?: string}}
77
+ */
78
+ function buildMetadataForMatcher(matcher, hmac) {
79
+ if (!matcher) {
80
+ return undefined;
81
+ }
82
+ if (typeof matcher === 'object') {
83
+ return { matcher };
84
+ }
85
+ const matcherString = matcher.toString();
86
+ return { matcher: matcherString, hmac: hmac(matcherString) };
87
+ }
88
+
89
+ /**
90
+ * @param {{matcher: string|object, hmac: string}} matcherMetadata The serialized matcher and its HMAC
91
+ * @param {function(string)} hmac A function that calculates a HMAC of the given string.
92
+ * @returns {Matcher} The matcher object or function.
93
+ */
94
+ function buildMatcherFromMetadata(matcherMetadata, hmac) {
95
+ let matcher;
96
+ if (typeof matcherMetadata.matcher === 'object') {
97
+ matcher = matcherMetadata.matcher;
98
+ } else {
99
+ assert(matcherMetadata.hmac === hmac(matcherMetadata.matcher), 'Invalid HMAC for matcher.');
100
+
101
+ matcher = eval('(' + matcherMetadata.matcher + ')').bind({}); // jshint ignore:line
102
+ }
103
+ return matcher;
104
+ }
105
+
106
+ /**
107
+ * Builds a factory function that, given a type string, returns an object matcher for
108
+ * documents whose payload contains that type at the given dot-notation path.
109
+ *
110
+ * @param {string} payloadPath Dot-notation path relative to the event payload (e.g. `'type'`, `'meta.kind'`).
111
+ * @returns {function(string): object} A function `(typeValue) => objectMatcher`.
112
+ */
113
+ function buildTypeMatcherFn(payloadPath) {
114
+ const parts = payloadPath.split('.');
115
+ return function(typeValue) {
116
+ let obj = typeValue;
117
+ for (let i = parts.length - 1; i >= 0; i--) {
118
+ obj = { [parts[i]]: obj };
119
+ }
120
+ return { payload: obj };
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Builds a raw-buffer matcher.
126
+ * It expects the Buffer to contain compact stringified JSON
127
+ * and supports matcher objects with sub properties and multi-value matches (OR/any of).
128
+ *
129
+ * @param {object} matcher Object matcher.
130
+ * @returns {function(Buffer): boolean}
131
+ */
132
+ function buildRawBufferMatcher(matcher = {}) {
133
+ assert(matcher && typeof matcher ==='object' && !Array.isArray(matcher), 'Matcher must be an object.', TypeError);
134
+
135
+ const root = buildMatcherTree(matcher);
136
+ if (root.children.length === 0) {
137
+ return () => true;
138
+ }
139
+
140
+ return function matchesRawBuffer(buffer) {
141
+ if (buffer[0] !== BYTE_OPEN_OBJECT) {
142
+ return false;
143
+ }
144
+ if (!preCheck(buffer, 1, root)) {
145
+ return false;
146
+ }
147
+ return matchesNode(buffer, 1, root);
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Optimization pass: check that every required byte pattern is present anywhere in the buffer
153
+ * before spending the more expensive per-depth scan in `matchesNode`.
154
+ */
155
+ function preCheck(buffer, startOffset, node) {
156
+ for (const child of node.children) {
157
+ if (child.valuePatterns && !child.valuePatterns.some((pattern, i) => {
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.
180
+ */
181
+ function buildMatcherTree(matcher) {
182
+ const node = { children: [] };
183
+
184
+ for (const [key, value] of Object.entries(matcher)) {
185
+ node.children.push(buildMatcherTreeChild(key, value));
186
+ }
187
+
188
+ return node;
189
+ }
190
+
191
+ function buildMatcherTreeChild(key, value) {
192
+ const keyPrefix = Buffer.from(`${JSON.stringify(key)}:`, 'utf8');
193
+ const child = { objectPattern: null, valuePatterns: null, node: null, objMatch: null, valueMatches: [] };
194
+ if (Array.isArray(value)) {
195
+ if (value.some(item => item && typeof item === 'object')) {
196
+ throw new TypeError('Array matcher values must be scalars.');
197
+ }
198
+ child.valuePatterns = value.map(item => buildValuePattern(keyPrefix, item));
199
+ return child;
200
+ } else if (value && typeof value === 'object') {
201
+ child.objectPattern = Buffer.concat([keyPrefix, Buffer.from('{', 'utf8')]);
202
+ child.node = buildMatcherTree(value);
203
+ return child;
204
+ }
205
+ child.valuePatterns = [buildValuePattern(keyPrefix, value)];
206
+ return child;
207
+ }
208
+
209
+ function buildValuePattern(keyPrefix, value) {
210
+ return Buffer.concat([keyPrefix, Buffer.from(JSON.stringify(value), 'utf8')]);
211
+ }
212
+
213
+ /**
214
+ * Verify that each required byte pattern in the tree is present at the correct JSON nesting
215
+ * depth so values inside nested objects don't satisfy a top-level match requirement.
216
+ */
217
+ function matchesNode(buffer, startOffset, node) {
218
+ for (const child of node.children) {
219
+ if (child.valuePatterns && !child.valuePatterns.some((pattern, i) => {
220
+ return indexOfSameLevel(buffer, pattern, startOffset, child.valueMatches[i]) !== -1;
221
+ })) {
222
+ return false;
223
+ }
224
+
225
+ if (child.node) {
226
+ const objectIndex = indexOfSameLevel(buffer, child.objectPattern, startOffset, child.objMatch);
227
+ if (objectIndex === -1) {
228
+ return false;
229
+ }
230
+ if (!matchesNode(buffer, objectIndex + child.objectPattern.length, child.node)) {
231
+ return false;
232
+ }
233
+ }
234
+ }
235
+
236
+ return true;
237
+ }
238
+
239
+ export {
240
+ createHmac,
241
+ matches,
242
+ buildMetadataHeader,
243
+ buildMetadataForMatcher,
244
+ buildMatcherFromMetadata,
245
+ buildTypeMatcherFn,
246
+ buildRawBufferMatcher
247
+ };
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Assert that actual and expected match or throw an Error with the given message appended by information about expected and actual value.
3
+ *
4
+ * @param {*} actual
5
+ * @param {*} expected
6
+ * @param {string} message
7
+ */
8
+ function assertEqual(actual, expected, message) {
9
+ if (actual !== expected) {
10
+ throw new Error(message + (message ? ' ' : '') + `Expected "${expected}" but got "${actual}".`);
11
+ }
12
+ }
13
+
14
+ /**
15
+ * Assert that the condition holds and if not, throw an error with the given message.
16
+ *
17
+ * @param {boolean} condition
18
+ * @param {string} message
19
+ * @param {typeof Error} ErrorType
20
+ */
21
+ function assert(condition, message, ErrorType = Error) {
22
+ if (!condition) {
23
+ throw new ErrorType(message);
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Return the amount required to align value to the given alignment.
29
+ * 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}
33
+ */
34
+ function alignTo(value, alignment) {
35
+ return (alignment - (value % alignment)) % alignment;
36
+ }
37
+
38
+ /**
39
+ * Method for hashing a string (e.g. a partition name) to a 32-bit unsigned integer.
40
+ *
41
+ * @param {string} str
42
+ * @returns {number}
43
+ */
44
+ function hash(str) {
45
+ /* istanbul ignore if */
46
+ if (str.length === 0) {
47
+ return 0;
48
+ }
49
+ let hash = 5381,
50
+ i = str.length;
51
+
52
+ while(i) {
53
+ hash = ((hash << 5) + hash) ^ str.charCodeAt(--i); // jshint ignore:line
54
+ }
55
+
56
+ /* JavaScript does bitwise operations (like XOR, above) on 32-bit signed
57
+ * integers. Since we want the results to be always positive, convert the
58
+ * signed int to an unsigned by doing an unsigned bitshift. */
59
+ return hash >>> 0; // jshint ignore:line
60
+ }
61
+
62
+ /**
63
+ * Do a binary search for number in the range 1-length with values retrieved via a provided getter.
64
+ *
65
+ * @param {number} number The value to search for
66
+ * @param {number} length The upper position to search up to
67
+ * @param {function(number)} get The getter function to retrieve the values at the specific position
68
+ * @returns {Array<number>} An array of the low and high position that match the searched number
69
+ */
70
+ function binarySearch(number, length, get) {
71
+ let low = 1;
72
+ let high = length;
73
+
74
+ if (get(low) > number) {
75
+ return [low, 0];
76
+ }
77
+ if (get(high) < number) {
78
+ return [0, high];
79
+ }
80
+
81
+ while (low <= high) {
82
+ const mid = low + ((high - low) >> 1);
83
+ const value = get(mid);
84
+ if (value === number) {
85
+ return [mid, mid];
86
+ }
87
+ if (value < number) {
88
+ low = mid + 1;
89
+ } else {
90
+ high = mid - 1;
91
+ }
92
+ }
93
+ return [low, high];
94
+ }
95
+
96
+ /**
97
+ * @param {number} index The 1-based index position to wrap around if < 0 and check against the bounds.
98
+ * @param {number} length The length of the index and upper bound.
99
+ * @returns {number} The wrapped index position or -1 if index out of bounds.
100
+ */
101
+ function wrapAndCheck(index, length) {
102
+ if (typeof index !== 'number') {
103
+ return -1;
104
+ }
105
+
106
+ if (index < 0) {
107
+ index += length + 1;
108
+ }
109
+ if (index < 1 || index > length) {
110
+ return -1;
111
+ }
112
+ return index;
113
+ }
114
+
115
+ /**
116
+ * Iterate an array-like list in forward or reverse order.
117
+ *
118
+ * @param {Iterable} entries
119
+ * @param {boolean} forwards
120
+ */
121
+ function* iterate(entries, forwards) {
122
+ if (forwards) {
123
+ yield* entries;
124
+ return;
125
+ }
126
+
127
+ for (let i = entries.length - 1; i >= 0; i--) {
128
+ yield entries[i];
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Perform a k-way merge over multiple iterables in sort-key order.
134
+ *
135
+ * Each iterable is primed by calling `.next()` once at startup. On each merge step the iterable
136
+ * with the best current value is advanced and its value is yielded (after passing through `visit`).
137
+ * An iterable is dropped once its iterator reports `done`.
138
+ *
139
+ * @param {Iterable[]|Iterator[]} iterables Iterables or bare iterators to merge.
140
+ * @param {function(*): number} getSortKey Extracts the numeric sort key from an iterable's current value.
141
+ * @param {boolean} [ascending=true] When true, yields items in ascending key order (min-merge).
142
+ * When false, yields in descending key order (max-merge).
143
+ * @param {function(*): *} [visit] Optional extractor for the yielded value. Defaults to identity.
144
+ * @returns {Generator<*>}
145
+ */
146
+ function *kWayMerge(iterables, getSortKey, ascending = true, visit = v => v) {
147
+ const states = [];
148
+ for (const iterable of iterables) {
149
+ const iterator = typeof iterable[Symbol.iterator] === 'function' ? iterable[Symbol.iterator]() : iterable;
150
+ const { value, done } = iterator.next();
151
+ if (!done) {
152
+ states.push({ iterator, current: value });
153
+ }
154
+ }
155
+
156
+ while (states.length > 0) {
157
+ let bestIdx = 0;
158
+ for (let i = 1; i < states.length; i++) {
159
+ const better = ascending
160
+ ? getSortKey(states[i].current) < getSortKey(states[bestIdx].current)
161
+ : getSortKey(states[i].current) > getSortKey(states[bestIdx].current);
162
+ if (better) bestIdx = i;
163
+ }
164
+ yield visit(states[bestIdx].current);
165
+ const { value, done } = states[bestIdx].iterator.next();
166
+ if (done) {
167
+ states.splice(bestIdx, 1);
168
+ } else {
169
+ states[bestIdx].current = value;
170
+ }
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Read a scalar value at a dot-notation path from an object.
176
+ * Returns `undefined` if any path segment is absent or an intermediate value is not an object.
177
+ *
178
+ * @param {object} obj
179
+ * @param {string} dotPath Dot-separated property path, e.g. `'payload.type'`.
180
+ * @returns {*}
181
+ */
182
+ function getPropertyAtPath(obj, dotPath) {
183
+ let current = obj;
184
+ const parts = dotPath.split('.');
185
+ for (const part of parts) {
186
+ if (current == null || typeof current !== 'object') return undefined;
187
+ current = current[part];
188
+ }
189
+ return current;
190
+ }
191
+
192
+ export {
193
+ assert,
194
+ assertEqual,
195
+ hash,
196
+ wrapAndCheck,
197
+ iterate,
198
+ binarySearch,
199
+ alignTo,
200
+ kWayMerge,
201
+ getPropertyAtPath
202
+ };
@@ -1,79 +0,0 @@
1
- import crypto from 'crypto';
2
-
3
- /**
4
- * @param {string} secret The secret to use for calculating further HMACs
5
- * @returns {function(string)} A function that calculates the HMAC for a given string
6
- */
7
- const createHmac = secret => string => {
8
- const hmac = crypto.createHmac('sha256', secret);
9
- hmac.update(string);
10
- return hmac.digest('hex');
11
- };
12
-
13
- /**
14
- * @typedef {object|function(object):boolean} Matcher
15
- */
16
-
17
- /**
18
- * @param {object} document The document to check against the matcher.
19
- * @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.
20
- * @returns {boolean} True if the document matches the matcher or false otherwise.
21
- */
22
- function matches(document, matcher) {
23
- if (typeof document === 'undefined') return false;
24
- if (typeof matcher === 'undefined') return true;
25
-
26
- if (typeof matcher === 'function') return matcher(document);
27
-
28
- for (let prop of Object.getOwnPropertyNames(matcher)) {
29
- if (typeof matcher[prop] === 'object') {
30
- if (!matches(document[prop], matcher[prop])) {
31
- return false;
32
- }
33
- } else if (typeof matcher[prop] !== 'undefined' && document[prop] !== matcher[prop]) {
34
- return false;
35
- }
36
- }
37
- return true;
38
- }
39
-
40
- /**
41
- * @param {Matcher} matcher The matcher object or function that should be serialized.
42
- * @param {function(string)} hmac A function that calculates a HMAC of the given string.
43
- * @returns {{matcher: string|object, hmac?: string}}
44
- */
45
- function buildMetadataForMatcher(matcher, hmac) {
46
- if (!matcher) {
47
- return undefined;
48
- }
49
- if (typeof matcher === 'object') {
50
- return { matcher };
51
- }
52
- const matcherString = matcher.toString();
53
- return { matcher: matcherString, hmac: hmac(matcherString) };
54
- }
55
-
56
- /**
57
- * @param {{matcher: string|object, hmac: string}} matcherMetadata The serialized matcher and its HMAC
58
- * @param {function(string)} hmac A function that calculates a HMAC of the given string.
59
- * @returns {Matcher} The matcher object or function.
60
- */
61
- function buildMatcherFromMetadata(matcherMetadata, hmac) {
62
- let matcher;
63
- if (typeof matcherMetadata.matcher === 'object') {
64
- matcher = matcherMetadata.matcher;
65
- } else {
66
- if (matcherMetadata.hmac !== hmac(matcherMetadata.matcher)) {
67
- throw new Error('Invalid HMAC for matcher.');
68
- }
69
- matcher = eval('(' + matcherMetadata.matcher + ')').bind({}); // jshint ignore:line
70
- }
71
- return matcher;
72
- }
73
-
74
- export {
75
- createHmac,
76
- matches,
77
- buildMetadataForMatcher,
78
- buildMatcherFromMetadata
79
- };