@evolu/common 7.2.3 → 7.3.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.
- package/dist/src/Function.d.ts +61 -0
- package/dist/src/Function.d.ts.map +1 -1
- package/dist/src/Function.js +19 -0
- package/dist/src/Object.d.ts +16 -0
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +16 -0
- package/dist/src/Result.d.ts +36 -29
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Sqlite.d.ts +9 -7
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +8 -0
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +6 -4
- package/dist/src/Type.d.ts +30 -2
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +82 -0
- package/dist/src/local-first/Db.d.ts +1 -5
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +109 -108
- package/dist/src/local-first/Evolu.d.ts +3 -0
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +6 -7
- package/dist/src/local-first/LocalAuth.d.ts +2 -2
- package/dist/src/local-first/Owner.d.ts +3 -0
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +10 -10
- package/dist/src/local-first/PublicKysely.js +1 -1
- package/dist/src/local-first/Query.d.ts +54 -6
- package/dist/src/local-first/Query.d.ts.map +1 -1
- package/dist/src/local-first/Query.js +130 -11
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +17 -18
- package/dist/src/local-first/Schema.d.ts +15 -14
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +43 -51
- package/dist/src/local-first/Storage.d.ts +2 -2
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +2 -6
- package/dist/src/local-first/Sync.d.ts +9 -4
- package/dist/src/local-first/Sync.d.ts.map +1 -1
- package/dist/src/local-first/Sync.js +132 -59
- package/dist/src/local-first/index.d.ts +0 -1
- package/dist/src/local-first/index.d.ts.map +1 -1
- package/dist/src/local-first/index.js +0 -1
- package/package.json +1 -1
- package/src/Function.ts +74 -0
- package/src/Object.ts +20 -0
- package/src/Result.ts +38 -29
- package/src/Sqlite.ts +18 -7
- package/src/Task.ts +6 -4
- package/src/Type.ts +134 -1
- package/src/local-first/Db.ts +181 -209
- package/src/local-first/Evolu.ts +9 -6
- package/src/local-first/LocalAuth.ts +2 -2
- package/src/local-first/Owner.ts +4 -0
- package/src/local-first/Protocol.ts +10 -16
- package/src/local-first/PublicKysely.ts +1 -1
- package/src/local-first/Query.ts +216 -21
- package/src/local-first/Relay.ts +20 -24
- package/src/local-first/Schema.ts +83 -70
- package/src/local-first/Storage.ts +3 -8
- package/src/local-first/Sync.ts +190 -66
- package/src/local-first/index.ts +0 -1
- package/dist/src/local-first/Diff.d.ts +0 -43
- package/dist/src/local-first/Diff.d.ts.map +0 -1
- package/dist/src/local-first/Diff.js +0 -97
- package/src/local-first/Diff.ts +0 -144
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import * as Kysely from "kysely";
|
|
2
|
-
import { mapObject,
|
|
2
|
+
import { createRecord, getProperty, mapObject, } from "../Object.js";
|
|
3
3
|
import { ok } from "../Result.js";
|
|
4
4
|
import { sql, SqliteBoolean, } from "../Sqlite.js";
|
|
5
|
-
import { array, DateIso, nullableToOptional, nullOr, object, omit, optional, String, validMutationSize, } from "../Type.js";
|
|
5
|
+
import { array, DateIso, nullableToOptional, nullOr, object, omit, optional, record, set, String, validMutationSize, } from "../Type.js";
|
|
6
6
|
import { OwnerId } from "./Owner.js";
|
|
7
|
+
import { readonly } from "../Function.js";
|
|
7
8
|
export const evoluSchemaToDbSchema = (schema, indexesConfig) => {
|
|
8
|
-
const tables =
|
|
9
|
-
name: tableName,
|
|
10
|
-
columns: objectToEntries(table)
|
|
11
|
-
.filter(([k]) => k !== "id")
|
|
12
|
-
.map(([k]) => k),
|
|
13
|
-
}));
|
|
9
|
+
const tables = mapObject(schema, (table) => new Set(Object.keys(table).filter((k) => k !== "id")));
|
|
14
10
|
const indexes = indexesConfig
|
|
15
11
|
? indexesConfig(createIndex).map((index) => ({
|
|
16
12
|
name: index.toOperationNode().name.name,
|
|
@@ -34,7 +30,8 @@ export const SystemColumns = object({
|
|
|
34
30
|
isDeleted: nullOr(SqliteBoolean),
|
|
35
31
|
ownerId: OwnerId,
|
|
36
32
|
});
|
|
37
|
-
export const systemColumns = Object.keys(SystemColumns.props);
|
|
33
|
+
export const systemColumns = readonly(new Set(Object.keys(SystemColumns.props)));
|
|
34
|
+
export const systemColumnsWithId = readonly([...systemColumns, "id"]);
|
|
38
35
|
/**
|
|
39
36
|
* Type Factory to create insertable {@link Type}. It makes nullable Types
|
|
40
37
|
* optional (so they are not required), omits Id, and ensures the
|
|
@@ -108,18 +105,14 @@ export const upsertable = (props) => {
|
|
|
108
105
|
};
|
|
109
106
|
return validMutationSize(nullableToOptional(propsWithDefaults));
|
|
110
107
|
};
|
|
111
|
-
export const DbTable = object({
|
|
112
|
-
name: String,
|
|
113
|
-
columns: array(String),
|
|
114
|
-
});
|
|
115
108
|
export const DbIndex = object({ name: String, sql: String });
|
|
116
109
|
export const DbSchema = object({
|
|
117
|
-
tables:
|
|
110
|
+
tables: record(String, set(String)),
|
|
118
111
|
indexes: array(DbIndex),
|
|
119
112
|
});
|
|
120
113
|
/** Get the current database schema by reading SQLite metadata. */
|
|
121
114
|
export const getDbSchema = (deps) => ({ allIndexes = false } = {}) => {
|
|
122
|
-
const
|
|
115
|
+
const tables = createRecord();
|
|
123
116
|
const tableAndColumnInfoRows = deps.sqlite.exec(sql `
|
|
124
117
|
select
|
|
125
118
|
sqlite_master.name as tableName,
|
|
@@ -132,11 +125,8 @@ export const getDbSchema = (deps) => ({ allIndexes = false } = {}) => {
|
|
|
132
125
|
return tableAndColumnInfoRows;
|
|
133
126
|
tableAndColumnInfoRows.value.rows.forEach((row) => {
|
|
134
127
|
const { tableName, columnName } = row;
|
|
135
|
-
|
|
136
|
-
map.set(tableName, []);
|
|
137
|
-
map.get(tableName)?.push(columnName);
|
|
128
|
+
(tables[tableName] ??= new Set()).add(columnName);
|
|
138
129
|
});
|
|
139
|
-
const tables = Array.from(map, ([name, columns]) => ({ name, columns }));
|
|
140
130
|
const indexesRows = deps.sqlite.exec(allIndexes
|
|
141
131
|
? sql `
|
|
142
132
|
select name, sql
|
|
@@ -167,38 +157,40 @@ export const getDbSchema = (deps) => ({ allIndexes = false } = {}) => {
|
|
|
167
157
|
return ok({ tables, indexes });
|
|
168
158
|
};
|
|
169
159
|
const indexesAreEqual = (self, that) => self.name === that.name && self.sql === that.sql;
|
|
170
|
-
export const ensureDbSchema = (deps) => (newSchema, currentSchema
|
|
160
|
+
export const ensureDbSchema = (deps) => (newSchema, currentSchema) => {
|
|
171
161
|
const queries = [];
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
if (!
|
|
175
|
-
|
|
162
|
+
if (!currentSchema) {
|
|
163
|
+
const dbSchema = getDbSchema(deps)();
|
|
164
|
+
if (!dbSchema.ok)
|
|
165
|
+
return dbSchema;
|
|
166
|
+
currentSchema = dbSchema.value;
|
|
167
|
+
}
|
|
168
|
+
for (const [tableName, newColumns] of Object.entries(newSchema.tables)) {
|
|
169
|
+
const currentColumns = getProperty(currentSchema.tables, tableName);
|
|
170
|
+
if (!currentColumns) {
|
|
171
|
+
queries.push(createAppTable(tableName, newColumns));
|
|
176
172
|
}
|
|
177
173
|
else {
|
|
178
|
-
|
|
179
|
-
.filter((newColumn) => !currentTable.columns.includes(newColumn))
|
|
180
|
-
.forEach((newColumn) => {
|
|
174
|
+
for (const newColumn of newColumns.difference(currentColumns)) {
|
|
181
175
|
queries.push(sql `
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
176
|
+
alter table ${sql.identifier(tableName)}
|
|
177
|
+
add column ${sql.identifier(newColumn)} any;
|
|
178
|
+
`);
|
|
179
|
+
}
|
|
186
180
|
}
|
|
187
|
-
});
|
|
188
|
-
if (options?.ignoreIndexes !== true) {
|
|
189
|
-
// Remove current indexes that are not in the newSchema.
|
|
190
|
-
currentSchema.indexes
|
|
191
|
-
.filter((currentIndex) => !newSchema.indexes.some((newIndex) => indexesAreEqual(newIndex, currentIndex)))
|
|
192
|
-
.forEach((index) => {
|
|
193
|
-
queries.push(sql `drop index ${sql.identifier(index.name)};`);
|
|
194
|
-
});
|
|
195
|
-
// Add new indexes that are not in the currentSchema.
|
|
196
|
-
newSchema.indexes
|
|
197
|
-
.filter((newIndex) => !currentSchema.indexes.some((currentIndex) => indexesAreEqual(newIndex, currentIndex)))
|
|
198
|
-
.forEach((newIndex) => {
|
|
199
|
-
queries.push({ sql: `${newIndex.sql};`, parameters: [] });
|
|
200
|
-
});
|
|
201
181
|
}
|
|
182
|
+
// Remove current indexes that are not in the newSchema.
|
|
183
|
+
currentSchema.indexes
|
|
184
|
+
.filter((currentIndex) => !newSchema.indexes.some((newIndex) => indexesAreEqual(newIndex, currentIndex)))
|
|
185
|
+
.forEach((index) => {
|
|
186
|
+
queries.push(sql `drop index ${sql.identifier(index.name)};`);
|
|
187
|
+
});
|
|
188
|
+
// Add new indexes that are not in the currentSchema.
|
|
189
|
+
newSchema.indexes
|
|
190
|
+
.filter((newIndex) => !currentSchema.indexes.some((currentIndex) => indexesAreEqual(newIndex, currentIndex)))
|
|
191
|
+
.forEach((newIndex) => {
|
|
192
|
+
queries.push({ sql: `${newIndex.sql};`, parameters: [] });
|
|
193
|
+
});
|
|
202
194
|
for (const query of queries) {
|
|
203
195
|
const result = deps.sqlite.exec(query);
|
|
204
196
|
if (!result.ok)
|
|
@@ -206,17 +198,17 @@ export const ensureDbSchema = (deps) => (newSchema, currentSchema, options) => {
|
|
|
206
198
|
}
|
|
207
199
|
return ok();
|
|
208
200
|
};
|
|
209
|
-
const createAppTable = (
|
|
210
|
-
create table ${sql.identifier(
|
|
201
|
+
const createAppTable = (tableName, columns) => sql `
|
|
202
|
+
create table ${sql.identifier(tableName)} (
|
|
211
203
|
"id" text,
|
|
212
|
-
${sql.raw(`${systemColumns
|
|
213
|
-
.concat(table.columns)
|
|
214
|
-
.filter((c) => c !== "id")
|
|
204
|
+
${sql.raw(`${[...systemColumns, ...columns]
|
|
215
205
|
// With strict tables and any type, data is preserved exactly as received
|
|
216
206
|
// without any type affinity coercion. This allows storing any data type
|
|
217
207
|
// while maintaining strict null enforcement for primary key columns.
|
|
208
|
+
// TODO: Use proper SQLite types for system columns (text for createdAt,
|
|
209
|
+
// updatedAt, ownerId, integer for isDeleted) instead of "any".
|
|
218
210
|
.map((name) => `${sql.identifier(name).sql} any`)
|
|
219
|
-
.join(", ")}`)}
|
|
211
|
+
.join(", ")}, `)}
|
|
220
212
|
primary key ("ownerId", "id")
|
|
221
213
|
)
|
|
222
214
|
without rowid, strict;
|
|
@@ -165,7 +165,7 @@ export interface CrdtMessage {
|
|
|
165
165
|
}
|
|
166
166
|
export declare const DbChangeValues: 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>]>>;
|
|
167
167
|
export type DbChangeValues = typeof DbChangeValues.Type;
|
|
168
|
-
export declare const ValidDbChangeValues: import("../Type.js").BrandType<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>]>>, "ValidDbChangeValues", ValidDbChangeValuesError, import("../Type.js").RecordError<import("../Type.js").StringError, never> | import("../Type.js").RecordError<import("../Type.js").StringError, import("../Type.js").UnionError<import("../Type.js").
|
|
168
|
+
export declare const ValidDbChangeValues: import("../Type.js").BrandType<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>]>>, "ValidDbChangeValues", ValidDbChangeValuesError, import("../Type.js").RecordError<import("../Type.js").StringError, never> | import("../Type.js").RecordError<import("../Type.js").StringError, import("../Type.js").UnionError<import("../Type.js").StringError | import("../Type.js").NumberError | import("../Type.js").Uint8ArrayError | import("../Type.js").NullError>>>;
|
|
169
169
|
export type ValidDbChangeValues = typeof ValidDbChangeValues.Type;
|
|
170
170
|
export interface ValidDbChangeValuesError extends TypeError<"ValidDbChangeValues"> {
|
|
171
171
|
readonly invalidColumns: ReadonlyArray<string>;
|
|
@@ -177,7 +177,7 @@ export interface ValidDbChangeValuesError extends TypeError<"ValidDbChangeValues
|
|
|
177
177
|
export declare const DbChange: import("../Type.js").ObjectType<{
|
|
178
178
|
table: import("../Type.js").Type<"String", string, string, import("../Type.js").StringError, string, import("../Type.js").StringError>;
|
|
179
179
|
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>;
|
|
180
|
-
values: import("../Type.js").BrandType<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>]>>, "ValidDbChangeValues", ValidDbChangeValuesError, import("../Type.js").RecordError<import("../Type.js").StringError, never> | import("../Type.js").RecordError<import("../Type.js").StringError, import("../Type.js").UnionError<import("../Type.js").
|
|
180
|
+
values: import("../Type.js").BrandType<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>]>>, "ValidDbChangeValues", ValidDbChangeValuesError, import("../Type.js").RecordError<import("../Type.js").StringError, never> | import("../Type.js").RecordError<import("../Type.js").StringError, import("../Type.js").UnionError<import("../Type.js").StringError | import("../Type.js").NumberError | import("../Type.js").Uint8ArrayError | import("../Type.js").NullError>>>;
|
|
181
181
|
isInsert: import("../Type.js").Type<"Boolean", boolean, boolean, import("../Type.js").BooleanError, boolean, import("../Type.js").BooleanError>;
|
|
182
182
|
isDelete: import("../Type.js").UnionType<[import("../Type.js").Type<"Null", null, null, import("../Type.js").NullError, null, import("../Type.js").NullError>, import("../Type.js").Type<"Boolean", boolean, boolean, import("../Type.js").BooleanError, boolean, import("../Type.js").BooleanError>]>;
|
|
183
183
|
}>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Storage.d.ts","sourceRoot":"","sources":["../../../src/local-first/Storage.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,qBAAqB,EACtB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAGpC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAW,MAAM,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAO,SAAS,EAAE,WAAW,EAAe,MAAM,cAAc,CAAC;AACxE,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAKL,cAAc,EAGd,WAAW,EAGX,SAAS,EACV,MAAM,YAAY,CAAC;AACpB,OAAO,
|
|
1
|
+
{"version":3,"file":"Storage.d.ts","sourceRoot":"","sources":["../../../src/local-first/Storage.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,7 +6,7 @@ import { decrement } from "../Number.js";
|
|
|
6
6
|
import { err, ok } from "../Result.js";
|
|
7
7
|
import { sql, SqliteValue } from "../Sqlite.js";
|
|
8
8
|
import { Boolean, brand, Id, NonNegativeInt, nullOr, object, PositiveInt, record, String, } from "../Type.js";
|
|
9
|
-
import {
|
|
9
|
+
import { systemColumnsWithId } from "./Schema.js";
|
|
10
10
|
import { orderTimestampBytes } from "./Timestamp.js";
|
|
11
11
|
export const fingerprintSize = NonNegativeInt.orThrow(12);
|
|
12
12
|
/** A fingerprint of an empty range. */
|
|
@@ -18,12 +18,8 @@ export const RangeType = {
|
|
|
18
18
|
Timestamps: 2,
|
|
19
19
|
};
|
|
20
20
|
export const DbChangeValues = record(String, SqliteValue);
|
|
21
|
-
// "createdAt" and "updatedAt" are derived from Timestamp.
|
|
22
|
-
// "id" and "isDeleted" are encoded separately to save bytes
|
|
23
|
-
// "ownerId" is part of the protocol message
|
|
24
|
-
const forbiddenSystemColumns = systemColumns.concat("id");
|
|
25
21
|
export const ValidDbChangeValues = brand("ValidDbChangeValues", DbChangeValues, (value) => {
|
|
26
|
-
const invalidColumns =
|
|
22
|
+
const invalidColumns = systemColumnsWithId.filter((key) => key in value);
|
|
27
23
|
if (invalidColumns.length > 0)
|
|
28
24
|
return err({
|
|
29
25
|
type: "ValidDbChangeValues",
|
|
@@ -7,10 +7,9 @@ import { Result } from "../Result.js";
|
|
|
7
7
|
import { SqliteDep, SqliteError } from "../Sqlite.js";
|
|
8
8
|
import { TimeDep } from "../Time.js";
|
|
9
9
|
import { CreateWebSocketDep } from "../WebSocket.js";
|
|
10
|
-
import
|
|
11
|
-
import { AppOwner, Owner, OwnerTransport, ReadonlyOwner } from "./Owner.js";
|
|
10
|
+
import { AppOwner, AppOwnerDep, Owner, OwnerTransport, ReadonlyOwner } from "./Owner.js";
|
|
12
11
|
import { ProtocolError, ProtocolInvalidDataError, ProtocolTimestampMismatchError } from "./Protocol.js";
|
|
13
|
-
import { MutationChange } from "./Schema.js";
|
|
12
|
+
import { DbSchemaDep, MutationChange } from "./Schema.js";
|
|
14
13
|
import { BaseSqliteStorage, Storage } from "./Storage.js";
|
|
15
14
|
import { Millis, Timestamp, TimestampConfigDep, TimestampCounterOverflowError, TimestampDriftError, TimestampTimeOutOfRangeError } from "./Timestamp.js";
|
|
16
15
|
export interface Sync extends Disposable {
|
|
@@ -48,7 +47,7 @@ export interface SyncConfig {
|
|
|
48
47
|
readonly onError: (error: ProtocolError | ProtocolInvalidDataError | ProtocolTimestampMismatchError | SqliteError | SymmetricCryptoDecryptError | TimestampCounterOverflowError | TimestampDriftError | TimestampTimeOutOfRangeError | TransferableError) => void;
|
|
49
48
|
readonly onReceive: () => void;
|
|
50
49
|
}
|
|
51
|
-
export declare const createSync: (deps: ClockDep & ConsoleDep & CreateWebSocketDep &
|
|
50
|
+
export declare const createSync: (deps: ClockDep & ConsoleDep & CreateWebSocketDep & DbSchemaDep & RandomBytesDep & RandomDep & SqliteDep & SymmetricCryptoDep & TimeDep & TimestampConfigDep) => (config: SyncConfig) => Result<Sync, SqliteError>;
|
|
52
51
|
export interface ClockDep {
|
|
53
52
|
readonly clock: Clock;
|
|
54
53
|
}
|
|
@@ -63,6 +62,12 @@ export interface ClientStorageDep {
|
|
|
63
62
|
readonly storage: ClientStorage;
|
|
64
63
|
}
|
|
65
64
|
export declare const applyLocalOnlyChange: (deps: SqliteDep & TimeDep & AppOwnerDep) => (change: MutationChange) => Result<void, SqliteError>;
|
|
65
|
+
/**
|
|
66
|
+
* Attempts to apply quarantined messages that may now be valid after a schema
|
|
67
|
+
* update. Messages are quarantined when they reference tables or columns that
|
|
68
|
+
* don't exist in the current schema (e.g., from a newer app version).
|
|
69
|
+
*/
|
|
70
|
+
export declare const tryApplyQuarantinedMessages: (deps: DbSchemaDep & SqliteDep) => () => Result<void, SqliteError>;
|
|
66
71
|
/**
|
|
67
72
|
* TODO: Rework for the new owners API.
|
|
68
73
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Sync.d.ts","sourceRoot":"","sources":["../../../src/local-first/Sync.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,qBAAqB,EACtB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EACL,cAAc,EACd,2BAA2B,EAC3B,kBAAkB,EACnB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAA2B,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAGzE,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,OAAO,EAAW,MAAM,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAKL,SAAS,EACT,WAAW,EAEZ,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"Sync.d.ts","sourceRoot":"","sources":["../../../src/local-first/Sync.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,7 +3,7 @@ import { assert, assertNonEmptyReadonlyArray } from "../Assert.js";
|
|
|
3
3
|
import { eqArrayNumber } from "../Eq.js";
|
|
4
4
|
import { createTransferableError } from "../Error.js";
|
|
5
5
|
import { constFalse, constTrue } from "../Function.js";
|
|
6
|
-
import { createRecord, objectToEntries } from "../Object.js";
|
|
6
|
+
import { createRecord, getProperty, objectToEntries } from "../Object.js";
|
|
7
7
|
import { createResources } from "../Resources.js";
|
|
8
8
|
import { err, ok } from "../Result.js";
|
|
9
9
|
import { booleanToSqliteBoolean, sql, SqliteBoolean, sqliteBooleanToBoolean, } from "../Sqlite.js";
|
|
@@ -11,6 +11,7 @@ import { createMutex } from "../Task.js";
|
|
|
11
11
|
import { Boolean, idBytesToId, idToIdBytes, } from "../Type.js";
|
|
12
12
|
import { ownerIdBytesToOwnerId, ownerIdToOwnerIdBytes, } from "./Owner.js";
|
|
13
13
|
import { applyProtocolMessageAsClient, createProtocolMessageForSync, createProtocolMessageForUnsubscribe, createProtocolMessageFromCrdtMessages, decryptAndDecodeDbChange, encodeAndEncryptDbChange, SubscriptionFlags, } from "./Protocol.js";
|
|
14
|
+
import { systemColumns } from "./Schema.js";
|
|
14
15
|
import { createBaseSqliteStorage, DbChange, getOwnerUsage, getTimestampInsertStrategy, updateOwnerUsage, } from "./Storage.js";
|
|
15
16
|
import { createInitialTimestamp, receiveTimestamp, sendTimestamp, timestampBytesToTimestamp, timestampToDateIso, timestampToTimestampBytes, } from "./Timestamp.js";
|
|
16
17
|
export const createSync = (deps) => (config) => {
|
|
@@ -237,7 +238,7 @@ const createClientStorage = (deps) => (config) => {
|
|
|
237
238
|
writeMessages: async (ownerIdBytes, encryptedMessages) => {
|
|
238
239
|
const ownerId = ownerIdBytesToOwnerId(ownerIdBytes);
|
|
239
240
|
// Everything is sync now, but we will need async crypto in the future.
|
|
240
|
-
const
|
|
241
|
+
const result = await mutex.withLock(async () => {
|
|
241
242
|
const owner = deps.getSyncOwner(ownerId);
|
|
242
243
|
// Owner can be removed during syncing.
|
|
243
244
|
// `ok(true)` means success, we just skipped the write.
|
|
@@ -267,9 +268,9 @@ const createClientStorage = (deps) => (config) => {
|
|
|
267
268
|
clockTimestamp = nextTimestamp.value;
|
|
268
269
|
}
|
|
269
270
|
if (isNonEmptyReadonlyArray(messages)) {
|
|
270
|
-
const
|
|
271
|
-
if (!
|
|
272
|
-
return
|
|
271
|
+
const result = applyMessages({ ...deps, storage })(owner.id, messages);
|
|
272
|
+
if (!result.ok)
|
|
273
|
+
return result;
|
|
273
274
|
}
|
|
274
275
|
return deps.clock.save(clockTimestamp);
|
|
275
276
|
});
|
|
@@ -277,9 +278,9 @@ const createClientStorage = (deps) => (config) => {
|
|
|
277
278
|
return transaction;
|
|
278
279
|
return ok(true);
|
|
279
280
|
})();
|
|
280
|
-
if (!
|
|
281
|
-
if (
|
|
282
|
-
config.onError(
|
|
281
|
+
if (!result.ok) {
|
|
282
|
+
if (result.error.type !== "AbortError") {
|
|
283
|
+
config.onError(result.error);
|
|
283
284
|
}
|
|
284
285
|
return err({ type: "StorageWriteError", ownerId });
|
|
285
286
|
}
|
|
@@ -294,6 +295,10 @@ const createClientStorage = (deps) => (config) => {
|
|
|
294
295
|
const result = deps.sqlite.exec(sql `
|
|
295
296
|
select "table", "id", "column", "value"
|
|
296
297
|
from evolu_history
|
|
298
|
+
where "ownerId" = ${ownerId} and "timestamp" = ${timestamp}
|
|
299
|
+
union all
|
|
300
|
+
select "table", "id", "column", "value"
|
|
301
|
+
from evolu_message_quarantine
|
|
297
302
|
where "ownerId" = ${ownerId} and "timestamp" = ${timestamp};
|
|
298
303
|
`);
|
|
299
304
|
if (!result.ok) {
|
|
@@ -301,7 +306,7 @@ const createClientStorage = (deps) => (config) => {
|
|
|
301
306
|
return null;
|
|
302
307
|
}
|
|
303
308
|
const { rows } = result.value;
|
|
304
|
-
assertNonEmptyReadonlyArray(rows, "Every timestamp must have rows");
|
|
309
|
+
assertNonEmptyReadonlyArray(rows, "Every timestamp must have rows in evolu_history or quarantine");
|
|
305
310
|
const { table, id } = firstInArray(rows);
|
|
306
311
|
const values = createRecord();
|
|
307
312
|
let isInsert;
|
|
@@ -392,65 +397,133 @@ const applyMessages = (deps) => (ownerId, messages) => {
|
|
|
392
397
|
return usage;
|
|
393
398
|
let { firstTimestamp, lastTimestamp } = usage.value;
|
|
394
399
|
for (const { timestamp, change } of messages) {
|
|
395
|
-
const timestampBytes = timestampToTimestampBytes(timestamp);
|
|
396
|
-
const idBytes = idToIdBytes(change.id);
|
|
397
400
|
const columns = dbChangeToColumns(change, timestampToDateIso(timestamp));
|
|
401
|
+
const idBytes = idToIdBytes(change.id);
|
|
402
|
+
const timestampBytes = timestampToTimestampBytes(timestamp);
|
|
398
403
|
for (const [column, value] of columns) {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const insertHistory = deps.sqlite.exec(sql.prepared `
|
|
423
|
-
insert into evolu_history
|
|
424
|
-
("ownerId", "table", "id", "column", "value", "timestamp")
|
|
425
|
-
values
|
|
426
|
-
(
|
|
427
|
-
${ownerIdBytes},
|
|
428
|
-
${change.table},
|
|
429
|
-
${idBytes},
|
|
430
|
-
${column},
|
|
431
|
-
${value},
|
|
432
|
-
${timestampBytes}
|
|
433
|
-
)
|
|
434
|
-
on conflict do nothing;
|
|
435
|
-
`);
|
|
436
|
-
if (!insertHistory.ok)
|
|
437
|
-
return insertHistory;
|
|
404
|
+
if (validateColumnValue(deps)(change.table, column, value)) {
|
|
405
|
+
const result = applyColumnChange(deps)(ownerIdBytes, ownerId, change.table, idBytes, change.id, column, value, timestampBytes);
|
|
406
|
+
if (!result.ok)
|
|
407
|
+
return result;
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
const result = deps.sqlite.exec(sql.prepared `
|
|
411
|
+
insert into evolu_message_quarantine
|
|
412
|
+
("ownerId", "timestamp", "table", "id", "column", "value")
|
|
413
|
+
values
|
|
414
|
+
(
|
|
415
|
+
${ownerIdBytes},
|
|
416
|
+
${timestampBytes},
|
|
417
|
+
${change.table},
|
|
418
|
+
${idBytes},
|
|
419
|
+
${column},
|
|
420
|
+
${value}
|
|
421
|
+
)
|
|
422
|
+
on conflict do nothing;
|
|
423
|
+
`);
|
|
424
|
+
if (!result.ok)
|
|
425
|
+
return result;
|
|
426
|
+
}
|
|
438
427
|
}
|
|
439
428
|
let strategy;
|
|
440
429
|
[strategy, firstTimestamp, lastTimestamp] = getTimestampInsertStrategy(timestampBytes, firstTimestamp, lastTimestamp);
|
|
441
|
-
const
|
|
442
|
-
if (!
|
|
443
|
-
return
|
|
430
|
+
const result = deps.storage.insertTimestamp(ownerIdBytes, timestampBytes, strategy);
|
|
431
|
+
if (!result.ok)
|
|
432
|
+
return result;
|
|
444
433
|
}
|
|
445
434
|
/**
|
|
446
|
-
* TODO: Implement proper storedBytes tracking for client using
|
|
447
|
-
* message sizes
|
|
448
|
-
* client...).
|
|
435
|
+
* TODO: Implement proper storedBytes tracking for client using received and
|
|
436
|
+
* sent encrypted message sizes.
|
|
449
437
|
*/
|
|
450
|
-
|
|
438
|
+
return updateOwnerUsage(deps)(ownerIdBytes, 1, // Placeholder until proper tracking implemented
|
|
451
439
|
firstTimestamp, lastTimestamp);
|
|
452
|
-
|
|
453
|
-
|
|
440
|
+
};
|
|
441
|
+
/**
|
|
442
|
+
* System columns that can appear in sync messages. Excludes `ownerId` because
|
|
443
|
+
* it's handled separately (stored per-row, not per-column in messages).
|
|
444
|
+
*/
|
|
445
|
+
const systemColumnsWithoutOwnerId = systemColumns.difference(new Set(["ownerId"]));
|
|
446
|
+
const validateColumnValue = (deps) => (table, column, _value) => {
|
|
447
|
+
const schemaColumns = getProperty(deps.dbSchema.tables, table);
|
|
448
|
+
return (schemaColumns != null &&
|
|
449
|
+
(systemColumnsWithoutOwnerId.has(column) || schemaColumns.has(column)));
|
|
450
|
+
};
|
|
451
|
+
const applyColumnChange = (deps) => (ownerIdBytes, ownerId, table, idBytes, id, column, value, timestampBytes) => {
|
|
452
|
+
const result = deps.sqlite.exec(sql.prepared `
|
|
453
|
+
with
|
|
454
|
+
existingTimestamp as (
|
|
455
|
+
select 1
|
|
456
|
+
from evolu_history
|
|
457
|
+
where
|
|
458
|
+
"ownerId" = ${ownerIdBytes}
|
|
459
|
+
and "table" = ${table}
|
|
460
|
+
and "id" = ${idBytes}
|
|
461
|
+
and "column" = ${column}
|
|
462
|
+
and "timestamp" >= ${timestampBytes}
|
|
463
|
+
limit 1
|
|
464
|
+
)
|
|
465
|
+
insert into ${sql.identifier(table)}
|
|
466
|
+
("ownerId", "id", ${sql.identifier(column)})
|
|
467
|
+
select ${ownerId}, ${id}, ${value}
|
|
468
|
+
where not exists (select 1 from existingTimestamp)
|
|
469
|
+
on conflict ("ownerId", "id") do update
|
|
470
|
+
set ${sql.identifier(column)} = ${value}
|
|
471
|
+
where not exists (select 1 from existingTimestamp);
|
|
472
|
+
`);
|
|
473
|
+
if (!result.ok)
|
|
474
|
+
return result;
|
|
475
|
+
{
|
|
476
|
+
const result = deps.sqlite.exec(sql.prepared `
|
|
477
|
+
insert into evolu_history
|
|
478
|
+
("ownerId", "table", "id", "column", "value", "timestamp")
|
|
479
|
+
values
|
|
480
|
+
(
|
|
481
|
+
${ownerIdBytes},
|
|
482
|
+
${table},
|
|
483
|
+
${idBytes},
|
|
484
|
+
${column},
|
|
485
|
+
${value},
|
|
486
|
+
${timestampBytes}
|
|
487
|
+
)
|
|
488
|
+
on conflict do nothing;
|
|
489
|
+
`);
|
|
490
|
+
if (!result.ok)
|
|
491
|
+
return result;
|
|
492
|
+
}
|
|
493
|
+
return ok();
|
|
494
|
+
};
|
|
495
|
+
/**
|
|
496
|
+
* Attempts to apply quarantined messages that may now be valid after a schema
|
|
497
|
+
* update. Messages are quarantined when they reference tables or columns that
|
|
498
|
+
* don't exist in the current schema (e.g., from a newer app version).
|
|
499
|
+
*/
|
|
500
|
+
export const tryApplyQuarantinedMessages = (deps) => () => {
|
|
501
|
+
const rows = deps.sqlite.exec(sql `
|
|
502
|
+
select "ownerId", "timestamp", "table", "id", "column", "value"
|
|
503
|
+
from evolu_message_quarantine;
|
|
504
|
+
`);
|
|
505
|
+
if (!rows.ok)
|
|
506
|
+
return rows;
|
|
507
|
+
for (const row of rows.value.rows) {
|
|
508
|
+
if (!validateColumnValue(deps)(row.table, row.column, row.value))
|
|
509
|
+
continue;
|
|
510
|
+
const result = applyColumnChange(deps)(row.ownerId, ownerIdBytesToOwnerId(row.ownerId), row.table, row.id, idBytesToId(row.id), row.column, row.value, row.timestamp);
|
|
511
|
+
if (!result.ok)
|
|
512
|
+
return result;
|
|
513
|
+
{
|
|
514
|
+
const result = deps.sqlite.exec(sql `
|
|
515
|
+
delete from evolu_message_quarantine
|
|
516
|
+
where
|
|
517
|
+
"ownerId" = ${row.ownerId}
|
|
518
|
+
and "timestamp" = ${row.timestamp}
|
|
519
|
+
and "table" = ${row.table}
|
|
520
|
+
and "id" = ${row.id}
|
|
521
|
+
and "column" = ${row.column};
|
|
522
|
+
`);
|
|
523
|
+
if (!result.ok)
|
|
524
|
+
return result;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
454
527
|
return ok();
|
|
455
528
|
};
|
|
456
529
|
export const initialSyncState = { type: "SyncStateInitial" };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/local-first/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,SAAS,CAAC;AACxB,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/local-first/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC"}
|