@evolu/common 7.4.0 → 8.0.0-next.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 (251) hide show
  1. package/README.md +4 -61
  2. package/dist/src/Array.d.ts +343 -102
  3. package/dist/src/Array.d.ts.map +1 -1
  4. package/dist/src/Array.js +181 -85
  5. package/dist/src/Assert.d.ts +57 -11
  6. package/dist/src/Assert.d.ts.map +1 -1
  7. package/dist/src/Assert.js +47 -11
  8. package/dist/src/BigInt.d.ts +6 -1
  9. package/dist/src/BigInt.d.ts.map +1 -1
  10. package/dist/src/BigInt.js +5 -0
  11. package/dist/src/Brand.d.ts +40 -12
  12. package/dist/src/Brand.d.ts.map +1 -1
  13. package/dist/src/Brand.js +5 -0
  14. package/dist/src/Buffer.d.ts +6 -1
  15. package/dist/src/Buffer.d.ts.map +1 -1
  16. package/dist/src/Buffer.js +7 -2
  17. package/dist/src/Cache.d.ts +10 -2
  18. package/dist/src/Cache.d.ts.map +1 -1
  19. package/dist/src/Cache.js +6 -0
  20. package/dist/src/Callbacks.d.ts +10 -7
  21. package/dist/src/Callbacks.d.ts.map +1 -1
  22. package/dist/src/Callbacks.js +23 -13
  23. package/dist/src/Console.d.ts +361 -69
  24. package/dist/src/Console.d.ts.map +1 -1
  25. package/dist/src/Console.js +217 -117
  26. package/dist/src/Crypto.d.ts +70 -21
  27. package/dist/src/Crypto.d.ts.map +1 -1
  28. package/dist/src/Crypto.js +72 -27
  29. package/dist/src/Eq.d.ts +22 -2
  30. package/dist/src/Eq.d.ts.map +1 -1
  31. package/dist/src/Eq.js +21 -1
  32. package/dist/src/Error.d.ts +23 -12
  33. package/dist/src/Error.d.ts.map +1 -1
  34. package/dist/src/Error.js +27 -11
  35. package/dist/src/Function.d.ts +128 -56
  36. package/dist/src/Function.d.ts.map +1 -1
  37. package/dist/src/Function.js +118 -14
  38. package/dist/src/Identicon.d.ts +5 -0
  39. package/dist/src/Identicon.d.ts.map +1 -1
  40. package/dist/src/Identicon.js +6 -1
  41. package/dist/src/Lookup.d.ts +160 -0
  42. package/dist/src/Lookup.d.ts.map +1 -0
  43. package/dist/src/Lookup.js +192 -0
  44. package/dist/src/Microtask.d.ts +21 -0
  45. package/dist/src/Microtask.d.ts.map +1 -0
  46. package/dist/src/Microtask.js +37 -0
  47. package/dist/src/Number.d.ts +18 -4
  48. package/dist/src/Number.d.ts.map +1 -1
  49. package/dist/src/Number.js +23 -1
  50. package/dist/src/Object.d.ts +110 -5
  51. package/dist/src/Object.d.ts.map +1 -1
  52. package/dist/src/Object.js +108 -5
  53. package/dist/src/Option.d.ts +58 -0
  54. package/dist/src/Option.d.ts.map +1 -0
  55. package/dist/src/Option.js +43 -0
  56. package/dist/src/Order.d.ts +5 -0
  57. package/dist/src/Order.d.ts.map +1 -1
  58. package/dist/src/Order.js +8 -3
  59. package/dist/src/Platform.d.ts +36 -2
  60. package/dist/src/Platform.d.ts.map +1 -1
  61. package/dist/src/Platform.js +10 -6
  62. package/dist/src/Polyfills.d.ts +27 -0
  63. package/dist/src/Polyfills.d.ts.map +1 -0
  64. package/dist/src/Polyfills.js +299 -0
  65. package/dist/src/Random.d.ts +18 -7
  66. package/dist/src/Random.d.ts.map +1 -1
  67. package/dist/src/Random.js +9 -7
  68. package/dist/src/Redacted.d.ts +7 -4
  69. package/dist/src/Redacted.d.ts.map +1 -1
  70. package/dist/src/Redacted.js +5 -0
  71. package/dist/src/Ref.d.ts +31 -16
  72. package/dist/src/Ref.d.ts.map +1 -1
  73. package/dist/src/Ref.js +35 -7
  74. package/dist/src/RefCount.d.ts +62 -0
  75. package/dist/src/RefCount.d.ts.map +1 -0
  76. package/dist/src/RefCount.js +83 -0
  77. package/dist/src/Relation.d.ts +84 -29
  78. package/dist/src/Relation.d.ts.map +1 -1
  79. package/dist/src/Relation.js +83 -105
  80. package/dist/src/Resource.d.ts +263 -0
  81. package/dist/src/Resource.d.ts.map +1 -0
  82. package/dist/src/Resource.js +389 -0
  83. package/dist/src/Result.d.ts +390 -374
  84. package/dist/src/Result.d.ts.map +1 -1
  85. package/dist/src/Result.js +142 -70
  86. package/dist/src/Schedule.d.ts +953 -0
  87. package/dist/src/Schedule.d.ts.map +1 -0
  88. package/dist/src/Schedule.js +1199 -0
  89. package/dist/src/Set.d.ts +181 -0
  90. package/dist/src/Set.d.ts.map +1 -0
  91. package/dist/src/Set.js +137 -0
  92. package/dist/src/Skiplist.d.ts +6 -1
  93. package/dist/src/Skiplist.d.ts.map +1 -1
  94. package/dist/src/Skiplist.js +5 -0
  95. package/dist/src/Sqlite.d.ts +160 -60
  96. package/dist/src/Sqlite.d.ts.map +1 -1
  97. package/dist/src/Sqlite.js +290 -190
  98. package/dist/src/Store.d.ts +36 -28
  99. package/dist/src/Store.d.ts.map +1 -1
  100. package/dist/src/Store.js +55 -15
  101. package/dist/src/String.d.ts +5 -0
  102. package/dist/src/String.d.ts.map +1 -1
  103. package/dist/src/String.js +5 -0
  104. package/dist/src/Task.d.ts +2263 -423
  105. package/dist/src/Task.d.ts.map +1 -1
  106. package/dist/src/Task.js +1488 -348
  107. package/dist/src/Test.d.ts +122 -0
  108. package/dist/src/Test.d.ts.map +1 -0
  109. package/dist/src/Test.js +66 -0
  110. package/dist/src/Time.d.ts +201 -48
  111. package/dist/src/Time.d.ts.map +1 -1
  112. package/dist/src/Time.js +190 -86
  113. package/dist/src/Tracer.d.ts +48 -0
  114. package/dist/src/Tracer.d.ts.map +1 -0
  115. package/dist/src/Tracer.js +6 -0
  116. package/dist/src/Type.d.ts +582 -209
  117. package/dist/src/Type.d.ts.map +1 -1
  118. package/dist/src/Type.js +362 -240
  119. package/dist/src/Types.d.ts +160 -6
  120. package/dist/src/Types.d.ts.map +1 -1
  121. package/dist/src/Types.js +22 -2
  122. package/dist/src/WebSocket.d.ts +113 -58
  123. package/dist/src/WebSocket.d.ts.map +1 -1
  124. package/dist/src/WebSocket.js +210 -118
  125. package/dist/src/Worker.d.ts +245 -60
  126. package/dist/src/Worker.d.ts.map +1 -1
  127. package/dist/src/Worker.js +220 -64
  128. package/dist/src/index.d.ts +25 -3
  129. package/dist/src/index.d.ts.map +1 -1
  130. package/dist/src/index.js +19 -3
  131. package/dist/src/local-first/Db.d.ts +24 -213
  132. package/dist/src/local-first/Db.d.ts.map +1 -1
  133. package/dist/src/local-first/Db.js +622 -293
  134. package/dist/src/local-first/Error.d.ts +12 -0
  135. package/dist/src/local-first/Error.d.ts.map +1 -0
  136. package/dist/src/local-first/Error.js +6 -0
  137. package/dist/src/local-first/Evolu.d.ts +330 -285
  138. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  139. package/dist/src/local-first/Evolu.js +472 -402
  140. package/dist/src/local-first/LocalAuth.d.ts +10 -10
  141. package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
  142. package/dist/src/local-first/LocalAuth.js +6 -1
  143. package/dist/src/local-first/Owner.d.ts +70 -27
  144. package/dist/src/local-first/Owner.d.ts.map +1 -1
  145. package/dist/src/local-first/Owner.js +27 -14
  146. package/dist/src/local-first/Protocol.d.ts +58 -50
  147. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  148. package/dist/src/local-first/Protocol.js +253 -155
  149. package/dist/src/local-first/Query.d.ts +133 -43
  150. package/dist/src/local-first/Query.d.ts.map +1 -1
  151. package/dist/src/local-first/Query.js +139 -90
  152. package/dist/src/local-first/Relay.d.ts +18 -38
  153. package/dist/src/local-first/Relay.d.ts.map +1 -1
  154. package/dist/src/local-first/Relay.js +59 -171
  155. package/dist/src/local-first/Schema.d.ts +139 -144
  156. package/dist/src/local-first/Schema.d.ts.map +1 -1
  157. package/dist/src/local-first/Schema.js +78 -163
  158. package/dist/src/local-first/Shared.d.ts +125 -0
  159. package/dist/src/local-first/Shared.d.ts.map +1 -0
  160. package/dist/src/local-first/Shared.js +569 -0
  161. package/dist/src/local-first/Storage.d.ts +50 -53
  162. package/dist/src/local-first/Storage.d.ts.map +1 -1
  163. package/dist/src/local-first/Storage.js +121 -173
  164. package/dist/src/local-first/Timestamp.d.ts +28 -35
  165. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  166. package/dist/src/local-first/Timestamp.js +24 -25
  167. package/dist/src/local-first/index.d.ts +2 -15
  168. package/dist/src/local-first/index.d.ts.map +1 -1
  169. package/dist/src/local-first/index.js +2 -15
  170. package/package.json +36 -26
  171. package/src/Array.ts +467 -106
  172. package/src/Assert.ts +87 -12
  173. package/src/BigInt.ts +7 -1
  174. package/src/Brand.ts +41 -12
  175. package/src/Buffer.ts +10 -4
  176. package/src/Cache.ts +11 -2
  177. package/src/Callbacks.ts +33 -23
  178. package/src/Console.ts +567 -178
  179. package/src/Crypto.ts +111 -62
  180. package/src/Eq.ts +30 -3
  181. package/src/Error.ts +50 -34
  182. package/src/Function.ts +140 -68
  183. package/src/Identicon.ts +7 -1
  184. package/src/Lookup.ts +415 -0
  185. package/src/Microtask.ts +58 -0
  186. package/src/Number.ts +40 -7
  187. package/src/Object.ts +155 -16
  188. package/src/Option.ts +74 -0
  189. package/src/Order.ts +11 -3
  190. package/src/Platform.ts +43 -7
  191. package/src/Polyfills.ts +465 -0
  192. package/src/Random.ts +25 -11
  193. package/src/Redacted.ts +8 -4
  194. package/src/Ref.ts +72 -21
  195. package/src/RefCount.ts +170 -0
  196. package/src/Relation.ts +188 -136
  197. package/src/Resource.ts +864 -0
  198. package/src/Result.ts +548 -381
  199. package/src/Schedule.ts +1471 -0
  200. package/src/Set.ts +247 -0
  201. package/src/Skiplist.ts +7 -1
  202. package/src/Sqlite.ts +409 -304
  203. package/src/Store.ts +94 -48
  204. package/src/String.ts +6 -0
  205. package/src/Task.ts +3685 -740
  206. package/src/Test.ts +162 -0
  207. package/src/Time.ts +348 -145
  208. package/src/Tracer.ts +54 -0
  209. package/src/Type.ts +1064 -630
  210. package/src/Types.ts +202 -7
  211. package/src/WebSocket.ts +311 -178
  212. package/src/Worker.ts +527 -147
  213. package/src/index.ts +74 -3
  214. package/src/local-first/Db.ts +871 -597
  215. package/src/local-first/Error.ts +17 -0
  216. package/src/local-first/Evolu.ts +884 -823
  217. package/src/local-first/LocalAuth.ts +10 -10
  218. package/src/local-first/Owner.ts +94 -42
  219. package/src/local-first/Protocol.ts +440 -303
  220. package/src/local-first/Query.ts +243 -194
  221. package/src/local-first/Relay.ts +91 -252
  222. package/src/local-first/Schema.ts +284 -394
  223. package/src/local-first/Shared.ts +932 -0
  224. package/src/local-first/Storage.ts +209 -282
  225. package/src/local-first/Timestamp.ts +38 -45
  226. package/src/local-first/index.ts +2 -16
  227. package/LICENSE +0 -21
  228. package/dist/src/Instances.d.ts +0 -34
  229. package/dist/src/Instances.d.ts.map +0 -1
  230. package/dist/src/Instances.js +0 -44
  231. package/dist/src/Resources.d.ts +0 -118
  232. package/dist/src/Resources.d.ts.map +0 -1
  233. package/dist/src/Resources.js +0 -197
  234. package/dist/src/local-first/Platform.d.ts +0 -25
  235. package/dist/src/local-first/Platform.d.ts.map +0 -1
  236. package/dist/src/local-first/Platform.js +0 -1
  237. package/dist/src/local-first/Public.d.ts +0 -22
  238. package/dist/src/local-first/Public.d.ts.map +0 -1
  239. package/dist/src/local-first/Public.js +0 -15
  240. package/dist/src/local-first/PublicKysely.d.ts +0 -148
  241. package/dist/src/local-first/PublicKysely.d.ts.map +0 -1
  242. package/dist/src/local-first/PublicKysely.js +0 -184
  243. package/dist/src/local-first/Sync.d.ts +0 -112
  244. package/dist/src/local-first/Sync.d.ts.map +0 -1
  245. package/dist/src/local-first/Sync.js +0 -529
  246. package/src/Instances.ts +0 -90
  247. package/src/Resources.ts +0 -367
  248. package/src/local-first/Platform.ts +0 -27
  249. package/src/local-first/Public.ts +0 -42
  250. package/src/local-first/PublicKysely.ts +0 -239
  251. package/src/local-first/Sync.ts +0 -960
@@ -1,960 +0,0 @@
1
- import {
2
- appendToArray,
3
- firstInArray,
4
- isNonEmptyReadonlyArray,
5
- NonEmptyArray,
6
- NonEmptyReadonlyArray,
7
- } from "../Array.js";
8
- import { assert, assertNonEmptyReadonlyArray } from "../Assert.js";
9
- import { Brand } from "../Brand.js";
10
- import { ConsoleDep } from "../Console.js";
11
- import {
12
- RandomBytesDep,
13
- SymmetricCryptoDecryptError,
14
- SymmetricCryptoDep,
15
- } from "../Crypto.js";
16
- import { eqArrayNumber } from "../Eq.js";
17
- import { createTransferableError, TransferableError } from "../Error.js";
18
- import { constFalse, constTrue } from "../Function.js";
19
- import { createRecord, getProperty, objectToEntries } from "../Object.js";
20
- import { RandomDep } from "../Random.js";
21
- import { createResources } from "../Resources.js";
22
- import { err, ok, Result } from "../Result.js";
23
- import {
24
- booleanToSqliteBoolean,
25
- sql,
26
- SqliteBoolean,
27
- sqliteBooleanToBoolean,
28
- SqliteDep,
29
- SqliteError,
30
- SqliteValue,
31
- } from "../Sqlite.js";
32
- import { AbortError, createMutex } from "../Task.js";
33
- import { TimeDep } from "../Time.js";
34
- import {
35
- Boolean,
36
- DateIso,
37
- Id,
38
- IdBytes,
39
- idBytesToId,
40
- idToIdBytes,
41
- PositiveInt,
42
- } from "../Type.js";
43
- import { CreateWebSocketDep, WebSocket } from "../WebSocket.js";
44
- import {
45
- AppOwner,
46
- AppOwnerDep,
47
- Owner,
48
- OwnerId,
49
- OwnerIdBytes,
50
- ownerIdBytesToOwnerId,
51
- ownerIdToOwnerIdBytes,
52
- OwnerTransport,
53
- ReadonlyOwner,
54
- } from "./Owner.js";
55
- import {
56
- applyProtocolMessageAsClient,
57
- createProtocolMessageForSync,
58
- createProtocolMessageForUnsubscribe,
59
- createProtocolMessageFromCrdtMessages,
60
- decryptAndDecodeDbChange,
61
- encodeAndEncryptDbChange,
62
- ProtocolError,
63
- ProtocolInvalidDataError,
64
- ProtocolTimestampMismatchError,
65
- SubscriptionFlags,
66
- } from "./Protocol.js";
67
- import { DbSchemaDep, MutationChange, systemColumns } from "./Schema.js";
68
- import {
69
- BaseSqliteStorage,
70
- CrdtMessage,
71
- createBaseSqliteStorage,
72
- DbChange,
73
- getOwnerUsage,
74
- getTimestampInsertStrategy,
75
- Storage,
76
- StorageWriteError,
77
- updateOwnerUsage,
78
- } from "./Storage.js";
79
- import {
80
- createInitialTimestamp,
81
- Millis,
82
- receiveTimestamp,
83
- sendTimestamp,
84
- Timestamp,
85
- TimestampBytes,
86
- timestampBytesToTimestamp,
87
- TimestampConfigDep,
88
- TimestampCounterOverflowError,
89
- TimestampDriftError,
90
- TimestampTimeOutOfRangeError,
91
- timestampToDateIso,
92
- timestampToTimestampBytes,
93
- } from "./Timestamp.js";
94
-
95
- export interface Sync extends Disposable {
96
- /**
97
- * Assigns or removes an owner to/from transports with reference counting.
98
- *
99
- * Owners are only synced if assigned to at least one transport. Uses
100
- * `owner.transports` or falls back to {@link SyncConfig} transports. Multiple
101
- * calls increment/decrement reference counts (useful for React Hooks).
102
- */
103
- readonly useOwner: (use: boolean, owner: SyncOwner) => void;
104
-
105
- readonly applyChanges: (
106
- changes: NonEmptyReadonlyArray<MutationChange>,
107
- ) => Result<
108
- void,
109
- | SqliteError
110
- | TimestampCounterOverflowError
111
- | TimestampDriftError
112
- | TimestampTimeOutOfRangeError
113
- >;
114
- }
115
-
116
- export interface SyncDep {
117
- readonly sync: Sync;
118
- }
119
-
120
- /**
121
- * Represents an owner for sync operations.
122
- *
123
- * Includes readonly owner fields plus optional write key (for clients that
124
- * write) and optional transports to override SyncConfig transports per owner.
125
- */
126
- export interface SyncOwner extends ReadonlyOwner {
127
- readonly writeKey?: Owner["writeKey"];
128
- readonly transports?: ReadonlyArray<OwnerTransport>;
129
- }
130
-
131
- export interface SyncConfig {
132
- readonly appOwner: AppOwner;
133
-
134
- readonly transports: ReadonlyArray<OwnerTransport>;
135
-
136
- /**
137
- * Delay in milliseconds before disposing unused WebSocket connections.
138
- * Defaults to 100ms.
139
- */
140
- readonly disposalDelayMs?: number;
141
-
142
- readonly onError: (
143
- error:
144
- | ProtocolError
145
- | ProtocolInvalidDataError
146
- | ProtocolTimestampMismatchError
147
- | SqliteError
148
- | SymmetricCryptoDecryptError
149
- | TimestampCounterOverflowError
150
- | TimestampDriftError
151
- | TimestampTimeOutOfRangeError
152
- | TransferableError,
153
- ) => void;
154
-
155
- readonly onReceive: () => void;
156
- }
157
-
158
- export const createSync =
159
- (
160
- deps: ClockDep &
161
- ConsoleDep &
162
- CreateWebSocketDep &
163
- DbSchemaDep &
164
- // PostMessageDep &
165
- RandomBytesDep &
166
- RandomDep &
167
- SqliteDep &
168
- SymmetricCryptoDep &
169
- TimeDep &
170
- TimestampConfigDep,
171
- ) =>
172
- (config: SyncConfig): Result<Sync, SqliteError> => {
173
- let isDisposed = false;
174
-
175
- /** Returns owner data only if actively assigned to at least one transport. */
176
- const getSyncOwner = (ownerId: OwnerId): SyncOwner | null => {
177
- if (isDisposed) return null;
178
- return resources.getConsumer(ownerId);
179
- };
180
-
181
- const storageResult = createClientStorage({
182
- ...deps,
183
- getSyncOwner,
184
- })(config);
185
-
186
- if (!storageResult.ok) return storageResult;
187
- const storage = storageResult.value;
188
-
189
- const createResource = (transport: OwnerTransport): WebSocket => {
190
- const transportKey = createTransportKey(transport);
191
-
192
- deps.console.log("[sync]", "createWebSocket", {
193
- transportKey,
194
- url: transport.url,
195
- });
196
-
197
- return deps.createWebSocket(transport.url, {
198
- binaryType: "arraybuffer",
199
-
200
- onOpen: () => {
201
- if (isDisposed) return;
202
-
203
- const webSocket = resources.getResource(transportKey);
204
- if (!webSocket) return;
205
-
206
- const ownerIds = resources.getConsumersForResource(transportKey);
207
- deps.console.log("[sync]", "onOpen", { transportKey, ownerIds });
208
-
209
- for (const ownerId of ownerIds) {
210
- const message = createProtocolMessageForSync({ storage })(
211
- ownerId,
212
- SubscriptionFlags.Subscribe,
213
- );
214
- if (!message) continue;
215
- deps.console.log("[sync]", "send", { message });
216
- webSocket.send(message);
217
- }
218
- },
219
-
220
- onClose: (event) => {
221
- deps.console.log("[sync]", "onClose", {
222
- transportKey,
223
- code: event.code,
224
- reason: event.reason,
225
- wasClean: event.wasClean,
226
- });
227
- },
228
-
229
- onError: (error) => {
230
- deps.console.warn("[sync]", "onError", { transportKey, error });
231
- },
232
-
233
- onMessage: (data: string | ArrayBuffer | Blob) => {
234
- // Only handle ArrayBuffer data for sync messages
235
- if (isDisposed || !(data instanceof ArrayBuffer)) return;
236
-
237
- const webSocket = resources.getResource(transportKey);
238
- if (!webSocket) return;
239
-
240
- const input = new Uint8Array(data);
241
- deps.console.log("[sync]", "onMessage", {
242
- transportKey,
243
- message: input,
244
- });
245
-
246
- applyProtocolMessageAsClient({ storage })(input, {
247
- // No write key, no sync (for a case when an owner was unused).
248
- getWriteKey: (ownerId) => getSyncOwner(ownerId)?.writeKey ?? null,
249
- })
250
- .then((message) => {
251
- if (!message.ok) {
252
- config.onError(message.error);
253
- return;
254
- }
255
-
256
- switch (message.value.type) {
257
- case "response":
258
- webSocket.send(message.value.message);
259
- break;
260
- case "no-response":
261
- // Sync complete, no response needed
262
- break;
263
- case "broadcast":
264
- // This was a broadcast message, don't affect sync counter
265
- break;
266
- }
267
- })
268
- .catch((error: unknown) => {
269
- config.onError(createTransferableError(error));
270
- });
271
- },
272
- });
273
- };
274
-
275
- const resources = createResources<
276
- WebSocket,
277
- TransportKey,
278
- OwnerTransport,
279
- SyncOwner,
280
- OwnerId
281
- >({
282
- createResource,
283
- getResourceKey: createTransportKey,
284
- getConsumerId: (owner) => owner.id,
285
- disposalDelay: config.disposalDelayMs ?? 100,
286
-
287
- onConsumerAdded: (owner, webSocket) => {
288
- deps.console.log("[sync]", "onConsumerAdded", {
289
- ownerId: owner.id,
290
- isOpen: webSocket.isOpen(),
291
- });
292
-
293
- // The onOpen handler will sync it.
294
- if (!webSocket.isOpen()) return;
295
- const message = createProtocolMessageForSync({ storage })(
296
- owner.id,
297
- SubscriptionFlags.Subscribe,
298
- );
299
- if (message) webSocket.send(message);
300
- },
301
-
302
- onConsumerRemoved: (owner, webSocket) => {
303
- deps.console.log("[sync]", "onConsumerRemoved", {
304
- ownerId: owner.id,
305
- isOpen: webSocket.isOpen(),
306
- });
307
-
308
- const message = createProtocolMessageForUnsubscribe(owner.id);
309
- webSocket.send(message);
310
- },
311
- });
312
-
313
- const sync: Sync = {
314
- useOwner: (use, owner) => {
315
- if (isDisposed) {
316
- deps.console.warn(
317
- "[sync]",
318
- "useOwner called on disposed Sync instance",
319
- { owner },
320
- );
321
- return;
322
- }
323
-
324
- deps.console.log("[sync]", "useOwner", { use, owner });
325
- const transports = owner.transports ?? config.transports;
326
-
327
- if (use) {
328
- resources.addConsumer(owner, transports);
329
- } else {
330
- const result = resources.removeConsumer(owner, transports);
331
- if (!result.ok) {
332
- deps.console.warn("[sync]", "Failed to remove consumer", {
333
- transports,
334
- ownerId: owner.id,
335
- error: result.error,
336
- });
337
- }
338
- }
339
- },
340
-
341
- applyChanges: (changes) => {
342
- deps.console.log("[sync]", "applyChanges", { changes });
343
-
344
- let clockTimestamp = deps.clock.get();
345
- const ownerMessages = new Map<OwnerId, NonEmptyArray<CrdtMessage>>();
346
-
347
- for (const change of changes) {
348
- const nextTimestamp = sendTimestamp(deps)(clockTimestamp);
349
- if (!nextTimestamp.ok) return nextTimestamp;
350
- clockTimestamp = nextTimestamp.value;
351
-
352
- const { ownerId = config.appOwner.id, ...dbChange } = change;
353
- const message: CrdtMessage = {
354
- timestamp: clockTimestamp,
355
- change: dbChange,
356
- };
357
-
358
- const messages = ownerMessages.get(ownerId);
359
- if (messages) messages.push(message);
360
- else ownerMessages.set(ownerId, [message]);
361
- }
362
-
363
- for (const [ownerId, messages] of ownerMessages) {
364
- const result = applyMessages({ ...deps, storage })(ownerId, messages);
365
- if (!result.ok) return result;
366
-
367
- const owner = getSyncOwner(ownerId);
368
- if (!owner?.writeKey) continue;
369
-
370
- const message = createProtocolMessageFromCrdtMessages(deps)(
371
- {
372
- id: owner.id,
373
- encryptionKey: owner.encryptionKey,
374
- writeKey: owner.writeKey,
375
- },
376
- messages,
377
- );
378
-
379
- const transports = owner.transports ?? config.transports;
380
-
381
- // Send message to all transports for this owner
382
- for (const transport of transports) {
383
- const transportKey = createTransportKey(transport);
384
-
385
- const webSocket = resources.getResource(transportKey);
386
- if (!webSocket) continue;
387
-
388
- if (webSocket.isOpen()) {
389
- deps.console.log("[sync]", "send", { transportKey, message });
390
- webSocket.send(message);
391
- }
392
- }
393
- }
394
-
395
- return deps.clock.save(clockTimestamp);
396
- },
397
-
398
- [Symbol.dispose]: () => {
399
- if (isDisposed) return;
400
- isDisposed = true;
401
- resources[Symbol.dispose]();
402
- },
403
- };
404
-
405
- return ok(sync);
406
- };
407
-
408
- export interface ClockDep {
409
- readonly clock: Clock;
410
- }
411
-
412
- export interface Clock {
413
- readonly get: () => Timestamp;
414
- readonly save: (timestamp: Timestamp) => Result<void, SqliteError>;
415
- }
416
-
417
- export const createClock =
418
- (deps: RandomBytesDep & SqliteDep) =>
419
- (initialTimestamp = createInitialTimestamp(deps)): Clock => {
420
- let currentTimestamp = initialTimestamp;
421
-
422
- return {
423
- get: () => currentTimestamp,
424
- save: (timestamp) => {
425
- currentTimestamp = timestamp;
426
-
427
- const result = deps.sqlite.exec(sql.prepared`
428
- update evolu_config
429
- set "clock" = ${timestampToTimestampBytes(timestamp)};
430
- `);
431
- if (!result.ok) return result;
432
-
433
- return ok();
434
- },
435
- };
436
- };
437
-
438
- interface GetSyncOwnerDep {
439
- readonly getSyncOwner: (ownerId: OwnerId) => SyncOwner | null;
440
- }
441
-
442
- export interface ClientStorage extends Storage, BaseSqliteStorage {}
443
-
444
- export interface ClientStorageDep {
445
- readonly storage: ClientStorage;
446
- }
447
-
448
- const createClientStorage =
449
- (
450
- deps: ClockDep &
451
- DbSchemaDep &
452
- GetSyncOwnerDep &
453
- RandomDep &
454
- SqliteDep &
455
- SymmetricCryptoDep &
456
- TimeDep &
457
- TimestampConfigDep,
458
- ) =>
459
- (config: {
460
- onError: (
461
- error:
462
- | ProtocolInvalidDataError
463
- | ProtocolTimestampMismatchError
464
- | SqliteError
465
- | SymmetricCryptoDecryptError
466
- | TimestampCounterOverflowError
467
- | TimestampDriftError
468
- | TimestampTimeOutOfRangeError,
469
- ) => void;
470
- onReceive: () => void;
471
- }): Result<ClientStorage, SqliteError> => {
472
- const sqliteStorageBase = createBaseSqliteStorage(deps)({
473
- onStorageError: config.onError,
474
- isOwnerWithinQuota: constTrue, // Clients don't have quota limits
475
- });
476
-
477
- // TODO: Mutex per OwnerId
478
- const mutex = createMutex();
479
-
480
- const storage: ClientStorage = {
481
- ...sqliteStorageBase,
482
-
483
- // Not implemented yet.
484
- validateWriteKey: constFalse,
485
- setWriteKey: constFalse,
486
-
487
- writeMessages: async (ownerIdBytes, encryptedMessages) => {
488
- const ownerId = ownerIdBytesToOwnerId(ownerIdBytes);
489
-
490
- // Everything is sync now, but we will need async crypto in the future.
491
- const result = await mutex.withLock<
492
- boolean,
493
- | AbortError
494
- | ProtocolInvalidDataError
495
- | ProtocolTimestampMismatchError
496
- | SqliteError
497
- | SymmetricCryptoDecryptError
498
- | TimestampCounterOverflowError
499
- | TimestampDriftError
500
- | TimestampTimeOutOfRangeError
501
- // eslint-disable-next-line @typescript-eslint/require-await
502
- >(async () => {
503
- const owner = deps.getSyncOwner(ownerId);
504
- // Owner can be removed during syncing.
505
- // `ok(true)` means success, we just skipped the write.
506
- if (!owner) return ok(true);
507
-
508
- // TODO: Add quota checking for collaborative scenarios.
509
- // When receiving messages from other owners via relay broadcast,
510
- // check if this owner is within quota before accepting the data.
511
- // This prevents an owner from exceeding storage limits when receiving
512
- // data shared by other collaborators.
513
-
514
- const messages: Array<CrdtMessage> = [];
515
-
516
- for (const message of encryptedMessages) {
517
- const change = decryptAndDecodeDbChange(deps)(
518
- message,
519
- owner.encryptionKey,
520
- );
521
- if (!change.ok) return change;
522
-
523
- messages.push({
524
- timestamp: message.timestamp,
525
- change: change.value,
526
- });
527
- }
528
-
529
- const transaction = deps.sqlite.transaction(() => {
530
- let clockTimestamp = deps.clock.get();
531
-
532
- for (const message of messages) {
533
- const nextTimestamp = receiveTimestamp(deps)(
534
- clockTimestamp,
535
- message.timestamp,
536
- );
537
- if (!nextTimestamp.ok) return nextTimestamp;
538
-
539
- clockTimestamp = nextTimestamp.value;
540
- }
541
-
542
- if (isNonEmptyReadonlyArray(messages)) {
543
- const result = applyMessages({ ...deps, storage })(
544
- owner.id,
545
- messages,
546
- );
547
- if (!result.ok) return result;
548
- }
549
-
550
- return deps.clock.save(clockTimestamp);
551
- });
552
-
553
- if (!transaction.ok) return transaction;
554
-
555
- return ok(true);
556
- })();
557
-
558
- if (!result.ok) {
559
- if (result.error.type !== "AbortError") {
560
- config.onError(result.error);
561
- }
562
- return err<StorageWriteError>({ type: "StorageWriteError", ownerId });
563
- }
564
-
565
- config.onReceive();
566
-
567
- return ok();
568
- },
569
-
570
- readDbChange: (ownerId, timestamp) => {
571
- const owner = deps.getSyncOwner(ownerIdBytesToOwnerId(ownerId));
572
- // Owner can be removed to stop syncing.
573
- if (!owner) return null;
574
-
575
- const result = deps.sqlite.exec<{
576
- readonly table: string;
577
- readonly id: IdBytes;
578
- readonly column: string;
579
- readonly value: SqliteValue;
580
- }>(sql`
581
- select "table", "id", "column", "value"
582
- from evolu_history
583
- where "ownerId" = ${ownerId} and "timestamp" = ${timestamp}
584
- union all
585
- select "table", "id", "column", "value"
586
- from evolu_message_quarantine
587
- where "ownerId" = ${ownerId} and "timestamp" = ${timestamp};
588
- `);
589
-
590
- if (!result.ok) {
591
- config.onError(result.error);
592
- return null;
593
- }
594
-
595
- const { rows } = result.value;
596
- assertNonEmptyReadonlyArray(
597
- rows,
598
- "Every timestamp must have rows in evolu_history or quarantine",
599
- );
600
-
601
- const { table, id } = firstInArray(rows);
602
- const values = createRecord<string, SqliteValue>();
603
- let isInsert;
604
- let isDelete: boolean | null = null;
605
-
606
- for (const r of rows) {
607
- assert(r.table === table, "All rows must have the same table");
608
- assert(eqArrayNumber(r.id, id), "All rows must have the same Id");
609
- switch (r.column) {
610
- case "createdAt":
611
- isInsert = true;
612
- break;
613
- case "updatedAt":
614
- isInsert = false;
615
- break;
616
- case "isDeleted":
617
- assert(
618
- SqliteBoolean.is(r.value),
619
- "isDeleted column must contain a valid SqliteBoolean",
620
- );
621
- isDelete = sqliteBooleanToBoolean(r.value);
622
- break;
623
- default:
624
- values[r.column] = r.value;
625
- }
626
- }
627
-
628
- assert(Boolean.is(isInsert), "isInsert must be in evolu_history");
629
-
630
- const message: CrdtMessage = {
631
- timestamp: timestampBytesToTimestamp(timestamp),
632
- change: DbChange.orThrow({
633
- table: rows[0].table,
634
- id: idBytesToId(rows[0].id),
635
- values,
636
- isInsert,
637
- isDelete,
638
- }),
639
- };
640
-
641
- return encodeAndEncryptDbChange(deps)(message, owner.encryptionKey);
642
- },
643
- };
644
-
645
- return ok(storage);
646
- };
647
-
648
- type TransportKey = string & Brand<"TransportKey">;
649
-
650
- /** Creates a unique identifier for a {@link OwnerTransport}. */
651
- const createTransportKey = (transport: OwnerTransport): TransportKey => {
652
- return `${transport.type}:${transport.url}` as TransportKey;
653
- };
654
-
655
- const dbChangeToColumns = (change: DbChange, now: DateIso) => {
656
- let values = objectToEntries(change.values);
657
-
658
- // SystemColumns are not encoded in change.values.
659
- values = appendToArray(values, [
660
- change.isInsert ? "createdAt" : "updatedAt",
661
- now,
662
- ]);
663
- if (change.isDelete != null) {
664
- values = appendToArray(values, [
665
- "isDeleted",
666
- booleanToSqliteBoolean(change.isDelete),
667
- ]);
668
- }
669
-
670
- return values;
671
- };
672
-
673
- export const applyLocalOnlyChange =
674
- (deps: SqliteDep & TimeDep & AppOwnerDep) =>
675
- (change: MutationChange): Result<void, SqliteError> => {
676
- if (change.isDelete) {
677
- const result = deps.sqlite.exec(sql`
678
- delete from ${sql.identifier(change.table)}
679
- where id = ${change.id};
680
- `);
681
- if (!result.ok) return result;
682
- } else {
683
- const ownerId = deps.appOwner.id;
684
- const columns = dbChangeToColumns(change, deps.time.nowIso());
685
-
686
- for (const [column, value] of columns) {
687
- const result = deps.sqlite.exec(sql.prepared`
688
- insert into ${sql.identifier(change.table)}
689
- ("ownerId", "id", ${sql.identifier(column)})
690
- values (${ownerId}, ${change.id}, ${value})
691
- on conflict ("ownerId", "id") do update
692
- set ${sql.identifier(column)} = ${value};
693
- `);
694
- if (!result.ok) return result;
695
- }
696
- }
697
-
698
- return ok();
699
- };
700
-
701
- const applyMessages =
702
- (deps: ClientStorageDep & ClockDep & DbSchemaDep & RandomDep & SqliteDep) =>
703
- (
704
- ownerId: OwnerId,
705
- messages: NonEmptyReadonlyArray<CrdtMessage>,
706
- ): Result<void, SqliteError> => {
707
- const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
708
-
709
- const usage = getOwnerUsage(deps)(
710
- ownerIdBytes,
711
- timestampToTimestampBytes(firstInArray(messages).timestamp),
712
- );
713
- if (!usage.ok) return usage;
714
-
715
- let { firstTimestamp, lastTimestamp } = usage.value;
716
-
717
- for (const { timestamp, change } of messages) {
718
- const columns = dbChangeToColumns(change, timestampToDateIso(timestamp));
719
- const idBytes = idToIdBytes(change.id);
720
- const timestampBytes = timestampToTimestampBytes(timestamp);
721
-
722
- for (const [column, value] of columns) {
723
- if (validateColumnValue(deps)(change.table, column, value)) {
724
- const result = applyColumnChange(deps)(
725
- ownerIdBytes,
726
- ownerId,
727
- change.table,
728
- idBytes,
729
- change.id,
730
- column,
731
- value,
732
- timestampBytes,
733
- );
734
- if (!result.ok) return result;
735
- } else {
736
- const result = deps.sqlite.exec(sql.prepared`
737
- insert into evolu_message_quarantine
738
- ("ownerId", "timestamp", "table", "id", "column", "value")
739
- values
740
- (
741
- ${ownerIdBytes},
742
- ${timestampBytes},
743
- ${change.table},
744
- ${idBytes},
745
- ${column},
746
- ${value}
747
- )
748
- on conflict do nothing;
749
- `);
750
- if (!result.ok) return result;
751
- }
752
- }
753
-
754
- let strategy;
755
- [strategy, firstTimestamp, lastTimestamp] = getTimestampInsertStrategy(
756
- timestampBytes,
757
- firstTimestamp,
758
- lastTimestamp,
759
- );
760
-
761
- const result = deps.storage.insertTimestamp(
762
- ownerIdBytes,
763
- timestampBytes,
764
- strategy,
765
- );
766
- if (!result.ok) return result;
767
- }
768
-
769
- /**
770
- * TODO: Implement proper storedBytes tracking for client using received and
771
- * sent encrypted message sizes.
772
- */
773
- return updateOwnerUsage(deps)(
774
- ownerIdBytes,
775
- 1 as PositiveInt, // Placeholder until proper tracking implemented
776
- firstTimestamp,
777
- lastTimestamp,
778
- );
779
- };
780
-
781
- /**
782
- * System columns that can appear in sync messages. Excludes `ownerId` because
783
- * it's handled separately (stored per-row, not per-column in messages).
784
- */
785
- const systemColumnsWithoutOwnerId = systemColumns.difference(
786
- new Set(["ownerId"]),
787
- );
788
-
789
- const validateColumnValue =
790
- (deps: DbSchemaDep) =>
791
- (table: string, column: string, _value: SqliteValue): boolean => {
792
- const schemaColumns = getProperty(deps.dbSchema.tables, table);
793
- return (
794
- schemaColumns != null &&
795
- (systemColumnsWithoutOwnerId.has(column) || schemaColumns.has(column))
796
- );
797
- };
798
-
799
- const applyColumnChange =
800
- (deps: SqliteDep) =>
801
- (
802
- ownerIdBytes: OwnerIdBytes,
803
- ownerId: OwnerId,
804
- table: string,
805
- idBytes: IdBytes,
806
- id: Id,
807
- column: string,
808
- value: SqliteValue,
809
- timestampBytes: TimestampBytes,
810
- ): Result<void, SqliteError> => {
811
- const result = deps.sqlite.exec(sql.prepared`
812
- with
813
- existingTimestamp as (
814
- select 1
815
- from evolu_history
816
- where
817
- "ownerId" = ${ownerIdBytes}
818
- and "table" = ${table}
819
- and "id" = ${idBytes}
820
- and "column" = ${column}
821
- and "timestamp" >= ${timestampBytes}
822
- limit 1
823
- )
824
- insert into ${sql.identifier(table)}
825
- ("ownerId", "id", ${sql.identifier(column)})
826
- select ${ownerId}, ${id}, ${value}
827
- where not exists (select 1 from existingTimestamp)
828
- on conflict ("ownerId", "id") do update
829
- set ${sql.identifier(column)} = ${value}
830
- where not exists (select 1 from existingTimestamp);
831
- `);
832
- if (!result.ok) return result;
833
-
834
- {
835
- const result = deps.sqlite.exec(sql.prepared`
836
- insert into evolu_history
837
- ("ownerId", "table", "id", "column", "value", "timestamp")
838
- values
839
- (
840
- ${ownerIdBytes},
841
- ${table},
842
- ${idBytes},
843
- ${column},
844
- ${value},
845
- ${timestampBytes}
846
- )
847
- on conflict do nothing;
848
- `);
849
- if (!result.ok) return result;
850
- }
851
-
852
- return ok();
853
- };
854
-
855
- /**
856
- * Attempts to apply quarantined messages that may now be valid after a schema
857
- * update. Messages are quarantined when they reference tables or columns that
858
- * don't exist in the current schema (e.g., from a newer app version).
859
- */
860
- export const tryApplyQuarantinedMessages =
861
- (deps: DbSchemaDep & SqliteDep) => (): Result<void, SqliteError> => {
862
- const rows = deps.sqlite.exec<{
863
- readonly ownerId: OwnerIdBytes;
864
- readonly timestamp: TimestampBytes;
865
- readonly table: string;
866
- readonly id: IdBytes;
867
- readonly column: string;
868
- readonly value: SqliteValue;
869
- }>(sql`
870
- select "ownerId", "timestamp", "table", "id", "column", "value"
871
- from evolu_message_quarantine;
872
- `);
873
- if (!rows.ok) return rows;
874
-
875
- for (const row of rows.value.rows) {
876
- if (!validateColumnValue(deps)(row.table, row.column, row.value))
877
- continue;
878
-
879
- const result = applyColumnChange(deps)(
880
- row.ownerId,
881
- ownerIdBytesToOwnerId(row.ownerId),
882
- row.table,
883
- row.id,
884
- idBytesToId(row.id),
885
- row.column,
886
- row.value,
887
- row.timestamp,
888
- );
889
- if (!result.ok) return result;
890
-
891
- {
892
- const result = deps.sqlite.exec(sql`
893
- delete from evolu_message_quarantine
894
- where
895
- "ownerId" = ${row.ownerId}
896
- and "timestamp" = ${row.timestamp}
897
- and "table" = ${row.table}
898
- and "id" = ${row.id}
899
- and "column" = ${row.column};
900
- `);
901
- if (!result.ok) return result;
902
- }
903
- }
904
-
905
- return ok();
906
- };
907
-
908
- /**
909
- * TODO: Rework for the new owners API.
910
- *
911
- * The possible states of a synchronization process. The `SyncState` can be one
912
- * of the following:
913
- *
914
- * - {@link SyncStateInitial}
915
- * - {@link SyncStateIsSyncing}
916
- * - {@link SyncStateIsSynced}
917
- * - {@link SyncStateIsNotSynced}
918
- */
919
- export type SyncState =
920
- | SyncStateInitial
921
- | SyncStateIsSyncing
922
- | SyncStateIsSynced
923
- | SyncStateIsNotSynced;
924
-
925
- /**
926
- * The initial synchronization state when the app starts. In this state, the app
927
- * needs to determine whether the data is synced.
928
- */
929
- export interface SyncStateInitial {
930
- readonly type: "SyncStateInitial";
931
- }
932
-
933
- export interface SyncStateIsSyncing {
934
- readonly type: "SyncStateIsSyncing";
935
- }
936
-
937
- export interface SyncStateIsSynced {
938
- readonly type: "SyncStateIsSynced";
939
- readonly time: Millis;
940
- }
941
-
942
- export interface SyncStateIsNotSynced {
943
- readonly type: "SyncStateIsNotSynced";
944
- readonly error: NetworkError | ServerError | PaymentRequiredError;
945
- }
946
-
947
- export interface NetworkError {
948
- readonly type: "NetworkError";
949
- }
950
-
951
- export interface ServerError {
952
- readonly type: "ServerError";
953
- readonly status: number;
954
- }
955
-
956
- export interface PaymentRequiredError {
957
- readonly type: "PaymentRequiredError";
958
- }
959
-
960
- export const initialSyncState: SyncStateInitial = { type: "SyncStateInitial" };