@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
@@ -0,0 +1,126 @@
1
+ import * as Http from "@effect/platform/HttpClient";
2
+ import * as S from "@effect/schema/Schema";
3
+ import { concatBytes } from "@noble/ciphers/utils";
4
+ import { BinaryReader, BinaryWriter } from "@protobuf-ts/runtime";
5
+ import * as Arr from "effect/Array";
6
+ import * as Context from "effect/Context";
7
+ import * as Deferred from "effect/Deferred";
8
+ import * as Effect from "effect/Effect";
9
+ import { absurd } from "effect/Function";
10
+ import * as Option from "effect/Option";
11
+ import * as Predicate from "effect/Predicate";
12
+ import { Config } from "./Config.js";
13
+ import { merkleTreeToString, unsafeMerkleTreeFromString, } from "./Crdt.js";
14
+ import { SecretBox } from "./Crypto.js";
15
+ import { Owner } from "./Owner.js";
16
+ import * as Protobuf from "./Protobuf.js";
17
+ export const Sync = Context.GenericTag("Sync");
18
+ // To preserve identity.
19
+ export const initialSyncState = { _tag: "SyncStateInitial" };
20
+ export class SyncFactory extends Context.Tag("SyncFactory")() {
21
+ }
22
+ export const createSync = Effect.gen(function* () {
23
+ const initContext = Context.empty().pipe(Context.add(SecretBox, yield* SecretBox));
24
+ const afterInitContext = yield* Deferred.make();
25
+ return Sync.of({
26
+ init: (owner) => Effect.logDebug(["Sync init", { owner }]).pipe(Effect.tap(Deferred.succeed(afterInitContext, initContext.pipe(Context.add(Owner, owner))))),
27
+ sync: ({ merkleTree, timestamp, messages }) => Effect.gen(function* () {
28
+ yield* Effect.logDebug([
29
+ "Sync request",
30
+ { merkleTree, timestamp, messages },
31
+ ]);
32
+ const secretBox = yield* SecretBox;
33
+ const owner = yield* Owner;
34
+ const config = yield* Config;
35
+ return yield* Effect.forEach(messages || [], ({ timestamp, ...newMessage }) => Effect.map(secretBox.seal(owner.encryptionKey, newMessageToBinary(newMessage)), (content) => ({ timestamp, content }))).pipe(Effect.map((encrypedMessages) => Protobuf.SyncRequest.toBinary({
36
+ messages: encrypedMessages,
37
+ userId: owner.id,
38
+ nodeId: timestamp.node,
39
+ merkleTree: merkleTreeToString(merkleTree),
40
+ }, binaryWriteOptions)), Effect.flatMap((body) => Http.request
41
+ .post(config.syncUrl)
42
+ .pipe(Http.request.uint8ArrayBody(body, "application/x-protobuf"), Http.client.fetchOk, Http.response.arrayBuffer)), Effect.map((buffer) => Protobuf.SyncResponse.fromBinary(new Uint8Array(buffer), binaryReadOptions)), Effect.flatMap((syncResponse) => Effect.forEach(syncResponse.messages, (encrypedMessage) => Effect.map(secretBox.open(owner.encryptionKey, encrypedMessage.content), (binary) => [binary, encrypedMessage.timestamp])).pipe(Effect.map(Arr.filterMap(([binary, timestamp]) => Option.map(newMessageFromBinary(binary), (newMessage) => ({ ...newMessage, timestamp })))), Effect.map((messages) => ({
43
+ messages,
44
+ merkleTree: unsafeMerkleTreeFromString(syncResponse.merkleTree),
45
+ })), Effect.tap((response) => Effect.logDebug(["Sync response", response])))), Effect.catchTag("RequestError", () => Effect.fail({
46
+ _tag: "SyncStateIsNotSynced",
47
+ error: { _tag: "NetworkError" },
48
+ })), Effect.catchTag("ResponseError", ({ response: { status } }) => {
49
+ switch (status) {
50
+ case 402:
51
+ return Effect.fail({
52
+ _tag: "SyncStateIsNotSynced",
53
+ error: { _tag: "PaymentRequiredError" },
54
+ });
55
+ default:
56
+ return Effect.fail({
57
+ _tag: "SyncStateIsNotSynced",
58
+ error: { _tag: "ServerError", status },
59
+ });
60
+ }
61
+ }));
62
+ }).pipe((effect) => Effect.flatMap(Deferred.await(afterInitContext), (context) => Effect.provide(effect, context))),
63
+ });
64
+ });
65
+ const newMessageToBinary = ({ value, ...rest }) => concatBytes(version1, Protobuf.MessageContent.toBinary({ value: valueToProtobuf(value), ...rest }, binaryWriteOptions));
66
+ const version1 = new Uint8Array([0, 1]);
67
+ const valueToProtobuf = (value) => {
68
+ switch (typeof value) {
69
+ case "string":
70
+ return { oneofKind: "stringValue", stringValue: value };
71
+ case "number":
72
+ return {
73
+ oneofKind: "numberValue",
74
+ numberValue: S.encodeSync(S.NumberFromString)(value),
75
+ };
76
+ }
77
+ if (value == null)
78
+ return { oneofKind: undefined };
79
+ if (Predicate.isUint8Array(value))
80
+ return { oneofKind: "bytesValue", bytesValue: value };
81
+ return { oneofKind: "jsonValue", jsonValue: JSON.stringify(value) };
82
+ };
83
+ // The 'protobuf-ts' uses TextEncoder, but polyfill fast-text-encoding
84
+ // doesn't support the fatal option.
85
+ // https://github.com/timostamm/protobuf-ts/issues/184#issuecomment-1658443836
86
+ const binaryWriteOptions = {
87
+ writerFactory: () => new BinaryWriter({
88
+ encode: (input) => new TextEncoder().encode(input),
89
+ }),
90
+ };
91
+ const binaryReadOptions = {
92
+ readerFactory: (bytes) => new BinaryReader(bytes, {
93
+ decode: (input) => new TextDecoder().decode(input),
94
+ }),
95
+ };
96
+ const newMessageFromBinary = (binary) => {
97
+ if (!startsWithArray(binary, version1))
98
+ return Option.none();
99
+ const { value, ...content } = Protobuf.MessageContent.fromBinary(binary.slice(version1.length), binaryReadOptions);
100
+ return Option.some({ value: valueFromProtobuf(value), ...content });
101
+ };
102
+ const startsWithArray = (array, prefix) => {
103
+ if (prefix.length > array.length)
104
+ return false;
105
+ for (let i = 0; i < prefix.length; i++) {
106
+ if (array[i] !== prefix[i])
107
+ return false;
108
+ }
109
+ return true;
110
+ };
111
+ const valueFromProtobuf = (value) => {
112
+ switch (value.oneofKind) {
113
+ case "numberValue":
114
+ return S.decodeSync(S.NumberFromString)(value.numberValue);
115
+ case "stringValue":
116
+ return value.stringValue;
117
+ case "bytesValue":
118
+ return value.bytesValue;
119
+ case "jsonValue":
120
+ return JSON.parse(value.jsonValue);
121
+ case undefined:
122
+ return null;
123
+ default:
124
+ return absurd(value);
125
+ }
126
+ };
@@ -2,18 +2,15 @@ export * from "./Config.js";
2
2
  export * from "./Crdt.js";
3
3
  export * from "./Crypto.js";
4
4
  export * from "./Db.js";
5
- export * from "./DbWorker.js";
6
5
  export * from "./Diff.js";
7
- export * from "./ErrorStore.js";
6
+ export * from "./Error.js";
8
7
  export * from "./Evolu.js";
9
8
  export * from "./Model.js";
10
9
  export * from "./Murmurhash.js";
11
- export * from "./OnCompletes.js";
12
10
  export * from "./Owner.js";
13
11
  export * from "./Platform.js";
14
12
  export * from "./Protobuf.js";
15
- export * from "./Public.js";
16
13
  export * from "./Sqlite.js";
17
14
  export * from "./Store.js";
18
- export * from "./SyncWorker.js";
15
+ export * from "./Sync.js";
19
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC"}
package/dist/src/index.js CHANGED
@@ -2,17 +2,14 @@ export * from "./Config.js";
2
2
  export * from "./Crdt.js";
3
3
  export * from "./Crypto.js";
4
4
  export * from "./Db.js";
5
- export * from "./DbWorker.js";
6
5
  export * from "./Diff.js";
7
- export * from "./ErrorStore.js";
6
+ export * from "./Error.js";
8
7
  export * from "./Evolu.js";
9
8
  export * from "./Model.js";
10
9
  export * from "./Murmurhash.js";
11
- export * from "./OnCompletes.js";
12
10
  export * from "./Owner.js";
13
11
  export * from "./Platform.js";
14
12
  export * from "./Protobuf.js";
15
- export * from "./Public.js";
16
13
  export * from "./Sqlite.js";
17
14
  export * from "./Store.js";
18
- export * from "./SyncWorker.js";
15
+ export * from "./Sync.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evolu/common",
3
- "version": "4.1.1",
3
+ "version": "5.0.1",
4
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
5
  "keywords": [
6
6
  "evolu",
@@ -44,28 +44,30 @@
44
44
  "README.md"
45
45
  ],
46
46
  "dependencies": {
47
- "@noble/ciphers": "^0.5.1",
47
+ "@noble/ciphers": "^0.5.2",
48
48
  "@noble/hashes": "^1.4.0",
49
49
  "@protobuf-ts/runtime": "^2.9.4",
50
- "@scure/bip39": "^1.2.2",
50
+ "@scure/bip39": "^1.3.0",
51
51
  "kysely": "^0.27.3",
52
- "nanoid": "^5.0.6"
52
+ "nanoid": "^5.0.7"
53
53
  },
54
54
  "devDependencies": {
55
- "@effect/schema": "^0.64.10",
55
+ "@effect/platform": "^0.50.7",
56
+ "@effect/schema": "^0.66.10",
56
57
  "@protobuf-ts/plugin": "^2.9.4",
57
58
  "@protobuf-ts/protoc": "^2.9.4",
58
59
  "array-shuffle": "^3.0.0",
59
- "effect": "2.4.10",
60
+ "effect": "^3.0.7",
60
61
  "eslint": "^8.57.0",
61
- "typescript": "^5.4.3",
62
- "vitest": "^1.4.0",
62
+ "typescript": "^5.4.5",
63
+ "vitest": "^1.5.2",
63
64
  "@evolu/tsconfig": "0.0.2",
64
65
  "eslint-config-evolu": "1.0.0"
65
66
  },
66
67
  "peerDependencies": {
67
- "@effect/schema": "^0.64.10",
68
- "effect": "2.4.10"
68
+ "@effect/platform": "^0.50.7",
69
+ "@effect/schema": "^0.66.10",
70
+ "effect": "^3.0.0"
69
71
  },
70
72
  "publishConfig": {
71
73
  "access": "public"
package/src/Config.ts CHANGED
@@ -1,62 +1,103 @@
1
1
  import * as Context from "effect/Context";
2
2
  import * as Layer from "effect/Layer";
3
- import { CreateIndexBuilder } from "kysely";
3
+ import * as LogLevel from "effect/LogLevel";
4
+ import * as Logger from "effect/Logger";
5
+ import * as ManagedRuntime from "effect/ManagedRuntime";
6
+ import * as Match from "effect/Match";
4
7
 
5
8
  export interface Config {
6
9
  /**
7
- * Use the `indexes` property to define SQLite indexes.
10
+ * URL to reload browser tabs after {@link Owner} reset or restore.
8
11
  *
9
- * Table and column names are not typed because Kysely doesn't support it.
10
- *
11
- * https://medium.com/@JasonWyatt/squeezing-performance-from-sqlite-indexes-indexes-c4e175f3c346
12
- *
13
- * @example
14
- * const indexes = [
15
- * createIndex("indexTodoCreatedAt").on("todo").column("createdAt"),
16
- *
17
- * createIndex("indexTodoCategoryCreatedAt")
18
- * .on("todoCategory")
19
- * .column("createdAt"),
20
- * ];
12
+ * The default value is `/`.
21
13
  */
22
- readonly indexes: ReadonlyArray<CreateIndexBuilder<any>>;
23
-
24
- /** Log SQL. */
25
- readonly logSql: boolean;
14
+ reloadUrl: string;
26
15
 
27
16
  /**
28
- * Alternate URL to reload browser tabs after {@link Owner} reset or restore.
29
- * The default value is `/`.
17
+ * URL for Evolu sync and backup server.
18
+ *
19
+ * The default value is `https://evolu.world`.
30
20
  */
31
- readonly reloadUrl: string;
21
+ syncUrl: string;
32
22
 
33
- /** Alternate URL for Evolu sync and backup server. */
34
- readonly syncUrl: string;
23
+ /**
24
+ * Evolu is multitenant - it can run more instances concurrently. Every Evolu
25
+ * instance has to have its own unique name. Database files are separated and
26
+ * invisible to each other.
27
+ *
28
+ * The default value is: `Evolu`.
29
+ */
30
+ name: string;
35
31
 
36
32
  /**
37
- * Evolu application name. For now, this is only useful for localhost
38
- * development, where we want each application to have its own database. The
39
- * default value is: "Evolu".
33
+ * Maximum physical clock drift allowed in ms.
34
+ *
35
+ * The default value is 5 * 60 * 1000 (5 minutes).
40
36
  */
41
- readonly name: string;
37
+ maxDrift: number;
42
38
 
43
39
  /**
44
- * Maximum physical clock drift allowed in ms. The default value is 5 * 60 *
45
- * 1000 (5 minutes).
40
+ * Setting the minimum log level. The default value is `none`.
41
+ *
42
+ * For development, use `trace` to log all events and `debug` to log only
43
+ * events with values. For production, use `warning`.
46
44
  */
47
- readonly maxDrift: number;
45
+ minimumLogLevel: "none" | "trace" | "debug" | "warning";
48
46
  }
49
47
 
50
- export const Config = Context.GenericTag<Config>("@services/Config");
48
+ export const Config = Context.GenericTag<Config>("Config");
51
49
 
52
- const defaultConfig: Config = {
53
- indexes: [],
54
- logSql: false,
50
+ export const defaultConfig: Config = {
55
51
  reloadUrl: "/",
56
52
  syncUrl: "https://evolu.world",
57
53
  name: "Evolu",
58
54
  maxDrift: 5 * 60 * 1000,
55
+ minimumLogLevel: "none",
59
56
  };
60
57
 
61
- export const ConfigLive = (config?: Partial<Config>): Layer.Layer<Config> =>
62
- Layer.succeed(Config, Config.of({ ...defaultConfig, ...config }));
58
+ /** https://effect.website/docs/guides/runtime */
59
+ export const createRuntime = (
60
+ partialConfig?: Partial<Config>,
61
+ ): ManagedRuntime.ManagedRuntime<Config, never> => {
62
+ const config = { ...defaultConfig, ...partialConfig };
63
+ const ConfigLive = Layer.succeed(Config, config);
64
+
65
+ const minimumLogLevel = Match.value(config.minimumLogLevel).pipe(
66
+ Match.when("debug", () => LogLevel.Debug),
67
+ Match.when("none", () => LogLevel.None),
68
+ Match.when("trace", () => LogLevel.Trace),
69
+ Match.when("warning", () => LogLevel.Warning),
70
+ Match.exhaustive,
71
+ );
72
+
73
+ const evoluLayer =
74
+ config.minimumLogLevel === "none"
75
+ ? ConfigLive
76
+ : Layer.mergeAll(
77
+ ConfigLive,
78
+ Logger.minimumLogLevel(minimumLogLevel),
79
+ Logger.replace(Logger.defaultLogger, makeEvoluLogger(config.name)),
80
+ );
81
+
82
+ return ManagedRuntime.make(evoluLayer);
83
+ };
84
+
85
+ // TODO: Spans and Effect native variadic log args.
86
+ const makeEvoluLogger = (name: string) =>
87
+ Logger.make(({ logLevel, message, date }) => {
88
+ const fn =
89
+ logLevel._tag === "Warning"
90
+ ? "warn"
91
+ : logLevel._tag === "Error"
92
+ ? "error"
93
+ : "log";
94
+
95
+ const hours = date.getHours().toString().padStart(2, "0");
96
+ const minutes = date.getMinutes().toString().padStart(2, "0");
97
+ const seconds = date.getSeconds().toString().padStart(2, "0");
98
+ const milliseconds = date.getMilliseconds().toString().padStart(3, "0");
99
+ const formattedDate = `${hours}:${minutes}:${seconds}:${milliseconds}`;
100
+
101
+ const messages: unknown[] = Array.isArray(message) ? message : [message];
102
+ globalThis.console[fn](`${formattedDate} [${name}]`, ...messages);
103
+ });
package/src/Crdt.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as S from "@effect/schema/Schema";
2
+ import * as Arr from "effect/Array";
2
3
  import * as Brand from "effect/Brand";
3
4
  import * as Context from "effect/Context";
4
5
  import * as Effect from "effect/Effect";
@@ -7,7 +8,6 @@ import { pipe } from "effect/Function";
7
8
  import * as Layer from "effect/Layer";
8
9
  import * as Number from "effect/Number";
9
10
  import * as Option from "effect/Option";
10
- import * as ReadonlyArray from "effect/ReadonlyArray";
11
11
  import * as String from "effect/String";
12
12
  import { Config } from "./Config.js";
13
13
  import { NanoIdGenerator, NodeId } from "./Crypto.js";
@@ -36,7 +36,7 @@ export const AllowedTimeRange = {
36
36
  * the device clock is out of range, Evolu will not store data until it's
37
37
  * fixed.
38
38
  */
39
- export const Millis = S.number.pipe(
39
+ export const Millis = S.Number.pipe(
40
40
  S.greaterThan(AllowedTimeRange.greaterThan),
41
41
  S.lessThan(AllowedTimeRange.lessThan),
42
42
  S.brand("Millis"),
@@ -48,7 +48,7 @@ export const initialMillis = S.decodeSync(Millis)(
48
48
  AllowedTimeRange.greaterThan + 1,
49
49
  );
50
50
 
51
- export const Counter = S.number.pipe(S.between(0, 65535), S.brand("Counter"));
51
+ export const Counter = S.Number.pipe(S.between(0, 65535), S.brand("Counter"));
52
52
  export type Counter = S.Schema.Type<typeof Counter>;
53
53
 
54
54
  const initialCounter = S.decodeSync(Counter)(0);
@@ -87,7 +87,7 @@ export const makeSyncTimestamp = (
87
87
  });
88
88
 
89
89
  export const makeInitialTimestamp = NanoIdGenerator.pipe(
90
- Effect.flatMap(({ nanoidAsNodeId }) => nanoidAsNodeId),
90
+ Effect.flatMap(({ nodeId }) => nodeId),
91
91
  Effect.map(
92
92
  (node): Timestamp => ({
93
93
  millis: initialMillis,
@@ -97,15 +97,13 @@ export const makeInitialTimestamp = NanoIdGenerator.pipe(
97
97
  ),
98
98
  );
99
99
 
100
- export interface Time {
101
- readonly now: Effect.Effect<Millis, TimestampTimeOutOfRangeError>;
102
- }
103
-
104
- export const Time = Context.GenericTag<Time>("@services/Time");
105
-
106
- export const TimeLive = Layer.succeed(
100
+ export class Time extends Context.Tag("Time")<
107
101
  Time,
108
- Time.of({
102
+ {
103
+ readonly now: Effect.Effect<Millis, TimestampTimeOutOfRangeError>;
104
+ }
105
+ >() {
106
+ static Live = Layer.succeed(Time, {
109
107
  now: Effect.suspend(() => S.decode(Millis)(Date.now())).pipe(
110
108
  Effect.catchTag("ParseError", () =>
111
109
  Effect.fail<TimestampTimeOutOfRangeError>({
@@ -113,8 +111,8 @@ export const TimeLive = Layer.succeed(
113
111
  }),
114
112
  ),
115
113
  ),
116
- }),
117
- );
114
+ });
115
+ }
118
116
 
119
117
  /**
120
118
  * The TimestampError type represents all Timestamp-related errors. If such an
@@ -146,39 +144,33 @@ export interface TimestampTimeOutOfRangeError {
146
144
  readonly _tag: "TimestampTimeOutOfRangeError";
147
145
  }
148
146
 
149
- const getNextMillis = (
150
- millis: ReadonlyArray<Millis>,
151
- ): Effect.Effect<
152
- Millis,
153
- TimestampDriftError | TimestampTimeOutOfRangeError,
154
- Time | Config
155
- > =>
156
- Effect.gen(function* (_) {
157
- const time = yield* _(Time);
158
- const config = yield* _(Config);
147
+ const getNextMillis = (millis: ReadonlyArray<Millis>) =>
148
+ Effect.gen(function* () {
149
+ const time = yield* Time;
150
+ const config = yield* Config;
159
151
 
160
- const now = yield* _(time.now);
152
+ const now = yield* time.now;
161
153
  const next = Math.max(now, ...millis) as Millis;
162
154
 
163
155
  if (next - now > config.maxDrift)
164
- yield* _(
165
- Effect.fail<TimestampDriftError>({
166
- _tag: "TimestampDriftError",
167
- now,
168
- next,
169
- }),
170
- );
156
+ yield* Effect.fail<TimestampDriftError>({
157
+ _tag: "TimestampDriftError",
158
+ now,
159
+ next,
160
+ });
171
161
 
172
162
  return next;
173
163
  });
174
164
 
175
- const incrementCounter = (
176
- counter: Counter,
177
- ): Either.Either<Counter, TimestampCounterOverflowError> =>
165
+ const incrementCounter = (counter: Counter) =>
178
166
  pipe(
179
167
  Number.increment(counter),
180
168
  S.decodeEither(Counter),
181
- Either.mapLeft(() => ({ _tag: "TimestampCounterOverflowError" })),
169
+ Either.mapLeft(
170
+ (): TimestampCounterOverflowError => ({
171
+ _tag: "TimestampCounterOverflowError",
172
+ }),
173
+ ),
182
174
  );
183
175
 
184
176
  const counterMin = S.decodeSync(Counter)(0);
@@ -192,11 +184,11 @@ export const sendTimestamp = (
192
184
  | TimestampTimeOutOfRangeError,
193
185
  Time | Config
194
186
  > =>
195
- Effect.gen(function* (_) {
196
- const millis = yield* _(getNextMillis([timestamp.millis]));
187
+ Effect.gen(function* () {
188
+ const millis = yield* getNextMillis([timestamp.millis]);
197
189
  const counter =
198
190
  millis === timestamp.millis
199
- ? yield* _(incrementCounter(timestamp.counter))
191
+ ? yield* incrementCounter(timestamp.counter)
200
192
  : counterMin;
201
193
  return { ...timestamp, millis, counter };
202
194
  });
@@ -217,23 +209,19 @@ export const receiveTimestamp = ({
217
209
  > =>
218
210
  Effect.gen(function* (_) {
219
211
  if (local.node === remote.node)
220
- yield* _(
221
- Effect.fail<TimestampDuplicateNodeError>({
222
- _tag: "TimestampDuplicateNodeError",
223
- node: local.node,
224
- }),
225
- );
226
-
227
- const millis = yield* _(getNextMillis([local.millis, remote.millis]));
228
- const counter = yield* _(
229
- millis === local.millis && millis === remote.millis
230
- ? incrementCounter(Math.max(local.counter, remote.counter) as Counter)
231
- : millis === local.millis
232
- ? incrementCounter(local.counter)
233
- : millis === remote.millis
234
- ? incrementCounter(remote.counter)
235
- : Either.right(counterMin),
236
- );
212
+ yield* Effect.fail<TimestampDuplicateNodeError>({
213
+ _tag: "TimestampDuplicateNodeError",
214
+ node: local.node,
215
+ });
216
+
217
+ const millis = yield* getNextMillis([local.millis, remote.millis]);
218
+ const counter = yield* millis === local.millis && millis === remote.millis
219
+ ? incrementCounter(Math.max(local.counter, remote.counter) as Counter)
220
+ : millis === local.millis
221
+ ? incrementCounter(local.counter)
222
+ : millis === remote.millis
223
+ ? incrementCounter(remote.counter)
224
+ : Either.right(counterMin);
237
225
 
238
226
  return { ...local, millis, counter };
239
227
  });
@@ -262,17 +250,15 @@ export const millisToMerkleTreePath = (millis: Millis): MerkleTreePath =>
262
250
  .toString(3)
263
251
  .split("") as MerkleTreePath;
264
252
 
265
- const merkleTreePathToMillis = (path: MerkleTreePath): Millis =>
253
+ const merkleTreePathToMillis = (path: MerkleTreePath) =>
266
254
  path.length === 0
267
255
  ? initialMillis
268
256
  : // 16 is the length of the base 3 value of the current time in minutes.
269
257
  // Ensure it's padded to create the full value.
270
258
  ((parseInt(path.join("").padEnd(16, "0"), 3) * 1000 * 60) as Millis);
271
259
 
272
- const xorTimestampHashes = (
273
- a: TimestampHash | undefined,
274
- b: TimestampHash,
275
- ): TimestampHash => ((a || 0) ^ b) as TimestampHash;
260
+ const xorTimestampHashes = (a: TimestampHash | undefined, b: TimestampHash) =>
261
+ ((a || 0) ^ b) as TimestampHash;
276
262
 
277
263
  const insertKey = (
278
264
  tree: MerkleTree,
@@ -292,23 +278,22 @@ const insertKey = (
292
278
  };
293
279
  };
294
280
 
295
- export const insertIntoMerkleTree =
296
- (timestamp: Timestamp) =>
297
- (tree: MerkleTree): MerkleTree => {
298
- const path = millisToMerkleTreePath(timestamp.millis);
299
- const hash = timestampToHash(timestamp);
300
- return insertKey(
301
- { ...tree, hash: xorTimestampHashes(tree.hash, hash) },
302
- path,
303
- hash,
304
- );
305
- };
281
+ export const insertIntoMerkleTree = (
282
+ tree: MerkleTree,
283
+ timestamp: Timestamp,
284
+ ): MerkleTree => {
285
+ const path = millisToMerkleTreePath(timestamp.millis);
286
+ const hash = timestampToHash(timestamp);
287
+ return insertKey(
288
+ { ...tree, hash: xorTimestampHashes(tree.hash, hash) },
289
+ path,
290
+ hash,
291
+ );
292
+ };
306
293
 
307
294
  const sortedMerkleTreeKeys: ReadonlyArray<MerkleTreeKey> = ["0", "1", "2"];
308
295
 
309
- const getSortedMerkleTreeKeys = (
310
- tree: MerkleTree,
311
- ): ReadonlyArray<MerkleTreeKey> =>
296
+ const getSortedMerkleTreeKeys = (tree: MerkleTree) =>
312
297
  sortedMerkleTreeKeys.filter((key) => key in tree);
313
298
 
314
299
  export const diffMerkleTrees = (
@@ -326,7 +311,7 @@ export const diffMerkleTrees = (
326
311
  // the top of this function should pass)
327
312
  // eslint-disable-next-line no-constant-condition
328
313
  while (1) {
329
- const keys = ReadonlyArray.dedupeWith(
314
+ const keys = Arr.dedupeWith(
330
315
  getSortedMerkleTreeKeys(node1).concat(getSortedMerkleTreeKeys(node2)),
331
316
  String.Equivalence,
332
317
  );