@evolu/common 5.4.8 → 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
@@ -0,0 +1,1281 @@
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
+ */
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
+ export interface SqliteStorageBase {
50
+ readonly insertTimestamp: (
51
+ ownerId: BinaryOwnerId,
52
+ timestamp: BinaryTimestamp,
53
+ ) => Result<void, SqliteError>;
54
+
55
+ readonly getSize: Storage["getSize"];
56
+ readonly fingerprint: Storage["fingerprint"];
57
+ readonly fingerprintRanges: Storage["fingerprintRanges"];
58
+ readonly findLowerBound: Storage["findLowerBound"];
59
+ readonly iterate: Storage["iterate"];
60
+ }
61
+
62
+ export interface SqliteStorageBaseDep {
63
+ readonly storage: SqliteStorageBase;
64
+ }
65
+
66
+ export type SqliteStorageDeps = SqliteDep & RandomDep;
67
+
68
+ export interface CreateSqliteStorageBaseOptions {
69
+ onStorageError: (error: SqliteError) => void;
70
+ }
71
+
72
+ export const createSqliteStorageBase =
73
+ (deps: SqliteStorageDeps) =>
74
+ (
75
+ options: CreateSqliteStorageBaseOptions,
76
+ ): Result<SqliteStorageBase, SqliteError> => {
77
+ const createTablesResult = createTables(deps);
78
+ if (!createTablesResult.ok) return createTablesResult;
79
+
80
+ const ownerStats = new Map<
81
+ OwnerId,
82
+ {
83
+ minT: BinaryTimestamp;
84
+ maxT: BinaryTimestamp;
85
+ }
86
+ >();
87
+
88
+ return ok({
89
+ insertTimestamp: (ownerId: BinaryOwnerId, timestamp: BinaryTimestamp) => {
90
+ const ownerIdString = binaryOwnerIdToOwnerId(ownerId);
91
+ const level = randomSkiplistLevel(deps);
92
+
93
+ let stats = ownerStats.get(ownerIdString);
94
+
95
+ if (!stats) {
96
+ const result = deps.sqlite.exec<{
97
+ maxT: BinaryTimestamp | null;
98
+ minT: BinaryTimestamp | null;
99
+ }>(sql.prepared`
100
+ select min(t) as minT, max(t) as maxT
101
+ from evolu_timestamp
102
+ where ownerId = ${ownerId};
103
+ `);
104
+ if (!result.ok) return result;
105
+
106
+ stats = {
107
+ minT: result.value.rows[0].minT ?? timestamp,
108
+ maxT: result.value.rows[0].maxT ?? timestamp,
109
+ };
110
+ ownerStats.set(ownerIdString, stats);
111
+ }
112
+
113
+ let strategy: InsertTimestampStrategy;
114
+
115
+ if (orderBinaryTimestamp(timestamp, stats.maxT) === 1) {
116
+ strategy = "append";
117
+ stats.maxT = timestamp;
118
+ } else if (orderBinaryTimestamp(timestamp, stats.minT) === -1) {
119
+ strategy = "prepend";
120
+ stats.minT = timestamp;
121
+ } else {
122
+ strategy = "insert";
123
+ }
124
+
125
+ return insertTimestamp(deps)(ownerId, timestamp, level, strategy);
126
+ },
127
+
128
+ getSize: (ownerId) => {
129
+ const size = getSize(deps)(ownerId);
130
+ if (!size.ok) {
131
+ options.onStorageError(size.error);
132
+ return null;
133
+ }
134
+ return size.value;
135
+ },
136
+
137
+ fingerprint: (ownerId, begin, end) => {
138
+ assertBeginEnd(begin, end);
139
+ const result = fingerprint(deps)(ownerId, begin, end);
140
+ if (!result.ok) {
141
+ options.onStorageError(result.error);
142
+ return null;
143
+ }
144
+ return result.value;
145
+ },
146
+
147
+ fingerprintRanges: (ownerId, buckets, upperBound) => {
148
+ const ranges = fingerprintRanges(deps)(ownerId, buckets, upperBound);
149
+ if (!ranges.ok) {
150
+ options.onStorageError(ranges.error);
151
+ return null;
152
+ }
153
+ return ranges.value;
154
+ },
155
+
156
+ findLowerBound: (ownerId, begin, end, upperBound) => {
157
+ const lowerBound = findLowerBound(deps)(
158
+ ownerId,
159
+ begin,
160
+ end,
161
+ upperBound,
162
+ );
163
+ if (!lowerBound.ok) {
164
+ options.onStorageError(lowerBound.error);
165
+ return null;
166
+ }
167
+ return lowerBound.value;
168
+ },
169
+
170
+ iterate: (ownerId, begin, end, callback) => {
171
+ assertBeginEnd(begin, end);
172
+ const length = end - begin;
173
+ if (length === 0) return;
174
+
175
+ // This is much faster than SQL limit with offset.
176
+ const first = getTimestampByIndex(deps)(ownerId, begin);
177
+ if (!first.ok) {
178
+ options.onStorageError(first.error);
179
+ return;
180
+ }
181
+
182
+ if (!callback(first.value, begin)) return;
183
+ if (length === 1) return;
184
+
185
+ /**
186
+ * TODO: In rare cases, we might overfetch a lot of rows here, but we
187
+ * don't have real usage numbers yet. Fetching one row at a time would
188
+ * probably be slower in almost all cases. In the future, we should
189
+ * fetch in chunks (e.g., 1,000 rows at a time). For now, consider
190
+ * logging unused rows to gather data and calculate an average, then use
191
+ * that information to determine an optimal chunk size. Before
192
+ * implementing chunking, be sure to run performance tests (including
193
+ * fetching one by one).
194
+ */
195
+ const result = deps.sqlite.exec<{ t: BinaryTimestamp }>(sql`
196
+ select t
197
+ from evolu_timestamp
198
+ where ownerId = ${ownerId} and t > ${first.value}
199
+ order by t
200
+ limit ${length - 1};
201
+ `);
202
+ if (!result.ok) {
203
+ options.onStorageError(result.error);
204
+ return;
205
+ }
206
+
207
+ for (let i = 0; i < result.value.rows.length; i++) {
208
+ const index = (begin + 1 + i) as NonNegativeInt;
209
+ if (!callback(result.value.rows[i].t, index)) return;
210
+ }
211
+ },
212
+ });
213
+ };
214
+
215
+ const assertBeginEnd = (begin: NonNegativeInt, end: NonNegativeInt) => {
216
+ assert(begin <= end, "invalid begin or end");
217
+ };
218
+
219
+ const createTables = (deps: SqliteDep): Result<void, SqliteError> => {
220
+ for (const query of [
221
+ /**
222
+ * Creates the `evolu_timestamp` table for storing timestamps of multiple
223
+ * owners.
224
+ *
225
+ * All timestamps are stored in a single table using `ownerId` as part of
226
+ * the primary key. The table implements a Skiplist structure via the `l`
227
+ * (level) column for fast, scalable queries without the need for tree
228
+ * balancing.
229
+ *
230
+ * Columns:
231
+ *
232
+ * - `t` – globally unique binary timestamp
233
+ * - `h1`/`h2` – 12-byte fingerprint split into two integers for fast XOR
234
+ * - `c` – incremental count
235
+ * - `l` – Skiplist level (1 to 32)
236
+ *
237
+ * For scaling or isolation, sharding is possible—each owner can have a
238
+ * separate SQLite database.
239
+ *
240
+ * Maybe we could use an integer surrogate key for ownerId, but it's fast
241
+ * enough even without it.
242
+ */
243
+ sql`
244
+ create table if not exists evolu_timestamp (
245
+ "ownerId" blob not null,
246
+ "t" blob not null,
247
+ "h1" integer,
248
+ "h2" integer,
249
+ "c" integer,
250
+ "l" integer not null,
251
+ primary key ("ownerId", "t")
252
+ )
253
+ strict;
254
+ `,
255
+
256
+ sql`
257
+ create index if not exists evolu_timestamp_index on evolu_timestamp (
258
+ "ownerId",
259
+ "l",
260
+ "t",
261
+ "h1",
262
+ "h2",
263
+ "c"
264
+ );
265
+ `,
266
+ ]) {
267
+ const result = deps.sqlite.exec(query);
268
+ if (!result.ok) return result;
269
+ }
270
+ return ok();
271
+ };
272
+
273
+ type InsertTimestampStrategy = "append" | "prepend" | "insert";
274
+
275
+ // AFAIK, we can't do both insert and update in one query, and that's probably
276
+ // why append is 2x faster than insert. Prepend also has to update parents, but
277
+ // it's constantly fast. Insert degrades for reversed (yet LIMIT X magically
278
+ // makes it much faster) but it's OK for append. It's probably because it's the
279
+ // most complicated SQL, but I believe it can be simplified. If not, we can
280
+ // optimize prepending by reversing the incoming timestamps if we detect that
281
+ // they will prepend. They are always sorted in ascending order by the
282
+ // Protocol.
283
+ const insertTimestamp =
284
+ (deps: SqliteDep) =>
285
+ (
286
+ ownerId: BinaryOwnerId,
287
+ timestamp: BinaryTimestamp,
288
+ level: PositiveInt,
289
+ strategy: InsertTimestampStrategy,
290
+ ): Result<void, SqliteError> => {
291
+ const [h1, h2] = fingerprintToSqliteFingerprint(
292
+ binaryTimestampToFingerprint(timestamp),
293
+ );
294
+
295
+ let queries: Array<ReturnType<typeof sql.prepared>> = [];
296
+
297
+ switch (strategy) {
298
+ case "append":
299
+ queries = [
300
+ level === 1
301
+ ? sql.prepared`
302
+ insert into evolu_timestamp
303
+ (ownerId, l, t, h1, h2, c)
304
+ values
305
+ (${ownerId}, 1, ${timestamp}, ${h1}, ${h2}, 1)
306
+ on conflict do nothing;
307
+ `
308
+ : sql.prepared`
309
+ with
310
+ fc(b, cl, pt, nt, ih1, ih2, ic) as (
311
+ select
312
+ 0,
313
+ (
314
+ select max(l)
315
+ from evolu_timestamp
316
+ where ownerId = ${ownerId}
317
+ ),
318
+ zeroblob(0),
319
+ null,
320
+ 0,
321
+ 0,
322
+ 0
323
+ union all
324
+ select
325
+ not b,
326
+ iif(b, iif(nt is null, cl - 1, cl), cl),
327
+ iif(b, ifnull(nt, pt), pt),
328
+ iif(
329
+ b,
330
+ null,
331
+ (
332
+ select t
333
+ from evolu_timestamp
334
+ where
335
+ ownerId = ${ownerId}
336
+ and l = cl
337
+ and t > pt
338
+ and t < ${timestamp}
339
+ order by t
340
+ limit 1
341
+ )
342
+ ),
343
+ iif(
344
+ b and cl < ${level} and nt is not null,
345
+ (
346
+ select (ih1 | h1) - (ih1 & h1)
347
+ from evolu_timestamp
348
+ where ownerId = ${ownerId} and t = nt
349
+ ),
350
+ ih1
351
+ ),
352
+ iif(
353
+ b and cl < ${level} and nt is not null,
354
+ (
355
+ select (ih2 | h2) - (ih2 & h2)
356
+ from evolu_timestamp
357
+ where ownerId = ${ownerId} and t = nt
358
+ ),
359
+ ih2
360
+ ),
361
+ iif(
362
+ b and cl < ${level} and nt is not null,
363
+ (
364
+ select ic + c
365
+ from evolu_timestamp
366
+ where ownerId = ${ownerId} and t = nt
367
+ ),
368
+ ic
369
+ )
370
+ from fc
371
+ where cl > 0
372
+ )
373
+ insert into evolu_timestamp (ownerId, t, l, h1, h2, c)
374
+ select
375
+ ${ownerId},
376
+ ${timestamp},
377
+ ${level},
378
+ (${h1} | ih1) - (${h1} & ih1),
379
+ (${h2} | ih2) - (${h2} & ih2),
380
+ ic + 1
381
+ from fc
382
+ order by cl asc
383
+ limit 1
384
+ on conflict do nothing;
385
+ `,
386
+ ];
387
+ break;
388
+
389
+ case "prepend":
390
+ queries = [
391
+ sql.prepared`
392
+ insert into evolu_timestamp
393
+ (ownerId, l, t, h1, h2, c)
394
+ values
395
+ (${ownerId}, ${level}, ${timestamp}, ${h1}, ${h2}, 1)
396
+ on conflict do nothing;
397
+ `,
398
+ sql.prepared`
399
+ with
400
+ ml(ml) as (
401
+ select max(l)
402
+ from evolu_timestamp
403
+ where ownerId = ${ownerId}
404
+ ),
405
+ fp(b, cl, pt, nt, h1, h2, c) as (
406
+ select
407
+ 0,
408
+ (select ml from ml),
409
+ null,
410
+ null,
411
+ null,
412
+ null,
413
+ null
414
+ union all
415
+ select
416
+ not b,
417
+ iif(b, cl - 1, cl),
418
+ iif(
419
+ b,
420
+ iif(nt is not null and (pt is null or nt < pt), nt, pt),
421
+ pt
422
+ ),
423
+ iif(
424
+ b,
425
+ null,
426
+ (
427
+ select t
428
+ from evolu_timestamp
429
+ where ownerId = ${ownerId} and l = cl and t > ${timestamp}
430
+ order by t
431
+ limit 1
432
+ )
433
+ ),
434
+ iif(
435
+ b and nt is not null and (pt is null or nt < pt),
436
+ (
437
+ select h1
438
+ from evolu_timestamp
439
+ where ownerId = ${ownerId} and t = nt
440
+ ),
441
+ null
442
+ ),
443
+ iif(
444
+ b and nt is not null and (pt is null or nt < pt),
445
+ (
446
+ select h2
447
+ from evolu_timestamp
448
+ where ownerId = ${ownerId} and t = nt
449
+ ),
450
+ null
451
+ ),
452
+ iif(
453
+ b and nt is not null and (pt is null or nt < pt),
454
+ (
455
+ select c
456
+ from evolu_timestamp
457
+ where ownerId = ${ownerId} and t = nt
458
+ ),
459
+ null
460
+ )
461
+ from fp
462
+ where cl > ${level}
463
+ ),
464
+ u(t, h1, h2) as (
465
+ select
466
+ pt,
467
+ (${h1} | h1) - (${h1} & h1),
468
+ (${h2} | h2) - (${h2} & h2)
469
+ from fp
470
+ where h1 is not null and pt is not null
471
+ order by pt
472
+ -- Check skiplistMaxLevel docs.
473
+ limit 10
474
+ )
475
+ update evolu_timestamp
476
+ set
477
+ h1 = u.h1,
478
+ h2 = u.h2,
479
+ c = c + 1
480
+ from u
481
+ where
482
+ changes() > 0
483
+ and ownerId = ${ownerId}
484
+ and evolu_timestamp.t = u.t;
485
+ `,
486
+ ];
487
+ break;
488
+
489
+ case "insert":
490
+ queries =
491
+ level === 1
492
+ ? [
493
+ sql.prepared`
494
+ insert into evolu_timestamp
495
+ (ownerId, l, t, h1, h2, c)
496
+ values
497
+ (${ownerId}, 1, ${timestamp}, ${h1}, ${h2}, 1)
498
+ on conflict do nothing;
499
+ `,
500
+
501
+ // DEV: Check whether a boolean flag is faster.
502
+ sql.prepared`
503
+ with
504
+ p(l, t, h1, h2) as (
505
+ select
506
+ (
507
+ select max(l) + 1
508
+ from evolu_timestamp
509
+ where ownerId = ${ownerId}
510
+ ),
511
+ null,
512
+ null,
513
+ null
514
+ union all
515
+ select
516
+ p.l - 1,
517
+ ifnull(
518
+ (
519
+ select t
520
+ from evolu_timestamp
521
+ where
522
+ ownerId = ${ownerId}
523
+ and l = p.l - 1
524
+ and t > ${timestamp}
525
+ and (p.t is null or p.t > t)
526
+ order by t
527
+ limit 1
528
+ ),
529
+ p.t
530
+ ),
531
+ (
532
+ select h1
533
+ from evolu_timestamp
534
+ where
535
+ ownerId = ${ownerId}
536
+ and l = p.l - 1
537
+ and t > ${timestamp}
538
+ and (p.t is null or p.t > t)
539
+ order by t
540
+ limit 1
541
+ ),
542
+ (
543
+ select h2
544
+ from evolu_timestamp
545
+ where
546
+ ownerId = ${ownerId}
547
+ and l = p.l - 1
548
+ and t > ${timestamp}
549
+ and (p.t is null or p.t > t)
550
+ order by t
551
+ limit 1
552
+ )
553
+ from p
554
+ where p.l > 2
555
+ -- Check skiplistMaxLevel docs.
556
+ limit 10
557
+ ),
558
+ u(t, h1, h2) as (
559
+ select
560
+ t,
561
+ (${h1} | h1) - (${h1} & h1),
562
+ (${h2} | h2) - (${h2} & h2)
563
+ from p
564
+ where h1 is not null
565
+ )
566
+ update evolu_timestamp
567
+ set
568
+ h1 = u.h1,
569
+ h2 = u.h2,
570
+ c = c + 1
571
+ from u
572
+ where
573
+ changes() > 0
574
+ and ownerId = ${ownerId}
575
+ and evolu_timestamp.t = u.t;
576
+ `,
577
+ ]
578
+ : [
579
+ sql.prepared`
580
+ insert into evolu_timestamp (ownerId, t, l)
581
+ values (${ownerId}, ${timestamp}, ${level})
582
+ on conflict do nothing;
583
+ `,
584
+
585
+ sql.prepared`
586
+ with
587
+ c0(b, cl, pt, nt, h1, h2, c) as (
588
+ select
589
+ 0,
590
+ (
591
+ select max(l)
592
+ from evolu_timestamp
593
+ where ownerId = ${ownerId}
594
+ ),
595
+ 0,
596
+ null,
597
+ null,
598
+ null,
599
+ null
600
+ union all
601
+ select
602
+ not b,
603
+ iif(b, iif(nt is null, cl - 1, cl), cl),
604
+ iif(b, ifnull(nt, pt), pt),
605
+ iif(
606
+ b,
607
+ null,
608
+ (
609
+ select t
610
+ from evolu_timestamp
611
+ where
612
+ ownerId = ${ownerId}
613
+ and l = cl
614
+ and t > pt
615
+ and t < ${timestamp}
616
+ order by t
617
+ limit 1
618
+ )
619
+ ),
620
+ iif(
621
+ b and cl < ${level} and nt is not null,
622
+ (
623
+ select h1
624
+ from evolu_timestamp
625
+ where ownerId = ${ownerId} and t = nt
626
+ ),
627
+ null
628
+ ),
629
+ iif(
630
+ b and cl < ${level} and nt is not null,
631
+ (
632
+ select h2
633
+ from evolu_timestamp
634
+ where ownerId = ${ownerId} and t = nt
635
+ ),
636
+ null
637
+ ),
638
+ iif(
639
+ b and cl < ${level} and nt is not null,
640
+ (
641
+ select c
642
+ from evolu_timestamp
643
+ where ownerId = ${ownerId} and t = nt
644
+ ),
645
+ null
646
+ )
647
+ from c0
648
+ where cl > 0
649
+ ),
650
+ c1(l, t, h1, h2, c) as (
651
+ select
652
+ ${level},
653
+ ${timestamp},
654
+ ${h1},
655
+ ${h2},
656
+ 1
657
+ union all
658
+ select cl, pt, h1, h2, c
659
+ from c0
660
+ where h1 is not null
661
+ ),
662
+ c2(rn, l, t, h1, h2, c) as (
663
+ select row_number() over (order by l), l, t, h1, h2, c
664
+ from c1
665
+ ),
666
+ c3(rn, l, t, h1, h2, c) as (
667
+ select rn, l, t, h1, h2, c from c2 where rn = 1
668
+ union all
669
+ select
670
+ c3.rn + 1,
671
+ c2.l,
672
+ c2.t,
673
+ (c2.h1 | c3.h1) - (c2.h1 & c3.h1),
674
+ (c2.h2 | c3.h2) - (c2.h2 & c3.h2),
675
+ c2.c + c3.c
676
+ from
677
+ c3
678
+ join c2 on c2.rn = c3.rn + 1
679
+ ),
680
+ c4(l, t, h1, h2, c, rn) as (
681
+ select l, t, h1, h2, c, max(rn)
682
+ from c3
683
+ group by l
684
+ ),
685
+ -- DEV: Check whether a boolean flag is faster.
686
+ n(l, t, h1, h2, c) as (
687
+ select
688
+ (
689
+ select max(l) + 1
690
+ from evolu_timestamp
691
+ where ownerId = ${ownerId}
692
+ ),
693
+ null,
694
+ null,
695
+ null,
696
+ null
697
+ union all
698
+ select
699
+ n.l - 1,
700
+ ifnull(
701
+ (
702
+ select t
703
+ from evolu_timestamp
704
+ where
705
+ ownerId = ${ownerId}
706
+ and l = n.l - 1
707
+ and t > ${timestamp}
708
+ and (n.t is null or t < n.t)
709
+ order by t
710
+ limit 1
711
+ ),
712
+ n.t
713
+ ),
714
+ (
715
+ select h1
716
+ from evolu_timestamp
717
+ where
718
+ ownerId = ${ownerId}
719
+ and l = n.l - 1
720
+ and t > ${timestamp}
721
+ and (n.t is null or t < n.t)
722
+ order by t
723
+ limit 1
724
+ ),
725
+ (
726
+ select h2
727
+ from evolu_timestamp
728
+ where
729
+ ownerId = ${ownerId}
730
+ and l = n.l - 1
731
+ and t > ${timestamp}
732
+ and (n.t is null or t < n.t)
733
+ order by t
734
+ limit 1
735
+ ),
736
+ (
737
+ select c
738
+ from evolu_timestamp
739
+ where
740
+ ownerId = ${ownerId}
741
+ and l = n.l - 1
742
+ and t > ${timestamp}
743
+ and (n.t is null or t < n.t)
744
+ order by t
745
+ limit 1
746
+ )
747
+ from n
748
+ where l - 1 > (select min(l) from c4)
749
+ ),
750
+ u(ut, uh1, uh2, uc) as (
751
+ select t, h1, h2, c from c4 where t = ${timestamp}
752
+ union all
753
+ select
754
+ max(t),
755
+ iif(
756
+ l > ${level},
757
+ (${h1} | h1) - (${h1} & h1),
758
+ (
759
+ select (c4.h1 | n.h1) - (c4.h1 & n.h1)
760
+ from c4
761
+ where
762
+ c4.l = (select max(l) from c4 where c4.l < n.l)
763
+ )
764
+ ),
765
+ iif(
766
+ l > ${level},
767
+ (${h2} | h2) - (${h2} & h2),
768
+ (
769
+ select (c4.h2 | n.h2) - (c4.h2 & n.h2)
770
+ from c4
771
+ where
772
+ c4.l = (select max(l) from c4 where c4.l < n.l)
773
+ )
774
+ ),
775
+ iif(
776
+ l > ${level},
777
+ c + 1,
778
+ (
779
+ select n.c - c4.c
780
+ from c4
781
+ where
782
+ c4.l = (select max(l) from c4 where c4.l < n.l)
783
+ )
784
+ )
785
+ from n
786
+ group by t
787
+ -- Check skiplistMaxLevel docs.
788
+ limit 10
789
+ )
790
+ update evolu_timestamp
791
+ set
792
+ h1 = uh1,
793
+ h2 = uh2,
794
+ c = uc
795
+ from u
796
+ where changes() > 0 and ownerId = ${ownerId} and t = ut;
797
+ `,
798
+ ];
799
+ break;
800
+ }
801
+
802
+ for (const query of queries) {
803
+ const result = deps.sqlite.exec(query);
804
+ if (!result.ok) return result;
805
+ }
806
+
807
+ return ok();
808
+ };
809
+
810
+ /**
811
+ * Generates a random skiplist level in the range [1, skiplistMaxLevel].
812
+ * Probabilistic approach avoids the need for explicit tree balancing.
813
+ */
814
+ const randomSkiplistLevel = (deps: RandomDep): PositiveInt => {
815
+ let level = 1;
816
+ while (
817
+ deps.random.next() <= skiplistProbability &&
818
+ level < skiplistMaxLevel
819
+ ) {
820
+ level += 1;
821
+ }
822
+ return level as PositiveInt;
823
+ };
824
+
825
+ /**
826
+ * Probability used for generating Skiplist levels.
827
+ *
828
+ * 0.5 is little faster for the first 30k.
829
+ */
830
+ const skiplistProbability = 0.25;
831
+
832
+ /**
833
+ * The SQLite has weird behaviour when we have to limit CTEs even when we don't
834
+ * actually limit anything; otherwise, it would not work. But without it, SQLite
835
+ * insert and prepend is slow. The 10 is the maximum allowed number, which
836
+ * "fixes" SQLite.
837
+ *
838
+ * Because we are using {@link skiplistProbability} 0.25, it's OK even for
839
+ * millions of rows.
840
+ */
841
+ const skiplistMaxLevel = 10;
842
+
843
+ /**
844
+ * {@link Fingerprint} encoded for efficient use in SQLite.
845
+ *
846
+ * Fingerprints are 12-byte binary values. While SQLite can XOR binary blobs or
847
+ * strings, it's not efficient. The fastest approach is to split the 12 bytes
848
+ * into two 6-byte integers, which can be XORed quickly in SQL. We use strings
849
+ * to represent these integers because not all SQLite drivers support native
850
+ * bigint types.
851
+ */
852
+ type SqliteFingerprint = [Int64String, Int64String] &
853
+ Brand<"SqliteFingerprint">;
854
+
855
+ const fingerprintToSqliteFingerprint = (
856
+ fingerprint: Fingerprint,
857
+ ): SqliteFingerprint => {
858
+ let part1 = BigInt(0);
859
+ let part2 = BigInt(0);
860
+ for (let i = 0; i < 6; i++) {
861
+ part1 = (part1 << BigInt(8)) | BigInt(fingerprint[i]);
862
+ }
863
+ for (let i = 6; i < 12; i++) {
864
+ part2 = (part2 << BigInt(8)) | BigInt(fingerprint[i]);
865
+ }
866
+ return [part1.toString(), part2.toString()] as SqliteFingerprint;
867
+ };
868
+
869
+ const sqliteFingerprintToFingerprint = ([
870
+ part1String,
871
+ part2String,
872
+ ]: SqliteFingerprint): Fingerprint => {
873
+ let part1 = BigInt(part1String);
874
+ let part2 = BigInt(part2String);
875
+
876
+ const hash = new Uint8Array(12);
877
+ for (let i = 5; i >= 0; i--) {
878
+ hash[i] = Number(part1 & BigInt(0xff));
879
+ part1 >>= BigInt(8);
880
+ }
881
+ for (let i = 11; i >= 6; i--) {
882
+ hash[i] = Number(part2 & BigInt(0xff));
883
+ part2 >>= BigInt(8);
884
+ }
885
+ return hash as Fingerprint;
886
+ };
887
+
888
+ const getSize =
889
+ (deps: SqliteDep) =>
890
+ (ownerId: BinaryOwnerId): Result<NonNegativeInt, SqliteError> => {
891
+ const result = deps.sqlite.exec<{ size: NonNegativeInt }>(sql.prepared`
892
+ with
893
+ ml(ml) as (
894
+ select max(l)
895
+ from evolu_timestamp
896
+ where ownerId = ${ownerId}
897
+ ),
898
+ sc(l, pt, c) as (
899
+ select (select ml + 1 from ml), zeroblob(0), 0
900
+ union all
901
+ select
902
+ sc.l - 1,
903
+ ifnull(
904
+ (
905
+ select max(t)
906
+ from evolu_timestamp as m
907
+ where ownerId = ${ownerId} and m.l = sc.l - 1 and m.t > sc.pt
908
+ ),
909
+ sc.pt
910
+ ),
911
+ ifnull(
912
+ (
913
+ select sum(m.c)
914
+ from evolu_timestamp as m
915
+ where ownerId = ${ownerId} and m.l = sc.l - 1 and m.t > sc.pt
916
+ ),
917
+ 0
918
+ )
919
+ from sc
920
+ where sc.l > 1
921
+ )
922
+ select sum(c) as size
923
+ from sc;
924
+ `);
925
+
926
+ if (!result.ok) return result;
927
+ return ok(result.value.rows[0].size);
928
+ };
929
+
930
+ const findLowerBound =
931
+ (deps: SqliteDep) =>
932
+ (
933
+ ownerId: BinaryOwnerId,
934
+ begin: NonNegativeInt,
935
+ end: NonNegativeInt,
936
+ upperBound: RangeUpperBound,
937
+ ): Result<NonNegativeInt, SqliteError> => {
938
+ assertBeginEnd(begin, end);
939
+
940
+ if (end === 0 || begin === end || upperBound === InfiniteUpperBound) {
941
+ return ok(end);
942
+ }
943
+
944
+ const result = deps.sqlite.exec<{
945
+ t: BinaryTimestamp;
946
+ }>(sql.prepared`
947
+ select t
948
+ from evolu_timestamp
949
+ where ownerId = ${ownerId} and t >= ${upperBound}
950
+ order by t
951
+ limit 1;
952
+ `);
953
+ if (!result.ok) return result;
954
+
955
+ if (result.value.rows.length === 0) {
956
+ return ok(end);
957
+ }
958
+
959
+ const count = getTimestampCount(deps)(ownerId, result.value.rows[0].t);
960
+ if (!count.ok) return count;
961
+
962
+ // `decrement` converts a count to an index.
963
+ return ok(decrement(count.value) as NonNegativeInt);
964
+ };
965
+
966
+ const getTimestampCount =
967
+ (deps: SqliteDep) =>
968
+ (
969
+ ownerId: BinaryOwnerId,
970
+ timestamp: BinaryTimestamp,
971
+ ): Result<PositiveInt, SqliteError> => {
972
+ const result = deps.sqlite.exec<{
973
+ count: PositiveInt;
974
+ }>(sql.prepared`
975
+ with
976
+ ml(ml) as (
977
+ select max(l) from evolu_timestamp where ownerId = ${ownerId}
978
+ ),
979
+ sc(l, pt, tc) as (
980
+ select ml + 1, zeroblob(0), 0 from ml
981
+ union all
982
+ select
983
+ sc.l - 1,
984
+ ifnull(
985
+ (
986
+ select max(t)
987
+ from evolu_timestamp
988
+ where
989
+ ownerId = ${ownerId}
990
+ and l = sc.l - 1
991
+ and t <= ${timestamp}
992
+ and t > sc.pt
993
+ order by t
994
+ ),
995
+ sc.pt
996
+ ),
997
+ ifnull(
998
+ (
999
+ select sum(c)
1000
+ from evolu_timestamp
1001
+ where
1002
+ ownerId = ${ownerId}
1003
+ and l = sc.l - 1
1004
+ and t <= ${timestamp}
1005
+ and t > sc.pt
1006
+ ),
1007
+ 0
1008
+ )
1009
+ from sc
1010
+ where sc.l > 1 and sc.pt != ${timestamp}
1011
+ )
1012
+ select sum(tc) as count
1013
+ from sc;
1014
+ `);
1015
+
1016
+ if (!result.ok) return result;
1017
+ return ok(result.value.rows[0].count);
1018
+ };
1019
+
1020
+ /**
1021
+ * TODO: We reuse {@link fingerprintRanges}, which returns upper bound, which we
1022
+ * don't need, so fingerprintRanges should have a parameter for that.
1023
+ */
1024
+ const fingerprint =
1025
+ (deps: SqliteDep) =>
1026
+ (
1027
+ ownerId: BinaryOwnerId,
1028
+ begin: NonNegativeInt,
1029
+ end: NonNegativeInt,
1030
+ ): Result<Fingerprint, SqliteError> => {
1031
+ // There is no need to fingerprint an empty range.
1032
+ if (end - begin === 0) {
1033
+ return ok(zeroFingerprint);
1034
+ }
1035
+
1036
+ if (begin === 0) {
1037
+ const result = fingerprintRanges(deps)(ownerId, [end]);
1038
+ if (!result.ok) return result;
1039
+ return ok(result.value[0].fingerprint);
1040
+ }
1041
+
1042
+ // We should have a param to skip the first result.
1043
+ const result = fingerprintRanges(deps)(ownerId, [begin, end]);
1044
+ if (!result.ok) return result;
1045
+ return ok(result.value[1].fingerprint);
1046
+ };
1047
+
1048
+ /**
1049
+ * First, check this: https://logperiodic.com/rbsr.html#tree-friendly-functions
1050
+ *
1051
+ * We are a little smarter. We leverage continuous ranges to have half of
1052
+ * traversals. 16 instead of 32. And we compute all of them in a single SQL
1053
+ * select. If only we could get rid of those subqueries. Then, it would be
1054
+ * perfect. Btw, reading h1, h2, c in the second step would be slighly faster.
1055
+ */
1056
+ const fingerprintRanges =
1057
+ (deps: SqliteDep) =>
1058
+ (
1059
+ ownerId: BinaryOwnerId,
1060
+ buckets: ReadonlyArray<NonNegativeInt>,
1061
+ upperBound: RangeUpperBound = InfiniteUpperBound,
1062
+ ): Result<ReadonlyArray<FingerprintRange>, SqliteError> => {
1063
+ const bucketsJson = JSON.stringify(buckets);
1064
+
1065
+ const result = deps.sqlite.exec<{
1066
+ b: BinaryTimestamp | null;
1067
+ h1: Int64String;
1068
+ h2: Int64String;
1069
+ }>(sql.prepared`
1070
+ with
1071
+ ml(ml) as (
1072
+ select max(l) from evolu_timestamp where ownerId = ${ownerId}
1073
+ ),
1074
+ c0(c) as (select value as c from json_each(${bucketsJson})),
1075
+ c1(c, b, nt, nc, nh1, nh2, ft, tt, dl, ic, h1, h2) as (
1076
+ select
1077
+ c,
1078
+ 1,
1079
+ null,
1080
+ null,
1081
+ null,
1082
+ null,
1083
+ zeroblob(0),
1084
+ X'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF',
1085
+ ml,
1086
+ 0,
1087
+ 0,
1088
+ 0
1089
+ from
1090
+ c0,
1091
+ ml
1092
+ union all
1093
+ select
1094
+ c,
1095
+ not b,
1096
+ iif(
1097
+ b,
1098
+ (
1099
+ select t
1100
+ from evolu_timestamp
1101
+ where l = dl and t > ft and t < tt and ownerId = ${ownerId}
1102
+ order by t
1103
+ limit 1
1104
+ ),
1105
+ null
1106
+ ),
1107
+ iif(
1108
+ b,
1109
+ (
1110
+ select c
1111
+ from evolu_timestamp
1112
+ where l = dl and t > ft and t < tt and ownerId = ${ownerId}
1113
+ order by t
1114
+ limit 1
1115
+ ),
1116
+ null
1117
+ ),
1118
+ iif(
1119
+ b,
1120
+ (
1121
+ select h1
1122
+ from evolu_timestamp
1123
+ where l = dl and t > ft and t < tt and ownerId = ${ownerId}
1124
+ order by t
1125
+ limit 1
1126
+ ),
1127
+ null
1128
+ ),
1129
+ iif(
1130
+ b,
1131
+ (
1132
+ select h2
1133
+ from evolu_timestamp
1134
+ where l = dl and t > ft and t < tt and ownerId = ${ownerId}
1135
+ order by t
1136
+ limit 1
1137
+ ),
1138
+ null
1139
+ ),
1140
+ iif(b, ft, iif(ic + nc <= c, nt, ft)),
1141
+ iif(b, tt, iif(ic + nc <= c, tt, ifnull(nt, tt))),
1142
+ iif(b, dl, iif(ic + nc <= c, dl, dl - 1)),
1143
+ iif(b, ic, iif(ic + nc <= c, ic + nc, ic)),
1144
+ iif(b, h1, iif(ic + nc <= c, ${x("h1", "nh1")}, h1)),
1145
+ iif(b, h2, iif(ic + nc <= c, ${x("h2", "nh2")}, h2))
1146
+ from c1
1147
+ where iif(b, 1, ic != c)
1148
+ ),
1149
+ c2(h1, h2, t, rn) as (
1150
+ select
1151
+ h1,
1152
+ h2,
1153
+ (
1154
+ select min(t)
1155
+ from evolu_timestamp
1156
+ where t > ft and ownerId = ${ownerId}
1157
+ ),
1158
+ row_number() over (order by c)
1159
+ from c1
1160
+ where c = ic and b = 1
1161
+ ),
1162
+ c3(oh1, oh2, b, rn, h1, h2) as (
1163
+ select h1, h2, t, rn, h1, h2 from c2 where rn = 1
1164
+ union all
1165
+ select
1166
+ c2.h1,
1167
+ c2.h2,
1168
+ t,
1169
+ c2.rn,
1170
+ ${x("c3.oh1", "c2.h1")},
1171
+ ${x("c3.oh2", "c2.h2")}
1172
+ from
1173
+ c2
1174
+ join c3 on c2.rn = c3.rn + 1
1175
+ )
1176
+ select b, cast(h1 as text) as h1, cast(h2 as text) as h2
1177
+ from c3;
1178
+ `);
1179
+
1180
+ if (!result.ok) return result;
1181
+
1182
+ const fingerprintRanges = result.value.rows.map(
1183
+ (row, i, arr): FingerprintRange => ({
1184
+ type: RangeType.Fingerprint,
1185
+ upperBound: i === arr.length - 1 ? upperBound : row.b!,
1186
+ fingerprint: sqliteFingerprintToFingerprint([
1187
+ row.h1,
1188
+ row.h2,
1189
+ ] as SqliteFingerprint),
1190
+ }),
1191
+ );
1192
+
1193
+ return ok(fingerprintRanges);
1194
+ };
1195
+
1196
+ // XOR in SQLite
1197
+ const x = (a: string, b: string) => sql.raw(`(${a} | ${b}) - (${a} & ${b})`);
1198
+
1199
+ export const getTimestampByIndex =
1200
+ (deps: SqliteDep) =>
1201
+ (
1202
+ ownerId: BinaryOwnerId,
1203
+ index: NonNegativeInt,
1204
+ ): Result<BinaryTimestamp, SqliteError> => {
1205
+ const result = deps.sqlite.exec<{
1206
+ readonly pt: BinaryTimestamp;
1207
+ }>(sql.prepared`
1208
+ with
1209
+ fi(b, cl, ic, pt, mt, nt, nc) as (
1210
+ select
1211
+ 0,
1212
+ (
1213
+ select max(l)
1214
+ from evolu_timestamp
1215
+ where ownerId = ${ownerId}
1216
+ ),
1217
+ 0,
1218
+ zeroblob(0),
1219
+ X'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF',
1220
+ null,
1221
+ 0
1222
+ union all
1223
+ select
1224
+ not b,
1225
+ iif(
1226
+ b,
1227
+ iif(nt is null or nt > mt or ic + nc > ${index + 1}, cl - 1, cl),
1228
+ cl
1229
+ ),
1230
+ iif(
1231
+ b,
1232
+ iif(nt is null or nt > mt or ic + nc > ${index + 1}, ic, ic + nc),
1233
+ ic
1234
+ ),
1235
+ iif(
1236
+ b,
1237
+ iif(nt is null or nt > mt or ic + nc > ${index + 1}, pt, nt),
1238
+ pt
1239
+ ),
1240
+ iif(
1241
+ b,
1242
+ iif(
1243
+ nt is null or nt > mt or ic + nc > ${index + 1},
1244
+ iif(nt is not null and nt < mt, nt, mt),
1245
+ mt
1246
+ ),
1247
+ mt
1248
+ ),
1249
+ iif(
1250
+ b,
1251
+ null,
1252
+ (
1253
+ select t
1254
+ from evolu_timestamp
1255
+ where ownerId = ${ownerId} and l = cl and t > pt
1256
+ order by t
1257
+ limit 1
1258
+ )
1259
+ ),
1260
+ iif(
1261
+ b,
1262
+ null,
1263
+ (
1264
+ select c
1265
+ from evolu_timestamp
1266
+ where ownerId = ${ownerId} and l = cl and t > pt
1267
+ order by t
1268
+ limit 1
1269
+ )
1270
+ )
1271
+ from fi
1272
+ where ic != ${index + 1}
1273
+ )
1274
+ select pt
1275
+ from fi
1276
+ where ic == ${index + 1};
1277
+ `);
1278
+
1279
+ if (!result.ok) return result;
1280
+ return ok(result.value.rows[0].pt);
1281
+ };