@evolu/common 6.0.1-preview.2 → 6.0.1-preview.20

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 (154) hide show
  1. package/dist/src/Assert.d.ts +6 -3
  2. package/dist/src/Assert.d.ts.map +1 -1
  3. package/dist/src/Assert.js +7 -4
  4. package/dist/src/Brand.d.ts +75 -0
  5. package/dist/src/Brand.d.ts.map +1 -0
  6. package/dist/src/Brand.js +1 -0
  7. package/dist/src/Buffer.d.ts +1 -1
  8. package/dist/src/Buffer.d.ts.map +1 -1
  9. package/dist/src/Buffer.js +1 -1
  10. package/dist/src/CallbackRegistry.d.ts +53 -0
  11. package/dist/src/CallbackRegistry.d.ts.map +1 -0
  12. package/dist/src/CallbackRegistry.js +25 -0
  13. package/dist/src/Console.d.ts +31 -6
  14. package/dist/src/Console.d.ts.map +1 -1
  15. package/dist/src/Console.js +72 -9
  16. package/dist/src/Crypto.d.ts +61 -34
  17. package/dist/src/Crypto.d.ts.map +1 -1
  18. package/dist/src/Crypto.js +29 -42
  19. package/dist/src/Evolu/Db.d.ts +137 -66
  20. package/dist/src/Evolu/Db.d.ts.map +1 -1
  21. package/dist/src/Evolu/Db.js +300 -705
  22. package/dist/src/Evolu/Diff.d.ts +3 -3
  23. package/dist/src/Evolu/Diff.d.ts.map +1 -1
  24. package/dist/src/Evolu/Diff.js +7 -5
  25. package/dist/src/Evolu/Evolu.d.ts +200 -115
  26. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  27. package/dist/src/Evolu/Evolu.js +287 -157
  28. package/dist/src/Evolu/Internal.d.ts +0 -2
  29. package/dist/src/Evolu/Internal.d.ts.map +1 -1
  30. package/dist/src/Evolu/Internal.js +0 -2
  31. package/dist/src/Evolu/LocalAuth.d.ts +144 -0
  32. package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
  33. package/dist/src/Evolu/LocalAuth.js +171 -0
  34. package/dist/src/Evolu/Owner.d.ts +152 -127
  35. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  36. package/dist/src/Evolu/Owner.js +88 -107
  37. package/dist/src/Evolu/Platform.d.ts +9 -7
  38. package/dist/src/Evolu/Platform.d.ts.map +1 -1
  39. package/dist/src/Evolu/Protocol.d.ts +170 -186
  40. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  41. package/dist/src/Evolu/Protocol.js +487 -343
  42. package/dist/src/Evolu/Public.d.ts +6 -8
  43. package/dist/src/Evolu/Public.d.ts.map +1 -1
  44. package/dist/src/Evolu/Public.js +2 -3
  45. package/dist/src/Evolu/PublicKysely.js +3 -3
  46. package/dist/src/Evolu/Query.d.ts +2 -1
  47. package/dist/src/Evolu/Query.d.ts.map +1 -1
  48. package/dist/src/Evolu/Relay.d.ts +4 -3
  49. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  50. package/dist/src/Evolu/Relay.js +50 -12
  51. package/dist/src/Evolu/Schema.d.ts +129 -73
  52. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  53. package/dist/src/Evolu/Schema.js +169 -89
  54. package/dist/src/Evolu/Storage.d.ts +159 -14
  55. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  56. package/dist/src/Evolu/Storage.js +42 -32
  57. package/dist/src/Evolu/Sync.d.ts +77 -13
  58. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  59. package/dist/src/Evolu/Sync.js +453 -20
  60. package/dist/src/Evolu/Timestamp.d.ts +35 -27
  61. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  62. package/dist/src/Evolu/Timestamp.js +27 -18
  63. package/dist/src/ManyToManyMap.d.ts +74 -10
  64. package/dist/src/ManyToManyMap.d.ts.map +1 -1
  65. package/dist/src/ManyToManyMap.js +41 -6
  66. package/dist/src/Number.d.ts +2 -1
  67. package/dist/src/Number.d.ts.map +1 -1
  68. package/dist/src/Random.d.ts +3 -2
  69. package/dist/src/Random.d.ts.map +1 -1
  70. package/dist/src/RefCountedResourceManager.d.ts +119 -0
  71. package/dist/src/RefCountedResourceManager.d.ts.map +1 -0
  72. package/dist/src/RefCountedResourceManager.js +197 -0
  73. package/dist/src/Result.d.ts +147 -51
  74. package/dist/src/Result.d.ts.map +1 -1
  75. package/dist/src/Result.js +7 -241
  76. package/dist/src/Sqlite.d.ts +21 -4
  77. package/dist/src/Sqlite.d.ts.map +1 -1
  78. package/dist/src/Sqlite.js +50 -8
  79. package/dist/src/Task.d.ts +511 -0
  80. package/dist/src/Task.d.ts.map +1 -0
  81. package/dist/src/Task.js +410 -0
  82. package/dist/src/Time.d.ts +59 -0
  83. package/dist/src/Time.d.ts.map +1 -1
  84. package/dist/src/Time.js +87 -4
  85. package/dist/src/Type.d.ts +462 -327
  86. package/dist/src/Type.d.ts.map +1 -1
  87. package/dist/src/Type.js +490 -436
  88. package/dist/src/Types.d.ts +0 -74
  89. package/dist/src/Types.d.ts.map +1 -1
  90. package/dist/src/WebSocket.d.ts +5 -2
  91. package/dist/src/WebSocket.d.ts.map +1 -1
  92. package/dist/src/WebSocket.js +12 -13
  93. package/dist/src/Worker.d.ts +39 -11
  94. package/dist/src/Worker.d.ts.map +1 -1
  95. package/dist/src/Worker.js +22 -4
  96. package/dist/src/index.d.ts +3 -3
  97. package/dist/src/index.d.ts.map +1 -1
  98. package/dist/src/index.js +3 -3
  99. package/package.json +14 -13
  100. package/src/Assert.ts +8 -7
  101. package/src/Brand.ts +75 -0
  102. package/src/Buffer.ts +1 -1
  103. package/src/CallbackRegistry.ts +84 -0
  104. package/src/Console.ts +91 -11
  105. package/src/Crypto.ts +94 -79
  106. package/src/Evolu/Db.ts +505 -1027
  107. package/src/Evolu/Diff.ts +7 -5
  108. package/src/Evolu/Evolu.ts +682 -329
  109. package/src/Evolu/Internal.ts +0 -2
  110. package/src/Evolu/LocalAuth.ts +422 -0
  111. package/src/Evolu/Owner.ts +221 -235
  112. package/src/Evolu/Platform.ts +9 -9
  113. package/src/Evolu/Protocol.ts +681 -614
  114. package/src/Evolu/Public.ts +7 -14
  115. package/src/Evolu/PublicKysely.ts +3 -3
  116. package/src/Evolu/Query.ts +2 -1
  117. package/src/Evolu/Relay.ts +62 -16
  118. package/src/Evolu/Schema.ts +391 -191
  119. package/src/Evolu/Storage.ts +275 -55
  120. package/src/Evolu/Sync.ts +758 -37
  121. package/src/Evolu/Timestamp.ts +38 -35
  122. package/src/ManyToManyMap.ts +127 -24
  123. package/src/Number.ts +2 -6
  124. package/src/Random.ts +3 -2
  125. package/src/RefCountedResourceManager.ts +368 -0
  126. package/src/Result.ts +152 -53
  127. package/src/Sqlite.ts +59 -23
  128. package/src/Task.ts +779 -0
  129. package/src/Time.ts +168 -4
  130. package/src/Type.ts +696 -660
  131. package/src/Types.ts +0 -76
  132. package/src/WebSocket.ts +23 -17
  133. package/src/Worker.ts +72 -23
  134. package/src/index.ts +3 -3
  135. package/dist/src/Callbacks.d.ts +0 -20
  136. package/dist/src/Callbacks.d.ts.map +0 -1
  137. package/dist/src/Callbacks.js +0 -18
  138. package/dist/src/Evolu/Config.d.ts +0 -69
  139. package/dist/src/Evolu/Config.d.ts.map +0 -1
  140. package/dist/src/Evolu/Config.js +0 -9
  141. package/dist/src/Evolu/Kysely.d.ts +0 -6
  142. package/dist/src/Evolu/Kysely.d.ts.map +0 -1
  143. package/dist/src/Evolu/Kysely.js +0 -21
  144. package/dist/src/NanoId.d.ts +0 -27
  145. package/dist/src/NanoId.d.ts.map +0 -1
  146. package/dist/src/NanoId.js +0 -6
  147. package/dist/src/Promise.d.ts +0 -180
  148. package/dist/src/Promise.d.ts.map +0 -1
  149. package/dist/src/Promise.js +0 -176
  150. package/src/Callbacks.ts +0 -43
  151. package/src/Evolu/Config.ts +0 -83
  152. package/src/Evolu/Kysely.ts +0 -38
  153. package/src/NanoId.ts +0 -39
  154. package/src/Promise.ts +0 -295
@@ -1,97 +1,38 @@
1
- import { pack } from "msgpackr";
1
+ import * as Kysely from "kysely";
2
2
  import { mapObject, objectToEntries } from "../Object.js";
3
- import { err, ok } from "../Result.js";
4
- import { SqliteBoolean } from "../Sqlite.js";
5
- import { brand, createTypeErrorFormatter, DateIsoString, EvoluType, nullableToOptional, nullOr, object, omit, optional, record, Unknown, } from "../Type.js";
6
- import { createIndexes } from "./Kysely.js";
7
- import { Base64Url256, } from "./Protocol.js";
8
- export const DefaultColumns = object({
9
- createdAt: DateIsoString,
10
- updatedAt: DateIsoString,
11
- isDeleted: nullOr(SqliteBoolean),
12
- });
13
- const isDefaultColumnName = (value) => value === "createdAt" || value === "updatedAt" || value === "isDeleted";
14
- /**
15
- * Valid {@link EvoluSchema}.
16
- *
17
- * - Table and column names must be Base64Url strings.
18
- * - Each table must include an `id` column of type {@link Id}.
19
- * - Default column names (`createdAt`, `updatedAt`, `isDeleted`) are not allowed.
20
- */
21
- export const ValidEvoluSchema = brand("ValidEvoluSchema", record(Base64Url256, object({ id: EvoluType }, record(Base64Url256, Unknown))), (value) => {
22
- for (const tableName in value) {
23
- for (const columnName in value[tableName]) {
24
- if (isDefaultColumnName(columnName)) {
25
- return err({
26
- type: "ValidEvoluSchema",
27
- value,
28
- reason: {
29
- kind: "DefaultColumnError",
30
- tableName,
31
- columnName,
32
- },
33
- });
34
- }
35
- }
36
- }
37
- return ok(value);
38
- });
39
- /**
40
- * Asserts that the given value is {@link ValidEvoluSchema}.
41
- *
42
- * Throws an error if the value is not a valid Evolu schema.
43
- */
44
- export const assertValidEvoluSchema = (value) => {
45
- const validEvoluSchema = ValidEvoluSchema.fromUnknown(value);
46
- if (!validEvoluSchema.ok) {
47
- const message = formatValidEvoluSchemaError(validEvoluSchema.error);
48
- throw new Error(`Invalid Evolu schema: ${message}`);
49
- }
50
- return validEvoluSchema.value;
51
- };
52
- const formatValidEvoluSchemaError = (error) => {
53
- if (error.type === "Record") {
54
- if (error.reason.kind === "Key") {
55
- return `The table "${error.reason.key}" has invalid name. A table name must be Base64Url256 string (A-Z, a-z, 0-9, -, _).`;
56
- }
57
- if (error.reason.kind === "Value" &&
58
- error.reason.error.reason.kind === "Props" &&
59
- error.reason.error.reason.errors.id?.type === "EvoluType") {
60
- return `The table "${error.reason.key}" has invalid ID column. Check examples.`;
61
- }
62
- if (error.reason.kind === "Value" &&
63
- error.reason.error.reason.kind === "IndexKey") {
64
- return `The table "${error.reason.key}" has invalid column name "${error.reason.error.reason.key}". A column name must be Base64Url256 string (A-Z, a-z, 0-9, -, _).`;
65
- }
66
- }
67
- if (error.type === "ValidEvoluSchema") {
68
- return `The table "${error.reason.tableName}" uses reserved column name "${error.reason.columnName}". Reserved column names are: createdAt, updatedAt, isDeleted.`;
69
- }
70
- return JSON.stringify(error, null, 2);
71
- };
72
- export const validEvoluSchemaToDbSchema = (validEvoluSchema, indexes) => {
73
- const tables = objectToEntries(validEvoluSchema).map(([tableName, table]) => ({
3
+ import { ok } from "../Result.js";
4
+ import { sql, SqliteBoolean, } from "../Sqlite.js";
5
+ import { array, DateIso, nullableToOptional, nullOr, object, omit, optional, String, validMutationSize, } from "../Type.js";
6
+ export const evoluSchemaToDbSchema = (schema, indexesConfig) => {
7
+ const tables = objectToEntries(schema).map(([tableName, table]) => ({
74
8
  name: tableName,
75
9
  columns: objectToEntries(table)
76
10
  .filter(([k]) => k !== "id")
77
11
  .map(([k]) => k),
78
12
  }));
79
- return {
80
- tables,
81
- indexes: createIndexes(indexes),
82
- };
13
+ const indexes = indexesConfig
14
+ ? indexesConfig(createIndex).map((index) => ({
15
+ name: index.toOperationNode().name.name,
16
+ sql: index.compile().sql,
17
+ }))
18
+ : [];
19
+ return { tables, indexes };
83
20
  };
84
21
  /**
85
- * Evolu has to limit the maximum mutation size. Otherwise, sync couldn't use
86
- * the {@link maxProtocolMessageRangesSize}. The max size is 640KB in bytes,
87
- * measured via MessagePack. Evolu Protocol DbChange will be smaller thanks to
88
- * various optimizations.
22
+ * Default columns automatically added to all tables.
23
+ *
24
+ * - `createdAt`: Set by Evolu when `insert` is called, or can be custom with
25
+ * `upsert`.
26
+ * - `updatedAt`: Always set by Evolu, derived from {@link CrdtMessage} timestamp.
27
+ * If you defer sync to avoid leaking time activity, use a custom column to
28
+ * preserve real update time.
29
+ * - `isDeleted`: Soft delete flag.
89
30
  */
90
- export const maxMutationSize = 655360;
91
- const validMutationSize = (type) => brand("ValidMutationSize", type, (value) => pack(value).byteLength <= maxMutationSize
92
- ? ok(value)
93
- : err({ type: "ValidMutationSize", value }));
94
- export const formatValidMutationSizeError = createTypeErrorFormatter((error) => `The mutation size exceeds the maximum limit of ${maxMutationSize} bytes. The provided mutation has a size of ${pack(error.value).byteLength} bytes.`);
31
+ export const DefaultColumns = object({
32
+ createdAt: DateIso,
33
+ updatedAt: DateIso,
34
+ isDeleted: nullOr(SqliteBoolean),
35
+ });
95
36
  /**
96
37
  * Type Factory to create insertable {@link Type}. It makes nullable Types
97
38
  * optional, omits Id, and ensures the {@link maxMutationSize}.
@@ -135,16 +76,155 @@ export const updateable = (props) => {
135
76
  return validMutationSize(object(updateableProps));
136
77
  };
137
78
  /**
138
- * Type Factory to create upsertable Type. It makes nullable Types optional and
139
- * ensures the {@link maxMutationSize}.
79
+ * Type Factory to create upsertable Type. It makes nullable Types optional,
80
+ * includes optional default columns (createdAt, isDeleted), and ensures the
81
+ * {@link maxMutationSize}.
140
82
  *
141
83
  * ### Example
142
84
  *
143
85
  * ```ts
144
86
  * const UpsertableTodo = upsertable(Schema.todo);
145
87
  * type UpsertableTodo = typeof UpsertableTodo.Type;
146
- * const todo = UpsertableTodo.from({ id, title });
88
+ * const todo = UpsertableTodo.from({
89
+ * id,
90
+ * title,
91
+ * createdAt: "2023-01-01T00:00:00.000Z",
92
+ * });
147
93
  * if (!todo.ok) return; // handle errors
148
94
  * ```
149
95
  */
150
- export const upsertable = (props) => validMutationSize(nullableToOptional(props));
96
+ export const upsertable = (props) => {
97
+ const propsWithDefaults = {
98
+ ...props,
99
+ createdAt: optional(DateIso),
100
+ isDeleted: optional(SqliteBoolean),
101
+ };
102
+ return validMutationSize(nullableToOptional(propsWithDefaults));
103
+ };
104
+ export const DbTable = object({
105
+ name: String,
106
+ columns: array(String),
107
+ });
108
+ export const DbIndex = object({ name: String, sql: String });
109
+ export const DbSchema = object({
110
+ tables: array(DbTable),
111
+ indexes: array(DbIndex),
112
+ });
113
+ /** Get the current database schema by reading SQLite metadata. */
114
+ export const getDbSchema = (deps) => ({ allIndexes = false } = {}) => {
115
+ const map = new Map();
116
+ const tableAndColumnInfoRows = deps.sqlite.exec(sql `
117
+ select
118
+ sqlite_master.name as tableName,
119
+ table_info.name as columnName
120
+ from
121
+ sqlite_master
122
+ join pragma_table_info(sqlite_master.name) as table_info;
123
+ `);
124
+ if (!tableAndColumnInfoRows.ok)
125
+ return tableAndColumnInfoRows;
126
+ tableAndColumnInfoRows.value.rows.forEach((row) => {
127
+ const { tableName, columnName } = row;
128
+ if (!map.has(tableName))
129
+ map.set(tableName, []);
130
+ map.get(tableName)?.push(columnName);
131
+ });
132
+ const tables = Array.from(map, ([name, columns]) => ({ name, columns }));
133
+ const indexesRows = deps.sqlite.exec(allIndexes
134
+ ? sql `
135
+ select name, sql
136
+ from sqlite_master
137
+ where type = 'index' and name not like 'sqlite_%';
138
+ `
139
+ : sql `
140
+ select name, sql
141
+ from sqlite_master
142
+ where
143
+ type = 'index'
144
+ and name not like 'sqlite_%'
145
+ and name not like 'evolu_%';
146
+ `);
147
+ if (!indexesRows.ok)
148
+ return indexesRows;
149
+ const indexes = indexesRows.value.rows.map((row) => ({
150
+ name: row.name,
151
+ /**
152
+ * SQLite returns "CREATE INDEX" for "create index" for some reason.
153
+ * Other keywords remain unchanged. We have to normalize the casing for
154
+ * {@link indexesAreEqual} manually.
155
+ */
156
+ sql: row.sql
157
+ .replace("CREATE INDEX", "create index")
158
+ .replace("CREATE UNIQUE INDEX", "create unique index"),
159
+ }));
160
+ return ok({ tables, indexes });
161
+ };
162
+ const indexesAreEqual = (self, that) => self.name === that.name && self.sql === that.sql;
163
+ export const ensureDbSchema = (deps) => (newSchema, currentSchema, options) => {
164
+ const queries = [];
165
+ newSchema.tables.forEach((newTable) => {
166
+ const currentTable = currentSchema.tables.find((t) => t.name === newTable.name);
167
+ if (!currentTable) {
168
+ queries.push({
169
+ sql: createTableWithDefaultColumns(newTable.name, newTable.columns),
170
+ parameters: [],
171
+ });
172
+ }
173
+ else {
174
+ newTable.columns
175
+ .filter((newColumn) => !currentTable.columns.includes(newColumn))
176
+ .forEach((newColumn) => {
177
+ queries.push(sql `
178
+ alter table ${sql.identifier(newTable.name)}
179
+ add column ${sql.identifier(newColumn)} blob;
180
+ `);
181
+ });
182
+ }
183
+ });
184
+ if (options?.ignoreIndexes !== true) {
185
+ // Remove current indexes that are not in the newSchema.
186
+ currentSchema.indexes
187
+ .filter((currentIndex) => !newSchema.indexes.some((newIndex) => indexesAreEqual(newIndex, currentIndex)))
188
+ .forEach((index) => {
189
+ queries.push(sql `drop index ${sql.identifier(index.name)};`);
190
+ });
191
+ // Add new indexes that are not in the currentSchema.
192
+ newSchema.indexes
193
+ .filter((newIndex) => !currentSchema.indexes.some((currentIndex) => indexesAreEqual(newIndex, currentIndex)))
194
+ .forEach((newIndex) => {
195
+ queries.push({ sql: `${newIndex.sql};`, parameters: [] });
196
+ });
197
+ }
198
+ for (const query of queries) {
199
+ const result = deps.sqlite.exec(query);
200
+ if (!result.ok)
201
+ return result;
202
+ }
203
+ return ok();
204
+ };
205
+ const createTableWithDefaultColumns = (tableName, columns) => `
206
+ create table ${sql.identifier(tableName).sql} (
207
+ "id" text primary key,
208
+ ${columns
209
+ // Add default columns.
210
+ .concat(["createdAt", "updatedAt", "isDeleted"])
211
+ .filter((c) => c !== "id")
212
+ // "A column with affinity BLOB does not prefer one storage class over another
213
+ // and no attempt is made to coerce data from one storage class into another."
214
+ // https://www.sqlite.org/datatype3.html
215
+ .map((name) => `${sql.identifier(name).sql} blob`)
216
+ .join(", ")}
217
+ );
218
+ `;
219
+ // https://kysely.dev/docs/recipes/splitting-query-building-and-execution
220
+ export const kysely = new Kysely.Kysely({
221
+ dialect: {
222
+ createAdapter: () => new Kysely.SqliteAdapter(),
223
+ createDriver: () => new Kysely.DummyDriver(),
224
+ createIntrospector() {
225
+ throw new Error("Not implemeneted");
226
+ },
227
+ createQueryCompiler: () => new Kysely.SqliteQueryCompiler(),
228
+ },
229
+ });
230
+ const createIndex = kysely.schema.createIndex.bind(kysely.schema);
@@ -1,10 +1,155 @@
1
+ import { NonEmptyReadonlyArray } from "../Array.js";
2
+ import { Brand } from "../Brand.js";
3
+ import { RandomDep } from "../Random.js";
4
+ import { Result } from "../Result.js";
5
+ import { SqliteDep, SqliteError } from "../Sqlite.js";
6
+ import { NonNegativeInt } from "../Type.js";
7
+ import { OwnerIdBytes, OwnerWriteKey } from "./Owner.js";
8
+ import { Timestamp, TimestampBytes } from "./Timestamp.js";
9
+ /**
10
+ * Evolu Storage
11
+ *
12
+ * The protocol using Storage is agnostic to storage implementation details—any
13
+ * storage can be plugged in, as long as it implements this interface.
14
+ * Implementations must handle their own errors; return values only indicate
15
+ * overall success or failure.
16
+ *
17
+ * The Storage API is synchronous because SQLite's synchronous API is the
18
+ * fastest way to use SQLite. Synchronous bindings (like better-sqlite3) call
19
+ * SQLite's C API directly with no context switching between the event loop and
20
+ * native code, and no promise microtasks or await overhead.
21
+ *
22
+ * The only exception is {@link Storage#writeMessages}, which is async to allow
23
+ * for async validation logic before writing to storage. The write operation
24
+ * itself remains synchronous.
25
+ */
26
+ export interface Storage {
27
+ readonly getSize: (ownerId: OwnerIdBytes) => NonNegativeInt | null;
28
+ readonly fingerprint: (ownerId: OwnerIdBytes, begin: NonNegativeInt, end: NonNegativeInt) => Fingerprint | null;
29
+ /**
30
+ * Computes fingerprints with their upper bounds in one call.
31
+ *
32
+ * This function can be replaced with many fingerprint/findLowerBound calls,
33
+ * but implementations can leverage it for batching and more efficient
34
+ * fingerprint computation.
35
+ */
36
+ readonly fingerprintRanges: (ownerId: OwnerIdBytes, buckets: ReadonlyArray<NonNegativeInt>, upperBound?: RangeUpperBound) => ReadonlyArray<FingerprintRange> | null;
37
+ readonly findLowerBound: (ownerId: OwnerIdBytes, begin: NonNegativeInt, end: NonNegativeInt, upperBound: RangeUpperBound) => NonNegativeInt | null;
38
+ readonly iterate: (ownerId: OwnerIdBytes, begin: NonNegativeInt, end: NonNegativeInt, callback: (timestamp: TimestampBytes, index: NonNegativeInt) => boolean) => void;
39
+ /**
40
+ * Validates the {@link OwnerWriteKey} for the given {@link Owner}.
41
+ *
42
+ * Returns `true` if the write key is valid, `false` otherwise.
43
+ */
44
+ readonly validateWriteKey: (ownerId: OwnerIdBytes, writeKey: OwnerWriteKey) => boolean;
45
+ /** Sets the {@link OwnerWriteKey} for the given {@link Owner}. */
46
+ readonly setWriteKey: (ownerId: OwnerIdBytes, writeKey: OwnerWriteKey) => boolean;
47
+ /**
48
+ * Write encrypted {@link CrdtMessage}s to storage.
49
+ *
50
+ * Must use a mutex (per ownerId on Relay) to ensure sequential processing and
51
+ * proper protocol logic handling during sync operations.
52
+ *
53
+ * Returns `true` on success, `false` on failure.
54
+ */
55
+ readonly writeMessages: (ownerId: OwnerIdBytes, messages: NonEmptyReadonlyArray<EncryptedCrdtMessage>) => Promise<boolean>;
56
+ /** Read encrypted {@link DbChange}s from storage. */
57
+ readonly readDbChange: (ownerId: OwnerIdBytes, timestamp: TimestampBytes) => EncryptedDbChange | null;
58
+ /**
59
+ * Delete all data for the given {@link Owner}.
60
+ *
61
+ * Returns `true` on success, `false` on failure.
62
+ */
63
+ readonly deleteOwner: (ownerId: OwnerIdBytes) => boolean;
64
+ }
65
+ export interface StorageDep {
66
+ readonly storage: Storage;
67
+ }
68
+ /**
69
+ * A cryptographic hash used for efficiently comparing collections of
70
+ * {@link TimestampBytes}s.
71
+ *
72
+ * It consists of the first {@link fingerprintSize} bytes of the SHA-256 hash of
73
+ * one or more timestamps.
74
+ */
75
+ export type Fingerprint = Uint8Array & Brand<"Fingerprint">;
76
+ export declare const fingerprintSize: NonNegativeInt;
77
+ /** A fingerprint of an empty range. */
78
+ export declare const zeroFingerprint: Fingerprint;
79
+ export interface BaseRange {
80
+ readonly upperBound: RangeUpperBound;
81
+ }
82
+ /**
83
+ * Union type for Range's upperBound: either a {@link TimestampBytes} or
84
+ * {@link InfiniteUpperBound}.
85
+ */
86
+ export type RangeUpperBound = TimestampBytes | InfiniteUpperBound;
87
+ export declare const InfiniteUpperBound: unique symbol;
88
+ export type InfiniteUpperBound = typeof InfiniteUpperBound;
89
+ export declare const RangeType: {
90
+ readonly Fingerprint: 1;
91
+ readonly Skip: 0;
92
+ readonly Timestamps: 2;
93
+ };
94
+ export type RangeType = (typeof RangeType)[keyof typeof RangeType];
95
+ export interface SkipRange extends BaseRange {
96
+ readonly type: typeof RangeType.Skip;
97
+ }
98
+ export interface FingerprintRange extends BaseRange {
99
+ readonly type: typeof RangeType.Fingerprint;
100
+ readonly fingerprint: Fingerprint;
101
+ }
102
+ export interface TimestampsRange extends BaseRange {
103
+ readonly type: typeof RangeType.Timestamps;
104
+ readonly timestamps: ReadonlyArray<TimestampBytes>;
105
+ }
106
+ export type Range = SkipRange | FingerprintRange | TimestampsRange;
107
+ /** An encrypted {@link CrdtMessage}. */
108
+ export interface EncryptedCrdtMessage {
109
+ readonly timestamp: Timestamp;
110
+ readonly change: EncryptedDbChange;
111
+ }
112
+ /** Encrypted DbChange */
113
+ export type EncryptedDbChange = Uint8Array & Brand<"EncryptedDbChange">;
1
114
  /**
2
- * Evolu Storage for SQLite
115
+ * A CRDT message combining a unique {@link Timestamp} with a {@link DbChange}.
116
+ *
117
+ * Used in Evolu's sync protocol to replicate data changes across devices. Evolu
118
+ * operates as a durable queue, providing exactly-once delivery guarantees for
119
+ * reliable synchronization across application restarts and network failures.
120
+ */
121
+ export interface CrdtMessage {
122
+ readonly timestamp: Timestamp;
123
+ readonly change: DbChange;
124
+ }
125
+ /**
126
+ * A DbChange is a change to a table row. Together with a unique
127
+ * {@link Timestamp}, it forms a {@link CrdtMessage}.
128
+ */
129
+ export declare const DbChange: import("../Type.js").ObjectType<{
130
+ table: import("../Type.js").Type<"String", string, string, import("../Type.js").StringError, string, import("../Type.js").StringError>;
131
+ id: import("../Type.js").BrandType<import("../Type.js").Type<"String", string, string, import("../Type.js").StringError, string, import("../Type.js").StringError>, "Id", import("../Type.js").IdError, import("../Type.js").StringError>;
132
+ values: import("../Type.js").RecordType<"String", string, string, import("../Type.js").StringError, string, import("../Type.js").StringError, import("../Type.js").UnionType<[import("../Type.js").Type<"Null", null, null, import("../Type.js").NullError, null, import("../Type.js").NullError>, import("../Type.js").Type<"String", string, string, import("../Type.js").StringError, string, import("../Type.js").StringError>, import("../Type.js").Type<"Number", number, number, import("../Type.js").NumberError, number, import("../Type.js").NumberError>, import("../Type.js").Type<"Uint8Array", Uint8Array<ArrayBufferLike>, Uint8Array<ArrayBufferLike>, import("../Type.js").Uint8ArrayError, Uint8Array<ArrayBufferLike>, import("../Type.js").Uint8ArrayError>]>>;
133
+ }>;
134
+ export type DbChange = typeof DbChange.Type;
135
+ /**
136
+ * Common interface for both client and relay SQLite storages.
3
137
  *
4
138
  * Evolu uses a Skiplist, which leverages SQLite indexes. The core logic is
5
139
  * implemented in SQL, so it doesn't have to make roundtrips to the DB.
6
140
  *
7
- * The ideal storage for a Relay should use a similar architecture to
141
+ * While the SQL implementation may look sophisticated, it's conceptually simple
142
+ * and LLMs can explain how it works. The Skiplist data structure is well
143
+ * explained in [this Stack Overflow
144
+ * answer](https://stackoverflow.com/questions/61944198/what-is-a-zip-tree-and-how-does-it-work).
145
+ * The logic resembles [Negentropy's C++
146
+ * storage](https://github.com/hoytech/negentropy), except we use a Skiplist to
147
+ * leverage SQLite indexes, which makes the code simpler.
148
+ *
149
+ * Note: A paid review by the SQLite team is planned, as they use the same
150
+ * algorithm for their rsync tool.
151
+ *
152
+ * The ideal storage for a Relay should use an architecture like
8
153
  * [strfry](https://github.com/hoytech/strfry) (a KV storage), but with Skiplist
9
154
  * to ensure that insertion order doesn't matter (local-first apps can often
10
155
  * write in the past.)
@@ -19,31 +164,31 @@
19
164
  * each other, if necessary. One relay should handle hundreds of thousands of
20
165
  * users, and when it goes down, nothing happens, because it will be
21
166
  * synchronized later.
22
- *
23
- * @module
24
167
  */
25
- import { RandomDep } from "../Random.js";
26
- import { Result } from "../Result.js";
27
- import { SqliteDep, SqliteError } from "../Sqlite.js";
28
- import { NonNegativeInt } from "../Type.js";
29
- import { BinaryOwnerId, Storage } from "./Protocol.js";
30
- import { BinaryTimestamp } from "./Timestamp.js";
31
- /** Common interface for both client and relay SQLite storages. */
32
168
  export interface SqliteStorageBase {
33
- readonly insertTimestamp: (ownerId: BinaryOwnerId, timestamp: BinaryTimestamp) => Result<void, SqliteError>;
169
+ /**
170
+ * Inserts a timestamp for an owner into the skiplist-based storage.
171
+ *
172
+ * Must be idempotent - inserting the same timestamp multiple times has no
173
+ * effect after the first insertion. This is crucial for sync reliability as
174
+ * messages may be received and processed multiple times.
175
+ */
176
+ readonly insertTimestamp: (ownerId: OwnerIdBytes, timestamp: TimestampBytes) => Result<void, SqliteError>;
34
177
  readonly getSize: Storage["getSize"];
35
178
  readonly fingerprint: Storage["fingerprint"];
36
179
  readonly fingerprintRanges: Storage["fingerprintRanges"];
37
180
  readonly findLowerBound: Storage["findLowerBound"];
38
181
  readonly iterate: Storage["iterate"];
182
+ readonly deleteOwner: Storage["deleteOwner"];
39
183
  }
40
184
  export interface SqliteStorageBaseDep {
41
185
  readonly storage: SqliteStorageBase;
42
186
  }
43
- export type SqliteStorageDeps = SqliteDep & RandomDep;
187
+ export type SqliteStorageDeps = RandomDep & SqliteDep;
44
188
  export interface CreateSqliteStorageBaseOptions {
45
189
  onStorageError: (error: SqliteError) => void;
46
190
  }
47
191
  export declare const createSqliteStorageBase: (deps: SqliteStorageDeps) => (options: CreateSqliteStorageBaseOptions) => Result<SqliteStorageBase, SqliteError>;
48
- export declare const getTimestampByIndex: (deps: SqliteDep) => (ownerId: BinaryOwnerId, index: NonNegativeInt) => Result<BinaryTimestamp, SqliteError>;
192
+ export declare const timestampBytesToFingerprint: (timestamp: TimestampBytes) => Fingerprint;
193
+ export declare const getTimestampByIndex: (deps: SqliteDep) => (ownerId: OwnerIdBytes, index: NonNegativeInt) => Result<TimestampBytes, SqliteError>;
49
194
  //# sourceMappingURL=Storage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Storage.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAM,MAAM,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAO,SAAS,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAe,cAAc,EAAe,MAAM,YAAY,CAAC;AAGtE,OAAO,EACL,aAAa,EAQb,OAAO,EAER,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,eAAe,EAAwB,MAAM,gBAAgB,CAAC;AAEvE,kEAAkE;AAClE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,eAAe,EAAE,CACxB,OAAO,EAAE,aAAa,EACtB,SAAS,EAAE,eAAe,KACvB,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IAE/B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;IACrC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;IAC7C,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACzD,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACnD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;CACrC;AAED,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,SAAS,CAAC;AAEtD,MAAM,WAAW,8BAA8B;IAC7C,cAAc,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;CAC9C;AAED,eAAO,MAAM,uBAAuB,GACjC,MAAM,iBAAiB,MAEtB,SAAS,8BAA8B,KACtC,MAAM,CAAC,iBAAiB,EAAE,WAAW,CAyIvC,CAAC;AA09BJ,eAAO,MAAM,mBAAmB,GAC7B,MAAM,SAAS,MAEd,SAAS,aAAa,EACtB,OAAO,cAAc,KACpB,MAAM,CAAC,eAAe,EAAE,WAAW,CA6ErC,CAAC"}
1
+ {"version":3,"file":"Storage.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Storage.ts"],"names":[],"mappings":"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"}
@@ -1,33 +1,29 @@
1
- /**
2
- * Evolu Storage for SQLite
3
- *
4
- * Evolu uses a Skiplist, which leverages SQLite indexes. The core logic is
5
- * implemented in SQL, so it doesn't have to make roundtrips to the DB.
6
- *
7
- * The ideal storage for a Relay should use a similar architecture to
8
- * [strfry](https://github.com/hoytech/strfry) (a KV storage), but with Skiplist
9
- * to ensure that insertion order doesn't matter (local-first apps can often
10
- * write in the past.)
11
- *
12
- * The ideal client implementation should probably use the SQLite extension
13
- * instead of SQL or even a KV storage, when such a thing for browsers/native
14
- * will exist and will be faster than SQLite.
15
- *
16
- * # Scaling
17
- *
18
- * The load can be distributed by deploying multiple relays, synchronized with
19
- * each other, if necessary. One relay should handle hundreds of thousands of
20
- * users, and when it goes down, nothing happens, because it will be
21
- * synchronized later.
22
- *
23
- * @module
24
- */
1
+ import { sha256 } from "@noble/hashes/sha2.js";
25
2
  import { assert } from "../Assert.js";
26
3
  import { decrement } from "../Number.js";
27
4
  import { ok } from "../Result.js";
28
- import { sql } from "../Sqlite.js";
29
- import { binaryOwnerIdToOwnerId, binaryTimestampToFingerprint, InfiniteUpperBound, RangeType, zeroFingerprint, } from "./Protocol.js";
30
- import { orderBinaryTimestamp } from "./Timestamp.js";
5
+ import { sql, SqliteValue } from "../Sqlite.js";
6
+ import { Id, object, record, String, } from "../Type.js";
7
+ import { ownerIdBytesToOwnerId, } from "./Owner.js";
8
+ import { orderTimestampBytes } from "./Timestamp.js";
9
+ export const fingerprintSize = 12;
10
+ /** A fingerprint of an empty range. */
11
+ export const zeroFingerprint = new Uint8Array(fingerprintSize);
12
+ export const InfiniteUpperBound = Symbol("InfiniteUpperBound");
13
+ export const RangeType = {
14
+ Fingerprint: 1,
15
+ Skip: 0,
16
+ Timestamps: 2,
17
+ };
18
+ /**
19
+ * A DbChange is a change to a table row. Together with a unique
20
+ * {@link Timestamp}, it forms a {@link CrdtMessage}.
21
+ */
22
+ export const DbChange = object({
23
+ table: String,
24
+ id: Id,
25
+ values: record(String, SqliteValue),
26
+ });
31
27
  export const createSqliteStorageBase = (deps) => (options) => {
32
28
  const createTablesResult = createTables(deps);
33
29
  if (!createTablesResult.ok)
@@ -35,7 +31,7 @@ export const createSqliteStorageBase = (deps) => (options) => {
35
31
  const ownerStats = new Map();
36
32
  return ok({
37
33
  insertTimestamp: (ownerId, timestamp) => {
38
- const ownerIdString = binaryOwnerIdToOwnerId(ownerId);
34
+ const ownerIdString = ownerIdBytesToOwnerId(ownerId);
39
35
  const level = randomSkiplistLevel(deps);
40
36
  let stats = ownerStats.get(ownerIdString);
41
37
  if (!stats) {
@@ -53,11 +49,11 @@ export const createSqliteStorageBase = (deps) => (options) => {
53
49
  ownerStats.set(ownerIdString, stats);
54
50
  }
55
51
  let strategy;
56
- if (orderBinaryTimestamp(timestamp, stats.maxT) === 1) {
52
+ if (orderTimestampBytes(timestamp, stats.maxT) === 1) {
57
53
  strategy = "append";
58
54
  stats.maxT = timestamp;
59
55
  }
60
- else if (orderBinaryTimestamp(timestamp, stats.minT) === -1) {
56
+ else if (orderTimestampBytes(timestamp, stats.minT) === -1) {
61
57
  strategy = "prepend";
62
58
  stats.minT = timestamp;
63
59
  }
@@ -141,6 +137,16 @@ export const createSqliteStorageBase = (deps) => (options) => {
141
137
  return;
142
138
  }
143
139
  },
140
+ deleteOwner: (ownerId) => {
141
+ const result = deps.sqlite.exec(sql `
142
+ delete from evolu_timestamp where ownerId = ${ownerId};
143
+ `);
144
+ if (!result.ok) {
145
+ options.onStorageError(result.error);
146
+ return false;
147
+ }
148
+ return true;
149
+ },
144
150
  });
145
151
  };
146
152
  const assertBeginEnd = (begin, end) => {
@@ -159,7 +165,7 @@ const createTables = (deps) => {
159
165
  *
160
166
  * Columns:
161
167
  *
162
- * - `t` – globally unique binary timestamp
168
+ * - `t` – TimestampBytes
163
169
  * - `h1`/`h2` – 12-byte fingerprint split into two integers for fast XOR
164
170
  * - `c` – incremental count
165
171
  * - `l` – Skiplist level (1 to 32)
@@ -208,7 +214,7 @@ const createTables = (deps) => {
208
214
  // they will prepend. They are always sorted in ascending order by the
209
215
  // Protocol.
210
216
  const insertTimestamp = (deps) => (ownerId, timestamp, level, strategy) => {
211
- const [h1, h2] = fingerprintToSqliteFingerprint(binaryTimestampToFingerprint(timestamp));
217
+ const [h1, h2] = fingerprintToSqliteFingerprint(timestampBytesToFingerprint(timestamp));
212
218
  let queries = [];
213
219
  switch (strategy) {
214
220
  case "append":
@@ -717,6 +723,10 @@ const insertTimestamp = (deps) => (ownerId, timestamp, level, strategy) => {
717
723
  }
718
724
  return ok();
719
725
  };
726
+ export const timestampBytesToFingerprint = (timestamp) => {
727
+ const hash = sha256(timestamp).slice(0, fingerprintSize);
728
+ return hash;
729
+ };
720
730
  /**
721
731
  * Generates a random skiplist level in the range [1, skiplistMaxLevel].
722
732
  * Probabilistic approach avoids the need for explicit tree balancing.