@evolu/common 8.7.0 → 8.9.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 (302) hide show
  1. package/dist/src/Array.d.ts +21 -21
  2. package/dist/src/Array.js +5 -5
  3. package/dist/src/Assert.d.ts +498 -141
  4. package/dist/src/Assert.d.ts.map +1 -1
  5. package/dist/src/Assert.js +648 -99
  6. package/dist/src/{Buffer.d.ts → Binary.d.ts} +85 -12
  7. package/dist/src/Binary.d.ts.map +1 -0
  8. package/dist/src/{Buffer.js → Binary.js} +206 -24
  9. package/dist/src/Console.d.ts +2 -10
  10. package/dist/src/Console.d.ts.map +1 -1
  11. package/dist/src/Console.js +6 -12
  12. package/dist/src/Crypto.d.ts +5 -5
  13. package/dist/src/Crypto.js +1 -1
  14. package/dist/src/Eq.d.ts +35 -40
  15. package/dist/src/Eq.d.ts.map +1 -1
  16. package/dist/src/Eq.js +51 -87
  17. package/dist/src/Function.d.ts +9 -14
  18. package/dist/src/Function.d.ts.map +1 -1
  19. package/dist/src/Function.js +7 -9
  20. package/dist/src/Http.d.ts +6 -7
  21. package/dist/src/Http.d.ts.map +1 -1
  22. package/dist/src/LeakDetector.d.ts.map +1 -1
  23. package/dist/src/LeakDetector.js +1 -0
  24. package/dist/src/LockManager.d.ts +1 -1
  25. package/dist/src/LockManager.js +1 -1
  26. package/dist/src/Lookup.d.ts +3 -2
  27. package/dist/src/Lookup.d.ts.map +1 -1
  28. package/dist/src/Lookup.js +4 -3
  29. package/dist/src/Number.d.ts +1 -1
  30. package/dist/src/Number.d.ts.map +1 -1
  31. package/dist/src/Number.js +1 -1
  32. package/dist/src/Object.d.ts +4 -4
  33. package/dist/src/Object.d.ts.map +1 -1
  34. package/dist/src/Object.js +5 -4
  35. package/dist/src/Option.d.ts +1 -2
  36. package/dist/src/Option.d.ts.map +1 -1
  37. package/dist/src/Option.js +1 -2
  38. package/dist/src/Platform.js +2 -0
  39. package/dist/src/Polyfills.d.ts +6 -0
  40. package/dist/src/Polyfills.d.ts.map +1 -1
  41. package/dist/src/Polyfills.js +10 -1
  42. package/dist/src/Random.d.ts +1 -1
  43. package/dist/src/Result.d.ts +49 -54
  44. package/dist/src/Result.d.ts.map +1 -1
  45. package/dist/src/Result.js +17 -22
  46. package/dist/src/Set.d.ts +6 -6
  47. package/dist/src/Set.js +2 -2
  48. package/dist/src/Sqlite.js +1 -1
  49. package/dist/src/String.js +3 -2
  50. package/dist/src/Task.d.ts +32 -32
  51. package/dist/src/Task.d.ts.map +1 -1
  52. package/dist/src/Task.js +20 -22
  53. package/dist/src/Test.d.ts +31 -3
  54. package/dist/src/Test.d.ts.map +1 -1
  55. package/dist/src/Test.js +46 -3
  56. package/dist/src/Time.d.ts +3 -11
  57. package/dist/src/Time.d.ts.map +1 -1
  58. package/dist/src/Time.js +10 -18
  59. package/dist/src/Type.d.ts +87 -85
  60. package/dist/src/Type.d.ts.map +1 -1
  61. package/dist/src/Type.js +70 -63
  62. package/dist/src/Types.d.ts +12 -12
  63. package/dist/src/Types.js +1 -1
  64. package/dist/src/WebSocket.d.ts +4 -7
  65. package/dist/src/WebSocket.d.ts.map +1 -1
  66. package/dist/src/Worker.d.ts +1 -1
  67. package/dist/src/Worker.js +7 -7
  68. package/dist/src/index.d.ts +1 -1
  69. package/dist/src/index.js +1 -1
  70. package/dist/src/intl/_en.d.ts.map +1 -1
  71. package/dist/src/intl/_en.js +1 -1
  72. package/dist/src/intl/ar.d.ts.map +1 -1
  73. package/dist/src/intl/ar.js +1 -1
  74. package/dist/src/intl/bn.d.ts.map +1 -1
  75. package/dist/src/intl/bn.js +1 -1
  76. package/dist/src/intl/ca.d.ts.map +1 -1
  77. package/dist/src/intl/ca.js +1 -1
  78. package/dist/src/intl/cs.d.ts.map +1 -1
  79. package/dist/src/intl/cs.js +1 -1
  80. package/dist/src/intl/da.d.ts.map +1 -1
  81. package/dist/src/intl/da.js +1 -1
  82. package/dist/src/intl/de.d.ts.map +1 -1
  83. package/dist/src/intl/de.js +1 -1
  84. package/dist/src/intl/el.d.ts.map +1 -1
  85. package/dist/src/intl/el.js +1 -1
  86. package/dist/src/intl/es.d.ts.map +1 -1
  87. package/dist/src/intl/es.js +1 -1
  88. package/dist/src/intl/fa.d.ts.map +1 -1
  89. package/dist/src/intl/fa.js +1 -1
  90. package/dist/src/intl/fi.d.ts.map +1 -1
  91. package/dist/src/intl/fi.js +1 -1
  92. package/dist/src/intl/fil.d.ts.map +1 -1
  93. package/dist/src/intl/fil.js +1 -1
  94. package/dist/src/intl/fr.d.ts.map +1 -1
  95. package/dist/src/intl/fr.js +1 -1
  96. package/dist/src/intl/he.d.ts.map +1 -1
  97. package/dist/src/intl/he.js +1 -1
  98. package/dist/src/intl/hi.d.ts.map +1 -1
  99. package/dist/src/intl/hi.js +1 -1
  100. package/dist/src/intl/hr.d.ts.map +1 -1
  101. package/dist/src/intl/hr.js +1 -1
  102. package/dist/src/intl/hu.d.ts.map +1 -1
  103. package/dist/src/intl/hu.js +1 -1
  104. package/dist/src/intl/id.d.ts.map +1 -1
  105. package/dist/src/intl/id.js +1 -1
  106. package/dist/src/intl/it.d.ts.map +1 -1
  107. package/dist/src/intl/it.js +1 -1
  108. package/dist/src/intl/ja.d.ts.map +1 -1
  109. package/dist/src/intl/ja.js +1 -1
  110. package/dist/src/intl/ko.d.ts.map +1 -1
  111. package/dist/src/intl/ko.js +1 -1
  112. package/dist/src/intl/ml.d.ts.map +1 -1
  113. package/dist/src/intl/ml.js +1 -1
  114. package/dist/src/intl/mr.d.ts.map +1 -1
  115. package/dist/src/intl/mr.js +1 -1
  116. package/dist/src/intl/ms.d.ts.map +1 -1
  117. package/dist/src/intl/ms.js +1 -1
  118. package/dist/src/intl/nb.d.ts.map +1 -1
  119. package/dist/src/intl/nb.js +1 -1
  120. package/dist/src/intl/nl.d.ts.map +1 -1
  121. package/dist/src/intl/nl.js +1 -1
  122. package/dist/src/intl/pa.d.ts.map +1 -1
  123. package/dist/src/intl/pa.js +1 -1
  124. package/dist/src/intl/pl.d.ts.map +1 -1
  125. package/dist/src/intl/pl.js +1 -1
  126. package/dist/src/intl/pt-BR.d.ts.map +1 -1
  127. package/dist/src/intl/pt-BR.js +1 -1
  128. package/dist/src/intl/pt.d.ts.map +1 -1
  129. package/dist/src/intl/pt.js +1 -1
  130. package/dist/src/intl/ro.d.ts.map +1 -1
  131. package/dist/src/intl/ro.js +1 -1
  132. package/dist/src/intl/sk.d.ts.map +1 -1
  133. package/dist/src/intl/sk.js +1 -1
  134. package/dist/src/intl/sl.d.ts.map +1 -1
  135. package/dist/src/intl/sl.js +1 -1
  136. package/dist/src/intl/sv.d.ts.map +1 -1
  137. package/dist/src/intl/sv.js +1 -1
  138. package/dist/src/intl/sw.d.ts.map +1 -1
  139. package/dist/src/intl/sw.js +1 -1
  140. package/dist/src/intl/ta.d.ts.map +1 -1
  141. package/dist/src/intl/ta.js +1 -1
  142. package/dist/src/intl/te.d.ts.map +1 -1
  143. package/dist/src/intl/te.js +1 -1
  144. package/dist/src/intl/th.d.ts.map +1 -1
  145. package/dist/src/intl/th.js +1 -1
  146. package/dist/src/intl/tr.d.ts.map +1 -1
  147. package/dist/src/intl/tr.js +1 -1
  148. package/dist/src/intl/uk.d.ts.map +1 -1
  149. package/dist/src/intl/uk.js +1 -1
  150. package/dist/src/intl/ur.d.ts.map +1 -1
  151. package/dist/src/intl/ur.js +1 -1
  152. package/dist/src/intl/vi.d.ts.map +1 -1
  153. package/dist/src/intl/vi.js +1 -1
  154. package/dist/src/intl/zh-CN.d.ts.map +1 -1
  155. package/dist/src/intl/zh-CN.js +1 -1
  156. package/dist/src/intl/zh-TW.d.ts.map +1 -1
  157. package/dist/src/intl/zh-TW.js +1 -1
  158. package/dist/src/local-first/Evolu.d.ts +12 -12
  159. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  160. package/dist/src/local-first/Evolu.js +2 -2
  161. package/dist/src/local-first/Protocol.d.ts +14 -76
  162. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  163. package/dist/src/local-first/Protocol.js +29 -206
  164. package/dist/src/local-first/Query.d.ts +11 -11
  165. package/dist/src/local-first/Query.js +6 -6
  166. package/dist/src/local-first/Relay.d.ts +2 -2
  167. package/dist/src/local-first/Schema.d.ts +3 -5
  168. package/dist/src/local-first/Schema.d.ts.map +1 -1
  169. package/dist/src/local-first/Schema.js +2 -4
  170. package/dist/src/local-first/Shared.d.ts.map +1 -1
  171. package/dist/src/local-first/Shared.js +5 -5
  172. package/dist/src/local-first/Storage.js +4 -4
  173. package/dist/src/local-first/Timestamp.d.ts +9 -0
  174. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  175. package/dist/src/local-first/Timestamp.js +13 -10
  176. package/package.json +7 -4
  177. package/src/Array.test.ts +809 -0
  178. package/src/Array.ts +21 -21
  179. package/src/Assert.test.ts +1232 -0
  180. package/src/Assert.ts +1023 -227
  181. package/src/BigInt.test.ts +28 -0
  182. package/src/Binary.test.ts +1128 -0
  183. package/src/{Buffer.ts → Binary.ts} +273 -46
  184. package/src/Brand.test.ts +90 -0
  185. package/src/Cache.test.ts +183 -0
  186. package/src/Callbacks.test.ts +66 -0
  187. package/src/Console.test.ts +809 -0
  188. package/src/Console.ts +9 -17
  189. package/src/Crypto.test.ts +138 -0
  190. package/src/Crypto.ts +5 -5
  191. package/src/Eq.test.ts +333 -0
  192. package/src/Eq.ts +110 -176
  193. package/src/Error.test.ts +118 -0
  194. package/src/Function.test.ts +204 -0
  195. package/src/Function.ts +10 -15
  196. package/src/Http.test.ts +816 -0
  197. package/src/Http.ts +6 -7
  198. package/src/LeakDetector.test.ts +210 -0
  199. package/src/LeakDetector.ts +1 -0
  200. package/src/LockManager.ts +2 -2
  201. package/src/Lookup.test.ts +418 -0
  202. package/src/Lookup.ts +4 -3
  203. package/src/Microtask.test.ts +140 -0
  204. package/src/Number.test.ts +218 -0
  205. package/src/Number.ts +2 -2
  206. package/src/Object.test.ts +293 -0
  207. package/src/Object.ts +5 -4
  208. package/src/Option.test.ts +104 -0
  209. package/src/Option.ts +1 -2
  210. package/src/Order.test.ts +56 -0
  211. package/src/Platform.ts +2 -0
  212. package/src/Polyfills.ts +9 -4
  213. package/src/Random.test.ts +89 -0
  214. package/src/Random.ts +1 -1
  215. package/src/Ref.test.ts +139 -0
  216. package/src/RefCount.test.ts +184 -0
  217. package/src/Relation.test.ts +505 -0
  218. package/src/Resource.test.ts +4623 -0
  219. package/src/Result.test.ts +1829 -0
  220. package/src/Result.ts +49 -54
  221. package/src/Schedule.test.ts +1554 -0
  222. package/src/Set.test.ts +279 -0
  223. package/src/Set.ts +6 -6
  224. package/src/Sqlite.ts +1 -1
  225. package/src/Store.test.ts +288 -0
  226. package/src/String.test.ts +54 -0
  227. package/src/String.ts +3 -3
  228. package/src/Task.test.ts +10820 -0
  229. package/src/Task.ts +38 -43
  230. package/src/Test.test.ts +88 -0
  231. package/src/Test.ts +49 -3
  232. package/src/Time.test.ts +926 -0
  233. package/src/Time.ts +13 -21
  234. package/src/Type.base64.test.ts +88 -0
  235. package/src/Type.test.ts +20465 -0
  236. package/src/Type.ts +114 -111
  237. package/src/Types.test.ts +324 -0
  238. package/src/Types.ts +13 -13
  239. package/src/WebSocket.ts +4 -7
  240. package/src/Worker.test.ts +755 -0
  241. package/src/Worker.ts +13 -18
  242. package/src/index.ts +1 -1
  243. package/src/intl/_en.ts +1 -1
  244. package/src/intl/ar.ts +1 -1
  245. package/src/intl/bn.ts +1 -1
  246. package/src/intl/ca.ts +1 -1
  247. package/src/intl/cs.ts +1 -1
  248. package/src/intl/da.ts +1 -1
  249. package/src/intl/de.ts +1 -1
  250. package/src/intl/el.ts +1 -1
  251. package/src/intl/es.ts +1 -1
  252. package/src/intl/fa.ts +1 -1
  253. package/src/intl/fi.ts +1 -1
  254. package/src/intl/fil.ts +1 -1
  255. package/src/intl/fr.ts +1 -1
  256. package/src/intl/he.ts +1 -1
  257. package/src/intl/hi.ts +1 -1
  258. package/src/intl/hr.ts +1 -1
  259. package/src/intl/hu.ts +1 -1
  260. package/src/intl/id.ts +1 -1
  261. package/src/intl/intl.test.ts +3106 -0
  262. package/src/intl/it.ts +1 -1
  263. package/src/intl/ja.ts +1 -1
  264. package/src/intl/ko.ts +1 -1
  265. package/src/intl/ml.ts +1 -1
  266. package/src/intl/mr.ts +1 -1
  267. package/src/intl/ms.ts +1 -1
  268. package/src/intl/nb.ts +1 -1
  269. package/src/intl/nl.ts +1 -1
  270. package/src/intl/pa.ts +1 -1
  271. package/src/intl/pl.ts +1 -1
  272. package/src/intl/pt-BR.ts +1 -1
  273. package/src/intl/pt.ts +1 -1
  274. package/src/intl/ro.ts +1 -1
  275. package/src/intl/sk.ts +1 -1
  276. package/src/intl/sl.ts +1 -1
  277. package/src/intl/sv.ts +1 -1
  278. package/src/intl/sw.ts +1 -1
  279. package/src/intl/ta.ts +1 -1
  280. package/src/intl/te.ts +1 -1
  281. package/src/intl/th.ts +1 -1
  282. package/src/intl/tr.ts +1 -1
  283. package/src/intl/uk.ts +1 -1
  284. package/src/intl/ur.ts +1 -1
  285. package/src/intl/vi.ts +1 -1
  286. package/src/intl/zh-CN.ts +1 -1
  287. package/src/intl/zh-TW.ts +1 -1
  288. package/src/local-first/Evolu.test.ts +1921 -0
  289. package/src/local-first/Evolu.ts +18 -14
  290. package/src/local-first/Owner.test.ts +83 -0
  291. package/src/local-first/Protocol.test.ts +1037 -0
  292. package/src/local-first/Protocol.ts +58 -261
  293. package/src/local-first/Query.test.ts +349 -0
  294. package/src/local-first/Query.ts +11 -11
  295. package/src/local-first/Relay.ts +2 -2
  296. package/src/local-first/Schema.ts +3 -5
  297. package/src/local-first/Shared.test.ts +1828 -0
  298. package/src/local-first/Shared.ts +9 -8
  299. package/src/local-first/Storage.ts +4 -4
  300. package/src/local-first/Timestamp.test.ts +374 -0
  301. package/src/local-first/Timestamp.ts +22 -10
  302. package/dist/src/Buffer.d.ts.map +0 -1
@@ -0,0 +1,505 @@
1
+ import { describe, it } from "node:test";
2
+ import {
3
+ assertEqual,
4
+ assertFalse,
5
+ assertSame,
6
+ assertThrowsInstanceOf,
7
+ assertTrue,
8
+ } from "./Assert.ts";
9
+
10
+ import {
11
+ createRefCountedRelation,
12
+ createRelation,
13
+ type RefCountedRelation,
14
+ } from "./Relation.ts";
15
+ import { assertType, type NonNegativeInt, type PositiveInt } from "./Type.ts";
16
+
17
+ describe("Relation", () => {
18
+ it("add and iterateA/iterateB", () => {
19
+ const relation = createRelation<string, number>();
20
+
21
+ relation.add("a", 1);
22
+ relation.add("a", 2);
23
+ relation.add("b", 2);
24
+
25
+ assertEqual([...relation.iterateA(1)], ["a"]);
26
+ assertEqual([...relation.iterateA(2)], ["a", "b"]);
27
+ assertEqual([...relation.iterateA(3)], []);
28
+
29
+ assertEqual([...relation.iterateB("a")], [1, 2]);
30
+ assertEqual([...relation.iterateB("b")], [2]);
31
+ assertEqual([...relation.iterateB("c")], []);
32
+ });
33
+
34
+ it("has", () => {
35
+ const relation = createRelation<string, number>();
36
+
37
+ relation.add("a", 1);
38
+ relation.add("b", 2);
39
+
40
+ assertTrue(relation.has("a", 1));
41
+ assertTrue(relation.has("b", 2));
42
+ assertFalse(relation.has("a", 2));
43
+ assertFalse(relation.has("b", 1));
44
+ assertFalse(relation.has("c", 1));
45
+ });
46
+
47
+ it("hasA and hasB", () => {
48
+ const relation = createRelation<string, number>();
49
+
50
+ relation.add("a", 1);
51
+ relation.add("a", 2);
52
+ relation.add("b", 2);
53
+
54
+ assertTrue(relation.hasA("a"));
55
+ assertTrue(relation.hasA("b"));
56
+ assertFalse(relation.hasA("c"));
57
+
58
+ assertTrue(relation.hasB(1));
59
+ assertTrue(relation.hasB(2));
60
+ assertFalse(relation.hasB(3));
61
+ });
62
+
63
+ it("remove deletes an existing pair", () => {
64
+ const relation = createRelation<string, number>();
65
+
66
+ relation.add("a", 1);
67
+ relation.add("a", 2);
68
+ relation.add("b", 2);
69
+
70
+ assertTrue(relation.remove("a", 1));
71
+ assertFalse(relation.has("a", 1));
72
+ assertTrue(relation.has("a", 2));
73
+ assertTrue(relation.has("b", 2));
74
+ });
75
+
76
+ it("remove returns false for missing pairs", () => {
77
+ const relation = createRelation<string, number>();
78
+
79
+ assertFalse(relation.remove("a", 3));
80
+ assertFalse(relation.remove("c", 1));
81
+ });
82
+
83
+ it("remove deletes empty side indexes", () => {
84
+ const relation = createRelation<string, number>();
85
+
86
+ relation.add("a", 1);
87
+ relation.add("a", 2);
88
+ relation.add("b", 2);
89
+
90
+ assertTrue(relation.remove("a", 1));
91
+ assertTrue(relation.remove("a", 2));
92
+ assertFalse(relation.hasA("a"));
93
+
94
+ assertTrue(relation.remove("b", 2));
95
+ assertFalse(relation.hasB(2));
96
+ });
97
+
98
+ it("removeByA", () => {
99
+ const relation = createRelation<string, number>();
100
+
101
+ relation.add("a", 1);
102
+ relation.add("a", 2);
103
+ relation.add("b", 2);
104
+ relation.add("c", 3);
105
+
106
+ assertTrue(relation.removeByA("a"));
107
+ assertFalse(relation.hasA("a"));
108
+ assertFalse(relation.hasB(1));
109
+ assertTrue(relation.hasB(2));
110
+ assertTrue(relation.hasA("b"));
111
+
112
+ assertFalse(relation.removeByA("nonexistent"));
113
+ });
114
+
115
+ it("removeByB", () => {
116
+ const relation = createRelation<string, number>();
117
+
118
+ relation.add("a", 1);
119
+ relation.add("a", 2);
120
+ relation.add("b", 2);
121
+ relation.add("c", 3);
122
+
123
+ assertTrue(relation.removeByB(2));
124
+ assertFalse(relation.hasB(2));
125
+ assertTrue(relation.hasA("a"));
126
+ assertFalse(relation.hasA("b"));
127
+
128
+ assertFalse(relation.removeByB(99));
129
+ });
130
+
131
+ it("removeByA and removeByB remove all related pairs", () => {
132
+ const relation = createRelation<string, number>();
133
+
134
+ relation.add("a", 1);
135
+ relation.add("a", 2);
136
+ relation.add("a", 3);
137
+ relation.add("b", 3);
138
+ relation.add("c", 3);
139
+
140
+ // Bulk removal mutates the same internal indexes it is iterating, so this
141
+ // guards against implementations that accidentally skip later pairs.
142
+ assertTrue(relation.removeByA("a"));
143
+ assertFalse(relation.hasA("a"));
144
+ assertEqual([...relation.iterateA(1)], []);
145
+ assertEqual([...relation.iterateA(2)], []);
146
+ assertEqual([...relation.iterateA(3)], ["b", "c"]);
147
+ assertEqual(relation.size(), 2);
148
+
149
+ assertTrue(relation.removeByB(3));
150
+ assertFalse(relation.hasB(3));
151
+ assertFalse(relation.hasA("b"));
152
+ assertFalse(relation.hasA("c"));
153
+ assertEqual(relation.aCount(), 0);
154
+ assertEqual(relation.bCount(), 0);
155
+ assertEqual(relation.size(), 0);
156
+ });
157
+
158
+ it("clear", () => {
159
+ const relation = createRelation<string, number>();
160
+
161
+ relation.add("a", 1);
162
+ relation.add("b", 2);
163
+
164
+ relation.clear();
165
+
166
+ assertFalse(relation.hasA("a"));
167
+ assertFalse(relation.hasA("b"));
168
+ assertFalse(relation.hasB(1));
169
+ assertFalse(relation.hasB(2));
170
+ assertEqual([...relation.iterateB("a")], []);
171
+ assertEqual([...relation.iterateA(1)], []);
172
+ assertEqual(relation.aCount(), 0);
173
+ assertEqual(relation.bCount(), 0);
174
+ assertEqual(relation.size(), 0);
175
+ });
176
+
177
+ it("works with complex objects as A and B", () => {
178
+ interface Person {
179
+ name: string;
180
+ age: number;
181
+ }
182
+ interface City {
183
+ name: string;
184
+ country: string;
185
+ }
186
+
187
+ const relation = createRelation<Person, City>();
188
+
189
+ const alice = { name: "Alice", age: 30 };
190
+ const bob = { name: "Bob", age: 25 };
191
+ const newyork = { name: "New York", country: "USA" };
192
+ const london = { name: "London", country: "UK" };
193
+
194
+ relation.add(alice, newyork);
195
+ relation.add(alice, london);
196
+ relation.add(bob, london);
197
+
198
+ assertTrue(relation.has(alice, newyork));
199
+ assertTrue(relation.has(alice, london));
200
+ assertTrue(relation.has(bob, london));
201
+ assertFalse(relation.has(bob, newyork));
202
+
203
+ assertEqual(new Set(relation.iterateB(alice)), new Set([newyork, london]));
204
+ assertEqual(new Set(relation.iterateA(london)), new Set([alice, bob]));
205
+
206
+ assertTrue(relation.remove(alice, newyork));
207
+ assertFalse(relation.has(alice, newyork));
208
+
209
+ assertTrue(relation.removeByA(alice));
210
+ assertFalse(relation.hasA(alice));
211
+ assertFalse(relation.has(alice, london));
212
+ });
213
+
214
+ it("duplicate adds and return value", () => {
215
+ const relation = createRelation<string, number>();
216
+ // new
217
+ assertTrue(relation.add("a", 1));
218
+ // duplicate
219
+ assertFalse(relation.add("a", 1));
220
+ // new B for existing A
221
+ assertTrue(relation.add("a", 2));
222
+ // new A referencing existing B
223
+ assertTrue(relation.add("b", 2));
224
+ assertEqual(relation.bCountForA("a"), 2);
225
+ assertEqual(relation.aCountForB(1), 1);
226
+ assertEqual(relation.aCountForB(2), 2);
227
+ });
228
+
229
+ it("directional counts return zero for missing keys", () => {
230
+ const relation = createRelation<string, number>();
231
+
232
+ assertEqual(relation.bCountForA("missing"), 0);
233
+ assertEqual(relation.aCountForB(99), 0);
234
+ });
235
+
236
+ it("iterator yields pairs", () => {
237
+ const relation = createRelation<string, number>();
238
+ relation.add("a", 1);
239
+ relation.add("a", 2);
240
+ relation.add("b", 2);
241
+ const pairs = [...relation];
242
+ assertEqual(pairs, [
243
+ ["a", 1],
244
+ ["a", 2],
245
+ ["b", 2],
246
+ ]);
247
+ });
248
+
249
+ it("counts grow when adding new pairs", () => {
250
+ const relation = createRelation<string, number>();
251
+
252
+ assertEqual(relation.aCount(), 0);
253
+ assertEqual(relation.bCount(), 0);
254
+ assertEqual(relation.size(), 0);
255
+
256
+ relation.add("a", 1);
257
+ assertEqual(relation.aCount(), 1);
258
+ assertEqual(relation.bCount(), 1);
259
+ assertEqual(relation.size(), 1);
260
+
261
+ relation.add("a", 2);
262
+ assertEqual(relation.aCount(), 1);
263
+ assertEqual(relation.bCount(), 2);
264
+ assertEqual(relation.size(), 2);
265
+
266
+ relation.add("b", 2);
267
+ assertEqual(relation.aCount(), 2);
268
+ assertEqual(relation.bCount(), 2);
269
+ assertEqual(relation.size(), 3);
270
+
271
+ relation.add("b", 2);
272
+ assertEqual(relation.size(), 3);
273
+ });
274
+
275
+ it("counts shrink when removing pairs", () => {
276
+ const relation = createRelation<string, number>();
277
+
278
+ relation.add("a", 1);
279
+ relation.add("a", 2);
280
+ relation.add("b", 2);
281
+
282
+ relation.remove("a", 1);
283
+ assertEqual(relation.size(), 2);
284
+
285
+ relation.removeByA("b");
286
+ assertEqual(relation.size(), 1);
287
+
288
+ relation.removeByB(2);
289
+ assertEqual(relation.size(), 0);
290
+ assertEqual(relation.aCount(), 0);
291
+ assertEqual(relation.bCount(), 0);
292
+ });
293
+
294
+ it("supports custom lookup functions with typed keys", () => {
295
+ const uint8ArrayLookup = (bytes: Uint8Array): string =>
296
+ JSON.stringify(Array.from(bytes));
297
+
298
+ const relation = createRelation({
299
+ lookupA: uint8ArrayLookup,
300
+ lookupB: uint8ArrayLookup,
301
+ });
302
+
303
+ const a1 = new Uint8Array([1, 2, 3]);
304
+ const a2 = new Uint8Array([1, 2, 3]);
305
+ const b1 = new Uint8Array([4, 5, 6]);
306
+ const b2 = new Uint8Array([4, 5, 6]);
307
+
308
+ assertTrue(relation.add(a1, b1));
309
+ assertFalse(relation.add(a2, b2));
310
+ assertTrue(relation.has(a2, b2));
311
+ assertEqual([...relation.iterateB(a2)], [b1]);
312
+ assertEqual([...relation.iterateA(b2)], [a1]);
313
+
314
+ // @ts-expect-error custom lookup restricts A to Uint8Array
315
+ relation.add("a", b1);
316
+ // @ts-expect-error custom lookup restricts B to Uint8Array
317
+ relation.add(a1, "b");
318
+ });
319
+ });
320
+
321
+ describe("RefCountedRelation", () => {
322
+ it("types expose canonical changes with precise count types", () => {
323
+ const relation = createRefCountedRelation<string, number>();
324
+
325
+ assertType<typeof relation, RefCountedRelation<string, number>>();
326
+ {
327
+ const actual = relation.increment("a", 1).count;
328
+ assertType<typeof actual, PositiveInt>();
329
+ }
330
+ {
331
+ const actual = relation.decrement("a", 1).count;
332
+ assertType<typeof actual, NonNegativeInt>();
333
+ }
334
+ });
335
+
336
+ it("increments and decrements pair counts while indexing both directions", () => {
337
+ const relation = createRefCountedRelation<string, number>();
338
+
339
+ assertEqual(relation.increment("a", 1), { a: "a", b: 1, count: 1 });
340
+ assertEqual(relation.increment("a", 1), { a: "a", b: 1, count: 2 });
341
+ relation.increment("a", 2);
342
+ relation.increment("b", 2);
343
+
344
+ assertEqual(relation.getCount("a", 1), 2);
345
+ assertEqual(relation.getCount("missing", 1), 0);
346
+ assertEqual(relation.getCount("a", 3), 0);
347
+ assertEqual(relation.getAs(2), ["a", "b"]);
348
+ assertEqual(relation.getAs(3), []);
349
+ assertEqual(relation.getBs("a"), [1, 2]);
350
+ assertEqual(relation.getBs("missing"), []);
351
+ assertTrue(relation.hasA("a"));
352
+ assertFalse(relation.hasA("missing"));
353
+ assertTrue(relation.hasB(2));
354
+ assertFalse(relation.hasB(3));
355
+ assertEqual(relation.getEntries(), [
356
+ ["a", 1, 2],
357
+ ["a", 2, 1],
358
+ ["b", 2, 1],
359
+ ]);
360
+
361
+ assertEqual(relation.decrement("a", 1), { a: "a", b: 1, count: 1 });
362
+ assertEqual(relation.decrement("a", 1), { a: "a", b: 1, count: 0 });
363
+ assertEqual(relation.getCount("a", 1), 0);
364
+ assertFalse(relation.hasB(1));
365
+ assertTrue(relation.hasA("a"));
366
+ });
367
+
368
+ it("preserves first-active canonical representatives and insertion order", () => {
369
+ interface Value {
370
+ readonly id: string;
371
+ readonly label: string;
372
+ }
373
+
374
+ const firstA = { id: "a", label: "first-a" };
375
+ const equivalentA = { id: "a", label: "equivalent-a" };
376
+ const secondA = { id: "b", label: "second-a" };
377
+ const firstB = { id: "x", label: "first-b" };
378
+ const equivalentB = { id: "x", label: "equivalent-b" };
379
+ const secondB = { id: "y", label: "second-b" };
380
+ const relation = createRefCountedRelation({
381
+ lookupA: (value: Value) => value.id,
382
+ lookupB: (value: Value) => value.id,
383
+ });
384
+
385
+ relation.increment(firstA, firstB);
386
+ relation.increment(secondA, equivalentB);
387
+ const incremented = relation.increment(equivalentA, secondB);
388
+
389
+ assertSame(incremented.a, firstA);
390
+ assertSame(incremented.b, secondB);
391
+ assertEqual(incremented.count, 1);
392
+ const relatedAs = relation.getAs(equivalentB);
393
+ assertSame(relatedAs[0], firstA);
394
+ assertSame(relatedAs[1], secondA);
395
+ const relatedBs = relation.getBs(equivalentA);
396
+ assertSame(relatedBs[0], firstB);
397
+ assertSame(relatedBs[1], secondB);
398
+
399
+ relation.decrement(firstA, firstB);
400
+ const removed = relation.decrement(equivalentA, secondB);
401
+
402
+ assertSame(removed.a, firstA);
403
+ assertSame(removed.b, secondB);
404
+ assertEqual(removed.count, 0);
405
+
406
+ relation.decrement(secondA, equivalentB);
407
+ const nextA = { id: "a", label: "next-a" };
408
+ const nextB = { id: "x", label: "next-b" };
409
+ const next = relation.increment(nextA, nextB);
410
+
411
+ assertSame(next.a, nextA);
412
+ assertSame(next.b, nextB);
413
+ assertEqual(next.count, 1);
414
+ });
415
+
416
+ it("preserves canonical representatives when decrement keeps a pair retained", () => {
417
+ interface Value {
418
+ readonly id: string;
419
+ readonly label: string;
420
+ }
421
+
422
+ const firstA = { id: "a", label: "first-a" };
423
+ const firstB = { id: "b", label: "first-b" };
424
+ const relation = createRefCountedRelation({
425
+ lookupA: (value: Value) => value.id,
426
+ lookupB: (value: Value) => value.id,
427
+ });
428
+
429
+ relation.increment(firstA, firstB);
430
+ const incremented = relation.increment(
431
+ { id: "a", label: "second-a" },
432
+ { id: "b", label: "second-b" },
433
+ );
434
+
435
+ assertSame(incremented.a, firstA);
436
+ assertSame(incremented.b, firstB);
437
+ assertEqual(incremented.count, 2);
438
+
439
+ const decremented = relation.decrement(
440
+ { id: "a", label: "third-a" },
441
+ { id: "b", label: "third-b" },
442
+ );
443
+
444
+ assertSame(decremented.a, firstA);
445
+ assertSame(decremented.b, firstB);
446
+ assertEqual(decremented.count, 1);
447
+ assertEqual(relation.getCount(firstA, firstB), 1);
448
+ assertSame(relation.getAs(firstB)[0], firstA);
449
+ assertSame(relation.getBs(firstA)[0], firstB);
450
+ });
451
+
452
+ it("directional and entry reads are snapshots", () => {
453
+ const relation = createRefCountedRelation<string, number>();
454
+ relation.increment("a", 1);
455
+
456
+ const as = relation.getAs(1);
457
+ const bs = relation.getBs("a");
458
+ const entries = relation.getEntries();
459
+ relation.increment("b", 1);
460
+ relation.increment("a", 2);
461
+
462
+ assertEqual(as, ["a"]);
463
+ assertEqual(bs, [1]);
464
+ assertEqual(entries, [["a", 1, 1]]);
465
+ });
466
+
467
+ it("decrement rejects a missing pair without changing existing relations", () => {
468
+ const relation = createRefCountedRelation<string, number>();
469
+ relation.increment("a", 1);
470
+
471
+ const missingPairError = assertThrowsInstanceOf(
472
+ () => relation.decrement("a", 2),
473
+ Error,
474
+ );
475
+ assertTrue(
476
+ missingPairError.message.includes(
477
+ "RefCountedRelation pair must exist before decrement.",
478
+ ),
479
+ );
480
+ const missingAError = assertThrowsInstanceOf(
481
+ () => relation.decrement("missing", 1),
482
+ Error,
483
+ );
484
+ assertTrue(
485
+ missingAError.message.includes(
486
+ "RefCountedRelation pair must exist before decrement.",
487
+ ),
488
+ );
489
+ assertEqual(relation.getEntries(), [["a", 1, 1]]);
490
+ });
491
+
492
+ it("clear removes pair counts and both directional indexes", () => {
493
+ const relation = createRefCountedRelation<string, number>();
494
+ relation.increment("a", 1);
495
+ relation.increment("b", 2);
496
+
497
+ relation.clear();
498
+
499
+ assertEqual(relation.getEntries(), []);
500
+ assertEqual(relation.getAs(1), []);
501
+ assertEqual(relation.getBs("a"), []);
502
+ assertFalse(relation.hasA("a"));
503
+ assertFalse(relation.hasB(1));
504
+ });
505
+ });