@tanstack/db-ivm 0.0.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 +58 -0
- package/package.json +63 -0
- package/src/d2.ts +150 -0
- package/src/graph.ts +131 -0
- package/src/index.ts +4 -0
- package/src/indexes.ts +117 -0
- package/src/multiset.ts +215 -0
- package/src/operators/concat.ts +45 -0
- package/src/operators/consolidate.ts +50 -0
- package/src/operators/count.ts +52 -0
- package/src/operators/debug.ts +58 -0
- package/src/operators/distinct.ts +98 -0
- package/src/operators/filter.ts +48 -0
- package/src/operators/filterBy.ts +31 -0
- package/src/operators/groupBy.ts +354 -0
- package/src/operators/index.ts +18 -0
- package/src/operators/join.ts +265 -0
- package/src/operators/keying.ts +34 -0
- package/src/operators/map.ts +48 -0
- package/src/operators/negate.ts +33 -0
- package/src/operators/orderBy.ts +213 -0
- package/src/operators/orderByBTree.ts +20 -0
- package/src/operators/output.ts +53 -0
- package/src/operators/pipe.ts +55 -0
- package/src/operators/reduce.ts +124 -0
- package/src/operators/topK.ts +92 -0
- package/src/operators/topKWithFractionalIndex.ts +347 -0
- package/src/operators/topKWithFractionalIndexBTree.ts +306 -0
- package/src/types.ts +376 -0
- package/src/utils.ts +163 -0
package/src/multiset.ts
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DefaultMap,
|
|
3
|
+
chunkedArrayPush,
|
|
4
|
+
globalObjectIdGenerator,
|
|
5
|
+
hash,
|
|
6
|
+
} from "./utils.js"
|
|
7
|
+
|
|
8
|
+
export type MultiSetArray<T> = Array<[T, number]>
|
|
9
|
+
export type KeyedData<T> = [key: string, value: T]
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A multiset of data.
|
|
13
|
+
*/
|
|
14
|
+
export class MultiSet<T> {
|
|
15
|
+
#inner: MultiSetArray<T>
|
|
16
|
+
|
|
17
|
+
constructor(data: MultiSetArray<T> = []) {
|
|
18
|
+
this.#inner = data
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
toString(indent = false): string {
|
|
22
|
+
return `MultiSet(${JSON.stringify(this.#inner, null, indent ? 2 : undefined)})`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
toJSON(): string {
|
|
26
|
+
return JSON.stringify(Array.from(this.getInner()))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static fromJSON<U>(json: string): MultiSet<U> {
|
|
30
|
+
return new MultiSet(JSON.parse(json))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Apply a function to all records in the collection.
|
|
35
|
+
*/
|
|
36
|
+
map<U>(f: (data: T) => U): MultiSet<U> {
|
|
37
|
+
return new MultiSet(
|
|
38
|
+
this.#inner.map(([data, multiplicity]) => [f(data), multiplicity])
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Filter out records for which a function f(record) evaluates to False.
|
|
44
|
+
*/
|
|
45
|
+
filter(f: (data: T) => boolean): MultiSet<T> {
|
|
46
|
+
return new MultiSet(this.#inner.filter(([data, _]) => f(data)))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Negate all multiplicities in the collection.
|
|
51
|
+
*/
|
|
52
|
+
negate(): MultiSet<T> {
|
|
53
|
+
return new MultiSet(
|
|
54
|
+
this.#inner.map(([data, multiplicity]) => [data, -multiplicity])
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Concatenate two collections together.
|
|
60
|
+
*/
|
|
61
|
+
concat(other: MultiSet<T>): MultiSet<T> {
|
|
62
|
+
const out: MultiSetArray<T> = []
|
|
63
|
+
chunkedArrayPush(out, this.#inner)
|
|
64
|
+
chunkedArrayPush(out, other.getInner())
|
|
65
|
+
return new MultiSet(out)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Produce as output a collection that is logically equivalent to the input
|
|
70
|
+
* but which combines identical instances of the same record into one
|
|
71
|
+
* (record, multiplicity) pair.
|
|
72
|
+
*/
|
|
73
|
+
consolidate(): MultiSet<T> {
|
|
74
|
+
// Check if this looks like a keyed multiset (first item is a tuple of length 2)
|
|
75
|
+
if (this.#inner.length > 0) {
|
|
76
|
+
const firstItem = this.#inner[0]?.[0]
|
|
77
|
+
if (Array.isArray(firstItem) && firstItem.length === 2) {
|
|
78
|
+
return this.#consolidateKeyed()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Fall back to original method for unkeyed data
|
|
83
|
+
return this.#consolidateUnkeyed()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Private method for consolidating keyed multisets where keys are strings/numbers
|
|
88
|
+
* and values are compared by reference equality.
|
|
89
|
+
*
|
|
90
|
+
* This method provides significant performance improvements over the hash-based approach
|
|
91
|
+
* by using WeakMap for object reference tracking and avoiding expensive serialization.
|
|
92
|
+
*
|
|
93
|
+
* Special handling for join operations: When values are tuples of length 2 (common in joins),
|
|
94
|
+
* we unpack them and compare each element individually to maintain proper equality semantics.
|
|
95
|
+
*/
|
|
96
|
+
#consolidateKeyed(): MultiSet<T> {
|
|
97
|
+
const consolidated = new Map<string, number>()
|
|
98
|
+
const values = new Map<string, T>()
|
|
99
|
+
|
|
100
|
+
// Use global object ID generator for consistent reference equality
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Special handler for tuples (arrays of length 2) commonly produced by join operations.
|
|
104
|
+
* Unpacks the tuple and generates an ID based on both elements to ensure proper
|
|
105
|
+
* consolidation of join results like ['A', null] and [null, 'X'].
|
|
106
|
+
*/
|
|
107
|
+
const getTupleId = (tuple: Array<any>): string => {
|
|
108
|
+
if (tuple.length !== 2) {
|
|
109
|
+
throw new Error(`Expected tuple of length 2`)
|
|
110
|
+
}
|
|
111
|
+
const [first, second] = tuple
|
|
112
|
+
return `${globalObjectIdGenerator.getStringId(first)}|${globalObjectIdGenerator.getStringId(second)}`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Process each item in the multiset
|
|
116
|
+
for (const [data, multiplicity] of this.#inner) {
|
|
117
|
+
// Verify this is still a keyed item (should be [key, value] pair)
|
|
118
|
+
if (!Array.isArray(data) || data.length !== 2) {
|
|
119
|
+
// Found non-keyed item, fall back to unkeyed consolidation
|
|
120
|
+
return this.#consolidateUnkeyed()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const [key, value] = data
|
|
124
|
+
|
|
125
|
+
// Verify key is string or number as expected for keyed multisets
|
|
126
|
+
if (typeof key !== `string` && typeof key !== `number`) {
|
|
127
|
+
// Found non-string/number key, fall back to unkeyed consolidation
|
|
128
|
+
return this.#consolidateUnkeyed()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Generate value ID with special handling for join tuples
|
|
132
|
+
let valueId: string
|
|
133
|
+
if (Array.isArray(value) && value.length === 2) {
|
|
134
|
+
// Special case: value is a tuple from join operations
|
|
135
|
+
valueId = getTupleId(value)
|
|
136
|
+
} else {
|
|
137
|
+
// Regular case: use reference/value equality
|
|
138
|
+
valueId = globalObjectIdGenerator.getStringId(value)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Create composite key and consolidate
|
|
142
|
+
const compositeKey = key + `|` + valueId
|
|
143
|
+
consolidated.set(
|
|
144
|
+
compositeKey,
|
|
145
|
+
(consolidated.get(compositeKey) || 0) + multiplicity
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
// Store the original data for the first occurrence
|
|
149
|
+
if (!values.has(compositeKey)) {
|
|
150
|
+
values.set(compositeKey, data as T)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Build result array, filtering out zero multiplicities
|
|
155
|
+
const result: MultiSetArray<T> = []
|
|
156
|
+
for (const [compositeKey, multiplicity] of consolidated) {
|
|
157
|
+
if (multiplicity !== 0) {
|
|
158
|
+
result.push([values.get(compositeKey)!, multiplicity])
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return new MultiSet(result)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Private method for consolidating unkeyed multisets using the original approach.
|
|
167
|
+
*/
|
|
168
|
+
#consolidateUnkeyed(): MultiSet<T> {
|
|
169
|
+
const consolidated = new DefaultMap<string | number, number>(() => 0)
|
|
170
|
+
const values = new Map<string, any>()
|
|
171
|
+
|
|
172
|
+
let hasString = false
|
|
173
|
+
let hasNumber = false
|
|
174
|
+
let hasOther = false
|
|
175
|
+
for (const [data, _] of this.#inner) {
|
|
176
|
+
if (typeof data === `string`) {
|
|
177
|
+
hasString = true
|
|
178
|
+
} else if (typeof data === `number`) {
|
|
179
|
+
hasNumber = true
|
|
180
|
+
} else {
|
|
181
|
+
hasOther = true
|
|
182
|
+
break
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const requireJson = hasOther || (hasString && hasNumber)
|
|
187
|
+
|
|
188
|
+
for (const [data, multiplicity] of this.#inner) {
|
|
189
|
+
const key = requireJson ? hash(data) : (data as string | number)
|
|
190
|
+
if (requireJson && !values.has(key as string)) {
|
|
191
|
+
values.set(key as string, data)
|
|
192
|
+
}
|
|
193
|
+
consolidated.update(key, (count) => count + multiplicity)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const result: MultiSetArray<T> = []
|
|
197
|
+
for (const [key, multiplicity] of consolidated.entries()) {
|
|
198
|
+
if (multiplicity !== 0) {
|
|
199
|
+
const parsedKey = requireJson ? values.get(key as string) : key
|
|
200
|
+
result.push([parsedKey as T, multiplicity])
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return new MultiSet(result)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
extend(other: MultiSet<T> | MultiSetArray<T>): void {
|
|
208
|
+
const otherArray = other instanceof MultiSet ? other.getInner() : other
|
|
209
|
+
chunkedArrayPush(this.#inner, otherArray)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
getInner(): MultiSetArray<T> {
|
|
213
|
+
return this.#inner
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { BinaryOperator, DifferenceStreamWriter } from "../graph.js"
|
|
2
|
+
import { StreamBuilder } from "../d2.js"
|
|
3
|
+
import type { IStreamBuilder, PipedOperator } from "../types.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Operator that concatenates two input streams
|
|
7
|
+
*/
|
|
8
|
+
export class ConcatOperator<T, T2> extends BinaryOperator<T | T2> {
|
|
9
|
+
run(): void {
|
|
10
|
+
for (const message of this.inputAMessages()) {
|
|
11
|
+
this.output.sendData(message)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
for (const message of this.inputBMessages()) {
|
|
15
|
+
this.output.sendData(message)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Concatenates two input streams
|
|
22
|
+
* @param other - The other stream to concatenate
|
|
23
|
+
*/
|
|
24
|
+
export function concat<T, T2>(
|
|
25
|
+
other: IStreamBuilder<T2>
|
|
26
|
+
): PipedOperator<T, T | T2> {
|
|
27
|
+
return (stream: IStreamBuilder<T | T2>): IStreamBuilder<T | T2> => {
|
|
28
|
+
if (stream.graph !== other.graph) {
|
|
29
|
+
throw new Error(`Cannot concat streams from different graphs`)
|
|
30
|
+
}
|
|
31
|
+
const output = new StreamBuilder<T | T2>(
|
|
32
|
+
stream.graph,
|
|
33
|
+
new DifferenceStreamWriter<T | T2>()
|
|
34
|
+
)
|
|
35
|
+
const operator = new ConcatOperator<T, T2>(
|
|
36
|
+
stream.graph.getNextOperatorId(),
|
|
37
|
+
stream.connectReader(),
|
|
38
|
+
other.connectReader(),
|
|
39
|
+
output.writer
|
|
40
|
+
)
|
|
41
|
+
stream.graph.addOperator(operator)
|
|
42
|
+
stream.graph.addStream(output.connectReader())
|
|
43
|
+
return output
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { DifferenceStreamWriter, UnaryOperator } from "../graph.js"
|
|
2
|
+
import { StreamBuilder } from "../d2.js"
|
|
3
|
+
import { MultiSet } from "../multiset.js"
|
|
4
|
+
import type { IStreamBuilder, PipedOperator } from "../types.js"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Operator that consolidates collections
|
|
8
|
+
*/
|
|
9
|
+
export class ConsolidateOperator<T> extends UnaryOperator<T> {
|
|
10
|
+
run(): void {
|
|
11
|
+
const messages = this.inputMessages()
|
|
12
|
+
if (messages.length === 0) {
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Combine all messages into a single MultiSet
|
|
17
|
+
const combined = new MultiSet<T>()
|
|
18
|
+
for (const message of messages) {
|
|
19
|
+
combined.extend(message)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Consolidate the combined MultiSet
|
|
23
|
+
const consolidated = combined.consolidate()
|
|
24
|
+
|
|
25
|
+
// Only send if there are results
|
|
26
|
+
if (consolidated.getInner().length > 0) {
|
|
27
|
+
this.output.sendData(consolidated)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Consolidates the elements in the stream
|
|
34
|
+
*/
|
|
35
|
+
export function consolidate<T>(): PipedOperator<T, T> {
|
|
36
|
+
return (stream: IStreamBuilder<T>): IStreamBuilder<T> => {
|
|
37
|
+
const output = new StreamBuilder<T>(
|
|
38
|
+
stream.graph,
|
|
39
|
+
new DifferenceStreamWriter<T>()
|
|
40
|
+
)
|
|
41
|
+
const operator = new ConsolidateOperator<T>(
|
|
42
|
+
stream.graph.getNextOperatorId(),
|
|
43
|
+
stream.connectReader(),
|
|
44
|
+
output.writer
|
|
45
|
+
)
|
|
46
|
+
stream.graph.addOperator(operator)
|
|
47
|
+
stream.graph.addStream(output.connectReader())
|
|
48
|
+
return output
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { DifferenceStreamWriter } from "../graph.js"
|
|
2
|
+
import { StreamBuilder } from "../d2.js"
|
|
3
|
+
import { ReduceOperator } from "./reduce.js"
|
|
4
|
+
import type { DifferenceStreamReader } from "../graph.js"
|
|
5
|
+
import type { IStreamBuilder, KeyValue } from "../types.js"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Operator that counts elements by key (version-free)
|
|
9
|
+
*/
|
|
10
|
+
export class CountOperator<K, V> extends ReduceOperator<K, V, number> {
|
|
11
|
+
constructor(
|
|
12
|
+
id: number,
|
|
13
|
+
inputA: DifferenceStreamReader<[K, V]>,
|
|
14
|
+
output: DifferenceStreamWriter<[K, number]>
|
|
15
|
+
) {
|
|
16
|
+
const countInner = (vals: Array<[V, number]>): Array<[number, number]> => {
|
|
17
|
+
let totalCount = 0
|
|
18
|
+
for (const [_, diff] of vals) {
|
|
19
|
+
totalCount += diff
|
|
20
|
+
}
|
|
21
|
+
return [[totalCount, 1]]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
super(id, inputA, output, countInner)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Counts the number of elements by key (version-free)
|
|
30
|
+
*/
|
|
31
|
+
export function count<
|
|
32
|
+
KType extends T extends KeyValue<infer K, infer _V> ? K : never,
|
|
33
|
+
VType extends T extends KeyValue<KType, infer V> ? V : never,
|
|
34
|
+
T,
|
|
35
|
+
>() {
|
|
36
|
+
return (
|
|
37
|
+
stream: IStreamBuilder<T>
|
|
38
|
+
): IStreamBuilder<KeyValue<KType, number>> => {
|
|
39
|
+
const output = new StreamBuilder<KeyValue<KType, number>>(
|
|
40
|
+
stream.graph,
|
|
41
|
+
new DifferenceStreamWriter<KeyValue<KType, number>>()
|
|
42
|
+
)
|
|
43
|
+
const operator = new CountOperator<KType, VType>(
|
|
44
|
+
stream.graph.getNextOperatorId(),
|
|
45
|
+
stream.connectReader() as DifferenceStreamReader<KeyValue<KType, VType>>,
|
|
46
|
+
output.writer
|
|
47
|
+
)
|
|
48
|
+
stream.graph.addOperator(operator)
|
|
49
|
+
stream.graph.addStream(output.connectReader())
|
|
50
|
+
return output
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { DifferenceStreamWriter, UnaryOperator } from "../graph.js"
|
|
2
|
+
import { StreamBuilder } from "../d2.js"
|
|
3
|
+
import type { IStreamBuilder, PipedOperator } from "../types.js"
|
|
4
|
+
import type { DifferenceStreamReader } from "../graph.js"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Operator that logs debug information about the stream
|
|
8
|
+
*/
|
|
9
|
+
export class DebugOperator<T> extends UnaryOperator<T> {
|
|
10
|
+
#name: string
|
|
11
|
+
#indent: boolean
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
id: number,
|
|
15
|
+
inputA: DifferenceStreamReader<T>,
|
|
16
|
+
output: DifferenceStreamWriter<T>,
|
|
17
|
+
name: string,
|
|
18
|
+
indent: boolean = false
|
|
19
|
+
) {
|
|
20
|
+
super(id, inputA, output)
|
|
21
|
+
this.#name = name
|
|
22
|
+
this.#indent = indent
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
run(): void {
|
|
26
|
+
for (const message of this.inputMessages()) {
|
|
27
|
+
console.log(`debug ${this.#name} data: ${message.toString(this.#indent)}`)
|
|
28
|
+
this.output.sendData(message)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Logs debug information about the stream using console.log
|
|
35
|
+
* @param name - The name to prefix debug messages with
|
|
36
|
+
* @param indent - Whether to indent the debug output
|
|
37
|
+
*/
|
|
38
|
+
export function debug<T>(
|
|
39
|
+
name: string,
|
|
40
|
+
indent: boolean = false
|
|
41
|
+
): PipedOperator<T, T> {
|
|
42
|
+
return (stream: IStreamBuilder<T>): IStreamBuilder<T> => {
|
|
43
|
+
const output = new StreamBuilder<T>(
|
|
44
|
+
stream.graph,
|
|
45
|
+
new DifferenceStreamWriter<T>()
|
|
46
|
+
)
|
|
47
|
+
const operator = new DebugOperator<T>(
|
|
48
|
+
stream.graph.getNextOperatorId(),
|
|
49
|
+
stream.connectReader(),
|
|
50
|
+
output.writer,
|
|
51
|
+
name,
|
|
52
|
+
indent
|
|
53
|
+
)
|
|
54
|
+
stream.graph.addOperator(operator)
|
|
55
|
+
stream.graph.addStream(output.connectReader())
|
|
56
|
+
return output
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { DifferenceStreamWriter, UnaryOperator } from "../graph.js"
|
|
2
|
+
import { StreamBuilder } from "../d2.js"
|
|
3
|
+
import { hash } from "../utils.js"
|
|
4
|
+
import { MultiSet } from "../multiset.js"
|
|
5
|
+
import type { DifferenceStreamReader } from "../graph.js"
|
|
6
|
+
import type { IStreamBuilder } from "../types.js"
|
|
7
|
+
|
|
8
|
+
type HashedValue = string
|
|
9
|
+
type Multiplicity = number
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Operator that removes duplicates
|
|
13
|
+
*/
|
|
14
|
+
export class DistinctOperator<T> extends UnaryOperator<T> {
|
|
15
|
+
#by: (value: T) => any
|
|
16
|
+
#values: Map<HashedValue, Multiplicity> // keeps track of the number of times each value has been seen
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
id: number,
|
|
20
|
+
input: DifferenceStreamReader<T>,
|
|
21
|
+
output: DifferenceStreamWriter<T>,
|
|
22
|
+
by: (value: T) => any = (value: T) => value
|
|
23
|
+
) {
|
|
24
|
+
super(id, input, output)
|
|
25
|
+
this.#by = by
|
|
26
|
+
this.#values = new Map()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
run(): void {
|
|
30
|
+
const updatedValues = new Map<HashedValue, [Multiplicity, T]>()
|
|
31
|
+
|
|
32
|
+
// Compute the new multiplicity for each value
|
|
33
|
+
for (const message of this.inputMessages()) {
|
|
34
|
+
for (const [value, diff] of message.getInner()) {
|
|
35
|
+
const hashedValue = hash(this.#by(value))
|
|
36
|
+
|
|
37
|
+
const oldMultiplicity =
|
|
38
|
+
updatedValues.get(hashedValue)?.[0] ??
|
|
39
|
+
this.#values.get(hashedValue) ??
|
|
40
|
+
0
|
|
41
|
+
const newMultiplicity = oldMultiplicity + diff
|
|
42
|
+
|
|
43
|
+
updatedValues.set(hashedValue, [newMultiplicity, value])
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const result: Array<[T, number]> = []
|
|
48
|
+
|
|
49
|
+
// Check which values became visible or disappeared
|
|
50
|
+
for (const [
|
|
51
|
+
hashedValue,
|
|
52
|
+
[newMultiplicity, value],
|
|
53
|
+
] of updatedValues.entries()) {
|
|
54
|
+
const oldMultiplicity = this.#values.get(hashedValue) ?? 0
|
|
55
|
+
|
|
56
|
+
if (newMultiplicity === 0) {
|
|
57
|
+
this.#values.delete(hashedValue)
|
|
58
|
+
} else {
|
|
59
|
+
this.#values.set(hashedValue, newMultiplicity)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (oldMultiplicity <= 0 && newMultiplicity > 0) {
|
|
63
|
+
// The value wasn't present in the stream
|
|
64
|
+
// but with this change it is now present in the stream
|
|
65
|
+
result.push([value, 1])
|
|
66
|
+
} else if (oldMultiplicity > 0 && newMultiplicity <= 0) {
|
|
67
|
+
// The value was present in the stream
|
|
68
|
+
// but with this change it is no longer present in the stream
|
|
69
|
+
result.push([value, -1])
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (result.length > 0) {
|
|
74
|
+
this.output.sendData(new MultiSet(result))
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Removes duplicate values
|
|
81
|
+
*/
|
|
82
|
+
export function distinct<T>(by: (value: T) => any = (value: T) => value) {
|
|
83
|
+
return (stream: IStreamBuilder<T>): IStreamBuilder<T> => {
|
|
84
|
+
const output = new StreamBuilder<T>(
|
|
85
|
+
stream.graph,
|
|
86
|
+
new DifferenceStreamWriter<T>()
|
|
87
|
+
)
|
|
88
|
+
const operator = new DistinctOperator<T>(
|
|
89
|
+
stream.graph.getNextOperatorId(),
|
|
90
|
+
stream.connectReader(),
|
|
91
|
+
output.writer,
|
|
92
|
+
by
|
|
93
|
+
)
|
|
94
|
+
stream.graph.addOperator(operator)
|
|
95
|
+
stream.graph.addStream(output.connectReader())
|
|
96
|
+
return output
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { DifferenceStreamWriter, LinearUnaryOperator } from "../graph.js"
|
|
2
|
+
import { StreamBuilder } from "../d2.js"
|
|
3
|
+
import type { IStreamBuilder, PipedOperator } from "../types.js"
|
|
4
|
+
import type { DifferenceStreamReader } from "../graph.js"
|
|
5
|
+
import type { MultiSet } from "../multiset.js"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Operator that filters elements from the input stream
|
|
9
|
+
*/
|
|
10
|
+
export class FilterOperator<T> extends LinearUnaryOperator<T, T> {
|
|
11
|
+
#f: (data: T) => boolean
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
id: number,
|
|
15
|
+
inputA: DifferenceStreamReader<T>,
|
|
16
|
+
output: DifferenceStreamWriter<T>,
|
|
17
|
+
f: (data: T) => boolean
|
|
18
|
+
) {
|
|
19
|
+
super(id, inputA, output)
|
|
20
|
+
this.#f = f
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
inner(collection: MultiSet<T>): MultiSet<T> {
|
|
24
|
+
return collection.filter(this.#f)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Filters elements from the input stream
|
|
30
|
+
* @param f - The predicate to filter elements
|
|
31
|
+
*/
|
|
32
|
+
export function filter<T>(f: (data: T) => boolean): PipedOperator<T, T> {
|
|
33
|
+
return (stream: IStreamBuilder<T>): IStreamBuilder<T> => {
|
|
34
|
+
const output = new StreamBuilder<T>(
|
|
35
|
+
stream.graph,
|
|
36
|
+
new DifferenceStreamWriter<T>()
|
|
37
|
+
)
|
|
38
|
+
const operator = new FilterOperator<T>(
|
|
39
|
+
stream.graph.getNextOperatorId(),
|
|
40
|
+
stream.connectReader(),
|
|
41
|
+
output.writer,
|
|
42
|
+
f
|
|
43
|
+
)
|
|
44
|
+
stream.graph.addOperator(operator)
|
|
45
|
+
stream.graph.addStream(output.connectReader())
|
|
46
|
+
return output
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { map } from "./map.js"
|
|
2
|
+
import { innerJoin } from "./join.js"
|
|
3
|
+
import { consolidate } from "./consolidate.js"
|
|
4
|
+
import type { KeyValue } from "../types.js"
|
|
5
|
+
import type { IStreamBuilder, PipedOperator } from "../types"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Filters the elements of a keyed stream, by keys of another stream.
|
|
9
|
+
* This allows you to build pipelies where you have multiple outputs that are related,
|
|
10
|
+
* such as a streams of issues and comments for a project.
|
|
11
|
+
*
|
|
12
|
+
* @param other - The other stream to filter by, which must have the same key type as the input stream
|
|
13
|
+
*/
|
|
14
|
+
export function filterBy<
|
|
15
|
+
K,
|
|
16
|
+
V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never,
|
|
17
|
+
T,
|
|
18
|
+
>(
|
|
19
|
+
other: IStreamBuilder<KeyValue<K, unknown>>
|
|
20
|
+
): PipedOperator<T, KeyValue<K, V1>> {
|
|
21
|
+
return (stream: IStreamBuilder<T>): IStreamBuilder<KeyValue<K, V1>> => {
|
|
22
|
+
const otherKeys = other.pipe(
|
|
23
|
+
map(([key, _]) => [key, null] as KeyValue<K, null>)
|
|
24
|
+
)
|
|
25
|
+
return stream.pipe(
|
|
26
|
+
innerJoin(otherKeys),
|
|
27
|
+
map(([key, [value, _]]) => [key, value] as KeyValue<K, V1>),
|
|
28
|
+
consolidate()
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
}
|