From 46e2e152164767e010b40130236b6b6fc342879c Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Mon, 16 Mar 2026 18:26:48 -0700 Subject: [PATCH 01/17] start on priority queue --- src/data_manager/PriorityQueue.ts | 113 ++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 src/data_manager/PriorityQueue.ts diff --git a/src/data_manager/PriorityQueue.ts b/src/data_manager/PriorityQueue.ts new file mode 100644 index 000000000..569bcd3ad --- /dev/null +++ b/src/data_manager/PriorityQueue.ts @@ -0,0 +1,113 @@ +type Entry = { + priority: P; + value: V; + heapIndex: number; +}; + +const entryToTuple = (entry?: Entry): [P, V] | undefined => entry && [entry.priority, entry.value]; + +const parentIndexOf = (i: number) => Math.ceil(i / 2) - 1; +const childIndexOf = (i: number) => i * 2 + 1; + +export class PriorityQueue { + private contents: (Entry | number)[]; + private indexes: Map; + private heap: number[]; + private nextIndex: number; + private compare: (i: P, j: P) => boolean; + + constructor(compare: (i: P, j: P) => boolean) { + this.compare = compare; + this.contents = []; + this.indexes = new Map(); + this.heap = []; + this.nextIndex = 0; + } + + get length() { + return this.heap.length; + } + + private siftUp(entry: Entry) { + // TODO + } + + private siftDown(heapIndex: number) { + // TODO + } + + private removeIndex(index: number | undefined): Entry | undefined { + if (index === undefined) { + return undefined; + } + + const result = this.contents[index]; + if (typeof result === "number") { + return undefined; + } + + this.indexes.delete(result.value); + this.contents[index] = this.nextIndex; + this.nextIndex = index; + this.heap[result.heapIndex] = this.heap.pop() as number; + this.siftDown(result.heapIndex); + + return result; + } + + insert(priority: P, value: V) { + const index = this.nextIndex; + const entry = { priority, value, heapIndex: this.length }; + + if (index < this.contents.length) { + // reuse an index + const nextFree = this.contents[this.nextIndex] as number; + this.contents[this.nextIndex] = entry; + this.nextIndex = nextFree; + } else { + // push onto the end + this.contents.push(entry); + this.nextIndex += 1; + } + + this.indexes.set(value, index); + this.heap.push(index); + this.siftUp(entry); + } + + update(value: V, newPriority: P): boolean { + const index = this.indexes.get(value); + if (index === undefined) { + return false; + } + + const entry = this.contents[index]; + if (typeof entry === "number") { + return false; + } + + const priorityIncreased = this.compare(newPriority, entry.priority); + entry.priority = newPriority; + if (priorityIncreased) { + this.siftUp(entry); + } else { + this.siftDown(entry.heapIndex); + } + + return true; + } + + // peek(): [P, V] { + // return this.contents[this.heap[0]] as [P, V]; + // } + + pop(): [P, V] | undefined { + const index = this.heap[0]; + return entryToTuple(this.removeIndex(index)); + } + + remove(value: V): P | undefined { + const index = this.indexes.get(value); + return this.removeIndex(index)?.priority; + } +} From 7f76be4cdb1216f598ebddd298a8d03d70a09d01 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 17 Mar 2026 11:13:26 -0700 Subject: [PATCH 02/17] rebuild `PriorityQueue` around new `IndexMap` class --- src/data_manager/PriorityQueue.ts | 101 +++++++++++++++++++----------- 1 file changed, 66 insertions(+), 35 deletions(-) diff --git a/src/data_manager/PriorityQueue.ts b/src/data_manager/PriorityQueue.ts index 569bcd3ad..bb982ee6c 100644 --- a/src/data_manager/PriorityQueue.ts +++ b/src/data_manager/PriorityQueue.ts @@ -9,19 +9,63 @@ const entryToTuple = (entry?: Entry): [P, V] | undefined => entry && const parentIndexOf = (i: number) => Math.ceil(i / 2) - 1; const childIndexOf = (i: number) => i * 2 + 1; +class IndexMap> { + private contents: (T | number)[] = []; + private nextIndex = 0; + + insert(value: T): number { + if (this.nextIndex < this.contents.length) { + // reuse an index + const index = this.nextIndex; + this.nextIndex = this.contents[this.nextIndex] as number; + this.contents[index] = value; + return index; + } else { + // push onto the end + const index = this.contents.length; + this.contents.push(value); + this.nextIndex += 1; + return index; + } + } + + get(index: number | undefined): T | undefined { + if (index === undefined) { + return undefined; + } + + const result = this.contents[index]; + + if (result === undefined || typeof result === "number") { + return undefined; + } + + return result; + } + + remove(index: number | undefined): T | undefined { + const entry = this.get(index); + + if (entry === undefined) { + return undefined; + } + + // `get` asserts that `index` is a number + this.contents[index as number] = this.nextIndex; + this.nextIndex = index as number; + return entry; + } +} + export class PriorityQueue { - private contents: (Entry | number)[]; + private contents: IndexMap>; private indexes: Map; private heap: number[]; - private nextIndex: number; - private compare: (i: P, j: P) => boolean; - constructor(compare: (i: P, j: P) => boolean) { - this.compare = compare; - this.contents = []; + constructor(private compare: (i: P, j: P) => boolean) { + this.contents = new IndexMap(); this.indexes = new Map(); this.heap = []; - this.nextIndex = 0; } get length() { @@ -37,18 +81,13 @@ export class PriorityQueue { } private removeIndex(index: number | undefined): Entry | undefined { - if (index === undefined) { - return undefined; - } + const result = this.contents.remove(index); - const result = this.contents[index]; - if (typeof result === "number") { + if (result === undefined) { return undefined; } this.indexes.delete(result.value); - this.contents[index] = this.nextIndex; - this.nextIndex = index; this.heap[result.heapIndex] = this.heap.pop() as number; this.siftDown(result.heapIndex); @@ -56,20 +95,8 @@ export class PriorityQueue { } insert(priority: P, value: V) { - const index = this.nextIndex; - const entry = { priority, value, heapIndex: this.length }; - - if (index < this.contents.length) { - // reuse an index - const nextFree = this.contents[this.nextIndex] as number; - this.contents[this.nextIndex] = entry; - this.nextIndex = nextFree; - } else { - // push onto the end - this.contents.push(entry); - this.nextIndex += 1; - } - + const entry = { priority, value, heapIndex: this.heap.length }; + const index = this.contents.insert(entry); this.indexes.set(value, index); this.heap.push(index); this.siftUp(entry); @@ -81,8 +108,8 @@ export class PriorityQueue { return false; } - const entry = this.contents[index]; - if (typeof entry === "number") { + const entry = this.contents.get(index); + if (entry === undefined) { return false; } @@ -97,13 +124,17 @@ export class PriorityQueue { return true; } - // peek(): [P, V] { - // return this.contents[this.heap[0]] as [P, V]; - // } + peek(): [P, V] | undefined { + const entry = this.contents.get(this.heap[0]); + if (entry === undefined) { + return undefined; + } + + return entryToTuple(entry); + } pop(): [P, V] | undefined { - const index = this.heap[0]; - return entryToTuple(this.removeIndex(index)); + return entryToTuple(this.removeIndex(this.heap[0])); } remove(value: V): P | undefined { From dc400e0a023423442808a7f27a012a0773c97d5a Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Wed, 18 Mar 2026 17:27:20 -0700 Subject: [PATCH 03/17] mostly working priority queue --- src/data_manager/PriorityQueue.ts | 96 +++++++++++++++++++++++-------- 1 file changed, 72 insertions(+), 24 deletions(-) diff --git a/src/data_manager/PriorityQueue.ts b/src/data_manager/PriorityQueue.ts index bb982ee6c..c8afb0663 100644 --- a/src/data_manager/PriorityQueue.ts +++ b/src/data_manager/PriorityQueue.ts @@ -4,12 +4,14 @@ type Entry = { heapIndex: number; }; +type ComparisonFn

= (i: P, j: P) => boolean; + const entryToTuple = (entry?: Entry): [P, V] | undefined => entry && [entry.priority, entry.value]; const parentIndexOf = (i: number) => Math.ceil(i / 2) - 1; const childIndexOf = (i: number) => i * 2 + 1; -class IndexMap> { +class IndexMap { private contents: (T | number)[] = []; private nextIndex = 0; @@ -58,26 +60,67 @@ class IndexMap> { } export class PriorityQueue { - private contents: IndexMap>; - private indexes: Map; - private heap: number[]; - - constructor(private compare: (i: P, j: P) => boolean) { - this.contents = new IndexMap(); - this.indexes = new Map(); - this.heap = []; + private contents: IndexMap> = new IndexMap(); + private indexes: Map = new Map(); + private heap: number[] = []; + + private gt: ComparisonFn

; + private lt: ComparisonFn

; + + constructor(greaterThan: ComparisonFn

) { + this.gt = greaterThan; + this.lt = (i, j) => greaterThan(j, i); } get length() { return this.heap.length; } - private siftUp(entry: Entry) { - // TODO + private trySwap(entry: Entry, swapHeapIndex: number, shouldSwap = this.gt): boolean { + const swapIndex = this.heap[swapHeapIndex]; + const swapEntry = this.contents.get(swapIndex); + + if (swapEntry !== undefined && shouldSwap(entry.priority, swapEntry.priority)) { + this.heap[entry.heapIndex] = swapIndex; + const newHeapIndex = swapEntry.heapIndex; + swapEntry.heapIndex = entry.heapIndex; + entry.heapIndex = newHeapIndex; + return true; + } + + return false; } - private siftDown(heapIndex: number) { - // TODO + private siftUp(index: number, entry: Entry) { + let parentHeapIndex = entry.heapIndex; + + do { + parentHeapIndex = parentIndexOf(parentHeapIndex); + } while (this.trySwap(entry, parentHeapIndex)); + + this.heap[entry.heapIndex] = index; + } + + private siftDown(index: number, entry: Entry) { + let childHeapIndex = entry.heapIndex; + + // eslint-disable-next-line no-constant-condition + while (true) { + childHeapIndex = childIndexOf(childHeapIndex); + + if (this.trySwap(entry, childHeapIndex, this.lt)) { + continue; + } + + childHeapIndex += 1; + if (this.trySwap(entry, childHeapIndex, this.lt)) { + continue; + } + + break; + } + + this.heap[entry.heapIndex] = index; } private removeIndex(index: number | undefined): Entry | undefined { @@ -88,8 +131,17 @@ export class PriorityQueue { } this.indexes.delete(result.value); - this.heap[result.heapIndex] = this.heap.pop() as number; - this.siftDown(result.heapIndex); + console.log(this.heap.slice(0, 15)); + + // Replace the removed entry with the last in the heap, then sift it back down + const siftIndex = this.heap.pop() as number; + this.heap[result.heapIndex] = siftIndex; + const siftedEntry = this.contents.get(siftIndex); + if (siftedEntry !== undefined) { + // TODO don't like this. rework? + siftedEntry.heapIndex = result.heapIndex; + this.siftDown(siftIndex, siftedEntry); + } return result; } @@ -99,26 +151,22 @@ export class PriorityQueue { const index = this.contents.insert(entry); this.indexes.set(value, index); this.heap.push(index); - this.siftUp(entry); + this.siftUp(index, entry); } update(value: V, newPriority: P): boolean { const index = this.indexes.get(value); - if (index === undefined) { - return false; - } - const entry = this.contents.get(index); - if (entry === undefined) { + if (entry === undefined || index === undefined) { return false; } - const priorityIncreased = this.compare(newPriority, entry.priority); + const priorityIncreased = this.gt(newPriority, entry.priority); entry.priority = newPriority; if (priorityIncreased) { - this.siftUp(entry); + this.siftUp(index, entry); } else { - this.siftDown(entry.heapIndex); + this.siftDown(index, entry); } return true; From 5b1a5b636a7846b5396b50fbbbe127bc137e80a2 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Wed, 18 Mar 2026 17:48:54 -0700 Subject: [PATCH 04/17] fix `siftDown` --- src/data_manager/PriorityQueue.ts | 46 +++++++++++++++---------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/data_manager/PriorityQueue.ts b/src/data_manager/PriorityQueue.ts index c8afb0663..1908269bd 100644 --- a/src/data_manager/PriorityQueue.ts +++ b/src/data_manager/PriorityQueue.ts @@ -76,10 +76,7 @@ export class PriorityQueue { return this.heap.length; } - private trySwap(entry: Entry, swapHeapIndex: number, shouldSwap = this.gt): boolean { - const swapIndex = this.heap[swapHeapIndex]; - const swapEntry = this.contents.get(swapIndex); - + private trySwap(entry: Entry, swapIndex: number, swapEntry?: Entry, shouldSwap = this.gt): boolean { if (swapEntry !== undefined && shouldSwap(entry.priority, swapEntry.priority)) { this.heap[entry.heapIndex] = swapIndex; const newHeapIndex = swapEntry.heapIndex; @@ -92,33 +89,37 @@ export class PriorityQueue { } private siftUp(index: number, entry: Entry) { - let parentHeapIndex = entry.heapIndex; + let parentIndex: number; + let parentEntry: Entry | undefined; do { - parentHeapIndex = parentIndexOf(parentHeapIndex); - } while (this.trySwap(entry, parentHeapIndex)); + const parentHeapIndex = parentIndexOf(entry.heapIndex); + parentIndex = this.heap[parentHeapIndex]; + parentEntry = this.contents.get(parentIndex); + } while (this.trySwap(entry, parentIndex, parentEntry)); this.heap[entry.heapIndex] = index; } private siftDown(index: number, entry: Entry) { - let childHeapIndex = entry.heapIndex; - - // eslint-disable-next-line no-constant-condition - while (true) { - childHeapIndex = childIndexOf(childHeapIndex); - - if (this.trySwap(entry, childHeapIndex, this.lt)) { - continue; - } + let childIndex: number; + let childEntry: Entry | undefined; - childHeapIndex += 1; - if (this.trySwap(entry, childHeapIndex, this.lt)) { - continue; + do { + const childHeapIndex = childIndexOf(entry.heapIndex); + const cIndex1 = this.heap[childHeapIndex]; + const cIndex2 = this.heap[childHeapIndex + 1]; + const cEntry1 = this.contents.get(cIndex1); + const cEntry2 = this.contents.get(cIndex2); + + if (cEntry1 !== undefined && cEntry2 !== undefined && this.gt(cEntry2.priority, cEntry1.priority)) { + childIndex = cIndex2; + childEntry = cEntry2; + } else { + childIndex = cIndex1; + childEntry = cEntry1; } - - break; - } + } while (this.trySwap(entry, childIndex, childEntry, this.lt)); this.heap[entry.heapIndex] = index; } @@ -131,7 +132,6 @@ export class PriorityQueue { } this.indexes.delete(result.value); - console.log(this.heap.slice(0, 15)); // Replace the removed entry with the last in the heap, then sift it back down const siftIndex = this.heap.pop() as number; From 8a41b66d8d614a097cf47775a4f7526ee33beb08 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Wed, 18 Mar 2026 17:50:33 -0700 Subject: [PATCH 05/17] start on queue tests --- src/test/PriorityQueue.test.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/test/PriorityQueue.test.ts diff --git a/src/test/PriorityQueue.test.ts b/src/test/PriorityQueue.test.ts new file mode 100644 index 000000000..7989d300e --- /dev/null +++ b/src/test/PriorityQueue.test.ts @@ -0,0 +1,23 @@ +import { PriorityQueue } from "../data_manager/PriorityQueue.js"; + +describe("PriorityQueue", () => { + it("pops items in priority order", () => { + const queue = new PriorityQueue((i, j) => i > j); + const entryMap = new Map(); + const priorities: number[] = []; + + for (let i = 0; i < 100; i++) { + const priority = Math.random(); + const key = i.toString(); + queue.insert(priority, key); + entryMap.set(priority, key); + priorities.push(priority); + } + + priorities.sort((a, b) => b - a); + for (const priority of priorities) { + expect(queue.pop()?.[1]).to.equal(entryMap.get(priority)); + } + expect(queue.pop()).to.equal(undefined); + }); +}); From 795396a9595b0aa80a8939d118cd3d2e763cf775 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Wed, 18 Mar 2026 18:55:03 -0700 Subject: [PATCH 06/17] I don't need `IndexMap`! This isn't Rust! --- src/data_manager/PriorityQueue.ts | 197 +++++++++++++----------------- 1 file changed, 87 insertions(+), 110 deletions(-) diff --git a/src/data_manager/PriorityQueue.ts b/src/data_manager/PriorityQueue.ts index 1908269bd..1ed5919d9 100644 --- a/src/data_manager/PriorityQueue.ts +++ b/src/data_manager/PriorityQueue.ts @@ -11,58 +11,58 @@ const entryToTuple = (entry?: Entry): [P, V] | undefined => entry && const parentIndexOf = (i: number) => Math.ceil(i / 2) - 1; const childIndexOf = (i: number) => i * 2 + 1; -class IndexMap { - private contents: (T | number)[] = []; - private nextIndex = 0; - - insert(value: T): number { - if (this.nextIndex < this.contents.length) { - // reuse an index - const index = this.nextIndex; - this.nextIndex = this.contents[this.nextIndex] as number; - this.contents[index] = value; - return index; - } else { - // push onto the end - const index = this.contents.length; - this.contents.push(value); - this.nextIndex += 1; - return index; - } - } - - get(index: number | undefined): T | undefined { - if (index === undefined) { - return undefined; - } - - const result = this.contents[index]; - - if (result === undefined || typeof result === "number") { - return undefined; - } - - return result; - } - - remove(index: number | undefined): T | undefined { - const entry = this.get(index); - - if (entry === undefined) { - return undefined; - } - - // `get` asserts that `index` is a number - this.contents[index as number] = this.nextIndex; - this.nextIndex = index as number; - return entry; - } -} +// class IndexMap { +// private contents: (T | number)[] = []; +// private nextIndex = 0; + +// insert(value: T): number { +// if (this.nextIndex < this.contents.length) { +// // reuse an index +// const index = this.nextIndex; +// this.nextIndex = this.contents[this.nextIndex] as number; +// this.contents[index] = value; +// return index; +// } else { +// // push onto the end +// const index = this.contents.length; +// this.contents.push(value); +// this.nextIndex += 1; +// return index; +// } +// } + +// get(index: number | undefined): T | undefined { +// if (index === undefined) { +// return undefined; +// } + +// const result = this.contents[index]; + +// if (result === undefined || typeof result === "number") { +// return undefined; +// } + +// return result; +// } + +// remove(index: number | undefined): T | undefined { +// const entry = this.get(index); + +// if (entry === undefined) { +// return undefined; +// } + +// // `get` asserts that `index` is a number +// this.contents[index as number] = this.nextIndex; +// this.nextIndex = index as number; +// return entry; +// } +// } export class PriorityQueue { - private contents: IndexMap> = new IndexMap(); - private indexes: Map = new Map(); - private heap: number[] = []; + // private contents: IndexMap> = new IndexMap(); + private keys: Map> = new Map(); + private heap: Entry[] = []; private gt: ComparisonFn

; private lt: ComparisonFn

; @@ -76,9 +76,9 @@ export class PriorityQueue { return this.heap.length; } - private trySwap(entry: Entry, swapIndex: number, swapEntry?: Entry, shouldSwap = this.gt): boolean { + private trySwap(entry: Entry, swapEntry?: Entry, shouldSwap = this.gt): boolean { if (swapEntry !== undefined && shouldSwap(entry.priority, swapEntry.priority)) { - this.heap[entry.heapIndex] = swapIndex; + this.heap[entry.heapIndex] = swapEntry; const newHeapIndex = swapEntry.heapIndex; swapEntry.heapIndex = entry.heapIndex; entry.heapIndex = newHeapIndex; @@ -88,105 +88,82 @@ export class PriorityQueue { return false; } - private siftUp(index: number, entry: Entry) { - let parentIndex: number; + private siftUp(entry: Entry) { let parentEntry: Entry | undefined; do { - const parentHeapIndex = parentIndexOf(entry.heapIndex); - parentIndex = this.heap[parentHeapIndex]; - parentEntry = this.contents.get(parentIndex); - } while (this.trySwap(entry, parentIndex, parentEntry)); + parentEntry = this.heap[parentIndexOf(entry.heapIndex)]; + } while (this.trySwap(entry, parentEntry)); - this.heap[entry.heapIndex] = index; + this.heap[entry.heapIndex] = entry; } - private siftDown(index: number, entry: Entry) { - let childIndex: number; + private siftDown(entry: Entry) { let childEntry: Entry | undefined; do { const childHeapIndex = childIndexOf(entry.heapIndex); - const cIndex1 = this.heap[childHeapIndex]; - const cIndex2 = this.heap[childHeapIndex + 1]; - const cEntry1 = this.contents.get(cIndex1); - const cEntry2 = this.contents.get(cIndex2); - - if (cEntry1 !== undefined && cEntry2 !== undefined && this.gt(cEntry2.priority, cEntry1.priority)) { - childIndex = cIndex2; - childEntry = cEntry2; - } else { - childIndex = cIndex1; - childEntry = cEntry1; - } - } while (this.trySwap(entry, childIndex, childEntry, this.lt)); - - this.heap[entry.heapIndex] = index; - } + const cEntry1 = this.heap[childHeapIndex]; + const cEntry2 = this.heap[childHeapIndex + 1]; + const use2 = cEntry1 !== undefined && cEntry2 !== undefined && this.gt(cEntry2.priority, cEntry1.priority); + childEntry = use2 ? cEntry2 : cEntry1; + } while (this.trySwap(entry, childEntry, this.lt)); - private removeIndex(index: number | undefined): Entry | undefined { - const result = this.contents.remove(index); + this.heap[entry.heapIndex] = entry; + } - if (result === undefined) { - return undefined; + private removeEntry(entry?: Entry) { + if (entry === undefined) { + return; } - this.indexes.delete(result.value); + this.keys.delete(entry.value); + const siftEntry = this.heap.pop(); - // Replace the removed entry with the last in the heap, then sift it back down - const siftIndex = this.heap.pop() as number; - this.heap[result.heapIndex] = siftIndex; - const siftedEntry = this.contents.get(siftIndex); - if (siftedEntry !== undefined) { - // TODO don't like this. rework? - siftedEntry.heapIndex = result.heapIndex; - this.siftDown(siftIndex, siftedEntry); + if (siftEntry !== undefined && siftEntry !== entry) { + this.heap[entry.heapIndex] = siftEntry; + siftEntry.heapIndex = entry.heapIndex; + this.siftDown(siftEntry); } - - return result; } insert(priority: P, value: V) { const entry = { priority, value, heapIndex: this.heap.length }; - const index = this.contents.insert(entry); - this.indexes.set(value, index); - this.heap.push(index); - this.siftUp(index, entry); + this.keys.set(value, entry); + this.heap.push(entry); + this.siftUp(entry); } update(value: V, newPriority: P): boolean { - const index = this.indexes.get(value); - const entry = this.contents.get(index); - if (entry === undefined || index === undefined) { + const entry = this.keys.get(value); + if (entry === undefined) { return false; } const priorityIncreased = this.gt(newPriority, entry.priority); entry.priority = newPriority; if (priorityIncreased) { - this.siftUp(index, entry); + this.siftUp(entry); } else { - this.siftDown(index, entry); + this.siftDown(entry); } return true; } peek(): [P, V] | undefined { - const entry = this.contents.get(this.heap[0]); - if (entry === undefined) { - return undefined; - } - - return entryToTuple(entry); + return entryToTuple(this.heap[0]); } pop(): [P, V] | undefined { - return entryToTuple(this.removeIndex(this.heap[0])); + const entry = this.heap[0]; + this.removeEntry(entry); + return entryToTuple(entry); } remove(value: V): P | undefined { - const index = this.indexes.get(value); - return this.removeIndex(index)?.priority; + const entry = this.keys.get(value); + this.removeEntry(entry); + return entry?.priority; } } From 2961b3032324821a9071ef1c9ea5abf2e1eeaa93 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Wed, 18 Mar 2026 19:13:04 -0700 Subject: [PATCH 07/17] delete `IndexMap` for real --- src/data_manager/PriorityQueue.ts | 49 ------------------------------- 1 file changed, 49 deletions(-) diff --git a/src/data_manager/PriorityQueue.ts b/src/data_manager/PriorityQueue.ts index 1ed5919d9..e1c4df8aa 100644 --- a/src/data_manager/PriorityQueue.ts +++ b/src/data_manager/PriorityQueue.ts @@ -11,56 +11,7 @@ const entryToTuple = (entry?: Entry): [P, V] | undefined => entry && const parentIndexOf = (i: number) => Math.ceil(i / 2) - 1; const childIndexOf = (i: number) => i * 2 + 1; -// class IndexMap { -// private contents: (T | number)[] = []; -// private nextIndex = 0; - -// insert(value: T): number { -// if (this.nextIndex < this.contents.length) { -// // reuse an index -// const index = this.nextIndex; -// this.nextIndex = this.contents[this.nextIndex] as number; -// this.contents[index] = value; -// return index; -// } else { -// // push onto the end -// const index = this.contents.length; -// this.contents.push(value); -// this.nextIndex += 1; -// return index; -// } -// } - -// get(index: number | undefined): T | undefined { -// if (index === undefined) { -// return undefined; -// } - -// const result = this.contents[index]; - -// if (result === undefined || typeof result === "number") { -// return undefined; -// } - -// return result; -// } - -// remove(index: number | undefined): T | undefined { -// const entry = this.get(index); - -// if (entry === undefined) { -// return undefined; -// } - -// // `get` asserts that `index` is a number -// this.contents[index as number] = this.nextIndex; -// this.nextIndex = index as number; -// return entry; -// } -// } - export class PriorityQueue { - // private contents: IndexMap> = new IndexMap(); private keys: Map> = new Map(); private heap: Entry[] = []; From 00c01bc6e9586cf307c9a8f71aef099f0dc533ed Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Wed, 18 Mar 2026 19:13:17 -0700 Subject: [PATCH 08/17] more tests! --- src/test/PriorityQueue.test.ts | 51 ++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/src/test/PriorityQueue.test.ts b/src/test/PriorityQueue.test.ts index 7989d300e..3239bfd6c 100644 --- a/src/test/PriorityQueue.test.ts +++ b/src/test/PriorityQueue.test.ts @@ -1,23 +1,50 @@ import { PriorityQueue } from "../data_manager/PriorityQueue.js"; +const setup = (): [PriorityQueue, Map, number[]] => { + const queue = new PriorityQueue((i, j) => i > j); + const entryMap = new Map(); + const priorities: number[] = []; + + for (let i = 0; i < 100; i++) { + const priority = Math.random(); + const key = i.toString(); + queue.insert(priority, key); + entryMap.set(priority, key); + priorities.push(priority); + } + + return [queue, entryMap, priorities]; +}; + +const sort = (priorities: number[]) => priorities.sort((a, b) => b - a); + describe("PriorityQueue", () => { it("pops items in priority order", () => { - const queue = new PriorityQueue((i, j) => i > j); - const entryMap = new Map(); - const priorities: number[] = []; - - for (let i = 0; i < 100; i++) { - const priority = Math.random(); - const key = i.toString(); - queue.insert(priority, key); - entryMap.set(priority, key); - priorities.push(priority); - } + const [queue, entryMap, priorities] = setup(); + sort(priorities); - priorities.sort((a, b) => b - a); for (const priority of priorities) { expect(queue.pop()?.[1]).to.equal(entryMap.get(priority)); } expect(queue.pop()).to.equal(undefined); }); + + it("keeps items in order after some have been removed", () => { + const [queue, entryMap, priorities] = setup(); + + const removed = priorities.slice(0, 5); + const rest = priorities.slice(5); + + for (const p of removed) { + const key = entryMap.get(p) as string; + const removedPriority = queue.remove(key); + expect(removedPriority).to.equal(p); + } + + sort(rest); + for (const p of rest) { + expect(queue.pop()?.[1]).to.equal(entryMap.get(p)); + } + expect(queue.pop()).to.equal(undefined); + }); }); From a986341a56bdd681b77f4ff35c697b86ec45756f Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Wed, 18 Mar 2026 23:54:24 -0700 Subject: [PATCH 09/17] another test! --- src/test/PriorityQueue.test.ts | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/test/PriorityQueue.test.ts b/src/test/PriorityQueue.test.ts index 3239bfd6c..a40512e9f 100644 --- a/src/test/PriorityQueue.test.ts +++ b/src/test/PriorityQueue.test.ts @@ -1,11 +1,13 @@ import { PriorityQueue } from "../data_manager/PriorityQueue.js"; +const ENTRY_COUNT = 100; + const setup = (): [PriorityQueue, Map, number[]] => { const queue = new PriorityQueue((i, j) => i > j); const entryMap = new Map(); const priorities: number[] = []; - for (let i = 0; i < 100; i++) { + for (let i = 0; i < ENTRY_COUNT; i++) { const priority = Math.random(); const key = i.toString(); queue.insert(priority, key); @@ -30,10 +32,11 @@ describe("PriorityQueue", () => { }); it("keeps items in order after some have been removed", () => { + const REMOVE_COUNT = 10; const [queue, entryMap, priorities] = setup(); - const removed = priorities.slice(0, 5); - const rest = priorities.slice(5); + const removed = priorities.slice(0, REMOVE_COUNT); + const rest = priorities.slice(REMOVE_COUNT); for (const p of removed) { const key = entryMap.get(p) as string; @@ -47,4 +50,22 @@ describe("PriorityQueue", () => { } expect(queue.pop()).to.equal(undefined); }); + + it("sorts items correctly when their priorities change", () => { + const [queue, entryMap, priorities] = setup(); + + const newPriorities = priorities.map((_, i) => { + const key = i.toString(); + const newPriority = Math.random(); + queue.update(key, newPriority); + entryMap.set(newPriority, key); + return newPriority; + }); + + sort(newPriorities); + for (const p of newPriorities) { + expect(queue.pop()?.[1]).to.equal(entryMap.get(p)); + } + expect(queue.pop()).to.equal(undefined); + }); }); From c5bf0737b631cc3286b0529e9f1ed528fd38b348 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Thu, 19 Mar 2026 00:15:05 -0700 Subject: [PATCH 10/17] don't insert duplicate keys --- src/data_manager/PriorityQueue.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/data_manager/PriorityQueue.ts b/src/data_manager/PriorityQueue.ts index e1c4df8aa..2e80e2e8c 100644 --- a/src/data_manager/PriorityQueue.ts +++ b/src/data_manager/PriorityQueue.ts @@ -78,13 +78,6 @@ export class PriorityQueue { } } - insert(priority: P, value: V) { - const entry = { priority, value, heapIndex: this.heap.length }; - this.keys.set(value, entry); - this.heap.push(entry); - this.siftUp(entry); - } - update(value: V, newPriority: P): boolean { const entry = this.keys.get(value); if (entry === undefined) { @@ -102,6 +95,17 @@ export class PriorityQueue { return true; } + insert(value: V, priority: P) { + if (this.update(value, priority)) { + return; + } + + const entry = { priority, value, heapIndex: this.heap.length }; + this.keys.set(value, entry); + this.heap.push(entry); + this.siftUp(entry); + } + peek(): [P, V] | undefined { return entryToTuple(this.heap[0]); } From e9491583c0615ebec2f0f91d170acfec6679456b Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Thu, 19 Mar 2026 00:18:42 -0700 Subject: [PATCH 11/17] fix tests (whoops) --- src/test/PriorityQueue.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/PriorityQueue.test.ts b/src/test/PriorityQueue.test.ts index a40512e9f..4701e8f4e 100644 --- a/src/test/PriorityQueue.test.ts +++ b/src/test/PriorityQueue.test.ts @@ -10,7 +10,7 @@ const setup = (): [PriorityQueue, Map, number[]] for (let i = 0; i < ENTRY_COUNT; i++) { const priority = Math.random(); const key = i.toString(); - queue.insert(priority, key); + queue.insert(key, priority); entryMap.set(priority, key); priorities.push(priority); } From 230080572cc28921620e6de09d41fc225e4fbf52 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Thu, 19 Mar 2026 10:30:52 -0700 Subject: [PATCH 12/17] doc comments for `PriorityQueue` --- src/data_manager/PriorityQueue.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/data_manager/PriorityQueue.ts b/src/data_manager/PriorityQueue.ts index 2e80e2e8c..e78309a99 100644 --- a/src/data_manager/PriorityQueue.ts +++ b/src/data_manager/PriorityQueue.ts @@ -27,6 +27,7 @@ export class PriorityQueue { return this.heap.length; } + /** Useful common primitive for `siftUp` and `siftDown`: compares two entries and swaps them if out of order. */ private trySwap(entry: Entry, swapEntry?: Entry, shouldSwap = this.gt): boolean { if (swapEntry !== undefined && shouldSwap(entry.priority, swapEntry.priority)) { this.heap[entry.heapIndex] = swapEntry; @@ -39,6 +40,7 @@ export class PriorityQueue { return false; } + /** Moves `entry` *up* to the correct position within the heap. */ private siftUp(entry: Entry) { let parentEntry: Entry | undefined; @@ -49,6 +51,7 @@ export class PriorityQueue { this.heap[entry.heapIndex] = entry; } + /** Moves `entry` *down* to the correct position within the heap. */ private siftDown(entry: Entry) { let childEntry: Entry | undefined; @@ -78,6 +81,7 @@ export class PriorityQueue { } } + /** Updates the priority of value `value` to `newPriority`, or returns `false` if no such value is in the queue. */ update(value: V, newPriority: P): boolean { const entry = this.keys.get(value); if (entry === undefined) { @@ -95,6 +99,7 @@ export class PriorityQueue { return true; } + /** Adds a new `value` to the queue with the given `priority`. */ insert(value: V, priority: P) { if (this.update(value, priority)) { return; @@ -106,16 +111,19 @@ export class PriorityQueue { this.siftUp(entry); } + /** Returns the highest-priority item in the queue, without removing it. */ peek(): [P, V] | undefined { return entryToTuple(this.heap[0]); } + /** Removes and returns the highest-priority item in the queue. */ pop(): [P, V] | undefined { const entry = this.heap[0]; this.removeEntry(entry); return entryToTuple(entry); } + /** Removes the item with value `value` from the queue. */ remove(value: V): P | undefined { const entry = this.keys.get(value); this.removeEntry(entry); From a36437b180c0d86650a2235609dcd23f1c83a2e4 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Fri, 27 Mar 2026 16:44:24 -0700 Subject: [PATCH 13/17] fix `PriorityQueue.removeEntry` --- src/data_manager/PriorityQueue.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/data_manager/PriorityQueue.ts b/src/data_manager/PriorityQueue.ts index e78309a99..9278bfcd3 100644 --- a/src/data_manager/PriorityQueue.ts +++ b/src/data_manager/PriorityQueue.ts @@ -77,7 +77,11 @@ export class PriorityQueue { if (siftEntry !== undefined && siftEntry !== entry) { this.heap[entry.heapIndex] = siftEntry; siftEntry.heapIndex = entry.heapIndex; - this.siftDown(siftEntry); + if (this.gt(entry.priority, siftEntry.priority)) { + this.siftDown(siftEntry); + } else { + this.siftUp(siftEntry); + } } } From 07d01e7b8566a3cf86ab991329865e64a697d07f Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Fri, 27 Mar 2026 16:51:46 -0700 Subject: [PATCH 14/17] use number-style comparison fns --- src/data_manager/PriorityQueue.ts | 12 +++++------- src/test/PriorityQueue.test.ts | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/data_manager/PriorityQueue.ts b/src/data_manager/PriorityQueue.ts index 9278bfcd3..537eaff82 100644 --- a/src/data_manager/PriorityQueue.ts +++ b/src/data_manager/PriorityQueue.ts @@ -4,8 +4,6 @@ type Entry = { heapIndex: number; }; -type ComparisonFn

= (i: P, j: P) => boolean; - const entryToTuple = (entry?: Entry): [P, V] | undefined => entry && [entry.priority, entry.value]; const parentIndexOf = (i: number) => Math.ceil(i / 2) - 1; @@ -15,12 +13,12 @@ export class PriorityQueue { private keys: Map> = new Map(); private heap: Entry[] = []; - private gt: ComparisonFn

; - private lt: ComparisonFn

; + private gt: (a: P, b: P) => boolean; + private lt: (a: P, b: P) => boolean; - constructor(greaterThan: ComparisonFn

) { - this.gt = greaterThan; - this.lt = (i, j) => greaterThan(j, i); + constructor(compare: (a: P, b: P) => number) { + this.gt = (a, b) => compare(a, b) < 0; + this.lt = (a, b) => compare(a, b) > 0; } get length() { diff --git a/src/test/PriorityQueue.test.ts b/src/test/PriorityQueue.test.ts index 4701e8f4e..4dd8d1b64 100644 --- a/src/test/PriorityQueue.test.ts +++ b/src/test/PriorityQueue.test.ts @@ -3,7 +3,7 @@ import { PriorityQueue } from "../data_manager/PriorityQueue.js"; const ENTRY_COUNT = 100; const setup = (): [PriorityQueue, Map, number[]] => { - const queue = new PriorityQueue((i, j) => i > j); + const queue = new PriorityQueue((i, j) => j - i); const entryMap = new Map(); const priorities: number[] = []; From cbf0fc0b96cfd36931d26d874c838124e4e633be Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Fri, 27 Mar 2026 16:58:21 -0700 Subject: [PATCH 15/17] get even cleverer in queue --- src/data_manager/PriorityQueue.ts | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/data_manager/PriorityQueue.ts b/src/data_manager/PriorityQueue.ts index 537eaff82..e512dc702 100644 --- a/src/data_manager/PriorityQueue.ts +++ b/src/data_manager/PriorityQueue.ts @@ -13,21 +13,15 @@ export class PriorityQueue { private keys: Map> = new Map(); private heap: Entry[] = []; - private gt: (a: P, b: P) => boolean; - private lt: (a: P, b: P) => boolean; - - constructor(compare: (a: P, b: P) => number) { - this.gt = (a, b) => compare(a, b) < 0; - this.lt = (a, b) => compare(a, b) > 0; - } + constructor(private compare: (a: P, b: P) => number) {} get length() { return this.heap.length; } /** Useful common primitive for `siftUp` and `siftDown`: compares two entries and swaps them if out of order. */ - private trySwap(entry: Entry, swapEntry?: Entry, shouldSwap = this.gt): boolean { - if (swapEntry !== undefined && shouldSwap(entry.priority, swapEntry.priority)) { + private trySwap(entry: Entry, swapEntry?: Entry, mul = 1): boolean { + if (swapEntry !== undefined && this.compare(entry.priority, swapEntry.priority) * mul < 0) { this.heap[entry.heapIndex] = swapEntry; const newHeapIndex = swapEntry.heapIndex; swapEntry.heapIndex = entry.heapIndex; @@ -55,11 +49,11 @@ export class PriorityQueue { do { const childHeapIndex = childIndexOf(entry.heapIndex); - const cEntry1 = this.heap[childHeapIndex]; - const cEntry2 = this.heap[childHeapIndex + 1]; - const use2 = cEntry1 !== undefined && cEntry2 !== undefined && this.gt(cEntry2.priority, cEntry1.priority); - childEntry = use2 ? cEntry2 : cEntry1; - } while (this.trySwap(entry, childEntry, this.lt)); + const cEnt1 = this.heap[childHeapIndex]; + const cEnt2 = this.heap[childHeapIndex + 1]; + const use2 = cEnt1 !== undefined && cEnt2 !== undefined && this.compare(cEnt2.priority, cEnt1.priority) < 0; + childEntry = use2 ? cEnt2 : cEnt1; + } while (this.trySwap(entry, childEntry, -1)); this.heap[entry.heapIndex] = entry; } @@ -75,7 +69,7 @@ export class PriorityQueue { if (siftEntry !== undefined && siftEntry !== entry) { this.heap[entry.heapIndex] = siftEntry; siftEntry.heapIndex = entry.heapIndex; - if (this.gt(entry.priority, siftEntry.priority)) { + if (this.compare(entry.priority, siftEntry.priority) < 0) { this.siftDown(siftEntry); } else { this.siftUp(siftEntry); @@ -90,7 +84,7 @@ export class PriorityQueue { return false; } - const priorityIncreased = this.gt(newPriority, entry.priority); + const priorityIncreased = this.compare(newPriority, entry.priority) < 0; entry.priority = newPriority; if (priorityIncreased) { this.siftUp(entry); From f6c0712623f0e6fabf7a478ae0b3a1d514d28d7f Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 6 Oct 2026 15:44:27 -0700 Subject: [PATCH 16/17] rename to underscores --- src/data_manager/{PriorityQueue.ts => priority_queue.ts} | 0 src/test/PriorityQueue.test.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/data_manager/{PriorityQueue.ts => priority_queue.ts} (100%) diff --git a/src/data_manager/PriorityQueue.ts b/src/data_manager/priority_queue.ts similarity index 100% rename from src/data_manager/PriorityQueue.ts rename to src/data_manager/priority_queue.ts diff --git a/src/test/PriorityQueue.test.ts b/src/test/PriorityQueue.test.ts index 4dd8d1b64..cafa0ce11 100644 --- a/src/test/PriorityQueue.test.ts +++ b/src/test/PriorityQueue.test.ts @@ -1,4 +1,4 @@ -import { PriorityQueue } from "../data_manager/PriorityQueue.js"; +import { PriorityQueue } from "../data_manager/priority_queue.js"; const ENTRY_COUNT = 100; From 6da01bf1cc98ca5fab23acc589efb28cce39c16d Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 6 Oct 2026 15:58:32 -0700 Subject: [PATCH 17/17] rename test module with underscores --- src/test/{PriorityQueue.test.ts => priority_queue.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/test/{PriorityQueue.test.ts => priority_queue.test.ts} (100%) diff --git a/src/test/PriorityQueue.test.ts b/src/test/priority_queue.test.ts similarity index 100% rename from src/test/PriorityQueue.test.ts rename to src/test/priority_queue.test.ts