@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
@@ -0,0 +1,136 @@
1
+ import { concatBytes } from "@noble/ciphers/utils";
2
+ import { BinaryReader, BinaryWriter } from "@protobuf-ts/runtime";
3
+ import { Context, Effect, Function, Layer, Match, Option, Predicate, ReadonlyArray, absurd, identity, } from "effect";
4
+ import { SecretBox } from "./Crypto.js";
5
+ import { makeUnexpectedError } from "./Errors.js";
6
+ import { merkleTreeToString, unsafeMerkleTreeFromString, } from "./MerkleTree.js";
7
+ import { Fetch, SyncLock } from "./Platform.js";
8
+ import { MessageContent, SyncRequest, SyncResponse, } from "./Protobuf.js";
9
+ export const SyncWorker = Context.Tag("evolu/SyncWorker");
10
+ export const SyncWorkerPostMessage = Context.Tag("evolu/SyncWorkerPostMessage");
11
+ const SyncWorkerOnMessage = Context.Tag("evolu/SyncWorkerOnMessage");
12
+ const version1 = new Uint8Array([0, 1]);
13
+ const valueToProtobuf = (value) => {
14
+ switch (typeof value) {
15
+ case "string":
16
+ return { oneofKind: "stringValue", stringValue: value };
17
+ case "number":
18
+ return { oneofKind: "numberValue", numberValue: value };
19
+ }
20
+ if (value == null)
21
+ return { oneofKind: undefined };
22
+ if (Predicate.isUint8Array(value))
23
+ return { oneofKind: "bytesValue", bytesValue: value };
24
+ return { oneofKind: "jsonValue", jsonValue: JSON.stringify(value) };
25
+ };
26
+ const valueFromProtobuf = (value) => {
27
+ switch (value.oneofKind) {
28
+ case "numberValue":
29
+ return value.numberValue;
30
+ case "stringValue":
31
+ return value.stringValue;
32
+ case "bytesValue":
33
+ return value.bytesValue;
34
+ case "jsonValue":
35
+ return JSON.parse(value.jsonValue);
36
+ case undefined:
37
+ return null;
38
+ default:
39
+ return absurd(value);
40
+ }
41
+ };
42
+ const newMessageToBinary = ({ value, ...rest }) => concatBytes(version1, MessageContent.toBinary({ value: valueToProtobuf(value), ...rest }, binaryWriteOptions));
43
+ const startsWithArray = (array, prefix) => {
44
+ if (prefix.length > array.length)
45
+ return false;
46
+ for (let i = 0; i < prefix.length; i++) {
47
+ if (array[i] !== prefix[i])
48
+ return false;
49
+ }
50
+ return true;
51
+ };
52
+ const newMessageFromBinary = (binary) => {
53
+ if (!startsWithArray(binary, version1))
54
+ return Option.none();
55
+ const { value, ...content } = MessageContent.fromBinary(binary.slice(version1.length), binaryReadOptions);
56
+ return Option.some({ value: valueFromProtobuf(value), ...content });
57
+ };
58
+ // The 'protobuf-ts' uses TextEncoder, but polyfill fast-text-encoding
59
+ // doesn't support the fatal option.
60
+ // https://github.com/timostamm/protobuf-ts/issues/184#issuecomment-1658443836
61
+ const binaryWriteOptions = {
62
+ writerFactory: () => new BinaryWriter({
63
+ encode: (input) => new TextEncoder().encode(input),
64
+ }),
65
+ };
66
+ const binaryReadOptions = {
67
+ readerFactory: (bytes) => new BinaryReader(bytes, {
68
+ decode: (input) => new TextDecoder().decode(input),
69
+ }),
70
+ };
71
+ const sync = (input) => Effect.gen(function* (_) {
72
+ const syncLock = yield* _(SyncLock);
73
+ const syncWorkerOnMessage = yield* _(SyncWorkerOnMessage);
74
+ const fetch = yield* _(Fetch);
75
+ const secretBox = yield* _(SecretBox);
76
+ if (input.syncLoopCount === 0) {
77
+ if (!(yield* _(syncLock.acquire)))
78
+ return;
79
+ syncWorkerOnMessage({ _tag: "SyncStateIsSyncing" });
80
+ }
81
+ yield* _(Effect.forEach(input.messages, ({ timestamp, ...newMessage }) => secretBox
82
+ .seal(input.owner.encryptionKey, newMessageToBinary(newMessage))
83
+ .pipe(Effect.map((content) => ({ timestamp, content })))), Effect.map((messages) => SyncRequest.toBinary({
84
+ messages,
85
+ userId: input.owner.id,
86
+ nodeId: input.timestamp.node,
87
+ merkleTree: merkleTreeToString(input.merkleTree),
88
+ }, binaryWriteOptions)), Effect.flatMap((body) => fetch(input.syncUrl, body)), Effect.catchTag("FetchError", () => Effect.fail({
89
+ _tag: "SyncStateIsNotSyncedError",
90
+ error: { _tag: "NetworkError" },
91
+ })), Effect.flatMap((response) => {
92
+ switch (response.status) {
93
+ case 402:
94
+ return Effect.fail({
95
+ _tag: "SyncStateIsNotSyncedError",
96
+ error: { _tag: "PaymentRequiredError" },
97
+ });
98
+ case 200:
99
+ return Effect.promise(() => response
100
+ .arrayBuffer()
101
+ .then((buffer) => new Uint8Array(buffer))
102
+ .then((array) => SyncResponse.fromBinary(array, binaryReadOptions)));
103
+ default:
104
+ return Effect.fail({
105
+ _tag: "SyncStateIsNotSyncedError",
106
+ error: { _tag: "ServerError", status: response.status },
107
+ });
108
+ }
109
+ }), Effect.flatMap((syncResponse) => Effect.forEach(syncResponse.messages, ({ timestamp, content }) => secretBox
110
+ .open(input.owner.encryptionKey, content)
111
+ .pipe(Effect.map(newMessageFromBinary), Effect.map(Option.map((newMessage) => ({ timestamp, ...newMessage }))))).pipe(Effect.map((messages) => ({
112
+ _tag: "SyncWorkerOutputSyncResponse",
113
+ messages: ReadonlyArray.filterMap(messages, identity),
114
+ merkleTree: unsafeMerkleTreeFromString(syncResponse.merkleTree),
115
+ syncLoopCount: input.syncLoopCount,
116
+ })))), Effect.tapError(() => syncLock.release), Effect.merge, Effect.map(syncWorkerOnMessage));
117
+ });
118
+ export const SyncWorkerLive = Layer.effect(SyncWorker, Effect.gen(function* (_) {
119
+ const syncLock = yield* _(SyncLock);
120
+ const fetch = yield* _(Fetch);
121
+ const secretBox = yield* _(SecretBox);
122
+ const onError = (error) => Effect.sync(() => {
123
+ syncWorker.onMessage(error);
124
+ });
125
+ const postMessage = (input) => {
126
+ void Match.value(input).pipe(Match.tagsExhaustive({
127
+ sync,
128
+ syncCompleted: () => syncLock.release,
129
+ }), Effect.catchAllDefect(makeUnexpectedError), Effect.catchAll(onError), Effect.provideService(SyncLock, syncLock), Effect.provideService(Fetch, fetch), Effect.provideService(SecretBox, secretBox), Effect.provideService(SyncWorkerOnMessage, syncWorker.onMessage), Effect.runPromise);
130
+ };
131
+ const syncWorker = {
132
+ postMessage,
133
+ onMessage: Function.constVoid,
134
+ };
135
+ return syncWorker;
136
+ }));
@@ -0,0 +1,44 @@
1
+ import * as Schema from "@effect/schema/Schema";
2
+ import { Brand, Context, Effect, Layer } from "effect";
3
+ import { Config } from "./Config.js";
4
+ import { NanoId, NodeId } from "./Crypto.js";
5
+ export interface Timestamp {
6
+ readonly node: NodeId;
7
+ readonly millis: Millis;
8
+ readonly counter: Counter;
9
+ }
10
+ export declare const Millis: Schema.BrandSchema<number, number & Brand.Brand<"Millis">>;
11
+ export type Millis = Schema.Schema.To<typeof Millis>;
12
+ export declare const Counter: Schema.BrandSchema<number, number & Brand.Brand<"Counter">>;
13
+ export type Counter = Schema.Schema.To<typeof Counter>;
14
+ export type TimestampHash = number & Brand.Brand<"TimestampHash">;
15
+ export type TimestampString = string & Brand.Brand<"TimestampString">;
16
+ export declare const timestampToString: (t: Timestamp) => TimestampString;
17
+ export declare const unsafeTimestampFromString: (s: TimestampString) => Timestamp;
18
+ export declare const timestampToHash: (t: Timestamp) => TimestampHash;
19
+ export declare const makeSyncTimestamp: (millis?: Millis) => Timestamp;
20
+ export declare const makeInitialTimestamp: Effect.Effect<NanoId, never, Timestamp>;
21
+ export interface Time {
22
+ readonly now: Effect.Effect<never, never, Millis>;
23
+ }
24
+ export declare const Time: Context.Tag<Time, Time>;
25
+ export declare const TimeLive: Layer.Layer<never, never, Time>;
26
+ export type TimestampError = TimestampDriftError | TimestampCounterOverflowError | TimestampDuplicateNodeError;
27
+ export interface TimestampDriftError {
28
+ readonly _tag: "TimestampDriftError";
29
+ readonly next: Millis;
30
+ readonly now: Millis;
31
+ }
32
+ export interface TimestampCounterOverflowError {
33
+ readonly _tag: "TimestampCounterOverflowError";
34
+ }
35
+ export declare const sendTimestamp: (timestamp: Timestamp) => Effect.Effect<Time | Config, TimestampDriftError | TimestampCounterOverflowError, Timestamp>;
36
+ export interface TimestampDuplicateNodeError {
37
+ readonly _tag: "TimestampDuplicateNodeError";
38
+ readonly node: NodeId;
39
+ }
40
+ export declare const receiveTimestamp: ({ local, remote, }: {
41
+ readonly local: Timestamp;
42
+ readonly remote: Timestamp;
43
+ }) => Effect.Effect<Time | Config, TimestampDriftError | TimestampCounterOverflowError | TimestampDuplicateNodeError, Timestamp>;
44
+ //# sourceMappingURL=Timestamp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Timestamp.d.ts","sourceRoot":"","sources":["../../src/Timestamp.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,uBAAuB,CAAC;AAChD,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAU,KAAK,EAAgB,MAAM,QAAQ,CAAC;AAC7E,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAM7C,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,eAAO,MAAM,MAAM,4DAGlB,CAAC;AACF,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,MAAM,CAAC,CAAC;AAIrD,eAAO,MAAM,OAAO,6DAGnB,CAAC;AACF,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,OAAO,CAAC,CAAC;AAIvD,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;AAElE,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;AAEtE,eAAO,MAAM,iBAAiB,MAAO,SAAS,KAAG,eAKjB,CAAC;AAEjC,eAAO,MAAM,yBAAyB,MAAO,eAAe,KAAG,SAO9D,CAAC;AAEF,eAAO,MAAM,eAAe,MAAO,SAAS,KAAG,aACI,CAAC;AAIpD,eAAO,MAAM,iBAAiB,YACpB,MAAM,KACb,SAID,CAAC;AAEH,eAAO,MAAM,oBAAoB,yCAShC,CAAC;AAEF,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;CACnD;AAED,eAAO,MAAM,IAAI,yBAAkC,CAAC;AAEpD,eAAO,MAAM,QAAQ,iCAKpB,CAAC;AAEF,MAAM,MAAM,cAAc,GACtB,mBAAmB,GACnB,6BAA6B,GAC7B,2BAA2B,CAAC;AAEhC,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,IAAI,EAAE,+BAA+B,CAAC;CAChD;AAmCD,eAAO,MAAM,aAAa,cACb,SAAS,KACnB,aAAa,CACd,IAAI,GAAG,MAAM,EACb,mBAAmB,GAAG,6BAA6B,EACnD,SAAS,CASP,CAAC;AAEL,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,IAAI,EAAE,6BAA6B,CAAC;IAC7C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,eAAO,MAAM,gBAAgB;oBAIX,SAAS;qBACR,SAAS;MACxB,aAAa,CACf,IAAI,GAAG,MAAM,EACX,mBAAmB,GACnB,6BAA6B,GAC7B,2BAA2B,EAC7B,SAAS,CAuBP,CAAC"}
@@ -0,0 +1,76 @@
1
+ import * as Schema from "@effect/schema/Schema";
2
+ import { Context, Effect, Either, Layer, Number, pipe } from "effect";
3
+ import { Config } from "./Config.js";
4
+ import { NanoId, NodeId } from "./Crypto.js";
5
+ import { murmurhash } from "./Murmurhash.js";
6
+ export const Millis = Schema.number.pipe(Schema.greaterThanOrEqualTo(0), Schema.brand("Millis"));
7
+ const initialMillis = Schema.parseSync(Millis)(0);
8
+ export const Counter = Schema.number.pipe(Schema.between(0, 65535), Schema.brand("Counter"));
9
+ const initialCounter = Schema.parseSync(Counter)(0);
10
+ export const timestampToString = (t) => [
11
+ new Date(t.millis).toISOString(),
12
+ t.counter.toString(16).toUpperCase().padStart(4, "0"),
13
+ t.node,
14
+ ].join("-");
15
+ export const unsafeTimestampFromString = (s) => {
16
+ const a = s.split("-");
17
+ return {
18
+ millis: Date.parse(a.slice(0, 3).join("-")).valueOf(),
19
+ counter: parseInt(a[3], 16),
20
+ node: a[4],
21
+ };
22
+ };
23
+ export const timestampToHash = (t) => murmurhash(timestampToString(t));
24
+ const syncNodeId = Schema.parseSync(NodeId)("0000000000000000");
25
+ export const makeSyncTimestamp = (millis = initialMillis) => ({
26
+ millis,
27
+ counter: initialCounter,
28
+ node: syncNodeId,
29
+ });
30
+ export const makeInitialTimestamp = NanoId.pipe(Effect.flatMap(({ nanoidAsNodeId }) => nanoidAsNodeId), Effect.map((node) => ({
31
+ millis: initialMillis,
32
+ counter: initialCounter,
33
+ node,
34
+ })));
35
+ export const Time = Context.Tag("evolu/Time");
36
+ export const TimeLive = Layer.succeed(Time, Time.of({
37
+ now: Effect.sync(() => Date.now()),
38
+ }));
39
+ const getNextMillis = (millis) => Effect.gen(function* (_) {
40
+ const time = yield* _(Time);
41
+ const config = yield* _(Config);
42
+ const now = yield* _(time.now);
43
+ const next = Math.max(now, ...millis);
44
+ if (next - now > config.maxDrift)
45
+ yield* _(Effect.fail({
46
+ _tag: "TimestampDriftError",
47
+ now,
48
+ next,
49
+ }));
50
+ return next;
51
+ });
52
+ const incrementCounter = (counter) => pipe(Number.increment(counter), Schema.parseEither(Counter), Either.mapLeft(() => ({ _tag: "TimestampCounterOverflowError" })));
53
+ const counterMin = Schema.parseSync(Counter)(0);
54
+ export const sendTimestamp = (timestamp) => Effect.gen(function* (_) {
55
+ const millis = yield* _(getNextMillis([timestamp.millis]));
56
+ const counter = millis === timestamp.millis
57
+ ? yield* _(incrementCounter(timestamp.counter))
58
+ : counterMin;
59
+ return { ...timestamp, millis, counter };
60
+ });
61
+ export const receiveTimestamp = ({ local, remote, }) => Effect.gen(function* (_) {
62
+ if (local.node === remote.node)
63
+ yield* _(Effect.fail({
64
+ _tag: "TimestampDuplicateNodeError",
65
+ node: local.node,
66
+ }));
67
+ const millis = yield* _(getNextMillis([local.millis, remote.millis]));
68
+ const counter = yield* _(millis === local.millis && millis === remote.millis
69
+ ? incrementCounter(Math.max(local.counter, remote.counter))
70
+ : millis === local.millis
71
+ ? incrementCounter(local.counter)
72
+ : millis === remote.millis
73
+ ? incrementCounter(remote.counter)
74
+ : Either.right(counterMin));
75
+ return { ...local, millis, counter };
76
+ });
@@ -0,0 +1,19 @@
1
+ export { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/sqlite";
2
+ export * from "./Config.js";
3
+ export * from "./Crypto.js";
4
+ export * from "./Db.js";
5
+ export * from "./DbWorker.js";
6
+ export * from "./Diff.js";
7
+ export * from "./Errors.js";
8
+ export * from "./Evolu.js";
9
+ export * from "./MerkleTree.js";
10
+ export * from "./Model.js";
11
+ export * from "./Murmurhash.js";
12
+ export * from "./Platform.js";
13
+ export * from "./Protobuf.js";
14
+ export * from "./Sql.js";
15
+ export * from "./Sqlite.js";
16
+ export * from "./Store.js";
17
+ export * from "./SyncWorker.js";
18
+ export * from "./Timestamp.js";
19
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACtE,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC"}
@@ -0,0 +1,18 @@
1
+ export { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/sqlite";
2
+ export * from "./Config.js";
3
+ export * from "./Crypto.js";
4
+ export * from "./Db.js";
5
+ export * from "./DbWorker.js";
6
+ export * from "./Diff.js";
7
+ export * from "./Errors.js";
8
+ export * from "./Evolu.js";
9
+ export * from "./MerkleTree.js";
10
+ export * from "./Model.js";
11
+ export * from "./Murmurhash.js";
12
+ export * from "./Platform.js";
13
+ export * from "./Protobuf.js";
14
+ export * from "./Sql.js";
15
+ export * from "./Sqlite.js";
16
+ export * from "./Store.js";
17
+ export * from "./SyncWorker.js";
18
+ export * from "./Timestamp.js";
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@evolu/common",
3
+ "version": "1.0.0",
4
+ "description": "Local-first platform designed for privacy, ease of use, and no vendor lock-in to sync and backup people's lifetime data",
5
+ "keywords": [
6
+ "evolu",
7
+ "local-first",
8
+ "sqlite",
9
+ "crdt",
10
+ "e2ee"
11
+ ],
12
+ "author": "Daniel Steigerwald <daniel@steigerwald.cz>",
13
+ "license": "MIT",
14
+ "repository": "evoluhq/evolu",
15
+ "bugs": {
16
+ "url": "https://github.com/evoluhq/evolu/issues"
17
+ },
18
+ "homepage": "https://evolu.dev",
19
+ "type": "module",
20
+ "types": "./dist/src/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/src/index.d.ts",
24
+ "import": "./dist/src/index.js",
25
+ "browser": "./dist/src/index.js"
26
+ }
27
+ },
28
+ "react-native": {
29
+ "./index.js": "./dist/src/index.js"
30
+ },
31
+ "files": [
32
+ "dist/src/**",
33
+ "src/**",
34
+ "README.md"
35
+ ],
36
+ "dependencies": {
37
+ "@noble/ciphers": "^0.3.0",
38
+ "@noble/hashes": "^1.3.2",
39
+ "@protobuf-ts/runtime": "^2.9.1",
40
+ "@scure/bip39": "^1.2.1",
41
+ "kysely": "^0.26.3",
42
+ "nanoid": "^5.0.1"
43
+ },
44
+ "devDependencies": {
45
+ "@effect/schema": "^0.37.1",
46
+ "@protobuf-ts/plugin": "^2.9.1",
47
+ "@protobuf-ts/protoc": "^2.9.1",
48
+ "array-shuffle": "^3.0.0",
49
+ "effect": "2.0.0-next.36",
50
+ "eslint": "^8.50.0",
51
+ "typescript": "^5.2.2",
52
+ "vitest": "^0.34.5",
53
+ "@evolu/tsconfig": "0.0.2",
54
+ "eslint-config-evolu": "0.0.2"
55
+ },
56
+ "peerDependencies": {
57
+ "@effect/schema": "^0.37.1",
58
+ "effect": "2.0.0-next.36"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public"
62
+ },
63
+ "engines": {
64
+ "node": ">=18.16"
65
+ },
66
+ "sideEffects": false,
67
+ "scripts": {
68
+ "dev": "tsc --watch",
69
+ "build": "rm -rf dist ./README.md && tsc && cp ../../README.md ./",
70
+ "lint": "TIMING=1 eslint src --ext .ts,.tsx",
71
+ "test": "vitest run",
72
+ "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
73
+ "format": "prettier --write \"src/*.{ts,tsx,md}\"",
74
+ "protobuf": "pnpm protoc --ts_out ./src --proto_path protobuf protobuf/Protobuf.proto --ts_opt eslint_disable --ts_opt optimize_code_size && pnpm format"
75
+ }
76
+ }
package/src/Config.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { Context, Layer } from "effect";
2
+
3
+ export interface Config {
4
+ /**
5
+ * Alternate URL to Evolu sync&backup server.
6
+ */
7
+ readonly syncUrl: string;
8
+ /**
9
+ * Alternate URL to reload browser tabs after `Owner` reset or restore.
10
+ * The default value is `/`.
11
+ */
12
+ readonly reloadUrl: string;
13
+ /**
14
+ * Maximum physical clock drift allowed in ms.
15
+ * The default value is 5 * 60 * 1000 (5 minutes).
16
+ */
17
+ readonly maxDrift: number;
18
+ }
19
+
20
+ export const Config = Context.Tag<Config>("evolu/Config");
21
+
22
+ export const ConfigLive = (
23
+ config?: Partial<Config>,
24
+ ): Layer.Layer<never, never, Config> =>
25
+ Layer.succeed(
26
+ Config,
27
+ Config.of({
28
+ syncUrl: "https://evolu.world",
29
+ maxDrift: 5 * 60 * 1000,
30
+ reloadUrl: "/",
31
+ ...config,
32
+ }),
33
+ );
package/src/Crypto.ts ADDED
@@ -0,0 +1,113 @@
1
+ import * as Schema from "@effect/schema/Schema";
2
+ import { secretbox } from "@noble/ciphers/salsa";
3
+ import { concatBytes } from "@noble/ciphers/utils";
4
+ import { hmac } from "@noble/hashes/hmac";
5
+ import { sha512 } from "@noble/hashes/sha512";
6
+ import { randomBytes } from "@noble/hashes/utils";
7
+ import { Brand, Context, Effect, Layer } from "effect";
8
+ import { customAlphabet, nanoid } from "nanoid";
9
+
10
+ export interface Bip39 {
11
+ readonly make: Effect.Effect<never, never, Mnemonic>;
12
+
13
+ readonly toSeed: (
14
+ mnemonic: Mnemonic,
15
+ ) => Effect.Effect<never, never, Uint8Array>;
16
+
17
+ readonly parse: (
18
+ mnemonic: string,
19
+ ) => Effect.Effect<never, InvalidMnemonicError, Mnemonic>;
20
+ }
21
+
22
+ export const Bip39 = Context.Tag<Bip39>("evolu/Bip39");
23
+
24
+ export interface InvalidMnemonicError {
25
+ readonly _tag: "InvalidMnemonicError";
26
+ }
27
+
28
+ /**
29
+ * Mnemonic is a password generated by Evolu in BIP39 format.
30
+ *
31
+ * A mnemonic, also known as a "seed phrase," is a set of 12 words in a
32
+ * specific order chosen from a predefined list. The purpose of the BIP39
33
+ * mnemonic is to provide a human-readable way of storing a private key.
34
+ */
35
+ export type Mnemonic = string & Brand.Brand<"Mnemonic">;
36
+
37
+ export interface NanoId {
38
+ readonly nanoid: Effect.Effect<never, never, string>;
39
+ readonly nanoidAsNodeId: Effect.Effect<never, never, NodeId>;
40
+ }
41
+
42
+ export const NanoId = Context.Tag<NanoId>("evolu/NanoId");
43
+
44
+ export const NodeId: Schema.BrandSchema<
45
+ string,
46
+ string & Brand.Brand<"NodeId">
47
+ > = Schema.string.pipe(Schema.pattern(/^[\w-]{16}$/), Schema.brand("NodeId"));
48
+ export type NodeId = Schema.Schema.To<typeof NodeId>;
49
+
50
+ const nanoidForNodeId = customAlphabet("0123456789abcdef", 16);
51
+
52
+ export const NanoIdLive = Layer.succeed(
53
+ NanoId,
54
+ NanoId.of({
55
+ nanoid: Effect.sync(() => nanoid()),
56
+ nanoidAsNodeId: Effect.sync(() => nanoidForNodeId() as NodeId),
57
+ }),
58
+ );
59
+
60
+ /**
61
+ * SLIP-21 implementation
62
+ * https://github.com/satoshilabs/slips/blob/master/slip-0021.md
63
+ */
64
+ export const slip21Derive = (
65
+ seed: Uint8Array,
66
+ path: string[],
67
+ ): Effect.Effect<never, never, Uint8Array> =>
68
+ Effect.sync(() => {
69
+ let m = hmac(sha512, "Symmetric key seed", seed);
70
+ for (let i = 0; i < path.length; i++) {
71
+ const p = new TextEncoder().encode(path[i]);
72
+ const e = new Uint8Array(p.byteLength + 1);
73
+ e[0] = 0;
74
+ e.set(p, 1);
75
+ m = hmac(sha512, m.slice(0, 32), e);
76
+ }
77
+ return m.slice(32, 64);
78
+ });
79
+
80
+ /**
81
+ * Alias to xsalsa20poly1305, for compatibility with libsodium / nacl
82
+ */
83
+ export interface SecretBox {
84
+ readonly seal: (
85
+ key: Uint8Array,
86
+ plaintext: Uint8Array,
87
+ ) => Effect.Effect<never, never, Uint8Array>;
88
+
89
+ readonly open: (
90
+ key: Uint8Array,
91
+ ciphertext: Uint8Array,
92
+ ) => Effect.Effect<never, never, Uint8Array>;
93
+ }
94
+
95
+ export const SecretBox = Context.Tag<SecretBox>("evolu/SecretBox");
96
+
97
+ export const SecretBoxLive = Layer.succeed(
98
+ SecretBox,
99
+ SecretBox.of({
100
+ seal: (key, plaintext) =>
101
+ Effect.sync(() => {
102
+ const nonce = randomBytes(24);
103
+ const ciphertext = secretbox(key, nonce).seal(plaintext);
104
+ return concatBytes(nonce, ciphertext);
105
+ }),
106
+ open: (key, ciphertext) =>
107
+ Effect.sync(() => {
108
+ const nonce = ciphertext.subarray(0, 24);
109
+ const ciphertextWithoutNonce = ciphertext.subarray(24);
110
+ return secretbox(key, nonce).open(ciphertextWithoutNonce);
111
+ }),
112
+ }),
113
+ );