@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/DbWorker.ts
ADDED
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
import { hexToBytes } from "@noble/ciphers/utils";
|
|
2
|
+
import {
|
|
3
|
+
Brand,
|
|
4
|
+
Context,
|
|
5
|
+
Effect,
|
|
6
|
+
Function,
|
|
7
|
+
Layer,
|
|
8
|
+
Match,
|
|
9
|
+
Option,
|
|
10
|
+
ReadonlyArray,
|
|
11
|
+
ReadonlyRecord,
|
|
12
|
+
Ref,
|
|
13
|
+
pipe,
|
|
14
|
+
} from "effect";
|
|
15
|
+
import { Config, ConfigLive } from "./Config.js";
|
|
16
|
+
import { Bip39, Mnemonic, NanoId } from "./Crypto.js";
|
|
17
|
+
import {
|
|
18
|
+
Owner,
|
|
19
|
+
OwnerId,
|
|
20
|
+
Table,
|
|
21
|
+
Tables,
|
|
22
|
+
ensureSchema,
|
|
23
|
+
lazyInit,
|
|
24
|
+
someDefectToNoSuchTableOrColumnError,
|
|
25
|
+
transaction,
|
|
26
|
+
} from "./Db.js";
|
|
27
|
+
import { QueryPatches, makePatches } from "./Diff.js";
|
|
28
|
+
import { EvoluError, makeUnexpectedError } from "./Errors.js";
|
|
29
|
+
import {
|
|
30
|
+
MerkleTree,
|
|
31
|
+
diffMerkleTrees,
|
|
32
|
+
insertIntoMerkleTree,
|
|
33
|
+
merkleTreeToString,
|
|
34
|
+
} from "./MerkleTree.js";
|
|
35
|
+
import { Id, SqliteDate, cast } from "./Model.js";
|
|
36
|
+
import {
|
|
37
|
+
insertIntoMessagesIfNew,
|
|
38
|
+
insertValueIntoTableRowColumn,
|
|
39
|
+
selectLastTimestampForTableRowColumn,
|
|
40
|
+
selectMessagesToSync,
|
|
41
|
+
selectOwner,
|
|
42
|
+
selectOwnerTimestampAndMerkleTree,
|
|
43
|
+
updateOwnerTimestampAndMerkleTree,
|
|
44
|
+
} from "./Sql.js";
|
|
45
|
+
import { Query, Row, Sqlite, Value, queryObjectFromQuery } from "./Sqlite.js";
|
|
46
|
+
import {
|
|
47
|
+
Message,
|
|
48
|
+
NewMessage,
|
|
49
|
+
SyncState,
|
|
50
|
+
SyncWorker,
|
|
51
|
+
SyncWorkerOutputSyncResponse,
|
|
52
|
+
SyncWorkerPostMessage,
|
|
53
|
+
} from "./SyncWorker.js";
|
|
54
|
+
import {
|
|
55
|
+
Time,
|
|
56
|
+
TimeLive,
|
|
57
|
+
Timestamp,
|
|
58
|
+
TimestampCounterOverflowError,
|
|
59
|
+
TimestampDriftError,
|
|
60
|
+
TimestampError,
|
|
61
|
+
TimestampString,
|
|
62
|
+
makeSyncTimestamp,
|
|
63
|
+
receiveTimestamp,
|
|
64
|
+
sendTimestamp,
|
|
65
|
+
timestampToString,
|
|
66
|
+
unsafeTimestampFromString,
|
|
67
|
+
} from "./Timestamp.js";
|
|
68
|
+
|
|
69
|
+
// TODO: Refactor to Effect.
|
|
70
|
+
export interface DbWorker {
|
|
71
|
+
readonly postMessage: (input: DbWorkerInput) => void;
|
|
72
|
+
onMessage: (output: DbWorkerOutput) => void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const DbWorker = Context.Tag<DbWorker>("evolu/DbWorker");
|
|
76
|
+
|
|
77
|
+
export type DbWorkerInput =
|
|
78
|
+
| DbWorkerInputInit
|
|
79
|
+
| DbWorkerInputQuery
|
|
80
|
+
| DbWorkerInputMutate
|
|
81
|
+
| DbWorkerInputSync
|
|
82
|
+
| DbWorkerInputReset
|
|
83
|
+
| DbWorkerInputEnsureSchema
|
|
84
|
+
| SyncWorkerOutputSyncResponse;
|
|
85
|
+
|
|
86
|
+
interface DbWorkerInputInit {
|
|
87
|
+
readonly _tag: "init";
|
|
88
|
+
readonly config: Config;
|
|
89
|
+
readonly tables: Tables;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface DbWorkerInputQuery {
|
|
93
|
+
readonly _tag: "query";
|
|
94
|
+
readonly queries: ReadonlyArray.NonEmptyReadonlyArray<Query>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface DbWorkerInputMutate {
|
|
98
|
+
readonly _tag: "mutate";
|
|
99
|
+
readonly items: ReadonlyArray.NonEmptyReadonlyArray<MutateItem>;
|
|
100
|
+
readonly queries: ReadonlyArray<Query>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface DbWorkerInputSync {
|
|
104
|
+
readonly _tag: "sync";
|
|
105
|
+
readonly queries: ReadonlyArray<Query>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface DbWorkerInputReset {
|
|
109
|
+
readonly _tag: "reset";
|
|
110
|
+
readonly mnemonic?: Mnemonic;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface DbWorkerInputEnsureSchema {
|
|
114
|
+
readonly _tag: "ensureSchema";
|
|
115
|
+
readonly tables: Tables;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
type DbWorkerOnMessage = DbWorker["onMessage"];
|
|
119
|
+
|
|
120
|
+
const DbWorkerOnMessage = Context.Tag<DbWorkerOnMessage>(
|
|
121
|
+
"evolu/DbWorkerOnMessage",
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
export type DbWorkerOutput =
|
|
125
|
+
| DbWorkerOutputOnError
|
|
126
|
+
| DbWorkerOutputOnOwner
|
|
127
|
+
| DbWorkerOutputOnQuery
|
|
128
|
+
| DbWorkerOutputOnReceive
|
|
129
|
+
| DbWorkerOutputOnResetOrRestore
|
|
130
|
+
| DbWorkerOutputOnSyncState;
|
|
131
|
+
|
|
132
|
+
interface DbWorkerOutputOnError {
|
|
133
|
+
readonly _tag: "onError";
|
|
134
|
+
readonly error: EvoluError;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface DbWorkerOutputOnOwner {
|
|
138
|
+
readonly _tag: "onOwner";
|
|
139
|
+
readonly owner: Owner;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface DbWorkerOutputOnQuery {
|
|
143
|
+
readonly _tag: "onQuery";
|
|
144
|
+
readonly queriesPatches: ReadonlyArray<QueryPatches>;
|
|
145
|
+
readonly onCompleteIds: ReadonlyArray<OnCompleteId>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export type OnCompleteId = string &
|
|
149
|
+
Brand.Brand<"Id"> &
|
|
150
|
+
Brand.Brand<"OnComplete">;
|
|
151
|
+
|
|
152
|
+
interface DbWorkerOutputOnReceive {
|
|
153
|
+
readonly _tag: "onReceive";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface DbWorkerOutputOnResetOrRestore {
|
|
157
|
+
readonly _tag: "onResetOrRestore";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
interface DbWorkerOutputOnSyncState {
|
|
161
|
+
readonly _tag: "onSyncState";
|
|
162
|
+
readonly state: SyncState;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface MutateItem {
|
|
166
|
+
readonly table: string;
|
|
167
|
+
readonly id: Id;
|
|
168
|
+
readonly values: ReadonlyRecord.ReadonlyRecord<
|
|
169
|
+
Value | Date | boolean | undefined
|
|
170
|
+
>;
|
|
171
|
+
readonly isInsert: boolean;
|
|
172
|
+
readonly now: SqliteDate;
|
|
173
|
+
readonly onCompleteId: OnCompleteId | null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const init = (
|
|
177
|
+
input: DbWorkerInputInit,
|
|
178
|
+
): Effect.Effect<Sqlite | Bip39 | NanoId, never, Owner> =>
|
|
179
|
+
Effect.gen(function* (_) {
|
|
180
|
+
const sqlite = yield* _(Sqlite);
|
|
181
|
+
|
|
182
|
+
return yield* _(
|
|
183
|
+
sqlite.exec(selectOwner),
|
|
184
|
+
Effect.map(
|
|
185
|
+
({ rows: [row] }): Owner => ({
|
|
186
|
+
id: row.id as OwnerId,
|
|
187
|
+
mnemonic: row.mnemonic as Mnemonic,
|
|
188
|
+
// expo-sqlite 11.3.2 doesn't support Uint8Array
|
|
189
|
+
encryptionKey: hexToBytes(row.encryptionKey as string),
|
|
190
|
+
}),
|
|
191
|
+
),
|
|
192
|
+
someDefectToNoSuchTableOrColumnError,
|
|
193
|
+
Effect.catchTag("NoSuchTableOrColumnError", () => lazyInit()),
|
|
194
|
+
Effect.tap(() => ensureSchema(input.tables)),
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
export type RowsCacheMap = ReadonlyMap<Query, ReadonlyArray<Row>>;
|
|
199
|
+
|
|
200
|
+
type RowsCacheRef = Ref.Ref<RowsCacheMap>;
|
|
201
|
+
const RowsCacheRef = Context.Tag<RowsCacheRef>("evolu/RowsCacheRef");
|
|
202
|
+
|
|
203
|
+
const query = ({
|
|
204
|
+
queries,
|
|
205
|
+
onCompleteIds = [],
|
|
206
|
+
}: {
|
|
207
|
+
readonly queries: ReadonlyArray<Query>;
|
|
208
|
+
readonly onCompleteIds?: ReadonlyArray<OnCompleteId>;
|
|
209
|
+
}): Effect.Effect<Sqlite | RowsCacheRef | DbWorkerOnMessage, never, void> =>
|
|
210
|
+
Effect.gen(function* (_) {
|
|
211
|
+
const sqlite = yield* _(Sqlite);
|
|
212
|
+
const rowsCache = yield* _(RowsCacheRef);
|
|
213
|
+
const dbWorkerOnMessage = yield* _(DbWorkerOnMessage);
|
|
214
|
+
|
|
215
|
+
const queriesRows = yield* _(
|
|
216
|
+
Effect.forEach(queries, (query) =>
|
|
217
|
+
sqlite
|
|
218
|
+
.exec(queryObjectFromQuery(query))
|
|
219
|
+
.pipe(Effect.map((result) => [query, result.rows] as const)),
|
|
220
|
+
),
|
|
221
|
+
);
|
|
222
|
+
const previous = yield* _(Ref.get(rowsCache));
|
|
223
|
+
yield* _(Ref.set(rowsCache, new Map([...previous, ...queriesRows])));
|
|
224
|
+
|
|
225
|
+
const queriesPatches = queriesRows.map(
|
|
226
|
+
([query, rows]): QueryPatches => ({
|
|
227
|
+
query,
|
|
228
|
+
patches: makePatches(previous.get(query), rows),
|
|
229
|
+
}),
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
dbWorkerOnMessage({ _tag: "onQuery", queriesPatches, onCompleteIds });
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
interface TimestampAndMerkleTree {
|
|
236
|
+
readonly timestamp: Timestamp;
|
|
237
|
+
readonly merkleTree: MerkleTree;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const readTimestampAndMerkleTree = Sqlite.pipe(
|
|
241
|
+
Effect.flatMap((sqlite) => sqlite.exec(selectOwnerTimestampAndMerkleTree)),
|
|
242
|
+
Effect.map((result) => result.rows),
|
|
243
|
+
Effect.map(
|
|
244
|
+
([{ timestamp, merkleTree }]): TimestampAndMerkleTree => ({
|
|
245
|
+
timestamp: unsafeTimestampFromString(timestamp as TimestampString),
|
|
246
|
+
merkleTree: merkleTree as MerkleTree,
|
|
247
|
+
}),
|
|
248
|
+
),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const mutateItemsToNewMessages = (
|
|
252
|
+
items: ReadonlyArray.NonEmptyReadonlyArray<MutateItem>,
|
|
253
|
+
): ReadonlyArray.NonEmptyReadonlyArray<NewMessage> =>
|
|
254
|
+
pipe(
|
|
255
|
+
items,
|
|
256
|
+
ReadonlyArray.mapNonEmpty(({ id, isInsert, now, table, values }) =>
|
|
257
|
+
pipe(
|
|
258
|
+
Object.entries(values),
|
|
259
|
+
ReadonlyArray.filterMap(([key, value]) =>
|
|
260
|
+
// The value can be undefined if exactOptionalPropertyTypes isn't true.
|
|
261
|
+
// Don't insert nulls because null is the default value.
|
|
262
|
+
value === undefined || (isInsert && value == null)
|
|
263
|
+
? Option.none()
|
|
264
|
+
: Option.some([key, value] as const),
|
|
265
|
+
),
|
|
266
|
+
ReadonlyArray.map(
|
|
267
|
+
([key, value]) =>
|
|
268
|
+
[
|
|
269
|
+
key,
|
|
270
|
+
typeof value === "boolean"
|
|
271
|
+
? cast(value)
|
|
272
|
+
: value instanceof Date
|
|
273
|
+
? cast(value)
|
|
274
|
+
: value,
|
|
275
|
+
] as const,
|
|
276
|
+
),
|
|
277
|
+
ReadonlyArray.append([isInsert ? "createdAt" : "updatedAt", now]),
|
|
278
|
+
ReadonlyArray.mapNonEmpty(
|
|
279
|
+
([key, value]): NewMessage => ({
|
|
280
|
+
table,
|
|
281
|
+
row: id,
|
|
282
|
+
column: key,
|
|
283
|
+
value,
|
|
284
|
+
}),
|
|
285
|
+
),
|
|
286
|
+
),
|
|
287
|
+
),
|
|
288
|
+
ReadonlyArray.flattenNonEmpty,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const ensureSchemaByMessages = (
|
|
292
|
+
messages: ReadonlyArray.NonEmptyReadonlyArray<Message>,
|
|
293
|
+
): Effect.Effect<Sqlite, never, void> =>
|
|
294
|
+
Effect.gen(function* (_) {
|
|
295
|
+
const tablesMap = new Map<string, Table>();
|
|
296
|
+
messages.forEach((message) => {
|
|
297
|
+
const table = tablesMap.get(message.table);
|
|
298
|
+
if (table == null) {
|
|
299
|
+
tablesMap.set(message.table, {
|
|
300
|
+
name: message.table,
|
|
301
|
+
columns: [message.column],
|
|
302
|
+
});
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (table.columns.includes(message.column)) return;
|
|
306
|
+
tablesMap.set(message.table, {
|
|
307
|
+
name: message.table,
|
|
308
|
+
columns: table.columns.concat(message.column),
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
yield* _(ensureSchema(Array.from(tablesMap.values())));
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const applyMessages = ({
|
|
315
|
+
merkleTree,
|
|
316
|
+
messages,
|
|
317
|
+
}: {
|
|
318
|
+
merkleTree: MerkleTree;
|
|
319
|
+
messages: ReadonlyArray.NonEmptyReadonlyArray<Message>;
|
|
320
|
+
}): Effect.Effect<Sqlite, never, MerkleTree> =>
|
|
321
|
+
Effect.gen(function* (_) {
|
|
322
|
+
const sqlite = yield* _(Sqlite);
|
|
323
|
+
|
|
324
|
+
for (const message of messages) {
|
|
325
|
+
const timestamp: TimestampString | null = yield* _(
|
|
326
|
+
sqlite.exec({
|
|
327
|
+
sql: selectLastTimestampForTableRowColumn,
|
|
328
|
+
parameters: [message.table, message.row, message.column],
|
|
329
|
+
}),
|
|
330
|
+
Effect.map((result) => result.rows),
|
|
331
|
+
Effect.flatMap(ReadonlyArray.head),
|
|
332
|
+
Effect.map((row) => row.timestamp as TimestampString),
|
|
333
|
+
Effect.catchTag("NoSuchElementException", () => Effect.succeed(null)),
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
if (timestamp == null || timestamp < message.timestamp) {
|
|
337
|
+
const insert = sqlite.exec({
|
|
338
|
+
sql: insertValueIntoTableRowColumn(message.table, message.column),
|
|
339
|
+
parameters: [message.row, message.value, message.value],
|
|
340
|
+
});
|
|
341
|
+
yield* _(
|
|
342
|
+
insert,
|
|
343
|
+
someDefectToNoSuchTableOrColumnError,
|
|
344
|
+
Effect.catchTag("NoSuchTableOrColumnError", () =>
|
|
345
|
+
ensureSchemaByMessages(messages).pipe(Effect.flatMap(() => insert)),
|
|
346
|
+
),
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (timestamp == null || timestamp !== message.timestamp) {
|
|
351
|
+
const { changes } = yield* _(
|
|
352
|
+
sqlite.exec({
|
|
353
|
+
sql: insertIntoMessagesIfNew,
|
|
354
|
+
parameters: [
|
|
355
|
+
message.timestamp,
|
|
356
|
+
message.table,
|
|
357
|
+
message.row,
|
|
358
|
+
message.column,
|
|
359
|
+
message.value,
|
|
360
|
+
],
|
|
361
|
+
}),
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
if (changes > 0)
|
|
365
|
+
merkleTree = insertIntoMerkleTree(
|
|
366
|
+
unsafeTimestampFromString(message.timestamp),
|
|
367
|
+
)(merkleTree);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return merkleTree;
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
const writeTimestampAndMerkleTree = ({
|
|
375
|
+
timestamp,
|
|
376
|
+
merkleTree,
|
|
377
|
+
}: TimestampAndMerkleTree): Effect.Effect<Sqlite, never, void> =>
|
|
378
|
+
Effect.flatMap(Sqlite, (sqlite) =>
|
|
379
|
+
sqlite.exec({
|
|
380
|
+
sql: updateOwnerTimestampAndMerkleTree,
|
|
381
|
+
parameters: [
|
|
382
|
+
timestampToString(timestamp),
|
|
383
|
+
merkleTreeToString(merkleTree),
|
|
384
|
+
],
|
|
385
|
+
}),
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const mutate = ({
|
|
389
|
+
items,
|
|
390
|
+
queries,
|
|
391
|
+
}: DbWorkerInputMutate): Effect.Effect<
|
|
392
|
+
| Sqlite
|
|
393
|
+
| Owner
|
|
394
|
+
| Time
|
|
395
|
+
| Config
|
|
396
|
+
| RowsCacheRef
|
|
397
|
+
| DbWorkerOnMessage
|
|
398
|
+
| SyncWorkerPostMessage,
|
|
399
|
+
TimestampDriftError | TimestampCounterOverflowError,
|
|
400
|
+
void
|
|
401
|
+
> =>
|
|
402
|
+
Effect.gen(function* (_) {
|
|
403
|
+
const owner = yield* _(Owner);
|
|
404
|
+
let { timestamp, merkleTree } = yield* _(readTimestampAndMerkleTree);
|
|
405
|
+
|
|
406
|
+
const messages = yield* _(
|
|
407
|
+
mutateItemsToNewMessages(items),
|
|
408
|
+
Effect.forEach((newMessage) =>
|
|
409
|
+
Effect.map(sendTimestamp(timestamp), (nextTimestamp): Message => {
|
|
410
|
+
timestamp = nextTimestamp;
|
|
411
|
+
return {
|
|
412
|
+
...newMessage,
|
|
413
|
+
timestamp: timestampToString(timestamp),
|
|
414
|
+
};
|
|
415
|
+
}),
|
|
416
|
+
),
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
if (ReadonlyArray.isNonEmptyReadonlyArray(messages)) {
|
|
420
|
+
merkleTree = yield* _(applyMessages({ merkleTree, messages }));
|
|
421
|
+
yield* _(writeTimestampAndMerkleTree({ timestamp, merkleTree }));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const onCompleteIds = ReadonlyArray.filterMap(items, (item) =>
|
|
425
|
+
Option.fromNullable(item.onCompleteId),
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
if (queries.length > 0 || onCompleteIds.length > 0)
|
|
429
|
+
yield* _(query({ queries, onCompleteIds }));
|
|
430
|
+
|
|
431
|
+
const [config, syncWorkerPostMessage] = yield* _(
|
|
432
|
+
Effect.all([Config, SyncWorkerPostMessage]),
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
syncWorkerPostMessage({
|
|
436
|
+
_tag: "sync",
|
|
437
|
+
syncUrl: config.syncUrl,
|
|
438
|
+
messages,
|
|
439
|
+
timestamp,
|
|
440
|
+
merkleTree,
|
|
441
|
+
owner,
|
|
442
|
+
syncLoopCount: 0,
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
const handleSyncResponse = ({
|
|
447
|
+
messages,
|
|
448
|
+
...response
|
|
449
|
+
}: SyncWorkerOutputSyncResponse): Effect.Effect<
|
|
450
|
+
Sqlite | Time | Config | DbWorkerOnMessage | SyncWorkerPostMessage | Owner,
|
|
451
|
+
TimestampError,
|
|
452
|
+
void
|
|
453
|
+
> =>
|
|
454
|
+
Effect.gen(function* (_) {
|
|
455
|
+
let { timestamp, merkleTree } = yield* _(readTimestampAndMerkleTree);
|
|
456
|
+
|
|
457
|
+
if (ReadonlyArray.isNonEmptyReadonlyArray(messages)) {
|
|
458
|
+
for (const message of messages)
|
|
459
|
+
timestamp = yield* _(
|
|
460
|
+
unsafeTimestampFromString(message.timestamp),
|
|
461
|
+
(remote) => receiveTimestamp({ local: timestamp, remote }),
|
|
462
|
+
);
|
|
463
|
+
merkleTree = yield* _(applyMessages({ merkleTree, messages }));
|
|
464
|
+
yield* _(writeTimestampAndMerkleTree({ timestamp, merkleTree }));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const dbWorkerOnMessage = yield* _(DbWorkerOnMessage);
|
|
468
|
+
dbWorkerOnMessage({ _tag: "onReceive" });
|
|
469
|
+
|
|
470
|
+
const diff = diffMerkleTrees(response.merkleTree, merkleTree);
|
|
471
|
+
|
|
472
|
+
const syncWorkerPostMessage = yield* _(SyncWorkerPostMessage);
|
|
473
|
+
|
|
474
|
+
if (Option.isNone(diff)) {
|
|
475
|
+
syncWorkerPostMessage({ _tag: "syncCompleted" });
|
|
476
|
+
dbWorkerOnMessage({
|
|
477
|
+
_tag: "onSyncState",
|
|
478
|
+
state: {
|
|
479
|
+
_tag: "SyncStateIsSynced",
|
|
480
|
+
time: yield* _(Time.pipe(Effect.flatMap((time) => time.now))),
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const [sqlite, config, owner] = yield* _(
|
|
487
|
+
Effect.all([Sqlite, Config, Owner]),
|
|
488
|
+
);
|
|
489
|
+
const messagesToSync = yield* _(
|
|
490
|
+
sqlite.exec({
|
|
491
|
+
sql: selectMessagesToSync,
|
|
492
|
+
parameters: [timestampToString(makeSyncTimestamp(diff.value))],
|
|
493
|
+
}),
|
|
494
|
+
Effect.map(({ rows }) => rows as unknown as ReadonlyArray<Message>),
|
|
495
|
+
);
|
|
496
|
+
syncWorkerPostMessage({
|
|
497
|
+
_tag: "sync",
|
|
498
|
+
syncUrl: config.syncUrl,
|
|
499
|
+
messages: messagesToSync,
|
|
500
|
+
timestamp,
|
|
501
|
+
merkleTree,
|
|
502
|
+
owner,
|
|
503
|
+
syncLoopCount: response.syncLoopCount + 1,
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
const sync = ({
|
|
508
|
+
queries,
|
|
509
|
+
}: DbWorkerInputSync): Effect.Effect<
|
|
510
|
+
| Sqlite
|
|
511
|
+
| Config
|
|
512
|
+
| DbWorkerOnMessage
|
|
513
|
+
| SyncWorkerPostMessage
|
|
514
|
+
| Owner
|
|
515
|
+
| RowsCacheRef,
|
|
516
|
+
never,
|
|
517
|
+
void
|
|
518
|
+
> =>
|
|
519
|
+
Effect.gen(function* (_) {
|
|
520
|
+
if (queries.length > 0) yield* _(query({ queries }));
|
|
521
|
+
|
|
522
|
+
const [syncWorkerPostMessage, config, owner, timestampAndMerkleTree] =
|
|
523
|
+
yield* _(
|
|
524
|
+
Effect.all([
|
|
525
|
+
SyncWorkerPostMessage,
|
|
526
|
+
Config,
|
|
527
|
+
Owner,
|
|
528
|
+
readTimestampAndMerkleTree,
|
|
529
|
+
]),
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
syncWorkerPostMessage({
|
|
533
|
+
...timestampAndMerkleTree,
|
|
534
|
+
_tag: "sync",
|
|
535
|
+
syncUrl: config.syncUrl,
|
|
536
|
+
owner,
|
|
537
|
+
messages: [],
|
|
538
|
+
syncLoopCount: 0,
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
const reset = (
|
|
543
|
+
input: DbWorkerInputReset,
|
|
544
|
+
): Effect.Effect<Sqlite | Bip39 | NanoId | DbWorkerOnMessage, never, void> =>
|
|
545
|
+
Effect.gen(function* (_) {
|
|
546
|
+
const sqlite = yield* _(Sqlite);
|
|
547
|
+
|
|
548
|
+
yield* _(
|
|
549
|
+
sqlite.exec(`SELECT "name" FROM "sqlite_master" WHERE "type" = 'table'`),
|
|
550
|
+
Effect.map((result) => result.rows),
|
|
551
|
+
Effect.flatMap(
|
|
552
|
+
// The dropped table is completely removed from the database schema and
|
|
553
|
+
// the disk file. The table can not be recovered.
|
|
554
|
+
// All indices and triggers associated with the table are also deleted.
|
|
555
|
+
// https://sqlite.org/lang_droptable.html
|
|
556
|
+
Effect.forEach(
|
|
557
|
+
({ name }) => sqlite.exec(`DROP TABLE "${name as string}"`),
|
|
558
|
+
{ discard: true },
|
|
559
|
+
),
|
|
560
|
+
),
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
if (input.mnemonic) yield* _(lazyInit(input.mnemonic));
|
|
564
|
+
|
|
565
|
+
const onMessage = yield* _(DbWorkerOnMessage);
|
|
566
|
+
onMessage({ _tag: "onResetOrRestore" });
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
export const DbWorkerLive = Layer.effect(
|
|
570
|
+
DbWorker,
|
|
571
|
+
Effect.gen(function* (_) {
|
|
572
|
+
const syncWorker = yield* _(SyncWorker);
|
|
573
|
+
const rowsCacheRef = yield* _(Ref.make<RowsCacheMap>(new Map()));
|
|
574
|
+
|
|
575
|
+
const onError = (error: EvoluError): Effect.Effect<never, never, void> =>
|
|
576
|
+
Effect.sync(() => {
|
|
577
|
+
dbWorker.onMessage({ _tag: "onError", error });
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
syncWorker.onMessage = (output): void => {
|
|
581
|
+
switch (output._tag) {
|
|
582
|
+
case "UnexpectedError":
|
|
583
|
+
onError(output).pipe(Effect.runSync);
|
|
584
|
+
break;
|
|
585
|
+
case "SyncWorkerOutputSyncResponse":
|
|
586
|
+
postMessage(output);
|
|
587
|
+
break;
|
|
588
|
+
default:
|
|
589
|
+
dbWorker.onMessage({ _tag: "onSyncState", state: output });
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
const context = Context.empty().pipe(
|
|
594
|
+
Context.add(Sqlite, yield* _(Sqlite)),
|
|
595
|
+
Context.add(Bip39, yield* _(Bip39)),
|
|
596
|
+
Context.add(NanoId, yield* _(NanoId)),
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
const run = (
|
|
600
|
+
effect: Effect.Effect<Sqlite | Bip39 | NanoId, EvoluError, void>,
|
|
601
|
+
): Promise<void> =>
|
|
602
|
+
effect.pipe(
|
|
603
|
+
Effect.catchAllDefect(makeUnexpectedError),
|
|
604
|
+
transaction,
|
|
605
|
+
Effect.catchAll(onError),
|
|
606
|
+
Effect.provide(context),
|
|
607
|
+
Effect.runPromise,
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
type Write = (input: DbWorkerInput) => Promise<void>;
|
|
611
|
+
|
|
612
|
+
// Write for reset only in case init fails.
|
|
613
|
+
const writeForInitFail: Write = (input): Promise<void> => {
|
|
614
|
+
if (input._tag !== "reset") return Promise.resolve(undefined);
|
|
615
|
+
return reset(input).pipe(
|
|
616
|
+
Effect.provideService(DbWorkerOnMessage, dbWorker.onMessage),
|
|
617
|
+
Effect.provide(context),
|
|
618
|
+
run,
|
|
619
|
+
);
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
const makeWriteForInitSuccess = (owner: Owner, config: Config): Write => {
|
|
623
|
+
let skipAllBecauseOfReset = false;
|
|
624
|
+
|
|
625
|
+
const layer = Layer.mergeAll(
|
|
626
|
+
ConfigLive(config),
|
|
627
|
+
Layer.succeed(DbWorkerOnMessage, dbWorker.onMessage),
|
|
628
|
+
Layer.succeed(Owner, owner),
|
|
629
|
+
Layer.succeed(SyncWorkerPostMessage, syncWorker.postMessage),
|
|
630
|
+
Layer.succeed(RowsCacheRef, rowsCacheRef),
|
|
631
|
+
TimeLive,
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
return (input) => {
|
|
635
|
+
if (skipAllBecauseOfReset) return Promise.resolve(undefined);
|
|
636
|
+
|
|
637
|
+
return Match.value(input).pipe(
|
|
638
|
+
Match.tagsExhaustive({
|
|
639
|
+
init: () =>
|
|
640
|
+
makeUnexpectedError(new Error("init must be called once")),
|
|
641
|
+
query,
|
|
642
|
+
mutate,
|
|
643
|
+
sync,
|
|
644
|
+
reset: (input) => {
|
|
645
|
+
skipAllBecauseOfReset = true;
|
|
646
|
+
return reset(input);
|
|
647
|
+
},
|
|
648
|
+
ensureSchema: (input) => ensureSchema(input.tables),
|
|
649
|
+
SyncWorkerOutputSyncResponse: handleSyncResponse,
|
|
650
|
+
}),
|
|
651
|
+
Effect.provide(layer),
|
|
652
|
+
run,
|
|
653
|
+
);
|
|
654
|
+
};
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
let write: Write = (input) => {
|
|
658
|
+
if (input._tag !== "init")
|
|
659
|
+
return run(makeUnexpectedError(new Error("init must be called first")));
|
|
660
|
+
write = writeForInitFail;
|
|
661
|
+
return init(input).pipe(
|
|
662
|
+
Effect.map((owner) => {
|
|
663
|
+
dbWorker.onMessage({ _tag: "onOwner", owner });
|
|
664
|
+
write = makeWriteForInitSuccess(owner, input.config);
|
|
665
|
+
}),
|
|
666
|
+
run,
|
|
667
|
+
);
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
let messageQueue: Promise<void> = Promise.resolve(undefined);
|
|
671
|
+
|
|
672
|
+
const postMessage: DbWorker["postMessage"] = (input) => {
|
|
673
|
+
messageQueue = messageQueue.then(() => write(input));
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
const dbWorker: DbWorker = {
|
|
677
|
+
postMessage,
|
|
678
|
+
onMessage: Function.constVoid,
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
return dbWorker;
|
|
682
|
+
}),
|
|
683
|
+
);
|