@evolu/common 8.10.0 → 8.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/dist/src/Bytes.d.ts +39 -2
  2. package/dist/src/Bytes.d.ts.map +1 -1
  3. package/dist/src/Bytes.js +50 -2
  4. package/dist/src/Config.d.ts +22 -22
  5. package/dist/src/Config.d.ts.map +1 -1
  6. package/dist/src/Console.d.ts +62 -7
  7. package/dist/src/Console.d.ts.map +1 -1
  8. package/dist/src/Console.js +20 -4
  9. package/dist/src/Crypto.d.ts +76 -4
  10. package/dist/src/Crypto.d.ts.map +1 -1
  11. package/dist/src/Crypto.js +55 -4
  12. package/dist/src/Error.d.ts +45 -0
  13. package/dist/src/Error.d.ts.map +1 -1
  14. package/dist/src/Error.js +69 -0
  15. package/dist/src/Fs.d.ts +92 -18
  16. package/dist/src/Fs.d.ts.map +1 -1
  17. package/dist/src/Fs.js +2 -0
  18. package/dist/src/Identicon.d.ts +2 -2
  19. package/dist/src/Identicon.js +2 -2
  20. package/dist/src/LeakDetector.d.ts +22 -3
  21. package/dist/src/LeakDetector.d.ts.map +1 -1
  22. package/dist/src/LeakDetector.js +12 -2
  23. package/dist/src/LockManager.d.ts +8 -0
  24. package/dist/src/LockManager.d.ts.map +1 -1
  25. package/dist/src/LockManager.js +6 -0
  26. package/dist/src/Object.d.ts.map +1 -1
  27. package/dist/src/Object.js +5 -0
  28. package/dist/src/Platform.d.ts +47 -7
  29. package/dist/src/Platform.d.ts.map +1 -1
  30. package/dist/src/Platform.js +24 -5
  31. package/dist/src/Random.d.ts +25 -2
  32. package/dist/src/Random.d.ts.map +1 -1
  33. package/dist/src/Random.js +14 -2
  34. package/dist/src/Resource.d.ts +156 -1
  35. package/dist/src/Resource.d.ts.map +1 -1
  36. package/dist/src/Resource.js +201 -72
  37. package/dist/src/Schedule.d.ts +11 -10
  38. package/dist/src/Schedule.d.ts.map +1 -1
  39. package/dist/src/Schedule.js +1 -1
  40. package/dist/src/Sqlite.d.ts +132 -16
  41. package/dist/src/Sqlite.d.ts.map +1 -1
  42. package/dist/src/Sqlite.js +63 -9
  43. package/dist/src/Task.d.ts +15 -4
  44. package/dist/src/Task.d.ts.map +1 -1
  45. package/dist/src/Task.js +41 -15
  46. package/dist/src/Test.d.ts +9 -0
  47. package/dist/src/Test.d.ts.map +1 -1
  48. package/dist/src/Test.js +4 -0
  49. package/dist/src/Time.d.ts +106 -9
  50. package/dist/src/Time.d.ts.map +1 -1
  51. package/dist/src/Time.js +55 -4
  52. package/dist/src/Type.d.ts +1455 -1310
  53. package/dist/src/Type.d.ts.map +1 -1
  54. package/dist/src/Type.js +1274 -517
  55. package/dist/src/WebSocket.d.ts +164 -13
  56. package/dist/src/WebSocket.d.ts.map +1 -1
  57. package/dist/src/WebSocket.js +133 -24
  58. package/dist/src/Worker.d.ts +90 -8
  59. package/dist/src/Worker.d.ts.map +1 -1
  60. package/dist/src/Worker.js +28 -2
  61. package/dist/src/index.d.ts +6 -7
  62. package/dist/src/index.d.ts.map +1 -1
  63. package/dist/src/index.js +2 -3
  64. package/dist/src/local-first/Db.d.ts +52 -3
  65. package/dist/src/local-first/Db.d.ts.map +1 -1
  66. package/dist/src/local-first/Db.js +412 -137
  67. package/dist/src/local-first/Evolu.d.ts +412 -213
  68. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  69. package/dist/src/local-first/Evolu.js +181 -18
  70. package/dist/src/local-first/Owner.d.ts +13 -30
  71. package/dist/src/local-first/Owner.d.ts.map +1 -1
  72. package/dist/src/local-first/Owner.js +13 -30
  73. package/dist/src/local-first/Protocol.d.ts +106 -19
  74. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  75. package/dist/src/local-first/Protocol.js +162 -60
  76. package/dist/src/local-first/Query.d.ts +8 -15
  77. package/dist/src/local-first/Query.d.ts.map +1 -1
  78. package/dist/src/local-first/Relay.d.ts.map +1 -1
  79. package/dist/src/local-first/Relay.js +4 -2
  80. package/dist/src/local-first/Schema.d.ts +346 -23
  81. package/dist/src/local-first/Schema.d.ts.map +1 -1
  82. package/dist/src/local-first/Schema.js +214 -17
  83. package/dist/src/local-first/Shared.d.ts +537 -22
  84. package/dist/src/local-first/Shared.d.ts.map +1 -1
  85. package/dist/src/local-first/Shared.js +1437 -234
  86. package/dist/src/local-first/Storage.d.ts +195 -17
  87. package/dist/src/local-first/Storage.d.ts.map +1 -1
  88. package/dist/src/local-first/Storage.js +85 -22
  89. package/dist/src/local-first/Timestamp.d.ts +392 -41
  90. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  91. package/dist/src/local-first/Timestamp.js +403 -81
  92. package/dist/src/local-first/index.d.ts +0 -1
  93. package/dist/src/local-first/index.d.ts.map +1 -1
  94. package/dist/src/local-first/index.js +0 -1
  95. package/package.json +1 -1
  96. package/src/Assert.test.ts +2 -5
  97. package/src/Bytes.test.ts +27 -0
  98. package/src/Bytes.ts +58 -2
  99. package/src/Config.test.ts +2 -6
  100. package/src/Config.ts +133 -133
  101. package/src/Console.ts +62 -7
  102. package/src/Crypto.ts +76 -4
  103. package/src/Eq.test.ts +2 -3
  104. package/src/Error.test.ts +76 -3
  105. package/src/Error.ts +71 -0
  106. package/src/Fs.ts +92 -18
  107. package/src/Identicon.ts +2 -2
  108. package/src/LeakDetector.ts +22 -3
  109. package/src/LockManager.ts +8 -0
  110. package/src/Object.test.ts +27 -12
  111. package/src/Object.ts +5 -0
  112. package/src/Platform.ts +50 -8
  113. package/src/Random.ts +25 -2
  114. package/src/Resource.test.ts +837 -0
  115. package/src/Resource.ts +235 -15
  116. package/src/Schedule.test.ts +50 -12
  117. package/src/Schedule.ts +24 -14
  118. package/src/Sqlite.ts +137 -17
  119. package/src/Task.test.ts +189 -8
  120. package/src/Task.ts +56 -17
  121. package/src/Test.ts +9 -0
  122. package/src/Time.ts +106 -9
  123. package/src/Type.test.ts +946 -1028
  124. package/src/Type.ts +4195 -3136
  125. package/src/Types.test.ts +4 -14
  126. package/src/WebSocket.ts +313 -40
  127. package/src/Worker.ts +90 -8
  128. package/src/index.ts +20 -6
  129. package/src/local-first/Db.ts +644 -339
  130. package/src/local-first/Evolu.test.ts +994 -22
  131. package/src/local-first/Evolu.ts +625 -232
  132. package/src/local-first/Owner.ts +13 -30
  133. package/src/local-first/Protocol.test.ts +634 -10
  134. package/src/local-first/Protocol.ts +255 -109
  135. package/src/local-first/Query.ts +8 -15
  136. package/src/local-first/Relay.ts +4 -2
  137. package/src/local-first/Schema.test.ts +143 -0
  138. package/src/local-first/Schema.ts +376 -26
  139. package/src/local-first/Shared.test.ts +7731 -559
  140. package/src/local-first/Shared.ts +2036 -267
  141. package/src/local-first/Storage.ts +224 -36
  142. package/src/local-first/Timestamp.test.ts +344 -70
  143. package/src/local-first/Timestamp.ts +434 -118
  144. package/src/local-first/index.ts +0 -1
  145. package/dist/src/local-first/Error.d.ts +0 -12
  146. package/dist/src/local-first/Error.d.ts.map +0 -1
  147. package/dist/src/local-first/Error.js +0 -6
  148. package/dist/src/local-first/LocalAuth.d.ts +0 -150
  149. package/dist/src/local-first/LocalAuth.d.ts.map +0 -1
  150. package/dist/src/local-first/LocalAuth.js +0 -179
  151. package/src/local-first/Error.ts +0 -17
  152. package/src/local-first/LocalAuth.ts +0 -457
@@ -9,7 +9,8 @@ import type { NonEmptyReadonlyArray } from "../Array.ts";
9
9
  import { firstInArray, isNonEmptyArray } from "../Array.ts";
10
10
  import { assert, assertNonNullable } from "../Assert.ts";
11
11
  import type { Brand } from "../Brand.ts";
12
- import { concatBytes } from "../Bytes.ts";
12
+ import { concatByteArrays } from "../Bytes.ts";
13
+ import type { DecryptWithXChaCha20Poly1305Error } from "../Crypto.ts";
13
14
  import { decrement } from "../Number.ts";
14
15
  import type { RandomDep } from "../Random.ts";
15
16
  import { err, ok } from "../Result.ts";
@@ -38,6 +39,10 @@ import {
38
39
  import type { Awaitable } from "../Types.ts";
39
40
  import type { Owner, OwnerError, OwnerIdBytes } from "./Owner.ts";
40
41
  import { OwnerId, OwnerWriteKey } from "./Owner.ts";
42
+ import type {
43
+ ProtocolInvalidDataError,
44
+ ProtocolTimestampMismatchError,
45
+ } from "./Protocol.ts";
41
46
  import { systemColumnsWithId } from "./Schema.ts";
42
47
  import {
43
48
  createTimestamp,
@@ -46,6 +51,11 @@ import {
46
51
  TimestampBytes,
47
52
  } from "./Timestamp.ts";
48
53
 
54
+ /**
55
+ * Configuration for {@link Storage}, such as quota checks.
56
+ *
57
+ * @group Core
58
+ */
49
59
  export interface StorageConfig {
50
60
  /**
51
61
  * Callback called before an attempt to write, to check if an {@link OwnerId}
@@ -99,17 +109,23 @@ export interface StorageConfig {
99
109
  */
100
110
  readonly isOwnerWithinQuota: (
101
111
  ownerId: OwnerId,
102
- requiredBytes: PositiveInt,
112
+ requiredBytes: NonNegativeInt,
103
113
  ) => Awaitable<boolean>;
104
114
  }
105
115
 
106
116
  /**
107
- * Evolu Storage.
117
+ * Replica storage used by Evolu's synchronization protocol.
118
+ *
119
+ * Protocol owns message framing, set reconciliation, and sync continuation.
120
+ * Storage owns batch acceptance, persistence, and the timestamp and fingerprint
121
+ * queries used by reconciliation. Protocol functions accept any implementation
122
+ * that satisfies this contract.
108
123
  *
109
- * Evolu Protocol is agnostic to storage implementation—any storage can be
110
- * plugged in, as long as it implements this interface. Implementations must
111
- * handle their own errors; return values only indicate overall success or
112
- * failure.
124
+ * {@link Storage.writeMessages} returns the {@link StorageWriteMessagesError}
125
+ * that made it store none of a batch. Implementations return expected write
126
+ * rejections without reporting them; the caller owns reporting. The client
127
+ * protocol forwards these errors unchanged, while the relay protocol maps them
128
+ * to wire error codes.
113
129
  *
114
130
  * The Storage API is synchronous because SQLite's synchronous API is the
115
131
  * fastest way to use SQLite. Synchronous bindings (like better-sqlite3) call
@@ -119,6 +135,8 @@ export interface StorageConfig {
119
135
  * The only exception is {@link Storage.writeMessages}, which is async to allow
120
136
  * for async validation logic before writing to storage. The write operation
121
137
  * itself remains synchronous.
138
+ *
139
+ * @group Core
122
140
  */
123
141
  export interface Storage {
124
142
  readonly getSize: (ownerId: OwnerIdBytes) => NonNegativeInt;
@@ -175,13 +193,16 @@ export interface Storage {
175
193
  /**
176
194
  * Write encrypted {@link CrdtMessage}s to storage.
177
195
  *
196
+ * Stores none of the messages and returns the cause when the batch cannot be
197
+ * accepted.
198
+ *
178
199
  * Must use a mutex per ownerId to ensure sequential processing and proper
179
200
  * protocol logic handling during sync operations.
180
201
  */
181
202
  readonly writeMessages: (
182
203
  ownerIdBytes: OwnerIdBytes,
183
204
  messages: NonEmptyReadonlyArray<EncryptedCrdtMessage>,
184
- ) => Task<void, StorageQuotaError>;
205
+ ) => Task<void, StorageWriteMessagesError>;
185
206
 
186
207
  /** Read encrypted {@link DbChange}s from storage. */
187
208
  readonly readDbChange: (
@@ -193,30 +214,72 @@ export interface Storage {
193
214
  readonly deleteOwner: (ownerId: OwnerIdBytes) => void;
194
215
  }
195
216
 
217
+ /**
218
+ * Dependency wrapper for {@link Storage}.
219
+ *
220
+ * @group Core
221
+ */
196
222
  export interface StorageDep {
197
223
  readonly storage: Storage;
198
224
  }
199
225
 
200
- /** Error when storage or billing quota is exceeded. */
226
+ /**
227
+ * Error when storage or billing quota is exceeded.
228
+ *
229
+ * @group Core
230
+ */
201
231
  export interface StorageQuotaError
202
232
  extends OwnerError, Typed<"StorageQuotaError"> {}
203
233
 
234
+ /**
235
+ * Expected reasons why {@link Storage.writeMessages} stored none of a batch.
236
+ *
237
+ * Each implementation returns the members that apply to it. The built-in relay
238
+ * storage currently stores opaque encrypted messages and rejects batches over
239
+ * quota. The built-in client storage decrypts and validates incoming messages
240
+ * before updating its clock and database tables. The contract permits quota
241
+ * checks on either side.
242
+ *
243
+ * @group Core
244
+ */
245
+ export type StorageWriteMessagesError =
246
+ | DecryptWithXChaCha20Poly1305Error
247
+ | ProtocolInvalidDataError
248
+ | ProtocolTimestampMismatchError
249
+ | StorageQuotaError;
250
+
204
251
  /**
205
252
  * A cryptographic hash used for efficiently comparing collections of
206
253
  * {@link TimestampBytes}es.
207
254
  *
208
255
  * It consists of the first {@link fingerprintSize} bytes of the SHA-256 hash of
209
256
  * one or more timestamps.
257
+ *
258
+ * @group Ranges
210
259
  */
211
260
  export type Fingerprint = Uint8Array & Brand<"Fingerprint">;
212
261
 
262
+ /**
263
+ * Number of leading SHA-256 bytes that form a {@link Fingerprint}.
264
+ *
265
+ * @group Ranges
266
+ */
213
267
  export const fingerprintSize = /*#__PURE__*/ NonNegativeInt.orThrow(12);
214
268
 
215
- /** A fingerprint of an empty range. */
269
+ /**
270
+ * A fingerprint of an empty range.
271
+ *
272
+ * @group Ranges
273
+ */
216
274
  export const zeroFingerprint = /*#__PURE__*/ new Uint8Array(
217
275
  fingerprintSize,
218
276
  ) as Fingerprint;
219
277
 
278
+ /**
279
+ * Common shape of every {@link Range}.
280
+ *
281
+ * @group Ranges
282
+ */
220
283
  export interface BaseRange {
221
284
  readonly upperBound: RangeUpperBound;
222
285
  }
@@ -224,45 +287,96 @@ export interface BaseRange {
224
287
  /**
225
288
  * Union type for Range's upperBound: either a {@link TimestampBytes} or
226
289
  * {@link InfiniteUpperBound}.
290
+ *
291
+ * @group Ranges
227
292
  */
228
293
  export type RangeUpperBound = TimestampBytes | InfiniteUpperBound;
229
294
 
295
+ /**
296
+ * Sentinel {@link RangeUpperBound} for a range without an upper limit.
297
+ *
298
+ * @group Ranges
299
+ */
230
300
  export const InfiniteUpperBound = /*#__PURE__*/ Symbol(
231
301
  "evolu.local-first.Storage.InfiniteUpperBound",
232
302
  );
303
+ /**
304
+ * Type of the {@link InfiniteUpperBound} sentinel.
305
+ *
306
+ * @group Ranges
307
+ */
233
308
  export type InfiniteUpperBound = typeof InfiniteUpperBound;
234
309
 
310
+ /**
311
+ * Numeric tags discriminating {@link Range} variants.
312
+ *
313
+ * @group Ranges
314
+ */
235
315
  export const RangeType = {
236
316
  Fingerprint: 1,
237
317
  Skip: 0,
238
318
  Timestamps: 2,
239
319
  } as const;
240
320
 
321
+ /**
322
+ * Numeric tag of one {@link Range} variant.
323
+ *
324
+ * @group Ranges
325
+ */
241
326
  export type RangeType = (typeof RangeType)[keyof typeof RangeType];
242
327
 
328
+ /**
329
+ * Range with nothing to reconcile.
330
+ *
331
+ * @group Ranges
332
+ */
243
333
  export interface SkipRange extends BaseRange {
244
334
  readonly type: typeof RangeType.Skip;
245
335
  }
246
336
 
337
+ /**
338
+ * Range summarized by a {@link Fingerprint} for comparison.
339
+ *
340
+ * @group Ranges
341
+ */
247
342
  export interface FingerprintRange extends BaseRange {
248
343
  readonly type: typeof RangeType.Fingerprint;
249
344
  readonly fingerprint: Fingerprint;
250
345
  }
251
346
 
347
+ /**
348
+ * Range listing its {@link TimestampBytes} explicitly.
349
+ *
350
+ * @group Ranges
351
+ */
252
352
  export interface TimestampsRange extends BaseRange {
253
353
  readonly type: typeof RangeType.Timestamps;
254
354
  readonly timestamps: ReadonlyArray<TimestampBytes>;
255
355
  }
256
356
 
357
+ /**
358
+ * Range exchanged during sync: {@link SkipRange}, {@link FingerprintRange}, or
359
+ * {@link TimestampsRange}.
360
+ *
361
+ * @group Ranges
362
+ */
257
363
  export type Range = SkipRange | FingerprintRange | TimestampsRange;
258
364
 
259
- /** An encrypted {@link CrdtMessage}. */
365
+ /**
366
+ * An encrypted {@link CrdtMessage}.
367
+ *
368
+ * @group Messages
369
+ */
260
370
  export interface EncryptedCrdtMessage {
261
371
  readonly timestamp: Timestamp;
262
372
  readonly change: EncryptedDbChange;
263
373
  }
264
374
 
265
- /** Encrypted DbChange */
375
+ /**
376
+ * Encrypted DbChange
377
+ *
378
+ * @group Messages
379
+ */
266
380
  export type EncryptedDbChange = Uint8Array & Brand<"EncryptedDbChange">;
267
381
 
268
382
  /**
@@ -271,13 +385,19 @@ export type EncryptedDbChange = Uint8Array & Brand<"EncryptedDbChange">;
271
385
  * Used in Evolu's sync protocol to replicate data changes across devices. Evolu
272
386
  * operates as a durable queue, providing exactly-once delivery guarantees for
273
387
  * reliable synchronization across application restarts and network failures.
388
+ *
389
+ * @group Messages
274
390
  */
275
391
  export interface CrdtMessage {
276
392
  readonly timestamp: Timestamp;
277
393
  readonly change: DbChange;
278
394
  }
279
395
 
280
- /** Test helper for creating a simple {@link CrdtMessage}. */
396
+ /**
397
+ * Test helper for creating a simple {@link CrdtMessage}.
398
+ *
399
+ * @group Testing
400
+ */
281
401
  export const testCreateCrdtMessage = (
282
402
  id: Id,
283
403
  millis: number,
@@ -296,9 +416,19 @@ export const testCreateCrdtMessage = (
296
416
  }),
297
417
  });
298
418
 
419
+ /**
420
+ * Column values of a {@link DbChange}, keyed by column name.
421
+ *
422
+ * @group Messages
423
+ */
299
424
  export const DbChangeValues = /*#__PURE__*/ record(String, SqliteValue);
300
425
  export type DbChangeValues = typeof DbChangeValues.Output;
301
426
 
427
+ /**
428
+ * Column values that contain no reserved system columns.
429
+ *
430
+ * @group Messages
431
+ */
302
432
  export const ValidDbChangeValues = /*#__PURE__*/ brand(
303
433
  "ValidDbChangeValues",
304
434
  DbChangeValues,
@@ -318,6 +448,11 @@ export const ValidDbChangeValues = /*#__PURE__*/ brand(
318
448
  );
319
449
  export type ValidDbChangeValues = typeof ValidDbChangeValues.Output;
320
450
 
451
+ /**
452
+ * Error produced when {@link DbChangeValues} contain reserved system columns.
453
+ *
454
+ * @group Messages
455
+ */
321
456
  export interface ValidDbChangeValuesError extends TypeError<"ValidDbChangeValues"> {
322
457
  readonly value: DbChangeValues;
323
458
  readonly invalidColumns: ReadonlyArray<string>;
@@ -326,6 +461,8 @@ export interface ValidDbChangeValuesError extends TypeError<"ValidDbChangeValues
326
461
  /**
327
462
  * A DbChange is a change to a table row. Together with a unique
328
463
  * {@link Timestamp}, it forms a {@link CrdtMessage}.
464
+ *
465
+ * @group Messages
329
466
  */
330
467
  export const DbChange: ObjectType<{
331
468
  readonly table: typeof String;
@@ -371,17 +508,24 @@ export interface DbChange extends InferType<typeof DbChange> {}
371
508
  * each other, if necessary. One relay should handle hundreds of thousands of
372
509
  * users, and when it goes down, nothing happens, because it will be
373
510
  * synchronized later.
511
+ *
512
+ * @group SQLite
374
513
  */
375
514
  export interface BaseSqliteStorage extends Omit<
376
515
  Storage,
377
516
  "validateWriteKey" | "setWriteKey" | "writeMessages" | "readDbChange"
378
517
  > {
379
- /** Inserts a timestamp for an owner into the skiplist-based storage. */
518
+ /**
519
+ * Inserts a timestamp for an owner into the skiplist-based storage.
520
+ *
521
+ * Returns whether the timestamp was new. An existing timestamp is left
522
+ * unchanged.
523
+ */
380
524
  readonly insertTimestamp: (
381
525
  ownerId: OwnerIdBytes,
382
526
  timestamp: TimestampBytes,
383
527
  strategy: StorageInsertTimestampStrategy,
384
- ) => void;
528
+ ) => boolean;
385
529
 
386
530
  /**
387
531
  * Efficiently checks which timestamps already exist in the database using a
@@ -393,10 +537,20 @@ export interface BaseSqliteStorage extends Omit<
393
537
  ) => ReadonlyArray<TimestampBytes>;
394
538
  }
395
539
 
540
+ /**
541
+ * Dependency wrapper for {@link BaseSqliteStorage}.
542
+ *
543
+ * @group SQLite
544
+ */
396
545
  export interface BaseSqliteStorageDep {
397
546
  readonly baseSqliteStorage: BaseSqliteStorage;
398
547
  }
399
548
 
549
+ /**
550
+ * Dependencies required by {@link createBaseSqliteStorage}.
551
+ *
552
+ * @group SQLite
553
+ */
400
554
  export type SqliteStorageDeps = RandomDep & SqliteDep;
401
555
 
402
556
  /**
@@ -411,6 +565,8 @@ export type SqliteStorageDeps = RandomDep & SqliteDep;
411
565
  * Cloudflare Workers with Durable Objects, and other platforms where memory
412
566
  * doesn't persist between requests. While not extensively tested in all these
413
567
  * environments yet, the stateless design should work well across them.
568
+ *
569
+ * @group SQLite
414
570
  */
415
571
  export const createBaseSqliteStorage = (
416
572
  deps: SqliteStorageDeps,
@@ -421,11 +577,13 @@ export const createBaseSqliteStorage = (
421
577
  strategy: StorageInsertTimestampStrategy,
422
578
  ) => {
423
579
  const level = randomSkiplistLevel(deps);
424
- insertTimestamp(deps)(ownerId, timestamp, level, strategy);
580
+ return insertTimestamp(deps)(ownerId, timestamp, level, strategy);
425
581
  },
426
582
 
427
583
  getExistingTimestamps: (ownerIdBytes, timestampsBytes) => {
428
- const concatenatedTimestamps = concatBytes(...timestampsBytes);
584
+ // A batch can hold hundreds of thousands of timestamps, too many to spread
585
+ // into concatBytes.
586
+ const concatenatedTimestamps = concatByteArrays(timestampsBytes);
429
587
 
430
588
  const result = deps.sqlite.exec<{
431
589
  timestampBytes: TimestampBytes;
@@ -509,6 +667,11 @@ const assertBeginEnd = (begin: NonNegativeInt, end: NonNegativeInt) => {
509
667
  assert(begin <= end, "invalid begin or end");
510
668
  };
511
669
 
670
+ /**
671
+ * Creates the SQLite tables used by {@link BaseSqliteStorage}.
672
+ *
673
+ * @group SQLite
674
+ */
512
675
  export const createBaseSqliteStorageTables = (deps: SqliteDep): void => {
513
676
  for (const query of [
514
677
  /**
@@ -575,6 +738,11 @@ export const createBaseSqliteStorageTables = (deps: SqliteDep): void => {
575
738
  }
576
739
  };
577
740
 
741
+ /**
742
+ * Position at which a timestamp is inserted relative to existing timestamps.
743
+ *
744
+ * @group SQLite
745
+ */
578
746
  export type StorageInsertTimestampStrategy = "append" | "prepend" | "insert";
579
747
 
580
748
  /**
@@ -582,6 +750,8 @@ export type StorageInsertTimestampStrategy = "append" | "prepend" | "insert";
582
750
  * relative to the current first and last timestamps.
583
751
  *
584
752
  * Returns a tuple with the strategy and updated timestamp bounds.
753
+ *
754
+ * @group SQLite
585
755
  */
586
756
  export const getTimestampInsertStrategy = (
587
757
  timestamp: TimestampBytes,
@@ -624,9 +794,9 @@ export const getTimestampInsertStrategy = (
624
794
  * key instead of repeating the same correlated range lookup for every column.
625
795
  *
626
796
  * Inserts are idempotent to support direct calls and message replay. `on
627
- * conflict do nothing` makes a duplicate insertion a no-op, and `changes() > 0`
628
- * ensures ancestor metadata is updated only when the preceding insertion added
629
- * a timestamp.
797
+ * conflict do nothing` makes a duplicate insertion a no-op that reports the
798
+ * timestamp as not new, so the follow-up statements update metadata only for a
799
+ * new timestamp.
630
800
  */
631
801
  const insertTimestamp =
632
802
  (deps: SqliteDep) =>
@@ -635,12 +805,15 @@ const insertTimestamp =
635
805
  timestamp: TimestampBytes,
636
806
  level: PositiveInt,
637
807
  strategy: StorageInsertTimestampStrategy,
638
- ): void => {
808
+ ): boolean => {
639
809
  const [h1, h2] = fingerprintToSqliteFingerprint(
640
810
  timestampBytesToFingerprint(timestamp),
641
811
  );
642
812
 
643
- let queries: Array<ReturnType<typeof sql.prepared>> = [];
813
+ let queries: [
814
+ insert: ReturnType<typeof sql.prepared>,
815
+ ...updates: Array<ReturnType<typeof sql.prepared>>,
816
+ ];
644
817
 
645
818
  switch (strategy) {
646
819
  case "append":
@@ -816,10 +989,7 @@ const insertTimestamp =
816
989
  h2 = u.h2,
817
990
  c = c + 1
818
991
  from u
819
- where
820
- changes() > 0
821
- and ownerId = ${ownerId}
822
- and evolu_timestamp.t = u.t;
992
+ where ownerId = ${ownerId} and evolu_timestamp.t = u.t;
823
993
  `,
824
994
  ];
825
995
  break;
@@ -886,10 +1056,7 @@ const insertTimestamp =
886
1056
  h2 = u.h2,
887
1057
  c = c + 1
888
1058
  from u
889
- where
890
- changes() > 0
891
- and ownerId = ${ownerId}
892
- and evolu_timestamp.t = u.t;
1059
+ where ownerId = ${ownerId} and evolu_timestamp.t = u.t;
893
1060
  `,
894
1061
  ]
895
1062
  : [
@@ -1072,17 +1239,25 @@ const insertTimestamp =
1072
1239
  h2 = uh2,
1073
1240
  c = uc
1074
1241
  from u
1075
- where changes() > 0 and ownerId = ${ownerId} and t = ut;
1242
+ where ownerId = ${ownerId} and t = ut;
1076
1243
  `,
1077
1244
  ];
1078
1245
  break;
1079
1246
  }
1080
1247
 
1081
- for (const query of queries) {
1082
- deps.sqlite.exec(query);
1083
- }
1248
+ // The insert uses "on conflict do nothing". An existing timestamp changes
1249
+ // nothing, and the metadata updates are skipped.
1250
+ const [insert, ...updates] = queries;
1251
+ if (deps.sqlite.exec(insert).changes === 0) return false;
1252
+ for (const update of updates) deps.sqlite.exec(update);
1253
+ return true;
1084
1254
  };
1085
1255
 
1256
+ /**
1257
+ * Computes the {@link Fingerprint} of a single timestamp.
1258
+ *
1259
+ * @group Ranges
1260
+ */
1086
1261
  export const timestampBytesToFingerprint = (
1087
1262
  timestamp: TimestampBytes,
1088
1263
  ): Fingerprint => {
@@ -1093,6 +1268,8 @@ export const timestampBytesToFingerprint = (
1093
1268
  /**
1094
1269
  * Computes a brute-force {@link Fingerprint} from {@link TimestampBytes} values
1095
1270
  * for tests and benchmarks.
1271
+ *
1272
+ * @group Testing
1096
1273
  */
1097
1274
  export const testFingerprintTimestamps = (
1098
1275
  timestamps: ReadonlyArray<TimestampBytes>,
@@ -1492,6 +1669,11 @@ const fingerprintRanges =
1492
1669
  // XOR in SQLite
1493
1670
  const x = (a: string, b: string) => sql.raw(`(${a} | ${b}) - (${a} & ${b})`);
1494
1671
 
1672
+ /**
1673
+ * Reads the timestamp at a position within an owner's ordered timestamps.
1674
+ *
1675
+ * @group SQLite
1676
+ */
1495
1677
  export const getTimestampByIndex =
1496
1678
  (deps: SqliteDep) =>
1497
1679
  (ownerId: OwnerIdBytes, index: NonNegativeInt): TimestampBytes => {
@@ -1572,7 +1754,11 @@ export const getTimestampByIndex =
1572
1754
  return result.rows[0].pt;
1573
1755
  };
1574
1756
 
1575
- /** Reads owner usage from SQLite and returns default bounds when absent. */
1757
+ /**
1758
+ * Reads owner usage from SQLite and returns default bounds when absent.
1759
+ *
1760
+ * @group SQLite
1761
+ */
1576
1762
  export const readOwnerUsageOrDefault =
1577
1763
  (deps: SqliteDep) =>
1578
1764
  (
@@ -1617,12 +1803,14 @@ export const readOwnerUsageOrDefault =
1617
1803
  *
1618
1804
  * Used by both relay and client to maintain firstTimestamp/lastTimestamp after
1619
1805
  * processing messages.
1806
+ *
1807
+ * @group SQLite
1620
1808
  */
1621
1809
  export const updateOwnerUsage =
1622
1810
  (deps: SqliteDep) =>
1623
1811
  (
1624
1812
  ownerIdBytes: OwnerIdBytes,
1625
- storedBytes: PositiveInt,
1813
+ storedBytes: NonNegativeInt,
1626
1814
  firstTimestamp: TimestampBytes,
1627
1815
  lastTimestamp: TimestampBytes,
1628
1816
  ): void => {