@@ -55,6 +55,20 @@ function checkPriority(priority: unknown): void {
5555 }
5656}
5757
58+ /**
59+ * Conditional rest-parameter tuple for the comparator options bag. When
60+ * the priority `P` is one of the primitive types that {@linkcode ascend}
61+ * orders correctly (`number`, `bigint`, `string`, plus their literal and
62+ * union subtypes), the options bag — and therefore `compare` itself —
63+ * is optional. For any other `P` (object, tuple, custom class), the
64+ * options bag is required and must provide `compare`; without it,
65+ * `ascend` would silently produce nonsense ordering (e.g., lexicographic
66+ * string comparison of `[number, number]` tuples).
67+ */
68+ type IndexedHeapCompareArgs < P > = [ P ] extends [ number | bigint | string ]
69+ ? [ options ?: { compare ?: ( a : P , b : P ) => number } ]
70+ : [ options : { compare : ( a : P , b : P ) => number } ] ;
71+
5872/**
5973 * A priority queue that supports looking up, removing, and re-prioritizing
6074 * entries by key. Each entry is a unique `(key, priority)` pair. The entry
@@ -102,7 +116,10 @@ function checkPriority(priority: unknown): void {
102116 * priority's contents (e.g., `heap.peek().priority[0] = 0`) corrupts
103117 * the heap invariant. Use
104118 * {@linkcode IndexedHeap.prototype.set | set} to change a key's
105- * priority.
119+ * priority. `set` itself uses reference equality (`===`) to short-circuit
120+ * when the priority is unchanged, so passing a structurally-equal but
121+ * distinct object/tuple priority replaces the reference and runs a sift
122+ * (no observable reorder, but not a no-op).
106123 *
107124 * @experimental **UNSTABLE**: New API, yet to be vetted.
108125 *
@@ -183,9 +200,12 @@ export class IndexedHeap<K, P = number> implements Iterable<HeapEntry<K, P>> {
183200 * @param entries An optional iterable of `[key, priority]` pairs. Each key
184201 * must be unique; duplicates throw a `TypeError`. Pass `null` or
185202 * `undefined` to create an empty heap with options.
186- * @param options Optional configuration. `compare` overrides the default
187- * ascending-numeric ordering; pass {@linkcode descend} for max-heap
188- * order, or a custom function for tuple/`bigint`/etc. priorities.
203+ * @param options Optional configuration when `P` is `number`, `bigint`,
204+ * or `string`; required when `P` is any other type (e.g. tuples or
205+ * objects) so the heap has a valid comparator. `compare` overrides the
206+ * default ascending-numeric ordering; pass {@linkcode descend} for
207+ * max-heap order, or a custom function for tuple/`bigint`/etc.
208+ * priorities.
189209 *
190210 * @example Empty heap
191211 * ```ts
@@ -227,6 +247,10 @@ export class IndexedHeap<K, P = number> implements Iterable<HeapEntry<K, P>> {
227247 * assertEquals(heap.peek(), { key: "b", priority: 5 });
228248 * ```
229249 */
250+ constructor (
251+ entries ?: Iterable < readonly [ K , P ] > | null ,
252+ ...options : IndexedHeapCompareArgs < P >
253+ ) ;
230254 constructor (
231255 entries ?: Iterable < readonly [ K , P ] > | null ,
232256 options ?: { compare ?: ( a : P , b : P ) => number } ,
@@ -240,7 +264,7 @@ export class IndexedHeap<K, P = number> implements Iterable<HeapEntry<K, P>> {
240264 this . #compare = compare ;
241265 if ( entries === undefined || entries === null ) return ;
242266 if (
243- typeof entries !== "object" && typeof entries !== "string" ||
267+ typeof entries !== "object" ||
244268 ! ( Symbol . iterator in Object ( entries ) )
245269 ) {
246270 throw new TypeError (
@@ -267,22 +291,14 @@ export class IndexedHeap<K, P = number> implements Iterable<HeapEntry<K, P>> {
267291 readonly [ Symbol . toStringTag ] = "IndexedHeap" as const ;
268292
269293 /**
270- * Create a new {@linkcode IndexedHeap} from an iterable of key-priority
271- * pairs, an array-like of key-priority pairs, or an existing
272- * {@linkcode IndexedHeap}. When copying from another `IndexedHeap`, the
273- * source's comparator is inherited unless `options.compare` overrides it.
294+ * Create a shallow copy of an existing {@linkcode IndexedHeap}. The
295+ * source's comparator is inherited unless `options.compare` overrides
296+ * it; when overridden, the copy is re-heapified in O(n).
274297 *
275- * @experimental **UNSTABLE**: New API, yet to be vetted.
298+ * Because the source already carries a valid comparator, `compare`
299+ * stays optional regardless of the priority type `P`.
276300 *
277- * @example Creating from an array of pairs
278- * ```ts
279- * import { IndexedHeap } from "@std/data-structures/unstable-indexed-heap";
280- * import { assertEquals } from "@std/assert";
281- *
282- * const heap = IndexedHeap.from([["a", 3], ["b", 1], ["c", 2]]);
283- * assertEquals(heap.peek(), { key: "b", priority: 1 });
284- * assertEquals(heap.size, 3);
285- * ```
301+ * @experimental **UNSTABLE**: New API, yet to be vetted.
286302 *
287303 * @example Creating from another IndexedHeap (shallow copy)
288304 * ```ts
@@ -299,6 +315,56 @@ export class IndexedHeap<K, P = number> implements Iterable<HeapEntry<K, P>> {
299315 * assertEquals(original.size, 2);
300316 * ```
301317 *
318+ * @example Re-ordering an existing heap with a different comparator
319+ * ```ts
320+ * import { IndexedHeap } from "@std/data-structures/unstable-indexed-heap";
321+ * import { descend } from "@std/data-structures";
322+ * import { assertEquals } from "@std/assert";
323+ *
324+ * const minHeap = new IndexedHeap<string>([["a", 1], ["b", 5], ["c", 3]]);
325+ * const maxHeap = IndexedHeap.from(minHeap, { compare: descend });
326+ *
327+ * assertEquals(minHeap.peek(), { key: "a", priority: 1 });
328+ * assertEquals(maxHeap.peek(), { key: "b", priority: 5 });
329+ * ```
330+ *
331+ * @typeParam K The type of the keys in the heap.
332+ * @typeParam P The type of the priority. Defaults to `number`.
333+ * @param collection The source {@linkcode IndexedHeap} to copy.
334+ * @param options Optional configuration. Omitting it inherits the
335+ * source's comparator; passing `compare` overrides it and triggers
336+ * a re-heapify.
337+ * @returns A new heap containing all entries from the source heap.
338+ */
339+ static from < K , P = number > (
340+ collection : IndexedHeap < K , P > ,
341+ options ?: { compare ?: ( a : P , b : P ) => number } ,
342+ ) : IndexedHeap < K , P > ;
343+ /**
344+ * Create a new {@linkcode IndexedHeap} from an iterable or array-like
345+ * of `[key, priority]` pairs.
346+ *
347+ * When the priority type `P` is `number`, `bigint`, or `string`, the
348+ * default {@linkcode ascend} comparator orders correctly and the
349+ * `options` bag is optional. For any other `P` (tuples, objects), the
350+ * `compare` option is required at the type level so the heap has a
351+ * valid comparator — otherwise `ascend` would silently mis-order.
352+ *
353+ * Use the {@linkcode IndexedHeap.from} overload that takes an existing
354+ * {@linkcode IndexedHeap} to copy from another heap instead.
355+ *
356+ * @experimental **UNSTABLE**: New API, yet to be vetted.
357+ *
358+ * @example Creating from an array of pairs
359+ * ```ts
360+ * import { IndexedHeap } from "@std/data-structures/unstable-indexed-heap";
361+ * import { assertEquals } from "@std/assert";
362+ *
363+ * const heap = IndexedHeap.from([["a", 3], ["b", 1], ["c", 2]]);
364+ * assertEquals(heap.peek(), { key: "b", priority: 1 });
365+ * assertEquals(heap.size, 3);
366+ * ```
367+ *
302368 * @example Creating from a Map (iterable of [key, value] pairs)
303369 * ```ts
304370 * import { IndexedHeap } from "@std/data-structures/unstable-indexed-heap";
@@ -309,28 +375,30 @@ export class IndexedHeap<K, P = number> implements Iterable<HeapEntry<K, P>> {
309375 * assertEquals(heap.peek(), { key: "task-b", priority: 1 });
310376 * ```
311377 *
312- * @example Re-ordering an existing heap with a different comparator
378+ * @example Tuple priority requires an explicit comparator
313379 * ```ts
314380 * import { IndexedHeap } from "@std/data-structures/unstable-indexed-heap";
315- * import { descend } from "@std/data-structures";
316381 * import { assertEquals } from "@std/assert";
317382 *
318- * const minHeap = new IndexedHeap<string>([["a", 1], ["b", 5], ["c", 3]]);
319- * const maxHeap = IndexedHeap.from(minHeap, { compare: descend });
320- *
321- * assertEquals(minHeap.peek(), { key: "a", priority: 1 } );
322- * assertEquals(maxHeap .peek(), { key: "b", priority: 5 });
383+ * const heap = IndexedHeap.from <string, [number, number]>(
384+ * [["a", [5, 0]], ["b", [3, 1]]],
385+ * { compare: (a, b) => a[0] - b[0] || a[1] - b[1] },
386+ * );
387+ * assertEquals(heap .peek(), { key: "b", priority: [3, 1] });
323388 * ```
324389 *
325390 * @typeParam K The type of the keys in the heap.
326391 * @typeParam P The type of the priority. Defaults to `number`.
327- * @param collection An iterable or array-like of `[key, priority]` pairs,
328- * or an existing {@linkcode IndexedHeap} to copy.
329- * @param options Optional configuration. `compare` overrides the
330- * ordering; when copying from another `IndexedHeap`, omitting this
331- * inherits the source's comparator.
392+ * @param collection An iterable or array-like of `[key, priority]` pairs.
393+ * @param options Optional configuration when `P` is `number`, `bigint`,
394+ * or `string`; required when `P` is any other type. `compare`
395+ * overrides the default ascending-numeric ordering.
332396 * @returns A new heap containing all entries from the collection.
333397 */
398+ static from < K , P = number > (
399+ collection : Iterable < readonly [ K , P ] > | ArrayLike < readonly [ K , P ] > ,
400+ ...options : IndexedHeapCompareArgs < P >
401+ ) : IndexedHeap < K , P > ;
334402 static from < K , P = number > (
335403 collection :
336404 | IndexedHeap < K , P >
@@ -340,7 +408,7 @@ export class IndexedHeap<K, P = number> implements Iterable<HeapEntry<K, P>> {
340408 ) : IndexedHeap < K , P > {
341409 if (
342410 collection === null || collection === undefined ||
343- typeof collection !== "object" && typeof collection !== "string" ||
411+ typeof collection !== "object" ||
344412 ! (
345413 Symbol . iterator in Object ( collection ) ||
346414 "length" in Object ( collection )
@@ -351,7 +419,7 @@ export class IndexedHeap<K, P = number> implements Iterable<HeapEntry<K, P>> {
351419 ) ;
352420 }
353421 if ( collection instanceof IndexedHeap ) {
354- const heap = new IndexedHeap < K , P > ( null , {
422+ const heap = IndexedHeap . #createInternal < K , P > ( {
355423 compare : options ?. compare ?? collection . #compare,
356424 } ) ;
357425 for ( const entry of collection . #data) {
@@ -368,7 +436,7 @@ export class IndexedHeap<K, P = number> implements Iterable<HeapEntry<K, P>> {
368436 }
369437 return heap ;
370438 }
371- const heap = new IndexedHeap < K , P > ( null , options ) ;
439+ const heap = IndexedHeap . #createInternal < K , P > ( options ) ;
372440 heap . #bulkLoad(
373441 Symbol . iterator in Object ( collection )
374442 ? collection as Iterable < readonly [ K , P ] >
@@ -377,6 +445,22 @@ export class IndexedHeap<K, P = number> implements Iterable<HeapEntry<K, P>> {
377445 return heap ;
378446 }
379447
448+ /**
449+ * Allocate a new {@linkcode IndexedHeap} from inside the class, bypassing
450+ * the public constructor's conditional rest-tuple constraint. Used by
451+ * {@linkcode IndexedHeap.from} where `P` is still generic and the
452+ * conditional `IndexedHeapCompareArgs<P>` cannot be resolved.
453+ */
454+ static #createInternal< K , P > (
455+ options ?: { compare ?: ( a : P , b : P ) => number } ,
456+ ) : IndexedHeap < K , P > {
457+ type RelaxedCtor = new < K2 , P2 > (
458+ entries ?: Iterable < readonly [ K2 , P2 ] > | null ,
459+ options ?: { compare ?: ( a : P2 , b : P2 ) => number } ,
460+ ) => IndexedHeap < K2 , P2 > ;
461+ return new ( IndexedHeap as RelaxedCtor ) < K , P > ( null , options ) ;
462+ }
463+
380464 /**
381465 * Append all `[key, priority]` pairs and heapify in linear time. Used by
382466 * the constructor and by {@linkcode IndexedHeap.from} for the
@@ -533,11 +617,9 @@ export class IndexedHeap<K, P = number> implements Iterable<HeapEntry<K, P>> {
533617 * Return the front entry (smallest priority) without removing it, or
534618 * `undefined` if the heap is empty.
535619 *
536- * The returned wrapper is a fresh object — replacing its `key` or
537- * `priority` property has no effect on the heap. Object-typed
538- * priorities (e.g., tuples) share their reference with the heap and
539- * must not be mutated; see the class-level note on priority
540- * mutability.
620+ * The returned entry is a fresh wrapper, but for object-typed priorities
621+ * the `priority` field shares its reference with the heap — see the
622+ * class-level note on treating object priorities as immutable.
541623 *
542624 * @experimental **UNSTABLE**: New API, yet to be vetted.
543625 *
@@ -668,6 +750,13 @@ export class IndexedHeap<K, P = number> implements Iterable<HeapEntry<K, P>> {
668750 * throws on existing keys) and is the natural operation for relaxation
669751 * steps in graph algorithms like Dijkstra's.
670752 *
753+ * When `key` already exists, the no-op fast path uses reference equality
754+ * (`===`) to skip work when the new priority is identical to the old.
755+ * For object/tuple priorities this means a structurally-equal but
756+ * distinct reference still replaces the stored reference and runs a
757+ * sift (no observable reorder, but not free). See the class-level note
758+ * on treating object priorities as immutable.
759+ *
671760 * @experimental **UNSTABLE**: New API, yet to be vetted.
672761 *
673762 * @example Inserting and updating
@@ -880,6 +969,10 @@ export class IndexedHeap<K, P = number> implements Iterable<HeapEntry<K, P>> {
880969 * Use {@linkcode IndexedHeap.prototype.drain | drain} to retrieve entries
881970 * in sorted (smallest-first) order.
882971 *
972+ * Each returned entry is a fresh wrapper, but for object-typed priorities
973+ * the `priority` field shares its reference with the heap — see the
974+ * class-level note on treating object priorities as immutable.
975+ *
883976 * @experimental **UNSTABLE**: New API, yet to be vetted.
884977 *
885978 * @example Usage
@@ -914,6 +1007,10 @@ export class IndexedHeap<K, P = number> implements Iterable<HeapEntry<K, P>> {
9141007 * Mutating the heap (`push`, `pop`, `set`, `delete`, `clear`) while
9151008 * iterating is not supported and may skip or repeat entries.
9161009 *
1010+ * Each yielded entry is a fresh wrapper, but for object-typed priorities
1011+ * the `priority` field shares its reference with the heap — see the
1012+ * class-level note on treating object priorities as immutable.
1013+ *
9171014 * @experimental **UNSTABLE**: New API, yet to be vetted.
9181015 *
9191016 * @example Usage
0 commit comments