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

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 +69 -5
  2. package/dist/src/Array.d.ts.map +1 -1
  3. package/dist/src/Array.js +64 -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 +161 -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 +273 -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 +238 -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 +240 -26
  61. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  62. package/dist/src/Evolu/Storage.js +189 -91
  63. package/dist/src/Evolu/Sync.d.ts +67 -13
  64. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  65. package/dist/src/Evolu/Sync.js +441 -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 +622 -340
  103. package/dist/src/Type.d.ts.map +1 -1
  104. package/dist/src/Type.js +666 -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 +90 -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 +517 -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 +355 -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 +437 -93
  137. package/src/Evolu/Schema.ts +391 -191
  138. package/src/Evolu/Storage.ts +532 -135
  139. package/src/Evolu/Sync.ts +766 -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 +1084 -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,18 +1,22 @@
1
1
  import { assert } from "../Assert.js";
2
- import { NanoIdLibDep } from "../NanoId.js";
2
+ import { Brand } from "../Brand.js";
3
+ import { bytesToHex } from "../Buffer.js";
4
+ import { RandomBytesDep } from "../Crypto.js";
5
+ import { createEqObject, eqNumber, eqString } from "../Eq.js";
3
6
  import { increment } from "../Number.js";
4
7
  import { Order, orderUint8Array } from "../Order.js";
5
- import { err, getOrThrow, ok, Result } from "../Result.js";
8
+ import { err, ok, Result } from "../Result.js";
6
9
  import { TimeDep } from "../Time.js";
7
10
  import {
8
11
  brand,
12
+ InferType,
9
13
  lessThanOrEqualTo,
10
14
  NonNegativeInt,
11
15
  object,
12
16
  regex,
13
17
  String,
18
+ Uint8Array,
14
19
  } from "../Type.js";
15
- import { Brand } from "../Types.js";
16
20
 
17
21
  export interface TimestampConfig {
18
22
  /**
@@ -30,7 +34,6 @@ export interface TimestampConfigDep {
30
34
  export type TimestampError =
31
35
  | TimestampDriftError
32
36
  | TimestampCounterOverflowError
33
- | TimestampDuplicateNodeError
34
37
  | TimestampTimeOutOfRangeError;
35
38
 
36
39
  export interface TimestampDriftError {
@@ -43,11 +46,6 @@ export interface TimestampCounterOverflowError {
43
46
  readonly type: "TimestampCounterOverflowError";
44
47
  }
45
48
 
46
- export interface TimestampDuplicateNodeError {
47
- readonly type: "TimestampDuplicateNodeError";
48
- readonly nodeId: NodeId;
49
- }
50
-
51
49
  export interface TimestampTimeOutOfRangeError {
52
50
  readonly type: "TimestampTimeOutOfRangeError";
53
51
  }
@@ -95,14 +93,17 @@ export const maxCounter = 65535 as Counter;
95
93
  *
96
94
  * https://lemire.me/blog/2019/12/12/are-64-bit-random-identifiers-free-from-collision
97
95
  *
98
- * What will happen if a different device generates the same NodeId?
96
+ * What happens if different devices generate the same NodeId?
99
97
  *
100
- * If the device belongs to a different owner, nothing will happen because
101
- * different owner have different owner IDs. Timestamps are partitioned by
102
- * OwnerId.
98
+ * If devices with the same NodeId use different owners, no issues occur.
103
99
  *
104
- * If the device belongs to the same owner, the other device will return
105
- * {@link TimestampDuplicateNodeError}.
100
+ * If devices with the same NodeId use the same owner, problems only arise when
101
+ * they generate CRDT messages with identical timestamps (same millis, counter,
102
+ * and NodeId). In this case, the protocol sync algorithm treats them as the
103
+ * same message: the first will be synced with the relay, while the affected
104
+ * message will not be delivered. The affected devices will see different data
105
+ * yet they will think they are synced. This is extremely rare and can be
106
+ * resolved by resetting one device to generate a new NodeId.
106
107
  */
107
108
  export const NodeId = regex("NodeId", /^[a-f0-9]{16}$/)(String);
108
109
  export type NodeId = typeof NodeId.Type;
@@ -113,16 +114,73 @@ export const maxNodeId = "ffffffffffffffff" as NodeId;
113
114
  /**
114
115
  * Hybrid Logical Clock timestamp.
115
116
  *
117
+ * Timestamps serve as globally unique, causally ordered identifiers for CRDT
118
+ * messages in Evolu's sync protocol.
119
+ *
120
+ * ### Why Hybrid Logical Clocks
121
+ *
122
+ * Evolu uses Hybrid Logical Clocks (HLC), which combine physical time (millis)
123
+ * with a logical counter. This hybrid approach preserves causality like logical
124
+ * clocks while staying close to physical time for better human
125
+ * interpretability.
126
+ *
127
+ * The counter component ensures causality is maintained even when physical
128
+ * clocks are imperfect. When clocks drift or operations occur concurrently, the
129
+ * counter increments to establish a total order. This means Evolu achieves
130
+ * well-defined, eventually-consistent behavior regardless of physical clock
131
+ * accuracy.
132
+ *
133
+ * Vector clocks can accurately track causality and detect concurrent
134
+ * operations, but they require unbounded space in peer-to-peer systems and
135
+ * crucially, still don't solve our fundamental problem: when they detect
136
+ * operations as concurrent, we still need a deterministic way to choose a
137
+ * winner. Additionally, any deterministic conflict resolution can be gamed by
138
+ * malicious actors.
139
+ *
140
+ * HLC timestamps work well in practice because modern device clocks accurately
141
+ * reflect the order of sequential edits in the common case. Evolu's `maxDrift`
142
+ * configuration protects against buggy clocks and prevents problematic
143
+ * future-dated entries from propagating through the network.
144
+ *
145
+ * ### References
146
+ *
116
147
  * - https://muratbuffalo.blogspot.com/2014/07/hybrid-logical-clocks.html
117
148
  * - https://sergeiturukin.com/2017/06/26/hybrid-logical-clocks.html
118
149
  * - https://jaredforsyth.com/posts/hybrid-logical-clocks/
150
+ * - https://willowprotocol.org/more/timestamps_really/index.html
151
+ *
152
+ * ### Privacy Considerations
153
+ *
154
+ * Timestamps are metadata visible to relays and collaborators. While it can be
155
+ * considered a privacy leak, let us explain why it's necessary, and how to
156
+ * avoid it if maximum privacy is required.
157
+ *
158
+ * With real-time communication, participants always see activity (receiving
159
+ * bytes). We cannot trust anyone not to store that information, so explicitly
160
+ * exposing timestamps doesn't add additional risk.
161
+ *
162
+ * If we really want not to leak user activity, we can implement a local write
163
+ * queue:
164
+ *
165
+ * 1. Write changes immediately to a local-only table
166
+ * 2. Periodically/randomly flush messages to sync tables
167
+ * 3. This decouples user activity from sync timing
168
+ *
169
+ * Tradeoff: It breaks real-time collaboration.
119
170
  */
120
171
  export const Timestamp = object({
121
172
  millis: Millis,
122
173
  counter: Counter,
123
174
  nodeId: NodeId,
124
175
  });
125
- export type Timestamp = typeof Timestamp.Type;
176
+ export interface Timestamp extends InferType<typeof Timestamp> {}
177
+
178
+ /** Equality function for comparing {@link Timestamp}. */
179
+ export const eqTimestamp = createEqObject<Timestamp>({
180
+ millis: eqNumber,
181
+ counter: eqNumber,
182
+ nodeId: eqString,
183
+ });
126
184
 
127
185
  export const createTimestamp = ({
128
186
  millis = minMillis,
@@ -130,14 +188,12 @@ export const createTimestamp = ({
130
188
  nodeId = minNodeId,
131
189
  }: Partial<Timestamp> = {}): Timestamp => ({ millis, counter, nodeId });
132
190
 
133
- const hexAlphabet = "0123456789abcdef";
134
-
135
- export const createInitialTimestamp = (deps: NanoIdLibDep): Timestamp => {
136
- const nodeId = deps.nanoIdLib.customAlphabet(hexAlphabet, 16)() as NodeId;
191
+ export const createInitialTimestamp = (deps: RandomBytesDep): Timestamp => {
192
+ const nodeId = bytesToHex(deps.randomBytes.create(8)) as NodeId;
137
193
  return createTimestamp({ nodeId });
138
194
  };
139
195
 
140
- /** TimestampString is a sortable string version of {@link Timestamp}. */
196
+ /** Sortable string representation of {@link Timestamp}. */
141
197
  export type TimestampString = string & Brand<"TimestampString">;
142
198
 
143
199
  export const timestampToTimestampString = (t: Timestamp): TimestampString =>
@@ -222,12 +278,8 @@ export const receiveTimestamp =
222
278
  Timestamp,
223
279
  | TimestampDriftError
224
280
  | TimestampCounterOverflowError
225
- | TimestampDuplicateNodeError
226
281
  | TimestampTimeOutOfRangeError
227
282
  > => {
228
- if (local.nodeId === remote.nodeId) {
229
- return err({ type: "TimestampDuplicateNodeError", nodeId: local.nodeId });
230
- }
231
283
  const millis = getNextMillis(deps)([local.millis, remote.millis]);
232
284
  if (!millis.ok) return millis;
233
285
 
@@ -249,18 +301,19 @@ export const receiveTimestamp =
249
301
  });
250
302
  };
251
303
 
252
- /** BinaryTimestamp is a binary and sortable version of {@link Timestamp} for DB. */
253
- export type BinaryTimestamp = Uint8Array & Brand<"BinaryTimestamp">;
304
+ /** Sortable bytes representation of {@link Timestamp}. */
305
+ export const TimestampBytes = brand("TimestampBytes", Uint8Array);
306
+ export type TimestampBytes = typeof TimestampBytes.Type;
254
307
 
255
- export const binaryTimestampLength = getOrThrow(NonNegativeInt.from(16));
308
+ export const timestampBytesLength = NonNegativeInt.orThrow(16);
256
309
 
257
- export const timestampToBinaryTimestamp = (
310
+ export const timestampToTimestampBytes = (
258
311
  timestamp: Timestamp,
259
- ): BinaryTimestamp => {
312
+ ): TimestampBytes => {
260
313
  const { millis, counter, nodeId } = timestamp;
261
314
 
262
315
  // 6 bytes for millis, 2 bytes for counter, 8 bytes for nodeId.
263
- const value = new Uint8Array(16);
316
+ const value = new globalThis.Uint8Array(16);
264
317
 
265
318
  // Encode `millis` into the first 6 bytes.
266
319
  const millisBigInt = BigInt(millis);
@@ -281,11 +334,11 @@ export const timestampToBinaryTimestamp = (
281
334
  value[8 + i] = byte;
282
335
  }
283
336
 
284
- return value as BinaryTimestamp;
337
+ return value as TimestampBytes;
285
338
  };
286
339
 
287
- export const binaryTimestampToTimestamp = (
288
- timestamp: BinaryTimestamp,
340
+ export const timestampBytesToTimestamp = (
341
+ timestamp: TimestampBytes,
289
342
  ): Timestamp => {
290
343
  // Decode `millis` from the first 6 bytes.
291
344
  const millis =
@@ -308,4 +361,4 @@ export const binaryTimestampToTimestamp = (
308
361
  return { millis: Number(millis), counter, nodeId } as Timestamp;
309
362
  };
310
363
 
311
- export const orderBinaryTimestamp: Order<BinaryTimestamp> = orderUint8Array;
364
+ export const orderTimestampBytes: Order<TimestampBytes> = orderUint8Array;
@@ -0,0 +1,197 @@
1
+ import type { Brand } from "./Brand.js";
2
+ import { Id, idToIdBytes } from "./Type.js";
3
+ import { md5 } from "@noble/hashes/legacy.js";
4
+
5
+ /**
6
+ * SVG string representing a visual identicon for an {@link Id}, created with
7
+ * {@link createIdenticon}.
8
+ */
9
+ export type Identicon = string & Brand<"Identicon">;
10
+
11
+ /** {@link Identicon} style. */
12
+ export type IdenticonStyle = "github" | "quadrant" | "gradient" | "sutnar";
13
+
14
+ /**
15
+ * Creates a deterministic identicon SVG from an {@link Id}.
16
+ *
17
+ * Works with any {@link Id} including branded IDs like `OwnerId`, etc.
18
+ *
19
+ * Available styles:
20
+ *
21
+ * - `"github"` (default): 5x5 grid with horizontal mirroring (GitHub-style)
22
+ * - `"quadrant"`: 2x2 grid with direct RGB color mapping from bytes
23
+ * - `"gradient"`: Diagonal stripes with smooth color gradients
24
+ * - `"sutnar"`: Three compositional variants with adaptive colors
25
+ *
26
+ * ### Example
27
+ *
28
+ * ```ts
29
+ * const svg = createIdenticon(id);
30
+ * const quadrantStyle = createIdenticon(id, "quadrant");
31
+ * const gradientStyle = createIdenticon(id, "gradient");
32
+ * const sutnarStyle = createIdenticon(id, "sutnar");
33
+ *
34
+ * // Works with branded IDs
35
+ * const ownerSvg = createIdenticon(ownerId);
36
+ * ```
37
+ */
38
+ export const createIdenticon = (
39
+ id: Id,
40
+ style: IdenticonStyle = "github",
41
+ ): Identicon => {
42
+ const bytes = idToIdBytes(id);
43
+
44
+ switch (style) {
45
+ case "github": {
46
+ // GitHub-style identicon: MD5 hash the bytes first
47
+ const hashedBytes = md5(bytes);
48
+
49
+ // Map function for value ranges
50
+ const map = (
51
+ value: number,
52
+ inMin: number,
53
+ inMax: number,
54
+ outMin: number,
55
+ outMax: number,
56
+ ): number =>
57
+ ((value - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
58
+
59
+ // Extract 12-bit hue from bytes[12] (lower 4 bits) + bytes[13]
60
+ const h = ((hashedBytes[12] & 0x0f) << 8) | hashedBytes[13];
61
+ const hue = map(h, 0, 4095, 0, 360);
62
+ const saturation = 65 - map(hashedBytes[14], 0, 255, 0, 20);
63
+ const lightness = 75 - map(hashedBytes[15], 0, 255, 0, 20);
64
+
65
+ const fgColor = `hsl(${hue},${saturation}%,${lightness}%)`;
66
+ const bgColor = `hsl(${hue},${saturation}%,90%)`;
67
+
68
+ let rects = `<rect width="5" height="5" fill="${bgColor}"/>`;
69
+
70
+ // Extract nibbles and generate pattern
71
+ let nibbleIndex = 0;
72
+ for (let x = 2; x >= 0; x--) {
73
+ for (let y = 0; y < 5; y++) {
74
+ const byte = hashedBytes[Math.floor(nibbleIndex / 2)];
75
+ const nibble = nibbleIndex % 2 === 0 ? byte >> 4 : byte & 0x0f;
76
+ const paint = nibble % 2 === 0;
77
+ nibbleIndex++;
78
+
79
+ if (paint) {
80
+ rects += `<rect x="${x}" y="${y}" width="1" height="1" fill="${fgColor}"/>`;
81
+ const mx = 4 - x;
82
+ if (mx !== x) {
83
+ rects += `<rect x="${mx}" y="${y}" width="1" height="1" fill="${fgColor}"/>`;
84
+ }
85
+ }
86
+ }
87
+ }
88
+
89
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 5 5" shape-rendering="crispEdges">${rects}</svg>` as Identicon;
90
+ }
91
+
92
+ case "quadrant": {
93
+ const toHex = (b: number): string => b.toString(16).padStart(2, "0");
94
+ let rects = "";
95
+ for (let i = 0; i < 4; i++) {
96
+ const x = i % 2;
97
+ const y = Math.floor(i / 2);
98
+ const r = bytes[i * 3];
99
+ const g = bytes[i * 3 + 1];
100
+ const b = bytes[i * 3 + 2];
101
+ const color = `#${toHex(r)}${toHex(g)}${toHex(b)}`;
102
+ rects += `<rect x="${x}" y="${y}" width="1" height="1" fill="${color}"/>`;
103
+ }
104
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2 2">${rects}</svg>` as Identicon;
105
+ }
106
+
107
+ case "gradient": {
108
+ // Smooth color gradients with diagonal stripes.
109
+ const toHex = (b: number): string => b.toString(16).padStart(2, "0");
110
+
111
+ // Generate colors from bytes.
112
+ const color1 = `#${toHex(bytes[0])}${toHex(bytes[1])}${toHex(bytes[2])}`;
113
+ const color2 = `#${toHex(bytes[3])}${toHex(bytes[4])}${toHex(bytes[5])}`;
114
+ const color3 = `#${toHex(bytes[6])}${toHex(bytes[7])}${toHex(bytes[8])}`;
115
+
116
+ let defs = "";
117
+ let shapes = "";
118
+
119
+ // Diagonal stripes with gradient.
120
+ defs += `<linearGradient id="grad1-${id}" x1="0%" y1="0%" x2="0%" y2="100%">`;
121
+ defs += `<stop offset="0%" style="stop-color:${color1};stop-opacity:1" />`;
122
+ defs += `<stop offset="100%" style="stop-color:${color2};stop-opacity:1" />`;
123
+ defs += `</linearGradient>`;
124
+
125
+ defs += `<linearGradient id="grad2-${id}" x1="0%" y1="0%" x2="0%" y2="100%">`;
126
+ defs += `<stop offset="0%" style="stop-color:${color2};stop-opacity:1" />`;
127
+ defs += `<stop offset="100%" style="stop-color:${color3};stop-opacity:1" />`;
128
+ defs += `</linearGradient>`;
129
+
130
+ shapes += `<rect width="100" height="100" fill="url(#grad1-${id})"/>`;
131
+
132
+ const stripeWidth = 15 + (bytes[9] / 255) * 20;
133
+ const angle = 30 + (bytes[10] / 255) * 60;
134
+
135
+ shapes += `<rect x="20" y="-50" width="${stripeWidth}" height="200" fill="url(#grad2-${id})" transform="rotate(${angle} 50 50)" opacity="0.7"/>`;
136
+ shapes += `<rect x="60" y="-50" width="${stripeWidth}" height="200" fill="url(#grad2-${id})" transform="rotate(${angle} 50 50)" opacity="0.5"/>`;
137
+
138
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs>${defs}</defs>${shapes}</svg>` as Identicon;
139
+ }
140
+
141
+ case "sutnar": {
142
+ // Three compositional variants with adaptive colors.
143
+ const hue = (bytes[0] / 255) * 360;
144
+ const saturation = 50 + (bytes[1] / 255) * 30;
145
+ const lightness = 50 + (bytes[2] / 255) * 20;
146
+
147
+ // Generate palette from base hue with variations
148
+ const toHsl = (h: number, s: number, l: number) =>
149
+ `hsl(${h},${s}%,${l}%)`;
150
+
151
+ const color1 = toHsl(hue, saturation, lightness);
152
+ const color2 = toHsl((hue + 120) % 360, saturation, lightness);
153
+ const color3 = toHsl((hue + 240) % 360, saturation, lightness);
154
+ const color4 = toHsl(hue, saturation * 0.3, lightness * 0.5);
155
+ const color5 = toHsl(
156
+ hue,
157
+ saturation * 0.5,
158
+ Math.min(lightness * 1.3, 90),
159
+ );
160
+
161
+ const palette = [color1, color2, color3, color4, color5] as const;
162
+
163
+ // Layout variant based on first byte.
164
+ const variant = bytes[3] % 3;
165
+
166
+ let shapes = "";
167
+
168
+ // Almost white background with subtle tint.
169
+ shapes += `<rect width="100" height="100" fill="${toHsl(hue, 10, 95)}"/>`;
170
+
171
+ if (variant === 0) {
172
+ // Composition A: Circle + horizontal bar.
173
+ const circleColor = palette[bytes[4] % palette.length];
174
+ const barColor = palette[(bytes[4] + 1) % palette.length];
175
+
176
+ shapes += `<circle cx="30" cy="50" r="22" fill="${circleColor}"/>`;
177
+ shapes += `<rect x="60" y="40" width="35" height="20" fill="${barColor}"/>`;
178
+ } else if (variant === 1) {
179
+ // Composition B: Vertical bar + circle.
180
+ const barColor = palette[bytes[5] % palette.length];
181
+ const circleColor = palette[(bytes[5] + 1) % palette.length];
182
+
183
+ shapes += `<rect x="15" y="10" width="18" height="80" fill="${barColor}"/>`;
184
+ shapes += `<circle cx="70" cy="50" r="15" fill="${circleColor}"/>`;
185
+ } else {
186
+ // Composition C: Square + circle.
187
+ const squareColor = palette[bytes[6] % palette.length];
188
+ const circleColor = palette[(bytes[6] + 1) % palette.length];
189
+
190
+ shapes += `<rect x="20" y="20" width="30" height="30" fill="${squareColor}"/>`;
191
+ shapes += `<circle cx="70" cy="70" r="18" fill="${circleColor}"/>`;
192
+ }
193
+
194
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">${shapes}</svg>` as Identicon;
195
+ }
196
+ }
197
+ };
@@ -0,0 +1,90 @@
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>
16
+ extends Disposable {
17
+ /**
18
+ * Ensures an instance exists for the given key, creating it if necessary. If
19
+ * the instance already exists, the optional `onCacheHit` callback is invoked
20
+ * to update the existing instance.
21
+ */
22
+ readonly ensure: (
23
+ key: K,
24
+ create: () => T,
25
+ onCacheHit?: (instance: T) => void,
26
+ ) => T;
27
+
28
+ /** Gets an instance by key, or returns `null` if it doesn't exist. */
29
+ readonly get: (key: K) => T | null;
30
+
31
+ /** Checks if an instance exists for the given key. */
32
+ readonly has: (key: K) => boolean;
33
+
34
+ /**
35
+ * Deletes and disposes an instance by key. Returns `true` if the instance
36
+ * existed and was deleted, `false` otherwise.
37
+ */
38
+ readonly delete: (key: K) => boolean;
39
+ }
40
+
41
+ /** Creates an {@link Instances}. */
42
+ export const createInstances = <
43
+ K extends string,
44
+ T extends Disposable,
45
+ >(): Instances<K, T> => {
46
+ const instances = new Map<K, T>();
47
+
48
+ return {
49
+ ensure: (key, create, onCacheHit) => {
50
+ let instance = instances.get(key);
51
+
52
+ if (instance == null) {
53
+ instance = create();
54
+ instances.set(key, instance);
55
+ } else if (onCacheHit) {
56
+ onCacheHit(instance);
57
+ }
58
+
59
+ return instance;
60
+ },
61
+
62
+ get: (key) => instances.get(key) ?? null,
63
+
64
+ has: (key) => instances.has(key),
65
+
66
+ delete: (key) => {
67
+ const instance = instances.get(key);
68
+ if (instance == null) return false;
69
+ instances.delete(key);
70
+ instance[Symbol.dispose]();
71
+ return true;
72
+ },
73
+
74
+ [Symbol.dispose]: () => {
75
+ const errors: Array<unknown> = [];
76
+ for (const instance of instances.values()) {
77
+ try {
78
+ instance[Symbol.dispose]();
79
+ } catch (error) {
80
+ errors.push(error);
81
+ }
82
+ }
83
+ instances.clear();
84
+ if (errors.length === 1) throw errors[0];
85
+ if (errors.length > 1) {
86
+ throw new AggregateError(errors, "Multiple disposal errors occurred");
87
+ }
88
+ },
89
+ };
90
+ };