@llblab/uniqueue 1.0.0

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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +120 -0
  3. package/index.js +245 -0
  4. package/package.json +31 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LLB
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # Uniqueue
2
+
3
+ A high-performance **priority queue with unique key constraint**.
4
+
5
+ Combines a binary heap with a key-to-index `Map`, enabling O(log n) inserts/updates and O(1) lookups. Designed for leaderboard deduplication, task scheduling with updates, and LRU-like eviction scenarios.
6
+
7
+ ## Features
8
+
9
+ - **Unique Constraint**: Ensures only one item per key exists in the queue.
10
+ - **In-Place Updates**: If an item with the same key is pushed, it updates the existing entry in-place (bubbling up or down as needed).
11
+ - **O(1) Lookup**: Tracks item positions internally, avoiding O(n) scans for updates.
12
+ - **Max Size Eviction**: Automatically removes the lowest-priority item when the limit is reached.
13
+ - **Zero Dependencies**: Pure ES6 class, ~1KB minified.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @llblab/uniqueue
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ### Basic Example (Max Heap)
24
+
25
+ ```javascript
26
+ import { UniQueue } from "uniqueue";
27
+
28
+ // Create a max-heap for leaderboard scores
29
+ const leaderboard = new UniQueue({
30
+ compare: (a, b) => b.score - a.score, // Sort by score descending
31
+ extractKey: (item) => item.playerId, // Unique by playerId
32
+ maxSize: 3, // Keep only top 3 scores
33
+ });
34
+
35
+ // Add items
36
+ leaderboard.push({ playerId: "alice", score: 100 });
37
+ leaderboard.push({ playerId: "bob", score: 80 });
38
+ leaderboard.push({ playerId: "charlie", score: 120 });
39
+
40
+ console.log(leaderboard.peek());
41
+ // Output: { playerId: "charlie", score: 120 }
42
+
43
+ // Update existing item (alice improves score)
44
+ leaderboard.push({ playerId: "alice", score: 150 });
45
+ // Now alice is top, bob is pushed down
46
+
47
+ // Add item that exceeds maxSize
48
+ leaderboard.push({ playerId: "dave", score: 200 });
49
+ // Dave enters, Bob (lowest score) is evicted
50
+
51
+ console.log(leaderboard.data);
52
+ // Contains dave (200), alice (150), charlie (120)
53
+ ```
54
+
55
+ ## API
56
+
57
+ ### `new UniQueue(options)`
58
+
59
+ Creates a new priority queue instance.
60
+
61
+ #### Options
62
+
63
+ | Option | Type | Default | Description |
64
+ | :----------- | :----------------- | :---------------- | :--------------------------------------------------------------------- |
65
+ | `data` | `Array<T>` | `[]` | Initial data array. |
66
+ | `maxSize` | `number` | `Infinity` | Maximum number of items. If exceeded, lowest priority item is evicted. |
67
+ | `compare` | `(a, b) => number` | `(a, b) => a - b` | Comparison function. Returns < 0 if a < b, > 0 if a > b. |
68
+ | `extractKey` | `(item) => string` | `(item) => item` | Function to extract unique key string from item. |
69
+
70
+ ### Instance Methods
71
+
72
+ #### `push(item: T): void`
73
+
74
+ Adds an item to the queue or updates an existing item with the same key.
75
+
76
+ - If the key is new: Adds item. If size > maxSize, removes and returns the lowest priority item.
77
+ - If the key exists: Updates the existing item with the new value and rebalances (bubbles up or down).
78
+
79
+ #### `pop(): T | undefined`
80
+
81
+ Removes and returns the highest priority item (the root of the heap).
82
+
83
+ #### `peek(): T | undefined`
84
+
85
+ Returns the highest priority item without removing it.
86
+
87
+ #### `remove(key: string): boolean`
88
+
89
+ Removes the item with the given key from the queue. Returns `true` if an item was removed, `false` otherwise.
90
+
91
+ #### `has(key: string): boolean`
92
+
93
+ Checks if an item with the given key exists in the queue.
94
+
95
+ #### `get(key: string): T | undefined`
96
+
97
+ Returns the item with the given key without removing it.
98
+
99
+ #### `clear(): void`
100
+
101
+ Removes all items from the queue.
102
+
103
+ #### `size: number`
104
+
105
+ Getter property that returns the number of items in the queue.
106
+
107
+ ## Complexity
108
+
109
+ | Operation | Time Complexity |
110
+ | :-------------- | :-------------- |
111
+ | `push` (insert) | O(log n) |
112
+ | `push` (update) | O(log n) |
113
+ | `pop` | O(log n) |
114
+ | `remove` | O(log n) |
115
+ | `peek` / `get` | O(1) |
116
+ | `has` | O(1) |
117
+
118
+ ## License
119
+
120
+ MIT
package/index.js ADDED
@@ -0,0 +1,245 @@
1
+ /**
2
+ * @template T
3
+ * @typedef {Object} UniQueueOptions
4
+ * @property {T[]} [data] - Initial data array
5
+ * @property {number} [maxSize] - Maximum queue size (default: Infinity)
6
+ * @property {(a: T, b: T) => number} [compare] - Comparison function for heap ordering
7
+ * @property {(item: T) => string} [extractKey] - Function to extract unique key from item
8
+ */
9
+
10
+ /**
11
+ * Priority queue with unique key constraint.
12
+ * Combines a min-heap with a key-to-index map for O(log n) push/pop with deduplication.
13
+ *
14
+ * @template T
15
+ */
16
+ export class UniQueue {
17
+ /** @type {T[]} */
18
+ data;
19
+
20
+ /** @type {Map<string, number>} */
21
+ indexes;
22
+
23
+ /** @type {number} */
24
+ #maxSize;
25
+
26
+ /** @type {(a: T, b: T) => number} */
27
+ #compare;
28
+
29
+ /** @type {(item: T) => string} */
30
+ #extractKey;
31
+
32
+ /**
33
+ * @param {UniQueueOptions<T>} [options]
34
+ */
35
+ constructor({
36
+ data = [],
37
+ maxSize = Infinity,
38
+ compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0),
39
+ extractKey = (item) =>
40
+ /** @type {string} */ (/** @type {unknown} */ (item)),
41
+ } = {}) {
42
+ this.data = data;
43
+ this.indexes = new Map(
44
+ data.map((item, index) => [extractKey(item), index]),
45
+ );
46
+ this.#maxSize = maxSize;
47
+ this.#compare = compare;
48
+ this.#extractKey = extractKey;
49
+
50
+ if (data.length > 0) {
51
+ for (let i = (data.length >>> 1) - 1; i >= 0; i--) {
52
+ this.#siftDown(i);
53
+ }
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Move item up the heap to maintain heap property.
59
+ * @param {number} pos
60
+ */
61
+ #siftUp(pos) {
62
+ const { data, indexes } = this;
63
+ const compare = this.#compare;
64
+ const extractKey = this.#extractKey;
65
+ const item = data[pos];
66
+
67
+ while (pos > 0) {
68
+ const parentIndex = (pos - 1) >>> 1;
69
+ const parent = data[parentIndex];
70
+ if (compare(item, parent) >= 0) break;
71
+ indexes.set(extractKey(parent), pos);
72
+ data[pos] = parent;
73
+ pos = parentIndex;
74
+ }
75
+
76
+ indexes.set(extractKey(item), pos);
77
+ data[pos] = item;
78
+ }
79
+
80
+ /**
81
+ * Move item down the heap to maintain heap property.
82
+ * @param {number} pos
83
+ */
84
+ #siftDown(pos) {
85
+ const { data, indexes } = this;
86
+ const compare = this.#compare;
87
+ const extractKey = this.#extractKey;
88
+ const item = data[pos];
89
+ const halfLength = data.length >>> 1;
90
+
91
+ while (pos < halfLength) {
92
+ let leftIndex = (pos << 1) + 1;
93
+ let best = data[leftIndex];
94
+ const rightIndex = leftIndex + 1;
95
+
96
+ if (rightIndex < data.length) {
97
+ const right = data[rightIndex];
98
+ if (compare(right, best) < 0) {
99
+ leftIndex = rightIndex;
100
+ best = right;
101
+ }
102
+ }
103
+
104
+ if (compare(best, item) >= 0) break;
105
+
106
+ indexes.set(extractKey(best), pos);
107
+ data[pos] = best;
108
+ pos = leftIndex;
109
+ }
110
+
111
+ indexes.set(extractKey(item), pos);
112
+ data[pos] = item;
113
+ }
114
+
115
+ /**
116
+ * Add or update an item in the queue.
117
+ * - If key exists: Updates item and rebalances (unconditional update).
118
+ * - If key new: Adds item. If size > maxSize, evicts and returns min item.
119
+ * @param {T} item
120
+ * @returns {T | undefined} Evicted item if queue was full
121
+ */
122
+ push(item) {
123
+ const key = this.#extractKey(item);
124
+ const index = this.indexes.get(key);
125
+
126
+ if (index === undefined) {
127
+ this.data.push(item);
128
+ this.#siftUp(this.data.length - 1);
129
+ if (this.data.length <= this.#maxSize) return;
130
+ return this.pop();
131
+ }
132
+
133
+ const oldItem = this.data[index];
134
+ this.data[index] = item;
135
+ const cmp = this.#compare(oldItem, item);
136
+
137
+ if (cmp < 0) {
138
+ this.#siftDown(index);
139
+ } else if (cmp > 0) {
140
+ this.#siftUp(index);
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Remove and return the top (minimum) item.
146
+ * @returns {T | undefined}
147
+ */
148
+ pop() {
149
+ if (this.data.length === 0) return;
150
+
151
+ const top = this.data[0];
152
+ this.indexes.delete(this.#extractKey(top));
153
+
154
+ const bottom = this.data.pop();
155
+ if (this.data.length > 0 && bottom !== undefined) {
156
+ this.indexes.set(this.#extractKey(bottom), 0);
157
+ this.data[0] = bottom;
158
+ this.#siftDown(0);
159
+ }
160
+
161
+ return top;
162
+ }
163
+
164
+ /**
165
+ * Return the top (minimum) item without removing it.
166
+ * @returns {T | undefined}
167
+ */
168
+ peek() {
169
+ return this.data[0];
170
+ }
171
+
172
+ /**
173
+ * Remove an item by key.
174
+ * @param {string} key
175
+ * @returns {boolean} true if item was removed
176
+ */
177
+ remove(key) {
178
+ const index = this.indexes.get(key);
179
+ if (index === undefined) return false;
180
+
181
+ const lastIndex = this.data.length - 1;
182
+ if (index === lastIndex) {
183
+ this.indexes.delete(key);
184
+ this.data.pop();
185
+ return true;
186
+ }
187
+
188
+ const item = /** @type {T} */ (this.data.pop());
189
+ this.indexes.delete(key);
190
+ this.indexes.set(this.#extractKey(item), index);
191
+ this.data[index] = item;
192
+
193
+ const parentIndex = (index - 1) >>> 1;
194
+ if (index > 0 && this.#compare(item, this.data[parentIndex]) < 0) {
195
+ this.#siftUp(index);
196
+ } else {
197
+ this.#siftDown(index);
198
+ }
199
+
200
+ return true;
201
+ }
202
+
203
+ /**
204
+ * Check if an item exists.
205
+ * @param {string} key
206
+ * @returns {boolean}
207
+ */
208
+ has(key) {
209
+ return this.indexes.has(key);
210
+ }
211
+
212
+ /**
213
+ * Get an item by key.
214
+ * @param {string} key
215
+ * @returns {T | undefined}
216
+ */
217
+ get(key) {
218
+ const index = this.indexes.get(key);
219
+ return index !== undefined ? this.data[index] : undefined;
220
+ }
221
+
222
+ /**
223
+ * Remove all items.
224
+ */
225
+ clear() {
226
+ this.data = [];
227
+ this.indexes.clear();
228
+ }
229
+
230
+ /**
231
+ * Get item count.
232
+ * @returns {number}
233
+ */
234
+ get size() {
235
+ return this.data.length;
236
+ }
237
+
238
+ /**
239
+ * Iterate over items (arbitrary heap order).
240
+ * @returns {IterableIterator<T>}
241
+ */
242
+ *[Symbol.iterator]() {
243
+ yield* this.data;
244
+ }
245
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@llblab/uniqueue",
3
+ "version": "1.0.0",
4
+ "description": "High-performance priority queue with unique key constraint (O(1) lookup, O(log n) update)",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "exports": {
8
+ ".": "./index.js"
9
+ },
10
+ "files": [
11
+ "index.js",
12
+ "README.md"
13
+ ],
14
+ "repository": "github:llblab/uniqueue",
15
+ "scripts": {
16
+ "test": "node --test test.js"
17
+ },
18
+ "keywords": [
19
+ "priority-queue",
20
+ "heap",
21
+ "unique",
22
+ "deduplication",
23
+ "leaderboard",
24
+ "cache",
25
+ "lru",
26
+ "performance"
27
+ ],
28
+ "author": "LLB <shlavik@gmail.com>",
29
+ "license": "MIT",
30
+ "sideEffects": false
31
+ }