@evolu/common 8.0.0-next.4 → 8.0.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 (237) hide show
  1. package/dist/src/Array.d.ts +279 -173
  2. package/dist/src/Array.d.ts.map +1 -1
  3. package/dist/src/Array.js +105 -101
  4. package/dist/src/Assert.d.ts +44 -44
  5. package/dist/src/Assert.d.ts.map +1 -1
  6. package/dist/src/Assert.js +44 -32
  7. package/dist/src/BigInt.d.ts +6 -3
  8. package/dist/src/BigInt.d.ts.map +1 -1
  9. package/dist/src/BigInt.js +5 -2
  10. package/dist/src/Brand.d.ts +27 -40
  11. package/dist/src/Brand.d.ts.map +1 -1
  12. package/dist/src/Buffer.d.ts +27 -11
  13. package/dist/src/Buffer.d.ts.map +1 -1
  14. package/dist/src/Cache.d.ts +9 -4
  15. package/dist/src/Cache.d.ts.map +1 -1
  16. package/dist/src/Cache.js +8 -3
  17. package/dist/src/Callbacks.d.ts +30 -19
  18. package/dist/src/Callbacks.d.ts.map +1 -1
  19. package/dist/src/Console.d.ts +145 -66
  20. package/dist/src/Console.d.ts.map +1 -1
  21. package/dist/src/Console.js +91 -43
  22. package/dist/src/Crypto.d.ts +85 -42
  23. package/dist/src/Crypto.d.ts.map +1 -1
  24. package/dist/src/Crypto.js +44 -20
  25. package/dist/src/Eq.d.ts +87 -21
  26. package/dist/src/Eq.d.ts.map +1 -1
  27. package/dist/src/Eq.js +67 -19
  28. package/dist/src/Error.d.ts +5 -3
  29. package/dist/src/Error.d.ts.map +1 -1
  30. package/dist/src/Error.js +16 -0
  31. package/dist/src/Function.d.ts +119 -58
  32. package/dist/src/Function.d.ts.map +1 -1
  33. package/dist/src/Function.js +86 -43
  34. package/dist/src/Http.d.ts +394 -0
  35. package/dist/src/Http.d.ts.map +1 -0
  36. package/dist/src/Http.js +102 -0
  37. package/dist/src/Identicon.d.ts +16 -7
  38. package/dist/src/Identicon.d.ts.map +1 -1
  39. package/dist/src/Identicon.js +14 -5
  40. package/dist/src/LeakDetector.d.ts +88 -0
  41. package/dist/src/LeakDetector.d.ts.map +1 -0
  42. package/dist/src/LeakDetector.js +86 -0
  43. package/dist/src/LockManager.d.ts +18 -9
  44. package/dist/src/LockManager.d.ts.map +1 -1
  45. package/dist/src/LockManager.js +7 -8
  46. package/dist/src/Lookup.d.ts +43 -13
  47. package/dist/src/Lookup.d.ts.map +1 -1
  48. package/dist/src/Lookup.js +28 -8
  49. package/dist/src/Microtask.d.ts +1 -1
  50. package/dist/src/Microtask.d.ts.map +1 -1
  51. package/dist/src/Microtask.js +3 -7
  52. package/dist/src/Number.d.ts +49 -13
  53. package/dist/src/Number.d.ts.map +1 -1
  54. package/dist/src/Number.js +34 -5
  55. package/dist/src/Object.d.ts +160 -40
  56. package/dist/src/Object.d.ts.map +1 -1
  57. package/dist/src/Object.js +92 -46
  58. package/dist/src/Option.d.ts +10 -7
  59. package/dist/src/Option.d.ts.map +1 -1
  60. package/dist/src/Option.js +9 -6
  61. package/dist/src/Order.d.ts +21 -21
  62. package/dist/src/Order.d.ts.map +1 -1
  63. package/dist/src/Order.js +18 -21
  64. package/dist/src/Platform.d.ts +31 -0
  65. package/dist/src/Platform.d.ts.map +1 -1
  66. package/dist/src/Platform.js +98 -0
  67. package/dist/src/Random.d.ts +32 -17
  68. package/dist/src/Random.d.ts.map +1 -1
  69. package/dist/src/Random.js +12 -8
  70. package/dist/src/Redacted.d.ts +43 -29
  71. package/dist/src/Redacted.d.ts.map +1 -1
  72. package/dist/src/Redacted.js +14 -3
  73. package/dist/src/Ref.d.ts +11 -3
  74. package/dist/src/Ref.d.ts.map +1 -1
  75. package/dist/src/Ref.js +2 -2
  76. package/dist/src/RefCount.d.ts +2 -6
  77. package/dist/src/RefCount.d.ts.map +1 -1
  78. package/dist/src/RefCount.js +33 -90
  79. package/dist/src/Relation.d.ts +70 -5
  80. package/dist/src/Relation.d.ts.map +1 -1
  81. package/dist/src/Relation.js +77 -2
  82. package/dist/src/Resource.d.ts +590 -155
  83. package/dist/src/Resource.d.ts.map +1 -1
  84. package/dist/src/Resource.js +592 -343
  85. package/dist/src/Result.d.ts +838 -191
  86. package/dist/src/Result.d.ts.map +1 -1
  87. package/dist/src/Result.js +163 -102
  88. package/dist/src/Schedule.d.ts +684 -231
  89. package/dist/src/Schedule.d.ts.map +1 -1
  90. package/dist/src/Schedule.js +766 -295
  91. package/dist/src/Set.d.ts +89 -40
  92. package/dist/src/Set.d.ts.map +1 -1
  93. package/dist/src/Set.js +50 -29
  94. package/dist/src/Sqlite.d.ts +29 -72
  95. package/dist/src/Sqlite.d.ts.map +1 -1
  96. package/dist/src/Sqlite.js +41 -46
  97. package/dist/src/StackTrace.d.ts +40 -0
  98. package/dist/src/StackTrace.d.ts.map +1 -0
  99. package/dist/src/StackTrace.js +140 -0
  100. package/dist/src/Store.d.ts +5 -4
  101. package/dist/src/Store.d.ts.map +1 -1
  102. package/dist/src/Store.js +5 -4
  103. package/dist/src/String.d.ts +7 -1
  104. package/dist/src/String.d.ts.map +1 -1
  105. package/dist/src/String.js +20 -7
  106. package/dist/src/Task.d.ts +3572 -1770
  107. package/dist/src/Task.d.ts.map +1 -1
  108. package/dist/src/Task.js +1879 -1274
  109. package/dist/src/Test.d.ts +17 -125
  110. package/dist/src/Test.d.ts.map +1 -1
  111. package/dist/src/Test.js +17 -68
  112. package/dist/src/Time.d.ts +108 -49
  113. package/dist/src/Time.d.ts.map +1 -1
  114. package/dist/src/Time.js +172 -73
  115. package/dist/src/Type.d.ts +3542 -1926
  116. package/dist/src/Type.d.ts.map +1 -1
  117. package/dist/src/Type.js +3070 -1784
  118. package/dist/src/Types.d.ts +200 -108
  119. package/dist/src/Types.d.ts.map +1 -1
  120. package/dist/src/Types.js +39 -8
  121. package/dist/src/WebSocket.d.ts +48 -23
  122. package/dist/src/WebSocket.d.ts.map +1 -1
  123. package/dist/src/WebSocket.js +15 -6
  124. package/dist/src/Worker.d.ts +94 -36
  125. package/dist/src/Worker.d.ts.map +1 -1
  126. package/dist/src/Worker.js +58 -24
  127. package/dist/src/index.d.ts +55 -52
  128. package/dist/src/index.d.ts.map +1 -1
  129. package/dist/src/index.js +3 -1
  130. package/dist/src/intl/cs.d.ts +94 -0
  131. package/dist/src/intl/cs.d.ts.map +1 -0
  132. package/dist/src/intl/cs.js +220 -0
  133. package/dist/src/intl/index.d.ts +7 -0
  134. package/dist/src/intl/index.d.ts.map +1 -0
  135. package/dist/src/intl/index.js +6 -0
  136. package/dist/src/local-first/Db.d.ts +12 -8
  137. package/dist/src/local-first/Db.d.ts.map +1 -1
  138. package/dist/src/local-first/Db.js +109 -132
  139. package/dist/src/local-first/Error.d.ts +4 -4
  140. package/dist/src/local-first/Evolu.d.ts +338 -92
  141. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  142. package/dist/src/local-first/Evolu.js +47 -55
  143. package/dist/src/local-first/LocalAuth.d.ts +4 -4
  144. package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
  145. package/dist/src/local-first/Owner.d.ts +61 -24
  146. package/dist/src/local-first/Owner.d.ts.map +1 -1
  147. package/dist/src/local-first/Owner.js +45 -8
  148. package/dist/src/local-first/Protocol.d.ts +44 -31
  149. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  150. package/dist/src/local-first/Protocol.js +36 -13
  151. package/dist/src/local-first/Query.d.ts +147 -62
  152. package/dist/src/local-first/Query.d.ts.map +1 -1
  153. package/dist/src/local-first/Query.js +112 -58
  154. package/dist/src/local-first/Relay.d.ts +44 -14
  155. package/dist/src/local-first/Relay.d.ts.map +1 -1
  156. package/dist/src/local-first/Schema.d.ts +92 -41
  157. package/dist/src/local-first/Schema.d.ts.map +1 -1
  158. package/dist/src/local-first/Schema.js +25 -8
  159. package/dist/src/local-first/Shared.d.ts +23 -21
  160. package/dist/src/local-first/Shared.d.ts.map +1 -1
  161. package/dist/src/local-first/Shared.js +100 -57
  162. package/dist/src/local-first/Storage.d.ts +53 -32
  163. package/dist/src/local-first/Storage.d.ts.map +1 -1
  164. package/dist/src/local-first/Storage.js +194 -234
  165. package/dist/src/local-first/Timestamp.d.ts +21 -21
  166. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  167. package/dist/src/local-first/Timestamp.js +2 -2
  168. package/dist/src/local-first/index.d.ts +11 -11
  169. package/package.json +14 -13
  170. package/src/Array.ts +306 -205
  171. package/src/Assert.ts +54 -69
  172. package/src/BigInt.ts +6 -3
  173. package/src/Brand.ts +27 -40
  174. package/src/Buffer.ts +28 -12
  175. package/src/Cache.ts +9 -4
  176. package/src/Callbacks.ts +30 -19
  177. package/src/Console.ts +156 -84
  178. package/src/Crypto.ts +83 -41
  179. package/src/Eq.ts +95 -22
  180. package/src/Error.ts +8 -4
  181. package/src/Function.ts +167 -59
  182. package/src/Http.ts +549 -0
  183. package/src/Identicon.ts +16 -7
  184. package/src/LeakDetector.ts +182 -0
  185. package/src/LockManager.ts +43 -39
  186. package/src/Lookup.ts +62 -28
  187. package/src/Microtask.ts +19 -23
  188. package/src/Number.ts +64 -12
  189. package/src/Object.ts +173 -49
  190. package/src/Option.ts +10 -7
  191. package/src/Order.ts +23 -21
  192. package/src/Platform.ts +136 -0
  193. package/src/Polyfills.ts +6 -12
  194. package/src/Random.ts +32 -19
  195. package/src/Redacted.ts +44 -30
  196. package/src/Ref.ts +14 -6
  197. package/src/RefCount.ts +38 -80
  198. package/src/Relation.ts +202 -10
  199. package/src/Resource.ts +1199 -591
  200. package/src/Result.ts +930 -271
  201. package/src/Schedule.ts +859 -343
  202. package/src/Set.ts +89 -40
  203. package/src/Sqlite.ts +144 -138
  204. package/src/StackTrace.ts +230 -0
  205. package/src/Store.ts +8 -7
  206. package/src/String.ts +22 -5
  207. package/src/Task.ts +5101 -2832
  208. package/src/Test.ts +20 -168
  209. package/src/Time.ts +293 -90
  210. package/src/Type.ts +10034 -4117
  211. package/src/Types.ts +227 -110
  212. package/src/WebSocket.ts +64 -36
  213. package/src/Worker.ts +180 -72
  214. package/src/index.ts +55 -52
  215. package/src/intl/cs.ts +388 -0
  216. package/src/intl/index.ts +7 -0
  217. package/src/local-first/Db.ts +152 -179
  218. package/src/local-first/Error.ts +4 -4
  219. package/src/local-first/Evolu.ts +495 -250
  220. package/src/local-first/LocalAuth.ts +6 -12
  221. package/src/local-first/Owner.ts +58 -21
  222. package/src/local-first/Protocol.ts +49 -31
  223. package/src/local-first/Query.ts +146 -61
  224. package/src/local-first/Relay.ts +51 -21
  225. package/src/local-first/Schema.ts +109 -51
  226. package/src/local-first/Shared.ts +266 -226
  227. package/src/local-first/Storage.ts +266 -264
  228. package/src/local-first/Timestamp.ts +25 -18
  229. package/src/local-first/index.ts +11 -11
  230. package/dist/src/Skiplist.d.ts +0 -28
  231. package/dist/src/Skiplist.d.ts.map +0 -1
  232. package/dist/src/Skiplist.js +0 -64
  233. package/dist/src/Tracer.d.ts +0 -48
  234. package/dist/src/Tracer.d.ts.map +0 -1
  235. package/dist/src/Tracer.js +0 -6
  236. package/src/Skiplist.ts +0 -108
  237. package/src/Tracer.ts +0 -54
@@ -5,40 +5,46 @@
5
5
  */
6
6
 
7
7
  import { sha256 } from "@noble/hashes/sha2.js";
8
- import type { NonEmptyReadonlyArray } from "../Array.js";
9
- import { firstInArray, isNonEmptyArray } from "../Array.js";
10
- import { assert } from "../Assert.js";
11
- import type { Brand } from "../Brand.js";
12
- import { concatBytes } from "../Buffer.js";
13
- import { decrement } from "../Number.js";
14
- import type { RandomDep } from "../Random.js";
15
- import { err, ok } from "../Result.js";
16
- import type { SqliteDep } from "../Sqlite.js";
17
- import { sql, SqliteValue } from "../Sqlite.js";
18
- import type { Task } from "../Task.js";
19
- import { Millis } from "../Time.js";
20
- import type { InferType, Int64String, Typed, TypeError } from "../Type.js";
8
+ import type { NonEmptyReadonlyArray } from "../Array.ts";
9
+ import { firstInArray, isNonEmptyArray } from "../Array.ts";
10
+ import { assert } from "../Assert.ts";
11
+ import type { Brand } from "../Brand.ts";
12
+ import { concatBytes } from "../Buffer.ts";
13
+ import { decrement } from "../Number.ts";
14
+ import type { RandomDep } from "../Random.ts";
15
+ import { err, ok } from "../Result.ts";
16
+ import type { SqliteDep } from "../Sqlite.ts";
17
+ import { sql, SqliteValue } from "../Sqlite.ts";
18
+ import type { Task } from "../Task.ts";
19
+ import { Millis } from "../Time.ts";
21
20
  import {
22
21
  Boolean,
23
22
  brand,
24
23
  Id,
24
+ type InferType,
25
+ type Int64String,
25
26
  NonNegativeInt,
27
+ Null,
26
28
  nullOr,
27
29
  object,
30
+ type ObjectType,
28
31
  PositiveInt,
29
32
  record,
30
33
  String,
31
- } from "../Type.js";
32
- import type { Awaitable } from "../Types.js";
33
- import type { Owner, OwnerError, OwnerIdBytes } from "./Owner.js";
34
- import { OwnerId, OwnerWriteKey } from "./Owner.js";
35
- import { systemColumnsWithId } from "./Schema.js";
34
+ type Typed,
35
+ type TypeError,
36
+ type UnionType,
37
+ } from "../Type.ts";
38
+ import type { Awaitable } from "../Types.ts";
39
+ import type { Owner, OwnerError, OwnerIdBytes } from "./Owner.ts";
40
+ import { OwnerId, OwnerWriteKey } from "./Owner.ts";
41
+ import { systemColumnsWithId } from "./Schema.ts";
36
42
  import {
37
43
  createTimestamp,
38
44
  orderTimestampBytes,
39
45
  Timestamp,
40
46
  TimestampBytes,
41
- } from "./Timestamp.js";
47
+ } from "./Timestamp.ts";
42
48
 
43
49
  export interface StorageConfig {
44
50
  /**
@@ -56,18 +62,37 @@ export interface StorageConfig {
56
62
  * The callback returns a boolean rather than an error because error handling
57
63
  * and logging are the responsibility of the callback implementation.
58
64
  *
65
+ * Relay deployments configure this callback. Client applications can observe
66
+ * a denied relay write as a `ProtocolQuotaError` by subscribing once to the
67
+ * shared `EvoluErrorDep.evoluError` store returned by `createEvoluDeps`.
68
+ *
59
69
  * ### Example
60
70
  *
61
71
  * ```ts
62
- * // Client
63
- * // evolu.subscribeError
72
+ * import {
73
+ * createAppOwner,
74
+ * createOwnerSecret,
75
+ * createRandomBytes,
76
+ * PositiveInt,
77
+ * } from "@evolu/common";
78
+ * import type { StorageConfig } from "@evolu/common/local-first";
64
79
  *
65
- * // Relay
66
- * isOwnerWithinQuota: (ownerId, requiredBytes) => {
67
- * console.log(ownerId, requiredBytes);
68
- * // Check error via evolu.subscribeError
69
- * return true;
80
+ * // Create once, persist the mnemonic securely, and restore it on later runs.
81
+ * const appOwner = createAppOwner(
82
+ * createOwnerSecret({ randomBytes: createRandomBytes() }),
83
+ * );
84
+ * const maxBytes = PositiveInt.orThrow(1024);
85
+ * const relayStorageConfig: StorageConfig = {
86
+ * isOwnerWithinQuota: (ownerId, requiredBytes) =>
87
+ * ownerId === appOwner.id && requiredBytes <= maxBytes,
70
88
  * };
89
+ *
90
+ * expect(
91
+ * await relayStorageConfig.isOwnerWithinQuota(
92
+ * appOwner.id,
93
+ * PositiveInt.orThrow(2048),
94
+ * ),
95
+ * ).toBe(false);
71
96
  * ```
72
97
  */
73
98
  readonly isOwnerWithinQuota: (
@@ -270,7 +295,7 @@ export const testCreateCrdtMessage = (
270
295
  });
271
296
 
272
297
  export const DbChangeValues = /*#__PURE__*/ record(String, SqliteValue);
273
- export type DbChangeValues = typeof DbChangeValues.Type;
298
+ export type DbChangeValues = typeof DbChangeValues.Output;
274
299
 
275
300
  export const ValidDbChangeValues = /*#__PURE__*/ brand(
276
301
  "ValidDbChangeValues",
@@ -284,12 +309,15 @@ export const ValidDbChangeValues = /*#__PURE__*/ brand(
284
309
  invalidColumns,
285
310
  });
286
311
 
287
- return ok(value);
312
+ return ok();
288
313
  },
314
+ (error) =>
315
+ `DbChange values contain reserved system columns: ${error.invalidColumns.join(", ")}.`,
289
316
  );
290
- export type ValidDbChangeValues = typeof ValidDbChangeValues.Type;
317
+ export type ValidDbChangeValues = typeof ValidDbChangeValues.Output;
291
318
 
292
319
  export interface ValidDbChangeValuesError extends TypeError<"ValidDbChangeValues"> {
320
+ readonly value: DbChangeValues;
293
321
  readonly invalidColumns: ReadonlyArray<string>;
294
322
  }
295
323
 
@@ -297,7 +325,13 @@ export interface ValidDbChangeValuesError extends TypeError<"ValidDbChangeValues
297
325
  * A DbChange is a change to a table row. Together with a unique
298
326
  * {@link Timestamp}, it forms a {@link CrdtMessage}.
299
327
  */
300
- export const DbChange = /*#__PURE__*/ object({
328
+ export const DbChange: ObjectType<{
329
+ readonly table: typeof String;
330
+ readonly id: typeof Id;
331
+ readonly values: typeof ValidDbChangeValues;
332
+ readonly isInsert: typeof Boolean;
333
+ readonly isDelete: UnionType<readonly [typeof Boolean, typeof Null]>;
334
+ }> = /*#__PURE__*/ object({
301
335
  table: String,
302
336
  id: Id,
303
337
  values: ValidDbChangeValues,
@@ -320,9 +354,6 @@ export interface DbChange extends InferType<typeof DbChange> {}
320
354
  * storage](https://github.com/hoytech/negentropy), except we use a Skiplist to
321
355
  * leverage SQLite indexes, which makes the code simpler.
322
356
  *
323
- * Note: A paid review by the SQLite team is planned, as they use the same
324
- * algorithm for their rsync tool.
325
- *
326
357
  * The ideal storage for a Relay should use an architecture like
327
358
  * [strfry](https://github.com/hoytech/strfry) (a KV storage), but with Skiplist
328
359
  * to ensure that insertion order doesn't matter (local-first apps can often
@@ -569,18 +600,31 @@ export const getTimestampInsertStrategy = (
569
600
  };
570
601
 
571
602
  /**
572
- * AFAIK, we can't do both insert and update in one query, and that's probably
573
- * why append is 2x faster than insert. Prepend also has to update parents, but
574
- * it's constantly fast. Insert degrades for reversed (yet LIMIT X magically
575
- * fixes that) but it's OK for append.
603
+ * Append derives the new node's metadata in its insertion statement. Prepend
604
+ * and insert require a second statement to update existing Skiplist ancestors.
605
+ *
606
+ * A non-level-1 append summarizes the suffix after the nearest preceding node
607
+ * at the same or a higher level. A reverse primary-key scan finds that boundary
608
+ * directly, so metadata traversal starts one level below the new node instead
609
+ * of descending from the maximum level.
610
+ *
611
+ * Append is the fastest strategy because level 1, which occurs 75% of the time,
612
+ * needs only the insertion, and promoted nodes derive their metadata without
613
+ * updating existing nodes. Prepend inserts first, then updates one successor at
614
+ * each relevant higher level through predictable edge searches. Insert is the
615
+ * slowest because an arbitrary interior position requires predecessor metadata
616
+ * for the new node and successor discovery to update existing ancestors. All
617
+ * traversals use bounded index ranges, but insert necessarily performs the most
618
+ * Skiplist work.
576
619
  *
577
- * Note: SQL operations are idempotent (using `on conflict do nothing` and
578
- * `changes() > 0`), but this is no longer required here since we use
579
- * {@link BaseSqliteStorage.getExistingTimestamps} to filter out duplicates
580
- * before insertion, which we need for quota checks anyway.
620
+ * SQLite does not support `LATERAL` subqueries. Recursive traversals therefore
621
+ * alternate between finding the next timestamp and loading that row by primary
622
+ * key instead of repeating the same correlated range lookup for every column.
581
623
  *
582
- * TODO: Remove idempotency (`on conflict do nothing` and `changes() > 0`) since
583
- * duplicates are now filtered before insertion.
624
+ * Inserts are idempotent to support direct calls and message replay. `on
625
+ * conflict do nothing` makes a duplicate insertion a no-op, and `changes() > 0`
626
+ * ensures ancestor metadata is updated only when the preceding insertion added
627
+ * a timestamp.
584
628
  */
585
629
  const insertTimestamp =
586
630
  (deps: SqliteDep) =>
@@ -612,12 +656,20 @@ const insertTimestamp =
612
656
  fc(b, cl, pt, nt, ih1, ih2, ic) as (
613
657
  select
614
658
  0,
615
- (
616
- select max(l)
617
- from evolu_timestamp
618
- where ownerId = ${ownerId}
659
+ ${level} - 1,
660
+ ifnull(
661
+ (
662
+ select t
663
+ from evolu_timestamp
664
+ where
665
+ ownerId = ${ownerId}
666
+ and l >= ${level}
667
+ and t < ${timestamp}
668
+ order by t desc
669
+ limit 1
670
+ ),
671
+ zeroblob(0)
619
672
  ),
620
- zeroblob(0),
621
673
  null,
622
674
  0,
623
675
  0,
@@ -643,33 +695,20 @@ const insertTimestamp =
643
695
  )
644
696
  ),
645
697
  iif(
646
- b and cl < ${level} and nt is not null,
647
- (
648
- select (ih1 | h1) - (ih1 & h1)
649
- from evolu_timestamp
650
- where ownerId = ${ownerId} and t = nt
651
- ),
698
+ node.t is not null,
699
+ (ih1 | node.h1) - (ih1 & node.h1),
652
700
  ih1
653
701
  ),
654
702
  iif(
655
- b and cl < ${level} and nt is not null,
656
- (
657
- select (ih2 | h2) - (ih2 & h2)
658
- from evolu_timestamp
659
- where ownerId = ${ownerId} and t = nt
660
- ),
703
+ node.t is not null,
704
+ (ih2 | node.h2) - (ih2 & node.h2),
661
705
  ih2
662
706
  ),
663
- iif(
664
- b and cl < ${level} and nt is not null,
665
- (
666
- select ic + c
667
- from evolu_timestamp
668
- where ownerId = ${ownerId} and t = nt
669
- ),
670
- ic
671
- )
672
- from fc
707
+ iif(node.t is not null, ic + node.c, ic)
708
+ from
709
+ fc
710
+ left join evolu_timestamp as node
711
+ on b and node.ownerId = ${ownerId} and node.t = nt
673
712
  where cl > 0
674
713
  )
675
714
  insert into evolu_timestamp (ownerId, t, l, h1, h2, c)
@@ -704,64 +743,60 @@ const insertTimestamp =
704
743
  from evolu_timestamp
705
744
  where ownerId = ${ownerId}
706
745
  ),
707
- fp(b, cl, pt, nt, h1, h2, c) as (
746
+ fp(b, cl, pt, nt, h1, h2) as (
708
747
  select
709
748
  0,
710
749
  (select ml from ml),
711
750
  null,
712
751
  null,
713
752
  null,
714
- null,
715
753
  null
716
754
  union all
717
755
  select
718
- not b,
719
- iif(b, cl - 1, cl),
756
+ not fp.b,
757
+ iif(fp.b, fp.cl - 1, fp.cl),
720
758
  iif(
721
- b,
722
- iif(nt is not null and (pt is null or nt < pt), nt, pt),
723
- pt
759
+ fp.b,
760
+ iif(
761
+ fp.nt is not null and (fp.pt is null or fp.nt < fp.pt),
762
+ fp.nt,
763
+ fp.pt
764
+ ),
765
+ fp.pt
724
766
  ),
725
767
  iif(
726
- b,
768
+ fp.b,
727
769
  null,
728
770
  (
729
771
  select t
730
772
  from evolu_timestamp
731
- where ownerId = ${ownerId} and l = cl and t > ${timestamp}
773
+ where
774
+ ownerId = ${ownerId}
775
+ and l = fp.cl
776
+ and t > ${timestamp}
732
777
  order by t
733
778
  limit 1
734
779
  )
735
780
  ),
736
781
  iif(
737
- b and nt is not null and (pt is null or nt < pt),
738
- (
739
- select h1
740
- from evolu_timestamp
741
- where ownerId = ${ownerId} and t = nt
742
- ),
743
- null
744
- ),
745
- iif(
746
- b and nt is not null and (pt is null or nt < pt),
747
- (
748
- select h2
749
- from evolu_timestamp
750
- where ownerId = ${ownerId} and t = nt
751
- ),
782
+ fp.b
783
+ and fp.nt is not null
784
+ and (fp.pt is null or fp.nt < fp.pt),
785
+ node.h1,
752
786
  null
753
787
  ),
754
788
  iif(
755
- b and nt is not null and (pt is null or nt < pt),
756
- (
757
- select c
758
- from evolu_timestamp
759
- where ownerId = ${ownerId} and t = nt
760
- ),
789
+ fp.b
790
+ and fp.nt is not null
791
+ and (fp.pt is null or fp.nt < fp.pt),
792
+ node.h2,
761
793
  null
762
794
  )
763
- from fp
764
- where cl > ${level}
795
+ from
796
+ fp
797
+ left join evolu_timestamp as node
798
+ on fp.b and node.ownerId = ${ownerId} and node.t = fp.nt
799
+ where fp.cl > ${level}
765
800
  ),
766
801
  u(t, h1, h2) as (
767
802
  select
@@ -771,8 +806,7 @@ const insertTimestamp =
771
806
  from fp
772
807
  where h1 is not null and pt is not null
773
808
  order by pt
774
- -- Check skiplistMaxLevel docs.
775
- limit 10
809
+ limit ${sql.raw(skiplistMaxLevel.toString())}
776
810
  )
777
811
  update evolu_timestamp
778
812
  set
@@ -800,19 +834,17 @@ const insertTimestamp =
800
834
  on conflict do nothing;
801
835
  `,
802
836
 
803
- // DEV: Check whether a boolean flag is faster.
804
837
  sql.prepared`
805
838
  with
806
- p(l, t, h1, h2) as (
839
+ p(l, t, pt) as (
807
840
  select
808
841
  (
809
842
  select max(l) + 1
810
843
  from evolu_timestamp
811
844
  where ownerId = ${ownerId}
812
845
  ),
813
- null,
814
- null,
815
- null
846
+ X'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF',
847
+ X'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'
816
848
  union all
817
849
  select
818
850
  p.l - 1,
@@ -824,46 +856,27 @@ const insertTimestamp =
824
856
  ownerId = ${ownerId}
825
857
  and l = p.l - 1
826
858
  and t > ${timestamp}
827
- and (p.t is null or p.t > t)
859
+ and t < p.t
828
860
  order by t
829
861
  limit 1
830
862
  ),
831
863
  p.t
832
864
  ),
833
- (
834
- select h1
835
- from evolu_timestamp
836
- where
837
- ownerId = ${ownerId}
838
- and l = p.l - 1
839
- and t > ${timestamp}
840
- and (p.t is null or p.t > t)
841
- order by t
842
- limit 1
843
- ),
844
- (
845
- select h2
846
- from evolu_timestamp
847
- where
848
- ownerId = ${ownerId}
849
- and l = p.l - 1
850
- and t > ${timestamp}
851
- and (p.t is null or p.t > t)
852
- order by t
853
- limit 1
854
- )
865
+ p.t
855
866
  from p
856
867
  where p.l > 2
857
- -- Check skiplistMaxLevel docs.
858
- limit 10
868
+ limit ${sql.raw(skiplistMaxLevel.toString())}
859
869
  ),
860
870
  u(t, h1, h2) as (
861
871
  select
862
- t,
863
- (${h1} | h1) - (${h1} & h1),
864
- (${h2} | h2) - (${h2} & h2)
865
- from p
866
- where h1 is not null
872
+ p.t,
873
+ (${h1} | node.h1) - (${h1} & node.h1),
874
+ (${h2} | node.h2) - (${h2} & node.h2)
875
+ from
876
+ p
877
+ join evolu_timestamp as node
878
+ on node.ownerId = ${ownerId} and node.t = p.t
879
+ where p.t is not p.pt
867
880
  )
868
881
  update evolu_timestamp
869
882
  set
@@ -919,34 +932,16 @@ const insertTimestamp =
919
932
  limit 1
920
933
  )
921
934
  ),
922
- iif(
923
- b and cl < ${level} and nt is not null,
924
- (
925
- select h1
926
- from evolu_timestamp
927
- where ownerId = ${ownerId} and t = nt
928
- ),
929
- null
930
- ),
931
- iif(
932
- b and cl < ${level} and nt is not null,
933
- (
934
- select h2
935
- from evolu_timestamp
936
- where ownerId = ${ownerId} and t = nt
937
- ),
938
- null
939
- ),
940
- iif(
941
- b and cl < ${level} and nt is not null,
942
- (
943
- select c
944
- from evolu_timestamp
945
- where ownerId = ${ownerId} and t = nt
946
- ),
947
- null
948
- )
949
- from c0
935
+ node.h1,
936
+ node.h2,
937
+ node.c
938
+ from
939
+ c0
940
+ left join evolu_timestamp as node
941
+ on c0.b
942
+ and c0.cl < ${level}
943
+ and node.ownerId = ${ownerId}
944
+ and node.t = c0.nt
950
945
  where cl > 0
951
946
  ),
952
947
  c1(l, t, h1, h2, c) as (
@@ -984,22 +979,27 @@ const insertTimestamp =
984
979
  from c3
985
980
  group by l
986
981
  ),
987
- -- DEV: Check whether a boolean flag is faster.
988
- n(l, t, h1, h2, c) as (
982
+ -- Alternate range discovery and primary-key loading.
983
+ n(b, l, t, nt, h1, h2, c) as (
989
984
  select
985
+ 1,
990
986
  (
991
987
  select max(l) + 1
992
988
  from evolu_timestamp
993
989
  where ownerId = ${ownerId}
994
990
  ),
991
+ X'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF',
995
992
  null,
996
993
  null,
997
994
  null,
998
995
  null
999
996
  union all
1000
997
  select
1001
- n.l - 1,
1002
- ifnull(
998
+ not n.b,
999
+ iif(n.b, n.l - 1, n.l),
1000
+ iif(n.b, n.t, ifnull(n.nt, n.t)),
1001
+ iif(
1002
+ n.b,
1003
1003
  (
1004
1004
  select t
1005
1005
  from evolu_timestamp
@@ -1007,47 +1007,22 @@ const insertTimestamp =
1007
1007
  ownerId = ${ownerId}
1008
1008
  and l = n.l - 1
1009
1009
  and t > ${timestamp}
1010
- and (n.t is null or t < n.t)
1010
+ and t < n.t
1011
1011
  order by t
1012
1012
  limit 1
1013
1013
  ),
1014
- n.t
1015
- ),
1016
- (
1017
- select h1
1018
- from evolu_timestamp
1019
- where
1020
- ownerId = ${ownerId}
1021
- and l = n.l - 1
1022
- and t > ${timestamp}
1023
- and (n.t is null or t < n.t)
1024
- order by t
1025
- limit 1
1026
- ),
1027
- (
1028
- select h2
1029
- from evolu_timestamp
1030
- where
1031
- ownerId = ${ownerId}
1032
- and l = n.l - 1
1033
- and t > ${timestamp}
1034
- and (n.t is null or t < n.t)
1035
- order by t
1036
- limit 1
1014
+ null
1037
1015
  ),
1038
- (
1039
- select c
1040
- from evolu_timestamp
1041
- where
1042
- ownerId = ${ownerId}
1043
- and l = n.l - 1
1044
- and t > ${timestamp}
1045
- and (n.t is null or t < n.t)
1046
- order by t
1047
- limit 1
1048
- )
1049
- from n
1050
- where l - 1 > (select min(l) from c4)
1016
+ iif(not n.b and n.nt is not null, node.h1, null),
1017
+ iif(not n.b and n.nt is not null, node.h2, null),
1018
+ iif(not n.b and n.nt is not null, node.c, null)
1019
+ from
1020
+ n
1021
+ left join evolu_timestamp as node
1022
+ on not n.b
1023
+ and node.ownerId = ${ownerId}
1024
+ and node.t = n.nt
1025
+ where iif(n.b, n.l - 1 > (select min(l) from c4), 1)
1051
1026
  ),
1052
1027
  u(ut, uh1, uh2, uc) as (
1053
1028
  select t, h1, h2, c from c4 where t = ${timestamp}
@@ -1085,9 +1060,9 @@ const insertTimestamp =
1085
1060
  )
1086
1061
  )
1087
1062
  from n
1063
+ where b
1088
1064
  group by t
1089
- -- Check skiplistMaxLevel docs.
1090
- limit 10
1065
+ limit ${sql.raw(skiplistMaxLevel.toString())}
1091
1066
  )
1092
1067
  update evolu_timestamp
1093
1068
  set
@@ -1113,6 +1088,24 @@ export const timestampBytesToFingerprint = (
1113
1088
  return hash as Fingerprint;
1114
1089
  };
1115
1090
 
1091
+ /**
1092
+ * Computes a brute-force {@link Fingerprint} from {@link TimestampBytes} values
1093
+ * for tests and benchmarks.
1094
+ */
1095
+ export const testFingerprintTimestamps = (
1096
+ timestamps: ReadonlyArray<TimestampBytes>,
1097
+ ): Fingerprint =>
1098
+ timestamps
1099
+ .map(timestampBytesToFingerprint)
1100
+ .reduce<Fingerprint>(
1101
+ (left, right) =>
1102
+ Uint8Array.from(
1103
+ left,
1104
+ (byte, index) => byte ^ right[index],
1105
+ ) as Fingerprint,
1106
+ zeroFingerprint,
1107
+ );
1108
+
1116
1109
  /**
1117
1110
  * Generates a random skiplist level in the range [1, skiplistMaxLevel].
1118
1111
  * Probabilistic approach avoids the need for explicit tree balancing.
@@ -1136,13 +1129,32 @@ const randomSkiplistLevel = (deps: RandomDep): PositiveInt => {
1136
1129
  const skiplistProbability = 0.25;
1137
1130
 
1138
1131
  /**
1139
- * The SQLite has weird behaviour when we have to limit CTEs even when we don't
1140
- * actually limit anything; otherwise, it would not work. But without it, SQLite
1141
- * insert and prepend is slow. The 10 is the maximum allowed number, which
1142
- * "fixes" SQLite.
1132
+ * Maximum Skiplist level and exact upper bound for planner-sensitive CTEs.
1133
+ *
1134
+ * The three CTEs bounded by this value feed `update ... from` statements in the
1135
+ * prepend and insert paths. Each emits at most one row per Skiplist level.
1136
+ * Without a known integer `limit`, SQLite estimates a recursive CTE at about
1137
+ * 2^32 rows and scans every timestamp for the owner instead of scanning the CTE
1138
+ * and looking up each timestamp by primary key.
1143
1139
  *
1144
- * Because we are using {@link skiplistProbability} 0.25, it's OK even for
1145
- * millions of rows.
1140
+ * Evolu does not run SQLite's `ANALYZE` command or `PRAGMA optimize`. `ANALYZE`
1141
+ * samples database contents and stores statistics in tables such as
1142
+ * `sqlite_stat1`; without those statistics, the query planner uses default
1143
+ * estimates. With those default estimates, 10 is the planner crossover.
1144
+ * Collected statistics or a planner change can move it because this is
1145
+ * undocumented cost model behavior. Do not enable planner statistics without
1146
+ * benchmarking every storage workload and verifying its query plans on a
1147
+ * representative populated database.
1148
+ *
1149
+ * The queries inject this value with `sql.raw`. SQLite versions before 3.47
1150
+ * cannot derive the estimate from a bound parameter. Newer versions require
1151
+ * QPSG to be disabled and invalidate the query plan whenever that parameter is
1152
+ * bound, which would recompile these hot cached statements on every execution.
1153
+ *
1154
+ * Because the limit can emit at most one row per Skiplist level, the Skiplist
1155
+ * is capped at the same value to prevent updates from being truncated. With
1156
+ * {@link skiplistProbability} 0.25, level 10 occurs once per 4^9 rows on
1157
+ * average, keeping the highest-level scan small for millions of rows.
1146
1158
  */
1147
1159
  const skiplistMaxLevel = 10;
1148
1160
 
@@ -1340,12 +1352,15 @@ const fingerprint =
1340
1352
  };
1341
1353
 
1342
1354
  /**
1343
- * First, check this: https://logperiodic.com/rbsr.html#tree-friendly-functions
1355
+ * Computes all RBSR fingerprint buckets in one SQL statement.
1356
+ *
1357
+ * Continuous ranges reduce the required traversals from 32 to 16. Each
1358
+ * recursive step first finds the next timestamp, then loads its count and
1359
+ * fingerprint by primary key in the following step. This two-phase traversal
1360
+ * avoids repeated correlated range lookups because SQLite does not support
1361
+ * `LATERAL` subqueries.
1344
1362
  *
1345
- * We are a little smarter. We leverage continuous ranges to have half of
1346
- * traversals. 16 instead of 32. And we compute all of them in a single SQL
1347
- * select. If only we could get rid of those subqueries. Then, it would be
1348
- * perfect. Btw, reading h1, h2, c in the second step would be slighly faster.
1363
+ * See https://logperiodic.com/rbsr.html#tree-friendly-functions.
1349
1364
  */
1350
1365
  const fingerprintRanges =
1351
1366
  (deps: SqliteDep) =>
@@ -1366,14 +1381,11 @@ const fingerprintRanges =
1366
1381
  select max(l) from evolu_timestamp where ownerId = ${ownerId}
1367
1382
  ),
1368
1383
  c0(c) as (select value as c from json_each(${bucketsJson})),
1369
- c1(c, b, nt, nc, nh1, nh2, ft, tt, dl, ic, h1, h2) as (
1384
+ c1(c, b, nt, ft, tt, dl, ic, h1, h2) as (
1370
1385
  select
1371
1386
  c,
1372
1387
  1,
1373
1388
  null,
1374
- null,
1375
- null,
1376
- null,
1377
1389
  zeroblob(0),
1378
1390
  X'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF',
1379
1391
  ml,
@@ -1385,60 +1397,50 @@ const fingerprintRanges =
1385
1397
  ml
1386
1398
  union all
1387
1399
  select
1388
- c,
1389
- not b,
1400
+ c1.c,
1401
+ not c1.b,
1390
1402
  iif(
1391
- b,
1403
+ c1.b,
1392
1404
  (
1393
1405
  select t
1394
1406
  from evolu_timestamp
1395
- where l = dl and t > ft and t < tt and ownerId = ${ownerId}
1407
+ where
1408
+ l = c1.dl
1409
+ and t > c1.ft
1410
+ and t < c1.tt
1411
+ and ownerId = ${ownerId}
1396
1412
  order by t
1397
1413
  limit 1
1398
1414
  ),
1399
1415
  null
1400
1416
  ),
1417
+ iif(c1.b, c1.ft, iif(c1.ic + node.c <= c1.c, c1.nt, c1.ft)),
1401
1418
  iif(
1402
- b,
1403
- (
1404
- select c
1405
- from evolu_timestamp
1406
- where l = dl and t > ft and t < tt and ownerId = ${ownerId}
1407
- order by t
1408
- limit 1
1409
- ),
1410
- null
1419
+ c1.b,
1420
+ c1.tt,
1421
+ iif(c1.ic + node.c <= c1.c, c1.tt, ifnull(c1.nt, c1.tt))
1411
1422
  ),
1423
+ iif(c1.b, c1.dl, iif(c1.ic + node.c <= c1.c, c1.dl, c1.dl - 1)),
1412
1424
  iif(
1413
- b,
1414
- (
1415
- select h1
1416
- from evolu_timestamp
1417
- where l = dl and t > ft and t < tt and ownerId = ${ownerId}
1418
- order by t
1419
- limit 1
1420
- ),
1421
- null
1425
+ c1.b,
1426
+ c1.ic,
1427
+ iif(c1.ic + node.c <= c1.c, c1.ic + node.c, c1.ic)
1422
1428
  ),
1423
1429
  iif(
1424
- b,
1425
- (
1426
- select h2
1427
- from evolu_timestamp
1428
- where l = dl and t > ft and t < tt and ownerId = ${ownerId}
1429
- order by t
1430
- limit 1
1431
- ),
1432
- null
1430
+ c1.b,
1431
+ c1.h1,
1432
+ iif(c1.ic + node.c <= c1.c, ${x("c1.h1", "node.h1")}, c1.h1)
1433
1433
  ),
1434
- iif(b, ft, iif(ic + nc <= c, nt, ft)),
1435
- iif(b, tt, iif(ic + nc <= c, tt, ifnull(nt, tt))),
1436
- iif(b, dl, iif(ic + nc <= c, dl, dl - 1)),
1437
- iif(b, ic, iif(ic + nc <= c, ic + nc, ic)),
1438
- iif(b, h1, iif(ic + nc <= c, ${x("h1", "nh1")}, h1)),
1439
- iif(b, h2, iif(ic + nc <= c, ${x("h2", "nh2")}, h2))
1440
- from c1
1441
- where iif(b, 1, ic != c)
1434
+ iif(
1435
+ c1.b,
1436
+ c1.h2,
1437
+ iif(c1.ic + node.c <= c1.c, ${x("c1.h2", "node.h2")}, c1.h2)
1438
+ )
1439
+ from
1440
+ c1
1441
+ left join evolu_timestamp as node
1442
+ on not c1.b and node.ownerId = ${ownerId} and node.t = c1.nt
1443
+ where iif(c1.b, 1, c1.ic != c1.c)
1442
1444
  ),
1443
1445
  c2(h1, h2, t, rn) as (
1444
1446
  select