@evolu/common 5.4.7 → 6.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 (242) hide show
  1. package/README.md +30 -34
  2. package/dist/src/Array.d.ts +17 -0
  3. package/dist/src/Array.d.ts.map +1 -0
  4. package/dist/src/Array.js +12 -0
  5. package/dist/src/Assert.d.ts +68 -0
  6. package/dist/src/Assert.d.ts.map +1 -0
  7. package/dist/src/Assert.js +77 -0
  8. package/dist/src/BigInt.d.ts +20 -0
  9. package/dist/src/BigInt.d.ts.map +1 -0
  10. package/dist/src/BigInt.js +18 -0
  11. package/dist/src/Buffer.d.ts +92 -0
  12. package/dist/src/Buffer.d.ts.map +1 -0
  13. package/dist/src/Buffer.js +62 -0
  14. package/dist/src/Callbacks.d.ts +20 -0
  15. package/dist/src/Callbacks.d.ts.map +1 -0
  16. package/dist/src/Callbacks.js +18 -0
  17. package/dist/src/Console.d.ts +78 -0
  18. package/dist/src/Console.d.ts.map +1 -0
  19. package/dist/src/Console.js +103 -0
  20. package/dist/src/Crypto.d.ts +72 -39
  21. package/dist/src/Crypto.d.ts.map +1 -1
  22. package/dist/src/Crypto.js +89 -54
  23. package/dist/src/Eq.d.ts +97 -0
  24. package/dist/src/Eq.d.ts.map +1 -0
  25. package/dist/src/Eq.js +167 -0
  26. package/dist/src/Error.d.ts +14 -10
  27. package/dist/src/Error.d.ts.map +1 -1
  28. package/dist/src/Error.js +43 -11
  29. package/dist/src/Evolu/Config.d.ts +69 -0
  30. package/dist/src/Evolu/Config.d.ts.map +1 -0
  31. package/dist/src/Evolu/Config.js +9 -0
  32. package/dist/src/Evolu/Db.d.ts +126 -0
  33. package/dist/src/Evolu/Db.d.ts.map +1 -0
  34. package/dist/src/Evolu/Db.js +774 -0
  35. package/dist/src/Evolu/Diff.d.ts +43 -0
  36. package/dist/src/Evolu/Diff.d.ts.map +1 -0
  37. package/dist/src/Evolu/Diff.js +95 -0
  38. package/dist/src/Evolu/Evolu.d.ts +334 -0
  39. package/dist/src/Evolu/Evolu.d.ts.map +1 -0
  40. package/dist/src/Evolu/Evolu.js +434 -0
  41. package/dist/src/Evolu/Internal.d.ts +26 -0
  42. package/dist/src/Evolu/Internal.d.ts.map +1 -0
  43. package/dist/src/Evolu/Internal.js +25 -0
  44. package/dist/src/Evolu/Kysely.d.ts +6 -0
  45. package/dist/src/Evolu/Kysely.d.ts.map +1 -0
  46. package/dist/src/Evolu/Kysely.js +21 -0
  47. package/dist/src/Evolu/Owner.d.ts +155 -0
  48. package/dist/src/Evolu/Owner.d.ts.map +1 -0
  49. package/dist/src/Evolu/Owner.js +126 -0
  50. package/dist/src/Evolu/Platform.d.ts +23 -0
  51. package/dist/src/Evolu/Platform.d.ts.map +1 -0
  52. package/dist/src/Evolu/Platform.js +1 -0
  53. package/dist/src/Evolu/Protocol.d.ts +401 -0
  54. package/dist/src/Evolu/Protocol.d.ts.map +1 -0
  55. package/dist/src/Evolu/Protocol.js +1151 -0
  56. package/dist/src/Evolu/Public.d.ts +18 -0
  57. package/dist/src/Evolu/Public.d.ts.map +1 -0
  58. package/dist/src/Evolu/Public.js +11 -0
  59. package/dist/src/Evolu/PublicKysely.d.ts +148 -0
  60. package/dist/src/Evolu/PublicKysely.d.ts.map +1 -0
  61. package/dist/src/Evolu/PublicKysely.js +185 -0
  62. package/dist/src/Evolu/Query.d.ts +63 -0
  63. package/dist/src/Evolu/Query.d.ts.map +1 -0
  64. package/dist/src/Evolu/Query.js +61 -0
  65. package/dist/src/Evolu/Relay.d.ts +13 -0
  66. package/dist/src/Evolu/Relay.d.ts.map +1 -0
  67. package/dist/src/Evolu/Relay.js +109 -0
  68. package/dist/src/Evolu/Schema.d.ts +201 -0
  69. package/dist/src/Evolu/Schema.d.ts.map +1 -0
  70. package/dist/src/Evolu/Schema.js +150 -0
  71. package/dist/src/Evolu/Storage.d.ts +49 -0
  72. package/dist/src/Evolu/Storage.d.ts.map +1 -0
  73. package/dist/src/Evolu/Storage.js +1111 -0
  74. package/dist/src/Evolu/Sync.d.ts +59 -0
  75. package/dist/src/Evolu/Sync.d.ts.map +1 -0
  76. package/dist/src/Evolu/Sync.js +29 -0
  77. package/dist/src/Evolu/Timestamp.d.ts +106 -0
  78. package/dist/src/Evolu/Timestamp.d.ts.map +1 -0
  79. package/dist/src/Evolu/Timestamp.js +179 -0
  80. package/dist/src/Function.d.ts +54 -0
  81. package/dist/src/Function.d.ts.map +1 -0
  82. package/dist/src/Function.js +38 -0
  83. package/dist/src/ManyToManyMap.d.ts +26 -0
  84. package/dist/src/ManyToManyMap.d.ts.map +1 -0
  85. package/dist/src/ManyToManyMap.js +92 -0
  86. package/dist/src/NanoId.d.ts +27 -0
  87. package/dist/src/NanoId.d.ts.map +1 -0
  88. package/dist/src/NanoId.js +6 -0
  89. package/dist/src/Number.d.ts +42 -0
  90. package/dist/src/Number.d.ts.map +1 -0
  91. package/dist/src/Number.js +55 -0
  92. package/dist/src/Object.d.ts +35 -0
  93. package/dist/src/Object.d.ts.map +1 -0
  94. package/dist/src/Object.js +36 -0
  95. package/dist/src/Order.d.ts +90 -0
  96. package/dist/src/Order.d.ts.map +1 -0
  97. package/dist/src/Order.js +85 -0
  98. package/dist/src/Promise.d.ts +180 -0
  99. package/dist/src/Promise.d.ts.map +1 -0
  100. package/dist/src/Promise.js +176 -0
  101. package/dist/src/Random.d.ts +52 -0
  102. package/dist/src/Random.d.ts.map +1 -0
  103. package/dist/src/Random.js +29 -0
  104. package/dist/src/Ref.d.ts +40 -0
  105. package/dist/src/Ref.d.ts.map +1 -0
  106. package/dist/src/Ref.js +13 -0
  107. package/dist/src/Result.d.ts +421 -0
  108. package/dist/src/Result.d.ts.map +1 -0
  109. package/dist/src/Result.js +357 -0
  110. package/dist/src/Skiplist.d.ts +23 -0
  111. package/dist/src/Skiplist.d.ts.map +1 -0
  112. package/dist/src/Skiplist.js +58 -0
  113. package/dist/src/Sqlite.d.ts +116 -52
  114. package/dist/src/Sqlite.d.ts.map +1 -1
  115. package/dist/src/Sqlite.js +183 -67
  116. package/dist/src/Store.d.ts +45 -8
  117. package/dist/src/Store.d.ts.map +1 -1
  118. package/dist/src/Store.js +33 -17
  119. package/dist/src/String.d.ts +2 -0
  120. package/dist/src/String.d.ts.map +1 -0
  121. package/dist/src/String.js +14 -0
  122. package/dist/src/Time.d.ts +20 -0
  123. package/dist/src/Time.d.ts.map +1 -0
  124. package/dist/src/Time.js +25 -0
  125. package/dist/src/Type.d.ts +1937 -0
  126. package/dist/src/Type.d.ts.map +1 -0
  127. package/dist/src/Type.js +2002 -0
  128. package/dist/src/Types.d.ts +188 -0
  129. package/dist/src/Types.d.ts.map +1 -0
  130. package/dist/src/Types.js +6 -0
  131. package/dist/src/WebSocket.d.ts +112 -0
  132. package/dist/src/WebSocket.d.ts.map +1 -0
  133. package/dist/src/WebSocket.js +139 -0
  134. package/dist/src/Worker.d.ts +44 -0
  135. package/dist/src/Worker.d.ts.map +1 -0
  136. package/dist/src/Worker.js +66 -0
  137. package/dist/src/index.d.ts +24 -11
  138. package/dist/src/index.d.ts.map +1 -1
  139. package/dist/src/index.js +24 -11
  140. package/package.json +29 -38
  141. package/src/Array.ts +39 -0
  142. package/src/Assert.ts +116 -0
  143. package/src/BigInt.ts +29 -0
  144. package/src/Buffer.ts +175 -0
  145. package/src/Callbacks.ts +43 -0
  146. package/src/Console.ts +159 -0
  147. package/src/Crypto.ts +169 -115
  148. package/src/Eq.ts +204 -0
  149. package/src/Error.ts +57 -20
  150. package/src/Evolu/Config.ts +83 -0
  151. package/src/Evolu/Db.ts +1275 -0
  152. package/src/Evolu/Diff.ts +142 -0
  153. package/src/Evolu/Evolu.ts +947 -0
  154. package/src/Evolu/Internal.ts +26 -0
  155. package/src/Evolu/Kysely.ts +38 -0
  156. package/src/Evolu/Owner.ts +296 -0
  157. package/src/Evolu/Platform.ts +27 -0
  158. package/src/Evolu/Protocol.ts +1857 -0
  159. package/src/Evolu/Public.ts +43 -0
  160. package/src/Evolu/PublicKysely.ts +240 -0
  161. package/src/Evolu/Query.ts +167 -0
  162. package/src/Evolu/Relay.ts +142 -0
  163. package/src/Evolu/Schema.ts +417 -0
  164. package/src/Evolu/Storage.ts +1281 -0
  165. package/src/Evolu/Sync.ts +105 -0
  166. package/src/Evolu/Timestamp.ts +311 -0
  167. package/src/Function.ts +58 -0
  168. package/src/ManyToManyMap.ts +140 -0
  169. package/src/NanoId.ts +39 -0
  170. package/src/Number.ts +90 -0
  171. package/src/Object.ts +64 -0
  172. package/src/Order.ts +113 -0
  173. package/src/Promise.ts +295 -0
  174. package/src/Random.ts +68 -0
  175. package/src/Ref.ts +63 -0
  176. package/src/Result.ts +453 -0
  177. package/src/Skiplist.ts +102 -0
  178. package/src/Sqlite.ts +366 -153
  179. package/src/Store.ts +79 -36
  180. package/src/String.ts +10 -0
  181. package/src/Time.ts +36 -0
  182. package/src/Type.ts +3978 -0
  183. package/src/Types.ts +209 -0
  184. package/src/WebSocket.ts +273 -0
  185. package/src/Worker.ts +129 -0
  186. package/src/index.ts +24 -11
  187. package/dist/src/Config.d.ts +0 -56
  188. package/dist/src/Config.d.ts.map +0 -1
  189. package/dist/src/Config.js +0 -39
  190. package/dist/src/Crdt.d.ts +0 -89
  191. package/dist/src/Crdt.d.ts.map +0 -1
  192. package/dist/src/Crdt.js +0 -181
  193. package/dist/src/Db.d.ts +0 -107
  194. package/dist/src/Db.d.ts.map +0 -1
  195. package/dist/src/Db.js +0 -443
  196. package/dist/src/Diff.d.ts +0 -27
  197. package/dist/src/Diff.d.ts.map +0 -1
  198. package/dist/src/Diff.js +0 -84
  199. package/dist/src/Evolu.d.ts +0 -426
  200. package/dist/src/Evolu.d.ts.map +0 -1
  201. package/dist/src/Evolu.js +0 -333
  202. package/dist/src/Model.d.ts +0 -141
  203. package/dist/src/Model.d.ts.map +0 -1
  204. package/dist/src/Model.js +0 -125
  205. package/dist/src/Murmurhash.d.ts +0 -2
  206. package/dist/src/Murmurhash.d.ts.map +0 -1
  207. package/dist/src/Murmurhash.js +0 -60
  208. package/dist/src/Owner.d.ts +0 -33
  209. package/dist/src/Owner.d.ts.map +0 -1
  210. package/dist/src/Owner.js +0 -26
  211. package/dist/src/Platform.d.ts +0 -37
  212. package/dist/src/Platform.d.ts.map +0 -1
  213. package/dist/src/Platform.js +0 -11
  214. package/dist/src/Protobuf.d.ts +0 -81
  215. package/dist/src/Protobuf.d.ts.map +0 -1
  216. package/dist/src/Protobuf.js +0 -92
  217. package/dist/src/Public.d.ts +0 -13
  218. package/dist/src/Public.d.ts.map +0 -1
  219. package/dist/src/Public.js +0 -6
  220. package/dist/src/Socket.d.ts +0 -8
  221. package/dist/src/Socket.d.ts.map +0 -1
  222. package/dist/src/Socket.js +0 -51
  223. package/dist/src/Sql.d.ts +0 -12
  224. package/dist/src/Sql.d.ts.map +0 -1
  225. package/dist/src/Sql.js +0 -30
  226. package/dist/src/Sync.d.ts +0 -70
  227. package/dist/src/Sync.d.ts.map +0 -1
  228. package/dist/src/Sync.js +0 -127
  229. package/src/Config.ts +0 -119
  230. package/src/Crdt.ts +0 -361
  231. package/src/Db.ts +0 -955
  232. package/src/Diff.ts +0 -114
  233. package/src/Evolu.ts +0 -1016
  234. package/src/Model.ts +0 -233
  235. package/src/Murmurhash.ts +0 -70
  236. package/src/Owner.ts +0 -69
  237. package/src/Platform.ts +0 -47
  238. package/src/Protobuf.ts +0 -155
  239. package/src/Public.ts +0 -12
  240. package/src/Socket.ts +0 -83
  241. package/src/Sql.ts +0 -41
  242. package/src/Sync.ts +0 -315
package/src/Crypto.ts CHANGED
@@ -1,137 +1,191 @@
1
- import * as S from "@effect/schema/Schema";
2
- import { secretbox } from "@noble/ciphers/salsa";
3
- import { concatBytes } from "@noble/ciphers/utils";
1
+ /**
2
+ * 🔒
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import { xchacha20poly1305 } from "@noble/ciphers/chacha";
4
8
  import { hmac } from "@noble/hashes/hmac";
5
- import { sha512 } from "@noble/hashes/sha512";
9
+ import { sha512 } from "@noble/hashes/sha2";
6
10
  import { randomBytes } from "@noble/hashes/utils";
7
11
  import * as bip39 from "@scure/bip39";
8
12
  import { wordlist } from "@scure/bip39/wordlists/english";
9
- import * as Brand from "effect/Brand";
10
- import * as Context from "effect/Context";
11
- import * as Effect from "effect/Effect";
12
- import * as Layer from "effect/Layer";
13
- import { Id } from "./Model.js";
13
+ import { urlAlphabet } from "nanoid";
14
+ import { getOrThrow, Result, trySync } from "./Result.js";
15
+ import {
16
+ brand,
17
+ Id,
18
+ length,
19
+ Mnemonic,
20
+ NonNegativeInt,
21
+ Uint8Array,
22
+ } from "./Type.js";
23
+ import { Brand } from "./Types.js";
24
+
25
+ /** `Uint8Array` created by {@link createRandomBytes}. */
26
+ export type RandomBytes = Uint8Array & Brand<"RandomBytes">;
27
+
28
+ export type CreateRandomBytes = (bytesLength?: number) => RandomBytes;
29
+
30
+ export interface CreateRandomBytesDep {
31
+ readonly createRandomBytes: CreateRandomBytes;
32
+ }
33
+
34
+ /** Cryptographically secure PRNG. Uses internal OS-level crypto.getRandomValues. */
35
+ export const createRandomBytes: CreateRandomBytes = (bytesLength = 32) =>
36
+ randomBytes(bytesLength) as RandomBytes;
37
+
38
+ export type CreateMnemonic = () => Mnemonic;
39
+
40
+ export interface CreateMnemonicDep {
41
+ readonly createMnemonic: CreateMnemonic;
42
+ }
43
+
44
+ export const createEnglishMnemonic: CreateMnemonic = () =>
45
+ bip39.generateMnemonic(wordlist, 128) as Mnemonic;
46
+
47
+ export type MnemonicSeed = Uint8Array & Brand<"MnemonicSeed">;
48
+
49
+ export const mnemonicToMnemonicSeed = (mnemonic: Mnemonic): MnemonicSeed =>
50
+ bip39.mnemonicToSeedSync(mnemonic) as MnemonicSeed;
14
51
 
15
52
  /**
16
- * Mnemonic is a password generated by Evolu in BIP39 format.
53
+ * SLIP21.
17
54
  *
18
- * A mnemonic, also known as a "seed phrase," is a set of 12 words in a specific
19
- * order chosen from a predefined list. The purpose of the BIP39 mnemonic is to
20
- * provide a human-readable way of storing a private key.
55
+ * https://github.com/satoshilabs/slips/blob/master/slip-0021.md
21
56
  */
22
- export type Mnemonic = string & Brand.Brand<"Mnemonic">;
57
+ export const createSlip21 = (
58
+ seed: MnemonicSeed,
59
+ path: ReadonlyArray<string>,
60
+ ): Uint8Array => {
61
+ let m = hmac(sha512, "Symmetric key seed", seed);
62
+ for (const component of path) {
63
+ const p = new TextEncoder().encode(component);
64
+ const e = new globalThis.Uint8Array(p.byteLength + 1);
65
+ e[0] = 0;
66
+ e.set(p, 1);
67
+ m = hmac(sha512, m.slice(0, 32), e);
68
+ }
69
+ return m.slice(32, 64);
70
+ };
23
71
 
24
72
  /**
25
- * Mnemonic is a password generated by Evolu in BIP39 format.
73
+ * Creates a 21-character Base64URL ID (also known as nanoid) from a SLIP-21
74
+ * derived key.
75
+ *
76
+ * Reduces the 256-bit SLIP-21 output to 126 bits (21 chars × 6 bits) for a
77
+ * compact, human-readable, and shareable identifier suitable for UI display or
78
+ * URL use. While this lowers entropy, 126 bits remains cryptographically secure
79
+ * for uniqueness and unpredictability in most applications (comparable to
80
+ * UUIDv4's 122 bits).
26
81
  *
27
- * A mnemonic, also known as a "seed phrase," is a set of 12 words in a specific
28
- * order chosen from a predefined list. The purpose of the BIP39 mnemonic is to
29
- * provide a human-readable way of storing a private key.
82
+ * See https://github.com/satoshilabs/slips/blob/master/slip-0021.md
30
83
  */
31
- export const createMnemonic = (): Mnemonic =>
32
- bip39.generateMnemonic(wordlist, 128) as Mnemonic;
84
+ export const createSlip21Id = (
85
+ seed: MnemonicSeed,
86
+ path: ReadonlyArray<string>,
87
+ ): Id => {
88
+ const slip21 = createSlip21(seed, path);
89
+ let id = "" as Id;
90
+
91
+ // Convert the key to the Id/NanoId/Base64Url format.
92
+ for (let i = 0; i < 21; i++) {
93
+ id = (id + urlAlphabet[slip21[i] & 63]) as Id;
94
+ }
33
95
 
34
- /** Parse a string to {@link Mnemonic}. */
35
- export const parseMnemonic = (
36
- mnemonic: string,
37
- ): Effect.Effect<Mnemonic, InvalidMnemonicError> => {
38
- const mnemonicTrimmed = mnemonic.trim();
39
- return bip39.validateMnemonic(mnemonicTrimmed, wordlist)
40
- ? Effect.succeed(mnemonicTrimmed as Mnemonic)
41
- : Effect.fail<InvalidMnemonicError>({
42
- _tag: "InvalidMnemonicError",
43
- });
96
+ return id;
44
97
  };
45
98
 
46
- export interface InvalidMnemonicError {
47
- readonly _tag: "InvalidMnemonicError";
99
+ /** The encryption key for {@link SymmetricCrypto}. */
100
+ export const EncryptionKey = brand("EncryptionKey", length(32)(Uint8Array));
101
+ export type EncryptionKey = typeof EncryptionKey.Type;
102
+
103
+ export const createEncryptionKey = (seed: MnemonicSeed): EncryptionKey =>
104
+ createSlip21(seed, ["Evolu", "Encryption Key"]) as EncryptionKey;
105
+
106
+ /** Symmetric cryptography. */
107
+ export interface SymmetricCrypto {
108
+ readonly nonceLength: NonNegativeInt;
109
+
110
+ readonly encrypt: (
111
+ plaintext: Uint8Array,
112
+ encryptionKey: EncryptionKey,
113
+ ) => {
114
+ readonly nonce: Uint8Array;
115
+ readonly ciphertext: Uint8Array;
116
+ };
117
+
118
+ readonly decrypt: (
119
+ ciphertext: Uint8Array,
120
+ encryptionKey: EncryptionKey,
121
+ nonce: Uint8Array,
122
+ ) => Result<Uint8Array, SymmetricCryptoDecryptError>;
48
123
  }
49
124
 
50
- export const mnemonicToSeed = (mnemonic: Mnemonic): Uint8Array =>
51
- bip39.mnemonicToSeedSync(mnemonic);
125
+ export interface SymmetricCryptoDep {
126
+ readonly symmetricCrypto: SymmetricCrypto;
127
+ }
52
128
 
53
- export class NanoIdGenerator extends Context.Tag("NanoIdGenerator")<
54
- NanoIdGenerator,
55
- {
56
- readonly nanoid: Effect.Effect<NanoId>;
57
- readonly nodeId: Effect.Effect<NodeId>;
58
- readonly rowId: Effect.Effect<Id>;
59
- }
60
- >() {}
61
-
62
- export const createNanoIdGeneratorLive = (
63
- customAlphabet: (
64
- alphabet: string,
65
- defaultSize?: number,
66
- ) => (size?: number) => string,
67
- nanoid: (size?: number) => string,
68
- ): Layer.Layer<NanoIdGenerator, never, never> => {
69
- const nanoidForNodeId = customAlphabet("0123456789abcdef", 16);
70
- return Layer.succeed(
71
- NanoIdGenerator,
72
- NanoIdGenerator.of({
73
- nanoid: Effect.sync(() => nanoid() as NanoId),
74
- nodeId: Effect.sync(() => nanoidForNodeId() as NodeId),
75
- rowId: Effect.sync(() => nanoid() as Id),
76
- }),
77
- );
78
- };
129
+ export interface SymmetricCryptoDecryptError {
130
+ readonly type: "SymmetricCryptoDecryptError";
131
+ readonly error: unknown;
132
+ }
79
133
 
80
- export type NanoId = string & Brand.Brand<"NanoId">;
134
+ /**
135
+ * XChaCha20-Poly1305 encryption
136
+ *
137
+ * https://github.com/paulmillr/noble-ciphers?tab=readme-ov-file#which-cipher-should-i-pick
138
+ */
139
+ export const createSymmetricCrypto = (
140
+ deps: CreateRandomBytesDep,
141
+ ): SymmetricCrypto => {
142
+ const nonceLength = getOrThrow(NonNegativeInt.from(24));
81
143
 
82
- export const NodeId = S.String.pipe(
83
- S.pattern(/^[\w-]{16}$/),
84
- S.brand("NodeId"),
85
- );
86
- export type NodeId = typeof NodeId.Type;
144
+ const symmetricCrypto: SymmetricCrypto = {
145
+ nonceLength,
87
146
 
88
- // SLIP-21 implementation
89
- // https://github.com/satoshilabs/slips/blob/master/slip-0021.md
90
- export const slip21Derive = (
91
- seed: Uint8Array,
92
- path: ReadonlyArray<string>,
93
- ): Effect.Effect<Uint8Array> =>
94
- Effect.sync(() => {
95
- let m = hmac(sha512, "Symmetric key seed", seed);
96
- for (let i = 0; i < path.length; i++) {
97
- const p = new TextEncoder().encode(path[i]);
98
- const e = new Uint8Array(p.byteLength + 1);
99
- e[0] = 0;
100
- e.set(p, 1);
101
- m = hmac(sha512, m.slice(0, 32), e);
102
- }
103
- return m.slice(32, 64);
104
- });
105
-
106
- export class SecretBox extends Context.Tag("SecretBox")<
107
- SecretBox,
108
- {
109
- readonly seal: (
110
- key: Uint8Array,
111
- plaintext: Uint8Array,
112
- ) => Effect.Effect<Uint8Array>;
113
-
114
- readonly open: (
115
- key: Uint8Array,
116
- ciphertext: Uint8Array,
117
- ) => Effect.Effect<Uint8Array>;
118
- }
119
- >() {
120
- static Live = Layer.succeed(
121
- SecretBox,
122
- SecretBox.of({
123
- seal: (key, plaintext) =>
124
- Effect.sync(() => {
125
- const nonce = randomBytes(24);
126
- const ciphertext = secretbox(key, nonce).seal(plaintext);
127
- return concatBytes(nonce, ciphertext);
128
- }),
129
- open: (key, ciphertext) =>
130
- Effect.sync(() => {
131
- const nonce = ciphertext.subarray(0, 24);
132
- const ciphertextWithoutNonce = ciphertext.subarray(24);
133
- return secretbox(key, nonce).open(ciphertextWithoutNonce);
147
+ encrypt: (plaintext, encryptionKey) => {
148
+ const nonce = deps.createRandomBytes(nonceLength);
149
+ const ciphertext = xchacha20poly1305(encryptionKey, nonce).encrypt(
150
+ plaintext,
151
+ );
152
+ return { nonce, ciphertext };
153
+ },
154
+
155
+ decrypt: (ciphertext, encryptionKey, nonce) =>
156
+ trySync(
157
+ () => xchacha20poly1305(encryptionKey, nonce).decrypt(ciphertext),
158
+ (error): SymmetricCryptoDecryptError => ({
159
+ type: "SymmetricCryptoDecryptError",
160
+ error,
134
161
  }),
135
- }),
136
- );
137
- }
162
+ ),
163
+ };
164
+
165
+ return symmetricCrypto;
166
+ };
167
+
168
+ /**
169
+ * Returns the PADMÉ padded length for a given input length.
170
+ *
171
+ * PADMÉ limits information leakage about the length of the plain-text for a
172
+ * wide range of encrypted data sizes. See the PURBs paper for details:
173
+ * https://bford.info/pub/sec/purb.pdf
174
+ */
175
+ export const padmePaddedLength = (length: NonNegativeInt): NonNegativeInt => {
176
+ if (length <= 0) return 0 as NonNegativeInt;
177
+ const e = 31 - Math.clz32(length >>> 0);
178
+ const s = 32 - Math.clz32(e >>> 0);
179
+ const z = Math.max(0, e - s);
180
+ const mask = (1 << z) - 1;
181
+ return ((length + mask) & ~mask) as NonNegativeInt;
182
+ };
183
+
184
+ /**
185
+ * Returns the PADMÉ padding length for a given input length. Uses
186
+ * {@link padmePaddedLength}.
187
+ */
188
+
189
+ export const padmePaddingLength = (length: NonNegativeInt): NonNegativeInt => {
190
+ return (padmePaddedLength(length) - length) as NonNegativeInt;
191
+ };
package/src/Eq.ts ADDED
@@ -0,0 +1,204 @@
1
+ import { JsonArray, JsonObject, JsonValue, JsonValueInput } from "./Type.js";
2
+ import { Order } from "./Order.js";
3
+
4
+ /**
5
+ * Compares two values of the same type `A` for equality.
6
+ *
7
+ * Equality functions start with an 'eq' prefix, e.g., `eqString`.
8
+ *
9
+ * TODO: Explain, examples (composition etc.)
10
+ */
11
+ export type Eq<in A> = (x: A, y: A) => boolean;
12
+
13
+ export const eqStrict = <A>(x: A, y: A): boolean => x === y;
14
+
15
+ export const eqString: Eq<string> = eqStrict;
16
+ export const eqNumber: Eq<number> = eqStrict;
17
+ export const eqBigInt: Eq<bigint> = eqStrict;
18
+ export const eqBoolean: Eq<boolean> = eqStrict;
19
+ export const eqUndefined: Eq<undefined> = eqStrict;
20
+ export const eqNull: Eq<null> = eqStrict;
21
+
22
+ /** Derives an {@link Eq} from an {@link Order}. */
23
+ export const eqFromOrder =
24
+ <A>(order: Order<A>): Eq<A> =>
25
+ (x, y) =>
26
+ order(x, y) === 0;
27
+
28
+ /**
29
+ * Creates an equivalence function for array-like structures based on an
30
+ * equivalence for their elements.
31
+ *
32
+ * ### Example
33
+ *
34
+ * ```ts
35
+ * const eqArrayNumber = createEqArrayLike(eqNumber);
36
+ * eqArrayNumber([1, 2, 3], [1, 2, 3]); // true (works with regular arrays)
37
+ * eqArrayNumber(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 3])); // true (works with Uint8Array)
38
+ * eqArrayNumber([1, 2, 3], [1, 2, 4]); // false
39
+ * ```
40
+ */
41
+ export const createEqArrayLike =
42
+ <A>(item: Eq<A>): Eq<ArrayLike<A>> =>
43
+ (x, y) => {
44
+ if (x === y) return true;
45
+ if (x.length !== y.length) return false;
46
+
47
+ for (let i = 0; i < x.length; i++) {
48
+ if (!item(x[i], y[i])) return false;
49
+ }
50
+
51
+ return true;
52
+ };
53
+
54
+ /**
55
+ * Compares two array-like structures of numbers for equality.
56
+ *
57
+ * ### Example
58
+ *
59
+ * ```ts
60
+ * eqArrayNumber([1, 2, 3], [1, 2, 3]); // true (works with regular arrays)
61
+ * eqArrayNumber(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 3])); // true (works with Uint8Array)
62
+ * eqArrayNumber([1, 2, 3], [1, 2, 4]); // false
63
+ * ```
64
+ */
65
+ export const eqArrayNumber = createEqArrayLike(eqNumber);
66
+
67
+ /**
68
+ * Creates an equivalence function for objects based on an equivalence for their
69
+ * fields.
70
+ *
71
+ * ### Example
72
+ *
73
+ * ```ts
74
+ * const eqObjectNumber = createEqObject({ a: eqNumber });
75
+ * eqObjectNumber({ a: 1 }, { a: 1 }); // true
76
+ * eqObjectNumber({ a: 1 }, { a: 2 }); // false
77
+ * ```
78
+ */
79
+ export const createEqObject =
80
+ <A>(eqs: { [K in keyof A]: Eq<A[K]> }): Eq<{
81
+ readonly [K in keyof A]: A[K];
82
+ }> =>
83
+ (x, y) => {
84
+ if (x === y) return true;
85
+ for (const key in eqs) {
86
+ if (!eqs[key](x[key], y[key])) {
87
+ return false;
88
+ }
89
+ }
90
+ return true;
91
+ };
92
+
93
+ /**
94
+ * Deeply compares two {@link JsonValue} values for equality.
95
+ *
96
+ * - Uses an iterative approach with a stack to handle large or deeply nested
97
+ * objects without risking stack overflow.
98
+ * - Handles circular references with a WeakMap to prevent infinite loops.
99
+ * - Unlike JSON.stringify, this function directly compares values, avoiding
100
+ * serialization overhead and leveraging short-circuit evaluation for faster
101
+ * failure on mismatched structures.
102
+ *
103
+ * ### Example
104
+ *
105
+ * ```ts
106
+ * const obj1: Json = { name: "Alice", hobbies: ["reading", "hiking"] };
107
+ * const obj2: Json = { name: "Alice", hobbies: ["reading", "hiking"] };
108
+ * console.log(eqJson(obj1, obj2)); // true
109
+ * ```
110
+ */
111
+ export const eqJsonValue = (a: JsonValue, b: JsonValue): boolean => {
112
+ const stack: Array<[JsonValue, JsonValue]> = [[a, b]];
113
+
114
+ const seen = new WeakMap<object, WeakSet<object>>();
115
+
116
+ while (stack.length > 0) {
117
+ const [x, y] = stack.pop()!;
118
+
119
+ if (x === y) continue;
120
+
121
+ const typeX = typeof x;
122
+ const typeY = typeof y;
123
+
124
+ if (typeX !== typeY || x === null || y === null) return false;
125
+
126
+ if (typeX === "number" && isNaN(x as number) && isNaN(y as number)) {
127
+ continue;
128
+ }
129
+
130
+ if (typeX === "object") {
131
+ const isArrayX = Array.isArray(x);
132
+ const isArrayY = Array.isArray(y);
133
+
134
+ if (isArrayX !== isArrayY) return false;
135
+
136
+ const xObj = x as object;
137
+ const yObj = y as object;
138
+
139
+ if (seen.has(xObj)) {
140
+ const ySet = seen.get(xObj)!;
141
+ if (ySet.has(yObj)) {
142
+ continue;
143
+ }
144
+ ySet.add(yObj);
145
+ } else {
146
+ const ySet = new WeakSet<object>();
147
+ ySet.add(yObj);
148
+ seen.set(xObj, ySet);
149
+ }
150
+
151
+ if (isArrayX && isArrayY) {
152
+ const xArr = x as JsonArray;
153
+ const yArr = y as JsonArray;
154
+
155
+ if (xArr.length !== yArr.length) return false;
156
+ for (let i = 0; i < xArr.length; i++) {
157
+ stack.push([xArr[i], yArr[i]]);
158
+ }
159
+ } else {
160
+ const xObjTyped = x as JsonObject;
161
+ const yObjTyped = y as JsonObject;
162
+
163
+ const xKeys = Object.keys(xObjTyped);
164
+ const yKeys = Object.keys(yObjTyped);
165
+
166
+ if (xKeys.length !== yKeys.length) return false;
167
+
168
+ const yKeySet = new Set(yKeys);
169
+
170
+ for (const key of xKeys) {
171
+ if (!yKeySet.has(key)) return false;
172
+ stack.push([xObjTyped[key], yObjTyped[key]]);
173
+ }
174
+ }
175
+ } else {
176
+ return false;
177
+ }
178
+ }
179
+
180
+ return true;
181
+ };
182
+
183
+ /**
184
+ * Deeply compares two {@link JsonValueInput} values for equality.
185
+ *
186
+ * - Uses an iterative approach with a stack to handle large or deeply nested
187
+ * objects without risking stack overflow.
188
+ * - Handles circular references with a WeakMap to prevent infinite loops.
189
+ * - Unlike JSON.stringify, this function directly compares values, avoiding
190
+ * serialization overhead and leveraging short-circuit evaluation for faster
191
+ * failure on mismatched structures.
192
+ *
193
+ * ### Example
194
+ *
195
+ * ```ts
196
+ * const obj1: Json = { name: "Alice", hobbies: ["reading", "hiking"] };
197
+ * const obj2: Json = { name: "Alice", hobbies: ["reading", "hiking"] };
198
+ * console.log(eqJson(obj1, obj2)); // true
199
+ * ```
200
+ */
201
+ export const eqJsonValueInput = (
202
+ a: JsonValueInput,
203
+ b: JsonValueInput,
204
+ ): boolean => eqJsonValue(a as JsonValue, b as JsonValue);
package/src/Error.ts CHANGED
@@ -1,29 +1,66 @@
1
- import { TimestampError } from "./Crdt.js";
2
-
3
- /** The EvoluError type is used to represent errors that can occur in Evolu. */
4
- export type EvoluError = TimestampError | UnexpectedError;
5
-
6
1
  /**
7
- * UnexpectedError represents errors that can occur unexpectedly anywhere, even
8
- * in third-party libraries, because Evolu uses Effect to track all errors.
2
+ * A serializable representation of an error for safe transfer between execution
3
+ * contexts, such as Web Workers and the main thread.
4
+ *
5
+ * Use this type for unknown (unexpected) errors that need to be transferred
6
+ * across boundaries where native Error objects cannot be sent directly. Not
7
+ * intended for regular (expected) errors.
8
+ *
9
+ * The `error` property contains a plain object with error details, a string, or
10
+ * a fallback value if serialization fails to preserve as much debugging
11
+ * information as possible.
9
12
  */
10
- export interface UnexpectedError {
11
- readonly _tag: "UnexpectedError";
13
+ export interface TransferableError {
14
+ readonly type: "TransferableError";
12
15
  readonly error: unknown;
13
16
  }
14
17
 
15
- export const makeUnexpectedError = (error: unknown): UnexpectedError => ({
16
- _tag: "UnexpectedError",
17
- error,
18
- });
18
+ /** Creates a {@link TransferableError} from an unknown error. */
19
+ export const createTransferableError = (error: unknown): TransferableError => {
20
+ const convertError = (err: Error): Record<string, unknown> => {
21
+ const transferableError: Record<string, unknown> =
22
+ Object.getOwnPropertyNames(err).reduce<Record<string, unknown>>(
23
+ (acc, key) => {
24
+ const value = (err as never)[key] as unknown;
25
+ if (key === "cause" && value instanceof Error) {
26
+ // Recursively process the `cause` property
27
+ acc[key] = convertError(value);
28
+ } else if (typeof value !== "function") {
29
+ acc[key] = value;
30
+ }
31
+ return acc;
32
+ },
33
+ {},
34
+ );
35
+ return transferableError;
36
+ };
37
+
38
+ if (error instanceof Error) {
39
+ return {
40
+ type: "TransferableError",
41
+ error: convertError(error),
42
+ };
43
+ }
19
44
 
20
- /** Error isn't a structured cloneable object. */
21
- export const ensureTransferableError = (error: unknown): unknown => {
22
- if (error instanceof Error)
45
+ try {
46
+ // Clone other values that are transferable
23
47
  return {
24
- message: error.message,
25
- name: error.name,
26
- stack: error.stack,
48
+ type: "TransferableError",
49
+ error: structuredClone(error),
27
50
  };
28
- return error;
51
+ } catch {
52
+ // Fallback for non-transferable or problematic values
53
+ try {
54
+ return {
55
+ type: "TransferableError",
56
+ error: String(error), // Attempt to convert to a string
57
+ };
58
+ } catch {
59
+ // Final fallback if even `String(error)` fails
60
+ return {
61
+ type: "TransferableError",
62
+ error: "[Unserializable Object]",
63
+ };
64
+ }
65
+ }
29
66
  };
@@ -0,0 +1,83 @@
1
+ import { ConsoleConfig } from "../Console.js";
2
+ import { getOrThrow } from "../Result.js";
3
+ import { Mnemonic, SimpleName } from "../Type.js";
4
+ import type { DbIndexesBuilder } from "./Kysely.js";
5
+
6
+ export interface Config extends ConsoleConfig {
7
+ /**
8
+ * The name of Evolu instances. Evolu is multitenant - it can run more
9
+ * instances concurrently. Every Evolu instance has to have its own unique
10
+ * name. Database files are separated and invisible to each other.
11
+ *
12
+ * The default value is: `Evolu`.
13
+ *
14
+ * ### Example
15
+ *
16
+ * ```ts
17
+ * // name: getOrThrow(SimpleName.from("MyApp"))
18
+ * ```
19
+ */
20
+ readonly name: SimpleName;
21
+
22
+ /**
23
+ * URL for Evolu sync and backup server.
24
+ *
25
+ * The default value is `https://evolu.world`.
26
+ */
27
+ readonly syncUrl: string;
28
+
29
+ /**
30
+ * URL to reload browser tabs after reset or restore.
31
+ *
32
+ * The default value is `/`.
33
+ */
34
+ readonly reloadUrl: string;
35
+
36
+ /**
37
+ * Maximum physical clock drift allowed in ms.
38
+ *
39
+ * The default value is 5 * 60 * 1000 (5 minutes).
40
+ */
41
+ readonly maxDrift: number;
42
+
43
+ /**
44
+ * Use the `indexes` option to define SQLite indexes.
45
+ *
46
+ * Table and column names are not typed because Kysely doesn't support it.
47
+ *
48
+ * https://medium.com/@JasonWyatt/squeezing-performance-from-sqlite-indexes-indexes-c4e175f3c346
49
+ *
50
+ * ### Example
51
+ *
52
+ * ```ts
53
+ * const evolu = createEvolu(evoluReactDeps)(Schema, {
54
+ * indexes: (create) => [
55
+ * create("todoCreatedAt").on("todo").column("createdAt"),
56
+ * create("todoCategoryCreatedAt")
57
+ * .on("todoCategory")
58
+ * .column("createdAt"),
59
+ * ],
60
+ * });
61
+ * ```
62
+ */
63
+ readonly indexes?: DbIndexesBuilder;
64
+
65
+ /**
66
+ * Use this option to create Evolu with the specified mnemonic. If omitted,
67
+ * the mnemonic will be autogenerated. That should be the default behavior
68
+ * until special UX requirements are needed (e.g., multitenancy).
69
+ */
70
+ readonly mnemonic?: Mnemonic;
71
+ }
72
+
73
+ export interface ConfigDep {
74
+ readonly config: Config;
75
+ }
76
+
77
+ export const defaultConfig: Config = {
78
+ name: getOrThrow(SimpleName.fromParent("Evolu")),
79
+ syncUrl: "https://evolu.world",
80
+ reloadUrl: "/",
81
+ maxDrift: 5 * 60 * 1000,
82
+ enableLogging: false,
83
+ };