@tanstack/db-ivm 0.1.25 → 0.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.
Files changed (151) hide show
  1. package/README.md +5 -6
  2. package/dist/cjs/d2.d.cts +3 -3
  3. package/dist/cjs/graph.d.cts +2 -2
  4. package/dist/cjs/hashing/hash.cjs +58 -3
  5. package/dist/cjs/hashing/hash.cjs.map +1 -1
  6. package/dist/cjs/hashing/hash.d.cts +9 -0
  7. package/dist/cjs/hashing/index.d.cts +2 -2
  8. package/dist/cjs/hashing/murmur.cjs +26 -15
  9. package/dist/cjs/hashing/murmur.cjs.map +1 -1
  10. package/dist/cjs/hashing/murmur.d.cts +2 -0
  11. package/dist/cjs/index.cjs +25 -51
  12. package/dist/cjs/index.cjs.map +1 -1
  13. package/dist/cjs/index.d.cts +6 -6
  14. package/dist/cjs/indexes.cjs +52 -16
  15. package/dist/cjs/indexes.cjs.map +1 -1
  16. package/dist/cjs/indexes.d.cts +42 -9
  17. package/dist/cjs/multiset.d.cts +8 -2
  18. package/dist/cjs/operators/concat.d.cts +2 -2
  19. package/dist/cjs/operators/consolidate.d.cts +2 -2
  20. package/dist/cjs/operators/distinct.d.cts +2 -2
  21. package/dist/cjs/operators/filter.d.cts +3 -3
  22. package/dist/cjs/operators/groupBy.cjs +33 -36
  23. package/dist/cjs/operators/groupBy.cjs.map +1 -1
  24. package/dist/cjs/operators/groupBy.d.cts +6 -7
  25. package/dist/cjs/operators/groupedOrderBy.d.cts +8 -2
  26. package/dist/cjs/operators/groupedTopKWithFractionalIndex.cjs +3 -9
  27. package/dist/cjs/operators/groupedTopKWithFractionalIndex.cjs.map +1 -1
  28. package/dist/cjs/operators/groupedTopKWithFractionalIndex.d.cts +24 -11
  29. package/dist/cjs/operators/index.d.cts +15 -21
  30. package/dist/cjs/operators/join.cjs +0 -20
  31. package/dist/cjs/operators/join.cjs.map +1 -1
  32. package/dist/cjs/operators/join.d.cts +29 -30
  33. package/dist/cjs/operators/map.d.cts +3 -3
  34. package/dist/cjs/operators/orderBy.cjs +2 -77
  35. package/dist/cjs/operators/orderBy.cjs.map +1 -1
  36. package/dist/cjs/operators/orderBy.d.cts +10 -24
  37. package/dist/cjs/operators/output.d.cts +3 -3
  38. package/dist/cjs/operators/pipe.d.cts +1 -1
  39. package/dist/cjs/operators/reduce.cjs +36 -5
  40. package/dist/cjs/operators/reduce.cjs.map +1 -1
  41. package/dist/cjs/operators/reduce.d.cts +38 -5
  42. package/dist/cjs/operators/tap.d.cts +3 -3
  43. package/dist/cjs/operators/topKArray.cjs +9 -0
  44. package/dist/cjs/operators/topKArray.cjs.map +1 -1
  45. package/dist/cjs/operators/topKArray.d.cts +17 -2
  46. package/dist/cjs/operators/topKState.cjs +3 -0
  47. package/dist/cjs/operators/topKState.cjs.map +1 -1
  48. package/dist/cjs/operators/topKState.d.cts +45 -8
  49. package/dist/cjs/operators/topKWithFractionalIndex.cjs +5 -9
  50. package/dist/cjs/operators/topKWithFractionalIndex.cjs.map +1 -1
  51. package/dist/cjs/operators/topKWithFractionalIndex.d.cts +37 -9
  52. package/dist/cjs/types.d.cts +6 -3
  53. package/dist/cjs/utils.cjs.map +1 -1
  54. package/dist/cjs/utils.d.cts +4 -8
  55. package/dist/esm/hashing/hash.d.ts +9 -0
  56. package/dist/esm/hashing/hash.js +60 -5
  57. package/dist/esm/hashing/hash.js.map +1 -1
  58. package/dist/esm/hashing/murmur.d.ts +2 -0
  59. package/dist/esm/hashing/murmur.js +26 -15
  60. package/dist/esm/hashing/murmur.js.map +1 -1
  61. package/dist/esm/index.js +11 -37
  62. package/dist/esm/index.js.map +1 -1
  63. package/dist/esm/indexes.d.ts +4 -1
  64. package/dist/esm/indexes.js +52 -16
  65. package/dist/esm/indexes.js.map +1 -1
  66. package/dist/esm/operators/groupBy.d.ts +1 -5
  67. package/dist/esm/operators/groupBy.js +33 -36
  68. package/dist/esm/operators/groupBy.js.map +1 -1
  69. package/dist/esm/operators/groupedTopKWithFractionalIndex.d.ts +1 -6
  70. package/dist/esm/operators/groupedTopKWithFractionalIndex.js +3 -9
  71. package/dist/esm/operators/groupedTopKWithFractionalIndex.js.map +1 -1
  72. package/dist/esm/operators/index.d.ts +0 -6
  73. package/dist/esm/operators/join.d.ts +1 -26
  74. package/dist/esm/operators/join.js +1 -21
  75. package/dist/esm/operators/join.js.map +1 -1
  76. package/dist/esm/operators/orderBy.d.ts +2 -22
  77. package/dist/esm/operators/orderBy.js +3 -78
  78. package/dist/esm/operators/orderBy.js.map +1 -1
  79. package/dist/esm/operators/reduce.d.ts +11 -2
  80. package/dist/esm/operators/reduce.js +36 -5
  81. package/dist/esm/operators/reduce.js.map +1 -1
  82. package/dist/esm/operators/topKArray.d.ts +6 -0
  83. package/dist/esm/operators/topKArray.js +10 -1
  84. package/dist/esm/operators/topKArray.js.map +1 -1
  85. package/dist/esm/operators/topKState.d.ts +1 -0
  86. package/dist/esm/operators/topKState.js +3 -0
  87. package/dist/esm/operators/topKState.js.map +1 -1
  88. package/dist/esm/operators/topKWithFractionalIndex.d.ts +7 -3
  89. package/dist/esm/operators/topKWithFractionalIndex.js +6 -10
  90. package/dist/esm/operators/topKWithFractionalIndex.js.map +1 -1
  91. package/dist/esm/utils.d.ts +0 -7
  92. package/dist/esm/utils.js.map +1 -1
  93. package/package.json +3 -5
  94. package/src/hashing/hash.ts +87 -5
  95. package/src/hashing/murmur.ts +31 -17
  96. package/src/indexes.ts +65 -16
  97. package/src/operators/groupBy.ts +48 -59
  98. package/src/operators/groupedTopKWithFractionalIndex.ts +4 -15
  99. package/src/operators/index.ts +0 -6
  100. package/src/operators/join.ts +1 -91
  101. package/src/operators/orderBy.ts +6 -141
  102. package/src/operators/reduce.ts +62 -5
  103. package/src/operators/topKArray.ts +15 -0
  104. package/src/operators/topKState.ts +7 -0
  105. package/src/operators/topKWithFractionalIndex.ts +15 -16
  106. package/src/utils.ts +0 -30
  107. package/dist/cjs/operators/count.cjs +0 -35
  108. package/dist/cjs/operators/count.cjs.map +0 -1
  109. package/dist/cjs/operators/count.d.cts +0 -13
  110. package/dist/cjs/operators/debug.cjs +0 -39
  111. package/dist/cjs/operators/debug.cjs.map +0 -1
  112. package/dist/cjs/operators/debug.d.cts +0 -16
  113. package/dist/cjs/operators/filterBy.cjs +0 -19
  114. package/dist/cjs/operators/filterBy.cjs.map +0 -1
  115. package/dist/cjs/operators/filterBy.d.cts +0 -9
  116. package/dist/cjs/operators/keying.cjs +0 -16
  117. package/dist/cjs/operators/keying.cjs.map +0 -1
  118. package/dist/cjs/operators/keying.d.cts +0 -19
  119. package/dist/cjs/operators/negate.cjs +0 -27
  120. package/dist/cjs/operators/negate.cjs.map +0 -1
  121. package/dist/cjs/operators/negate.d.cts +0 -13
  122. package/dist/cjs/operators/topK.cjs +0 -39
  123. package/dist/cjs/operators/topK.cjs.map +0 -1
  124. package/dist/cjs/operators/topK.d.cts +0 -31
  125. package/dist/cjs/operators/topKWithFractionalIndexBTree.d.cts +0 -28
  126. package/dist/esm/operators/count.d.ts +0 -13
  127. package/dist/esm/operators/count.js +0 -35
  128. package/dist/esm/operators/count.js.map +0 -1
  129. package/dist/esm/operators/debug.d.ts +0 -16
  130. package/dist/esm/operators/debug.js +0 -39
  131. package/dist/esm/operators/debug.js.map +0 -1
  132. package/dist/esm/operators/filterBy.d.ts +0 -9
  133. package/dist/esm/operators/filterBy.js +0 -19
  134. package/dist/esm/operators/filterBy.js.map +0 -1
  135. package/dist/esm/operators/keying.d.ts +0 -19
  136. package/dist/esm/operators/keying.js +0 -16
  137. package/dist/esm/operators/keying.js.map +0 -1
  138. package/dist/esm/operators/negate.d.ts +0 -13
  139. package/dist/esm/operators/negate.js +0 -27
  140. package/dist/esm/operators/negate.js.map +0 -1
  141. package/dist/esm/operators/topK.d.ts +0 -31
  142. package/dist/esm/operators/topK.js +0 -39
  143. package/dist/esm/operators/topK.js.map +0 -1
  144. package/dist/esm/operators/topKWithFractionalIndexBTree.d.ts +0 -28
  145. package/src/operators/count.ts +0 -51
  146. package/src/operators/debug.ts +0 -57
  147. package/src/operators/filterBy.ts +0 -30
  148. package/src/operators/keying.ts +0 -34
  149. package/src/operators/negate.ts +0 -32
  150. package/src/operators/topK.ts +0 -91
  151. package/src/operators/topKWithFractionalIndexBTree.ts +0 -297
@@ -1,61 +1,58 @@
1
+ import { structuralIdentity } from "../hashing/hash.js";
1
2
  import { serializeValue } from "../utils.js";
2
3
  import { map } from "./map.js";
3
4
  import { reduce } from "./reduce.js";
4
- function isPipedAggregateFunction(aggregate) {
5
- return `pipe` in aggregate;
6
- }
7
5
  function groupBy(keyExtractor, aggregates = {}) {
8
- const basicAggregates = Object.fromEntries(
9
- Object.entries(aggregates).filter(
10
- ([_, aggregate]) => !isPipedAggregateFunction(aggregate)
11
- )
12
- );
13
- Object.fromEntries(
14
- Object.entries(aggregates).filter(
15
- ([_, aggregate]) => isPipedAggregateFunction(aggregate)
16
- )
17
- );
18
6
  return (stream) => {
19
7
  const KEY_SENTINEL = /* @__PURE__ */ Symbol(`original_group_key`);
8
+ const aggregateEntries = Object.entries(aggregates);
20
9
  const withKeysAndValues = stream.pipe(
21
10
  map((data) => {
22
11
  const key = keyExtractor(data);
23
12
  const keyString = serializeValue(key);
24
13
  const values = {};
25
14
  values[KEY_SENTINEL] = key;
26
- for (const [name, aggregate] of Object.entries(basicAggregates)) {
27
- values[name] = aggregate.preMap(data);
15
+ let identity = structuralIdentity(key) ?? null;
16
+ for (const [name, aggregate] of aggregateEntries) {
17
+ const value = aggregate.preMap(data);
18
+ values[name] = value;
19
+ if (identity === null) continue;
20
+ const part = structuralIdentity(value);
21
+ identity = part === void 0 ? null : `${identity}|${part}`;
28
22
  }
29
- return [keyString, values];
23
+ return [keyString, [identity, values]];
30
24
  })
31
25
  );
32
26
  const reduced = withKeysAndValues.pipe(
33
- reduce((values) => {
34
- let totalMultiplicity = 0;
35
- for (const [_, multiplicity] of values) {
36
- totalMultiplicity += multiplicity;
37
- }
38
- if (totalMultiplicity <= 0) {
39
- return [];
40
- }
41
- const result = {};
42
- const originalKey = values[0]?.[0]?.[KEY_SENTINEL];
43
- result[KEY_SENTINEL] = originalKey;
44
- for (const [name, aggregate] of Object.entries(basicAggregates)) {
45
- const preValues = values.map(
46
- ([v, m]) => [v[name], m]
47
- );
48
- result[name] = aggregate.reduce(preValues);
49
- }
50
- return [[result, 1]];
51
- })
27
+ reduce(
28
+ (contributions) => {
29
+ let totalMultiplicity = 0;
30
+ for (const [_, multiplicity] of contributions) {
31
+ totalMultiplicity += multiplicity;
32
+ }
33
+ if (totalMultiplicity <= 0) {
34
+ return [];
35
+ }
36
+ const result = {};
37
+ const originalKey = contributions[0]?.[0][1][KEY_SENTINEL];
38
+ result[KEY_SENTINEL] = originalKey;
39
+ for (const [name, aggregate] of aggregateEntries) {
40
+ const preValues = contributions.map(
41
+ ([[, v], m]) => [v[name], m]
42
+ );
43
+ result[name] = aggregate.reduce(preValues);
44
+ }
45
+ return [[result, 1]];
46
+ },
47
+ { prefixIdentity: true }
48
+ )
52
49
  );
53
50
  return reduced.pipe(
54
51
  map(([keyString, values]) => {
55
52
  const key = values[KEY_SENTINEL];
56
53
  const result = {};
57
54
  Object.assign(result, key);
58
- for (const [name, aggregate] of Object.entries(basicAggregates)) {
55
+ for (const [name, aggregate] of aggregateEntries) {
59
56
  if (aggregate.postMap) {
60
57
  result[name] = aggregate.postMap(values[name]);
61
58
  } else {
@@ -1 +1 @@
1
- {"version":3,"file":"groupBy.js","sources":["../../../src/operators/groupBy.ts"],"sourcesContent":["import { serializeValue } from '../utils.js'\nimport { map } from './map.js'\nimport { reduce } from './reduce.js'\nimport type { IStreamBuilder, KeyValue } from '../types.js'\n\ntype GroupKey = Record<string, unknown>\n\ntype BasicAggregateFunction<T, R, V = unknown> = {\n preMap: (data: T) => V\n reduce: (values: Array<[V, number]>) => V\n postMap?: (result: V) => R\n}\n\ntype PipedAggregateFunction<T, R> = {\n pipe: (stream: IStreamBuilder<T>) => IStreamBuilder<KeyValue<string, R>>\n}\n\ntype AggregateFunction<T, R, V = unknown> =\n | BasicAggregateFunction<T, R, V>\n | PipedAggregateFunction<T, R>\n\ntype ExtractAggregateReturnType<T, A> =\n A extends AggregateFunction<T, infer R, any> ? R : never\n\ntype AggregatesReturnType<T, A> = {\n [K in keyof A]: ExtractAggregateReturnType<T, A[K]>\n}\n\nfunction isPipedAggregateFunction<T, R>(\n aggregate: AggregateFunction<T, R>,\n): aggregate is PipedAggregateFunction<T, R> {\n return `pipe` in aggregate\n}\n\n/**\n * Groups data by key and applies multiple aggregate operations\n * @param keyExtractor Function to extract grouping key from data\n * @param aggregates Object mapping aggregate names to aggregate functions\n */\nexport function groupBy<\n T,\n K extends GroupKey,\n A extends Record<string, AggregateFunction<T, any, any>>,\n>(keyExtractor: (data: T) => K, aggregates: A = {} as A) {\n type ResultType = K & AggregatesReturnType<T, A>\n\n const basicAggregates = Object.fromEntries(\n Object.entries(aggregates).filter(\n ([_, aggregate]) => !isPipedAggregateFunction(aggregate),\n ),\n ) as Record<string, BasicAggregateFunction<T, any, any>>\n\n // @ts-expect-error - TODO: we don't use this yet, but we will\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const pipedAggregates = Object.fromEntries(\n Object.entries(aggregates).filter(([_, aggregate]) =>\n isPipedAggregateFunction(aggregate),\n ),\n ) as Record<string, PipedAggregateFunction<T, any>>\n\n return (\n stream: IStreamBuilder<T>,\n ): IStreamBuilder<KeyValue<string, ResultType>> => {\n // Special key to store the original key object\n const KEY_SENTINEL = Symbol(`original_group_key`)\n\n // First map to extract keys and pre-aggregate values\n const withKeysAndValues = stream.pipe(\n map((data) => {\n const key = keyExtractor(data)\n const keyString = serializeValue(key)\n\n // Create values object with pre-aggregated values\n const values: Record<string | symbol, unknown> = {}\n\n // Store the original key object\n values[KEY_SENTINEL] = key\n\n // Add pre-aggregated values\n for (const [name, aggregate] of Object.entries(basicAggregates)) {\n values[name] = aggregate.preMap(data)\n }\n\n return [keyString, values] as KeyValue<\n string,\n Record<string | symbol, unknown>\n >\n }),\n )\n\n // Then reduce to compute aggregates\n const reduced = withKeysAndValues.pipe(\n reduce((values) => {\n // Calculate total multiplicity to check if the group should exist\n let totalMultiplicity = 0\n for (const [_, multiplicity] of values) {\n totalMultiplicity += multiplicity\n }\n\n // If total multiplicity is 0 or negative, the group should be removed completely\n if (totalMultiplicity <= 0) {\n return []\n }\n\n const result: Record<PropertyKey, unknown> = {}\n\n // Get the original key from first value in group\n const originalKey = values[0]?.[0]?.[KEY_SENTINEL]\n result[KEY_SENTINEL] = originalKey\n\n // Apply each aggregate function\n for (const [name, aggregate] of Object.entries(basicAggregates)) {\n const preValues = values.map(\n ([v, m]) => [v[name], m] as [any, number],\n )\n result[name] = aggregate.reduce(preValues)\n }\n\n return [[result, 1]]\n }),\n )\n\n // Finally map to extract the key and include all values\n return reduced.pipe(\n map(([keyString, values]) => {\n // Extract the original key\n const key = values[KEY_SENTINEL] as K\n\n // Create intermediate result with key values and aggregate results\n const result: Record<string, unknown> = {}\n\n // Add key properties to result\n Object.assign(result, key)\n\n // Apply postMap if provided\n for (const [name, aggregate] of Object.entries(basicAggregates)) {\n if (aggregate.postMap) {\n result[name] = aggregate.postMap(values[name])\n } else {\n result[name] = values[name]\n }\n }\n\n // Return with the string key instead of the object\n return [keyString, result] as KeyValue<string, ResultType>\n }),\n )\n }\n}\n\n/**\n * Creates a sum aggregate function\n */\nexport function sum<T>(\n valueExtractor: (value: T) => number = (v) => v as unknown as number,\n): AggregateFunction<T, number, number> {\n return {\n preMap: (data: T) => valueExtractor(data),\n reduce: (values: Array<[number, number]>) => {\n let total = 0\n for (const [value, multiplicity] of values) {\n total += value * multiplicity\n }\n return total\n },\n }\n}\n\n/**\n * Creates a count aggregate function\n */\nexport function count<T>(\n valueExtractor: (value: T) => any = (v) => v,\n): AggregateFunction<T, number, number> {\n return {\n // Count only not-null values (the `== null` comparison gives true for both null and undefined)\n preMap: (data: T) => (valueExtractor(data) == null ? 0 : 1),\n reduce: (values: Array<[number, number]>) => {\n let totalCount = 0\n for (const [nullMultiplier, multiplicity] of values) {\n totalCount += nullMultiplier * multiplicity\n }\n return totalCount\n },\n }\n}\n\n/**\n * Creates an average aggregate function\n */\nexport function avg<T>(\n valueExtractor: (value: T) => number = (v) => v as unknown as number,\n): AggregateFunction<T, number, { sum: number; count: number }> {\n return {\n preMap: (data: T) => ({\n sum: valueExtractor(data),\n count: 0,\n }),\n reduce: (values: Array<[{ sum: number; count: number }, number]>) => {\n let totalSum = 0\n let totalCount = 0\n for (const [value, multiplicity] of values) {\n totalSum += value.sum * multiplicity\n totalCount += multiplicity\n }\n return {\n sum: totalSum,\n count: totalCount,\n }\n },\n postMap: (result: { sum: number; count: number }) => {\n return result.sum / result.count\n },\n }\n}\n\ntype CanMinMax = number | Date | bigint | string\n\n/**\n * Creates a min aggregate function that computes the minimum value in a group\n * @param valueExtractor Function to extract a comparable value from each data entry\n */\nexport function min<T extends CanMinMax>(): AggregateFunction<\n T,\n T | undefined,\n T | undefined\n>\nexport function min<T, V extends CanMinMax>(\n valueExtractor: (value: T) => V,\n): AggregateFunction<T, V | undefined, V | undefined>\nexport function min<T, V extends CanMinMax>(\n valueExtractor?: (value: T) => V,\n): AggregateFunction<T, V | undefined, V | undefined> {\n const extractor = valueExtractor ?? ((v: T) => v as unknown as V)\n return {\n preMap: (data: T) => extractor(data),\n reduce: (values) => {\n let minValue: V | undefined\n for (const [value, _multiplicity] of values) {\n if (\n value !== undefined &&\n (minValue === undefined || value < minValue)\n ) {\n minValue = value\n }\n }\n return minValue\n },\n }\n}\n\n/**\n * Creates a max aggregate function that computes the maximum value in a group\n * @param valueExtractor Function to extract a comparable value from each data entry\n */\nexport function max<T extends CanMinMax>(): AggregateFunction<\n T,\n T | undefined,\n T | undefined\n>\nexport function max<T, V extends CanMinMax>(\n valueExtractor: (value: T) => V,\n): AggregateFunction<T, V | undefined, V | undefined>\nexport function max<T, V extends CanMinMax>(\n valueExtractor?: (value: T) => V,\n): AggregateFunction<T, V | undefined, V | undefined> {\n const extractor = valueExtractor ?? ((v: T) => v as unknown as V)\n return {\n preMap: (data: T) => extractor(data),\n reduce: (values) => {\n let maxValue: V | undefined\n for (const [value, _multiplicity] of values) {\n if (\n value !== undefined &&\n (maxValue === undefined || value > maxValue)\n ) {\n maxValue = value\n }\n }\n return maxValue\n },\n }\n}\n\n/**\n * Creates a median aggregate function that computes the middle value in a sorted group\n * If there's an even number of values, returns the average of the two middle values\n * @param valueExtractor Function to extract a numeric value from each data entry\n */\nexport function median<T>(\n valueExtractor: (value: T) => number = (v) => v as unknown as number,\n): AggregateFunction<T, number, Array<number>> {\n return {\n preMap: (data: T) => [valueExtractor(data)],\n reduce: (values: Array<[Array<number>, number]>) => {\n // Flatten all values, taking multiplicity into account\n const allValues: Array<number> = []\n for (const [valueArray, multiplicity] of values) {\n for (const value of valueArray) {\n // Add each value multiple times based on multiplicity\n for (let i = 0; i < multiplicity; i++) {\n allValues.push(value)\n }\n }\n }\n\n // Return empty array if no values\n if (allValues.length === 0) {\n return []\n }\n\n // Sort values\n allValues.sort((a, b) => a - b)\n\n return allValues\n },\n postMap: (result: Array<number>) => {\n if (result.length === 0) return 0\n\n const mid = Math.floor(result.length / 2)\n\n // If even number of values, average the two middle values\n if (result.length % 2 === 0) {\n return (result[mid - 1]! + result[mid]!) / 2\n }\n\n // If odd number of values, return the middle value\n return result[mid]!\n },\n }\n}\n\n/**\n * Creates a mode aggregate function that computes the most frequent value in a group\n * If multiple values have the same highest frequency, returns the first one encountered\n * @param valueExtractor Function to extract a value from each data entry\n */\nexport function mode<T>(\n valueExtractor: (value: T) => number = (v) => v as unknown as number,\n): AggregateFunction<T, number, Map<number, number>> {\n return {\n preMap: (data: T) => {\n const value = valueExtractor(data)\n const frequencyMap = new Map<number, number>()\n frequencyMap.set(value, 1)\n return frequencyMap\n },\n reduce: (values: Array<[Map<number, number>, number]>) => {\n // Combine all frequency maps\n const combinedMap = new Map<number, number>()\n\n for (const [frequencyMap, multiplicity] of values) {\n for (const [value, frequencyCount] of frequencyMap.entries()) {\n const currentCount = combinedMap.get(value) || 0\n combinedMap.set(value, currentCount + frequencyCount * multiplicity)\n }\n }\n\n return combinedMap\n },\n postMap: (result: Map<number, number>) => {\n if (result.size === 0) return 0\n\n let modeValue = 0\n let maxFrequency = 0\n\n for (const [value, frequency] of result.entries()) {\n if (frequency > maxFrequency) {\n maxFrequency = frequency\n modeValue = value\n }\n }\n\n return modeValue\n },\n }\n}\n\nexport const groupByOperators = {\n sum,\n count,\n avg,\n min,\n max,\n median,\n mode,\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"groupBy.js","sources":["../../../src/operators/groupBy.ts"],"sourcesContent":["import { structuralIdentity } from '../hashing/hash.js'\nimport { serializeValue } from '../utils.js'\nimport { map } from './map.js'\nimport { reduce } from './reduce.js'\nimport type { IStreamBuilder, KeyValue } from '../types.js'\n\ntype GroupKey = Record<string, unknown>\n\ntype AggregateFunction<T, R, V = unknown> = {\n preMap: (data: T) => V\n reduce: (values: Array<[V, number]>) => V\n postMap?: (result: V) => R\n}\n\ntype ExtractAggregateReturnType<T, A> =\n A extends AggregateFunction<T, infer R, any> ? R : never\n\ntype AggregatesReturnType<T, A> = {\n [K in keyof A]: ExtractAggregateReturnType<T, A[K]>\n}\n\n/**\n * Groups data by key and applies multiple aggregate operations\n * @param keyExtractor Function to extract grouping key from data\n * @param aggregates Object mapping aggregate names to aggregate functions\n */\nexport function groupBy<\n T,\n K extends GroupKey,\n A extends Record<string, AggregateFunction<T, any, any>>,\n>(keyExtractor: (data: T) => K, aggregates: A = {} as A) {\n type ResultType = K & AggregatesReturnType<T, A>\n\n return (\n stream: IStreamBuilder<T>,\n ): IStreamBuilder<KeyValue<string, ResultType>> => {\n // Special key to store the original key object\n const KEY_SENTINEL = Symbol(`original_group_key`)\n\n const aggregateEntries = Object.entries(aggregates)\n\n // First map to extract keys and pre-aggregate values. Each contribution\n // leads with an identity built from its group key and pre-aggregate\n // values when every one of them has one. Within one group the reduce\n // index then merges equal contributions by that identity instead of\n // hashing them. A null identity falls back to the structural hash.\n const withKeysAndValues = stream.pipe(\n map((data) => {\n const key = keyExtractor(data)\n const keyString = serializeValue(key)\n\n // Create values object with pre-aggregated values\n const values: Record<string | symbol, unknown> = {}\n\n // Store the original key object. Contributions whose keys serialize\n // alike but hash apart must not merge, so the key is part of the\n // identity.\n values[KEY_SENTINEL] = key\n\n // Add pre-aggregated values\n let identity: string | null = structuralIdentity(key) ?? null\n for (const [name, aggregate] of aggregateEntries) {\n const value = aggregate.preMap(data)\n values[name] = value\n if (identity === null) continue\n const part = structuralIdentity(value)\n identity = part === undefined ? null : `${identity}|${part}`\n }\n\n return [keyString, [identity, values]] as KeyValue<\n string,\n [string | null, Record<string | symbol, unknown>]\n >\n }),\n )\n\n // Then reduce to compute aggregates\n const reduced = withKeysAndValues.pipe(\n reduce(\n (contributions) => {\n // Calculate total multiplicity to check if the group should exist\n let totalMultiplicity = 0\n for (const [_, multiplicity] of contributions) {\n totalMultiplicity += multiplicity\n }\n\n // If total multiplicity is 0 or negative, the group should be removed completely\n if (totalMultiplicity <= 0) {\n return []\n }\n\n const result: Record<PropertyKey, unknown> = {}\n\n // Get the original key from first value in group\n const originalKey = contributions[0]?.[0][1][KEY_SENTINEL]\n result[KEY_SENTINEL] = originalKey\n\n // Apply each aggregate function\n for (const [name, aggregate] of aggregateEntries) {\n const preValues = contributions.map(\n ([[, v], m]) => [v[name], m] as [any, number],\n )\n result[name] = aggregate.reduce(preValues)\n }\n\n return [[result, 1]]\n },\n { prefixIdentity: true },\n ),\n )\n\n // Finally map to extract the key and include all values\n return reduced.pipe(\n map(([keyString, values]) => {\n // Extract the original key\n const key = values[KEY_SENTINEL] as K\n\n // Create intermediate result with key values and aggregate results\n const result: Record<string, unknown> = {}\n\n // Add key properties to result\n Object.assign(result, key)\n\n // Apply postMap if provided\n for (const [name, aggregate] of aggregateEntries) {\n if (aggregate.postMap) {\n result[name] = aggregate.postMap(values[name])\n } else {\n result[name] = values[name]\n }\n }\n\n // Return with the string key instead of the object\n return [keyString, result] as KeyValue<string, ResultType>\n }),\n )\n }\n}\n\n/**\n * Creates a sum aggregate function\n */\nexport function sum<T>(\n valueExtractor: (value: T) => number = (v) => v as unknown as number,\n): AggregateFunction<T, number, number> {\n return {\n preMap: (data: T) => valueExtractor(data),\n reduce: (values: Array<[number, number]>) => {\n let total = 0\n for (const [value, multiplicity] of values) {\n total += value * multiplicity\n }\n return total\n },\n }\n}\n\n/**\n * Creates a count aggregate function\n */\nexport function count<T>(\n valueExtractor: (value: T) => any = (v) => v,\n): AggregateFunction<T, number, number> {\n return {\n // Count only not-null values (the `== null` comparison gives true for both null and undefined)\n preMap: (data: T) => (valueExtractor(data) == null ? 0 : 1),\n reduce: (values: Array<[number, number]>) => {\n let totalCount = 0\n for (const [nullMultiplier, multiplicity] of values) {\n totalCount += nullMultiplier * multiplicity\n }\n return totalCount\n },\n }\n}\n\n/**\n * Creates an average aggregate function\n */\nexport function avg<T>(\n valueExtractor: (value: T) => number = (v) => v as unknown as number,\n): AggregateFunction<T, number, { sum: number; count: number }> {\n return {\n preMap: (data: T) => ({\n sum: valueExtractor(data),\n count: 0,\n }),\n reduce: (values: Array<[{ sum: number; count: number }, number]>) => {\n let totalSum = 0\n let totalCount = 0\n for (const [value, multiplicity] of values) {\n totalSum += value.sum * multiplicity\n totalCount += multiplicity\n }\n return {\n sum: totalSum,\n count: totalCount,\n }\n },\n postMap: (result: { sum: number; count: number }) => {\n return result.sum / result.count\n },\n }\n}\n\ntype CanMinMax = number | Date | bigint | string\n\n/**\n * Creates a min aggregate function that computes the minimum value in a group\n * @param valueExtractor Function to extract a comparable value from each data entry\n */\nexport function min<T extends CanMinMax>(): AggregateFunction<\n T,\n T | undefined,\n T | undefined\n>\nexport function min<T, V extends CanMinMax>(\n valueExtractor: (value: T) => V,\n): AggregateFunction<T, V | undefined, V | undefined>\nexport function min<T, V extends CanMinMax>(\n valueExtractor?: (value: T) => V,\n): AggregateFunction<T, V | undefined, V | undefined> {\n const extractor = valueExtractor ?? ((v: T) => v as unknown as V)\n return {\n preMap: (data: T) => extractor(data),\n reduce: (values) => {\n let minValue: V | undefined\n for (const [value, _multiplicity] of values) {\n if (\n value !== undefined &&\n (minValue === undefined || value < minValue)\n ) {\n minValue = value\n }\n }\n return minValue\n },\n }\n}\n\n/**\n * Creates a max aggregate function that computes the maximum value in a group\n * @param valueExtractor Function to extract a comparable value from each data entry\n */\nexport function max<T extends CanMinMax>(): AggregateFunction<\n T,\n T | undefined,\n T | undefined\n>\nexport function max<T, V extends CanMinMax>(\n valueExtractor: (value: T) => V,\n): AggregateFunction<T, V | undefined, V | undefined>\nexport function max<T, V extends CanMinMax>(\n valueExtractor?: (value: T) => V,\n): AggregateFunction<T, V | undefined, V | undefined> {\n const extractor = valueExtractor ?? ((v: T) => v as unknown as V)\n return {\n preMap: (data: T) => extractor(data),\n reduce: (values) => {\n let maxValue: V | undefined\n for (const [value, _multiplicity] of values) {\n if (\n value !== undefined &&\n (maxValue === undefined || value > maxValue)\n ) {\n maxValue = value\n }\n }\n return maxValue\n },\n }\n}\n\n/**\n * Creates a median aggregate function that computes the middle value in a sorted group\n * If there's an even number of values, returns the average of the two middle values\n * @param valueExtractor Function to extract a numeric value from each data entry\n */\nexport function median<T>(\n valueExtractor: (value: T) => number = (v) => v as unknown as number,\n): AggregateFunction<T, number, Array<number>> {\n return {\n preMap: (data: T) => [valueExtractor(data)],\n reduce: (values: Array<[Array<number>, number]>) => {\n // Flatten all values, taking multiplicity into account\n const allValues: Array<number> = []\n for (const [valueArray, multiplicity] of values) {\n for (const value of valueArray) {\n // Add each value multiple times based on multiplicity\n for (let i = 0; i < multiplicity; i++) {\n allValues.push(value)\n }\n }\n }\n\n // Return empty array if no values\n if (allValues.length === 0) {\n return []\n }\n\n // Sort values\n allValues.sort((a, b) => a - b)\n\n return allValues\n },\n postMap: (result: Array<number>) => {\n if (result.length === 0) return 0\n\n const mid = Math.floor(result.length / 2)\n\n // If even number of values, average the two middle values\n if (result.length % 2 === 0) {\n return (result[mid - 1]! + result[mid]!) / 2\n }\n\n // If odd number of values, return the middle value\n return result[mid]!\n },\n }\n}\n\n/**\n * Creates a mode aggregate function that computes the most frequent value in a group\n * If multiple values have the same highest frequency, returns the first one encountered\n * @param valueExtractor Function to extract a value from each data entry\n */\nexport function mode<T>(\n valueExtractor: (value: T) => number = (v) => v as unknown as number,\n): AggregateFunction<T, number, Map<number, number>> {\n return {\n preMap: (data: T) => {\n const value = valueExtractor(data)\n const frequencyMap = new Map<number, number>()\n frequencyMap.set(value, 1)\n return frequencyMap\n },\n reduce: (values: Array<[Map<number, number>, number]>) => {\n // Combine all frequency maps\n const combinedMap = new Map<number, number>()\n\n for (const [frequencyMap, multiplicity] of values) {\n for (const [value, frequencyCount] of frequencyMap.entries()) {\n const currentCount = combinedMap.get(value) || 0\n combinedMap.set(value, currentCount + frequencyCount * multiplicity)\n }\n }\n\n return combinedMap\n },\n postMap: (result: Map<number, number>) => {\n if (result.size === 0) return 0\n\n let modeValue = 0\n let maxFrequency = 0\n\n for (const [value, frequency] of result.entries()) {\n if (frequency > maxFrequency) {\n maxFrequency = frequency\n modeValue = value\n }\n }\n\n return modeValue\n },\n }\n}\n\nexport const groupByOperators = {\n sum,\n count,\n avg,\n min,\n max,\n median,\n mode,\n}\n"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { DifferenceStreamWriter, UnaryOperator, DifferenceStreamReader } from '../graph.js';
2
- import { IndexedValue, TopK } from './topKArray.js';
2
+ import { IndexedValue } from './topKArray.js';
3
3
  import { PipedOperator } from '../types.js';
4
4
  export interface GroupedTopKWithFractionalIndexOptions<K, T> {
5
5
  limit?: number;
@@ -27,11 +27,6 @@ export interface GroupedTopKWithFractionalIndexOptions<K, T> {
27
27
  export declare class GroupedTopKWithFractionalIndexOperator<K extends string | number, T> extends UnaryOperator<[K, T], [K, IndexedValue<T>]> {
28
28
  #private;
29
29
  constructor(id: number, inputA: DifferenceStreamReader<[K, T]>, output: DifferenceStreamWriter<[K, IndexedValue<T>]>, comparator: (a: T, b: T) => number, options: GroupedTopKWithFractionalIndexOptions<K, T>);
30
- /**
31
- * Creates a new TopK data structure for a group.
32
- * Can be overridden in subclasses to use different implementations (e.g., B+ tree).
33
- */
34
- protected createTopK(offset: number, limit: number, comparator: (a: [K, T], b: [K, T]) => number): TopK<[K, T]>;
35
30
  run(): void;
36
31
  }
37
32
  /**
@@ -18,13 +18,6 @@ class GroupedTopKWithFractionalIndexOperator extends UnaryOperator {
18
18
  options.setSizeCallback?.(() => this.#getTotalSize());
19
19
  options.setWindowFn?.(this.#moveTopK.bind(this));
20
20
  }
21
- /**
22
- * Creates a new TopK data structure for a group.
23
- * Can be overridden in subclasses to use different implementations (e.g., B+ tree).
24
- */
25
- createTopK(offset, limit, comparator) {
26
- return new TopKArray(offset, limit, comparator);
27
- }
28
21
  #getTotalSize() {
29
22
  let size = 0;
30
23
  for (const state of this.#groupStates.values()) {
@@ -35,8 +28,9 @@ class GroupedTopKWithFractionalIndexOperator extends UnaryOperator {
35
28
  #getOrCreateGroupState(groupKey) {
36
29
  let state = this.#groupStates.get(groupKey);
37
30
  if (!state) {
38
- const topK = this.createTopK(this.#offset, this.#limit, this.#comparator);
39
- state = new TopKState(topK);
31
+ state = new TopKState(
32
+ new TopKArray(this.#offset, this.#limit, this.#comparator)
33
+ );
40
34
  this.#groupStates.set(groupKey, state);
41
35
  }
42
36
  return state;
@@ -1 +1 @@
1
- {"version":3,"file":"groupedTopKWithFractionalIndex.js","sources":["../../../src/operators/groupedTopKWithFractionalIndex.ts"],"sourcesContent":["import { DifferenceStreamWriter, UnaryOperator } from '../graph.js'\nimport { StreamBuilder } from '../d2.js'\nimport { MultiSet } from '../multiset.js'\nimport {\n TopKState,\n handleMoveIn,\n handleMoveOut,\n topKBatch,\n} from './topKState.js'\nimport { TopKArray, createKeyedComparator } from './topKArray.js'\nimport type { IndexedValue, TopK } from './topKArray.js'\nimport type { DifferenceStreamReader } from '../graph.js'\nimport type { IStreamBuilder, PipedOperator } from '../types.js'\n\nexport interface GroupedTopKWithFractionalIndexOptions<K, T> {\n limit?: number\n offset?: number\n setSizeCallback?: (getSize: () => number) => void\n setWindowFn?: (\n windowFn: (options: { offset?: number; limit?: number }) => void,\n ) => void\n /**\n * Function to extract a group key from the element's key and value.\n * Elements with the same group key will be sorted and limited together.\n */\n groupKeyFn: (key: K, value: T) => unknown\n}\n\n/**\n * Operator for grouped fractional indexed topK operations.\n * This operator maintains separate topK windows for each group,\n * allowing per-group limits and ordering.\n *\n * The input is a keyed stream [K, T] and outputs [K, IndexedValue<T>].\n * Elements are grouped by the groupKeyFn, and each group maintains\n * its own sorted collection with independent limit/offset.\n */\nexport class GroupedTopKWithFractionalIndexOperator<\n K extends string | number,\n T,\n> extends UnaryOperator<[K, T], [K, IndexedValue<T>]> {\n #groupStates: Map<unknown, TopKState<K, T>> = new Map()\n #groupKeyFn: (key: K, value: T) => unknown\n #comparator: (a: [K, T], b: [K, T]) => number\n #offset: number\n #limit: number\n\n constructor(\n id: number,\n inputA: DifferenceStreamReader<[K, T]>,\n output: DifferenceStreamWriter<[K, IndexedValue<T>]>,\n comparator: (a: T, b: T) => number,\n options: GroupedTopKWithFractionalIndexOptions<K, T>,\n ) {\n super(id, inputA, output)\n this.#groupKeyFn = options.groupKeyFn\n this.#limit = options.limit ?? Infinity\n this.#offset = options.offset ?? 0\n this.#comparator = createKeyedComparator(comparator)\n options.setSizeCallback?.(() => this.#getTotalSize())\n options.setWindowFn?.(this.#moveTopK.bind(this))\n }\n\n /**\n * Creates a new TopK data structure for a group.\n * Can be overridden in subclasses to use different implementations (e.g., B+ tree).\n */\n protected createTopK(\n offset: number,\n limit: number,\n comparator: (a: [K, T], b: [K, T]) => number,\n ): TopK<[K, T]> {\n return new TopKArray(offset, limit, comparator)\n }\n\n #getTotalSize(): number {\n let size = 0\n for (const state of this.#groupStates.values()) {\n size += state.size\n }\n return size\n }\n\n #getOrCreateGroupState(groupKey: unknown): TopKState<K, T> {\n let state = this.#groupStates.get(groupKey)\n if (!state) {\n const topK = this.createTopK(this.#offset, this.#limit, this.#comparator)\n state = new TopKState(topK)\n this.#groupStates.set(groupKey, state)\n }\n return state\n }\n\n #cleanupGroupIfEmpty(groupKey: unknown, state: TopKState<K, T>): void {\n if (state.isEmpty) {\n this.#groupStates.delete(groupKey)\n }\n }\n\n /**\n * Moves the topK window for all groups based on the provided offset and limit.\n * Any changes to the topK are sent to the output.\n */\n #moveTopK({ offset, limit }: { offset?: number; limit?: number }): void {\n if (offset !== undefined) {\n this.#offset = offset\n }\n if (limit !== undefined) {\n this.#limit = limit\n }\n\n const result: Array<[[K, IndexedValue<T>], number]> = []\n let hasChanges = false\n\n for (const state of this.#groupStates.values()) {\n const diff = state.move({ offset: this.#offset, limit: this.#limit }) // TODO: think we should just pass offset and limit\n\n diff.moveIns.forEach((moveIn) => handleMoveIn(moveIn, result))\n diff.moveOuts.forEach((moveOut) => handleMoveOut(moveOut, result))\n\n if (diff.changes) {\n hasChanges = true\n }\n }\n\n if (hasChanges) {\n this.output.sendData(new MultiSet(result))\n }\n }\n\n run(): void {\n const result: Array<[[K, IndexedValue<T>], number]> = []\n for (const [[key, value], multiplicity] of topKBatch(\n this.inputMessages(),\n )) {\n this.#processElement(key, value, multiplicity, result)\n }\n\n if (result.length > 0) {\n this.output.sendData(new MultiSet(result))\n }\n }\n\n #processElement(\n key: K,\n value: T,\n multiplicity: number,\n result: Array<[[K, IndexedValue<T>], number]>,\n ): void {\n const groupKey = this.#groupKeyFn(key, value)\n const state = this.#getOrCreateGroupState(groupKey)\n\n const changes = state.processElement(key, value, multiplicity)\n handleMoveIn(changes.moveIn, result)\n handleMoveOut(changes.moveOut, result)\n\n // Cleanup empty groups to prevent memory leaks\n this.#cleanupGroupIfEmpty(groupKey, state)\n }\n}\n\n/**\n * Limits the number of results per group based on a comparator, with optional offset.\n * Uses fractional indexing to minimize the number of changes when elements move positions.\n * Each element is assigned a fractional index that is lexicographically sortable.\n * When elements move, only the indices of the moved elements are updated, not all elements.\n *\n * This operator groups elements by the provided groupKeyFn and applies the limit/offset\n * independently to each group.\n *\n * @param comparator - A function that compares two elements for ordering\n * @param options - Configuration including groupKeyFn, limit, and offset\n * @returns A piped operator that orders elements per group and limits results per group\n */\nexport function groupedTopKWithFractionalIndex<K extends string | number, T>(\n comparator: (a: T, b: T) => number,\n options: GroupedTopKWithFractionalIndexOptions<K, T>,\n): PipedOperator<[K, T], [K, IndexedValue<T>]> {\n return (\n stream: IStreamBuilder<[K, T]>,\n ): IStreamBuilder<[K, IndexedValue<T>]> => {\n const output = new StreamBuilder<[K, IndexedValue<T>]>(\n stream.graph,\n new DifferenceStreamWriter<[K, IndexedValue<T>]>(),\n )\n const operator = new GroupedTopKWithFractionalIndexOperator<K, T>(\n stream.graph.getNextOperatorId(),\n stream.connectReader(),\n output.writer,\n comparator,\n options,\n )\n stream.graph.addOperator(operator)\n return output\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"groupedTopKWithFractionalIndex.js","sources":["../../../src/operators/groupedTopKWithFractionalIndex.ts"],"sourcesContent":["import { DifferenceStreamWriter, UnaryOperator } from '../graph.js'\nimport { StreamBuilder } from '../d2.js'\nimport { MultiSet } from '../multiset.js'\nimport {\n TopKState,\n handleMoveIn,\n handleMoveOut,\n topKBatch,\n} from './topKState.js'\nimport { TopKArray, createKeyedComparator } from './topKArray.js'\nimport type { IndexedValue } from './topKArray.js'\nimport type { DifferenceStreamReader } from '../graph.js'\nimport type { IStreamBuilder, PipedOperator } from '../types.js'\n\nexport interface GroupedTopKWithFractionalIndexOptions<K, T> {\n limit?: number\n offset?: number\n setSizeCallback?: (getSize: () => number) => void\n setWindowFn?: (\n windowFn: (options: { offset?: number; limit?: number }) => void,\n ) => void\n /**\n * Function to extract a group key from the element's key and value.\n * Elements with the same group key will be sorted and limited together.\n */\n groupKeyFn: (key: K, value: T) => unknown\n}\n\n/**\n * Operator for grouped fractional indexed topK operations.\n * This operator maintains separate topK windows for each group,\n * allowing per-group limits and ordering.\n *\n * The input is a keyed stream [K, T] and outputs [K, IndexedValue<T>].\n * Elements are grouped by the groupKeyFn, and each group maintains\n * its own sorted collection with independent limit/offset.\n */\nexport class GroupedTopKWithFractionalIndexOperator<\n K extends string | number,\n T,\n> extends UnaryOperator<[K, T], [K, IndexedValue<T>]> {\n #groupStates: Map<unknown, TopKState<K, T>> = new Map()\n #groupKeyFn: (key: K, value: T) => unknown\n #comparator: (a: [K, T], b: [K, T]) => number\n #offset: number\n #limit: number\n\n constructor(\n id: number,\n inputA: DifferenceStreamReader<[K, T]>,\n output: DifferenceStreamWriter<[K, IndexedValue<T>]>,\n comparator: (a: T, b: T) => number,\n options: GroupedTopKWithFractionalIndexOptions<K, T>,\n ) {\n super(id, inputA, output)\n this.#groupKeyFn = options.groupKeyFn\n this.#limit = options.limit ?? Infinity\n this.#offset = options.offset ?? 0\n this.#comparator = createKeyedComparator(comparator)\n options.setSizeCallback?.(() => this.#getTotalSize())\n options.setWindowFn?.(this.#moveTopK.bind(this))\n }\n\n #getTotalSize(): number {\n let size = 0\n for (const state of this.#groupStates.values()) {\n size += state.size\n }\n return size\n }\n\n #getOrCreateGroupState(groupKey: unknown): TopKState<K, T> {\n let state = this.#groupStates.get(groupKey)\n if (!state) {\n state = new TopKState(\n new TopKArray(this.#offset, this.#limit, this.#comparator),\n )\n this.#groupStates.set(groupKey, state)\n }\n return state\n }\n\n #cleanupGroupIfEmpty(groupKey: unknown, state: TopKState<K, T>): void {\n if (state.isEmpty) {\n this.#groupStates.delete(groupKey)\n }\n }\n\n /**\n * Moves the topK window for all groups based on the provided offset and limit.\n * Any changes to the topK are sent to the output.\n */\n #moveTopK({ offset, limit }: { offset?: number; limit?: number }): void {\n if (offset !== undefined) {\n this.#offset = offset\n }\n if (limit !== undefined) {\n this.#limit = limit\n }\n\n const result: Array<[[K, IndexedValue<T>], number]> = []\n let hasChanges = false\n\n for (const state of this.#groupStates.values()) {\n const diff = state.move({ offset: this.#offset, limit: this.#limit }) // TODO: think we should just pass offset and limit\n\n diff.moveIns.forEach((moveIn) => handleMoveIn(moveIn, result))\n diff.moveOuts.forEach((moveOut) => handleMoveOut(moveOut, result))\n\n if (diff.changes) {\n hasChanges = true\n }\n }\n\n if (hasChanges) {\n this.output.sendData(new MultiSet(result))\n }\n }\n\n run(): void {\n const result: Array<[[K, IndexedValue<T>], number]> = []\n for (const [[key, value], multiplicity] of topKBatch(\n this.inputMessages(),\n )) {\n this.#processElement(key, value, multiplicity, result)\n }\n\n if (result.length > 0) {\n this.output.sendData(new MultiSet(result))\n }\n }\n\n #processElement(\n key: K,\n value: T,\n multiplicity: number,\n result: Array<[[K, IndexedValue<T>], number]>,\n ): void {\n const groupKey = this.#groupKeyFn(key, value)\n const state = this.#getOrCreateGroupState(groupKey)\n\n const changes = state.processElement(key, value, multiplicity)\n handleMoveIn(changes.moveIn, result)\n handleMoveOut(changes.moveOut, result)\n\n // Cleanup empty groups to prevent memory leaks\n this.#cleanupGroupIfEmpty(groupKey, state)\n }\n}\n\n/**\n * Limits the number of results per group based on a comparator, with optional offset.\n * Uses fractional indexing to minimize the number of changes when elements move positions.\n * Each element is assigned a fractional index that is lexicographically sortable.\n * When elements move, only the indices of the moved elements are updated, not all elements.\n *\n * This operator groups elements by the provided groupKeyFn and applies the limit/offset\n * independently to each group.\n *\n * @param comparator - A function that compares two elements for ordering\n * @param options - Configuration including groupKeyFn, limit, and offset\n * @returns A piped operator that orders elements per group and limits results per group\n */\nexport function groupedTopKWithFractionalIndex<K extends string | number, T>(\n comparator: (a: T, b: T) => number,\n options: GroupedTopKWithFractionalIndexOptions<K, T>,\n): PipedOperator<[K, T], [K, IndexedValue<T>]> {\n return (\n stream: IStreamBuilder<[K, T]>,\n ): IStreamBuilder<[K, IndexedValue<T>]> => {\n const output = new StreamBuilder<[K, IndexedValue<T>]>(\n stream.graph,\n new DifferenceStreamWriter<[K, IndexedValue<T>]>(),\n )\n const operator = new GroupedTopKWithFractionalIndexOperator<K, T>(\n stream.graph.getNextOperatorId(),\n stream.connectReader(),\n output.writer,\n comparator,\n options,\n )\n stream.graph.addOperator(operator)\n return output\n }\n}\n"],"names":[],"mappings":"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"}
@@ -2,20 +2,14 @@ export * from './pipe.js';
2
2
  export * from './map.js';
3
3
  export * from './tap.js';
4
4
  export * from './filter.js';
5
- export * from './negate.js';
6
5
  export * from './concat.js';
7
- export * from './debug.js';
8
6
  export * from './output.js';
9
7
  export * from './consolidate.js';
10
8
  export * from './join.js';
11
9
  export * from './reduce.js';
12
- export * from './count.js';
13
10
  export * from './distinct.js';
14
- export * from './keying.js';
15
- export * from './topK.js';
16
11
  export * from './topKWithFractionalIndex.js';
17
12
  export * from './groupedTopKWithFractionalIndex.js';
18
13
  export * from './orderBy.js';
19
14
  export * from './groupedOrderBy.js';
20
- export * from './filterBy.js';
21
15
  export { groupBy, groupByOperators } from './groupBy.js';
@@ -23,29 +23,4 @@ export declare class JoinOperator<K, V1, V2> extends BinaryOperator<[
23
23
  * @param other - The other stream to join with
24
24
  * @param type - The type of join to perform
25
25
  */
26
- export declare function join<K, V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never, V2, T>(other: IStreamBuilder<KeyValue<K, V2>>, type?: JoinType): PipedOperator<T, KeyValue<K, [V1 | null, V2 | null]>>;
27
- /**
28
- * Joins two input streams (inner join)
29
- * @param other - The other stream to join with
30
- */
31
- export declare function innerJoin<K, V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never, V2, T>(other: IStreamBuilder<KeyValue<K, V2>>): PipedOperator<T, KeyValue<K, [V1, V2]>>;
32
- /**
33
- * Joins two input streams (anti join)
34
- * @param other - The other stream to join with
35
- */
36
- export declare function antiJoin<K, V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never, V2, T>(other: IStreamBuilder<KeyValue<K, V2>>): PipedOperator<T, KeyValue<K, [V1, null]>>;
37
- /**
38
- * Joins two input streams (left join)
39
- * @param other - The other stream to join with
40
- */
41
- export declare function leftJoin<K, V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never, V2, T>(other: IStreamBuilder<KeyValue<K, V2>>): PipedOperator<T, KeyValue<K, [V1, V2 | null]>>;
42
- /**
43
- * Joins two input streams (right join)
44
- * @param other - The other stream to join with
45
- */
46
- export declare function rightJoin<K, V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never, V2, T>(other: IStreamBuilder<KeyValue<K, V2>>): PipedOperator<T, KeyValue<K, [V1 | null, V2]>>;
47
- /**
48
- * Joins two input streams (full join)
49
- * @param other - The other stream to join with
50
- */
51
- export declare function fullJoin<K, V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never, V2, T>(other: IStreamBuilder<KeyValue<K, V2>>): PipedOperator<T, KeyValue<K, [V1 | null, V2 | null]>>;
26
+ export declare function join<K, V1 extends (T extends KeyValue<infer _KT, infer VT> ? VT : never), V2, T>(other: IStreamBuilder<KeyValue<K, V2>>, type?: JoinType): PipedOperator<T, KeyValue<K, [V1 | null, V2 | null]>>;
@@ -128,28 +128,8 @@ function join(other, type = `inner`) {
128
128
  return output;
129
129
  };
130
130
  }
131
- function innerJoin(other) {
132
- return join(other, `inner`);
133
- }
134
- function antiJoin(other) {
135
- return join(other, `anti`);
136
- }
137
- function leftJoin(other) {
138
- return join(other, `left`);
139
- }
140
- function rightJoin(other) {
141
- return join(other, `right`);
142
- }
143
- function fullJoin(other) {
144
- return join(other, `full`);
145
- }
146
131
  export {
147
132
  JoinOperator,
148
- antiJoin,
149
- fullJoin,
150
- innerJoin,
151
- join,
152
- leftJoin,
153
- rightJoin
133
+ join
154
134
  };
155
135
  //# sourceMappingURL=join.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"join.js","sources":["../../../src/operators/join.ts"],"sourcesContent":["/**\n * # Direct Join Algorithms for Incremental View Maintenance\n *\n * High-performance join operations implementing all join types (inner, left, right, full, anti)\n * with minimal state and optimized performance.\n *\n * ## Algorithm\n *\n * For each tick, the algorithm processes incoming changes (deltas) and emits join results:\n *\n * 1. **Build deltas**: Create delta indexes from input messages using `Index.fromMultiSet()`\n * 2. **Inner results**: Emit `ΔA⋈B_old + A_old⋈ΔB + ΔA⋈ΔB` (matched pairs)\n * 3. **Outer results**: For unmatched rows, emit null-extended tuples:\n * - New unmatched rows from deltas (when opposite side empty)\n * - Presence transitions: when key goes `0→>0` (retract nulls) or `>0→0` (emit nulls)\n * 4. **Update state**: Append deltas to indexes (consolidated multiplicity tracking automatic)\n *\n * **Consolidated multiplicity tracking** enables O(1) presence checks instead of scanning index buckets.\n *\n * ## State\n *\n * **Indexes** store the actual data:\n * - `indexA: Index<K, V1>` - all left-side rows accumulated over time\n * - `indexB: Index<K, V2>` - all right-side rows accumulated over time\n *\n * **Consolidated multiplicity tracking** (built into Index):\n * - Each Index maintains sum of multiplicities per key internally\n * - Provides O(1) presence checks: `index.hasPresence(key)` and `index.getConsolidatedMultiplicity(key)`\n * - Avoids scanning entire index buckets just to check if key has any rows\n *\n * ## Join Types\n *\n * - **Inner**: Standard delta terms only\n * - **Outer**: Inner results + null-extended unmatched rows with transition handling\n * - **Anti**: Unmatched rows only (no inner results)\n *\n * ## Key Optimizations\n *\n * - **No temp copying**: Uses `(A⊎ΔA)⋈ΔB = A⋈ΔB ⊎ ΔA⋈ΔB` distributive property\n * - **Early-out checks**: Skip phases when no deltas present\n * - **Zero-entry pruning**: Keep maps compact, O(distinct keys) memory\n * - **Final presence logic**: Avoid emit→retract churn within same tick\n *\n * ## Correctness\n *\n * - **Ordering**: Pre-append snapshots for emissions, post-emit state updates\n * - **Presence**: Key matched iff mass ≠ 0, transitions trigger null handling\n * - **Bag semantics**: Proper multiplicity handling including negatives\n */\n\nimport { BinaryOperator, DifferenceStreamWriter } from '../graph.js'\nimport { StreamBuilder } from '../d2.js'\nimport { MultiSet } from '../multiset.js'\nimport { Index } from '../indexes.js'\nimport type { DifferenceStreamReader } from '../graph.js'\nimport type { IStreamBuilder, KeyValue, PipedOperator } from '../types.js'\n\n/**\n * Type of join to perform\n */\nexport type JoinType = `inner` | `left` | `right` | `full` | `anti`\n\n/**\n * Operator that joins two input streams using direct join algorithms\n */\nexport class JoinOperator<K, V1, V2> extends BinaryOperator<\n [K, V1] | [K, V2] | [K, [V1, V2]] | [K, [V1 | null, V2 | null]]\n> {\n #indexA = new Index<K, V1>()\n #indexB = new Index<K, V2>()\n #mode: JoinType\n\n constructor(\n id: number,\n inputA: DifferenceStreamReader<[K, V1]>,\n inputB: DifferenceStreamReader<[K, V2]>,\n output: DifferenceStreamWriter<any>,\n mode: JoinType = `inner`,\n ) {\n super(id, inputA, inputB, output)\n this.#mode = mode\n }\n\n run(): void {\n // Build deltas from input messages\n const deltaA = Index.fromMultiSets<K, V1>(\n this.inputAMessages() as Array<MultiSet<[K, V1]>>,\n )\n const deltaB = Index.fromMultiSets<K, V2>(\n this.inputBMessages() as Array<MultiSet<[K, V2]>>,\n )\n\n // Early-out if nothing changed\n if (deltaA.size === 0 && deltaB.size === 0) return\n\n const results = new MultiSet<any>()\n\n // Emit inner results (all modes except anti)\n if (this.#mode !== `anti`) {\n this.emitInnerResults(deltaA, deltaB, results)\n }\n\n // Emit left outer/anti results\n if (\n this.#mode === `left` ||\n this.#mode === `full` ||\n this.#mode === `anti`\n ) {\n this.emitLeftOuterResults(deltaA, deltaB, results)\n }\n\n // Emit right outer results\n if (this.#mode === `right` || this.#mode === `full`) {\n this.emitRightOuterResults(deltaA, deltaB, results)\n }\n\n // Update state and send results\n // IMPORTANT: All emissions use pre-append snapshots of indexA/indexB.\n // Now append ALL deltas to indices - this happens unconditionally for every key,\n // regardless of whether presence flipped. Consolidated multiplicity tracking is automatic.\n this.#indexA.append(deltaA)\n this.#indexB.append(deltaB)\n\n // Send results\n if (results.getInner().length > 0) {\n this.output.sendData(results)\n }\n }\n\n private emitInnerResults(\n deltaA: Index<K, V1>,\n deltaB: Index<K, V2>,\n results: MultiSet<any>,\n ): void {\n // Emit the three standard delta terms: ΔA⋈B_old, A_old⋈ΔB, ΔA⋈ΔB\n if (deltaA.size > 0) results.extend(deltaA.join(this.#indexB))\n if (deltaB.size > 0) results.extend(this.#indexA.join(deltaB))\n if (deltaA.size > 0 && deltaB.size > 0) results.extend(deltaA.join(deltaB))\n }\n\n private emitLeftOuterResults(\n deltaA: Index<K, V1>,\n deltaB: Index<K, V2>,\n results: MultiSet<any>,\n ): void {\n // Emit unmatched left rows from deltaA\n if (deltaA.size > 0) {\n for (const [key, valueIterator] of deltaA.entriesIterators()) {\n const currentMultiplicityB =\n this.#indexB.getConsolidatedMultiplicity(key)\n const deltaMultiplicityB = deltaB.getConsolidatedMultiplicity(key)\n const finalMultiplicityB = currentMultiplicityB + deltaMultiplicityB\n\n if (finalMultiplicityB === 0) {\n for (const [value, multiplicity] of valueIterator) {\n if (multiplicity !== 0) {\n results.add([key, [value, null]], multiplicity)\n }\n }\n }\n }\n }\n\n // Handle presence transitions from right side changes\n if (deltaB.size > 0) {\n for (const key of deltaB.getPresenceKeys()) {\n const before = this.#indexB.getConsolidatedMultiplicity(key)\n const deltaMult = deltaB.getConsolidatedMultiplicity(key)\n if (deltaMult === 0) continue\n const after = before + deltaMult\n\n // Skip transition handling if presence doesn't flip (both zero or both non-zero)\n // Note: Index updates happen later regardless - we're only skipping null-extension emissions here\n if ((before === 0) === (after === 0)) continue\n\n // Determine the type of transition:\n // - 0 → non-zero: Right becomes non-empty, left rows transition from unmatched to matched\n // → RETRACT previously emitted null-extended rows (emit with negative multiplicity)\n // - non-zero → 0: Right becomes empty, left rows transition from matched to unmatched\n // → EMIT new null-extended rows (emit with positive multiplicity)\n const transitioningToMatched = before === 0\n\n for (const [value, multiplicity] of this.#indexA.getIterator(key)) {\n if (multiplicity !== 0) {\n results.add(\n [key, [value, null]],\n transitioningToMatched ? -multiplicity : +multiplicity,\n )\n }\n }\n }\n }\n }\n\n private emitRightOuterResults(\n deltaA: Index<K, V1>,\n deltaB: Index<K, V2>,\n results: MultiSet<any>,\n ): void {\n // Emit unmatched right rows from deltaB\n if (deltaB.size > 0) {\n for (const [key, valueIterator] of deltaB.entriesIterators()) {\n const currentMultiplicityA =\n this.#indexA.getConsolidatedMultiplicity(key)\n const deltaMultiplicityA = deltaA.getConsolidatedMultiplicity(key)\n const finalMultiplicityA = currentMultiplicityA + deltaMultiplicityA\n\n if (finalMultiplicityA === 0) {\n for (const [value, multiplicity] of valueIterator) {\n if (multiplicity !== 0) {\n results.add([key, [null, value]], multiplicity)\n }\n }\n }\n }\n }\n\n // Handle presence transitions from left side changes\n if (deltaA.size > 0) {\n for (const key of deltaA.getPresenceKeys()) {\n const before = this.#indexA.getConsolidatedMultiplicity(key)\n const deltaMult = deltaA.getConsolidatedMultiplicity(key)\n if (deltaMult === 0) continue\n const after = before + deltaMult\n\n // Skip transition handling if presence doesn't flip (both zero or both non-zero)\n // Note: Index updates happen later regardless - we're only skipping null-extension emissions here\n if ((before === 0) === (after === 0)) continue\n\n // Determine the type of transition:\n // - 0 → non-zero: Left becomes non-empty, right rows transition from unmatched to matched\n // → RETRACT previously emitted null-extended rows (emit with negative multiplicity)\n // - non-zero → 0: Left becomes empty, right rows transition from matched to unmatched\n // → EMIT new null-extended rows (emit with positive multiplicity)\n const transitioningToMatched = before === 0\n\n for (const [value, multiplicity] of this.#indexB.getIterator(key)) {\n if (multiplicity !== 0) {\n results.add(\n [key, [null, value]],\n transitioningToMatched ? -multiplicity : +multiplicity,\n )\n }\n }\n }\n }\n }\n}\n\n/**\n * Joins two input streams\n * @param other - The other stream to join with\n * @param type - The type of join to perform\n */\nexport function join<\n K,\n V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never,\n V2,\n T,\n>(\n other: IStreamBuilder<KeyValue<K, V2>>,\n type: JoinType = `inner`,\n): PipedOperator<T, KeyValue<K, [V1 | null, V2 | null]>> {\n return (\n stream: IStreamBuilder<T>,\n ): IStreamBuilder<KeyValue<K, [V1 | null, V2 | null]>> => {\n if (stream.graph !== other.graph) {\n throw new Error(`Cannot join streams from different graphs`)\n }\n const output = new StreamBuilder<KeyValue<K, [V1 | null, V2 | null]>>(\n stream.graph,\n new DifferenceStreamWriter<KeyValue<K, [V1 | null, V2 | null]>>(),\n )\n const operator = new JoinOperator<K, V1, V2>(\n stream.graph.getNextOperatorId(),\n stream.connectReader() as DifferenceStreamReader<KeyValue<K, V1>>,\n other.connectReader(),\n output.writer,\n type,\n )\n stream.graph.addOperator(operator)\n return output\n }\n}\n\n/**\n * Joins two input streams (inner join)\n * @param other - The other stream to join with\n */\nexport function innerJoin<\n K,\n V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never,\n V2,\n T,\n>(\n other: IStreamBuilder<KeyValue<K, V2>>,\n): PipedOperator<T, KeyValue<K, [V1, V2]>> {\n return join(other, `inner`) as unknown as PipedOperator<\n T,\n KeyValue<K, [V1, V2]>\n >\n}\n\n/**\n * Joins two input streams (anti join)\n * @param other - The other stream to join with\n */\nexport function antiJoin<\n K,\n V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never,\n V2,\n T,\n>(\n other: IStreamBuilder<KeyValue<K, V2>>,\n): PipedOperator<T, KeyValue<K, [V1, null]>> {\n return join(other, `anti`) as unknown as PipedOperator<\n T,\n KeyValue<K, [V1, null]>\n >\n}\n\n/**\n * Joins two input streams (left join)\n * @param other - The other stream to join with\n */\nexport function leftJoin<\n K,\n V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never,\n V2,\n T,\n>(\n other: IStreamBuilder<KeyValue<K, V2>>,\n): PipedOperator<T, KeyValue<K, [V1, V2 | null]>> {\n return join(other, `left`) as unknown as PipedOperator<\n T,\n KeyValue<K, [V1, V2 | null]>\n >\n}\n\n/**\n * Joins two input streams (right join)\n * @param other - The other stream to join with\n */\nexport function rightJoin<\n K,\n V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never,\n V2,\n T,\n>(\n other: IStreamBuilder<KeyValue<K, V2>>,\n): PipedOperator<T, KeyValue<K, [V1 | null, V2]>> {\n return join(other, `right`) as unknown as PipedOperator<\n T,\n KeyValue<K, [V1 | null, V2]>\n >\n}\n\n/**\n * Joins two input streams (full join)\n * @param other - The other stream to join with\n */\nexport function fullJoin<\n K,\n V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never,\n V2,\n T,\n>(\n other: IStreamBuilder<KeyValue<K, V2>>,\n): PipedOperator<T, KeyValue<K, [V1 | null, V2 | null]>> {\n return join(other, `full`) as unknown as PipedOperator<\n T,\n KeyValue<K, [V1 | null, V2 | null]>\n >\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"join.js","sources":["../../../src/operators/join.ts"],"sourcesContent":["/**\n * # Direct Join Algorithms for Incremental View Maintenance\n *\n * High-performance join operations implementing all join types (inner, left, right, full, anti)\n * with minimal state and optimized performance.\n *\n * ## Algorithm\n *\n * For each tick, the algorithm processes incoming changes (deltas) and emits join results:\n *\n * 1. **Build deltas**: Create delta indexes from input messages using `Index.fromMultiSet()`\n * 2. **Inner results**: Emit `ΔA⋈B_old + A_old⋈ΔB + ΔA⋈ΔB` (matched pairs)\n * 3. **Outer results**: For unmatched rows, emit null-extended tuples:\n * - New unmatched rows from deltas (when opposite side empty)\n * - Presence transitions: when key goes `0→>0` (retract nulls) or `>0→0` (emit nulls)\n * 4. **Update state**: Append deltas to indexes (consolidated multiplicity tracking automatic)\n *\n * **Consolidated multiplicity tracking** enables O(1) presence checks instead of scanning index buckets.\n *\n * ## State\n *\n * **Indexes** store the actual data:\n * - `indexA: Index<K, V1>` - all left-side rows accumulated over time\n * - `indexB: Index<K, V2>` - all right-side rows accumulated over time\n *\n * **Consolidated multiplicity tracking** (built into Index):\n * - Each Index maintains sum of multiplicities per key internally\n * - Provides O(1) presence checks: `index.hasPresence(key)` and `index.getConsolidatedMultiplicity(key)`\n * - Avoids scanning entire index buckets just to check if key has any rows\n *\n * ## Join Types\n *\n * - **Inner**: Standard delta terms only\n * - **Outer**: Inner results + null-extended unmatched rows with transition handling\n * - **Anti**: Unmatched rows only (no inner results)\n *\n * ## Key Optimizations\n *\n * - **No temp copying**: Uses `(A⊎ΔA)⋈ΔB = A⋈ΔB ⊎ ΔA⋈ΔB` distributive property\n * - **Early-out checks**: Skip phases when no deltas present\n * - **Zero-entry pruning**: Keep maps compact, O(distinct keys) memory\n * - **Final presence logic**: Avoid emit→retract churn within same tick\n *\n * ## Correctness\n *\n * - **Ordering**: Pre-append snapshots for emissions, post-emit state updates\n * - **Presence**: Key matched iff mass ≠ 0, transitions trigger null handling\n * - **Bag semantics**: Proper multiplicity handling including negatives\n */\n\nimport { BinaryOperator, DifferenceStreamWriter } from '../graph.js'\nimport { StreamBuilder } from '../d2.js'\nimport { MultiSet } from '../multiset.js'\nimport { Index } from '../indexes.js'\nimport type { DifferenceStreamReader } from '../graph.js'\nimport type { IStreamBuilder, KeyValue, PipedOperator } from '../types.js'\n\n/**\n * Type of join to perform\n */\nexport type JoinType = `inner` | `left` | `right` | `full` | `anti`\n\n/**\n * Operator that joins two input streams using direct join algorithms\n */\nexport class JoinOperator<K, V1, V2> extends BinaryOperator<\n [K, V1] | [K, V2] | [K, [V1, V2]] | [K, [V1 | null, V2 | null]]\n> {\n #indexA = new Index<K, V1>()\n #indexB = new Index<K, V2>()\n #mode: JoinType\n\n constructor(\n id: number,\n inputA: DifferenceStreamReader<[K, V1]>,\n inputB: DifferenceStreamReader<[K, V2]>,\n output: DifferenceStreamWriter<any>,\n mode: JoinType = `inner`,\n ) {\n super(id, inputA, inputB, output)\n this.#mode = mode\n }\n\n run(): void {\n // Build deltas from input messages\n const deltaA = Index.fromMultiSets<K, V1>(\n this.inputAMessages() as Array<MultiSet<[K, V1]>>,\n )\n const deltaB = Index.fromMultiSets<K, V2>(\n this.inputBMessages() as Array<MultiSet<[K, V2]>>,\n )\n\n // Early-out if nothing changed\n if (deltaA.size === 0 && deltaB.size === 0) return\n\n const results = new MultiSet<any>()\n\n // Emit inner results (all modes except anti)\n if (this.#mode !== `anti`) {\n this.emitInnerResults(deltaA, deltaB, results)\n }\n\n // Emit left outer/anti results\n if (\n this.#mode === `left` ||\n this.#mode === `full` ||\n this.#mode === `anti`\n ) {\n this.emitLeftOuterResults(deltaA, deltaB, results)\n }\n\n // Emit right outer results\n if (this.#mode === `right` || this.#mode === `full`) {\n this.emitRightOuterResults(deltaA, deltaB, results)\n }\n\n // Update state and send results\n // IMPORTANT: All emissions use pre-append snapshots of indexA/indexB.\n // Now append ALL deltas to indices - this happens unconditionally for every key,\n // regardless of whether presence flipped. Consolidated multiplicity tracking is automatic.\n this.#indexA.append(deltaA)\n this.#indexB.append(deltaB)\n\n // Send results\n if (results.getInner().length > 0) {\n this.output.sendData(results)\n }\n }\n\n private emitInnerResults(\n deltaA: Index<K, V1>,\n deltaB: Index<K, V2>,\n results: MultiSet<any>,\n ): void {\n // Emit the three standard delta terms: ΔA⋈B_old, A_old⋈ΔB, ΔA⋈ΔB\n if (deltaA.size > 0) results.extend(deltaA.join(this.#indexB))\n if (deltaB.size > 0) results.extend(this.#indexA.join(deltaB))\n if (deltaA.size > 0 && deltaB.size > 0) results.extend(deltaA.join(deltaB))\n }\n\n private emitLeftOuterResults(\n deltaA: Index<K, V1>,\n deltaB: Index<K, V2>,\n results: MultiSet<any>,\n ): void {\n // Emit unmatched left rows from deltaA\n if (deltaA.size > 0) {\n for (const [key, valueIterator] of deltaA.entriesIterators()) {\n const currentMultiplicityB =\n this.#indexB.getConsolidatedMultiplicity(key)\n const deltaMultiplicityB = deltaB.getConsolidatedMultiplicity(key)\n const finalMultiplicityB = currentMultiplicityB + deltaMultiplicityB\n\n if (finalMultiplicityB === 0) {\n for (const [value, multiplicity] of valueIterator) {\n if (multiplicity !== 0) {\n results.add([key, [value, null]], multiplicity)\n }\n }\n }\n }\n }\n\n // Handle presence transitions from right side changes\n if (deltaB.size > 0) {\n for (const key of deltaB.getPresenceKeys()) {\n const before = this.#indexB.getConsolidatedMultiplicity(key)\n const deltaMult = deltaB.getConsolidatedMultiplicity(key)\n if (deltaMult === 0) continue\n const after = before + deltaMult\n\n // Skip transition handling if presence doesn't flip (both zero or both non-zero)\n // Note: Index updates happen later regardless - we're only skipping null-extension emissions here\n if ((before === 0) === (after === 0)) continue\n\n // Determine the type of transition:\n // - 0 → non-zero: Right becomes non-empty, left rows transition from unmatched to matched\n // → RETRACT previously emitted null-extended rows (emit with negative multiplicity)\n // - non-zero → 0: Right becomes empty, left rows transition from matched to unmatched\n // → EMIT new null-extended rows (emit with positive multiplicity)\n const transitioningToMatched = before === 0\n\n for (const [value, multiplicity] of this.#indexA.getIterator(key)) {\n if (multiplicity !== 0) {\n results.add(\n [key, [value, null]],\n transitioningToMatched ? -multiplicity : +multiplicity,\n )\n }\n }\n }\n }\n }\n\n private emitRightOuterResults(\n deltaA: Index<K, V1>,\n deltaB: Index<K, V2>,\n results: MultiSet<any>,\n ): void {\n // Emit unmatched right rows from deltaB\n if (deltaB.size > 0) {\n for (const [key, valueIterator] of deltaB.entriesIterators()) {\n const currentMultiplicityA =\n this.#indexA.getConsolidatedMultiplicity(key)\n const deltaMultiplicityA = deltaA.getConsolidatedMultiplicity(key)\n const finalMultiplicityA = currentMultiplicityA + deltaMultiplicityA\n\n if (finalMultiplicityA === 0) {\n for (const [value, multiplicity] of valueIterator) {\n if (multiplicity !== 0) {\n results.add([key, [null, value]], multiplicity)\n }\n }\n }\n }\n }\n\n // Handle presence transitions from left side changes\n if (deltaA.size > 0) {\n for (const key of deltaA.getPresenceKeys()) {\n const before = this.#indexA.getConsolidatedMultiplicity(key)\n const deltaMult = deltaA.getConsolidatedMultiplicity(key)\n if (deltaMult === 0) continue\n const after = before + deltaMult\n\n // Skip transition handling if presence doesn't flip (both zero or both non-zero)\n // Note: Index updates happen later regardless - we're only skipping null-extension emissions here\n if ((before === 0) === (after === 0)) continue\n\n // Determine the type of transition:\n // - 0 → non-zero: Left becomes non-empty, right rows transition from unmatched to matched\n // → RETRACT previously emitted null-extended rows (emit with negative multiplicity)\n // - non-zero → 0: Left becomes empty, right rows transition from matched to unmatched\n // → EMIT new null-extended rows (emit with positive multiplicity)\n const transitioningToMatched = before === 0\n\n for (const [value, multiplicity] of this.#indexB.getIterator(key)) {\n if (multiplicity !== 0) {\n results.add(\n [key, [null, value]],\n transitioningToMatched ? -multiplicity : +multiplicity,\n )\n }\n }\n }\n }\n }\n}\n\n/**\n * Joins two input streams\n * @param other - The other stream to join with\n * @param type - The type of join to perform\n */\nexport function join<\n K,\n V1 extends (T extends KeyValue<infer _KT, infer VT> ? VT : never),\n V2,\n T,\n>(\n other: IStreamBuilder<KeyValue<K, V2>>,\n type: JoinType = `inner`,\n): PipedOperator<T, KeyValue<K, [V1 | null, V2 | null]>> {\n return (\n stream: IStreamBuilder<T>,\n ): IStreamBuilder<KeyValue<K, [V1 | null, V2 | null]>> => {\n if (stream.graph !== other.graph) {\n throw new Error(`Cannot join streams from different graphs`)\n }\n const output = new StreamBuilder<KeyValue<K, [V1 | null, V2 | null]>>(\n stream.graph,\n new DifferenceStreamWriter<KeyValue<K, [V1 | null, V2 | null]>>(),\n )\n const operator = new JoinOperator<K, V1, V2>(\n stream.graph.getNextOperatorId(),\n stream.connectReader() as DifferenceStreamReader<KeyValue<K, V1>>,\n other.connectReader(),\n output.writer,\n type,\n )\n stream.graph.addOperator(operator)\n return output\n }\n}\n"],"names":[],"mappings":"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"}
@@ -1,37 +1,17 @@
1
- import { topKWithFractionalIndex } from './topKWithFractionalIndex.js';
2
1
  import { IStreamBuilder, KeyValue } from '../types.js';
2
+ import { SetSizeCallback } from './topKWithFractionalIndex.js';
3
3
  export interface OrderByOptions<Ve> {
4
4
  comparator?: (a: Ve, b: Ve) => number;
5
5
  limit?: number;
6
6
  offset?: number;
7
7
  }
8
8
  type OrderByWithFractionalIndexOptions<Ve> = OrderByOptions<Ve> & {
9
- setSizeCallback?: (getSize: () => number) => void;
9
+ setSizeCallback?: SetSizeCallback;
10
10
  setWindowFn?: (windowFn: (options: {
11
11
  offset?: number;
12
12
  limit?: number;
13
13
  }) => void) => void;
14
14
  };
15
- /**
16
- * Orders the elements and limits the number of results, with optional offset
17
- * This requires a keyed stream, and uses the `topK` operator to order all the elements.
18
- *
19
- * @param valueExtractor - A function that extracts the value to order by from the element
20
- * @param options - An optional object containing comparator, limit and offset properties
21
- * @returns A piped operator that orders the elements and limits the number of results
22
- */
23
- export declare function orderBy<T extends KeyValue<unknown, unknown>, Ve = unknown>(valueExtractor: (value: T extends KeyValue<unknown, infer V> ? V : never) => Ve, options?: OrderByOptions<Ve>): (stream: IStreamBuilder<T>) => IStreamBuilder<T>;
24
- /**
25
- * Orders the elements and limits the number of results, with optional offset and
26
- * annotates the value with the index.
27
- * This requires a keyed stream, and uses the `topKWithIndex` operator to order all the elements.
28
- *
29
- * @param valueExtractor - A function that extracts the value to order by from the element
30
- * @param options - An optional object containing comparator, limit and offset properties
31
- * @returns A piped operator that orders the elements and limits the number of results
32
- */
33
- export declare function orderByWithIndex<T extends KeyValue<unknown, unknown>, Ve = unknown>(valueExtractor: (value: T extends KeyValue<unknown, infer V> ? V : never) => Ve, options?: OrderByOptions<Ve>): (stream: IStreamBuilder<T>) => IStreamBuilder<KeyValue<T extends KeyValue<infer K, unknown> ? K : never, [T extends KeyValue<unknown, infer V> ? V : never, number]>>;
34
- export declare function orderByWithFractionalIndexBase<T extends KeyValue<unknown, unknown>, Ve = unknown>(topKFunction: typeof topKWithFractionalIndex, valueExtractor: (value: T extends KeyValue<unknown, infer V> ? V : never) => Ve, options?: OrderByWithFractionalIndexOptions<Ve>): (stream: IStreamBuilder<T>) => IStreamBuilder<[T extends KeyValue<infer K, unknown> ? K : never, [T extends KeyValue<unknown, infer V> ? V : never, string]]>;
35
15
  /**
36
16
  * Orders the elements and limits the number of results, with optional offset and
37
17
  * annotates the value with a fractional index.
@@ -1,71 +1,6 @@
1
- import { topK, topKWithIndex } from "./topK.js";
2
1
  import { topKWithFractionalIndex } from "./topKWithFractionalIndex.js";
3
- import { map } from "./map.js";
4
- import { innerJoin } from "./join.js";
5
2
  import { consolidate } from "./consolidate.js";
6
- function orderBy(valueExtractor, options) {
7
- const limit = options?.limit ?? Infinity;
8
- const offset = options?.offset ?? 0;
9
- const comparator = options?.comparator ?? ((a, b) => {
10
- if (a === b) return 0;
11
- if (a < b) return -1;
12
- return 1;
13
- });
14
- return (stream) => {
15
- return stream.pipe(
16
- map(
17
- ([key, value]) => [
18
- null,
19
- [
20
- key,
21
- valueExtractor(
22
- value
23
- )
24
- ]
25
- ]
26
- ),
27
- topK((a, b) => comparator(a[1], b[1]), { limit, offset }),
28
- map(([_, [key]]) => [key, null]),
29
- innerJoin(stream),
30
- map(([key, value]) => {
31
- return [key, value[1]];
32
- }),
33
- consolidate()
34
- );
35
- };
36
- }
37
- function orderByWithIndex(valueExtractor, options) {
38
- const limit = options?.limit ?? Infinity;
39
- const offset = options?.offset ?? 0;
40
- const comparator = options?.comparator ?? ((a, b) => {
41
- if (a === b) return 0;
42
- if (a < b) return -1;
43
- return 1;
44
- });
45
- return (stream) => {
46
- return stream.pipe(
47
- map(
48
- ([key, value]) => [
49
- null,
50
- [
51
- key,
52
- valueExtractor(
53
- value
54
- )
55
- ]
56
- ]
57
- ),
58
- topKWithIndex((a, b) => comparator(a[1], b[1]), { limit, offset }),
59
- map(([_, [[key], index]]) => [key, index]),
60
- innerJoin(stream),
61
- map(([key, [index, value]]) => {
62
- return [key, [value, index]];
63
- }),
64
- consolidate()
65
- );
66
- };
67
- }
68
- function orderByWithFractionalIndexBase(topKFunction, valueExtractor, options) {
3
+ function orderByWithFractionalIndex(valueExtractor, options) {
69
4
  const limit = options?.limit ?? Infinity;
70
5
  const offset = options?.offset ?? 0;
71
6
  const setSizeCallback = options?.setSizeCallback;
@@ -77,7 +12,7 @@ function orderByWithFractionalIndexBase(topKFunction, valueExtractor, options) {
77
12
  });
78
13
  return (stream) => {
79
14
  return stream.pipe(
80
- topKFunction(
15
+ topKWithFractionalIndex(
81
16
  (a, b) => comparator(valueExtractor(a), valueExtractor(b)),
82
17
  {
83
18
  limit,
@@ -90,17 +25,7 @@ function orderByWithFractionalIndexBase(topKFunction, valueExtractor, options) {
90
25
  );
91
26
  };
92
27
  }
93
- function orderByWithFractionalIndex(valueExtractor, options) {
94
- return orderByWithFractionalIndexBase(
95
- topKWithFractionalIndex,
96
- valueExtractor,
97
- options
98
- );
99
- }
100
28
  export {
101
- orderBy,
102
- orderByWithFractionalIndex,
103
- orderByWithFractionalIndexBase,
104
- orderByWithIndex
29
+ orderByWithFractionalIndex
105
30
  };
106
31
  //# sourceMappingURL=orderBy.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"orderBy.js","sources":["../../../src/operators/orderBy.ts"],"sourcesContent":["import { topK, topKWithIndex } from './topK.js'\nimport { topKWithFractionalIndex } from './topKWithFractionalIndex.js'\nimport { map } from './map.js'\nimport { innerJoin } from './join.js'\nimport { consolidate } from './consolidate.js'\nimport type { IStreamBuilder, KeyValue } from '../types.js'\n\nexport interface OrderByOptions<Ve> {\n comparator?: (a: Ve, b: Ve) => number\n limit?: number\n offset?: number\n}\n\ntype OrderByWithFractionalIndexOptions<Ve> = OrderByOptions<Ve> & {\n setSizeCallback?: (getSize: () => number) => void\n setWindowFn?: (\n windowFn: (options: { offset?: number; limit?: number }) => void,\n ) => void\n}\n\n/**\n * Orders the elements and limits the number of results, with optional offset\n * This requires a keyed stream, and uses the `topK` operator to order all the elements.\n *\n * @param valueExtractor - A function that extracts the value to order by from the element\n * @param options - An optional object containing comparator, limit and offset properties\n * @returns A piped operator that orders the elements and limits the number of results\n */\nexport function orderBy<T extends KeyValue<unknown, unknown>, Ve = unknown>(\n valueExtractor: (\n value: T extends KeyValue<unknown, infer V> ? V : never,\n ) => Ve,\n options?: OrderByOptions<Ve>,\n) {\n const limit = options?.limit ?? Infinity\n const offset = options?.offset ?? 0\n const comparator =\n options?.comparator ??\n ((a, b) => {\n // Default to JS like ordering\n if (a === b) return 0\n if (a < b) return -1\n return 1\n })\n\n return (stream: IStreamBuilder<T>): IStreamBuilder<T> => {\n type KeyType = T extends KeyValue<infer K, unknown> ? K : never\n\n return stream.pipe(\n map(\n ([key, value]) =>\n [\n null,\n [\n key,\n valueExtractor(\n value as T extends KeyValue<unknown, infer V> ? V : never,\n ),\n ],\n ] as KeyValue<null, [KeyType, Ve]>,\n ),\n topK((a, b) => comparator(a[1], b[1]), { limit, offset }),\n map(([_, [key]]) => [key, null] as KeyValue<KeyType, null>),\n innerJoin(stream),\n map(([key, value]) => {\n return [key, value[1]] as T\n }),\n consolidate(),\n )\n }\n}\n\n/**\n * Orders the elements and limits the number of results, with optional offset and\n * annotates the value with the index.\n * This requires a keyed stream, and uses the `topKWithIndex` operator to order all the elements.\n *\n * @param valueExtractor - A function that extracts the value to order by from the element\n * @param options - An optional object containing comparator, limit and offset properties\n * @returns A piped operator that orders the elements and limits the number of results\n */\nexport function orderByWithIndex<\n T extends KeyValue<unknown, unknown>,\n Ve = unknown,\n>(\n valueExtractor: (\n value: T extends KeyValue<unknown, infer V> ? V : never,\n ) => Ve,\n options?: OrderByOptions<Ve>,\n) {\n const limit = options?.limit ?? Infinity\n const offset = options?.offset ?? 0\n const comparator =\n options?.comparator ??\n ((a, b) => {\n // Default to JS like ordering\n if (a === b) return 0\n if (a < b) return -1\n return 1\n })\n\n return (\n stream: IStreamBuilder<T>,\n ): IStreamBuilder<\n KeyValue<\n T extends KeyValue<infer K, unknown> ? K : never,\n [T extends KeyValue<unknown, infer V> ? V : never, number]\n >\n > => {\n type KeyType = T extends KeyValue<infer K, unknown> ? K : never\n type ValueType = T extends KeyValue<unknown, infer V> ? V : never\n\n return stream.pipe(\n map(\n ([key, value]) =>\n [\n null,\n [\n key,\n valueExtractor(\n value as T extends KeyValue<unknown, infer V> ? V : never,\n ),\n ],\n ] as KeyValue<null, [KeyType, Ve]>,\n ),\n topKWithIndex((a, b) => comparator(a[1], b[1]), { limit, offset }),\n map(([_, [[key], index]]) => [key, index] as KeyValue<KeyType, number>),\n innerJoin(stream),\n map(([key, [index, value]]) => {\n return [key, [value, index]] as KeyValue<KeyType, [ValueType, number]>\n }),\n consolidate(),\n )\n }\n}\n\nexport function orderByWithFractionalIndexBase<\n T extends KeyValue<unknown, unknown>,\n Ve = unknown,\n>(\n topKFunction: typeof topKWithFractionalIndex,\n valueExtractor: (\n value: T extends KeyValue<unknown, infer V> ? V : never,\n ) => Ve,\n options?: OrderByWithFractionalIndexOptions<Ve>,\n) {\n type KeyType = T extends KeyValue<infer K, unknown> ? K : never\n type ValueType = T extends KeyValue<unknown, infer V> ? V : never\n\n const limit = options?.limit ?? Infinity\n const offset = options?.offset ?? 0\n const setSizeCallback = options?.setSizeCallback\n const setWindowFn = options?.setWindowFn\n const comparator =\n options?.comparator ??\n ((a, b) => {\n // Default to JS like ordering\n if (a === b) return 0\n if (a < b) return -1\n return 1\n })\n\n return (\n stream: IStreamBuilder<T>,\n ): IStreamBuilder<[KeyType, [ValueType, string]]> => {\n return stream.pipe(\n topKFunction(\n (a: ValueType, b: ValueType) =>\n comparator(valueExtractor(a), valueExtractor(b)),\n {\n limit,\n offset,\n setSizeCallback,\n setWindowFn,\n },\n ),\n consolidate(),\n )\n }\n}\n\n/**\n * Orders the elements and limits the number of results, with optional offset and\n * annotates the value with a fractional index.\n * This requires a keyed stream, and uses the `topKWithFractionalIndex` operator to order all the elements.\n *\n * @param valueExtractor - A function that extracts the value to order by from the element\n * @param options - An optional object containing comparator, limit and offset properties\n * @returns A piped operator that orders the elements and limits the number of results\n */\nexport function orderByWithFractionalIndex<\n T extends KeyValue<unknown, unknown>,\n Ve = unknown,\n>(\n valueExtractor: (\n value: T extends KeyValue<unknown, infer V> ? V : never,\n ) => Ve,\n options?: OrderByWithFractionalIndexOptions<Ve>,\n) {\n return orderByWithFractionalIndexBase(\n topKWithFractionalIndex,\n valueExtractor,\n options,\n )\n}\n"],"names":[],"mappings":";;;;;AA4BO,SAAS,QACd,gBAGA,SACA;AACA,QAAM,QAAQ,SAAS,SAAS;AAChC,QAAM,SAAS,SAAS,UAAU;AAClC,QAAM,aACJ,SAAS,eACR,CAAC,GAAG,MAAM;AAET,QAAI,MAAM,EAAG,QAAO;AACpB,QAAI,IAAI,EAAG,QAAO;AAClB,WAAO;AAAA,EACT;AAEF,SAAO,CAAC,WAAiD;AAGvD,WAAO,OAAO;AAAA,MACZ;AAAA,QACE,CAAC,CAAC,KAAK,KAAK,MACV;AAAA,UACE;AAAA,UACA;AAAA,YACE;AAAA,YACA;AAAA,cACE;AAAA,YAAA;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,MAEJ,KAAK,CAAC,GAAG,MAAM,WAAW,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,OAAO,QAAQ;AAAA,MACxD,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,IAAI,CAA4B;AAAA,MAC1D,UAAU,MAAM;AAAA,MAChB,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM;AACpB,eAAO,CAAC,KAAK,MAAM,CAAC,CAAC;AAAA,MACvB,CAAC;AAAA,MACD,YAAA;AAAA,IAAY;AAAA,EAEhB;AACF;AAWO,SAAS,iBAId,gBAGA,SACA;AACA,QAAM,QAAQ,SAAS,SAAS;AAChC,QAAM,SAAS,SAAS,UAAU;AAClC,QAAM,aACJ,SAAS,eACR,CAAC,GAAG,MAAM;AAET,QAAI,MAAM,EAAG,QAAO;AACpB,QAAI,IAAI,EAAG,QAAO;AAClB,WAAO;AAAA,EACT;AAEF,SAAO,CACL,WAMG;AAIH,WAAO,OAAO;AAAA,MACZ;AAAA,QACE,CAAC,CAAC,KAAK,KAAK,MACV;AAAA,UACE;AAAA,UACA;AAAA,YACE;AAAA,YACA;AAAA,cACE;AAAA,YAAA;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,MAEJ,cAAc,CAAC,GAAG,MAAM,WAAW,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,OAAO,QAAQ;AAAA,MACjE,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,KAAK,CAA8B;AAAA,MACtE,UAAU,MAAM;AAAA,MAChB,IAAI,CAAC,CAAC,KAAK,CAAC,OAAO,KAAK,CAAC,MAAM;AAC7B,eAAO,CAAC,KAAK,CAAC,OAAO,KAAK,CAAC;AAAA,MAC7B,CAAC;AAAA,MACD,YAAA;AAAA,IAAY;AAAA,EAEhB;AACF;AAEO,SAAS,+BAId,cACA,gBAGA,SACA;AAIA,QAAM,QAAQ,SAAS,SAAS;AAChC,QAAM,SAAS,SAAS,UAAU;AAClC,QAAM,kBAAkB,SAAS;AACjC,QAAM,cAAc,SAAS;AAC7B,QAAM,aACJ,SAAS,eACR,CAAC,GAAG,MAAM;AAET,QAAI,MAAM,EAAG,QAAO;AACpB,QAAI,IAAI,EAAG,QAAO;AAClB,WAAO;AAAA,EACT;AAEF,SAAO,CACL,WACmD;AACnD,WAAO,OAAO;AAAA,MACZ;AAAA,QACE,CAAC,GAAc,MACb,WAAW,eAAe,CAAC,GAAG,eAAe,CAAC,CAAC;AAAA,QACjD;AAAA,UACE;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QAAA;AAAA,MACF;AAAA,MAEF,YAAA;AAAA,IAAY;AAAA,EAEhB;AACF;AAWO,SAAS,2BAId,gBAGA,SACA;AACA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;"}
1
+ {"version":3,"file":"orderBy.js","sources":["../../../src/operators/orderBy.ts"],"sourcesContent":["import { topKWithFractionalIndex } from './topKWithFractionalIndex.js'\nimport { consolidate } from './consolidate.js'\nimport type { IStreamBuilder, KeyValue } from '../types.js'\nimport type { SetSizeCallback } from './topKWithFractionalIndex.js'\n\nexport interface OrderByOptions<Ve> {\n comparator?: (a: Ve, b: Ve) => number\n limit?: number\n offset?: number\n}\n\ntype OrderByWithFractionalIndexOptions<Ve> = OrderByOptions<Ve> & {\n setSizeCallback?: SetSizeCallback\n setWindowFn?: (\n windowFn: (options: { offset?: number; limit?: number }) => void,\n ) => void\n}\n\n/**\n * Orders the elements and limits the number of results, with optional offset and\n * annotates the value with a fractional index.\n * This requires a keyed stream, and uses the `topKWithFractionalIndex` operator to order all the elements.\n *\n * @param valueExtractor - A function that extracts the value to order by from the element\n * @param options - An optional object containing comparator, limit and offset properties\n * @returns A piped operator that orders the elements and limits the number of results\n */\nexport function orderByWithFractionalIndex<\n T extends KeyValue<unknown, unknown>,\n Ve = unknown,\n>(\n valueExtractor: (\n value: T extends KeyValue<unknown, infer V> ? V : never,\n ) => Ve,\n options?: OrderByWithFractionalIndexOptions<Ve>,\n) {\n type KeyType = T extends KeyValue<infer K, unknown> ? K : never\n type ValueType = T extends KeyValue<unknown, infer V> ? V : never\n\n const limit = options?.limit ?? Infinity\n const offset = options?.offset ?? 0\n const setSizeCallback = options?.setSizeCallback\n const setWindowFn = options?.setWindowFn\n const comparator =\n options?.comparator ??\n ((a, b) => {\n // Default to JS like ordering\n if (a === b) return 0\n if (a < b) return -1\n return 1\n })\n\n return (\n stream: IStreamBuilder<T>,\n ): IStreamBuilder<[KeyType, [ValueType, string]]> => {\n return stream.pipe(\n topKWithFractionalIndex(\n (a: ValueType, b: ValueType) =>\n comparator(valueExtractor(a), valueExtractor(b)),\n {\n limit,\n offset,\n setSizeCallback,\n setWindowFn,\n },\n ),\n consolidate(),\n )\n }\n}\n"],"names":[],"mappings":";;AA2BO,SAAS,2BAId,gBAGA,SACA;AAIA,QAAM,QAAQ,SAAS,SAAS;AAChC,QAAM,SAAS,SAAS,UAAU;AAClC,QAAM,kBAAkB,SAAS;AACjC,QAAM,cAAc,SAAS;AAC7B,QAAM,aACJ,SAAS,eACR,CAAC,GAAG,MAAM;AAET,QAAI,MAAM,EAAG,QAAO;AACpB,QAAI,IAAI,EAAG,QAAO;AAClB,WAAO;AAAA,EACT;AAEF,SAAO,CACL,WACmD;AACnD,WAAO,OAAO;AAAA,MACZ;AAAA,QACE,CAAC,GAAc,MACb,WAAW,eAAe,CAAC,GAAG,eAAe,CAAC,CAAC;AAAA,QACjD;AAAA,UACE;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QAAA;AAAA,MACF;AAAA,MAEF,YAAA;AAAA,IAAY;AAAA,EAEhB;AACF;"}
@@ -5,10 +5,19 @@ import { IStreamBuilder, KeyValue } from '../types.js';
5
5
  */
6
6
  export declare class ReduceOperator<K, V1, V2> extends UnaryOperator<[K, V1], [K, V2]> {
7
7
  #private;
8
- constructor(id: number, inputA: DifferenceStreamReader<[K, V1]>, output: DifferenceStreamWriter<[K, V2]>, f: (values: Array<[V1, number]>) => Array<[V2, number]>);
8
+ constructor(id: number, inputA: DifferenceStreamReader<[K, V1]>, output: DifferenceStreamWriter<[K, V2]>, f: (values: Array<[V1, number]>) => Array<[V2, number]>, options?: ReduceOptions);
9
9
  run(): void;
10
10
  }
11
+ /** @internal groupBy's option; its producer must keep the identity exact. */
12
+ export type ReduceOptions = {
13
+ /**
14
+ * Input values are `[identity, value]` with a string or number identity
15
+ * that determines the value: two inputs under one key with an equal
16
+ * identity are equal. They merge without a structural hash.
17
+ */
18
+ prefixIdentity?: boolean;
19
+ };
11
20
  /**
12
21
  * Reduces the elements in the stream by key (version-free)
13
22
  */
14
- export declare function reduce<KType extends T extends KeyValue<infer K, infer _V> ? K : never, V1Type extends T extends KeyValue<KType, infer V> ? V : never, R, T>(f: (values: Array<[V1Type, number]>) => Array<[R, number]>): (stream: IStreamBuilder<T>) => IStreamBuilder<KeyValue<KType, R>>;
23
+ export declare function reduce<KType extends (T extends KeyValue<infer K, infer _V> ? K : never), V1Type extends (T extends KeyValue<KType, infer V> ? V : never), R, T>(f: (values: Array<[V1Type, number]>) => Array<[R, number]>, options?: ReduceOptions): (stream: IStreamBuilder<T>) => IStreamBuilder<KeyValue<KType, R>>;