@evolu/common 6.0.1-preview.2 → 6.0.1-preview.21

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 (154) hide show
  1. package/dist/src/Assert.d.ts +6 -3
  2. package/dist/src/Assert.d.ts.map +1 -1
  3. package/dist/src/Assert.js +7 -4
  4. package/dist/src/Brand.d.ts +75 -0
  5. package/dist/src/Brand.d.ts.map +1 -0
  6. package/dist/src/Brand.js +1 -0
  7. package/dist/src/Buffer.d.ts +1 -1
  8. package/dist/src/Buffer.d.ts.map +1 -1
  9. package/dist/src/Buffer.js +1 -1
  10. package/dist/src/CallbackRegistry.d.ts +53 -0
  11. package/dist/src/CallbackRegistry.d.ts.map +1 -0
  12. package/dist/src/CallbackRegistry.js +25 -0
  13. package/dist/src/Console.d.ts +31 -6
  14. package/dist/src/Console.d.ts.map +1 -1
  15. package/dist/src/Console.js +72 -9
  16. package/dist/src/Crypto.d.ts +61 -34
  17. package/dist/src/Crypto.d.ts.map +1 -1
  18. package/dist/src/Crypto.js +29 -42
  19. package/dist/src/Evolu/Db.d.ts +159 -66
  20. package/dist/src/Evolu/Db.d.ts.map +1 -1
  21. package/dist/src/Evolu/Db.js +300 -705
  22. package/dist/src/Evolu/Diff.d.ts +3 -3
  23. package/dist/src/Evolu/Diff.d.ts.map +1 -1
  24. package/dist/src/Evolu/Diff.js +7 -5
  25. package/dist/src/Evolu/Evolu.d.ts +200 -115
  26. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  27. package/dist/src/Evolu/Evolu.js +287 -157
  28. package/dist/src/Evolu/Internal.d.ts +0 -2
  29. package/dist/src/Evolu/Internal.d.ts.map +1 -1
  30. package/dist/src/Evolu/Internal.js +0 -2
  31. package/dist/src/Evolu/LocalAuth.d.ts +144 -0
  32. package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
  33. package/dist/src/Evolu/LocalAuth.js +171 -0
  34. package/dist/src/Evolu/Owner.d.ts +209 -124
  35. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  36. package/dist/src/Evolu/Owner.js +118 -104
  37. package/dist/src/Evolu/Platform.d.ts +9 -7
  38. package/dist/src/Evolu/Platform.d.ts.map +1 -1
  39. package/dist/src/Evolu/Protocol.d.ts +256 -211
  40. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  41. package/dist/src/Evolu/Protocol.js +558 -359
  42. package/dist/src/Evolu/Public.d.ts +6 -8
  43. package/dist/src/Evolu/Public.d.ts.map +1 -1
  44. package/dist/src/Evolu/Public.js +2 -3
  45. package/dist/src/Evolu/PublicKysely.js +3 -3
  46. package/dist/src/Evolu/Query.d.ts +2 -1
  47. package/dist/src/Evolu/Query.d.ts.map +1 -1
  48. package/dist/src/Evolu/Relay.d.ts +72 -4
  49. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  50. package/dist/src/Evolu/Relay.js +124 -13
  51. package/dist/src/Evolu/Schema.d.ts +129 -73
  52. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  53. package/dist/src/Evolu/Schema.js +169 -89
  54. package/dist/src/Evolu/Storage.d.ts +159 -14
  55. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  56. package/dist/src/Evolu/Storage.js +42 -32
  57. package/dist/src/Evolu/Sync.d.ts +68 -13
  58. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  59. package/dist/src/Evolu/Sync.js +414 -20
  60. package/dist/src/Evolu/Timestamp.d.ts +35 -27
  61. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  62. package/dist/src/Evolu/Timestamp.js +27 -18
  63. package/dist/src/ManyToManyMap.d.ts +74 -10
  64. package/dist/src/ManyToManyMap.d.ts.map +1 -1
  65. package/dist/src/ManyToManyMap.js +41 -6
  66. package/dist/src/Number.d.ts +2 -1
  67. package/dist/src/Number.d.ts.map +1 -1
  68. package/dist/src/Random.d.ts +3 -2
  69. package/dist/src/Random.d.ts.map +1 -1
  70. package/dist/src/RefCountedResourceManager.d.ts +119 -0
  71. package/dist/src/RefCountedResourceManager.d.ts.map +1 -0
  72. package/dist/src/RefCountedResourceManager.js +197 -0
  73. package/dist/src/Result.d.ts +176 -51
  74. package/dist/src/Result.d.ts.map +1 -1
  75. package/dist/src/Result.js +30 -241
  76. package/dist/src/Sqlite.d.ts +21 -4
  77. package/dist/src/Sqlite.d.ts.map +1 -1
  78. package/dist/src/Sqlite.js +50 -8
  79. package/dist/src/Task.d.ts +511 -0
  80. package/dist/src/Task.d.ts.map +1 -0
  81. package/dist/src/Task.js +410 -0
  82. package/dist/src/Time.d.ts +59 -0
  83. package/dist/src/Time.d.ts.map +1 -1
  84. package/dist/src/Time.js +87 -4
  85. package/dist/src/Type.d.ts +474 -327
  86. package/dist/src/Type.d.ts.map +1 -1
  87. package/dist/src/Type.js +502 -436
  88. package/dist/src/Types.d.ts +0 -74
  89. package/dist/src/Types.d.ts.map +1 -1
  90. package/dist/src/WebSocket.d.ts +5 -2
  91. package/dist/src/WebSocket.d.ts.map +1 -1
  92. package/dist/src/WebSocket.js +12 -13
  93. package/dist/src/Worker.d.ts +39 -11
  94. package/dist/src/Worker.d.ts.map +1 -1
  95. package/dist/src/Worker.js +22 -4
  96. package/dist/src/index.d.ts +3 -3
  97. package/dist/src/index.d.ts.map +1 -1
  98. package/dist/src/index.js +3 -3
  99. package/package.json +14 -13
  100. package/src/Assert.ts +8 -7
  101. package/src/Brand.ts +75 -0
  102. package/src/Buffer.ts +1 -1
  103. package/src/CallbackRegistry.ts +84 -0
  104. package/src/Console.ts +91 -11
  105. package/src/Crypto.ts +94 -79
  106. package/src/Evolu/Db.ts +528 -1027
  107. package/src/Evolu/Diff.ts +7 -5
  108. package/src/Evolu/Evolu.ts +682 -329
  109. package/src/Evolu/Internal.ts +0 -2
  110. package/src/Evolu/LocalAuth.ts +422 -0
  111. package/src/Evolu/Owner.ts +282 -227
  112. package/src/Evolu/Platform.ts +9 -9
  113. package/src/Evolu/Protocol.ts +788 -640
  114. package/src/Evolu/Public.ts +7 -14
  115. package/src/Evolu/PublicKysely.ts +3 -3
  116. package/src/Evolu/Query.ts +2 -1
  117. package/src/Evolu/Relay.ts +238 -18
  118. package/src/Evolu/Schema.ts +391 -191
  119. package/src/Evolu/Storage.ts +275 -55
  120. package/src/Evolu/Sync.ts +708 -37
  121. package/src/Evolu/Timestamp.ts +38 -35
  122. package/src/ManyToManyMap.ts +127 -24
  123. package/src/Number.ts +2 -6
  124. package/src/Random.ts +3 -2
  125. package/src/RefCountedResourceManager.ts +368 -0
  126. package/src/Result.ts +183 -53
  127. package/src/Sqlite.ts +59 -23
  128. package/src/Task.ts +779 -0
  129. package/src/Time.ts +168 -4
  130. package/src/Type.ts +708 -660
  131. package/src/Types.ts +0 -76
  132. package/src/WebSocket.ts +23 -17
  133. package/src/Worker.ts +72 -23
  134. package/src/index.ts +3 -3
  135. package/dist/src/Callbacks.d.ts +0 -20
  136. package/dist/src/Callbacks.d.ts.map +0 -1
  137. package/dist/src/Callbacks.js +0 -18
  138. package/dist/src/Evolu/Config.d.ts +0 -69
  139. package/dist/src/Evolu/Config.d.ts.map +0 -1
  140. package/dist/src/Evolu/Config.js +0 -9
  141. package/dist/src/Evolu/Kysely.d.ts +0 -6
  142. package/dist/src/Evolu/Kysely.d.ts.map +0 -1
  143. package/dist/src/Evolu/Kysely.js +0 -21
  144. package/dist/src/NanoId.d.ts +0 -27
  145. package/dist/src/NanoId.d.ts.map +0 -1
  146. package/dist/src/NanoId.js +0 -6
  147. package/dist/src/Promise.d.ts +0 -180
  148. package/dist/src/Promise.d.ts.map +0 -1
  149. package/dist/src/Promise.js +0 -176
  150. package/src/Callbacks.ts +0 -43
  151. package/src/Evolu/Config.ts +0 -83
  152. package/src/Evolu/Kysely.ts +0 -38
  153. package/src/NanoId.ts +0 -39
  154. package/src/Promise.ts +0 -295
@@ -1,10 +1,230 @@
1
+ import { sha256 } from "@noble/hashes/sha2.js";
2
+ import { NonEmptyReadonlyArray } from "../Array.js";
3
+ import { assert } from "../Assert.js";
4
+ import { Brand } from "../Brand.js";
5
+ import { decrement } from "../Number.js";
6
+ import { RandomDep } from "../Random.js";
7
+ import { ok, Result } from "../Result.js";
8
+ import { sql, SqliteDep, SqliteError, SqliteValue } from "../Sqlite.js";
9
+ import {
10
+ Id,
11
+ Int64String,
12
+ NonNegativeInt,
13
+ object,
14
+ PositiveInt,
15
+ record,
16
+ String,
17
+ } from "../Type.js";
18
+ import {
19
+ Owner,
20
+ OwnerId,
21
+ OwnerIdBytes,
22
+ ownerIdBytesToOwnerId,
23
+ OwnerWriteKey,
24
+ } from "./Owner.js";
25
+ import { orderTimestampBytes, Timestamp, TimestampBytes } from "./Timestamp.js";
26
+
1
27
  /**
2
- * Evolu Storage for SQLite
28
+ * Evolu Storage
29
+ *
30
+ * The protocol using Storage is agnostic to storage implementation details—any
31
+ * storage can be plugged in, as long as it implements this interface.
32
+ * Implementations must handle their own errors; return values only indicate
33
+ * overall success or failure.
34
+ *
35
+ * The Storage API is synchronous because SQLite's synchronous API is the
36
+ * fastest way to use SQLite. Synchronous bindings (like better-sqlite3) call
37
+ * SQLite's C API directly with no context switching between the event loop and
38
+ * native code, and no promise microtasks or await overhead.
39
+ *
40
+ * The only exception is {@link Storage#writeMessages}, which is async to allow
41
+ * for async validation logic before writing to storage. The write operation
42
+ * itself remains synchronous.
43
+ */
44
+ export interface Storage {
45
+ readonly getSize: (ownerId: OwnerIdBytes) => NonNegativeInt | null;
46
+
47
+ readonly fingerprint: (
48
+ ownerId: OwnerIdBytes,
49
+ begin: NonNegativeInt,
50
+ end: NonNegativeInt,
51
+ ) => Fingerprint | null;
52
+
53
+ /**
54
+ * Computes fingerprints with their upper bounds in one call.
55
+ *
56
+ * This function can be replaced with many fingerprint/findLowerBound calls,
57
+ * but implementations can leverage it for batching and more efficient
58
+ * fingerprint computation.
59
+ */
60
+ readonly fingerprintRanges: (
61
+ ownerId: OwnerIdBytes,
62
+ buckets: ReadonlyArray<NonNegativeInt>,
63
+ upperBound?: RangeUpperBound,
64
+ ) => ReadonlyArray<FingerprintRange> | null;
65
+
66
+ readonly findLowerBound: (
67
+ ownerId: OwnerIdBytes,
68
+ begin: NonNegativeInt,
69
+ end: NonNegativeInt,
70
+ upperBound: RangeUpperBound,
71
+ ) => NonNegativeInt | null;
72
+
73
+ readonly iterate: (
74
+ ownerId: OwnerIdBytes,
75
+ begin: NonNegativeInt,
76
+ end: NonNegativeInt,
77
+ callback: (timestamp: TimestampBytes, index: NonNegativeInt) => boolean,
78
+ ) => void;
79
+
80
+ /**
81
+ * Validates the {@link OwnerWriteKey} for the given {@link Owner}.
82
+ *
83
+ * Returns `true` if the write key is valid, `false` otherwise.
84
+ */
85
+ readonly validateWriteKey: (
86
+ ownerId: OwnerIdBytes,
87
+ writeKey: OwnerWriteKey,
88
+ ) => boolean;
89
+
90
+ /** Sets the {@link OwnerWriteKey} for the given {@link Owner}. */
91
+ readonly setWriteKey: (
92
+ ownerId: OwnerIdBytes,
93
+ writeKey: OwnerWriteKey,
94
+ ) => boolean;
95
+
96
+ /**
97
+ * Write encrypted {@link CrdtMessage}s to storage.
98
+ *
99
+ * Must use a mutex (per ownerId on Relay) to ensure sequential processing and
100
+ * proper protocol logic handling during sync operations.
101
+ *
102
+ * Returns `true` on success, `false` on failure.
103
+ */
104
+ readonly writeMessages: (
105
+ ownerId: OwnerIdBytes,
106
+ messages: NonEmptyReadonlyArray<EncryptedCrdtMessage>,
107
+ ) => Promise<boolean>;
108
+
109
+ /** Read encrypted {@link DbChange}s from storage. */
110
+ readonly readDbChange: (
111
+ ownerId: OwnerIdBytes,
112
+ timestamp: TimestampBytes,
113
+ ) => EncryptedDbChange | null;
114
+
115
+ /**
116
+ * Delete all data for the given {@link Owner}.
117
+ *
118
+ * Returns `true` on success, `false` on failure.
119
+ */
120
+ readonly deleteOwner: (ownerId: OwnerIdBytes) => boolean;
121
+ }
122
+
123
+ export interface StorageDep {
124
+ readonly storage: Storage;
125
+ }
126
+
127
+ /**
128
+ * A cryptographic hash used for efficiently comparing collections of
129
+ * {@link TimestampBytes}s.
130
+ *
131
+ * It consists of the first {@link fingerprintSize} bytes of the SHA-256 hash of
132
+ * one or more timestamps.
133
+ */
134
+ export type Fingerprint = Uint8Array & Brand<"Fingerprint">;
135
+
136
+ export const fingerprintSize = 12 as NonNegativeInt;
137
+
138
+ /** A fingerprint of an empty range. */
139
+ export const zeroFingerprint = new Uint8Array(fingerprintSize) as Fingerprint;
140
+
141
+ export interface BaseRange {
142
+ readonly upperBound: RangeUpperBound;
143
+ }
144
+
145
+ /**
146
+ * Union type for Range's upperBound: either a {@link TimestampBytes} or
147
+ * {@link InfiniteUpperBound}.
148
+ */
149
+ export type RangeUpperBound = TimestampBytes | InfiniteUpperBound;
150
+
151
+ export const InfiniteUpperBound = Symbol("InfiniteUpperBound");
152
+ export type InfiniteUpperBound = typeof InfiniteUpperBound;
153
+
154
+ export const RangeType = {
155
+ Fingerprint: 1,
156
+ Skip: 0,
157
+ Timestamps: 2,
158
+ } as const;
159
+
160
+ export type RangeType = (typeof RangeType)[keyof typeof RangeType];
161
+
162
+ export interface SkipRange extends BaseRange {
163
+ readonly type: typeof RangeType.Skip;
164
+ }
165
+
166
+ export interface FingerprintRange extends BaseRange {
167
+ readonly type: typeof RangeType.Fingerprint;
168
+ readonly fingerprint: Fingerprint;
169
+ }
170
+
171
+ export interface TimestampsRange extends BaseRange {
172
+ readonly type: typeof RangeType.Timestamps;
173
+ readonly timestamps: ReadonlyArray<TimestampBytes>;
174
+ }
175
+
176
+ export type Range = SkipRange | FingerprintRange | TimestampsRange;
177
+
178
+ /** An encrypted {@link CrdtMessage}. */
179
+ export interface EncryptedCrdtMessage {
180
+ readonly timestamp: Timestamp;
181
+ readonly change: EncryptedDbChange;
182
+ }
183
+
184
+ /** Encrypted DbChange */
185
+ export type EncryptedDbChange = Uint8Array & Brand<"EncryptedDbChange">;
186
+
187
+ /**
188
+ * A CRDT message combining a unique {@link Timestamp} with a {@link DbChange}.
189
+ *
190
+ * Used in Evolu's sync protocol to replicate data changes across devices. Evolu
191
+ * operates as a durable queue, providing exactly-once delivery guarantees for
192
+ * reliable synchronization across application restarts and network failures.
193
+ */
194
+ export interface CrdtMessage {
195
+ readonly timestamp: Timestamp;
196
+ readonly change: DbChange;
197
+ }
198
+
199
+ /**
200
+ * A DbChange is a change to a table row. Together with a unique
201
+ * {@link Timestamp}, it forms a {@link CrdtMessage}.
202
+ */
203
+ export const DbChange = object({
204
+ table: String,
205
+ id: Id,
206
+ values: record(String, SqliteValue),
207
+ });
208
+ export type DbChange = typeof DbChange.Type;
209
+
210
+ /**
211
+ * Common interface for both client and relay SQLite storages.
3
212
  *
4
213
  * Evolu uses a Skiplist, which leverages SQLite indexes. The core logic is
5
214
  * implemented in SQL, so it doesn't have to make roundtrips to the DB.
6
215
  *
7
- * The ideal storage for a Relay should use a similar architecture to
216
+ * While the SQL implementation may look sophisticated, it's conceptually simple
217
+ * and LLMs can explain how it works. The Skiplist data structure is well
218
+ * explained in [this Stack Overflow
219
+ * answer](https://stackoverflow.com/questions/61944198/what-is-a-zip-tree-and-how-does-it-work).
220
+ * The logic resembles [Negentropy's C++
221
+ * storage](https://github.com/hoytech/negentropy), except we use a Skiplist to
222
+ * leverage SQLite indexes, which makes the code simpler.
223
+ *
224
+ * Note: A paid review by the SQLite team is planned, as they use the same
225
+ * algorithm for their rsync tool.
226
+ *
227
+ * The ideal storage for a Relay should use an architecture like
8
228
  * [strfry](https://github.com/hoytech/strfry) (a KV storage), but with Skiplist
9
229
  * to ensure that insertion order doesn't matter (local-first apps can often
10
230
  * write in the past.)
@@ -19,37 +239,18 @@
19
239
  * each other, if necessary. One relay should handle hundreds of thousands of
20
240
  * users, and when it goes down, nothing happens, because it will be
21
241
  * synchronized later.
22
- *
23
- * @module
24
242
  */
25
-
26
- import { assert } from "../Assert.js";
27
- import { decrement } from "../Number.js";
28
- import { RandomDep } from "../Random.js";
29
- import { ok, Result } from "../Result.js";
30
- import { sql, SqliteDep, SqliteError } from "../Sqlite.js";
31
- import { Int64String, NonNegativeInt, PositiveInt } from "../Type.js";
32
- import { Brand } from "../Types.js";
33
- import { OwnerId } from "./Owner.js";
34
- import {
35
- BinaryOwnerId,
36
- binaryOwnerIdToOwnerId,
37
- binaryTimestampToFingerprint,
38
- Fingerprint,
39
- FingerprintRange,
40
- InfiniteUpperBound,
41
- RangeType,
42
- RangeUpperBound,
43
- Storage,
44
- zeroFingerprint,
45
- } from "./Protocol.js";
46
- import { BinaryTimestamp, orderBinaryTimestamp } from "./Timestamp.js";
47
-
48
- /** Common interface for both client and relay SQLite storages. */
49
243
  export interface SqliteStorageBase {
244
+ /**
245
+ * Inserts a timestamp for an owner into the skiplist-based storage.
246
+ *
247
+ * Must be idempotent - inserting the same timestamp multiple times has no
248
+ * effect after the first insertion. This is crucial for sync reliability as
249
+ * messages may be received and processed multiple times.
250
+ */
50
251
  readonly insertTimestamp: (
51
- ownerId: BinaryOwnerId,
52
- timestamp: BinaryTimestamp,
252
+ ownerId: OwnerIdBytes,
253
+ timestamp: TimestampBytes,
53
254
  ) => Result<void, SqliteError>;
54
255
 
55
256
  readonly getSize: Storage["getSize"];
@@ -57,13 +258,14 @@ export interface SqliteStorageBase {
57
258
  readonly fingerprintRanges: Storage["fingerprintRanges"];
58
259
  readonly findLowerBound: Storage["findLowerBound"];
59
260
  readonly iterate: Storage["iterate"];
261
+ readonly deleteOwner: Storage["deleteOwner"];
60
262
  }
61
263
 
62
264
  export interface SqliteStorageBaseDep {
63
265
  readonly storage: SqliteStorageBase;
64
266
  }
65
267
 
66
- export type SqliteStorageDeps = SqliteDep & RandomDep;
268
+ export type SqliteStorageDeps = RandomDep & SqliteDep;
67
269
 
68
270
  export interface CreateSqliteStorageBaseOptions {
69
271
  onStorageError: (error: SqliteError) => void;
@@ -80,22 +282,22 @@ export const createSqliteStorageBase =
80
282
  const ownerStats = new Map<
81
283
  OwnerId,
82
284
  {
83
- minT: BinaryTimestamp;
84
- maxT: BinaryTimestamp;
285
+ minT: TimestampBytes;
286
+ maxT: TimestampBytes;
85
287
  }
86
288
  >();
87
289
 
88
290
  return ok({
89
- insertTimestamp: (ownerId: BinaryOwnerId, timestamp: BinaryTimestamp) => {
90
- const ownerIdString = binaryOwnerIdToOwnerId(ownerId);
291
+ insertTimestamp: (ownerId: OwnerIdBytes, timestamp: TimestampBytes) => {
292
+ const ownerIdString = ownerIdBytesToOwnerId(ownerId);
91
293
  const level = randomSkiplistLevel(deps);
92
294
 
93
295
  let stats = ownerStats.get(ownerIdString);
94
296
 
95
297
  if (!stats) {
96
298
  const result = deps.sqlite.exec<{
97
- maxT: BinaryTimestamp | null;
98
- minT: BinaryTimestamp | null;
299
+ maxT: TimestampBytes | null;
300
+ minT: TimestampBytes | null;
99
301
  }>(sql.prepared`
100
302
  select min(t) as minT, max(t) as maxT
101
303
  from evolu_timestamp
@@ -112,10 +314,10 @@ export const createSqliteStorageBase =
112
314
 
113
315
  let strategy: InsertTimestampStrategy;
114
316
 
115
- if (orderBinaryTimestamp(timestamp, stats.maxT) === 1) {
317
+ if (orderTimestampBytes(timestamp, stats.maxT) === 1) {
116
318
  strategy = "append";
117
319
  stats.maxT = timestamp;
118
- } else if (orderBinaryTimestamp(timestamp, stats.minT) === -1) {
320
+ } else if (orderTimestampBytes(timestamp, stats.minT) === -1) {
119
321
  strategy = "prepend";
120
322
  stats.minT = timestamp;
121
323
  } else {
@@ -192,7 +394,7 @@ export const createSqliteStorageBase =
192
394
  * implementing chunking, be sure to run performance tests (including
193
395
  * fetching one by one).
194
396
  */
195
- const result = deps.sqlite.exec<{ t: BinaryTimestamp }>(sql`
397
+ const result = deps.sqlite.exec<{ t: TimestampBytes }>(sql`
196
398
  select t
197
399
  from evolu_timestamp
198
400
  where ownerId = ${ownerId} and t > ${first.value}
@@ -209,6 +411,17 @@ export const createSqliteStorageBase =
209
411
  if (!callback(result.value.rows[i].t, index)) return;
210
412
  }
211
413
  },
414
+
415
+ deleteOwner: (ownerId) => {
416
+ const result = deps.sqlite.exec(sql`
417
+ delete from evolu_timestamp where ownerId = ${ownerId};
418
+ `);
419
+ if (!result.ok) {
420
+ options.onStorageError(result.error);
421
+ return false;
422
+ }
423
+ return true;
424
+ },
212
425
  });
213
426
  };
214
427
 
@@ -229,7 +442,7 @@ const createTables = (deps: SqliteDep): Result<void, SqliteError> => {
229
442
  *
230
443
  * Columns:
231
444
  *
232
- * - `t` – globally unique binary timestamp
445
+ * - `t` – TimestampBytes
233
446
  * - `h1`/`h2` – 12-byte fingerprint split into two integers for fast XOR
234
447
  * - `c` – incremental count
235
448
  * - `l` – Skiplist level (1 to 32)
@@ -283,13 +496,13 @@ type InsertTimestampStrategy = "append" | "prepend" | "insert";
283
496
  const insertTimestamp =
284
497
  (deps: SqliteDep) =>
285
498
  (
286
- ownerId: BinaryOwnerId,
287
- timestamp: BinaryTimestamp,
499
+ ownerId: OwnerIdBytes,
500
+ timestamp: TimestampBytes,
288
501
  level: PositiveInt,
289
502
  strategy: InsertTimestampStrategy,
290
503
  ): Result<void, SqliteError> => {
291
504
  const [h1, h2] = fingerprintToSqliteFingerprint(
292
- binaryTimestampToFingerprint(timestamp),
505
+ timestampBytesToFingerprint(timestamp),
293
506
  );
294
507
 
295
508
  let queries: Array<ReturnType<typeof sql.prepared>> = [];
@@ -807,6 +1020,13 @@ const insertTimestamp =
807
1020
  return ok();
808
1021
  };
809
1022
 
1023
+ export const timestampBytesToFingerprint = (
1024
+ timestamp: TimestampBytes,
1025
+ ): Fingerprint => {
1026
+ const hash = sha256(timestamp).slice(0, fingerprintSize);
1027
+ return hash as Fingerprint;
1028
+ };
1029
+
810
1030
  /**
811
1031
  * Generates a random skiplist level in the range [1, skiplistMaxLevel].
812
1032
  * Probabilistic approach avoids the need for explicit tree balancing.
@@ -887,7 +1107,7 @@ const sqliteFingerprintToFingerprint = ([
887
1107
 
888
1108
  const getSize =
889
1109
  (deps: SqliteDep) =>
890
- (ownerId: BinaryOwnerId): Result<NonNegativeInt, SqliteError> => {
1110
+ (ownerId: OwnerIdBytes): Result<NonNegativeInt, SqliteError> => {
891
1111
  const result = deps.sqlite.exec<{ size: NonNegativeInt }>(sql.prepared`
892
1112
  with
893
1113
  ml(ml) as (
@@ -930,7 +1150,7 @@ const getSize =
930
1150
  const findLowerBound =
931
1151
  (deps: SqliteDep) =>
932
1152
  (
933
- ownerId: BinaryOwnerId,
1153
+ ownerId: OwnerIdBytes,
934
1154
  begin: NonNegativeInt,
935
1155
  end: NonNegativeInt,
936
1156
  upperBound: RangeUpperBound,
@@ -942,7 +1162,7 @@ const findLowerBound =
942
1162
  }
943
1163
 
944
1164
  const result = deps.sqlite.exec<{
945
- t: BinaryTimestamp;
1165
+ t: TimestampBytes;
946
1166
  }>(sql.prepared`
947
1167
  select t
948
1168
  from evolu_timestamp
@@ -966,8 +1186,8 @@ const findLowerBound =
966
1186
  const getTimestampCount =
967
1187
  (deps: SqliteDep) =>
968
1188
  (
969
- ownerId: BinaryOwnerId,
970
- timestamp: BinaryTimestamp,
1189
+ ownerId: OwnerIdBytes,
1190
+ timestamp: TimestampBytes,
971
1191
  ): Result<PositiveInt, SqliteError> => {
972
1192
  const result = deps.sqlite.exec<{
973
1193
  count: PositiveInt;
@@ -1024,7 +1244,7 @@ const getTimestampCount =
1024
1244
  const fingerprint =
1025
1245
  (deps: SqliteDep) =>
1026
1246
  (
1027
- ownerId: BinaryOwnerId,
1247
+ ownerId: OwnerIdBytes,
1028
1248
  begin: NonNegativeInt,
1029
1249
  end: NonNegativeInt,
1030
1250
  ): Result<Fingerprint, SqliteError> => {
@@ -1056,14 +1276,14 @@ const fingerprint =
1056
1276
  const fingerprintRanges =
1057
1277
  (deps: SqliteDep) =>
1058
1278
  (
1059
- ownerId: BinaryOwnerId,
1279
+ ownerId: OwnerIdBytes,
1060
1280
  buckets: ReadonlyArray<NonNegativeInt>,
1061
1281
  upperBound: RangeUpperBound = InfiniteUpperBound,
1062
1282
  ): Result<ReadonlyArray<FingerprintRange>, SqliteError> => {
1063
1283
  const bucketsJson = JSON.stringify(buckets);
1064
1284
 
1065
1285
  const result = deps.sqlite.exec<{
1066
- b: BinaryTimestamp | null;
1286
+ b: TimestampBytes | null;
1067
1287
  h1: Int64String;
1068
1288
  h2: Int64String;
1069
1289
  }>(sql.prepared`
@@ -1199,11 +1419,11 @@ const x = (a: string, b: string) => sql.raw(`(${a} | ${b}) - (${a} & ${b})`);
1199
1419
  export const getTimestampByIndex =
1200
1420
  (deps: SqliteDep) =>
1201
1421
  (
1202
- ownerId: BinaryOwnerId,
1422
+ ownerId: OwnerIdBytes,
1203
1423
  index: NonNegativeInt,
1204
- ): Result<BinaryTimestamp, SqliteError> => {
1424
+ ): Result<TimestampBytes, SqliteError> => {
1205
1425
  const result = deps.sqlite.exec<{
1206
- readonly pt: BinaryTimestamp;
1426
+ readonly pt: TimestampBytes;
1207
1427
  }>(sql.prepared`
1208
1428
  with
1209
1429
  fi(b, cl, ic, pt, mt, nt, nc) as (