@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,53 +1,61 @@
1
+ /**
2
+ * Relay server for data synchronization.
3
+ *
4
+ * @module
5
+ */
6
+
1
7
  import {
8
+ dedupeArray,
2
9
  filterArray,
3
10
  firstInArray,
4
- isNonEmptyReadonlyArray,
11
+ isNonEmptyArray,
5
12
  mapArray,
6
13
  } from "../Array.js";
7
- import { ConsoleConfig, ConsoleDep } from "../Console.js";
8
- import { TimingSafeEqualDep } from "../Crypto.js";
9
- import { LazyValue } from "../Function.js";
10
- import { createInstances } from "../Instances.js";
11
- import { err, ok, Result } from "../Result.js";
12
- import { sql, SqliteDep, SqliteError } from "../Sqlite.js";
13
- import { createMutex, isAsync, MaybeAsync, Mutex } from "../Task.js";
14
- import { PositiveInt, SimpleName } from "../Type.js";
14
+ import { assert } from "../Assert.js";
15
+ import type { TimingSafeEqualDep } from "../Crypto.js";
16
+ import { err, ok } from "../Result.js";
17
+ import type { SqliteDep } from "../Sqlite.js";
18
+ import { sql } from "../Sqlite.js";
19
+ import { createMutexByKey } from "../Task.js";
20
+ import { Name, PositiveInt, uint8ArrayToBase64Url } from "../Type.js";
21
+ import { isPromiseLike, type Awaitable } from "../Types.js";
15
22
  import {
16
23
  OwnerId,
17
24
  ownerIdBytesToOwnerId,
18
- OwnerTransport,
25
+ // OwnerTransport,
19
26
  OwnerWriteKey,
20
27
  } from "./Owner.js";
21
- import { ProtocolInvalidDataError } from "./Protocol.js";
22
- import {
23
- createBaseSqliteStorage,
24
- CreateBaseSqliteStorageConfig,
28
+ import type {
25
29
  EncryptedDbChange,
26
- getOwnerUsage,
27
- getTimestampInsertStrategy,
28
30
  SqliteStorageDeps,
29
31
  Storage,
30
32
  StorageConfig,
31
33
  StorageQuotaError,
34
+ } from "./Storage.js";
35
+ import {
36
+ createBaseSqliteStorage,
37
+ getTimestampInsertStrategy,
38
+ readOwnerUsageOrDefault,
32
39
  updateOwnerUsage,
33
40
  } from "./Storage.js";
34
41
  import { timestampToTimestampBytes } from "./Timestamp.js";
35
42
 
36
- export interface RelayConfig extends ConsoleConfig, StorageConfig {
43
+ export interface RelayConfig extends StorageConfig {
37
44
  /**
38
45
  * The relay name.
39
46
  *
40
47
  * Implementations can use this for identification purposes (e.g., database
41
48
  * file name, logging).
42
49
  */
43
- readonly name?: SimpleName;
50
+ readonly name?: Name;
44
51
 
45
52
  /**
46
53
  * Optional callback to check if an {@link OwnerId} is allowed to access the
47
54
  * relay. If this callback is not provided, all owners are allowed.
48
55
  *
49
- * The callback receives the {@link OwnerId} and returns a {@link MaybeAsync}
50
- * boolean: `true` to allow access, or `false` to deny.
56
+ * The callback receives the {@link OwnerId} and an options object with an
57
+ * abort `AbortSignal`, and returns a {@link Awaitable} boolean: `true` to
58
+ * allow access, or `false` to deny.
51
59
  *
52
60
  * The callback can be synchronous (for SQLite or in-memory checks) or
53
61
  * asynchronous (for calling remote APIs).
@@ -60,11 +68,11 @@ export interface RelayConfig extends ConsoleConfig, StorageConfig {
60
68
  * relay access, not write permissions. Since all data is encrypted on the
61
69
  * relay, OwnerId exposure is safe.
62
70
  *
63
- * Owners specify which relays to connect to via {@link OwnerTransport}. In
71
+ * Owners specify which relays to connect to via `OwnerTransport`. In
64
72
  * WebSocket-based implementations, this check occurs before accepting the
65
- * connection, with the OwnerId typically extracted from the URL Path (e.g.,
66
- * `ws://localhost:4000/<ownerId>`). The relay requires the URL to be in the
67
- * correct format for OwnerId extraction.
73
+ * connection, with the OwnerId typically extracted from the URL query string
74
+ * (e.g., `ws://localhost:4000?ownerId=...`). The relay requires the URL to be
75
+ * in the correct format for OwnerId extraction.
68
76
  *
69
77
  * ### Example
70
78
  *
@@ -81,37 +89,39 @@ export interface RelayConfig extends ConsoleConfig, StorageConfig {
81
89
  *
82
90
  *
83
91
  * // Relay
84
- * isOwnerAllowed: (ownerId) =>
92
+ * isOwnerAllowed: (ownerId, { signal: _signal }) =>
85
93
  * Promise.resolve(ownerId === "6jy_2F4RT5qqeLgJ14_dnQ"),
86
94
  * ```
87
95
  */
88
- readonly isOwnerAllowed?: (ownerId: OwnerId) => MaybeAsync<boolean>;
96
+ readonly isOwnerAllowed?: (
97
+ ownerId: OwnerId,
98
+ options: {
99
+ /** Aborted when the relay stops waiting for the access check. */
100
+ readonly signal: AbortSignal;
101
+ },
102
+ ) => Awaitable<boolean>;
89
103
  }
90
104
 
91
105
  /**
92
- * A completely interchangeable server for syncing and backing up encrypted data
93
- * between Evolu clients.
106
+ * Sync and backup relay for Evolu clients.
94
107
  *
95
- * Unlike traditional servers, relays are blind by design—they transmit
96
- * encrypted data without understanding its shape or meaning. This enables true
97
- * decentralization and infinite horizontal scalability with minimal
98
- * infrastructure.
108
+ * A relay syncs and backs up encrypted data for Evolu apps. Evolu apps can use
109
+ * multiple relays at the same time, combining self-hosted and cloud relays for
110
+ * resilience. Relays are blind by design: they transmit and store encrypted
111
+ * data without understanding its shape or meaning.
99
112
  */
100
- export interface Relay extends Disposable {}
113
+ export interface Relay extends AsyncDisposable {
114
+ /** The TCP port actually bound by the relay. */
115
+ readonly port: number;
116
+ }
101
117
 
102
118
  export const createRelaySqliteStorage =
103
119
  (deps: SqliteStorageDeps & TimingSafeEqualDep) =>
104
- (config: CreateBaseSqliteStorageConfig): Storage => {
105
- const sqliteStorageBase = createBaseSqliteStorage(deps)(config);
106
-
107
- /**
108
- * Mutex instances are cached per OwnerId to prevent concurrent writes for
109
- * the same owner. Instances are never evicted, causing a memory leak
110
- * proportional to unique owner count. However, per-instance overhead should
111
- * be small. Monitor production memory usage to determine if
112
- * eviction/cleanup is needed.
113
- */
114
- const ownerMutexes = createInstances<OwnerId, Mutex>();
120
+ (config: StorageConfig): Storage => {
121
+ const sqliteStorageBase = createBaseSqliteStorage(deps);
122
+
123
+ /** Mutex keyed by OwnerId to prevent concurrent writes. */
124
+ const mutexByOwnerId = createMutexByKey<OwnerId>();
115
125
 
116
126
  return {
117
127
  ...sqliteStorageBase,
@@ -133,97 +143,91 @@ export const createRelaySqliteStorage =
133
143
  where ownerId = ${ownerId};
134
144
  `,
135
145
  );
136
- if (!selectWriteKey.ok) {
137
- config.onStorageError(selectWriteKey.error);
138
- return false;
139
- }
140
-
141
- const { rows } = selectWriteKey.value;
146
+ const { rows } = selectWriteKey;
142
147
 
143
- if (isNonEmptyReadonlyArray(rows)) {
148
+ if (isNonEmptyArray(rows)) {
144
149
  return deps.timingSafeEqual(rows[0].writeKey, writeKey);
145
150
  }
146
151
 
147
- const insertWriteKey = deps.sqlite.exec(sql`
152
+ deps.sqlite.exec(sql`
148
153
  insert into evolu_writeKey (ownerId, writeKey)
149
154
  values (${ownerId}, ${writeKey});
150
155
  `);
151
- if (!insertWriteKey.ok) {
152
- config.onStorageError(insertWriteKey.error);
153
- return false;
154
- }
155
156
 
156
157
  return true;
157
158
  },
158
159
 
159
160
  setWriteKey: (ownerId, writeKey) => {
160
- const upsertWriteKey = deps.sqlite.exec(sql`
161
+ deps.sqlite.exec(sql`
161
162
  insert into evolu_writeKey (ownerId, writeKey)
162
163
  values (${ownerId}, ${writeKey})
163
164
  on conflict (ownerId) do update
164
165
  set writeKey = excluded.writeKey;
165
166
  `);
166
- if (!upsertWriteKey.ok) {
167
- config.onStorageError(upsertWriteKey.error);
168
- return false;
169
- }
170
-
171
- return true;
172
167
  },
173
168
 
174
- writeMessages: async (ownerIdBytes, messages) => {
169
+ writeMessages: (ownerIdBytes, messages) => async (run) => {
175
170
  const ownerId = ownerIdBytesToOwnerId(ownerIdBytes);
176
- const messagesWithTimestampBytes = mapArray(messages, (m) => ({
177
- timestamp: timestampToTimestampBytes(m.timestamp),
178
- change: m.change,
179
- }));
180
-
181
- const result = await ownerMutexes
182
- .ensure(ownerId, createMutex)
183
- .withLock<void, SqliteError | StorageQuotaError>(async () => {
171
+ const uniqueMessagesWithTimestampBytes = dedupeArray(
172
+ mapArray(messages, (m) => ({
173
+ timestamp: timestampToTimestampBytes(m.timestamp),
174
+ change: m.change,
175
+ })),
176
+ (message) => uint8ArrayToBase64Url(message.timestamp),
177
+ );
178
+
179
+ return run(
180
+ mutexByOwnerId.withLock(ownerId, async () => {
184
181
  const existingTimestampsResult =
185
182
  sqliteStorageBase.getExistingTimestamps(
186
183
  ownerIdBytes,
187
- mapArray(messagesWithTimestampBytes, (m) => m.timestamp),
184
+ mapArray(uniqueMessagesWithTimestampBytes, (m) => m.timestamp),
188
185
  );
189
- if (!existingTimestampsResult.ok) return existingTimestampsResult;
190
186
 
191
- const existingTimestampsSet = new Set(
192
- existingTimestampsResult.value.map((t) => t.toString()),
187
+ const existingTimestampKeys = new Set(
188
+ mapArray(existingTimestampsResult, uint8ArrayToBase64Url),
193
189
  );
194
190
  const newMessages = filterArray(
195
- messagesWithTimestampBytes,
196
- (m) => !existingTimestampsSet.has(m.timestamp.toString()),
191
+ uniqueMessagesWithTimestampBytes,
192
+ (message) =>
193
+ !existingTimestampKeys.has(
194
+ uint8ArrayToBase64Url(message.timestamp),
195
+ ),
197
196
  );
198
197
 
199
198
  // Nothing to write
200
- if (!isNonEmptyReadonlyArray(newMessages)) {
199
+ if (!isNonEmptyArray(newMessages)) {
201
200
  return ok();
202
201
  }
203
202
 
204
- const usage = getOwnerUsage(deps)(
203
+ const usage = readOwnerUsageOrDefault(deps)(
205
204
  ownerIdBytes,
206
205
  firstInArray(newMessages).timestamp,
207
206
  );
208
- if (!usage.ok) return usage;
209
-
210
- const { storedBytes } = usage.value;
211
207
 
212
208
  const incomingBytes = newMessages.reduce(
213
209
  (sum, m) => sum + m.change.length,
214
210
  0,
215
211
  );
216
212
  const newStoredBytes = PositiveInt.orThrow(
217
- (storedBytes ?? 0) + incomingBytes,
213
+ (usage.storedBytes ?? 0) + incomingBytes,
218
214
  );
219
215
 
220
- const result = config.isOwnerWithinQuota(ownerId, newStoredBytes);
221
- const isWithinQuota = isAsync(result) ? await result : result;
216
+ const quotaResult = config.isOwnerWithinQuota(
217
+ ownerId,
218
+ newStoredBytes,
219
+ );
220
+ const isWithinQuota = isPromiseLike(quotaResult)
221
+ ? await quotaResult
222
+ : quotaResult;
222
223
  if (!isWithinQuota) {
223
- return err({ type: "StorageQuotaError", ownerId });
224
+ return err<StorageQuotaError>({
225
+ type: "StorageQuotaError",
226
+ ownerId,
227
+ });
224
228
  }
225
229
 
226
- let { firstTimestamp, lastTimestamp } = usage.value;
230
+ let { firstTimestamp, lastTimestamp } = usage;
227
231
 
228
232
  return deps.sqlite.transaction(() => {
229
233
  for (const { timestamp, change } of newMessages) {
@@ -235,45 +239,31 @@ export const createRelaySqliteStorage =
235
239
  lastTimestamp,
236
240
  );
237
241
 
238
- {
239
- const result = sqliteStorageBase.insertTimestamp(
240
- ownerIdBytes,
241
- timestamp,
242
- strategy,
243
- );
244
- if (!result.ok) return result;
245
- }
246
-
247
- {
248
- const result = deps.sqlite.exec(sql`
249
- insert into evolu_message ("ownerId", "timestamp", "change")
250
- values (${ownerIdBytes}, ${timestamp}, ${change})
251
- on conflict do nothing;
252
- `);
253
- if (!result.ok) return result;
254
- }
242
+ sqliteStorageBase.insertTimestamp(
243
+ ownerIdBytes,
244
+ timestamp,
245
+ strategy,
246
+ );
247
+
248
+ deps.sqlite.exec(sql`
249
+ insert into evolu_message
250
+ ("ownerId", "timestamp", "change")
251
+ values (${ownerIdBytes}, ${timestamp}, ${change})
252
+ on conflict do nothing;
253
+ `);
255
254
  }
256
255
 
257
- return updateOwnerUsage(deps)(
256
+ updateOwnerUsage(deps)(
258
257
  ownerIdBytes,
259
258
  newStoredBytes,
260
259
  firstTimestamp,
261
260
  lastTimestamp,
262
261
  );
263
- });
264
- })();
265
-
266
- if (!result.ok && result.error.type !== "AbortError") {
267
- switch (result.error.type) {
268
- case "SqliteError":
269
- config.onStorageError(result.error);
270
- return err({ type: "StorageWriteError", ownerId });
271
- case "StorageQuotaError":
272
- return err({ type: "StorageQuotaError", ownerId });
273
- }
274
- }
275
262
 
276
- return ok();
263
+ return ok();
264
+ });
265
+ }),
266
+ );
277
267
  },
278
268
 
279
269
  readDbChange: (ownerId, timestamp) => {
@@ -284,51 +274,35 @@ export const createRelaySqliteStorage =
284
274
  from evolu_message
285
275
  where "ownerId" = ${ownerId} and "timestamp" = ${timestamp};
286
276
  `);
287
- if (!result.ok) {
288
- config.onStorageError(result.error);
289
- return null;
290
- }
291
277
 
292
- return result.value.rows[0]?.change;
278
+ const row = result.rows[0];
279
+ assert(row, "Every timestamp must have a change");
280
+ return row.change;
293
281
  },
294
282
 
295
283
  deleteOwner: (ownerId) => {
296
- const transactionResult = deps.sqlite.transaction(() => {
297
- const deleteWriteKey = deps.sqlite.exec(sql`
284
+ deps.sqlite.transaction(() => {
285
+ deps.sqlite.exec(sql`
298
286
  delete from evolu_writeKey where ownerId = ${ownerId};
299
287
  `);
300
- if (!deleteWriteKey.ok) return deleteWriteKey;
301
288
 
302
- const deleteMessages = deps.sqlite.exec(sql`
289
+ deps.sqlite.exec(sql`
303
290
  delete from evolu_message where ownerId = ${ownerId};
304
291
  `);
305
- if (!deleteMessages.ok) return deleteMessages;
306
292
 
307
- const deleteUsage = deps.sqlite.exec(sql`
293
+ deps.sqlite.exec(sql`
308
294
  delete from evolu_usage where ownerId = ${ownerId};
309
295
  `);
310
- if (!deleteUsage.ok) return deleteUsage;
311
296
 
312
- const deleteBaseOwner = sqliteStorageBase.deleteOwner(ownerId);
313
- if (!deleteBaseOwner) return err(null);
297
+ sqliteStorageBase.deleteOwner(ownerId);
314
298
 
315
299
  return ok();
316
300
  });
317
-
318
- if (!transactionResult.ok) {
319
- if (transactionResult.error)
320
- config.onStorageError(transactionResult.error);
321
- return false;
322
- }
323
-
324
- return true;
325
301
  },
326
302
  };
327
303
  };
328
304
 
329
- export const createRelayStorageTables = (
330
- deps: SqliteDep,
331
- ): Result<void, SqliteError> => {
305
+ export const createRelayStorageTables = (deps: SqliteDep): void => {
332
306
  for (const query of [
333
307
  sql`
334
308
  create table evolu_writeKey (
@@ -349,134 +323,6 @@ export const createRelayStorageTables = (
349
323
  strict;
350
324
  `,
351
325
  ]) {
352
- const result = deps.sqlite.exec(query);
353
- if (!result.ok) return result;
326
+ deps.sqlite.exec(query);
354
327
  }
355
-
356
- return ok();
357
328
  };
358
-
359
- export interface RelayLogger {
360
- readonly started: (enableLogging: boolean, port: number) => void;
361
- readonly storageError: (error: unknown) => void;
362
- readonly upgradeSocketError: (error: Error) => void;
363
- readonly invalidOrMissingOwnerIdInUrl: (url: string | undefined) => void;
364
- readonly unauthorizedOwner: (ownerId: OwnerId) => void;
365
- readonly connectionEstablished: (totalConnectionCount: number) => void;
366
- readonly connectionWebSocketError: (error: Error) => void;
367
- readonly relayOptionSubscribe: (
368
- ownerId: OwnerId,
369
- getSubscriberCount: LazyValue<number>,
370
- ) => void;
371
- readonly relayOptionUnsubscribe: (
372
- ownerId: OwnerId,
373
- getSubscriberCount: LazyValue<number>,
374
- ) => void;
375
- readonly relayOptionBroadcast: (
376
- ownerId: OwnerId,
377
- broadcastCount: number,
378
- subscriberCount: number,
379
- ) => void;
380
- readonly messageLength: (messageLength: number) => void;
381
- readonly applyProtocolMessageAsRelayError: (
382
- error: ProtocolInvalidDataError,
383
- ) => void;
384
- readonly responseLength: (responseLength: number) => void;
385
- readonly applyProtocolMessageAsRelayUnknownError: (error: unknown) => void;
386
- readonly connectionClosed: (totalConnectionCount: number) => void;
387
- readonly shuttingDown: () => void;
388
- readonly webSocketServerDisposed: () => void;
389
- readonly httpServerDisposed: () => void;
390
- }
391
-
392
- export const createRelayLogger = (deps: ConsoleDep): RelayLogger => ({
393
- started: (enableLogging, port) => {
394
- deps.console.enabled = true;
395
- deps.console.log(`Evolu Relay started on port ${port}`);
396
- deps.console.enabled = enableLogging;
397
- },
398
-
399
- storageError: (error) => {
400
- deps.console.error("[relay]", "storage", error);
401
- },
402
-
403
- upgradeSocketError: (error) => {
404
- deps.console.warn("[relay]", "socket error", { error });
405
- },
406
-
407
- invalidOrMissingOwnerIdInUrl: (url) => {
408
- deps.console.warn("[relay]", "invalid or missing ownerId in URL", { url });
409
- },
410
-
411
- unauthorizedOwner: (ownerId) => {
412
- deps.console.warn("[relay]", "unauthorized owner", { ownerId });
413
- },
414
-
415
- connectionEstablished: (totalConnectionCount) => {
416
- deps.console.log("[relay]", "connection", { totalConnectionCount });
417
- },
418
-
419
- connectionWebSocketError: (error) => {
420
- deps.console.error("[relay]", "error", { error });
421
- },
422
-
423
- relayOptionSubscribe: (ownerId, getSubscriberCount) => {
424
- if (deps.console.enabled)
425
- deps.console.log("[relay]", "subscribe", {
426
- ownerId,
427
- subscriberCount: getSubscriberCount(),
428
- });
429
- },
430
-
431
- relayOptionUnsubscribe: (ownerId, getSubscriberCount) => {
432
- if (deps.console.enabled)
433
- deps.console.log("[relay]", "unsubscribe", {
434
- ownerId,
435
- subscriberCount: getSubscriberCount(),
436
- });
437
- },
438
-
439
- relayOptionBroadcast: (ownerId, broadcastCount, totalSubscribers) => {
440
- deps.console.log("[relay]", "broadcast", {
441
- ownerId,
442
- broadcastCount,
443
- totalSubscribers,
444
- });
445
- },
446
-
447
- messageLength: (messageLength) => {
448
- deps.console.log("[relay]", "on message", { messageLength });
449
- },
450
-
451
- applyProtocolMessageAsRelayError: (error) => {
452
- deps.console.error("[relay]", "applyProtocolMessageAsRelay", error);
453
- },
454
-
455
- responseLength: (responseLength) => {
456
- deps.console.log("[relay]", "responseLength", { responseLength });
457
- },
458
-
459
- applyProtocolMessageAsRelayUnknownError: (error) => {
460
- deps.console.error(
461
- "[relay]",
462
- "applyProtocolMessageAsRelayUnknownError",
463
- error,
464
- );
465
- },
466
-
467
- connectionClosed: (totalConnectionCount) => {
468
- deps.console.log("[relay]", "close", { totalConnectionCount });
469
- },
470
-
471
- shuttingDown: () => {
472
- deps.console.log("Shutting down Evolu Relay...");
473
- },
474
-
475
- webSocketServerDisposed: () => {
476
- deps.console.log("Evolu Relay WebSocketServer disposed");
477
- },
478
-
479
- httpServerDisposed: () => {
480
- deps.console.log("Evolu Relay HTTP server disposed");
481
- },
482
- });