@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.
Files changed (173) hide show
  1. package/dist/src/Array.d.ts +58 -5
  2. package/dist/src/Array.d.ts.map +1 -1
  3. package/dist/src/Array.js +53 -5
  4. package/dist/src/Assert.d.ts +6 -16
  5. package/dist/src/Assert.d.ts.map +1 -1
  6. package/dist/src/Assert.js +6 -18
  7. package/dist/src/Brand.d.ts +75 -0
  8. package/dist/src/Brand.d.ts.map +1 -0
  9. package/dist/src/Brand.js +1 -0
  10. package/dist/src/Buffer.d.ts +1 -1
  11. package/dist/src/Buffer.d.ts.map +1 -1
  12. package/dist/src/Buffer.js +8 -7
  13. package/dist/src/Cache.d.ts +44 -0
  14. package/dist/src/Cache.d.ts.map +1 -0
  15. package/dist/src/Cache.js +52 -0
  16. package/dist/src/Callbacks.d.ts +45 -12
  17. package/dist/src/Callbacks.d.ts.map +1 -1
  18. package/dist/src/Callbacks.js +14 -7
  19. package/dist/src/Console.d.ts +31 -6
  20. package/dist/src/Console.d.ts.map +1 -1
  21. package/dist/src/Console.js +72 -9
  22. package/dist/src/Crypto.d.ts +61 -34
  23. package/dist/src/Crypto.d.ts.map +1 -1
  24. package/dist/src/Crypto.js +32 -45
  25. package/dist/src/Evolu/Db.d.ts +158 -65
  26. package/dist/src/Evolu/Db.d.ts.map +1 -1
  27. package/dist/src/Evolu/Db.js +286 -694
  28. package/dist/src/Evolu/Diff.d.ts +3 -3
  29. package/dist/src/Evolu/Diff.d.ts.map +1 -1
  30. package/dist/src/Evolu/Diff.js +7 -5
  31. package/dist/src/Evolu/Evolu.d.ts +208 -133
  32. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  33. package/dist/src/Evolu/Evolu.js +188 -183
  34. package/dist/src/Evolu/Internal.d.ts +0 -2
  35. package/dist/src/Evolu/Internal.d.ts.map +1 -1
  36. package/dist/src/Evolu/Internal.js +0 -2
  37. package/dist/src/Evolu/LocalAuth.d.ts +150 -0
  38. package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
  39. package/dist/src/Evolu/LocalAuth.js +174 -0
  40. package/dist/src/Evolu/Owner.d.ts +264 -120
  41. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  42. package/dist/src/Evolu/Owner.js +130 -104
  43. package/dist/src/Evolu/Platform.d.ts +9 -7
  44. package/dist/src/Evolu/Platform.d.ts.map +1 -1
  45. package/dist/src/Evolu/Protocol.d.ts +277 -232
  46. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  47. package/dist/src/Evolu/Protocol.js +603 -378
  48. package/dist/src/Evolu/Public.d.ts +6 -8
  49. package/dist/src/Evolu/Public.d.ts.map +1 -1
  50. package/dist/src/Evolu/Public.js +2 -3
  51. package/dist/src/Evolu/PublicKysely.js +3 -3
  52. package/dist/src/Evolu/Query.d.ts +2 -1
  53. package/dist/src/Evolu/Query.d.ts.map +1 -1
  54. package/dist/src/Evolu/Relay.d.ts +92 -7
  55. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  56. package/dist/src/Evolu/Relay.js +243 -76
  57. package/dist/src/Evolu/Schema.d.ts +129 -73
  58. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  59. package/dist/src/Evolu/Schema.js +169 -89
  60. package/dist/src/Evolu/Storage.d.ts +212 -26
  61. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  62. package/dist/src/Evolu/Storage.js +137 -79
  63. package/dist/src/Evolu/Sync.d.ts +68 -13
  64. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  65. package/dist/src/Evolu/Sync.js +422 -20
  66. package/dist/src/Evolu/Timestamp.d.ts +85 -27
  67. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  68. package/dist/src/Evolu/Timestamp.js +77 -18
  69. package/dist/src/Identicon.d.ts +35 -0
  70. package/dist/src/Identicon.d.ts.map +1 -0
  71. package/dist/src/Identicon.js +143 -0
  72. package/dist/src/Instances.d.ts +34 -0
  73. package/dist/src/Instances.d.ts.map +1 -0
  74. package/dist/src/Instances.js +44 -0
  75. package/dist/src/ManyToManyMap.d.ts +71 -10
  76. package/dist/src/ManyToManyMap.d.ts.map +1 -1
  77. package/dist/src/ManyToManyMap.js +41 -6
  78. package/dist/src/Number.d.ts +4 -3
  79. package/dist/src/Number.d.ts.map +1 -1
  80. package/dist/src/Number.js +5 -4
  81. package/dist/src/Platform.d.ts +20 -0
  82. package/dist/src/Platform.d.ts.map +1 -0
  83. package/dist/src/Platform.js +22 -0
  84. package/dist/src/Random.d.ts +3 -2
  85. package/dist/src/Random.d.ts.map +1 -1
  86. package/dist/src/Resources.d.ts +118 -0
  87. package/dist/src/Resources.d.ts.map +1 -0
  88. package/dist/src/Resources.js +197 -0
  89. package/dist/src/Result.d.ts +184 -52
  90. package/dist/src/Result.d.ts.map +1 -1
  91. package/dist/src/Result.js +30 -241
  92. package/dist/src/Skiplist.js +2 -1
  93. package/dist/src/Sqlite.d.ts +63 -5
  94. package/dist/src/Sqlite.d.ts.map +1 -1
  95. package/dist/src/Sqlite.js +110 -9
  96. package/dist/src/Task.d.ts +586 -0
  97. package/dist/src/Task.d.ts.map +1 -0
  98. package/dist/src/Task.js +469 -0
  99. package/dist/src/Time.d.ts +66 -1
  100. package/dist/src/Time.d.ts.map +1 -1
  101. package/dist/src/Time.js +99 -5
  102. package/dist/src/Type.d.ts +621 -340
  103. package/dist/src/Type.d.ts.map +1 -1
  104. package/dist/src/Type.js +665 -464
  105. package/dist/src/Types.d.ts +1 -75
  106. package/dist/src/Types.d.ts.map +1 -1
  107. package/dist/src/WebSocket.d.ts +5 -2
  108. package/dist/src/WebSocket.d.ts.map +1 -1
  109. package/dist/src/WebSocket.js +12 -18
  110. package/dist/src/Worker.d.ts +39 -11
  111. package/dist/src/Worker.d.ts.map +1 -1
  112. package/dist/src/Worker.js +22 -4
  113. package/dist/src/index.d.ts +7 -2
  114. package/dist/src/index.d.ts.map +1 -1
  115. package/dist/src/index.js +7 -2
  116. package/package.json +14 -13
  117. package/src/Array.ts +76 -11
  118. package/src/Assert.ts +6 -24
  119. package/src/Brand.ts +75 -0
  120. package/src/Buffer.ts +7 -7
  121. package/src/Cache.ts +85 -0
  122. package/src/Callbacks.ts +62 -22
  123. package/src/Console.ts +91 -11
  124. package/src/Crypto.ts +97 -82
  125. package/src/Evolu/Db.ts +514 -1020
  126. package/src/Evolu/Diff.ts +7 -5
  127. package/src/Evolu/Evolu.ts +464 -355
  128. package/src/Evolu/Internal.ts +0 -2
  129. package/src/Evolu/LocalAuth.ts +463 -0
  130. package/src/Evolu/Owner.ts +369 -228
  131. package/src/Evolu/Platform.ts +9 -9
  132. package/src/Evolu/Protocol.ts +859 -676
  133. package/src/Evolu/Public.ts +7 -14
  134. package/src/Evolu/PublicKysely.ts +3 -3
  135. package/src/Evolu/Query.ts +2 -1
  136. package/src/Evolu/Relay.ts +420 -92
  137. package/src/Evolu/Schema.ts +391 -191
  138. package/src/Evolu/Storage.ts +451 -118
  139. package/src/Evolu/Sync.ts +720 -37
  140. package/src/Evolu/Timestamp.ts +88 -35
  141. package/src/Identicon.ts +197 -0
  142. package/src/Instances.ts +90 -0
  143. package/src/ManyToManyMap.ts +124 -24
  144. package/src/Number.ts +6 -10
  145. package/src/Platform.ts +26 -0
  146. package/src/Random.ts +3 -2
  147. package/src/Resources.ts +367 -0
  148. package/src/Result.ts +191 -54
  149. package/src/Skiplist.ts +1 -1
  150. package/src/Sqlite.ts +122 -17
  151. package/src/Task.ts +901 -0
  152. package/src/Time.ts +180 -5
  153. package/src/Type.ts +1083 -727
  154. package/src/Types.ts +1 -77
  155. package/src/WebSocket.ts +27 -25
  156. package/src/Worker.ts +72 -23
  157. package/src/index.ts +7 -2
  158. package/dist/src/Evolu/Config.d.ts +0 -69
  159. package/dist/src/Evolu/Config.d.ts.map +0 -1
  160. package/dist/src/Evolu/Config.js +0 -9
  161. package/dist/src/Evolu/Kysely.d.ts +0 -6
  162. package/dist/src/Evolu/Kysely.d.ts.map +0 -1
  163. package/dist/src/Evolu/Kysely.js +0 -21
  164. package/dist/src/NanoId.d.ts +0 -27
  165. package/dist/src/NanoId.d.ts.map +0 -1
  166. package/dist/src/NanoId.js +0 -6
  167. package/dist/src/Promise.d.ts +0 -180
  168. package/dist/src/Promise.d.ts.map +0 -1
  169. package/dist/src/Promise.js +0 -176
  170. package/src/Evolu/Config.ts +0 -83
  171. package/src/Evolu/Kysely.ts +0 -38
  172. package/src/NanoId.ts +0 -39
  173. package/src/Promise.ts +0 -295
@@ -1,10 +1,196 @@
1
+ import { NonEmptyReadonlyArray } from "../Array.js";
2
+ import { Brand } from "../Brand.js";
3
+ import { RandomDep } from "../Random.js";
4
+ import { Result } from "../Result.js";
5
+ import { SqliteDep, SqliteError } from "../Sqlite.js";
6
+ import { MaybeAsync } from "../Task.js";
7
+ import { NonNegativeInt, PositiveInt } from "../Type.js";
8
+ import { BaseOwnerError, OwnerId, OwnerIdBytes, OwnerWriteKey } from "./Owner.js";
9
+ import { Timestamp, TimestampBytes } from "./Timestamp.js";
10
+ export interface StorageConfig {
11
+ /**
12
+ * Callback called before an attempt to write, to check if an {@link OwnerId}
13
+ * has sufficient quota for the write.
14
+ *
15
+ * The callback receives the {@link OwnerId} and the number of bytes required
16
+ * for the write, and returns a {@link MaybeAsync} boolean: `true` to allow the
17
+ * write, or `false` to deny it due to quota limits.
18
+ *
19
+ * The callback can be synchronous (for SQLite or in-memory checks) or
20
+ * asynchronous (for calling remote APIs).
21
+ *
22
+ * The callback returns a boolean rather than an error type because error
23
+ * handling and logging are the responsibility of the callback
24
+ * implementation.
25
+ *
26
+ * ### Example
27
+ *
28
+ * ```ts
29
+ * // Client
30
+ * // evolu.subscribeError
31
+ *
32
+ * // Relay
33
+ * isOwnerWithinQuota: (ownerId, requiredBytes) => {
34
+ * console.log(ownerId, requiredBytes);
35
+ * // Check error via evolu.subscribeError
36
+ * return true;
37
+ * };
38
+ * ```
39
+ */
40
+ readonly isOwnerWithinQuota: (ownerId: OwnerId, requiredBytes: PositiveInt) => MaybeAsync<boolean>;
41
+ }
1
42
  /**
2
- * Evolu Storage for SQLite
43
+ * Evolu Storage
44
+ *
45
+ * Evolu protocol using Storage is agnostic to storage implementation
46
+ * details—any storage can be plugged in, as long as it implements this
47
+ * interface. Implementations must handle their own errors; return values only
48
+ * indicate overall success or failure.
49
+ *
50
+ * The Storage API is synchronous because SQLite's synchronous API is the
51
+ * fastest way to use SQLite. Synchronous bindings (like better-sqlite3) call
52
+ * SQLite's C API directly with no context switching between the event loop and
53
+ * native code, and no promise microtasks or await overhead.
54
+ *
55
+ * The only exception is {@link Storage#writeMessages}, which is async to allow
56
+ * for async validation logic before writing to storage. The write operation
57
+ * itself remains synchronous.
58
+ */
59
+ export interface Storage {
60
+ readonly getSize: (ownerId: OwnerIdBytes) => NonNegativeInt | null;
61
+ readonly fingerprint: (ownerId: OwnerIdBytes, begin: NonNegativeInt, end: NonNegativeInt) => Fingerprint | null;
62
+ /**
63
+ * Computes fingerprints with their upper bounds in one call.
64
+ *
65
+ * This function can be replaced with many fingerprint/findLowerBound calls,
66
+ * but implementations can leverage it for batching and more efficient
67
+ * fingerprint computation.
68
+ */
69
+ readonly fingerprintRanges: (ownerId: OwnerIdBytes, buckets: ReadonlyArray<NonNegativeInt>, upperBound?: RangeUpperBound) => ReadonlyArray<FingerprintRange> | null;
70
+ readonly findLowerBound: (ownerId: OwnerIdBytes, begin: NonNegativeInt, end: NonNegativeInt, upperBound: RangeUpperBound) => NonNegativeInt | null;
71
+ readonly iterate: (ownerId: OwnerIdBytes, begin: NonNegativeInt, end: NonNegativeInt, callback: (timestamp: TimestampBytes, index: NonNegativeInt) => boolean) => void;
72
+ /**
73
+ * Validates the {@link OwnerWriteKey} for the given {@link Owner}.
74
+ *
75
+ * Returns `true` if the write key is valid, `false` otherwise.
76
+ */
77
+ readonly validateWriteKey: (ownerId: OwnerIdBytes, writeKey: OwnerWriteKey) => boolean;
78
+ /** Sets the {@link OwnerWriteKey} for the given {@link Owner}. */
79
+ readonly setWriteKey: (ownerId: OwnerIdBytes, writeKey: OwnerWriteKey) => boolean;
80
+ /**
81
+ * Write encrypted {@link CrdtMessage}s to storage.
82
+ *
83
+ * Must use a mutex per ownerId to ensure sequential processing and proper
84
+ * protocol logic handling during sync operations.
85
+ *
86
+ * TODO: Use MaybeAsync
87
+ */
88
+ readonly writeMessages: (ownerIdBytes: OwnerIdBytes, messages: NonEmptyReadonlyArray<EncryptedCrdtMessage>) => MaybeAsync<Result<void, StorageWriteError | StorageQuotaError>>;
89
+ /** Read encrypted {@link DbChange}s from storage. */
90
+ readonly readDbChange: (ownerId: OwnerIdBytes, timestamp: TimestampBytes) => EncryptedDbChange | null;
91
+ /**
92
+ * Delete all data for the given {@link Owner}.
93
+ *
94
+ * Returns `true` on success, `false` on failure.
95
+ */
96
+ readonly deleteOwner: (ownerId: OwnerIdBytes) => boolean;
97
+ }
98
+ export interface StorageDep {
99
+ readonly storage: Storage;
100
+ }
101
+ /** Error indicating a serious write failure. */
102
+ export interface StorageWriteError extends BaseOwnerError {
103
+ readonly type: "StorageWriteError";
104
+ }
105
+ /** Error when storage or billing quota is exceeded. */
106
+ export interface StorageQuotaError extends BaseOwnerError {
107
+ readonly type: "StorageQuotaError";
108
+ }
109
+ /**
110
+ * A cryptographic hash used for efficiently comparing collections of
111
+ * {@link TimestampBytes}s.
112
+ *
113
+ * It consists of the first {@link fingerprintSize} bytes of the SHA-256 hash of
114
+ * one or more timestamps.
115
+ */
116
+ export type Fingerprint = Uint8Array & Brand<"Fingerprint">;
117
+ export declare const fingerprintSize: number & Brand<"Int"> & Brand<"NonNegative">;
118
+ /** A fingerprint of an empty range. */
119
+ export declare const zeroFingerprint: Fingerprint;
120
+ export interface BaseRange {
121
+ readonly upperBound: RangeUpperBound;
122
+ }
123
+ /**
124
+ * Union type for Range's upperBound: either a {@link TimestampBytes} or
125
+ * {@link InfiniteUpperBound}.
126
+ */
127
+ export type RangeUpperBound = TimestampBytes | InfiniteUpperBound;
128
+ export declare const InfiniteUpperBound: unique symbol;
129
+ export type InfiniteUpperBound = typeof InfiniteUpperBound;
130
+ export declare const RangeType: {
131
+ readonly Fingerprint: 1;
132
+ readonly Skip: 0;
133
+ readonly Timestamps: 2;
134
+ };
135
+ export type RangeType = (typeof RangeType)[keyof typeof RangeType];
136
+ export interface SkipRange extends BaseRange {
137
+ readonly type: typeof RangeType.Skip;
138
+ }
139
+ export interface FingerprintRange extends BaseRange {
140
+ readonly type: typeof RangeType.Fingerprint;
141
+ readonly fingerprint: Fingerprint;
142
+ }
143
+ export interface TimestampsRange extends BaseRange {
144
+ readonly type: typeof RangeType.Timestamps;
145
+ readonly timestamps: ReadonlyArray<TimestampBytes>;
146
+ }
147
+ export type Range = SkipRange | FingerprintRange | TimestampsRange;
148
+ /** An encrypted {@link CrdtMessage}. */
149
+ export interface EncryptedCrdtMessage {
150
+ readonly timestamp: Timestamp;
151
+ readonly change: EncryptedDbChange;
152
+ }
153
+ /** Encrypted DbChange */
154
+ export type EncryptedDbChange = Uint8Array & Brand<"EncryptedDbChange">;
155
+ /**
156
+ * A CRDT message combining a unique {@link Timestamp} with a {@link DbChange}.
157
+ *
158
+ * Used in Evolu's sync protocol to replicate data changes across devices. Evolu
159
+ * operates as a durable queue, providing exactly-once delivery guarantees for
160
+ * reliable synchronization across application restarts and network failures.
161
+ */
162
+ export interface CrdtMessage {
163
+ readonly timestamp: Timestamp;
164
+ readonly change: DbChange;
165
+ }
166
+ /**
167
+ * A DbChange is a change to a table row. Together with a unique
168
+ * {@link Timestamp}, it forms a {@link CrdtMessage}.
169
+ */
170
+ export declare const DbChange: import("../Type.js").ObjectType<{
171
+ table: import("../Type.js").Type<"String", string, string, import("../Type.js").StringError, string, import("../Type.js").StringError>;
172
+ id: import("../Type.js").BrandType<import("../Type.js").Type<"String", string, string, import("../Type.js").StringError, string, import("../Type.js").StringError>, "Id", import("../Type.js").IdError, import("../Type.js").StringError>;
173
+ values: import("../Type.js").RecordType<"String", string, string, import("../Type.js").StringError, string, import("../Type.js").StringError, import("../Type.js").UnionType<[import("../Type.js").Type<"Null", null, null, import("../Type.js").NullError, null, import("../Type.js").NullError>, import("../Type.js").Type<"String", string, string, import("../Type.js").StringError, string, import("../Type.js").StringError>, import("../Type.js").Type<"Number", number, number, import("../Type.js").NumberError, number, import("../Type.js").NumberError>, import("../Type.js").Type<"Uint8Array", Uint8Array<ArrayBufferLike>, Uint8Array<ArrayBufferLike>, import("../Type.js").Uint8ArrayError, Uint8Array<ArrayBufferLike>, import("../Type.js").Uint8ArrayError>]>>;
174
+ }>;
175
+ export type DbChange = typeof DbChange.Type;
176
+ /**
177
+ * Common interface for both client and relay SQLite storages.
3
178
  *
4
179
  * Evolu uses a Skiplist, which leverages SQLite indexes. The core logic is
5
180
  * implemented in SQL, so it doesn't have to make roundtrips to the DB.
6
181
  *
7
- * The ideal storage for a Relay should use a similar architecture to
182
+ * While the SQL implementation may look sophisticated, it's conceptually simple
183
+ * and LLMs can explain how it works. The Skiplist data structure is well
184
+ * explained in [this Stack Overflow
185
+ * answer](https://stackoverflow.com/questions/61944198/what-is-a-zip-tree-and-how-does-it-work).
186
+ * The logic resembles [Negentropy's C++
187
+ * storage](https://github.com/hoytech/negentropy), except we use a Skiplist to
188
+ * leverage SQLite indexes, which makes the code simpler.
189
+ *
190
+ * Note: A paid review by the SQLite team is planned, as they use the same
191
+ * algorithm for their rsync tool.
192
+ *
193
+ * The ideal storage for a Relay should use an architecture like
8
194
  * [strfry](https://github.com/hoytech/strfry) (a KV storage), but with Skiplist
9
195
  * to ensure that insertion order doesn't matter (local-first apps can often
10
196
  * write in the past.)
@@ -19,31 +205,31 @@
19
205
  * each other, if necessary. One relay should handle hundreds of thousands of
20
206
  * users, and when it goes down, nothing happens, because it will be
21
207
  * synchronized later.
22
- *
23
- * @module
24
208
  */
25
- import { RandomDep } from "../Random.js";
26
- import { Result } from "../Result.js";
27
- import { SqliteDep, SqliteError } from "../Sqlite.js";
28
- import { NonNegativeInt } from "../Type.js";
29
- import { BinaryOwnerId, Storage } from "./Protocol.js";
30
- import { BinaryTimestamp } from "./Timestamp.js";
31
- /** Common interface for both client and relay SQLite storages. */
32
- export interface SqliteStorageBase {
33
- readonly insertTimestamp: (ownerId: BinaryOwnerId, timestamp: BinaryTimestamp) => Result<void, SqliteError>;
34
- readonly getSize: Storage["getSize"];
35
- readonly fingerprint: Storage["fingerprint"];
36
- readonly fingerprintRanges: Storage["fingerprintRanges"];
37
- readonly findLowerBound: Storage["findLowerBound"];
38
- readonly iterate: Storage["iterate"];
39
- }
40
- export interface SqliteStorageBaseDep {
41
- readonly storage: SqliteStorageBase;
42
- }
43
- export type SqliteStorageDeps = SqliteDep & RandomDep;
44
- export interface CreateSqliteStorageBaseOptions {
209
+ export interface BaseSqliteStorage extends Pick<Storage, "getSize" | "fingerprint" | "fingerprintRanges" | "findLowerBound" | "iterate" | "deleteOwner"> {
210
+ /**
211
+ * Inserts a timestamp for an owner into the skiplist-based storage.
212
+ *
213
+ * Must be idempotent - inserting the same timestamp multiple times has no
214
+ * effect after the first insertion. This is crucial for sync reliability as
215
+ * messages may be received and processed multiple times.
216
+ */
217
+ readonly insertTimestamp: (ownerId: OwnerIdBytes, timestamp: TimestampBytes) => Result<void, SqliteError>;
218
+ /**
219
+ * Efficiently checks which timestamps already exist in the database using a
220
+ * single CTE query instead of N individual queries.
221
+ */
222
+ readonly getExistingTimestamps: (ownerIdBytes: OwnerIdBytes, timestampsBytes: NonEmptyReadonlyArray<TimestampBytes>) => Result<ReadonlyArray<TimestampBytes>, SqliteError>;
223
+ }
224
+ export interface BaseSqliteStorageDep {
225
+ readonly storage: BaseSqliteStorage;
226
+ }
227
+ export type SqliteStorageDeps = RandomDep & SqliteDep;
228
+ export interface CreateBaseSqliteStorageConfig extends StorageConfig {
45
229
  onStorageError: (error: SqliteError) => void;
46
230
  }
47
- export declare const createSqliteStorageBase: (deps: SqliteStorageDeps) => (options: CreateSqliteStorageBaseOptions) => Result<SqliteStorageBase, SqliteError>;
48
- export declare const getTimestampByIndex: (deps: SqliteDep) => (ownerId: BinaryOwnerId, index: NonNegativeInt) => Result<BinaryTimestamp, SqliteError>;
231
+ export declare const createBaseSqliteStorage: (deps: SqliteStorageDeps) => (config: CreateBaseSqliteStorageConfig) => BaseSqliteStorage;
232
+ export declare const createBaseSqliteStorageTables: (deps: SqliteDep) => Result<void, SqliteError>;
233
+ export declare const timestampBytesToFingerprint: (timestamp: TimestampBytes) => Fingerprint;
234
+ export declare const getTimestampByIndex: (deps: SqliteDep) => (ownerId: OwnerIdBytes, index: NonNegativeInt) => Result<TimestampBytes, SqliteError>;
49
235
  //# sourceMappingURL=Storage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Storage.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAM,MAAM,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAO,SAAS,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAe,cAAc,EAAe,MAAM,YAAY,CAAC;AAGtE,OAAO,EACL,aAAa,EAQb,OAAO,EAER,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,eAAe,EAAwB,MAAM,gBAAgB,CAAC;AAEvE,kEAAkE;AAClE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,eAAe,EAAE,CACxB,OAAO,EAAE,aAAa,EACtB,SAAS,EAAE,eAAe,KACvB,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IAE/B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;IACrC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;IAC7C,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACzD,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACnD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;CACrC;AAED,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,SAAS,CAAC;AAEtD,MAAM,WAAW,8BAA8B;IAC7C,cAAc,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;CAC9C;AAED,eAAO,MAAM,uBAAuB,GACjC,MAAM,iBAAiB,MAEtB,SAAS,8BAA8B,KACtC,MAAM,CAAC,iBAAiB,EAAE,WAAW,CAyIvC,CAAC;AA09BJ,eAAO,MAAM,mBAAmB,GAC7B,MAAM,SAAS,MAEd,SAAS,aAAa,EACtB,OAAO,cAAc,KACpB,MAAM,CAAC,eAAe,EAAE,WAAW,CA6ErC,CAAC"}
1
+ {"version":3,"file":"Storage.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Storage.ts"],"names":[],"mappings":"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"}
@@ -1,75 +1,38 @@
1
- /**
2
- * Evolu Storage for SQLite
3
- *
4
- * Evolu uses a Skiplist, which leverages SQLite indexes. The core logic is
5
- * implemented in SQL, so it doesn't have to make roundtrips to the DB.
6
- *
7
- * The ideal storage for a Relay should use a similar architecture to
8
- * [strfry](https://github.com/hoytech/strfry) (a KV storage), but with Skiplist
9
- * to ensure that insertion order doesn't matter (local-first apps can often
10
- * write in the past.)
11
- *
12
- * The ideal client implementation should probably use the SQLite extension
13
- * instead of SQL or even a KV storage, when such a thing for browsers/native
14
- * will exist and will be faster than SQLite.
15
- *
16
- * # Scaling
17
- *
18
- * The load can be distributed by deploying multiple relays, synchronized with
19
- * each other, if necessary. One relay should handle hundreds of thousands of
20
- * users, and when it goes down, nothing happens, because it will be
21
- * synchronized later.
22
- *
23
- * @module
24
- */
1
+ import { sha256 } from "@noble/hashes/sha2.js";
25
2
  import { assert } from "../Assert.js";
3
+ import { concatBytes } from "../Buffer.js";
26
4
  import { decrement } from "../Number.js";
27
5
  import { ok } from "../Result.js";
28
- import { sql } from "../Sqlite.js";
29
- import { binaryOwnerIdToOwnerId, binaryTimestampToFingerprint, InfiniteUpperBound, RangeType, zeroFingerprint, } from "./Protocol.js";
30
- import { orderBinaryTimestamp } from "./Timestamp.js";
31
- export const createSqliteStorageBase = (deps) => (options) => {
32
- const createTablesResult = createTables(deps);
33
- if (!createTablesResult.ok)
34
- return createTablesResult;
6
+ import { sql, SqliteValue } from "../Sqlite.js";
7
+ import { Id, NonNegativeInt, object, PositiveInt, record, String, } from "../Type.js";
8
+ import { ownerIdBytesToOwnerId, } from "./Owner.js";
9
+ import { orderTimestampBytes } from "./Timestamp.js";
10
+ export const fingerprintSize = NonNegativeInt.orThrow(12);
11
+ /** A fingerprint of an empty range. */
12
+ export const zeroFingerprint = new Uint8Array(fingerprintSize);
13
+ export const InfiniteUpperBound = Symbol("InfiniteUpperBound");
14
+ export const RangeType = {
15
+ Fingerprint: 1,
16
+ Skip: 0,
17
+ Timestamps: 2,
18
+ };
19
+ /**
20
+ * A DbChange is a change to a table row. Together with a unique
21
+ * {@link Timestamp}, it forms a {@link CrdtMessage}.
22
+ */
23
+ export const DbChange = object({
24
+ table: String,
25
+ id: Id,
26
+ values: record(String, SqliteValue),
27
+ });
28
+ export const createBaseSqliteStorage = (deps) => (config) => {
29
+ // TODO: Use evolu_usage table.
35
30
  const ownerStats = new Map();
36
- return ok({
37
- insertTimestamp: (ownerId, timestamp) => {
38
- const ownerIdString = binaryOwnerIdToOwnerId(ownerId);
39
- const level = randomSkiplistLevel(deps);
40
- let stats = ownerStats.get(ownerIdString);
41
- if (!stats) {
42
- const result = deps.sqlite.exec(sql.prepared `
43
- select min(t) as minT, max(t) as maxT
44
- from evolu_timestamp
45
- where ownerId = ${ownerId};
46
- `);
47
- if (!result.ok)
48
- return result;
49
- stats = {
50
- minT: result.value.rows[0].minT ?? timestamp,
51
- maxT: result.value.rows[0].maxT ?? timestamp,
52
- };
53
- ownerStats.set(ownerIdString, stats);
54
- }
55
- let strategy;
56
- if (orderBinaryTimestamp(timestamp, stats.maxT) === 1) {
57
- strategy = "append";
58
- stats.maxT = timestamp;
59
- }
60
- else if (orderBinaryTimestamp(timestamp, stats.minT) === -1) {
61
- strategy = "prepend";
62
- stats.minT = timestamp;
63
- }
64
- else {
65
- strategy = "insert";
66
- }
67
- return insertTimestamp(deps)(ownerId, timestamp, level, strategy);
68
- },
31
+ return {
69
32
  getSize: (ownerId) => {
70
33
  const size = getSize(deps)(ownerId);
71
34
  if (!size.ok) {
72
- options.onStorageError(size.error);
35
+ config.onStorageError(size.error);
73
36
  return null;
74
37
  }
75
38
  return size.value;
@@ -78,7 +41,7 @@ export const createSqliteStorageBase = (deps) => (options) => {
78
41
  assertBeginEnd(begin, end);
79
42
  const result = fingerprint(deps)(ownerId, begin, end);
80
43
  if (!result.ok) {
81
- options.onStorageError(result.error);
44
+ config.onStorageError(result.error);
82
45
  return null;
83
46
  }
84
47
  return result.value;
@@ -86,7 +49,7 @@ export const createSqliteStorageBase = (deps) => (options) => {
86
49
  fingerprintRanges: (ownerId, buckets, upperBound) => {
87
50
  const ranges = fingerprintRanges(deps)(ownerId, buckets, upperBound);
88
51
  if (!ranges.ok) {
89
- options.onStorageError(ranges.error);
52
+ config.onStorageError(ranges.error);
90
53
  return null;
91
54
  }
92
55
  return ranges.value;
@@ -94,7 +57,7 @@ export const createSqliteStorageBase = (deps) => (options) => {
94
57
  findLowerBound: (ownerId, begin, end, upperBound) => {
95
58
  const lowerBound = findLowerBound(deps)(ownerId, begin, end, upperBound);
96
59
  if (!lowerBound.ok) {
97
- options.onStorageError(lowerBound.error);
60
+ config.onStorageError(lowerBound.error);
98
61
  return null;
99
62
  }
100
63
  return lowerBound.value;
@@ -107,7 +70,7 @@ export const createSqliteStorageBase = (deps) => (options) => {
107
70
  // This is much faster than SQL limit with offset.
108
71
  const first = getTimestampByIndex(deps)(ownerId, begin);
109
72
  if (!first.ok) {
110
- options.onStorageError(first.error);
73
+ config.onStorageError(first.error);
111
74
  return;
112
75
  }
113
76
  if (!callback(first.value, begin))
@@ -132,21 +95,88 @@ export const createSqliteStorageBase = (deps) => (options) => {
132
95
  limit ${length - 1};
133
96
  `);
134
97
  if (!result.ok) {
135
- options.onStorageError(result.error);
98
+ config.onStorageError(result.error);
136
99
  return;
137
100
  }
138
101
  for (let i = 0; i < result.value.rows.length; i++) {
139
- const index = (begin + 1 + i);
102
+ const index = NonNegativeInt.orThrow(begin + 1 + i);
140
103
  if (!callback(result.value.rows[i].t, index))
141
104
  return;
142
105
  }
143
106
  },
144
- });
107
+ deleteOwner: (ownerId) => {
108
+ const result = deps.sqlite.exec(sql `
109
+ delete from evolu_timestamp where ownerId = ${ownerId};
110
+ `);
111
+ if (!result.ok) {
112
+ config.onStorageError(result.error);
113
+ return false;
114
+ }
115
+ return true;
116
+ },
117
+ insertTimestamp: (ownerId, timestamp) => {
118
+ const ownerIdString = ownerIdBytesToOwnerId(ownerId);
119
+ const level = randomSkiplistLevel(deps);
120
+ let stats = ownerStats.get(ownerIdString);
121
+ if (!stats) {
122
+ const result = deps.sqlite.exec(sql.prepared `
123
+ select min(t) as minT, max(t) as maxT
124
+ from evolu_timestamp
125
+ where ownerId = ${ownerId};
126
+ `);
127
+ if (!result.ok)
128
+ return result;
129
+ stats = {
130
+ minT: result.value.rows[0].minT ?? timestamp,
131
+ maxT: result.value.rows[0].maxT ?? timestamp,
132
+ };
133
+ ownerStats.set(ownerIdString, stats);
134
+ }
135
+ let strategy;
136
+ if (orderTimestampBytes(timestamp, stats.maxT) === 1) {
137
+ strategy = "append";
138
+ stats.maxT = timestamp;
139
+ }
140
+ else if (orderTimestampBytes(timestamp, stats.minT) === -1) {
141
+ strategy = "prepend";
142
+ stats.minT = timestamp;
143
+ }
144
+ else {
145
+ strategy = "insert";
146
+ }
147
+ return insertTimestamp(deps)(ownerId, timestamp, level, strategy);
148
+ },
149
+ getExistingTimestamps: (ownerIdBytes, timestampsBytes) => {
150
+ const concatenatedTimestamps = concatBytes(...timestampsBytes);
151
+ const result = deps.sqlite.exec(sql `
152
+ with recursive
153
+ split_timestamps(timestampBytes, pos) as (
154
+ select
155
+ substr(${concatenatedTimestamps}, 1, 16),
156
+ 17 as pos
157
+ union all
158
+ select
159
+ substr(${concatenatedTimestamps}, pos, 16),
160
+ pos + 16
161
+ from split_timestamps
162
+ where pos <= length(${concatenatedTimestamps})
163
+ )
164
+ select s.timestampBytes
165
+ from
166
+ split_timestamps s
167
+ join evolu_timestamp t
168
+ on t.ownerId = ${ownerIdBytes} and s.timestampBytes = t.t;
169
+ `);
170
+ if (!result.ok)
171
+ return result;
172
+ return ok(result.value.rows.map((row) => row.timestampBytes));
173
+ },
174
+ };
145
175
  };
146
176
  const assertBeginEnd = (begin, end) => {
147
177
  assert(begin <= end, "invalid begin or end");
148
178
  };
149
- const createTables = (deps) => {
179
+ export const createBaseSqliteStorageTables = (deps) => {
150
180
  for (const query of [
151
181
  /**
152
182
  * Creates the `evolu_timestamp` table for storing timestamps of multiple
@@ -159,7 +189,7 @@ const createTables = (deps) => {
159
189
  *
160
190
  * Columns:
161
191
  *
162
- * - `t` – globally unique binary timestamp
192
+ * - `t` – TimestampBytes
163
193
  * - `h1`/`h2` – 12-byte fingerprint split into two integers for fast XOR
164
194
  * - `c` – incremental count
165
195
  * - `l` – Skiplist level (1 to 32)
@@ -171,7 +201,7 @@ const createTables = (deps) => {
171
201
  * enough even without it.
172
202
  */
173
203
  sql `
174
- create table if not exists evolu_timestamp (
204
+ create table evolu_timestamp (
175
205
  "ownerId" blob not null,
176
206
  "t" blob not null,
177
207
  "h1" integer,
@@ -183,7 +213,7 @@ const createTables = (deps) => {
183
213
  strict;
184
214
  `,
185
215
  sql `
186
- create index if not exists evolu_timestamp_index on evolu_timestamp (
216
+ create index evolu_timestamp_index on evolu_timestamp (
187
217
  "ownerId",
188
218
  "l",
189
219
  "t",
@@ -191,6 +221,30 @@ const createTables = (deps) => {
191
221
  "h2",
192
222
  "c"
193
223
  );
224
+ `,
225
+ /**
226
+ * Creates the `evolu_usage` table for tracking data consumption per owner.
227
+ *
228
+ * Columns:
229
+ *
230
+ * - `ownerId` – OwnerIdBytes (primary key)
231
+ * - `storedBytes` – total bytes stored in database
232
+ * - `receivedBytes` – TODO: Decide how to use
233
+ * - `sentBytes` – TODO: Decide how to use
234
+ * - `firstTimestamp` – TODO: Decide how to use (nullable)
235
+ * - `lastTimestamp` – TODO: Decide how to use (nullable)
236
+ */
237
+ sql `
238
+ create table evolu_usage (
239
+ "ownerId" blob primary key,
240
+ "storedBytes" integer not null
241
+ -- TODO: Decide how to use receivedBytes, sentBytes, firstTimestamp, lastTimestamp
242
+ -- "receivedBytes" integer not null,
243
+ -- "sentBytes" integer not null,
244
+ -- "firstTimestamp" blob,
245
+ -- "lastTimestamp" blob
246
+ )
247
+ strict;
194
248
  `,
195
249
  ]) {
196
250
  const result = deps.sqlite.exec(query);
@@ -208,7 +262,7 @@ const createTables = (deps) => {
208
262
  // they will prepend. They are always sorted in ascending order by the
209
263
  // Protocol.
210
264
  const insertTimestamp = (deps) => (ownerId, timestamp, level, strategy) => {
211
- const [h1, h2] = fingerprintToSqliteFingerprint(binaryTimestampToFingerprint(timestamp));
265
+ const [h1, h2] = fingerprintToSqliteFingerprint(timestampBytesToFingerprint(timestamp));
212
266
  let queries = [];
213
267
  switch (strategy) {
214
268
  case "append":
@@ -717,6 +771,10 @@ const insertTimestamp = (deps) => (ownerId, timestamp, level, strategy) => {
717
771
  }
718
772
  return ok();
719
773
  };
774
+ export const timestampBytesToFingerprint = (timestamp) => {
775
+ const hash = sha256(timestamp).slice(0, fingerprintSize);
776
+ return hash;
777
+ };
720
778
  /**
721
779
  * Generates a random skiplist level in the range [1, skiplistMaxLevel].
722
780
  * Probabilistic approach avoids the need for explicit tree balancing.
@@ -727,7 +785,7 @@ const randomSkiplistLevel = (deps) => {
727
785
  level < skiplistMaxLevel) {
728
786
  level += 1;
729
787
  }
730
- return level;
788
+ return PositiveInt.orThrow(level);
731
789
  };
732
790
  /**
733
791
  * Probability used for generating Skiplist levels.
@@ -830,7 +888,7 @@ const findLowerBound = (deps) => (ownerId, begin, end, upperBound) => {
830
888
  if (!count.ok)
831
889
  return count;
832
890
  // `decrement` converts a count to an index.
833
- return ok(decrement(count.value));
891
+ return ok(NonNegativeInt.orThrow(decrement(count.value)));
834
892
  };
835
893
  const getTimestampCount = (deps) => (ownerId, timestamp) => {
836
894
  const result = deps.sqlite.exec(sql.prepared `