@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.
Files changed (251) hide show
  1. package/README.md +4 -61
  2. package/dist/src/Array.d.ts +343 -102
  3. package/dist/src/Array.d.ts.map +1 -1
  4. package/dist/src/Array.js +181 -85
  5. package/dist/src/Assert.d.ts +57 -11
  6. package/dist/src/Assert.d.ts.map +1 -1
  7. package/dist/src/Assert.js +47 -11
  8. package/dist/src/BigInt.d.ts +6 -1
  9. package/dist/src/BigInt.d.ts.map +1 -1
  10. package/dist/src/BigInt.js +5 -0
  11. package/dist/src/Brand.d.ts +40 -12
  12. package/dist/src/Brand.d.ts.map +1 -1
  13. package/dist/src/Brand.js +5 -0
  14. package/dist/src/Buffer.d.ts +6 -1
  15. package/dist/src/Buffer.d.ts.map +1 -1
  16. package/dist/src/Buffer.js +7 -2
  17. package/dist/src/Cache.d.ts +10 -2
  18. package/dist/src/Cache.d.ts.map +1 -1
  19. package/dist/src/Cache.js +6 -0
  20. package/dist/src/Callbacks.d.ts +10 -7
  21. package/dist/src/Callbacks.d.ts.map +1 -1
  22. package/dist/src/Callbacks.js +23 -13
  23. package/dist/src/Console.d.ts +361 -69
  24. package/dist/src/Console.d.ts.map +1 -1
  25. package/dist/src/Console.js +217 -117
  26. package/dist/src/Crypto.d.ts +70 -21
  27. package/dist/src/Crypto.d.ts.map +1 -1
  28. package/dist/src/Crypto.js +72 -27
  29. package/dist/src/Eq.d.ts +22 -2
  30. package/dist/src/Eq.d.ts.map +1 -1
  31. package/dist/src/Eq.js +21 -1
  32. package/dist/src/Error.d.ts +23 -12
  33. package/dist/src/Error.d.ts.map +1 -1
  34. package/dist/src/Error.js +27 -11
  35. package/dist/src/Function.d.ts +128 -56
  36. package/dist/src/Function.d.ts.map +1 -1
  37. package/dist/src/Function.js +118 -14
  38. package/dist/src/Identicon.d.ts +5 -0
  39. package/dist/src/Identicon.d.ts.map +1 -1
  40. package/dist/src/Identicon.js +6 -1
  41. package/dist/src/Lookup.d.ts +160 -0
  42. package/dist/src/Lookup.d.ts.map +1 -0
  43. package/dist/src/Lookup.js +192 -0
  44. package/dist/src/Microtask.d.ts +21 -0
  45. package/dist/src/Microtask.d.ts.map +1 -0
  46. package/dist/src/Microtask.js +37 -0
  47. package/dist/src/Number.d.ts +18 -4
  48. package/dist/src/Number.d.ts.map +1 -1
  49. package/dist/src/Number.js +23 -1
  50. package/dist/src/Object.d.ts +110 -5
  51. package/dist/src/Object.d.ts.map +1 -1
  52. package/dist/src/Object.js +108 -5
  53. package/dist/src/Option.d.ts +58 -0
  54. package/dist/src/Option.d.ts.map +1 -0
  55. package/dist/src/Option.js +43 -0
  56. package/dist/src/Order.d.ts +5 -0
  57. package/dist/src/Order.d.ts.map +1 -1
  58. package/dist/src/Order.js +8 -3
  59. package/dist/src/Platform.d.ts +36 -2
  60. package/dist/src/Platform.d.ts.map +1 -1
  61. package/dist/src/Platform.js +10 -6
  62. package/dist/src/Polyfills.d.ts +27 -0
  63. package/dist/src/Polyfills.d.ts.map +1 -0
  64. package/dist/src/Polyfills.js +299 -0
  65. package/dist/src/Random.d.ts +18 -7
  66. package/dist/src/Random.d.ts.map +1 -1
  67. package/dist/src/Random.js +9 -7
  68. package/dist/src/Redacted.d.ts +7 -4
  69. package/dist/src/Redacted.d.ts.map +1 -1
  70. package/dist/src/Redacted.js +5 -0
  71. package/dist/src/Ref.d.ts +31 -16
  72. package/dist/src/Ref.d.ts.map +1 -1
  73. package/dist/src/Ref.js +35 -7
  74. package/dist/src/RefCount.d.ts +62 -0
  75. package/dist/src/RefCount.d.ts.map +1 -0
  76. package/dist/src/RefCount.js +83 -0
  77. package/dist/src/Relation.d.ts +84 -29
  78. package/dist/src/Relation.d.ts.map +1 -1
  79. package/dist/src/Relation.js +83 -105
  80. package/dist/src/Resource.d.ts +263 -0
  81. package/dist/src/Resource.d.ts.map +1 -0
  82. package/dist/src/Resource.js +389 -0
  83. package/dist/src/Result.d.ts +390 -374
  84. package/dist/src/Result.d.ts.map +1 -1
  85. package/dist/src/Result.js +142 -70
  86. package/dist/src/Schedule.d.ts +953 -0
  87. package/dist/src/Schedule.d.ts.map +1 -0
  88. package/dist/src/Schedule.js +1199 -0
  89. package/dist/src/Set.d.ts +181 -0
  90. package/dist/src/Set.d.ts.map +1 -0
  91. package/dist/src/Set.js +137 -0
  92. package/dist/src/Skiplist.d.ts +6 -1
  93. package/dist/src/Skiplist.d.ts.map +1 -1
  94. package/dist/src/Skiplist.js +5 -0
  95. package/dist/src/Sqlite.d.ts +160 -60
  96. package/dist/src/Sqlite.d.ts.map +1 -1
  97. package/dist/src/Sqlite.js +290 -190
  98. package/dist/src/Store.d.ts +36 -28
  99. package/dist/src/Store.d.ts.map +1 -1
  100. package/dist/src/Store.js +55 -15
  101. package/dist/src/String.d.ts +5 -0
  102. package/dist/src/String.d.ts.map +1 -1
  103. package/dist/src/String.js +5 -0
  104. package/dist/src/Task.d.ts +2263 -423
  105. package/dist/src/Task.d.ts.map +1 -1
  106. package/dist/src/Task.js +1488 -348
  107. package/dist/src/Test.d.ts +122 -0
  108. package/dist/src/Test.d.ts.map +1 -0
  109. package/dist/src/Test.js +66 -0
  110. package/dist/src/Time.d.ts +201 -48
  111. package/dist/src/Time.d.ts.map +1 -1
  112. package/dist/src/Time.js +190 -86
  113. package/dist/src/Tracer.d.ts +48 -0
  114. package/dist/src/Tracer.d.ts.map +1 -0
  115. package/dist/src/Tracer.js +6 -0
  116. package/dist/src/Type.d.ts +582 -209
  117. package/dist/src/Type.d.ts.map +1 -1
  118. package/dist/src/Type.js +362 -240
  119. package/dist/src/Types.d.ts +160 -6
  120. package/dist/src/Types.d.ts.map +1 -1
  121. package/dist/src/Types.js +22 -2
  122. package/dist/src/WebSocket.d.ts +113 -58
  123. package/dist/src/WebSocket.d.ts.map +1 -1
  124. package/dist/src/WebSocket.js +210 -118
  125. package/dist/src/Worker.d.ts +245 -60
  126. package/dist/src/Worker.d.ts.map +1 -1
  127. package/dist/src/Worker.js +220 -64
  128. package/dist/src/index.d.ts +25 -3
  129. package/dist/src/index.d.ts.map +1 -1
  130. package/dist/src/index.js +19 -3
  131. package/dist/src/local-first/Db.d.ts +24 -213
  132. package/dist/src/local-first/Db.d.ts.map +1 -1
  133. package/dist/src/local-first/Db.js +622 -293
  134. package/dist/src/local-first/Error.d.ts +12 -0
  135. package/dist/src/local-first/Error.d.ts.map +1 -0
  136. package/dist/src/local-first/Error.js +6 -0
  137. package/dist/src/local-first/Evolu.d.ts +330 -285
  138. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  139. package/dist/src/local-first/Evolu.js +472 -402
  140. package/dist/src/local-first/LocalAuth.d.ts +10 -10
  141. package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
  142. package/dist/src/local-first/LocalAuth.js +6 -1
  143. package/dist/src/local-first/Owner.d.ts +70 -27
  144. package/dist/src/local-first/Owner.d.ts.map +1 -1
  145. package/dist/src/local-first/Owner.js +27 -14
  146. package/dist/src/local-first/Protocol.d.ts +58 -50
  147. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  148. package/dist/src/local-first/Protocol.js +253 -155
  149. package/dist/src/local-first/Query.d.ts +133 -43
  150. package/dist/src/local-first/Query.d.ts.map +1 -1
  151. package/dist/src/local-first/Query.js +139 -90
  152. package/dist/src/local-first/Relay.d.ts +18 -38
  153. package/dist/src/local-first/Relay.d.ts.map +1 -1
  154. package/dist/src/local-first/Relay.js +59 -171
  155. package/dist/src/local-first/Schema.d.ts +139 -144
  156. package/dist/src/local-first/Schema.d.ts.map +1 -1
  157. package/dist/src/local-first/Schema.js +78 -163
  158. package/dist/src/local-first/Shared.d.ts +125 -0
  159. package/dist/src/local-first/Shared.d.ts.map +1 -0
  160. package/dist/src/local-first/Shared.js +569 -0
  161. package/dist/src/local-first/Storage.d.ts +50 -53
  162. package/dist/src/local-first/Storage.d.ts.map +1 -1
  163. package/dist/src/local-first/Storage.js +121 -173
  164. package/dist/src/local-first/Timestamp.d.ts +28 -35
  165. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  166. package/dist/src/local-first/Timestamp.js +24 -25
  167. package/dist/src/local-first/index.d.ts +2 -15
  168. package/dist/src/local-first/index.d.ts.map +1 -1
  169. package/dist/src/local-first/index.js +2 -15
  170. package/package.json +36 -26
  171. package/src/Array.ts +467 -106
  172. package/src/Assert.ts +87 -12
  173. package/src/BigInt.ts +7 -1
  174. package/src/Brand.ts +41 -12
  175. package/src/Buffer.ts +10 -4
  176. package/src/Cache.ts +11 -2
  177. package/src/Callbacks.ts +33 -23
  178. package/src/Console.ts +567 -178
  179. package/src/Crypto.ts +111 -62
  180. package/src/Eq.ts +30 -3
  181. package/src/Error.ts +50 -34
  182. package/src/Function.ts +140 -68
  183. package/src/Identicon.ts +7 -1
  184. package/src/Lookup.ts +415 -0
  185. package/src/Microtask.ts +58 -0
  186. package/src/Number.ts +40 -7
  187. package/src/Object.ts +155 -16
  188. package/src/Option.ts +74 -0
  189. package/src/Order.ts +11 -3
  190. package/src/Platform.ts +43 -7
  191. package/src/Polyfills.ts +465 -0
  192. package/src/Random.ts +25 -11
  193. package/src/Redacted.ts +8 -4
  194. package/src/Ref.ts +72 -21
  195. package/src/RefCount.ts +170 -0
  196. package/src/Relation.ts +188 -136
  197. package/src/Resource.ts +864 -0
  198. package/src/Result.ts +548 -381
  199. package/src/Schedule.ts +1471 -0
  200. package/src/Set.ts +247 -0
  201. package/src/Skiplist.ts +7 -1
  202. package/src/Sqlite.ts +409 -304
  203. package/src/Store.ts +94 -48
  204. package/src/String.ts +6 -0
  205. package/src/Task.ts +3685 -740
  206. package/src/Test.ts +162 -0
  207. package/src/Time.ts +348 -145
  208. package/src/Tracer.ts +54 -0
  209. package/src/Type.ts +1064 -630
  210. package/src/Types.ts +202 -7
  211. package/src/WebSocket.ts +311 -178
  212. package/src/Worker.ts +527 -147
  213. package/src/index.ts +74 -3
  214. package/src/local-first/Db.ts +871 -597
  215. package/src/local-first/Error.ts +17 -0
  216. package/src/local-first/Evolu.ts +884 -823
  217. package/src/local-first/LocalAuth.ts +10 -10
  218. package/src/local-first/Owner.ts +94 -42
  219. package/src/local-first/Protocol.ts +440 -303
  220. package/src/local-first/Query.ts +243 -194
  221. package/src/local-first/Relay.ts +91 -252
  222. package/src/local-first/Schema.ts +284 -394
  223. package/src/local-first/Shared.ts +932 -0
  224. package/src/local-first/Storage.ts +209 -282
  225. package/src/local-first/Timestamp.ts +38 -45
  226. package/src/local-first/index.ts +2 -16
  227. package/LICENSE +0 -21
  228. package/dist/src/Instances.d.ts +0 -34
  229. package/dist/src/Instances.d.ts.map +0 -1
  230. package/dist/src/Instances.js +0 -44
  231. package/dist/src/Resources.d.ts +0 -118
  232. package/dist/src/Resources.d.ts.map +0 -1
  233. package/dist/src/Resources.js +0 -197
  234. package/dist/src/local-first/Platform.d.ts +0 -25
  235. package/dist/src/local-first/Platform.d.ts.map +0 -1
  236. package/dist/src/local-first/Platform.js +0 -1
  237. package/dist/src/local-first/Public.d.ts +0 -22
  238. package/dist/src/local-first/Public.d.ts.map +0 -1
  239. package/dist/src/local-first/Public.js +0 -15
  240. package/dist/src/local-first/PublicKysely.d.ts +0 -148
  241. package/dist/src/local-first/PublicKysely.d.ts.map +0 -1
  242. package/dist/src/local-first/PublicKysely.js +0 -184
  243. package/dist/src/local-first/Sync.d.ts +0 -112
  244. package/dist/src/local-first/Sync.d.ts.map +0 -1
  245. package/dist/src/local-first/Sync.js +0 -529
  246. package/src/Instances.ts +0 -90
  247. package/src/Resources.ts +0 -367
  248. package/src/local-first/Platform.ts +0 -27
  249. package/src/local-first/Public.ts +0 -42
  250. package/src/local-first/PublicKysely.ts +0 -239
  251. package/src/local-first/Sync.ts +0 -960
@@ -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
- * - Iteration helpers (`forEach`, iterator) allow treating the structure as a set
15
- * of pairs when needed.
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*` / `get*` each perform a constant number of Map/Set
20
- * operations (O(1) expected).
21
- * - `deleteA` and `deleteB` are O(d) where d is the number of associated elements
22
- * (the degree). This is optimal because every associated pair must be touched
23
- * once.
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
- * Object identity:
49
+ * Use the default identity semantics.
26
50
  *
27
- * - Elements are compared by reference (standard Map / Set semantics). Structural
28
- * hashing of objects in JavaScript is non-trivial, can be expensive, and
29
- * collision-prone if done naively. Prefer using stable primitive identifiers
30
- * (ids, strings) instead of attempting to hash full object structures.
31
- * - If structural equivalence is truly required, wrap objects in an adapter that
32
- * supplies a canonical hash/id and stores/retrieves the original objects
33
- * separately. This is a rare need; avoid unless you have clear requirements.
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
- /** Gets all B elements related to an A element. */
49
- readonly getB: (a: A) => ReadonlySet<B> | undefined;
90
+ /** Removes all pairs containing the given A element. */
91
+ readonly removeByA: (a: A) => boolean;
50
92
 
51
- /** Gets all A elements related to a B element. */
52
- readonly getA: (b: B) => ReadonlySet<A> | undefined;
93
+ /** Removes all pairs containing the given B element. */
94
+ readonly removeByB: (b: B) => boolean;
53
95
 
54
96
  /**
55
- * Iterates over each pair in the relation (in insertion order of A elements,
56
- * then B elements per A).
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 forEach: (callback: (a: A, b: B) => void) => void;
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 const createRelation = <A, B>(): Relation<A, B> => {
96
- const aToB = new Map<A, Set<B>>();
97
- const bToA = new Map<B, Set<A>>();
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 relation: Relation<A, B> = {
101
- add(a: A, b: B) {
102
- let bSet = aToB.get(a);
103
- if (bSet?.has(b)) return false;
104
- if (!bSet) {
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
- let aSet = bToA.get(b);
111
- if (!aSet) {
112
- aSet = new Set<A>();
113
- bToA.set(b, aSet);
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
- remove(a: A, b: B) {
121
- const bSet = aToB.get(a);
122
- if (!bSet?.has(b)) return false;
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
- bSet.delete(b);
125
- if (bSet.size === 0) {
126
- aToB.delete(a);
127
- }
188
+ relatedA.delete(a);
189
+ if (relatedA.size === 0) {
190
+ aByB.delete(b);
191
+ }
192
+
193
+ sizeInternal--;
194
+ };
128
195
 
129
- const aSet = bToA.get(b);
130
- assert(aSet, "Relation mapping inconsistency");
196
+ return {
197
+ add: (a, b) => {
198
+ const canonicalA = bByA.getKey(a) ?? a;
199
+ const canonicalB = aByB.getKey(b) ?? b;
131
200
 
132
- aSet.delete(a);
133
- if (aSet.size === 0) {
134
- bToA.delete(b);
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
- sizeInternal--;
137
- return true;
138
- },
207
+ relatedB.add(canonicalB);
139
208
 
140
- getB(a: A): ReadonlySet<B> | undefined {
141
- return aToB.get(a);
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
- getA(b: B): ReadonlySet<A> | undefined {
145
- return bToA.get(b);
216
+ sizeInternal++;
217
+ return true;
146
218
  },
147
219
 
148
- forEach(callback: (a: A, b: B) => void) {
149
- for (const [a, bSet] of aToB) {
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
- [Symbol.iterator](): IterableIterator<readonly [A, B]> {
155
- const iterator = function* () {
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
- has(a: A, b: B) {
166
- const bSet = aToB.get(a);
167
- return bSet?.has(b) ?? false;
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
- hasA(a: A) {
171
- return aToB.has(a);
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
- hasB(b: B) {
175
- return bToA.has(b);
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
- deleteA(a: A) {
179
- const bSet = aToB.get(a);
180
- if (!bSet) return false;
181
- const removed = bSet.size;
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
- deleteB(b: B) {
197
- const aSet = bToA.get(b);
198
- if (!aSet) return false;
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
- clear() {
215
- aToB.clear();
216
- bToA.clear();
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
- size() {
229
- return sizeInternal;
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
- return relation;
282
+ const assertRelationMappingConsistency: (
283
+ condition: unknown,
284
+ ) => asserts condition = (condition) => {
285
+ assert(condition, "Relation mapping inconsistency");
234
286
  };