@evolu/common 4.1.1 → 5.0.1

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 (78) hide show
  1. package/README.md +1 -1
  2. package/dist/src/Config.d.ts +27 -30
  3. package/dist/src/Config.d.ts.map +1 -1
  4. package/dist/src/Config.js +32 -5
  5. package/dist/src/Crdt.d.ts +5 -4
  6. package/dist/src/Crdt.d.ts.map +1 -1
  7. package/dist/src/Crdt.js +27 -24
  8. package/dist/src/Crypto.d.ts +16 -10
  9. package/dist/src/Crypto.d.ts.map +1 -1
  10. package/dist/src/Crypto.js +24 -11
  11. package/dist/src/Db.d.ts +58 -78
  12. package/dist/src/Db.d.ts.map +1 -1
  13. package/dist/src/Db.js +368 -170
  14. package/dist/src/Diff.d.ts +9 -3
  15. package/dist/src/Diff.d.ts.map +1 -1
  16. package/dist/src/Diff.js +12 -11
  17. package/dist/src/Error.d.ts +15 -0
  18. package/dist/src/Error.d.ts.map +1 -0
  19. package/dist/src/Error.js +14 -0
  20. package/dist/src/Evolu.d.ts +139 -91
  21. package/dist/src/Evolu.d.ts.map +1 -1
  22. package/dist/src/Evolu.js +267 -239
  23. package/dist/src/Model.d.ts +52 -0
  24. package/dist/src/Model.d.ts.map +1 -1
  25. package/dist/src/Model.js +47 -5
  26. package/dist/src/Owner.d.ts.map +1 -1
  27. package/dist/src/Owner.js +14 -8
  28. package/dist/src/Platform.d.ts +24 -34
  29. package/dist/src/Platform.d.ts.map +1 -1
  30. package/dist/src/Platform.js +9 -31
  31. package/dist/src/Public.d.ts +3 -5
  32. package/dist/src/Public.d.ts.map +1 -1
  33. package/dist/src/Public.js +2 -3
  34. package/dist/src/Sqlite.d.ts +25 -16
  35. package/dist/src/Sqlite.d.ts.map +1 -1
  36. package/dist/src/Sqlite.js +28 -20
  37. package/dist/src/Store.d.ts.map +1 -1
  38. package/dist/src/Sync.d.ts +70 -0
  39. package/dist/src/Sync.d.ts.map +1 -0
  40. package/dist/src/Sync.js +126 -0
  41. package/dist/src/index.d.ts +2 -5
  42. package/dist/src/index.d.ts.map +1 -1
  43. package/dist/src/index.js +2 -5
  44. package/package.json +12 -10
  45. package/src/Config.ts +77 -36
  46. package/src/Crdt.ts +61 -76
  47. package/src/Crypto.ts +83 -60
  48. package/src/Db.ts +782 -352
  49. package/src/Diff.ts +23 -21
  50. package/src/Error.ts +29 -0
  51. package/src/Evolu.ts +599 -499
  52. package/src/Model.ts +102 -6
  53. package/src/Owner.ts +24 -24
  54. package/src/Platform.ts +33 -81
  55. package/src/Public.ts +3 -5
  56. package/src/Sqlite.ts +81 -44
  57. package/src/Sync.ts +306 -0
  58. package/src/index.ts +2 -5
  59. package/dist/src/DbWorker.d.ts +0 -82
  60. package/dist/src/DbWorker.d.ts.map +0 -1
  61. package/dist/src/DbWorker.js +0 -335
  62. package/dist/src/ErrorStore.d.ts +0 -29
  63. package/dist/src/ErrorStore.d.ts.map +0 -1
  64. package/dist/src/ErrorStore.js +0 -13
  65. package/dist/src/OnCompletes.d.ts +0 -14
  66. package/dist/src/OnCompletes.d.ts.map +0 -1
  67. package/dist/src/OnCompletes.js +0 -25
  68. package/dist/src/SyncWorker.d.ts +0 -80
  69. package/dist/src/SyncWorker.d.ts.map +0 -1
  70. package/dist/src/SyncWorker.js +0 -150
  71. package/dist/src/Types.d.ts +0 -13
  72. package/dist/src/Types.d.ts.map +0 -1
  73. package/dist/src/Types.js +0 -1
  74. package/src/DbWorker.ts +0 -721
  75. package/src/ErrorStore.ts +0 -46
  76. package/src/OnCompletes.ts +0 -51
  77. package/src/SyncWorker.ts +0 -361
  78. package/src/Types.ts +0 -11
@@ -1,335 +0,0 @@
1
- import * as Context from "effect/Context";
2
- import * as Effect from "effect/Effect";
3
- import * as Function from "effect/Function";
4
- import { pipe } from "effect/Function";
5
- import * as Layer from "effect/Layer";
6
- import * as Match from "effect/Match";
7
- import * as Option from "effect/Option";
8
- import * as ReadonlyArray from "effect/ReadonlyArray";
9
- import { Config, ConfigLive } from "./Config.js";
10
- import { Time, TimeLive, diffMerkleTrees, insertIntoMerkleTree, makeSyncTimestamp, merkleTreeToString, receiveTimestamp, sendTimestamp, timestampToString, unsafeTimestampFromString, } from "./Crdt.js";
11
- import { Bip39, NanoIdGenerator } from "./Crypto.js";
12
- import { RowsStore, RowsStoreLive, deserializeQuery, dropAllTables, ensureSchema, lazyInit, someDefectToNoSuchTableOrColumnError, transaction, } from "./Db.js";
13
- import { makePatches } from "./Diff.js";
14
- import { makeUnexpectedError } from "./ErrorStore.js";
15
- import { cast } from "./Model.js";
16
- import { Owner } from "./Owner.js";
17
- import { DbWorkerLock } from "./Platform.js";
18
- import * as Sql from "./Sql.js";
19
- import { Sqlite, drawSqliteQueryPlan as drawExplainQueryPlan, } from "./Sqlite.js";
20
- import { NewMessageEquivalence, SyncWorker, SyncWorkerPostMessage, } from "./SyncWorker.js";
21
- export const DbWorker = Context.GenericTag("@services/DbWorker");
22
- const DbWorkerOnMessage = Context.GenericTag("@services/DbWorkerOnMessage");
23
- const init = Effect.gen(function* (_) {
24
- const sqlite = yield* _(Sqlite);
25
- return yield* _(sqlite.exec(Sql.selectOwner), Effect.map(({ rows: [row] }) => ({
26
- id: row.id,
27
- mnemonic: row.mnemonic,
28
- encryptionKey: row.encryptionKey,
29
- })), someDefectToNoSuchTableOrColumnError, Effect.catchTag("NoSuchTableOrColumnError", () => lazyInit()));
30
- });
31
- const query = ({ queries, onCompleteIds = [], }) => Effect.gen(function* (_) {
32
- const sqlite = yield* _(Sqlite);
33
- const rowsStore = yield* _(RowsStore);
34
- const dbWorkerOnMessage = yield* _(DbWorkerOnMessage);
35
- const queriesRows = yield* _(ReadonlyArray.dedupe(queries), Effect.forEach((query) => {
36
- const sqliteQuery = deserializeQuery(query);
37
- return sqlite.exec(sqliteQuery).pipe(Effect.map((result) => [query, result.rows]), Effect.tap(() => {
38
- if (!sqliteQuery.options?.logExplainQueryPlan)
39
- return;
40
- return sqlite
41
- .exec({
42
- ...sqliteQuery,
43
- sql: `EXPLAIN QUERY PLAN ${sqliteQuery.sql}`,
44
- })
45
- .pipe(Effect.tap(() => Effect.log("ExplainQueryPlan")), Effect.tap(({ rows }) => {
46
- // Not using Effect.log because of formating
47
- // eslint-disable-next-line no-console
48
- console.log(drawExplainQueryPlan(rows));
49
- }));
50
- }));
51
- }));
52
- const previous = rowsStore.getState();
53
- yield* _(rowsStore.setState(new Map([...previous, ...queriesRows])));
54
- const queriesPatches = queriesRows.map(([query, rows]) => ({
55
- query,
56
- patches: makePatches(previous.get(query), rows),
57
- }));
58
- dbWorkerOnMessage({ _tag: "onQuery", queriesPatches, onCompleteIds });
59
- });
60
- const readTimestampAndMerkleTree = Sqlite.pipe(Effect.flatMap((sqlite) => sqlite.exec(Sql.selectOwnerTimestampAndMerkleTree)), Effect.map(({ rows: [{ timestamp, merkleTree }] }) => ({
61
- timestamp: unsafeTimestampFromString(timestamp),
62
- merkleTree: merkleTree,
63
- })));
64
- export const mutationsToNewMessages = (mutations) => pipe(mutations, ReadonlyArray.map(({ id, isInsert, table, values }) => pipe(Object.entries(values),
65
- // Filter values.
66
- ReadonlyArray.filterMap(([key, value]) =>
67
- // The value can be undefined if exactOptionalPropertyTypes isn't true.
68
- // Don't insert nulls because null is the default value.
69
- value === undefined || (isInsert && value == null)
70
- ? Option.none()
71
- : Option.some([key, value])),
72
- // Cast values.
73
- ReadonlyArray.map(([key, value]) => [
74
- key,
75
- typeof value === "boolean"
76
- ? cast(value)
77
- : value instanceof Date
78
- ? cast(value)
79
- : value,
80
- ]), ReadonlyArray.map(([key, value]) => ({
81
- table,
82
- row: id,
83
- column: key,
84
- value,
85
- })))), (a) => a.flat(), ReadonlyArray.dedupeWith(NewMessageEquivalence));
86
- const ensureSchemaByNewMessages = (messages) => Effect.gen(function* (_) {
87
- const tablesMap = new Map();
88
- messages.forEach((message) => {
89
- const table = tablesMap.get(message.table);
90
- if (table == null) {
91
- tablesMap.set(message.table, {
92
- name: message.table,
93
- columns: [message.column, "createdAt", "updatedAt"],
94
- });
95
- return;
96
- }
97
- if (table.columns.includes(message.column))
98
- return;
99
- tablesMap.set(message.table, {
100
- name: message.table,
101
- columns: table.columns.concat(message.column),
102
- });
103
- });
104
- const tables = Array.from(tablesMap.values());
105
- yield* _(ensureSchema({ tables, indexes: [] }));
106
- });
107
- export const upsertValueIntoTableRowColumn = (message, messages, millis) => Effect.gen(function* (_) {
108
- const sqlite = yield* _(Sqlite);
109
- const createdAtOrUpdatedAt = cast(new Date(millis));
110
- const insert = sqlite.exec({
111
- sql: `
112
- insert into
113
- "${message.table}" ("id", "${message.column}", "createdAt", "updatedAt")
114
- values
115
- (?, ?, ?, ?)
116
- on conflict do update set
117
- "${message.column}" = ?,
118
- "updatedAt" = ?
119
- `.trim(),
120
- parameters: [
121
- message.row,
122
- message.value,
123
- createdAtOrUpdatedAt,
124
- createdAtOrUpdatedAt,
125
- message.value,
126
- createdAtOrUpdatedAt,
127
- ],
128
- });
129
- yield* _(insert, someDefectToNoSuchTableOrColumnError, Effect.catchTag("NoSuchTableOrColumnError", () =>
130
- // If one message fails, we ensure schema for all messages.
131
- ensureSchemaByNewMessages(messages).pipe(Effect.zipRight(insert))));
132
- });
133
- const applyMessages = ({ merkleTree, messages, }) => Effect.gen(function* (_) {
134
- const sqlite = yield* _(Sqlite);
135
- for (const message of messages) {
136
- const timestamp = yield* _(sqlite.exec({
137
- ...Sql.selectLastTimestampForTableRowColumn,
138
- parameters: [message.table, message.row, message.column, 1],
139
- }), Effect.map((result) => result.rows), Effect.flatMap(ReadonlyArray.head), Effect.map((row) => row.timestamp), Effect.catchTag("NoSuchElementException", () => Effect.succeed(null)));
140
- if (timestamp == null || timestamp < message.timestamp) {
141
- const { millis } = unsafeTimestampFromString(message.timestamp);
142
- yield* _(upsertValueIntoTableRowColumn(message, messages, millis));
143
- }
144
- if (timestamp == null || timestamp !== message.timestamp) {
145
- const { changes } = yield* _(sqlite.exec({
146
- ...Sql.insertIntoMessagesIfNew,
147
- parameters: [
148
- message.timestamp,
149
- message.table,
150
- message.row,
151
- message.column,
152
- message.value,
153
- ],
154
- }));
155
- if (changes > 0) {
156
- const timestamp = unsafeTimestampFromString(message.timestamp);
157
- merkleTree = insertIntoMerkleTree(timestamp)(merkleTree);
158
- }
159
- }
160
- }
161
- return merkleTree;
162
- });
163
- const writeTimestampAndMerkleTree = ({ timestamp, merkleTree, }) => Effect.flatMap(Sqlite, (sqlite) => sqlite.exec({
164
- ...Sql.updateOwnerTimestampAndMerkleTree,
165
- parameters: [
166
- merkleTreeToString(merkleTree),
167
- timestampToString(timestamp),
168
- ],
169
- }));
170
- const mutate = ({ mutations, queries, }) => Effect.gen(function* (_) {
171
- const [toSync, localOnly] = ReadonlyArray.partition(mutations, (item) => item.table.startsWith("_"));
172
- const [toUpsert, toDelete] = ReadonlyArray.partition(localOnly, (item) => mutationsToNewMessages([item]).some((message) => message.column === "isDeleted" && message.value === 1)).map(mutationsToNewMessages);
173
- const time = yield* _(Time);
174
- for (const messageToUpsert of toUpsert) {
175
- const now = yield* _(time.now);
176
- yield* _(upsertValueIntoTableRowColumn(messageToUpsert, toUpsert, now));
177
- }
178
- const { exec } = yield* _(Sqlite);
179
- yield* _(Effect.forEach(toDelete, ({ table, row }) => exec({
180
- sql: `delete from "${table}" where "id" = ?;`,
181
- parameters: [row],
182
- })));
183
- if (toSync.length > 0) {
184
- let { timestamp, merkleTree } = yield* _(readTimestampAndMerkleTree);
185
- const messages = yield* _(mutationsToNewMessages(toSync), Effect.forEach((message) => Effect.map(sendTimestamp(timestamp), (nextTimestamp) => {
186
- timestamp = nextTimestamp;
187
- return { ...message, timestamp: timestampToString(timestamp) };
188
- })));
189
- merkleTree = yield* _(applyMessages({ merkleTree, messages }));
190
- yield* _(writeTimestampAndMerkleTree({ timestamp, merkleTree }));
191
- (yield* _(SyncWorkerPostMessage))({
192
- _tag: "sync",
193
- syncUrl: (yield* _(Config)).syncUrl,
194
- messages,
195
- timestamp,
196
- merkleTree,
197
- owner: yield* _(Owner),
198
- syncLoopCount: 0,
199
- });
200
- }
201
- const onCompleteIds = ReadonlyArray.filterMap(mutations, (item) => Option.fromNullable(item.onCompleteId));
202
- if (queries.length > 0 || onCompleteIds.length > 0)
203
- yield* _(query({ queries, onCompleteIds }));
204
- });
205
- const handleSyncResponse = ({ messages, ...response }) => Effect.gen(function* (_) {
206
- let { timestamp, merkleTree } = yield* _(readTimestampAndMerkleTree);
207
- const dbWorkerOnMessage = yield* _(DbWorkerOnMessage);
208
- if (messages.length > 0) {
209
- for (const message of messages)
210
- timestamp = yield* _(unsafeTimestampFromString(message.timestamp), (remote) => receiveTimestamp({ local: timestamp, remote }));
211
- merkleTree = yield* _(applyMessages({ merkleTree, messages }));
212
- yield* _(writeTimestampAndMerkleTree({ timestamp, merkleTree }));
213
- dbWorkerOnMessage({ _tag: "onReceive" });
214
- }
215
- const diff = diffMerkleTrees(response.merkleTree, merkleTree);
216
- const syncWorkerPostMessage = yield* _(SyncWorkerPostMessage);
217
- if (Option.isNone(diff)) {
218
- syncWorkerPostMessage({ _tag: "syncCompleted" });
219
- dbWorkerOnMessage({
220
- _tag: "onSyncState",
221
- state: {
222
- _tag: "SyncStateIsSynced",
223
- time: yield* _(Time.pipe(Effect.flatMap((time) => time.now))),
224
- },
225
- });
226
- return;
227
- }
228
- const sqlite = yield* _(Sqlite);
229
- const config = yield* _(Config);
230
- const owner = yield* _(Owner);
231
- const messagesToSync = yield* _(sqlite.exec({
232
- ...Sql.selectMessagesToSync,
233
- parameters: [timestampToString(makeSyncTimestamp(diff.value))],
234
- }), Effect.map(({ rows }) => rows));
235
- if (response.syncLoopCount > 100) {
236
- // TODO: dbWorkerOnMessage({ _tag: "onError" });
237
- // eslint-disable-next-line no-console
238
- console.error("Evolu: syncLoopCount > 100");
239
- return;
240
- }
241
- syncWorkerPostMessage({
242
- _tag: "sync",
243
- syncUrl: config.syncUrl,
244
- messages: messagesToSync,
245
- timestamp,
246
- merkleTree,
247
- owner,
248
- syncLoopCount: response.syncLoopCount + 1,
249
- });
250
- });
251
- const sync = ({ queries, }) => Effect.gen(function* (_) {
252
- if (queries.length > 0)
253
- yield* _(query({ queries }));
254
- const syncWorkerPostMessage = yield* _(SyncWorkerPostMessage);
255
- syncWorkerPostMessage({
256
- _tag: "sync",
257
- ...(yield* _(readTimestampAndMerkleTree)),
258
- syncUrl: (yield* _(Config)).syncUrl,
259
- owner: yield* _(Owner),
260
- messages: [],
261
- syncLoopCount: 0,
262
- });
263
- });
264
- const reset = (input) => Effect.gen(function* (_) {
265
- yield* _(dropAllTables);
266
- if (input.mnemonic)
267
- yield* _(lazyInit(input.mnemonic));
268
- const onMessage = yield* _(DbWorkerOnMessage);
269
- onMessage({ _tag: "onResetOrRestore" });
270
- });
271
- export const DbWorkerCommonLive = Layer.effect(DbWorker, Effect.gen(function* (_) {
272
- const syncWorker = yield* _(SyncWorker);
273
- const onError = (error) => Effect.sync(() => {
274
- dbWorker.onMessage({ _tag: "onError", error });
275
- });
276
- syncWorker.onMessage = (output) => {
277
- switch (output._tag) {
278
- case "UnexpectedError":
279
- onError(output).pipe(Effect.runSync);
280
- break;
281
- case "SyncWorkerOutputSyncResponse":
282
- dbWorker.postMessage(output);
283
- break;
284
- default:
285
- dbWorker.onMessage({ _tag: "onSyncState", state: output });
286
- }
287
- };
288
- const runContext = Context.empty().pipe(Context.add(Sqlite, yield* _(Sqlite)), Context.add(Bip39, yield* _(Bip39)), Context.add(NanoIdGenerator, yield* _(NanoIdGenerator)), Context.add(DbWorkerOnMessage, (output) => {
289
- dbWorker.onMessage(output);
290
- }));
291
- const run = (effect) => effect.pipe(Effect.catchAllDefect(makeUnexpectedError), transaction, Effect.catchAll(onError), Effect.provide(runContext), Effect.runPromise);
292
- /** If init fails, we have to allow reset at least. */
293
- const handleInputForInitFail = (input) => {
294
- if (input._tag !== "reset")
295
- return Promise.resolve(undefined);
296
- return reset(input).pipe(run);
297
- };
298
- const makeHandleInputForInitSuccess = (config, owner) => {
299
- let skipAllBecauseOfReset = false;
300
- const layer = Layer.mergeAll(ConfigLive(config), Layer.succeed(Owner, owner), Layer.succeed(SyncWorkerPostMessage, syncWorker.postMessage), RowsStoreLive, TimeLive).pipe(Layer.memoize, Effect.scoped, Effect.runSync);
301
- return (input) => {
302
- if (skipAllBecauseOfReset)
303
- return Promise.resolve(undefined);
304
- return Match.value(input).pipe(Match.tagsExhaustive({
305
- init: () => makeUnexpectedError(new Error("init must be called once")),
306
- query,
307
- mutate,
308
- sync,
309
- reset: (input) => {
310
- skipAllBecauseOfReset = true;
311
- return reset(input);
312
- },
313
- ensureSchema,
314
- SyncWorkerOutputSyncResponse: handleSyncResponse,
315
- }), Effect.provide(layer), run);
316
- };
317
- };
318
- let handleInput = (input) => {
319
- if (input._tag !== "init")
320
- return run(makeUnexpectedError(new Error("init must be called first")));
321
- handleInput = handleInputForInitFail;
322
- return init.pipe(Effect.map((owner) => {
323
- dbWorker.onMessage({ _tag: "onOwner", owner });
324
- handleInput = makeHandleInputForInitSuccess(input.config, owner);
325
- }), run);
326
- };
327
- const dbWorkerLock = yield* _(DbWorkerLock);
328
- const dbWorker = {
329
- postMessage: (input) => {
330
- dbWorkerLock(() => handleInput(input));
331
- },
332
- onMessage: Function.constVoid,
333
- };
334
- return dbWorker;
335
- }));
@@ -1,29 +0,0 @@
1
- import * as Effect from "effect/Effect";
2
- import { TimestampError } from "./Crdt.js";
3
- export declare const makeErrorStore: Effect.Effect<import("./Store.js").Store<EvoluError | null>, never, never>;
4
- /** The EvoluError type is used to represent errors that can occur in Evolu. */
5
- export type EvoluError = UnexpectedError | TimestampError;
6
- /**
7
- * The UnexpectedError interface is designed to represent errors that can occur
8
- * unexpectedly anywhere, even in third-party libraries, because Evolu uses
9
- * Effect to track all errors.
10
- */
11
- export interface UnexpectedError {
12
- readonly _tag: "UnexpectedError";
13
- readonly error: TransferableError;
14
- }
15
- export declare const makeUnexpectedError: (error: unknown) => Effect.Effect<never, UnexpectedError>;
16
- /**
17
- * We can't use the whole error because of WebWorker postMessage DataCloneError
18
- * in Safari and Firefox. TODO:
19
- * https://discord.com/channels/795981131316985866/795983589644304396/1096736473396564079
20
- */
21
- interface TransferableError {
22
- readonly message: string;
23
- readonly stack: string | undefined;
24
- }
25
- export interface EvoluTypeError<E extends string> {
26
- readonly __evoluTypeError__: E;
27
- }
28
- export {};
29
- //# sourceMappingURL=ErrorStore.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ErrorStore.d.ts","sourceRoot":"","sources":["../../src/ErrorStore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAG3C,eAAO,MAAM,cAAc,4EAAqC,CAAC;AAEjE,+EAA+E;AAC/E,MAAM,MAAM,UAAU,GAAG,eAAe,GAAG,cAAc,CAAC;AAE1D;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC;CACnC;AAED,eAAO,MAAM,mBAAmB,UACvB,OAAO,KACb,aAAa,CAAC,KAAK,EAAE,eAAe,CAUtC,CAAC;AAEF;;;;GAIG;AACH,UAAU,iBAAiB;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;CACpC;AAED,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,MAAM;IAC9C,QAAQ,CAAC,kBAAkB,EAAE,CAAC,CAAC;CAChC"}
@@ -1,13 +0,0 @@
1
- import * as Effect from "effect/Effect";
2
- import { makeStore } from "./Store.js";
3
- export const makeErrorStore = makeStore(null);
4
- export const makeUnexpectedError = (error) => {
5
- const isError = error instanceof Error;
6
- return Effect.fail({
7
- _tag: "UnexpectedError",
8
- error: {
9
- message: isError ? error.message : String(error),
10
- stack: isError ? error.stack : undefined,
11
- },
12
- });
13
- };
@@ -1,14 +0,0 @@
1
- import * as Brand from "effect/Brand";
2
- import * as Context from "effect/Context";
3
- import * as Effect from "effect/Effect";
4
- import * as Layer from "effect/Layer";
5
- import { NanoId, NanoIdGenerator } from "./Crypto.js";
6
- export interface OnCompletes {
7
- readonly add: (onComplete: OnComplete) => Effect.Effect<OnCompleteId>;
8
- readonly complete: (onCompleteIds: readonly OnCompleteId[]) => Effect.Effect<void>;
9
- }
10
- export type OnComplete = () => void;
11
- export type OnCompleteId = NanoId & Brand.Brand<"OnCompleteId">;
12
- export declare const OnCompletes: Context.Tag<OnCompletes, OnCompletes>;
13
- export declare const OnCompletesLive: Layer.Layer<OnCompletes, never, NanoIdGenerator>;
14
- //# sourceMappingURL=OnCompletes.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"OnCompletes.d.ts","sourceRoot":"","sources":["../../src/OnCompletes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAC;AAC1C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AAGtC,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEtD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,GAAG,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IAEtE,QAAQ,CAAC,QAAQ,EAAE,CACjB,aAAa,EAAE,SAAS,YAAY,EAAE,KACnC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,MAAM,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC;AAEpC,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;AAEhE,eAAO,MAAM,WAAW,uCAEvB,CAAC;AAEF,eAAO,MAAM,eAAe,kDA0B3B,CAAC"}
@@ -1,25 +0,0 @@
1
- import * as Context from "effect/Context";
2
- import * as Effect from "effect/Effect";
3
- import * as Layer from "effect/Layer";
4
- import * as Option from "effect/Option";
5
- import * as ReadonlyArray from "effect/ReadonlyArray";
6
- import { NanoIdGenerator } from "./Crypto.js";
7
- export const OnCompletes = Context.GenericTag("@services/OnCompletes");
8
- export const OnCompletesLive = Layer.effect(OnCompletes, Effect.gen(function* (_) {
9
- const { nanoid } = yield* _(NanoIdGenerator);
10
- const map = new Map();
11
- return OnCompletes.of({
12
- add: (onComplete) => nanoid.pipe(Effect.map((nanoid) => {
13
- const id = nanoid;
14
- map.set(id, onComplete);
15
- return id;
16
- })),
17
- complete: (onCompleteIds) => Effect.sync(() => {
18
- ReadonlyArray.filterMap(onCompleteIds, (id) => {
19
- const onComplete = map.get(id);
20
- map.delete(id);
21
- return Option.fromNullable(onComplete);
22
- }).forEach((onComplete) => onComplete());
23
- }),
24
- });
25
- }));
@@ -1,80 +0,0 @@
1
- import * as Context from "effect/Context";
2
- import * as Equivalence from "effect/Equivalence";
3
- import * as Layer from "effect/Layer";
4
- import { MerkleTree, Millis, Timestamp, TimestampString } from "./Crdt.js";
5
- import { SecretBox } from "./Crypto.js";
6
- import { UnexpectedError } from "./ErrorStore.js";
7
- import { Id } from "./Model.js";
8
- import { Owner } from "./Owner.js";
9
- import { Fetch, SyncLock } from "./Platform.js";
10
- import { Value } from "./Sqlite.js";
11
- import { Messaging } from "./Types.js";
12
- export interface SyncWorker extends Messaging<SyncWorkerInput, SyncWorkerOutput> {
13
- }
14
- export declare const SyncWorker: Context.Tag<SyncWorker, SyncWorker>;
15
- export type SyncWorkerPostMessage = SyncWorker["postMessage"];
16
- export declare const SyncWorkerPostMessage: Context.Tag<(input: SyncWorkerInput) => void, (input: SyncWorkerInput) => void>;
17
- export type SyncWorkerInput = SyncWorkerInputSync | SyncWorkerInputSyncCompleted;
18
- interface SyncWorkerInputSync {
19
- readonly _tag: "sync";
20
- readonly syncUrl: string;
21
- readonly messages: ReadonlyArray<Message>;
22
- readonly merkleTree: MerkleTree;
23
- readonly timestamp: Timestamp;
24
- readonly owner: Owner;
25
- readonly syncLoopCount: number;
26
- }
27
- export interface NewMessage {
28
- readonly table: string;
29
- readonly row: Id;
30
- readonly column: string;
31
- readonly value: Value;
32
- }
33
- export declare const NewMessageEquivalence: Equivalence.Equivalence<NewMessage>;
34
- export interface Message extends NewMessage {
35
- readonly timestamp: TimestampString;
36
- }
37
- interface SyncWorkerInputSyncCompleted {
38
- readonly _tag: "syncCompleted";
39
- }
40
- export type SyncWorkerOutput = UnexpectedError | SyncWorkerOutputSyncResponse | SyncStateIsNotSyncedError | SyncStateIsSyncing;
41
- export type SyncWorkerOutputSyncResponse = {
42
- readonly _tag: "SyncWorkerOutputSyncResponse";
43
- readonly messages: ReadonlyArray<Message>;
44
- readonly merkleTree: MerkleTree;
45
- readonly syncLoopCount: number;
46
- };
47
- /**
48
- * The SyncState type represents the various states that a synchronization
49
- * process can be in. Evolu apps should not bother users with an "unsynced"
50
- * state. Instead, they should warn users if data have not been synced for too
51
- * long (a week, for example).
52
- */
53
- export type SyncState = SyncStateInitial | SyncStateIsSyncing | SyncStateIsSynced | SyncStateIsNotSyncedError;
54
- export interface SyncStateInitial {
55
- readonly _tag: "SyncStateInitial";
56
- }
57
- export interface SyncStateIsSyncing {
58
- readonly _tag: "SyncStateIsSyncing";
59
- }
60
- export interface SyncStateIsSynced {
61
- readonly _tag: "SyncStateIsSynced";
62
- readonly time: Millis;
63
- }
64
- export interface SyncStateIsNotSyncedError {
65
- readonly _tag: "SyncStateIsNotSyncedError";
66
- readonly error: SyncStateNetworkError | SyncStateServerError | SyncStatePaymentRequiredError;
67
- }
68
- export interface SyncStateNetworkError {
69
- readonly _tag: "NetworkError";
70
- }
71
- export interface SyncStateServerError {
72
- readonly _tag: "ServerError";
73
- readonly status: number;
74
- }
75
- export interface SyncStatePaymentRequiredError {
76
- readonly _tag: "PaymentRequiredError";
77
- }
78
- export declare const SyncWorkerCommonLive: Layer.Layer<SyncWorker, never, SecretBox | SyncLock | Fetch>;
79
- export {};
80
- //# sourceMappingURL=SyncWorker.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"SyncWorker.d.ts","sourceRoot":"","sources":["../../src/SyncWorker.ts"],"names":[],"mappings":"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"}
@@ -1,150 +0,0 @@
1
- import * as S from "@effect/schema/Schema";
2
- import { concatBytes } from "@noble/ciphers/utils";
3
- import { BinaryReader, BinaryWriter } from "@protobuf-ts/runtime";
4
- import * as Context from "effect/Context";
5
- import * as Effect from "effect/Effect";
6
- import * as Function from "effect/Function";
7
- import { absurd, identity } from "effect/Function";
8
- import * as Layer from "effect/Layer";
9
- import * as Match from "effect/Match";
10
- import * as Option from "effect/Option";
11
- import * as Predicate from "effect/Predicate";
12
- import * as ReadonlyArray from "effect/ReadonlyArray";
13
- import { merkleTreeToString, unsafeMerkleTreeFromString, } from "./Crdt.js";
14
- import { SecretBox } from "./Crypto.js";
15
- import { makeUnexpectedError } from "./ErrorStore.js";
16
- import { Fetch, SyncLock } from "./Platform.js";
17
- import { MessageContent, SyncRequest, SyncResponse, } from "./Protobuf.js";
18
- export const SyncWorker = Context.GenericTag("@services/SyncWorker");
19
- export const SyncWorkerPostMessage = Context.GenericTag("@services/SyncWorkerPostMessage");
20
- export const NewMessageEquivalence = (a, b) => a.table === b.table &&
21
- a.row === b.row &&
22
- a.column === b.column &&
23
- JSON.stringify(a.value) === JSON.stringify(b.value);
24
- const SyncWorkerOnMessage = Context.GenericTag("@services/SyncWorkerOnMessage");
25
- const version1 = new Uint8Array([0, 1]);
26
- const valueToProtobuf = (value) => {
27
- switch (typeof value) {
28
- case "string":
29
- return { oneofKind: "stringValue", stringValue: value };
30
- case "number":
31
- return {
32
- oneofKind: "numberValue",
33
- numberValue: S.encodeSync(S.NumberFromString)(value),
34
- };
35
- }
36
- if (value == null)
37
- return { oneofKind: undefined };
38
- if (Predicate.isUint8Array(value))
39
- return { oneofKind: "bytesValue", bytesValue: value };
40
- return { oneofKind: "jsonValue", jsonValue: JSON.stringify(value) };
41
- };
42
- const valueFromProtobuf = (value) => {
43
- switch (value.oneofKind) {
44
- case "numberValue":
45
- return S.decodeSync(S.NumberFromString)(value.numberValue);
46
- case "stringValue":
47
- return value.stringValue;
48
- case "bytesValue":
49
- return value.bytesValue;
50
- case "jsonValue":
51
- return JSON.parse(value.jsonValue);
52
- case undefined:
53
- return null;
54
- default:
55
- return absurd(value);
56
- }
57
- };
58
- const newMessageToBinary = ({ value, ...rest }) => concatBytes(version1, MessageContent.toBinary({ value: valueToProtobuf(value), ...rest }, binaryWriteOptions));
59
- const startsWithArray = (array, prefix) => {
60
- if (prefix.length > array.length)
61
- return false;
62
- for (let i = 0; i < prefix.length; i++) {
63
- if (array[i] !== prefix[i])
64
- return false;
65
- }
66
- return true;
67
- };
68
- const newMessageFromBinary = (binary) => {
69
- if (!startsWithArray(binary, version1))
70
- return Option.none();
71
- const { value, ...content } = MessageContent.fromBinary(binary.slice(version1.length), binaryReadOptions);
72
- return Option.some({ value: valueFromProtobuf(value), ...content });
73
- };
74
- // The 'protobuf-ts' uses TextEncoder, but polyfill fast-text-encoding
75
- // doesn't support the fatal option.
76
- // https://github.com/timostamm/protobuf-ts/issues/184#issuecomment-1658443836
77
- const binaryWriteOptions = {
78
- writerFactory: () => new BinaryWriter({
79
- encode: (input) => new TextEncoder().encode(input),
80
- }),
81
- };
82
- const binaryReadOptions = {
83
- readerFactory: (bytes) => new BinaryReader(bytes, {
84
- decode: (input) => new TextDecoder().decode(input),
85
- }),
86
- };
87
- const sync = (input) => Effect.gen(function* (_) {
88
- const syncLock = yield* _(SyncLock);
89
- const syncWorkerOnMessage = yield* _(SyncWorkerOnMessage);
90
- const fetch = yield* _(Fetch);
91
- const secretBox = yield* _(SecretBox);
92
- if (input.syncLoopCount === 0) {
93
- if (!(yield* _(syncLock.acquire)))
94
- return;
95
- syncWorkerOnMessage({ _tag: "SyncStateIsSyncing" });
96
- }
97
- yield* _(Effect.forEach(input.messages, ({ timestamp, ...newMessage }) => secretBox
98
- .seal(input.owner.encryptionKey, newMessageToBinary(newMessage))
99
- .pipe(Effect.map((content) => ({ timestamp, content })))), Effect.map((messages) => SyncRequest.toBinary({
100
- messages,
101
- userId: input.owner.id,
102
- nodeId: input.timestamp.node,
103
- merkleTree: merkleTreeToString(input.merkleTree),
104
- }, binaryWriteOptions)), Effect.flatMap((body) => fetch(input.syncUrl, body)), Effect.catchTag("FetchError", () => Effect.fail({
105
- _tag: "SyncStateIsNotSyncedError",
106
- error: { _tag: "NetworkError" },
107
- })), Effect.flatMap((response) => {
108
- switch (response.status) {
109
- case 402:
110
- return Effect.fail({
111
- _tag: "SyncStateIsNotSyncedError",
112
- error: { _tag: "PaymentRequiredError" },
113
- });
114
- case 200:
115
- return Effect.promise(() => response
116
- .arrayBuffer()
117
- .then((buffer) => new Uint8Array(buffer))
118
- .then((array) => SyncResponse.fromBinary(array, binaryReadOptions)));
119
- default:
120
- return Effect.fail({
121
- _tag: "SyncStateIsNotSyncedError",
122
- error: { _tag: "ServerError", status: response.status },
123
- });
124
- }
125
- }), Effect.flatMap((syncResponse) => Effect.forEach(syncResponse.messages, ({ timestamp, content }) => secretBox
126
- .open(input.owner.encryptionKey, content)
127
- .pipe(Effect.map(newMessageFromBinary), Effect.map(Option.map((newMessage) => ({ timestamp, ...newMessage }))))).pipe(Effect.map((messages) => ({
128
- _tag: "SyncWorkerOutputSyncResponse",
129
- messages: ReadonlyArray.filterMap(messages, identity),
130
- merkleTree: unsafeMerkleTreeFromString(syncResponse.merkleTree),
131
- syncLoopCount: input.syncLoopCount,
132
- })))), Effect.tapError(() => syncLock.release), Effect.merge, Effect.map(syncWorkerOnMessage));
133
- });
134
- export const SyncWorkerCommonLive = Layer.effect(SyncWorker, Effect.gen(function* (_) {
135
- const syncLock = yield* _(SyncLock);
136
- const onError = (error) => Effect.sync(() => {
137
- syncWorker.onMessage(error);
138
- });
139
- const context = Context.empty().pipe(Context.add(SyncLock, syncLock), Context.add(Fetch, yield* _(Fetch)), Context.add(SecretBox, yield* _(SecretBox)));
140
- const syncWorker = {
141
- postMessage: (input) => {
142
- Match.value(input).pipe(Match.tagsExhaustive({
143
- sync,
144
- syncCompleted: () => syncLock.release,
145
- }), Effect.catchAllDefect(makeUnexpectedError), Effect.catchAll(onError), Effect.provide(context), Effect.provideService(SyncWorkerOnMessage, syncWorker.onMessage), Effect.runPromise);
146
- },
147
- onMessage: Function.constVoid,
148
- };
149
- return syncWorker;
150
- }));