@evolu/common 5.4.7 → 6.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/README.md +30 -34
- package/dist/src/Array.d.ts +17 -0
- package/dist/src/Array.d.ts.map +1 -0
- package/dist/src/Array.js +12 -0
- package/dist/src/Assert.d.ts +68 -0
- package/dist/src/Assert.d.ts.map +1 -0
- package/dist/src/Assert.js +77 -0
- package/dist/src/BigInt.d.ts +20 -0
- package/dist/src/BigInt.d.ts.map +1 -0
- package/dist/src/BigInt.js +18 -0
- package/dist/src/Buffer.d.ts +92 -0
- package/dist/src/Buffer.d.ts.map +1 -0
- package/dist/src/Buffer.js +62 -0
- package/dist/src/Callbacks.d.ts +20 -0
- package/dist/src/Callbacks.d.ts.map +1 -0
- package/dist/src/Callbacks.js +18 -0
- package/dist/src/Console.d.ts +78 -0
- package/dist/src/Console.d.ts.map +1 -0
- package/dist/src/Console.js +103 -0
- package/dist/src/Crypto.d.ts +72 -39
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +89 -54
- package/dist/src/Eq.d.ts +97 -0
- package/dist/src/Eq.d.ts.map +1 -0
- package/dist/src/Eq.js +167 -0
- package/dist/src/Error.d.ts +14 -10
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +43 -11
- package/dist/src/Evolu/Config.d.ts +69 -0
- package/dist/src/Evolu/Config.d.ts.map +1 -0
- package/dist/src/Evolu/Config.js +9 -0
- package/dist/src/Evolu/Db.d.ts +126 -0
- package/dist/src/Evolu/Db.d.ts.map +1 -0
- package/dist/src/Evolu/Db.js +774 -0
- package/dist/src/Evolu/Diff.d.ts +43 -0
- package/dist/src/Evolu/Diff.d.ts.map +1 -0
- package/dist/src/Evolu/Diff.js +95 -0
- package/dist/src/Evolu/Evolu.d.ts +334 -0
- package/dist/src/Evolu/Evolu.d.ts.map +1 -0
- package/dist/src/Evolu/Evolu.js +434 -0
- package/dist/src/Evolu/Internal.d.ts +26 -0
- package/dist/src/Evolu/Internal.d.ts.map +1 -0
- package/dist/src/Evolu/Internal.js +25 -0
- package/dist/src/Evolu/Kysely.d.ts +6 -0
- package/dist/src/Evolu/Kysely.d.ts.map +1 -0
- package/dist/src/Evolu/Kysely.js +21 -0
- package/dist/src/Evolu/Owner.d.ts +155 -0
- package/dist/src/Evolu/Owner.d.ts.map +1 -0
- package/dist/src/Evolu/Owner.js +126 -0
- package/dist/src/Evolu/Platform.d.ts +23 -0
- package/dist/src/Evolu/Platform.d.ts.map +1 -0
- package/dist/src/Evolu/Platform.js +1 -0
- package/dist/src/Evolu/Protocol.d.ts +401 -0
- package/dist/src/Evolu/Protocol.d.ts.map +1 -0
- package/dist/src/Evolu/Protocol.js +1151 -0
- package/dist/src/Evolu/Public.d.ts +18 -0
- package/dist/src/Evolu/Public.d.ts.map +1 -0
- package/dist/src/Evolu/Public.js +11 -0
- package/dist/src/Evolu/PublicKysely.d.ts +148 -0
- package/dist/src/Evolu/PublicKysely.d.ts.map +1 -0
- package/dist/src/Evolu/PublicKysely.js +185 -0
- package/dist/src/Evolu/Query.d.ts +63 -0
- package/dist/src/Evolu/Query.d.ts.map +1 -0
- package/dist/src/Evolu/Query.js +61 -0
- package/dist/src/Evolu/Relay.d.ts +13 -0
- package/dist/src/Evolu/Relay.d.ts.map +1 -0
- package/dist/src/Evolu/Relay.js +109 -0
- package/dist/src/Evolu/Schema.d.ts +201 -0
- package/dist/src/Evolu/Schema.d.ts.map +1 -0
- package/dist/src/Evolu/Schema.js +150 -0
- package/dist/src/Evolu/Storage.d.ts +49 -0
- package/dist/src/Evolu/Storage.d.ts.map +1 -0
- package/dist/src/Evolu/Storage.js +1111 -0
- package/dist/src/Evolu/Sync.d.ts +59 -0
- package/dist/src/Evolu/Sync.d.ts.map +1 -0
- package/dist/src/Evolu/Sync.js +29 -0
- package/dist/src/Evolu/Timestamp.d.ts +106 -0
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -0
- package/dist/src/Evolu/Timestamp.js +179 -0
- package/dist/src/Function.d.ts +54 -0
- package/dist/src/Function.d.ts.map +1 -0
- package/dist/src/Function.js +38 -0
- package/dist/src/ManyToManyMap.d.ts +26 -0
- package/dist/src/ManyToManyMap.d.ts.map +1 -0
- package/dist/src/ManyToManyMap.js +92 -0
- package/dist/src/NanoId.d.ts +27 -0
- package/dist/src/NanoId.d.ts.map +1 -0
- package/dist/src/NanoId.js +6 -0
- package/dist/src/Number.d.ts +42 -0
- package/dist/src/Number.d.ts.map +1 -0
- package/dist/src/Number.js +55 -0
- package/dist/src/Object.d.ts +35 -0
- package/dist/src/Object.d.ts.map +1 -0
- package/dist/src/Object.js +36 -0
- package/dist/src/Order.d.ts +90 -0
- package/dist/src/Order.d.ts.map +1 -0
- package/dist/src/Order.js +85 -0
- package/dist/src/Promise.d.ts +180 -0
- package/dist/src/Promise.d.ts.map +1 -0
- package/dist/src/Promise.js +176 -0
- package/dist/src/Random.d.ts +52 -0
- package/dist/src/Random.d.ts.map +1 -0
- package/dist/src/Random.js +29 -0
- package/dist/src/Ref.d.ts +40 -0
- package/dist/src/Ref.d.ts.map +1 -0
- package/dist/src/Ref.js +13 -0
- package/dist/src/Result.d.ts +421 -0
- package/dist/src/Result.d.ts.map +1 -0
- package/dist/src/Result.js +357 -0
- package/dist/src/Skiplist.d.ts +23 -0
- package/dist/src/Skiplist.d.ts.map +1 -0
- package/dist/src/Skiplist.js +58 -0
- package/dist/src/Sqlite.d.ts +116 -52
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +183 -67
- package/dist/src/Store.d.ts +45 -8
- package/dist/src/Store.d.ts.map +1 -1
- package/dist/src/Store.js +33 -17
- package/dist/src/String.d.ts +2 -0
- package/dist/src/String.d.ts.map +1 -0
- package/dist/src/String.js +14 -0
- package/dist/src/Time.d.ts +20 -0
- package/dist/src/Time.d.ts.map +1 -0
- package/dist/src/Time.js +25 -0
- package/dist/src/Type.d.ts +1937 -0
- package/dist/src/Type.d.ts.map +1 -0
- package/dist/src/Type.js +2002 -0
- package/dist/src/Types.d.ts +188 -0
- package/dist/src/Types.d.ts.map +1 -0
- package/dist/src/Types.js +6 -0
- package/dist/src/WebSocket.d.ts +112 -0
- package/dist/src/WebSocket.d.ts.map +1 -0
- package/dist/src/WebSocket.js +139 -0
- package/dist/src/Worker.d.ts +44 -0
- package/dist/src/Worker.d.ts.map +1 -0
- package/dist/src/Worker.js +66 -0
- package/dist/src/index.d.ts +24 -11
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +24 -11
- package/package.json +29 -38
- package/src/Array.ts +39 -0
- package/src/Assert.ts +116 -0
- package/src/BigInt.ts +29 -0
- package/src/Buffer.ts +175 -0
- package/src/Callbacks.ts +43 -0
- package/src/Console.ts +159 -0
- package/src/Crypto.ts +169 -115
- package/src/Eq.ts +204 -0
- package/src/Error.ts +57 -20
- package/src/Evolu/Config.ts +83 -0
- package/src/Evolu/Db.ts +1275 -0
- package/src/Evolu/Diff.ts +142 -0
- package/src/Evolu/Evolu.ts +947 -0
- package/src/Evolu/Internal.ts +26 -0
- package/src/Evolu/Kysely.ts +38 -0
- package/src/Evolu/Owner.ts +296 -0
- package/src/Evolu/Platform.ts +27 -0
- package/src/Evolu/Protocol.ts +1857 -0
- package/src/Evolu/Public.ts +43 -0
- package/src/Evolu/PublicKysely.ts +240 -0
- package/src/Evolu/Query.ts +167 -0
- package/src/Evolu/Relay.ts +142 -0
- package/src/Evolu/Schema.ts +417 -0
- package/src/Evolu/Storage.ts +1281 -0
- package/src/Evolu/Sync.ts +105 -0
- package/src/Evolu/Timestamp.ts +311 -0
- package/src/Function.ts +58 -0
- package/src/ManyToManyMap.ts +140 -0
- package/src/NanoId.ts +39 -0
- package/src/Number.ts +90 -0
- package/src/Object.ts +64 -0
- package/src/Order.ts +113 -0
- package/src/Promise.ts +295 -0
- package/src/Random.ts +68 -0
- package/src/Ref.ts +63 -0
- package/src/Result.ts +453 -0
- package/src/Skiplist.ts +102 -0
- package/src/Sqlite.ts +366 -153
- package/src/Store.ts +79 -36
- package/src/String.ts +10 -0
- package/src/Time.ts +36 -0
- package/src/Type.ts +3978 -0
- package/src/Types.ts +209 -0
- package/src/WebSocket.ts +273 -0
- package/src/Worker.ts +129 -0
- package/src/index.ts +24 -11
- package/dist/src/Config.d.ts +0 -56
- package/dist/src/Config.d.ts.map +0 -1
- package/dist/src/Config.js +0 -39
- package/dist/src/Crdt.d.ts +0 -89
- package/dist/src/Crdt.d.ts.map +0 -1
- package/dist/src/Crdt.js +0 -181
- package/dist/src/Db.d.ts +0 -107
- package/dist/src/Db.d.ts.map +0 -1
- package/dist/src/Db.js +0 -443
- package/dist/src/Diff.d.ts +0 -27
- package/dist/src/Diff.d.ts.map +0 -1
- package/dist/src/Diff.js +0 -84
- package/dist/src/Evolu.d.ts +0 -426
- package/dist/src/Evolu.d.ts.map +0 -1
- package/dist/src/Evolu.js +0 -333
- package/dist/src/Model.d.ts +0 -141
- package/dist/src/Model.d.ts.map +0 -1
- package/dist/src/Model.js +0 -125
- package/dist/src/Murmurhash.d.ts +0 -2
- package/dist/src/Murmurhash.d.ts.map +0 -1
- package/dist/src/Murmurhash.js +0 -60
- package/dist/src/Owner.d.ts +0 -33
- package/dist/src/Owner.d.ts.map +0 -1
- package/dist/src/Owner.js +0 -26
- package/dist/src/Platform.d.ts +0 -37
- package/dist/src/Platform.d.ts.map +0 -1
- package/dist/src/Platform.js +0 -11
- package/dist/src/Protobuf.d.ts +0 -81
- package/dist/src/Protobuf.d.ts.map +0 -1
- package/dist/src/Protobuf.js +0 -92
- package/dist/src/Public.d.ts +0 -13
- package/dist/src/Public.d.ts.map +0 -1
- package/dist/src/Public.js +0 -6
- package/dist/src/Socket.d.ts +0 -8
- package/dist/src/Socket.d.ts.map +0 -1
- package/dist/src/Socket.js +0 -51
- package/dist/src/Sql.d.ts +0 -12
- package/dist/src/Sql.d.ts.map +0 -1
- package/dist/src/Sql.js +0 -30
- package/dist/src/Sync.d.ts +0 -70
- package/dist/src/Sync.d.ts.map +0 -1
- package/dist/src/Sync.js +0 -127
- package/src/Config.ts +0 -119
- package/src/Crdt.ts +0 -361
- package/src/Db.ts +0 -955
- package/src/Diff.ts +0 -114
- package/src/Evolu.ts +0 -1016
- package/src/Model.ts +0 -233
- package/src/Murmurhash.ts +0 -70
- package/src/Owner.ts +0 -69
- package/src/Platform.ts +0 -47
- package/src/Protobuf.ts +0 -155
- package/src/Public.ts +0 -12
- package/src/Socket.ts +0 -83
- package/src/Sql.ts +0 -41
- package/src/Sync.ts +0 -315
package/src/Number.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { NonEmptyReadonlyArray } from "./Array.js";
|
|
2
|
+
import { assertNonEmptyReadonlyArray } from "./Assert.js";
|
|
3
|
+
import { err, ok, Result } from "./Result.js";
|
|
4
|
+
import { NonNegativeInt, PositiveInt } from "./Type.js";
|
|
5
|
+
import {
|
|
6
|
+
IntentionalNever,
|
|
7
|
+
IsBranded,
|
|
8
|
+
Predicate,
|
|
9
|
+
WidenLiteral,
|
|
10
|
+
} from "./Types.js";
|
|
11
|
+
|
|
12
|
+
export const increment = (n: number): number => n + 1;
|
|
13
|
+
|
|
14
|
+
export const decrement = (n: number): number => n - 1;
|
|
15
|
+
|
|
16
|
+
/** Clamps a number within a given range. */
|
|
17
|
+
export const clamp =
|
|
18
|
+
(min: number, max: number) =>
|
|
19
|
+
(n: number): number =>
|
|
20
|
+
Math.min(Math.max(n, min), max);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates a predicate that checks if a number is within a range, inclusive.
|
|
24
|
+
*
|
|
25
|
+
* ### Example
|
|
26
|
+
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* const isBetween10And20 = isBetween(10, 20);
|
|
29
|
+
* console.log(isBetween10And20(15)); // true
|
|
30
|
+
* console.log(isBetween10And20(25)); // false
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export const isBetween =
|
|
34
|
+
(min: number, max: number): Predicate<number> =>
|
|
35
|
+
(value) =>
|
|
36
|
+
value >= min && value <= max;
|
|
37
|
+
|
|
38
|
+
/** Returns the minimum value, preserving branded type if applicable. */
|
|
39
|
+
export const min = <T extends number>(
|
|
40
|
+
...values: [T, ...ReadonlyArray<T>]
|
|
41
|
+
): IsBranded<T> extends true ? T : WidenLiteral<T> =>
|
|
42
|
+
values.reduce((a, b) => (a < b ? a : b)) as IntentionalNever;
|
|
43
|
+
|
|
44
|
+
/** Returns the maximum value, preserving branded type if applicable. */
|
|
45
|
+
export const max = <T extends number>(
|
|
46
|
+
...values: [T, ...ReadonlyArray<T>]
|
|
47
|
+
): IsBranded<T> extends true ? T : WidenLiteral<T> =>
|
|
48
|
+
values.reduce((a, b) => (a > b ? a : b)) as IntentionalNever;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Divides items into buckets as evenly as possible, ensuring each bucket has at
|
|
52
|
+
* least the minimum number of items. Returns a success result if the minimum is
|
|
53
|
+
* met, or an error result with the required number of items if not.
|
|
54
|
+
*
|
|
55
|
+
* ### Example
|
|
56
|
+
*
|
|
57
|
+
* ```ts
|
|
58
|
+
* computeBalancedBuckets(10, 3, 2); // Returns ok([4, 7, 10])
|
|
59
|
+
* computeBalancedBuckets(5, 3, 2); // Returns err(6)
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export const computeBalancedBuckets = (
|
|
63
|
+
numberOfItems: NonNegativeInt,
|
|
64
|
+
|
|
65
|
+
/** Default: 16 */
|
|
66
|
+
numberOfBuckets = 16 as PositiveInt,
|
|
67
|
+
|
|
68
|
+
/** Default: 2 */
|
|
69
|
+
minNumberOfItemsPerBucket = 2 as PositiveInt,
|
|
70
|
+
): Result<NonEmptyReadonlyArray<PositiveInt>, PositiveInt> => {
|
|
71
|
+
const minRequiredItems = numberOfBuckets * minNumberOfItemsPerBucket;
|
|
72
|
+
|
|
73
|
+
if (numberOfItems < minRequiredItems)
|
|
74
|
+
return err(minRequiredItems as PositiveInt);
|
|
75
|
+
|
|
76
|
+
const indexes: Array<PositiveInt> = [];
|
|
77
|
+
const itemsPerBucket = Math.floor(numberOfItems / numberOfBuckets);
|
|
78
|
+
const extraItems = numberOfItems % numberOfBuckets;
|
|
79
|
+
|
|
80
|
+
let bucketBoundary = 0;
|
|
81
|
+
for (let i = 0; i < numberOfBuckets; i++) {
|
|
82
|
+
const hasExtraItem = i < extraItems;
|
|
83
|
+
const itemsInThisBucket = itemsPerBucket + (hasExtraItem ? 1 : 0);
|
|
84
|
+
bucketBoundary += itemsInThisBucket;
|
|
85
|
+
indexes.push(bucketBoundary as PositiveInt);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
assertNonEmptyReadonlyArray(indexes);
|
|
89
|
+
return ok(indexes);
|
|
90
|
+
};
|
package/src/Object.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if a value is a plain object (e.g., created with `{}` or `Object`).
|
|
3
|
+
*
|
|
4
|
+
* ### Example
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* isPlainObject({}); // true
|
|
8
|
+
* isPlainObject(new Date()); // false
|
|
9
|
+
* isPlainObject([]); // false
|
|
10
|
+
* isPlainObject(null); // false
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export const isPlainObject = (
|
|
14
|
+
value: unknown,
|
|
15
|
+
): value is Record<string, unknown> =>
|
|
16
|
+
Object.prototype.toString.call(value) === "[object Object]";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A read-only `Record<K, V>` with `K extends keyof any` to preserve branded key
|
|
20
|
+
* types (e.g., in {@link mapObject}).
|
|
21
|
+
*/
|
|
22
|
+
export type ReadonlyRecord<K extends keyof any, V> = Readonly<Record<K, V>>;
|
|
23
|
+
|
|
24
|
+
// A helper type to remove symbol keys (e.g for branded objects).
|
|
25
|
+
type StringKeyOf<T> = Extract<keyof T, string>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Converts a record to entries, preserving branded string key types (e.g.,
|
|
29
|
+
* `type Id = 'id' & string`) via `StringKeyOf<T>`, unlike `Object.entries`
|
|
30
|
+
* which widens keys to `string`.
|
|
31
|
+
*/
|
|
32
|
+
export const objectToEntries = <T extends Record<string, any>>(
|
|
33
|
+
record: T,
|
|
34
|
+
): Array<[StringKeyOf<T>, T[StringKeyOf<T>]]> =>
|
|
35
|
+
Object.entries(record) as Array<[StringKeyOf<T>, T[StringKeyOf<T>]]>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Maps a `ReadonlyRecord<K, V>` to a new `ReadonlyRecord<K, U>`, preserving
|
|
39
|
+
* branded key types (e.g., `type Id = 'id' & string`) lost by `Object.entries`.
|
|
40
|
+
* Uses `K extends string` for precision.
|
|
41
|
+
*/
|
|
42
|
+
export const mapObject = <K extends string, V, U>(
|
|
43
|
+
record: ReadonlyRecord<K, V>,
|
|
44
|
+
fn: (value: V, key: K) => U,
|
|
45
|
+
): ReadonlyRecord<K, U> =>
|
|
46
|
+
Object.fromEntries(
|
|
47
|
+
Object.entries(record).map(([key, value]) => [
|
|
48
|
+
key,
|
|
49
|
+
fn(value as V, key as K),
|
|
50
|
+
]),
|
|
51
|
+
) as ReadonlyRecord<K, U>;
|
|
52
|
+
|
|
53
|
+
/** Conditionally excludes a property from an object. */
|
|
54
|
+
export const excludeProp = <T extends object, K extends keyof T>(
|
|
55
|
+
obj: T,
|
|
56
|
+
prop: K,
|
|
57
|
+
condition?: boolean,
|
|
58
|
+
): typeof condition extends true ? T : Omit<T, K> => {
|
|
59
|
+
if (condition) {
|
|
60
|
+
return { ...obj };
|
|
61
|
+
}
|
|
62
|
+
const { [prop]: _, ...rest } = obj;
|
|
63
|
+
return rest;
|
|
64
|
+
};
|
package/src/Order.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🔢
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Compares two values of type `A` and returns their ordering.
|
|
9
|
+
*
|
|
10
|
+
* Ordering functions start with an 'order' prefix, e.g., `orderNumber`.
|
|
11
|
+
*
|
|
12
|
+
* - Returns `-1` if `x` is less than `y`.
|
|
13
|
+
* - Returns `0` if `x` is equal to `y`.
|
|
14
|
+
* - Returns `1` if `x` is greater than `y`.
|
|
15
|
+
*/
|
|
16
|
+
export type Order<in A> = (x: A, y: A) => Ordering;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A type representing the result of an ordering operation.
|
|
20
|
+
*
|
|
21
|
+
* Compatible with the return values expected by `Array.prototype.sort`.
|
|
22
|
+
*/
|
|
23
|
+
export type Ordering = -1 | 0 | 1;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates an ordering function from a "less than" comparator.
|
|
27
|
+
*
|
|
28
|
+
* ### Example
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* const orderNumber = createOrder<number>((x, y) => x < y);
|
|
32
|
+
* expect(orderNumber(1, 2)).toEqual(-1);
|
|
33
|
+
* expect(orderNumber(2, 1)).toEqual(1);
|
|
34
|
+
* expect(orderNumber(1, 1)).toEqual(0);
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export const createOrder =
|
|
38
|
+
<A>(isLessThan: (x: A, y: A) => boolean): Order<A> =>
|
|
39
|
+
(x, y) =>
|
|
40
|
+
x === y ? 0 : isLessThan(x, y) ? -1 : 1;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Returns an order that reverses the order of the given order.
|
|
44
|
+
*
|
|
45
|
+
* ### Example
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* reverseOrder(orderNumber)(1, 2); // 1
|
|
49
|
+
* reverseOrder(orderNumber)(2, 1); // -1
|
|
50
|
+
* reverseOrder(orderNumber)(1, 1); // 0
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export const reverseOrder =
|
|
54
|
+
<A>(order: Order<A>): Order<A> =>
|
|
55
|
+
(a, b) =>
|
|
56
|
+
order(b, a);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* An order for `string` values in ascending order.
|
|
60
|
+
*
|
|
61
|
+
* ### Example
|
|
62
|
+
*
|
|
63
|
+
* ```ts
|
|
64
|
+
* orderString("a", "b"); // -1
|
|
65
|
+
* orderString("b", "a"); // 1
|
|
66
|
+
* orderString("a", "a"); // 0
|
|
67
|
+
* ["c", "b", "a"].toSorted(orderString); // ["a", "b", "c"]
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export const orderString: Order<string> = createOrder((a, b) => a < b);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* An order for numbers in ascending order.
|
|
74
|
+
*
|
|
75
|
+
* ### Example
|
|
76
|
+
*
|
|
77
|
+
* ```ts
|
|
78
|
+
* orderNumber(1, 2); // -1
|
|
79
|
+
* orderNumber(2, 1); // 1
|
|
80
|
+
* orderNumber(1, 1); // 0
|
|
81
|
+
* [2, 1, 3].toSorted(orderNumber); // [1, 2, 3]
|
|
82
|
+
* reverseOrder(orderNumber)(1, 2); // 1
|
|
83
|
+
* reverseOrder(orderNumber)(2, 1); // -1
|
|
84
|
+
* reverseOrder(orderNumber)(1, 1); // 0
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export const orderNumber = createOrder<number>((a, b) => a < b);
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* An order for bigints in ascending order.
|
|
91
|
+
*
|
|
92
|
+
* ### Example
|
|
93
|
+
*
|
|
94
|
+
* ```ts
|
|
95
|
+
* orderBigInt(1n, 2n); // -1
|
|
96
|
+
* orderBigInt(2n, 1n); // 1
|
|
97
|
+
* orderBigInt(1n, 1n); // 0
|
|
98
|
+
* [2n, 1n, 3n].toSorted(orderBigInt); // [1n, 2n, 3n]
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export const orderBigInt = createOrder<bigint>((a, b) => a < b);
|
|
102
|
+
|
|
103
|
+
export const orderUint8Array: Order<globalThis.Uint8Array> = (a, b) => {
|
|
104
|
+
if (a.byteLength > b.byteLength) return 1;
|
|
105
|
+
if (a.byteLength < b.byteLength) return -1;
|
|
106
|
+
|
|
107
|
+
for (let i = 0; i < a.byteLength; i++) {
|
|
108
|
+
if (a[i] < b[i]) return -1;
|
|
109
|
+
if (a[i] > b[i]) return 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return 0;
|
|
113
|
+
};
|
package/src/Promise.ts
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { constTrue } from "./Function.js";
|
|
2
|
+
import { Result, err, ok } from "./Result.js";
|
|
3
|
+
import { Predicate } from "./Types.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Helper function to delay execution for a specified number of milliseconds.
|
|
7
|
+
*
|
|
8
|
+
* ### Example
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* await wait(10);
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export const wait = (ms: number): Promise<Result<void, never>> =>
|
|
15
|
+
new Promise((resolve) =>
|
|
16
|
+
setTimeout(() => {
|
|
17
|
+
resolve(ok());
|
|
18
|
+
}, ms),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
/** Options for configuring retry behavior. */
|
|
22
|
+
export interface RetryOptions<E> {
|
|
23
|
+
/**
|
|
24
|
+
* Maximum number of retry attempts after the initial attempt (default: 3).
|
|
25
|
+
* For example, with maxRetries = 3, the function will be called up to 4 times
|
|
26
|
+
* (1 initial attempt + 3 retries).
|
|
27
|
+
*/
|
|
28
|
+
maxRetries?: number;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Initial delay between retry attempts in milliseconds (default: 100). This
|
|
32
|
+
* is the delay after the first failed attempt. Subsequent delays increase
|
|
33
|
+
* exponentially according to the factor option.
|
|
34
|
+
*/
|
|
35
|
+
initialDelay?: number;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Maximum delay between retry attempts in milliseconds (default: 10000). This
|
|
39
|
+
* caps the exponential backoff to prevent extremely long delays after many
|
|
40
|
+
* retries.
|
|
41
|
+
*/
|
|
42
|
+
maxDelay?: number;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Multiplier that determines how quickly the delay increases (default: 2).
|
|
46
|
+
* With the default value, each successive delay is twice as long as the
|
|
47
|
+
* previous one (e.g., 100ms, 200ms, 400ms, 800ms, etc).
|
|
48
|
+
*/
|
|
49
|
+
factor?: number;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Random jitter factor between 0 and 1 (default: 0.1). Adds randomness to
|
|
53
|
+
* delay times to prevent retry storms in distributed systems.
|
|
54
|
+
*/
|
|
55
|
+
jitter?: number;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Optional AbortSignal to cancel retries. If the signal is aborted, the retry
|
|
59
|
+
* operation stops and returns a RetryAbortError.
|
|
60
|
+
*/
|
|
61
|
+
signal?: AbortSignal;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Optional predicate to determine if an error should be retried. Returns true
|
|
65
|
+
* if the error is retryable, false otherwise. This allows selectively
|
|
66
|
+
* retrying only certain types of errors. By default, all errors are
|
|
67
|
+
* considered retryable.
|
|
68
|
+
*/
|
|
69
|
+
retryable?: Predicate<E>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Optional callback called before each retry attempt. Receives the error that
|
|
73
|
+
* caused the retry, the current attempt number (starting at 1), and the delay
|
|
74
|
+
* in milliseconds before the next attempt.
|
|
75
|
+
*/
|
|
76
|
+
onRetry?: (error: E, attempt: number, delay: number) => void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Error representing a retry operation that failed after multiple attempts. */
|
|
80
|
+
export interface RetryError<E> {
|
|
81
|
+
readonly type: "RetryError";
|
|
82
|
+
/** The original error that caused the retry to fail */
|
|
83
|
+
readonly cause: E;
|
|
84
|
+
/** Number of retry attempts made */
|
|
85
|
+
readonly attempts: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Error representing a retry operation that was aborted. */
|
|
89
|
+
export interface RetryAbortError {
|
|
90
|
+
readonly type: "RetryAbortError";
|
|
91
|
+
readonly abortedBeforeExecution: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Executes a function with retry logic using exponential backoff and jitter.
|
|
96
|
+
*
|
|
97
|
+
* ### Example with Result-based API
|
|
98
|
+
*
|
|
99
|
+
* ```ts
|
|
100
|
+
* interface ApiError {
|
|
101
|
+
* type: "ApiError";
|
|
102
|
+
* statusCode: number;
|
|
103
|
+
* }
|
|
104
|
+
*
|
|
105
|
+
* const fetchData = async (
|
|
106
|
+
* url: string,
|
|
107
|
+
* ): Promise<Result<Data, ApiError>> => {
|
|
108
|
+
* // Implementation that returns Result
|
|
109
|
+
* };
|
|
110
|
+
*
|
|
111
|
+
* const result = await retry(
|
|
112
|
+
* async () => fetchData("https://api.example.com/data"),
|
|
113
|
+
* {
|
|
114
|
+
* maxRetries: 5,
|
|
115
|
+
* initialDelay: 200,
|
|
116
|
+
* // Only retry on specific status codes
|
|
117
|
+
* retryable: (error) =>
|
|
118
|
+
* error.type === "ApiError" && [429, 503].includes(error.statusCode),
|
|
119
|
+
* },
|
|
120
|
+
* );
|
|
121
|
+
*
|
|
122
|
+
* if (!result.ok) {
|
|
123
|
+
* if (result.error.type === "RetryAbortError") {
|
|
124
|
+
* console.log("Operation was aborted");
|
|
125
|
+
* } else {
|
|
126
|
+
* console.log(`Failed after ${result.error.attempts} attempts`);
|
|
127
|
+
* }
|
|
128
|
+
* return;
|
|
129
|
+
* }
|
|
130
|
+
*
|
|
131
|
+
* // Use result.value
|
|
132
|
+
* ```
|
|
133
|
+
*
|
|
134
|
+
* ### Example with tryAsync for exception-based API
|
|
135
|
+
*
|
|
136
|
+
* ```ts
|
|
137
|
+
* interface FetchError {
|
|
138
|
+
* type: "FetchError";
|
|
139
|
+
* message: string;
|
|
140
|
+
* }
|
|
141
|
+
*
|
|
142
|
+
* const controller = new AbortController();
|
|
143
|
+
*
|
|
144
|
+
* const result = await retry(
|
|
145
|
+
* async () =>
|
|
146
|
+
* tryAsync(
|
|
147
|
+
* async () => {
|
|
148
|
+
* const response = await fetch("https://api.example.com/data", {
|
|
149
|
+
* signal: controller.signal,
|
|
150
|
+
* });
|
|
151
|
+
*
|
|
152
|
+
* if (!response.ok) {
|
|
153
|
+
* throw new Error(`HTTP error ${response.status}`);
|
|
154
|
+
* }
|
|
155
|
+
*
|
|
156
|
+
* return await response.json();
|
|
157
|
+
* },
|
|
158
|
+
* (error): FetchError => ({
|
|
159
|
+
* type: "FetchError",
|
|
160
|
+
* message: String(error),
|
|
161
|
+
* }),
|
|
162
|
+
* ),
|
|
163
|
+
* {
|
|
164
|
+
* maxRetries: 3,
|
|
165
|
+
* signal: controller.signal,
|
|
166
|
+
* },
|
|
167
|
+
* );
|
|
168
|
+
* ```
|
|
169
|
+
*
|
|
170
|
+
* ## HTTP Request Recommendations
|
|
171
|
+
*
|
|
172
|
+
* For HTTP requests, configure the `retryable` option to only retry on
|
|
173
|
+
* appropriate errors:
|
|
174
|
+
*
|
|
175
|
+
* - **DO retry**: 429 (Too Many Requests), 503 (Service Unavailable), network
|
|
176
|
+
* errors
|
|
177
|
+
* - **DON'T retry**: 4xx client errors (except 429), most 5xx server errors
|
|
178
|
+
*/
|
|
179
|
+
export const retry = async <T, E>(
|
|
180
|
+
fn: () => Promise<Result<T, E>>,
|
|
181
|
+
options: RetryOptions<E> = {},
|
|
182
|
+
): Promise<Result<T, RetryError<E> | RetryAbortError>> => {
|
|
183
|
+
const {
|
|
184
|
+
maxRetries = 3,
|
|
185
|
+
initialDelay = 100,
|
|
186
|
+
maxDelay = 10000,
|
|
187
|
+
factor = 2,
|
|
188
|
+
jitter = 0.1,
|
|
189
|
+
signal,
|
|
190
|
+
retryable = constTrue,
|
|
191
|
+
onRetry,
|
|
192
|
+
} = options;
|
|
193
|
+
|
|
194
|
+
let attempt = 0;
|
|
195
|
+
|
|
196
|
+
if (signal?.aborted) {
|
|
197
|
+
return err({ type: "RetryAbortError", abortedBeforeExecution: true });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
201
|
+
while (true) {
|
|
202
|
+
const result = await fn();
|
|
203
|
+
|
|
204
|
+
if (result.ok) {
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
attempt += 1;
|
|
209
|
+
|
|
210
|
+
if (attempt > maxRetries || !retryable(result.error)) {
|
|
211
|
+
return err({
|
|
212
|
+
type: "RetryError",
|
|
213
|
+
cause: result.error,
|
|
214
|
+
attempts: attempt,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Calculate delay with exponential backoff
|
|
219
|
+
const exponentialDelay = initialDelay * Math.pow(factor, attempt);
|
|
220
|
+
const cappedDelay = Math.min(exponentialDelay, maxDelay);
|
|
221
|
+
|
|
222
|
+
// Apply jitter to prevent thundering herd problem
|
|
223
|
+
const randomFactor = 1 - jitter + Math.random() * jitter * 2;
|
|
224
|
+
const delay = Math.floor(cappedDelay * randomFactor);
|
|
225
|
+
|
|
226
|
+
if (onRetry) {
|
|
227
|
+
onRetry(result.error, attempt, delay);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (signal?.aborted) {
|
|
231
|
+
return err({ type: "RetryAbortError", abortedBeforeExecution: false });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Wait with abort support
|
|
235
|
+
if (signal) {
|
|
236
|
+
const abortPromise = new Promise<Result<never, RetryAbortError>>(
|
|
237
|
+
(resolve) => {
|
|
238
|
+
const onAbort = () => {
|
|
239
|
+
resolve(
|
|
240
|
+
err({ type: "RetryAbortError", abortedBeforeExecution: false }),
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
244
|
+
},
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const delayPromise = wait(delay);
|
|
248
|
+
const raceResult = await Promise.race([abortPromise, delayPromise]);
|
|
249
|
+
|
|
250
|
+
if (!raceResult.ok) {
|
|
251
|
+
return raceResult;
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
await wait(delay);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
export interface TimeoutError {
|
|
260
|
+
readonly type: "TimeoutError";
|
|
261
|
+
readonly timeoutMs: number;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Wraps an async function with a timeout, returning {@link Result} that fails
|
|
266
|
+
* with {@link TimeoutError} if the timeout is exceeded. The provided function
|
|
267
|
+
* must accept an AbortSignal and return a Result.
|
|
268
|
+
*
|
|
269
|
+
* ### Example
|
|
270
|
+
*
|
|
271
|
+
* ```ts
|
|
272
|
+
* const fetchWithTimeout = () =>
|
|
273
|
+
* withTimeout((signal) => fetch("url", signal), 5000);
|
|
274
|
+
* const result = await retry(fetchWithTimeout, { maxRetries: 3 });
|
|
275
|
+
* ```
|
|
276
|
+
*/
|
|
277
|
+
export const withTimeout = async <T, E>(
|
|
278
|
+
fn: (signal: AbortSignal) => Promise<Result<T, E>>,
|
|
279
|
+
timeoutMs: number,
|
|
280
|
+
): Promise<Result<T, E | TimeoutError>> => {
|
|
281
|
+
const controller = new AbortController();
|
|
282
|
+
const signal = controller.signal;
|
|
283
|
+
const timeoutId = setTimeout(() => {
|
|
284
|
+
controller.abort();
|
|
285
|
+
}, timeoutMs);
|
|
286
|
+
|
|
287
|
+
const result = await fn(signal);
|
|
288
|
+
clearTimeout(timeoutId);
|
|
289
|
+
|
|
290
|
+
if (signal.aborted) {
|
|
291
|
+
return err({ type: "TimeoutError", timeoutMs });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return result;
|
|
295
|
+
};
|
package/src/Random.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🎲
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Random as RandomLib } from "random";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A simple wrapper around Math.random(). Most apps need only this. For more
|
|
11
|
+
* complex needs check {@link RandomLibDep}.
|
|
12
|
+
*
|
|
13
|
+
* ### Example
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* // For apps
|
|
17
|
+
* const random = createRandom();
|
|
18
|
+
* random.next();
|
|
19
|
+
*
|
|
20
|
+
* // For tests
|
|
21
|
+
* const random = createRandomWithSeed("test");
|
|
22
|
+
* random.next();
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export interface Random {
|
|
26
|
+
/** Returns a floating point number in [0, 1). Just like Math.random(). */
|
|
27
|
+
next: () => number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RandomDep {
|
|
31
|
+
random: Random;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Creates a {@link Random} using Math.random(). */
|
|
35
|
+
export const createRandom = (): Random => ({
|
|
36
|
+
next: () => Math.random(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Creates {@link Random} using {@link RandomLibDep} with a seed which is useful
|
|
41
|
+
* for tests.
|
|
42
|
+
*/
|
|
43
|
+
export const createRandomWithSeed = (seed: string): Random => {
|
|
44
|
+
const random = new RandomLib(seed);
|
|
45
|
+
return {
|
|
46
|
+
next: () => random.next(),
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A random number generator using the NPM `random` package dependency.
|
|
52
|
+
*
|
|
53
|
+
* https://github.com/transitive-bullshit/random
|
|
54
|
+
*/
|
|
55
|
+
export interface RandomLibDep {
|
|
56
|
+
random: RandomLib;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Creates a `RandomLib` using the NPM `random` package. */
|
|
60
|
+
export const createRandomLib = (): RandomLib => new RandomLib();
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Creates {@link RandomLibDep} using the NPM `random` package with a seed which
|
|
64
|
+
* is useful for tests.
|
|
65
|
+
*/
|
|
66
|
+
export const createRandomLibWithSeed = (seed: string): RandomLibDep => ({
|
|
67
|
+
random: new RandomLib(seed),
|
|
68
|
+
});
|
package/src/Ref.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A mutable reference for managing state
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import type { Store } from "./Store.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `Ref` provides a simple API to hold and update a value, similar to a "ref" in
|
|
10
|
+
* functional programming or React. It exposes methods to get, set, and modify
|
|
11
|
+
* the current state.
|
|
12
|
+
*
|
|
13
|
+
* Use a Ref instead of a variable when you want to pass state around as an
|
|
14
|
+
* object or update it in a controlled way. If you need subscriptions, see
|
|
15
|
+
* {@link Store}.
|
|
16
|
+
*
|
|
17
|
+
* Updating in a controlled way means all changes go through specific methods
|
|
18
|
+
* (`set` or `modify`), making state updates predictable and easy to track.
|
|
19
|
+
*
|
|
20
|
+
* ### Example
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* const count = createRef(0);
|
|
24
|
+
* count.set(1);
|
|
25
|
+
* count.modify((n) => n + 1);
|
|
26
|
+
* console.log(count.get()); // 2
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* ### Example of using Ref as a dependency
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* interface CounterRefDep {
|
|
33
|
+
* readonly counterRef: Ref<number>;
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export interface Ref<T> {
|
|
38
|
+
/** Returns the current state. */
|
|
39
|
+
readonly get: () => T;
|
|
40
|
+
|
|
41
|
+
/** Sets the state. */
|
|
42
|
+
readonly set: (state: T) => void;
|
|
43
|
+
|
|
44
|
+
/** Modifies the state using an updater function. */
|
|
45
|
+
readonly modify: (updater: (current: T) => T) => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Creates a {@link Ref} with the given initial state. */
|
|
49
|
+
export const createRef = <T>(initialState: T): Ref<T> => {
|
|
50
|
+
let currentState = initialState;
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
get: () => currentState,
|
|
54
|
+
|
|
55
|
+
set: (state) => {
|
|
56
|
+
currentState = state;
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
modify: (updater) => {
|
|
60
|
+
currentState = updater(currentState);
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
};
|