@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.
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/index.js +245 -0
- 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
|
+
}
|