@tanstack/db-ivm 0.1.10 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/operators/orderBy.cjs +3 -1
- package/dist/cjs/operators/orderBy.cjs.map +1 -1
- package/dist/cjs/operators/orderBy.d.cts +4 -0
- package/dist/cjs/operators/topKWithFractionalIndex.cjs +64 -10
- package/dist/cjs/operators/topKWithFractionalIndex.cjs.map +1 -1
- package/dist/cjs/operators/topKWithFractionalIndex.d.cts +22 -0
- package/dist/cjs/utils.cjs +21 -0
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +11 -0
- package/dist/esm/operators/orderBy.d.ts +4 -0
- package/dist/esm/operators/orderBy.js +3 -1
- package/dist/esm/operators/orderBy.js.map +1 -1
- package/dist/esm/operators/topKWithFractionalIndex.d.ts +22 -0
- package/dist/esm/operators/topKWithFractionalIndex.js +65 -11
- package/dist/esm/operators/topKWithFractionalIndex.js.map +1 -1
- package/dist/esm/utils.d.ts +11 -0
- package/dist/esm/utils.js +21 -0
- package/dist/esm/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/operators/orderBy.ts +5 -0
- package/src/operators/topKWithFractionalIndex.ts +111 -13
- package/src/utils.ts +33 -0
|
@@ -71,6 +71,7 @@ function orderByWithFractionalIndexBase(topKFunction, valueExtractor, options) {
|
|
|
71
71
|
const limit = options?.limit ?? Infinity;
|
|
72
72
|
const offset = options?.offset ?? 0;
|
|
73
73
|
const setSizeCallback = options?.setSizeCallback;
|
|
74
|
+
const setWindowFn = options?.setWindowFn;
|
|
74
75
|
const comparator = options?.comparator ?? ((a, b) => {
|
|
75
76
|
if (a === b) return 0;
|
|
76
77
|
if (a < b) return -1;
|
|
@@ -83,7 +84,8 @@ function orderByWithFractionalIndexBase(topKFunction, valueExtractor, options) {
|
|
|
83
84
|
{
|
|
84
85
|
limit,
|
|
85
86
|
offset,
|
|
86
|
-
setSizeCallback
|
|
87
|
+
setSizeCallback,
|
|
88
|
+
setWindowFn
|
|
87
89
|
}
|
|
88
90
|
),
|
|
89
91
|
consolidate.consolidate()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"orderBy.cjs","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}\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 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 }\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":["map","topK","innerJoin","consolidate","topKWithIndex","topKWithFractionalIndex"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"orderBy.cjs","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":["map","topK","innerJoin","consolidate","topKWithIndex","topKWithFractionalIndex"],"mappings":"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"}
|
|
@@ -7,6 +7,10 @@ export interface OrderByOptions<Ve> {
|
|
|
7
7
|
}
|
|
8
8
|
type OrderByWithFractionalIndexOptions<Ve> = OrderByOptions<Ve> & {
|
|
9
9
|
setSizeCallback?: (getSize: () => number) => void;
|
|
10
|
+
setWindowFn?: (windowFn: (options: {
|
|
11
|
+
offset?: number;
|
|
12
|
+
limit?: number;
|
|
13
|
+
}) => void) => void;
|
|
10
14
|
};
|
|
11
15
|
/**
|
|
12
16
|
* Orders the elements and limits the number of results, with optional offset
|
|
@@ -21,6 +21,36 @@ class TopKArray {
|
|
|
21
21
|
const available = this.#sortedValues.length - offset;
|
|
22
22
|
return Math.max(0, Math.min(limit, available));
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Moves the topK window
|
|
26
|
+
*/
|
|
27
|
+
move({
|
|
28
|
+
offset,
|
|
29
|
+
limit
|
|
30
|
+
}) {
|
|
31
|
+
const oldOffset = this.#topKStart;
|
|
32
|
+
const oldLimit = this.#topKEnd - this.#topKStart;
|
|
33
|
+
const oldRange = [this.#topKStart, this.#topKEnd];
|
|
34
|
+
this.#topKStart = offset ?? oldOffset;
|
|
35
|
+
this.#topKEnd = this.#topKStart + (limit ?? oldLimit);
|
|
36
|
+
const newRange = [this.#topKStart, this.#topKEnd];
|
|
37
|
+
const { onlyInA, onlyInB } = utils.diffHalfOpen(oldRange, newRange);
|
|
38
|
+
const moveIns = [];
|
|
39
|
+
onlyInB.forEach((index) => {
|
|
40
|
+
const value = this.#sortedValues[index];
|
|
41
|
+
if (value) {
|
|
42
|
+
moveIns.push(value);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
const moveOuts = [];
|
|
46
|
+
onlyInA.forEach((index) => {
|
|
47
|
+
const value = this.#sortedValues[index];
|
|
48
|
+
if (value) {
|
|
49
|
+
moveOuts.push(value);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
return { moveIns, moveOuts, changes: onlyInA.length + onlyInB.length > 0 };
|
|
53
|
+
}
|
|
24
54
|
insert(value) {
|
|
25
55
|
const result = { moveIn: null, moveOut: null };
|
|
26
56
|
const index = this.#findIndex(value);
|
|
@@ -86,10 +116,9 @@ class TopKWithFractionalIndexOperator extends graph.UnaryOperator {
|
|
|
86
116
|
* and returns changes to the topK.
|
|
87
117
|
*/
|
|
88
118
|
#topK;
|
|
89
|
-
#limit;
|
|
90
119
|
constructor(id, inputA, output, comparator, options) {
|
|
91
120
|
super(id, inputA, output);
|
|
92
|
-
|
|
121
|
+
const limit = options.limit ?? Infinity;
|
|
93
122
|
const offset = options.offset ?? 0;
|
|
94
123
|
const compareTaggedValues = (a, b) => {
|
|
95
124
|
const valueComparison = comparator(getVal(a), getVal(b));
|
|
@@ -100,12 +129,31 @@ class TopKWithFractionalIndexOperator extends graph.UnaryOperator {
|
|
|
100
129
|
const tieBreakerB = getTag(b);
|
|
101
130
|
return tieBreakerA - tieBreakerB;
|
|
102
131
|
};
|
|
103
|
-
this.#topK = this.createTopK(offset,
|
|
132
|
+
this.#topK = this.createTopK(offset, limit, compareTaggedValues);
|
|
104
133
|
options.setSizeCallback?.(() => this.#topK.size);
|
|
134
|
+
options.setWindowFn?.(this.moveTopK.bind(this));
|
|
105
135
|
}
|
|
106
136
|
createTopK(offset, limit, comparator) {
|
|
107
137
|
return new TopKArray(offset, limit, comparator);
|
|
108
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Moves the topK window based on the provided offset and limit.
|
|
141
|
+
* Any changes to the topK are sent to the output.
|
|
142
|
+
*/
|
|
143
|
+
moveTopK({ offset, limit }) {
|
|
144
|
+
if (!(this.#topK instanceof TopKArray)) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Cannot move B+-tree implementation of TopK with fractional index`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
const result = [];
|
|
150
|
+
const diff = this.#topK.move({ offset, limit });
|
|
151
|
+
diff.moveIns.forEach((moveIn) => this.handleMoveIn(moveIn, result));
|
|
152
|
+
diff.moveOuts.forEach((moveOut) => this.handleMoveOut(moveOut, result));
|
|
153
|
+
if (diff.changes) {
|
|
154
|
+
this.output.sendData(new multiset.MultiSet(result));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
109
157
|
run() {
|
|
110
158
|
const result = [];
|
|
111
159
|
for (const message of this.inputMessages()) {
|
|
@@ -131,21 +179,27 @@ class TopKWithFractionalIndexOperator extends graph.UnaryOperator {
|
|
|
131
179
|
const taggedValue = tagValue(key, value);
|
|
132
180
|
res = this.#topK.delete(taggedValue);
|
|
133
181
|
} else ;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
182
|
+
this.handleMoveIn(res.moveIn, result);
|
|
183
|
+
this.handleMoveOut(res.moveOut, result);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
handleMoveIn(moveIn, result) {
|
|
187
|
+
if (moveIn) {
|
|
188
|
+
const index = getIndex(moveIn);
|
|
189
|
+
const taggedValue = getValue(moveIn);
|
|
137
190
|
const k = getKey(taggedValue);
|
|
138
191
|
const val = getVal(taggedValue);
|
|
139
192
|
result.push([[k, [val, index]], 1]);
|
|
140
193
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
194
|
+
}
|
|
195
|
+
handleMoveOut(moveOut, result) {
|
|
196
|
+
if (moveOut) {
|
|
197
|
+
const index = getIndex(moveOut);
|
|
198
|
+
const taggedValue = getValue(moveOut);
|
|
144
199
|
const k = getKey(taggedValue);
|
|
145
200
|
const val = getVal(taggedValue);
|
|
146
201
|
result.push([[k, [val, index]], -1]);
|
|
147
202
|
}
|
|
148
|
-
return;
|
|
149
203
|
}
|
|
150
204
|
getMultiplicity(key) {
|
|
151
205
|
return this.#index.get(key) ?? 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"topKWithFractionalIndex.cjs","sources":["../../../src/operators/topKWithFractionalIndex.ts"],"sourcesContent":["import { generateKeyBetween } from \"fractional-indexing\"\nimport { DifferenceStreamWriter, UnaryOperator } from \"../graph.js\"\nimport { StreamBuilder } from \"../d2.js\"\nimport { MultiSet } from \"../multiset.js\"\nimport { binarySearch, globalObjectIdGenerator } from \"../utils.js\"\nimport type { DifferenceStreamReader } from \"../graph.js\"\nimport type { IStreamBuilder, PipedOperator } from \"../types.js\"\n\nexport interface TopKWithFractionalIndexOptions {\n limit?: number\n offset?: number\n setSizeCallback?: (getSize: () => number) => void\n}\n\nexport type TopKChanges<V> = {\n /** Indicates which element moves into the topK (if any) */\n moveIn: IndexedValue<V> | null\n /** Indicates which element moves out of the topK (if any) */\n moveOut: IndexedValue<V> | null\n}\n\n/**\n * A topK data structure that supports insertions and deletions\n * and returns changes to the topK.\n */\nexport interface TopK<V> {\n size: number\n insert: (value: V) => TopKChanges<V>\n delete: (value: V) => TopKChanges<V>\n}\n\n/**\n * Implementation of a topK data structure.\n * Uses a sorted array internally to store the values and keeps a topK window over that array.\n * Inserts and deletes are O(n) operations because worst case an element is inserted/deleted\n * at the start of the array which causes all the elements to shift to the right/left.\n */\nclass TopKArray<V> implements TopK<V> {\n #sortedValues: Array<IndexedValue<V>> = []\n #comparator: (a: V, b: V) => number\n #topKStart: number\n #topKEnd: number\n\n constructor(\n offset: number,\n limit: number,\n comparator: (a: V, b: V) => number\n ) {\n this.#topKStart = offset\n this.#topKEnd = offset + limit\n this.#comparator = comparator\n }\n\n get size(): number {\n const offset = this.#topKStart\n const limit = this.#topKEnd - this.#topKStart\n const available = this.#sortedValues.length - offset\n return Math.max(0, Math.min(limit, available))\n }\n\n insert(value: V): TopKChanges<V> {\n const result: TopKChanges<V> = { moveIn: null, moveOut: null }\n\n // Lookup insert position\n const index = this.#findIndex(value)\n // Generate fractional index based on the fractional indices of the elements before and after it\n const indexBefore =\n index === 0 ? null : getIndex(this.#sortedValues[index - 1]!)\n const indexAfter =\n index === this.#sortedValues.length\n ? null\n : getIndex(this.#sortedValues[index]!)\n const fractionalIndex = generateKeyBetween(indexBefore, indexAfter)\n\n // Insert the value at the correct position\n const val = indexedValue(value, fractionalIndex)\n // Splice is O(n) where n = all elements in the collection (i.e. n >= k) !\n this.#sortedValues.splice(index, 0, val)\n\n // Check if the topK changed\n if (index < this.#topKEnd) {\n // The inserted element is either before the top K or within the top K\n // If it is before the top K then it moves the element that was right before the topK into the topK\n // If it is within the top K then the inserted element moves into the top K\n // In both cases the last element of the old top K now moves out of the top K\n const moveInIndex = Math.max(index, this.#topKStart)\n if (moveInIndex < this.#sortedValues.length) {\n // We actually have a topK\n // because in some cases there may not be enough elements in the array to reach the start of the topK\n // e.g. [1, 2, 3] with K = 2 and offset = 3 does not have a topK\n result.moveIn = this.#sortedValues[moveInIndex]!\n\n // We need to remove the element that falls out of the top K\n // The element that falls out of the top K has shifted one to the right\n // because of the element we inserted, so we find it at index topKEnd\n if (this.#topKEnd < this.#sortedValues.length) {\n result.moveOut = this.#sortedValues[this.#topKEnd]!\n }\n }\n }\n\n return result\n }\n\n /**\n * Deletes a value that may or may not be in the topK.\n * IMPORTANT: this assumes that the value is present in the collection\n * if it's not the case it will remove the element\n * that is on the position where the provided `value` would be.\n */\n delete(value: V): TopKChanges<V> {\n const result: TopKChanges<V> = { moveIn: null, moveOut: null }\n\n // Lookup delete position\n const index = this.#findIndex(value)\n // Remove the value at that position\n const [removedElem] = this.#sortedValues.splice(index, 1)\n\n // Check if the topK changed\n if (index < this.#topKEnd) {\n // The removed element is either before the top K or within the top K\n // If it is before the top K then the first element of the topK moves out of the topK\n // If it is within the top K then the removed element moves out of the topK\n result.moveOut = removedElem!\n if (index < this.#topKStart) {\n // The removed element is before the topK\n // so actually, the first element of the topK moves out of the topK\n // and not the element that we removed\n // The first element of the topK is now at index topKStart - 1\n // since we removed an element before the topK\n const moveOutIndex = this.#topKStart - 1\n if (moveOutIndex < this.#sortedValues.length) {\n result.moveOut = this.#sortedValues[moveOutIndex]!\n } else {\n // No value is moving out of the topK\n // because there are no elements in the topK\n result.moveOut = null\n }\n }\n\n // Since we removed an element that was before or in the topK\n // the first element after the topK moved one position to the left\n // and thus falls into the topK now\n const moveInIndex = this.#topKEnd - 1\n if (moveInIndex < this.#sortedValues.length) {\n result.moveIn = this.#sortedValues[moveInIndex]!\n }\n }\n\n return result\n }\n\n // TODO: see if there is a way to refactor the code for insert and delete in the topK above\n // because they are very similar, one is shifting the topK window to the left and the other is shifting it to the right\n // so i have the feeling there is a common pattern here and we can implement both cases using that pattern\n\n #findIndex(value: V): number {\n return binarySearch(this.#sortedValues, indexedValue(value, ``), (a, b) =>\n this.#comparator(getValue(a), getValue(b))\n )\n }\n}\n\n/**\n * Operator for fractional indexed topK operations\n * This operator maintains fractional indices for sorted elements\n * and only updates indices when elements move position\n */\nexport class TopKWithFractionalIndexOperator<K, T> extends UnaryOperator<\n [K, T],\n [K, IndexedValue<T>]\n> {\n #index: Map<K, number> = new Map() // maps keys to their multiplicity\n\n /**\n * topK data structure that supports insertions and deletions\n * and returns changes to the topK.\n */\n #topK: TopK<TaggedValue<K, T>>\n\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: TopKWithFractionalIndexOptions\n ) {\n super(id, inputA, output)\n this.#limit = options.limit ?? Infinity\n const offset = options.offset ?? 0\n const compareTaggedValues = (\n a: TaggedValue<K, T>,\n b: TaggedValue<K, T>\n ) => {\n // First compare on the value\n const valueComparison = comparator(getVal(a), getVal(b))\n if (valueComparison !== 0) {\n return valueComparison\n }\n // If the values are equal, compare on the tag (object identity)\n const tieBreakerA = getTag(a)\n const tieBreakerB = getTag(b)\n return tieBreakerA - tieBreakerB\n }\n this.#topK = this.createTopK(offset, this.#limit, compareTaggedValues)\n options.setSizeCallback?.(() => this.#topK.size)\n }\n\n protected createTopK(\n offset: number,\n limit: number,\n comparator: (a: TaggedValue<K, T>, b: TaggedValue<K, T>) => number\n ): TopK<TaggedValue<K, T>> {\n return new TopKArray(offset, limit, comparator)\n }\n\n run(): void {\n const result: Array<[[K, IndexedValue<T>], number]> = []\n for (const message of this.inputMessages()) {\n for (const [item, multiplicity] of message.getInner()) {\n const [key, value] = item\n this.processElement(key, value, multiplicity, result)\n }\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 { oldMultiplicity, newMultiplicity } = this.addKey(key, multiplicity)\n\n let res: TopKChanges<TaggedValue<K, T>> = {\n moveIn: null,\n moveOut: null,\n }\n if (oldMultiplicity <= 0 && newMultiplicity > 0) {\n // The value was invisible but should now be visible\n // Need to insert it into the array of sorted values\n const taggedValue = tagValue(key, value)\n res = this.#topK.insert(taggedValue)\n } else if (oldMultiplicity > 0 && newMultiplicity <= 0) {\n // The value was visible but should now be invisible\n // Need to remove it from the array of sorted values\n const taggedValue = tagValue(key, value)\n res = this.#topK.delete(taggedValue)\n } else {\n // The value was invisible and it remains invisible\n // or it was visible and remains visible\n // so it doesn't affect the topK\n }\n\n if (res.moveIn) {\n const index = getIndex(res.moveIn)\n const taggedValue = getValue(res.moveIn)\n const k = getKey(taggedValue)\n const val = getVal(taggedValue)\n result.push([[k, [val, index]], 1])\n }\n\n if (res.moveOut) {\n const index = getIndex(res.moveOut)\n const taggedValue = getValue(res.moveOut)\n const k = getKey(taggedValue)\n const val = getVal(taggedValue)\n result.push([[k, [val, index]], -1])\n }\n\n return\n }\n\n private getMultiplicity(key: K): number {\n return this.#index.get(key) ?? 0\n }\n\n private addKey(\n key: K,\n multiplicity: number\n ): { oldMultiplicity: number; newMultiplicity: number } {\n const oldMultiplicity = this.getMultiplicity(key)\n const newMultiplicity = oldMultiplicity + multiplicity\n if (newMultiplicity === 0) {\n this.#index.delete(key)\n } else {\n this.#index.set(key, newMultiplicity)\n }\n return { oldMultiplicity, newMultiplicity }\n }\n}\n\n/**\n * Limits the number of results 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 * @param comparator - A function that compares two elements\n * @param options - An optional object containing limit and offset properties\n * @returns A piped operator that orders the elements and limits the number of results\n */\nexport function topKWithFractionalIndex<KType, T>(\n comparator: (a: T, b: T) => number,\n options?: TopKWithFractionalIndexOptions\n): PipedOperator<[KType, T], [KType, IndexedValue<T>]> {\n const opts = options || {}\n\n return (\n stream: IStreamBuilder<[KType, T]>\n ): IStreamBuilder<[KType, IndexedValue<T>]> => {\n const output = new StreamBuilder<[KType, IndexedValue<T>]>(\n stream.graph,\n new DifferenceStreamWriter<[KType, IndexedValue<T>]>()\n )\n const operator = new TopKWithFractionalIndexOperator<KType, T>(\n stream.graph.getNextOperatorId(),\n stream.connectReader(),\n output.writer,\n comparator,\n opts\n )\n stream.graph.addOperator(operator)\n return output\n }\n}\n\n// Abstraction for fractionally indexed values\nexport type FractionalIndex = string\nexport type IndexedValue<V> = [V, FractionalIndex]\n\nexport function indexedValue<V>(\n value: V,\n index: FractionalIndex\n): IndexedValue<V> {\n return [value, index]\n}\n\nexport function getValue<V>(indexedVal: IndexedValue<V>): V {\n return indexedVal[0]\n}\n\nexport function getIndex<V>(indexedVal: IndexedValue<V>): FractionalIndex {\n return indexedVal[1]\n}\n\nexport type Tag = number\nexport type TaggedValue<K, V> = [K, V, Tag]\n\nfunction tagValue<K, V>(key: K, value: V): TaggedValue<K, V> {\n return [key, value, globalObjectIdGenerator.getId(key)]\n}\n\nfunction getKey<K, V>(tieBreakerTaggedValue: TaggedValue<K, V>): K {\n return tieBreakerTaggedValue[0]\n}\n\nfunction getVal<K, V>(tieBreakerTaggedValue: TaggedValue<K, V>): V {\n return tieBreakerTaggedValue[1]\n}\n\nfunction getTag<K, V>(tieBreakerTaggedValue: TaggedValue<K, V>): Tag {\n return tieBreakerTaggedValue[2]\n}\n"],"names":["generateKeyBetween","binarySearch","UnaryOperator","MultiSet","StreamBuilder","DifferenceStreamWriter","globalObjectIdGenerator"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"topKWithFractionalIndex.cjs","sources":["../../../src/operators/topKWithFractionalIndex.ts"],"sourcesContent":["import { generateKeyBetween } from \"fractional-indexing\"\nimport { DifferenceStreamWriter, UnaryOperator } from \"../graph.js\"\nimport { StreamBuilder } from \"../d2.js\"\nimport { MultiSet } from \"../multiset.js\"\nimport {\n binarySearch,\n diffHalfOpen,\n globalObjectIdGenerator,\n} from \"../utils.js\"\nimport type { HRange } from \"../utils.js\"\nimport type { DifferenceStreamReader } from \"../graph.js\"\nimport type { IStreamBuilder, PipedOperator } from \"../types.js\"\n\nexport interface TopKWithFractionalIndexOptions {\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\nexport type TopKChanges<V> = {\n /** Indicates which element moves into the topK (if any) */\n moveIn: IndexedValue<V> | null\n /** Indicates which element moves out of the topK (if any) */\n moveOut: IndexedValue<V> | null\n}\n\nexport type TopKMoveChanges<V> = {\n /** Flag that marks whether there were any changes to the topK */\n changes: boolean\n /** Indicates which elements move into the topK (if any) */\n moveIns: Array<IndexedValue<V>>\n /** Indicates which elements move out of the topK (if any) */\n moveOuts: Array<IndexedValue<V>>\n}\n\n/**\n * A topK data structure that supports insertions and deletions\n * and returns changes to the topK.\n */\nexport interface TopK<V> {\n size: number\n insert: (value: V) => TopKChanges<V>\n delete: (value: V) => TopKChanges<V>\n}\n\n/**\n * Implementation of a topK data structure.\n * Uses a sorted array internally to store the values and keeps a topK window over that array.\n * Inserts and deletes are O(n) operations because worst case an element is inserted/deleted\n * at the start of the array which causes all the elements to shift to the right/left.\n */\nclass TopKArray<V> implements TopK<V> {\n #sortedValues: Array<IndexedValue<V>> = []\n #comparator: (a: V, b: V) => number\n #topKStart: number\n #topKEnd: number\n\n constructor(\n offset: number,\n limit: number,\n comparator: (a: V, b: V) => number\n ) {\n this.#topKStart = offset\n this.#topKEnd = offset + limit\n this.#comparator = comparator\n }\n\n get size(): number {\n const offset = this.#topKStart\n const limit = this.#topKEnd - this.#topKStart\n const available = this.#sortedValues.length - offset\n return Math.max(0, Math.min(limit, available))\n }\n\n /**\n * Moves the topK window\n */\n move({\n offset,\n limit,\n }: {\n offset?: number\n limit?: number\n }): TopKMoveChanges<V> {\n const oldOffset = this.#topKStart\n const oldLimit = this.#topKEnd - this.#topKStart\n const oldRange: HRange = [this.#topKStart, this.#topKEnd]\n\n this.#topKStart = offset ?? oldOffset\n this.#topKEnd = this.#topKStart + (limit ?? oldLimit)\n\n const newRange: HRange = [this.#topKStart, this.#topKEnd]\n const { onlyInA, onlyInB } = diffHalfOpen(oldRange, newRange)\n\n const moveIns: Array<IndexedValue<V>> = []\n onlyInB.forEach((index) => {\n const value = this.#sortedValues[index]\n if (value) {\n moveIns.push(value)\n }\n })\n\n const moveOuts: Array<IndexedValue<V>> = []\n onlyInA.forEach((index) => {\n const value = this.#sortedValues[index]\n if (value) {\n moveOuts.push(value)\n }\n })\n\n // It could be that there are changes (i.e. moveIns or moveOuts)\n // but that the collection is lazy so we don't have the data yet that needs to move in/out\n // so `moveIns` and `moveOuts` will be empty but `changes` will be true\n // this will tell the caller that it needs to run the graph to load more data\n return { moveIns, moveOuts, changes: onlyInA.length + onlyInB.length > 0 }\n }\n\n insert(value: V): TopKChanges<V> {\n const result: TopKChanges<V> = { moveIn: null, moveOut: null }\n\n // Lookup insert position\n const index = this.#findIndex(value)\n // Generate fractional index based on the fractional indices of the elements before and after it\n const indexBefore =\n index === 0 ? null : getIndex(this.#sortedValues[index - 1]!)\n const indexAfter =\n index === this.#sortedValues.length\n ? null\n : getIndex(this.#sortedValues[index]!)\n const fractionalIndex = generateKeyBetween(indexBefore, indexAfter)\n\n // Insert the value at the correct position\n const val = indexedValue(value, fractionalIndex)\n // Splice is O(n) where n = all elements in the collection (i.e. n >= k) !\n this.#sortedValues.splice(index, 0, val)\n\n // Check if the topK changed\n if (index < this.#topKEnd) {\n // The inserted element is either before the top K or within the top K\n // If it is before the top K then it moves the element that was right before the topK into the topK\n // If it is within the top K then the inserted element moves into the top K\n // In both cases the last element of the old top K now moves out of the top K\n const moveInIndex = Math.max(index, this.#topKStart)\n if (moveInIndex < this.#sortedValues.length) {\n // We actually have a topK\n // because in some cases there may not be enough elements in the array to reach the start of the topK\n // e.g. [1, 2, 3] with K = 2 and offset = 3 does not have a topK\n result.moveIn = this.#sortedValues[moveInIndex]!\n\n // We need to remove the element that falls out of the top K\n // The element that falls out of the top K has shifted one to the right\n // because of the element we inserted, so we find it at index topKEnd\n if (this.#topKEnd < this.#sortedValues.length) {\n result.moveOut = this.#sortedValues[this.#topKEnd]!\n }\n }\n }\n\n return result\n }\n\n /**\n * Deletes a value that may or may not be in the topK.\n * IMPORTANT: this assumes that the value is present in the collection\n * if it's not the case it will remove the element\n * that is on the position where the provided `value` would be.\n */\n delete(value: V): TopKChanges<V> {\n const result: TopKChanges<V> = { moveIn: null, moveOut: null }\n\n // Lookup delete position\n const index = this.#findIndex(value)\n // Remove the value at that position\n const [removedElem] = this.#sortedValues.splice(index, 1)\n\n // Check if the topK changed\n if (index < this.#topKEnd) {\n // The removed element is either before the top K or within the top K\n // If it is before the top K then the first element of the topK moves out of the topK\n // If it is within the top K then the removed element moves out of the topK\n result.moveOut = removedElem!\n if (index < this.#topKStart) {\n // The removed element is before the topK\n // so actually, the first element of the topK moves out of the topK\n // and not the element that we removed\n // The first element of the topK is now at index topKStart - 1\n // since we removed an element before the topK\n const moveOutIndex = this.#topKStart - 1\n if (moveOutIndex < this.#sortedValues.length) {\n result.moveOut = this.#sortedValues[moveOutIndex]!\n } else {\n // No value is moving out of the topK\n // because there are no elements in the topK\n result.moveOut = null\n }\n }\n\n // Since we removed an element that was before or in the topK\n // the first element after the topK moved one position to the left\n // and thus falls into the topK now\n const moveInIndex = this.#topKEnd - 1\n if (moveInIndex < this.#sortedValues.length) {\n result.moveIn = this.#sortedValues[moveInIndex]!\n }\n }\n\n return result\n }\n\n // TODO: see if there is a way to refactor the code for insert and delete in the topK above\n // because they are very similar, one is shifting the topK window to the left and the other is shifting it to the right\n // so i have the feeling there is a common pattern here and we can implement both cases using that pattern\n\n #findIndex(value: V): number {\n return binarySearch(this.#sortedValues, indexedValue(value, ``), (a, b) =>\n this.#comparator(getValue(a), getValue(b))\n )\n }\n}\n\n/**\n * Operator for fractional indexed topK operations\n * This operator maintains fractional indices for sorted elements\n * and only updates indices when elements move position\n */\nexport class TopKWithFractionalIndexOperator<K, T> extends UnaryOperator<\n [K, T],\n [K, IndexedValue<T>]\n> {\n #index: Map<K, number> = new Map() // maps keys to their multiplicity\n\n /**\n * topK data structure that supports insertions and deletions\n * and returns changes to the topK.\n */\n #topK: TopK<TaggedValue<K, T>>\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: TopKWithFractionalIndexOptions\n ) {\n super(id, inputA, output)\n const limit = options.limit ?? Infinity\n const offset = options.offset ?? 0\n const compareTaggedValues = (\n a: TaggedValue<K, T>,\n b: TaggedValue<K, T>\n ) => {\n // First compare on the value\n const valueComparison = comparator(getVal(a), getVal(b))\n if (valueComparison !== 0) {\n return valueComparison\n }\n // If the values are equal, compare on the tag (object identity)\n const tieBreakerA = getTag(a)\n const tieBreakerB = getTag(b)\n return tieBreakerA - tieBreakerB\n }\n this.#topK = this.createTopK(offset, limit, compareTaggedValues)\n options.setSizeCallback?.(() => this.#topK.size)\n options.setWindowFn?.(this.moveTopK.bind(this))\n }\n\n protected createTopK(\n offset: number,\n limit: number,\n comparator: (a: TaggedValue<K, T>, b: TaggedValue<K, T>) => number\n ): TopK<TaggedValue<K, T>> {\n return new TopKArray(offset, limit, comparator)\n }\n\n /**\n * Moves the topK window 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 }) {\n if (!(this.#topK instanceof TopKArray)) {\n throw new Error(\n `Cannot move B+-tree implementation of TopK with fractional index`\n )\n }\n\n const result: Array<[[K, IndexedValue<T>], number]> = []\n\n const diff = this.#topK.move({ offset, limit })\n\n diff.moveIns.forEach((moveIn) => this.handleMoveIn(moveIn, result))\n diff.moveOuts.forEach((moveOut) => this.handleMoveOut(moveOut, result))\n\n if (diff.changes) {\n // There are changes to the topK\n // it could be that moveIns and moveOuts are empty\n // because the collection is lazy, so we will run the graph again to load the data\n this.output.sendData(new MultiSet(result))\n }\n }\n\n run(): void {\n const result: Array<[[K, IndexedValue<T>], number]> = []\n for (const message of this.inputMessages()) {\n for (const [item, multiplicity] of message.getInner()) {\n const [key, value] = item\n this.processElement(key, value, multiplicity, result)\n }\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 { oldMultiplicity, newMultiplicity } = this.addKey(key, multiplicity)\n\n let res: TopKChanges<TaggedValue<K, T>> = {\n moveIn: null,\n moveOut: null,\n }\n if (oldMultiplicity <= 0 && newMultiplicity > 0) {\n // The value was invisible but should now be visible\n // Need to insert it into the array of sorted values\n const taggedValue = tagValue(key, value)\n res = this.#topK.insert(taggedValue)\n } else if (oldMultiplicity > 0 && newMultiplicity <= 0) {\n // The value was visible but should now be invisible\n // Need to remove it from the array of sorted values\n const taggedValue = tagValue(key, value)\n res = this.#topK.delete(taggedValue)\n } else {\n // The value was invisible and it remains invisible\n // or it was visible and remains visible\n // so it doesn't affect the topK\n }\n\n this.handleMoveIn(res.moveIn, result)\n this.handleMoveOut(res.moveOut, result)\n\n return\n }\n\n private handleMoveIn(\n moveIn: IndexedValue<TaggedValue<K, T>> | null,\n result: Array<[[K, IndexedValue<T>], number]>\n ) {\n if (moveIn) {\n const index = getIndex(moveIn)\n const taggedValue = getValue(moveIn)\n const k = getKey(taggedValue)\n const val = getVal(taggedValue)\n result.push([[k, [val, index]], 1])\n }\n }\n\n private handleMoveOut(\n moveOut: IndexedValue<TaggedValue<K, T>> | null,\n result: Array<[[K, IndexedValue<T>], number]>\n ) {\n if (moveOut) {\n const index = getIndex(moveOut)\n const taggedValue = getValue(moveOut)\n const k = getKey(taggedValue)\n const val = getVal(taggedValue)\n result.push([[k, [val, index]], -1])\n }\n }\n\n private getMultiplicity(key: K): number {\n return this.#index.get(key) ?? 0\n }\n\n private addKey(\n key: K,\n multiplicity: number\n ): { oldMultiplicity: number; newMultiplicity: number } {\n const oldMultiplicity = this.getMultiplicity(key)\n const newMultiplicity = oldMultiplicity + multiplicity\n if (newMultiplicity === 0) {\n this.#index.delete(key)\n } else {\n this.#index.set(key, newMultiplicity)\n }\n return { oldMultiplicity, newMultiplicity }\n }\n}\n\n/**\n * Limits the number of results 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 * @param comparator - A function that compares two elements\n * @param options - An optional object containing limit and offset properties\n * @returns A piped operator that orders the elements and limits the number of results\n */\nexport function topKWithFractionalIndex<KType, T>(\n comparator: (a: T, b: T) => number,\n options?: TopKWithFractionalIndexOptions\n): PipedOperator<[KType, T], [KType, IndexedValue<T>]> {\n const opts = options || {}\n\n return (\n stream: IStreamBuilder<[KType, T]>\n ): IStreamBuilder<[KType, IndexedValue<T>]> => {\n const output = new StreamBuilder<[KType, IndexedValue<T>]>(\n stream.graph,\n new DifferenceStreamWriter<[KType, IndexedValue<T>]>()\n )\n const operator = new TopKWithFractionalIndexOperator<KType, T>(\n stream.graph.getNextOperatorId(),\n stream.connectReader(),\n output.writer,\n comparator,\n opts\n )\n stream.graph.addOperator(operator)\n return output\n }\n}\n\n// Abstraction for fractionally indexed values\nexport type FractionalIndex = string\nexport type IndexedValue<V> = [V, FractionalIndex]\n\nexport function indexedValue<V>(\n value: V,\n index: FractionalIndex\n): IndexedValue<V> {\n return [value, index]\n}\n\nexport function getValue<V>(indexedVal: IndexedValue<V>): V {\n return indexedVal[0]\n}\n\nexport function getIndex<V>(indexedVal: IndexedValue<V>): FractionalIndex {\n return indexedVal[1]\n}\n\nexport type Tag = number\nexport type TaggedValue<K, V> = [K, V, Tag]\n\nfunction tagValue<K, V>(key: K, value: V): TaggedValue<K, V> {\n return [key, value, globalObjectIdGenerator.getId(key)]\n}\n\nfunction getKey<K, V>(tieBreakerTaggedValue: TaggedValue<K, V>): K {\n return tieBreakerTaggedValue[0]\n}\n\nfunction getVal<K, V>(tieBreakerTaggedValue: TaggedValue<K, V>): V {\n return tieBreakerTaggedValue[1]\n}\n\nfunction getTag<K, V>(tieBreakerTaggedValue: TaggedValue<K, V>): Tag {\n return tieBreakerTaggedValue[2]\n}\n"],"names":["diffHalfOpen","generateKeyBetween","binarySearch","UnaryOperator","MultiSet","StreamBuilder","DifferenceStreamWriter","globalObjectIdGenerator"],"mappings":"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"}
|
|
@@ -4,6 +4,10 @@ export interface TopKWithFractionalIndexOptions {
|
|
|
4
4
|
limit?: number;
|
|
5
5
|
offset?: number;
|
|
6
6
|
setSizeCallback?: (getSize: () => number) => void;
|
|
7
|
+
setWindowFn?: (windowFn: (options: {
|
|
8
|
+
offset?: number;
|
|
9
|
+
limit?: number;
|
|
10
|
+
}) => void) => void;
|
|
7
11
|
}
|
|
8
12
|
export type TopKChanges<V> = {
|
|
9
13
|
/** Indicates which element moves into the topK (if any) */
|
|
@@ -11,6 +15,14 @@ export type TopKChanges<V> = {
|
|
|
11
15
|
/** Indicates which element moves out of the topK (if any) */
|
|
12
16
|
moveOut: IndexedValue<V> | null;
|
|
13
17
|
};
|
|
18
|
+
export type TopKMoveChanges<V> = {
|
|
19
|
+
/** Flag that marks whether there were any changes to the topK */
|
|
20
|
+
changes: boolean;
|
|
21
|
+
/** Indicates which elements move into the topK (if any) */
|
|
22
|
+
moveIns: Array<IndexedValue<V>>;
|
|
23
|
+
/** Indicates which elements move out of the topK (if any) */
|
|
24
|
+
moveOuts: Array<IndexedValue<V>>;
|
|
25
|
+
};
|
|
14
26
|
/**
|
|
15
27
|
* A topK data structure that supports insertions and deletions
|
|
16
28
|
* and returns changes to the topK.
|
|
@@ -35,8 +47,18 @@ export declare class TopKWithFractionalIndexOperator<K, T> extends UnaryOperator
|
|
|
35
47
|
#private;
|
|
36
48
|
constructor(id: number, inputA: DifferenceStreamReader<[K, T]>, output: DifferenceStreamWriter<[K, IndexedValue<T>]>, comparator: (a: T, b: T) => number, options: TopKWithFractionalIndexOptions);
|
|
37
49
|
protected createTopK(offset: number, limit: number, comparator: (a: TaggedValue<K, T>, b: TaggedValue<K, T>) => number): TopK<TaggedValue<K, T>>;
|
|
50
|
+
/**
|
|
51
|
+
* Moves the topK window based on the provided offset and limit.
|
|
52
|
+
* Any changes to the topK are sent to the output.
|
|
53
|
+
*/
|
|
54
|
+
moveTopK({ offset, limit }: {
|
|
55
|
+
offset?: number;
|
|
56
|
+
limit?: number;
|
|
57
|
+
}): void;
|
|
38
58
|
run(): void;
|
|
39
59
|
processElement(key: K, value: T, multiplicity: number, result: Array<[[K, IndexedValue<T>], number]>): void;
|
|
60
|
+
private handleMoveIn;
|
|
61
|
+
private handleMoveOut;
|
|
40
62
|
private getMultiplicity;
|
|
41
63
|
private addKey;
|
|
42
64
|
}
|
package/dist/cjs/utils.cjs
CHANGED
|
@@ -85,9 +85,30 @@ class ObjectIdGenerator {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
const globalObjectIdGenerator = new ObjectIdGenerator();
|
|
88
|
+
function diffHalfOpen(a, b) {
|
|
89
|
+
const [a1, a2] = a;
|
|
90
|
+
const [b1, b2] = b;
|
|
91
|
+
const onlyInA = [
|
|
92
|
+
...range(a1, Math.min(a2, b1)),
|
|
93
|
+
// left side of A outside B
|
|
94
|
+
...range(Math.max(a1, b2), a2)
|
|
95
|
+
// right side of A outside B
|
|
96
|
+
];
|
|
97
|
+
const onlyInB = [
|
|
98
|
+
...range(b1, Math.min(b2, a1)),
|
|
99
|
+
...range(Math.max(b1, a2), b2)
|
|
100
|
+
];
|
|
101
|
+
return { onlyInA, onlyInB };
|
|
102
|
+
}
|
|
103
|
+
function range(start, end) {
|
|
104
|
+
const out = [];
|
|
105
|
+
for (let i = start; i < end; i++) out.push(i);
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
88
108
|
exports.DefaultMap = DefaultMap;
|
|
89
109
|
exports.ObjectIdGenerator = ObjectIdGenerator;
|
|
90
110
|
exports.binarySearch = binarySearch;
|
|
91
111
|
exports.chunkedArrayPush = chunkedArrayPush;
|
|
112
|
+
exports.diffHalfOpen = diffHalfOpen;
|
|
92
113
|
exports.globalObjectIdGenerator = globalObjectIdGenerator;
|
|
93
114
|
//# sourceMappingURL=utils.cjs.map
|
package/dist/cjs/utils.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.cjs","sources":["../../src/utils.ts"],"sourcesContent":["/**\n * Simple assertion function for runtime checks.\n * Throws an error if the condition is false.\n */\nexport function assert(\n condition: unknown,\n message?: string\n): asserts condition {\n if (!condition) {\n throw new Error(message || `Assertion failed`)\n }\n}\n\n/**\n * A map that returns a default value for keys that are not present.\n */\nexport class DefaultMap<K, V> extends Map<K, V> {\n constructor(\n private defaultValue: () => V,\n entries?: Iterable<[K, V]>\n ) {\n super(entries)\n }\n\n get(key: K): V {\n if (!this.has(key)) {\n // this.set(key, this.defaultValue())\n return this.defaultValue()\n }\n return super.get(key)!\n }\n\n /**\n * Update the value for a key using a function.\n */\n update(key: K, updater: (value: V) => V): V {\n const value = this.get(key)\n const newValue = updater(value)\n this.set(key, newValue)\n return newValue\n }\n}\n\n// JS engines have various limits on how many args can be passed to a function\n// with a spread operator, so we need to split the operation into chunks\n// 32767 is the max for Chrome 14, all others are higher\n// TODO: investigate the performance of this and other approaches\nconst chunkSize = 30000\nexport function chunkedArrayPush(array: Array<unknown>, other: Array<unknown>) {\n if (other.length <= chunkSize) {\n array.push(...other)\n } else {\n for (let i = 0; i < other.length; i += chunkSize) {\n const chunk = other.slice(i, i + chunkSize)\n array.push(...chunk)\n }\n }\n}\n\nexport function binarySearch<T>(\n array: Array<T>,\n value: T,\n comparator: (a: T, b: T) => number\n): number {\n let low = 0\n let high = array.length\n while (low < high) {\n const mid = Math.floor((low + high) / 2)\n const comparison = comparator(array[mid]!, value)\n if (comparison < 0) {\n low = mid + 1\n } else if (comparison > 0) {\n high = mid\n } else {\n return mid\n }\n }\n return low\n}\n\n/**\n * Utility for generating unique IDs for objects and values.\n * Uses WeakMap for object reference tracking and consistent hashing for primitives.\n */\nexport class ObjectIdGenerator {\n private objectIds = new WeakMap<object, number>()\n private nextId = 0\n\n /**\n * Get a unique identifier for any value.\n * - Objects: Uses WeakMap for reference-based identity\n * - Primitives: Uses consistent string-based hashing\n */\n getId(value: any): number {\n // For primitives, use a simple hash of their string representation\n if (typeof value !== `object` || value === null) {\n const str = String(value)\n let hashValue = 0\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i)\n hashValue = (hashValue << 5) - hashValue + char\n hashValue = hashValue & hashValue // Convert to 32-bit integer\n }\n return hashValue\n }\n\n // For objects, use WeakMap to assign unique IDs\n if (!this.objectIds.has(value)) {\n this.objectIds.set(value, this.nextId++)\n }\n return this.objectIds.get(value)!\n }\n\n /**\n * Get a string representation of the ID for use in composite keys.\n */\n getStringId(value: any): string {\n if (value === null) return `null`\n if (value === undefined) return `undefined`\n if (typeof value !== `object`) return `str_${String(value)}`\n\n return `obj_${this.getId(value)}`\n }\n}\n\n/**\n * Global instance for cases where a shared object ID space is needed.\n */\nexport const globalObjectIdGenerator = new ObjectIdGenerator()\n\nexport function* concatIterable<T>(\n ...iterables: Array<Iterable<T>>\n): Iterable<T> {\n for (const iterable of iterables) {\n yield* iterable\n }\n}\n\nexport function* mapIterable<T, U>(\n it: Iterable<T>,\n fn: (t: T) => U\n): Iterable<U> {\n for (const t of it) {\n yield fn(t)\n }\n}\n"],"names":[],"mappings":";;AAgBO,MAAM,mBAAyB,IAAU;AAAA,EAC9C,YACU,cACR,SACA;AACA,UAAM,OAAO;AAHL,SAAA,eAAA;AAAA,EAIV;AAAA,EAEA,IAAI,KAAW;AACb,QAAI,CAAC,KAAK,IAAI,GAAG,GAAG;AAElB,aAAO,KAAK,aAAA;AAAA,IACd;AACA,WAAO,MAAM,IAAI,GAAG;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,KAAQ,SAA6B;AAC1C,UAAM,QAAQ,KAAK,IAAI,GAAG;AAC1B,UAAM,WAAW,QAAQ,KAAK;AAC9B,SAAK,IAAI,KAAK,QAAQ;AACtB,WAAO;AAAA,EACT;AACF;AAMA,MAAM,YAAY;AACX,SAAS,iBAAiB,OAAuB,OAAuB;AAC7E,MAAI,MAAM,UAAU,WAAW;AAC7B,UAAM,KAAK,GAAG,KAAK;AAAA,EACrB,OAAO;AACL,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,WAAW;AAChD,YAAM,QAAQ,MAAM,MAAM,GAAG,IAAI,SAAS;AAC1C,YAAM,KAAK,GAAG,KAAK;AAAA,IACrB;AAAA,EACF;AACF;AAEO,SAAS,aACd,OACA,OACA,YACQ;AACR,MAAI,MAAM;AACV,MAAI,OAAO,MAAM;AACjB,SAAO,MAAM,MAAM;AACjB,UAAM,MAAM,KAAK,OAAO,MAAM,QAAQ,CAAC;AACvC,UAAM,aAAa,WAAW,MAAM,GAAG,GAAI,KAAK;AAChD,QAAI,aAAa,GAAG;AAClB,YAAM,MAAM;AAAA,IACd,WAAW,aAAa,GAAG;AACzB,aAAO;AAAA,IACT,OAAO;AACL,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAMO,MAAM,kBAAkB;AAAA,EAAxB,cAAA;AACL,SAAQ,gCAAgB,QAAA;AACxB,SAAQ,SAAS;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOjB,MAAM,OAAoB;AAExB,QAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C,YAAM,MAAM,OAAO,KAAK;AACxB,UAAI,YAAY;AAChB,eAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACnC,cAAM,OAAO,IAAI,WAAW,CAAC;AAC7B,qBAAa,aAAa,KAAK,YAAY;AAC3C,oBAAY,YAAY;AAAA,MAC1B;AACA,aAAO;AAAA,IACT;AAGA,QAAI,CAAC,KAAK,UAAU,IAAI,KAAK,GAAG;AAC9B,WAAK,UAAU,IAAI,OAAO,KAAK,QAAQ;AAAA,IACzC;AACA,WAAO,KAAK,UAAU,IAAI,KAAK;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY,OAAoB;AAC9B,QAAI,UAAU,KAAM,QAAO;AAC3B,QAAI,UAAU,OAAW,QAAO;AAChC,QAAI,OAAO,UAAU,iBAAiB,OAAO,OAAO,KAAK,CAAC;AAE1D,WAAO,OAAO,KAAK,MAAM,KAAK,CAAC;AAAA,EACjC;AACF;AAKO,MAAM,0BAA0B,IAAI,kBAAA
|
|
1
|
+
{"version":3,"file":"utils.cjs","sources":["../../src/utils.ts"],"sourcesContent":["/**\n * Simple assertion function for runtime checks.\n * Throws an error if the condition is false.\n */\nexport function assert(\n condition: unknown,\n message?: string\n): asserts condition {\n if (!condition) {\n throw new Error(message || `Assertion failed`)\n }\n}\n\n/**\n * A map that returns a default value for keys that are not present.\n */\nexport class DefaultMap<K, V> extends Map<K, V> {\n constructor(\n private defaultValue: () => V,\n entries?: Iterable<[K, V]>\n ) {\n super(entries)\n }\n\n get(key: K): V {\n if (!this.has(key)) {\n // this.set(key, this.defaultValue())\n return this.defaultValue()\n }\n return super.get(key)!\n }\n\n /**\n * Update the value for a key using a function.\n */\n update(key: K, updater: (value: V) => V): V {\n const value = this.get(key)\n const newValue = updater(value)\n this.set(key, newValue)\n return newValue\n }\n}\n\n// JS engines have various limits on how many args can be passed to a function\n// with a spread operator, so we need to split the operation into chunks\n// 32767 is the max for Chrome 14, all others are higher\n// TODO: investigate the performance of this and other approaches\nconst chunkSize = 30000\nexport function chunkedArrayPush(array: Array<unknown>, other: Array<unknown>) {\n if (other.length <= chunkSize) {\n array.push(...other)\n } else {\n for (let i = 0; i < other.length; i += chunkSize) {\n const chunk = other.slice(i, i + chunkSize)\n array.push(...chunk)\n }\n }\n}\n\nexport function binarySearch<T>(\n array: Array<T>,\n value: T,\n comparator: (a: T, b: T) => number\n): number {\n let low = 0\n let high = array.length\n while (low < high) {\n const mid = Math.floor((low + high) / 2)\n const comparison = comparator(array[mid]!, value)\n if (comparison < 0) {\n low = mid + 1\n } else if (comparison > 0) {\n high = mid\n } else {\n return mid\n }\n }\n return low\n}\n\n/**\n * Utility for generating unique IDs for objects and values.\n * Uses WeakMap for object reference tracking and consistent hashing for primitives.\n */\nexport class ObjectIdGenerator {\n private objectIds = new WeakMap<object, number>()\n private nextId = 0\n\n /**\n * Get a unique identifier for any value.\n * - Objects: Uses WeakMap for reference-based identity\n * - Primitives: Uses consistent string-based hashing\n */\n getId(value: any): number {\n // For primitives, use a simple hash of their string representation\n if (typeof value !== `object` || value === null) {\n const str = String(value)\n let hashValue = 0\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i)\n hashValue = (hashValue << 5) - hashValue + char\n hashValue = hashValue & hashValue // Convert to 32-bit integer\n }\n return hashValue\n }\n\n // For objects, use WeakMap to assign unique IDs\n if (!this.objectIds.has(value)) {\n this.objectIds.set(value, this.nextId++)\n }\n return this.objectIds.get(value)!\n }\n\n /**\n * Get a string representation of the ID for use in composite keys.\n */\n getStringId(value: any): string {\n if (value === null) return `null`\n if (value === undefined) return `undefined`\n if (typeof value !== `object`) return `str_${String(value)}`\n\n return `obj_${this.getId(value)}`\n }\n}\n\n/**\n * Global instance for cases where a shared object ID space is needed.\n */\nexport const globalObjectIdGenerator = new ObjectIdGenerator()\n\nexport function* concatIterable<T>(\n ...iterables: Array<Iterable<T>>\n): Iterable<T> {\n for (const iterable of iterables) {\n yield* iterable\n }\n}\n\nexport function* mapIterable<T, U>(\n it: Iterable<T>,\n fn: (t: T) => U\n): Iterable<U> {\n for (const t of it) {\n yield fn(t)\n }\n}\n\nexport type HRange = [number, number] // half-open [start, end[ i.e. end is exclusive\n\n/**\n * Computes the difference between two half-open ranges.\n * @param a - The first half-open range\n * @param b - The second half-open range\n * @returns The difference between the two ranges\n */\nexport function diffHalfOpen(a: HRange, b: HRange) {\n const [a1, a2] = a\n const [b1, b2] = b\n\n // A \\ B can be up to two segments (left and right of the overlap)\n const onlyInA: Array<number> = [\n ...range(a1, Math.min(a2, b1)), // left side of A outside B\n ...range(Math.max(a1, b2), a2), // right side of A outside B\n ]\n\n // B \\ A similarly\n const onlyInB: Array<number> = [\n ...range(b1, Math.min(b2, a1)),\n ...range(Math.max(b1, a2), b2),\n ]\n\n return { onlyInA, onlyInB }\n}\n\nfunction range(start: number, end: number): Array<number> {\n const out: Array<number> = []\n for (let i = start; i < end; i++) out.push(i)\n return out\n}\n"],"names":[],"mappings":"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"}
|
package/dist/cjs/utils.d.cts
CHANGED
|
@@ -41,3 +41,14 @@ export declare class ObjectIdGenerator {
|
|
|
41
41
|
export declare const globalObjectIdGenerator: ObjectIdGenerator;
|
|
42
42
|
export declare function concatIterable<T>(...iterables: Array<Iterable<T>>): Iterable<T>;
|
|
43
43
|
export declare function mapIterable<T, U>(it: Iterable<T>, fn: (t: T) => U): Iterable<U>;
|
|
44
|
+
export type HRange = [number, number];
|
|
45
|
+
/**
|
|
46
|
+
* Computes the difference between two half-open ranges.
|
|
47
|
+
* @param a - The first half-open range
|
|
48
|
+
* @param b - The second half-open range
|
|
49
|
+
* @returns The difference between the two ranges
|
|
50
|
+
*/
|
|
51
|
+
export declare function diffHalfOpen(a: HRange, b: HRange): {
|
|
52
|
+
onlyInA: number[];
|
|
53
|
+
onlyInB: number[];
|
|
54
|
+
};
|
|
@@ -7,6 +7,10 @@ export interface OrderByOptions<Ve> {
|
|
|
7
7
|
}
|
|
8
8
|
type OrderByWithFractionalIndexOptions<Ve> = OrderByOptions<Ve> & {
|
|
9
9
|
setSizeCallback?: (getSize: () => number) => void;
|
|
10
|
+
setWindowFn?: (windowFn: (options: {
|
|
11
|
+
offset?: number;
|
|
12
|
+
limit?: number;
|
|
13
|
+
}) => void) => void;
|
|
10
14
|
};
|
|
11
15
|
/**
|
|
12
16
|
* Orders the elements and limits the number of results, with optional offset
|
|
@@ -69,6 +69,7 @@ function orderByWithFractionalIndexBase(topKFunction, valueExtractor, options) {
|
|
|
69
69
|
const limit = options?.limit ?? Infinity;
|
|
70
70
|
const offset = options?.offset ?? 0;
|
|
71
71
|
const setSizeCallback = options?.setSizeCallback;
|
|
72
|
+
const setWindowFn = options?.setWindowFn;
|
|
72
73
|
const comparator = options?.comparator ?? ((a, b) => {
|
|
73
74
|
if (a === b) return 0;
|
|
74
75
|
if (a < b) return -1;
|
|
@@ -81,7 +82,8 @@ function orderByWithFractionalIndexBase(topKFunction, valueExtractor, options) {
|
|
|
81
82
|
{
|
|
82
83
|
limit,
|
|
83
84
|
offset,
|
|
84
|
-
setSizeCallback
|
|
85
|
+
setSizeCallback,
|
|
86
|
+
setWindowFn
|
|
85
87
|
}
|
|
86
88
|
),
|
|
87
89
|
consolidate()
|
|
@@ -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}\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 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 }\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":";;;;;
|
|
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;"}
|
|
@@ -4,6 +4,10 @@ export interface TopKWithFractionalIndexOptions {
|
|
|
4
4
|
limit?: number;
|
|
5
5
|
offset?: number;
|
|
6
6
|
setSizeCallback?: (getSize: () => number) => void;
|
|
7
|
+
setWindowFn?: (windowFn: (options: {
|
|
8
|
+
offset?: number;
|
|
9
|
+
limit?: number;
|
|
10
|
+
}) => void) => void;
|
|
7
11
|
}
|
|
8
12
|
export type TopKChanges<V> = {
|
|
9
13
|
/** Indicates which element moves into the topK (if any) */
|
|
@@ -11,6 +15,14 @@ export type TopKChanges<V> = {
|
|
|
11
15
|
/** Indicates which element moves out of the topK (if any) */
|
|
12
16
|
moveOut: IndexedValue<V> | null;
|
|
13
17
|
};
|
|
18
|
+
export type TopKMoveChanges<V> = {
|
|
19
|
+
/** Flag that marks whether there were any changes to the topK */
|
|
20
|
+
changes: boolean;
|
|
21
|
+
/** Indicates which elements move into the topK (if any) */
|
|
22
|
+
moveIns: Array<IndexedValue<V>>;
|
|
23
|
+
/** Indicates which elements move out of the topK (if any) */
|
|
24
|
+
moveOuts: Array<IndexedValue<V>>;
|
|
25
|
+
};
|
|
14
26
|
/**
|
|
15
27
|
* A topK data structure that supports insertions and deletions
|
|
16
28
|
* and returns changes to the topK.
|
|
@@ -35,8 +47,18 @@ export declare class TopKWithFractionalIndexOperator<K, T> extends UnaryOperator
|
|
|
35
47
|
#private;
|
|
36
48
|
constructor(id: number, inputA: DifferenceStreamReader<[K, T]>, output: DifferenceStreamWriter<[K, IndexedValue<T>]>, comparator: (a: T, b: T) => number, options: TopKWithFractionalIndexOptions);
|
|
37
49
|
protected createTopK(offset: number, limit: number, comparator: (a: TaggedValue<K, T>, b: TaggedValue<K, T>) => number): TopK<TaggedValue<K, T>>;
|
|
50
|
+
/**
|
|
51
|
+
* Moves the topK window based on the provided offset and limit.
|
|
52
|
+
* Any changes to the topK are sent to the output.
|
|
53
|
+
*/
|
|
54
|
+
moveTopK({ offset, limit }: {
|
|
55
|
+
offset?: number;
|
|
56
|
+
limit?: number;
|
|
57
|
+
}): void;
|
|
38
58
|
run(): void;
|
|
39
59
|
processElement(key: K, value: T, multiplicity: number, result: Array<[[K, IndexedValue<T>], number]>): void;
|
|
60
|
+
private handleMoveIn;
|
|
61
|
+
private handleMoveOut;
|
|
40
62
|
private getMultiplicity;
|
|
41
63
|
private addKey;
|
|
42
64
|
}
|
|
@@ -2,7 +2,7 @@ import { generateKeyBetween } from "fractional-indexing";
|
|
|
2
2
|
import { UnaryOperator, DifferenceStreamWriter } from "../graph.js";
|
|
3
3
|
import { StreamBuilder } from "../d2.js";
|
|
4
4
|
import { MultiSet } from "../multiset.js";
|
|
5
|
-
import { binarySearch, globalObjectIdGenerator } from "../utils.js";
|
|
5
|
+
import { diffHalfOpen, binarySearch, globalObjectIdGenerator } from "../utils.js";
|
|
6
6
|
class TopKArray {
|
|
7
7
|
#sortedValues = [];
|
|
8
8
|
#comparator;
|
|
@@ -19,6 +19,36 @@ class TopKArray {
|
|
|
19
19
|
const available = this.#sortedValues.length - offset;
|
|
20
20
|
return Math.max(0, Math.min(limit, available));
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Moves the topK window
|
|
24
|
+
*/
|
|
25
|
+
move({
|
|
26
|
+
offset,
|
|
27
|
+
limit
|
|
28
|
+
}) {
|
|
29
|
+
const oldOffset = this.#topKStart;
|
|
30
|
+
const oldLimit = this.#topKEnd - this.#topKStart;
|
|
31
|
+
const oldRange = [this.#topKStart, this.#topKEnd];
|
|
32
|
+
this.#topKStart = offset ?? oldOffset;
|
|
33
|
+
this.#topKEnd = this.#topKStart + (limit ?? oldLimit);
|
|
34
|
+
const newRange = [this.#topKStart, this.#topKEnd];
|
|
35
|
+
const { onlyInA, onlyInB } = diffHalfOpen(oldRange, newRange);
|
|
36
|
+
const moveIns = [];
|
|
37
|
+
onlyInB.forEach((index) => {
|
|
38
|
+
const value = this.#sortedValues[index];
|
|
39
|
+
if (value) {
|
|
40
|
+
moveIns.push(value);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
const moveOuts = [];
|
|
44
|
+
onlyInA.forEach((index) => {
|
|
45
|
+
const value = this.#sortedValues[index];
|
|
46
|
+
if (value) {
|
|
47
|
+
moveOuts.push(value);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return { moveIns, moveOuts, changes: onlyInA.length + onlyInB.length > 0 };
|
|
51
|
+
}
|
|
22
52
|
insert(value) {
|
|
23
53
|
const result = { moveIn: null, moveOut: null };
|
|
24
54
|
const index = this.#findIndex(value);
|
|
@@ -84,10 +114,9 @@ class TopKWithFractionalIndexOperator extends UnaryOperator {
|
|
|
84
114
|
* and returns changes to the topK.
|
|
85
115
|
*/
|
|
86
116
|
#topK;
|
|
87
|
-
#limit;
|
|
88
117
|
constructor(id, inputA, output, comparator, options) {
|
|
89
118
|
super(id, inputA, output);
|
|
90
|
-
|
|
119
|
+
const limit = options.limit ?? Infinity;
|
|
91
120
|
const offset = options.offset ?? 0;
|
|
92
121
|
const compareTaggedValues = (a, b) => {
|
|
93
122
|
const valueComparison = comparator(getVal(a), getVal(b));
|
|
@@ -98,12 +127,31 @@ class TopKWithFractionalIndexOperator extends UnaryOperator {
|
|
|
98
127
|
const tieBreakerB = getTag(b);
|
|
99
128
|
return tieBreakerA - tieBreakerB;
|
|
100
129
|
};
|
|
101
|
-
this.#topK = this.createTopK(offset,
|
|
130
|
+
this.#topK = this.createTopK(offset, limit, compareTaggedValues);
|
|
102
131
|
options.setSizeCallback?.(() => this.#topK.size);
|
|
132
|
+
options.setWindowFn?.(this.moveTopK.bind(this));
|
|
103
133
|
}
|
|
104
134
|
createTopK(offset, limit, comparator) {
|
|
105
135
|
return new TopKArray(offset, limit, comparator);
|
|
106
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Moves the topK window based on the provided offset and limit.
|
|
139
|
+
* Any changes to the topK are sent to the output.
|
|
140
|
+
*/
|
|
141
|
+
moveTopK({ offset, limit }) {
|
|
142
|
+
if (!(this.#topK instanceof TopKArray)) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Cannot move B+-tree implementation of TopK with fractional index`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
const result = [];
|
|
148
|
+
const diff = this.#topK.move({ offset, limit });
|
|
149
|
+
diff.moveIns.forEach((moveIn) => this.handleMoveIn(moveIn, result));
|
|
150
|
+
diff.moveOuts.forEach((moveOut) => this.handleMoveOut(moveOut, result));
|
|
151
|
+
if (diff.changes) {
|
|
152
|
+
this.output.sendData(new MultiSet(result));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
107
155
|
run() {
|
|
108
156
|
const result = [];
|
|
109
157
|
for (const message of this.inputMessages()) {
|
|
@@ -129,21 +177,27 @@ class TopKWithFractionalIndexOperator extends UnaryOperator {
|
|
|
129
177
|
const taggedValue = tagValue(key, value);
|
|
130
178
|
res = this.#topK.delete(taggedValue);
|
|
131
179
|
} else ;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
180
|
+
this.handleMoveIn(res.moveIn, result);
|
|
181
|
+
this.handleMoveOut(res.moveOut, result);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
handleMoveIn(moveIn, result) {
|
|
185
|
+
if (moveIn) {
|
|
186
|
+
const index = getIndex(moveIn);
|
|
187
|
+
const taggedValue = getValue(moveIn);
|
|
135
188
|
const k = getKey(taggedValue);
|
|
136
189
|
const val = getVal(taggedValue);
|
|
137
190
|
result.push([[k, [val, index]], 1]);
|
|
138
191
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
192
|
+
}
|
|
193
|
+
handleMoveOut(moveOut, result) {
|
|
194
|
+
if (moveOut) {
|
|
195
|
+
const index = getIndex(moveOut);
|
|
196
|
+
const taggedValue = getValue(moveOut);
|
|
142
197
|
const k = getKey(taggedValue);
|
|
143
198
|
const val = getVal(taggedValue);
|
|
144
199
|
result.push([[k, [val, index]], -1]);
|
|
145
200
|
}
|
|
146
|
-
return;
|
|
147
201
|
}
|
|
148
202
|
getMultiplicity(key) {
|
|
149
203
|
return this.#index.get(key) ?? 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"topKWithFractionalIndex.js","sources":["../../../src/operators/topKWithFractionalIndex.ts"],"sourcesContent":["import { generateKeyBetween } from \"fractional-indexing\"\nimport { DifferenceStreamWriter, UnaryOperator } from \"../graph.js\"\nimport { StreamBuilder } from \"../d2.js\"\nimport { MultiSet } from \"../multiset.js\"\nimport { binarySearch, globalObjectIdGenerator } from \"../utils.js\"\nimport type { DifferenceStreamReader } from \"../graph.js\"\nimport type { IStreamBuilder, PipedOperator } from \"../types.js\"\n\nexport interface TopKWithFractionalIndexOptions {\n limit?: number\n offset?: number\n setSizeCallback?: (getSize: () => number) => void\n}\n\nexport type TopKChanges<V> = {\n /** Indicates which element moves into the topK (if any) */\n moveIn: IndexedValue<V> | null\n /** Indicates which element moves out of the topK (if any) */\n moveOut: IndexedValue<V> | null\n}\n\n/**\n * A topK data structure that supports insertions and deletions\n * and returns changes to the topK.\n */\nexport interface TopK<V> {\n size: number\n insert: (value: V) => TopKChanges<V>\n delete: (value: V) => TopKChanges<V>\n}\n\n/**\n * Implementation of a topK data structure.\n * Uses a sorted array internally to store the values and keeps a topK window over that array.\n * Inserts and deletes are O(n) operations because worst case an element is inserted/deleted\n * at the start of the array which causes all the elements to shift to the right/left.\n */\nclass TopKArray<V> implements TopK<V> {\n #sortedValues: Array<IndexedValue<V>> = []\n #comparator: (a: V, b: V) => number\n #topKStart: number\n #topKEnd: number\n\n constructor(\n offset: number,\n limit: number,\n comparator: (a: V, b: V) => number\n ) {\n this.#topKStart = offset\n this.#topKEnd = offset + limit\n this.#comparator = comparator\n }\n\n get size(): number {\n const offset = this.#topKStart\n const limit = this.#topKEnd - this.#topKStart\n const available = this.#sortedValues.length - offset\n return Math.max(0, Math.min(limit, available))\n }\n\n insert(value: V): TopKChanges<V> {\n const result: TopKChanges<V> = { moveIn: null, moveOut: null }\n\n // Lookup insert position\n const index = this.#findIndex(value)\n // Generate fractional index based on the fractional indices of the elements before and after it\n const indexBefore =\n index === 0 ? null : getIndex(this.#sortedValues[index - 1]!)\n const indexAfter =\n index === this.#sortedValues.length\n ? null\n : getIndex(this.#sortedValues[index]!)\n const fractionalIndex = generateKeyBetween(indexBefore, indexAfter)\n\n // Insert the value at the correct position\n const val = indexedValue(value, fractionalIndex)\n // Splice is O(n) where n = all elements in the collection (i.e. n >= k) !\n this.#sortedValues.splice(index, 0, val)\n\n // Check if the topK changed\n if (index < this.#topKEnd) {\n // The inserted element is either before the top K or within the top K\n // If it is before the top K then it moves the element that was right before the topK into the topK\n // If it is within the top K then the inserted element moves into the top K\n // In both cases the last element of the old top K now moves out of the top K\n const moveInIndex = Math.max(index, this.#topKStart)\n if (moveInIndex < this.#sortedValues.length) {\n // We actually have a topK\n // because in some cases there may not be enough elements in the array to reach the start of the topK\n // e.g. [1, 2, 3] with K = 2 and offset = 3 does not have a topK\n result.moveIn = this.#sortedValues[moveInIndex]!\n\n // We need to remove the element that falls out of the top K\n // The element that falls out of the top K has shifted one to the right\n // because of the element we inserted, so we find it at index topKEnd\n if (this.#topKEnd < this.#sortedValues.length) {\n result.moveOut = this.#sortedValues[this.#topKEnd]!\n }\n }\n }\n\n return result\n }\n\n /**\n * Deletes a value that may or may not be in the topK.\n * IMPORTANT: this assumes that the value is present in the collection\n * if it's not the case it will remove the element\n * that is on the position where the provided `value` would be.\n */\n delete(value: V): TopKChanges<V> {\n const result: TopKChanges<V> = { moveIn: null, moveOut: null }\n\n // Lookup delete position\n const index = this.#findIndex(value)\n // Remove the value at that position\n const [removedElem] = this.#sortedValues.splice(index, 1)\n\n // Check if the topK changed\n if (index < this.#topKEnd) {\n // The removed element is either before the top K or within the top K\n // If it is before the top K then the first element of the topK moves out of the topK\n // If it is within the top K then the removed element moves out of the topK\n result.moveOut = removedElem!\n if (index < this.#topKStart) {\n // The removed element is before the topK\n // so actually, the first element of the topK moves out of the topK\n // and not the element that we removed\n // The first element of the topK is now at index topKStart - 1\n // since we removed an element before the topK\n const moveOutIndex = this.#topKStart - 1\n if (moveOutIndex < this.#sortedValues.length) {\n result.moveOut = this.#sortedValues[moveOutIndex]!\n } else {\n // No value is moving out of the topK\n // because there are no elements in the topK\n result.moveOut = null\n }\n }\n\n // Since we removed an element that was before or in the topK\n // the first element after the topK moved one position to the left\n // and thus falls into the topK now\n const moveInIndex = this.#topKEnd - 1\n if (moveInIndex < this.#sortedValues.length) {\n result.moveIn = this.#sortedValues[moveInIndex]!\n }\n }\n\n return result\n }\n\n // TODO: see if there is a way to refactor the code for insert and delete in the topK above\n // because they are very similar, one is shifting the topK window to the left and the other is shifting it to the right\n // so i have the feeling there is a common pattern here and we can implement both cases using that pattern\n\n #findIndex(value: V): number {\n return binarySearch(this.#sortedValues, indexedValue(value, ``), (a, b) =>\n this.#comparator(getValue(a), getValue(b))\n )\n }\n}\n\n/**\n * Operator for fractional indexed topK operations\n * This operator maintains fractional indices for sorted elements\n * and only updates indices when elements move position\n */\nexport class TopKWithFractionalIndexOperator<K, T> extends UnaryOperator<\n [K, T],\n [K, IndexedValue<T>]\n> {\n #index: Map<K, number> = new Map() // maps keys to their multiplicity\n\n /**\n * topK data structure that supports insertions and deletions\n * and returns changes to the topK.\n */\n #topK: TopK<TaggedValue<K, T>>\n\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: TopKWithFractionalIndexOptions\n ) {\n super(id, inputA, output)\n this.#limit = options.limit ?? Infinity\n const offset = options.offset ?? 0\n const compareTaggedValues = (\n a: TaggedValue<K, T>,\n b: TaggedValue<K, T>\n ) => {\n // First compare on the value\n const valueComparison = comparator(getVal(a), getVal(b))\n if (valueComparison !== 0) {\n return valueComparison\n }\n // If the values are equal, compare on the tag (object identity)\n const tieBreakerA = getTag(a)\n const tieBreakerB = getTag(b)\n return tieBreakerA - tieBreakerB\n }\n this.#topK = this.createTopK(offset, this.#limit, compareTaggedValues)\n options.setSizeCallback?.(() => this.#topK.size)\n }\n\n protected createTopK(\n offset: number,\n limit: number,\n comparator: (a: TaggedValue<K, T>, b: TaggedValue<K, T>) => number\n ): TopK<TaggedValue<K, T>> {\n return new TopKArray(offset, limit, comparator)\n }\n\n run(): void {\n const result: Array<[[K, IndexedValue<T>], number]> = []\n for (const message of this.inputMessages()) {\n for (const [item, multiplicity] of message.getInner()) {\n const [key, value] = item\n this.processElement(key, value, multiplicity, result)\n }\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 { oldMultiplicity, newMultiplicity } = this.addKey(key, multiplicity)\n\n let res: TopKChanges<TaggedValue<K, T>> = {\n moveIn: null,\n moveOut: null,\n }\n if (oldMultiplicity <= 0 && newMultiplicity > 0) {\n // The value was invisible but should now be visible\n // Need to insert it into the array of sorted values\n const taggedValue = tagValue(key, value)\n res = this.#topK.insert(taggedValue)\n } else if (oldMultiplicity > 0 && newMultiplicity <= 0) {\n // The value was visible but should now be invisible\n // Need to remove it from the array of sorted values\n const taggedValue = tagValue(key, value)\n res = this.#topK.delete(taggedValue)\n } else {\n // The value was invisible and it remains invisible\n // or it was visible and remains visible\n // so it doesn't affect the topK\n }\n\n if (res.moveIn) {\n const index = getIndex(res.moveIn)\n const taggedValue = getValue(res.moveIn)\n const k = getKey(taggedValue)\n const val = getVal(taggedValue)\n result.push([[k, [val, index]], 1])\n }\n\n if (res.moveOut) {\n const index = getIndex(res.moveOut)\n const taggedValue = getValue(res.moveOut)\n const k = getKey(taggedValue)\n const val = getVal(taggedValue)\n result.push([[k, [val, index]], -1])\n }\n\n return\n }\n\n private getMultiplicity(key: K): number {\n return this.#index.get(key) ?? 0\n }\n\n private addKey(\n key: K,\n multiplicity: number\n ): { oldMultiplicity: number; newMultiplicity: number } {\n const oldMultiplicity = this.getMultiplicity(key)\n const newMultiplicity = oldMultiplicity + multiplicity\n if (newMultiplicity === 0) {\n this.#index.delete(key)\n } else {\n this.#index.set(key, newMultiplicity)\n }\n return { oldMultiplicity, newMultiplicity }\n }\n}\n\n/**\n * Limits the number of results 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 * @param comparator - A function that compares two elements\n * @param options - An optional object containing limit and offset properties\n * @returns A piped operator that orders the elements and limits the number of results\n */\nexport function topKWithFractionalIndex<KType, T>(\n comparator: (a: T, b: T) => number,\n options?: TopKWithFractionalIndexOptions\n): PipedOperator<[KType, T], [KType, IndexedValue<T>]> {\n const opts = options || {}\n\n return (\n stream: IStreamBuilder<[KType, T]>\n ): IStreamBuilder<[KType, IndexedValue<T>]> => {\n const output = new StreamBuilder<[KType, IndexedValue<T>]>(\n stream.graph,\n new DifferenceStreamWriter<[KType, IndexedValue<T>]>()\n )\n const operator = new TopKWithFractionalIndexOperator<KType, T>(\n stream.graph.getNextOperatorId(),\n stream.connectReader(),\n output.writer,\n comparator,\n opts\n )\n stream.graph.addOperator(operator)\n return output\n }\n}\n\n// Abstraction for fractionally indexed values\nexport type FractionalIndex = string\nexport type IndexedValue<V> = [V, FractionalIndex]\n\nexport function indexedValue<V>(\n value: V,\n index: FractionalIndex\n): IndexedValue<V> {\n return [value, index]\n}\n\nexport function getValue<V>(indexedVal: IndexedValue<V>): V {\n return indexedVal[0]\n}\n\nexport function getIndex<V>(indexedVal: IndexedValue<V>): FractionalIndex {\n return indexedVal[1]\n}\n\nexport type Tag = number\nexport type TaggedValue<K, V> = [K, V, Tag]\n\nfunction tagValue<K, V>(key: K, value: V): TaggedValue<K, V> {\n return [key, value, globalObjectIdGenerator.getId(key)]\n}\n\nfunction getKey<K, V>(tieBreakerTaggedValue: TaggedValue<K, V>): K {\n return tieBreakerTaggedValue[0]\n}\n\nfunction getVal<K, V>(tieBreakerTaggedValue: TaggedValue<K, V>): V {\n return tieBreakerTaggedValue[1]\n}\n\nfunction getTag<K, V>(tieBreakerTaggedValue: TaggedValue<K, V>): Tag {\n return tieBreakerTaggedValue[2]\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"topKWithFractionalIndex.js","sources":["../../../src/operators/topKWithFractionalIndex.ts"],"sourcesContent":["import { generateKeyBetween } from \"fractional-indexing\"\nimport { DifferenceStreamWriter, UnaryOperator } from \"../graph.js\"\nimport { StreamBuilder } from \"../d2.js\"\nimport { MultiSet } from \"../multiset.js\"\nimport {\n binarySearch,\n diffHalfOpen,\n globalObjectIdGenerator,\n} from \"../utils.js\"\nimport type { HRange } from \"../utils.js\"\nimport type { DifferenceStreamReader } from \"../graph.js\"\nimport type { IStreamBuilder, PipedOperator } from \"../types.js\"\n\nexport interface TopKWithFractionalIndexOptions {\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\nexport type TopKChanges<V> = {\n /** Indicates which element moves into the topK (if any) */\n moveIn: IndexedValue<V> | null\n /** Indicates which element moves out of the topK (if any) */\n moveOut: IndexedValue<V> | null\n}\n\nexport type TopKMoveChanges<V> = {\n /** Flag that marks whether there were any changes to the topK */\n changes: boolean\n /** Indicates which elements move into the topK (if any) */\n moveIns: Array<IndexedValue<V>>\n /** Indicates which elements move out of the topK (if any) */\n moveOuts: Array<IndexedValue<V>>\n}\n\n/**\n * A topK data structure that supports insertions and deletions\n * and returns changes to the topK.\n */\nexport interface TopK<V> {\n size: number\n insert: (value: V) => TopKChanges<V>\n delete: (value: V) => TopKChanges<V>\n}\n\n/**\n * Implementation of a topK data structure.\n * Uses a sorted array internally to store the values and keeps a topK window over that array.\n * Inserts and deletes are O(n) operations because worst case an element is inserted/deleted\n * at the start of the array which causes all the elements to shift to the right/left.\n */\nclass TopKArray<V> implements TopK<V> {\n #sortedValues: Array<IndexedValue<V>> = []\n #comparator: (a: V, b: V) => number\n #topKStart: number\n #topKEnd: number\n\n constructor(\n offset: number,\n limit: number,\n comparator: (a: V, b: V) => number\n ) {\n this.#topKStart = offset\n this.#topKEnd = offset + limit\n this.#comparator = comparator\n }\n\n get size(): number {\n const offset = this.#topKStart\n const limit = this.#topKEnd - this.#topKStart\n const available = this.#sortedValues.length - offset\n return Math.max(0, Math.min(limit, available))\n }\n\n /**\n * Moves the topK window\n */\n move({\n offset,\n limit,\n }: {\n offset?: number\n limit?: number\n }): TopKMoveChanges<V> {\n const oldOffset = this.#topKStart\n const oldLimit = this.#topKEnd - this.#topKStart\n const oldRange: HRange = [this.#topKStart, this.#topKEnd]\n\n this.#topKStart = offset ?? oldOffset\n this.#topKEnd = this.#topKStart + (limit ?? oldLimit)\n\n const newRange: HRange = [this.#topKStart, this.#topKEnd]\n const { onlyInA, onlyInB } = diffHalfOpen(oldRange, newRange)\n\n const moveIns: Array<IndexedValue<V>> = []\n onlyInB.forEach((index) => {\n const value = this.#sortedValues[index]\n if (value) {\n moveIns.push(value)\n }\n })\n\n const moveOuts: Array<IndexedValue<V>> = []\n onlyInA.forEach((index) => {\n const value = this.#sortedValues[index]\n if (value) {\n moveOuts.push(value)\n }\n })\n\n // It could be that there are changes (i.e. moveIns or moveOuts)\n // but that the collection is lazy so we don't have the data yet that needs to move in/out\n // so `moveIns` and `moveOuts` will be empty but `changes` will be true\n // this will tell the caller that it needs to run the graph to load more data\n return { moveIns, moveOuts, changes: onlyInA.length + onlyInB.length > 0 }\n }\n\n insert(value: V): TopKChanges<V> {\n const result: TopKChanges<V> = { moveIn: null, moveOut: null }\n\n // Lookup insert position\n const index = this.#findIndex(value)\n // Generate fractional index based on the fractional indices of the elements before and after it\n const indexBefore =\n index === 0 ? null : getIndex(this.#sortedValues[index - 1]!)\n const indexAfter =\n index === this.#sortedValues.length\n ? null\n : getIndex(this.#sortedValues[index]!)\n const fractionalIndex = generateKeyBetween(indexBefore, indexAfter)\n\n // Insert the value at the correct position\n const val = indexedValue(value, fractionalIndex)\n // Splice is O(n) where n = all elements in the collection (i.e. n >= k) !\n this.#sortedValues.splice(index, 0, val)\n\n // Check if the topK changed\n if (index < this.#topKEnd) {\n // The inserted element is either before the top K or within the top K\n // If it is before the top K then it moves the element that was right before the topK into the topK\n // If it is within the top K then the inserted element moves into the top K\n // In both cases the last element of the old top K now moves out of the top K\n const moveInIndex = Math.max(index, this.#topKStart)\n if (moveInIndex < this.#sortedValues.length) {\n // We actually have a topK\n // because in some cases there may not be enough elements in the array to reach the start of the topK\n // e.g. [1, 2, 3] with K = 2 and offset = 3 does not have a topK\n result.moveIn = this.#sortedValues[moveInIndex]!\n\n // We need to remove the element that falls out of the top K\n // The element that falls out of the top K has shifted one to the right\n // because of the element we inserted, so we find it at index topKEnd\n if (this.#topKEnd < this.#sortedValues.length) {\n result.moveOut = this.#sortedValues[this.#topKEnd]!\n }\n }\n }\n\n return result\n }\n\n /**\n * Deletes a value that may or may not be in the topK.\n * IMPORTANT: this assumes that the value is present in the collection\n * if it's not the case it will remove the element\n * that is on the position where the provided `value` would be.\n */\n delete(value: V): TopKChanges<V> {\n const result: TopKChanges<V> = { moveIn: null, moveOut: null }\n\n // Lookup delete position\n const index = this.#findIndex(value)\n // Remove the value at that position\n const [removedElem] = this.#sortedValues.splice(index, 1)\n\n // Check if the topK changed\n if (index < this.#topKEnd) {\n // The removed element is either before the top K or within the top K\n // If it is before the top K then the first element of the topK moves out of the topK\n // If it is within the top K then the removed element moves out of the topK\n result.moveOut = removedElem!\n if (index < this.#topKStart) {\n // The removed element is before the topK\n // so actually, the first element of the topK moves out of the topK\n // and not the element that we removed\n // The first element of the topK is now at index topKStart - 1\n // since we removed an element before the topK\n const moveOutIndex = this.#topKStart - 1\n if (moveOutIndex < this.#sortedValues.length) {\n result.moveOut = this.#sortedValues[moveOutIndex]!\n } else {\n // No value is moving out of the topK\n // because there are no elements in the topK\n result.moveOut = null\n }\n }\n\n // Since we removed an element that was before or in the topK\n // the first element after the topK moved one position to the left\n // and thus falls into the topK now\n const moveInIndex = this.#topKEnd - 1\n if (moveInIndex < this.#sortedValues.length) {\n result.moveIn = this.#sortedValues[moveInIndex]!\n }\n }\n\n return result\n }\n\n // TODO: see if there is a way to refactor the code for insert and delete in the topK above\n // because they are very similar, one is shifting the topK window to the left and the other is shifting it to the right\n // so i have the feeling there is a common pattern here and we can implement both cases using that pattern\n\n #findIndex(value: V): number {\n return binarySearch(this.#sortedValues, indexedValue(value, ``), (a, b) =>\n this.#comparator(getValue(a), getValue(b))\n )\n }\n}\n\n/**\n * Operator for fractional indexed topK operations\n * This operator maintains fractional indices for sorted elements\n * and only updates indices when elements move position\n */\nexport class TopKWithFractionalIndexOperator<K, T> extends UnaryOperator<\n [K, T],\n [K, IndexedValue<T>]\n> {\n #index: Map<K, number> = new Map() // maps keys to their multiplicity\n\n /**\n * topK data structure that supports insertions and deletions\n * and returns changes to the topK.\n */\n #topK: TopK<TaggedValue<K, T>>\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: TopKWithFractionalIndexOptions\n ) {\n super(id, inputA, output)\n const limit = options.limit ?? Infinity\n const offset = options.offset ?? 0\n const compareTaggedValues = (\n a: TaggedValue<K, T>,\n b: TaggedValue<K, T>\n ) => {\n // First compare on the value\n const valueComparison = comparator(getVal(a), getVal(b))\n if (valueComparison !== 0) {\n return valueComparison\n }\n // If the values are equal, compare on the tag (object identity)\n const tieBreakerA = getTag(a)\n const tieBreakerB = getTag(b)\n return tieBreakerA - tieBreakerB\n }\n this.#topK = this.createTopK(offset, limit, compareTaggedValues)\n options.setSizeCallback?.(() => this.#topK.size)\n options.setWindowFn?.(this.moveTopK.bind(this))\n }\n\n protected createTopK(\n offset: number,\n limit: number,\n comparator: (a: TaggedValue<K, T>, b: TaggedValue<K, T>) => number\n ): TopK<TaggedValue<K, T>> {\n return new TopKArray(offset, limit, comparator)\n }\n\n /**\n * Moves the topK window 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 }) {\n if (!(this.#topK instanceof TopKArray)) {\n throw new Error(\n `Cannot move B+-tree implementation of TopK with fractional index`\n )\n }\n\n const result: Array<[[K, IndexedValue<T>], number]> = []\n\n const diff = this.#topK.move({ offset, limit })\n\n diff.moveIns.forEach((moveIn) => this.handleMoveIn(moveIn, result))\n diff.moveOuts.forEach((moveOut) => this.handleMoveOut(moveOut, result))\n\n if (diff.changes) {\n // There are changes to the topK\n // it could be that moveIns and moveOuts are empty\n // because the collection is lazy, so we will run the graph again to load the data\n this.output.sendData(new MultiSet(result))\n }\n }\n\n run(): void {\n const result: Array<[[K, IndexedValue<T>], number]> = []\n for (const message of this.inputMessages()) {\n for (const [item, multiplicity] of message.getInner()) {\n const [key, value] = item\n this.processElement(key, value, multiplicity, result)\n }\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 { oldMultiplicity, newMultiplicity } = this.addKey(key, multiplicity)\n\n let res: TopKChanges<TaggedValue<K, T>> = {\n moveIn: null,\n moveOut: null,\n }\n if (oldMultiplicity <= 0 && newMultiplicity > 0) {\n // The value was invisible but should now be visible\n // Need to insert it into the array of sorted values\n const taggedValue = tagValue(key, value)\n res = this.#topK.insert(taggedValue)\n } else if (oldMultiplicity > 0 && newMultiplicity <= 0) {\n // The value was visible but should now be invisible\n // Need to remove it from the array of sorted values\n const taggedValue = tagValue(key, value)\n res = this.#topK.delete(taggedValue)\n } else {\n // The value was invisible and it remains invisible\n // or it was visible and remains visible\n // so it doesn't affect the topK\n }\n\n this.handleMoveIn(res.moveIn, result)\n this.handleMoveOut(res.moveOut, result)\n\n return\n }\n\n private handleMoveIn(\n moveIn: IndexedValue<TaggedValue<K, T>> | null,\n result: Array<[[K, IndexedValue<T>], number]>\n ) {\n if (moveIn) {\n const index = getIndex(moveIn)\n const taggedValue = getValue(moveIn)\n const k = getKey(taggedValue)\n const val = getVal(taggedValue)\n result.push([[k, [val, index]], 1])\n }\n }\n\n private handleMoveOut(\n moveOut: IndexedValue<TaggedValue<K, T>> | null,\n result: Array<[[K, IndexedValue<T>], number]>\n ) {\n if (moveOut) {\n const index = getIndex(moveOut)\n const taggedValue = getValue(moveOut)\n const k = getKey(taggedValue)\n const val = getVal(taggedValue)\n result.push([[k, [val, index]], -1])\n }\n }\n\n private getMultiplicity(key: K): number {\n return this.#index.get(key) ?? 0\n }\n\n private addKey(\n key: K,\n multiplicity: number\n ): { oldMultiplicity: number; newMultiplicity: number } {\n const oldMultiplicity = this.getMultiplicity(key)\n const newMultiplicity = oldMultiplicity + multiplicity\n if (newMultiplicity === 0) {\n this.#index.delete(key)\n } else {\n this.#index.set(key, newMultiplicity)\n }\n return { oldMultiplicity, newMultiplicity }\n }\n}\n\n/**\n * Limits the number of results 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 * @param comparator - A function that compares two elements\n * @param options - An optional object containing limit and offset properties\n * @returns A piped operator that orders the elements and limits the number of results\n */\nexport function topKWithFractionalIndex<KType, T>(\n comparator: (a: T, b: T) => number,\n options?: TopKWithFractionalIndexOptions\n): PipedOperator<[KType, T], [KType, IndexedValue<T>]> {\n const opts = options || {}\n\n return (\n stream: IStreamBuilder<[KType, T]>\n ): IStreamBuilder<[KType, IndexedValue<T>]> => {\n const output = new StreamBuilder<[KType, IndexedValue<T>]>(\n stream.graph,\n new DifferenceStreamWriter<[KType, IndexedValue<T>]>()\n )\n const operator = new TopKWithFractionalIndexOperator<KType, T>(\n stream.graph.getNextOperatorId(),\n stream.connectReader(),\n output.writer,\n comparator,\n opts\n )\n stream.graph.addOperator(operator)\n return output\n }\n}\n\n// Abstraction for fractionally indexed values\nexport type FractionalIndex = string\nexport type IndexedValue<V> = [V, FractionalIndex]\n\nexport function indexedValue<V>(\n value: V,\n index: FractionalIndex\n): IndexedValue<V> {\n return [value, index]\n}\n\nexport function getValue<V>(indexedVal: IndexedValue<V>): V {\n return indexedVal[0]\n}\n\nexport function getIndex<V>(indexedVal: IndexedValue<V>): FractionalIndex {\n return indexedVal[1]\n}\n\nexport type Tag = number\nexport type TaggedValue<K, V> = [K, V, Tag]\n\nfunction tagValue<K, V>(key: K, value: V): TaggedValue<K, V> {\n return [key, value, globalObjectIdGenerator.getId(key)]\n}\n\nfunction getKey<K, V>(tieBreakerTaggedValue: TaggedValue<K, V>): K {\n return tieBreakerTaggedValue[0]\n}\n\nfunction getVal<K, V>(tieBreakerTaggedValue: TaggedValue<K, V>): V {\n return tieBreakerTaggedValue[1]\n}\n\nfunction getTag<K, V>(tieBreakerTaggedValue: TaggedValue<K, V>): Tag {\n return tieBreakerTaggedValue[2]\n}\n"],"names":[],"mappings":";;;;;AAsDA,MAAM,UAAgC;AAAA,EACpC,gBAAwC,CAAA;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YACE,QACA,OACA,YACA;AACA,SAAK,aAAa;AAClB,SAAK,WAAW,SAAS;AACzB,SAAK,cAAc;AAAA,EACrB;AAAA,EAEA,IAAI,OAAe;AACjB,UAAM,SAAS,KAAK;AACpB,UAAM,QAAQ,KAAK,WAAW,KAAK;AACnC,UAAM,YAAY,KAAK,cAAc,SAAS;AAC9C,WAAO,KAAK,IAAI,GAAG,KAAK,IAAI,OAAO,SAAS,CAAC;AAAA,EAC/C;AAAA;AAAA;AAAA;AAAA,EAKA,KAAK;AAAA,IACH;AAAA,IACA;AAAA,EAAA,GAIqB;AACrB,UAAM,YAAY,KAAK;AACvB,UAAM,WAAW,KAAK,WAAW,KAAK;AACtC,UAAM,WAAmB,CAAC,KAAK,YAAY,KAAK,QAAQ;AAExD,SAAK,aAAa,UAAU;AAC5B,SAAK,WAAW,KAAK,cAAc,SAAS;AAE5C,UAAM,WAAmB,CAAC,KAAK,YAAY,KAAK,QAAQ;AACxD,UAAM,EAAE,SAAS,QAAA,IAAY,aAAa,UAAU,QAAQ;AAE5D,UAAM,UAAkC,CAAA;AACxC,YAAQ,QAAQ,CAAC,UAAU;AACzB,YAAM,QAAQ,KAAK,cAAc,KAAK;AACtC,UAAI,OAAO;AACT,gBAAQ,KAAK,KAAK;AAAA,MACpB;AAAA,IACF,CAAC;AAED,UAAM,WAAmC,CAAA;AACzC,YAAQ,QAAQ,CAAC,UAAU;AACzB,YAAM,QAAQ,KAAK,cAAc,KAAK;AACtC,UAAI,OAAO;AACT,iBAAS,KAAK,KAAK;AAAA,MACrB;AAAA,IACF,CAAC;AAMD,WAAO,EAAE,SAAS,UAAU,SAAS,QAAQ,SAAS,QAAQ,SAAS,EAAA;AAAA,EACzE;AAAA,EAEA,OAAO,OAA0B;AAC/B,UAAM,SAAyB,EAAE,QAAQ,MAAM,SAAS,KAAA;AAGxD,UAAM,QAAQ,KAAK,WAAW,KAAK;AAEnC,UAAM,cACJ,UAAU,IAAI,OAAO,SAAS,KAAK,cAAc,QAAQ,CAAC,CAAE;AAC9D,UAAM,aACJ,UAAU,KAAK,cAAc,SACzB,OACA,SAAS,KAAK,cAAc,KAAK,CAAE;AACzC,UAAM,kBAAkB,mBAAmB,aAAa,UAAU;AAGlE,UAAM,MAAM,aAAa,OAAO,eAAe;AAE/C,SAAK,cAAc,OAAO,OAAO,GAAG,GAAG;AAGvC,QAAI,QAAQ,KAAK,UAAU;AAKzB,YAAM,cAAc,KAAK,IAAI,OAAO,KAAK,UAAU;AACnD,UAAI,cAAc,KAAK,cAAc,QAAQ;AAI3C,eAAO,SAAS,KAAK,cAAc,WAAW;AAK9C,YAAI,KAAK,WAAW,KAAK,cAAc,QAAQ;AAC7C,iBAAO,UAAU,KAAK,cAAc,KAAK,QAAQ;AAAA,QACnD;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,OAAO,OAA0B;AAC/B,UAAM,SAAyB,EAAE,QAAQ,MAAM,SAAS,KAAA;AAGxD,UAAM,QAAQ,KAAK,WAAW,KAAK;AAEnC,UAAM,CAAC,WAAW,IAAI,KAAK,cAAc,OAAO,OAAO,CAAC;AAGxD,QAAI,QAAQ,KAAK,UAAU;AAIzB,aAAO,UAAU;AACjB,UAAI,QAAQ,KAAK,YAAY;AAM3B,cAAM,eAAe,KAAK,aAAa;AACvC,YAAI,eAAe,KAAK,cAAc,QAAQ;AAC5C,iBAAO,UAAU,KAAK,cAAc,YAAY;AAAA,QAClD,OAAO;AAGL,iBAAO,UAAU;AAAA,QACnB;AAAA,MACF;AAKA,YAAM,cAAc,KAAK,WAAW;AACpC,UAAI,cAAc,KAAK,cAAc,QAAQ;AAC3C,eAAO,SAAS,KAAK,cAAc,WAAW;AAAA,MAChD;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAMA,WAAW,OAAkB;AAC3B,WAAO;AAAA,MAAa,KAAK;AAAA,MAAe,aAAa,OAAO,EAAE;AAAA,MAAG,CAAC,GAAG,MACnE,KAAK,YAAY,SAAS,CAAC,GAAG,SAAS,CAAC,CAAC;AAAA,IAAA;AAAA,EAE7C;AACF;AAOO,MAAM,wCAA8C,cAGzD;AAAA,EACA,6BAA6B,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAM7B;AAAA,EAEA,YACE,IACA,QACA,QACA,YACA,SACA;AACA,UAAM,IAAI,QAAQ,MAAM;AACxB,UAAM,QAAQ,QAAQ,SAAS;AAC/B,UAAM,SAAS,QAAQ,UAAU;AACjC,UAAM,sBAAsB,CAC1B,GACA,MACG;AAEH,YAAM,kBAAkB,WAAW,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC;AACvD,UAAI,oBAAoB,GAAG;AACzB,eAAO;AAAA,MACT;AAEA,YAAM,cAAc,OAAO,CAAC;AAC5B,YAAM,cAAc,OAAO,CAAC;AAC5B,aAAO,cAAc;AAAA,IACvB;AACA,SAAK,QAAQ,KAAK,WAAW,QAAQ,OAAO,mBAAmB;AAC/D,YAAQ,kBAAkB,MAAM,KAAK,MAAM,IAAI;AAC/C,YAAQ,cAAc,KAAK,SAAS,KAAK,IAAI,CAAC;AAAA,EAChD;AAAA,EAEU,WACR,QACA,OACA,YACyB;AACzB,WAAO,IAAI,UAAU,QAAQ,OAAO,UAAU;AAAA,EAChD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,SAAS,EAAE,QAAQ,SAA8C;AAC/D,QAAI,EAAE,KAAK,iBAAiB,YAAY;AACtC,YAAM,IAAI;AAAA,QACR;AAAA,MAAA;AAAA,IAEJ;AAEA,UAAM,SAAgD,CAAA;AAEtD,UAAM,OAAO,KAAK,MAAM,KAAK,EAAE,QAAQ,OAAO;AAE9C,SAAK,QAAQ,QAAQ,CAAC,WAAW,KAAK,aAAa,QAAQ,MAAM,CAAC;AAClE,SAAK,SAAS,QAAQ,CAAC,YAAY,KAAK,cAAc,SAAS,MAAM,CAAC;AAEtE,QAAI,KAAK,SAAS;AAIhB,WAAK,OAAO,SAAS,IAAI,SAAS,MAAM,CAAC;AAAA,IAC3C;AAAA,EACF;AAAA,EAEA,MAAY;AACV,UAAM,SAAgD,CAAA;AACtD,eAAW,WAAW,KAAK,iBAAiB;AAC1C,iBAAW,CAAC,MAAM,YAAY,KAAK,QAAQ,YAAY;AACrD,cAAM,CAAC,KAAK,KAAK,IAAI;AACrB,aAAK,eAAe,KAAK,OAAO,cAAc,MAAM;AAAA,MACtD;AAAA,IACF;AAEA,QAAI,OAAO,SAAS,GAAG;AACrB,WAAK,OAAO,SAAS,IAAI,SAAS,MAAM,CAAC;AAAA,IAC3C;AAAA,EACF;AAAA,EAEA,eACE,KACA,OACA,cACA,QACM;AACN,UAAM,EAAE,iBAAiB,gBAAA,IAAoB,KAAK,OAAO,KAAK,YAAY;AAE1E,QAAI,MAAsC;AAAA,MACxC,QAAQ;AAAA,MACR,SAAS;AAAA,IAAA;AAEX,QAAI,mBAAmB,KAAK,kBAAkB,GAAG;AAG/C,YAAM,cAAc,SAAS,KAAK,KAAK;AACvC,YAAM,KAAK,MAAM,OAAO,WAAW;AAAA,IACrC,WAAW,kBAAkB,KAAK,mBAAmB,GAAG;AAGtD,YAAM,cAAc,SAAS,KAAK,KAAK;AACvC,YAAM,KAAK,MAAM,OAAO,WAAW;AAAA,IACrC,MAAO;AAMP,SAAK,aAAa,IAAI,QAAQ,MAAM;AACpC,SAAK,cAAc,IAAI,SAAS,MAAM;AAEtC;AAAA,EACF;AAAA,EAEQ,aACN,QACA,QACA;AACA,QAAI,QAAQ;AACV,YAAM,QAAQ,SAAS,MAAM;AAC7B,YAAM,cAAc,SAAS,MAAM;AACnC,YAAM,IAAI,OAAO,WAAW;AAC5B,YAAM,MAAM,OAAO,WAAW;AAC9B,aAAO,KAAK,CAAC,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC,CAAC;AAAA,IACpC;AAAA,EACF;AAAA,EAEQ,cACN,SACA,QACA;AACA,QAAI,SAAS;AACX,YAAM,QAAQ,SAAS,OAAO;AAC9B,YAAM,cAAc,SAAS,OAAO;AACpC,YAAM,IAAI,OAAO,WAAW;AAC5B,YAAM,MAAM,OAAO,WAAW;AAC9B,aAAO,KAAK,CAAC,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,GAAG,EAAE,CAAC;AAAA,IACrC;AAAA,EACF;AAAA,EAEQ,gBAAgB,KAAgB;AACtC,WAAO,KAAK,OAAO,IAAI,GAAG,KAAK;AAAA,EACjC;AAAA,EAEQ,OACN,KACA,cACsD;AACtD,UAAM,kBAAkB,KAAK,gBAAgB,GAAG;AAChD,UAAM,kBAAkB,kBAAkB;AAC1C,QAAI,oBAAoB,GAAG;AACzB,WAAK,OAAO,OAAO,GAAG;AAAA,IACxB,OAAO;AACL,WAAK,OAAO,IAAI,KAAK,eAAe;AAAA,IACtC;AACA,WAAO,EAAE,iBAAiB,gBAAA;AAAA,EAC5B;AACF;AAYO,SAAS,wBACd,YACA,SACqD;AACrD,QAAM,OAAO,WAAW,CAAA;AAExB,SAAO,CACL,WAC6C;AAC7C,UAAM,SAAS,IAAI;AAAA,MACjB,OAAO;AAAA,MACP,IAAI,uBAAA;AAAA,IAAiD;AAEvD,UAAM,WAAW,IAAI;AAAA,MACnB,OAAO,MAAM,kBAAA;AAAA,MACb,OAAO,cAAA;AAAA,MACP,OAAO;AAAA,MACP;AAAA,MACA;AAAA,IAAA;AAEF,WAAO,MAAM,YAAY,QAAQ;AACjC,WAAO;AAAA,EACT;AACF;AAMO,SAAS,aACd,OACA,OACiB;AACjB,SAAO,CAAC,OAAO,KAAK;AACtB;AAEO,SAAS,SAAY,YAAgC;AAC1D,SAAO,WAAW,CAAC;AACrB;AAEO,SAAS,SAAY,YAA8C;AACxE,SAAO,WAAW,CAAC;AACrB;AAKA,SAAS,SAAe,KAAQ,OAA6B;AAC3D,SAAO,CAAC,KAAK,OAAO,wBAAwB,MAAM,GAAG,CAAC;AACxD;AAEA,SAAS,OAAa,uBAA6C;AACjE,SAAO,sBAAsB,CAAC;AAChC;AAEA,SAAS,OAAa,uBAA6C;AACjE,SAAO,sBAAsB,CAAC;AAChC;AAEA,SAAS,OAAa,uBAA+C;AACnE,SAAO,sBAAsB,CAAC;AAChC;"}
|
package/dist/esm/utils.d.ts
CHANGED
|
@@ -41,3 +41,14 @@ export declare class ObjectIdGenerator {
|
|
|
41
41
|
export declare const globalObjectIdGenerator: ObjectIdGenerator;
|
|
42
42
|
export declare function concatIterable<T>(...iterables: Array<Iterable<T>>): Iterable<T>;
|
|
43
43
|
export declare function mapIterable<T, U>(it: Iterable<T>, fn: (t: T) => U): Iterable<U>;
|
|
44
|
+
export type HRange = [number, number];
|
|
45
|
+
/**
|
|
46
|
+
* Computes the difference between two half-open ranges.
|
|
47
|
+
* @param a - The first half-open range
|
|
48
|
+
* @param b - The second half-open range
|
|
49
|
+
* @returns The difference between the two ranges
|
|
50
|
+
*/
|
|
51
|
+
export declare function diffHalfOpen(a: HRange, b: HRange): {
|
|
52
|
+
onlyInA: number[];
|
|
53
|
+
onlyInB: number[];
|
|
54
|
+
};
|
package/dist/esm/utils.js
CHANGED
|
@@ -83,11 +83,32 @@ class ObjectIdGenerator {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
const globalObjectIdGenerator = new ObjectIdGenerator();
|
|
86
|
+
function diffHalfOpen(a, b) {
|
|
87
|
+
const [a1, a2] = a;
|
|
88
|
+
const [b1, b2] = b;
|
|
89
|
+
const onlyInA = [
|
|
90
|
+
...range(a1, Math.min(a2, b1)),
|
|
91
|
+
// left side of A outside B
|
|
92
|
+
...range(Math.max(a1, b2), a2)
|
|
93
|
+
// right side of A outside B
|
|
94
|
+
];
|
|
95
|
+
const onlyInB = [
|
|
96
|
+
...range(b1, Math.min(b2, a1)),
|
|
97
|
+
...range(Math.max(b1, a2), b2)
|
|
98
|
+
];
|
|
99
|
+
return { onlyInA, onlyInB };
|
|
100
|
+
}
|
|
101
|
+
function range(start, end) {
|
|
102
|
+
const out = [];
|
|
103
|
+
for (let i = start; i < end; i++) out.push(i);
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
86
106
|
export {
|
|
87
107
|
DefaultMap,
|
|
88
108
|
ObjectIdGenerator,
|
|
89
109
|
binarySearch,
|
|
90
110
|
chunkedArrayPush,
|
|
111
|
+
diffHalfOpen,
|
|
91
112
|
globalObjectIdGenerator
|
|
92
113
|
};
|
|
93
114
|
//# sourceMappingURL=utils.js.map
|
package/dist/esm/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["/**\n * Simple assertion function for runtime checks.\n * Throws an error if the condition is false.\n */\nexport function assert(\n condition: unknown,\n message?: string\n): asserts condition {\n if (!condition) {\n throw new Error(message || `Assertion failed`)\n }\n}\n\n/**\n * A map that returns a default value for keys that are not present.\n */\nexport class DefaultMap<K, V> extends Map<K, V> {\n constructor(\n private defaultValue: () => V,\n entries?: Iterable<[K, V]>\n ) {\n super(entries)\n }\n\n get(key: K): V {\n if (!this.has(key)) {\n // this.set(key, this.defaultValue())\n return this.defaultValue()\n }\n return super.get(key)!\n }\n\n /**\n * Update the value for a key using a function.\n */\n update(key: K, updater: (value: V) => V): V {\n const value = this.get(key)\n const newValue = updater(value)\n this.set(key, newValue)\n return newValue\n }\n}\n\n// JS engines have various limits on how many args can be passed to a function\n// with a spread operator, so we need to split the operation into chunks\n// 32767 is the max for Chrome 14, all others are higher\n// TODO: investigate the performance of this and other approaches\nconst chunkSize = 30000\nexport function chunkedArrayPush(array: Array<unknown>, other: Array<unknown>) {\n if (other.length <= chunkSize) {\n array.push(...other)\n } else {\n for (let i = 0; i < other.length; i += chunkSize) {\n const chunk = other.slice(i, i + chunkSize)\n array.push(...chunk)\n }\n }\n}\n\nexport function binarySearch<T>(\n array: Array<T>,\n value: T,\n comparator: (a: T, b: T) => number\n): number {\n let low = 0\n let high = array.length\n while (low < high) {\n const mid = Math.floor((low + high) / 2)\n const comparison = comparator(array[mid]!, value)\n if (comparison < 0) {\n low = mid + 1\n } else if (comparison > 0) {\n high = mid\n } else {\n return mid\n }\n }\n return low\n}\n\n/**\n * Utility for generating unique IDs for objects and values.\n * Uses WeakMap for object reference tracking and consistent hashing for primitives.\n */\nexport class ObjectIdGenerator {\n private objectIds = new WeakMap<object, number>()\n private nextId = 0\n\n /**\n * Get a unique identifier for any value.\n * - Objects: Uses WeakMap for reference-based identity\n * - Primitives: Uses consistent string-based hashing\n */\n getId(value: any): number {\n // For primitives, use a simple hash of their string representation\n if (typeof value !== `object` || value === null) {\n const str = String(value)\n let hashValue = 0\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i)\n hashValue = (hashValue << 5) - hashValue + char\n hashValue = hashValue & hashValue // Convert to 32-bit integer\n }\n return hashValue\n }\n\n // For objects, use WeakMap to assign unique IDs\n if (!this.objectIds.has(value)) {\n this.objectIds.set(value, this.nextId++)\n }\n return this.objectIds.get(value)!\n }\n\n /**\n * Get a string representation of the ID for use in composite keys.\n */\n getStringId(value: any): string {\n if (value === null) return `null`\n if (value === undefined) return `undefined`\n if (typeof value !== `object`) return `str_${String(value)}`\n\n return `obj_${this.getId(value)}`\n }\n}\n\n/**\n * Global instance for cases where a shared object ID space is needed.\n */\nexport const globalObjectIdGenerator = new ObjectIdGenerator()\n\nexport function* concatIterable<T>(\n ...iterables: Array<Iterable<T>>\n): Iterable<T> {\n for (const iterable of iterables) {\n yield* iterable\n }\n}\n\nexport function* mapIterable<T, U>(\n it: Iterable<T>,\n fn: (t: T) => U\n): Iterable<U> {\n for (const t of it) {\n yield fn(t)\n }\n}\n"],"names":[],"mappings":"AAgBO,MAAM,mBAAyB,IAAU;AAAA,EAC9C,YACU,cACR,SACA;AACA,UAAM,OAAO;AAHL,SAAA,eAAA;AAAA,EAIV;AAAA,EAEA,IAAI,KAAW;AACb,QAAI,CAAC,KAAK,IAAI,GAAG,GAAG;AAElB,aAAO,KAAK,aAAA;AAAA,IACd;AACA,WAAO,MAAM,IAAI,GAAG;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,KAAQ,SAA6B;AAC1C,UAAM,QAAQ,KAAK,IAAI,GAAG;AAC1B,UAAM,WAAW,QAAQ,KAAK;AAC9B,SAAK,IAAI,KAAK,QAAQ;AACtB,WAAO;AAAA,EACT;AACF;AAMA,MAAM,YAAY;AACX,SAAS,iBAAiB,OAAuB,OAAuB;AAC7E,MAAI,MAAM,UAAU,WAAW;AAC7B,UAAM,KAAK,GAAG,KAAK;AAAA,EACrB,OAAO;AACL,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,WAAW;AAChD,YAAM,QAAQ,MAAM,MAAM,GAAG,IAAI,SAAS;AAC1C,YAAM,KAAK,GAAG,KAAK;AAAA,IACrB;AAAA,EACF;AACF;AAEO,SAAS,aACd,OACA,OACA,YACQ;AACR,MAAI,MAAM;AACV,MAAI,OAAO,MAAM;AACjB,SAAO,MAAM,MAAM;AACjB,UAAM,MAAM,KAAK,OAAO,MAAM,QAAQ,CAAC;AACvC,UAAM,aAAa,WAAW,MAAM,GAAG,GAAI,KAAK;AAChD,QAAI,aAAa,GAAG;AAClB,YAAM,MAAM;AAAA,IACd,WAAW,aAAa,GAAG;AACzB,aAAO;AAAA,IACT,OAAO;AACL,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAMO,MAAM,kBAAkB;AAAA,EAAxB,cAAA;AACL,SAAQ,gCAAgB,QAAA;AACxB,SAAQ,SAAS;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOjB,MAAM,OAAoB;AAExB,QAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C,YAAM,MAAM,OAAO,KAAK;AACxB,UAAI,YAAY;AAChB,eAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACnC,cAAM,OAAO,IAAI,WAAW,CAAC;AAC7B,qBAAa,aAAa,KAAK,YAAY;AAC3C,oBAAY,YAAY;AAAA,MAC1B;AACA,aAAO;AAAA,IACT;AAGA,QAAI,CAAC,KAAK,UAAU,IAAI,KAAK,GAAG;AAC9B,WAAK,UAAU,IAAI,OAAO,KAAK,QAAQ;AAAA,IACzC;AACA,WAAO,KAAK,UAAU,IAAI,KAAK;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY,OAAoB;AAC9B,QAAI,UAAU,KAAM,QAAO;AAC3B,QAAI,UAAU,OAAW,QAAO;AAChC,QAAI,OAAO,UAAU,iBAAiB,OAAO,OAAO,KAAK,CAAC;AAE1D,WAAO,OAAO,KAAK,MAAM,KAAK,CAAC;AAAA,EACjC;AACF;AAKO,MAAM,0BAA0B,IAAI,kBAAA;"}
|
|
1
|
+
{"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["/**\n * Simple assertion function for runtime checks.\n * Throws an error if the condition is false.\n */\nexport function assert(\n condition: unknown,\n message?: string\n): asserts condition {\n if (!condition) {\n throw new Error(message || `Assertion failed`)\n }\n}\n\n/**\n * A map that returns a default value for keys that are not present.\n */\nexport class DefaultMap<K, V> extends Map<K, V> {\n constructor(\n private defaultValue: () => V,\n entries?: Iterable<[K, V]>\n ) {\n super(entries)\n }\n\n get(key: K): V {\n if (!this.has(key)) {\n // this.set(key, this.defaultValue())\n return this.defaultValue()\n }\n return super.get(key)!\n }\n\n /**\n * Update the value for a key using a function.\n */\n update(key: K, updater: (value: V) => V): V {\n const value = this.get(key)\n const newValue = updater(value)\n this.set(key, newValue)\n return newValue\n }\n}\n\n// JS engines have various limits on how many args can be passed to a function\n// with a spread operator, so we need to split the operation into chunks\n// 32767 is the max for Chrome 14, all others are higher\n// TODO: investigate the performance of this and other approaches\nconst chunkSize = 30000\nexport function chunkedArrayPush(array: Array<unknown>, other: Array<unknown>) {\n if (other.length <= chunkSize) {\n array.push(...other)\n } else {\n for (let i = 0; i < other.length; i += chunkSize) {\n const chunk = other.slice(i, i + chunkSize)\n array.push(...chunk)\n }\n }\n}\n\nexport function binarySearch<T>(\n array: Array<T>,\n value: T,\n comparator: (a: T, b: T) => number\n): number {\n let low = 0\n let high = array.length\n while (low < high) {\n const mid = Math.floor((low + high) / 2)\n const comparison = comparator(array[mid]!, value)\n if (comparison < 0) {\n low = mid + 1\n } else if (comparison > 0) {\n high = mid\n } else {\n return mid\n }\n }\n return low\n}\n\n/**\n * Utility for generating unique IDs for objects and values.\n * Uses WeakMap for object reference tracking and consistent hashing for primitives.\n */\nexport class ObjectIdGenerator {\n private objectIds = new WeakMap<object, number>()\n private nextId = 0\n\n /**\n * Get a unique identifier for any value.\n * - Objects: Uses WeakMap for reference-based identity\n * - Primitives: Uses consistent string-based hashing\n */\n getId(value: any): number {\n // For primitives, use a simple hash of their string representation\n if (typeof value !== `object` || value === null) {\n const str = String(value)\n let hashValue = 0\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i)\n hashValue = (hashValue << 5) - hashValue + char\n hashValue = hashValue & hashValue // Convert to 32-bit integer\n }\n return hashValue\n }\n\n // For objects, use WeakMap to assign unique IDs\n if (!this.objectIds.has(value)) {\n this.objectIds.set(value, this.nextId++)\n }\n return this.objectIds.get(value)!\n }\n\n /**\n * Get a string representation of the ID for use in composite keys.\n */\n getStringId(value: any): string {\n if (value === null) return `null`\n if (value === undefined) return `undefined`\n if (typeof value !== `object`) return `str_${String(value)}`\n\n return `obj_${this.getId(value)}`\n }\n}\n\n/**\n * Global instance for cases where a shared object ID space is needed.\n */\nexport const globalObjectIdGenerator = new ObjectIdGenerator()\n\nexport function* concatIterable<T>(\n ...iterables: Array<Iterable<T>>\n): Iterable<T> {\n for (const iterable of iterables) {\n yield* iterable\n }\n}\n\nexport function* mapIterable<T, U>(\n it: Iterable<T>,\n fn: (t: T) => U\n): Iterable<U> {\n for (const t of it) {\n yield fn(t)\n }\n}\n\nexport type HRange = [number, number] // half-open [start, end[ i.e. end is exclusive\n\n/**\n * Computes the difference between two half-open ranges.\n * @param a - The first half-open range\n * @param b - The second half-open range\n * @returns The difference between the two ranges\n */\nexport function diffHalfOpen(a: HRange, b: HRange) {\n const [a1, a2] = a\n const [b1, b2] = b\n\n // A \\ B can be up to two segments (left and right of the overlap)\n const onlyInA: Array<number> = [\n ...range(a1, Math.min(a2, b1)), // left side of A outside B\n ...range(Math.max(a1, b2), a2), // right side of A outside B\n ]\n\n // B \\ A similarly\n const onlyInB: Array<number> = [\n ...range(b1, Math.min(b2, a1)),\n ...range(Math.max(b1, a2), b2),\n ]\n\n return { onlyInA, onlyInB }\n}\n\nfunction range(start: number, end: number): Array<number> {\n const out: Array<number> = []\n for (let i = start; i < end; i++) out.push(i)\n return out\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/operators/orderBy.ts
CHANGED
|
@@ -13,6 +13,9 @@ export interface OrderByOptions<Ve> {
|
|
|
13
13
|
|
|
14
14
|
type OrderByWithFractionalIndexOptions<Ve> = OrderByOptions<Ve> & {
|
|
15
15
|
setSizeCallback?: (getSize: () => number) => void
|
|
16
|
+
setWindowFn?: (
|
|
17
|
+
windowFn: (options: { offset?: number; limit?: number }) => void
|
|
18
|
+
) => void
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
/**
|
|
@@ -147,6 +150,7 @@ export function orderByWithFractionalIndexBase<
|
|
|
147
150
|
const limit = options?.limit ?? Infinity
|
|
148
151
|
const offset = options?.offset ?? 0
|
|
149
152
|
const setSizeCallback = options?.setSizeCallback
|
|
153
|
+
const setWindowFn = options?.setWindowFn
|
|
150
154
|
const comparator =
|
|
151
155
|
options?.comparator ??
|
|
152
156
|
((a, b) => {
|
|
@@ -167,6 +171,7 @@ export function orderByWithFractionalIndexBase<
|
|
|
167
171
|
limit,
|
|
168
172
|
offset,
|
|
169
173
|
setSizeCallback,
|
|
174
|
+
setWindowFn,
|
|
170
175
|
}
|
|
171
176
|
),
|
|
172
177
|
consolidate()
|
|
@@ -2,7 +2,12 @@ import { generateKeyBetween } from "fractional-indexing"
|
|
|
2
2
|
import { DifferenceStreamWriter, UnaryOperator } from "../graph.js"
|
|
3
3
|
import { StreamBuilder } from "../d2.js"
|
|
4
4
|
import { MultiSet } from "../multiset.js"
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
binarySearch,
|
|
7
|
+
diffHalfOpen,
|
|
8
|
+
globalObjectIdGenerator,
|
|
9
|
+
} from "../utils.js"
|
|
10
|
+
import type { HRange } from "../utils.js"
|
|
6
11
|
import type { DifferenceStreamReader } from "../graph.js"
|
|
7
12
|
import type { IStreamBuilder, PipedOperator } from "../types.js"
|
|
8
13
|
|
|
@@ -10,6 +15,9 @@ export interface TopKWithFractionalIndexOptions {
|
|
|
10
15
|
limit?: number
|
|
11
16
|
offset?: number
|
|
12
17
|
setSizeCallback?: (getSize: () => number) => void
|
|
18
|
+
setWindowFn?: (
|
|
19
|
+
windowFn: (options: { offset?: number; limit?: number }) => void
|
|
20
|
+
) => void
|
|
13
21
|
}
|
|
14
22
|
|
|
15
23
|
export type TopKChanges<V> = {
|
|
@@ -19,6 +27,15 @@ export type TopKChanges<V> = {
|
|
|
19
27
|
moveOut: IndexedValue<V> | null
|
|
20
28
|
}
|
|
21
29
|
|
|
30
|
+
export type TopKMoveChanges<V> = {
|
|
31
|
+
/** Flag that marks whether there were any changes to the topK */
|
|
32
|
+
changes: boolean
|
|
33
|
+
/** Indicates which elements move into the topK (if any) */
|
|
34
|
+
moveIns: Array<IndexedValue<V>>
|
|
35
|
+
/** Indicates which elements move out of the topK (if any) */
|
|
36
|
+
moveOuts: Array<IndexedValue<V>>
|
|
37
|
+
}
|
|
38
|
+
|
|
22
39
|
/**
|
|
23
40
|
* A topK data structure that supports insertions and deletions
|
|
24
41
|
* and returns changes to the topK.
|
|
@@ -58,6 +75,49 @@ class TopKArray<V> implements TopK<V> {
|
|
|
58
75
|
return Math.max(0, Math.min(limit, available))
|
|
59
76
|
}
|
|
60
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Moves the topK window
|
|
80
|
+
*/
|
|
81
|
+
move({
|
|
82
|
+
offset,
|
|
83
|
+
limit,
|
|
84
|
+
}: {
|
|
85
|
+
offset?: number
|
|
86
|
+
limit?: number
|
|
87
|
+
}): TopKMoveChanges<V> {
|
|
88
|
+
const oldOffset = this.#topKStart
|
|
89
|
+
const oldLimit = this.#topKEnd - this.#topKStart
|
|
90
|
+
const oldRange: HRange = [this.#topKStart, this.#topKEnd]
|
|
91
|
+
|
|
92
|
+
this.#topKStart = offset ?? oldOffset
|
|
93
|
+
this.#topKEnd = this.#topKStart + (limit ?? oldLimit)
|
|
94
|
+
|
|
95
|
+
const newRange: HRange = [this.#topKStart, this.#topKEnd]
|
|
96
|
+
const { onlyInA, onlyInB } = diffHalfOpen(oldRange, newRange)
|
|
97
|
+
|
|
98
|
+
const moveIns: Array<IndexedValue<V>> = []
|
|
99
|
+
onlyInB.forEach((index) => {
|
|
100
|
+
const value = this.#sortedValues[index]
|
|
101
|
+
if (value) {
|
|
102
|
+
moveIns.push(value)
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const moveOuts: Array<IndexedValue<V>> = []
|
|
107
|
+
onlyInA.forEach((index) => {
|
|
108
|
+
const value = this.#sortedValues[index]
|
|
109
|
+
if (value) {
|
|
110
|
+
moveOuts.push(value)
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// It could be that there are changes (i.e. moveIns or moveOuts)
|
|
115
|
+
// but that the collection is lazy so we don't have the data yet that needs to move in/out
|
|
116
|
+
// so `moveIns` and `moveOuts` will be empty but `changes` will be true
|
|
117
|
+
// this will tell the caller that it needs to run the graph to load more data
|
|
118
|
+
return { moveIns, moveOuts, changes: onlyInA.length + onlyInB.length > 0 }
|
|
119
|
+
}
|
|
120
|
+
|
|
61
121
|
insert(value: V): TopKChanges<V> {
|
|
62
122
|
const result: TopKChanges<V> = { moveIn: null, moveOut: null }
|
|
63
123
|
|
|
@@ -178,8 +238,6 @@ export class TopKWithFractionalIndexOperator<K, T> extends UnaryOperator<
|
|
|
178
238
|
*/
|
|
179
239
|
#topK: TopK<TaggedValue<K, T>>
|
|
180
240
|
|
|
181
|
-
#limit: number
|
|
182
|
-
|
|
183
241
|
constructor(
|
|
184
242
|
id: number,
|
|
185
243
|
inputA: DifferenceStreamReader<[K, T]>,
|
|
@@ -188,7 +246,7 @@ export class TopKWithFractionalIndexOperator<K, T> extends UnaryOperator<
|
|
|
188
246
|
options: TopKWithFractionalIndexOptions
|
|
189
247
|
) {
|
|
190
248
|
super(id, inputA, output)
|
|
191
|
-
|
|
249
|
+
const limit = options.limit ?? Infinity
|
|
192
250
|
const offset = options.offset ?? 0
|
|
193
251
|
const compareTaggedValues = (
|
|
194
252
|
a: TaggedValue<K, T>,
|
|
@@ -204,8 +262,9 @@ export class TopKWithFractionalIndexOperator<K, T> extends UnaryOperator<
|
|
|
204
262
|
const tieBreakerB = getTag(b)
|
|
205
263
|
return tieBreakerA - tieBreakerB
|
|
206
264
|
}
|
|
207
|
-
this.#topK = this.createTopK(offset,
|
|
265
|
+
this.#topK = this.createTopK(offset, limit, compareTaggedValues)
|
|
208
266
|
options.setSizeCallback?.(() => this.#topK.size)
|
|
267
|
+
options.setWindowFn?.(this.moveTopK.bind(this))
|
|
209
268
|
}
|
|
210
269
|
|
|
211
270
|
protected createTopK(
|
|
@@ -216,6 +275,32 @@ export class TopKWithFractionalIndexOperator<K, T> extends UnaryOperator<
|
|
|
216
275
|
return new TopKArray(offset, limit, comparator)
|
|
217
276
|
}
|
|
218
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Moves the topK window based on the provided offset and limit.
|
|
280
|
+
* Any changes to the topK are sent to the output.
|
|
281
|
+
*/
|
|
282
|
+
moveTopK({ offset, limit }: { offset?: number; limit?: number }) {
|
|
283
|
+
if (!(this.#topK instanceof TopKArray)) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`Cannot move B+-tree implementation of TopK with fractional index`
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const result: Array<[[K, IndexedValue<T>], number]> = []
|
|
290
|
+
|
|
291
|
+
const diff = this.#topK.move({ offset, limit })
|
|
292
|
+
|
|
293
|
+
diff.moveIns.forEach((moveIn) => this.handleMoveIn(moveIn, result))
|
|
294
|
+
diff.moveOuts.forEach((moveOut) => this.handleMoveOut(moveOut, result))
|
|
295
|
+
|
|
296
|
+
if (diff.changes) {
|
|
297
|
+
// There are changes to the topK
|
|
298
|
+
// it could be that moveIns and moveOuts are empty
|
|
299
|
+
// because the collection is lazy, so we will run the graph again to load the data
|
|
300
|
+
this.output.sendData(new MultiSet(result))
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
219
304
|
run(): void {
|
|
220
305
|
const result: Array<[[K, IndexedValue<T>], number]> = []
|
|
221
306
|
for (const message of this.inputMessages()) {
|
|
@@ -258,23 +343,36 @@ export class TopKWithFractionalIndexOperator<K, T> extends UnaryOperator<
|
|
|
258
343
|
// so it doesn't affect the topK
|
|
259
344
|
}
|
|
260
345
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
346
|
+
this.handleMoveIn(res.moveIn, result)
|
|
347
|
+
this.handleMoveOut(res.moveOut, result)
|
|
348
|
+
|
|
349
|
+
return
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private handleMoveIn(
|
|
353
|
+
moveIn: IndexedValue<TaggedValue<K, T>> | null,
|
|
354
|
+
result: Array<[[K, IndexedValue<T>], number]>
|
|
355
|
+
) {
|
|
356
|
+
if (moveIn) {
|
|
357
|
+
const index = getIndex(moveIn)
|
|
358
|
+
const taggedValue = getValue(moveIn)
|
|
264
359
|
const k = getKey(taggedValue)
|
|
265
360
|
const val = getVal(taggedValue)
|
|
266
361
|
result.push([[k, [val, index]], 1])
|
|
267
362
|
}
|
|
363
|
+
}
|
|
268
364
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
365
|
+
private handleMoveOut(
|
|
366
|
+
moveOut: IndexedValue<TaggedValue<K, T>> | null,
|
|
367
|
+
result: Array<[[K, IndexedValue<T>], number]>
|
|
368
|
+
) {
|
|
369
|
+
if (moveOut) {
|
|
370
|
+
const index = getIndex(moveOut)
|
|
371
|
+
const taggedValue = getValue(moveOut)
|
|
272
372
|
const k = getKey(taggedValue)
|
|
273
373
|
const val = getVal(taggedValue)
|
|
274
374
|
result.push([[k, [val, index]], -1])
|
|
275
375
|
}
|
|
276
|
-
|
|
277
|
-
return
|
|
278
376
|
}
|
|
279
377
|
|
|
280
378
|
private getMultiplicity(key: K): number {
|
package/src/utils.ts
CHANGED
|
@@ -144,3 +144,36 @@ export function* mapIterable<T, U>(
|
|
|
144
144
|
yield fn(t)
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
|
+
|
|
148
|
+
export type HRange = [number, number] // half-open [start, end[ i.e. end is exclusive
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Computes the difference between two half-open ranges.
|
|
152
|
+
* @param a - The first half-open range
|
|
153
|
+
* @param b - The second half-open range
|
|
154
|
+
* @returns The difference between the two ranges
|
|
155
|
+
*/
|
|
156
|
+
export function diffHalfOpen(a: HRange, b: HRange) {
|
|
157
|
+
const [a1, a2] = a
|
|
158
|
+
const [b1, b2] = b
|
|
159
|
+
|
|
160
|
+
// A \ B can be up to two segments (left and right of the overlap)
|
|
161
|
+
const onlyInA: Array<number> = [
|
|
162
|
+
...range(a1, Math.min(a2, b1)), // left side of A outside B
|
|
163
|
+
...range(Math.max(a1, b2), a2), // right side of A outside B
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
// B \ A similarly
|
|
167
|
+
const onlyInB: Array<number> = [
|
|
168
|
+
...range(b1, Math.min(b2, a1)),
|
|
169
|
+
...range(Math.max(b1, a2), b2),
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
return { onlyInA, onlyInB }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function range(start: number, end: number): Array<number> {
|
|
176
|
+
const out: Array<number> = []
|
|
177
|
+
for (let i = start; i < end; i++) out.push(i)
|
|
178
|
+
return out
|
|
179
|
+
}
|