@evolu/common 6.0.1-preview.24 → 6.0.1-preview.26
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/Assert.d.ts +0 -13
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +0 -15
- 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/Evolu/Db.d.ts +6 -6
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +4 -0
- package/dist/src/Evolu/LocalAuth.d.ts +2 -2
- package/dist/src/Evolu/LocalAuth.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.d.ts +116 -86
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +48 -45
- package/dist/src/Evolu/Relay.d.ts +6 -5
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +40 -39
- package/dist/src/Evolu/Storage.d.ts +6 -11
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +30 -9
- package/dist/src/Evolu/Sync.d.ts +5 -5
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +3 -5
- package/dist/src/Evolu/Timestamp.d.ts +24 -0
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +24 -0
- 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/ManyToManyMap.d.ts +0 -3
- package/dist/src/ManyToManyMap.d.ts.map +1 -1
- package/dist/src/Result.d.ts +13 -6
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Sqlite.d.ts +36 -1
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +56 -3
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +36 -0
- package/dist/src/Type.d.ts +73 -9
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +124 -22
- package/dist/src/Types.d.ts +1 -1
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +2 -7
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/package.json +1 -1
- package/src/Assert.ts +0 -19
- package/src/Cache.ts +85 -0
- package/src/Evolu/Db.ts +11 -7
- package/src/Evolu/LocalAuth.ts +19 -7
- package/src/Evolu/Owner.ts +140 -103
- package/src/Evolu/Relay.ts +52 -48
- package/src/Evolu/Storage.ts +47 -23
- package/src/Evolu/Sync.ts +11 -12
- package/src/Evolu/Timestamp.ts +24 -0
- package/src/Identicon.ts +197 -0
- package/src/ManyToManyMap.ts +0 -3
- package/src/Result.ts +13 -6
- package/src/Sqlite.ts +68 -5
- package/src/Task.ts +41 -0
- package/src/Type.ts +262 -27
- package/src/Types.ts +1 -1
- package/src/WebSocket.ts +6 -10
- package/src/index.ts +2 -0
package/src/Evolu/Storage.ts
CHANGED
|
@@ -240,7 +240,16 @@ export type DbChange = typeof DbChange.Type;
|
|
|
240
240
|
* users, and when it goes down, nothing happens, because it will be
|
|
241
241
|
* synchronized later.
|
|
242
242
|
*/
|
|
243
|
-
export interface
|
|
243
|
+
export interface BaseSqliteStorage
|
|
244
|
+
extends Pick<
|
|
245
|
+
Storage,
|
|
246
|
+
| "getSize"
|
|
247
|
+
| "fingerprint"
|
|
248
|
+
| "fingerprintRanges"
|
|
249
|
+
| "findLowerBound"
|
|
250
|
+
| "iterate"
|
|
251
|
+
| "deleteOwner"
|
|
252
|
+
> {
|
|
244
253
|
/**
|
|
245
254
|
* Inserts a timestamp for an owner into the skiplist-based storage.
|
|
246
255
|
*
|
|
@@ -252,33 +261,22 @@ export interface SqliteStorageBase {
|
|
|
252
261
|
ownerId: OwnerIdBytes,
|
|
253
262
|
timestamp: TimestampBytes,
|
|
254
263
|
) => Result<void, SqliteError>;
|
|
255
|
-
|
|
256
|
-
readonly getSize: Storage["getSize"];
|
|
257
|
-
readonly fingerprint: Storage["fingerprint"];
|
|
258
|
-
readonly fingerprintRanges: Storage["fingerprintRanges"];
|
|
259
|
-
readonly findLowerBound: Storage["findLowerBound"];
|
|
260
|
-
readonly iterate: Storage["iterate"];
|
|
261
|
-
readonly deleteOwner: Storage["deleteOwner"];
|
|
262
264
|
}
|
|
263
265
|
|
|
264
|
-
export interface
|
|
265
|
-
readonly storage:
|
|
266
|
+
export interface BaseSqliteStorageDep {
|
|
267
|
+
readonly storage: BaseSqliteStorage;
|
|
266
268
|
}
|
|
267
269
|
|
|
268
270
|
export type SqliteStorageDeps = RandomDep & SqliteDep;
|
|
269
271
|
|
|
270
|
-
export interface
|
|
272
|
+
export interface CreateBaseSqliteStorageOptions {
|
|
271
273
|
onStorageError: (error: SqliteError) => void;
|
|
272
274
|
}
|
|
273
275
|
|
|
274
|
-
export const
|
|
276
|
+
export const createBaseSqliteStorage =
|
|
275
277
|
(deps: SqliteStorageDeps) =>
|
|
276
|
-
(
|
|
277
|
-
|
|
278
|
-
): Result<SqliteStorageBase, SqliteError> => {
|
|
279
|
-
const createTablesResult = createTables(deps);
|
|
280
|
-
if (!createTablesResult.ok) return createTablesResult;
|
|
281
|
-
|
|
278
|
+
(options: CreateBaseSqliteStorageOptions): BaseSqliteStorage => {
|
|
279
|
+
// TODO: Use OwnerUsage table.
|
|
282
280
|
const ownerStats = new Map<
|
|
283
281
|
OwnerId,
|
|
284
282
|
{
|
|
@@ -287,7 +285,7 @@ export const createSqliteStorageBase =
|
|
|
287
285
|
}
|
|
288
286
|
>();
|
|
289
287
|
|
|
290
|
-
return
|
|
288
|
+
return {
|
|
291
289
|
insertTimestamp: (ownerId: OwnerIdBytes, timestamp: TimestampBytes) => {
|
|
292
290
|
const ownerIdString = ownerIdBytesToOwnerId(ownerId);
|
|
293
291
|
const level = randomSkiplistLevel(deps);
|
|
@@ -422,14 +420,16 @@ export const createSqliteStorageBase =
|
|
|
422
420
|
}
|
|
423
421
|
return true;
|
|
424
422
|
},
|
|
425
|
-
}
|
|
423
|
+
};
|
|
426
424
|
};
|
|
427
425
|
|
|
428
426
|
const assertBeginEnd = (begin: NonNegativeInt, end: NonNegativeInt) => {
|
|
429
427
|
assert(begin <= end, "invalid begin or end");
|
|
430
428
|
};
|
|
431
429
|
|
|
432
|
-
const
|
|
430
|
+
export const createBaseSqliteStorageTables = (
|
|
431
|
+
deps: SqliteDep,
|
|
432
|
+
): Result<void, SqliteError> => {
|
|
433
433
|
for (const query of [
|
|
434
434
|
/**
|
|
435
435
|
* Creates the `evolu_timestamp` table for storing timestamps of multiple
|
|
@@ -454,7 +454,7 @@ const createTables = (deps: SqliteDep): Result<void, SqliteError> => {
|
|
|
454
454
|
* enough even without it.
|
|
455
455
|
*/
|
|
456
456
|
sql`
|
|
457
|
-
create table
|
|
457
|
+
create table evolu_timestamp (
|
|
458
458
|
"ownerId" blob not null,
|
|
459
459
|
"t" blob not null,
|
|
460
460
|
"h1" integer,
|
|
@@ -467,7 +467,7 @@ const createTables = (deps: SqliteDep): Result<void, SqliteError> => {
|
|
|
467
467
|
`,
|
|
468
468
|
|
|
469
469
|
sql`
|
|
470
|
-
create index
|
|
470
|
+
create index evolu_timestamp_index on evolu_timestamp (
|
|
471
471
|
"ownerId",
|
|
472
472
|
"l",
|
|
473
473
|
"t",
|
|
@@ -476,6 +476,30 @@ const createTables = (deps: SqliteDep): Result<void, SqliteError> => {
|
|
|
476
476
|
"c"
|
|
477
477
|
);
|
|
478
478
|
`,
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Creates the `evolu_usage` table for tracking data consumption per owner.
|
|
482
|
+
*
|
|
483
|
+
* Columns:
|
|
484
|
+
*
|
|
485
|
+
* - `ownerId` – OwnerIdBytes (primary key)
|
|
486
|
+
* - `storedBytes` – total bytes stored in database
|
|
487
|
+
* - `receivedBytes` – total bytes received from clients
|
|
488
|
+
* - `sentBytes` – total bytes sent to clients
|
|
489
|
+
* - `firstTimestamp` – minimum timestamp (nullable)
|
|
490
|
+
* - `lastTimestamp` – maximum timestamp (nullable)
|
|
491
|
+
*/
|
|
492
|
+
sql`
|
|
493
|
+
create table evolu_usage (
|
|
494
|
+
"ownerId" blob primary key,
|
|
495
|
+
"storedBytes" integer not null,
|
|
496
|
+
"receivedBytes" integer not null,
|
|
497
|
+
"sentBytes" integer not null,
|
|
498
|
+
"firstTimestamp" blob,
|
|
499
|
+
"lastTimestamp" blob
|
|
500
|
+
)
|
|
501
|
+
strict;
|
|
502
|
+
`,
|
|
479
503
|
]) {
|
|
480
504
|
const result = deps.sqlite.exec(query);
|
|
481
505
|
if (!result.ok) return result;
|
package/src/Evolu/Sync.ts
CHANGED
|
@@ -27,11 +27,11 @@ import {
|
|
|
27
27
|
OwnerIdBytes,
|
|
28
28
|
ownerIdBytesToOwnerId,
|
|
29
29
|
ownerIdToOwnerIdBytes,
|
|
30
|
+
OwnerTransport,
|
|
30
31
|
OwnerWriteKey,
|
|
31
32
|
ShardOwner,
|
|
32
33
|
SharedOwner,
|
|
33
34
|
SharedReadonlyOwner,
|
|
34
|
-
TransportConfig,
|
|
35
35
|
} from "./Owner.js";
|
|
36
36
|
import {
|
|
37
37
|
applyProtocolMessageAsClient,
|
|
@@ -47,10 +47,10 @@ import {
|
|
|
47
47
|
} from "./Protocol.js";
|
|
48
48
|
import { MutationChange } from "./Schema.js";
|
|
49
49
|
import {
|
|
50
|
+
BaseSqliteStorage,
|
|
51
|
+
createBaseSqliteStorage,
|
|
50
52
|
CrdtMessage,
|
|
51
|
-
createSqliteStorageBase,
|
|
52
53
|
DbChange,
|
|
53
|
-
SqliteStorageBase,
|
|
54
54
|
Storage,
|
|
55
55
|
} from "./Storage.js";
|
|
56
56
|
import {
|
|
@@ -106,13 +106,13 @@ export interface SyncOwner {
|
|
|
106
106
|
readonly encryptionKey: OwnerEncryptionKey;
|
|
107
107
|
/** Optional for read-only owners like {@link SharedReadonlyOwner}. */
|
|
108
108
|
readonly writeKey?: OwnerWriteKey;
|
|
109
|
-
readonly transports?: ReadonlyArray<
|
|
109
|
+
readonly transports?: ReadonlyArray<OwnerTransport>;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
export interface SyncConfig {
|
|
113
113
|
readonly appOwner: AppOwner;
|
|
114
114
|
|
|
115
|
-
readonly transports: ReadonlyArray<
|
|
115
|
+
readonly transports: ReadonlyArray<OwnerTransport>;
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
118
|
* Delay in milliseconds before disposing unused WebSocket connections.
|
|
@@ -166,7 +166,7 @@ export const createSync =
|
|
|
166
166
|
if (!storageResult.ok) return storageResult;
|
|
167
167
|
const storage = storageResult.value;
|
|
168
168
|
|
|
169
|
-
const createResource = (transportConfig:
|
|
169
|
+
const createResource = (transportConfig: OwnerTransport): WebSocket => {
|
|
170
170
|
const transportKey = createTransportKey(transportConfig);
|
|
171
171
|
|
|
172
172
|
deps.console.log("[sync]", "createWebSocket", {
|
|
@@ -255,7 +255,7 @@ export const createSync =
|
|
|
255
255
|
const transports = createRefCountedResourceManager<
|
|
256
256
|
WebSocket,
|
|
257
257
|
TransportKey,
|
|
258
|
-
|
|
258
|
+
OwnerTransport,
|
|
259
259
|
SyncOwner,
|
|
260
260
|
OwnerId
|
|
261
261
|
>({
|
|
@@ -418,7 +418,7 @@ interface GetSyncOwnerDep {
|
|
|
418
418
|
readonly getSyncOwner: (ownerId: OwnerId) => SyncOwner | null;
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
-
export interface ClientStorage extends
|
|
421
|
+
export interface ClientStorage extends Storage, BaseSqliteStorage {}
|
|
422
422
|
|
|
423
423
|
export interface ClientStorageDep {
|
|
424
424
|
readonly storage: ClientStorage;
|
|
@@ -448,15 +448,14 @@ const createClientStorage =
|
|
|
448
448
|
) => void;
|
|
449
449
|
onReceive: () => void;
|
|
450
450
|
}): Result<ClientStorage, SqliteError> => {
|
|
451
|
-
const sqliteStorageBase =
|
|
451
|
+
const sqliteStorageBase = createBaseSqliteStorage(deps)({
|
|
452
452
|
onStorageError: config.onError,
|
|
453
453
|
});
|
|
454
|
-
if (!sqliteStorageBase.ok) return sqliteStorageBase;
|
|
455
454
|
|
|
456
455
|
const mutex = createMutex();
|
|
457
456
|
|
|
458
457
|
const storage: ClientStorage = {
|
|
459
|
-
...sqliteStorageBase
|
|
458
|
+
...sqliteStorageBase,
|
|
460
459
|
|
|
461
460
|
validateWriteKey: constFalse,
|
|
462
461
|
setWriteKey: constFalse,
|
|
@@ -590,7 +589,7 @@ const createClientStorage =
|
|
|
590
589
|
type TransportKey = string & Brand<"TransportKey">;
|
|
591
590
|
|
|
592
591
|
/** Creates a unique identifier for a transport configuration. */
|
|
593
|
-
const createTransportKey = (transportConfig:
|
|
592
|
+
const createTransportKey = (transportConfig: OwnerTransport): TransportKey => {
|
|
594
593
|
return `${transportConfig.type}:${transportConfig.url}` as TransportKey;
|
|
595
594
|
};
|
|
596
595
|
|
package/src/Evolu/Timestamp.ts
CHANGED
|
@@ -114,9 +114,33 @@ export const maxNodeId = "ffffffffffffffff" as NodeId;
|
|
|
114
114
|
/**
|
|
115
115
|
* Hybrid Logical Clock timestamp.
|
|
116
116
|
*
|
|
117
|
+
* Timestamps serve as globally unique, causally ordered identifiers for CRDT
|
|
118
|
+
* messages in Evolu's sync protocol.
|
|
119
|
+
*
|
|
120
|
+
* ### References
|
|
121
|
+
*
|
|
117
122
|
* - https://muratbuffalo.blogspot.com/2014/07/hybrid-logical-clocks.html
|
|
118
123
|
* - https://sergeiturukin.com/2017/06/26/hybrid-logical-clocks.html
|
|
119
124
|
* - https://jaredforsyth.com/posts/hybrid-logical-clocks/
|
|
125
|
+
*
|
|
126
|
+
* ### Privacy Considerations
|
|
127
|
+
*
|
|
128
|
+
* Timestamps are metadata visible to relays and collaborators. While it can be
|
|
129
|
+
* considered a privacy leak, let us explain why it's necessary, and how to
|
|
130
|
+
* avoid it if maximum privacy is required.
|
|
131
|
+
*
|
|
132
|
+
* With real-time communication, participants always see activity (receiving
|
|
133
|
+
* bytes). We cannot trust anyone not to store that information, so explicitly
|
|
134
|
+
* exposing timestamps doesn't add additional risk.
|
|
135
|
+
*
|
|
136
|
+
* If we really want not to leak user activity, we can implement a local write
|
|
137
|
+
* queue:
|
|
138
|
+
*
|
|
139
|
+
* 1. Write changes immediately to a local-only table
|
|
140
|
+
* 2. Periodically/randomly flush messages to sync tables
|
|
141
|
+
* 3. This decouples user activity from sync timing
|
|
142
|
+
*
|
|
143
|
+
* Tradeoff: It breaks real-time collaboration.
|
|
120
144
|
*/
|
|
121
145
|
export const Timestamp = object({
|
|
122
146
|
millis: Millis,
|
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/ManyToManyMap.ts
CHANGED
|
@@ -22,9 +22,6 @@ import { assert } from "./Assert.js";
|
|
|
22
22
|
* - `deleteKey` and `deleteValue` are O(d) where d is the number of associated
|
|
23
23
|
* values / keys (the degree). This is optimal because every associated pair
|
|
24
24
|
* must be touched once.
|
|
25
|
-
* - In the Relay use case a socket (value) typically has only dozens of owners
|
|
26
|
-
* (degree small), and connection closes (triggering deleteValue) are
|
|
27
|
-
* relatively infrequent, so O(d) is acceptable.
|
|
28
25
|
*
|
|
29
26
|
* Object identity:
|
|
30
27
|
*
|
package/src/Result.ts
CHANGED
|
@@ -87,8 +87,8 @@
|
|
|
87
87
|
* ### Naming Convention
|
|
88
88
|
*
|
|
89
89
|
* - For values: `const user = getUser()`
|
|
90
|
-
* - For void
|
|
91
|
-
* - For
|
|
90
|
+
* - For a single void operation: `const result = foo()`
|
|
91
|
+
* - For multiple void operations: use descriptive names for all
|
|
92
92
|
*
|
|
93
93
|
* ```ts
|
|
94
94
|
* const processUser = () => {
|
|
@@ -96,13 +96,20 @@
|
|
|
96
96
|
* const user = getUser();
|
|
97
97
|
* if (!user.ok) return user;
|
|
98
98
|
*
|
|
99
|
-
* // void operation
|
|
99
|
+
* // single void operation
|
|
100
100
|
* const result = saveToDatabase(user.value);
|
|
101
101
|
* if (!result.ok) return result;
|
|
102
102
|
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
103
|
+
* return ok();
|
|
104
|
+
* };
|
|
105
|
+
*
|
|
106
|
+
* const setupDatabase = () => {
|
|
107
|
+
* // multiple void operations - use descriptive names
|
|
108
|
+
* const baseTables = createBaseTables();
|
|
109
|
+
* if (!baseTables.ok) return baseTables;
|
|
110
|
+
*
|
|
111
|
+
* const relayTables = createRelayTables();
|
|
112
|
+
* if (!relayTables.ok) return relayTables;
|
|
106
113
|
*
|
|
107
114
|
* return ok();
|
|
108
115
|
* };
|
package/src/Sqlite.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { Brand } from "./Brand.js";
|
|
2
|
+
import { createLruCache } from "./Cache.js";
|
|
2
3
|
import { ConsoleDep } from "./Console.js";
|
|
3
4
|
import { EncryptionKey } from "./Crypto.js";
|
|
4
5
|
import { createTransferableError, TransferableError } from "./Error.js";
|
|
5
6
|
import { err, ok, Result, tryAsync, trySync } from "./Result.js";
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
Null,
|
|
9
|
+
Number,
|
|
10
|
+
PositiveInt,
|
|
11
|
+
SimpleName,
|
|
12
|
+
String,
|
|
13
|
+
Uint8Array,
|
|
14
|
+
union,
|
|
15
|
+
} from "./Type.js";
|
|
7
16
|
import { IntentionalNever, Predicate } from "./Types.js";
|
|
8
17
|
|
|
9
18
|
/**
|
|
@@ -294,7 +303,38 @@ export interface RawSql {
|
|
|
294
303
|
|
|
295
304
|
export type SqlTemplateParam = SqliteValue | SqlIdentifier | RawSql;
|
|
296
305
|
|
|
297
|
-
/**
|
|
306
|
+
/**
|
|
307
|
+
* Creates a safe SQL query using a tagged template literal.
|
|
308
|
+
*
|
|
309
|
+
* Parameters are automatically escaped and bound as SQLite values. Use
|
|
310
|
+
* `sql.identifier` for column/table names and `sql.raw` for unescaped SQL.
|
|
311
|
+
*
|
|
312
|
+
* ### Example
|
|
313
|
+
*
|
|
314
|
+
* ```ts
|
|
315
|
+
* const id = 42;
|
|
316
|
+
* const name = "Alice";
|
|
317
|
+
*
|
|
318
|
+
* const result = sqlite.exec(sql`
|
|
319
|
+
* select *
|
|
320
|
+
* from users
|
|
321
|
+
* where id = ${id} and name = ${name};
|
|
322
|
+
* `);
|
|
323
|
+
*
|
|
324
|
+
* // For identifiers
|
|
325
|
+
* const tableName = "users";
|
|
326
|
+
* sqlite.exec(sql`
|
|
327
|
+
* create table ${sql.identifier(tableName)} (
|
|
328
|
+
* "id" text primary key,
|
|
329
|
+
* "name" text not null
|
|
330
|
+
* );
|
|
331
|
+
* `);
|
|
332
|
+
*
|
|
333
|
+
* // For raw SQL (use with caution)
|
|
334
|
+
* const orderBy = "created_at desc";
|
|
335
|
+
* sqlite.exec(sql`select * from users order by ${sql.raw(orderBy)};`);
|
|
336
|
+
* ```
|
|
337
|
+
*/
|
|
298
338
|
export const sql = (
|
|
299
339
|
strings: TemplateStringsArray,
|
|
300
340
|
...parameters: Array<SqlTemplateParam>
|
|
@@ -342,6 +382,29 @@ sql.prepared = (
|
|
|
342
382
|
return { ...query, options: { prepare: true } };
|
|
343
383
|
};
|
|
344
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Checks if a SQL string contains mutation keywords (insert, update, delete,
|
|
387
|
+
* etc.). Results are cached for performance.
|
|
388
|
+
*/
|
|
389
|
+
export const isSqlMutation: Predicate<string> = (sql) => {
|
|
390
|
+
/**
|
|
391
|
+
* Without cache, "insert 1_000_000" Storage test dropped from 57742
|
|
392
|
+
* inserts/sec to 34k. Regex we used was fast, but CodeQL flagged it as a
|
|
393
|
+
* potential ReDoS vulnerability, so manual comment removal was the only
|
|
394
|
+
* option. LRU cache restores performance.
|
|
395
|
+
*/
|
|
396
|
+
const cached = isSqlMutationCache.get(sql);
|
|
397
|
+
if (cached !== undefined) return cached;
|
|
398
|
+
|
|
399
|
+
const result = isSqlMutationRegEx.test(removeSqlComments(sql));
|
|
400
|
+
isSqlMutationCache.set(sql, result);
|
|
401
|
+
return result;
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const isSqlMutationCache = createLruCache<string, boolean>(
|
|
405
|
+
PositiveInt.orThrow(10_000),
|
|
406
|
+
);
|
|
407
|
+
|
|
345
408
|
const isSqlMutationRegEx = new RegExp(
|
|
346
409
|
`\\b(${[
|
|
347
410
|
"alter",
|
|
@@ -365,6 +428,9 @@ const isSqlMutationRegEx = new RegExp(
|
|
|
365
428
|
* ReDoS vulnerabilities.
|
|
366
429
|
*/
|
|
367
430
|
const removeSqlComments = (sql: string): string => {
|
|
431
|
+
// Fast path: if there are no comments, return the original string
|
|
432
|
+
if (!sql.includes("--")) return sql;
|
|
433
|
+
|
|
368
434
|
let result = "";
|
|
369
435
|
let i = 0;
|
|
370
436
|
|
|
@@ -390,9 +456,6 @@ const removeSqlComments = (sql: string): string => {
|
|
|
390
456
|
return result;
|
|
391
457
|
};
|
|
392
458
|
|
|
393
|
-
export const isSqlMutation: Predicate<string> = (sql) =>
|
|
394
|
-
isSqlMutationRegEx.test(removeSqlComments(sql));
|
|
395
|
-
|
|
396
459
|
export interface SqliteQueryPlanRow {
|
|
397
460
|
id: number;
|
|
398
461
|
parent: number;
|
package/src/Task.ts
CHANGED
|
@@ -216,6 +216,32 @@ const isAbortError = (error: unknown): error is AbortError =>
|
|
|
216
216
|
error !== null &&
|
|
217
217
|
(error as { type?: unknown }).type === "AbortError";
|
|
218
218
|
|
|
219
|
+
// For React Native
|
|
220
|
+
if (typeof AbortSignal.any !== "function") {
|
|
221
|
+
AbortSignal.any = function (signals: Array<AbortSignal>): AbortSignal {
|
|
222
|
+
const controller = new AbortController();
|
|
223
|
+
|
|
224
|
+
const onAbort = (event: Event) => {
|
|
225
|
+
controller.abort((event.target as AbortSignal).reason);
|
|
226
|
+
cleanup();
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const cleanup = () => {
|
|
230
|
+
for (const s of signals) s.removeEventListener("abort", onAbort);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
for (const s of signals) {
|
|
234
|
+
if (s.aborted) {
|
|
235
|
+
controller.abort(s.reason);
|
|
236
|
+
return controller.signal;
|
|
237
|
+
}
|
|
238
|
+
s.addEventListener("abort", onAbort);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return controller.signal;
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
219
245
|
/**
|
|
220
246
|
* Combines user signal from context with an internal signal.
|
|
221
247
|
*
|
|
@@ -304,6 +330,21 @@ export const toTask = <T, E>(
|
|
|
304
330
|
]);
|
|
305
331
|
}) as Task<T, E>;
|
|
306
332
|
|
|
333
|
+
// For React Native
|
|
334
|
+
if (typeof AbortSignal.timeout !== "function") {
|
|
335
|
+
AbortSignal.timeout = function (ms: number): AbortSignal {
|
|
336
|
+
const controller = new AbortController();
|
|
337
|
+
const id = setTimeout(() => {
|
|
338
|
+
controller.abort();
|
|
339
|
+
}, ms);
|
|
340
|
+
// clear timeout if aborted early
|
|
341
|
+
controller.signal.addEventListener("abort", () => {
|
|
342
|
+
clearTimeout(id);
|
|
343
|
+
});
|
|
344
|
+
return controller.signal;
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
307
348
|
/**
|
|
308
349
|
* Creates a {@link Task} that waits for the specified duration.
|
|
309
350
|
*
|