@evolu/common 7.4.0 → 8.0.0-next.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 +4 -61
- package/dist/src/Array.d.ts +343 -102
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +181 -85
- package/dist/src/Assert.d.ts +57 -11
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +47 -11
- package/dist/src/BigInt.d.ts +6 -1
- package/dist/src/BigInt.d.ts.map +1 -1
- package/dist/src/BigInt.js +5 -0
- package/dist/src/Brand.d.ts +40 -12
- package/dist/src/Brand.d.ts.map +1 -1
- package/dist/src/Brand.js +5 -0
- package/dist/src/Buffer.d.ts +6 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +7 -2
- package/dist/src/Cache.d.ts +10 -2
- package/dist/src/Cache.d.ts.map +1 -1
- package/dist/src/Cache.js +6 -0
- package/dist/src/Callbacks.d.ts +10 -7
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Callbacks.js +23 -13
- package/dist/src/Console.d.ts +361 -69
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +217 -117
- package/dist/src/Crypto.d.ts +70 -21
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +72 -27
- package/dist/src/Eq.d.ts +22 -2
- package/dist/src/Eq.d.ts.map +1 -1
- package/dist/src/Eq.js +21 -1
- package/dist/src/Error.d.ts +23 -12
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +27 -11
- package/dist/src/Function.d.ts +128 -56
- package/dist/src/Function.d.ts.map +1 -1
- package/dist/src/Function.js +118 -14
- package/dist/src/Identicon.d.ts +5 -0
- package/dist/src/Identicon.d.ts.map +1 -1
- package/dist/src/Identicon.js +6 -1
- package/dist/src/Lookup.d.ts +160 -0
- package/dist/src/Lookup.d.ts.map +1 -0
- package/dist/src/Lookup.js +192 -0
- package/dist/src/Microtask.d.ts +21 -0
- package/dist/src/Microtask.d.ts.map +1 -0
- package/dist/src/Microtask.js +37 -0
- package/dist/src/Number.d.ts +18 -4
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +23 -1
- package/dist/src/Object.d.ts +110 -5
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +108 -5
- package/dist/src/Option.d.ts +58 -0
- package/dist/src/Option.d.ts.map +1 -0
- package/dist/src/Option.js +43 -0
- package/dist/src/Order.d.ts +5 -0
- package/dist/src/Order.d.ts.map +1 -1
- package/dist/src/Order.js +8 -3
- package/dist/src/Platform.d.ts +36 -2
- package/dist/src/Platform.d.ts.map +1 -1
- package/dist/src/Platform.js +10 -6
- package/dist/src/Polyfills.d.ts +27 -0
- package/dist/src/Polyfills.d.ts.map +1 -0
- package/dist/src/Polyfills.js +299 -0
- package/dist/src/Random.d.ts +18 -7
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Random.js +9 -7
- package/dist/src/Redacted.d.ts +7 -4
- package/dist/src/Redacted.d.ts.map +1 -1
- package/dist/src/Redacted.js +5 -0
- package/dist/src/Ref.d.ts +31 -16
- package/dist/src/Ref.d.ts.map +1 -1
- package/dist/src/Ref.js +35 -7
- package/dist/src/RefCount.d.ts +62 -0
- package/dist/src/RefCount.d.ts.map +1 -0
- package/dist/src/RefCount.js +83 -0
- package/dist/src/Relation.d.ts +84 -29
- package/dist/src/Relation.d.ts.map +1 -1
- package/dist/src/Relation.js +83 -105
- package/dist/src/Resource.d.ts +263 -0
- package/dist/src/Resource.d.ts.map +1 -0
- package/dist/src/Resource.js +389 -0
- package/dist/src/Result.d.ts +390 -374
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +142 -70
- package/dist/src/Schedule.d.ts +953 -0
- package/dist/src/Schedule.d.ts.map +1 -0
- package/dist/src/Schedule.js +1199 -0
- package/dist/src/Set.d.ts +181 -0
- package/dist/src/Set.d.ts.map +1 -0
- package/dist/src/Set.js +137 -0
- package/dist/src/Skiplist.d.ts +6 -1
- package/dist/src/Skiplist.d.ts.map +1 -1
- package/dist/src/Skiplist.js +5 -0
- package/dist/src/Sqlite.d.ts +160 -60
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +290 -190
- package/dist/src/Store.d.ts +36 -28
- package/dist/src/Store.d.ts.map +1 -1
- package/dist/src/Store.js +55 -15
- package/dist/src/String.d.ts +5 -0
- package/dist/src/String.d.ts.map +1 -1
- package/dist/src/String.js +5 -0
- package/dist/src/Task.d.ts +2263 -423
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +1488 -348
- package/dist/src/Test.d.ts +122 -0
- package/dist/src/Test.d.ts.map +1 -0
- package/dist/src/Test.js +66 -0
- package/dist/src/Time.d.ts +201 -48
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +190 -86
- package/dist/src/Tracer.d.ts +48 -0
- package/dist/src/Tracer.d.ts.map +1 -0
- package/dist/src/Tracer.js +6 -0
- package/dist/src/Type.d.ts +582 -209
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +362 -240
- package/dist/src/Types.d.ts +160 -6
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/Types.js +22 -2
- package/dist/src/WebSocket.d.ts +113 -58
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +210 -118
- package/dist/src/Worker.d.ts +245 -60
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +220 -64
- package/dist/src/index.d.ts +25 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +19 -3
- package/dist/src/local-first/Db.d.ts +24 -213
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +622 -293
- package/dist/src/local-first/Error.d.ts +12 -0
- package/dist/src/local-first/Error.d.ts.map +1 -0
- package/dist/src/local-first/Error.js +6 -0
- package/dist/src/local-first/Evolu.d.ts +330 -285
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +472 -402
- package/dist/src/local-first/LocalAuth.d.ts +10 -10
- package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
- package/dist/src/local-first/LocalAuth.js +6 -1
- package/dist/src/local-first/Owner.d.ts +70 -27
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +27 -14
- package/dist/src/local-first/Protocol.d.ts +58 -50
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +253 -155
- package/dist/src/local-first/Query.d.ts +133 -43
- package/dist/src/local-first/Query.d.ts.map +1 -1
- package/dist/src/local-first/Query.js +139 -90
- package/dist/src/local-first/Relay.d.ts +18 -38
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +59 -171
- package/dist/src/local-first/Schema.d.ts +139 -144
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +78 -163
- package/dist/src/local-first/Shared.d.ts +125 -0
- package/dist/src/local-first/Shared.d.ts.map +1 -0
- package/dist/src/local-first/Shared.js +569 -0
- package/dist/src/local-first/Storage.d.ts +50 -53
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +121 -173
- package/dist/src/local-first/Timestamp.d.ts +28 -35
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +24 -25
- package/dist/src/local-first/index.d.ts +2 -15
- package/dist/src/local-first/index.d.ts.map +1 -1
- package/dist/src/local-first/index.js +2 -15
- package/package.json +36 -26
- package/src/Array.ts +467 -106
- package/src/Assert.ts +87 -12
- package/src/BigInt.ts +7 -1
- package/src/Brand.ts +41 -12
- package/src/Buffer.ts +10 -4
- package/src/Cache.ts +11 -2
- package/src/Callbacks.ts +33 -23
- package/src/Console.ts +567 -178
- package/src/Crypto.ts +111 -62
- package/src/Eq.ts +30 -3
- package/src/Error.ts +50 -34
- package/src/Function.ts +140 -68
- package/src/Identicon.ts +7 -1
- package/src/Lookup.ts +415 -0
- package/src/Microtask.ts +58 -0
- package/src/Number.ts +40 -7
- package/src/Object.ts +155 -16
- package/src/Option.ts +74 -0
- package/src/Order.ts +11 -3
- package/src/Platform.ts +43 -7
- package/src/Polyfills.ts +465 -0
- package/src/Random.ts +25 -11
- package/src/Redacted.ts +8 -4
- package/src/Ref.ts +72 -21
- package/src/RefCount.ts +170 -0
- package/src/Relation.ts +188 -136
- package/src/Resource.ts +864 -0
- package/src/Result.ts +548 -381
- package/src/Schedule.ts +1471 -0
- package/src/Set.ts +247 -0
- package/src/Skiplist.ts +7 -1
- package/src/Sqlite.ts +409 -304
- package/src/Store.ts +94 -48
- package/src/String.ts +6 -0
- package/src/Task.ts +3685 -740
- package/src/Test.ts +162 -0
- package/src/Time.ts +348 -145
- package/src/Tracer.ts +54 -0
- package/src/Type.ts +1064 -630
- package/src/Types.ts +202 -7
- package/src/WebSocket.ts +311 -178
- package/src/Worker.ts +527 -147
- package/src/index.ts +74 -3
- package/src/local-first/Db.ts +871 -597
- package/src/local-first/Error.ts +17 -0
- package/src/local-first/Evolu.ts +884 -823
- package/src/local-first/LocalAuth.ts +10 -10
- package/src/local-first/Owner.ts +94 -42
- package/src/local-first/Protocol.ts +440 -303
- package/src/local-first/Query.ts +243 -194
- package/src/local-first/Relay.ts +91 -252
- package/src/local-first/Schema.ts +284 -394
- package/src/local-first/Shared.ts +932 -0
- package/src/local-first/Storage.ts +209 -282
- package/src/local-first/Timestamp.ts +38 -45
- package/src/local-first/index.ts +2 -16
- package/LICENSE +0 -21
- package/dist/src/Instances.d.ts +0 -34
- package/dist/src/Instances.d.ts.map +0 -1
- package/dist/src/Instances.js +0 -44
- package/dist/src/Resources.d.ts +0 -118
- package/dist/src/Resources.d.ts.map +0 -1
- package/dist/src/Resources.js +0 -197
- package/dist/src/local-first/Platform.d.ts +0 -25
- package/dist/src/local-first/Platform.d.ts.map +0 -1
- package/dist/src/local-first/Platform.js +0 -1
- package/dist/src/local-first/Public.d.ts +0 -22
- package/dist/src/local-first/Public.d.ts.map +0 -1
- package/dist/src/local-first/Public.js +0 -15
- package/dist/src/local-first/PublicKysely.d.ts +0 -148
- package/dist/src/local-first/PublicKysely.d.ts.map +0 -1
- package/dist/src/local-first/PublicKysely.js +0 -184
- package/dist/src/local-first/Sync.d.ts +0 -112
- package/dist/src/local-first/Sync.d.ts.map +0 -1
- package/dist/src/local-first/Sync.js +0 -529
- package/src/Instances.ts +0 -90
- package/src/Resources.ts +0 -367
- package/src/local-first/Platform.ts +0 -27
- package/src/local-first/Public.ts +0 -42
- package/src/local-first/PublicKysely.ts +0 -239
- package/src/local-first/Sync.ts +0 -960
package/src/RefCount.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reference counting helpers.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { assert, assertNotDisposed } from "./Assert.js";
|
|
8
|
+
import { identity } from "./Function.js";
|
|
9
|
+
import { createLookupMap, type Lookup, type LookupOption } from "./Lookup.js";
|
|
10
|
+
import { NonNegativeInt, PositiveInt, zeroNonNegativeInt } from "./Type.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Reference count for one retained value.
|
|
14
|
+
*
|
|
15
|
+
* Decrementing below zero is a programmer error checked with {@link assert}.
|
|
16
|
+
*/
|
|
17
|
+
export interface RefCount extends Disposable {
|
|
18
|
+
/** Increments the count and returns the new count. */
|
|
19
|
+
readonly increment: () => PositiveInt;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Decrements the count and returns the new count.
|
|
23
|
+
*
|
|
24
|
+
* Decrementing below zero is a programmer error checked with {@link assert}.
|
|
25
|
+
*/
|
|
26
|
+
readonly decrement: () => NonNegativeInt;
|
|
27
|
+
|
|
28
|
+
/** Returns the current count. */
|
|
29
|
+
readonly getCount: () => NonNegativeInt;
|
|
30
|
+
|
|
31
|
+
/** Disposes and invalidates the helper. Further method calls throw. */
|
|
32
|
+
readonly [Symbol.dispose]: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Creates {@link RefCount}. */
|
|
36
|
+
export const createRefCount = (): RefCount => {
|
|
37
|
+
let count = zeroNonNegativeInt;
|
|
38
|
+
const stack = new DisposableStack();
|
|
39
|
+
stack.defer(() => {
|
|
40
|
+
count = zeroNonNegativeInt;
|
|
41
|
+
});
|
|
42
|
+
const moved = stack.move();
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
increment: () => {
|
|
46
|
+
assertNotDisposed(moved);
|
|
47
|
+
const nextCount = PositiveInt.orThrow(count + 1);
|
|
48
|
+
count = nextCount;
|
|
49
|
+
return nextCount;
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
decrement: () => {
|
|
53
|
+
assertNotDisposed(moved);
|
|
54
|
+
assert(count > 0, "RefCount must not be decremented below zero.");
|
|
55
|
+
count = NonNegativeInt.orThrow(count - 1);
|
|
56
|
+
return count;
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
getCount: () => {
|
|
60
|
+
assertNotDisposed(moved);
|
|
61
|
+
return count;
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
[Symbol.dispose]: () => moved.dispose(),
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Reference counts keyed by logical identity.
|
|
70
|
+
*
|
|
71
|
+
* By default, {@link createRefCountByKey} uses reference identity, the same as
|
|
72
|
+
* `Map` keys. Callers may instead provide a {@link Lookup lookup} so logical
|
|
73
|
+
* equality is based on a derived stable key. Decrementing a missing key is a
|
|
74
|
+
* programmer error checked with {@link assert}.
|
|
75
|
+
*/
|
|
76
|
+
export interface RefCountByKey<TKey> extends Disposable {
|
|
77
|
+
/** Increments key count and returns the new count. */
|
|
78
|
+
readonly increment: (key: TKey) => PositiveInt;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Decrements key count and returns the new count.
|
|
82
|
+
*
|
|
83
|
+
* Decrementing a missing key is a programmer error checked with
|
|
84
|
+
* {@link assert}.
|
|
85
|
+
*/
|
|
86
|
+
readonly decrement: (key: TKey) => NonNegativeInt;
|
|
87
|
+
|
|
88
|
+
/** Gets current count for key. Returns `0` when the key is not tracked. */
|
|
89
|
+
readonly getCount: (key: TKey) => NonNegativeInt;
|
|
90
|
+
|
|
91
|
+
/** Returns `true` when the key is tracked with count greater than zero. */
|
|
92
|
+
readonly has: (key: TKey) => boolean;
|
|
93
|
+
|
|
94
|
+
/** Returns all currently tracked keys. */
|
|
95
|
+
readonly keys: () => ReadonlySet<TKey>;
|
|
96
|
+
|
|
97
|
+
/** Disposes and invalidates the helper. Further method calls throw. */
|
|
98
|
+
readonly [Symbol.dispose]: () => void;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Options for {@link createRefCountByKey}. */
|
|
102
|
+
export interface CreateRefCountByKeyOptions<
|
|
103
|
+
TKey,
|
|
104
|
+
L = TKey,
|
|
105
|
+
> extends LookupOption<TKey, L> {}
|
|
106
|
+
|
|
107
|
+
/** Creates {@link RefCountByKey}. */
|
|
108
|
+
export function createRefCountByKey<TKey = unknown>(): RefCountByKey<TKey>;
|
|
109
|
+
export function createRefCountByKey<TKey, L>(
|
|
110
|
+
options: CreateRefCountByKeyOptions<TKey, L>,
|
|
111
|
+
): RefCountByKey<TKey>;
|
|
112
|
+
export function createRefCountByKey<TKey, L = TKey>({
|
|
113
|
+
lookup = identity as Lookup<TKey, L>,
|
|
114
|
+
}: CreateRefCountByKeyOptions<TKey, L> = {}): RefCountByKey<TKey> {
|
|
115
|
+
const stack = new DisposableStack();
|
|
116
|
+
|
|
117
|
+
const refCountByKey = stack.adopt(
|
|
118
|
+
createLookupMap<TKey, RefCount, L>({ lookup }),
|
|
119
|
+
(refCountByKey) => {
|
|
120
|
+
for (const refCount of refCountByKey.values()) refCount[Symbol.dispose]();
|
|
121
|
+
refCountByKey.clear();
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const moved = stack.move();
|
|
126
|
+
|
|
127
|
+
const getRefCount = (key: TKey): RefCount => {
|
|
128
|
+
let refCount = refCountByKey.get(key);
|
|
129
|
+
if (!refCount) {
|
|
130
|
+
refCount = createRefCount();
|
|
131
|
+
refCountByKey.set(key, refCount);
|
|
132
|
+
}
|
|
133
|
+
return refCount;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
increment: (key) => {
|
|
138
|
+
assertNotDisposed(moved);
|
|
139
|
+
return getRefCount(key).increment();
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
decrement: (key) => {
|
|
143
|
+
assertNotDisposed(moved);
|
|
144
|
+
const refCount = getRefCount(key);
|
|
145
|
+
const nextCount = refCount.decrement();
|
|
146
|
+
if (nextCount === 0) {
|
|
147
|
+
refCount[Symbol.dispose]();
|
|
148
|
+
refCountByKey.delete(key);
|
|
149
|
+
}
|
|
150
|
+
return nextCount;
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
getCount: (key) => {
|
|
154
|
+
assertNotDisposed(moved);
|
|
155
|
+
return refCountByKey.get(key)?.getCount() ?? zeroNonNegativeInt;
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
has: (key) => {
|
|
159
|
+
assertNotDisposed(moved);
|
|
160
|
+
return refCountByKey.has(key);
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
keys: () => {
|
|
164
|
+
assertNotDisposed(moved);
|
|
165
|
+
return new Set(refCountByKey.keys());
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
[Symbol.dispose]: () => moved.dispose(),
|
|
169
|
+
};
|
|
170
|
+
}
|
package/src/Relation.ts
CHANGED
|
@@ -1,36 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bidirectional relations with O(1) lookup in both directions.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { emptyArray } from "./Array.js";
|
|
1
8
|
import { assert } from "./Assert.js";
|
|
9
|
+
import { identity } from "./Function.js";
|
|
10
|
+
import {
|
|
11
|
+
createLookupMap,
|
|
12
|
+
createLookupSet,
|
|
13
|
+
type Lookup,
|
|
14
|
+
type LookupSet,
|
|
15
|
+
} from "./Lookup.js";
|
|
2
16
|
|
|
3
17
|
/**
|
|
4
18
|
* Bidirectional relation between two types.
|
|
5
19
|
*
|
|
6
|
-
* Why useful:
|
|
7
|
-
*
|
|
8
20
|
* - Provides O(1) (amortized) lookup in both directions (A → B and B → A) without
|
|
9
21
|
* maintaining two maps manually and risking them diverging.
|
|
10
22
|
* - Natural fit for symmetric associations such as ownerId ↔ WebSocket, tag ↔
|
|
11
23
|
* item, user ↔ role, entity ↔ subscription where both directions are
|
|
12
24
|
* frequently queried.
|
|
13
25
|
* - Supports fast membership tests via `has`, `hasA`, and `hasB`.
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
26
|
+
* - Exposes directional iterators for imperative `for...of` traversal.
|
|
27
|
+
* - The relation itself is iterable, allowing it to be treated as a set of pairs
|
|
28
|
+
* when needed.
|
|
16
29
|
*
|
|
17
30
|
* Complexity:
|
|
18
31
|
*
|
|
19
|
-
* - `add` / `remove` / `has*` /
|
|
20
|
-
* operations (O(1) expected).
|
|
21
|
-
* - `
|
|
22
|
-
*
|
|
23
|
-
*
|
|
32
|
+
* - `add` / `remove` / `has*` / `*Count*` each perform a constant number of
|
|
33
|
+
* Map/Set operations (O(1) expected).
|
|
34
|
+
* - `iterateA` and `iterateB` create iterators in O(1); consuming them is O(d)
|
|
35
|
+
* where d is the number of associated elements.
|
|
36
|
+
* - `removeByA` and `removeByB` are O(d) where d is the number of associated
|
|
37
|
+
* elements, because every associated pair must be touched once.
|
|
38
|
+
*
|
|
39
|
+
* By default, {@link createRelation} uses reference identity for both sides,
|
|
40
|
+
* matching native `Map` and `Set`. Callers may instead provide
|
|
41
|
+
* {@link Lookup lookup} functions so logical equality is based on a derived
|
|
42
|
+
* stable key.
|
|
43
|
+
*
|
|
44
|
+
* The input parameter types of `lookupA` and `lookupB` determine which values
|
|
45
|
+
* the returned relation accepts on each side.
|
|
46
|
+
*
|
|
47
|
+
* ### Example
|
|
24
48
|
*
|
|
25
|
-
*
|
|
49
|
+
* Use the default identity semantics.
|
|
26
50
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
51
|
+
* ```ts
|
|
52
|
+
* const relation = createRelation<WebSocket, string>();
|
|
53
|
+
* relation.add(socket, "owner-1");
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* ### Example
|
|
57
|
+
*
|
|
58
|
+
* Use lookup-derived equality.
|
|
59
|
+
*
|
|
60
|
+
* ```ts
|
|
61
|
+
* interface Person {
|
|
62
|
+
* readonly id: string;
|
|
63
|
+
* readonly name: string;
|
|
64
|
+
* }
|
|
65
|
+
*
|
|
66
|
+
* const relation = createRelation({
|
|
67
|
+
* lookupA: (person: Person) => person.id,
|
|
68
|
+
* lookupB: (group: { readonly id: string }) => group.id,
|
|
69
|
+
* });
|
|
70
|
+
*
|
|
71
|
+
* relation.add({ id: "1", name: "Ada" }, { id: "admins" });
|
|
72
|
+
* relation.has({ id: "1", name: "Grace" }, { id: "admins" }); // true
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* @see {@link createRelation}
|
|
34
76
|
*/
|
|
35
77
|
export interface Relation<A, B> {
|
|
36
78
|
/**
|
|
@@ -45,17 +87,31 @@ export interface Relation<A, B> {
|
|
|
45
87
|
*/
|
|
46
88
|
readonly remove: (a: A, b: B) => boolean;
|
|
47
89
|
|
|
48
|
-
/**
|
|
49
|
-
readonly
|
|
90
|
+
/** Removes all pairs containing the given A element. */
|
|
91
|
+
readonly removeByA: (a: A) => boolean;
|
|
50
92
|
|
|
51
|
-
/**
|
|
52
|
-
readonly
|
|
93
|
+
/** Removes all pairs containing the given B element. */
|
|
94
|
+
readonly removeByB: (b: B) => boolean;
|
|
53
95
|
|
|
54
96
|
/**
|
|
55
|
-
* Iterates over
|
|
56
|
-
*
|
|
97
|
+
* Iterates over all A elements related to a B element.
|
|
98
|
+
*
|
|
99
|
+
* Returns a live iterator over the current relation state rather than a
|
|
100
|
+
* snapshot copy.
|
|
101
|
+
*
|
|
102
|
+
* Returns an empty iterator when the B element has no related values.
|
|
57
103
|
*/
|
|
58
|
-
readonly
|
|
104
|
+
readonly iterateA: (b: B) => IterableIterator<A>;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Iterates over all B elements related to an A element.
|
|
108
|
+
*
|
|
109
|
+
* Returns a live iterator over the current relation state rather than a
|
|
110
|
+
* snapshot copy.
|
|
111
|
+
*
|
|
112
|
+
* Returns an empty iterator when the A element has no related values.
|
|
113
|
+
*/
|
|
114
|
+
readonly iterateB: (a: A) => IterableIterator<B>;
|
|
59
115
|
|
|
60
116
|
/**
|
|
61
117
|
* Iterator over all pairs enabling for..of and spread. Yields readonly [a, b]
|
|
@@ -72,12 +128,6 @@ export interface Relation<A, B> {
|
|
|
72
128
|
/** Checks if a B element exists in the relation. */
|
|
73
129
|
readonly hasB: (b: B) => boolean;
|
|
74
130
|
|
|
75
|
-
/** Deletes all pairs containing the given A element. */
|
|
76
|
-
readonly deleteA: (a: A) => boolean;
|
|
77
|
-
|
|
78
|
-
/** Deletes all pairs containing the given B element. */
|
|
79
|
-
readonly deleteB: (b: B) => boolean;
|
|
80
|
-
|
|
81
131
|
/** Clears all pairs from the relation. */
|
|
82
132
|
readonly clear: () => void;
|
|
83
133
|
|
|
@@ -87,148 +137,150 @@ export interface Relation<A, B> {
|
|
|
87
137
|
/** Number of distinct B elements currently present. */
|
|
88
138
|
readonly bCount: () => number;
|
|
89
139
|
|
|
140
|
+
/** Number of B elements related to the given A element. */
|
|
141
|
+
readonly bCountForA: (a: A) => number;
|
|
142
|
+
|
|
143
|
+
/** Number of A elements related to the given B element. */
|
|
144
|
+
readonly aCountForB: (b: B) => number;
|
|
145
|
+
|
|
90
146
|
/** Number of pairs currently stored in the relation. */
|
|
91
147
|
readonly size: () => number;
|
|
92
148
|
}
|
|
93
149
|
|
|
150
|
+
/** Options for {@link createRelation}. */
|
|
151
|
+
export interface CreateRelationOptions<A, B, LA = A, LB = B> {
|
|
152
|
+
/** Derives logical identity for A values. Defaults to {@link identity}. */
|
|
153
|
+
readonly lookupA?: Lookup<A, LA>;
|
|
154
|
+
|
|
155
|
+
/** Derives logical identity for B values. Defaults to {@link identity}. */
|
|
156
|
+
readonly lookupB?: Lookup<B, LB>;
|
|
157
|
+
}
|
|
158
|
+
|
|
94
159
|
/** Creates a {@link Relation}. */
|
|
95
|
-
export
|
|
96
|
-
|
|
97
|
-
|
|
160
|
+
export function createRelation<A, B>(): Relation<A, B>;
|
|
161
|
+
export function createRelation<A, B, LA, LB>(
|
|
162
|
+
options: CreateRelationOptions<A, B, LA, LB>,
|
|
163
|
+
): Relation<A, B>;
|
|
164
|
+
export function createRelation<A, B, LA = A, LB = B>({
|
|
165
|
+
lookupA = identity as Lookup<A, LA>,
|
|
166
|
+
lookupB = identity as Lookup<B, LB>,
|
|
167
|
+
}: CreateRelationOptions<A, B, LA, LB> = {}): Relation<A, B> {
|
|
168
|
+
const bByA = createLookupMap<A, LookupSet<B>, LA>({ lookup: lookupA });
|
|
169
|
+
const aByB = createLookupMap<B, LookupSet<A>, LB>({ lookup: lookupB });
|
|
98
170
|
let sizeInternal = 0;
|
|
99
171
|
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
bSet = new Set<B>();
|
|
106
|
-
aToB.set(a, bSet);
|
|
107
|
-
}
|
|
108
|
-
bSet.add(b);
|
|
172
|
+
const removePair = (a: A, b: B): void => {
|
|
173
|
+
const relatedB = bByA.get(a);
|
|
174
|
+
// This should only fail if a leaked view was mutated via an unsafe cast.
|
|
175
|
+
assertRelationMappingConsistency(relatedB);
|
|
176
|
+
assertRelationMappingConsistency(relatedB.has(b));
|
|
109
177
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
aSet.add(a);
|
|
116
|
-
sizeInternal++;
|
|
117
|
-
return true;
|
|
118
|
-
},
|
|
178
|
+
relatedB.delete(b);
|
|
179
|
+
if (relatedB.size === 0) {
|
|
180
|
+
bByA.delete(a);
|
|
181
|
+
}
|
|
119
182
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
183
|
+
const relatedA = aByB.get(b);
|
|
184
|
+
// This should only fail if a leaked view was mutated via an unsafe cast.
|
|
185
|
+
assertRelationMappingConsistency(relatedA);
|
|
186
|
+
assertRelationMappingConsistency(relatedA.has(a));
|
|
123
187
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
188
|
+
relatedA.delete(a);
|
|
189
|
+
if (relatedA.size === 0) {
|
|
190
|
+
aByB.delete(b);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
sizeInternal--;
|
|
194
|
+
};
|
|
128
195
|
|
|
129
|
-
|
|
130
|
-
|
|
196
|
+
return {
|
|
197
|
+
add: (a, b) => {
|
|
198
|
+
const canonicalA = bByA.getKey(a) ?? a;
|
|
199
|
+
const canonicalB = aByB.getKey(b) ?? b;
|
|
131
200
|
|
|
132
|
-
|
|
133
|
-
if (
|
|
134
|
-
|
|
201
|
+
let relatedB = bByA.get(canonicalA);
|
|
202
|
+
if (relatedB?.has(canonicalB)) return false;
|
|
203
|
+
if (!relatedB) {
|
|
204
|
+
relatedB = createLookupSet<B, LB>({ lookup: lookupB });
|
|
205
|
+
bByA.set(canonicalA, relatedB);
|
|
135
206
|
}
|
|
136
|
-
|
|
137
|
-
return true;
|
|
138
|
-
},
|
|
207
|
+
relatedB.add(canonicalB);
|
|
139
208
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
209
|
+
let relatedA = aByB.get(canonicalB);
|
|
210
|
+
if (!relatedA) {
|
|
211
|
+
relatedA = createLookupSet<A, LA>({ lookup: lookupA });
|
|
212
|
+
aByB.set(canonicalB, relatedA);
|
|
213
|
+
}
|
|
214
|
+
relatedA.add(canonicalA);
|
|
143
215
|
|
|
144
|
-
|
|
145
|
-
return
|
|
216
|
+
sizeInternal++;
|
|
217
|
+
return true;
|
|
146
218
|
},
|
|
147
219
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
for (const b of bSet) callback(a, b);
|
|
151
|
-
}
|
|
152
|
-
},
|
|
220
|
+
remove: (a, b) => {
|
|
221
|
+
if (!bByA.get(a)?.has(b)) return false;
|
|
153
222
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
for (const [a, bSet] of aToB) {
|
|
157
|
-
for (const b of bSet) {
|
|
158
|
-
yield [a, b] as const;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
return iterator();
|
|
223
|
+
removePair(a, b);
|
|
224
|
+
return true;
|
|
163
225
|
},
|
|
164
226
|
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
227
|
+
removeByA: (a) => {
|
|
228
|
+
const relatedB = bByA.get(a);
|
|
229
|
+
if (!relatedB) return false;
|
|
230
|
+
for (const b of [...relatedB.keys()]) removePair(a, b);
|
|
231
|
+
return true;
|
|
168
232
|
},
|
|
169
233
|
|
|
170
|
-
|
|
171
|
-
|
|
234
|
+
removeByB: (b) => {
|
|
235
|
+
const relatedA = aByB.get(b);
|
|
236
|
+
if (!relatedA) return false;
|
|
237
|
+
for (const a of [...relatedA.keys()]) removePair(a, b);
|
|
238
|
+
return true;
|
|
172
239
|
},
|
|
173
240
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
241
|
+
iterateA: (b) =>
|
|
242
|
+
aByB.get(b)?.keys() ?? (emptyArray.values() as IterableIterator<A>),
|
|
243
|
+
|
|
244
|
+
iterateB: (a) =>
|
|
245
|
+
bByA.get(a)?.keys() ?? (emptyArray.values() as IterableIterator<B>),
|
|
177
246
|
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
for (const b of bSet) {
|
|
183
|
-
const aSet = bToA.get(b);
|
|
184
|
-
if (aSet) {
|
|
185
|
-
aSet.delete(a);
|
|
186
|
-
if (aSet.size === 0) {
|
|
187
|
-
bToA.delete(b);
|
|
188
|
-
}
|
|
247
|
+
*[Symbol.iterator](): IterableIterator<readonly [A, B]> {
|
|
248
|
+
for (const [a, relatedB] of bByA) {
|
|
249
|
+
for (const b of relatedB.keys()) {
|
|
250
|
+
yield [a, b] as const;
|
|
189
251
|
}
|
|
190
252
|
}
|
|
191
|
-
aToB.delete(a);
|
|
192
|
-
sizeInternal -= removed;
|
|
193
|
-
return true;
|
|
194
253
|
},
|
|
195
254
|
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
const removed = aSet.size;
|
|
200
|
-
for (const a of aSet) {
|
|
201
|
-
const bSet = aToB.get(a);
|
|
202
|
-
if (bSet) {
|
|
203
|
-
bSet.delete(b);
|
|
204
|
-
if (bSet.size === 0) {
|
|
205
|
-
aToB.delete(a);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
bToA.delete(b);
|
|
210
|
-
sizeInternal -= removed;
|
|
211
|
-
return true;
|
|
255
|
+
has: (a, b) => {
|
|
256
|
+
const relatedB = bByA.get(a);
|
|
257
|
+
return relatedB?.has(b) ?? false;
|
|
212
258
|
},
|
|
213
259
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
260
|
+
hasA: (a) => bByA.has(a),
|
|
261
|
+
|
|
262
|
+
hasB: (b) => aByB.has(b),
|
|
263
|
+
|
|
264
|
+
clear: () => {
|
|
265
|
+
bByA.clear();
|
|
266
|
+
aByB.clear();
|
|
217
267
|
sizeInternal = 0;
|
|
218
268
|
},
|
|
219
269
|
|
|
220
|
-
aCount()
|
|
221
|
-
return aToB.size;
|
|
222
|
-
},
|
|
270
|
+
aCount: () => bByA.size,
|
|
223
271
|
|
|
224
|
-
bCount()
|
|
225
|
-
return bToA.size;
|
|
226
|
-
},
|
|
272
|
+
bCount: () => aByB.size,
|
|
227
273
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
274
|
+
bCountForA: (a) => bByA.get(a)?.size ?? 0,
|
|
275
|
+
|
|
276
|
+
aCountForB: (b) => aByB.get(b)?.size ?? 0,
|
|
277
|
+
|
|
278
|
+
size: () => sizeInternal,
|
|
231
279
|
};
|
|
280
|
+
}
|
|
232
281
|
|
|
233
|
-
|
|
282
|
+
const assertRelationMappingConsistency: (
|
|
283
|
+
condition: unknown,
|
|
284
|
+
) => asserts condition = (condition) => {
|
|
285
|
+
assert(condition, "Relation mapping inconsistency");
|
|
234
286
|
};
|