@evolu/common 6.0.1-preview.3 → 6.0.1-preview.30

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 (173) hide show
  1. package/dist/src/Array.d.ts +58 -5
  2. package/dist/src/Array.d.ts.map +1 -1
  3. package/dist/src/Array.js +53 -5
  4. package/dist/src/Assert.d.ts +6 -16
  5. package/dist/src/Assert.d.ts.map +1 -1
  6. package/dist/src/Assert.js +6 -18
  7. package/dist/src/Brand.d.ts +75 -0
  8. package/dist/src/Brand.d.ts.map +1 -0
  9. package/dist/src/Brand.js +1 -0
  10. package/dist/src/Buffer.d.ts +1 -1
  11. package/dist/src/Buffer.d.ts.map +1 -1
  12. package/dist/src/Buffer.js +8 -7
  13. package/dist/src/Cache.d.ts +44 -0
  14. package/dist/src/Cache.d.ts.map +1 -0
  15. package/dist/src/Cache.js +52 -0
  16. package/dist/src/Callbacks.d.ts +45 -12
  17. package/dist/src/Callbacks.d.ts.map +1 -1
  18. package/dist/src/Callbacks.js +14 -7
  19. package/dist/src/Console.d.ts +31 -6
  20. package/dist/src/Console.d.ts.map +1 -1
  21. package/dist/src/Console.js +72 -9
  22. package/dist/src/Crypto.d.ts +61 -34
  23. package/dist/src/Crypto.d.ts.map +1 -1
  24. package/dist/src/Crypto.js +32 -45
  25. package/dist/src/Evolu/Db.d.ts +158 -65
  26. package/dist/src/Evolu/Db.d.ts.map +1 -1
  27. package/dist/src/Evolu/Db.js +286 -694
  28. package/dist/src/Evolu/Diff.d.ts +3 -3
  29. package/dist/src/Evolu/Diff.d.ts.map +1 -1
  30. package/dist/src/Evolu/Diff.js +7 -5
  31. package/dist/src/Evolu/Evolu.d.ts +208 -133
  32. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  33. package/dist/src/Evolu/Evolu.js +188 -183
  34. package/dist/src/Evolu/Internal.d.ts +0 -2
  35. package/dist/src/Evolu/Internal.d.ts.map +1 -1
  36. package/dist/src/Evolu/Internal.js +0 -2
  37. package/dist/src/Evolu/LocalAuth.d.ts +150 -0
  38. package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
  39. package/dist/src/Evolu/LocalAuth.js +174 -0
  40. package/dist/src/Evolu/Owner.d.ts +264 -120
  41. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  42. package/dist/src/Evolu/Owner.js +130 -104
  43. package/dist/src/Evolu/Platform.d.ts +9 -7
  44. package/dist/src/Evolu/Platform.d.ts.map +1 -1
  45. package/dist/src/Evolu/Protocol.d.ts +277 -232
  46. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  47. package/dist/src/Evolu/Protocol.js +603 -378
  48. package/dist/src/Evolu/Public.d.ts +6 -8
  49. package/dist/src/Evolu/Public.d.ts.map +1 -1
  50. package/dist/src/Evolu/Public.js +2 -3
  51. package/dist/src/Evolu/PublicKysely.js +3 -3
  52. package/dist/src/Evolu/Query.d.ts +2 -1
  53. package/dist/src/Evolu/Query.d.ts.map +1 -1
  54. package/dist/src/Evolu/Relay.d.ts +92 -7
  55. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  56. package/dist/src/Evolu/Relay.js +243 -76
  57. package/dist/src/Evolu/Schema.d.ts +129 -73
  58. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  59. package/dist/src/Evolu/Schema.js +169 -89
  60. package/dist/src/Evolu/Storage.d.ts +212 -26
  61. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  62. package/dist/src/Evolu/Storage.js +137 -79
  63. package/dist/src/Evolu/Sync.d.ts +68 -13
  64. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  65. package/dist/src/Evolu/Sync.js +422 -20
  66. package/dist/src/Evolu/Timestamp.d.ts +85 -27
  67. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  68. package/dist/src/Evolu/Timestamp.js +77 -18
  69. package/dist/src/Identicon.d.ts +35 -0
  70. package/dist/src/Identicon.d.ts.map +1 -0
  71. package/dist/src/Identicon.js +143 -0
  72. package/dist/src/Instances.d.ts +34 -0
  73. package/dist/src/Instances.d.ts.map +1 -0
  74. package/dist/src/Instances.js +44 -0
  75. package/dist/src/ManyToManyMap.d.ts +71 -10
  76. package/dist/src/ManyToManyMap.d.ts.map +1 -1
  77. package/dist/src/ManyToManyMap.js +41 -6
  78. package/dist/src/Number.d.ts +4 -3
  79. package/dist/src/Number.d.ts.map +1 -1
  80. package/dist/src/Number.js +5 -4
  81. package/dist/src/Platform.d.ts +20 -0
  82. package/dist/src/Platform.d.ts.map +1 -0
  83. package/dist/src/Platform.js +22 -0
  84. package/dist/src/Random.d.ts +3 -2
  85. package/dist/src/Random.d.ts.map +1 -1
  86. package/dist/src/Resources.d.ts +118 -0
  87. package/dist/src/Resources.d.ts.map +1 -0
  88. package/dist/src/Resources.js +197 -0
  89. package/dist/src/Result.d.ts +184 -52
  90. package/dist/src/Result.d.ts.map +1 -1
  91. package/dist/src/Result.js +30 -241
  92. package/dist/src/Skiplist.js +2 -1
  93. package/dist/src/Sqlite.d.ts +63 -5
  94. package/dist/src/Sqlite.d.ts.map +1 -1
  95. package/dist/src/Sqlite.js +110 -9
  96. package/dist/src/Task.d.ts +586 -0
  97. package/dist/src/Task.d.ts.map +1 -0
  98. package/dist/src/Task.js +469 -0
  99. package/dist/src/Time.d.ts +66 -1
  100. package/dist/src/Time.d.ts.map +1 -1
  101. package/dist/src/Time.js +99 -5
  102. package/dist/src/Type.d.ts +621 -340
  103. package/dist/src/Type.d.ts.map +1 -1
  104. package/dist/src/Type.js +665 -464
  105. package/dist/src/Types.d.ts +1 -75
  106. package/dist/src/Types.d.ts.map +1 -1
  107. package/dist/src/WebSocket.d.ts +5 -2
  108. package/dist/src/WebSocket.d.ts.map +1 -1
  109. package/dist/src/WebSocket.js +12 -18
  110. package/dist/src/Worker.d.ts +39 -11
  111. package/dist/src/Worker.d.ts.map +1 -1
  112. package/dist/src/Worker.js +22 -4
  113. package/dist/src/index.d.ts +7 -2
  114. package/dist/src/index.d.ts.map +1 -1
  115. package/dist/src/index.js +7 -2
  116. package/package.json +14 -13
  117. package/src/Array.ts +76 -11
  118. package/src/Assert.ts +6 -24
  119. package/src/Brand.ts +75 -0
  120. package/src/Buffer.ts +7 -7
  121. package/src/Cache.ts +85 -0
  122. package/src/Callbacks.ts +62 -22
  123. package/src/Console.ts +91 -11
  124. package/src/Crypto.ts +97 -82
  125. package/src/Evolu/Db.ts +514 -1020
  126. package/src/Evolu/Diff.ts +7 -5
  127. package/src/Evolu/Evolu.ts +464 -355
  128. package/src/Evolu/Internal.ts +0 -2
  129. package/src/Evolu/LocalAuth.ts +463 -0
  130. package/src/Evolu/Owner.ts +369 -228
  131. package/src/Evolu/Platform.ts +9 -9
  132. package/src/Evolu/Protocol.ts +859 -676
  133. package/src/Evolu/Public.ts +7 -14
  134. package/src/Evolu/PublicKysely.ts +3 -3
  135. package/src/Evolu/Query.ts +2 -1
  136. package/src/Evolu/Relay.ts +420 -92
  137. package/src/Evolu/Schema.ts +391 -191
  138. package/src/Evolu/Storage.ts +451 -118
  139. package/src/Evolu/Sync.ts +720 -37
  140. package/src/Evolu/Timestamp.ts +88 -35
  141. package/src/Identicon.ts +197 -0
  142. package/src/Instances.ts +90 -0
  143. package/src/ManyToManyMap.ts +124 -24
  144. package/src/Number.ts +6 -10
  145. package/src/Platform.ts +26 -0
  146. package/src/Random.ts +3 -2
  147. package/src/Resources.ts +367 -0
  148. package/src/Result.ts +191 -54
  149. package/src/Skiplist.ts +1 -1
  150. package/src/Sqlite.ts +122 -17
  151. package/src/Task.ts +901 -0
  152. package/src/Time.ts +180 -5
  153. package/src/Type.ts +1083 -727
  154. package/src/Types.ts +1 -77
  155. package/src/WebSocket.ts +27 -25
  156. package/src/Worker.ts +72 -23
  157. package/src/index.ts +7 -2
  158. package/dist/src/Evolu/Config.d.ts +0 -69
  159. package/dist/src/Evolu/Config.d.ts.map +0 -1
  160. package/dist/src/Evolu/Config.js +0 -9
  161. package/dist/src/Evolu/Kysely.d.ts +0 -6
  162. package/dist/src/Evolu/Kysely.d.ts.map +0 -1
  163. package/dist/src/Evolu/Kysely.js +0 -21
  164. package/dist/src/NanoId.d.ts +0 -27
  165. package/dist/src/NanoId.d.ts.map +0 -1
  166. package/dist/src/NanoId.js +0 -6
  167. package/dist/src/Promise.d.ts +0 -180
  168. package/dist/src/Promise.d.ts.map +0 -1
  169. package/dist/src/Promise.js +0 -176
  170. package/src/Evolu/Config.ts +0 -83
  171. package/src/Evolu/Kysely.ts +0 -38
  172. package/src/NanoId.ts +0 -39
  173. package/src/Promise.ts +0 -295
@@ -5,23 +5,17 @@
5
5
  */
6
6
 
7
7
  export { createEvolu } from "./Evolu.js";
8
- export type {
9
- Evolu,
10
- EvoluConfigWithInitialData,
11
- EvoluDeps,
12
- EvoluError,
13
- } from "./Evolu.js";
8
+ export type { Evolu, EvoluConfig, EvoluDeps, EvoluError } from "./Evolu.js";
14
9
  export * from "./Owner.js";
15
- export { binaryIdToId, idToBinaryId } from "./Protocol.js";
16
- export type { BinaryId } from "./Protocol.js";
10
+ export * from "./LocalAuth.js";
17
11
  export * as kysely from "./PublicKysely.js";
18
12
  export type { InferRow, Query, QueryRows, Row } from "./Query.js";
19
- export { formatValidMutationSizeError } from "./Schema.js";
20
- export type { EvoluSchema, ValidMutationSizeError } from "./Schema.js";
13
+ export type { EvoluSchema } from "./Schema.js";
21
14
  export type {
22
15
  NetworkError,
23
16
  PaymentRequiredError,
24
17
  ServerError,
18
+ SyncOwner,
25
19
  SyncState,
26
20
  SyncStateInitial,
27
21
  SyncStateIsNotSynced,
@@ -29,15 +23,14 @@ export type {
29
23
  SyncStateIsSyncing,
30
24
  } from "./Sync.js";
31
25
  export {
32
- binaryTimestampToTimestamp,
33
26
  Timestamp,
34
- timestampToBinaryTimestamp,
27
+ timestampBytesToTimestamp,
28
+ timestampToTimestampBytes,
35
29
  } from "./Timestamp.js";
36
30
  export type {
37
- BinaryTimestamp,
31
+ TimestampBytes,
38
32
  TimestampCounterOverflowError,
39
33
  TimestampDriftError,
40
- TimestampDuplicateNodeError,
41
34
  TimestampError,
42
35
  TimestampTimeOutOfRangeError,
43
36
  } from "./Timestamp.js";
@@ -65,7 +65,7 @@ export type { NotNull } from "kysely";
65
65
  * from "person"
66
66
  * ```
67
67
  */
68
- // SQL formatting breaks generated SQL for some reason.
68
+ // Kysely expects strict AST.
69
69
  // prettier-ignore
70
70
  export function jsonArrayFrom<O>(
71
71
  expr: SelectQueryBuilderExpression<O>,
@@ -122,7 +122,7 @@ export function jsonArrayFrom<O>(
122
122
  * from "person";
123
123
  * ```
124
124
  */
125
- // SQL formatting breaks generated SQL for some reason.
125
+ // Kysely expects strict AST.
126
126
  // prettier-ignore
127
127
  export function jsonObjectFrom<O>(
128
128
  expr: SelectQueryBuilderExpression<O>,
@@ -172,7 +172,7 @@ export function jsonObjectFrom<O>(
172
172
  * from "person"
173
173
  * ```
174
174
  */
175
- // SQL formatting breaks generated SQL for some reason.
175
+ // Kysely expects strict AST.
176
176
  // prettier-ignore
177
177
  export function jsonBuildObject<O extends Record<string, Expression<unknown>>>(
178
178
  obj: O,
@@ -8,7 +8,8 @@ import {
8
8
  SqliteValue,
9
9
  } from "../Sqlite.js";
10
10
  import { Store, StoreSubscribe } from "../Store.js";
11
- import { Brand, Simplify } from "../Types.js";
11
+ import { Simplify } from "../Types.js";
12
+ import { Brand } from "../Brand.js";
12
13
 
13
14
  /**
14
15
  * A type-safe SQL query.
@@ -1,59 +1,115 @@
1
- import { isNonEmptyReadonlyArray } from "../Array.js";
2
- import { ConsoleConfig } from "../Console.js";
3
- import { eqArrayNumber } from "../Eq.js";
4
- import { ok, Result } from "../Result.js";
5
- import { sql, SqliteError } from "../Sqlite.js";
6
- import { SimpleName } from "../Type.js";
7
- import { OwnerId, WriteKey } from "./Owner.js";
8
- import { EncryptedDbChange, Storage } from "./Protocol.js";
1
+ import { filterArray, isNonEmptyReadonlyArray, mapArray } from "../Array.js";
2
+ import { ConsoleConfig, ConsoleDep } from "../Console.js";
3
+ import { TimingSafeEqualDep } from "../Crypto.js";
4
+ import { LazyValue } from "../Function.js";
5
+ import { createInstances } from "../Instances.js";
6
+ import { err, ok, Result } from "../Result.js";
7
+ import { sql, SqliteDep, SqliteError } from "../Sqlite.js";
8
+ import { createMutex, isAsync, MaybeAsync, Mutex } from "../Task.js";
9
+ import { NonNegativeInt, PositiveInt, SimpleName } from "../Type.js";
9
10
  import {
10
- createSqliteStorageBase,
11
- CreateSqliteStorageBaseOptions,
11
+ OwnerId,
12
+ ownerIdBytesToOwnerId,
13
+ OwnerTransport,
14
+ OwnerWriteKey,
15
+ } from "./Owner.js";
16
+ import { ProtocolInvalidDataError } from "./Protocol.js";
17
+ import {
18
+ createBaseSqliteStorage,
19
+ CreateBaseSqliteStorageConfig,
20
+ EncryptedDbChange,
12
21
  SqliteStorageDeps,
22
+ Storage,
23
+ StorageConfig,
24
+ StorageQuotaError,
13
25
  } from "./Storage.js";
14
- import { timestampToBinaryTimestamp } from "./Timestamp.js";
15
-
16
- export interface Relay extends Disposable {}
26
+ import { timestampToTimestampBytes } from "./Timestamp.js";
17
27
 
18
- export interface RelayConfig extends ConsoleConfig {
28
+ export interface RelayConfig extends ConsoleConfig, StorageConfig {
29
+ /**
30
+ * The relay name.
31
+ *
32
+ * Implementations can use this for identification purposes (e.g., database
33
+ * file name, logging).
34
+ */
19
35
  readonly name?: SimpleName;
36
+
37
+ /**
38
+ * Optional callback to check if an {@link OwnerId} is allowed to access the
39
+ * relay. If this callback is not provided, all owners are allowed.
40
+ *
41
+ * The callback receives the {@link OwnerId} and returns a {@link MaybeAsync}
42
+ * boolean: `true` to allow access, or `false` to deny.
43
+ *
44
+ * The callback can be synchronous (for SQLite or in-memory checks) or
45
+ * asynchronous (for calling remote APIs).
46
+ *
47
+ * The callback returns a boolean rather than an error type because error
48
+ * handling and logging are the responsibility of the callback
49
+ * implementation.
50
+ *
51
+ * OwnerId is used rather than short-lived tokens because this only controls
52
+ * relay access, not write permissions. Since all data is encrypted on the
53
+ * relay, OwnerId exposure is safe.
54
+ *
55
+ * Owners specify which relays to connect to via {@link OwnerTransport}. In
56
+ * WebSocket-based implementations, this check occurs before accepting the
57
+ * connection, with the OwnerId typically extracted from the URL Path (e.g.,
58
+ * `ws://localhost:4000/<ownerId>`). The relay requires the URL to be in the
59
+ * correct format for OwnerId extraction.
60
+ *
61
+ * ### Example
62
+ *
63
+ * ```ts
64
+ * // Client
65
+ * const transport = createOwnerWebSocketTransport({
66
+ * url: "wss://relay.evolu.dev",
67
+ * ownerId: owner.id,
68
+ * });
69
+ *
70
+ * const evolu = createEvolu(deps)(Schema, {
71
+ * transports: [transport],
72
+ * });
73
+ *
74
+ *
75
+ * // Relay
76
+ * isOwnerAllowed: (ownerId) =>
77
+ * Promise.resolve(ownerId === "6jy_2F4RT5qqeLgJ14_dnQ"),
78
+ * ```
79
+ */
80
+ readonly isOwnerAllowed?: (ownerId: OwnerId) => MaybeAsync<boolean>;
20
81
  }
21
82
 
22
- export const createRelayStorage =
23
- (deps: SqliteStorageDeps) =>
24
- (options: CreateSqliteStorageBaseOptions): Result<Storage, SqliteError> => {
25
- const sqliteStorageBase = createSqliteStorageBase(deps)(options);
26
- if (!sqliteStorageBase.ok) return sqliteStorageBase;
27
-
28
- for (const query of [
29
- sql`
30
- create table if not exists evolu_writeKey (
31
- "ownerId" blob not null,
32
- "writeKey" blob not null,
33
- primary key ("ownerId")
34
- )
35
- strict;
36
- `,
37
-
38
- sql`
39
- create table if not exists evolu_message (
40
- "ownerId" blob not null,
41
- "timestamp" blob not null,
42
- "change" blob not null,
43
- primary key ("ownerId", "timestamp")
44
- )
45
- strict;
46
- `,
47
- ]) {
48
- const result = deps.sqlite.exec(query);
49
- if (!result.ok) return result;
50
- }
51
-
52
- return ok({
53
- ...sqliteStorageBase.value,
83
+ /**
84
+ * A completely interchangeable server for syncing and backing up encrypted data
85
+ * between Evolu clients.
86
+ *
87
+ * Unlike traditional servers, relays are blind by design—they transmit
88
+ * encrypted data without understanding its shape or meaning. This enables true
89
+ * decentralization and infinite horizontal scalability with minimal
90
+ * infrastructure.
91
+ */
92
+ export interface Relay extends Disposable {}
93
+
94
+ export const createRelaySqliteStorage =
95
+ (deps: SqliteStorageDeps & TimingSafeEqualDep) =>
96
+ (config: CreateBaseSqliteStorageConfig): Storage => {
97
+ const sqliteStorageBase = createBaseSqliteStorage(deps)(config);
98
+
99
+ /**
100
+ * Mutex instances are cached per OwnerId to prevent concurrent writes for
101
+ * the same owner. Instances are never evicted, causing a memory leak
102
+ * proportional to unique owner count. However, per-instance overhead should
103
+ * be small. Monitor production memory usage to determine if
104
+ * eviction/cleanup is needed.
105
+ */
106
+ const ownerMutexes = createInstances<OwnerId, Mutex>();
107
+
108
+ return {
109
+ ...sqliteStorageBase,
54
110
 
55
111
  /**
56
- * Lazily authorizes the initiator's {@link WriteKey} for the given
112
+ * Lazily authorizes the initiator's {@link OwnerWriteKey} for the given
57
113
  * {@link OwnerId}.
58
114
  *
59
115
  * - If the {@link OwnerId} does not exist, it is created and associated with
@@ -62,65 +118,150 @@ export const createRelayStorage =
62
118
  * stored key.
63
119
  */
64
120
  validateWriteKey: (ownerId, writeKey) => {
65
- const selectWriteKey = deps.sqlite.exec<{ writeKey: WriteKey }>(sql`
66
- select writeKey
67
- from evolu_writeKey
68
- where ownerId = ${ownerId};
69
- `);
121
+ const selectWriteKey = deps.sqlite.exec<{ writeKey: OwnerWriteKey }>(
122
+ sql`
123
+ select writeKey
124
+ from evolu_writeKey
125
+ where ownerId = ${ownerId};
126
+ `,
127
+ );
70
128
  if (!selectWriteKey.ok) {
71
- options.onStorageError(selectWriteKey.error);
129
+ config.onStorageError(selectWriteKey.error);
72
130
  return false;
73
131
  }
74
132
 
75
133
  const { rows } = selectWriteKey.value;
76
134
 
77
- if (!isNonEmptyReadonlyArray(rows)) {
78
- const insertWriteKey = deps.sqlite.exec(sql`
79
- insert into evolu_writeKey (ownerId, writeKey)
80
- values (${ownerId}, ${writeKey});
81
- `);
82
- if (!insertWriteKey.ok) {
83
- options.onStorageError(insertWriteKey.error);
84
- return false;
85
- }
135
+ if (isNonEmptyReadonlyArray(rows)) {
136
+ return deps.timingSafeEqual(rows[0].writeKey, writeKey);
137
+ }
138
+
139
+ const insertWriteKey = deps.sqlite.exec(sql`
140
+ insert into evolu_writeKey (ownerId, writeKey)
141
+ values (${ownerId}, ${writeKey});
142
+ `);
143
+ if (!insertWriteKey.ok) {
144
+ config.onStorageError(insertWriteKey.error);
145
+ return false;
146
+ }
86
147
 
87
- return true;
148
+ return true;
149
+ },
150
+
151
+ setWriteKey: (ownerId, writeKey) => {
152
+ const upsertWriteKey = deps.sqlite.exec(sql`
153
+ insert into evolu_writeKey (ownerId, writeKey)
154
+ values (${ownerId}, ${writeKey})
155
+ on conflict (ownerId) do update
156
+ set writeKey = excluded.writeKey;
157
+ `);
158
+ if (!upsertWriteKey.ok) {
159
+ config.onStorageError(upsertWriteKey.error);
160
+ return false;
88
161
  }
89
162
 
90
- return eqArrayNumber(rows[0].writeKey, writeKey);
163
+ return true;
91
164
  },
92
165
 
93
- writeMessages: (ownerId, messages) => {
94
- const result = deps.sqlite.transaction(() => {
95
- for (const message of messages) {
96
- const insertTimestampResult =
97
- sqliteStorageBase.value.insertTimestamp(
98
- ownerId,
99
- timestampToBinaryTimestamp(message.timestamp),
166
+ writeMessages: async (ownerIdBytes, messages) => {
167
+ const ownerId = ownerIdBytesToOwnerId(ownerIdBytes);
168
+ const messagesWithTimestampBytes = mapArray(messages, (m) => ({
169
+ timestamp: timestampToTimestampBytes(m.timestamp),
170
+ change: m.change,
171
+ }));
172
+
173
+ const result = await ownerMutexes
174
+ .ensure(ownerId, createMutex)
175
+ .withLock<void, SqliteError | StorageQuotaError>(async () => {
176
+ const existingTimestampsResult =
177
+ sqliteStorageBase.getExistingTimestamps(
178
+ ownerIdBytes,
179
+ mapArray(messagesWithTimestampBytes, (m) => m.timestamp),
100
180
  );
101
- if (!insertTimestampResult.ok) return insertTimestampResult;
102
-
103
- const insertMessage = deps.sqlite.exec(sql`
104
- insert into evolu_message ("ownerId", "timestamp", "change")
105
- values
106
- (
107
- ${ownerId},
108
- ${timestampToBinaryTimestamp(message.timestamp)},
109
- ${message.change}
110
- )
111
- on conflict do nothing;
181
+ if (!existingTimestampsResult.ok) return existingTimestampsResult;
182
+
183
+ const existingTimestampsSet = new Set(
184
+ existingTimestampsResult.value.map((t) => t.toString()),
185
+ );
186
+ const newMessages = filterArray(
187
+ messagesWithTimestampBytes,
188
+ (m) => !existingTimestampsSet.has(m.timestamp.toString()),
189
+ );
190
+
191
+ // Nothing to write
192
+ if (!isNonEmptyReadonlyArray(newMessages)) {
193
+ return ok();
194
+ }
195
+
196
+ const storedBytesResult = deps.sqlite.exec<{
197
+ storedBytes: NonNegativeInt;
198
+ }>(sql`
199
+ select storedBytes
200
+ from evolu_usage
201
+ where ownerId = ${ownerIdBytes};
112
202
  `);
113
- if (!insertMessage.ok) return insertMessage;
114
- }
115
- return ok();
116
- });
203
+ if (!storedBytesResult.ok) return storedBytesResult;
117
204
 
118
- if (!result.ok) {
119
- options.onStorageError(result.error);
120
- return false;
205
+ const storedBytes =
206
+ storedBytesResult.value.rows[0]?.storedBytes ?? 0;
207
+ const incomingBytes = newMessages.reduce(
208
+ (sum, m) => sum + m.change.length,
209
+ 0,
210
+ );
211
+ const newStoredBytes = PositiveInt.orThrow(
212
+ storedBytes + incomingBytes,
213
+ );
214
+
215
+ const withinQuotaResult = config.isOwnerWithinQuota(
216
+ ownerId,
217
+ newStoredBytes,
218
+ );
219
+ const isWithinQuota = isAsync(withinQuotaResult)
220
+ ? await withinQuotaResult
221
+ : withinQuotaResult;
222
+ if (!isWithinQuota) {
223
+ return err({ type: "StorageQuotaError", ownerId });
224
+ }
225
+
226
+ return deps.sqlite.transaction(() => {
227
+ for (const { timestamp, change } of newMessages) {
228
+ const insertTimestampResult = sqliteStorageBase.insertTimestamp(
229
+ ownerIdBytes,
230
+ timestamp,
231
+ );
232
+ if (!insertTimestampResult.ok) return insertTimestampResult;
233
+
234
+ const insertMessage = deps.sqlite.exec(sql`
235
+ insert into evolu_message ("ownerId", "timestamp", "change")
236
+ values (${ownerIdBytes}, ${timestamp}, ${change})
237
+ on conflict do nothing;
238
+ `);
239
+ if (!insertMessage.ok) return insertMessage;
240
+ }
241
+
242
+ const updateUsage = deps.sqlite.exec(sql`
243
+ insert into evolu_usage ("ownerId", "storedBytes")
244
+ values (${ownerIdBytes}, ${newStoredBytes})
245
+ on conflict (ownerId) do update
246
+ set storedBytes = ${newStoredBytes};
247
+ `);
248
+ if (!updateUsage.ok) return updateUsage;
249
+
250
+ return ok();
251
+ });
252
+ })();
253
+
254
+ if (!result.ok && result.error.type !== "AbortError") {
255
+ switch (result.error.type) {
256
+ case "SqliteError":
257
+ config.onStorageError(result.error);
258
+ return err({ type: "StorageWriteError", ownerId });
259
+ case "StorageQuotaError":
260
+ return err({ type: "StorageQuotaError", ownerId });
261
+ }
121
262
  }
122
263
 
123
- return true;
264
+ return ok();
124
265
  },
125
266
 
126
267
  readDbChange: (ownerId, timestamp) => {
@@ -132,11 +273,198 @@ export const createRelayStorage =
132
273
  where "ownerId" = ${ownerId} and "timestamp" = ${timestamp};
133
274
  `);
134
275
  if (!result.ok) {
135
- options.onStorageError(result.error);
276
+ config.onStorageError(result.error);
136
277
  return null;
137
278
  }
138
279
 
139
280
  return result.value.rows[0]?.change;
140
281
  },
141
- });
282
+
283
+ deleteOwner: (ownerId) => {
284
+ const transactionResult = deps.sqlite.transaction(() => {
285
+ const deleteWriteKey = deps.sqlite.exec(sql`
286
+ delete from evolu_writeKey where ownerId = ${ownerId};
287
+ `);
288
+ if (!deleteWriteKey.ok) return deleteWriteKey;
289
+
290
+ const deleteMessages = deps.sqlite.exec(sql`
291
+ delete from evolu_message where ownerId = ${ownerId};
292
+ `);
293
+ if (!deleteMessages.ok) return deleteMessages;
294
+
295
+ const deleteUsage = deps.sqlite.exec(sql`
296
+ delete from evolu_usage where ownerId = ${ownerId};
297
+ `);
298
+ if (!deleteUsage.ok) return deleteUsage;
299
+
300
+ const deleteBaseOwner = sqliteStorageBase.deleteOwner(ownerId);
301
+ if (!deleteBaseOwner) return err(null);
302
+
303
+ return ok();
304
+ });
305
+
306
+ if (!transactionResult.ok) {
307
+ if (transactionResult.error)
308
+ config.onStorageError(transactionResult.error);
309
+ return false;
310
+ }
311
+
312
+ return true;
313
+ },
314
+ };
142
315
  };
316
+
317
+ export const createRelayStorageTables = (
318
+ deps: SqliteDep,
319
+ ): Result<void, SqliteError> => {
320
+ for (const query of [
321
+ sql`
322
+ create table evolu_writeKey (
323
+ "ownerId" blob not null,
324
+ "writeKey" blob not null,
325
+ primary key ("ownerId")
326
+ )
327
+ strict;
328
+ `,
329
+
330
+ sql`
331
+ create table evolu_message (
332
+ "ownerId" blob not null,
333
+ "timestamp" blob not null,
334
+ "change" blob not null,
335
+ primary key ("ownerId", "timestamp")
336
+ )
337
+ strict;
338
+ `,
339
+ ]) {
340
+ const result = deps.sqlite.exec(query);
341
+ if (!result.ok) return result;
342
+ }
343
+
344
+ return ok();
345
+ };
346
+
347
+ export interface RelayLogger {
348
+ readonly started: (enableLogging: boolean, port: number) => void;
349
+ readonly storageError: (error: unknown) => void;
350
+ readonly upgradeSocketError: (error: Error) => void;
351
+ readonly invalidOrMissingOwnerIdInUrl: (url: string | undefined) => void;
352
+ readonly unauthorizedOwner: (ownerId: OwnerId) => void;
353
+ readonly connectionEstablished: (totalConnectionCount: number) => void;
354
+ readonly connectionWebSocketError: (error: Error) => void;
355
+ readonly relayOptionSubscribe: (
356
+ ownerId: OwnerId,
357
+ getSubscriberCount: LazyValue<number>,
358
+ ) => void;
359
+ readonly relayOptionUnsubscribe: (
360
+ ownerId: OwnerId,
361
+ getSubscriberCount: LazyValue<number>,
362
+ ) => void;
363
+ readonly relayOptionBroadcast: (
364
+ ownerId: OwnerId,
365
+ broadcastCount: number,
366
+ subscriberCount: number,
367
+ ) => void;
368
+ readonly messageLength: (messageLength: number) => void;
369
+ readonly applyProtocolMessageAsRelayError: (
370
+ error: ProtocolInvalidDataError,
371
+ ) => void;
372
+ readonly responseLength: (responseLength: number) => void;
373
+ readonly applyProtocolMessageAsRelayUnknownError: (error: unknown) => void;
374
+ readonly connectionClosed: (totalConnectionCount: number) => void;
375
+ readonly shuttingDown: () => void;
376
+ readonly webSocketServerDisposed: () => void;
377
+ readonly httpServerDisposed: () => void;
378
+ }
379
+
380
+ export const createRelayLogger = (deps: ConsoleDep): RelayLogger => ({
381
+ started: (enableLogging, port) => {
382
+ deps.console.enabled = true;
383
+ deps.console.log(`Evolu Relay started on port ${port}`);
384
+ deps.console.enabled = enableLogging;
385
+ },
386
+
387
+ storageError: (error) => {
388
+ deps.console.error("[relay]", "storage", error);
389
+ },
390
+
391
+ upgradeSocketError: (error) => {
392
+ deps.console.warn("[relay]", "socket error", { error });
393
+ },
394
+
395
+ invalidOrMissingOwnerIdInUrl: (url) => {
396
+ deps.console.warn("[relay]", "invalid or missing ownerId in URL", { url });
397
+ },
398
+
399
+ unauthorizedOwner: (ownerId) => {
400
+ deps.console.warn("[relay]", "unauthorized owner", { ownerId });
401
+ },
402
+
403
+ connectionEstablished: (totalConnectionCount) => {
404
+ deps.console.log("[relay]", "connection", { totalConnectionCount });
405
+ },
406
+
407
+ connectionWebSocketError: (error) => {
408
+ deps.console.error("[relay]", "error", { error });
409
+ },
410
+
411
+ relayOptionSubscribe: (ownerId, getSubscriberCount) => {
412
+ if (deps.console.enabled)
413
+ deps.console.log("[relay]", "subscribe", {
414
+ ownerId,
415
+ subscriberCount: getSubscriberCount(),
416
+ });
417
+ },
418
+
419
+ relayOptionUnsubscribe: (ownerId, getSubscriberCount) => {
420
+ if (deps.console.enabled)
421
+ deps.console.log("[relay]", "unsubscribe", {
422
+ ownerId,
423
+ subscriberCount: getSubscriberCount(),
424
+ });
425
+ },
426
+
427
+ relayOptionBroadcast: (ownerId, broadcastCount, totalSubscribers) => {
428
+ deps.console.log("[relay]", "broadcast", {
429
+ ownerId,
430
+ broadcastCount,
431
+ totalSubscribers,
432
+ });
433
+ },
434
+
435
+ messageLength: (messageLength) => {
436
+ deps.console.log("[relay]", "on message", { messageLength });
437
+ },
438
+
439
+ applyProtocolMessageAsRelayError: (error) => {
440
+ deps.console.error("[relay]", "applyProtocolMessageAsRelay", error);
441
+ },
442
+
443
+ responseLength: (responseLength) => {
444
+ deps.console.log("[relay]", "responseLength", { responseLength });
445
+ },
446
+
447
+ applyProtocolMessageAsRelayUnknownError: (error) => {
448
+ deps.console.error(
449
+ "[relay]",
450
+ "applyProtocolMessageAsRelayUnknownError",
451
+ error,
452
+ );
453
+ },
454
+
455
+ connectionClosed: (totalConnectionCount) => {
456
+ deps.console.log("[relay]", "close", { totalConnectionCount });
457
+ },
458
+
459
+ shuttingDown: () => {
460
+ deps.console.log("Shutting down Evolu Relay...");
461
+ },
462
+
463
+ webSocketServerDisposed: () => {
464
+ deps.console.log("Evolu Relay WebSocketServer disposed");
465
+ },
466
+
467
+ httpServerDisposed: () => {
468
+ deps.console.log("Evolu Relay HTTP server disposed");
469
+ },
470
+ });