• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

albe / node-event-storage / 29284894307

13 Jul 2026 09:03PM UTC coverage: 98.252% (-0.1%) from 98.373%
29284894307

Pull #328

github

web-flow
Merge 60ce6c612 into 83943a8a4
Pull Request #328: Selector algebra

1432 of 1491 branches covered (96.04%)

Branch coverage included in aggregate %.

356 of 364 new or added lines in 3 files covered. (97.8%)

7057 of 7149 relevant lines covered (98.71%)

879.02 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.37
/src/JoinEventStream.js
1
import EventStream from './EventStream.js';
4✔
2
import {assert, iterate} from './utils/util.js';
4✔
3
import {intersect, normalizeSelector, union} from './utils/indexUtil.js';
4✔
4
import {normalizeRevision} from './utils/apiHelpers.js';
4✔
5

4✔
6
/**
4✔
7
 * JoinEventStream is a virtual stream over one or multiple physical stream indexes.
4✔
8
 *
4✔
9
 * It is the canonical implementation behind `EventStore.fromStreams(...)` and supports
4✔
10
 * nested selector algebra with alternating operators by depth:
4✔
11
 *
4✔
12
 * - depth 0 (top-level array): OR
4✔
13
 * - depth 1: AND
4✔
14
 * - depth 2: OR
4✔
15
 * - ... alternating by depth parity
4✔
16
 *
4✔
17
 * Flat arrays (e.g. `['a', 'b']`) therefore keep the legacy join semantics (OR).
4✔
18
 *
4✔
19
 * @extends EventStream
4✔
20
 */
4✔
21
class JoinEventStream extends EventStream {
4✔
22

4✔
23
    /**
4✔
24
     * @param {string} name The name of this virtual stream.
4✔
25
     * @param {Array<string|Array>} selector Stream selector as flat or nested arrays.
4✔
26
     * @param {EventStore} eventStore The event store instance.
4✔
27
     * @param {number} [minRevision=1] Global minimum revision (inclusive).
4✔
28
     * @param {number} [maxRevision=-1] Global maximum revision (inclusive).
4✔
29
     * @param {function|object|null} [predicate] Optional matcher (same semantics as EventStream).
4✔
30
     * @param {boolean} [raw=false] If true, emit NDJSON buffers.
4✔
31
     */
4✔
32
    constructor(name, selector, eventStore, minRevision = 1, maxRevision = -1, predicate = null, raw = false) {
4✔
33
        super(name, eventStore, minRevision, maxRevision, predicate, raw);
264✔
34
        assert(Array.isArray(selector) && selector.length > 0, `Invalid selector supplied to JoinEventStream ${name}.`);
264✔
35

264✔
36
        this.eventStore = eventStore;
264✔
37
        this.selector = normalizeSelector(selector);
264✔
38
        this.streamIndex = eventStore.storage.index;
264✔
39
        this.minRevision = normalizeRevision(minRevision, eventStore.length);
264✔
40
        this.maxRevision = normalizeRevision(maxRevision, eventStore.length);
264✔
41
        this.version = this.streamIndex.length;
264✔
42

264✔
43
        this._combinedRanges = null;
264✔
44

264✔
45
        this.fetch = () => this.iterateDocuments();
264✔
46
        this._iterator = null;
264✔
47
    }
264✔
48

4✔
49

4✔
50
    /**
4✔
51
     * Resolve and cache the combined index-entry ranges for the full selector.
4✔
52
     *
4✔
53
     * @private
4✔
54
     * @returns {Array<Array<number>>}
4✔
55
     */
4✔
56
    resolveCombinedRanges() {
4✔
57
        if (this._combinedRanges) {
216!
NEW
58
            return this._combinedRanges;
×
NEW
59
        }
×
60

216✔
61
        this._combinedRanges = this.resolveSelectorRanges(this.selector);
216✔
62
        return this._combinedRanges;
216✔
63
    }
216✔
64

4✔
65
    /**
4✔
66
     * Resolve one selector node into a sorted index-entry range.
4✔
67
     * Selector is already normalized and optimized from constructor, so we just traverse and collect ranges.
4✔
68
     *
4✔
69
     * @private
4✔
70
     * @param {string|Array<string|Array>} selectorNode
4✔
71
     * @param {number} [depth=0]
4✔
72
     * @returns {Array<Array<number>>}
4✔
73
     */
4✔
74
    resolveSelectorRanges(selectorNode, depth = 0) {
4✔
75
        if (typeof selectorNode === 'string') {
608✔
76
            const index = this.eventStore.streams[selectorNode]?.index;
468✔
77
            return this.resolveIndexRange(index);
468✔
78
        }
468✔
79

140✔
80
        const childRanges = selectorNode.map(node => this.resolveSelectorRanges(node, depth + 1));
140✔
81
        return depth % 2 === 0 ? union(...childRanges) : intersect(...childRanges);
608!
82
    }
608✔
83

4✔
84
    /**
4✔
85
     * Resolve one stream index to the global revision-bounded entry range.
4✔
86
     *
4✔
87
     * @private
4✔
88
     * @param {object|undefined} index
4✔
89
     * @returns {Array<Array<number>>}
4✔
90
     */
4✔
91
    resolveIndexRange(index) {
4✔
92
        if (!index || index.length === 0) {
468✔
93
            return [];
12✔
94
        }
12✔
95

456✔
96
        const ascending = this.minRevision <= this.maxRevision;
456✔
97
        const from = index.find(this.minRevision, ascending);
456✔
98
        const until = index.find(this.maxRevision, !ascending);
456✔
99
        if (
456✔
100
            from === 0 ||
456✔
101
            until === 0 ||
468✔
102
            (ascending ? from > until : from < until)
456✔
103
        ) {
468✔
104
            return [];
4✔
105
        }
4✔
106

452✔
107
        const rangeFrom = ascending ? from : until;
468✔
108
        const rangeUntil = ascending ? until : from;
468✔
109
        return index.range(rangeFrom, rangeUntil) || [];
468!
110
    }
468✔
111

4✔
112
    /**
4✔
113
     * Iterate matching index entries in requested direction.
4✔
114
     *
4✔
115
     * @private
4✔
116
     * @returns {Generator<Array<number>>}
4✔
117
     */
4✔
118
    * iterateEntries() {
4✔
119
        const entries = this.resolveCombinedRanges();
216✔
120
        const forwards = this.minRevision <= this.maxRevision;
216✔
121
        yield* iterate(entries, forwards);
216✔
122
    }
216✔
123

4✔
124
    /**
4✔
125
     * Iterate storage documents lazily from combined index entries.
4✔
126
     *
4✔
127
     * @private
4✔
128
     * @returns {Generator<object|{ buffer: Buffer, time64: number, sequenceNumber: number }>}
4✔
129
     */
4✔
130
    * iterateDocuments() {
4✔
131
        const forwards = this.minRevision <= this.maxRevision;
216✔
132
        for (const entry of this.iterateEntries()) {
216✔
133
            const event = this.eventStore.storage.readFrom(
912✔
134
                entry.partition,
912✔
135
                entry.position,
912✔
136
                entry.size,
912✔
137
                this.raw,
912✔
138
                !forwards
912✔
139
            );
912✔
140
            if (event) {
912✔
141
                yield event;
912✔
142
            }
900✔
143
        }
912✔
144
    }
216✔
145

4✔
146
    /**
4✔
147
     * Reset fetch state and cached selector ranges when stream boundaries changed.
4✔
148
     *
4✔
149
     * @returns {JoinEventStream}
4✔
150
     */
4✔
151
    reset() {
4✔
152
        this._combinedRanges = null;
20✔
153
        return super.reset();
20✔
154
    }
20✔
155
}
4✔
156

4✔
157
export default JoinEventStream;
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc