@mark1russell7/collections 0.3.0 → 0.5.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 +1 -0
- package/build/core/layers.d.ts +2 -0
- package/build/core/layers.js +20 -0
- package/build/core/layers.js.map +1 -0
- package/build/index.d.ts +31 -20
- package/build/index.d.ts.map +1 -1
- package/build/index.js +138 -11
- package/build/index.js.map +1 -1
- package/build/layers.d.ts +59 -0
- package/build/layers.d.ts.map +1 -0
- package/package.json +8 -1
- package/src/behaviors/bounded.ts +10 -5
- package/src/behaviors/evented.ts +1 -1
- package/src/behaviors/lru.ts +43 -5
- package/src/behaviors/safe.ts +1 -1
- package/src/behaviors/ttl.ts +1 -1
- package/src/core/capacity.ts +21 -0
- package/src/core/layers.ts +86 -0
- package/src/core/middleware.ts +13 -95
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This module holds the one fold of a stack of layers. A layer wraps the next thing, so a stack
|
|
3
|
+
* of layers wraps a base. The next thing can be a collection, a function or any other value.
|
|
4
|
+
*
|
|
5
|
+
* This file has no imports. A package that needs only the fold loads this one file.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const parse = compose(
|
|
9
|
+
* (next: (text: string) => number) => (text: string) => next(text.trim()),
|
|
10
|
+
* (next: (text: string) => number) => (text: string) => next(text) * 2
|
|
11
|
+
* )(Number);
|
|
12
|
+
* parse(" 21 "); // 42
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* A layer takes the next thing and gives a wrapped thing.
|
|
16
|
+
*
|
|
17
|
+
* @template Inner The type that the layer wraps
|
|
18
|
+
* @template Outer The type that the layer gives
|
|
19
|
+
*/
|
|
20
|
+
type Layer<Inner = any, Outer = Inner> = (next: Inner) => Outer;
|
|
21
|
+
/** This type gives the type that a layer wraps. */
|
|
22
|
+
type InnerOf<L> = L extends ((next: infer I) => any) ? I : never;
|
|
23
|
+
/** This type gives the type that a layer gives. */
|
|
24
|
+
type OuterOf<L> = L extends ((next: any) => infer O) ? O : never;
|
|
25
|
+
/**
|
|
26
|
+
* This type checks each neighbor pair of a list of layers. The result of the inner neighbor must
|
|
27
|
+
* fit the input of the outer neighbor. At the first bad pair, the compiler marks that argument.
|
|
28
|
+
*
|
|
29
|
+
* The type is tail recursive, so the compiler accepts a chain of 100 layers.
|
|
30
|
+
*/
|
|
31
|
+
type Chain<L extends readonly unknown[], Done extends readonly unknown[] = readonly []> = L extends readonly [infer A, infer B, ...infer R] ? [OuterOf<B>] extends [InnerOf<A>] ? Chain<readonly [B, ...R], readonly [...Done, A]> : readonly [...Done, A, Layer<any, InnerOf<A>>, ...R] : readonly [...Done, ...L];
|
|
32
|
+
/**
|
|
33
|
+
* This type gives the function that {@link compose} returns. Its input is the input of the last
|
|
34
|
+
* layer, and its result is the result of the first layer. No layers give the identity function.
|
|
35
|
+
* A list that the compiler cannot see as a tuple gives the types of one element.
|
|
36
|
+
*/
|
|
37
|
+
type Composed<L extends readonly unknown[]> = L extends readonly [] ? <B>(base: B) => B : L extends readonly [infer First, ...unknown[]] ? L extends readonly [...unknown[], infer Last] ? (base: InnerOf<Last>) => OuterOf<First> : never : L extends readonly (infer E)[] ? (base: InnerOf<E>) => OuterOf<E> : never;
|
|
38
|
+
/** This type gives the intersection of the results of a list of layers. */
|
|
39
|
+
type Outers<L extends readonly unknown[], Sum = unknown> = L extends readonly [infer First, ...infer Rest] ? Outers<Rest, Sum & OuterOf<First>> : Sum;
|
|
40
|
+
/**
|
|
41
|
+
* This type gives the function for layers that keep each capability of the layers under them.
|
|
42
|
+
* Its result has the result of each layer and the base. Use it only when that law holds.
|
|
43
|
+
*/
|
|
44
|
+
type Stacked<L extends readonly unknown[]> = L extends readonly [...unknown[], infer Last] ? (base: InnerOf<Last>) => Outers<L> & InnerOf<Last> : Composed<L>;
|
|
45
|
+
/**
|
|
46
|
+
* This function folds layers into one function that wraps a base. The first layer is the outermost.
|
|
47
|
+
*
|
|
48
|
+
* The fold holds four laws:
|
|
49
|
+
* - Identity: `compose()(b)` is `b`.
|
|
50
|
+
* - One layer: `compose(f)(b)` is `f(b)`.
|
|
51
|
+
* - Associativity: `compose(f, compose(g, h))` equals `compose(compose(f, g), h)`.
|
|
52
|
+
* - Order: `compose(f, g)(b)` is `f(g(b))`.
|
|
53
|
+
*
|
|
54
|
+
* @param layers The layers, from the outermost to the innermost
|
|
55
|
+
* @returns A function that wraps a base in the layers
|
|
56
|
+
*/
|
|
57
|
+
declare function compose<const L extends readonly Layer[]>(...layers: L & Chain<L>): Composed<L>;
|
|
58
|
+
export { OuterOf as a, compose as c, Layer as i, Composed as n, Outers as o, InnerOf as r, Stacked as s, Chain as t };
|
|
59
|
+
//# sourceMappingURL=layers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layers.d.ts","names":[],"sources":["../src/core/layers.ts"],"mappings":";;;;;;;;;;;;;;;;;;;KAoBY,MAAM,aAAa,QAAQ,UAAU,MAAM,UAAU;;KAGrD,QAAQ,KAAK,YAAW,YAAY,aAAY;;KAGhD,QAAQ,KAAK,YAAW,oBAAoB,KAAI;;;;;;;KAQhD,MAAM,8BAA8B,iDAC9C,0BAA0B,SAAS,YAAY,MAC1C,QAAQ,aAAa,QAAQ,MAC5B,gBAAgB,MAAM,iBAAiB,MAAM,mBAChC,MAAM,GAAG,WAAW,QAAQ,QAAQ,kBACtC,SAAS;;;;;;KAOhB,SAAS,gCACnB,yBACK,GAAG,MAAM,MAAM,IAChB,0BAA0B,uBACxB,wCAAwC,SACrC,MAAM,QAAQ,UAAU,QAAQ,iBAEnC,0BAA0B,QACvB,MAAM,QAAQ,OAAO,QAAQ;;KAI5B,OAAO,8BAA8B,iBAC/C,0BAA0B,gBAAgB,QAAQ,OAAO,MAAM,MAAM,QAAQ,UAAU;;;;;KAM7E,QAAQ,gCAClB,wCAAwC,SACnC,MAAM,QAAQ,UAAU,OAAO,KAAK,QAAQ,QAC7C,SAAS;;;;;;;;;;;;;iBAcC,cAAc,mBAAmB,YAAY,QAAQ,IAAI,MAAM,KAAK,SAAS"}
|
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.5.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",
|
|
@@ -43,6 +43,10 @@
|
|
|
43
43
|
"types": "./build/graph/index.d.ts",
|
|
44
44
|
"default": "./build/graph/index.js"
|
|
45
45
|
},
|
|
46
|
+
"./layers": {
|
|
47
|
+
"types": "./build/core/layers.d.ts",
|
|
48
|
+
"default": "./build/core/layers.js"
|
|
49
|
+
},
|
|
46
50
|
"./testing": {
|
|
47
51
|
"types": "./build/testing/index.d.ts",
|
|
48
52
|
"default": "./build/testing/index.js"
|
|
@@ -64,6 +68,9 @@
|
|
|
64
68
|
},
|
|
65
69
|
"devDependencies": {
|
|
66
70
|
"@mark1russell7/cue": "^2.0.0",
|
|
71
|
+
"@mark1russell7/rigor-bench": "^0.2.5",
|
|
72
|
+
"@mark1russell7/rigor-generators": "^0.1.5",
|
|
73
|
+
"@mark1russell7/rigor-statistics": "^0.1.8",
|
|
67
74
|
"@types/node": "^26.6.5",
|
|
68
75
|
"esbuild": "^0.28.2",
|
|
69
76
|
"fast-check": "^4.10.2",
|
package/src/behaviors/bounded.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @example
|
|
8
8
|
* const list = compose(
|
|
9
|
-
* boundedList({ capacity: 100, policy: 'drop-oldest' })
|
|
9
|
+
* boundedList<number>({ capacity: 100, policy: 'drop-oldest' })
|
|
10
10
|
* )(arrayList<number>())
|
|
11
11
|
*
|
|
12
12
|
* // Add 101 elements - first element will be dropped
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* }
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { checkCapacity } from "../core/capacity.js";
|
|
18
19
|
import type { Middleware } from "../core/middleware.js";
|
|
19
20
|
import type { List } from "../interfaces/list.js";
|
|
20
21
|
import type { Queue, Deque } from "../interfaces/queue.js";
|
|
@@ -66,7 +67,8 @@ export interface BoundedCollection {
|
|
|
66
67
|
export function boundedList<T>(
|
|
67
68
|
options: BoundedOptions<T>
|
|
68
69
|
): Middleware<List<T>, List<T> & BoundedCollection> {
|
|
69
|
-
const {
|
|
70
|
+
const { policy = "throw", onOverflow } = options;
|
|
71
|
+
const capacity = checkCapacity(options.capacity, "boundedList");
|
|
70
72
|
|
|
71
73
|
return (next: List<T>): List<T> & BoundedCollection => {
|
|
72
74
|
// An unshift puts the element at the front, so drop-oldest drops the last element for it
|
|
@@ -235,7 +237,8 @@ export function boundedList<T>(
|
|
|
235
237
|
export function boundedQueue<T>(
|
|
236
238
|
options: BoundedOptions<T>
|
|
237
239
|
): Middleware<Queue<T>, Queue<T> & BoundedCollection> {
|
|
238
|
-
const {
|
|
240
|
+
const { policy = "throw", onOverflow } = options;
|
|
241
|
+
const capacity = checkCapacity(options.capacity, "boundedQueue");
|
|
239
242
|
|
|
240
243
|
return (next: Queue<T>): Queue<T> & BoundedCollection => {
|
|
241
244
|
const handleOverflow = (element: T): void => {
|
|
@@ -306,7 +309,8 @@ export function boundedQueue<T>(
|
|
|
306
309
|
export function boundedDeque<T>(
|
|
307
310
|
options: BoundedOptions<T>
|
|
308
311
|
): Middleware<Deque<T>, Deque<T> & BoundedCollection> {
|
|
309
|
-
const {
|
|
312
|
+
const { policy = "throw", onOverflow } = options;
|
|
313
|
+
const capacity = checkCapacity(options.capacity, "boundedDeque");
|
|
310
314
|
|
|
311
315
|
return (next: Deque<T>): Deque<T> & BoundedCollection => {
|
|
312
316
|
const handleOverflow = (element: T): void => {
|
|
@@ -383,7 +387,8 @@ export function boundedMap<K, V>(
|
|
|
383
387
|
onOverflow?: (context: Omit<OverflowContext<V>, "newElement"> & { key: K; value: V }) => void;
|
|
384
388
|
}
|
|
385
389
|
): Middleware<MapLike<K, V>, MapLike<K, V> & BoundedCollection> {
|
|
386
|
-
const {
|
|
390
|
+
const { policy = "throw", onOverflow } = options;
|
|
391
|
+
const capacity = checkCapacity(options.capacity, "boundedMap");
|
|
387
392
|
|
|
388
393
|
return (next: MapLike<K, V>): MapLike<K, V> & BoundedCollection => {
|
|
389
394
|
let insertionOrder: K[] = []; // Track insertion order for drop-oldest
|
package/src/behaviors/evented.ts
CHANGED
package/src/behaviors/lru.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @example
|
|
8
8
|
* const cache = compose(
|
|
9
|
-
* lruMap({ capacity: 100 })
|
|
9
|
+
* lruMap<string, Data>({ capacity: 100 })
|
|
10
10
|
* )(hashMap<string, Data>())
|
|
11
11
|
*
|
|
12
12
|
* cache.set('key1', data1) // Accessed
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* // When full, least recently accessed keys are evicted
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { checkCapacity } from "../core/capacity.js";
|
|
17
18
|
import type { Middleware } from "../core/middleware.js";
|
|
18
19
|
import type { MapLike } from "../interfaces/map.js";
|
|
19
20
|
import { compositeMapMethod } from "./composite.js";
|
|
@@ -23,7 +24,8 @@ import { compositeMapMethod } from "./composite.js";
|
|
|
23
24
|
*/
|
|
24
25
|
export interface LRUOptions {
|
|
25
26
|
/**
|
|
26
|
-
*
|
|
27
|
+
* The maximum number of entries, a positive whole number. When the map is full, a new key
|
|
28
|
+
* evicts the least recently used entry.
|
|
27
29
|
*/
|
|
28
30
|
capacity: number;
|
|
29
31
|
|
|
@@ -50,11 +52,14 @@ interface LRUNode<K, V> {
|
|
|
50
52
|
* This function maintains a doubly-linked list to track access order.
|
|
51
53
|
* Most recently accessed items are at the head.
|
|
52
54
|
* When capacity is exceeded, items at the tail are evicted.
|
|
55
|
+
*
|
|
56
|
+
* @throws RangeError when `options.capacity` is not a positive whole number.
|
|
53
57
|
*/
|
|
54
58
|
export function lruMap<K, V>(
|
|
55
59
|
options: LRUOptions
|
|
56
60
|
): Middleware<MapLike<K, V>, MapLike<K, V> & { readonly isFull: boolean }> {
|
|
57
|
-
const {
|
|
61
|
+
const { onEvict } = options;
|
|
62
|
+
const capacity = checkCapacity(options.capacity, "lruMap");
|
|
58
63
|
|
|
59
64
|
return (next: MapLike<K, V>): MapLike<K, V> & { readonly isFull: boolean } => {
|
|
60
65
|
// Doubly-linked list for LRU tracking
|
|
@@ -290,8 +295,13 @@ export class LRUCache<K, V> {
|
|
|
290
295
|
private head: LRUNode<K, V> | null = null;
|
|
291
296
|
private tail: LRUNode<K, V> | null = null;
|
|
292
297
|
|
|
298
|
+
/**
|
|
299
|
+
* @param capacity - The maximum number of entries, a positive whole number.
|
|
300
|
+
* @param onEvict - The function that this cache starts for each entry that it evicts.
|
|
301
|
+
* @throws RangeError when `capacity` is not a positive whole number.
|
|
302
|
+
*/
|
|
293
303
|
constructor(capacity: number, onEvict?: ((key: K, value: V) => void) | undefined) {
|
|
294
|
-
this.capacity = capacity;
|
|
304
|
+
this.capacity = checkCapacity(capacity, "LRUCache");
|
|
295
305
|
this.onEvict = onEvict;
|
|
296
306
|
}
|
|
297
307
|
|
|
@@ -359,6 +369,32 @@ export class LRUCache<K, V> {
|
|
|
359
369
|
return true;
|
|
360
370
|
}
|
|
361
371
|
|
|
372
|
+
/**
|
|
373
|
+
* This method deletes each entry for which `matches` gives true, and it gives the number of
|
|
374
|
+
* deleted entries. It is not a use: the order of the other entries does not change, and it
|
|
375
|
+
* does not start `onEvict`. It starts `matches` once for each entry, the most recent first. If
|
|
376
|
+
* `matches` throws, the entries that it matched before stay deleted.
|
|
377
|
+
*
|
|
378
|
+
* @param matches - The test of an entry.
|
|
379
|
+
* @returns The number of deleted entries.
|
|
380
|
+
*/
|
|
381
|
+
deleteWhere(matches: (value: V, key: K) => boolean): number {
|
|
382
|
+
const nodes: LRUNode<K, V>[] = [];
|
|
383
|
+
for (let node = this.head; node; node = node.next) nodes.push(node);
|
|
384
|
+
let deleted = 0;
|
|
385
|
+
for (const node of nodes) {
|
|
386
|
+
// The function `matches` can change the cache. Skip each node that is not in it now.
|
|
387
|
+
if (this.map.get(node.key)?.node !== node) continue;
|
|
388
|
+
if (!matches(node.value, node.key)) continue;
|
|
389
|
+
// The function `matches` can delete this node itself, so check again.
|
|
390
|
+
if (this.map.get(node.key)?.node !== node) continue;
|
|
391
|
+
this.removeNode(node);
|
|
392
|
+
this.map.delete(node.key);
|
|
393
|
+
deleted++;
|
|
394
|
+
}
|
|
395
|
+
return deleted;
|
|
396
|
+
}
|
|
397
|
+
|
|
362
398
|
clear(): void {
|
|
363
399
|
this.map.clear();
|
|
364
400
|
this.head = null;
|
|
@@ -439,7 +475,9 @@ export class LRUCache<K, V> {
|
|
|
439
475
|
}
|
|
440
476
|
|
|
441
477
|
/**
|
|
442
|
-
*
|
|
478
|
+
* This function makes an LRU cache.
|
|
479
|
+
*
|
|
480
|
+
* @throws RangeError when `capacity` is not a positive whole number.
|
|
443
481
|
*/
|
|
444
482
|
export function lruCache<K, V>(
|
|
445
483
|
capacity: number,
|
package/src/behaviors/safe.ts
CHANGED
package/src/behaviors/ttl.ts
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The check of a capacity, for each layer and class that has a maximum size.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* This function gives the capacity when it is a positive whole number. Otherwise it throws a
|
|
7
|
+
* `RangeError`. A capacity of `NaN` makes a comparison `size >= capacity` always false, so a
|
|
8
|
+
* structure with that capacity grows without a limit. A capacity of 0 or less, or a fraction,
|
|
9
|
+
* has no clear meaning.
|
|
10
|
+
*
|
|
11
|
+
* @param capacity - The capacity to check.
|
|
12
|
+
* @param owner - The name of the layer or class, for the message of the error.
|
|
13
|
+
* @returns The same capacity.
|
|
14
|
+
* @throws RangeError when the capacity is not a positive whole number.
|
|
15
|
+
*/
|
|
16
|
+
export function checkCapacity(capacity: number, owner: string): number {
|
|
17
|
+
if (!Number.isSafeInteger(capacity) || capacity < 1) {
|
|
18
|
+
throw new RangeError(`${owner}: the capacity must be a positive whole number, not ${String(capacity)}`);
|
|
19
|
+
}
|
|
20
|
+
return capacity;
|
|
21
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This module holds the one fold of a stack of layers. A layer wraps the next thing, so a stack
|
|
3
|
+
* of layers wraps a base. The next thing can be a collection, a function or any other value.
|
|
4
|
+
*
|
|
5
|
+
* This file has no imports. A package that needs only the fold loads this one file.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const parse = compose(
|
|
9
|
+
* (next: (text: string) => number) => (text: string) => next(text.trim()),
|
|
10
|
+
* (next: (text: string) => number) => (text: string) => next(text) * 2
|
|
11
|
+
* )(Number);
|
|
12
|
+
* parse(" 21 "); // 42
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A layer takes the next thing and gives a wrapped thing.
|
|
17
|
+
*
|
|
18
|
+
* @template Inner The type that the layer wraps
|
|
19
|
+
* @template Outer The type that the layer gives
|
|
20
|
+
*/
|
|
21
|
+
export type Layer<Inner = any, Outer = Inner> = (next: Inner) => Outer;
|
|
22
|
+
|
|
23
|
+
/** This type gives the type that a layer wraps. */
|
|
24
|
+
export type InnerOf<L> = L extends (next: infer I) => any ? I : never;
|
|
25
|
+
|
|
26
|
+
/** This type gives the type that a layer gives. */
|
|
27
|
+
export type OuterOf<L> = L extends (next: any) => infer O ? O : never;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* This type checks each neighbor pair of a list of layers. The result of the inner neighbor must
|
|
31
|
+
* fit the input of the outer neighbor. At the first bad pair, the compiler marks that argument.
|
|
32
|
+
*
|
|
33
|
+
* The type is tail recursive, so the compiler accepts a chain of 100 layers.
|
|
34
|
+
*/
|
|
35
|
+
export type Chain<L extends readonly unknown[], Done extends readonly unknown[] = readonly []> =
|
|
36
|
+
L extends readonly [infer A, infer B, ...infer R]
|
|
37
|
+
? [OuterOf<B>] extends [InnerOf<A>]
|
|
38
|
+
? Chain<readonly [B, ...R], readonly [...Done, A]>
|
|
39
|
+
: readonly [...Done, A, Layer<any, InnerOf<A>>, ...R]
|
|
40
|
+
: readonly [...Done, ...L];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* This type gives the function that {@link compose} returns. Its input is the input of the last
|
|
44
|
+
* layer, and its result is the result of the first layer. No layers give the identity function.
|
|
45
|
+
* A list that the compiler cannot see as a tuple gives the types of one element.
|
|
46
|
+
*/
|
|
47
|
+
export type Composed<L extends readonly unknown[]> =
|
|
48
|
+
L extends readonly []
|
|
49
|
+
? <B>(base: B) => B
|
|
50
|
+
: L extends readonly [infer First, ...unknown[]]
|
|
51
|
+
? L extends readonly [...unknown[], infer Last]
|
|
52
|
+
? (base: InnerOf<Last>) => OuterOf<First>
|
|
53
|
+
: never
|
|
54
|
+
: L extends readonly (infer E)[]
|
|
55
|
+
? (base: InnerOf<E>) => OuterOf<E>
|
|
56
|
+
: never;
|
|
57
|
+
|
|
58
|
+
/** This type gives the intersection of the results of a list of layers. */
|
|
59
|
+
export type Outers<L extends readonly unknown[], Sum = unknown> =
|
|
60
|
+
L extends readonly [infer First, ...infer Rest] ? Outers<Rest, Sum & OuterOf<First>> : Sum;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* This type gives the function for layers that keep each capability of the layers under them.
|
|
64
|
+
* Its result has the result of each layer and the base. Use it only when that law holds.
|
|
65
|
+
*/
|
|
66
|
+
export type Stacked<L extends readonly unknown[]> =
|
|
67
|
+
L extends readonly [...unknown[], infer Last]
|
|
68
|
+
? (base: InnerOf<Last>) => Outers<L> & InnerOf<Last>
|
|
69
|
+
: Composed<L>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* This function folds layers into one function that wraps a base. The first layer is the outermost.
|
|
73
|
+
*
|
|
74
|
+
* The fold holds four laws:
|
|
75
|
+
* - Identity: `compose()(b)` is `b`.
|
|
76
|
+
* - One layer: `compose(f)(b)` is `f(b)`.
|
|
77
|
+
* - Associativity: `compose(f, compose(g, h))` equals `compose(compose(f, g), h)`.
|
|
78
|
+
* - Order: `compose(f, g)(b)` is `f(g(b))`.
|
|
79
|
+
*
|
|
80
|
+
* @param layers The layers, from the outermost to the innermost
|
|
81
|
+
* @returns A function that wraps a base in the layers
|
|
82
|
+
*/
|
|
83
|
+
export function compose<const L extends readonly Layer[]>(...layers: L & Chain<L>): Composed<L> {
|
|
84
|
+
const list: readonly Layer[] = layers;
|
|
85
|
+
return ((base: unknown) => list.reduceRight((acc, layer) => layer(acc), base)) as Composed<L>;
|
|
86
|
+
}
|
package/src/core/middleware.ts
CHANGED
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @example
|
|
11
11
|
* const list = compose(
|
|
12
|
-
* safeList(),
|
|
13
|
-
* eventedList(),
|
|
14
|
-
* boundedList({ capacity: 100 })
|
|
12
|
+
* safeList<number>(),
|
|
13
|
+
* eventedList<number>(),
|
|
14
|
+
* boundedList<number>({ capacity: 100 })
|
|
15
15
|
* )(arrayList<number>())
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import type { Collection } from "../interfaces/collection.js";
|
|
19
|
+
import { compose as fold, type Chain, type Layer, type Stacked } from "./layers.js";
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* A middleware is a function that takes a collection and gives
|
|
@@ -52,9 +53,9 @@ export type Middleware<In = any, Out extends In = In> = (next: In) => Out;
|
|
|
52
53
|
* // 3. bounded - enforces capacity limits
|
|
53
54
|
* // 4. base - the underlying ArrayList
|
|
54
55
|
* const enhancer = compose(
|
|
55
|
-
* safeList(),
|
|
56
|
-
* eventedList(),
|
|
57
|
-
* boundedList({ capacity: 100 })
|
|
56
|
+
* safeList<number>(),
|
|
57
|
+
* eventedList<number>(),
|
|
58
|
+
* boundedList<number>({ capacity: 100 })
|
|
58
59
|
* )
|
|
59
60
|
* const list = enhancer(arrayList<number>())
|
|
60
61
|
* // Type: List<number> & SafeCollection & EventedCollection & BoundedCollection
|
|
@@ -62,49 +63,8 @@ export type Middleware<In = any, Out extends In = In> = (next: In) => Out;
|
|
|
62
63
|
* @param layers The middleware layers to compose
|
|
63
64
|
* @returns A function that applies all layers to a base collection
|
|
64
65
|
*/
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
export function compose<C, R1 extends C>(
|
|
68
|
-
m1: Middleware<C, R1>
|
|
69
|
-
): (base: C) => R1;
|
|
70
|
-
|
|
71
|
-
// Overload for 2 middleware
|
|
72
|
-
export function compose<C, R1 extends C, R2 extends R1>(
|
|
73
|
-
m1: Middleware<R1, R2>,
|
|
74
|
-
m2: Middleware<C, R1>
|
|
75
|
-
): (base: C) => R2;
|
|
76
|
-
|
|
77
|
-
// Overload for 3 middleware
|
|
78
|
-
export function compose<C, R1 extends C, R2 extends R1, R3 extends R2>(
|
|
79
|
-
m1: Middleware<R2, R3>,
|
|
80
|
-
m2: Middleware<R1, R2>,
|
|
81
|
-
m3: Middleware<C, R1>
|
|
82
|
-
): (base: C) => R3;
|
|
83
|
-
|
|
84
|
-
// Overload for 4 middleware
|
|
85
|
-
export function compose<C, R1 extends C, R2 extends R1, R3 extends R2, R4 extends R3>(
|
|
86
|
-
m1: Middleware<R3, R4>,
|
|
87
|
-
m2: Middleware<R2, R3>,
|
|
88
|
-
m3: Middleware<R1, R2>,
|
|
89
|
-
m4: Middleware<C, R1>
|
|
90
|
-
): (base: C) => R4;
|
|
91
|
-
|
|
92
|
-
// Overload for 5 middleware
|
|
93
|
-
export function compose<C, R1 extends C, R2 extends R1, R3 extends R2, R4 extends R3, R5 extends R4>(
|
|
94
|
-
m1: Middleware<R4, R5>,
|
|
95
|
-
m2: Middleware<R3, R4>,
|
|
96
|
-
m3: Middleware<R2, R3>,
|
|
97
|
-
m4: Middleware<R1, R2>,
|
|
98
|
-
m5: Middleware<C, R1>
|
|
99
|
-
): (base: C) => R5;
|
|
100
|
-
|
|
101
|
-
// General fallback for any number of middleware
|
|
102
|
-
export function compose<C>(...layers: Middleware<any, any>[]): (base: C) => any;
|
|
103
|
-
|
|
104
|
-
// Implementation
|
|
105
|
-
export function compose(...layers: Middleware<any, any>[]) {
|
|
106
|
-
return (base: any): any =>
|
|
107
|
-
layers.reduceRight((acc, layer) => layer(acc), base);
|
|
66
|
+
export function compose<const L extends readonly Layer[]>(...layers: L & Chain<L>): Stacked<L> {
|
|
67
|
+
return fold(...(layers as readonly Layer[])) as unknown as Stacked<L>;
|
|
108
68
|
}
|
|
109
69
|
|
|
110
70
|
/**
|
|
@@ -361,53 +321,11 @@ export const enhanceMethod = <C extends object, K extends keyof C & string>(
|
|
|
361
321
|
*
|
|
362
322
|
* @example
|
|
363
323
|
* const standardBehaviors = bundle(
|
|
364
|
-
* boundedList({ capacity: 1000 }),
|
|
365
|
-
* eventedList(),
|
|
366
|
-
* safeList()
|
|
324
|
+
* boundedList<number>({ capacity: 1000 }),
|
|
325
|
+
* eventedList<number>(),
|
|
326
|
+
* safeList<number>()
|
|
367
327
|
* )
|
|
368
328
|
*
|
|
369
329
|
* const list = compose(standardBehaviors)(arrayList<number>())
|
|
370
330
|
*/
|
|
371
|
-
|
|
372
|
-
// Overload for 1 middleware
|
|
373
|
-
export function bundle<C, R1 extends C>(
|
|
374
|
-
m1: Middleware<C, R1>
|
|
375
|
-
): Middleware<C, R1>;
|
|
376
|
-
|
|
377
|
-
// Overload for 2 middleware
|
|
378
|
-
export function bundle<C, R1 extends C, R2 extends R1>(
|
|
379
|
-
m1: Middleware<R1, R2>,
|
|
380
|
-
m2: Middleware<C, R1>
|
|
381
|
-
): Middleware<C, R2>;
|
|
382
|
-
|
|
383
|
-
// Overload for 3 middleware
|
|
384
|
-
export function bundle<C, R1 extends C, R2 extends R1, R3 extends R2>(
|
|
385
|
-
m1: Middleware<R2, R3>,
|
|
386
|
-
m2: Middleware<R1, R2>,
|
|
387
|
-
m3: Middleware<C, R1>
|
|
388
|
-
): Middleware<C, R3>;
|
|
389
|
-
|
|
390
|
-
// Overload for 4 middleware
|
|
391
|
-
export function bundle<C, R1 extends C, R2 extends R1, R3 extends R2, R4 extends R3>(
|
|
392
|
-
m1: Middleware<R3, R4>,
|
|
393
|
-
m2: Middleware<R2, R3>,
|
|
394
|
-
m3: Middleware<R1, R2>,
|
|
395
|
-
m4: Middleware<C, R1>
|
|
396
|
-
): Middleware<C, R4>;
|
|
397
|
-
|
|
398
|
-
// Overload for 5 middleware
|
|
399
|
-
export function bundle<C, R1 extends C, R2 extends R1, R3 extends R2, R4 extends R3, R5 extends R4>(
|
|
400
|
-
m1: Middleware<R4, R5>,
|
|
401
|
-
m2: Middleware<R3, R4>,
|
|
402
|
-
m3: Middleware<R2, R3>,
|
|
403
|
-
m4: Middleware<R1, R2>,
|
|
404
|
-
m5: Middleware<C, R1>
|
|
405
|
-
): Middleware<C, R5>;
|
|
406
|
-
|
|
407
|
-
// General fallback for any number of middleware
|
|
408
|
-
export function bundle<C>(...middleware: Middleware<any, any>[]): Middleware<C, any>;
|
|
409
|
-
|
|
410
|
-
// Implementation
|
|
411
|
-
export function bundle(...middleware: Middleware<any, any>[]): Middleware<any, any> {
|
|
412
|
-
return (next: any): any => compose(...middleware)(next);
|
|
413
|
-
}
|
|
331
|
+
export const bundle: typeof compose = compose;
|