@llblab/uniqueue 1.0.0 → 1.1.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/README.md CHANGED
@@ -10,7 +10,8 @@ Combines a binary heap with a key-to-index `Map`, enabling O(log n) inserts/upda
10
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
11
  - **O(1) Lookup**: Tracks item positions internally, avoiding O(n) scans for updates.
12
12
  - **Max Size Eviction**: Automatically removes the lowest-priority item when the limit is reached.
13
- - **Zero Dependencies**: Pure ES6 class, ~1KB minified.
13
+ - **Iterable**: Supports `for...of` iteration over items.
14
+ - **Zero Dependencies**: ~1KB minified.
14
15
 
15
16
  ## Installation
16
17
 
@@ -23,7 +24,7 @@ npm install @llblab/uniqueue
23
24
  ### Basic Example (Max Heap)
24
25
 
25
26
  ```javascript
26
- import { UniQueue } from "uniqueue";
27
+ import { UniQueue } from "@llblab/uniqueue";
27
28
 
28
29
  // Create a max-heap for leaderboard scores
29
30
  const leaderboard = new UniQueue({
@@ -50,6 +51,11 @@ leaderboard.push({ playerId: "dave", score: 200 });
50
51
 
51
52
  console.log(leaderboard.data);
52
53
  // Contains dave (200), alice (150), charlie (120)
54
+
55
+ // Iterate over all items
56
+ for (const player of leaderboard) {
57
+ console.log(player.playerId, player.score);
58
+ }
53
59
  ```
54
60
 
55
61
  ## API
@@ -60,21 +66,21 @@ Creates a new priority queue instance.
60
66
 
61
67
  #### Options
62
68
 
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
+ | Option | Type | Default | Description |
70
+ | :----------- | :----------------- | :--------------------------------------- | :--------------------------------------------------------------------- |
71
+ | `data` | `T[]` | `[]` | Initial data array. |
72
+ | `maxSize` | `number` | `Infinity` | Maximum number of items. If exceeded, lowest priority item is evicted. |
73
+ | `compare` | `(a, b) => number` | `(a, b) => (a < b ? -1 : a > b ? 1 : 0)` | Comparison function for heap ordering. |
74
+ | `extractKey` | `(item) => string` | `(item) => item` | Function to extract unique key string from item. |
69
75
 
70
76
  ### Instance Methods
71
77
 
72
- #### `push(item: T): void`
78
+ #### `push(item: T): T | undefined`
73
79
 
74
80
  Adds an item to the queue or updates an existing item with the same key.
75
81
 
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).
82
+ - If the key is new: Adds item. If size > maxSize, removes and returns the evicted item.
83
+ - If the key exists: Updates the existing item and rebalances. Returns `undefined`.
78
84
 
79
85
  #### `pop(): T | undefined`
80
86
 
@@ -86,11 +92,11 @@ Returns the highest priority item without removing it.
86
92
 
87
93
  #### `remove(key: string): boolean`
88
94
 
89
- Removes the item with the given key from the queue. Returns `true` if an item was removed, `false` otherwise.
95
+ Removes the item with the given key. Returns `true` if removed, `false` otherwise.
90
96
 
91
97
  #### `has(key: string): boolean`
92
98
 
93
- Checks if an item with the given key exists in the queue.
99
+ Checks if an item with the given key exists.
94
100
 
95
101
  #### `get(key: string): T | undefined`
96
102
 
@@ -102,7 +108,11 @@ Removes all items from the queue.
102
108
 
103
109
  #### `size: number`
104
110
 
105
- Getter property that returns the number of items in the queue.
111
+ Getter property that returns the number of items.
112
+
113
+ #### `[Symbol.iterator](): IterableIterator<T>`
114
+
115
+ Iterates over items in heap order (not sorted).
106
116
 
107
117
  ## Complexity
108
118
 
@@ -0,0 +1,22 @@
1
+ export interface UniQueueOptions<T> {
2
+ data?: T[];
3
+ maxSize?: number;
4
+ compare?: (a: T, b: T) => number;
5
+ extractKey?: (item: T) => string;
6
+ }
7
+ export declare class UniQueue<T> {
8
+ #private;
9
+ data: T[];
10
+ indexes: Map<string, number>;
11
+ constructor({ data, maxSize, compare, extractKey, }?: UniQueueOptions<T>);
12
+ push(item: T): T | undefined;
13
+ pop(): T | undefined;
14
+ peek(): T | undefined;
15
+ remove(key: string): boolean;
16
+ has(key: string): boolean;
17
+ get(key: string): T | undefined;
18
+ clear(): void;
19
+ get size(): number;
20
+ [Symbol.iterator](): IterableIterator<T>;
21
+ }
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;IACX,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,MAAM,CAAC;IACjC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC;CAClC;AAED,qBAAa,QAAQ,CAAC,CAAC;;IACrB,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;gBAMjB,EACV,IAAS,EACT,OAAkB,EAClB,OAAgD,EAChD,UAAgD,GACjD,GAAE,eAAe,CAAC,CAAC,CAAM;IAkE1B,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,GAAG,SAAS;IAsB5B,GAAG,IAAI,CAAC,GAAG,SAAS;IAgBpB,IAAI,IAAI,CAAC,GAAG,SAAS;IAIrB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IA0B5B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAIzB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAK/B,KAAK,IAAI,IAAI;IAKb,IAAI,IAAI,IAAI,MAAM,CAEjB;IAEA,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,gBAAgB,CAAC,CAAC,CAAC;CAG1C"}
package/dist/index.js ADDED
@@ -0,0 +1,147 @@
1
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
2
+ if (kind === "m") throw new TypeError("Private method is not writable");
3
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
4
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
5
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
6
+ };
7
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
+ };
12
+ var _UniQueue_instances, _UniQueue_maxSize, _UniQueue_compare, _UniQueue_extractKey, _UniQueue_siftUp, _UniQueue_siftDown;
13
+ export class UniQueue {
14
+ constructor({ data = [], maxSize = Infinity, compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0), extractKey = (item) => item, } = {}) {
15
+ _UniQueue_instances.add(this);
16
+ _UniQueue_maxSize.set(this, void 0);
17
+ _UniQueue_compare.set(this, void 0);
18
+ _UniQueue_extractKey.set(this, void 0);
19
+ this.data = data;
20
+ this.indexes = new Map(data.map((item, index) => [extractKey(item), index]));
21
+ __classPrivateFieldSet(this, _UniQueue_maxSize, maxSize, "f");
22
+ __classPrivateFieldSet(this, _UniQueue_compare, compare, "f");
23
+ __classPrivateFieldSet(this, _UniQueue_extractKey, extractKey, "f");
24
+ if (data.length > 0) {
25
+ for (let i = (data.length >>> 1) - 1; i >= 0; i--) {
26
+ __classPrivateFieldGet(this, _UniQueue_instances, "m", _UniQueue_siftDown).call(this, i);
27
+ }
28
+ }
29
+ }
30
+ push(item) {
31
+ const key = __classPrivateFieldGet(this, _UniQueue_extractKey, "f").call(this, item);
32
+ const index = this.indexes.get(key);
33
+ if (index === undefined) {
34
+ this.data.push(item);
35
+ __classPrivateFieldGet(this, _UniQueue_instances, "m", _UniQueue_siftUp).call(this, this.data.length - 1);
36
+ if (this.data.length <= __classPrivateFieldGet(this, _UniQueue_maxSize, "f"))
37
+ return;
38
+ return this.pop();
39
+ }
40
+ const oldItem = this.data[index];
41
+ this.data[index] = item;
42
+ const cmp = __classPrivateFieldGet(this, _UniQueue_compare, "f").call(this, oldItem, item);
43
+ if (cmp < 0) {
44
+ __classPrivateFieldGet(this, _UniQueue_instances, "m", _UniQueue_siftDown).call(this, index);
45
+ }
46
+ else if (cmp > 0) {
47
+ __classPrivateFieldGet(this, _UniQueue_instances, "m", _UniQueue_siftUp).call(this, index);
48
+ }
49
+ }
50
+ pop() {
51
+ if (this.data.length === 0)
52
+ return;
53
+ const top = this.data[0];
54
+ this.indexes.delete(__classPrivateFieldGet(this, _UniQueue_extractKey, "f").call(this, top));
55
+ const bottom = this.data.pop();
56
+ if (this.data.length > 0 && bottom !== undefined) {
57
+ this.indexes.set(__classPrivateFieldGet(this, _UniQueue_extractKey, "f").call(this, bottom), 0);
58
+ this.data[0] = bottom;
59
+ __classPrivateFieldGet(this, _UniQueue_instances, "m", _UniQueue_siftDown).call(this, 0);
60
+ }
61
+ return top;
62
+ }
63
+ peek() {
64
+ return this.data[0];
65
+ }
66
+ remove(key) {
67
+ const index = this.indexes.get(key);
68
+ if (index === undefined)
69
+ return false;
70
+ const lastIndex = this.data.length - 1;
71
+ if (index === lastIndex) {
72
+ this.indexes.delete(key);
73
+ this.data.pop();
74
+ return true;
75
+ }
76
+ const item = this.data.pop();
77
+ this.indexes.delete(key);
78
+ this.indexes.set(__classPrivateFieldGet(this, _UniQueue_extractKey, "f").call(this, item), index);
79
+ this.data[index] = item;
80
+ const parentIndex = (index - 1) >>> 1;
81
+ if (index > 0 && __classPrivateFieldGet(this, _UniQueue_compare, "f").call(this, item, this.data[parentIndex]) < 0) {
82
+ __classPrivateFieldGet(this, _UniQueue_instances, "m", _UniQueue_siftUp).call(this, index);
83
+ }
84
+ else {
85
+ __classPrivateFieldGet(this, _UniQueue_instances, "m", _UniQueue_siftDown).call(this, index);
86
+ }
87
+ return true;
88
+ }
89
+ has(key) {
90
+ return this.indexes.has(key);
91
+ }
92
+ get(key) {
93
+ const index = this.indexes.get(key);
94
+ return index !== undefined ? this.data[index] : undefined;
95
+ }
96
+ clear() {
97
+ this.data = [];
98
+ this.indexes.clear();
99
+ }
100
+ get size() {
101
+ return this.data.length;
102
+ }
103
+ *[(_UniQueue_maxSize = new WeakMap(), _UniQueue_compare = new WeakMap(), _UniQueue_extractKey = new WeakMap(), _UniQueue_instances = new WeakSet(), _UniQueue_siftUp = function _UniQueue_siftUp(pos) {
104
+ const { data, indexes } = this;
105
+ const compare = __classPrivateFieldGet(this, _UniQueue_compare, "f");
106
+ const extractKey = __classPrivateFieldGet(this, _UniQueue_extractKey, "f");
107
+ const item = data[pos];
108
+ while (pos > 0) {
109
+ const parentIndex = (pos - 1) >>> 1;
110
+ const parent = data[parentIndex];
111
+ if (compare(item, parent) >= 0)
112
+ break;
113
+ indexes.set(extractKey(parent), pos);
114
+ data[pos] = parent;
115
+ pos = parentIndex;
116
+ }
117
+ indexes.set(extractKey(item), pos);
118
+ data[pos] = item;
119
+ }, _UniQueue_siftDown = function _UniQueue_siftDown(pos) {
120
+ const { data, indexes } = this;
121
+ const compare = __classPrivateFieldGet(this, _UniQueue_compare, "f");
122
+ const extractKey = __classPrivateFieldGet(this, _UniQueue_extractKey, "f");
123
+ const item = data[pos];
124
+ const halfLength = data.length >>> 1;
125
+ while (pos < halfLength) {
126
+ let leftIndex = (pos << 1) + 1;
127
+ let best = data[leftIndex];
128
+ const rightIndex = leftIndex + 1;
129
+ if (rightIndex < data.length) {
130
+ const right = data[rightIndex];
131
+ if (compare(right, best) < 0) {
132
+ leftIndex = rightIndex;
133
+ best = right;
134
+ }
135
+ }
136
+ if (compare(best, item) >= 0)
137
+ break;
138
+ indexes.set(extractKey(best), pos);
139
+ data[pos] = best;
140
+ pos = leftIndex;
141
+ }
142
+ indexes.set(extractKey(item), pos);
143
+ data[pos] = item;
144
+ }, Symbol.iterator)]() {
145
+ yield* this.data;
146
+ }
147
+ }
package/package.json CHANGED
@@ -1,19 +1,25 @@
1
1
  {
2
2
  "name": "@llblab/uniqueue",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "High-performance priority queue with unique key constraint (O(1) lookup, O(log n) update)",
5
5
  "type": "module",
6
- "main": "index.js",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
7
8
  "exports": {
8
- ".": "./index.js"
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
9
13
  },
10
14
  "files": [
11
- "index.js",
15
+ "dist",
12
16
  "README.md"
13
17
  ],
14
18
  "repository": "github:llblab/uniqueue",
15
19
  "scripts": {
16
- "test": "node --test test.js"
20
+ "build": "tsc",
21
+ "prepack": "npm run build",
22
+ "test": "npm run build && node --test test.js"
17
23
  },
18
24
  "keywords": [
19
25
  "priority-queue",
@@ -27,5 +33,8 @@
27
33
  ],
28
34
  "author": "LLB <shlavik@gmail.com>",
29
35
  "license": "MIT",
30
- "sideEffects": false
36
+ "sideEffects": false,
37
+ "devDependencies": {
38
+ "typescript": "^5.9.3"
39
+ }
31
40
  }
package/index.js DELETED
@@ -1,245 +0,0 @@
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
- }