@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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +205 -0
  3. package/dist/src/Config.d.ts +20 -0
  4. package/dist/src/Config.d.ts.map +1 -0
  5. package/dist/src/Config.js +8 -0
  6. package/dist/src/Crypto.d.ts +42 -0
  7. package/dist/src/Crypto.d.ts.map +1 -0
  8. package/dist/src/Crypto.js +44 -0
  9. package/dist/src/Db.d.ts +60 -0
  10. package/dist/src/Db.d.ts.map +1 -0
  11. package/dist/src/Db.js +118 -0
  12. package/dist/src/DbWorker.d.ts +78 -0
  13. package/dist/src/DbWorker.d.ts.map +1 -0
  14. package/dist/src/DbWorker.js +282 -0
  15. package/dist/src/Diff.d.ts +19 -0
  16. package/dist/src/Diff.d.ts.map +1 -0
  17. package/dist/src/Diff.js +86 -0
  18. package/dist/src/Errors.d.ts +19 -0
  19. package/dist/src/Errors.d.ts.map +1 -0
  20. package/dist/src/Errors.js +11 -0
  21. package/dist/src/Evolu.d.ts +71 -0
  22. package/dist/src/Evolu.d.ts.map +1 -0
  23. package/dist/src/Evolu.js +235 -0
  24. package/dist/src/MerkleTree.d.ts +15 -0
  25. package/dist/src/MerkleTree.d.ts.map +1 -0
  26. package/dist/src/MerkleTree.js +49 -0
  27. package/dist/src/Model.d.ts +143 -0
  28. package/dist/src/Model.d.ts.map +1 -0
  29. package/dist/src/Model.js +103 -0
  30. package/dist/src/Murmurhash.d.ts +2 -0
  31. package/dist/src/Murmurhash.d.ts.map +1 -0
  32. package/dist/src/Murmurhash.js +60 -0
  33. package/dist/src/Platform.d.ts +29 -0
  34. package/dist/src/Platform.d.ts.map +1 -0
  35. package/dist/src/Platform.js +17 -0
  36. package/dist/src/Protobuf.d.ts +130 -0
  37. package/dist/src/Protobuf.d.ts.map +1 -0
  38. package/dist/src/Protobuf.js +100 -0
  39. package/dist/src/Public.d.ts +7 -0
  40. package/dist/src/Public.d.ts.map +1 -0
  41. package/dist/src/Public.js +2 -0
  42. package/dist/src/Sql.d.ts +12 -0
  43. package/dist/src/Sql.d.ts.map +1 -0
  44. package/dist/src/Sql.js +100 -0
  45. package/dist/src/Sqlite.d.ts +30 -0
  46. package/dist/src/Sqlite.d.ts.map +1 -0
  47. package/dist/src/Sqlite.js +41 -0
  48. package/dist/src/Store.d.ts +9 -0
  49. package/dist/src/Store.d.ts.map +1 -0
  50. package/dist/src/Store.js +18 -0
  51. package/dist/src/SyncWorker.d.ts +73 -0
  52. package/dist/src/SyncWorker.d.ts.map +1 -0
  53. package/dist/src/SyncWorker.js +136 -0
  54. package/dist/src/Timestamp.d.ts +44 -0
  55. package/dist/src/Timestamp.d.ts.map +1 -0
  56. package/dist/src/Timestamp.js +76 -0
  57. package/dist/src/index.d.ts +19 -0
  58. package/dist/src/index.d.ts.map +1 -0
  59. package/dist/src/index.js +18 -0
  60. package/package.json +76 -0
  61. package/src/Config.ts +33 -0
  62. package/src/Crypto.ts +113 -0
  63. package/src/Db.ts +323 -0
  64. package/src/DbWorker.ts +683 -0
  65. package/src/Diff.ts +113 -0
  66. package/src/Errors.ts +33 -0
  67. package/src/Evolu.ts +527 -0
  68. package/src/MerkleTree.ts +83 -0
  69. package/src/Model.ts +212 -0
  70. package/src/Murmurhash.ts +70 -0
  71. package/src/Platform.ts +63 -0
  72. package/src/Protobuf.ts +204 -0
  73. package/src/Public.ts +6 -0
  74. package/src/Sql.ts +114 -0
  75. package/src/Sqlite.ts +92 -0
  76. package/src/Store.ts +32 -0
  77. package/src/SyncWorker.ts +344 -0
  78. package/src/Timestamp.ts +192 -0
  79. 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
+ );