@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
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
+ );