@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
package/src/ErrorStore.ts DELETED
@@ -1,46 +0,0 @@
1
- import * as Effect from "effect/Effect";
2
- import { TimestampError } from "./Crdt.js";
3
- import { makeStore } from "./Store.js";
4
-
5
- export const makeErrorStore = makeStore<EvoluError | null>(null);
6
-
7
- /** The EvoluError type is used to represent errors that can occur in Evolu. */
8
- export type EvoluError = UnexpectedError | TimestampError;
9
-
10
- /**
11
- * The UnexpectedError interface is designed to represent errors that can occur
12
- * unexpectedly anywhere, even in third-party libraries, because Evolu uses
13
- * Effect to track all errors.
14
- */
15
- export interface UnexpectedError {
16
- readonly _tag: "UnexpectedError";
17
- readonly error: TransferableError;
18
- }
19
-
20
- export const makeUnexpectedError = (
21
- error: unknown,
22
- ): Effect.Effect<never, UnexpectedError> => {
23
- const isError = error instanceof Error;
24
-
25
- return Effect.fail({
26
- _tag: "UnexpectedError",
27
- error: {
28
- message: isError ? error.message : String(error),
29
- stack: isError ? error.stack : undefined,
30
- },
31
- });
32
- };
33
-
34
- /**
35
- * We can't use the whole error because of WebWorker postMessage DataCloneError
36
- * in Safari and Firefox. TODO:
37
- * https://discord.com/channels/795981131316985866/795983589644304396/1096736473396564079
38
- */
39
- interface TransferableError {
40
- readonly message: string;
41
- readonly stack: string | undefined;
42
- }
43
-
44
- export interface EvoluTypeError<E extends string> {
45
- readonly __evoluTypeError__: E;
46
- }
@@ -1,51 +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 * as Option from "effect/Option";
6
- import * as ReadonlyArray from "effect/ReadonlyArray";
7
- import { NanoId, NanoIdGenerator } from "./Crypto.js";
8
-
9
- export interface OnCompletes {
10
- readonly add: (onComplete: OnComplete) => Effect.Effect<OnCompleteId>;
11
-
12
- readonly complete: (
13
- onCompleteIds: readonly OnCompleteId[],
14
- ) => Effect.Effect<void>;
15
- }
16
-
17
- export type OnComplete = () => void;
18
-
19
- export type OnCompleteId = NanoId & Brand.Brand<"OnCompleteId">;
20
-
21
- export const OnCompletes = Context.GenericTag<OnCompletes>(
22
- "@services/OnCompletes",
23
- );
24
-
25
- export const OnCompletesLive = Layer.effect(
26
- OnCompletes,
27
- Effect.gen(function* (_) {
28
- const { nanoid } = yield* _(NanoIdGenerator);
29
- const map = new Map<OnCompleteId, OnComplete>();
30
-
31
- return OnCompletes.of({
32
- add: (onComplete) =>
33
- nanoid.pipe(
34
- Effect.map((nanoid) => {
35
- const id = nanoid as OnCompleteId;
36
- map.set(id, onComplete);
37
- return id;
38
- }),
39
- ),
40
-
41
- complete: (onCompleteIds) =>
42
- Effect.sync(() => {
43
- ReadonlyArray.filterMap(onCompleteIds, (id) => {
44
- const onComplete = map.get(id);
45
- map.delete(id);
46
- return Option.fromNullable(onComplete);
47
- }).forEach((onComplete) => onComplete());
48
- }),
49
- });
50
- }),
51
- );
package/src/SyncWorker.ts DELETED
@@ -1,361 +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 Equivalence from "effect/Equivalence";
7
- import * as Function from "effect/Function";
8
- import { absurd, identity } from "effect/Function";
9
- import * as Layer from "effect/Layer";
10
- import * as Match from "effect/Match";
11
- import * as Option from "effect/Option";
12
- import * as Predicate from "effect/Predicate";
13
- import * as ReadonlyArray from "effect/ReadonlyArray";
14
- import {
15
- MerkleTree,
16
- Millis,
17
- Timestamp,
18
- TimestampString,
19
- merkleTreeToString,
20
- unsafeMerkleTreeFromString,
21
- } from "./Crdt.js";
22
- import { SecretBox } from "./Crypto.js";
23
- import { UnexpectedError, makeUnexpectedError } from "./ErrorStore.js";
24
- import { Id } from "./Model.js";
25
- import { Owner } from "./Owner.js";
26
- import { Fetch, SyncLock } from "./Platform.js";
27
- import {
28
- EncryptedMessage,
29
- MessageContent,
30
- SyncRequest,
31
- SyncResponse,
32
- } from "./Protobuf.js";
33
- import { JsonObjectOrArray, Value } from "./Sqlite.js";
34
- import { Messaging } from "./Types.js";
35
-
36
- export interface SyncWorker
37
- extends Messaging<SyncWorkerInput, SyncWorkerOutput> {}
38
- export const SyncWorker = Context.GenericTag<SyncWorker>(
39
- "@services/SyncWorker",
40
- );
41
-
42
- export type SyncWorkerPostMessage = SyncWorker["postMessage"];
43
-
44
- export const SyncWorkerPostMessage = Context.GenericTag<SyncWorkerPostMessage>(
45
- "@services/SyncWorkerPostMessage",
46
- );
47
-
48
- export type SyncWorkerInput =
49
- | SyncWorkerInputSync
50
- | SyncWorkerInputSyncCompleted;
51
-
52
- interface SyncWorkerInputSync {
53
- readonly _tag: "sync";
54
- readonly syncUrl: string;
55
- readonly messages: ReadonlyArray<Message>;
56
- readonly merkleTree: MerkleTree;
57
- readonly timestamp: Timestamp;
58
- readonly owner: Owner;
59
- readonly syncLoopCount: number;
60
- }
61
-
62
- export interface NewMessage {
63
- readonly table: string;
64
- readonly row: Id;
65
- readonly column: string;
66
- readonly value: Value;
67
- }
68
-
69
- export const NewMessageEquivalence: Equivalence.Equivalence<NewMessage> = (
70
- a,
71
- b,
72
- ) =>
73
- a.table === b.table &&
74
- a.row === b.row &&
75
- a.column === b.column &&
76
- JSON.stringify(a.value) === JSON.stringify(b.value);
77
-
78
- export interface Message extends NewMessage {
79
- readonly timestamp: TimestampString;
80
- }
81
-
82
- interface SyncWorkerInputSyncCompleted {
83
- readonly _tag: "syncCompleted";
84
- }
85
-
86
- type SyncWorkerOnMessage = SyncWorker["onMessage"];
87
-
88
- const SyncWorkerOnMessage = Context.GenericTag<SyncWorkerOnMessage>(
89
- "@services/SyncWorkerOnMessage",
90
- );
91
-
92
- export type SyncWorkerOutput =
93
- | UnexpectedError
94
- | SyncWorkerOutputSyncResponse
95
- | SyncStateIsNotSyncedError
96
- | SyncStateIsSyncing;
97
-
98
- export type SyncWorkerOutputSyncResponse = {
99
- readonly _tag: "SyncWorkerOutputSyncResponse";
100
- readonly messages: ReadonlyArray<Message>;
101
- readonly merkleTree: MerkleTree;
102
- readonly syncLoopCount: number;
103
- };
104
-
105
- /**
106
- * The SyncState type represents the various states that a synchronization
107
- * process can be in. Evolu apps should not bother users with an "unsynced"
108
- * state. Instead, they should warn users if data have not been synced for too
109
- * long (a week, for example).
110
- */
111
- export type SyncState =
112
- | SyncStateInitial
113
- | SyncStateIsSyncing
114
- | SyncStateIsSynced
115
- | SyncStateIsNotSyncedError;
116
-
117
- export interface SyncStateInitial {
118
- readonly _tag: "SyncStateInitial";
119
- }
120
-
121
- export interface SyncStateIsSyncing {
122
- readonly _tag: "SyncStateIsSyncing";
123
- }
124
-
125
- export interface SyncStateIsSynced {
126
- readonly _tag: "SyncStateIsSynced";
127
- readonly time: Millis;
128
- }
129
-
130
- export interface SyncStateIsNotSyncedError {
131
- readonly _tag: "SyncStateIsNotSyncedError";
132
- readonly error:
133
- | SyncStateNetworkError
134
- | SyncStateServerError
135
- | SyncStatePaymentRequiredError;
136
- }
137
-
138
- export interface SyncStateNetworkError {
139
- readonly _tag: "NetworkError";
140
- }
141
-
142
- export interface SyncStateServerError {
143
- readonly _tag: "ServerError";
144
- readonly status: number;
145
- }
146
-
147
- export interface SyncStatePaymentRequiredError {
148
- readonly _tag: "PaymentRequiredError";
149
- }
150
-
151
- const version1 = new Uint8Array([0, 1]);
152
-
153
- const valueToProtobuf = (value: Value): MessageContent["value"] => {
154
- switch (typeof value) {
155
- case "string":
156
- return { oneofKind: "stringValue", stringValue: value };
157
- case "number":
158
- return {
159
- oneofKind: "numberValue",
160
- numberValue: S.encodeSync(S.NumberFromString)(value),
161
- };
162
- }
163
- if (value == null) return { oneofKind: undefined };
164
- if (Predicate.isUint8Array(value))
165
- return { oneofKind: "bytesValue", bytesValue: value };
166
- return { oneofKind: "jsonValue", jsonValue: JSON.stringify(value) };
167
- };
168
-
169
- const valueFromProtobuf = (value: MessageContent["value"]): Value => {
170
- switch (value.oneofKind) {
171
- case "numberValue":
172
- return S.decodeSync(S.NumberFromString)(value.numberValue);
173
- case "stringValue":
174
- return value.stringValue;
175
- case "bytesValue":
176
- return value.bytesValue;
177
- case "jsonValue":
178
- return JSON.parse(value.jsonValue) as JsonObjectOrArray;
179
- case undefined:
180
- return null;
181
- default:
182
- return absurd(value);
183
- }
184
- };
185
-
186
- const newMessageToBinary = ({ value, ...rest }: NewMessage): Uint8Array =>
187
- concatBytes(
188
- version1,
189
- MessageContent.toBinary(
190
- { value: valueToProtobuf(value), ...rest },
191
- binaryWriteOptions,
192
- ),
193
- );
194
-
195
- const startsWithArray = (array: Uint8Array, prefix: Uint8Array): boolean => {
196
- if (prefix.length > array.length) return false;
197
- for (let i = 0; i < prefix.length; i++) {
198
- if (array[i] !== prefix[i]) return false;
199
- }
200
- return true;
201
- };
202
-
203
- const newMessageFromBinary = (
204
- binary: Uint8Array,
205
- ): Option.Option<NewMessage> => {
206
- if (!startsWithArray(binary, version1)) return Option.none();
207
- const { value, ...content } = MessageContent.fromBinary(
208
- binary.slice(version1.length),
209
- binaryReadOptions,
210
- );
211
- return Option.some({ value: valueFromProtobuf(value), ...content });
212
- };
213
-
214
- // The 'protobuf-ts' uses TextEncoder, but polyfill fast-text-encoding
215
- // doesn't support the fatal option.
216
- // https://github.com/timostamm/protobuf-ts/issues/184#issuecomment-1658443836
217
- const binaryWriteOptions = {
218
- writerFactory: (): BinaryWriter =>
219
- new BinaryWriter({
220
- encode: (input: string): Uint8Array => new TextEncoder().encode(input),
221
- }),
222
- };
223
- const binaryReadOptions = {
224
- readerFactory: (bytes: Uint8Array): BinaryReader =>
225
- new BinaryReader(bytes, {
226
- decode: (input: Uint8Array): string => new TextDecoder().decode(input),
227
- }),
228
- };
229
-
230
- const sync = (
231
- input: SyncWorkerInputSync,
232
- ): Effect.Effect<
233
- void,
234
- never,
235
- SyncLock | SyncWorkerOnMessage | Fetch | SecretBox
236
- > =>
237
- Effect.gen(function* (_) {
238
- const syncLock = yield* _(SyncLock);
239
- const syncWorkerOnMessage = yield* _(SyncWorkerOnMessage);
240
- const fetch = yield* _(Fetch);
241
- const secretBox = yield* _(SecretBox);
242
-
243
- if (input.syncLoopCount === 0) {
244
- if (!(yield* _(syncLock.acquire))) return;
245
- syncWorkerOnMessage({ _tag: "SyncStateIsSyncing" });
246
- }
247
-
248
- yield* _(
249
- Effect.forEach(input.messages, ({ timestamp, ...newMessage }) =>
250
- secretBox
251
- .seal(input.owner.encryptionKey, newMessageToBinary(newMessage))
252
- .pipe(
253
- Effect.map((content): EncryptedMessage => ({ timestamp, content })),
254
- ),
255
- ),
256
- Effect.map((messages) =>
257
- SyncRequest.toBinary(
258
- {
259
- messages,
260
- userId: input.owner.id,
261
- nodeId: input.timestamp.node,
262
- merkleTree: merkleTreeToString(input.merkleTree),
263
- },
264
- binaryWriteOptions,
265
- ),
266
- ),
267
- Effect.flatMap((body) => fetch(input.syncUrl, body)),
268
- Effect.catchTag("FetchError", () =>
269
- Effect.fail<SyncStateIsNotSyncedError>({
270
- _tag: "SyncStateIsNotSyncedError",
271
- error: { _tag: "NetworkError" },
272
- }),
273
- ),
274
- Effect.flatMap((response) => {
275
- switch (response.status) {
276
- case 402:
277
- return Effect.fail<SyncStateIsNotSyncedError>({
278
- _tag: "SyncStateIsNotSyncedError",
279
- error: { _tag: "PaymentRequiredError" },
280
- });
281
- case 200:
282
- return Effect.promise(() =>
283
- response
284
- .arrayBuffer()
285
- .then((buffer) => new Uint8Array(buffer))
286
- .then((array) =>
287
- SyncResponse.fromBinary(array, binaryReadOptions),
288
- ),
289
- );
290
- default:
291
- return Effect.fail<SyncStateIsNotSyncedError>({
292
- _tag: "SyncStateIsNotSyncedError",
293
- error: { _tag: "ServerError", status: response.status },
294
- });
295
- }
296
- }),
297
- Effect.flatMap((syncResponse) =>
298
- Effect.forEach(syncResponse.messages, ({ timestamp, content }) =>
299
- secretBox
300
- .open(input.owner.encryptionKey, content)
301
- .pipe(
302
- Effect.map(newMessageFromBinary),
303
- Effect.map(
304
- Option.map(
305
- (newMessage): Message => ({ timestamp, ...newMessage }),
306
- ),
307
- ),
308
- ),
309
- ).pipe(
310
- Effect.map(
311
- (messages): SyncWorkerOutputSyncResponse => ({
312
- _tag: "SyncWorkerOutputSyncResponse",
313
- messages: ReadonlyArray.filterMap(messages, identity),
314
- merkleTree: unsafeMerkleTreeFromString(syncResponse.merkleTree),
315
- syncLoopCount: input.syncLoopCount,
316
- }),
317
- ),
318
- ),
319
- ),
320
- Effect.tapError(() => syncLock.release),
321
- Effect.merge,
322
- Effect.map(syncWorkerOnMessage),
323
- );
324
- });
325
-
326
- export const SyncWorkerCommonLive = Layer.effect(
327
- SyncWorker,
328
- Effect.gen(function* (_) {
329
- const syncLock = yield* _(SyncLock);
330
-
331
- const onError = (error: UnexpectedError): Effect.Effect<void> =>
332
- Effect.sync(() => {
333
- syncWorker.onMessage(error);
334
- });
335
-
336
- const context = Context.empty().pipe(
337
- Context.add(SyncLock, syncLock),
338
- Context.add(Fetch, yield* _(Fetch)),
339
- Context.add(SecretBox, yield* _(SecretBox)),
340
- );
341
-
342
- const syncWorker: SyncWorker = {
343
- postMessage: (input) => {
344
- Match.value(input).pipe(
345
- Match.tagsExhaustive({
346
- sync,
347
- syncCompleted: () => syncLock.release,
348
- }),
349
- Effect.catchAllDefect(makeUnexpectedError),
350
- Effect.catchAll(onError),
351
- Effect.provide(context),
352
- Effect.provideService(SyncWorkerOnMessage, syncWorker.onMessage),
353
- Effect.runPromise,
354
- );
355
- },
356
- onMessage: Function.constVoid,
357
- };
358
-
359
- return syncWorker;
360
- }),
361
- );
package/src/Types.ts DELETED
@@ -1,11 +0,0 @@
1
- /**
2
- * Add types for WebWorker and BroadcastChannel. Messages must have a `_tag`
3
- * property.
4
- */
5
- export interface Messaging<
6
- Input extends { _tag: string },
7
- Output extends { _tag: string },
8
- > {
9
- readonly postMessage: (input: Input) => void;
10
- onMessage: (output: Output) => void;
11
- }