{"version":3,"file":"topKArray.cjs","sources":["../../../src/operators/topKArray.ts"],"sourcesContent":["import { generateKeyBetween } from 'fractional-indexing'\nimport { binarySearch, compareKeys, diffHalfOpen } from '../utils.js'\nimport type { HRange } from '../utils.js'\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\n/**\n * Creates a comparator for [key, value] tuples that first compares values,\n * then uses the row key as a stable tie-breaker.\n */\nexport function createKeyedComparator<K extends string | number, T>(\n  comparator: (a: T, b: T) => number,\n): (a: [K, T], b: [K, T]) => number {\n  return ([aKey, aVal], [bKey, bVal]) => {\n    // First compare on the value\n    const valueComparison = comparator(aVal, bVal)\n    if (valueComparison !== 0) {\n      return valueComparison\n    }\n    // If the values are equal, use the row key as tie-breaker\n    // This provides stable, deterministic ordering since keys are string | number\n    return compareKeys(aKey, bKey)\n  }\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 */\nexport class 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\n    // `this.#topKEnd` can be `Infinity` if it has no limit\n    // but `diffHalfOpen` expects a finite range\n    // so we restrict it to the size of the topK if topKEnd is infinite\n    const oldRange: HRange = [\n      this.#topKStart,\n      this.#topKEnd === Infinity ? this.#topKStart + this.size : this.#topKEnd,\n    ]\n\n    this.#topKStart = offset ?? oldOffset\n    this.#topKEnd = this.#topKStart + (limit ?? oldLimit) // can be `Infinity` if limit is `Infinity`\n\n    // Also handle `Infinity` in the newRange\n    const newRange: HRange = [\n      this.#topKStart,\n      this.#topKEnd === Infinity\n        ? Math.max(this.#topKStart + this.size, oldRange[1]) // since the new limit is Infinity we need to take everything (so we need to take the biggest (finite) topKEnd)\n        : this.#topKEnd,\n    ]\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"],"names":["compareKeys","diffHalfOpen","generateKeyBetween","binarySearch"],"mappings":";;;;AAQO,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;AAMO,SAAS,sBACd,YACkC;AAClC,SAAO,CAAC,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,MAAM;AAErC,UAAM,kBAAkB,WAAW,MAAM,IAAI;AAC7C,QAAI,oBAAoB,GAAG;AACzB,aAAO;AAAA,IACT;AAGA,WAAOA,MAAAA,YAAY,MAAM,IAAI;AAAA,EAC/B;AACF;AAkCO,MAAM,UAAgC;AAAA,EAC3C,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;AAKtC,UAAM,WAAmB;AAAA,MACvB,KAAK;AAAA,MACL,KAAK,aAAa,WAAW,KAAK,aAAa,KAAK,OAAO,KAAK;AAAA,IAAA;AAGlE,SAAK,aAAa,UAAU;AAC5B,SAAK,WAAW,KAAK,cAAc,SAAS;AAG5C,UAAM,WAAmB;AAAA,MACvB,KAAK;AAAA,MACL,KAAK,aAAa,WACd,KAAK,IAAI,KAAK,aAAa,KAAK,MAAM,SAAS,CAAC,CAAC,IACjD,KAAK;AAAA,IAAA;AAEX,UAAM,EAAE,SAAS,QAAA,IAAYC,MAAAA,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,kBAAkBC,mBAAAA,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,WAAOC,MAAAA;AAAAA,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;;;;;;"}