@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.
- package/dist/src/Assert.d.ts +6 -3
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +7 -4
- package/dist/src/Brand.d.ts +75 -0
- package/dist/src/Brand.d.ts.map +1 -0
- package/dist/src/Brand.js +1 -0
- package/dist/src/Buffer.d.ts +1 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +1 -1
- package/dist/src/CallbackRegistry.d.ts +53 -0
- package/dist/src/CallbackRegistry.d.ts.map +1 -0
- package/dist/src/CallbackRegistry.js +25 -0
- package/dist/src/Console.d.ts +31 -6
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +72 -9
- package/dist/src/Crypto.d.ts +61 -34
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +29 -42
- package/dist/src/Evolu/Db.d.ts +137 -66
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +300 -705
- package/dist/src/Evolu/Diff.d.ts +3 -3
- package/dist/src/Evolu/Diff.d.ts.map +1 -1
- package/dist/src/Evolu/Diff.js +7 -5
- package/dist/src/Evolu/Evolu.d.ts +200 -115
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +287 -157
- package/dist/src/Evolu/Internal.d.ts +0 -2
- package/dist/src/Evolu/Internal.d.ts.map +1 -1
- package/dist/src/Evolu/Internal.js +0 -2
- package/dist/src/Evolu/LocalAuth.d.ts +144 -0
- package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
- package/dist/src/Evolu/LocalAuth.js +171 -0
- package/dist/src/Evolu/Owner.d.ts +152 -127
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +88 -107
- package/dist/src/Evolu/Platform.d.ts +9 -7
- package/dist/src/Evolu/Platform.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.d.ts +170 -186
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +487 -343
- package/dist/src/Evolu/Public.d.ts +6 -8
- package/dist/src/Evolu/Public.d.ts.map +1 -1
- package/dist/src/Evolu/Public.js +2 -3
- package/dist/src/Evolu/PublicKysely.js +3 -3
- package/dist/src/Evolu/Query.d.ts +2 -1
- package/dist/src/Evolu/Query.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.d.ts +4 -3
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +50 -12
- package/dist/src/Evolu/Schema.d.ts +129 -73
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +169 -89
- package/dist/src/Evolu/Storage.d.ts +159 -14
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +42 -32
- package/dist/src/Evolu/Sync.d.ts +77 -13
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +453 -20
- package/dist/src/Evolu/Timestamp.d.ts +35 -27
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +27 -18
- package/dist/src/ManyToManyMap.d.ts +74 -10
- package/dist/src/ManyToManyMap.d.ts.map +1 -1
- package/dist/src/ManyToManyMap.js +41 -6
- package/dist/src/Number.d.ts +2 -1
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Random.d.ts +3 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/RefCountedResourceManager.d.ts +119 -0
- package/dist/src/RefCountedResourceManager.d.ts.map +1 -0
- package/dist/src/RefCountedResourceManager.js +197 -0
- package/dist/src/Result.d.ts +147 -51
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +7 -241
- package/dist/src/Sqlite.d.ts +21 -4
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +50 -8
- package/dist/src/Task.d.ts +511 -0
- package/dist/src/Task.d.ts.map +1 -0
- package/dist/src/Task.js +410 -0
- package/dist/src/Time.d.ts +59 -0
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +87 -4
- package/dist/src/Type.d.ts +462 -327
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +490 -436
- package/dist/src/Types.d.ts +0 -74
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/WebSocket.d.ts +5 -2
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +12 -13
- package/dist/src/Worker.d.ts +39 -11
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +22 -4
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -3
- package/package.json +14 -13
- package/src/Assert.ts +8 -7
- package/src/Brand.ts +75 -0
- package/src/Buffer.ts +1 -1
- package/src/CallbackRegistry.ts +84 -0
- package/src/Console.ts +91 -11
- package/src/Crypto.ts +94 -79
- package/src/Evolu/Db.ts +505 -1027
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +682 -329
- package/src/Evolu/Internal.ts +0 -2
- package/src/Evolu/LocalAuth.ts +422 -0
- package/src/Evolu/Owner.ts +221 -235
- package/src/Evolu/Platform.ts +9 -9
- package/src/Evolu/Protocol.ts +681 -614
- package/src/Evolu/Public.ts +7 -14
- package/src/Evolu/PublicKysely.ts +3 -3
- package/src/Evolu/Query.ts +2 -1
- package/src/Evolu/Relay.ts +62 -16
- package/src/Evolu/Schema.ts +391 -191
- package/src/Evolu/Storage.ts +275 -55
- package/src/Evolu/Sync.ts +758 -37
- package/src/Evolu/Timestamp.ts +38 -35
- package/src/ManyToManyMap.ts +127 -24
- package/src/Number.ts +2 -6
- package/src/Random.ts +3 -2
- package/src/RefCountedResourceManager.ts +368 -0
- package/src/Result.ts +152 -53
- package/src/Sqlite.ts +59 -23
- package/src/Task.ts +779 -0
- package/src/Time.ts +168 -4
- package/src/Type.ts +696 -660
- package/src/Types.ts +0 -76
- package/src/WebSocket.ts +23 -17
- package/src/Worker.ts +72 -23
- package/src/index.ts +3 -3
- package/dist/src/Callbacks.d.ts +0 -20
- package/dist/src/Callbacks.d.ts.map +0 -1
- package/dist/src/Callbacks.js +0 -18
- package/dist/src/Evolu/Config.d.ts +0 -69
- package/dist/src/Evolu/Config.d.ts.map +0 -1
- package/dist/src/Evolu/Config.js +0 -9
- package/dist/src/Evolu/Kysely.d.ts +0 -6
- package/dist/src/Evolu/Kysely.d.ts.map +0 -1
- package/dist/src/Evolu/Kysely.js +0 -21
- package/dist/src/NanoId.d.ts +0 -27
- package/dist/src/NanoId.d.ts.map +0 -1
- package/dist/src/NanoId.js +0 -6
- package/dist/src/Promise.d.ts +0 -180
- package/dist/src/Promise.d.ts.map +0 -1
- package/dist/src/Promise.js +0 -176
- package/src/Callbacks.ts +0 -43
- package/src/Evolu/Config.ts +0 -83
- package/src/Evolu/Kysely.ts +0 -38
- package/src/NanoId.ts +0 -39
- package/src/Promise.ts +0 -295
package/dist/src/Evolu/Schema.js
CHANGED
|
@@ -1,97 +1,38 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as Kysely from "kysely";
|
|
2
2
|
import { mapObject, objectToEntries } from "../Object.js";
|
|
3
|
-
import {
|
|
4
|
-
import { SqliteBoolean } from "../Sqlite.js";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
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
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
:
|
|
94
|
-
|
|
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
|
|
139
|
-
* ensures the
|
|
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({
|
|
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) =>
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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":"
|
|
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 {
|
|
30
|
-
import {
|
|
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 =
|
|
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 (
|
|
52
|
+
if (orderTimestampBytes(timestamp, stats.maxT) === 1) {
|
|
57
53
|
strategy = "append";
|
|
58
54
|
stats.maxT = timestamp;
|
|
59
55
|
}
|
|
60
|
-
else if (
|
|
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` –
|
|
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(
|
|
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.
|