@evolu/common 7.4.1 → 8.0.0-next.1

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 (250) 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 +162 -0
  42. package/dist/src/Lookup.d.ts.map +1 -0
  43. package/dist/src/Lookup.js +200 -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 +29 -0
  63. package/dist/src/Polyfills.d.ts.map +1 -0
  64. package/dist/src/Polyfills.js +329 -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 +2274 -423
  105. package/dist/src/Task.d.ts.map +1 -1
  106. package/dist/src/Task.js +1499 -348
  107. package/dist/src/Test.d.ts +150 -0
  108. package/dist/src/Test.d.ts.map +1 -0
  109. package/dist/src/Test.js +97 -0
  110. package/dist/src/Time.d.ts +202 -48
  111. package/dist/src/Time.d.ts.map +1 -1
  112. package/dist/src/Time.js +199 -88
  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 +587 -209
  117. package/dist/src/Type.d.ts.map +1 -1
  118. package/dist/src/Type.js +370 -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 +130 -58
  123. package/dist/src/WebSocket.d.ts.map +1 -1
  124. package/dist/src/WebSocket.js +274 -113
  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 +621 -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 +34 -49
  153. package/dist/src/local-first/Relay.d.ts.map +1 -1
  154. package/dist/src/local-first/Relay.js +59 -177
  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 +54 -58
  162. package/dist/src/local-first/Storage.d.ts.map +1 -1
  163. package/dist/src/local-first/Storage.js +127 -179
  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 +37 -24
  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 +429 -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 +520 -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 +3696 -740
  206. package/src/Test.ts +202 -0
  207. package/src/Time.ts +357 -147
  208. package/src/Tracer.ts +54 -0
  209. package/src/Type.ts +866 -430
  210. package/src/Types.ts +202 -7
  211. package/src/WebSocket.ts +404 -167
  212. package/src/Worker.ts +527 -147
  213. package/src/index.ts +74 -3
  214. package/src/local-first/Db.ts +867 -597
  215. package/src/local-first/Error.ts +17 -0
  216. package/src/local-first/Evolu.ts +884 -824
  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 +441 -307
  220. package/src/local-first/Query.ts +243 -194
  221. package/src/local-first/Relay.ts +119 -273
  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 +211 -283
  225. package/src/local-first/Timestamp.ts +38 -45
  226. package/src/local-first/index.ts +2 -16
  227. package/dist/src/Instances.d.ts +0 -34
  228. package/dist/src/Instances.d.ts.map +0 -1
  229. package/dist/src/Instances.js +0 -44
  230. package/dist/src/Resources.d.ts +0 -118
  231. package/dist/src/Resources.d.ts.map +0 -1
  232. package/dist/src/Resources.js +0 -197
  233. package/dist/src/local-first/Platform.d.ts +0 -25
  234. package/dist/src/local-first/Platform.d.ts.map +0 -1
  235. package/dist/src/local-first/Platform.js +0 -1
  236. package/dist/src/local-first/Public.d.ts +0 -22
  237. package/dist/src/local-first/Public.d.ts.map +0 -1
  238. package/dist/src/local-first/Public.js +0 -15
  239. package/dist/src/local-first/PublicKysely.d.ts +0 -148
  240. package/dist/src/local-first/PublicKysely.d.ts.map +0 -1
  241. package/dist/src/local-first/PublicKysely.js +0 -184
  242. package/dist/src/local-first/Sync.d.ts +0 -112
  243. package/dist/src/local-first/Sync.d.ts.map +0 -1
  244. package/dist/src/local-first/Sync.js +0 -526
  245. package/src/Instances.ts +0 -92
  246. package/src/Resources.ts +0 -367
  247. package/src/local-first/Platform.ts +0 -27
  248. package/src/local-first/Public.ts +0 -42
  249. package/src/local-first/PublicKysely.ts +0 -239
  250. package/src/local-first/Sync.ts +0 -949
@@ -1,526 +0,0 @@
1
- import { appendToArray, firstInArray, isNonEmptyReadonlyArray, } from "../Array.js";
2
- import { assertNonEmptyReadonlyArray } from "../Assert.js";
3
- import { createTransferableError } from "../Error.js";
4
- import { constFalse, constTrue } from "../Function.js";
5
- import { createRecord, getProperty, objectToEntries } from "../Object.js";
6
- import { createResources } from "../Resources.js";
7
- import { err, ok } from "../Result.js";
8
- import { booleanToSqliteBoolean, sql, SqliteBoolean, sqliteBooleanToBoolean, } from "../Sqlite.js";
9
- import { createMutex } from "../Task.js";
10
- import { idBytesToId, idToIdBytes, } from "../Type.js";
11
- import { ownerIdBytesToOwnerId, ownerIdToOwnerIdBytes, } from "./Owner.js";
12
- import { applyProtocolMessageAsClient, createProtocolMessageForSync, createProtocolMessageForUnsubscribe, createProtocolMessageFromCrdtMessages, decryptAndDecodeDbChange, encodeAndEncryptDbChange, SubscriptionFlags, } from "./Protocol.js";
13
- import { systemColumns } from "./Schema.js";
14
- import { createBaseSqliteStorage, DbChange, getOwnerUsage, getTimestampInsertStrategy, updateOwnerUsage, } from "./Storage.js";
15
- import { createInitialTimestamp, receiveTimestamp, sendTimestamp, timestampBytesToTimestamp, timestampToDateIso, timestampToTimestampBytes, } from "./Timestamp.js";
16
- export const createSync = (deps) => (config) => {
17
- let isDisposed = false;
18
- /** Returns owner data only if actively assigned to at least one transport. */
19
- const getSyncOwner = (ownerId) => {
20
- if (isDisposed)
21
- return null;
22
- return resources.getConsumer(ownerId);
23
- };
24
- const storageResult = createClientStorage({
25
- ...deps,
26
- getSyncOwner,
27
- })(config);
28
- if (!storageResult.ok)
29
- return storageResult;
30
- const storage = storageResult.value;
31
- const createResource = (transport) => {
32
- const transportKey = createTransportKey(transport);
33
- deps.console.log("[sync]", "createWebSocket", {
34
- transportKey,
35
- url: transport.url,
36
- });
37
- return deps.createWebSocket(transport.url, {
38
- binaryType: "arraybuffer",
39
- onOpen: () => {
40
- if (isDisposed)
41
- return;
42
- const webSocket = resources.getResource(transportKey);
43
- if (!webSocket)
44
- return;
45
- const ownerIds = resources.getConsumersForResource(transportKey);
46
- deps.console.log("[sync]", "onOpen", { transportKey, ownerIds });
47
- for (const ownerId of ownerIds) {
48
- const message = createProtocolMessageForSync({ storage })(ownerId, SubscriptionFlags.Subscribe);
49
- if (!message)
50
- continue;
51
- deps.console.log("[sync]", "send", { message });
52
- webSocket.send(message);
53
- }
54
- },
55
- onClose: (event) => {
56
- deps.console.log("[sync]", "onClose", {
57
- transportKey,
58
- code: event.code,
59
- reason: event.reason,
60
- wasClean: event.wasClean,
61
- });
62
- },
63
- onError: (error) => {
64
- deps.console.warn("[sync]", "onError", { transportKey, error });
65
- },
66
- onMessage: (data) => {
67
- // Only handle ArrayBuffer data for sync messages
68
- if (isDisposed || !(data instanceof ArrayBuffer))
69
- return;
70
- const webSocket = resources.getResource(transportKey);
71
- if (!webSocket)
72
- return;
73
- const input = new Uint8Array(data);
74
- deps.console.log("[sync]", "onMessage", {
75
- transportKey,
76
- message: input,
77
- });
78
- applyProtocolMessageAsClient({ storage })(input, {
79
- // No write key, no sync (for a case when an owner was unused).
80
- getWriteKey: (ownerId) => getSyncOwner(ownerId)?.writeKey ?? null,
81
- })
82
- .then((message) => {
83
- if (!message.ok) {
84
- config.onError(message.error);
85
- return;
86
- }
87
- switch (message.value.type) {
88
- case "response":
89
- webSocket.send(message.value.message);
90
- break;
91
- case "no-response":
92
- // Sync complete, no response needed
93
- break;
94
- case "broadcast":
95
- // This was a broadcast message, don't affect sync counter
96
- break;
97
- }
98
- })
99
- .catch((error) => {
100
- config.onError(createTransferableError(error));
101
- });
102
- },
103
- });
104
- };
105
- const resources = createResources({
106
- createResource,
107
- getResourceKey: createTransportKey,
108
- getConsumerId: (owner) => owner.id,
109
- disposalDelay: config.disposalDelayMs ?? 100,
110
- onConsumerAdded: (owner, webSocket) => {
111
- deps.console.log("[sync]", "onConsumerAdded", {
112
- ownerId: owner.id,
113
- isOpen: webSocket.isOpen(),
114
- });
115
- // The onOpen handler will sync it.
116
- if (!webSocket.isOpen())
117
- return;
118
- const message = createProtocolMessageForSync({ storage })(owner.id, SubscriptionFlags.Subscribe);
119
- if (message)
120
- webSocket.send(message);
121
- },
122
- onConsumerRemoved: (owner, webSocket) => {
123
- deps.console.log("[sync]", "onConsumerRemoved", {
124
- ownerId: owner.id,
125
- isOpen: webSocket.isOpen(),
126
- });
127
- const message = createProtocolMessageForUnsubscribe(owner.id);
128
- webSocket.send(message);
129
- },
130
- });
131
- const sync = {
132
- useOwner: (use, owner) => {
133
- if (isDisposed) {
134
- deps.console.warn("[sync]", "useOwner called on disposed Sync instance", { owner });
135
- return;
136
- }
137
- deps.console.log("[sync]", "useOwner", { use, owner });
138
- const transports = owner.transports ?? config.transports;
139
- if (use) {
140
- resources.addConsumer(owner, transports);
141
- }
142
- else {
143
- const result = resources.removeConsumer(owner, transports);
144
- if (!result.ok) {
145
- deps.console.warn("[sync]", "Failed to remove consumer", {
146
- transports,
147
- ownerId: owner.id,
148
- error: result.error,
149
- });
150
- }
151
- }
152
- },
153
- applyChanges: (changes) => {
154
- deps.console.log("[sync]", "applyChanges", { changes });
155
- let clockTimestamp = deps.clock.get();
156
- const ownerMessages = new Map();
157
- for (const change of changes) {
158
- const nextTimestamp = sendTimestamp(deps)(clockTimestamp);
159
- if (!nextTimestamp.ok)
160
- return nextTimestamp;
161
- clockTimestamp = nextTimestamp.value;
162
- const { ownerId = config.appOwner.id, ...dbChange } = change;
163
- const message = {
164
- timestamp: clockTimestamp,
165
- change: dbChange,
166
- };
167
- const messages = ownerMessages.get(ownerId);
168
- if (messages)
169
- messages.push(message);
170
- else
171
- ownerMessages.set(ownerId, [message]);
172
- }
173
- for (const [ownerId, messages] of ownerMessages) {
174
- const result = applyMessages({ ...deps, storage })(ownerId, messages);
175
- if (!result.ok)
176
- return result;
177
- const owner = getSyncOwner(ownerId);
178
- if (!owner?.writeKey)
179
- continue;
180
- const message = createProtocolMessageFromCrdtMessages(deps)({
181
- id: owner.id,
182
- encryptionKey: owner.encryptionKey,
183
- writeKey: owner.writeKey,
184
- }, messages);
185
- const transports = owner.transports ?? config.transports;
186
- // Send message to all transports for this owner
187
- for (const transport of transports) {
188
- const transportKey = createTransportKey(transport);
189
- const webSocket = resources.getResource(transportKey);
190
- if (!webSocket)
191
- continue;
192
- if (webSocket.isOpen()) {
193
- deps.console.log("[sync]", "send", { transportKey, message });
194
- webSocket.send(message);
195
- }
196
- }
197
- }
198
- return deps.clock.save(clockTimestamp);
199
- },
200
- [Symbol.dispose]: () => {
201
- if (isDisposed)
202
- return;
203
- isDisposed = true;
204
- resources[Symbol.dispose]();
205
- },
206
- };
207
- return ok(sync);
208
- };
209
- export const createClock = (deps) => (initialTimestamp = createInitialTimestamp(deps)) => {
210
- let currentTimestamp = initialTimestamp;
211
- return {
212
- get: () => currentTimestamp,
213
- save: (timestamp) => {
214
- currentTimestamp = timestamp;
215
- const result = deps.sqlite.exec(sql.prepared `
216
- update evolu_config
217
- set "clock" = ${timestampToTimestampBytes(timestamp)};
218
- `);
219
- if (!result.ok)
220
- return result;
221
- return ok();
222
- },
223
- };
224
- };
225
- const createClientStorage = (deps) => (config) => {
226
- const sqliteStorageBase = createBaseSqliteStorage(deps)({
227
- onStorageError: config.onError,
228
- isOwnerWithinQuota: constTrue, // Clients don't have quota limits
229
- });
230
- // TODO: Mutex per OwnerId
231
- const mutex = createMutex();
232
- const storage = {
233
- ...sqliteStorageBase,
234
- // Not implemented yet.
235
- validateWriteKey: constFalse,
236
- setWriteKey: constFalse,
237
- writeMessages: async (ownerIdBytes, encryptedMessages) => {
238
- const ownerId = ownerIdBytesToOwnerId(ownerIdBytes);
239
- // Everything is sync now, but we will need async crypto in the future.
240
- const result = await mutex.withLock(async () => {
241
- const owner = deps.getSyncOwner(ownerId);
242
- // Owner can be removed during syncing.
243
- // `ok(true)` means success, we just skipped the write.
244
- if (!owner)
245
- return ok(true);
246
- // TODO: Add quota checking for collaborative scenarios.
247
- // When receiving messages from other owners via relay broadcast,
248
- // check if this owner is within quota before accepting the data.
249
- // This prevents an owner from exceeding storage limits when receiving
250
- // data shared by other collaborators.
251
- const messages = [];
252
- for (const message of encryptedMessages) {
253
- const change = decryptAndDecodeDbChange(deps)(message, owner.encryptionKey);
254
- if (!change.ok)
255
- return change;
256
- messages.push({
257
- timestamp: message.timestamp,
258
- change: change.value,
259
- });
260
- }
261
- const transaction = deps.sqlite.transaction(() => {
262
- let clockTimestamp = deps.clock.get();
263
- for (const message of messages) {
264
- const nextTimestamp = receiveTimestamp(deps)(clockTimestamp, message.timestamp);
265
- if (!nextTimestamp.ok)
266
- return nextTimestamp;
267
- clockTimestamp = nextTimestamp.value;
268
- }
269
- if (isNonEmptyReadonlyArray(messages)) {
270
- const result = applyMessages({ ...deps, storage })(owner.id, messages);
271
- if (!result.ok)
272
- return result;
273
- }
274
- return deps.clock.save(clockTimestamp);
275
- });
276
- if (!transaction.ok)
277
- return transaction;
278
- return ok(true);
279
- })();
280
- if (!result.ok) {
281
- if (result.error.type !== "AbortError") {
282
- config.onError(result.error);
283
- }
284
- return err({ type: "StorageWriteError", ownerId });
285
- }
286
- config.onReceive();
287
- return ok();
288
- },
289
- readDbChange: (ownerId, timestamp) => {
290
- const owner = deps.getSyncOwner(ownerIdBytesToOwnerId(ownerId));
291
- // Owner can be removed to stop syncing.
292
- if (!owner)
293
- return null;
294
- const result = deps.sqlite.exec(sql `
295
- select "table", "id", "column", "value"
296
- from evolu_history
297
- where "ownerId" = ${ownerId} and "timestamp" = ${timestamp}
298
- union all
299
- select "table", "id", "column", "value"
300
- from evolu_message_quarantine
301
- where "ownerId" = ${ownerId} and "timestamp" = ${timestamp};
302
- `);
303
- if (!result.ok) {
304
- config.onError(result.error);
305
- return null;
306
- }
307
- const { rows } = result.value;
308
- assertNonEmptyReadonlyArray(rows, "Every timestamp must have rows");
309
- const firstRow = firstInArray(rows);
310
- const values = createRecord();
311
- let isInsert = false;
312
- let isDelete = null;
313
- for (const r of rows) {
314
- switch (r.column) {
315
- case "createdAt":
316
- isInsert = true;
317
- break;
318
- case "updatedAt":
319
- isInsert = false;
320
- break;
321
- case "isDeleted":
322
- if (SqliteBoolean.is(r.value)) {
323
- isDelete = sqliteBooleanToBoolean(r.value);
324
- }
325
- break;
326
- default:
327
- values[r.column] = r.value;
328
- }
329
- }
330
- const message = {
331
- timestamp: timestampBytesToTimestamp(timestamp),
332
- change: DbChange.orThrow({
333
- table: firstRow.table,
334
- id: idBytesToId(firstRow.id),
335
- values,
336
- isInsert,
337
- isDelete,
338
- }),
339
- };
340
- return encodeAndEncryptDbChange(deps)(message, owner.encryptionKey);
341
- },
342
- };
343
- return ok(storage);
344
- };
345
- /** Creates a unique identifier for a {@link OwnerTransport}. */
346
- const createTransportKey = (transport) => {
347
- return `${transport.type}:${transport.url}`;
348
- };
349
- const dbChangeToColumns = (change, now) => {
350
- let values = objectToEntries(change.values);
351
- // SystemColumns are not encoded in change.values.
352
- values = appendToArray(values, [
353
- change.isInsert ? "createdAt" : "updatedAt",
354
- now,
355
- ]);
356
- if (change.isDelete != null) {
357
- values = appendToArray(values, [
358
- "isDeleted",
359
- booleanToSqliteBoolean(change.isDelete),
360
- ]);
361
- }
362
- return values;
363
- };
364
- export const applyLocalOnlyChange = (deps) => (change) => {
365
- if (change.isDelete) {
366
- const result = deps.sqlite.exec(sql `
367
- delete from ${sql.identifier(change.table)}
368
- where id = ${change.id};
369
- `);
370
- if (!result.ok)
371
- return result;
372
- }
373
- else {
374
- const ownerId = deps.appOwner.id;
375
- const columns = dbChangeToColumns(change, deps.time.nowIso());
376
- for (const [column, value] of columns) {
377
- const result = deps.sqlite.exec(sql.prepared `
378
- insert into ${sql.identifier(change.table)}
379
- ("ownerId", "id", ${sql.identifier(column)})
380
- values (${ownerId}, ${change.id}, ${value})
381
- on conflict ("ownerId", "id") do update
382
- set ${sql.identifier(column)} = ${value};
383
- `);
384
- if (!result.ok)
385
- return result;
386
- }
387
- }
388
- return ok();
389
- };
390
- const applyMessages = (deps) => (ownerId, messages) => {
391
- const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
392
- const usage = getOwnerUsage(deps)(ownerIdBytes, timestampToTimestampBytes(firstInArray(messages).timestamp));
393
- if (!usage.ok)
394
- return usage;
395
- let { firstTimestamp, lastTimestamp } = usage.value;
396
- for (const { timestamp, change } of messages) {
397
- const columns = dbChangeToColumns(change, timestampToDateIso(timestamp));
398
- const idBytes = idToIdBytes(change.id);
399
- const timestampBytes = timestampToTimestampBytes(timestamp);
400
- for (const [column, value] of columns) {
401
- if (validateColumnValue(deps)(change.table, column, value)) {
402
- const result = applyColumnChange(deps)(ownerIdBytes, ownerId, change.table, idBytes, change.id, column, value, timestampBytes);
403
- if (!result.ok)
404
- return result;
405
- }
406
- else {
407
- const result = deps.sqlite.exec(sql.prepared `
408
- insert into evolu_message_quarantine
409
- ("ownerId", "timestamp", "table", "id", "column", "value")
410
- values
411
- (
412
- ${ownerIdBytes},
413
- ${timestampBytes},
414
- ${change.table},
415
- ${idBytes},
416
- ${column},
417
- ${value}
418
- )
419
- on conflict do nothing;
420
- `);
421
- if (!result.ok)
422
- return result;
423
- }
424
- }
425
- let strategy;
426
- [strategy, firstTimestamp, lastTimestamp] = getTimestampInsertStrategy(timestampBytes, firstTimestamp, lastTimestamp);
427
- const result = deps.storage.insertTimestamp(ownerIdBytes, timestampBytes, strategy);
428
- if (!result.ok)
429
- return result;
430
- }
431
- /**
432
- * TODO: Implement proper storedBytes tracking for client using received and
433
- * sent encrypted message sizes.
434
- */
435
- return updateOwnerUsage(deps)(ownerIdBytes, 1, // Placeholder until proper tracking implemented
436
- firstTimestamp, lastTimestamp);
437
- };
438
- /**
439
- * System columns that can appear in sync messages. Excludes `ownerId` because
440
- * it's handled separately (stored per-row, not per-column in messages).
441
- */
442
- const systemColumnsWithoutOwnerId = systemColumns.difference(new Set(["ownerId"]));
443
- const validateColumnValue = (deps) => (table, column, _value) => {
444
- const schemaColumns = getProperty(deps.dbSchema.tables, table);
445
- return (schemaColumns != null &&
446
- (systemColumnsWithoutOwnerId.has(column) || schemaColumns.has(column)));
447
- };
448
- const applyColumnChange = (deps) => (ownerIdBytes, ownerId, table, idBytes, id, column, value, timestampBytes) => {
449
- const result = deps.sqlite.exec(sql.prepared `
450
- with
451
- existingTimestamp as (
452
- select 1
453
- from evolu_history
454
- where
455
- "ownerId" = ${ownerIdBytes}
456
- and "table" = ${table}
457
- and "id" = ${idBytes}
458
- and "column" = ${column}
459
- and "timestamp" >= ${timestampBytes}
460
- limit 1
461
- )
462
- insert into ${sql.identifier(table)}
463
- ("ownerId", "id", ${sql.identifier(column)})
464
- select ${ownerId}, ${id}, ${value}
465
- where not exists (select 1 from existingTimestamp)
466
- on conflict ("ownerId", "id") do update
467
- set ${sql.identifier(column)} = ${value}
468
- where not exists (select 1 from existingTimestamp);
469
- `);
470
- if (!result.ok)
471
- return result;
472
- {
473
- const result = deps.sqlite.exec(sql.prepared `
474
- insert into evolu_history
475
- ("ownerId", "table", "id", "column", "value", "timestamp")
476
- values
477
- (
478
- ${ownerIdBytes},
479
- ${table},
480
- ${idBytes},
481
- ${column},
482
- ${value},
483
- ${timestampBytes}
484
- )
485
- on conflict do nothing;
486
- `);
487
- if (!result.ok)
488
- return result;
489
- }
490
- return ok();
491
- };
492
- /**
493
- * Attempts to apply quarantined messages that may now be valid after a schema
494
- * update. Messages are quarantined when they reference tables or columns that
495
- * don't exist in the current schema (e.g., from a newer app version).
496
- */
497
- export const tryApplyQuarantinedMessages = (deps) => () => {
498
- const rows = deps.sqlite.exec(sql `
499
- select "ownerId", "timestamp", "table", "id", "column", "value"
500
- from evolu_message_quarantine;
501
- `);
502
- if (!rows.ok)
503
- return rows;
504
- for (const row of rows.value.rows) {
505
- if (!validateColumnValue(deps)(row.table, row.column, row.value))
506
- continue;
507
- const result = applyColumnChange(deps)(row.ownerId, ownerIdBytesToOwnerId(row.ownerId), row.table, row.id, idBytesToId(row.id), row.column, row.value, row.timestamp);
508
- if (!result.ok)
509
- return result;
510
- {
511
- const result = deps.sqlite.exec(sql `
512
- delete from evolu_message_quarantine
513
- where
514
- "ownerId" = ${row.ownerId}
515
- and "timestamp" = ${row.timestamp}
516
- and "table" = ${row.table}
517
- and "id" = ${row.id}
518
- and "column" = ${row.column};
519
- `);
520
- if (!result.ok)
521
- return result;
522
- }
523
- }
524
- return ok();
525
- };
526
- export const initialSyncState = { type: "SyncStateInitial" };
package/src/Instances.ts DELETED
@@ -1,92 +0,0 @@
1
- /**
2
- * Manages disposable instances by key, ensuring exactly one instance per key.
3
- *
4
- * Use cases:
5
- *
6
- * - One mutex per key to prevent concurrent writes
7
- * - Preserving state during hot module reloading
8
- *
9
- * **Important:** Do not use this as global shared state. Use it locally or pass
10
- * it as a dependency instead. The only exception is for hot reloading support,
11
- * where Evolu uses it to ensure only one instance exists across module reloads
12
- * (having two Evolu instances with the same name would mean two SQLite
13
- * connections to the same file, which could corrupt data).
14
- */
15
- export interface Instances<
16
- K extends string,
17
- T extends Disposable,
18
- > extends Disposable {
19
- /**
20
- * Ensures an instance exists for the given key, creating it if necessary. If
21
- * the instance already exists, the optional `onCacheHit` callback is invoked
22
- * to update the existing instance.
23
- */
24
- readonly ensure: (
25
- key: K,
26
- create: () => T,
27
- onCacheHit?: (instance: T) => void,
28
- ) => T;
29
-
30
- /** Gets an instance by key, or returns `null` if it doesn't exist. */
31
- readonly get: (key: K) => T | null;
32
-
33
- /** Checks if an instance exists for the given key. */
34
- readonly has: (key: K) => boolean;
35
-
36
- /**
37
- * Deletes and disposes an instance by key. Returns `true` if the instance
38
- * existed and was deleted, `false` otherwise.
39
- */
40
- readonly delete: (key: K) => boolean;
41
- }
42
-
43
- /** Creates an {@link Instances}. */
44
- export const createInstances = <
45
- K extends string,
46
- T extends Disposable,
47
- >(): Instances<K, T> => {
48
- const instances = new Map<K, T>();
49
-
50
- return {
51
- ensure: (key, create, onCacheHit) => {
52
- let instance = instances.get(key);
53
-
54
- if (instance == null) {
55
- instance = create();
56
- instances.set(key, instance);
57
- } else if (onCacheHit) {
58
- onCacheHit(instance);
59
- }
60
-
61
- return instance;
62
- },
63
-
64
- get: (key) => instances.get(key) ?? null,
65
-
66
- has: (key) => instances.has(key),
67
-
68
- delete: (key) => {
69
- const instance = instances.get(key);
70
- if (instance == null) return false;
71
- instances.delete(key);
72
- instance[Symbol.dispose]();
73
- return true;
74
- },
75
-
76
- [Symbol.dispose]: () => {
77
- const errors: Array<unknown> = [];
78
- for (const instance of instances.values()) {
79
- try {
80
- instance[Symbol.dispose]();
81
- } catch (error) {
82
- errors.push(error);
83
- }
84
- }
85
- instances.clear();
86
- if (errors.length === 1) throw errors[0];
87
- if (errors.length > 1) {
88
- throw new AggregateError(errors, "Multiple disposal errors occurred");
89
- }
90
- },
91
- };
92
- };