@evolu/common 6.0.1-preview.24 → 6.0.1-preview.26

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 (66) hide show
  1. package/dist/src/Assert.d.ts +0 -13
  2. package/dist/src/Assert.d.ts.map +1 -1
  3. package/dist/src/Assert.js +0 -15
  4. package/dist/src/Cache.d.ts +44 -0
  5. package/dist/src/Cache.d.ts.map +1 -0
  6. package/dist/src/Cache.js +52 -0
  7. package/dist/src/Evolu/Db.d.ts +6 -6
  8. package/dist/src/Evolu/Db.d.ts.map +1 -1
  9. package/dist/src/Evolu/Db.js +4 -0
  10. package/dist/src/Evolu/LocalAuth.d.ts +2 -2
  11. package/dist/src/Evolu/LocalAuth.d.ts.map +1 -1
  12. package/dist/src/Evolu/Owner.d.ts +116 -86
  13. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  14. package/dist/src/Evolu/Owner.js +48 -45
  15. package/dist/src/Evolu/Relay.d.ts +6 -5
  16. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  17. package/dist/src/Evolu/Relay.js +40 -39
  18. package/dist/src/Evolu/Storage.d.ts +6 -11
  19. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  20. package/dist/src/Evolu/Storage.js +30 -9
  21. package/dist/src/Evolu/Sync.d.ts +5 -5
  22. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  23. package/dist/src/Evolu/Sync.js +3 -5
  24. package/dist/src/Evolu/Timestamp.d.ts +24 -0
  25. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  26. package/dist/src/Evolu/Timestamp.js +24 -0
  27. package/dist/src/Identicon.d.ts +35 -0
  28. package/dist/src/Identicon.d.ts.map +1 -0
  29. package/dist/src/Identicon.js +143 -0
  30. package/dist/src/ManyToManyMap.d.ts +0 -3
  31. package/dist/src/ManyToManyMap.d.ts.map +1 -1
  32. package/dist/src/Result.d.ts +13 -6
  33. package/dist/src/Result.d.ts.map +1 -1
  34. package/dist/src/Sqlite.d.ts +36 -1
  35. package/dist/src/Sqlite.d.ts.map +1 -1
  36. package/dist/src/Sqlite.js +56 -3
  37. package/dist/src/Task.d.ts.map +1 -1
  38. package/dist/src/Task.js +36 -0
  39. package/dist/src/Type.d.ts +73 -9
  40. package/dist/src/Type.d.ts.map +1 -1
  41. package/dist/src/Type.js +124 -22
  42. package/dist/src/Types.d.ts +1 -1
  43. package/dist/src/WebSocket.d.ts.map +1 -1
  44. package/dist/src/WebSocket.js +2 -7
  45. package/dist/src/index.d.ts +2 -0
  46. package/dist/src/index.d.ts.map +1 -1
  47. package/dist/src/index.js +2 -0
  48. package/package.json +1 -1
  49. package/src/Assert.ts +0 -19
  50. package/src/Cache.ts +85 -0
  51. package/src/Evolu/Db.ts +11 -7
  52. package/src/Evolu/LocalAuth.ts +19 -7
  53. package/src/Evolu/Owner.ts +140 -103
  54. package/src/Evolu/Relay.ts +52 -48
  55. package/src/Evolu/Storage.ts +47 -23
  56. package/src/Evolu/Sync.ts +11 -12
  57. package/src/Evolu/Timestamp.ts +24 -0
  58. package/src/Identicon.ts +197 -0
  59. package/src/ManyToManyMap.ts +0 -3
  60. package/src/Result.ts +13 -6
  61. package/src/Sqlite.ts +68 -5
  62. package/src/Task.ts +41 -0
  63. package/src/Type.ts +262 -27
  64. package/src/Types.ts +1 -1
  65. package/src/WebSocket.ts +6 -10
  66. package/src/index.ts +2 -0
package/src/Cache.ts ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * 🗄️ Generic cache interface and LRU cache implementation.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import { PositiveInt } from "./Type.js";
8
+
9
+ /**
10
+ * Generic cache interface providing basic key-value storage operations.
11
+ *
12
+ * Keys are compared by reference (standard Map semantics).
13
+ *
14
+ * Note: Cache does not extend Map because eviction policies (like in LRU)
15
+ * violate the Liskov Substitution Principle.
16
+ */
17
+ export interface Cache<K, V> {
18
+ /** Checks if a key exists in the cache. */
19
+ has: (key: K) => boolean;
20
+
21
+ /** Retrieves the value for a key, or undefined if not present. */
22
+ get: (key: K) => V | undefined;
23
+
24
+ /** Stores a key-value pair in the cache. */
25
+ set: (key: K, val: V) => void;
26
+
27
+ /** Removes a key from the cache. */
28
+ delete: (key: K) => void;
29
+
30
+ /** Returns a readonly view of the internal Map. */
31
+ readonly map: ReadonlyMap<K, V>;
32
+ }
33
+
34
+ /**
35
+ * Creates a Least Recently Used (LRU) cache with a maximum capacity.
36
+ *
37
+ * When the cache reaches capacity, the least recently used entry is evicted.
38
+ * Both `get` and `set` operations update the access order.
39
+ *
40
+ * ### Example
41
+ *
42
+ * ```ts
43
+ * const cache = createLruCache<string, number>(2);
44
+ * cache.set("a", 1);
45
+ * cache.set("b", 2);
46
+ * cache.set("c", 3); // Evicts "a"
47
+ * cache.has("a"); // false
48
+ * ```
49
+ */
50
+ export const createLruCache = <K, V>(capacity: PositiveInt): Cache<K, V> => {
51
+ const internalMap = new Map<K, V>();
52
+
53
+ return {
54
+ has: (key) => internalMap.has(key),
55
+
56
+ get: (key) => {
57
+ const value = internalMap.get(key);
58
+ if (value === undefined) return undefined;
59
+
60
+ // Move to end (most recently used)
61
+ internalMap.delete(key);
62
+ internalMap.set(key, value);
63
+ return value;
64
+ },
65
+
66
+ set: (key, val) => {
67
+ // If key exists, delete it first to update order
68
+ if (internalMap.has(key)) {
69
+ internalMap.delete(key);
70
+ } else if (internalMap.size === capacity) {
71
+ // Evict least recently used (first entry)
72
+ const firstKey = internalMap.keys().next().value as K;
73
+ internalMap.delete(firstKey);
74
+ }
75
+
76
+ internalMap.set(key, val);
77
+ },
78
+
79
+ delete: (key) => {
80
+ internalMap.delete(key);
81
+ },
82
+
83
+ map: internalMap,
84
+ };
85
+ };
package/src/Evolu/Db.ts CHANGED
@@ -31,12 +31,12 @@ import {
31
31
  AppOwner,
32
32
  createAppOwner,
33
33
  createOwnerSecret,
34
- createWebSocketTransportConfig,
34
+ createOwnerWebSocketTransport,
35
35
  mnemonicToOwnerSecret,
36
36
  OwnerEncryptionKey,
37
37
  OwnerId,
38
+ OwnerTransport,
38
39
  OwnerWriteKey,
39
- TransportConfig,
40
40
  } from "./Owner.js";
41
41
  import { ProtocolError, protocolVersion } from "./Protocol.js";
42
42
  import {
@@ -52,6 +52,7 @@ import {
52
52
  getDbSchema,
53
53
  MutationChange,
54
54
  } from "./Schema.js";
55
+ import { createBaseSqliteStorageTables } from "./Storage.js";
55
56
  import {
56
57
  applyLocalOnlyChange,
57
58
  Clock,
@@ -107,7 +108,7 @@ export interface DbConfig extends ConsoleConfig, TimestampConfig {
107
108
  * added and removed for any owner (including {@link AppOwner}) via
108
109
  * {@link Evolu#useOwner}.
109
110
  *
110
- * Use {@link createWebSocketTransportConfig} to create WebSocket transport
111
+ * Use {@link createOwnerWebSocketTransport} to create WebSocket transport
111
112
  * configurations with proper URL formatting and {@link OwnerId} inclusion. The
112
113
  * {@link OwnerId} in the URL enables relay authentication, allowing relay
113
114
  * servers to control access (e.g., for paid tiers or private instances).
@@ -134,16 +135,16 @@ export interface DbConfig extends ConsoleConfig, TimestampConfig {
134
135
  * // to work offline before the app connects
135
136
  * transports: [];
136
137
  *
137
- * // Using createWebSocketTransportConfig helper for relay authentication
138
+ * // Using createOwnerWebSocketTransport helper for relay authentication
138
139
  * transports: [
139
- * createWebSocketTransportConfig({
140
- * relayUrl: "ws://localhost:4000",
140
+ * createOwnerWebSocketTransport({
141
+ * url: "ws://localhost:4000",
141
142
  * ownerId,
142
143
  * }),
143
144
  * ];
144
145
  * ```
145
146
  */
146
- readonly transports: ReadonlyArray<TransportConfig>;
147
+ readonly transports: ReadonlyArray<OwnerTransport>;
147
148
 
148
149
  /**
149
150
  * External AppOwner to use when creating Evolu instance. Use this when you
@@ -573,6 +574,9 @@ const initializeDb =
573
574
  if (!result.ok) return result;
574
575
  }
575
576
 
577
+ const result = createBaseSqliteStorageTables(deps);
578
+ if (!result.ok) return result;
579
+
576
580
  return ok();
577
581
  };
578
582
 
@@ -10,9 +10,9 @@ import {
10
10
  OwnerWriteKey,
11
11
  } from "./Owner.js";
12
12
 
13
- /**
14
- * Local authentication and authorization system for Evolu.
15
- * This is API is subject to change and not recommended for production use.
13
+ /**
14
+ * Local authentication and authorization system for Evolu. This is API is
15
+ * subject to change and not recommended for production use.
16
16
  *
17
17
  * @experimental
18
18
  */
@@ -178,10 +178,22 @@ export const createLocalAuth = (
178
178
  return {
179
179
  ...newOptions,
180
180
  authenticationPrompt: {
181
- title: replaceMessageTokens(newOptions.authenticationPrompt?.title ?? "", username),
182
- cancel: replaceMessageTokens(newOptions.authenticationPrompt?.cancel ?? "", username),
183
- subtitle: replaceMessageTokens(newOptions.authenticationPrompt?.subtitle ?? "", username),
184
- description: replaceMessageTokens(newOptions.authenticationPrompt?.description ?? "", username),
181
+ title: replaceMessageTokens(
182
+ newOptions.authenticationPrompt?.title ?? "",
183
+ username,
184
+ ),
185
+ cancel: replaceMessageTokens(
186
+ newOptions.authenticationPrompt?.cancel ?? "",
187
+ username,
188
+ ),
189
+ subtitle: replaceMessageTokens(
190
+ newOptions.authenticationPrompt?.subtitle ?? "",
191
+ username,
192
+ ),
193
+ description: replaceMessageTokens(
194
+ newOptions.authenticationPrompt?.description ?? "",
195
+ username,
196
+ ),
185
197
  },
186
198
  };
187
199
  };
@@ -16,29 +16,12 @@ import {
16
16
  idToIdBytes,
17
17
  Mnemonic,
18
18
  NonNegativeInt,
19
+ PositiveInt,
19
20
  } from "../Type.js";
20
21
  import { getOrNull } from "../Result.js";
21
-
22
- /**
23
- * 32 bytes of cryptographic entropy used to derive {@link Owner} keys.
24
- *
25
- * Can be created using {@link createOwnerSecret} or converted from a
26
- * {@link Mnemonic} using {@link mnemonicToOwnerSecret}.
27
- */
28
- export const OwnerSecret = brand("OwnerSecret", Entropy32);
29
- export type OwnerSecret = typeof OwnerSecret.Type;
30
-
31
- /** Creates a {@link OwnerSecret}. */
32
- export const createOwnerSecret = (deps: RandomBytesDep): OwnerSecret =>
33
- deps.randomBytes.create(32) as OwnerSecret;
34
-
35
- /** Converts an {@link OwnerSecret} to a {@link Mnemonic}. */
36
- export const ownerSecretToMnemonic = (secret: OwnerSecret): Mnemonic =>
37
- bip39.entropyToMnemonic(secret, wordlist) as Mnemonic;
38
-
39
- /** Converts a {@link Mnemonic} to an {@link OwnerSecret}. */
40
- export const mnemonicToOwnerSecret = (mnemonic: Mnemonic): OwnerSecret =>
41
- bip39.mnemonicToEntropy(mnemonic, wordlist) as OwnerSecret;
22
+ import type { Timestamp } from "./Timestamp.js";
23
+ import { TimestampBytes } from "./Timestamp.js";
24
+ import type { Storage } from "./Storage.js";
42
25
 
43
26
  /**
44
27
  * The Owner represents ownership of data in Evolu. Every database change is
@@ -105,6 +88,27 @@ export type OwnerEncryptionKey = typeof OwnerEncryptionKey.Type;
105
88
  export const OwnerWriteKey = brand("OwnerWriteKey", Entropy16);
106
89
  export type OwnerWriteKey = typeof OwnerWriteKey.Type;
107
90
 
91
+ /**
92
+ * 32 bytes of cryptographic entropy used to derive {@link Owner} keys.
93
+ *
94
+ * Can be created using {@link createOwnerSecret} or converted from a
95
+ * {@link Mnemonic} using {@link mnemonicToOwnerSecret}.
96
+ */
97
+ export const OwnerSecret = brand("OwnerSecret", Entropy32);
98
+ export type OwnerSecret = typeof OwnerSecret.Type;
99
+
100
+ /** Creates a {@link OwnerSecret}. */
101
+ export const createOwnerSecret = (deps: RandomBytesDep): OwnerSecret =>
102
+ deps.randomBytes.create(32) as OwnerSecret;
103
+
104
+ /** Converts an {@link OwnerSecret} to a {@link Mnemonic}. */
105
+ export const ownerSecretToMnemonic = (secret: OwnerSecret): Mnemonic =>
106
+ bip39.entropyToMnemonic(secret, wordlist) as Mnemonic;
107
+
108
+ /** Converts a {@link Mnemonic} to an {@link OwnerSecret}. */
109
+ export const mnemonicToOwnerSecret = (mnemonic: Mnemonic): OwnerSecret =>
110
+ bip39.mnemonicToEntropy(mnemonic, wordlist) as OwnerSecret;
111
+
108
112
  /** Creates a randomly generated {@link OwnerWriteKey}. */
109
113
  export const createOwnerWriteKey = (deps: RandomBytesDep): OwnerWriteKey =>
110
114
  deps.randomBytes.create(16) as OwnerWriteKey;
@@ -174,82 +178,6 @@ export const createAppOwner = (secret: OwnerSecret): AppOwner => ({
174
178
  ...createOwner(secret),
175
179
  });
176
180
 
177
- /**
178
- * Transport configuration for connecting to relays.
179
- *
180
- * Each {@link Owner} can specify one or more transports to connect to different
181
- * relays for data synchronization. Currently supports WebSocket transport, with
182
- * future support planned for Bluetooth, LocalNetwork, and other protocols.
183
- */
184
- export type TransportConfig = WebSocketTransportConfig;
185
-
186
- /**
187
- * WebSocket transport configuration for relay connections.
188
- *
189
- * Use {@link createWebSocketTransportConfig} to create a properly formatted URL
190
- * with {@link OwnerId}. The relay uses {@link parseOwnerIdFromUrl} to extract the
191
- * OwnerId from the query string.
192
- *
193
- * ### Authentication and Error Handling
194
- *
195
- * When a relay rejects a connection (invalid OwnerId, unauthorized owner, or
196
- * server error), the browser WebSocket API does not expose the specific HTTP
197
- * status code or reason - it only reports a generic connection failure. The
198
- * client automatically retries with exponential backoff and jitter, eventually
199
- * succeeding once the configuration or server issue is resolved.
200
- *
201
- * Legitimate clients will be properly configured with valid credentials, so
202
- * automatic retry is appropriate.
203
- *
204
- * @see {@link createWebSocketTransportConfig}
205
- * @see {@link parseOwnerIdFromUrl}
206
- */
207
- export interface WebSocketTransportConfig {
208
- readonly type: "WebSocket";
209
- readonly url: string;
210
- }
211
-
212
- /**
213
- * Creates a {@link WebSocketTransportConfig} for the given relay URL and
214
- * {@link OwnerId}.
215
- *
216
- * ### Example
217
- *
218
- * ```ts
219
- * const transport = createWebSocketTransportConfig({
220
- * relayUrl: "wss://relay.evolu.dev",
221
- * ownerId: owner.id,
222
- * });
223
- * // Result: { type: "WebSocket", url: "wss://relay.evolu.dev?ownerId=..." }
224
- * ```
225
- */
226
- export const createWebSocketTransportConfig = ({
227
- relayUrl,
228
- ownerId,
229
- }: {
230
- readonly relayUrl: string;
231
- readonly ownerId: OwnerId;
232
- }): WebSocketTransportConfig => ({
233
- type: "WebSocket",
234
- url: `${relayUrl}?ownerId=${ownerId}`,
235
- });
236
-
237
- /**
238
- * Extracts {@link OwnerId} from a URL query string.
239
- *
240
- * Parses the query string `?ownerId=...` and validates that the extracted value
241
- * is a valid {@link OwnerId}.
242
- *
243
- * ### Example
244
- *
245
- * ```ts
246
- * parseOwnerIdFromUrl("/sync?ownerId=_12345678abcdefgh");
247
- * // Returns: OwnerId or null
248
- * ```
249
- */
250
- export const parseOwnerIdFromUrl = (url: string | undefined): OwnerId | null =>
251
- getOrNull(OwnerId.fromUnknown(url?.split("=")[1]));
252
-
253
181
  /**
254
182
  * An {@link Owner} for sharding data.
255
183
  *
@@ -263,13 +191,13 @@ export const parseOwnerIdFromUrl = (url: string | undefined): OwnerId | null =>
263
191
  */
264
192
  export interface ShardOwner extends Owner {
265
193
  readonly type: "ShardOwner";
266
- readonly transports?: ReadonlyArray<TransportConfig>;
194
+ readonly transports?: ReadonlyArray<OwnerTransport>;
267
195
  }
268
196
 
269
197
  /** Creates a {@link ShardOwner} from an {@link OwnerSecret}. */
270
198
  export const createShardOwner = (
271
199
  secret: OwnerSecret,
272
- transports?: ReadonlyArray<TransportConfig>,
200
+ transports?: ReadonlyArray<OwnerTransport>,
273
201
  ): ShardOwner => {
274
202
  return {
275
203
  type: "ShardOwner",
@@ -299,7 +227,7 @@ export const createShardOwner = (
299
227
  export const deriveShardOwner = (
300
228
  owner: AppOwner,
301
229
  path: NonEmptyReadonlyArray<string | number>,
302
- transports?: ReadonlyArray<TransportConfig>,
230
+ transports?: ReadonlyArray<OwnerTransport>,
303
231
  ): ShardOwner => {
304
232
  const secret = createSlip21(owner.encryptionKey, path) as OwnerSecret;
305
233
 
@@ -313,7 +241,7 @@ export const deriveShardOwner = (
313
241
  /** An {@link Owner} for collaborative data with write access. */
314
242
  export interface SharedOwner extends Owner {
315
243
  readonly type: "SharedOwner";
316
- readonly transports?: ReadonlyArray<TransportConfig>;
244
+ readonly transports?: ReadonlyArray<OwnerTransport>;
317
245
  }
318
246
 
319
247
  /**
@@ -325,7 +253,7 @@ export interface SharedOwner extends Owner {
325
253
  */
326
254
  export const createSharedOwner = (
327
255
  secret: OwnerSecret,
328
- transports?: ReadonlyArray<TransportConfig>,
256
+ transports?: ReadonlyArray<OwnerTransport>,
329
257
  ): SharedOwner => {
330
258
  return {
331
259
  type: "SharedOwner",
@@ -343,7 +271,7 @@ export interface SharedReadonlyOwner {
343
271
  readonly type: "SharedReadonlyOwner";
344
272
  readonly id: OwnerId;
345
273
  readonly encryptionKey: EncryptionKey;
346
- readonly transports?: ReadonlyArray<TransportConfig>;
274
+ readonly transports?: ReadonlyArray<OwnerTransport>;
347
275
  }
348
276
 
349
277
  /** Creates a {@link SharedReadonlyOwner} from a {@link SharedOwner}. */
@@ -355,3 +283,112 @@ export const createSharedReadonlyOwner = (
355
283
  encryptionKey: sharedOwner.encryptionKey,
356
284
  ...(sharedOwner.transports && { transports: sharedOwner.transports }),
357
285
  });
286
+
287
+ /**
288
+ * Transport configuration for connecting to relays.
289
+ *
290
+ * Currently only WebSocket, in the future Bluetooth, LocalNetwork, etc.
291
+ */
292
+ export type OwnerTransport = OwnerWebSocketTransport;
293
+
294
+ /**
295
+ * WebSocket transport configuration.
296
+ *
297
+ * ### Authentication and Error Handling
298
+ *
299
+ * When a relay rejects a connection (invalid OwnerId, unauthorized owner, or
300
+ * server error), the browser WebSocket API does not expose the specific HTTP
301
+ * status code or reason - it only reports a generic connection failure. The
302
+ * client automatically retries with exponential backoff and jitter, eventually
303
+ * succeeding once the configuration or server issue is resolved.
304
+ *
305
+ * Legitimate clients will be properly configured with valid credentials, so
306
+ * automatic retry is OK.
307
+ *
308
+ * @see {@link createOwnerWebSocketTransport}
309
+ * @see {@link parseOwnerIdFromOwnerWebSocketTransportUrl}
310
+ */
311
+ export interface OwnerWebSocketTransport {
312
+ readonly type: "WebSocket";
313
+ readonly url: string;
314
+ }
315
+
316
+ /**
317
+ * Creates an {@link OwnerWebSocketTransport} for the given relay URL and
318
+ * {@link OwnerId}.
319
+ *
320
+ * ### Example
321
+ *
322
+ * ```ts
323
+ * const transport = createOwnerWebSocketTransport({
324
+ * url: "wss://relay.evolu.dev",
325
+ * ownerId: owner.id,
326
+ * });
327
+ * // Result: { type: "WebSocket", url: "wss://relay.evolu.dev?ownerId=..." }
328
+ * ```
329
+ */
330
+ export const createOwnerWebSocketTransport = (config: {
331
+ readonly url: string;
332
+ readonly ownerId: OwnerId;
333
+ }): OwnerWebSocketTransport => ({
334
+ type: "WebSocket",
335
+ url: `${config.url}?ownerId=${config.ownerId}`,
336
+ });
337
+
338
+ /**
339
+ * Extracts {@link OwnerId} from an {@link OwnerWebSocketTransport} URL query
340
+ * string.
341
+ *
342
+ * Parses the query string `?ownerId=...` and validates that the extracted value
343
+ * is a valid {@link OwnerId}.
344
+ *
345
+ * ### Example
346
+ *
347
+ * ```ts
348
+ * parseOwnerIdFromOwnerWebSocketTransportUrl(
349
+ * "/sync?ownerId=_12345678abcdefgh",
350
+ * );
351
+ * // Returns: OwnerId or null
352
+ * ```
353
+ */
354
+ export const parseOwnerIdFromOwnerWebSocketTransportUrl = (
355
+ url: string,
356
+ ): OwnerId | null => getOrNull(OwnerId.fromUnknown(url.split("=")[1]));
357
+
358
+ /**
359
+ * Usage data for an {@link OwnerId}.
360
+ *
361
+ * Tracks data consumption to monitor usage patterns and enforce quotas if
362
+ * needed. Used by both relays and clients.
363
+ *
364
+ * Relays and clients must handle rate limiting, connection limits, and request
365
+ * throttling separately with in-memory state.
366
+ */
367
+ export interface OwnerUsage {
368
+ /** The {@link Owner} this usage data belongs to. */
369
+ readonly ownerId: OwnerIdBytes;
370
+
371
+ /** Total bytes stored in the database. */
372
+ readonly storedBytes: PositiveInt;
373
+
374
+ /** Total bytes received. */
375
+ readonly receivedBytes: number;
376
+
377
+ /** Total bytes sent. */
378
+ readonly sentBytes: number;
379
+
380
+ /**
381
+ * The minimum {@link Timestamp}.
382
+ *
383
+ * Helps {@link Storage} choose faster algorithms.
384
+ */
385
+ readonly firstTimestamp: TimestampBytes | null;
386
+
387
+ /**
388
+ * The maximum {@link Timestamp}.
389
+ *
390
+ * Helps {@link Storage} choose faster algorithms. Free relays can use it to
391
+ * identify inactive accounts for cleanup or archival.
392
+ */
393
+ readonly lastTimestamp: TimestampBytes | null;
394
+ }
@@ -3,13 +3,13 @@ import { ConsoleConfig, ConsoleDep } from "../Console.js";
3
3
  import { TimingSafeEqualDep } from "../Crypto.js";
4
4
  import { LazyValue } from "../Function.js";
5
5
  import { err, ok, Result } from "../Result.js";
6
- import { sql, SqliteError } from "../Sqlite.js";
6
+ import { sql, SqliteDep, SqliteError } from "../Sqlite.js";
7
7
  import { SimpleName } from "../Type.js";
8
- import { OwnerId, OwnerWriteKey, TransportConfig } from "./Owner.js";
8
+ import { OwnerId, OwnerTransport, OwnerWriteKey } from "./Owner.js";
9
9
  import { ProtocolInvalidDataError } from "./Protocol.js";
10
10
  import {
11
- createSqliteStorageBase,
12
- CreateSqliteStorageBaseOptions,
11
+ createBaseSqliteStorage,
12
+ CreateBaseSqliteStorageOptions,
13
13
  EncryptedDbChange,
14
14
  SqliteStorageDeps,
15
15
  Storage,
@@ -44,9 +44,9 @@ export interface RelayConfig extends ConsoleConfig {
44
44
  * this only controls relay access, not write permissions. Since all data is
45
45
  * encrypted on the relay, OwnerId exposure is safe.
46
46
  *
47
- * Owners specify which relays to connect to via {@link TransportConfig}. In
47
+ * Owners specify which relays to connect to via {@link OwnerTransport}. In
48
48
  * WebSocket-based implementations, this check occurs before accepting the
49
- * connection, with the OwnerId typically extracted from the URL path (e.g.,
49
+ * connection, with the OwnerId typically extracted from the URL Path (e.g.,
50
50
  * `ws://localhost:4000/<ownerId>`).
51
51
  *
52
52
  * ### Example
@@ -68,36 +68,11 @@ export interface RelayConfig extends ConsoleConfig {
68
68
 
69
69
  export const createRelaySqliteStorage =
70
70
  (deps: SqliteStorageDeps & TimingSafeEqualDep) =>
71
- (options: CreateSqliteStorageBaseOptions): Result<Storage, SqliteError> => {
72
- const sqliteStorageBase = createSqliteStorageBase(deps)(options);
73
- if (!sqliteStorageBase.ok) return sqliteStorageBase;
74
-
75
- for (const query of [
76
- sql`
77
- create table if not exists evolu_writeKey (
78
- "ownerId" blob not null,
79
- "writeKey" blob not null,
80
- primary key ("ownerId")
81
- )
82
- strict;
83
- `,
84
-
85
- sql`
86
- create table if not exists evolu_message (
87
- "ownerId" blob not null,
88
- "timestamp" blob not null,
89
- "change" blob not null,
90
- primary key ("ownerId", "timestamp")
91
- )
92
- strict;
93
- `,
94
- ]) {
95
- const result = deps.sqlite.exec(query);
96
- if (!result.ok) return result;
97
- }
98
-
99
- return ok({
100
- ...sqliteStorageBase.value,
71
+ (options: CreateBaseSqliteStorageOptions): Storage => {
72
+ const sqliteStorageBase = createBaseSqliteStorage(deps)(options);
73
+
74
+ return {
75
+ ...sqliteStorageBase,
101
76
 
102
77
  /**
103
78
  * Lazily authorizes the initiator's {@link OwnerWriteKey} for the given
@@ -159,11 +134,10 @@ export const createRelaySqliteStorage =
159
134
  writeMessages: async (ownerId, messages) => {
160
135
  const result = deps.sqlite.transaction(() => {
161
136
  for (const message of messages) {
162
- const insertTimestampResult =
163
- sqliteStorageBase.value.insertTimestamp(
164
- ownerId,
165
- timestampToTimestampBytes(message.timestamp),
166
- );
137
+ const insertTimestampResult = sqliteStorageBase.insertTimestamp(
138
+ ownerId,
139
+ timestampToTimestampBytes(message.timestamp),
140
+ );
167
141
  if (!insertTimestampResult.ok) return insertTimestampResult;
168
142
 
169
143
  const insertMessage = deps.sqlite.exec(sql`
@@ -207,18 +181,18 @@ export const createRelaySqliteStorage =
207
181
 
208
182
  deleteOwner: (ownerId) => {
209
183
  const result = deps.sqlite.transaction(() => {
210
- const del1 = deps.sqlite.exec(sql`
184
+ const deleteWriteKey = deps.sqlite.exec(sql`
211
185
  delete from evolu_writeKey where ownerId = ${ownerId};
212
186
  `);
213
- if (!del1.ok) return del1;
187
+ if (!deleteWriteKey.ok) return deleteWriteKey;
214
188
 
215
- const del2 = deps.sqlite.exec(sql`
189
+ const deleteMessages = deps.sqlite.exec(sql`
216
190
  delete from evolu_message where ownerId = ${ownerId};
217
191
  `);
218
- if (!del2.ok) return del2;
192
+ if (!deleteMessages.ok) return deleteMessages;
219
193
 
220
- const del3 = sqliteStorageBase.value.deleteOwner(ownerId);
221
- if (!del3) return err(null);
194
+ const deleteBaseOwner = sqliteStorageBase.deleteOwner(ownerId);
195
+ if (!deleteBaseOwner) return err(null);
222
196
 
223
197
  return ok();
224
198
  });
@@ -228,9 +202,39 @@ export const createRelaySqliteStorage =
228
202
  }
229
203
  return true;
230
204
  },
231
- });
205
+ };
232
206
  };
233
207
 
208
+ export const createRelayStorageTables = (
209
+ deps: SqliteDep,
210
+ ): Result<void, SqliteError> => {
211
+ for (const query of [
212
+ sql`
213
+ create table evolu_writeKey (
214
+ "ownerId" blob not null,
215
+ "writeKey" blob not null,
216
+ primary key ("ownerId")
217
+ )
218
+ strict;
219
+ `,
220
+
221
+ sql`
222
+ create table evolu_message (
223
+ "ownerId" blob not null,
224
+ "timestamp" blob not null,
225
+ "change" blob not null,
226
+ primary key ("ownerId", "timestamp")
227
+ )
228
+ strict;
229
+ `,
230
+ ]) {
231
+ const result = deps.sqlite.exec(query);
232
+ if (!result.ok) return result;
233
+ }
234
+
235
+ return ok();
236
+ };
237
+
234
238
  export interface RelayLogger {
235
239
  readonly started: (enableLogging: boolean, port: number) => void;
236
240
  readonly storageError: (error: unknown) => void;