@evolu/common 6.0.1-preview.3 → 6.0.1-preview.30

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 (173) hide show
  1. package/dist/src/Array.d.ts +58 -5
  2. package/dist/src/Array.d.ts.map +1 -1
  3. package/dist/src/Array.js +53 -5
  4. package/dist/src/Assert.d.ts +6 -16
  5. package/dist/src/Assert.d.ts.map +1 -1
  6. package/dist/src/Assert.js +6 -18
  7. package/dist/src/Brand.d.ts +75 -0
  8. package/dist/src/Brand.d.ts.map +1 -0
  9. package/dist/src/Brand.js +1 -0
  10. package/dist/src/Buffer.d.ts +1 -1
  11. package/dist/src/Buffer.d.ts.map +1 -1
  12. package/dist/src/Buffer.js +8 -7
  13. package/dist/src/Cache.d.ts +44 -0
  14. package/dist/src/Cache.d.ts.map +1 -0
  15. package/dist/src/Cache.js +52 -0
  16. package/dist/src/Callbacks.d.ts +45 -12
  17. package/dist/src/Callbacks.d.ts.map +1 -1
  18. package/dist/src/Callbacks.js +14 -7
  19. package/dist/src/Console.d.ts +31 -6
  20. package/dist/src/Console.d.ts.map +1 -1
  21. package/dist/src/Console.js +72 -9
  22. package/dist/src/Crypto.d.ts +61 -34
  23. package/dist/src/Crypto.d.ts.map +1 -1
  24. package/dist/src/Crypto.js +32 -45
  25. package/dist/src/Evolu/Db.d.ts +158 -65
  26. package/dist/src/Evolu/Db.d.ts.map +1 -1
  27. package/dist/src/Evolu/Db.js +286 -694
  28. package/dist/src/Evolu/Diff.d.ts +3 -3
  29. package/dist/src/Evolu/Diff.d.ts.map +1 -1
  30. package/dist/src/Evolu/Diff.js +7 -5
  31. package/dist/src/Evolu/Evolu.d.ts +208 -133
  32. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  33. package/dist/src/Evolu/Evolu.js +188 -183
  34. package/dist/src/Evolu/Internal.d.ts +0 -2
  35. package/dist/src/Evolu/Internal.d.ts.map +1 -1
  36. package/dist/src/Evolu/Internal.js +0 -2
  37. package/dist/src/Evolu/LocalAuth.d.ts +150 -0
  38. package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
  39. package/dist/src/Evolu/LocalAuth.js +174 -0
  40. package/dist/src/Evolu/Owner.d.ts +264 -120
  41. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  42. package/dist/src/Evolu/Owner.js +130 -104
  43. package/dist/src/Evolu/Platform.d.ts +9 -7
  44. package/dist/src/Evolu/Platform.d.ts.map +1 -1
  45. package/dist/src/Evolu/Protocol.d.ts +277 -232
  46. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  47. package/dist/src/Evolu/Protocol.js +603 -378
  48. package/dist/src/Evolu/Public.d.ts +6 -8
  49. package/dist/src/Evolu/Public.d.ts.map +1 -1
  50. package/dist/src/Evolu/Public.js +2 -3
  51. package/dist/src/Evolu/PublicKysely.js +3 -3
  52. package/dist/src/Evolu/Query.d.ts +2 -1
  53. package/dist/src/Evolu/Query.d.ts.map +1 -1
  54. package/dist/src/Evolu/Relay.d.ts +92 -7
  55. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  56. package/dist/src/Evolu/Relay.js +243 -76
  57. package/dist/src/Evolu/Schema.d.ts +129 -73
  58. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  59. package/dist/src/Evolu/Schema.js +169 -89
  60. package/dist/src/Evolu/Storage.d.ts +212 -26
  61. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  62. package/dist/src/Evolu/Storage.js +137 -79
  63. package/dist/src/Evolu/Sync.d.ts +68 -13
  64. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  65. package/dist/src/Evolu/Sync.js +422 -20
  66. package/dist/src/Evolu/Timestamp.d.ts +85 -27
  67. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  68. package/dist/src/Evolu/Timestamp.js +77 -18
  69. package/dist/src/Identicon.d.ts +35 -0
  70. package/dist/src/Identicon.d.ts.map +1 -0
  71. package/dist/src/Identicon.js +143 -0
  72. package/dist/src/Instances.d.ts +34 -0
  73. package/dist/src/Instances.d.ts.map +1 -0
  74. package/dist/src/Instances.js +44 -0
  75. package/dist/src/ManyToManyMap.d.ts +71 -10
  76. package/dist/src/ManyToManyMap.d.ts.map +1 -1
  77. package/dist/src/ManyToManyMap.js +41 -6
  78. package/dist/src/Number.d.ts +4 -3
  79. package/dist/src/Number.d.ts.map +1 -1
  80. package/dist/src/Number.js +5 -4
  81. package/dist/src/Platform.d.ts +20 -0
  82. package/dist/src/Platform.d.ts.map +1 -0
  83. package/dist/src/Platform.js +22 -0
  84. package/dist/src/Random.d.ts +3 -2
  85. package/dist/src/Random.d.ts.map +1 -1
  86. package/dist/src/Resources.d.ts +118 -0
  87. package/dist/src/Resources.d.ts.map +1 -0
  88. package/dist/src/Resources.js +197 -0
  89. package/dist/src/Result.d.ts +184 -52
  90. package/dist/src/Result.d.ts.map +1 -1
  91. package/dist/src/Result.js +30 -241
  92. package/dist/src/Skiplist.js +2 -1
  93. package/dist/src/Sqlite.d.ts +63 -5
  94. package/dist/src/Sqlite.d.ts.map +1 -1
  95. package/dist/src/Sqlite.js +110 -9
  96. package/dist/src/Task.d.ts +586 -0
  97. package/dist/src/Task.d.ts.map +1 -0
  98. package/dist/src/Task.js +469 -0
  99. package/dist/src/Time.d.ts +66 -1
  100. package/dist/src/Time.d.ts.map +1 -1
  101. package/dist/src/Time.js +99 -5
  102. package/dist/src/Type.d.ts +621 -340
  103. package/dist/src/Type.d.ts.map +1 -1
  104. package/dist/src/Type.js +665 -464
  105. package/dist/src/Types.d.ts +1 -75
  106. package/dist/src/Types.d.ts.map +1 -1
  107. package/dist/src/WebSocket.d.ts +5 -2
  108. package/dist/src/WebSocket.d.ts.map +1 -1
  109. package/dist/src/WebSocket.js +12 -18
  110. package/dist/src/Worker.d.ts +39 -11
  111. package/dist/src/Worker.d.ts.map +1 -1
  112. package/dist/src/Worker.js +22 -4
  113. package/dist/src/index.d.ts +7 -2
  114. package/dist/src/index.d.ts.map +1 -1
  115. package/dist/src/index.js +7 -2
  116. package/package.json +14 -13
  117. package/src/Array.ts +76 -11
  118. package/src/Assert.ts +6 -24
  119. package/src/Brand.ts +75 -0
  120. package/src/Buffer.ts +7 -7
  121. package/src/Cache.ts +85 -0
  122. package/src/Callbacks.ts +62 -22
  123. package/src/Console.ts +91 -11
  124. package/src/Crypto.ts +97 -82
  125. package/src/Evolu/Db.ts +514 -1020
  126. package/src/Evolu/Diff.ts +7 -5
  127. package/src/Evolu/Evolu.ts +464 -355
  128. package/src/Evolu/Internal.ts +0 -2
  129. package/src/Evolu/LocalAuth.ts +463 -0
  130. package/src/Evolu/Owner.ts +369 -228
  131. package/src/Evolu/Platform.ts +9 -9
  132. package/src/Evolu/Protocol.ts +859 -676
  133. package/src/Evolu/Public.ts +7 -14
  134. package/src/Evolu/PublicKysely.ts +3 -3
  135. package/src/Evolu/Query.ts +2 -1
  136. package/src/Evolu/Relay.ts +420 -92
  137. package/src/Evolu/Schema.ts +391 -191
  138. package/src/Evolu/Storage.ts +451 -118
  139. package/src/Evolu/Sync.ts +720 -37
  140. package/src/Evolu/Timestamp.ts +88 -35
  141. package/src/Identicon.ts +197 -0
  142. package/src/Instances.ts +90 -0
  143. package/src/ManyToManyMap.ts +124 -24
  144. package/src/Number.ts +6 -10
  145. package/src/Platform.ts +26 -0
  146. package/src/Random.ts +3 -2
  147. package/src/Resources.ts +367 -0
  148. package/src/Result.ts +191 -54
  149. package/src/Skiplist.ts +1 -1
  150. package/src/Sqlite.ts +122 -17
  151. package/src/Task.ts +901 -0
  152. package/src/Time.ts +180 -5
  153. package/src/Type.ts +1083 -727
  154. package/src/Types.ts +1 -77
  155. package/src/WebSocket.ts +27 -25
  156. package/src/Worker.ts +72 -23
  157. package/src/index.ts +7 -2
  158. package/dist/src/Evolu/Config.d.ts +0 -69
  159. package/dist/src/Evolu/Config.d.ts.map +0 -1
  160. package/dist/src/Evolu/Config.js +0 -9
  161. package/dist/src/Evolu/Kysely.d.ts +0 -6
  162. package/dist/src/Evolu/Kysely.d.ts.map +0 -1
  163. package/dist/src/Evolu/Kysely.js +0 -21
  164. package/dist/src/NanoId.d.ts +0 -27
  165. package/dist/src/NanoId.d.ts.map +0 -1
  166. package/dist/src/NanoId.js +0 -6
  167. package/dist/src/Promise.d.ts +0 -180
  168. package/dist/src/Promise.d.ts.map +0 -1
  169. package/dist/src/Promise.js +0 -176
  170. package/src/Evolu/Config.ts +0 -83
  171. package/src/Evolu/Kysely.ts +0 -38
  172. package/src/NanoId.ts +0 -39
  173. package/src/Promise.ts +0 -295
@@ -1,17 +1,72 @@
1
- /** Represents a many-to-many mapping between keys and values. */
1
+ /**
2
+ * Bidirectional many-to-many map between keys and values.
3
+ *
4
+ * Why useful:
5
+ *
6
+ * - Provides O(1) (amortized) forward lookup (key -> values) and reverse lookup
7
+ * (value -> keys) without maintaining two maps manually and risking them
8
+ * diverging.
9
+ * - Natural fit for pub/sub style relations such as ownerId ↔ WebSocket, tag ↔
10
+ * item, user ↔ role, entity ↔ subscription where both directions are
11
+ * frequently queried.
12
+ * - Supports fast membership tests via `hasPair`, `hasKey`, and `hasValue`.
13
+ * - Iteration helpers (`forEach`, iterator) allow treating the structure as an
14
+ * edge list when needed.
15
+ *
16
+ * Complexity:
17
+ *
18
+ * - `add` / `remove` / `has*` / `get*` each perform a constant number of Map/Set
19
+ * operations (O(1) expected).
20
+ * - `deleteKey` and `deleteValue` are O(d) where d is the number of associated
21
+ * values / keys (the degree). This is optimal because every associated pair
22
+ * must be touched once.
23
+ *
24
+ * Object identity:
25
+ *
26
+ * - Keys and values are compared by reference (standard Map / Set semantics).
27
+ * Structural hashing of objects in JavaScript is non-trivial, can be
28
+ * expensive, and collision-prone if done naively. Prefer using stable
29
+ * primitive identifiers (ids, strings) instead of attempting to hash full
30
+ * 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.
34
+ */
2
35
  export interface ManyToManyMap<K, V> {
3
- /** Adds a key-value association to the map. */
4
- add: (key: K, value: V) => ManyToManyMap<K, V>;
5
- /** Removes a specific key-value association. */
36
+ /**
37
+ * Adds a key-value association to the map. Returns true if the pair was newly
38
+ * added, false if it already existed.
39
+ */
40
+ add: (key: K, value: V) => boolean;
41
+ /**
42
+ * Removes a specific key-value association. Returns true if the pair existed
43
+ * and was removed, false if it was not present.
44
+ */
6
45
  remove: (key: K, value: V) => boolean;
7
- /** Gets all values associated with a key. */
8
- getValues: (key: K) => Set<V> | undefined;
9
- /** Gets all keys associated with a value. */
10
- getKeys: (value: V) => Set<K> | undefined;
46
+ /**
47
+ * Gets all values associated with a key. Returned set is the internal Set
48
+ * instance typed as ReadonlySet. Do not mutate.
49
+ */
50
+ getValues: (key: K) => ReadonlySet<V> | undefined;
51
+ /**
52
+ * Gets all keys associated with a value. Returned set is the internal Set
53
+ * instance typed as ReadonlySet. Do not mutate.
54
+ */
55
+ getKeys: (value: V) => ReadonlySet<K> | undefined;
56
+ /**
57
+ * Iterates over each key-value pair (in insertion order of keys, then values
58
+ * per key).
59
+ */
60
+ forEach: (callback: (key: K, value: V) => void) => void;
61
+ /**
62
+ * Iterator over all key-value pairs enabling for..of and spread. Yields
63
+ * readonly [key, value] tuples.
64
+ */
65
+ readonly [Symbol.iterator]: () => IterableIterator<readonly [K, V]>;
11
66
  /** Checks if a specific key-value pair exists. */
12
67
  hasPair: (key: K, value: V) => boolean;
13
68
  /** Checks if a key exists in the map. */
14
- hasKey(key: K): boolean;
69
+ hasKey: (key: K) => boolean;
15
70
  /** Checks if a value exists in the map. */
16
71
  hasValue: (value: V) => boolean;
17
72
  /** Deletes all associations for a key. */
@@ -19,7 +74,13 @@ export interface ManyToManyMap<K, V> {
19
74
  /** Deletes all associations for a value. */
20
75
  deleteValue: (value: V) => boolean;
21
76
  /** Clears all associations in the map. */
22
- clear(): void;
77
+ clear: () => void;
78
+ /** Number of distinct keys currently present. */
79
+ keyCount: () => number;
80
+ /** Number of distinct values currently present. */
81
+ valueCount: () => number;
82
+ /** Number of key-value pairs (associations) currently stored. */
83
+ pairCount: () => number;
23
84
  }
24
85
  /** Creates a {@link ManyToManyMap}. */
25
86
  export declare const createManyToManyMap: <K, V>() => ManyToManyMap<K, V>;
@@ -1 +1 @@
1
- {"version":3,"file":"ManyToManyMap.d.ts","sourceRoot":"","sources":["../../src/ManyToManyMap.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,MAAM,WAAW,aAAa,CAAC,CAAC,EAAE,CAAC;IACjC,+CAA+C;IAC/C,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAE/C,gDAAgD;IAChD,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC;IAEtC,6CAA6C;IAC7C,SAAS,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;IAE1C,6CAA6C;IAC7C,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;IAE1C,kDAAkD;IAClD,OAAO,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC;IAEvC,yCAAyC;IACzC,MAAM,CAAC,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC;IAExB,2CAA2C;IAC3C,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC;IAEhC,0CAA0C;IAC1C,SAAS,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,OAAO,CAAC;IAE/B,4CAA4C;IAC5C,WAAW,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC;IAEnC,0CAA0C;IAC1C,KAAK,IAAI,IAAI,CAAC;CACf;AAED,uCAAuC;AACvC,eAAO,MAAM,mBAAmB,GAAI,CAAC,EAAE,CAAC,OAAK,aAAa,CAAC,CAAC,EAAE,CAAC,CAyG9D,CAAC"}
1
+ {"version":3,"file":"ManyToManyMap.d.ts","sourceRoot":"","sources":["../../src/ManyToManyMap.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,WAAW,aAAa,CAAC,CAAC,EAAE,CAAC;IACjC;;;OAGG;IACH,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC;IAEnC;;;OAGG;IACH,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC;IAEtC;;;OAGG;IACH,SAAS,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,WAAW,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;IAElD;;;OAGG;IACH,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,WAAW,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;IAElD;;;OAGG;IACH,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,IAAI,KAAK,IAAI,CAAC;IAExD;;;OAGG;IACH,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,gBAAgB,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAEpE,kDAAkD;IAClD,OAAO,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC;IAEvC,yCAAyC;IACzC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,OAAO,CAAC;IAE5B,2CAA2C;IAC3C,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC;IAEhC,0CAA0C;IAC1C,SAAS,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,OAAO,CAAC;IAE/B,4CAA4C;IAC5C,WAAW,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC;IAEnC,0CAA0C;IAC1C,KAAK,EAAE,MAAM,IAAI,CAAC;IAElB,iDAAiD;IACjD,QAAQ,EAAE,MAAM,MAAM,CAAC;IACvB,mDAAmD;IACnD,UAAU,EAAE,MAAM,MAAM,CAAC;IACzB,iEAAiE;IACjE,SAAS,EAAE,MAAM,MAAM,CAAC;CACzB;AAED,uCAAuC;AACvC,eAAO,MAAM,mBAAmB,GAAI,CAAC,EAAE,CAAC,OAAK,aAAa,CAAC,CAAC,EAAE,CAAC,CA2I9D,CAAC"}
@@ -1,10 +1,14 @@
1
+ import { assert } from "./Assert.js";
1
2
  /** Creates a {@link ManyToManyMap}. */
2
3
  export const createManyToManyMap = () => {
3
4
  const forwardMap = new Map();
4
5
  const reverseMap = new Map();
6
+ let pairCountInternal = 0;
5
7
  const map = {
6
8
  add(key, value) {
7
9
  let values = forwardMap.get(key);
10
+ if (values?.has(value))
11
+ return false;
8
12
  if (!values) {
9
13
  values = new Set();
10
14
  forwardMap.set(key, values);
@@ -16,7 +20,8 @@ export const createManyToManyMap = () => {
16
20
  reverseMap.set(value, keys);
17
21
  }
18
22
  keys.add(key);
19
- return map;
23
+ pairCountInternal++;
24
+ return true;
20
25
  },
21
26
  remove(key, value) {
22
27
  const values = forwardMap.get(key);
@@ -27,12 +32,12 @@ export const createManyToManyMap = () => {
27
32
  forwardMap.delete(key);
28
33
  }
29
34
  const keys = reverseMap.get(value);
30
- if (keys?.size) {
31
- keys.delete(key);
32
- if (keys.size === 0) {
33
- reverseMap.delete(value);
34
- }
35
+ assert(keys, "Key-value mapping inconsistency");
36
+ keys.delete(key);
37
+ if (keys.size === 0) {
38
+ reverseMap.delete(value);
35
39
  }
40
+ pairCountInternal--;
36
41
  return true;
37
42
  },
38
43
  getValues(key) {
@@ -41,6 +46,22 @@ export const createManyToManyMap = () => {
41
46
  getKeys(value) {
42
47
  return reverseMap.get(value);
43
48
  },
49
+ forEach(callback) {
50
+ for (const [key, values] of forwardMap) {
51
+ for (const value of values)
52
+ callback(key, value);
53
+ }
54
+ },
55
+ [Symbol.iterator]() {
56
+ const iterator = function* () {
57
+ for (const [key, values] of forwardMap) {
58
+ for (const value of values) {
59
+ yield [key, value];
60
+ }
61
+ }
62
+ };
63
+ return iterator();
64
+ },
44
65
  hasPair(key, value) {
45
66
  const values = forwardMap.get(key);
46
67
  return values?.has(value) ?? false;
@@ -55,6 +76,7 @@ export const createManyToManyMap = () => {
55
76
  const values = forwardMap.get(key);
56
77
  if (!values)
57
78
  return false;
79
+ const removed = values.size;
58
80
  for (const value of values) {
59
81
  const keys = reverseMap.get(value);
60
82
  if (keys) {
@@ -65,12 +87,14 @@ export const createManyToManyMap = () => {
65
87
  }
66
88
  }
67
89
  forwardMap.delete(key);
90
+ pairCountInternal -= removed;
68
91
  return true;
69
92
  },
70
93
  deleteValue(value) {
71
94
  const keys = reverseMap.get(value);
72
95
  if (!keys)
73
96
  return false;
97
+ const removed = keys.size;
74
98
  for (const key of keys) {
75
99
  const values = forwardMap.get(key);
76
100
  if (values) {
@@ -81,11 +105,22 @@ export const createManyToManyMap = () => {
81
105
  }
82
106
  }
83
107
  reverseMap.delete(value);
108
+ pairCountInternal -= removed;
84
109
  return true;
85
110
  },
86
111
  clear() {
87
112
  forwardMap.clear();
88
113
  reverseMap.clear();
114
+ pairCountInternal = 0;
115
+ },
116
+ keyCount() {
117
+ return forwardMap.size;
118
+ },
119
+ valueCount() {
120
+ return reverseMap.size;
121
+ },
122
+ pairCount() {
123
+ return pairCountInternal;
89
124
  },
90
125
  };
91
126
  return map;
@@ -1,7 +1,8 @@
1
1
  import { NonEmptyReadonlyArray } from "./Array.js";
2
2
  import { Result } from "./Result.js";
3
3
  import { NonNegativeInt, PositiveInt } from "./Type.js";
4
- import { IsBranded, Predicate, WidenLiteral } from "./Types.js";
4
+ import { Predicate, WidenLiteral } from "./Types.js";
5
+ import { IsBranded } from "./Brand.js";
5
6
  export declare const increment: (n: number) => number;
6
7
  export declare const decrement: (n: number) => number;
7
8
  /** Clamps a number within a given range. */
@@ -36,7 +37,7 @@ export declare const max: <T extends number>(...values: [T, ...ReadonlyArray<T>]
36
37
  */
37
38
  export declare const computeBalancedBuckets: (numberOfItems: NonNegativeInt,
38
39
  /** Default: 16 */
39
- numberOfBuckets?: PositiveInt,
40
+ numberOfBuckets?: number & import("./Brand.js").Brand<"Int"> & import("./Brand.js").Brand<"NonNegative"> & import("./Brand.js").Brand<"Positive">,
40
41
  /** Default: 2 */
41
- minNumberOfItemsPerBucket?: PositiveInt) => Result<NonEmptyReadonlyArray<PositiveInt>, PositiveInt>;
42
+ minNumberOfItemsPerBucket?: number & import("./Brand.js").Brand<"Int"> & import("./Brand.js").Brand<"NonNegative"> & import("./Brand.js").Brand<"Positive">) => Result<NonEmptyReadonlyArray<PositiveInt>, PositiveInt>;
42
43
  //# sourceMappingURL=Number.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Number.d.ts","sourceRoot":"","sources":["../../src/Number.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAEnD,OAAO,EAAW,MAAM,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxD,OAAO,EAEL,SAAS,EACT,SAAS,EACT,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,SAAS,GAAI,GAAG,MAAM,KAAG,MAAe,CAAC;AAEtD,eAAO,MAAM,SAAS,GAAI,GAAG,MAAM,KAAG,MAAe,CAAC;AAEtD,4CAA4C;AAC5C,eAAO,MAAM,KAAK,GACf,KAAK,MAAM,EAAE,KAAK,MAAM,MACxB,GAAG,MAAM,KAAG,MACoB,CAAC;AAEpC;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,GACnB,KAAK,MAAM,EAAE,KAAK,MAAM,KAAG,SAAS,CAAC,MAAM,CAEd,CAAC;AAEjC,wEAAwE;AACxE,eAAO,MAAM,GAAG,GAAI,CAAC,SAAS,MAAM,EAClC,GAAG,QAAQ,CAAC,CAAC,EAAE,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC,KAClC,SAAS,CAAC,CAAC,CAAC,SAAS,IAAI,GAAG,CAAC,GAAG,YAAY,CAAC,CAAC,CACa,CAAC;AAE/D,wEAAwE;AACxE,eAAO,MAAM,GAAG,GAAI,CAAC,SAAS,MAAM,EAClC,GAAG,QAAQ,CAAC,CAAC,EAAE,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC,KAClC,SAAS,CAAC,CAAC,CAAC,SAAS,IAAI,GAAG,CAAC,GAAG,YAAY,CAAC,CAAC,CACa,CAAC;AAE/D;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,GACjC,eAAe,cAAc;AAE7B,kBAAkB;AAClB,kBAAwB,WAAW;AAEnC,iBAAiB;AACjB,4BAAiC,WAAW,KAC3C,MAAM,CAAC,qBAAqB,CAAC,WAAW,CAAC,EAAE,WAAW,CAoBxD,CAAC"}
1
+ {"version":3,"file":"Number.d.ts","sourceRoot":"","sources":["../../src/Number.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAEnD,OAAO,EAAW,MAAM,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxD,OAAO,EAAoB,SAAS,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACvE,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,eAAO,MAAM,SAAS,GAAI,GAAG,MAAM,KAAG,MAAe,CAAC;AAEtD,eAAO,MAAM,SAAS,GAAI,GAAG,MAAM,KAAG,MAAe,CAAC;AAEtD,4CAA4C;AAC5C,eAAO,MAAM,KAAK,GACf,KAAK,MAAM,EAAE,KAAK,MAAM,MACxB,GAAG,MAAM,KAAG,MACoB,CAAC;AAEpC;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,GACnB,KAAK,MAAM,EAAE,KAAK,MAAM,KAAG,SAAS,CAAC,MAAM,CAEd,CAAC;AAEjC,wEAAwE;AACxE,eAAO,MAAM,GAAG,GAAI,CAAC,SAAS,MAAM,EAClC,GAAG,QAAQ,CAAC,CAAC,EAAE,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC,KAClC,SAAS,CAAC,CAAC,CAAC,SAAS,IAAI,GAAG,CAAC,GAAG,YAAY,CAAC,CAAC,CACa,CAAC;AAE/D,wEAAwE;AACxE,eAAO,MAAM,GAAG,GAAI,CAAC,SAAS,MAAM,EAClC,GAAG,QAAQ,CAAC,CAAC,EAAE,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC,KAClC,SAAS,CAAC,CAAC,CAAC,SAAS,IAAI,GAAG,CAAC,GAAG,YAAY,CAAC,CAAC,CACa,CAAC;AAE/D;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,GACjC,eAAe,cAAc;AAE7B,kBAAkB;AAClB,iJAAyC;AAEzC,iBAAiB;AACjB,2JAAkD,KACjD,MAAM,CAAC,qBAAqB,CAAC,WAAW,CAAC,EAAE,WAAW,CAoBxD,CAAC"}
@@ -1,5 +1,6 @@
1
1
  import { assertNonEmptyReadonlyArray } from "./Assert.js";
2
2
  import { err, ok } from "./Result.js";
3
+ import { PositiveInt } from "./Type.js";
3
4
  export const increment = (n) => n + 1;
4
5
  export const decrement = (n) => n - 1;
5
6
  /** Clamps a number within a given range. */
@@ -34,12 +35,12 @@ export const max = (...values) => values.reduce((a, b) => (a > b ? a : b));
34
35
  */
35
36
  export const computeBalancedBuckets = (numberOfItems,
36
37
  /** Default: 16 */
37
- numberOfBuckets = 16,
38
+ numberOfBuckets = PositiveInt.orThrow(16),
38
39
  /** Default: 2 */
39
- minNumberOfItemsPerBucket = 2) => {
40
+ minNumberOfItemsPerBucket = PositiveInt.orThrow(2)) => {
40
41
  const minRequiredItems = numberOfBuckets * minNumberOfItemsPerBucket;
41
42
  if (numberOfItems < minRequiredItems)
42
- return err(minRequiredItems);
43
+ return err(PositiveInt.orThrow(minRequiredItems));
43
44
  const indexes = [];
44
45
  const itemsPerBucket = Math.floor(numberOfItems / numberOfBuckets);
45
46
  const extraItems = numberOfItems % numberOfBuckets;
@@ -48,7 +49,7 @@ minNumberOfItemsPerBucket = 2) => {
48
49
  const hasExtraItem = i < extraItems;
49
50
  const itemsInThisBucket = itemsPerBucket + (hasExtraItem ? 1 : 0);
50
51
  bucketBoundary += itemsInThisBucket;
51
- indexes.push(bucketBoundary);
52
+ indexes.push(PositiveInt.orThrow(bucketBoundary));
52
53
  }
53
54
  assertNonEmptyReadonlyArray(indexes);
54
55
  return ok(indexes);
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Platform detection utilities for Evolu.
3
+ *
4
+ * @module
5
+ */
6
+ /** Detects if the code is running in React Native environment. */
7
+ export declare const isReactNative: boolean;
8
+ /**
9
+ * Detects if Node.js Buffer is available and should be used.
10
+ *
11
+ * React Native apps often polyfill Node.js APIs like Buffer, but we want to use
12
+ * native methods when available for better performance.
13
+ *
14
+ * Returns false in React Native even if Buffer is polyfilled, as we prefer
15
+ * native methods in that environment.
16
+ *
17
+ * @see https://github.com/craftzdog/react-native-quick-base64#installation
18
+ */
19
+ export declare const hasNodeBuffer: boolean;
20
+ //# sourceMappingURL=Platform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Platform.d.ts","sourceRoot":"","sources":["../../src/Platform.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,kEAAkE;AAClE,eAAO,MAAM,aAAa,SAIoB,CAAC;AAE/C;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,SACkC,CAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Platform detection utilities for Evolu.
3
+ *
4
+ * @module
5
+ */
6
+ /** Detects if the code is running in React Native environment. */
7
+ export const isReactNative = typeof navigator !== "undefined" &&
8
+ "product" in navigator &&
9
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
10
+ navigator.product === "ReactNative";
11
+ /**
12
+ * Detects if Node.js Buffer is available and should be used.
13
+ *
14
+ * React Native apps often polyfill Node.js APIs like Buffer, but we want to use
15
+ * native methods when available for better performance.
16
+ *
17
+ * Returns false in React Native even if Buffer is polyfilled, as we prefer
18
+ * native methods in that environment.
19
+ *
20
+ * @see https://github.com/craftzdog/react-native-quick-base64#installation
21
+ */
22
+ export const hasNodeBuffer = !isReactNative && typeof globalThis.Buffer !== "undefined";
@@ -5,8 +5,9 @@
5
5
  */
6
6
  import { Random as RandomLib } from "random";
7
7
  /**
8
- * A simple wrapper around Math.random(). Most apps need only this. For more
9
- * complex needs check {@link RandomLibDep}.
8
+ * A simple wrapper around Math.random().
9
+ *
10
+ * For more complex needs check {@link RandomLibDep}.
10
11
  *
11
12
  * ### Example
12
13
  *
@@ -1 +1 @@
1
- {"version":3,"file":"Random.d.ts","sourceRoot":"","sources":["../../src/Random.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,QAAQ,CAAC;AAE7C;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,MAAM;IACrB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,oDAAoD;AACpD,eAAO,MAAM,YAAY,QAAO,MAE9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,KAAG,MAKnD,CAAC;AAEF;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,SAAS,CAAC;CACnB;AAED,4DAA4D;AAC5D,eAAO,MAAM,eAAe,QAAO,SAA4B,CAAC;AAEhE;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GAAI,MAAM,MAAM,KAAG,YAErD,CAAC"}
1
+ {"version":3,"file":"Random.d.ts","sourceRoot":"","sources":["../../src/Random.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,QAAQ,CAAC;AAE7C;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,MAAM;IACrB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,oDAAoD;AACpD,eAAO,MAAM,YAAY,QAAO,MAE9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,KAAG,MAKnD,CAAC;AAEF;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,SAAS,CAAC;CACnB;AAED,4DAA4D;AAC5D,eAAO,MAAM,eAAe,QAAO,SAA4B,CAAC;AAEhE;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GAAI,MAAM,MAAM,KAAG,YAErD,CAAC"}
@@ -0,0 +1,118 @@
1
+ import { Result } from "./Result.js";
2
+ /**
3
+ * A generic resource manager that handles reference counting and delayed
4
+ * disposal of shared resources. Useful for managing expensive resources like
5
+ * WebSocket connections that need to be shared among multiple consumers.
6
+ */
7
+ export interface Resources<TResource extends Disposable, TResourceKey extends string, TResourceConfig, TConsumer, TConsumerId extends string> extends Disposable {
8
+ /**
9
+ * Adds a consumer to resources, creating them if necessary. Increments
10
+ * reference counts for existing consumer-resource pairs.
11
+ */
12
+ readonly addConsumer: (consumer: TConsumer, resourceConfigs: ReadonlyArray<TResourceConfig>) => void;
13
+ /**
14
+ * Removes a consumer from resources. Decrements reference counts and
15
+ * schedules disposal when no consumers remain.
16
+ *
17
+ * Returns an error if the resource doesn't exist or if the consumer wasn't
18
+ * added to the resource.
19
+ */
20
+ readonly removeConsumer: (consumer: TConsumer, resourceConfigs: ReadonlyArray<TResourceConfig>) => Result<void, ResourceNotFoundError<TResourceKey> | ConsumerNotFoundError<TConsumerId, TResourceKey>>;
21
+ /** Gets the resource for the specified key, or null if it doesn't exist. */
22
+ readonly getResource: (key: TResourceKey) => TResource | null;
23
+ /** Gets all consumer IDs currently using the specified resource key. */
24
+ readonly getConsumersForResource: (key: TResourceKey) => ReadonlyArray<TConsumerId>;
25
+ /** Checks if a consumer is currently using any resources. */
26
+ readonly hasConsumerAnyResource: (consumer: TConsumer) => boolean;
27
+ /**
28
+ * Gets the consumer for the specified consumer ID, or null if not found or
29
+ * not using any resources.
30
+ */
31
+ readonly getConsumer: (consumerId: TConsumerId) => TConsumer | null;
32
+ }
33
+ /** Error when trying to remove a consumer from a resource that doesn't exist. */
34
+ export interface ResourceNotFoundError<TResourceKey extends string = string> {
35
+ readonly type: "ResourceNotFoundError";
36
+ readonly resourceKey: TResourceKey;
37
+ }
38
+ /** Error when trying to remove a consumer that wasn't added to a resource. */
39
+ export interface ConsumerNotFoundError<TConsumerId extends string = string, TResourceKey extends string = string> {
40
+ readonly type: "ConsumerNotFoundError";
41
+ readonly consumerId: TConsumerId;
42
+ readonly resourceKey: TResourceKey;
43
+ }
44
+ export interface ResourcesConfig<TResource extends Disposable, TResourceKey extends string, TResourceConfig, TConsumer, TConsumerId extends string> {
45
+ /** Creates a new resource for the given config. */
46
+ readonly createResource: (config: TResourceConfig) => TResource;
47
+ /** Extracts a unique key from a resource config for deduplication. */
48
+ readonly getResourceKey: (config: TResourceConfig) => TResourceKey;
49
+ /** Extracts a unique identifier from a consumer for reference counting. */
50
+ readonly getConsumerId: (consumer: TConsumer) => TConsumerId;
51
+ /**
52
+ * Delay in milliseconds before disposing unused resources. Helps avoid
53
+ * resource churn during rapid add/remove cycles. Defaults to 100ms.
54
+ */
55
+ readonly disposalDelay?: number;
56
+ /**
57
+ * Called when a consumer is added to a resource for the first time. This
58
+ * happens when the consumer's reference count goes from 0 to 1 for this
59
+ * resource.
60
+ */
61
+ readonly onConsumerAdded?: (consumer: TConsumer, resource: TResource, resourceKey: TResourceKey) => void;
62
+ /**
63
+ * Called when a consumer is completely removed from a resource. This happens
64
+ * when the consumer's reference count goes from 1 to 0 for this resource.
65
+ */
66
+ readonly onConsumerRemoved?: (consumer: TConsumer, resource: TResource, resourceKey: TResourceKey) => void;
67
+ }
68
+ /**
69
+ * Creates {@link Resources}.
70
+ *
71
+ * This tracks which consumers are using which resources and maintains reference
72
+ * counts to know when it's safe to dispose resources. Resources are created
73
+ * on-demand and disposed with a configurable delay to avoid churn.
74
+ *
75
+ * ### Example Usage
76
+ *
77
+ * ```ts
78
+ * // WebSocket connections
79
+ * interface WebSocketConfig {
80
+ * readonly url: WebSocketUrl;
81
+ * }
82
+ *
83
+ * type WebSocketUrl = string & Brand<"WebSocketUrl">;
84
+ * type UserId = string & Brand<"UserId">;
85
+ *
86
+ * const webSockets = createResources<
87
+ * WebSocket,
88
+ * WebSocketUrl,
89
+ * WebSocketConfig,
90
+ * User,
91
+ * UserId
92
+ * >({
93
+ * createResource: (config) => new WebSocket(config.url),
94
+ * getResourceKey: (config) => config.url,
95
+ * getConsumerId: (user) => user.id,
96
+ * disposalDelay: 1000,
97
+ * });
98
+ *
99
+ * // Add users to WebSocket connections
100
+ * webSockets.addConsumer(user1, [
101
+ * { url: "ws://server1.com" as WebSocketUrl },
102
+ * { url: "ws://server2.com" as WebSocketUrl },
103
+ * ]);
104
+ * webSockets.addConsumer(user2, [
105
+ * { url: "ws://server1.com" as WebSocketUrl },
106
+ * ]);
107
+ *
108
+ * // Remove users - server1 stays alive (user2 still using it)
109
+ * webSockets.removeConsumer(user1, [
110
+ * { url: "ws://server1.com" as WebSocketUrl },
111
+ * { url: "ws://server2.com" as WebSocketUrl },
112
+ * ]);
113
+ *
114
+ * // server2 gets disposed after delay, server1 stays alive
115
+ * ```
116
+ */
117
+ export declare const createResources: <TResource extends Disposable, TResourceKey extends string, TResourceConfig, TConsumer, TConsumerId extends string>(config: ResourcesConfig<TResource, TResourceKey, TResourceConfig, TConsumer, TConsumerId>) => Resources<TResource, TResourceKey, TResourceConfig, TConsumer, TConsumerId>;
118
+ //# sourceMappingURL=Resources.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Resources.d.ts","sourceRoot":"","sources":["../../src/Resources.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,197 @@
1
+ import { err, ok } from "./Result.js";
2
+ import { PositiveInt } from "./Type.js";
3
+ /**
4
+ * Creates {@link Resources}.
5
+ *
6
+ * This tracks which consumers are using which resources and maintains reference
7
+ * counts to know when it's safe to dispose resources. Resources are created
8
+ * on-demand and disposed with a configurable delay to avoid churn.
9
+ *
10
+ * ### Example Usage
11
+ *
12
+ * ```ts
13
+ * // WebSocket connections
14
+ * interface WebSocketConfig {
15
+ * readonly url: WebSocketUrl;
16
+ * }
17
+ *
18
+ * type WebSocketUrl = string & Brand<"WebSocketUrl">;
19
+ * type UserId = string & Brand<"UserId">;
20
+ *
21
+ * const webSockets = createResources<
22
+ * WebSocket,
23
+ * WebSocketUrl,
24
+ * WebSocketConfig,
25
+ * User,
26
+ * UserId
27
+ * >({
28
+ * createResource: (config) => new WebSocket(config.url),
29
+ * getResourceKey: (config) => config.url,
30
+ * getConsumerId: (user) => user.id,
31
+ * disposalDelay: 1000,
32
+ * });
33
+ *
34
+ * // Add users to WebSocket connections
35
+ * webSockets.addConsumer(user1, [
36
+ * { url: "ws://server1.com" as WebSocketUrl },
37
+ * { url: "ws://server2.com" as WebSocketUrl },
38
+ * ]);
39
+ * webSockets.addConsumer(user2, [
40
+ * { url: "ws://server1.com" as WebSocketUrl },
41
+ * ]);
42
+ *
43
+ * // Remove users - server1 stays alive (user2 still using it)
44
+ * webSockets.removeConsumer(user1, [
45
+ * { url: "ws://server1.com" as WebSocketUrl },
46
+ * { url: "ws://server2.com" as WebSocketUrl },
47
+ * ]);
48
+ *
49
+ * // server2 gets disposed after delay, server1 stays alive
50
+ * ```
51
+ */
52
+ export const createResources = (config) => {
53
+ let isDisposed = false;
54
+ const resourcesMap = new Map();
55
+ const consumerCounts = new Map();
56
+ const consumers = new Map();
57
+ const disposalTimeouts = new Map();
58
+ const disposalDelay = config.disposalDelay ?? 100;
59
+ const ensureResource = (resourceConfig) => {
60
+ const key = config.getResourceKey(resourceConfig);
61
+ const timeout = disposalTimeouts.get(key);
62
+ if (timeout) {
63
+ clearTimeout(timeout);
64
+ disposalTimeouts.delete(key);
65
+ }
66
+ if (!resourcesMap.has(key)) {
67
+ const resource = config.createResource(resourceConfig);
68
+ resourcesMap.set(key, resource);
69
+ }
70
+ };
71
+ const scheduleDisposal = (key) => {
72
+ const timeout = setTimeout(() => {
73
+ const resource = resourcesMap.get(key);
74
+ if (resource) {
75
+ resource[Symbol.dispose]();
76
+ resourcesMap.delete(key);
77
+ }
78
+ disposalTimeouts.delete(key);
79
+ }, disposalDelay);
80
+ disposalTimeouts.set(key, timeout);
81
+ };
82
+ const resources = {
83
+ addConsumer: (consumer, resourceConfigs) => {
84
+ if (isDisposed)
85
+ return;
86
+ const consumerId = config.getConsumerId(consumer);
87
+ // Store consumer (last added consumer for this ID)
88
+ consumers.set(consumerId, consumer);
89
+ for (const resourceConfig of resourceConfigs) {
90
+ ensureResource(resourceConfig);
91
+ const resourceKey = config.getResourceKey(resourceConfig);
92
+ let counts = consumerCounts.get(resourceKey);
93
+ if (!counts) {
94
+ counts = new Map();
95
+ consumerCounts.set(resourceKey, counts);
96
+ }
97
+ const currentCount = counts.get(consumerId) ?? 0;
98
+ const newCount = currentCount + 1;
99
+ counts.set(consumerId, PositiveInt.orThrow(newCount));
100
+ // Call onConsumerAdded callback only when consumer is added for the first time (0 -> 1)
101
+ if (currentCount === 0 && config.onConsumerAdded) {
102
+ const resource = resourcesMap.get(resourceKey);
103
+ if (resource) {
104
+ config.onConsumerAdded(consumer, resource, resourceKey);
105
+ }
106
+ }
107
+ }
108
+ },
109
+ removeConsumer: (consumer, resourceConfigs) => {
110
+ if (isDisposed)
111
+ return ok();
112
+ const consumerId = config.getConsumerId(consumer);
113
+ for (const resourceConfig of resourceConfigs) {
114
+ const key = config.getResourceKey(resourceConfig);
115
+ const counts = consumerCounts.get(key);
116
+ if (!counts) {
117
+ return err({ type: "ResourceNotFoundError", resourceKey: key });
118
+ }
119
+ const currentCount = counts.get(consumerId);
120
+ if (currentCount == null) {
121
+ return err({
122
+ type: "ConsumerNotFoundError",
123
+ consumerId: consumerId,
124
+ resourceKey: key,
125
+ });
126
+ }
127
+ if (currentCount === 1) {
128
+ counts.delete(consumerId);
129
+ // Call onConsumerRemoved callback only when consumer is completely removed (1 -> 0)
130
+ if (config.onConsumerRemoved) {
131
+ const resource = resourcesMap.get(key);
132
+ if (resource) {
133
+ config.onConsumerRemoved(consumer, resource, key);
134
+ }
135
+ }
136
+ if (counts.size === 0) {
137
+ consumerCounts.delete(key);
138
+ scheduleDisposal(key);
139
+ }
140
+ }
141
+ else {
142
+ counts.set(consumerId, PositiveInt.orThrow(currentCount - 1));
143
+ }
144
+ }
145
+ if (!resources.hasConsumerAnyResource(consumer)) {
146
+ consumers.delete(consumerId);
147
+ }
148
+ return ok();
149
+ },
150
+ getResource: (key) => {
151
+ if (isDisposed)
152
+ return null;
153
+ return resourcesMap.get(key) ?? null;
154
+ },
155
+ getConsumersForResource: (key) => {
156
+ if (isDisposed)
157
+ return [];
158
+ const counts = consumerCounts.get(key);
159
+ return counts ? Array.from(counts.keys()) : [];
160
+ },
161
+ hasConsumerAnyResource: (consumer) => {
162
+ if (isDisposed)
163
+ return false;
164
+ const consumerId = config.getConsumerId(consumer);
165
+ // If slow, can be optimized with reverse index
166
+ return Array.from(consumerCounts.values()).some((counts) => counts.has(consumerId));
167
+ },
168
+ getConsumer: (consumerId) => {
169
+ if (isDisposed)
170
+ return null;
171
+ const consumer = consumers.get(consumerId);
172
+ if (!consumer)
173
+ return null;
174
+ // Only return consumer if it's currently using any resources
175
+ if (!resources.hasConsumerAnyResource(consumer)) {
176
+ return null;
177
+ }
178
+ return consumer;
179
+ },
180
+ [Symbol.dispose]: () => {
181
+ if (isDisposed)
182
+ return;
183
+ isDisposed = true;
184
+ for (const timeout of disposalTimeouts.values()) {
185
+ clearTimeout(timeout);
186
+ }
187
+ disposalTimeouts.clear();
188
+ for (const resource of resourcesMap.values()) {
189
+ resource[Symbol.dispose]();
190
+ }
191
+ resourcesMap.clear();
192
+ consumerCounts.clear();
193
+ consumers.clear();
194
+ },
195
+ };
196
+ return resources;
197
+ };