@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/Sql.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// TODO: This file should be generated from a script via Kysely.
|
|
2
|
+
// The reason for not using Kysely directly is bundle size.
|
|
3
|
+
// [Playground Link](https://kyse.link/?p=b&i=haFkpnNxbGl0ZaF2pjAuMjYuMKFz2gFoaW1wb3J0IHsgR2VuZXJhdGVkIH0gZnJvbSAna3lzZWx5JwoKZGVjbGFyZSBnbG9iYWwgewogIGludGVyZmFjZSBEQiB7CiAgICBldm9sdV9tZXNzYWdlOiB7CiAgICAgIHRpbWVzdGFtcDogc3RyaW5nLAogICAgICB0YWJsZTogc3RyaW5nLAogICAgICByb3c6IHN0cmluZywKICAgICAgY29sdW1uOiBzdHJpbmcsCiAgICAgIHZhbHVlOiB1bmtub3duCiAgICB9LAoKICAgIGV2b2x1X293bmVyOiB7CiAgICAgIGlkOiBzdHJpbmcKICAgICAgbW5lbW9uaWM6IHN0cmluZwogICAgICBlbmNyeXB0aW9uS2V5OiBVaW50OEFycmF5LAogICAgICB0aW1lc3RhbXA6IHN0cmluZywKICAgICAgbWVya2xlVHJlZTogc3RyaW5nCiAgICB9CiAgfQp9oXHaBnhhd2FpdCBreXNlbHkuc2VsZWN0RnJvbSgiZXZvbHVfb3duZXIiKQogIC5zZWxlY3QoWyJpZCIsICJtbmVtb25pYyIsICJlbmNyeXB0aW9uS2V5Il0pCiAgLmV4ZWN1dGUoKQoKYXdhaXQga3lzZWx5LnNjaGVtYQogIC5jcmVhdGVUYWJsZSgnZXZvbHVfbWVzc2FnZScpCiAgLmFkZENvbHVtbigndGltZXN0YW1wJywgJ2Jsb2InLCBjb2wgPT4gY29sLnByaW1hcnlLZXkoKSkKICAuYWRkQ29sdW1uKCd0YWJsZScsICdibG9iJykKICAuYWRkQ29sdW1uKCdyb3cnLCAnYmxvYicpCiAgLmFkZENvbHVtbignY29sdW1uJywgJ2Jsb2InKQogIC5hZGRDb2x1bW4oJ3ZhbHVlJywgJ2Jsb2InKQogIC5leGVjdXRlKCkKCmF3YWl0IGt5c2VseS5zY2hlbWEKICAuY3JlYXRlSW5kZXgoImluZGV4X2V2b2x1X21lc3NhZ2UiKQogIC5vbigiZXZvbHVfbWVzc2FnZSIpCiAgLmNvbHVtbnMoWyJ0YWJsZSIsICJyb3ciLCAiY29sdW1uIiwgInRpbWVzdGFtcCJdKQogIC5leGVjdXRlKCkKCmF3YWl0IGt5c2VseS5zY2hlbWEKICAuY3JlYXRlVGFibGUoJ2V2b2x1X19vd25lcicpCiAgLmFkZENvbHVtbignaWQnLCAnYmxvYicpCiAgLmFkZENvbHVtbignbW5lbW9uaWMnLCAnYmxvYicpCiAgLmFkZENvbHVtbignZW5jcnlwdGlvbktleScsICdibG9iJykKICAuYWRkQ29sdW1uKCd0aW1lc3RhbXAnLCAnYmxvYicpCiAgLmFkZENvbHVtbignbWVya2xlVHJlZScsICdibG9iJykKICAuZXhlY3V0ZSgpCgphd2FpdCBreXNlbHkuaW5zZXJ0SW50bygiZXZvbHVfb3duZXIiKQogIC52YWx1ZXMoewogICAgImlkIjogImIiLAogICAgIm1uZW1vbmljIjogImEiLAogICAgImVuY3J5cHRpb25LZXkiOiBuZXcgVWludDhBcnJheSgpLAogICAgInRpbWVzdGFtcCI6ICJhIiwKICAgICJtZXJrbGVUcmVlIjogImIiCiAgfSkKICAuZXhlY3V0ZSgpCgphd2FpdCBreXNlbHkuc2VsZWN0RnJvbSgiZXZvbHVfb3duZXIiKQogIC5zZWxlY3QoWyJ0aW1lc3RhbXAiLCAibWVya2xlVHJlZSJdKQogIC5leGVjdXRlKCkKCmF3YWl0IGt5c2VseS5zZWxlY3RGcm9tKCJldm9sdV9tZXNzYWdlIikKICAuc2VsZWN0KCJ0aW1lc3RhbXAiKQogIC53aGVyZSgndGFibGUnLCAnPScsICcxJykKICAud2hlcmUoJ3JvdycsICc9JywgJzInKQogIC53aGVyZSgnY29sdW1uJywgJz0nLCAnMycpCiAgLm9yZGVyQnkoInRpbWVzdGFtcCIsICJkZXNjIikKICAubGltaXQoMSkKICAuZXhlY3V0ZVRha2VGaXJzdCgpCgphd2FpdCBreXNlbHkuaW5zZXJ0SW50bygiZXZvbHVfbWVzc2FnZSIpCiAgLnZhbHVlcyh7CiAgICAidGltZXN0YW1wIjogJzEnLAogICAgInRhYmxlIjogJzInLAogICAgInJvdyI6ICczJywKICAgICJjb2x1bW4iOiAnNCcsCiAgICAidmFsdWUiOiAnNScsCiAgfSkKICAub25Db25mbGljdChvYyA9PiBvYy5kb05vdGhpbmcoKSkKICAuZXhlY3V0ZSgpCgphd2FpdCBreXNlbHkudXBkYXRlVGFibGUoImV2b2x1X293bmVyIikKICAuc2V0KHsKICAgICJtZXJrbGVUcmVlIjogJzEnLAogICAgInRpbWVzdGFtcCI6ICIyIgogIH0pCiAgLmV4ZWN1dGUoKQoKYXdhaXQga3lzZWx5LnNlbGVjdEZyb20oImV2b2x1X21lc3NhZ2UiKQogIC5zZWxlY3RBbGwoKQogIC53aGVyZSgidGltZXN0YW1wIiwgIj49IiwgJzEnKQogIC5vcmRlckJ5KCJ0aW1lc3RhbXAiKQogIC5leGVjdXRlKCmhY8M=)
|
|
4
|
+
|
|
5
|
+
export const selectOwner = `
|
|
6
|
+
SELECT
|
|
7
|
+
"id",
|
|
8
|
+
"mnemonic",
|
|
9
|
+
"encryptionKey"
|
|
10
|
+
FROM
|
|
11
|
+
"evolu_owner"
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
export const createMessageTable = `
|
|
15
|
+
CREATE TABLE
|
|
16
|
+
"evolu_message" (
|
|
17
|
+
"timestamp" blob PRIMARY KEY,
|
|
18
|
+
"table" blob,
|
|
19
|
+
"row" blob,
|
|
20
|
+
"column" blob,
|
|
21
|
+
"value" blob
|
|
22
|
+
);
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
export const createMessageTableIndex = `
|
|
26
|
+
CREATE INDEX "index_evolu_message" ON "evolu_message" (
|
|
27
|
+
"table", "row", "column", "timestamp"
|
|
28
|
+
);
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
export const createOwnerTable = `
|
|
32
|
+
CREATE TABLE
|
|
33
|
+
"evolu_owner" (
|
|
34
|
+
"id" blob,
|
|
35
|
+
"mnemonic" blob,
|
|
36
|
+
"encryptionKey" blob,
|
|
37
|
+
"timestamp" blob,
|
|
38
|
+
"merkleTree" blob
|
|
39
|
+
);
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
export const insertOwner = `
|
|
43
|
+
INSERT INTO
|
|
44
|
+
"evolu_owner" (
|
|
45
|
+
"id",
|
|
46
|
+
"mnemonic",
|
|
47
|
+
"encryptionKey",
|
|
48
|
+
"timestamp",
|
|
49
|
+
"merkleTree"
|
|
50
|
+
)
|
|
51
|
+
VALUES
|
|
52
|
+
(?, ?, ?, ?, ?);
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
export const selectOwnerTimestampAndMerkleTree = `
|
|
56
|
+
SELECT
|
|
57
|
+
"timestamp",
|
|
58
|
+
"merkleTree"
|
|
59
|
+
FROM
|
|
60
|
+
"evolu_owner"
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
export const selectLastTimestampForTableRowColumn = `
|
|
64
|
+
SELECT
|
|
65
|
+
"timestamp"
|
|
66
|
+
FROM
|
|
67
|
+
"evolu_message"
|
|
68
|
+
WHERE
|
|
69
|
+
"table" = ?
|
|
70
|
+
AND "row" = ?
|
|
71
|
+
AND "column" = ?
|
|
72
|
+
ORDER BY
|
|
73
|
+
"timestamp" DESC
|
|
74
|
+
LIMIT
|
|
75
|
+
1
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
export const insertValueIntoTableRowColumn = (
|
|
79
|
+
table: string,
|
|
80
|
+
column: string,
|
|
81
|
+
): string => `
|
|
82
|
+
INSERT INTO
|
|
83
|
+
"${table}" ("id", "${column}")
|
|
84
|
+
VALUES
|
|
85
|
+
(?, ?)
|
|
86
|
+
ON CONFLICT DO UPDATE SET
|
|
87
|
+
"${column}" = ?
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
export const insertIntoMessagesIfNew = `
|
|
91
|
+
INSERT INTO
|
|
92
|
+
"evolu_message" ("timestamp", "table", "row", "column", "value")
|
|
93
|
+
VALUES
|
|
94
|
+
(?, ?, ?, ?, ?)
|
|
95
|
+
ON CONFLICT DO NOTHING
|
|
96
|
+
`;
|
|
97
|
+
|
|
98
|
+
export const updateOwnerTimestampAndMerkleTree = `
|
|
99
|
+
UPDATE "evolu_owner"
|
|
100
|
+
SET
|
|
101
|
+
"timestamp" = ?,
|
|
102
|
+
"merkleTree" = ?
|
|
103
|
+
`;
|
|
104
|
+
|
|
105
|
+
export const selectMessagesToSync = `
|
|
106
|
+
SELECT
|
|
107
|
+
*
|
|
108
|
+
FROM
|
|
109
|
+
"evolu_message"
|
|
110
|
+
WHERE
|
|
111
|
+
"timestamp" >= ?
|
|
112
|
+
ORDER BY
|
|
113
|
+
"timestamp"
|
|
114
|
+
`;
|
package/src/Sqlite.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Brand, Context, Effect, Predicate, ReadonlyRecord } from "effect";
|
|
2
|
+
|
|
3
|
+
export interface Sqlite {
|
|
4
|
+
readonly exec: (
|
|
5
|
+
arg: string | QueryObject,
|
|
6
|
+
) => Effect.Effect<never, never, ExecResult>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const Sqlite = Context.Tag<Sqlite>("evolu/Sqlite");
|
|
10
|
+
|
|
11
|
+
export interface QueryObject {
|
|
12
|
+
readonly sql: string;
|
|
13
|
+
readonly parameters: ReadonlyArray<Value>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type Value = SqliteValue | JsonObjectOrArray;
|
|
17
|
+
|
|
18
|
+
export type SqliteValue = null | string | number | Uint8Array;
|
|
19
|
+
export type SqliteRow = Record<string, SqliteValue>;
|
|
20
|
+
|
|
21
|
+
export type JsonObjectOrArray = JsonObject | JsonArray;
|
|
22
|
+
|
|
23
|
+
type JsonObject = ReadonlyRecord.ReadonlyRecord<Json>;
|
|
24
|
+
type JsonArray = ReadonlyArray<Json>;
|
|
25
|
+
type Json = string | number | boolean | null | JsonObject | JsonArray;
|
|
26
|
+
|
|
27
|
+
interface ExecResult {
|
|
28
|
+
readonly rows: ReadonlyArray<Row>;
|
|
29
|
+
readonly changes: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type Row = ReadonlyRecord.ReadonlyRecord<
|
|
33
|
+
Value | Row | ReadonlyArray<Row>
|
|
34
|
+
>;
|
|
35
|
+
|
|
36
|
+
export type Query = string & Brand.Brand<"Query">;
|
|
37
|
+
|
|
38
|
+
export const isJsonObjectOrArray: Predicate.Refinement<
|
|
39
|
+
Value,
|
|
40
|
+
JsonObjectOrArray
|
|
41
|
+
> = (value): value is JsonObjectOrArray =>
|
|
42
|
+
value !== null && typeof value === "object" && !(value instanceof Uint8Array);
|
|
43
|
+
|
|
44
|
+
export const valuesToSqliteValues = (
|
|
45
|
+
values: ReadonlyArray<Value>,
|
|
46
|
+
): SqliteValue[] =>
|
|
47
|
+
values.map((value) =>
|
|
48
|
+
isJsonObjectOrArray(value) ? JSON.stringify(value) : value,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
export const queryObjectToQuery = ({ sql, parameters }: QueryObject): Query =>
|
|
52
|
+
JSON.stringify({ sql, parameters }) as Query;
|
|
53
|
+
|
|
54
|
+
export const queryObjectFromQuery = (s: Query): QueryObject =>
|
|
55
|
+
JSON.parse(s) as QueryObject;
|
|
56
|
+
|
|
57
|
+
export const parseJsonResults = (rows: SqliteRow[]): void => {
|
|
58
|
+
parseArray(rows);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const parseArray = <T>(a: T[]): T[] => {
|
|
62
|
+
for (let i = 0; i < a.length; ++i) a[i] = parse(a[i]) as T;
|
|
63
|
+
return a;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const parse = (o: unknown): unknown => {
|
|
67
|
+
if (Predicate.isString(o)) return parseString(o);
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
69
|
+
if (Array.isArray(o)) return parseArray(o);
|
|
70
|
+
// Predicate.isReadonlyRecord
|
|
71
|
+
if (typeof o === "object" && o !== null && !Predicate.isUint8Array(o))
|
|
72
|
+
return parseObject(o as Record<string, unknown>);
|
|
73
|
+
return o;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const parseString = (s: string): unknown => {
|
|
77
|
+
if (maybeJson(s))
|
|
78
|
+
try {
|
|
79
|
+
return parse(JSON.parse(s));
|
|
80
|
+
} catch (err) {
|
|
81
|
+
// Nothing to do.
|
|
82
|
+
}
|
|
83
|
+
return s;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const maybeJson: Predicate.Predicate<string> = (value) =>
|
|
87
|
+
value.match(/^[[{]/) != null;
|
|
88
|
+
|
|
89
|
+
const parseObject = (o: Record<string, unknown>): Record<string, unknown> => {
|
|
90
|
+
for (const key in o) o[key] = parse(o[key]);
|
|
91
|
+
return o;
|
|
92
|
+
};
|
package/src/Store.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type StoreListener = () => void;
|
|
2
|
+
|
|
3
|
+
export type StoreUnsubscribe = () => void;
|
|
4
|
+
|
|
5
|
+
export interface Store<T> {
|
|
6
|
+
readonly subscribe: (listener: StoreListener) => StoreUnsubscribe;
|
|
7
|
+
readonly setState: (state: T) => void;
|
|
8
|
+
readonly getState: () => T;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const makeStore = <T>(initialState: T): Store<T> => {
|
|
12
|
+
let currentState = initialState;
|
|
13
|
+
|
|
14
|
+
const listeners = new Set<StoreListener>();
|
|
15
|
+
|
|
16
|
+
const subscribe: Store<T>["subscribe"] = (listener) => {
|
|
17
|
+
listeners.add(listener);
|
|
18
|
+
return () => {
|
|
19
|
+
listeners.delete(listener);
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const setState: Store<T>["setState"] = (state: T) => {
|
|
24
|
+
if (state === currentState) return;
|
|
25
|
+
currentState = state;
|
|
26
|
+
listeners.forEach((listener) => listener());
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const getState: Store<T>["getState"] = () => currentState;
|
|
30
|
+
|
|
31
|
+
return { subscribe, setState, getState };
|
|
32
|
+
};
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { concatBytes } from "@noble/ciphers/utils";
|
|
2
|
+
import { BinaryReader, BinaryWriter } from "@protobuf-ts/runtime";
|
|
3
|
+
import {
|
|
4
|
+
Context,
|
|
5
|
+
Effect,
|
|
6
|
+
Function,
|
|
7
|
+
Layer,
|
|
8
|
+
Match,
|
|
9
|
+
Option,
|
|
10
|
+
Predicate,
|
|
11
|
+
ReadonlyArray,
|
|
12
|
+
absurd,
|
|
13
|
+
identity,
|
|
14
|
+
} from "effect";
|
|
15
|
+
import { SecretBox } from "./Crypto.js";
|
|
16
|
+
import { Owner } from "./Db.js";
|
|
17
|
+
import { UnexpectedError, makeUnexpectedError } from "./Errors.js";
|
|
18
|
+
import {
|
|
19
|
+
MerkleTree,
|
|
20
|
+
merkleTreeToString,
|
|
21
|
+
unsafeMerkleTreeFromString,
|
|
22
|
+
} from "./MerkleTree.js";
|
|
23
|
+
import { Id } from "./Model.js";
|
|
24
|
+
import { Fetch, SyncLock } from "./Platform.js";
|
|
25
|
+
import {
|
|
26
|
+
EncryptedMessage,
|
|
27
|
+
MessageContent,
|
|
28
|
+
SyncRequest,
|
|
29
|
+
SyncResponse,
|
|
30
|
+
} from "./Protobuf.js";
|
|
31
|
+
import { JsonObjectOrArray, Value } from "./Sqlite.js";
|
|
32
|
+
import { Millis, Timestamp, TimestampString } from "./Timestamp.js";
|
|
33
|
+
|
|
34
|
+
export interface SyncWorker {
|
|
35
|
+
readonly postMessage: (input: SyncWorkerInput) => void;
|
|
36
|
+
onMessage: (output: SyncWorkerOutput) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const SyncWorker = Context.Tag<SyncWorker>("evolu/SyncWorker");
|
|
40
|
+
|
|
41
|
+
export type SyncWorkerPostMessage = SyncWorker["postMessage"];
|
|
42
|
+
|
|
43
|
+
export const SyncWorkerPostMessage = Context.Tag<SyncWorkerPostMessage>(
|
|
44
|
+
"evolu/SyncWorkerPostMessage",
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
export type SyncWorkerInput =
|
|
48
|
+
| SyncWorkerInputSync
|
|
49
|
+
| SyncWorkerInputSyncCompleted;
|
|
50
|
+
|
|
51
|
+
interface SyncWorkerInputSync {
|
|
52
|
+
readonly _tag: "sync";
|
|
53
|
+
readonly syncUrl: string;
|
|
54
|
+
readonly messages: ReadonlyArray<Message>;
|
|
55
|
+
readonly merkleTree: MerkleTree;
|
|
56
|
+
readonly timestamp: Timestamp;
|
|
57
|
+
readonly owner: Owner;
|
|
58
|
+
readonly syncLoopCount: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface NewMessage {
|
|
62
|
+
readonly table: string;
|
|
63
|
+
readonly row: Id;
|
|
64
|
+
readonly column: string;
|
|
65
|
+
readonly value: Value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface Message extends NewMessage {
|
|
69
|
+
readonly timestamp: TimestampString;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface SyncWorkerInputSyncCompleted {
|
|
73
|
+
readonly _tag: "syncCompleted";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type SyncWorkerOnMessage = SyncWorker["onMessage"];
|
|
77
|
+
|
|
78
|
+
const SyncWorkerOnMessage = Context.Tag<SyncWorkerOnMessage>(
|
|
79
|
+
"evolu/SyncWorkerOnMessage",
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
export type SyncWorkerOutput =
|
|
83
|
+
| UnexpectedError
|
|
84
|
+
| SyncWorkerOutputSyncResponse
|
|
85
|
+
| SyncStateIsNotSyncedError
|
|
86
|
+
| SyncStateIsSyncing;
|
|
87
|
+
|
|
88
|
+
export type SyncWorkerOutputSyncResponse = {
|
|
89
|
+
readonly _tag: "SyncWorkerOutputSyncResponse";
|
|
90
|
+
readonly messages: ReadonlyArray<Message>;
|
|
91
|
+
readonly merkleTree: MerkleTree;
|
|
92
|
+
readonly syncLoopCount: number;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export type SyncState =
|
|
96
|
+
| SyncStateInitial
|
|
97
|
+
| SyncStateIsSyncing
|
|
98
|
+
| SyncStateIsSynced
|
|
99
|
+
| SyncStateIsNotSyncedError;
|
|
100
|
+
|
|
101
|
+
export interface SyncStateInitial {
|
|
102
|
+
readonly _tag: "SyncStateInitial";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface SyncStateIsSyncing {
|
|
106
|
+
readonly _tag: "SyncStateIsSyncing";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface SyncStateIsSynced {
|
|
110
|
+
readonly _tag: "SyncStateIsSynced";
|
|
111
|
+
readonly time: Millis;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface SyncStateIsNotSyncedError {
|
|
115
|
+
readonly _tag: "SyncStateIsNotSyncedError";
|
|
116
|
+
readonly error:
|
|
117
|
+
| SyncStateNetworkError
|
|
118
|
+
| SyncStateServerError
|
|
119
|
+
| SyncStatePaymentRequiredError;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface SyncStateNetworkError {
|
|
123
|
+
readonly _tag: "NetworkError";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface SyncStateServerError {
|
|
127
|
+
readonly _tag: "ServerError";
|
|
128
|
+
readonly status: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface SyncStatePaymentRequiredError {
|
|
132
|
+
readonly _tag: "PaymentRequiredError";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const version1 = new Uint8Array([0, 1]);
|
|
136
|
+
|
|
137
|
+
const valueToProtobuf = (value: Value): MessageContent["value"] => {
|
|
138
|
+
switch (typeof value) {
|
|
139
|
+
case "string":
|
|
140
|
+
return { oneofKind: "stringValue", stringValue: value };
|
|
141
|
+
case "number":
|
|
142
|
+
return { oneofKind: "numberValue", numberValue: value };
|
|
143
|
+
}
|
|
144
|
+
if (value == null) return { oneofKind: undefined };
|
|
145
|
+
if (Predicate.isUint8Array(value))
|
|
146
|
+
return { oneofKind: "bytesValue", bytesValue: value };
|
|
147
|
+
return { oneofKind: "jsonValue", jsonValue: JSON.stringify(value) };
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const valueFromProtobuf = (value: MessageContent["value"]): Value => {
|
|
151
|
+
switch (value.oneofKind) {
|
|
152
|
+
case "numberValue":
|
|
153
|
+
return value.numberValue;
|
|
154
|
+
case "stringValue":
|
|
155
|
+
return value.stringValue;
|
|
156
|
+
case "bytesValue":
|
|
157
|
+
return value.bytesValue;
|
|
158
|
+
case "jsonValue":
|
|
159
|
+
return JSON.parse(value.jsonValue) as JsonObjectOrArray;
|
|
160
|
+
case undefined:
|
|
161
|
+
return null;
|
|
162
|
+
default:
|
|
163
|
+
return absurd(value);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const newMessageToBinary = ({ value, ...rest }: NewMessage): Uint8Array =>
|
|
168
|
+
concatBytes(
|
|
169
|
+
version1,
|
|
170
|
+
MessageContent.toBinary(
|
|
171
|
+
{ value: valueToProtobuf(value), ...rest },
|
|
172
|
+
binaryWriteOptions,
|
|
173
|
+
),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const startsWithArray = (array: Uint8Array, prefix: Uint8Array): boolean => {
|
|
177
|
+
if (prefix.length > array.length) return false;
|
|
178
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
179
|
+
if (array[i] !== prefix[i]) return false;
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const newMessageFromBinary = (
|
|
185
|
+
binary: Uint8Array,
|
|
186
|
+
): Option.Option<NewMessage> => {
|
|
187
|
+
if (!startsWithArray(binary, version1)) return Option.none();
|
|
188
|
+
const { value, ...content } = MessageContent.fromBinary(
|
|
189
|
+
binary.slice(version1.length),
|
|
190
|
+
binaryReadOptions,
|
|
191
|
+
);
|
|
192
|
+
return Option.some({ value: valueFromProtobuf(value), ...content });
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// The 'protobuf-ts' uses TextEncoder, but polyfill fast-text-encoding
|
|
196
|
+
// doesn't support the fatal option.
|
|
197
|
+
// https://github.com/timostamm/protobuf-ts/issues/184#issuecomment-1658443836
|
|
198
|
+
const binaryWriteOptions = {
|
|
199
|
+
writerFactory: (): BinaryWriter =>
|
|
200
|
+
new BinaryWriter({
|
|
201
|
+
encode: (input: string): Uint8Array => new TextEncoder().encode(input),
|
|
202
|
+
}),
|
|
203
|
+
};
|
|
204
|
+
const binaryReadOptions = {
|
|
205
|
+
readerFactory: (bytes: Uint8Array): BinaryReader =>
|
|
206
|
+
new BinaryReader(bytes, {
|
|
207
|
+
decode: (input: Uint8Array): string => new TextDecoder().decode(input),
|
|
208
|
+
}),
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const sync = (
|
|
212
|
+
input: SyncWorkerInputSync,
|
|
213
|
+
): Effect.Effect<
|
|
214
|
+
SyncLock | SyncWorkerOnMessage | Fetch | SecretBox,
|
|
215
|
+
never,
|
|
216
|
+
void
|
|
217
|
+
> =>
|
|
218
|
+
Effect.gen(function* (_) {
|
|
219
|
+
const syncLock = yield* _(SyncLock);
|
|
220
|
+
const syncWorkerOnMessage = yield* _(SyncWorkerOnMessage);
|
|
221
|
+
const fetch = yield* _(Fetch);
|
|
222
|
+
const secretBox = yield* _(SecretBox);
|
|
223
|
+
|
|
224
|
+
if (input.syncLoopCount === 0) {
|
|
225
|
+
if (!(yield* _(syncLock.acquire))) return;
|
|
226
|
+
syncWorkerOnMessage({ _tag: "SyncStateIsSyncing" });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
yield* _(
|
|
230
|
+
Effect.forEach(input.messages, ({ timestamp, ...newMessage }) =>
|
|
231
|
+
secretBox
|
|
232
|
+
.seal(input.owner.encryptionKey, newMessageToBinary(newMessage))
|
|
233
|
+
.pipe(
|
|
234
|
+
Effect.map((content): EncryptedMessage => ({ timestamp, content })),
|
|
235
|
+
),
|
|
236
|
+
),
|
|
237
|
+
Effect.map((messages) =>
|
|
238
|
+
SyncRequest.toBinary(
|
|
239
|
+
{
|
|
240
|
+
messages,
|
|
241
|
+
userId: input.owner.id,
|
|
242
|
+
nodeId: input.timestamp.node,
|
|
243
|
+
merkleTree: merkleTreeToString(input.merkleTree),
|
|
244
|
+
},
|
|
245
|
+
binaryWriteOptions,
|
|
246
|
+
),
|
|
247
|
+
),
|
|
248
|
+
Effect.flatMap((body) => fetch(input.syncUrl, body)),
|
|
249
|
+
Effect.catchTag("FetchError", () =>
|
|
250
|
+
Effect.fail<SyncStateIsNotSyncedError>({
|
|
251
|
+
_tag: "SyncStateIsNotSyncedError",
|
|
252
|
+
error: { _tag: "NetworkError" },
|
|
253
|
+
}),
|
|
254
|
+
),
|
|
255
|
+
Effect.flatMap((response) => {
|
|
256
|
+
switch (response.status) {
|
|
257
|
+
case 402:
|
|
258
|
+
return Effect.fail<SyncStateIsNotSyncedError>({
|
|
259
|
+
_tag: "SyncStateIsNotSyncedError",
|
|
260
|
+
error: { _tag: "PaymentRequiredError" },
|
|
261
|
+
});
|
|
262
|
+
case 200:
|
|
263
|
+
return Effect.promise(() =>
|
|
264
|
+
response
|
|
265
|
+
.arrayBuffer()
|
|
266
|
+
.then((buffer) => new Uint8Array(buffer))
|
|
267
|
+
.then((array) =>
|
|
268
|
+
SyncResponse.fromBinary(array, binaryReadOptions),
|
|
269
|
+
),
|
|
270
|
+
);
|
|
271
|
+
default:
|
|
272
|
+
return Effect.fail<SyncStateIsNotSyncedError>({
|
|
273
|
+
_tag: "SyncStateIsNotSyncedError",
|
|
274
|
+
error: { _tag: "ServerError", status: response.status },
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}),
|
|
278
|
+
Effect.flatMap((syncResponse) =>
|
|
279
|
+
Effect.forEach(syncResponse.messages, ({ timestamp, content }) =>
|
|
280
|
+
secretBox
|
|
281
|
+
.open(input.owner.encryptionKey, content)
|
|
282
|
+
.pipe(
|
|
283
|
+
Effect.map(newMessageFromBinary),
|
|
284
|
+
Effect.map(
|
|
285
|
+
Option.map(
|
|
286
|
+
(newMessage): Message => ({ timestamp, ...newMessage }),
|
|
287
|
+
),
|
|
288
|
+
),
|
|
289
|
+
),
|
|
290
|
+
).pipe(
|
|
291
|
+
Effect.map(
|
|
292
|
+
(messages): SyncWorkerOutputSyncResponse => ({
|
|
293
|
+
_tag: "SyncWorkerOutputSyncResponse",
|
|
294
|
+
messages: ReadonlyArray.filterMap(messages, identity),
|
|
295
|
+
merkleTree: unsafeMerkleTreeFromString(syncResponse.merkleTree),
|
|
296
|
+
syncLoopCount: input.syncLoopCount,
|
|
297
|
+
}),
|
|
298
|
+
),
|
|
299
|
+
),
|
|
300
|
+
),
|
|
301
|
+
Effect.tapError(() => syncLock.release),
|
|
302
|
+
Effect.merge,
|
|
303
|
+
Effect.map(syncWorkerOnMessage),
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
export const SyncWorkerLive = Layer.effect(
|
|
308
|
+
SyncWorker,
|
|
309
|
+
Effect.gen(function* (_) {
|
|
310
|
+
const syncLock = yield* _(SyncLock);
|
|
311
|
+
const fetch = yield* _(Fetch);
|
|
312
|
+
const secretBox = yield* _(SecretBox);
|
|
313
|
+
|
|
314
|
+
const onError = (
|
|
315
|
+
error: UnexpectedError,
|
|
316
|
+
): Effect.Effect<never, never, void> =>
|
|
317
|
+
Effect.sync(() => {
|
|
318
|
+
syncWorker.onMessage(error);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const postMessage: SyncWorker["postMessage"] = (input) => {
|
|
322
|
+
void Match.value(input).pipe(
|
|
323
|
+
Match.tagsExhaustive({
|
|
324
|
+
sync,
|
|
325
|
+
syncCompleted: () => syncLock.release,
|
|
326
|
+
}),
|
|
327
|
+
Effect.catchAllDefect(makeUnexpectedError),
|
|
328
|
+
Effect.catchAll(onError),
|
|
329
|
+
Effect.provideService(SyncLock, syncLock),
|
|
330
|
+
Effect.provideService(Fetch, fetch),
|
|
331
|
+
Effect.provideService(SecretBox, secretBox),
|
|
332
|
+
Effect.provideService(SyncWorkerOnMessage, syncWorker.onMessage),
|
|
333
|
+
Effect.runPromise,
|
|
334
|
+
);
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const syncWorker: SyncWorker = {
|
|
338
|
+
postMessage,
|
|
339
|
+
onMessage: Function.constVoid,
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
return syncWorker;
|
|
343
|
+
}),
|
|
344
|
+
);
|