@mark1russell7/collections 0.7.1 → 0.8.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 +6 -0
- package/build/index.d.ts +11 -6
- package/build/index.d.ts.map +1 -1
- package/build/index.js +80 -51
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/behaviors/bounded.ts +30 -3
- package/src/behaviors/lru.ts +4 -2
- package/src/behaviors/ttl.ts +80 -84
- package/src/fx/iter.ts +6 -5
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@mark1russell7/collections",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.8.0",
|
|
5
5
|
"description": "Collections with no dependencies: lists, maps, queues, a summary tree, channels, lazy operations and dependency graphs.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"collections",
|
package/src/behaviors/bounded.ts
CHANGED
|
@@ -231,6 +231,19 @@ export function boundedList<T>(
|
|
|
231
231
|
};
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
/**
|
|
235
|
+
* This function adds each element with `add`, one after the other, until `add` refuses one.
|
|
236
|
+
* It gives true when one element goes in. A throw of `add` stops it, and the earlier elements stay.
|
|
237
|
+
*/
|
|
238
|
+
function addEach<T>(elements: Iterable<T>, add: (element: T) => boolean): boolean {
|
|
239
|
+
let added = false;
|
|
240
|
+
for (const element of Array.from(elements)) {
|
|
241
|
+
if (!add(element)) break;
|
|
242
|
+
added = true;
|
|
243
|
+
}
|
|
244
|
+
return added;
|
|
245
|
+
}
|
|
246
|
+
|
|
234
247
|
/**
|
|
235
248
|
* This function makes a bounded Queue middleware.
|
|
236
249
|
*/
|
|
@@ -294,6 +307,12 @@ export function boundedQueue<T>(
|
|
|
294
307
|
}
|
|
295
308
|
return (value as Function).call(target, element);
|
|
296
309
|
};
|
|
310
|
+
|
|
311
|
+
// The base addAll goes past the bound, so each element goes through add of this proxy (C10)
|
|
312
|
+
case "addAll":
|
|
313
|
+
return function (elements: Iterable<T>): boolean {
|
|
314
|
+
return addEach(elements, (element) => (receiver as Queue<T>).add(element));
|
|
315
|
+
};
|
|
297
316
|
}
|
|
298
317
|
}
|
|
299
318
|
|
|
@@ -313,7 +332,8 @@ export function boundedDeque<T>(
|
|
|
313
332
|
const capacity = checkCapacity(options.capacity, "boundedDeque");
|
|
314
333
|
|
|
315
334
|
return (next: Deque<T>): Deque<T> & BoundedCollection => {
|
|
316
|
-
|
|
335
|
+
// An insert at the front drops the last element, as unshift of boundedList does (C10)
|
|
336
|
+
const handleOverflow = (element: T, dropFrom: "first" | "last"): void => {
|
|
317
337
|
if (onOverflow) {
|
|
318
338
|
const context: OverflowContext<T> = {
|
|
319
339
|
newElement: element,
|
|
@@ -329,7 +349,8 @@ export function boundedDeque<T>(
|
|
|
329
349
|
throw new Error(`Deque is full (capacity: ${capacity})`);
|
|
330
350
|
case "drop-oldest":
|
|
331
351
|
if (!next.isEmpty) {
|
|
332
|
-
next.
|
|
352
|
+
if (dropFrom === "last") next.removeLast();
|
|
353
|
+
else next.removeFirst();
|
|
333
354
|
}
|
|
334
355
|
break;
|
|
335
356
|
case "drop-newest":
|
|
@@ -363,13 +384,19 @@ export function boundedDeque<T>(
|
|
|
363
384
|
case "add":
|
|
364
385
|
return function (element: T): any {
|
|
365
386
|
if (target.size >= capacity && policy !== "grow") {
|
|
366
|
-
handleOverflow(element);
|
|
387
|
+
handleOverflow(element, prop === "addFirst" || prop === "offerFirst" ? "last" : "first");
|
|
367
388
|
if (policy === "reject" || policy === "drop-newest") {
|
|
368
389
|
return prop.startsWith("offer") || prop === "add" ? false : undefined;
|
|
369
390
|
}
|
|
370
391
|
}
|
|
371
392
|
return (value as Function).call(target, element);
|
|
372
393
|
};
|
|
394
|
+
|
|
395
|
+
// The base addAll goes past the bound, so each element goes through add of this proxy (C10)
|
|
396
|
+
case "addAll":
|
|
397
|
+
return function (elements: Iterable<T>): boolean {
|
|
398
|
+
return addEach(elements, (element) => (receiver as Deque<T>).add(element));
|
|
399
|
+
};
|
|
373
400
|
}
|
|
374
401
|
}
|
|
375
402
|
|
package/src/behaviors/lru.ts
CHANGED
|
@@ -240,8 +240,10 @@ export function lruMap<K, V>(
|
|
|
240
240
|
if (stale) {
|
|
241
241
|
removeNode(stale);
|
|
242
242
|
}
|
|
243
|
-
// Keep the node list bounded
|
|
244
|
-
|
|
243
|
+
// Keep the node list bounded. At most `capacity` nodes are live, so at twice the capacity a scan
|
|
244
|
+
// removes at least `capacity` stale nodes: the cost of a set is amortized O(1). A scan at the
|
|
245
|
+
// capacity read each node at each set into a full map (C10). evictLRU skips the other stale nodes.
|
|
246
|
+
if (nodeMap.size >= 2 * capacity) {
|
|
245
247
|
dropStaleNodes();
|
|
246
248
|
}
|
|
247
249
|
if (target.size >= capacity) {
|
package/src/behaviors/ttl.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import type { Middleware } from "../core/middleware.js";
|
|
17
|
-
import type { MapLike } from "../interfaces/map.js";
|
|
17
|
+
import type { Entry, MapLike } from "../interfaces/map.js";
|
|
18
18
|
import { compositeMapMethod } from "./composite.js";
|
|
19
19
|
import type { Collection } from "../interfaces/collection.js";
|
|
20
20
|
|
|
@@ -62,8 +62,13 @@ interface TTLEntry<V> {
|
|
|
62
62
|
/**
|
|
63
63
|
* This function makes a TTL Map middleware.
|
|
64
64
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
65
|
+
* An entry is live while the time is less than the time of its last `set` plus `ttl`.
|
|
66
|
+
* Each access to the map first removes the expired entries, so each read shows only live entries.
|
|
67
|
+
* The layer gives each expired entry to `onExpire` one time, in the order of expiry.
|
|
68
|
+
* A background interval also removes expired entries, so `onExpire` occurs with no access.
|
|
69
|
+
*
|
|
70
|
+
* The layer keeps the times in the order of expiry, so the removal before an access is amortized O(1).
|
|
71
|
+
* This order needs a clock that does not go back. If `Date.now()` goes back, an entry can stay until the entries before it expire.
|
|
67
72
|
*/
|
|
68
73
|
export function ttlMap<K, V>(
|
|
69
74
|
options: TTLOptions
|
|
@@ -73,39 +78,22 @@ export function ttlMap<K, V>(
|
|
|
73
78
|
return (
|
|
74
79
|
next: MapLike<K, V>
|
|
75
80
|
): MapLike<K, V> & { readonly dispose: () => void } => {
|
|
81
|
+
// The times of the live keys, in the order of expiry: a set deletes its key before it adds it again
|
|
76
82
|
const metadata = new Map<K, TTLEntry<V>>();
|
|
77
83
|
let intervalId: ReturnType<typeof setInterval> | null = null;
|
|
78
84
|
|
|
79
|
-
|
|
85
|
+
// This function removes the expired entries from the front of the order, and stops at the first live entry
|
|
86
|
+
const sweep = (): void => {
|
|
80
87
|
const now = Date.now();
|
|
81
|
-
const expired: K[] = [];
|
|
82
|
-
|
|
83
88
|
for (const [key, entry] of metadata) {
|
|
84
|
-
if (now
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
for (const key of expired) {
|
|
90
|
-
const entry = metadata.get(key);
|
|
91
|
-
if (entry) {
|
|
92
|
-
next.delete(key);
|
|
93
|
-
metadata.delete(key);
|
|
94
|
-
|
|
95
|
-
if (onExpire) {
|
|
96
|
-
onExpire({
|
|
97
|
-
key,
|
|
98
|
-
value: entry.value,
|
|
99
|
-
createdAt: entry.createdAt,
|
|
100
|
-
expiredAt: now,
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
}
|
|
89
|
+
if (now < entry.expiresAt) return;
|
|
90
|
+
metadata.delete(key);
|
|
91
|
+
next.delete(key);
|
|
92
|
+
onExpire?.({ key, value: entry.value, createdAt: entry.createdAt, expiredAt: now });
|
|
104
93
|
}
|
|
105
94
|
};
|
|
106
95
|
|
|
107
|
-
|
|
108
|
-
intervalId = startExpirationTimer(checkExpired, checkInterval);
|
|
96
|
+
intervalId = startExpirationTimer(sweep, checkInterval);
|
|
109
97
|
|
|
110
98
|
const dispose = (): void => {
|
|
111
99
|
if (intervalId) {
|
|
@@ -116,7 +104,6 @@ export function ttlMap<K, V>(
|
|
|
116
104
|
|
|
117
105
|
return new Proxy(next, {
|
|
118
106
|
get(target, prop, receiver) {
|
|
119
|
-
// Add dispose method
|
|
120
107
|
if (prop === "dispose") {
|
|
121
108
|
return dispose;
|
|
122
109
|
}
|
|
@@ -128,64 +115,73 @@ export function ttlMap<K, V>(
|
|
|
128
115
|
return composite;
|
|
129
116
|
}
|
|
130
117
|
|
|
118
|
+
// A getter (size, isEmpty) reads only live entries
|
|
119
|
+
sweep();
|
|
131
120
|
const value = Reflect.get(target, prop, receiver);
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
switch (prop) {
|
|
135
|
-
case "get":
|
|
136
|
-
return function (key: K): V {
|
|
137
|
-
// Check if expired before returning
|
|
138
|
-
const entry = metadata.get(key);
|
|
139
|
-
if (entry && Date.now() >= entry.expiresAt) {
|
|
140
|
-
target.delete(key);
|
|
141
|
-
metadata.delete(key);
|
|
142
|
-
throw new Error(`Key expired: ${key}`);
|
|
143
|
-
}
|
|
144
|
-
return (value as Function).call(target, key);
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
case "has":
|
|
148
|
-
return function (key: K): boolean {
|
|
149
|
-
// Check if expired
|
|
150
|
-
const entry = metadata.get(key);
|
|
151
|
-
if (entry && Date.now() >= entry.expiresAt) {
|
|
152
|
-
target.delete(key);
|
|
153
|
-
metadata.delete(key);
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
return (value as Function).call(target, key);
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
case "set":
|
|
160
|
-
return function (key: K, val: V): V | undefined {
|
|
161
|
-
const now = Date.now();
|
|
162
|
-
const result = (value as Function).call(target, key, val);
|
|
163
|
-
|
|
164
|
-
metadata.set(key, {
|
|
165
|
-
value: val,
|
|
166
|
-
expiresAt: now + ttl,
|
|
167
|
-
createdAt: now,
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
return result;
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
case "delete":
|
|
174
|
-
return function (key: K): V | undefined {
|
|
175
|
-
const result = (value as Function).call(target, key);
|
|
176
|
-
metadata.delete(key);
|
|
177
|
-
return result;
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
case "clear":
|
|
181
|
-
return function (): void {
|
|
182
|
-
(value as Function).call(target);
|
|
183
|
-
metadata.clear();
|
|
184
|
-
};
|
|
185
|
-
}
|
|
121
|
+
if (typeof value !== "function") {
|
|
122
|
+
return value;
|
|
186
123
|
}
|
|
187
124
|
|
|
188
|
-
|
|
125
|
+
switch (prop) {
|
|
126
|
+
case "set":
|
|
127
|
+
return function (key: K, val: V): V | undefined {
|
|
128
|
+
sweep();
|
|
129
|
+
const result = (value as Function).call(target, key, val);
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
metadata.delete(key);
|
|
132
|
+
metadata.set(key, { value: val, expiresAt: now + ttl, createdAt: now });
|
|
133
|
+
return result;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
case "delete":
|
|
137
|
+
return function (key: K): V | undefined {
|
|
138
|
+
sweep();
|
|
139
|
+
metadata.delete(key);
|
|
140
|
+
return (value as Function).call(target, key);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
case "clear":
|
|
144
|
+
return function (): void {
|
|
145
|
+
metadata.clear();
|
|
146
|
+
(value as Function).call(target);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// The iteration gives the entries that are live when it starts. A removal while it runs (by a read in
|
|
150
|
+
// the loop, or by the interval between the steps of an async loop) does not change it, so the base
|
|
151
|
+
// iterator never sees a change and never throws.
|
|
152
|
+
case Symbol.iterator:
|
|
153
|
+
case "entries":
|
|
154
|
+
return function (): IterableIterator<Entry<K, V>> {
|
|
155
|
+
sweep();
|
|
156
|
+
return Array.from(target as Iterable<Entry<K, V>>).values();
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
case "keys":
|
|
160
|
+
return function (): IterableIterator<K> {
|
|
161
|
+
sweep();
|
|
162
|
+
return Array.from(target as Iterable<Entry<K, V>>, (e) => e.key).values();
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
case "values":
|
|
166
|
+
return function (): IterableIterator<V> {
|
|
167
|
+
sweep();
|
|
168
|
+
return Array.from(target as Iterable<Entry<K, V>>, (e) => e.value).values();
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
case "forEach":
|
|
172
|
+
return function (callback: (value: V, key: K, map: MapLike<K, V>) => void): void {
|
|
173
|
+
sweep();
|
|
174
|
+
for (const e of Array.from(target as Iterable<Entry<K, V>>)) callback(e.value, e.key, receiver as MapLike<K, V>);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
default:
|
|
178
|
+
// Each other method (get, has, getOrUndefined, the iteration) reads only live entries,
|
|
179
|
+
// also when a caller keeps the method and calls it later
|
|
180
|
+
return function (this: unknown, ...args: unknown[]): unknown {
|
|
181
|
+
sweep();
|
|
182
|
+
return (value as Function).apply(target, args);
|
|
183
|
+
};
|
|
184
|
+
}
|
|
189
185
|
},
|
|
190
186
|
}) as MapLike<K, V> & { readonly dispose: () => void };
|
|
191
187
|
};
|
package/src/fx/iter.ts
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* @example
|
|
8
8
|
* const result = pipe(
|
|
9
9
|
* [1, 2, 3, 4, 5],
|
|
10
|
-
* map(x => x * 2),
|
|
11
|
-
* filter(x => x > 5),
|
|
12
|
-
* take(2),
|
|
10
|
+
* (it) => map(it, x => x * 2),
|
|
11
|
+
* (it) => filter(it, x => x > 5),
|
|
12
|
+
* (it) => take(it, 2),
|
|
13
13
|
* toArray
|
|
14
14
|
* ) // [6, 8]
|
|
15
15
|
*/
|
|
@@ -76,13 +76,14 @@ export function* flatten<T>(iterable: Iterable<Iterable<T>>): Generator<T> {
|
|
|
76
76
|
// ============================================================================
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
|
-
* This function takes the first n elements.
|
|
79
|
+
* This function takes the first n elements. It reads no element after the n-th one, and it reads nothing when n is 0.
|
|
80
80
|
*/
|
|
81
81
|
export function* take<T>(iterable: Iterable<T>, n: number): Generator<T> {
|
|
82
|
+
if (n <= 0) return;
|
|
82
83
|
let count = 0;
|
|
83
84
|
for (const value of iterable) {
|
|
84
|
-
if (count++ >= n) break;
|
|
85
85
|
yield value;
|
|
86
|
+
if (++count >= n) return;
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
|