@@ -2,11 +2,7 @@ import { generateKeyBetween } from 'fractional-indexing'
22import { DifferenceStreamWriter , UnaryOperator } from '../graph.js'
33import { StreamBuilder } from '../d2.js'
44import { MultiSet } from '../multiset.js'
5- import {
6- binarySearch ,
7- diffHalfOpen ,
8- globalObjectIdGenerator ,
9- } from '../utils.js'
5+ import { binarySearch , compareKeys , diffHalfOpen } from '../utils.js'
106import type { HRange } from '../utils.js'
117import type { DifferenceStreamReader } from '../graph.js'
128import type { IStreamBuilder , PipedOperator } from '../types.js'
@@ -239,17 +235,18 @@ class TopKArray<V> implements TopK<V> {
239235 * This operator maintains fractional indices for sorted elements
240236 * and only updates indices when elements move position
241237 */
242- export class TopKWithFractionalIndexOperator < K , T > extends UnaryOperator <
243- [ K , T ] ,
244- [ K , IndexedValue < T > ]
245- > {
238+ export class TopKWithFractionalIndexOperator <
239+ K extends string | number ,
240+ T ,
241+ > extends UnaryOperator < [ K , T ] , [ K , IndexedValue < T > ] > {
246242 #index: Map < K , number > = new Map ( ) // maps keys to their multiplicity
247243
248244 /**
249245 * topK data structure that supports insertions and deletions
250246 * and returns changes to the topK.
247+ * Elements are stored as [key, value] tuples for stable tie-breaking.
251248 */
252- #topK: TopK < TaggedValue < K , T > >
249+ #topK: TopK < [ K , T ] >
253250
254251 constructor (
255252 id : number ,
@@ -261,30 +258,20 @@ export class TopKWithFractionalIndexOperator<K, T> extends UnaryOperator<
261258 super ( id , inputA , output )
262259 const limit = options . limit ?? Infinity
263260 const offset = options . offset ?? 0
264- const compareTaggedValues = (
265- a : TaggedValue < K , T > ,
266- b : TaggedValue < K , T > ,
267- ) => {
268- // First compare on the value
269- const valueComparison = comparator ( getVal ( a ) , getVal ( b ) )
270- if ( valueComparison !== 0 ) {
271- return valueComparison
272- }
273- // If the values are equal, compare on the tag (object identity)
274- const tieBreakerA = getTag ( a )
275- const tieBreakerB = getTag ( b )
276- return tieBreakerA - tieBreakerB
277- }
278- this . #topK = this . createTopK ( offset , limit , compareTaggedValues )
261+ this . #topK = this . createTopK (
262+ offset ,
263+ limit ,
264+ createKeyedComparator ( comparator ) ,
265+ )
279266 options . setSizeCallback ?.( ( ) => this . #topK. size )
280267 options . setWindowFn ?.( this . moveTopK . bind ( this ) )
281268 }
282269
283270 protected createTopK (
284271 offset : number ,
285272 limit : number ,
286- comparator : ( a : TaggedValue < K , T > , b : TaggedValue < K , T > ) => number ,
287- ) : TopK < TaggedValue < K , T > > {
273+ comparator : ( a : [ K , T ] , b : [ K , T ] ) => number ,
274+ ) : TopK < [ K , T ] > {
288275 return new TopKArray ( offset , limit , comparator )
289276 }
290277
@@ -336,20 +323,18 @@ export class TopKWithFractionalIndexOperator<K, T> extends UnaryOperator<
336323 ) : void {
337324 const { oldMultiplicity, newMultiplicity } = this . addKey ( key , multiplicity )
338325
339- let res : TopKChanges < TaggedValue < K , T > > = {
326+ let res : TopKChanges < [ K , T ] > = {
340327 moveIn : null ,
341328 moveOut : null ,
342329 }
343330 if ( oldMultiplicity <= 0 && newMultiplicity > 0 ) {
344331 // The value was invisible but should now be visible
345332 // Need to insert it into the array of sorted values
346- const taggedValue = tagValue ( key , value )
347- res = this . #topK. insert ( taggedValue )
333+ res = this . #topK. insert ( [ key , value ] )
348334 } else if ( oldMultiplicity > 0 && newMultiplicity <= 0 ) {
349335 // The value was visible but should now be invisible
350336 // Need to remove it from the array of sorted values
351- const taggedValue = tagValue ( key , value )
352- res = this . #topK. delete ( taggedValue )
337+ res = this . #topK. delete ( [ key , value ] )
353338 } else {
354339 // The value was invisible and it remains invisible
355340 // or it was visible and remains visible
@@ -363,28 +348,22 @@ export class TopKWithFractionalIndexOperator<K, T> extends UnaryOperator<
363348 }
364349
365350 private handleMoveIn (
366- moveIn : IndexedValue < TaggedValue < K , T > > | null ,
351+ moveIn : IndexedValue < [ K , T ] > | null ,
367352 result : Array < [ [ K , IndexedValue < T > ] , number ] > ,
368353 ) {
369354 if ( moveIn ) {
370- const index = getIndex ( moveIn )
371- const taggedValue = getValue ( moveIn )
372- const k = getKey ( taggedValue )
373- const val = getVal ( taggedValue )
374- result . push ( [ [ k , [ val , index ] ] , 1 ] )
355+ const [ [ key , value ] , index ] = moveIn
356+ result . push ( [ [ key , [ value , index ] ] , 1 ] )
375357 }
376358 }
377359
378360 private handleMoveOut (
379- moveOut : IndexedValue < TaggedValue < K , T > > | null ,
361+ moveOut : IndexedValue < [ K , T ] > | null ,
380362 result : Array < [ [ K , IndexedValue < T > ] , number ] > ,
381363 ) {
382364 if ( moveOut ) {
383- const index = getIndex ( moveOut )
384- const taggedValue = getValue ( moveOut )
385- const k = getKey ( taggedValue )
386- const val = getVal ( taggedValue )
387- result . push ( [ [ k , [ val , index ] ] , - 1 ] )
365+ const [ [ key , value ] , index ] = moveOut
366+ result . push ( [ [ key , [ value , index ] ] , - 1 ] )
388367 }
389368 }
390369
@@ -417,7 +396,7 @@ export class TopKWithFractionalIndexOperator<K, T> extends UnaryOperator<
417396 * @param options - An optional object containing limit and offset properties
418397 * @returns A piped operator that orders the elements and limits the number of results
419398 */
420- export function topKWithFractionalIndex < KType , T > (
399+ export function topKWithFractionalIndex < KType extends string | number , T > (
421400 comparator : ( a : T , b : T ) => number ,
422401 options ?: TopKWithFractionalIndexOptions ,
423402) : PipedOperator < [ KType , T ] , [ KType , IndexedValue < T > ] > {
@@ -461,21 +440,21 @@ export function getIndex<V>(indexedVal: IndexedValue<V>): FractionalIndex {
461440 return indexedVal [ 1 ]
462441}
463442
464- export type Tag = number
465- export type TaggedValue < K , V > = [ K , V , Tag ]
466-
467- function tagValue < K , V > ( key : K , value : V ) : TaggedValue < K , V > {
468- return [ key , value , globalObjectIdGenerator . getId ( key ) ]
469- }
470-
471- function getKey < K , V > ( tieBreakerTaggedValue : TaggedValue < K , V > ) : K {
472- return tieBreakerTaggedValue [ 0 ]
473- }
474-
475- function getVal < K , V > ( tieBreakerTaggedValue : TaggedValue < K , V > ) : V {
476- return tieBreakerTaggedValue [ 1 ]
477- }
478-
479- function getTag < K , V > ( tieBreakerTaggedValue : TaggedValue < K , V > ) : Tag {
480- return tieBreakerTaggedValue [ 2 ]
443+ /**
444+ * Creates a comparator for [key, value] tuples that first compares values,
445+ * then uses the row key as a stable tie-breaker.
446+ */
447+ function createKeyedComparator < K extends string | number , T > (
448+ comparator : ( a : T , b : T ) => number ,
449+ ) : ( a : [ K , T ] , b : [ K , T ] ) => number {
450+ return ( [ aKey , aVal ] , [ bKey , bVal ] ) => {
451+ // First compare on the value
452+ const valueComparison = comparator ( aVal , bVal )
453+ if ( valueComparison !== 0 ) {
454+ return valueComparison
455+ }
456+ // If the values are equal, use the row key as tie-breaker
457+ // This provides stable, deterministic ordering since keys are string | number
458+ return compareKeys ( aKey , bKey )
459+ }
481460}
0 commit comments