event-storage 1.1.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
+ };
@@ -113,26 +113,60 @@ function wrapAndCheck(index, length) {
113
113
  }
114
114
 
115
115
  /**
116
- * Perform a k-way merge over multiple streams, invoking a callback for each item in ascending key order.
117
- * Each stream object is mutated in place by the `advance` function.
116
+ * Iterate an array-like list in forward or reverse order.
118
117
  *
119
- * @param {object[]} streams Array of stream state objects; entries are removed when exhausted.
120
- * @param {function(object): number} getKey Returns the current sort key for a stream state.
121
- * @param {function(object): boolean} advance Advances the stream to its next item.
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.
118
+ * @param {Iterable} entries
119
+ * @param {boolean} forwards
124
120
  */
125
- function kWayMerge(streams, getKey, advance, visit) {
126
- while (streams.length > 0) {
127
- let minIdx = 0;
128
- for (let i = 1; i < streams.length; i++) {
129
- if (getKey(streams[i]) < getKey(streams[minIdx])) {
130
- minIdx = i;
131
- }
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;
132
163
  }
133
- visit(streams[minIdx]);
134
- if (!advance(streams[minIdx])) {
135
- streams.splice(minIdx, 1);
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;
136
170
  }
137
171
  }
138
172
  }
@@ -160,6 +194,7 @@ export {
160
194
  assertEqual,
161
195
  hash,
162
196
  wrapAndCheck,
197
+ iterate,
163
198
  binarySearch,
164
199
  alignTo,
165
200
  kWayMerge,
@@ -1,126 +0,0 @@
1
- import crypto from 'crypto';
2
- import { assertEqual } from './util.js';
3
-
4
- /**
5
- * Build a buffer containing the file magic header and a JSON stringified metadata block, padded to be a multiple of 16 bytes long.
6
- *
7
- * @param {string} magic
8
- * @param {object} metadata
9
- * @returns {Buffer} A buffer containing the header data
10
- */
11
- function buildMetadataHeader(magic, metadata) {
12
- assertEqual(magic.length, 8, 'The header magic bytes length is wrong.');
13
- let metadataString = JSON.stringify(metadata);
14
- let metadataSize = Buffer.byteLength(metadataString, 'utf8');
15
- // 8 byte MAGIC, 4 byte metadata size, 1 byte line break
16
- const pad = (16 - ((8 + 4 + metadataSize + 1) % 16)) % 16;
17
- metadataString += ' '.repeat(pad) + "\n";
18
- metadataSize += pad + 1;
19
- const metadataBuffer = Buffer.allocUnsafe(8 + 4 + metadataSize);
20
- metadataBuffer.write(magic, 0, 8, 'utf8');
21
- metadataBuffer.writeUInt32BE(metadataSize, 8);
22
- metadataBuffer.write(metadataString, 8 + 4, metadataSize, 'utf8');
23
- return metadataBuffer;
24
- }
25
-
26
- /**
27
- * @param {string} secret The secret to use for calculating further HMACs
28
- * @returns {function(string)} A function that calculates the HMAC for a given string
29
- */
30
- const createHmac = secret => string => {
31
- const hmac = crypto.createHmac('sha256', secret);
32
- hmac.update(string);
33
- return hmac.digest('hex');
34
- };
35
-
36
- /**
37
- * @typedef {object|function(object):boolean} Matcher
38
- */
39
-
40
- /**
41
- * @param {object} document The document to check against the matcher.
42
- * @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.
43
- * @returns {boolean} True if the document matches the matcher or false otherwise.
44
- */
45
- function matches(document, matcher) {
46
- if (typeof document === 'undefined') return false;
47
- if (typeof matcher === 'undefined') return true;
48
-
49
- if (typeof matcher === 'function') return matcher(document);
50
-
51
- for (let prop of Object.getOwnPropertyNames(matcher)) {
52
- if (Array.isArray(matcher[prop])) {
53
- if (!matcher[prop].includes(document[prop])) {
54
- return false;
55
- }
56
- } else if (typeof matcher[prop] === 'object') {
57
- if (!matches(document[prop], matcher[prop])) {
58
- return false;
59
- }
60
- } else if (typeof matcher[prop] !== 'undefined' && document[prop] !== matcher[prop]) {
61
- return false;
62
- }
63
- }
64
- return true;
65
- }
66
-
67
- /**
68
- * @param {Matcher} matcher The matcher object or function that should be serialized.
69
- * @param {function(string)} hmac A function that calculates a HMAC of the given string.
70
- * @returns {{matcher: string|object, hmac?: string}}
71
- */
72
- function buildMetadataForMatcher(matcher, hmac) {
73
- if (!matcher) {
74
- return undefined;
75
- }
76
- if (typeof matcher === 'object') {
77
- return { matcher };
78
- }
79
- const matcherString = matcher.toString();
80
- return { matcher: matcherString, hmac: hmac(matcherString) };
81
- }
82
-
83
- /**
84
- * @param {{matcher: string|object, hmac: string}} matcherMetadata The serialized matcher and its HMAC
85
- * @param {function(string)} hmac A function that calculates a HMAC of the given string.
86
- * @returns {Matcher} The matcher object or function.
87
- */
88
- function buildMatcherFromMetadata(matcherMetadata, hmac) {
89
- let matcher;
90
- if (typeof matcherMetadata.matcher === 'object') {
91
- matcher = matcherMetadata.matcher;
92
- } else {
93
- if (matcherMetadata.hmac !== hmac(matcherMetadata.matcher)) {
94
- throw new Error('Invalid HMAC for matcher.');
95
- }
96
- matcher = eval('(' + matcherMetadata.matcher + ')').bind({}); // jshint ignore:line
97
- }
98
- return matcher;
99
- }
100
-
101
- /**
102
- * Builds a factory function that, given a type string, returns an object matcher for
103
- * documents whose payload contains that type at the given dot-notation path.
104
- *
105
- * @param {string} payloadPath Dot-notation path relative to the event payload (e.g. `'type'`, `'meta.kind'`).
106
- * @returns {function(string): object} A function `(typeValue) => objectMatcher`.
107
- */
108
- function buildTypeMatcherFn(payloadPath) {
109
- const parts = payloadPath.split('.');
110
- return function(typeValue) {
111
- let obj = typeValue;
112
- for (let i = parts.length - 1; i >= 0; i--) {
113
- obj = { [parts[i]]: obj };
114
- }
115
- return { payload: obj };
116
- };
117
- }
118
-
119
- export {
120
- createHmac,
121
- matches,
122
- buildMetadataHeader,
123
- buildMetadataForMatcher,
124
- buildMatcherFromMetadata,
125
- buildTypeMatcherFn
126
- };
File without changes