@evolu/common 6.0.1-preview.8 → 7.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 (184) hide show
  1. package/README.md +2 -2
  2. package/dist/src/Array.d.ts +256 -12
  3. package/dist/src/Array.d.ts.map +1 -1
  4. package/dist/src/Array.js +215 -9
  5. package/dist/src/Assert.d.ts +0 -13
  6. package/dist/src/Assert.d.ts.map +1 -1
  7. package/dist/src/Assert.js +0 -15
  8. package/dist/src/Brand.d.ts +75 -0
  9. package/dist/src/Brand.d.ts.map +1 -0
  10. package/dist/src/Brand.js +1 -0
  11. package/dist/src/Buffer.d.ts +1 -1
  12. package/dist/src/Buffer.d.ts.map +1 -1
  13. package/dist/src/Buffer.js +8 -7
  14. package/dist/src/Cache.d.ts +44 -0
  15. package/dist/src/Cache.d.ts.map +1 -0
  16. package/dist/src/Cache.js +52 -0
  17. package/dist/src/Callbacks.d.ts +45 -12
  18. package/dist/src/Callbacks.d.ts.map +1 -1
  19. package/dist/src/Callbacks.js +14 -7
  20. package/dist/src/Console.d.ts +31 -6
  21. package/dist/src/Console.d.ts.map +1 -1
  22. package/dist/src/Console.js +72 -9
  23. package/dist/src/Crypto.d.ts +56 -42
  24. package/dist/src/Crypto.d.ts.map +1 -1
  25. package/dist/src/Crypto.js +40 -53
  26. package/dist/src/Evolu/Db.d.ts +162 -74
  27. package/dist/src/Evolu/Db.d.ts.map +1 -1
  28. package/dist/src/Evolu/Db.js +284 -702
  29. package/dist/src/Evolu/Diff.d.ts +3 -3
  30. package/dist/src/Evolu/Diff.d.ts.map +1 -1
  31. package/dist/src/Evolu/Diff.js +7 -5
  32. package/dist/src/Evolu/Evolu.d.ts +214 -134
  33. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  34. package/dist/src/Evolu/Evolu.js +189 -180
  35. package/dist/src/Evolu/Internal.d.ts +0 -2
  36. package/dist/src/Evolu/Internal.d.ts.map +1 -1
  37. package/dist/src/Evolu/Internal.js +0 -2
  38. package/dist/src/Evolu/LocalAuth.d.ts +150 -0
  39. package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
  40. package/dist/src/Evolu/LocalAuth.js +174 -0
  41. package/dist/src/Evolu/Owner.d.ts +273 -120
  42. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  43. package/dist/src/Evolu/Owner.js +130 -104
  44. package/dist/src/Evolu/Platform.d.ts +9 -7
  45. package/dist/src/Evolu/Platform.d.ts.map +1 -1
  46. package/dist/src/Evolu/Protocol.d.ts +268 -240
  47. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  48. package/dist/src/Evolu/Protocol.js +600 -454
  49. package/dist/src/Evolu/Public.d.ts +6 -8
  50. package/dist/src/Evolu/Public.d.ts.map +1 -1
  51. package/dist/src/Evolu/Public.js +2 -3
  52. package/dist/src/Evolu/PublicKysely.d.ts.map +1 -1
  53. package/dist/src/Evolu/PublicKysely.js +3 -4
  54. package/dist/src/Evolu/Query.d.ts +2 -1
  55. package/dist/src/Evolu/Query.d.ts.map +1 -1
  56. package/dist/src/Evolu/Query.js +1 -1
  57. package/dist/src/Evolu/Relay.d.ts +91 -8
  58. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  59. package/dist/src/Evolu/Relay.js +214 -88
  60. package/dist/src/Evolu/Schema.d.ts +125 -47
  61. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  62. package/dist/src/Evolu/Schema.js +175 -31
  63. package/dist/src/Evolu/Storage.d.ts +249 -27
  64. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  65. package/dist/src/Evolu/Storage.js +198 -92
  66. package/dist/src/Evolu/Sync.d.ts +68 -13
  67. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  68. package/dist/src/Evolu/Sync.js +469 -20
  69. package/dist/src/Evolu/Timestamp.d.ts +83 -30
  70. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  71. package/dist/src/Evolu/Timestamp.js +79 -34
  72. package/dist/src/Identicon.d.ts +35 -0
  73. package/dist/src/Identicon.d.ts.map +1 -0
  74. package/dist/src/Identicon.js +143 -0
  75. package/dist/src/Instances.d.ts +34 -0
  76. package/dist/src/Instances.d.ts.map +1 -0
  77. package/dist/src/Instances.js +44 -0
  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/Object.d.ts +10 -4
  82. package/dist/src/Object.d.ts.map +1 -1
  83. package/dist/src/Object.js +9 -3
  84. package/dist/src/Platform.d.ts +20 -0
  85. package/dist/src/Platform.d.ts.map +1 -0
  86. package/dist/src/Platform.js +22 -0
  87. package/dist/src/Random.d.ts +3 -2
  88. package/dist/src/Random.d.ts.map +1 -1
  89. package/dist/src/Relation.d.ts +79 -0
  90. package/dist/src/Relation.d.ts.map +1 -0
  91. package/dist/src/Relation.js +127 -0
  92. package/dist/src/Resources.d.ts +118 -0
  93. package/dist/src/Resources.d.ts.map +1 -0
  94. package/dist/src/Resources.js +197 -0
  95. package/dist/src/Result.d.ts +184 -52
  96. package/dist/src/Result.d.ts.map +1 -1
  97. package/dist/src/Result.js +30 -241
  98. package/dist/src/Skiplist.js +2 -1
  99. package/dist/src/Sqlite.d.ts +89 -5
  100. package/dist/src/Sqlite.d.ts.map +1 -1
  101. package/dist/src/Sqlite.js +136 -9
  102. package/dist/src/Task.d.ts +586 -0
  103. package/dist/src/Task.d.ts.map +1 -0
  104. package/dist/src/Task.js +469 -0
  105. package/dist/src/Time.d.ts +66 -1
  106. package/dist/src/Time.d.ts.map +1 -1
  107. package/dist/src/Time.js +99 -5
  108. package/dist/src/Type.d.ts +676 -343
  109. package/dist/src/Type.d.ts.map +1 -1
  110. package/dist/src/Type.js +718 -467
  111. package/dist/src/Types.d.ts +1 -75
  112. package/dist/src/Types.d.ts.map +1 -1
  113. package/dist/src/WebSocket.d.ts +5 -2
  114. package/dist/src/WebSocket.d.ts.map +1 -1
  115. package/dist/src/WebSocket.js +12 -18
  116. package/dist/src/Worker.d.ts +39 -11
  117. package/dist/src/Worker.d.ts.map +1 -1
  118. package/dist/src/Worker.js +22 -4
  119. package/dist/src/index.d.ts +8 -3
  120. package/dist/src/index.d.ts.map +1 -1
  121. package/dist/src/index.js +8 -3
  122. package/package.json +15 -14
  123. package/src/Array.ts +301 -19
  124. package/src/Assert.ts +0 -21
  125. package/src/Brand.ts +75 -0
  126. package/src/Buffer.ts +7 -7
  127. package/src/Cache.ts +85 -0
  128. package/src/Callbacks.ts +62 -22
  129. package/src/Console.ts +91 -11
  130. package/src/Crypto.ts +94 -90
  131. package/src/Evolu/Db.ts +519 -1026
  132. package/src/Evolu/Diff.ts +7 -5
  133. package/src/Evolu/Evolu.ts +477 -361
  134. package/src/Evolu/Internal.ts +0 -2
  135. package/src/Evolu/LocalAuth.ts +463 -0
  136. package/src/Evolu/Owner.ts +355 -228
  137. package/src/Evolu/Platform.ts +9 -9
  138. package/src/Evolu/Protocol.ts +842 -742
  139. package/src/Evolu/Public.ts +7 -14
  140. package/src/Evolu/PublicKysely.ts +4 -5
  141. package/src/Evolu/Query.ts +3 -2
  142. package/src/Evolu/Relay.ts +406 -103
  143. package/src/Evolu/Schema.ts +323 -91
  144. package/src/Evolu/Storage.ts +559 -137
  145. package/src/Evolu/Sync.ts +819 -36
  146. package/src/Evolu/Timestamp.ts +90 -58
  147. package/src/Identicon.ts +197 -0
  148. package/src/Instances.ts +90 -0
  149. package/src/Number.ts +6 -10
  150. package/src/Object.ts +13 -5
  151. package/src/Platform.ts +26 -0
  152. package/src/Random.ts +3 -2
  153. package/src/Relation.ts +234 -0
  154. package/src/Resources.ts +367 -0
  155. package/src/Result.ts +191 -54
  156. package/src/Skiplist.ts +1 -1
  157. package/src/Sqlite.ts +152 -17
  158. package/src/Task.ts +901 -0
  159. package/src/Time.ts +180 -5
  160. package/src/Type.ts +1135 -730
  161. package/src/Types.ts +1 -77
  162. package/src/WebSocket.ts +27 -25
  163. package/src/Worker.ts +72 -23
  164. package/src/index.ts +8 -3
  165. package/dist/src/Evolu/Config.d.ts +0 -69
  166. package/dist/src/Evolu/Config.d.ts.map +0 -1
  167. package/dist/src/Evolu/Config.js +0 -9
  168. package/dist/src/Evolu/Kysely.d.ts +0 -6
  169. package/dist/src/Evolu/Kysely.d.ts.map +0 -1
  170. package/dist/src/Evolu/Kysely.js +0 -21
  171. package/dist/src/ManyToManyMap.d.ts +0 -26
  172. package/dist/src/ManyToManyMap.d.ts.map +0 -1
  173. package/dist/src/ManyToManyMap.js +0 -92
  174. package/dist/src/NanoId.d.ts +0 -27
  175. package/dist/src/NanoId.d.ts.map +0 -1
  176. package/dist/src/NanoId.js +0 -6
  177. package/dist/src/Promise.d.ts +0 -180
  178. package/dist/src/Promise.d.ts.map +0 -1
  179. package/dist/src/Promise.js +0 -176
  180. package/src/Evolu/Config.ts +0 -83
  181. package/src/Evolu/Kysely.ts +0 -38
  182. package/src/ManyToManyMap.ts +0 -140
  183. package/src/NanoId.ts +0 -39
  184. package/src/Promise.ts +0 -295
@@ -1,8 +1,8 @@
1
- import { NanoIdLibDep } from "../NanoId.js";
1
+ import { RandomBytesDep } from "../Crypto.js";
2
2
  import { Order } from "../Order.js";
3
3
  import { Result } from "../Result.js";
4
4
  import { TimeDep } from "../Time.js";
5
- import { Brand } from "../Types.js";
5
+ import { DateIso, InferType } from "../Type.js";
6
6
  export interface TimestampConfig {
7
7
  /**
8
8
  * Maximum physical clock drift allowed in ms.
@@ -14,7 +14,7 @@ export interface TimestampConfig {
14
14
  export interface TimestampConfigDep {
15
15
  readonly timestampConfig: TimestampConfig;
16
16
  }
17
- export type TimestampError = TimestampDriftError | TimestampCounterOverflowError | TimestampDuplicateNodeError | TimestampTimeOutOfRangeError;
17
+ export type TimestampError = TimestampDriftError | TimestampCounterOverflowError | TimestampTimeOutOfRangeError;
18
18
  export interface TimestampDriftError {
19
19
  readonly type: "TimestampDriftError";
20
20
  readonly next: Millis;
@@ -23,10 +23,6 @@ export interface TimestampDriftError {
23
23
  export interface TimestampCounterOverflowError {
24
24
  readonly type: "TimestampCounterOverflowError";
25
25
  }
26
- export interface TimestampDuplicateNodeError {
27
- readonly type: "TimestampDuplicateNodeError";
28
- readonly nodeId: NodeId;
29
- }
30
26
  export interface TimestampTimeOutOfRangeError {
31
27
  readonly type: "TimestampTimeOutOfRangeError";
32
28
  }
@@ -42,11 +38,11 @@ export interface TimestampTimeOutOfRangeError {
42
38
  *
43
39
  * `new Date(281474976710654).toString()` = Tue Aug 02 10889 07:31:49
44
40
  */
45
- export declare const Millis: import("../Type.js").BrandType<import("../Type.js").BrandType<import("../Type.js").Type<"Brand", number & Brand<"Int"> & Brand<"NonNegative">, number, import("../Type.js").NonNegativeError, number & Brand<"Int">, import("../Type.js").NumberError | import("../Type.js").IntError>, `LessThanOrEqualTo${number}`, import("../Type.js").LessThanOrEqualToError<number>, import("../Type.js").NumberError | import("../Type.js").NonNegativeError | import("../Type.js").IntError>, "Millis", import("../Type.js").BrandWithoutRefineError<"Millis", import("../Type.js").NumberError | import("../Type.js").NonNegativeError | import("../Type.js").IntError | import("../Type.js").LessThanOrEqualToError<number>>, never>;
41
+ export declare const Millis: import("../Type.js").BrandType<import("../Type.js").BrandType<import("../Type.js").Type<"Brand", number & import("../Brand.js").Brand<"Int"> & import("../Brand.js").Brand<"NonNegative">, number, import("../Type.js").NonNegativeError, number & import("../Brand.js").Brand<"Int">, import("../Type.js").IntError | import("../Type.js").NumberError>, `LessThanOrEqualTo${number}`, import("../Type.js").LessThanOrEqualToError<number>, import("../Type.js").NonNegativeError | import("../Type.js").IntError | import("../Type.js").NumberError>, "Millis", import("../Type.js").BrandWithoutRefineError<"Millis", import("../Type.js").NonNegativeError | import("../Type.js").IntError | import("../Type.js").NumberError | import("../Type.js").LessThanOrEqualToError<number>>, never>;
46
42
  export type Millis = typeof Millis.Type;
47
43
  export declare const minMillis: Millis;
48
44
  export declare const maxMillis: Millis;
49
- export declare const Counter: import("../Type.js").BrandType<import("../Type.js").BrandType<import("../Type.js").Type<"Brand", number & Brand<"Int"> & Brand<"NonNegative">, number, import("../Type.js").NonNegativeError, number & Brand<"Int">, import("../Type.js").NumberError | import("../Type.js").IntError>, "LessThanOrEqualTo65535", import("../Type.js").LessThanOrEqualToError<65535>, import("../Type.js").NumberError | import("../Type.js").NonNegativeError | import("../Type.js").IntError>, "Counter", import("../Type.js").BrandWithoutRefineError<"Counter", import("../Type.js").NumberError | import("../Type.js").NonNegativeError | import("../Type.js").IntError | import("../Type.js").LessThanOrEqualToError<65535>>, never>;
45
+ export declare const Counter: import("../Type.js").BrandType<import("../Type.js").BrandType<import("../Type.js").Type<"Brand", number & import("../Brand.js").Brand<"Int"> & import("../Brand.js").Brand<"NonNegative">, number, import("../Type.js").NonNegativeError, number & import("../Brand.js").Brand<"Int">, import("../Type.js").IntError | import("../Type.js").NumberError>, "LessThanOrEqualTo65535", import("../Type.js").LessThanOrEqualToError<65535>, import("../Type.js").NonNegativeError | import("../Type.js").IntError | import("../Type.js").NumberError>, "Counter", import("../Type.js").BrandWithoutRefineError<"Counter", import("../Type.js").NonNegativeError | import("../Type.js").IntError | import("../Type.js").NumberError | import("../Type.js").LessThanOrEqualToError<65535>>, never>;
50
46
  export type Counter = typeof Counter.Type;
51
47
  export declare const minCounter: Counter;
52
48
  export declare const maxCounter: Counter;
@@ -63,14 +59,17 @@ export declare const maxCounter: Counter;
63
59
  *
64
60
  * https://lemire.me/blog/2019/12/12/are-64-bit-random-identifiers-free-from-collision
65
61
  *
66
- * What will happen if a different device generates the same NodeId?
62
+ * What happens if different devices generate the same NodeId?
67
63
  *
68
- * If the device belongs to a different owner, nothing will happen because
69
- * different owner have different owner IDs. Timestamps are partitioned by
70
- * OwnerId.
64
+ * If devices with the same NodeId use different owners, no issues occur.
71
65
  *
72
- * If the device belongs to the same owner, the other device will return
73
- * {@link TimestampDuplicateNodeError}.
66
+ * If devices with the same NodeId use the same owner, problems only arise when
67
+ * they generate CRDT messages with identical timestamps (same millis, counter,
68
+ * and NodeId). In this case, the protocol sync algorithm treats them as the
69
+ * same message: the first will be synced with the relay, while the affected
70
+ * message will not be delivered. The affected devices will see different data
71
+ * yet they will think they are synced. This is extremely rare and can be
72
+ * resolved by resetting one device to generate a new NodeId.
74
73
  */
75
74
  export declare const NodeId: import("../Type.js").BrandType<import("../Type.js").Type<"String", string, string, import("../Type.js").StringError, string, import("../Type.js").StringError>, "NodeId", import("../Type.js").RegexError<"NodeId">, import("../Type.js").StringError>;
76
75
  export type NodeId = typeof NodeId.Type;
@@ -79,28 +78,82 @@ export declare const maxNodeId: NodeId;
79
78
  /**
80
79
  * Hybrid Logical Clock timestamp.
81
80
  *
81
+ * Timestamps serve as globally unique, causally ordered identifiers for CRDT
82
+ * messages in Evolu's sync protocol.
83
+ *
84
+ * ### Why Hybrid Logical Clocks
85
+ *
86
+ * Evolu uses Hybrid Logical Clocks (HLC), which combine physical time (millis)
87
+ * with a logical counter. This hybrid approach preserves causality like logical
88
+ * clocks while staying close to physical time for better human
89
+ * interpretability.
90
+ *
91
+ * The counter component ensures causality is maintained even when physical
92
+ * clocks are imperfect. When clocks drift or operations occur concurrently, the
93
+ * counter increments to establish a total order. This means Evolu achieves
94
+ * well-defined, eventually-consistent behavior regardless of physical clock
95
+ * accuracy.
96
+ *
97
+ * Vector clocks can accurately track causality and detect concurrent
98
+ * operations, but they require unbounded space in peer-to-peer systems and
99
+ * crucially, still don't solve our fundamental problem: when they detect
100
+ * operations as concurrent, we still need a deterministic way to choose a
101
+ * winner. Additionally, any deterministic conflict resolution can be gamed by
102
+ * malicious actors.
103
+ *
104
+ * HLC timestamps work well in practice because modern device clocks accurately
105
+ * reflect the order of sequential edits in the common case. Evolu's `maxDrift`
106
+ * configuration protects against buggy clocks and prevents problematic
107
+ * future-dated entries from propagating through the network.
108
+ *
109
+ * ### References
110
+ *
82
111
  * - https://muratbuffalo.blogspot.com/2014/07/hybrid-logical-clocks.html
83
112
  * - https://sergeiturukin.com/2017/06/26/hybrid-logical-clocks.html
84
113
  * - https://jaredforsyth.com/posts/hybrid-logical-clocks/
114
+ * - https://willowprotocol.org/more/timestamps_really/index.html
115
+ *
116
+ * ### Privacy Considerations
117
+ *
118
+ * Timestamps are metadata visible to relays and collaborators. While it can be
119
+ * considered a privacy leak, let us explain why it's necessary, and how to
120
+ * avoid it if maximum privacy is required.
121
+ *
122
+ * With real-time communication, participants always see activity (receiving
123
+ * bytes). We cannot trust anyone not to store that information, so explicitly
124
+ * exposing timestamps doesn't add additional risk.
125
+ *
126
+ * If we really want not to leak user activity, we can implement a local write
127
+ * queue:
128
+ *
129
+ * 1. Write changes immediately to a local-only table
130
+ * 2. Periodically and randomly flush messages to sync tables
131
+ *
132
+ * **Trade-off:** It breaks real-time collaboration.
85
133
  */
86
134
  export declare const Timestamp: import("../Type.js").ObjectType<{
87
- millis: import("../Type.js").BrandType<import("../Type.js").BrandType<import("../Type.js").Type<"Brand", number & Brand<"Int"> & Brand<"NonNegative">, number, import("../Type.js").NonNegativeError, number & Brand<"Int">, import("../Type.js").NumberError | import("../Type.js").IntError>, `LessThanOrEqualTo${number}`, import("../Type.js").LessThanOrEqualToError<number>, import("../Type.js").NumberError | import("../Type.js").NonNegativeError | import("../Type.js").IntError>, "Millis", import("../Type.js").BrandWithoutRefineError<"Millis", import("../Type.js").NumberError | import("../Type.js").NonNegativeError | import("../Type.js").IntError | import("../Type.js").LessThanOrEqualToError<number>>, never>;
88
- counter: import("../Type.js").BrandType<import("../Type.js").BrandType<import("../Type.js").Type<"Brand", number & Brand<"Int"> & Brand<"NonNegative">, number, import("../Type.js").NonNegativeError, number & Brand<"Int">, import("../Type.js").NumberError | import("../Type.js").IntError>, "LessThanOrEqualTo65535", import("../Type.js").LessThanOrEqualToError<65535>, import("../Type.js").NumberError | import("../Type.js").NonNegativeError | import("../Type.js").IntError>, "Counter", import("../Type.js").BrandWithoutRefineError<"Counter", import("../Type.js").NumberError | import("../Type.js").NonNegativeError | import("../Type.js").IntError | import("../Type.js").LessThanOrEqualToError<65535>>, never>;
135
+ millis: import("../Type.js").BrandType<import("../Type.js").BrandType<import("../Type.js").Type<"Brand", number & import("../Brand.js").Brand<"Int"> & import("../Brand.js").Brand<"NonNegative">, number, import("../Type.js").NonNegativeError, number & import("../Brand.js").Brand<"Int">, import("../Type.js").IntError | import("../Type.js").NumberError>, `LessThanOrEqualTo${number}`, import("../Type.js").LessThanOrEqualToError<number>, import("../Type.js").NonNegativeError | import("../Type.js").IntError | import("../Type.js").NumberError>, "Millis", import("../Type.js").BrandWithoutRefineError<"Millis", import("../Type.js").NonNegativeError | import("../Type.js").IntError | import("../Type.js").NumberError | import("../Type.js").LessThanOrEqualToError<number>>, never>;
136
+ counter: import("../Type.js").BrandType<import("../Type.js").BrandType<import("../Type.js").Type<"Brand", number & import("../Brand.js").Brand<"Int"> & import("../Brand.js").Brand<"NonNegative">, number, import("../Type.js").NonNegativeError, number & import("../Brand.js").Brand<"Int">, import("../Type.js").IntError | import("../Type.js").NumberError>, "LessThanOrEqualTo65535", import("../Type.js").LessThanOrEqualToError<65535>, import("../Type.js").NonNegativeError | import("../Type.js").IntError | import("../Type.js").NumberError>, "Counter", import("../Type.js").BrandWithoutRefineError<"Counter", import("../Type.js").NonNegativeError | import("../Type.js").IntError | import("../Type.js").NumberError | import("../Type.js").LessThanOrEqualToError<65535>>, never>;
89
137
  nodeId: import("../Type.js").BrandType<import("../Type.js").Type<"String", string, string, import("../Type.js").StringError, string, import("../Type.js").StringError>, "NodeId", import("../Type.js").RegexError<"NodeId">, import("../Type.js").StringError>;
90
138
  }>;
91
- export type Timestamp = typeof Timestamp.Type;
139
+ export interface Timestamp extends InferType<typeof Timestamp> {
140
+ }
141
+ /** Equality function for comparing {@link Timestamp}. */
142
+ export declare const eqTimestamp: import("../Eq.js").Eq<{
143
+ readonly millis: number & import("../Brand.js").Brand<"Int"> & import("../Brand.js").Brand<"NonNegative"> & import("../Brand.js").Brand<`LessThanOrEqualTo${number}`> & import("../Brand.js").Brand<"Millis">;
144
+ readonly counter: number & import("../Brand.js").Brand<"Int"> & import("../Brand.js").Brand<"NonNegative"> & import("../Brand.js").Brand<"LessThanOrEqualTo65535"> & import("../Brand.js").Brand<"Counter">;
145
+ readonly nodeId: string & import("../Brand.js").Brand<"NodeId">;
146
+ }>;
92
147
  export declare const createTimestamp: ({ millis, counter, nodeId, }?: Partial<Timestamp>) => Timestamp;
93
- export declare const createInitialTimestamp: (deps: NanoIdLibDep) => Timestamp;
94
- /** TimestampString is a sortable string version of {@link Timestamp}. */
95
- export type TimestampString = string & Brand<"TimestampString">;
96
- export declare const timestampToTimestampString: (t: Timestamp) => TimestampString;
97
- export declare const timestampStringToTimestamp: (timestampString: TimestampString) => Timestamp;
148
+ export declare const createInitialTimestamp: (deps: RandomBytesDep) => Timestamp;
98
149
  export declare const sendTimestamp: (deps: TimeDep & TimestampConfigDep) => (timestamp: Timestamp) => Result<Timestamp, TimestampDriftError | TimestampCounterOverflowError | TimestampTimeOutOfRangeError>;
99
- export declare const receiveTimestamp: (deps: TimeDep & TimestampConfigDep) => (local: Timestamp, remote: Timestamp) => Result<Timestamp, TimestampDriftError | TimestampCounterOverflowError | TimestampDuplicateNodeError | TimestampTimeOutOfRangeError>;
100
- /** BinaryTimestamp is a binary and sortable version of {@link Timestamp} for DB. */
101
- export type BinaryTimestamp = Uint8Array & Brand<"BinaryTimestamp">;
102
- export declare const binaryTimestampLength: number & Brand<"Int"> & Brand<"NonNegative">;
103
- export declare const timestampToBinaryTimestamp: (timestamp: Timestamp) => BinaryTimestamp;
104
- export declare const binaryTimestampToTimestamp: (timestamp: BinaryTimestamp) => Timestamp;
105
- export declare const orderBinaryTimestamp: Order<BinaryTimestamp>;
150
+ export declare const receiveTimestamp: (deps: TimeDep & TimestampConfigDep) => (local: Timestamp, remote: Timestamp) => Result<Timestamp, TimestampDriftError | TimestampCounterOverflowError | TimestampTimeOutOfRangeError>;
151
+ /** Sortable bytes representation of {@link Timestamp}. */
152
+ export declare const TimestampBytes: import("../Type.js").BrandType<import("../Type.js").Type<"Uint8Array", Uint8Array<ArrayBufferLike>, Uint8Array<ArrayBufferLike>, import("../Type.js").Uint8ArrayError, Uint8Array<ArrayBufferLike>, import("../Type.js").Uint8ArrayError>, "TimestampBytes", import("../Type.js").BrandWithoutRefineError<"TimestampBytes", import("../Type.js").Uint8ArrayError>, never>;
153
+ export type TimestampBytes = typeof TimestampBytes.Type;
154
+ export declare const timestampBytesLength: number & import("../Brand.js").Brand<"Int"> & import("../Brand.js").Brand<"NonNegative">;
155
+ export declare const timestampToTimestampBytes: (timestamp: Timestamp) => TimestampBytes;
156
+ export declare const timestampBytesToTimestamp: (timestamp: TimestampBytes) => Timestamp;
157
+ export declare const orderTimestampBytes: Order<TimestampBytes>;
158
+ export declare const timestampToDateIso: (timestamp: Timestamp) => DateIso;
106
159
  //# sourceMappingURL=Timestamp.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Timestamp.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Timestamp.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Timestamp.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Timestamp.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,9 @@
1
- import { assert } from "../Assert.js";
1
+ import { bytesToHex } from "../Buffer.js";
2
+ import { createEqObject, eqNumber, eqString } from "../Eq.js";
2
3
  import { increment } from "../Number.js";
3
4
  import { orderUint8Array } from "../Order.js";
4
- import { err, getOrThrow, ok } from "../Result.js";
5
- import { brand, lessThanOrEqualTo, NonNegativeInt, object, regex, String, } from "../Type.js";
5
+ import { err, ok } from "../Result.js";
6
+ import { brand, lessThanOrEqualTo, NonNegativeInt, object, regex, String, Uint8Array, } from "../Type.js";
6
7
  /**
7
8
  * Millis is a timestamp in milliseconds, like `Date.now()`, but limited to the
8
9
  * maximum value representable in 6 bytes (281474976710655) minus 1 (reserved
@@ -34,14 +35,17 @@ export const maxCounter = 65535;
34
35
  *
35
36
  * https://lemire.me/blog/2019/12/12/are-64-bit-random-identifiers-free-from-collision
36
37
  *
37
- * What will happen if a different device generates the same NodeId?
38
+ * What happens if different devices generate the same NodeId?
38
39
  *
39
- * If the device belongs to a different owner, nothing will happen because
40
- * different owner have different owner IDs. Timestamps are partitioned by
41
- * OwnerId.
40
+ * If devices with the same NodeId use different owners, no issues occur.
42
41
  *
43
- * If the device belongs to the same owner, the other device will return
44
- * {@link TimestampDuplicateNodeError}.
42
+ * If devices with the same NodeId use the same owner, problems only arise when
43
+ * they generate CRDT messages with identical timestamps (same millis, counter,
44
+ * and NodeId). In this case, the protocol sync algorithm treats them as the
45
+ * same message: the first will be synced with the relay, while the affected
46
+ * message will not be delivered. The affected devices will see different data
47
+ * yet they will think they are synced. This is extremely rare and can be
48
+ * resolved by resetting one device to generate a new NodeId.
45
49
  */
46
50
  export const NodeId = regex("NodeId", /^[a-f0-9]{16}$/)(String);
47
51
  export const minNodeId = "0000000000000000";
@@ -49,36 +53,75 @@ export const maxNodeId = "ffffffffffffffff";
49
53
  /**
50
54
  * Hybrid Logical Clock timestamp.
51
55
  *
56
+ * Timestamps serve as globally unique, causally ordered identifiers for CRDT
57
+ * messages in Evolu's sync protocol.
58
+ *
59
+ * ### Why Hybrid Logical Clocks
60
+ *
61
+ * Evolu uses Hybrid Logical Clocks (HLC), which combine physical time (millis)
62
+ * with a logical counter. This hybrid approach preserves causality like logical
63
+ * clocks while staying close to physical time for better human
64
+ * interpretability.
65
+ *
66
+ * The counter component ensures causality is maintained even when physical
67
+ * clocks are imperfect. When clocks drift or operations occur concurrently, the
68
+ * counter increments to establish a total order. This means Evolu achieves
69
+ * well-defined, eventually-consistent behavior regardless of physical clock
70
+ * accuracy.
71
+ *
72
+ * Vector clocks can accurately track causality and detect concurrent
73
+ * operations, but they require unbounded space in peer-to-peer systems and
74
+ * crucially, still don't solve our fundamental problem: when they detect
75
+ * operations as concurrent, we still need a deterministic way to choose a
76
+ * winner. Additionally, any deterministic conflict resolution can be gamed by
77
+ * malicious actors.
78
+ *
79
+ * HLC timestamps work well in practice because modern device clocks accurately
80
+ * reflect the order of sequential edits in the common case. Evolu's `maxDrift`
81
+ * configuration protects against buggy clocks and prevents problematic
82
+ * future-dated entries from propagating through the network.
83
+ *
84
+ * ### References
85
+ *
52
86
  * - https://muratbuffalo.blogspot.com/2014/07/hybrid-logical-clocks.html
53
87
  * - https://sergeiturukin.com/2017/06/26/hybrid-logical-clocks.html
54
88
  * - https://jaredforsyth.com/posts/hybrid-logical-clocks/
89
+ * - https://willowprotocol.org/more/timestamps_really/index.html
90
+ *
91
+ * ### Privacy Considerations
92
+ *
93
+ * Timestamps are metadata visible to relays and collaborators. While it can be
94
+ * considered a privacy leak, let us explain why it's necessary, and how to
95
+ * avoid it if maximum privacy is required.
96
+ *
97
+ * With real-time communication, participants always see activity (receiving
98
+ * bytes). We cannot trust anyone not to store that information, so explicitly
99
+ * exposing timestamps doesn't add additional risk.
100
+ *
101
+ * If we really want not to leak user activity, we can implement a local write
102
+ * queue:
103
+ *
104
+ * 1. Write changes immediately to a local-only table
105
+ * 2. Periodically and randomly flush messages to sync tables
106
+ *
107
+ * **Trade-off:** It breaks real-time collaboration.
55
108
  */
56
109
  export const Timestamp = object({
57
110
  millis: Millis,
58
111
  counter: Counter,
59
112
  nodeId: NodeId,
60
113
  });
114
+ /** Equality function for comparing {@link Timestamp}. */
115
+ export const eqTimestamp = createEqObject({
116
+ millis: eqNumber,
117
+ counter: eqNumber,
118
+ nodeId: eqString,
119
+ });
61
120
  export const createTimestamp = ({ millis = minMillis, counter = minCounter, nodeId = minNodeId, } = {}) => ({ millis, counter, nodeId });
62
- const hexAlphabet = "0123456789abcdef";
63
121
  export const createInitialTimestamp = (deps) => {
64
- const nodeId = deps.nanoIdLib.customAlphabet(hexAlphabet, 16)();
122
+ const nodeId = bytesToHex(deps.randomBytes.create(8));
65
123
  return createTimestamp({ nodeId });
66
124
  };
67
- export const timestampToTimestampString = (t) => [
68
- new Date(t.millis).toISOString(),
69
- t.counter.toString(16).toUpperCase().padStart(4, "0"),
70
- t.nodeId,
71
- ].join("-");
72
- export const timestampStringToTimestamp = (timestampString) => {
73
- const array = timestampString.split("-");
74
- const timestamp = {
75
- millis: Date.parse(array.slice(0, 3).join("-")).valueOf(),
76
- counter: parseInt(array[3], 16),
77
- nodeId: array[4],
78
- };
79
- assert(Timestamp.is(timestamp), "timestampString is malformed");
80
- return timestamp;
81
- };
82
125
  const getNextMillis = (deps) => (millis) => {
83
126
  const now = Millis.from(deps.time.now());
84
127
  if (!now.ok) {
@@ -115,9 +158,6 @@ export const sendTimestamp = (deps) => (timestamp) => {
115
158
  });
116
159
  };
117
160
  export const receiveTimestamp = (deps) => (local, remote) => {
118
- if (local.nodeId === remote.nodeId) {
119
- return err({ type: "TimestampDuplicateNodeError", nodeId: local.nodeId });
120
- }
121
161
  const millis = getNextMillis(deps)([local.millis, remote.millis]);
122
162
  if (!millis.ok)
123
163
  return millis;
@@ -136,11 +176,13 @@ export const receiveTimestamp = (deps) => (local, remote) => {
136
176
  nodeId: local.nodeId,
137
177
  });
138
178
  };
139
- export const binaryTimestampLength = getOrThrow(NonNegativeInt.from(16));
140
- export const timestampToBinaryTimestamp = (timestamp) => {
179
+ /** Sortable bytes representation of {@link Timestamp}. */
180
+ export const TimestampBytes = brand("TimestampBytes", Uint8Array);
181
+ export const timestampBytesLength = NonNegativeInt.orThrow(16);
182
+ export const timestampToTimestampBytes = (timestamp) => {
141
183
  const { millis, counter, nodeId } = timestamp;
142
184
  // 6 bytes for millis, 2 bytes for counter, 8 bytes for nodeId.
143
- const value = new Uint8Array(16);
185
+ const value = new globalThis.Uint8Array(16);
144
186
  // Encode `millis` into the first 6 bytes.
145
187
  const millisBigInt = BigInt(millis);
146
188
  value[0] = Number((millisBigInt >> 40n) & 0xffn);
@@ -159,7 +201,7 @@ export const timestampToBinaryTimestamp = (timestamp) => {
159
201
  }
160
202
  return value;
161
203
  };
162
- export const binaryTimestampToTimestamp = (timestamp) => {
204
+ export const timestampBytesToTimestamp = (timestamp) => {
163
205
  // Decode `millis` from the first 6 bytes.
164
206
  const millis = (BigInt(timestamp[0]) << 40n) |
165
207
  (BigInt(timestamp[1]) << 32n) |
@@ -176,4 +218,7 @@ export const binaryTimestampToTimestamp = (timestamp) => {
176
218
  }
177
219
  return { millis: Number(millis), counter, nodeId };
178
220
  };
179
- export const orderBinaryTimestamp = orderUint8Array;
221
+ export const orderTimestampBytes = orderUint8Array;
222
+ export const timestampToDateIso = (timestamp) =>
223
+ // `as DateIso` is safe because the timestamp is always valid
224
+ new Date(timestamp.millis).toISOString();
@@ -0,0 +1,35 @@
1
+ import type { Brand } from "./Brand.js";
2
+ import { Id } from "./Type.js";
3
+ /**
4
+ * SVG string representing a visual identicon for an {@link Id}, created with
5
+ * {@link createIdenticon}.
6
+ */
7
+ export type Identicon = string & Brand<"Identicon">;
8
+ /** {@link Identicon} style. */
9
+ export type IdenticonStyle = "github" | "quadrant" | "gradient" | "sutnar";
10
+ /**
11
+ * Creates a deterministic identicon SVG from an {@link Id}.
12
+ *
13
+ * Works with any {@link Id} including branded IDs like `OwnerId`, etc.
14
+ *
15
+ * Available styles:
16
+ *
17
+ * - `"github"` (default): 5x5 grid with horizontal mirroring (GitHub-style)
18
+ * - `"quadrant"`: 2x2 grid with direct RGB color mapping from bytes
19
+ * - `"gradient"`: Diagonal stripes with smooth color gradients
20
+ * - `"sutnar"`: Three compositional variants with adaptive colors
21
+ *
22
+ * ### Example
23
+ *
24
+ * ```ts
25
+ * const svg = createIdenticon(id);
26
+ * const quadrantStyle = createIdenticon(id, "quadrant");
27
+ * const gradientStyle = createIdenticon(id, "gradient");
28
+ * const sutnarStyle = createIdenticon(id, "sutnar");
29
+ *
30
+ * // Works with branded IDs
31
+ * const ownerSvg = createIdenticon(ownerId);
32
+ * ```
33
+ */
34
+ export declare const createIdenticon: (id: Id, style?: IdenticonStyle) => Identicon;
35
+ //# sourceMappingURL=Identicon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Identicon.d.ts","sourceRoot":"","sources":["../../src/Identicon.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,EAAE,EAAe,MAAM,WAAW,CAAC;AAG5C;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,KAAK,CAAC,WAAW,CAAC,CAAC;AAEpD,+BAA+B;AAC/B,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,GAAG,QAAQ,CAAC;AAE3E;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,eAAe,GAC1B,IAAI,EAAE,EACN,QAAO,cAAyB,KAC/B,SA4JF,CAAC"}
@@ -0,0 +1,143 @@
1
+ import { idToIdBytes } from "./Type.js";
2
+ import { md5 } from "@noble/hashes/legacy.js";
3
+ /**
4
+ * Creates a deterministic identicon SVG from an {@link Id}.
5
+ *
6
+ * Works with any {@link Id} including branded IDs like `OwnerId`, etc.
7
+ *
8
+ * Available styles:
9
+ *
10
+ * - `"github"` (default): 5x5 grid with horizontal mirroring (GitHub-style)
11
+ * - `"quadrant"`: 2x2 grid with direct RGB color mapping from bytes
12
+ * - `"gradient"`: Diagonal stripes with smooth color gradients
13
+ * - `"sutnar"`: Three compositional variants with adaptive colors
14
+ *
15
+ * ### Example
16
+ *
17
+ * ```ts
18
+ * const svg = createIdenticon(id);
19
+ * const quadrantStyle = createIdenticon(id, "quadrant");
20
+ * const gradientStyle = createIdenticon(id, "gradient");
21
+ * const sutnarStyle = createIdenticon(id, "sutnar");
22
+ *
23
+ * // Works with branded IDs
24
+ * const ownerSvg = createIdenticon(ownerId);
25
+ * ```
26
+ */
27
+ export const createIdenticon = (id, style = "github") => {
28
+ const bytes = idToIdBytes(id);
29
+ switch (style) {
30
+ case "github": {
31
+ // GitHub-style identicon: MD5 hash the bytes first
32
+ const hashedBytes = md5(bytes);
33
+ // Map function for value ranges
34
+ const map = (value, inMin, inMax, outMin, outMax) => ((value - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
35
+ // Extract 12-bit hue from bytes[12] (lower 4 bits) + bytes[13]
36
+ const h = ((hashedBytes[12] & 0x0f) << 8) | hashedBytes[13];
37
+ const hue = map(h, 0, 4095, 0, 360);
38
+ const saturation = 65 - map(hashedBytes[14], 0, 255, 0, 20);
39
+ const lightness = 75 - map(hashedBytes[15], 0, 255, 0, 20);
40
+ const fgColor = `hsl(${hue},${saturation}%,${lightness}%)`;
41
+ const bgColor = `hsl(${hue},${saturation}%,90%)`;
42
+ let rects = `<rect width="5" height="5" fill="${bgColor}"/>`;
43
+ // Extract nibbles and generate pattern
44
+ let nibbleIndex = 0;
45
+ for (let x = 2; x >= 0; x--) {
46
+ for (let y = 0; y < 5; y++) {
47
+ const byte = hashedBytes[Math.floor(nibbleIndex / 2)];
48
+ const nibble = nibbleIndex % 2 === 0 ? byte >> 4 : byte & 0x0f;
49
+ const paint = nibble % 2 === 0;
50
+ nibbleIndex++;
51
+ if (paint) {
52
+ rects += `<rect x="${x}" y="${y}" width="1" height="1" fill="${fgColor}"/>`;
53
+ const mx = 4 - x;
54
+ if (mx !== x) {
55
+ rects += `<rect x="${mx}" y="${y}" width="1" height="1" fill="${fgColor}"/>`;
56
+ }
57
+ }
58
+ }
59
+ }
60
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 5 5" shape-rendering="crispEdges">${rects}</svg>`;
61
+ }
62
+ case "quadrant": {
63
+ const toHex = (b) => b.toString(16).padStart(2, "0");
64
+ let rects = "";
65
+ for (let i = 0; i < 4; i++) {
66
+ const x = i % 2;
67
+ const y = Math.floor(i / 2);
68
+ const r = bytes[i * 3];
69
+ const g = bytes[i * 3 + 1];
70
+ const b = bytes[i * 3 + 2];
71
+ const color = `#${toHex(r)}${toHex(g)}${toHex(b)}`;
72
+ rects += `<rect x="${x}" y="${y}" width="1" height="1" fill="${color}"/>`;
73
+ }
74
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2 2">${rects}</svg>`;
75
+ }
76
+ case "gradient": {
77
+ // Smooth color gradients with diagonal stripes.
78
+ const toHex = (b) => b.toString(16).padStart(2, "0");
79
+ // Generate colors from bytes.
80
+ const color1 = `#${toHex(bytes[0])}${toHex(bytes[1])}${toHex(bytes[2])}`;
81
+ const color2 = `#${toHex(bytes[3])}${toHex(bytes[4])}${toHex(bytes[5])}`;
82
+ const color3 = `#${toHex(bytes[6])}${toHex(bytes[7])}${toHex(bytes[8])}`;
83
+ let defs = "";
84
+ let shapes = "";
85
+ // Diagonal stripes with gradient.
86
+ defs += `<linearGradient id="grad1-${id}" x1="0%" y1="0%" x2="0%" y2="100%">`;
87
+ defs += `<stop offset="0%" style="stop-color:${color1};stop-opacity:1" />`;
88
+ defs += `<stop offset="100%" style="stop-color:${color2};stop-opacity:1" />`;
89
+ defs += `</linearGradient>`;
90
+ defs += `<linearGradient id="grad2-${id}" x1="0%" y1="0%" x2="0%" y2="100%">`;
91
+ defs += `<stop offset="0%" style="stop-color:${color2};stop-opacity:1" />`;
92
+ defs += `<stop offset="100%" style="stop-color:${color3};stop-opacity:1" />`;
93
+ defs += `</linearGradient>`;
94
+ shapes += `<rect width="100" height="100" fill="url(#grad1-${id})"/>`;
95
+ const stripeWidth = 15 + (bytes[9] / 255) * 20;
96
+ const angle = 30 + (bytes[10] / 255) * 60;
97
+ shapes += `<rect x="20" y="-50" width="${stripeWidth}" height="200" fill="url(#grad2-${id})" transform="rotate(${angle} 50 50)" opacity="0.7"/>`;
98
+ shapes += `<rect x="60" y="-50" width="${stripeWidth}" height="200" fill="url(#grad2-${id})" transform="rotate(${angle} 50 50)" opacity="0.5"/>`;
99
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs>${defs}</defs>${shapes}</svg>`;
100
+ }
101
+ case "sutnar": {
102
+ // Three compositional variants with adaptive colors.
103
+ const hue = (bytes[0] / 255) * 360;
104
+ const saturation = 50 + (bytes[1] / 255) * 30;
105
+ const lightness = 50 + (bytes[2] / 255) * 20;
106
+ // Generate palette from base hue with variations
107
+ const toHsl = (h, s, l) => `hsl(${h},${s}%,${l}%)`;
108
+ const color1 = toHsl(hue, saturation, lightness);
109
+ const color2 = toHsl((hue + 120) % 360, saturation, lightness);
110
+ const color3 = toHsl((hue + 240) % 360, saturation, lightness);
111
+ const color4 = toHsl(hue, saturation * 0.3, lightness * 0.5);
112
+ const color5 = toHsl(hue, saturation * 0.5, Math.min(lightness * 1.3, 90));
113
+ const palette = [color1, color2, color3, color4, color5];
114
+ // Layout variant based on first byte.
115
+ const variant = bytes[3] % 3;
116
+ let shapes = "";
117
+ // Almost white background with subtle tint.
118
+ shapes += `<rect width="100" height="100" fill="${toHsl(hue, 10, 95)}"/>`;
119
+ if (variant === 0) {
120
+ // Composition A: Circle + horizontal bar.
121
+ const circleColor = palette[bytes[4] % palette.length];
122
+ const barColor = palette[(bytes[4] + 1) % palette.length];
123
+ shapes += `<circle cx="30" cy="50" r="22" fill="${circleColor}"/>`;
124
+ shapes += `<rect x="60" y="40" width="35" height="20" fill="${barColor}"/>`;
125
+ }
126
+ else if (variant === 1) {
127
+ // Composition B: Vertical bar + circle.
128
+ const barColor = palette[bytes[5] % palette.length];
129
+ const circleColor = palette[(bytes[5] + 1) % palette.length];
130
+ shapes += `<rect x="15" y="10" width="18" height="80" fill="${barColor}"/>`;
131
+ shapes += `<circle cx="70" cy="50" r="15" fill="${circleColor}"/>`;
132
+ }
133
+ else {
134
+ // Composition C: Square + circle.
135
+ const squareColor = palette[bytes[6] % palette.length];
136
+ const circleColor = palette[(bytes[6] + 1) % palette.length];
137
+ shapes += `<rect x="20" y="20" width="30" height="30" fill="${squareColor}"/>`;
138
+ shapes += `<circle cx="70" cy="70" r="18" fill="${circleColor}"/>`;
139
+ }
140
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">${shapes}</svg>`;
141
+ }
142
+ }
143
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Manages disposable instances by key, ensuring exactly one instance per key.
3
+ *
4
+ * Use cases:
5
+ *
6
+ * - One mutex per key to prevent concurrent writes
7
+ * - Preserving state during hot module reloading
8
+ *
9
+ * **Important:** Do not use this as global shared state. Use it locally or pass
10
+ * it as a dependency instead. The only exception is for hot reloading support,
11
+ * where Evolu uses it to ensure only one instance exists across module reloads
12
+ * (having two Evolu instances with the same name would mean two SQLite
13
+ * connections to the same file, which could corrupt data).
14
+ */
15
+ export interface Instances<K extends string, T extends Disposable> extends Disposable {
16
+ /**
17
+ * Ensures an instance exists for the given key, creating it if necessary. If
18
+ * the instance already exists, the optional `onCacheHit` callback is invoked
19
+ * to update the existing instance.
20
+ */
21
+ readonly ensure: (key: K, create: () => T, onCacheHit?: (instance: T) => void) => T;
22
+ /** Gets an instance by key, or returns `null` if it doesn't exist. */
23
+ readonly get: (key: K) => T | null;
24
+ /** Checks if an instance exists for the given key. */
25
+ readonly has: (key: K) => boolean;
26
+ /**
27
+ * Deletes and disposes an instance by key. Returns `true` if the instance
28
+ * existed and was deleted, `false` otherwise.
29
+ */
30
+ readonly delete: (key: K) => boolean;
31
+ }
32
+ /** Creates an {@link Instances}. */
33
+ export declare const createInstances: <K extends string, T extends Disposable>() => Instances<K, T>;
34
+ //# sourceMappingURL=Instances.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Instances.d.ts","sourceRoot":"","sources":["../../src/Instances.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,SAAS,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,UAAU,CAC/D,SAAQ,UAAU;IAClB;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,CACf,GAAG,EAAE,CAAC,EACN,MAAM,EAAE,MAAM,CAAC,EACf,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,KAC/B,CAAC,CAAC;IAEP,sEAAsE;IACtE,QAAQ,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IAEnC,sDAAsD;IACtD,QAAQ,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,OAAO,CAAC;IAElC;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,OAAO,CAAC;CACtC;AAED,oCAAoC;AACpC,eAAO,MAAM,eAAe,GAC1B,CAAC,SAAS,MAAM,EAChB,CAAC,SAAS,UAAU,OACjB,SAAS,CAAC,CAAC,EAAE,CAAC,CA6ClB,CAAC"}
@@ -0,0 +1,44 @@
1
+ /** Creates an {@link Instances}. */
2
+ export const createInstances = () => {
3
+ const instances = new Map();
4
+ return {
5
+ ensure: (key, create, onCacheHit) => {
6
+ let instance = instances.get(key);
7
+ if (instance == null) {
8
+ instance = create();
9
+ instances.set(key, instance);
10
+ }
11
+ else if (onCacheHit) {
12
+ onCacheHit(instance);
13
+ }
14
+ return instance;
15
+ },
16
+ get: (key) => instances.get(key) ?? null,
17
+ has: (key) => instances.has(key),
18
+ delete: (key) => {
19
+ const instance = instances.get(key);
20
+ if (instance == null)
21
+ return false;
22
+ instances.delete(key);
23
+ instance[Symbol.dispose]();
24
+ return true;
25
+ },
26
+ [Symbol.dispose]: () => {
27
+ const errors = [];
28
+ for (const instance of instances.values()) {
29
+ try {
30
+ instance[Symbol.dispose]();
31
+ }
32
+ catch (error) {
33
+ errors.push(error);
34
+ }
35
+ }
36
+ instances.clear();
37
+ if (errors.length === 1)
38
+ throw errors[0];
39
+ if (errors.length > 1) {
40
+ throw new AggregateError(errors, "Multiple disposal errors occurred");
41
+ }
42
+ },
43
+ };
44
+ };