@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.
- package/dist/src/Array.d.ts +58 -5
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +53 -5
- package/dist/src/Assert.d.ts +6 -16
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +6 -18
- package/dist/src/Brand.d.ts +75 -0
- package/dist/src/Brand.d.ts.map +1 -0
- package/dist/src/Brand.js +1 -0
- package/dist/src/Buffer.d.ts +1 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +8 -7
- package/dist/src/Cache.d.ts +44 -0
- package/dist/src/Cache.d.ts.map +1 -0
- package/dist/src/Cache.js +52 -0
- package/dist/src/Callbacks.d.ts +45 -12
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Callbacks.js +14 -7
- package/dist/src/Console.d.ts +31 -6
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +72 -9
- package/dist/src/Crypto.d.ts +61 -34
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +32 -45
- package/dist/src/Evolu/Db.d.ts +158 -65
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +286 -694
- package/dist/src/Evolu/Diff.d.ts +3 -3
- package/dist/src/Evolu/Diff.d.ts.map +1 -1
- package/dist/src/Evolu/Diff.js +7 -5
- package/dist/src/Evolu/Evolu.d.ts +208 -133
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +188 -183
- package/dist/src/Evolu/Internal.d.ts +0 -2
- package/dist/src/Evolu/Internal.d.ts.map +1 -1
- package/dist/src/Evolu/Internal.js +0 -2
- package/dist/src/Evolu/LocalAuth.d.ts +150 -0
- package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
- package/dist/src/Evolu/LocalAuth.js +174 -0
- package/dist/src/Evolu/Owner.d.ts +264 -120
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +130 -104
- package/dist/src/Evolu/Platform.d.ts +9 -7
- package/dist/src/Evolu/Platform.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.d.ts +277 -232
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +603 -378
- package/dist/src/Evolu/Public.d.ts +6 -8
- package/dist/src/Evolu/Public.d.ts.map +1 -1
- package/dist/src/Evolu/Public.js +2 -3
- package/dist/src/Evolu/PublicKysely.js +3 -3
- package/dist/src/Evolu/Query.d.ts +2 -1
- package/dist/src/Evolu/Query.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.d.ts +92 -7
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +243 -76
- package/dist/src/Evolu/Schema.d.ts +129 -73
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +169 -89
- package/dist/src/Evolu/Storage.d.ts +212 -26
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +137 -79
- package/dist/src/Evolu/Sync.d.ts +68 -13
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +422 -20
- package/dist/src/Evolu/Timestamp.d.ts +85 -27
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +77 -18
- package/dist/src/Identicon.d.ts +35 -0
- package/dist/src/Identicon.d.ts.map +1 -0
- package/dist/src/Identicon.js +143 -0
- package/dist/src/Instances.d.ts +34 -0
- package/dist/src/Instances.d.ts.map +1 -0
- package/dist/src/Instances.js +44 -0
- package/dist/src/ManyToManyMap.d.ts +71 -10
- package/dist/src/ManyToManyMap.d.ts.map +1 -1
- package/dist/src/ManyToManyMap.js +41 -6
- package/dist/src/Number.d.ts +4 -3
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +5 -4
- package/dist/src/Platform.d.ts +20 -0
- package/dist/src/Platform.d.ts.map +1 -0
- package/dist/src/Platform.js +22 -0
- package/dist/src/Random.d.ts +3 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Resources.d.ts +118 -0
- package/dist/src/Resources.d.ts.map +1 -0
- package/dist/src/Resources.js +197 -0
- package/dist/src/Result.d.ts +184 -52
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +30 -241
- package/dist/src/Skiplist.js +2 -1
- package/dist/src/Sqlite.d.ts +63 -5
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +110 -9
- package/dist/src/Task.d.ts +586 -0
- package/dist/src/Task.d.ts.map +1 -0
- package/dist/src/Task.js +469 -0
- package/dist/src/Time.d.ts +66 -1
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +99 -5
- package/dist/src/Type.d.ts +621 -340
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +665 -464
- package/dist/src/Types.d.ts +1 -75
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/WebSocket.d.ts +5 -2
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +12 -18
- package/dist/src/Worker.d.ts +39 -11
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +22 -4
- package/dist/src/index.d.ts +7 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -2
- package/package.json +14 -13
- package/src/Array.ts +76 -11
- package/src/Assert.ts +6 -24
- package/src/Brand.ts +75 -0
- package/src/Buffer.ts +7 -7
- package/src/Cache.ts +85 -0
- package/src/Callbacks.ts +62 -22
- package/src/Console.ts +91 -11
- package/src/Crypto.ts +97 -82
- package/src/Evolu/Db.ts +514 -1020
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +464 -355
- package/src/Evolu/Internal.ts +0 -2
- package/src/Evolu/LocalAuth.ts +463 -0
- package/src/Evolu/Owner.ts +369 -228
- package/src/Evolu/Platform.ts +9 -9
- package/src/Evolu/Protocol.ts +859 -676
- package/src/Evolu/Public.ts +7 -14
- package/src/Evolu/PublicKysely.ts +3 -3
- package/src/Evolu/Query.ts +2 -1
- package/src/Evolu/Relay.ts +420 -92
- package/src/Evolu/Schema.ts +391 -191
- package/src/Evolu/Storage.ts +451 -118
- package/src/Evolu/Sync.ts +720 -37
- package/src/Evolu/Timestamp.ts +88 -35
- package/src/Identicon.ts +197 -0
- package/src/Instances.ts +90 -0
- package/src/ManyToManyMap.ts +124 -24
- package/src/Number.ts +6 -10
- package/src/Platform.ts +26 -0
- package/src/Random.ts +3 -2
- package/src/Resources.ts +367 -0
- package/src/Result.ts +191 -54
- package/src/Skiplist.ts +1 -1
- package/src/Sqlite.ts +122 -17
- package/src/Task.ts +901 -0
- package/src/Time.ts +180 -5
- package/src/Type.ts +1083 -727
- package/src/Types.ts +1 -77
- package/src/WebSocket.ts +27 -25
- package/src/Worker.ts +72 -23
- package/src/index.ts +7 -2
- package/dist/src/Evolu/Config.d.ts +0 -69
- package/dist/src/Evolu/Config.d.ts.map +0 -1
- package/dist/src/Evolu/Config.js +0 -9
- package/dist/src/Evolu/Kysely.d.ts +0 -6
- package/dist/src/Evolu/Kysely.d.ts.map +0 -1
- package/dist/src/Evolu/Kysely.js +0 -21
- package/dist/src/NanoId.d.ts +0 -27
- package/dist/src/NanoId.d.ts.map +0 -1
- package/dist/src/NanoId.js +0 -6
- package/dist/src/Promise.d.ts +0 -180
- package/dist/src/Promise.d.ts.map +0 -1
- package/dist/src/Promise.js +0 -176
- package/src/Evolu/Config.ts +0 -83
- package/src/Evolu/Kysely.ts +0 -38
- package/src/NanoId.ts +0 -39
- package/src/Promise.ts +0 -295
package/src/Evolu/Timestamp.ts
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { assert } from "../Assert.js";
|
|
2
|
-
import {
|
|
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,
|
|
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
|
|
96
|
+
* What happens if different devices generate the same NodeId?
|
|
99
97
|
*
|
|
100
|
-
* If the
|
|
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
|
|
105
|
-
*
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
253
|
-
export
|
|
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
|
|
308
|
+
export const timestampBytesLength = NonNegativeInt.orThrow(16);
|
|
256
309
|
|
|
257
|
-
export const
|
|
310
|
+
export const timestampToTimestampBytes = (
|
|
258
311
|
timestamp: Timestamp,
|
|
259
|
-
):
|
|
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
|
|
337
|
+
return value as TimestampBytes;
|
|
285
338
|
};
|
|
286
339
|
|
|
287
|
-
export const
|
|
288
|
-
timestamp:
|
|
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
|
|
364
|
+
export const orderTimestampBytes: Order<TimestampBytes> = orderUint8Array;
|
package/src/Identicon.ts
ADDED
|
@@ -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
|
+
};
|
package/src/Instances.ts
ADDED
|
@@ -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
|
+
};
|