@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.
@@ -0,0 +1,354 @@
1
+ import { map } from "./map.js"
2
+ import { reduce } from "./reduce.js"
3
+ import type { IStreamBuilder, KeyValue } from "../types.js"
4
+
5
+ type GroupKey = Record<string, unknown>
6
+
7
+ type BasicAggregateFunction<T, R, V = unknown> = {
8
+ preMap: (data: T) => V
9
+ reduce: (values: Array<[V, number]>) => V
10
+ postMap?: (result: V) => R
11
+ }
12
+
13
+ type PipedAggregateFunction<T, R> = {
14
+ pipe: (stream: IStreamBuilder<T>) => IStreamBuilder<KeyValue<string, R>>
15
+ }
16
+
17
+ type AggregateFunction<T, R, V = unknown> =
18
+ | BasicAggregateFunction<T, R, V>
19
+ | PipedAggregateFunction<T, R>
20
+
21
+ type ExtractAggregateReturnType<T, A> =
22
+ A extends AggregateFunction<T, infer R, any> ? R : never
23
+
24
+ type AggregatesReturnType<T, A> = {
25
+ [K in keyof A]: ExtractAggregateReturnType<T, A[K]>
26
+ }
27
+
28
+ function isPipedAggregateFunction<T, R>(
29
+ aggregate: AggregateFunction<T, R>
30
+ ): aggregate is PipedAggregateFunction<T, R> {
31
+ return `pipe` in aggregate
32
+ }
33
+
34
+ /**
35
+ * Groups data by key and applies multiple aggregate operations
36
+ * @param keyExtractor Function to extract grouping key from data
37
+ * @param aggregates Object mapping aggregate names to aggregate functions
38
+ */
39
+ export function groupBy<
40
+ T,
41
+ K extends GroupKey,
42
+ A extends Record<string, AggregateFunction<T, any, any>>,
43
+ >(keyExtractor: (data: T) => K, aggregates: A = {} as A) {
44
+ type ResultType = K & AggregatesReturnType<T, A>
45
+
46
+ const basicAggregates = Object.fromEntries(
47
+ Object.entries(aggregates).filter(
48
+ ([_, aggregate]) => !isPipedAggregateFunction(aggregate)
49
+ )
50
+ ) as Record<string, BasicAggregateFunction<T, any, any>>
51
+
52
+ // @ts-expect-error - TODO: we don't use this yet, but we will
53
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
54
+ const pipedAggregates = Object.fromEntries(
55
+ Object.entries(aggregates).filter(([_, aggregate]) =>
56
+ isPipedAggregateFunction(aggregate)
57
+ )
58
+ ) as Record<string, PipedAggregateFunction<T, any>>
59
+
60
+ return (
61
+ stream: IStreamBuilder<T>
62
+ ): IStreamBuilder<KeyValue<string, ResultType>> => {
63
+ // Special key to store the original key object
64
+ const KEY_SENTINEL = `__original_key__`
65
+
66
+ // First map to extract keys and pre-aggregate values
67
+ const withKeysAndValues = stream.pipe(
68
+ map((data) => {
69
+ const key = keyExtractor(data)
70
+ const keyString = JSON.stringify(key)
71
+
72
+ // Create values object with pre-aggregated values
73
+ const values: Record<string, unknown> = {}
74
+
75
+ // Store the original key object
76
+ values[KEY_SENTINEL] = key
77
+
78
+ // Add pre-aggregated values
79
+ for (const [name, aggregate] of Object.entries(basicAggregates)) {
80
+ values[name] = aggregate.preMap(data)
81
+ }
82
+
83
+ return [keyString, values] as KeyValue<string, Record<string, unknown>>
84
+ })
85
+ )
86
+
87
+ // Then reduce to compute aggregates
88
+ const reduced = withKeysAndValues.pipe(
89
+ reduce((values) => {
90
+ // Calculate total multiplicity to check if the group should exist
91
+ let totalMultiplicity = 0
92
+ for (const [_, multiplicity] of values) {
93
+ totalMultiplicity += multiplicity
94
+ }
95
+
96
+ // If total multiplicity is 0 or negative, the group should be removed completely
97
+ if (totalMultiplicity <= 0) {
98
+ return []
99
+ }
100
+
101
+ const result: Record<string, unknown> = {}
102
+
103
+ // Get the original key from first value in group
104
+ const originalKey = values[0]?.[0]?.[KEY_SENTINEL]
105
+ result[KEY_SENTINEL] = originalKey
106
+
107
+ // Apply each aggregate function
108
+ for (const [name, aggregate] of Object.entries(basicAggregates)) {
109
+ const preValues = values.map(
110
+ ([v, m]) => [v[name], m] as [any, number]
111
+ )
112
+ result[name] = aggregate.reduce(preValues)
113
+ }
114
+
115
+ return [[result, 1]]
116
+ })
117
+ )
118
+
119
+ // Finally map to extract the key and include all values
120
+ return reduced.pipe(
121
+ map(([keyString, values]) => {
122
+ // Extract the original key
123
+ const key = values[KEY_SENTINEL] as K
124
+
125
+ // Create intermediate result with key values and aggregate results
126
+ const result: Record<string, unknown> = {}
127
+
128
+ // Add key properties to result
129
+ Object.assign(result, key)
130
+
131
+ // Apply postMap if provided
132
+ for (const [name, aggregate] of Object.entries(basicAggregates)) {
133
+ if (aggregate.postMap) {
134
+ result[name] = aggregate.postMap(values[name])
135
+ } else {
136
+ result[name] = values[name]
137
+ }
138
+ }
139
+
140
+ // Return with the string key instead of the object
141
+ return [keyString, result] as KeyValue<string, ResultType>
142
+ })
143
+ )
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Creates a sum aggregate function
149
+ */
150
+ export function sum<T>(
151
+ valueExtractor: (value: T) => number = (v) => v as unknown as number
152
+ ): AggregateFunction<T, number, number> {
153
+ return {
154
+ preMap: (data: T) => valueExtractor(data),
155
+ reduce: (values: Array<[number, number]>) => {
156
+ let total = 0
157
+ for (const [value, multiplicity] of values) {
158
+ total += value * multiplicity
159
+ }
160
+ return total
161
+ },
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Creates a count aggregate function
167
+ */
168
+ export function count<T>(): AggregateFunction<T, number, number> {
169
+ return {
170
+ preMap: () => 1,
171
+ reduce: (values: Array<[number, number]>) => {
172
+ let totalCount = 0
173
+ for (const [_, multiplicity] of values) {
174
+ totalCount += multiplicity
175
+ }
176
+ return totalCount
177
+ },
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Creates an average aggregate function
183
+ */
184
+ export function avg<T>(
185
+ valueExtractor: (value: T) => number = (v) => v as unknown as number
186
+ ): AggregateFunction<T, number, { sum: number; count: number }> {
187
+ return {
188
+ preMap: (data: T) => ({
189
+ sum: valueExtractor(data),
190
+ count: 0,
191
+ }),
192
+ reduce: (values: Array<[{ sum: number; count: number }, number]>) => {
193
+ let totalSum = 0
194
+ let totalCount = 0
195
+ for (const [value, multiplicity] of values) {
196
+ totalSum += value.sum * multiplicity
197
+ totalCount += multiplicity
198
+ }
199
+ return {
200
+ sum: totalSum,
201
+ count: totalCount,
202
+ }
203
+ },
204
+ postMap: (result: { sum: number; count: number }) => {
205
+ return result.sum / result.count
206
+ },
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Creates a min aggregate function that computes the minimum value in a group
212
+ * @param valueExtractor Function to extract a numeric value from each data entry
213
+ */
214
+ export function min<T>(
215
+ valueExtractor: (value: T) => number = (v) => v as unknown as number
216
+ ): AggregateFunction<T, number, number> {
217
+ return {
218
+ preMap: (data: T) => valueExtractor(data),
219
+ reduce: (values: Array<[number, number]>) => {
220
+ let minValue = Number.POSITIVE_INFINITY
221
+ for (const [value, _multiplicity] of values) {
222
+ if (value < minValue) {
223
+ minValue = value
224
+ }
225
+ }
226
+ return minValue === Number.POSITIVE_INFINITY ? 0 : minValue
227
+ },
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Creates a max aggregate function that computes the maximum value in a group
233
+ * @param valueExtractor Function to extract a numeric value from each data entry
234
+ */
235
+ export function max<T>(
236
+ valueExtractor: (value: T) => number = (v) => v as unknown as number
237
+ ): AggregateFunction<T, number, number> {
238
+ return {
239
+ preMap: (data: T) => valueExtractor(data),
240
+ reduce: (values: Array<[number, number]>) => {
241
+ let maxValue = Number.NEGATIVE_INFINITY
242
+ for (const [value, _multiplicity] of values) {
243
+ if (value > maxValue) {
244
+ maxValue = value
245
+ }
246
+ }
247
+ return maxValue === Number.NEGATIVE_INFINITY ? 0 : maxValue
248
+ },
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Creates a median aggregate function that computes the middle value in a sorted group
254
+ * If there's an even number of values, returns the average of the two middle values
255
+ * @param valueExtractor Function to extract a numeric value from each data entry
256
+ */
257
+ export function median<T>(
258
+ valueExtractor: (value: T) => number = (v) => v as unknown as number
259
+ ): AggregateFunction<T, number, Array<number>> {
260
+ return {
261
+ preMap: (data: T) => [valueExtractor(data)],
262
+ reduce: (values: Array<[Array<number>, number]>) => {
263
+ // Flatten all values, taking multiplicity into account
264
+ const allValues: Array<number> = []
265
+ for (const [valueArray, multiplicity] of values) {
266
+ for (const value of valueArray) {
267
+ // Add each value multiple times based on multiplicity
268
+ for (let i = 0; i < multiplicity; i++) {
269
+ allValues.push(value)
270
+ }
271
+ }
272
+ }
273
+
274
+ // Return empty array if no values
275
+ if (allValues.length === 0) {
276
+ return []
277
+ }
278
+
279
+ // Sort values
280
+ allValues.sort((a, b) => a - b)
281
+
282
+ return allValues
283
+ },
284
+ postMap: (result: Array<number>) => {
285
+ if (result.length === 0) return 0
286
+
287
+ const mid = Math.floor(result.length / 2)
288
+
289
+ // If even number of values, average the two middle values
290
+ if (result.length % 2 === 0) {
291
+ return (result[mid - 1]! + result[mid]!) / 2
292
+ }
293
+
294
+ // If odd number of values, return the middle value
295
+ return result[mid]!
296
+ },
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Creates a mode aggregate function that computes the most frequent value in a group
302
+ * If multiple values have the same highest frequency, returns the first one encountered
303
+ * @param valueExtractor Function to extract a value from each data entry
304
+ */
305
+ export function mode<T>(
306
+ valueExtractor: (value: T) => number = (v) => v as unknown as number
307
+ ): AggregateFunction<T, number, Map<number, number>> {
308
+ return {
309
+ preMap: (data: T) => {
310
+ const value = valueExtractor(data)
311
+ const frequencyMap = new Map<number, number>()
312
+ frequencyMap.set(value, 1)
313
+ return frequencyMap
314
+ },
315
+ reduce: (values: Array<[Map<number, number>, number]>) => {
316
+ // Combine all frequency maps
317
+ const combinedMap = new Map<number, number>()
318
+
319
+ for (const [frequencyMap, multiplicity] of values) {
320
+ for (const [value, frequencyCount] of frequencyMap.entries()) {
321
+ const currentCount = combinedMap.get(value) || 0
322
+ combinedMap.set(value, currentCount + frequencyCount * multiplicity)
323
+ }
324
+ }
325
+
326
+ return combinedMap
327
+ },
328
+ postMap: (result: Map<number, number>) => {
329
+ if (result.size === 0) return 0
330
+
331
+ let modeValue = 0
332
+ let maxFrequency = 0
333
+
334
+ for (const [value, frequency] of result.entries()) {
335
+ if (frequency > maxFrequency) {
336
+ maxFrequency = frequency
337
+ modeValue = value
338
+ }
339
+ }
340
+
341
+ return modeValue
342
+ },
343
+ }
344
+ }
345
+
346
+ export const groupByOperators = {
347
+ sum,
348
+ count,
349
+ avg,
350
+ min,
351
+ max,
352
+ median,
353
+ mode,
354
+ }
@@ -0,0 +1,18 @@
1
+ export * from "./pipe.js"
2
+ export * from "./map.js"
3
+ export * from "./filter.js"
4
+ export * from "./negate.js"
5
+ export * from "./concat.js"
6
+ export * from "./debug.js"
7
+ export * from "./output.js"
8
+ export * from "./consolidate.js"
9
+ export * from "./join.js"
10
+ export * from "./reduce.js"
11
+ export * from "./count.js"
12
+ export * from "./distinct.js"
13
+ export * from "./keying.js"
14
+ export * from "./topK.js"
15
+ export * from "./topKWithFractionalIndex.js"
16
+ export * from "./orderBy.js"
17
+ export * from "./filterBy.js"
18
+ export { groupBy, groupByOperators } from "./groupBy.js"
@@ -0,0 +1,265 @@
1
+ import { BinaryOperator, DifferenceStreamWriter } from "../graph.js"
2
+ import { StreamBuilder } from "../d2.js"
3
+ import { MultiSet } from "../multiset.js"
4
+ import { Index } from "../indexes.js"
5
+ import { negate } from "./negate.js"
6
+ import { map } from "./map.js"
7
+ import { concat } from "./concat.js"
8
+ import type { DifferenceStreamReader } from "../graph.js"
9
+ import type { IStreamBuilder, KeyValue, PipedOperator } from "../types.js"
10
+
11
+ /**
12
+ * Type of join to perform
13
+ */
14
+ export type JoinType = `inner` | `left` | `right` | `full` | `anti`
15
+
16
+ /**
17
+ * Operator that joins two input streams
18
+ */
19
+ export class JoinOperator<K, V1, V2> extends BinaryOperator<
20
+ [K, V1] | [K, V2] | [K, [V1, V2]]
21
+ > {
22
+ #indexA = new Index<K, V1>()
23
+ #indexB = new Index<K, V2>()
24
+
25
+ constructor(
26
+ id: number,
27
+ inputA: DifferenceStreamReader<[K, V1]>,
28
+ inputB: DifferenceStreamReader<[K, V2]>,
29
+ output: DifferenceStreamWriter<[K, [V1, V2]]>
30
+ ) {
31
+ super(id, inputA, inputB, output)
32
+ }
33
+
34
+ run(): void {
35
+ const deltaA = new Index<K, V1>()
36
+ const deltaB = new Index<K, V2>()
37
+
38
+ // Process input A - process ALL messages, not just the first one
39
+ const messagesA = this.inputAMessages()
40
+ for (const message of messagesA) {
41
+ const multiSetMessage = message as unknown as MultiSet<[K, V1]>
42
+ for (const [item, multiplicity] of multiSetMessage.getInner()) {
43
+ const [key, value] = item
44
+ deltaA.addValue(key, [value, multiplicity])
45
+ }
46
+ }
47
+
48
+ // Process input B - process ALL messages, not just the first one
49
+ const messagesB = this.inputBMessages()
50
+ for (const message of messagesB) {
51
+ const multiSetMessage = message as unknown as MultiSet<[K, V2]>
52
+ for (const [item, multiplicity] of multiSetMessage.getInner()) {
53
+ const [key, value] = item
54
+ deltaB.addValue(key, [value, multiplicity])
55
+ }
56
+ }
57
+
58
+ // Process results
59
+ const results = new MultiSet<[K, [V1, V2]]>()
60
+
61
+ // Join deltaA with existing indexB
62
+ results.extend(deltaA.join(this.#indexB))
63
+
64
+ // Append deltaA to indexA
65
+ this.#indexA.append(deltaA)
66
+
67
+ // Join existing indexA with deltaB
68
+ results.extend(this.#indexA.join(deltaB))
69
+
70
+ // Send results
71
+ if (results.getInner().length > 0) {
72
+ this.output.sendData(results)
73
+ }
74
+
75
+ // Append deltaB to indexB
76
+ this.#indexB.append(deltaB)
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Joins two input streams
82
+ * @param other - The other stream to join with
83
+ * @param type - The type of join to perform
84
+ */
85
+ export function join<
86
+ K,
87
+ V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never,
88
+ V2,
89
+ T,
90
+ >(
91
+ other: IStreamBuilder<KeyValue<K, V2>>,
92
+ type: JoinType = `inner`
93
+ ): PipedOperator<T, KeyValue<K, [V1 | null, V2 | null]>> {
94
+ switch (type) {
95
+ case `inner`:
96
+ return innerJoin(other) as unknown as PipedOperator<
97
+ T,
98
+ KeyValue<K, [V1, V2]>
99
+ >
100
+ case `anti`:
101
+ return antiJoin(other) as unknown as PipedOperator<
102
+ T,
103
+ KeyValue<K, [V1, null]>
104
+ >
105
+ case `left`:
106
+ return leftJoin(other) as unknown as PipedOperator<
107
+ T,
108
+ KeyValue<K, [V1, V2 | null]>
109
+ >
110
+ case `right`:
111
+ return rightJoin(other) as unknown as PipedOperator<
112
+ T,
113
+ KeyValue<K, [V1 | null, V2]>
114
+ >
115
+ case `full`:
116
+ return fullJoin(other) as unknown as PipedOperator<
117
+ T,
118
+ KeyValue<K, [V1 | null, V2 | null]>
119
+ >
120
+ default:
121
+ throw new Error(`Join type ${type} is invalid`)
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Joins two input streams
127
+ * @param other - The other stream to join with
128
+ */
129
+ export function innerJoin<
130
+ K,
131
+ V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never,
132
+ V2,
133
+ T,
134
+ >(
135
+ other: IStreamBuilder<KeyValue<K, V2>>
136
+ ): PipedOperator<T, KeyValue<K, [V1, V2]>> {
137
+ return (stream: IStreamBuilder<T>): IStreamBuilder<KeyValue<K, [V1, V2]>> => {
138
+ if (stream.graph !== other.graph) {
139
+ throw new Error(`Cannot join streams from different graphs`)
140
+ }
141
+ const output = new StreamBuilder<KeyValue<K, [V1, V2]>>(
142
+ stream.graph,
143
+ new DifferenceStreamWriter<KeyValue<K, [V1, V2]>>()
144
+ )
145
+ const operator = new JoinOperator<K, V1, V2>(
146
+ stream.graph.getNextOperatorId(),
147
+ stream.connectReader() as DifferenceStreamReader<KeyValue<K, V1>>,
148
+ other.connectReader(),
149
+ output.writer
150
+ )
151
+ stream.graph.addOperator(operator)
152
+ stream.graph.addStream(output.connectReader())
153
+ return output
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Joins two input streams
159
+ * @param other - The other stream to join with
160
+ */
161
+ export function antiJoin<
162
+ K,
163
+ V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never,
164
+ V2,
165
+ T,
166
+ >(
167
+ other: IStreamBuilder<KeyValue<K, V2>>
168
+ ): PipedOperator<T, KeyValue<K, [V1, null]>> {
169
+ return (
170
+ stream: IStreamBuilder<T>
171
+ ): IStreamBuilder<KeyValue<K, [V1, null]>> => {
172
+ const matchedLeft = stream.pipe(
173
+ innerJoin(other),
174
+ map(([key, [valueLeft, _valueRight]]) => [key, valueLeft])
175
+ )
176
+ const anti = stream.pipe(
177
+ concat(matchedLeft.pipe(negate())),
178
+ // @ts-ignore TODO: fix this
179
+ map(([key, value]) => [key, [value, null]])
180
+ )
181
+ return anti as IStreamBuilder<KeyValue<K, [V1, null]>>
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Joins two input streams
187
+ * @param other - The other stream to join with
188
+ */
189
+ export function leftJoin<
190
+ K,
191
+ V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never,
192
+ V2,
193
+ T,
194
+ >(
195
+ other: IStreamBuilder<KeyValue<K, V2>>
196
+ ): PipedOperator<T, KeyValue<K, [V1, V2 | null]>> {
197
+ return (
198
+ stream: IStreamBuilder<T>
199
+ ): IStreamBuilder<KeyValue<K, [V1, V2 | null]>> => {
200
+ const left = stream
201
+ const right = other
202
+ const inner = left.pipe(innerJoin(right))
203
+ const anti = left.pipe(antiJoin(right))
204
+ return inner.pipe(concat(anti)) as IStreamBuilder<
205
+ KeyValue<K, [V1, V2 | null]>
206
+ >
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Joins two input streams
212
+ * @param other - The other stream to join with
213
+ */
214
+ export function rightJoin<
215
+ K,
216
+ V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never,
217
+ V2,
218
+ T,
219
+ >(
220
+ other: IStreamBuilder<KeyValue<K, V2>>
221
+ ): PipedOperator<T, KeyValue<K, [V1 | null, V2]>> {
222
+ return (
223
+ stream: IStreamBuilder<T>
224
+ ): IStreamBuilder<KeyValue<K, [V1 | null, V2]>> => {
225
+ const left = stream as IStreamBuilder<KeyValue<K, V1>>
226
+ const right = other
227
+ const inner = left.pipe(innerJoin(right))
228
+ const anti = right.pipe(
229
+ antiJoin(left),
230
+ map(([key, [a, b]]) => [key, [b, a]])
231
+ )
232
+ return inner.pipe(concat(anti)) as IStreamBuilder<
233
+ KeyValue<K, [V1 | null, V2]>
234
+ >
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Joins two input streams
240
+ * @param other - The other stream to join with
241
+ */
242
+ export function fullJoin<
243
+ K,
244
+ V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never,
245
+ V2,
246
+ T,
247
+ >(
248
+ other: IStreamBuilder<KeyValue<K, V2>>
249
+ ): PipedOperator<T, KeyValue<K, [V1 | null, V2 | null]>> {
250
+ return (
251
+ stream: IStreamBuilder<T>
252
+ ): IStreamBuilder<KeyValue<K, [V1 | null, V2 | null]>> => {
253
+ const left = stream as IStreamBuilder<KeyValue<K, V1>>
254
+ const right = other
255
+ const inner = left.pipe(innerJoin(right))
256
+ const antiLeft = left.pipe(antiJoin(right))
257
+ const antiRight = right.pipe(
258
+ antiJoin(left),
259
+ map(([key, [a, b]]) => [key, [b, a]])
260
+ )
261
+ return inner.pipe(concat(antiLeft), concat(antiRight)) as IStreamBuilder<
262
+ KeyValue<K, [V1 | null, V2 | null]>
263
+ >
264
+ }
265
+ }
@@ -0,0 +1,34 @@
1
+ import { map } from "./map.js"
2
+ import type { PipedOperator } from "../types.js"
3
+
4
+ /**
5
+ * Type for a keyed value
6
+ */
7
+ export type Keyed<K, V> = [K, V]
8
+
9
+ /**
10
+ * Takes an unkeyed input and returns a keyed result
11
+ * @param keyFn - Function to generate the key for each value
12
+ */
13
+ export function keyBy<T, K>(
14
+ keyFn: (value: T) => K
15
+ ): PipedOperator<T, Keyed<K, T>> {
16
+ return map((value: T): Keyed<K, T> => [keyFn(value), value])
17
+ }
18
+
19
+ /**
20
+ * Removes the key from a keyed stream
21
+ */
22
+ export function unkey<K, V>(): PipedOperator<Keyed<K, V>, V> {
23
+ return map(([_, value]: Keyed<K, V>): V => value)
24
+ }
25
+
26
+ /**
27
+ * Takes a keyed input and rekeys it with a new key
28
+ * @param keyFn - Function to generate the new key for each value
29
+ */
30
+ export function rekey<K1, K2, V>(
31
+ keyFn: (value: V) => K2
32
+ ): PipedOperator<Keyed<K1, V>, Keyed<K2, V>> {
33
+ return map(([_, value]: Keyed<K1, V>): Keyed<K2, V> => [keyFn(value), value])
34
+ }