@evolu/common 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +205 -0
- package/dist/src/Config.d.ts +20 -0
- package/dist/src/Config.d.ts.map +1 -0
- package/dist/src/Config.js +8 -0
- package/dist/src/Crypto.d.ts +42 -0
- package/dist/src/Crypto.d.ts.map +1 -0
- package/dist/src/Crypto.js +44 -0
- package/dist/src/Db.d.ts +60 -0
- package/dist/src/Db.d.ts.map +1 -0
- package/dist/src/Db.js +118 -0
- package/dist/src/DbWorker.d.ts +78 -0
- package/dist/src/DbWorker.d.ts.map +1 -0
- package/dist/src/DbWorker.js +282 -0
- package/dist/src/Diff.d.ts +19 -0
- package/dist/src/Diff.d.ts.map +1 -0
- package/dist/src/Diff.js +86 -0
- package/dist/src/Errors.d.ts +19 -0
- package/dist/src/Errors.d.ts.map +1 -0
- package/dist/src/Errors.js +11 -0
- package/dist/src/Evolu.d.ts +71 -0
- package/dist/src/Evolu.d.ts.map +1 -0
- package/dist/src/Evolu.js +235 -0
- package/dist/src/MerkleTree.d.ts +15 -0
- package/dist/src/MerkleTree.d.ts.map +1 -0
- package/dist/src/MerkleTree.js +49 -0
- package/dist/src/Model.d.ts +143 -0
- package/dist/src/Model.d.ts.map +1 -0
- package/dist/src/Model.js +103 -0
- package/dist/src/Murmurhash.d.ts +2 -0
- package/dist/src/Murmurhash.d.ts.map +1 -0
- package/dist/src/Murmurhash.js +60 -0
- package/dist/src/Platform.d.ts +29 -0
- package/dist/src/Platform.d.ts.map +1 -0
- package/dist/src/Platform.js +17 -0
- package/dist/src/Protobuf.d.ts +130 -0
- package/dist/src/Protobuf.d.ts.map +1 -0
- package/dist/src/Protobuf.js +100 -0
- package/dist/src/Public.d.ts +7 -0
- package/dist/src/Public.d.ts.map +1 -0
- package/dist/src/Public.js +2 -0
- package/dist/src/Sql.d.ts +12 -0
- package/dist/src/Sql.d.ts.map +1 -0
- package/dist/src/Sql.js +100 -0
- package/dist/src/Sqlite.d.ts +30 -0
- package/dist/src/Sqlite.d.ts.map +1 -0
- package/dist/src/Sqlite.js +41 -0
- package/dist/src/Store.d.ts +9 -0
- package/dist/src/Store.d.ts.map +1 -0
- package/dist/src/Store.js +18 -0
- package/dist/src/SyncWorker.d.ts +73 -0
- package/dist/src/SyncWorker.d.ts.map +1 -0
- package/dist/src/SyncWorker.js +136 -0
- package/dist/src/Timestamp.d.ts +44 -0
- package/dist/src/Timestamp.d.ts.map +1 -0
- package/dist/src/Timestamp.js +76 -0
- package/dist/src/index.d.ts +19 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +18 -0
- package/package.json +76 -0
- package/src/Config.ts +33 -0
- package/src/Crypto.ts +113 -0
- package/src/Db.ts +323 -0
- package/src/DbWorker.ts +683 -0
- package/src/Diff.ts +113 -0
- package/src/Errors.ts +33 -0
- package/src/Evolu.ts +527 -0
- package/src/MerkleTree.ts +83 -0
- package/src/Model.ts +212 -0
- package/src/Murmurhash.ts +70 -0
- package/src/Platform.ts +63 -0
- package/src/Protobuf.ts +204 -0
- package/src/Public.ts +6 -0
- package/src/Sql.ts +114 -0
- package/src/Sqlite.ts +92 -0
- package/src/Store.ts +32 -0
- package/src/SyncWorker.ts +344 -0
- package/src/Timestamp.ts +192 -0
- package/src/index.ts +18 -0
package/src/Db.ts
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import * as AST from "@effect/schema/AST";
|
|
2
|
+
import * as S from "@effect/schema/Schema";
|
|
3
|
+
import { make } from "@effect/schema/Schema";
|
|
4
|
+
import { bytesToHex } from "@noble/ciphers/utils";
|
|
5
|
+
import {
|
|
6
|
+
Brand,
|
|
7
|
+
Context,
|
|
8
|
+
Effect,
|
|
9
|
+
Exit,
|
|
10
|
+
Option,
|
|
11
|
+
Predicate,
|
|
12
|
+
ReadonlyArray,
|
|
13
|
+
ReadonlyRecord,
|
|
14
|
+
String,
|
|
15
|
+
pipe,
|
|
16
|
+
} from "effect";
|
|
17
|
+
import * as Kysely from "kysely";
|
|
18
|
+
import { urlAlphabet } from "nanoid";
|
|
19
|
+
import { Bip39, Mnemonic, NanoId, slip21Derive } from "./Crypto.js";
|
|
20
|
+
import { initialMerkleTree, merkleTreeToString } from "./MerkleTree.js";
|
|
21
|
+
import { Id, SqliteBoolean, SqliteDate } from "./Model.js";
|
|
22
|
+
import {
|
|
23
|
+
createMessageTable,
|
|
24
|
+
createMessageTableIndex,
|
|
25
|
+
createOwnerTable,
|
|
26
|
+
insertOwner,
|
|
27
|
+
} from "./Sql.js";
|
|
28
|
+
import {
|
|
29
|
+
Query,
|
|
30
|
+
QueryObject,
|
|
31
|
+
Row,
|
|
32
|
+
Sqlite,
|
|
33
|
+
Value,
|
|
34
|
+
queryObjectToQuery,
|
|
35
|
+
} from "./Sqlite.js";
|
|
36
|
+
import { makeInitialTimestamp, timestampToString } from "./Timestamp.js";
|
|
37
|
+
|
|
38
|
+
export type TableSchema = ReadonlyRecord.ReadonlyRecord<Value> & {
|
|
39
|
+
readonly id: Id;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type Schema = ReadonlyRecord.ReadonlyRecord<TableSchema>;
|
|
43
|
+
|
|
44
|
+
export type CreateQuery<S extends Schema> = (
|
|
45
|
+
queryCallback: QueryCallback<S, Row>,
|
|
46
|
+
) => Query;
|
|
47
|
+
|
|
48
|
+
export type QueryCallback<S extends Schema, QueryRow> = (
|
|
49
|
+
db: KyselyWithoutMutation<SchemaForQuery<S>>,
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
|
+
) => Kysely.SelectQueryBuilder<any, any, QueryRow>;
|
|
52
|
+
|
|
53
|
+
type KyselyWithoutMutation<DB> = Pick<Kysely.Kysely<DB>, "selectFrom" | "fn">;
|
|
54
|
+
|
|
55
|
+
type SchemaForQuery<S extends Schema> = {
|
|
56
|
+
readonly [Table in keyof S]: NullableExceptOfId<
|
|
57
|
+
{
|
|
58
|
+
readonly [Column in keyof S[Table]]: S[Table][Column];
|
|
59
|
+
} & CommonColumns
|
|
60
|
+
>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type NullableExceptOfId<T> = {
|
|
64
|
+
readonly [K in keyof T]: K extends "id" ? T[K] : T[K] | null;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export interface CommonColumns {
|
|
68
|
+
readonly createdAt: SqliteDate;
|
|
69
|
+
readonly updatedAt: SqliteDate;
|
|
70
|
+
readonly isDeleted: SqliteBoolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface Table {
|
|
74
|
+
readonly name: string;
|
|
75
|
+
readonly columns: ReadonlyArray<string>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type Tables = ReadonlyArray<Table>;
|
|
79
|
+
|
|
80
|
+
const commonColumns = ["createdAt", "updatedAt", "isDeleted"];
|
|
81
|
+
|
|
82
|
+
const kysely: Kysely.Kysely<SchemaForQuery<Schema>> = new Kysely.Kysely({
|
|
83
|
+
dialect: {
|
|
84
|
+
createAdapter: () => new Kysely.SqliteAdapter(),
|
|
85
|
+
createDriver: () => new Kysely.DummyDriver(),
|
|
86
|
+
createIntrospector(): Kysely.DatabaseIntrospector {
|
|
87
|
+
throw "Not implemeneted";
|
|
88
|
+
},
|
|
89
|
+
createQueryCompiler: () => new Kysely.SqliteQueryCompiler(),
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export const makeCreateQuery =
|
|
94
|
+
<T extends Schema>(): CreateQuery<T> =>
|
|
95
|
+
(queryCallback) =>
|
|
96
|
+
queryObjectToQuery(queryCallback(kysely as never).compile() as QueryObject);
|
|
97
|
+
|
|
98
|
+
// https://github.com/Effect-TS/schema/releases/tag/v0.18.0
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
100
|
+
const getPropertySignatures = <I extends { [K in keyof A]: any }, A>(
|
|
101
|
+
schema: S.Schema<I, A>,
|
|
102
|
+
): { [K in keyof A]: S.Schema<I[K], A[K]> } => {
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
104
|
+
const out: Record<PropertyKey, S.Schema<any>> = {};
|
|
105
|
+
const propertySignatures = AST.getPropertySignatures(schema.ast);
|
|
106
|
+
for (let i = 0; i < propertySignatures.length; i++) {
|
|
107
|
+
const propertySignature = propertySignatures[i];
|
|
108
|
+
out[propertySignature.name] = make(propertySignature.type);
|
|
109
|
+
}
|
|
110
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any
|
|
111
|
+
return out as any;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const schemaToTables = (
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
116
|
+
schema: S.Schema<any, any>,
|
|
117
|
+
): Tables =>
|
|
118
|
+
pipe(
|
|
119
|
+
getPropertySignatures(schema),
|
|
120
|
+
ReadonlyRecord.toEntries,
|
|
121
|
+
ReadonlyArray.map(
|
|
122
|
+
([name, schema]): Table => ({
|
|
123
|
+
name,
|
|
124
|
+
columns: Object.keys(getPropertySignatures(schema)).concat(
|
|
125
|
+
commonColumns,
|
|
126
|
+
),
|
|
127
|
+
}),
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* `Owner` represents the Evolu database owner. Evolu auto-generates `Owner`
|
|
133
|
+
* on the first run. `Owner` can be reset on the current device and restored
|
|
134
|
+
* on a different one.
|
|
135
|
+
*/
|
|
136
|
+
export interface Owner {
|
|
137
|
+
/** The `Mnemonic` associated with `Owner`. */
|
|
138
|
+
readonly mnemonic: Mnemonic;
|
|
139
|
+
/** The unique identifier of `Owner` safely derived from its `Mnemonic`. */
|
|
140
|
+
readonly id: OwnerId;
|
|
141
|
+
/* The encryption key used by `Owner` derived from its `Mnemonic`. */
|
|
142
|
+
readonly encryptionKey: Uint8Array;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const Owner = Context.Tag<Owner>("evolu/Owner");
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The unique identifier of `Owner` safely derived from its `Mnemonic`.
|
|
149
|
+
*/
|
|
150
|
+
export type OwnerId = Id & Brand.Brand<"Owner">;
|
|
151
|
+
|
|
152
|
+
export const transaction = <R, E, A>(
|
|
153
|
+
effect: Effect.Effect<R, E, A>,
|
|
154
|
+
): Effect.Effect<Sqlite | R, E, A> =>
|
|
155
|
+
Effect.flatMap(Sqlite, (sqlite) =>
|
|
156
|
+
Effect.acquireUseRelease(
|
|
157
|
+
sqlite.exec("BEGIN"),
|
|
158
|
+
() => effect,
|
|
159
|
+
(_, exit) =>
|
|
160
|
+
Exit.isFailure(exit) ? sqlite.exec("ROLLBACK") : sqlite.exec("END"),
|
|
161
|
+
),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
export interface NoSuchTableOrColumnError {
|
|
165
|
+
readonly _tag: "NoSuchTableOrColumnError";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export const someDefectToNoSuchTableOrColumnError = Effect.catchSomeDefect(
|
|
169
|
+
(error) => {
|
|
170
|
+
if (
|
|
171
|
+
typeof error === "object" &&
|
|
172
|
+
error != null &&
|
|
173
|
+
"message" in error &&
|
|
174
|
+
typeof error.message === "string" &&
|
|
175
|
+
error.message.includes("code 1") &&
|
|
176
|
+
(error.message.includes("no such table") ||
|
|
177
|
+
error.message.includes("no such column") ||
|
|
178
|
+
error.message.includes("has no column"))
|
|
179
|
+
)
|
|
180
|
+
return Option.some(
|
|
181
|
+
Effect.fail<NoSuchTableOrColumnError>({
|
|
182
|
+
_tag: "NoSuchTableOrColumnError",
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
return Option.none();
|
|
187
|
+
},
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
export const makeOwner = (
|
|
191
|
+
mnemonic?: Mnemonic,
|
|
192
|
+
): Effect.Effect<Bip39, never, Owner> =>
|
|
193
|
+
Effect.gen(function* (_) {
|
|
194
|
+
const bip39 = yield* _(Bip39);
|
|
195
|
+
|
|
196
|
+
if (mnemonic == null) mnemonic = yield* _(bip39.make);
|
|
197
|
+
|
|
198
|
+
const seed = yield* _(bip39.toSeed(mnemonic));
|
|
199
|
+
|
|
200
|
+
const id = yield* _(
|
|
201
|
+
slip21Derive(seed, ["Evolu", "Owner Id"]).pipe(
|
|
202
|
+
Effect.map((key) => {
|
|
203
|
+
// convert key to nanoid
|
|
204
|
+
let id = "";
|
|
205
|
+
for (let i = 0; i < 21; i++) {
|
|
206
|
+
id += urlAlphabet[key[i] & 63];
|
|
207
|
+
}
|
|
208
|
+
return id as OwnerId;
|
|
209
|
+
}),
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const encryptionKey = yield* _(
|
|
214
|
+
slip21Derive(seed, ["Evolu", "Encryption Key"]),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
return { mnemonic, id, encryptionKey };
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
export const lazyInit = (
|
|
221
|
+
mnemonic?: Mnemonic,
|
|
222
|
+
): Effect.Effect<Sqlite | Bip39 | NanoId, never, Owner> =>
|
|
223
|
+
Effect.gen(function* (_) {
|
|
224
|
+
const [owner, sqlite, initialTimestampString] = yield* _(
|
|
225
|
+
Effect.all([makeOwner(mnemonic), Sqlite, makeInitialTimestamp], {
|
|
226
|
+
concurrency: "unbounded",
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
yield* _(
|
|
231
|
+
Effect.all([
|
|
232
|
+
sqlite.exec(createMessageTable),
|
|
233
|
+
sqlite.exec(createMessageTableIndex),
|
|
234
|
+
sqlite.exec(createOwnerTable),
|
|
235
|
+
sqlite.exec({
|
|
236
|
+
sql: insertOwner,
|
|
237
|
+
parameters: [
|
|
238
|
+
owner.id,
|
|
239
|
+
owner.mnemonic,
|
|
240
|
+
// expo-sqlite 11.3.2 doesn't support Uint8Array
|
|
241
|
+
bytesToHex(owner.encryptionKey),
|
|
242
|
+
timestampToString(initialTimestampString),
|
|
243
|
+
merkleTreeToString(initialMerkleTree),
|
|
244
|
+
],
|
|
245
|
+
}),
|
|
246
|
+
]),
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
return owner;
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const getTables: Effect.Effect<
|
|
253
|
+
Sqlite,
|
|
254
|
+
never,
|
|
255
|
+
ReadonlyArray<string>
|
|
256
|
+
> = Sqlite.pipe(
|
|
257
|
+
Effect.flatMap((sqlite) =>
|
|
258
|
+
sqlite.exec(`SELECT "name" FROM "sqlite_schema" WHERE "type" = 'table'`),
|
|
259
|
+
),
|
|
260
|
+
Effect.map((result) => result.rows),
|
|
261
|
+
Effect.map(ReadonlyArray.map((row) => (row.name as string) + "")),
|
|
262
|
+
Effect.map(ReadonlyArray.filter(Predicate.not(String.startsWith("__")))),
|
|
263
|
+
Effect.map(ReadonlyArray.dedupeWith(String.Equivalence)),
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const updateTable = ({
|
|
267
|
+
name,
|
|
268
|
+
columns,
|
|
269
|
+
}: Table): Effect.Effect<Sqlite, never, void> =>
|
|
270
|
+
Effect.gen(function* (_) {
|
|
271
|
+
const sqlite = yield* _(Sqlite);
|
|
272
|
+
const sql = yield* _(
|
|
273
|
+
sqlite.exec(`PRAGMA table_info (${name})`),
|
|
274
|
+
Effect.map((result) => result.rows),
|
|
275
|
+
Effect.map(ReadonlyArray.map((row) => row.name as string)),
|
|
276
|
+
Effect.map((existingColumns) =>
|
|
277
|
+
ReadonlyArray.differenceWith(String.Equivalence)(existingColumns)(
|
|
278
|
+
columns,
|
|
279
|
+
),
|
|
280
|
+
),
|
|
281
|
+
Effect.map(
|
|
282
|
+
ReadonlyArray.map(
|
|
283
|
+
(newColumn) =>
|
|
284
|
+
`ALTER TABLE "${name}" ADD COLUMN "${newColumn}" blob;`,
|
|
285
|
+
),
|
|
286
|
+
),
|
|
287
|
+
Effect.map(ReadonlyArray.join("")),
|
|
288
|
+
);
|
|
289
|
+
if (sql) yield* _(sqlite.exec(sql));
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const createTable = ({
|
|
293
|
+
name,
|
|
294
|
+
columns,
|
|
295
|
+
}: Table): Effect.Effect<Sqlite, never, void> =>
|
|
296
|
+
Effect.flatMap(Sqlite, (sqlite) =>
|
|
297
|
+
sqlite.exec(`
|
|
298
|
+
CREATE TABLE ${name} (
|
|
299
|
+
"id" text primary key,
|
|
300
|
+
${columns
|
|
301
|
+
.filter((c) => c !== "id")
|
|
302
|
+
// "A column with affinity BLOB does not prefer one storage class over another
|
|
303
|
+
// and no attempt is made to coerce data from one storage class into another."
|
|
304
|
+
// https://www.sqlite.org/datatype3.html
|
|
305
|
+
.map((name) => `"${name}" blob`)
|
|
306
|
+
.join(", ")}
|
|
307
|
+
);
|
|
308
|
+
`),
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
export const ensureSchema = (
|
|
312
|
+
tables: Tables,
|
|
313
|
+
): Effect.Effect<Sqlite, never, void> =>
|
|
314
|
+
Effect.flatMap(getTables, (existingTables) =>
|
|
315
|
+
Effect.forEach(
|
|
316
|
+
tables,
|
|
317
|
+
(tableDefinition) =>
|
|
318
|
+
existingTables.includes(tableDefinition.name)
|
|
319
|
+
? updateTable(tableDefinition)
|
|
320
|
+
: createTable(tableDefinition),
|
|
321
|
+
{ discard: true },
|
|
322
|
+
),
|
|
323
|
+
);
|