@evolu/common 8.0.0-next.4 → 8.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/dist/src/Array.d.ts +279 -173
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +105 -101
- package/dist/src/Assert.d.ts +44 -44
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +44 -32
- package/dist/src/BigInt.d.ts +6 -3
- package/dist/src/BigInt.d.ts.map +1 -1
- package/dist/src/BigInt.js +5 -2
- package/dist/src/Brand.d.ts +27 -40
- package/dist/src/Brand.d.ts.map +1 -1
- package/dist/src/Buffer.d.ts +27 -11
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Cache.d.ts +9 -4
- package/dist/src/Cache.d.ts.map +1 -1
- package/dist/src/Cache.js +8 -3
- package/dist/src/Callbacks.d.ts +30 -19
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Console.d.ts +145 -66
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +91 -43
- package/dist/src/Crypto.d.ts +85 -42
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +44 -20
- package/dist/src/Eq.d.ts +87 -21
- package/dist/src/Eq.d.ts.map +1 -1
- package/dist/src/Eq.js +67 -19
- package/dist/src/Error.d.ts +5 -3
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +16 -0
- package/dist/src/Function.d.ts +119 -58
- package/dist/src/Function.d.ts.map +1 -1
- package/dist/src/Function.js +86 -43
- package/dist/src/Http.d.ts +394 -0
- package/dist/src/Http.d.ts.map +1 -0
- package/dist/src/Http.js +102 -0
- package/dist/src/Identicon.d.ts +16 -7
- package/dist/src/Identicon.d.ts.map +1 -1
- package/dist/src/Identicon.js +14 -5
- package/dist/src/LeakDetector.d.ts +88 -0
- package/dist/src/LeakDetector.d.ts.map +1 -0
- package/dist/src/LeakDetector.js +86 -0
- package/dist/src/LockManager.d.ts +18 -9
- package/dist/src/LockManager.d.ts.map +1 -1
- package/dist/src/LockManager.js +7 -8
- package/dist/src/Lookup.d.ts +43 -13
- package/dist/src/Lookup.d.ts.map +1 -1
- package/dist/src/Lookup.js +28 -8
- package/dist/src/Microtask.d.ts +1 -1
- package/dist/src/Microtask.d.ts.map +1 -1
- package/dist/src/Microtask.js +3 -7
- package/dist/src/Number.d.ts +49 -13
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +34 -5
- package/dist/src/Object.d.ts +160 -40
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +92 -46
- package/dist/src/Option.d.ts +10 -7
- package/dist/src/Option.d.ts.map +1 -1
- package/dist/src/Option.js +9 -6
- package/dist/src/Order.d.ts +21 -21
- package/dist/src/Order.d.ts.map +1 -1
- package/dist/src/Order.js +18 -21
- package/dist/src/Platform.d.ts +31 -0
- package/dist/src/Platform.d.ts.map +1 -1
- package/dist/src/Platform.js +98 -0
- package/dist/src/Random.d.ts +32 -17
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Random.js +12 -8
- package/dist/src/Redacted.d.ts +43 -29
- package/dist/src/Redacted.d.ts.map +1 -1
- package/dist/src/Redacted.js +14 -3
- package/dist/src/Ref.d.ts +11 -3
- package/dist/src/Ref.d.ts.map +1 -1
- package/dist/src/Ref.js +2 -2
- package/dist/src/RefCount.d.ts +2 -6
- package/dist/src/RefCount.d.ts.map +1 -1
- package/dist/src/RefCount.js +33 -90
- package/dist/src/Relation.d.ts +70 -5
- package/dist/src/Relation.d.ts.map +1 -1
- package/dist/src/Relation.js +77 -2
- package/dist/src/Resource.d.ts +590 -155
- package/dist/src/Resource.d.ts.map +1 -1
- package/dist/src/Resource.js +592 -343
- package/dist/src/Result.d.ts +838 -191
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +163 -102
- package/dist/src/Schedule.d.ts +684 -231
- package/dist/src/Schedule.d.ts.map +1 -1
- package/dist/src/Schedule.js +766 -295
- package/dist/src/Set.d.ts +89 -40
- package/dist/src/Set.d.ts.map +1 -1
- package/dist/src/Set.js +50 -29
- package/dist/src/Sqlite.d.ts +29 -72
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +41 -46
- package/dist/src/StackTrace.d.ts +40 -0
- package/dist/src/StackTrace.d.ts.map +1 -0
- package/dist/src/StackTrace.js +140 -0
- package/dist/src/Store.d.ts +5 -4
- package/dist/src/Store.d.ts.map +1 -1
- package/dist/src/Store.js +5 -4
- package/dist/src/String.d.ts +7 -1
- package/dist/src/String.d.ts.map +1 -1
- package/dist/src/String.js +20 -7
- package/dist/src/Task.d.ts +3572 -1770
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +1879 -1274
- package/dist/src/Test.d.ts +17 -125
- package/dist/src/Test.d.ts.map +1 -1
- package/dist/src/Test.js +17 -68
- package/dist/src/Time.d.ts +108 -49
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +172 -73
- package/dist/src/Type.d.ts +3542 -1926
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +3070 -1784
- package/dist/src/Types.d.ts +200 -108
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/Types.js +39 -8
- package/dist/src/WebSocket.d.ts +48 -23
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +15 -6
- package/dist/src/Worker.d.ts +94 -36
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +58 -24
- package/dist/src/index.d.ts +55 -52
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -1
- package/dist/src/intl/cs.d.ts +94 -0
- package/dist/src/intl/cs.d.ts.map +1 -0
- package/dist/src/intl/cs.js +220 -0
- package/dist/src/intl/index.d.ts +7 -0
- package/dist/src/intl/index.d.ts.map +1 -0
- package/dist/src/intl/index.js +6 -0
- package/dist/src/local-first/Db.d.ts +12 -8
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +109 -132
- package/dist/src/local-first/Error.d.ts +4 -4
- package/dist/src/local-first/Evolu.d.ts +338 -92
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +47 -55
- package/dist/src/local-first/LocalAuth.d.ts +4 -4
- package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
- package/dist/src/local-first/Owner.d.ts +61 -24
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +45 -8
- package/dist/src/local-first/Protocol.d.ts +44 -31
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +36 -13
- package/dist/src/local-first/Query.d.ts +147 -62
- package/dist/src/local-first/Query.d.ts.map +1 -1
- package/dist/src/local-first/Query.js +112 -58
- package/dist/src/local-first/Relay.d.ts +44 -14
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Schema.d.ts +92 -41
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +25 -8
- package/dist/src/local-first/Shared.d.ts +23 -21
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +100 -57
- package/dist/src/local-first/Storage.d.ts +53 -32
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +194 -234
- package/dist/src/local-first/Timestamp.d.ts +21 -21
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +2 -2
- package/dist/src/local-first/index.d.ts +11 -11
- package/package.json +14 -13
- package/src/Array.ts +306 -205
- package/src/Assert.ts +54 -69
- package/src/BigInt.ts +6 -3
- package/src/Brand.ts +27 -40
- package/src/Buffer.ts +28 -12
- package/src/Cache.ts +9 -4
- package/src/Callbacks.ts +30 -19
- package/src/Console.ts +156 -84
- package/src/Crypto.ts +83 -41
- package/src/Eq.ts +95 -22
- package/src/Error.ts +8 -4
- package/src/Function.ts +167 -59
- package/src/Http.ts +549 -0
- package/src/Identicon.ts +16 -7
- package/src/LeakDetector.ts +182 -0
- package/src/LockManager.ts +43 -39
- package/src/Lookup.ts +62 -28
- package/src/Microtask.ts +19 -23
- package/src/Number.ts +64 -12
- package/src/Object.ts +173 -49
- package/src/Option.ts +10 -7
- package/src/Order.ts +23 -21
- package/src/Platform.ts +136 -0
- package/src/Polyfills.ts +6 -12
- package/src/Random.ts +32 -19
- package/src/Redacted.ts +44 -30
- package/src/Ref.ts +14 -6
- package/src/RefCount.ts +38 -80
- package/src/Relation.ts +202 -10
- package/src/Resource.ts +1199 -591
- package/src/Result.ts +930 -271
- package/src/Schedule.ts +859 -343
- package/src/Set.ts +89 -40
- package/src/Sqlite.ts +144 -138
- package/src/StackTrace.ts +230 -0
- package/src/Store.ts +8 -7
- package/src/String.ts +22 -5
- package/src/Task.ts +5101 -2832
- package/src/Test.ts +20 -168
- package/src/Time.ts +293 -90
- package/src/Type.ts +10034 -4117
- package/src/Types.ts +227 -110
- package/src/WebSocket.ts +64 -36
- package/src/Worker.ts +180 -72
- package/src/index.ts +55 -52
- package/src/intl/cs.ts +388 -0
- package/src/intl/index.ts +7 -0
- package/src/local-first/Db.ts +152 -179
- package/src/local-first/Error.ts +4 -4
- package/src/local-first/Evolu.ts +495 -250
- package/src/local-first/LocalAuth.ts +6 -12
- package/src/local-first/Owner.ts +58 -21
- package/src/local-first/Protocol.ts +49 -31
- package/src/local-first/Query.ts +146 -61
- package/src/local-first/Relay.ts +51 -21
- package/src/local-first/Schema.ts +109 -51
- package/src/local-first/Shared.ts +266 -226
- package/src/local-first/Storage.ts +266 -264
- package/src/local-first/Timestamp.ts +25 -18
- package/src/local-first/index.ts +11 -11
- package/dist/src/Skiplist.d.ts +0 -28
- package/dist/src/Skiplist.d.ts.map +0 -1
- package/dist/src/Skiplist.js +0 -64
- package/dist/src/Tracer.d.ts +0 -48
- package/dist/src/Tracer.d.ts.map +0 -1
- package/dist/src/Tracer.js +0 -6
- package/src/Skiplist.ts +0 -108
- package/src/Tracer.ts +0 -54
package/src/Random.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Random number generation.
|
|
3
3
|
*
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Random as RandomLib } from "random";
|
|
8
|
-
import type { Brand } from "./Brand.
|
|
8
|
+
import type { Brand } from "./Brand.ts";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* A random floating point number in [0, 1).
|
|
@@ -22,22 +22,32 @@ export type RandomNumber = number & Brand<"RandomNumber">;
|
|
|
22
22
|
* ### Example
|
|
23
23
|
*
|
|
24
24
|
* ```ts
|
|
25
|
-
*
|
|
25
|
+
* import {
|
|
26
|
+
* createRandom,
|
|
27
|
+
* testCreateRandom,
|
|
28
|
+
* type RandomNumber,
|
|
29
|
+
* } from "@evolu/common";
|
|
30
|
+
*
|
|
31
|
+
* // For apps, use the Math.random-backed implementation.
|
|
26
32
|
* const random = createRandom();
|
|
27
|
-
* random.next();
|
|
33
|
+
* const value = random.next();
|
|
34
|
+
* expectTypeOf(value).toEqualTypeOf<RandomNumber>();
|
|
35
|
+
* expect(value).toBeGreaterThanOrEqual(0);
|
|
36
|
+
* expect(value).toBeLessThan(1);
|
|
28
37
|
*
|
|
29
|
-
* // For tests
|
|
30
|
-
* const
|
|
31
|
-
*
|
|
38
|
+
* // For tests, use a seed.
|
|
39
|
+
* const firstTestRandom = testCreateRandom("test");
|
|
40
|
+
* const secondTestRandom = testCreateRandom("test");
|
|
41
|
+
* expect(firstTestRandom.next()).toBe(secondTestRandom.next());
|
|
32
42
|
* ```
|
|
33
43
|
*/
|
|
34
44
|
export interface Random {
|
|
35
45
|
/** Returns a floating point number in [0, 1). Just like Math.random(). */
|
|
36
|
-
next: () => RandomNumber;
|
|
46
|
+
readonly next: () => RandomNumber;
|
|
37
47
|
}
|
|
38
48
|
|
|
39
49
|
export interface RandomDep {
|
|
40
|
-
random: Random;
|
|
50
|
+
readonly random: Random;
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
/** Creates a {@link Random} using Math.random(). */
|
|
@@ -45,15 +55,12 @@ export const createRandom = (): Random => ({
|
|
|
45
55
|
next: () => Math.random() as RandomNumber,
|
|
46
56
|
});
|
|
47
57
|
|
|
48
|
-
/** Creates a seeded {@link Random} for deterministic tests. Default seed "evolu". */
|
|
49
|
-
export const testCreateRandom = (seed = "evolu"): Random =>
|
|
50
|
-
createRandomWithSeed(seed);
|
|
51
|
-
|
|
52
58
|
/**
|
|
53
|
-
* Creates
|
|
54
|
-
*
|
|
59
|
+
* Creates a seeded {@link Random} for deterministic tests.
|
|
60
|
+
*
|
|
61
|
+
* Default seed "evolu".
|
|
55
62
|
*/
|
|
56
|
-
export const
|
|
63
|
+
export const testCreateRandom = (seed = "evolu"): Random => {
|
|
57
64
|
const random = new RandomLib(seed);
|
|
58
65
|
return {
|
|
59
66
|
next: () => random.next() as RandomNumber,
|
|
@@ -61,7 +68,8 @@ export const createRandomWithSeed = (seed: string): Random => {
|
|
|
61
68
|
};
|
|
62
69
|
|
|
63
70
|
/**
|
|
64
|
-
*
|
|
71
|
+
* Seedable random number generator supporting many common distributions,
|
|
72
|
+
* provided by the NPM `random` package.
|
|
65
73
|
*
|
|
66
74
|
* https://github.com/transitive-bullshit/random
|
|
67
75
|
*/
|
|
@@ -69,9 +77,14 @@ export interface RandomLibDep {
|
|
|
69
77
|
readonly randomLib: RandomLib;
|
|
70
78
|
}
|
|
71
79
|
|
|
72
|
-
/** Creates a
|
|
80
|
+
/** Creates a random number generator from the NPM `random` package. */
|
|
73
81
|
export const createRandomLib = (): RandomLib => new RandomLib();
|
|
74
82
|
|
|
75
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* Creates a seeded random number generator from the NPM `random` package for
|
|
85
|
+
* deterministic tests.
|
|
86
|
+
*
|
|
87
|
+
* Default seed "evolu".
|
|
88
|
+
*/
|
|
76
89
|
export const testCreateRandomLib = (seed = "evolu"): RandomLib =>
|
|
77
90
|
new RandomLib(seed);
|
package/src/Redacted.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { assert } from "./Assert.
|
|
8
|
-
import type { Brand } from "./Brand.
|
|
9
|
-
import type { Eq } from "./Eq.
|
|
7
|
+
import { assert } from "./Assert.ts";
|
|
8
|
+
import type { Brand } from "./Brand.ts";
|
|
9
|
+
import type { Eq } from "./Eq.ts";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* A wrapper type that prevents sensitive values from being accidentally exposed
|
|
@@ -29,35 +29,38 @@ import type { Eq } from "./Eq.js";
|
|
|
29
29
|
* ### Example
|
|
30
30
|
*
|
|
31
31
|
* ```ts
|
|
32
|
-
*
|
|
32
|
+
* import {
|
|
33
|
+
* createRedacted,
|
|
34
|
+
* revealRedacted,
|
|
35
|
+
* type Brand,
|
|
36
|
+
* type Redacted,
|
|
37
|
+
* } from "@evolu/common";
|
|
38
|
+
*
|
|
33
39
|
* type ApiKey = string & Brand<"ApiKey">;
|
|
34
40
|
* type DbPassword = string & Brand<"DbPassword">;
|
|
35
|
-
*
|
|
36
|
-
* // Wrap them with Redacted for safe passing
|
|
37
41
|
* type RedactedApiKey = Redacted<ApiKey>;
|
|
38
|
-
* type RedactedDbPassword = Redacted<DbPassword>;
|
|
39
42
|
*
|
|
40
|
-
* //
|
|
43
|
+
* // Apply brands only after validation or at another trusted boundary.
|
|
41
44
|
* const apiKey: ApiKey = "secret-123" as ApiKey;
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* fetchUser(
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
45
|
+
* using redactedKey: RedactedApiKey = createRedacted(apiKey);
|
|
46
|
+
* const fetchUser = (key: RedactedApiKey): ApiKey => revealRedacted(key);
|
|
47
|
+
*
|
|
48
|
+
* expect(redactedKey.toString()).toBe("<redacted>");
|
|
49
|
+
* expect(JSON.stringify({ apiKey: redactedKey })).toBe(
|
|
50
|
+
* '{"apiKey":"<redacted>"}',
|
|
51
|
+
* );
|
|
52
|
+
* expect(fetchUser(redactedKey)).toBe(apiKey);
|
|
53
|
+
*
|
|
54
|
+
* using password = createRedacted("password" as DbPassword);
|
|
55
|
+
* // @ts-expect-error Redacted secrets retain their distinct branded types.
|
|
56
|
+
* fetchUser(password);
|
|
57
|
+
*
|
|
58
|
+
* const disposedKey = (() => {
|
|
59
|
+
* using key = createRedacted(apiKey);
|
|
60
|
+
* return key;
|
|
61
|
+
* })();
|
|
62
|
+
* // Leaving the `using` scope removes the value from memory.
|
|
63
|
+
* expect(() => revealRedacted(disposedKey)).toThrow();
|
|
61
64
|
* ```
|
|
62
65
|
*/
|
|
63
66
|
export interface Redacted<A> extends Brand<"Redacted">, Disposable {
|
|
@@ -108,12 +111,23 @@ export const isRedacted = (value: unknown): value is Redacted<unknown> =>
|
|
|
108
111
|
* ### Example
|
|
109
112
|
*
|
|
110
113
|
* ```ts
|
|
114
|
+
* import {
|
|
115
|
+
* createEqRedacted,
|
|
116
|
+
* createRedacted,
|
|
117
|
+
* eqString,
|
|
118
|
+
* type Brand,
|
|
119
|
+
* } from "@evolu/common";
|
|
120
|
+
*
|
|
111
121
|
* type ApiKey = string & Brand<"ApiKey">;
|
|
112
122
|
* const eqRedactedApiKey = createEqRedacted<ApiKey>(eqString);
|
|
113
123
|
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
124
|
+
* // Apply brands only after validation or at another trusted boundary.
|
|
125
|
+
* using a = createRedacted("x" as ApiKey);
|
|
126
|
+
* using b = createRedacted("x" as ApiKey);
|
|
127
|
+
* using c = createRedacted("y" as ApiKey);
|
|
128
|
+
*
|
|
129
|
+
* expect(eqRedactedApiKey(a, b)).toBe(true);
|
|
130
|
+
* expect(eqRedactedApiKey(a, c)).toBe(false);
|
|
117
131
|
* ```
|
|
118
132
|
*/
|
|
119
133
|
export const createEqRedacted =
|
package/src/Ref.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { Store } from "./Store.
|
|
7
|
+
import type { Store } from "./Store.ts";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Mutable reference to an immutable value.
|
|
@@ -21,18 +21,26 @@ import type { Store } from "./Store.js";
|
|
|
21
21
|
* ### Example
|
|
22
22
|
*
|
|
23
23
|
* ```ts
|
|
24
|
+
* import { createRef } from "@evolu/common";
|
|
25
|
+
*
|
|
24
26
|
* const count = createRef(0);
|
|
25
27
|
* count.set(1);
|
|
26
28
|
* count.update((n) => n + 1);
|
|
27
|
-
*
|
|
29
|
+
* expect(count.get()).toBe(2);
|
|
28
30
|
* ```
|
|
29
31
|
*
|
|
30
|
-
* ###
|
|
32
|
+
* ### As a dependency
|
|
31
33
|
*
|
|
32
34
|
* ```ts
|
|
35
|
+
* import { createRef, type Ref } from "@evolu/common";
|
|
36
|
+
*
|
|
33
37
|
* interface CounterRefDep {
|
|
34
38
|
* readonly counterRef: Ref<number>;
|
|
35
39
|
* }
|
|
40
|
+
*
|
|
41
|
+
* const deps: CounterRefDep = { counterRef: createRef(0) };
|
|
42
|
+
* deps.counterRef.update((count) => count + 1);
|
|
43
|
+
* expect(deps.counterRef.get()).toBe(1);
|
|
36
44
|
* ```
|
|
37
45
|
*/
|
|
38
46
|
export interface Ref<T> {
|
|
@@ -59,7 +67,7 @@ export interface Ref<T> {
|
|
|
59
67
|
|
|
60
68
|
/** Modifies the current value and returns a computed result. */
|
|
61
69
|
readonly modify: <R>(
|
|
62
|
-
|
|
70
|
+
modifier: (current: T) => readonly [result: R, nextValue: T],
|
|
63
71
|
) => R;
|
|
64
72
|
}
|
|
65
73
|
|
|
@@ -100,8 +108,8 @@ export const createRef = <T>(initialValue: T): Ref<T> => {
|
|
|
100
108
|
return currentValue;
|
|
101
109
|
},
|
|
102
110
|
|
|
103
|
-
modify: (
|
|
104
|
-
const [result, nextValue] =
|
|
111
|
+
modify: (modifier) => {
|
|
112
|
+
const [result, nextValue] = modifier(currentValue);
|
|
105
113
|
currentValue = nextValue;
|
|
106
114
|
return result;
|
|
107
115
|
},
|
package/src/RefCount.ts
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { assert
|
|
8
|
-
import { identity } from "./Function.
|
|
9
|
-
import { createLookupMap, type Lookup, type LookupOption } from "./Lookup.
|
|
10
|
-
import { NonNegativeInt, PositiveInt, zeroNonNegativeInt } from "./Type.
|
|
7
|
+
import { assert } from "./Assert.ts";
|
|
8
|
+
import { disposable, identity } from "./Function.ts";
|
|
9
|
+
import { createLookupMap, type Lookup, type LookupOption } from "./Lookup.ts";
|
|
10
|
+
import { NonNegativeInt, PositiveInt, zeroNonNegativeInt } from "./Type.ts";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Reference count for one retained value.
|
|
@@ -27,40 +27,27 @@ export interface RefCount extends Disposable {
|
|
|
27
27
|
|
|
28
28
|
/** Returns the current count. */
|
|
29
29
|
readonly getCount: () => NonNegativeInt;
|
|
30
|
-
|
|
31
|
-
/** Disposes and invalidates the helper. Further method calls throw. */
|
|
32
|
-
readonly [Symbol.dispose]: () => void;
|
|
33
30
|
}
|
|
34
31
|
|
|
35
32
|
/** Creates {@link RefCount}. */
|
|
36
33
|
export const createRefCount = (): RefCount => {
|
|
37
|
-
// We use DisposableStack only because of assertNotDisposed.
|
|
38
|
-
using disposer = new DisposableStack();
|
|
39
34
|
let count = zeroNonNegativeInt;
|
|
40
|
-
const disposables = disposer.move();
|
|
41
35
|
|
|
42
|
-
return {
|
|
36
|
+
return disposable<RefCount>({
|
|
43
37
|
increment: () => {
|
|
44
|
-
assertNotDisposed(disposables);
|
|
45
38
|
const nextCount = PositiveInt.orThrow(count + 1);
|
|
46
39
|
count = nextCount;
|
|
47
40
|
return nextCount;
|
|
48
41
|
},
|
|
49
42
|
|
|
50
43
|
decrement: () => {
|
|
51
|
-
assertNotDisposed(disposables);
|
|
52
44
|
assert(count > 0, "RefCount must not be decremented below zero.");
|
|
53
45
|
count = NonNegativeInt.orThrow(count - 1);
|
|
54
46
|
return count;
|
|
55
47
|
},
|
|
56
48
|
|
|
57
|
-
getCount: () =>
|
|
58
|
-
|
|
59
|
-
return count;
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
[Symbol.dispose]: () => disposables.dispose(),
|
|
63
|
-
};
|
|
49
|
+
getCount: () => count,
|
|
50
|
+
});
|
|
64
51
|
};
|
|
65
52
|
|
|
66
53
|
/**
|
|
@@ -91,9 +78,6 @@ export interface RefCountByKey<TKey> extends Disposable {
|
|
|
91
78
|
|
|
92
79
|
/** Returns all currently tracked keys. */
|
|
93
80
|
readonly keys: () => ReadonlySet<TKey>;
|
|
94
|
-
|
|
95
|
-
/** Disposes and invalidates the helper. Further method calls throw. */
|
|
96
|
-
readonly [Symbol.dispose]: () => void;
|
|
97
81
|
}
|
|
98
82
|
|
|
99
83
|
/** Options for {@link createRefCountByKey}. */
|
|
@@ -110,65 +94,39 @@ export function createRefCountByKey<TKey, L>(
|
|
|
110
94
|
export function createRefCountByKey<TKey, L = TKey>({
|
|
111
95
|
lookup = identity as Lookup<TKey, L>,
|
|
112
96
|
}: CreateRefCountByKeyOptions<TKey, L> = {}): RefCountByKey<TKey> {
|
|
113
|
-
// We use DisposableStack only because of assertNotDisposed.
|
|
114
97
|
using disposer = new DisposableStack();
|
|
115
98
|
|
|
116
|
-
const refCountByKey =
|
|
117
|
-
|
|
118
|
-
(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
99
|
+
const refCountByKey = createLookupMap<TKey, RefCount, L>({ lookup });
|
|
100
|
+
disposer.defer(() => {
|
|
101
|
+
refCountByKey.clear();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return disposable<RefCountByKey<TKey>>(
|
|
105
|
+
{
|
|
106
|
+
increment: (key) =>
|
|
107
|
+
refCountByKey.getOrInsertComputed(key, createRefCount).increment(),
|
|
108
|
+
|
|
109
|
+
decrement: (key) => {
|
|
110
|
+
const refCount = refCountByKey.get(key);
|
|
111
|
+
assert(
|
|
112
|
+
refCount,
|
|
113
|
+
"RefCount must not be decremented for an untracked key.",
|
|
114
|
+
);
|
|
115
|
+
const nextCount = refCount.decrement();
|
|
116
|
+
if (nextCount === 0) {
|
|
117
|
+
refCountByKey.delete(key);
|
|
118
|
+
refCount[Symbol.dispose]();
|
|
119
|
+
}
|
|
120
|
+
return nextCount;
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
getCount: (key) =>
|
|
124
|
+
refCountByKey.get(key)?.getCount() ?? zeroNonNegativeInt,
|
|
125
|
+
|
|
126
|
+
has: (key) => refCountByKey.has(key),
|
|
127
|
+
|
|
128
|
+
keys: () => new Set(refCountByKey.keys()),
|
|
126
129
|
},
|
|
130
|
+
disposer,
|
|
127
131
|
);
|
|
128
|
-
|
|
129
|
-
const disposables = disposer.move();
|
|
130
|
-
|
|
131
|
-
const getRefCount = (key: TKey): RefCount => {
|
|
132
|
-
let refCount = refCountByKey.get(key);
|
|
133
|
-
if (!refCount) {
|
|
134
|
-
refCount = createRefCount();
|
|
135
|
-
refCountByKey.set(key, refCount);
|
|
136
|
-
}
|
|
137
|
-
return refCount;
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
return {
|
|
141
|
-
increment: (key) => {
|
|
142
|
-
assertNotDisposed(disposables);
|
|
143
|
-
return getRefCount(key).increment();
|
|
144
|
-
},
|
|
145
|
-
|
|
146
|
-
decrement: (key) => {
|
|
147
|
-
assertNotDisposed(disposables);
|
|
148
|
-
const refCount = getRefCount(key);
|
|
149
|
-
const nextCount = refCount.decrement();
|
|
150
|
-
if (nextCount === 0) {
|
|
151
|
-
refCountByKey.delete(key);
|
|
152
|
-
refCount[Symbol.dispose]();
|
|
153
|
-
}
|
|
154
|
-
return nextCount;
|
|
155
|
-
},
|
|
156
|
-
|
|
157
|
-
getCount: (key) => {
|
|
158
|
-
assertNotDisposed(disposables);
|
|
159
|
-
return refCountByKey.get(key)?.getCount() ?? zeroNonNegativeInt;
|
|
160
|
-
},
|
|
161
|
-
|
|
162
|
-
has: (key) => {
|
|
163
|
-
assertNotDisposed(disposables);
|
|
164
|
-
return refCountByKey.has(key);
|
|
165
|
-
},
|
|
166
|
-
|
|
167
|
-
keys: () => {
|
|
168
|
-
assertNotDisposed(disposables);
|
|
169
|
-
return new Set(refCountByKey.keys());
|
|
170
|
-
},
|
|
171
|
-
|
|
172
|
-
[Symbol.dispose]: () => disposables.dispose(),
|
|
173
|
-
};
|
|
174
132
|
}
|
package/src/Relation.ts
CHANGED
|
@@ -4,15 +4,22 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { emptyArray } from "./Array.
|
|
8
|
-
import { assert } from "./Assert.
|
|
9
|
-
import { identity } from "./Function.
|
|
7
|
+
import { emptyArray } from "./Array.ts";
|
|
8
|
+
import { assert } from "./Assert.ts";
|
|
9
|
+
import { identity } from "./Function.ts";
|
|
10
10
|
import {
|
|
11
11
|
createLookupMap,
|
|
12
12
|
createLookupSet,
|
|
13
13
|
type Lookup,
|
|
14
|
+
type LookupMap,
|
|
14
15
|
type LookupSet,
|
|
15
|
-
} from "./Lookup.
|
|
16
|
+
} from "./Lookup.ts";
|
|
17
|
+
import {
|
|
18
|
+
NonNegativeInt,
|
|
19
|
+
onePositiveInt,
|
|
20
|
+
PositiveInt,
|
|
21
|
+
zeroNonNegativeInt,
|
|
22
|
+
} from "./Type.ts";
|
|
16
23
|
|
|
17
24
|
/**
|
|
18
25
|
* Bidirectional relation between two types.
|
|
@@ -44,20 +51,33 @@ import {
|
|
|
44
51
|
* The input parameter types of `lookupA` and `lookupB` determine which values
|
|
45
52
|
* the returned relation accepts on each side.
|
|
46
53
|
*
|
|
47
|
-
* ###
|
|
54
|
+
* ### Reference identity
|
|
48
55
|
*
|
|
49
56
|
* Use the default identity semantics.
|
|
50
57
|
*
|
|
51
58
|
* ```ts
|
|
52
|
-
*
|
|
59
|
+
* import { createRelation } from "@evolu/common";
|
|
60
|
+
*
|
|
61
|
+
* interface Socket {
|
|
62
|
+
* readonly id: string;
|
|
63
|
+
* }
|
|
64
|
+
*
|
|
65
|
+
* const relation = createRelation<Socket, string>();
|
|
66
|
+
* const socket = { id: "socket-1" };
|
|
67
|
+
*
|
|
53
68
|
* relation.add(socket, "owner-1");
|
|
69
|
+
*
|
|
70
|
+
* expect(relation.has(socket, "owner-1")).toBe(true);
|
|
71
|
+
* expect(relation.has({ id: "socket-1" }, "owner-1")).toBe(false);
|
|
54
72
|
* ```
|
|
55
73
|
*
|
|
56
|
-
* ###
|
|
74
|
+
* ### Lookup-derived equality
|
|
57
75
|
*
|
|
58
76
|
* Use lookup-derived equality.
|
|
59
77
|
*
|
|
60
78
|
* ```ts
|
|
79
|
+
* import { createRelation } from "@evolu/common";
|
|
80
|
+
*
|
|
61
81
|
* interface Person {
|
|
62
82
|
* readonly id: string;
|
|
63
83
|
* readonly name: string;
|
|
@@ -69,7 +89,10 @@ import {
|
|
|
69
89
|
* });
|
|
70
90
|
*
|
|
71
91
|
* relation.add({ id: "1", name: "Ada" }, { id: "admins" });
|
|
72
|
-
*
|
|
92
|
+
*
|
|
93
|
+
* expect(relation.has({ id: "1", name: "Grace" }, { id: "admins" })).toBe(
|
|
94
|
+
* true,
|
|
95
|
+
* );
|
|
73
96
|
* ```
|
|
74
97
|
*
|
|
75
98
|
* @see {@link createRelation}
|
|
@@ -171,7 +194,6 @@ export function createRelation<A, B, LA = A, LB = B>({
|
|
|
171
194
|
|
|
172
195
|
const removePair = (a: A, b: B): void => {
|
|
173
196
|
const relatedB = bByA.get(a);
|
|
174
|
-
// This should only fail if a leaked view was mutated via an unsafe cast.
|
|
175
197
|
assertRelationMappingConsistency(relatedB);
|
|
176
198
|
assertRelationMappingConsistency(relatedB.has(b));
|
|
177
199
|
|
|
@@ -181,7 +203,6 @@ export function createRelation<A, B, LA = A, LB = B>({
|
|
|
181
203
|
}
|
|
182
204
|
|
|
183
205
|
const relatedA = aByB.get(b);
|
|
184
|
-
// This should only fail if a leaked view was mutated via an unsafe cast.
|
|
185
206
|
assertRelationMappingConsistency(relatedA);
|
|
186
207
|
assertRelationMappingConsistency(relatedA.has(a));
|
|
187
208
|
|
|
@@ -279,8 +300,179 @@ export function createRelation<A, B, LA = A, LB = B>({
|
|
|
279
300
|
};
|
|
280
301
|
}
|
|
281
302
|
|
|
303
|
+
// This can fail if a lookup function violates its pure and stable contract.
|
|
282
304
|
const assertRelationMappingConsistency: (
|
|
283
305
|
condition: unknown,
|
|
284
306
|
) => asserts condition = (condition) => {
|
|
285
307
|
assert(condition, "Relation mapping inconsistency");
|
|
286
308
|
};
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Bidirectional relation with a positive retain count for every active pair.
|
|
312
|
+
*
|
|
313
|
+
* Logical equality and canonical representatives follow the same lookup rules
|
|
314
|
+
* as {@link Relation}. The first active representative on each side remains
|
|
315
|
+
* canonical until that value has no active pairs. Increment and decrement
|
|
316
|
+
* return those representatives so callers can reliably handle pair transitions
|
|
317
|
+
* without repeating lookup or canonicalization logic.
|
|
318
|
+
*
|
|
319
|
+
* Unlike {@link Relation}'s live iterators, directional and entry reads return
|
|
320
|
+
* snapshots so callers can mutate this relation while iterating. Their order
|
|
321
|
+
* follows first insertion of the current canonical representatives and pairs.
|
|
322
|
+
*/
|
|
323
|
+
export interface RefCountedRelation<A, B> {
|
|
324
|
+
/** Increments a pair count and returns its canonical values and new count. */
|
|
325
|
+
readonly increment: (
|
|
326
|
+
a: A,
|
|
327
|
+
b: B,
|
|
328
|
+
) => {
|
|
329
|
+
readonly a: A;
|
|
330
|
+
readonly b: B;
|
|
331
|
+
readonly count: PositiveInt;
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Decrements a pair count and returns its canonical values and new count.
|
|
336
|
+
* Decrementing a missing pair is a programmer error checked with
|
|
337
|
+
* {@link assert}.
|
|
338
|
+
*/
|
|
339
|
+
readonly decrement: (
|
|
340
|
+
a: A,
|
|
341
|
+
b: B,
|
|
342
|
+
) => {
|
|
343
|
+
readonly a: A;
|
|
344
|
+
readonly b: B;
|
|
345
|
+
readonly count: NonNegativeInt;
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
/** Returns the pair count, or zero when the pair is absent. */
|
|
349
|
+
readonly getCount: (a: A, b: B) => NonNegativeInt;
|
|
350
|
+
|
|
351
|
+
/** Returns a snapshot of canonical A values related to `b`. */
|
|
352
|
+
readonly getAs: (b: B) => ReadonlyArray<A>;
|
|
353
|
+
|
|
354
|
+
/** Returns a snapshot of canonical B values related to `a`. */
|
|
355
|
+
readonly getBs: (a: A) => ReadonlyArray<B>;
|
|
356
|
+
|
|
357
|
+
/** Returns whether an A value has at least one active pair. */
|
|
358
|
+
readonly hasA: (a: A) => boolean;
|
|
359
|
+
|
|
360
|
+
/** Returns whether a B value has at least one active pair. */
|
|
361
|
+
readonly hasB: (b: B) => boolean;
|
|
362
|
+
|
|
363
|
+
/** Returns a snapshot of every canonical pair and its positive count. */
|
|
364
|
+
readonly getEntries: () => ReadonlyArray<
|
|
365
|
+
readonly [a: A, b: B, count: PositiveInt]
|
|
366
|
+
>;
|
|
367
|
+
|
|
368
|
+
/** Removes all pair counts and both directional indexes. */
|
|
369
|
+
readonly clear: () => void;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Creates a {@link RefCountedRelation}. */
|
|
373
|
+
export function createRefCountedRelation<A, B>(): RefCountedRelation<A, B>;
|
|
374
|
+
export function createRefCountedRelation<A, B, LA, LB>(
|
|
375
|
+
options: CreateRelationOptions<A, B, LA, LB>,
|
|
376
|
+
): RefCountedRelation<A, B>;
|
|
377
|
+
export function createRefCountedRelation<A, B, LA = A, LB = B>({
|
|
378
|
+
lookupA = identity as Lookup<A, LA>,
|
|
379
|
+
lookupB = identity as Lookup<B, LB>,
|
|
380
|
+
}: CreateRelationOptions<A, B, LA, LB> = {}): RefCountedRelation<A, B> {
|
|
381
|
+
interface RelatedBs {
|
|
382
|
+
readonly a: A;
|
|
383
|
+
readonly countsByB: LookupMap<B, PositiveInt>;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
interface RelatedAs {
|
|
387
|
+
readonly b: B;
|
|
388
|
+
readonly as: LookupSet<A>;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const relatedBsByA = createLookupMap<A, RelatedBs, LA>({ lookup: lookupA });
|
|
392
|
+
const relatedAsByB = createLookupMap<B, RelatedAs, LB>({ lookup: lookupB });
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
increment: (a, b) => {
|
|
396
|
+
let relatedBs = relatedBsByA.get(a);
|
|
397
|
+
const canonicalA = relatedBs ? relatedBs.a : a;
|
|
398
|
+
const relatedAs = relatedAsByB.get(b);
|
|
399
|
+
const canonicalB = relatedAs ? relatedAs.b : b;
|
|
400
|
+
|
|
401
|
+
if (!relatedBs) {
|
|
402
|
+
relatedBs = {
|
|
403
|
+
a: canonicalA,
|
|
404
|
+
countsByB: createLookupMap<B, PositiveInt, LB>({ lookup: lookupB }),
|
|
405
|
+
};
|
|
406
|
+
relatedBsByA.set(canonicalA, relatedBs);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const count = relatedBs.countsByB.get(canonicalB);
|
|
410
|
+
if (count) {
|
|
411
|
+
const nextCount = PositiveInt.orThrow(count + 1);
|
|
412
|
+
relatedBs.countsByB.set(canonicalB, nextCount);
|
|
413
|
+
return { a: canonicalA, b: canonicalB, count: nextCount };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
relatedBs.countsByB.set(canonicalB, onePositiveInt);
|
|
417
|
+
if (relatedAs) {
|
|
418
|
+
relatedAs.as.add(canonicalA);
|
|
419
|
+
} else {
|
|
420
|
+
relatedAsByB.set(canonicalB, {
|
|
421
|
+
b: canonicalB,
|
|
422
|
+
as: createLookupSet<A, LA>({
|
|
423
|
+
lookup: lookupA,
|
|
424
|
+
values: [canonicalA],
|
|
425
|
+
}),
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
return { a: canonicalA, b: canonicalB, count: onePositiveInt };
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
decrement: (a, b) => {
|
|
432
|
+
const relatedBs = relatedBsByA.get(a);
|
|
433
|
+
const relatedAs = relatedAsByB.get(b);
|
|
434
|
+
const count = relatedBs?.countsByB.get(b);
|
|
435
|
+
assert(
|
|
436
|
+
relatedBs && relatedAs && count,
|
|
437
|
+
"RefCountedRelation pair must exist before decrement.",
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
const nextCount = NonNegativeInt.orThrow(count - 1);
|
|
441
|
+
if (nextCount > 0) {
|
|
442
|
+
relatedBs.countsByB.set(relatedAs.b, PositiveInt.orThrow(nextCount));
|
|
443
|
+
} else {
|
|
444
|
+
relatedBs.countsByB.delete(relatedAs.b);
|
|
445
|
+
if (relatedBs.countsByB.size === 0) relatedBsByA.delete(relatedBs.a);
|
|
446
|
+
|
|
447
|
+
relatedAs.as.delete(relatedBs.a);
|
|
448
|
+
if (relatedAs.as.size === 0) relatedAsByB.delete(relatedAs.b);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return { a: relatedBs.a, b: relatedAs.b, count: nextCount };
|
|
452
|
+
},
|
|
453
|
+
|
|
454
|
+
getCount: (a, b) =>
|
|
455
|
+
relatedBsByA.get(a)?.countsByB.get(b) ?? zeroNonNegativeInt,
|
|
456
|
+
|
|
457
|
+
getAs: (b) => [...(relatedAsByB.get(b)?.as ?? emptyArray)],
|
|
458
|
+
|
|
459
|
+
getBs: (a) => [...(relatedBsByA.get(a)?.countsByB.keys() ?? emptyArray)],
|
|
460
|
+
|
|
461
|
+
hasA: (a) => relatedBsByA.has(a),
|
|
462
|
+
|
|
463
|
+
hasB: (b) => relatedAsByB.has(b),
|
|
464
|
+
|
|
465
|
+
getEntries: () => {
|
|
466
|
+
const entries: Array<readonly [A, B, PositiveInt]> = [];
|
|
467
|
+
for (const { a, countsByB } of relatedBsByA.values()) {
|
|
468
|
+
for (const [b, count] of countsByB) entries.push([a, b, count]);
|
|
469
|
+
}
|
|
470
|
+
return entries;
|
|
471
|
+
},
|
|
472
|
+
|
|
473
|
+
clear: () => {
|
|
474
|
+
relatedBsByA.clear();
|
|
475
|
+
relatedAsByB.clear();
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
}
|