@evolu/common 5.4.7 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/README.md +30 -34
  2. package/dist/src/Array.d.ts +17 -0
  3. package/dist/src/Array.d.ts.map +1 -0
  4. package/dist/src/Array.js +12 -0
  5. package/dist/src/Assert.d.ts +68 -0
  6. package/dist/src/Assert.d.ts.map +1 -0
  7. package/dist/src/Assert.js +77 -0
  8. package/dist/src/BigInt.d.ts +20 -0
  9. package/dist/src/BigInt.d.ts.map +1 -0
  10. package/dist/src/BigInt.js +18 -0
  11. package/dist/src/Buffer.d.ts +92 -0
  12. package/dist/src/Buffer.d.ts.map +1 -0
  13. package/dist/src/Buffer.js +62 -0
  14. package/dist/src/Callbacks.d.ts +20 -0
  15. package/dist/src/Callbacks.d.ts.map +1 -0
  16. package/dist/src/Callbacks.js +18 -0
  17. package/dist/src/Console.d.ts +78 -0
  18. package/dist/src/Console.d.ts.map +1 -0
  19. package/dist/src/Console.js +103 -0
  20. package/dist/src/Crypto.d.ts +72 -39
  21. package/dist/src/Crypto.d.ts.map +1 -1
  22. package/dist/src/Crypto.js +89 -54
  23. package/dist/src/Eq.d.ts +97 -0
  24. package/dist/src/Eq.d.ts.map +1 -0
  25. package/dist/src/Eq.js +167 -0
  26. package/dist/src/Error.d.ts +14 -10
  27. package/dist/src/Error.d.ts.map +1 -1
  28. package/dist/src/Error.js +43 -11
  29. package/dist/src/Evolu/Config.d.ts +69 -0
  30. package/dist/src/Evolu/Config.d.ts.map +1 -0
  31. package/dist/src/Evolu/Config.js +9 -0
  32. package/dist/src/Evolu/Db.d.ts +126 -0
  33. package/dist/src/Evolu/Db.d.ts.map +1 -0
  34. package/dist/src/Evolu/Db.js +774 -0
  35. package/dist/src/Evolu/Diff.d.ts +43 -0
  36. package/dist/src/Evolu/Diff.d.ts.map +1 -0
  37. package/dist/src/Evolu/Diff.js +95 -0
  38. package/dist/src/Evolu/Evolu.d.ts +334 -0
  39. package/dist/src/Evolu/Evolu.d.ts.map +1 -0
  40. package/dist/src/Evolu/Evolu.js +434 -0
  41. package/dist/src/Evolu/Internal.d.ts +26 -0
  42. package/dist/src/Evolu/Internal.d.ts.map +1 -0
  43. package/dist/src/Evolu/Internal.js +25 -0
  44. package/dist/src/Evolu/Kysely.d.ts +6 -0
  45. package/dist/src/Evolu/Kysely.d.ts.map +1 -0
  46. package/dist/src/Evolu/Kysely.js +21 -0
  47. package/dist/src/Evolu/Owner.d.ts +155 -0
  48. package/dist/src/Evolu/Owner.d.ts.map +1 -0
  49. package/dist/src/Evolu/Owner.js +126 -0
  50. package/dist/src/Evolu/Platform.d.ts +23 -0
  51. package/dist/src/Evolu/Platform.d.ts.map +1 -0
  52. package/dist/src/Evolu/Platform.js +1 -0
  53. package/dist/src/Evolu/Protocol.d.ts +401 -0
  54. package/dist/src/Evolu/Protocol.d.ts.map +1 -0
  55. package/dist/src/Evolu/Protocol.js +1151 -0
  56. package/dist/src/Evolu/Public.d.ts +18 -0
  57. package/dist/src/Evolu/Public.d.ts.map +1 -0
  58. package/dist/src/Evolu/Public.js +11 -0
  59. package/dist/src/Evolu/PublicKysely.d.ts +148 -0
  60. package/dist/src/Evolu/PublicKysely.d.ts.map +1 -0
  61. package/dist/src/Evolu/PublicKysely.js +185 -0
  62. package/dist/src/Evolu/Query.d.ts +63 -0
  63. package/dist/src/Evolu/Query.d.ts.map +1 -0
  64. package/dist/src/Evolu/Query.js +61 -0
  65. package/dist/src/Evolu/Relay.d.ts +13 -0
  66. package/dist/src/Evolu/Relay.d.ts.map +1 -0
  67. package/dist/src/Evolu/Relay.js +109 -0
  68. package/dist/src/Evolu/Schema.d.ts +201 -0
  69. package/dist/src/Evolu/Schema.d.ts.map +1 -0
  70. package/dist/src/Evolu/Schema.js +150 -0
  71. package/dist/src/Evolu/Storage.d.ts +49 -0
  72. package/dist/src/Evolu/Storage.d.ts.map +1 -0
  73. package/dist/src/Evolu/Storage.js +1111 -0
  74. package/dist/src/Evolu/Sync.d.ts +59 -0
  75. package/dist/src/Evolu/Sync.d.ts.map +1 -0
  76. package/dist/src/Evolu/Sync.js +29 -0
  77. package/dist/src/Evolu/Timestamp.d.ts +106 -0
  78. package/dist/src/Evolu/Timestamp.d.ts.map +1 -0
  79. package/dist/src/Evolu/Timestamp.js +179 -0
  80. package/dist/src/Function.d.ts +54 -0
  81. package/dist/src/Function.d.ts.map +1 -0
  82. package/dist/src/Function.js +38 -0
  83. package/dist/src/ManyToManyMap.d.ts +26 -0
  84. package/dist/src/ManyToManyMap.d.ts.map +1 -0
  85. package/dist/src/ManyToManyMap.js +92 -0
  86. package/dist/src/NanoId.d.ts +27 -0
  87. package/dist/src/NanoId.d.ts.map +1 -0
  88. package/dist/src/NanoId.js +6 -0
  89. package/dist/src/Number.d.ts +42 -0
  90. package/dist/src/Number.d.ts.map +1 -0
  91. package/dist/src/Number.js +55 -0
  92. package/dist/src/Object.d.ts +35 -0
  93. package/dist/src/Object.d.ts.map +1 -0
  94. package/dist/src/Object.js +36 -0
  95. package/dist/src/Order.d.ts +90 -0
  96. package/dist/src/Order.d.ts.map +1 -0
  97. package/dist/src/Order.js +85 -0
  98. package/dist/src/Promise.d.ts +180 -0
  99. package/dist/src/Promise.d.ts.map +1 -0
  100. package/dist/src/Promise.js +176 -0
  101. package/dist/src/Random.d.ts +52 -0
  102. package/dist/src/Random.d.ts.map +1 -0
  103. package/dist/src/Random.js +29 -0
  104. package/dist/src/Ref.d.ts +40 -0
  105. package/dist/src/Ref.d.ts.map +1 -0
  106. package/dist/src/Ref.js +13 -0
  107. package/dist/src/Result.d.ts +421 -0
  108. package/dist/src/Result.d.ts.map +1 -0
  109. package/dist/src/Result.js +357 -0
  110. package/dist/src/Skiplist.d.ts +23 -0
  111. package/dist/src/Skiplist.d.ts.map +1 -0
  112. package/dist/src/Skiplist.js +58 -0
  113. package/dist/src/Sqlite.d.ts +116 -52
  114. package/dist/src/Sqlite.d.ts.map +1 -1
  115. package/dist/src/Sqlite.js +183 -67
  116. package/dist/src/Store.d.ts +45 -8
  117. package/dist/src/Store.d.ts.map +1 -1
  118. package/dist/src/Store.js +33 -17
  119. package/dist/src/String.d.ts +2 -0
  120. package/dist/src/String.d.ts.map +1 -0
  121. package/dist/src/String.js +14 -0
  122. package/dist/src/Time.d.ts +20 -0
  123. package/dist/src/Time.d.ts.map +1 -0
  124. package/dist/src/Time.js +25 -0
  125. package/dist/src/Type.d.ts +1937 -0
  126. package/dist/src/Type.d.ts.map +1 -0
  127. package/dist/src/Type.js +2002 -0
  128. package/dist/src/Types.d.ts +188 -0
  129. package/dist/src/Types.d.ts.map +1 -0
  130. package/dist/src/Types.js +6 -0
  131. package/dist/src/WebSocket.d.ts +112 -0
  132. package/dist/src/WebSocket.d.ts.map +1 -0
  133. package/dist/src/WebSocket.js +139 -0
  134. package/dist/src/Worker.d.ts +44 -0
  135. package/dist/src/Worker.d.ts.map +1 -0
  136. package/dist/src/Worker.js +66 -0
  137. package/dist/src/index.d.ts +24 -11
  138. package/dist/src/index.d.ts.map +1 -1
  139. package/dist/src/index.js +24 -11
  140. package/package.json +29 -38
  141. package/src/Array.ts +39 -0
  142. package/src/Assert.ts +116 -0
  143. package/src/BigInt.ts +29 -0
  144. package/src/Buffer.ts +175 -0
  145. package/src/Callbacks.ts +43 -0
  146. package/src/Console.ts +159 -0
  147. package/src/Crypto.ts +169 -115
  148. package/src/Eq.ts +204 -0
  149. package/src/Error.ts +57 -20
  150. package/src/Evolu/Config.ts +83 -0
  151. package/src/Evolu/Db.ts +1275 -0
  152. package/src/Evolu/Diff.ts +142 -0
  153. package/src/Evolu/Evolu.ts +947 -0
  154. package/src/Evolu/Internal.ts +26 -0
  155. package/src/Evolu/Kysely.ts +38 -0
  156. package/src/Evolu/Owner.ts +296 -0
  157. package/src/Evolu/Platform.ts +27 -0
  158. package/src/Evolu/Protocol.ts +1857 -0
  159. package/src/Evolu/Public.ts +43 -0
  160. package/src/Evolu/PublicKysely.ts +240 -0
  161. package/src/Evolu/Query.ts +167 -0
  162. package/src/Evolu/Relay.ts +142 -0
  163. package/src/Evolu/Schema.ts +417 -0
  164. package/src/Evolu/Storage.ts +1281 -0
  165. package/src/Evolu/Sync.ts +105 -0
  166. package/src/Evolu/Timestamp.ts +311 -0
  167. package/src/Function.ts +58 -0
  168. package/src/ManyToManyMap.ts +140 -0
  169. package/src/NanoId.ts +39 -0
  170. package/src/Number.ts +90 -0
  171. package/src/Object.ts +64 -0
  172. package/src/Order.ts +113 -0
  173. package/src/Promise.ts +295 -0
  174. package/src/Random.ts +68 -0
  175. package/src/Ref.ts +63 -0
  176. package/src/Result.ts +453 -0
  177. package/src/Skiplist.ts +102 -0
  178. package/src/Sqlite.ts +366 -153
  179. package/src/Store.ts +79 -36
  180. package/src/String.ts +10 -0
  181. package/src/Time.ts +36 -0
  182. package/src/Type.ts +3978 -0
  183. package/src/Types.ts +209 -0
  184. package/src/WebSocket.ts +273 -0
  185. package/src/Worker.ts +129 -0
  186. package/src/index.ts +24 -11
  187. package/dist/src/Config.d.ts +0 -56
  188. package/dist/src/Config.d.ts.map +0 -1
  189. package/dist/src/Config.js +0 -39
  190. package/dist/src/Crdt.d.ts +0 -89
  191. package/dist/src/Crdt.d.ts.map +0 -1
  192. package/dist/src/Crdt.js +0 -181
  193. package/dist/src/Db.d.ts +0 -107
  194. package/dist/src/Db.d.ts.map +0 -1
  195. package/dist/src/Db.js +0 -443
  196. package/dist/src/Diff.d.ts +0 -27
  197. package/dist/src/Diff.d.ts.map +0 -1
  198. package/dist/src/Diff.js +0 -84
  199. package/dist/src/Evolu.d.ts +0 -426
  200. package/dist/src/Evolu.d.ts.map +0 -1
  201. package/dist/src/Evolu.js +0 -333
  202. package/dist/src/Model.d.ts +0 -141
  203. package/dist/src/Model.d.ts.map +0 -1
  204. package/dist/src/Model.js +0 -125
  205. package/dist/src/Murmurhash.d.ts +0 -2
  206. package/dist/src/Murmurhash.d.ts.map +0 -1
  207. package/dist/src/Murmurhash.js +0 -60
  208. package/dist/src/Owner.d.ts +0 -33
  209. package/dist/src/Owner.d.ts.map +0 -1
  210. package/dist/src/Owner.js +0 -26
  211. package/dist/src/Platform.d.ts +0 -37
  212. package/dist/src/Platform.d.ts.map +0 -1
  213. package/dist/src/Platform.js +0 -11
  214. package/dist/src/Protobuf.d.ts +0 -81
  215. package/dist/src/Protobuf.d.ts.map +0 -1
  216. package/dist/src/Protobuf.js +0 -92
  217. package/dist/src/Public.d.ts +0 -13
  218. package/dist/src/Public.d.ts.map +0 -1
  219. package/dist/src/Public.js +0 -6
  220. package/dist/src/Socket.d.ts +0 -8
  221. package/dist/src/Socket.d.ts.map +0 -1
  222. package/dist/src/Socket.js +0 -51
  223. package/dist/src/Sql.d.ts +0 -12
  224. package/dist/src/Sql.d.ts.map +0 -1
  225. package/dist/src/Sql.js +0 -30
  226. package/dist/src/Sync.d.ts +0 -70
  227. package/dist/src/Sync.d.ts.map +0 -1
  228. package/dist/src/Sync.js +0 -127
  229. package/src/Config.ts +0 -119
  230. package/src/Crdt.ts +0 -361
  231. package/src/Db.ts +0 -955
  232. package/src/Diff.ts +0 -114
  233. package/src/Evolu.ts +0 -1016
  234. package/src/Model.ts +0 -233
  235. package/src/Murmurhash.ts +0 -70
  236. package/src/Owner.ts +0 -69
  237. package/src/Platform.ts +0 -47
  238. package/src/Protobuf.ts +0 -155
  239. package/src/Public.ts +0 -12
  240. package/src/Socket.ts +0 -83
  241. package/src/Sql.ts +0 -41
  242. package/src/Sync.ts +0 -315
@@ -0,0 +1,1275 @@
1
+ import {
2
+ isNonEmptyArray,
3
+ isNonEmptyReadonlyArray,
4
+ NonEmptyReadonlyArray,
5
+ } from "../Array.js";
6
+ import { assert, assertNonEmptyReadonlyArray } from "../Assert.js";
7
+ import { CallbackId } from "../Callbacks.js";
8
+ import { ConsoleDep } from "../Console.js";
9
+ import {
10
+ CreateMnemonicDep,
11
+ CreateRandomBytesDep,
12
+ createSymmetricCrypto,
13
+ SymmetricCryptoDecryptError,
14
+ SymmetricCryptoDep,
15
+ } from "../Crypto.js";
16
+ import { eqArrayNumber } from "../Eq.js";
17
+ import { TransferableError } from "../Error.js";
18
+ import { constFalse, exhaustiveCheck } from "../Function.js";
19
+ import { NanoIdLibDep } from "../NanoId.js";
20
+ import { objectToEntries } from "../Object.js";
21
+ import { RandomDep } from "../Random.js";
22
+ import { createRef, Ref } from "../Ref.js";
23
+ import { err, ok, Result } from "../Result.js";
24
+ import {
25
+ createSqlite,
26
+ CreateSqliteDriverDep,
27
+ explainSqliteQueryPlan,
28
+ SafeSql,
29
+ sql,
30
+ SqliteDep,
31
+ SqliteError,
32
+ SqliteQuery,
33
+ SqliteRow,
34
+ SqliteValue,
35
+ } from "../Sqlite.js";
36
+ import { TimeDep } from "../Time.js";
37
+ import { Id, Mnemonic, object, SimpleName, String } from "../Type.js";
38
+ import {
39
+ createInitializedWorker,
40
+ Worker,
41
+ WorkerPostMessageDep,
42
+ } from "../Worker.js";
43
+ import { Config } from "./Config.js";
44
+ import { makePatches, QueryPatches } from "./Diff.js";
45
+ import {
46
+ AppOwner,
47
+ createAppOwner,
48
+ createOwnerRow,
49
+ OwnerRow,
50
+ OwnerWithWriteAccess,
51
+ } from "./Owner.js";
52
+ import {
53
+ applyProtocolMessageAsClient,
54
+ Base64Url256,
55
+ BinaryId,
56
+ binaryIdToId,
57
+ CrdtMessage,
58
+ createProtocolMessageForSync,
59
+ createProtocolMessageFromCrdtMessages,
60
+ DbChange,
61
+ decryptAndDecodeDbChange,
62
+ encodeAndEncryptDbChange,
63
+ idToBinaryId,
64
+ ownerIdToBinaryOwnerId,
65
+ ProtocolError,
66
+ protocolVersion,
67
+ Storage,
68
+ StorageDep,
69
+ } from "./Protocol.js";
70
+ import {
71
+ createQueryRowsCache,
72
+ deserializeQuery,
73
+ emptyRows,
74
+ Query,
75
+ QueryRowsCache,
76
+ } from "./Query.js";
77
+ import {
78
+ createSqliteStorageBase,
79
+ CreateSqliteStorageBaseOptions,
80
+ SqliteStorageBase,
81
+ } from "./Storage.js";
82
+ import { CreateSyncDep, SyncConfig, SyncDep } from "./Sync.js";
83
+ import {
84
+ Millis,
85
+ receiveTimestamp,
86
+ sendTimestamp,
87
+ Timestamp,
88
+ TimestampConfigDep,
89
+ TimestampCounterOverflowError,
90
+ TimestampDriftError,
91
+ TimestampError,
92
+ TimestampString,
93
+ timestampStringToTimestamp,
94
+ TimestampTimeOutOfRangeError,
95
+ timestampToBinaryTimestamp,
96
+ timestampToTimestampString,
97
+ } from "./Timestamp.js";
98
+
99
+ export interface DbSchema {
100
+ readonly tables: ReadonlyArray<DbTable>;
101
+ readonly indexes: ReadonlyArray<DbIndex>;
102
+ }
103
+
104
+ export interface DbTable {
105
+ readonly name: Base64Url256;
106
+ readonly columns: ReadonlyArray<Base64Url256>;
107
+ }
108
+
109
+ export const DbIndex = object({ name: String, sql: String });
110
+ export type DbIndex = typeof DbIndex.Type;
111
+
112
+ export type DbWorker = Worker<DbWorkerInput, DbWorkerOutput>;
113
+
114
+ export type CreateDbWorker = (name: SimpleName) => DbWorker;
115
+
116
+ export interface CreateDbWorkerDep {
117
+ readonly createDbWorker: CreateDbWorker;
118
+ }
119
+
120
+ export type DbWorkerInput =
121
+ | {
122
+ readonly type: "init";
123
+ readonly config: Config;
124
+ readonly dbSchema: DbSchema;
125
+ readonly initialData: ReadonlyArray<DbChange>;
126
+ }
127
+ | {
128
+ readonly type: "mutate";
129
+ readonly tabId: Id;
130
+ readonly changes: NonEmptyReadonlyArray<DbChange>;
131
+ readonly onCompleteIds: ReadonlyArray<CallbackId>;
132
+ readonly subscribedQueries: ReadonlyArray<Query>;
133
+ }
134
+ | {
135
+ readonly type: "query";
136
+ readonly tabId: Id;
137
+ readonly queries: NonEmptyReadonlyArray<Query>;
138
+ }
139
+ | {
140
+ readonly type: "reset";
141
+ readonly onCompleteId: CallbackId;
142
+ readonly reload: boolean;
143
+ readonly restore?: {
144
+ readonly dbSchema: DbSchema;
145
+ readonly mnemonic: Mnemonic;
146
+ };
147
+ }
148
+ | {
149
+ readonly type: "ensureDbSchema";
150
+ readonly dbSchema: DbSchema;
151
+ }
152
+ | {
153
+ readonly type: "export";
154
+ readonly onCompleteId: CallbackId;
155
+ };
156
+
157
+ export type DbWorkerOutput =
158
+ | {
159
+ readonly type: "onInit";
160
+ readonly owner: AppOwner;
161
+ }
162
+ | {
163
+ readonly type: "onError";
164
+ readonly error:
165
+ | SqliteError
166
+ | TransferableError
167
+ | TimestampError
168
+ | ProtocolError
169
+ | SymmetricCryptoDecryptError;
170
+ }
171
+ | {
172
+ readonly type: "onChange";
173
+ readonly tabId: Id;
174
+ readonly patches: ReadonlyArray<QueryPatches>;
175
+ readonly onCompleteIds: ReadonlyArray<CallbackId>;
176
+ }
177
+ | {
178
+ readonly type: "onReceive";
179
+ readonly tabId?: Id;
180
+ }
181
+ | {
182
+ readonly type: "onReset";
183
+ readonly onCompleteId: CallbackId;
184
+ readonly reload: boolean;
185
+ }
186
+ | {
187
+ readonly type: "onExport";
188
+ readonly onCompleteId: CallbackId;
189
+ readonly file: Uint8Array;
190
+ };
191
+
192
+ export type DbWorkerPlatformDeps = CreateSqliteDriverDep &
193
+ CreateSyncDep &
194
+ ConsoleDep &
195
+ TimeDep &
196
+ RandomDep &
197
+ NanoIdLibDep &
198
+ CreateMnemonicDep &
199
+ CreateRandomBytesDep;
200
+
201
+ type DbWorkerDeps = Omit<
202
+ DbWorkerPlatformDeps,
203
+ keyof CreateSqliteDriverDep | keyof CreateSyncDep
204
+ > &
205
+ SqliteDep &
206
+ SyncDep &
207
+ TimestampConfigDep &
208
+ SymmetricCryptoDep &
209
+ PostMessageDep &
210
+ OwnerRowRefDep &
211
+ GetQueryRowsCacheDep &
212
+ ClientStorageDep;
213
+
214
+ type PostMessageDep = WorkerPostMessageDep<DbWorkerOutput>;
215
+
216
+ // TODO: More owners (the whole table with ad-hoc added)
217
+ export interface OwnerRowRefDep {
218
+ readonly ownerRowRef: Ref<OwnerRow>;
219
+ }
220
+
221
+ interface GetQueryRowsCacheDep {
222
+ readonly getQueryRowsCache: (tabId: Id) => QueryRowsCache;
223
+ }
224
+
225
+ export const createDbWorkerForPlatform = (
226
+ platformDeps: DbWorkerPlatformDeps,
227
+ ): DbWorker => {
228
+ const tabQueryRowsCacheMap = new Map<Id, QueryRowsCache>();
229
+ const getQueryRowsCache = (tabId: Id) => {
230
+ let cache = tabQueryRowsCacheMap.get(tabId);
231
+ if (!cache) {
232
+ cache = createQueryRowsCache();
233
+ tabQueryRowsCacheMap.set(tabId, cache);
234
+ }
235
+ return cache;
236
+ };
237
+
238
+ return createInitializedWorker<DbWorkerInput, DbWorkerOutput, DbWorkerDeps>({
239
+ init: async (initMessage, postMessage, safeHandler) => {
240
+ platformDeps.console.enabled = initMessage.config.enableLogging ?? false;
241
+
242
+ const sqliteResult = await createSqlite(platformDeps)(
243
+ initMessage.config.name,
244
+ );
245
+
246
+ if (!sqliteResult.ok) {
247
+ postMessage({ type: "onError", error: sqliteResult.error });
248
+ return null;
249
+ }
250
+ const sqlite = sqliteResult.value;
251
+
252
+ const deps = sqlite.transaction(() => {
253
+ const currentDbSchema = getDbSchema({ sqlite })();
254
+ if (!currentDbSchema.ok) return currentDbSchema;
255
+
256
+ const ensureDbSchemaResult = ensureDbSchema({ sqlite })(
257
+ initMessage.dbSchema,
258
+ currentDbSchema.value,
259
+ );
260
+ if (!ensureDbSchemaResult.ok) return ensureDbSchemaResult;
261
+
262
+ const maybeMigrateToVersion0Result = maybeMigrateToVersion0({ sqlite })(
263
+ currentDbSchema.value,
264
+ );
265
+ if (!maybeMigrateToVersion0Result.ok)
266
+ return maybeMigrateToVersion0Result;
267
+
268
+ const ownerExists = currentDbSchema.value.tables.some(
269
+ (table) => table.name === "evolu_owner",
270
+ );
271
+
272
+ const appOwnerAndOwnerRow = ownerExists
273
+ ? selectAppOwner({ sqlite })
274
+ : initializeDb({ ...platformDeps, sqlite })(
275
+ maybeMigrateToVersion0Result.value?.mnemonic ??
276
+ initMessage.config.mnemonic,
277
+ );
278
+ if (!appOwnerAndOwnerRow.ok) return appOwnerAndOwnerRow;
279
+
280
+ const [appOwner, ownerRow] = appOwnerAndOwnerRow.value;
281
+
282
+ const depsWithoutSyncAndStorage = {
283
+ ...platformDeps,
284
+ postMessage,
285
+ sqlite,
286
+ timestampConfig: initMessage.config,
287
+ symmetricCrypto: createSymmetricCrypto(platformDeps),
288
+ getQueryRowsCache,
289
+ ownerRowRef: createRef(ownerRow),
290
+ };
291
+
292
+ const storage = createClientStorage(depsWithoutSyncAndStorage)({
293
+ onStorageError: (error) => {
294
+ postMessage({ type: "onError", error });
295
+ },
296
+ });
297
+ if (!storage.ok) return storage;
298
+
299
+ const depsWithoutSync = {
300
+ ...depsWithoutSyncAndStorage,
301
+ storage: storage.value,
302
+ };
303
+
304
+ if (maybeMigrateToVersion0Result.value) {
305
+ const result = applyMessages(depsWithoutSync)(
306
+ maybeMigrateToVersion0Result.value.messages,
307
+ maybeMigrateToVersion0Result.value.lastTimestamp,
308
+ );
309
+ if (!result.ok) return result;
310
+ }
311
+
312
+ if (!ownerExists && isNonEmptyReadonlyArray(initMessage.initialData)) {
313
+ const result = applyChanges(depsWithoutSync)(initMessage.initialData);
314
+ if (!result.ok) return result;
315
+ }
316
+
317
+ postMessage({ type: "onInit", owner: appOwner });
318
+
319
+ const sync = platformDeps.createSync(platformDeps)({
320
+ ...initMessage.config,
321
+ onOpen: safeHandler(handleSyncOpen(depsWithoutSync)),
322
+ onMessage: safeHandler(createHandleSyncMessage(depsWithoutSync)),
323
+ });
324
+
325
+ return ok({ ...depsWithoutSync, sync });
326
+ });
327
+
328
+ if (!deps.ok) {
329
+ postMessage({ type: "onError", error: deps.error });
330
+ return null;
331
+ }
332
+
333
+ return deps.value;
334
+ },
335
+
336
+ onMessage: (deps) => (message) => {
337
+ switch (message.type) {
338
+ case "mutate": {
339
+ const mutate = deps.sqlite.transaction(() => {
340
+ const toSyncChanges = [];
341
+ const localOnlyChanges = [];
342
+
343
+ for (const change of message.changes) {
344
+ // Table name starting with '_' is local-only (not synced).
345
+ if (change.table.startsWith("_")) localOnlyChanges.push(change);
346
+ else toSyncChanges.push(change);
347
+ }
348
+
349
+ for (const change of localOnlyChanges) {
350
+ if (
351
+ "isDeleted" in change.values &&
352
+ change.values.isDeleted === 1
353
+ ) {
354
+ const result = deps.sqlite.exec(sql`
355
+ delete from ${sql.identifier(change.table)}
356
+ where id = ${change.id};
357
+ `);
358
+ if (!result.ok) return result;
359
+ } else {
360
+ const millis = Millis.from(deps.time.now());
361
+ if (!millis.ok) {
362
+ return err<TimestampTimeOutOfRangeError>({
363
+ type: "TimestampTimeOutOfRangeError",
364
+ });
365
+ }
366
+ const date = new Date(millis.value).toISOString();
367
+ for (const [column, value] of objectToEntries(change.values)) {
368
+ const result = deps.sqlite.exec(sql.prepared`
369
+ insert into ${sql.identifier(change.table)}
370
+ ("id", ${sql.identifier(column)}, createdAt, updatedAt)
371
+ values (${change.id}, ${value}, ${date}, ${date})
372
+ on conflict ("id") do update
373
+ set
374
+ ${sql.identifier(column)} = ${value},
375
+ updatedAt = ${date};
376
+ `);
377
+ if (!result.ok) return result;
378
+ }
379
+ }
380
+ }
381
+
382
+ /**
383
+ * We don't have to wait for the transaction to end because changes
384
+ * are idempotent, so there is no reason why they should fail. We
385
+ * want to call onChange ASAP.
386
+ */
387
+ const onChange = () => {
388
+ deps.console.log("[db]", "onChange", {
389
+ subscribedQueries: message.subscribedQueries,
390
+ });
391
+ const patches = loadQueries(deps)(
392
+ message.tabId,
393
+ message.subscribedQueries,
394
+ );
395
+ if (!patches.ok) {
396
+ deps.postMessage({ type: "onError", error: patches.error });
397
+ return;
398
+ }
399
+ // Notify the tab that performed the mutation.
400
+ deps.postMessage({
401
+ type: "onChange",
402
+ tabId: message.tabId,
403
+ patches: patches.value,
404
+ onCompleteIds: message.onCompleteIds,
405
+ });
406
+ // Notify other tabs to refresh their queries.
407
+ deps.postMessage({ type: "onReceive", tabId: message.tabId });
408
+ };
409
+
410
+ if (!isNonEmptyArray(toSyncChanges)) {
411
+ onChange();
412
+ return ok();
413
+ }
414
+
415
+ const messages = applyChanges(deps)(toSyncChanges, onChange);
416
+ if (!messages.ok) return messages;
417
+
418
+ const owner = deps.ownerRowRef.get();
419
+ // TODO: Check owner whether it's allowed to write, return an
420
+ // error if not.
421
+ if (owner.writeKey == null) {
422
+ return ok();
423
+ }
424
+
425
+ const protocolMessage = createProtocolMessageFromCrdtMessages(deps)(
426
+ owner as OwnerWithWriteAccess,
427
+ messages.value,
428
+ );
429
+
430
+ deps.console.log(
431
+ "[db]",
432
+ "send data message",
433
+ messages.value,
434
+ protocolMessage,
435
+ );
436
+ deps.sync.send(protocolMessage);
437
+
438
+ return ok();
439
+ });
440
+
441
+ if (!mutate.ok) {
442
+ deps.postMessage({ type: "onError", error: mutate.error });
443
+ return;
444
+ }
445
+
446
+ break;
447
+ }
448
+
449
+ case "query": {
450
+ const patches = loadQueries(deps)(message.tabId, message.queries);
451
+
452
+ if (!patches.ok) {
453
+ deps.postMessage({ type: "onError", error: patches.error });
454
+ return;
455
+ }
456
+
457
+ deps.postMessage({
458
+ type: "onChange",
459
+ tabId: message.tabId,
460
+ patches: patches.value,
461
+ onCompleteIds: [],
462
+ });
463
+ break;
464
+ }
465
+
466
+ case "reset": {
467
+ const resetResult = deps.sqlite.transaction(() => {
468
+ const dropAllTablesResult = dropAllTables(deps);
469
+ if (!dropAllTablesResult.ok) return dropAllTablesResult;
470
+
471
+ if (message.restore) {
472
+ const dbSchema = getDbSchema(deps)();
473
+ if (!dbSchema.ok) return dbSchema;
474
+
475
+ const ensureDbSchemaResult = ensureDbSchema(deps)(
476
+ message.restore.dbSchema,
477
+ dbSchema.value,
478
+ );
479
+ if (!ensureDbSchemaResult.ok) return ensureDbSchemaResult;
480
+
481
+ const initializeDbResult = initializeDb(deps)(
482
+ message.restore.mnemonic,
483
+ );
484
+ if (!initializeDbResult.ok) return initializeDbResult;
485
+ }
486
+ return ok();
487
+ });
488
+
489
+ if (!resetResult.ok) {
490
+ deps.postMessage({ type: "onError", error: resetResult.error });
491
+ return;
492
+ }
493
+
494
+ deps.postMessage({
495
+ type: "onReset",
496
+ onCompleteId: message.onCompleteId,
497
+ reload: message.reload,
498
+ });
499
+ deps.sqlite[Symbol.dispose]();
500
+
501
+ break;
502
+ }
503
+
504
+ case "ensureDbSchema": {
505
+ const ensureSchema = deps.sqlite.transaction(() => {
506
+ const dbSchema = getDbSchema(deps)();
507
+ if (!dbSchema.ok) return dbSchema;
508
+
509
+ const ensureDbSchemaResult = ensureDbSchema(deps)(
510
+ message.dbSchema,
511
+ dbSchema.value,
512
+ );
513
+ if (!ensureDbSchemaResult.ok) return ensureDbSchemaResult;
514
+
515
+ return ok();
516
+ });
517
+
518
+ if (!ensureSchema.ok) {
519
+ deps.postMessage({ type: "onError", error: ensureSchema.error });
520
+ return;
521
+ }
522
+ break;
523
+ }
524
+
525
+ case "export": {
526
+ const file = deps.sqlite.export();
527
+
528
+ if (!file.ok) {
529
+ deps.postMessage({ type: "onError", error: file.error });
530
+ return;
531
+ }
532
+
533
+ deps.postMessage({
534
+ type: "onExport",
535
+ onCompleteId: message.onCompleteId,
536
+ file: file.value,
537
+ });
538
+ break;
539
+ }
540
+
541
+ default:
542
+ exhaustiveCheck(message);
543
+ }
544
+ },
545
+ });
546
+ };
547
+
548
+ /**
549
+ * Get the current database schema by reading SQLite metadata.
550
+ *
551
+ * TODO: Refactor out Evolu stuff and move it to Sqlite.
552
+ */
553
+ export const getDbSchema =
554
+ (deps: SqliteDep) =>
555
+ ({ allIndexes = false }: { allIndexes?: boolean } = {}): Result<
556
+ DbSchema,
557
+ SqliteError
558
+ > => {
559
+ const map = new Map<Base64Url256, Array<Base64Url256>>();
560
+
561
+ const tableAndColumnInfoRows = deps.sqlite.exec(sql`
562
+ select
563
+ sqlite_master.name as tableName,
564
+ table_info.name as columnName
565
+ from
566
+ sqlite_master
567
+ join pragma_table_info(sqlite_master.name) as table_info;
568
+ `);
569
+
570
+ if (!tableAndColumnInfoRows.ok) return tableAndColumnInfoRows;
571
+
572
+ tableAndColumnInfoRows.value.rows.forEach((row) => {
573
+ const { tableName, columnName } = row as unknown as {
574
+ tableName: Base64Url256;
575
+ columnName: Base64Url256;
576
+ };
577
+ if (!map.has(tableName)) map.set(tableName, []);
578
+ map.get(tableName)?.push(columnName);
579
+ });
580
+
581
+ const tables = Array.from(map, ([name, columns]) => ({ name, columns }));
582
+
583
+ const indexesRows = deps.sqlite.exec(
584
+ allIndexes
585
+ ? sql`
586
+ select name, sql
587
+ from sqlite_master
588
+ where type = 'index' and name not like 'sqlite_%';
589
+ `
590
+ : sql`
591
+ select name, sql
592
+ from sqlite_master
593
+ where
594
+ type = 'index'
595
+ and name not like 'sqlite_%'
596
+ and name not like 'evolu_%';
597
+ `,
598
+ );
599
+
600
+ if (!indexesRows.ok) return indexesRows;
601
+
602
+ const indexes = indexesRows.value.rows.map(
603
+ (row): DbIndex => ({
604
+ name: row.name as string,
605
+ /**
606
+ * SQLite returns "CREATE INDEX" for "create index" for some reason.
607
+ * Other keywords remain unchanged. We have to normalize the casing for
608
+ * {@link indexesAreEqual} manually.
609
+ */
610
+ sql: (row.sql as string)
611
+ .replace("CREATE INDEX", "create index")
612
+ .replace("CREATE UNIQUE INDEX", "create unique index"),
613
+ }),
614
+ );
615
+
616
+ return ok({ tables, indexes });
617
+ };
618
+
619
+ const indexesAreEqual = (self: DbIndex, that: DbIndex): boolean =>
620
+ self.name === that.name && self.sql === that.sql;
621
+
622
+ export interface DbSnapshot {
623
+ readonly schema: DbSchema;
624
+ readonly rows: Array<{
625
+ rows: ReadonlyArray<SqliteRow>;
626
+ name: string;
627
+ }>;
628
+ }
629
+
630
+ // TODO: Move it to Sqlite.
631
+ export const getDbSnapshot = (
632
+ deps: SqliteDep,
633
+ ): Result<DbSnapshot, SqliteError> => {
634
+ const schema = getDbSchema(deps)({ allIndexes: true });
635
+ if (!schema.ok) return schema;
636
+
637
+ const rows = [];
638
+
639
+ for (const table of schema.value.tables) {
640
+ const result = deps.sqlite.exec(sql`
641
+ select * from ${sql.identifier(table.name)};
642
+ `);
643
+ if (!result.ok) return result;
644
+
645
+ rows.push({
646
+ rows: result.value.rows,
647
+ name: table.name,
648
+ });
649
+ }
650
+
651
+ return ok({ schema: schema.value, rows });
652
+ };
653
+
654
+ const ensureDbSchema =
655
+ (deps: SqliteDep) =>
656
+ (
657
+ newSchema: DbSchema,
658
+ currentSchema: DbSchema,
659
+ options?: { ignoreIndexes: boolean },
660
+ ): Result<void, SqliteError> => {
661
+ const queries: Array<SqliteQuery> = [];
662
+
663
+ newSchema.tables.forEach((newTable) => {
664
+ const currentTable = currentSchema.tables.find(
665
+ (t) => t.name === newTable.name,
666
+ );
667
+ if (!currentTable) {
668
+ queries.push({
669
+ sql: createAppTable(newTable.name, newTable.columns),
670
+ parameters: [],
671
+ });
672
+ } else {
673
+ newTable.columns
674
+ .filter((newColumn) => !currentTable.columns.includes(newColumn))
675
+ .forEach((newColumn) => {
676
+ queries.push(sql`
677
+ alter table ${sql.identifier(newTable.name)}
678
+ add column ${sql.identifier(newColumn)} blob;
679
+ `);
680
+ });
681
+ }
682
+ });
683
+
684
+ if (options?.ignoreIndexes !== true) {
685
+ // Remove current indexes that are not in the newSchema.
686
+ currentSchema.indexes
687
+ .filter(
688
+ (currentIndex) =>
689
+ !newSchema.indexes.some((newIndex) =>
690
+ indexesAreEqual(newIndex, currentIndex),
691
+ ),
692
+ )
693
+ .forEach((index) => {
694
+ queries.push(sql`drop index ${sql.identifier(index.name)};`);
695
+ });
696
+
697
+ // Add new indexes that are not in the currentSchema.
698
+ newSchema.indexes
699
+ .filter(
700
+ (newIndex) =>
701
+ !currentSchema.indexes.some((currentIndex) =>
702
+ indexesAreEqual(newIndex, currentIndex),
703
+ ),
704
+ )
705
+ .forEach((newIndex) => {
706
+ queries.push({ sql: `${newIndex.sql};` as SafeSql, parameters: [] });
707
+ });
708
+ }
709
+
710
+ for (const query of queries) {
711
+ const result = deps.sqlite.exec(query);
712
+ if (!result.ok) return result;
713
+ }
714
+ return ok();
715
+ };
716
+
717
+ export const createAppTable = (
718
+ tableName: string,
719
+ columns: ReadonlyArray<string>,
720
+ ): SafeSql =>
721
+ `
722
+ create table ${sql.identifier(tableName).sql} (
723
+ "id" text primary key,
724
+ ${columns
725
+ // Add default columns.
726
+ .concat(["createdAt", "updatedAt", "isDeleted"])
727
+ .filter((c) => c !== "id")
728
+ // "A column with affinity BLOB does not prefer one storage class over another
729
+ // and no attempt is made to coerce data from one storage class into another."
730
+ // https://www.sqlite.org/datatype3.html
731
+ .map((name) => `${sql.identifier(name).sql} blob`)
732
+ .join(", ")}
733
+ );
734
+ ` as SafeSql;
735
+
736
+ const selectAppOwner = (
737
+ deps: SqliteDep,
738
+ ): Result<[AppOwner, OwnerRow], SqliteError> => {
739
+ const result = deps.sqlite.exec<OwnerRow>(sql`
740
+ select mnemonic, id, createdAt, encryptionKey, writeKey, timestamp
741
+ from evolu_owner
742
+ order by createdAt asc
743
+ limit 1;
744
+ `);
745
+
746
+ if (!result.ok) return result;
747
+
748
+ const {
749
+ rows: [ownerRow],
750
+ } = result.value;
751
+
752
+ assert(ownerRow.writeKey != null, "The writeKey is null");
753
+
754
+ const appOwner: AppOwner = {
755
+ type: "AppOwner",
756
+ mnemonic: ownerRow.mnemonic,
757
+ createdAt: ownerRow.createdAt,
758
+ id: ownerRow.id,
759
+ encryptionKey: ownerRow.encryptionKey,
760
+ writeKey: ownerRow.writeKey,
761
+ };
762
+
763
+ return ok([appOwner, ownerRow]);
764
+ };
765
+
766
+ const initializeDb =
767
+ (
768
+ deps: SqliteDep &
769
+ NanoIdLibDep &
770
+ CreateMnemonicDep &
771
+ TimeDep &
772
+ CreateRandomBytesDep,
773
+ ) =>
774
+ (mnemonic?: Mnemonic): Result<[AppOwner, OwnerRow], SqliteError> => {
775
+ for (const query of [
776
+ sql`
777
+ create table evolu_config (
778
+ "key" text not null primary key,
779
+ "value" any not null
780
+ )
781
+ strict;
782
+ `,
783
+
784
+ sql`
785
+ insert into evolu_config ("key", "value")
786
+ values ('protocolVersion', ${protocolVersion});
787
+ `,
788
+
789
+ /**
790
+ * The History table stores all values per timestamp, table, row, and
791
+ * column. It's required for merging without conflicts. Evolu uses
792
+ * last-write-win CRDT. In case of last-write-win value isn't what we
793
+ * want, we can use time travel. The current implementation prefers
794
+ * performance over storage size. The History table denormalizes Timestamp
795
+ * and DbChange to leverage a covering index. Hence, every value change
796
+ * has its timestamp, table, row, and column. In the future, we will
797
+ * rethink that and store history more efficiently.
798
+ *
799
+ * There is no need to use `OwnerId` on the client, because timestamps
800
+ * (which include a `NodeId`) are globally unique. The relay needs
801
+ * `OwnerId` because it hosts multiple apps/owners, but client storage
802
+ * represents one DB, and even when it hosts multiple owners, their
803
+ * timestamps remain unique.
804
+ */
805
+ sql`
806
+ create table evolu_history (
807
+ "timestamp" blob not null,
808
+ "table" text not null,
809
+ "row" blob not null,
810
+ "column" text not null,
811
+ "value" any
812
+ )
813
+ strict;
814
+ `,
815
+
816
+ sql`
817
+ create index evolu_history_timestamp on evolu_history ("timestamp");
818
+ `,
819
+
820
+ sql`
821
+ create unique index evolu_history_row_column_table_timestampDesc on evolu_history (
822
+ "row",
823
+ "column",
824
+ "table",
825
+ "timestamp" desc
826
+ );
827
+ `,
828
+
829
+ sql`
830
+ create table evolu_owner (
831
+ "mnemonic" text not null primary key,
832
+ "id" text not null,
833
+ "encryptionKey" blob not null,
834
+ "createdAt" text not null,
835
+ "writeKey" blob,
836
+ "timestamp" text not null
837
+ )
838
+ strict;
839
+ `,
840
+ ]) {
841
+ const result = deps.sqlite.exec(query);
842
+ if (!result.ok) return result;
843
+ }
844
+
845
+ const appOwner = createAppOwner(deps)(mnemonic);
846
+ const ownerRow = createOwnerRow(deps)(appOwner);
847
+
848
+ const result = deps.sqlite.exec(sql`
849
+ insert into evolu_owner
850
+ (mnemonic, id, encryptionKey, createdAt, writeKey, timestamp)
851
+ values
852
+ (
853
+ ${ownerRow.mnemonic},
854
+ ${ownerRow.id},
855
+ ${ownerRow.encryptionKey},
856
+ ${ownerRow.createdAt},
857
+ ${ownerRow.writeKey},
858
+ ${ownerRow.timestamp}
859
+ );
860
+ `);
861
+
862
+ if (!result.ok) return result;
863
+
864
+ return ok([appOwner, ownerRow]);
865
+ };
866
+
867
+ const applyChanges =
868
+ (
869
+ deps: SqliteDep &
870
+ TimeDep &
871
+ TimestampConfigDep &
872
+ RandomDep &
873
+ OwnerRowRefDep &
874
+ ClientStorageDep,
875
+ ) =>
876
+ (
877
+ changes: NonEmptyReadonlyArray<DbChange>,
878
+ onChange?: () => void,
879
+ ): Result<
880
+ NonEmptyReadonlyArray<CrdtMessage>,
881
+ | TimestampTimeOutOfRangeError
882
+ | TimestampDriftError
883
+ | TimestampCounterOverflowError
884
+ | SqliteError
885
+ > => {
886
+ let lastTimestamp = timestampStringToTimestamp(
887
+ deps.ownerRowRef.get().timestamp,
888
+ );
889
+
890
+ const messages: Array<CrdtMessage> = [];
891
+
892
+ for (const change of changes) {
893
+ const nextTimestamp = sendTimestamp(deps)(lastTimestamp);
894
+ if (!nextTimestamp.ok) return nextTimestamp;
895
+ lastTimestamp = nextTimestamp.value;
896
+ messages.push({ timestamp: lastTimestamp, change });
897
+ }
898
+
899
+ const apply = applyMessages(deps)(messages, lastTimestamp);
900
+ if (!apply.ok) return apply;
901
+
902
+ if (onChange) onChange();
903
+
904
+ assertNonEmptyReadonlyArray(messages);
905
+ return ok(messages);
906
+ };
907
+
908
+ const applyMessages =
909
+ (deps: SqliteDep & RandomDep & OwnerRowRefDep & ClientStorageDep) =>
910
+ (
911
+ messages: ReadonlyArray<CrdtMessage>,
912
+ lastTimestamp: Timestamp,
913
+ options: { isMigrationFromVersion0?: boolean } = {},
914
+ ): Result<void, SqliteError> => {
915
+ for (const message of messages) {
916
+ if (!options.isMigrationFromVersion0) {
917
+ const apply1 = applyMessageToAppTable(deps)(message);
918
+ if (!apply1.ok) return apply1;
919
+ }
920
+
921
+ const apply2 = applyMessageToTimestampAndHistoryTables(deps)(message);
922
+ if (!apply2.ok) return apply2;
923
+ }
924
+
925
+ const timestamp = timestampToTimestampString(lastTimestamp);
926
+ deps.ownerRowRef.modify((owner) => ({ ...owner, timestamp }));
927
+ const saveTimestamp = deps.sqlite.exec(sql.prepared`
928
+ update evolu_owner set "timestamp" = ${timestamp};
929
+ `);
930
+ if (!saveTimestamp.ok) return saveTimestamp;
931
+
932
+ return ok();
933
+ };
934
+
935
+ const applyMessageToAppTable =
936
+ (deps: SqliteDep) =>
937
+ (message: CrdtMessage): Result<void, SqliteError> => {
938
+ const date = new Date(message.timestamp.millis).toISOString();
939
+ const timestamp = timestampToBinaryTimestamp(message.timestamp);
940
+
941
+ for (const [column, value] of objectToEntries(message.change.values)) {
942
+ const result = deps.sqlite.exec(sql.prepared`
943
+ with
944
+ lastTimestamp as (
945
+ select timestamp
946
+ from evolu_history
947
+ where
948
+ "row" = ${message.change.id}
949
+ and "column" = ${column}
950
+ and "table" = ${message.change.table}
951
+ order by timestamp desc
952
+ limit 1
953
+ )
954
+ insert into ${sql.identifier(message.change.table)}
955
+ ("id", ${sql.identifier(column)}, createdAt, updatedAt)
956
+ select ${message.change.id}, ${value}, ${date}, ${date}
957
+ where
958
+ (select timestamp from lastTimestamp) is null
959
+ or (select timestamp from lastTimestamp) < ${timestamp}
960
+ on conflict ("id") do update
961
+ set
962
+ ${sql.identifier(column)} = ${value},
963
+ updatedAt = ${date}
964
+ where
965
+ (select timestamp from lastTimestamp) is null
966
+ or (select timestamp from lastTimestamp) < ${timestamp};
967
+ `);
968
+
969
+ if (!result.ok) return result;
970
+ }
971
+
972
+ return ok();
973
+ };
974
+
975
+ export const applyMessageToTimestampAndHistoryTables =
976
+ (deps: SqliteDep & RandomDep & OwnerRowRefDep & ClientStorageDep) =>
977
+ (message: CrdtMessage): Result<void, SqliteError> => {
978
+ const timestamp = timestampToBinaryTimestamp(message.timestamp);
979
+ const ownerId = ownerIdToBinaryOwnerId(deps.ownerRowRef.get().id);
980
+ const id = idToBinaryId(message.change.id);
981
+
982
+ const result = deps.storage.insertTimestamp(ownerId, timestamp);
983
+ if (!result.ok) return result;
984
+
985
+ for (const [column, value] of Object.entries(message.change.values)) {
986
+ const result = deps.sqlite.exec(sql.prepared`
987
+ insert into evolu_history
988
+ ("timestamp", "table", "row", "column", "value")
989
+ values
990
+ (${timestamp}, ${message.change.table}, ${id}, ${column}, ${value})
991
+ on conflict do nothing;
992
+ `);
993
+ if (!result.ok) return result;
994
+ }
995
+
996
+ return ok();
997
+ };
998
+
999
+ const loadQueries =
1000
+ (deps: GetQueryRowsCacheDep & SqliteDep) =>
1001
+ (
1002
+ tabId: Id,
1003
+ queries: ReadonlyArray<Query>,
1004
+ ): Result<ReadonlyArray<QueryPatches>, SqliteError> => {
1005
+ const queriesRows = [];
1006
+
1007
+ for (const query of queries) {
1008
+ const sqlQuery = deserializeQuery(query);
1009
+ const result = deps.sqlite.exec(sqlQuery);
1010
+ if (!result.ok) return result;
1011
+
1012
+ queriesRows.push([query, result.value.rows] as const);
1013
+ if (sqlQuery.options?.logExplainQueryPlan) {
1014
+ explainSqliteQueryPlan(deps)(sqlQuery);
1015
+ }
1016
+ }
1017
+
1018
+ const queryRowsCache = deps.getQueryRowsCache(tabId);
1019
+
1020
+ const previousState = queryRowsCache.get();
1021
+ queryRowsCache.set(queriesRows);
1022
+
1023
+ const currentState = queryRowsCache.get();
1024
+
1025
+ const queryPatchesArray = queries.map(
1026
+ (query): QueryPatches => ({
1027
+ query,
1028
+ patches: makePatches(
1029
+ previousState.get(query),
1030
+ currentState.get(query) ?? emptyRows,
1031
+ ),
1032
+ }),
1033
+ );
1034
+ return ok(queryPatchesArray);
1035
+ };
1036
+
1037
+ export const maybeMigrateToVersion0 =
1038
+ (deps: SqliteDep) =>
1039
+ (
1040
+ schema: DbSchema,
1041
+ ): Result<
1042
+ {
1043
+ readonly messages: ReadonlyArray<CrdtMessage>;
1044
+ readonly mnemonic: Mnemonic;
1045
+ readonly lastTimestamp: Timestamp;
1046
+ } | null,
1047
+ SqliteError
1048
+ > => {
1049
+ // evolu_history is a new table
1050
+ const hasOwnerButNoHistory =
1051
+ schema.tables.some((t) => t.name === "evolu_owner") &&
1052
+ !schema.tables.some((t) => t.name === "evolu_history");
1053
+ if (!hasOwnerButNoHistory) {
1054
+ return ok(null);
1055
+ }
1056
+
1057
+ const mnemonicAndLastTimestamp = deps.sqlite.exec<{
1058
+ mnemonic: Mnemonic;
1059
+ timestamp: TimestampString;
1060
+ }>(sql` select mnemonic, timestamp from evolu_owner limit 1; `);
1061
+ if (!mnemonicAndLastTimestamp.ok) return mnemonicAndLastTimestamp;
1062
+
1063
+ const messagesRows = deps.sqlite.exec<{
1064
+ timestamp: TimestampString;
1065
+ table: Base64Url256;
1066
+ row: Id;
1067
+ column: Base64Url256;
1068
+ value: SqliteValue;
1069
+ }>(sql`
1070
+ select "timestamp", "table", "row", "column", "value" from evolu_message;
1071
+ `);
1072
+
1073
+ if (!messagesRows.ok) return messagesRows;
1074
+
1075
+ for (const query of [
1076
+ sql`drop table evolu_owner;`,
1077
+ sql`drop table evolu_message;`,
1078
+ ]) {
1079
+ const result = deps.sqlite.exec(query);
1080
+ if (!result.ok) return result;
1081
+ }
1082
+
1083
+ const messages = messagesRows.value.rows.map((message) => ({
1084
+ timestamp: timestampStringToTimestamp(message.timestamp),
1085
+ change: {
1086
+ id: message.row,
1087
+ table: message.table,
1088
+ values: { [message.column]: message.value },
1089
+ },
1090
+ }));
1091
+
1092
+ const {
1093
+ rows: [{ mnemonic, timestamp }],
1094
+ } = mnemonicAndLastTimestamp.value;
1095
+ const lastTimestamp = timestampStringToTimestamp(timestamp);
1096
+
1097
+ return ok({ messages, mnemonic, lastTimestamp });
1098
+ };
1099
+
1100
+ const dropAllTables = (deps: SqliteDep): Result<void, SqliteError> => {
1101
+ const schema = getDbSchema(deps)();
1102
+ if (!schema.ok) return schema;
1103
+ for (const table of schema.value.tables) {
1104
+ /**
1105
+ * The dropped table is completely removed from the database schema and the
1106
+ * disk file. The table can not be recovered. All indices and triggers
1107
+ * associated with the table are also deleted.
1108
+ * https://sqlite.org/lang_droptable.html
1109
+ */
1110
+ const result = deps.sqlite.exec(sql`
1111
+ drop table ${sql.identifier(table.name)};
1112
+ `);
1113
+ if (!result.ok) return result;
1114
+ }
1115
+ return ok();
1116
+ };
1117
+
1118
+ const handleSyncOpen =
1119
+ (deps: OwnerRowRefDep & StorageDep & ConsoleDep): SyncConfig["onOpen"] =>
1120
+ (send) => {
1121
+ const ownerId = deps.ownerRowRef.get().id;
1122
+ const message = createProtocolMessageForSync(deps)(ownerId);
1123
+ if (message) {
1124
+ deps.console.log("[db]", "send initial sync message", message);
1125
+ send(message);
1126
+ }
1127
+ };
1128
+
1129
+ const createHandleSyncMessage =
1130
+ (
1131
+ deps: PostMessageDep & StorageDep & SqliteDep & ConsoleDep & OwnerRowRefDep,
1132
+ ): SyncConfig["onMessage"] =>
1133
+ (input, send) => {
1134
+ deps.console.log("[db]", "receive sync message", input);
1135
+ const { writeKey } = deps.ownerRowRef.get();
1136
+
1137
+ const output = deps.sqlite.transaction(() =>
1138
+ applyProtocolMessageAsClient(deps)(input, {
1139
+ getWriteKey: (_ownerId) => writeKey,
1140
+ }),
1141
+ );
1142
+ if (!output.ok) {
1143
+ deps.postMessage({ type: "onError", error: output.error });
1144
+ return;
1145
+ }
1146
+
1147
+ if (output.value) {
1148
+ deps.console.log("[db]", "respond sync message", output.value);
1149
+ send(output.value);
1150
+ }
1151
+ };
1152
+
1153
+ export interface ClientStorage extends SqliteStorageBase, Storage {}
1154
+
1155
+ export interface ClientStorageDep {
1156
+ readonly storage: ClientStorage;
1157
+ }
1158
+
1159
+ const createClientStorage =
1160
+ (
1161
+ deps: SqliteDep &
1162
+ PostMessageDep &
1163
+ SymmetricCryptoDep &
1164
+ OwnerRowRefDep &
1165
+ RandomDep &
1166
+ TimeDep &
1167
+ TimestampConfigDep,
1168
+ ) =>
1169
+ (
1170
+ options: CreateSqliteStorageBaseOptions,
1171
+ ): Result<ClientStorage, SqliteError> => {
1172
+ const sqliteStorageBase = createSqliteStorageBase(deps)(options);
1173
+ if (!sqliteStorageBase.ok) return sqliteStorageBase;
1174
+
1175
+ const storage: ClientStorage = {
1176
+ ...sqliteStorageBase.value,
1177
+
1178
+ validateWriteKey: constFalse,
1179
+
1180
+ writeMessages: (_ownerId, messages) => {
1181
+ // TODO: Get owner by _ownerId when we support more.
1182
+ const owner = deps.ownerRowRef.get();
1183
+ const decodedAndDecryptedMessages: Array<CrdtMessage> = [];
1184
+
1185
+ for (const message of messages) {
1186
+ const dbChange = decryptAndDecodeDbChange(deps)(
1187
+ message.change,
1188
+ owner.encryptionKey,
1189
+ );
1190
+
1191
+ if (!dbChange.ok) {
1192
+ deps.postMessage({
1193
+ type: "onError",
1194
+ error: dbChange.error,
1195
+ });
1196
+ return false;
1197
+ }
1198
+
1199
+ decodedAndDecryptedMessages.push({
1200
+ timestamp: message.timestamp,
1201
+ change: dbChange.value,
1202
+ });
1203
+ }
1204
+
1205
+ let timestamp = timestampStringToTimestamp(owner.timestamp);
1206
+
1207
+ for (const message of messages) {
1208
+ const receive = receiveTimestamp(deps)(timestamp, message.timestamp);
1209
+ if (!receive.ok) {
1210
+ deps.postMessage({ type: "onError", error: receive.error });
1211
+ return false;
1212
+ }
1213
+ timestamp = receive.value;
1214
+ }
1215
+
1216
+ const applyMessagesResult = applyMessages({ ...deps, storage })(
1217
+ decodedAndDecryptedMessages,
1218
+ timestamp,
1219
+ );
1220
+
1221
+ if (!applyMessagesResult.ok) {
1222
+ deps.postMessage({
1223
+ type: "onError",
1224
+ error: applyMessagesResult.error,
1225
+ });
1226
+ return false;
1227
+ }
1228
+
1229
+ deps.postMessage({ type: "onReceive" });
1230
+
1231
+ return true;
1232
+ },
1233
+
1234
+ readDbChange: (_ownerId, timestamp) => {
1235
+ const result = deps.sqlite.exec<{
1236
+ table: Base64Url256;
1237
+ row: BinaryId;
1238
+ column: Base64Url256;
1239
+ value: SqliteValue;
1240
+ }>(sql`
1241
+ select "table", "row", "column", "value"
1242
+ from evolu_history
1243
+ where "timestamp" = ${timestamp};
1244
+ `);
1245
+ if (!result.ok) {
1246
+ deps.postMessage({ type: "onError", error: result.error });
1247
+ return null;
1248
+ }
1249
+
1250
+ const { rows } = result.value;
1251
+ assert(rows.length > 0, "Rows must not be empty");
1252
+
1253
+ const { table, row } = rows[0];
1254
+ const values: Record<string, SqliteValue> = {};
1255
+
1256
+ for (const r of rows) {
1257
+ assert(r.table === table, "All rows must have the same table");
1258
+ assert(eqArrayNumber(r.row, row), "All rows must have the same Id");
1259
+ values[r.column] = r.value;
1260
+ }
1261
+
1262
+ const change: DbChange = {
1263
+ table: rows[0].table,
1264
+ id: binaryIdToId(rows[0].row),
1265
+ values,
1266
+ };
1267
+
1268
+ const { encryptionKey } = deps.ownerRowRef.get();
1269
+
1270
+ return encodeAndEncryptDbChange(deps)(change, encryptionKey);
1271
+ },
1272
+ };
1273
+
1274
+ return ok(storage);
1275
+ };