@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.
Files changed (237) hide show
  1. package/dist/src/Array.d.ts +279 -173
  2. package/dist/src/Array.d.ts.map +1 -1
  3. package/dist/src/Array.js +105 -101
  4. package/dist/src/Assert.d.ts +44 -44
  5. package/dist/src/Assert.d.ts.map +1 -1
  6. package/dist/src/Assert.js +44 -32
  7. package/dist/src/BigInt.d.ts +6 -3
  8. package/dist/src/BigInt.d.ts.map +1 -1
  9. package/dist/src/BigInt.js +5 -2
  10. package/dist/src/Brand.d.ts +27 -40
  11. package/dist/src/Brand.d.ts.map +1 -1
  12. package/dist/src/Buffer.d.ts +27 -11
  13. package/dist/src/Buffer.d.ts.map +1 -1
  14. package/dist/src/Cache.d.ts +9 -4
  15. package/dist/src/Cache.d.ts.map +1 -1
  16. package/dist/src/Cache.js +8 -3
  17. package/dist/src/Callbacks.d.ts +30 -19
  18. package/dist/src/Callbacks.d.ts.map +1 -1
  19. package/dist/src/Console.d.ts +145 -66
  20. package/dist/src/Console.d.ts.map +1 -1
  21. package/dist/src/Console.js +91 -43
  22. package/dist/src/Crypto.d.ts +85 -42
  23. package/dist/src/Crypto.d.ts.map +1 -1
  24. package/dist/src/Crypto.js +44 -20
  25. package/dist/src/Eq.d.ts +87 -21
  26. package/dist/src/Eq.d.ts.map +1 -1
  27. package/dist/src/Eq.js +67 -19
  28. package/dist/src/Error.d.ts +5 -3
  29. package/dist/src/Error.d.ts.map +1 -1
  30. package/dist/src/Error.js +16 -0
  31. package/dist/src/Function.d.ts +119 -58
  32. package/dist/src/Function.d.ts.map +1 -1
  33. package/dist/src/Function.js +86 -43
  34. package/dist/src/Http.d.ts +394 -0
  35. package/dist/src/Http.d.ts.map +1 -0
  36. package/dist/src/Http.js +102 -0
  37. package/dist/src/Identicon.d.ts +16 -7
  38. package/dist/src/Identicon.d.ts.map +1 -1
  39. package/dist/src/Identicon.js +14 -5
  40. package/dist/src/LeakDetector.d.ts +88 -0
  41. package/dist/src/LeakDetector.d.ts.map +1 -0
  42. package/dist/src/LeakDetector.js +86 -0
  43. package/dist/src/LockManager.d.ts +18 -9
  44. package/dist/src/LockManager.d.ts.map +1 -1
  45. package/dist/src/LockManager.js +7 -8
  46. package/dist/src/Lookup.d.ts +43 -13
  47. package/dist/src/Lookup.d.ts.map +1 -1
  48. package/dist/src/Lookup.js +28 -8
  49. package/dist/src/Microtask.d.ts +1 -1
  50. package/dist/src/Microtask.d.ts.map +1 -1
  51. package/dist/src/Microtask.js +3 -7
  52. package/dist/src/Number.d.ts +49 -13
  53. package/dist/src/Number.d.ts.map +1 -1
  54. package/dist/src/Number.js +34 -5
  55. package/dist/src/Object.d.ts +160 -40
  56. package/dist/src/Object.d.ts.map +1 -1
  57. package/dist/src/Object.js +92 -46
  58. package/dist/src/Option.d.ts +10 -7
  59. package/dist/src/Option.d.ts.map +1 -1
  60. package/dist/src/Option.js +9 -6
  61. package/dist/src/Order.d.ts +21 -21
  62. package/dist/src/Order.d.ts.map +1 -1
  63. package/dist/src/Order.js +18 -21
  64. package/dist/src/Platform.d.ts +31 -0
  65. package/dist/src/Platform.d.ts.map +1 -1
  66. package/dist/src/Platform.js +98 -0
  67. package/dist/src/Random.d.ts +32 -17
  68. package/dist/src/Random.d.ts.map +1 -1
  69. package/dist/src/Random.js +12 -8
  70. package/dist/src/Redacted.d.ts +43 -29
  71. package/dist/src/Redacted.d.ts.map +1 -1
  72. package/dist/src/Redacted.js +14 -3
  73. package/dist/src/Ref.d.ts +11 -3
  74. package/dist/src/Ref.d.ts.map +1 -1
  75. package/dist/src/Ref.js +2 -2
  76. package/dist/src/RefCount.d.ts +2 -6
  77. package/dist/src/RefCount.d.ts.map +1 -1
  78. package/dist/src/RefCount.js +33 -90
  79. package/dist/src/Relation.d.ts +70 -5
  80. package/dist/src/Relation.d.ts.map +1 -1
  81. package/dist/src/Relation.js +77 -2
  82. package/dist/src/Resource.d.ts +590 -155
  83. package/dist/src/Resource.d.ts.map +1 -1
  84. package/dist/src/Resource.js +592 -343
  85. package/dist/src/Result.d.ts +838 -191
  86. package/dist/src/Result.d.ts.map +1 -1
  87. package/dist/src/Result.js +163 -102
  88. package/dist/src/Schedule.d.ts +684 -231
  89. package/dist/src/Schedule.d.ts.map +1 -1
  90. package/dist/src/Schedule.js +766 -295
  91. package/dist/src/Set.d.ts +89 -40
  92. package/dist/src/Set.d.ts.map +1 -1
  93. package/dist/src/Set.js +50 -29
  94. package/dist/src/Sqlite.d.ts +29 -72
  95. package/dist/src/Sqlite.d.ts.map +1 -1
  96. package/dist/src/Sqlite.js +41 -46
  97. package/dist/src/StackTrace.d.ts +40 -0
  98. package/dist/src/StackTrace.d.ts.map +1 -0
  99. package/dist/src/StackTrace.js +140 -0
  100. package/dist/src/Store.d.ts +5 -4
  101. package/dist/src/Store.d.ts.map +1 -1
  102. package/dist/src/Store.js +5 -4
  103. package/dist/src/String.d.ts +7 -1
  104. package/dist/src/String.d.ts.map +1 -1
  105. package/dist/src/String.js +20 -7
  106. package/dist/src/Task.d.ts +3572 -1770
  107. package/dist/src/Task.d.ts.map +1 -1
  108. package/dist/src/Task.js +1879 -1274
  109. package/dist/src/Test.d.ts +17 -125
  110. package/dist/src/Test.d.ts.map +1 -1
  111. package/dist/src/Test.js +17 -68
  112. package/dist/src/Time.d.ts +108 -49
  113. package/dist/src/Time.d.ts.map +1 -1
  114. package/dist/src/Time.js +172 -73
  115. package/dist/src/Type.d.ts +3542 -1926
  116. package/dist/src/Type.d.ts.map +1 -1
  117. package/dist/src/Type.js +3070 -1784
  118. package/dist/src/Types.d.ts +200 -108
  119. package/dist/src/Types.d.ts.map +1 -1
  120. package/dist/src/Types.js +39 -8
  121. package/dist/src/WebSocket.d.ts +48 -23
  122. package/dist/src/WebSocket.d.ts.map +1 -1
  123. package/dist/src/WebSocket.js +15 -6
  124. package/dist/src/Worker.d.ts +94 -36
  125. package/dist/src/Worker.d.ts.map +1 -1
  126. package/dist/src/Worker.js +58 -24
  127. package/dist/src/index.d.ts +55 -52
  128. package/dist/src/index.d.ts.map +1 -1
  129. package/dist/src/index.js +3 -1
  130. package/dist/src/intl/cs.d.ts +94 -0
  131. package/dist/src/intl/cs.d.ts.map +1 -0
  132. package/dist/src/intl/cs.js +220 -0
  133. package/dist/src/intl/index.d.ts +7 -0
  134. package/dist/src/intl/index.d.ts.map +1 -0
  135. package/dist/src/intl/index.js +6 -0
  136. package/dist/src/local-first/Db.d.ts +12 -8
  137. package/dist/src/local-first/Db.d.ts.map +1 -1
  138. package/dist/src/local-first/Db.js +109 -132
  139. package/dist/src/local-first/Error.d.ts +4 -4
  140. package/dist/src/local-first/Evolu.d.ts +338 -92
  141. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  142. package/dist/src/local-first/Evolu.js +47 -55
  143. package/dist/src/local-first/LocalAuth.d.ts +4 -4
  144. package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
  145. package/dist/src/local-first/Owner.d.ts +61 -24
  146. package/dist/src/local-first/Owner.d.ts.map +1 -1
  147. package/dist/src/local-first/Owner.js +45 -8
  148. package/dist/src/local-first/Protocol.d.ts +44 -31
  149. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  150. package/dist/src/local-first/Protocol.js +36 -13
  151. package/dist/src/local-first/Query.d.ts +147 -62
  152. package/dist/src/local-first/Query.d.ts.map +1 -1
  153. package/dist/src/local-first/Query.js +112 -58
  154. package/dist/src/local-first/Relay.d.ts +44 -14
  155. package/dist/src/local-first/Relay.d.ts.map +1 -1
  156. package/dist/src/local-first/Schema.d.ts +92 -41
  157. package/dist/src/local-first/Schema.d.ts.map +1 -1
  158. package/dist/src/local-first/Schema.js +25 -8
  159. package/dist/src/local-first/Shared.d.ts +23 -21
  160. package/dist/src/local-first/Shared.d.ts.map +1 -1
  161. package/dist/src/local-first/Shared.js +100 -57
  162. package/dist/src/local-first/Storage.d.ts +53 -32
  163. package/dist/src/local-first/Storage.d.ts.map +1 -1
  164. package/dist/src/local-first/Storage.js +194 -234
  165. package/dist/src/local-first/Timestamp.d.ts +21 -21
  166. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  167. package/dist/src/local-first/Timestamp.js +2 -2
  168. package/dist/src/local-first/index.d.ts +11 -11
  169. package/package.json +14 -13
  170. package/src/Array.ts +306 -205
  171. package/src/Assert.ts +54 -69
  172. package/src/BigInt.ts +6 -3
  173. package/src/Brand.ts +27 -40
  174. package/src/Buffer.ts +28 -12
  175. package/src/Cache.ts +9 -4
  176. package/src/Callbacks.ts +30 -19
  177. package/src/Console.ts +156 -84
  178. package/src/Crypto.ts +83 -41
  179. package/src/Eq.ts +95 -22
  180. package/src/Error.ts +8 -4
  181. package/src/Function.ts +167 -59
  182. package/src/Http.ts +549 -0
  183. package/src/Identicon.ts +16 -7
  184. package/src/LeakDetector.ts +182 -0
  185. package/src/LockManager.ts +43 -39
  186. package/src/Lookup.ts +62 -28
  187. package/src/Microtask.ts +19 -23
  188. package/src/Number.ts +64 -12
  189. package/src/Object.ts +173 -49
  190. package/src/Option.ts +10 -7
  191. package/src/Order.ts +23 -21
  192. package/src/Platform.ts +136 -0
  193. package/src/Polyfills.ts +6 -12
  194. package/src/Random.ts +32 -19
  195. package/src/Redacted.ts +44 -30
  196. package/src/Ref.ts +14 -6
  197. package/src/RefCount.ts +38 -80
  198. package/src/Relation.ts +202 -10
  199. package/src/Resource.ts +1199 -591
  200. package/src/Result.ts +930 -271
  201. package/src/Schedule.ts +859 -343
  202. package/src/Set.ts +89 -40
  203. package/src/Sqlite.ts +144 -138
  204. package/src/StackTrace.ts +230 -0
  205. package/src/Store.ts +8 -7
  206. package/src/String.ts +22 -5
  207. package/src/Task.ts +5101 -2832
  208. package/src/Test.ts +20 -168
  209. package/src/Time.ts +293 -90
  210. package/src/Type.ts +10034 -4117
  211. package/src/Types.ts +227 -110
  212. package/src/WebSocket.ts +64 -36
  213. package/src/Worker.ts +180 -72
  214. package/src/index.ts +55 -52
  215. package/src/intl/cs.ts +388 -0
  216. package/src/intl/index.ts +7 -0
  217. package/src/local-first/Db.ts +152 -179
  218. package/src/local-first/Error.ts +4 -4
  219. package/src/local-first/Evolu.ts +495 -250
  220. package/src/local-first/LocalAuth.ts +6 -12
  221. package/src/local-first/Owner.ts +58 -21
  222. package/src/local-first/Protocol.ts +49 -31
  223. package/src/local-first/Query.ts +146 -61
  224. package/src/local-first/Relay.ts +51 -21
  225. package/src/local-first/Schema.ts +109 -51
  226. package/src/local-first/Shared.ts +266 -226
  227. package/src/local-first/Storage.ts +266 -264
  228. package/src/local-first/Timestamp.ts +25 -18
  229. package/src/local-first/index.ts +11 -11
  230. package/dist/src/Skiplist.d.ts +0 -28
  231. package/dist/src/Skiplist.d.ts.map +0 -1
  232. package/dist/src/Skiplist.js +0 -64
  233. package/dist/src/Tracer.d.ts +0 -48
  234. package/dist/src/Tracer.d.ts.map +0 -1
  235. package/dist/src/Tracer.js +0 -6
  236. package/src/Skiplist.ts +0 -108
  237. package/src/Tracer.ts +0 -54
package/src/Random.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Seeded random number generation.
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.js";
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
- * // For apps
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 random = createRandomWithSeed("test");
31
- * random.next();
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 {@link Random} using {@link RandomLibDep} with a seed which is useful
54
- * for tests.
59
+ * Creates a seeded {@link Random} for deterministic tests.
60
+ *
61
+ * Default seed "evolu".
55
62
  */
56
- export const createRandomWithSeed = (seed: string): Random => {
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
- * A random number generator using the NPM `random` package dependency.
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 `RandomLib` using the NPM `random` package. */
80
+ /** Creates a random number generator from the NPM `random` package. */
73
81
  export const createRandomLib = (): RandomLib => new RandomLib();
74
82
 
75
- /** Creates a seeded `RandomLib` for deterministic tests. Default seed "evolu". */
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.js";
8
- import type { Brand } from "./Brand.js";
9
- import type { Eq } from "./Eq.js";
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
- * // Define branded types for your secrets
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
- * // Create a redacted secret
43
+ * // Apply brands only after validation or at another trusted boundary.
41
44
  * const apiKey: ApiKey = "secret-123" as ApiKey;
42
- * const redactedKey: RedactedApiKey = createRedacted(apiKey);
43
- *
44
- * console.log(redactedKey); // <redacted>
45
- * console.log(revealRedacted(redactedKey)); // secret-123
46
- *
47
- * // Type safety: RedactedApiKey ≠ RedactedDbPassword
48
- * const fetchUser = (key: RedactedApiKey) => {
49
- * const value: ApiKey = revealRedacted(key);
50
- * // use value...
51
- * };
52
- *
53
- * fetchUser(redactedKey); // ok
54
- * // fetchUser(createRedacted("x" as DbPassword)); // type error
55
- *
56
- * // Automatic cleanup with `using`
57
- * {
58
- * using secret = createRedacted("sensitive" as ApiKey);
59
- * // ... use secret ...
60
- * } // automatically wiped from memory
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
- * const a = createRedacted("x" as ApiKey);
115
- * const b = createRedacted("x" as ApiKey);
116
- * eqRedactedApiKey(a, b); // true
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.js";
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
- * console.log(count.get()); // 2
29
+ * expect(count.get()).toBe(2);
28
30
  * ```
29
31
  *
30
- * ### Example of using Ref as a dependency
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
- updater: (current: T) => readonly [result: R, nextValue: T],
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: (updater) => {
104
- const [result, nextValue] = updater(currentValue);
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, 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";
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
- assertNotDisposed(disposables);
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 = disposer.adopt(
117
- createLookupMap<TKey, RefCount, L>({ lookup }),
118
- (refCountByKey) => {
119
- using disposer = new DisposableStack();
120
- disposer.defer(() => {
121
- refCountByKey.clear();
122
- });
123
- for (const refCount of refCountByKey.values()) {
124
- disposer.use(refCount);
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.js";
8
- import { assert } from "./Assert.js";
9
- import { identity } from "./Function.js";
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.js";
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
- * ### Example
54
+ * ### Reference identity
48
55
  *
49
56
  * Use the default identity semantics.
50
57
  *
51
58
  * ```ts
52
- * const relation = createRelation<WebSocket, string>();
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
- * ### Example
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
- * relation.has({ id: "1", name: "Grace" }, { id: "admins" }); // true
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
+ }