@evolu/common 4.1.0 → 5.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 (78) hide show
  1. package/README.md +1 -1
  2. package/dist/src/Config.d.ts +26 -27
  3. package/dist/src/Config.d.ts.map +1 -1
  4. package/dist/src/Config.js +35 -7
  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 +383 -185
  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 -15
  35. package/dist/src/Sqlite.d.ts.map +1 -1
  36. package/dist/src/Sqlite.js +28 -7
  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 +82 -39
  46. package/src/Crdt.ts +61 -76
  47. package/src/Crypto.ts +83 -60
  48. package/src/Db.ts +802 -370
  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 -27
  57. package/src/Sync.ts +306 -0
  58. package/src/index.ts +2 -5
  59. package/dist/src/DbWorker.d.ts +0 -82
  60. package/dist/src/DbWorker.d.ts.map +0 -1
  61. package/dist/src/DbWorker.js +0 -335
  62. package/dist/src/ErrorStore.d.ts +0 -29
  63. package/dist/src/ErrorStore.d.ts.map +0 -1
  64. package/dist/src/ErrorStore.js +0 -13
  65. package/dist/src/OnCompletes.d.ts +0 -14
  66. package/dist/src/OnCompletes.d.ts.map +0 -1
  67. package/dist/src/OnCompletes.js +0 -25
  68. package/dist/src/SyncWorker.d.ts +0 -80
  69. package/dist/src/SyncWorker.d.ts.map +0 -1
  70. package/dist/src/SyncWorker.js +0 -150
  71. package/dist/src/Types.d.ts +0 -13
  72. package/dist/src/Types.d.ts.map +0 -1
  73. package/dist/src/Types.js +0 -1
  74. package/src/DbWorker.ts +0 -721
  75. package/src/ErrorStore.ts +0 -46
  76. package/src/OnCompletes.ts +0 -51
  77. package/src/SyncWorker.ts +0 -361
  78. package/src/Types.ts +0 -11
package/src/Model.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import * as S from "@effect/schema/Schema";
2
- import { maybeJson } from "./Sqlite.js";
2
+ import * as Types from "effect/Types";
3
+ import { Value, maybeJson } from "./Sqlite.js";
3
4
 
4
5
  /** Branded Id Schema. To create Id Schema for a specific table, use {@link id}. */
5
- export const Id = S.string.pipe(S.pattern(/^[\w-]{21}$/), S.brand("Id"));
6
+ export const Id = S.String.pipe(S.pattern(/^[\w-]{21}$/), S.brand("Id"));
6
7
  export type Id = S.Schema.Type<typeof Id>;
7
8
 
8
9
  /**
@@ -25,7 +26,7 @@ export const id = <T extends string>(
25
26
  * the {@link cast} helper to cast SqliteDate from Date and back.
26
27
  * https://www.sqlite.org/quirks.html#no_separate_datetime_datatype
27
28
  */
28
- export const SqliteDate = S.string.pipe(
29
+ export const SqliteDate = S.String.pipe(
29
30
  S.filter((s) => !isNaN(Date.parse(s))),
30
31
  S.brand("SqliteDate"),
31
32
  );
@@ -36,7 +37,7 @@ export type SqliteDate = S.Schema.Type<typeof SqliteDate>;
36
37
  * Use the {@link cast} helper to cast SqliteBoolean from boolean and back.
37
38
  * https://www.sqlite.org/quirks.html#no_separate_boolean_datatype
38
39
  */
39
- export const SqliteBoolean = S.number.pipe(
40
+ export const SqliteBoolean = S.Number.pipe(
40
41
  S.int(),
41
42
  S.filter((s) => s === 0 || s === 1),
42
43
  S.brand("SqliteBoolean"),
@@ -71,13 +72,108 @@ export function cast(
71
72
  return new Date(value);
72
73
  }
73
74
 
75
+ interface EvoluTypeError<E extends string> {
76
+ readonly __evoluTypeError__: E;
77
+ }
78
+
79
+ /**
80
+ * Create table schema.
81
+ *
82
+ * Supported types are null, string, number, Uint8Array, JSON Object, and JSON
83
+ * Array. Use SqliteDate for dates and SqliteBoolean for booleans.
84
+ *
85
+ * Reserved columns are createdAt, updatedAt, isDeleted. Those columns are added
86
+ * by default.
87
+ *
88
+ * @example
89
+ * const TodoId = id("Todo");
90
+ * type TodoId = S.Schema.Type<typeof TodoId>;
91
+ *
92
+ * const TodoTable = table({
93
+ * id: TodoId,
94
+ * title: NonEmptyString1000,
95
+ * isCompleted: S.nullable(SqliteBoolean),
96
+ * });
97
+ * type TodoTable = S.Schema.Type<typeof TodoTable>;
98
+ */
99
+ export const table = <Fields extends TableFields>(
100
+ fields: Fields,
101
+ // Because Schema is invariant, we have to do validation like this.
102
+ ): ValidateFieldsTypes<Fields> extends true
103
+ ? ValidateFieldsNames<Fields> extends true
104
+ ? ValidateFieldsHasId<Fields> extends true
105
+ ? S.Schema<
106
+ Types.Simplify<
107
+ S.Struct.Type<Fields> & S.Schema.Type<typeof ReservedColumns>
108
+ >,
109
+ Types.Simplify<
110
+ S.Struct.Encoded<Fields> & S.Schema.Encoded<typeof ReservedColumns>
111
+ >
112
+ >
113
+ : EvoluTypeError<"table() called without id column.">
114
+ : EvoluTypeError<"table() called with a reserved column. Reserved columns are createdAt, updatedAt, isDeleted. Those columns are added by default.">
115
+ : EvoluTypeError<"table() called with unsupported type. Supported types are null, string, number, Uint8Array, JSON Object, and JSON Array. Use SqliteDate for dates and SqliteBoolean for booleans."> =>
116
+ S.Struct(fields).pipe(S.extend(ReservedColumns)) as never;
117
+
118
+ const ReservedColumns = S.Struct({
119
+ createdAt: SqliteDate,
120
+ updatedAt: SqliteDate,
121
+ isDeleted: SqliteBoolean,
122
+ });
123
+
124
+ type TableFields = Record<string, S.Schema<any>>;
125
+
126
+ type ValidateFieldsTypes<Fields extends TableFields> =
127
+ keyof Fields extends infer K
128
+ ? K extends keyof Fields
129
+ ? Fields[K] extends TableFields
130
+ ? ValidateFieldsTypes<Fields[K]>
131
+ : // eslint-disable-next-line @typescript-eslint/no-unused-vars
132
+ Fields[K] extends S.Schema<infer A, infer _I>
133
+ ? A extends Value
134
+ ? true
135
+ : false
136
+ : never
137
+ : never
138
+ : never;
139
+
140
+ type ValidateFieldsNames<Fields extends TableFields> =
141
+ keyof Fields extends infer K
142
+ ? K extends keyof Fields
143
+ ? K extends "createdAt" | "updatedAt" | "isDeleted"
144
+ ? false
145
+ : true
146
+ : never
147
+ : never;
148
+
149
+ type ValidateFieldsHasId<Fields extends TableFields> = "id" extends keyof Fields
150
+ ? true
151
+ : false;
152
+
153
+ /**
154
+ * Create database schema.
155
+ *
156
+ * Tables with a name prefixed with _ are local-only, which means they are not
157
+ * synced. Local-only tables are useful for device-specific or temporal data.
158
+ *
159
+ * @example
160
+ * const Database = database({
161
+ * // A local-only table.
162
+ * _todo: TodoTable,
163
+ * todo: TodoTable,
164
+ * todoCategory: TodoCategoryTable,
165
+ * });
166
+ * type Database = S.Schema.Type<typeof Database>;
167
+ */
168
+ export const database = S.Struct;
169
+
74
170
  /**
75
171
  * String schema represents a string that is not stringified JSON. Using String
76
172
  * schema for strings stored in SQLite is crucial to ensure a stored string is
77
173
  * not automatically parsed to a JSON object or array when retrieved. Use String
78
174
  * schema for all string-based schemas.
79
175
  */
80
- export const String = S.string.pipe(
176
+ export const String = S.String.pipe(
81
177
  S.filter(
82
178
  (s) => {
83
179
  if (!maybeJson(s)) return true;
@@ -131,7 +227,7 @@ export type NonEmptyString1000 = S.Schema.Type<typeof NonEmptyString1000>;
131
227
  *
132
228
  * S.decode(PositiveInt)(value);
133
229
  */
134
- export const PositiveInt = S.number.pipe(
230
+ export const PositiveInt = S.Number.pipe(
135
231
  S.int(),
136
232
  S.positive(),
137
233
  S.brand("PositiveInt"),
package/src/Owner.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import * as Brand from "effect/Brand";
2
2
  import * as Context from "effect/Context";
3
3
  import * as Effect from "effect/Effect";
4
- import { urlAlphabet } from "nanoid";
5
4
  import { Bip39, Mnemonic, slip21Derive } from "./Crypto.js";
6
5
  import { Id } from "./Model.js";
7
6
 
@@ -28,7 +27,7 @@ export interface Owner {
28
27
  readonly encryptionKey: Uint8Array;
29
28
  }
30
29
 
31
- export const Owner = Context.GenericTag<Owner>("@services/Owner");
30
+ export const Owner = Context.GenericTag<Owner>("Owner");
32
31
 
33
32
  /**
34
33
  * The unique identifier of {@link Owner} safely derived from its
@@ -40,28 +39,29 @@ export const makeOwner = (
40
39
  mnemonic?: Mnemonic,
41
40
  ): Effect.Effect<Owner, never, Bip39> =>
42
41
  Effect.gen(function* (_) {
43
- const bip39 = yield* _(Bip39);
44
-
45
- if (mnemonic == null) mnemonic = yield* _(bip39.make);
46
-
47
- const seed = yield* _(bip39.toSeed(mnemonic));
48
-
49
- const id = yield* _(
50
- slip21Derive(seed, ["Evolu", "Owner Id"]).pipe(
51
- Effect.map((key) => {
52
- // convert key to nanoid
53
- let id = "";
54
- for (let i = 0; i < 21; i++) {
55
- id += urlAlphabet[key[i] & 63];
56
- }
57
- return id as OwnerId;
58
- }),
59
- ),
42
+ const bip39 = yield* Bip39;
43
+ if (mnemonic == null) mnemonic = yield* bip39.make;
44
+ const seed = yield* bip39.toSeed(mnemonic);
45
+ const id = yield* Effect.map(
46
+ slip21Derive(seed, ["Evolu", "Owner Id"]),
47
+ (key) => {
48
+ // convert key to nanoid
49
+ let id = "";
50
+ for (let i = 0; i < 21; i++) {
51
+ id += urlAlphabet[key[i] & 63];
52
+ }
53
+ return id as OwnerId;
54
+ },
60
55
  );
61
-
62
- const encryptionKey = yield* _(
63
- slip21Derive(seed, ["Evolu", "Encryption Key"]),
64
- );
65
-
56
+ const encryptionKey = yield* slip21Derive(seed, [
57
+ "Evolu",
58
+ "Encryption Key",
59
+ ]);
66
60
  return { mnemonic, id, encryptionKey };
67
61
  });
62
+
63
+ // From https://github.com/ai/nanoid/blob/main/url-alphabet/index.js
64
+ // It's copy-pasted for now because NanoId import breaks Metro bundler.
65
+ // https://github.com/ai/nanoid/issues/468
66
+ const urlAlphabet =
67
+ "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
package/src/Platform.ts CHANGED
@@ -1,95 +1,47 @@
1
1
  import * as Context from "effect/Context";
2
2
  import * as Effect from "effect/Effect";
3
- import * as Layer from "effect/Layer";
3
+ import * as Scope from "effect/Scope";
4
+ import { Config } from "./Config.js";
4
5
 
5
6
  /**
6
- * FlushSync lets you force React to flush any updates inside the provided
7
- * callback synchronously. This ensures that the DOM is updated immediately.
7
+ * FlushSync is a service for libraries like React to synchronously flush
8
+ * updates inside the provided callback to ensure the DOM is updated
9
+ * immediately.
8
10
  *
9
- * It's required only for React DOM. The other UI libraries probably don't need
10
- * that, so it should be a no-op.
11
+ * https://react.dev/reference/react-dom/flushSync
11
12
  */
12
13
  export type FlushSync = (callback: () => void) => void;
14
+ export const FlushSync = Context.GenericTag<FlushSync>("FlushSync");
15
+
16
+ export class AppState extends Context.Tag("AppState")<
17
+ AppState,
18
+ {
19
+ readonly init: (options: {
20
+ readonly reloadUrl: string;
21
+ readonly onRequestSync: () => void;
22
+ }) => Effect.Effect<AppStateReset>;
23
+ }
24
+ >() {}
13
25
 
14
- export const FlushSync = Context.GenericTag<FlushSync>("@services/FlushSync");
15
-
16
- export const FlushSyncDefaultLive = Layer.succeed(FlushSync, (callback) =>
17
- callback(),
18
- );
19
-
20
- export interface SyncLock {
21
- /**
22
- * Try to acquire a sync lock. The caller must not call sync if a sync lock
23
- * can't be acquired.
24
- */
25
- readonly acquire: Effect.Effect<boolean>;
26
-
27
- /** Release a sync lock. */
28
- readonly release: Effect.Effect<void>;
29
- }
30
-
31
- export const SyncLock = Context.GenericTag<SyncLock>("@services/SyncLock");
32
-
33
- export type DbWorkerLock = (callback: () => Promise<void>) => void;
34
- export const DbWorkerLock = Context.GenericTag<DbWorkerLock>(
35
- "@services/DbWorkerLock",
36
- );
37
-
38
- export type Fetch = (
39
- url: string,
40
- body: Uint8Array,
41
- ) => Effect.Effect<Response, FetchError>;
42
-
43
- export const Fetch = Context.GenericTag<Fetch>("@services/Fetch");
44
-
45
- /**
46
- * This error occurs when there is a problem with the network connection, or the
47
- * server cannot be reached.
48
- */
49
- export interface FetchError {
50
- readonly _tag: "FetchError";
26
+ interface AppStateReset {
27
+ readonly reset: Effect.Effect<void>;
51
28
  }
52
29
 
53
- export const FetchLive = Layer.succeed(
54
- Fetch,
55
- Fetch.of((url, body) =>
56
- Effect.tryPromise({
57
- try: () =>
58
- fetch(url, {
59
- method: "POST",
60
- body,
61
- headers: {
62
- "Content-Type": "application/x-protobuf",
63
- "Content-Length": body.length.toString(),
64
- },
65
- }),
66
- catch: (): FetchError => ({ _tag: "FetchError" }),
67
- }),
68
- ),
69
- );
30
+ export class SyncLock extends Context.Tag("SyncLock")<
31
+ SyncLock,
32
+ {
33
+ readonly tryAcquire: Effect.Effect<
34
+ SyncLockRelease,
35
+ SyncLockAlreadySyncingError,
36
+ Config | Scope.Scope
37
+ >;
38
+ }
39
+ >() {}
70
40
 
71
- interface AppStateConfig {
72
- readonly onRequestSync: () => void;
41
+ export interface SyncLockRelease {
42
+ readonly release: Effect.Effect<void>;
73
43
  }
74
44
 
75
- export interface AppState {
76
- readonly init: (config: AppStateConfig) => void;
77
- readonly reset: Effect.Effect<void>;
45
+ export class SyncLockAlreadySyncingError {
46
+ readonly _tag = "SyncLockAlreadySyncingError";
78
47
  }
79
-
80
- export const AppState = Context.GenericTag<AppState>("@services/AppState");
81
-
82
- /** To detect whether DOM can be used. */
83
- export const canUseDom = ((): boolean => {
84
- // IDK why try-catch is necessary, but it is.
85
- // "ReferenceError: window is not defined" should not happen, but it does.
86
- try {
87
- return !!(
88
- typeof window !== "undefined" &&
89
- window.document &&
90
- window.document.createElement
91
- );
92
- } catch (e) {
93
- return false;
94
- }
95
- })();
package/src/Public.ts CHANGED
@@ -3,11 +3,9 @@ export type { NotNull } from "kysely";
3
3
  export { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/sqlite";
4
4
  export type { Timestamp, TimestampError } from "./Crdt.js";
5
5
  export type { InvalidMnemonicError, Mnemonic } from "./Crypto.js";
6
- export { database, table } from "./Db.js";
7
6
  export type { ExtractRow, QueryResult } from "./Db.js";
8
- export type { EvoluError, UnexpectedError } from "./ErrorStore.js";
9
- export { createIndex } from "./Evolu.js";
7
+ export * from "./Error.js";
8
+ export { createIndexes } from "./Evolu.js";
10
9
  export * from "./Model.js";
11
10
  export type { Owner, OwnerId } from "./Owner.js";
12
- export { canUseDom } from "./Platform.js";
13
- export type { SyncState } from "./SyncWorker.js";
11
+ export type { SyncState } from "./Sync.js";
package/src/Sqlite.ts CHANGED
@@ -1,13 +1,81 @@
1
+ import * as Console from "effect/Console";
1
2
  import * as Context from "effect/Context";
2
3
  import * as Effect from "effect/Effect";
3
- import { Equivalence } from "effect/Equivalence";
4
+ import * as Exit from "effect/Exit";
5
+ import * as Layer from "effect/Layer";
4
6
  import * as Predicate from "effect/Predicate";
7
+ import { Config } from "./Config.js";
5
8
 
6
9
  export interface Sqlite {
7
10
  readonly exec: (query: SqliteQuery) => Effect.Effect<SqliteExecResult>;
11
+ readonly transaction: (
12
+ /**
13
+ * Use `exclusive` for mutations and `shared` for read-only queries. This
14
+ * shared/exclusive lock pattern allows multiple simultaneous readers but
15
+ * only one writer. In Evolu, this pattern also ensures that every write can
16
+ * be immediately read without waiting to complete. For example, we can add
17
+ * data on one page and then immediately redirect to another, and the data
18
+ * will be there.
19
+ *
20
+ * There is also a `last` mode that ensures no other transaction can run.
21
+ * It's for Db reset to ensure no data are accidentally saved after database
22
+ * wipe-out.
23
+ */
24
+ mode: SqliteTransactionMode,
25
+ ) => <A, E, R>(
26
+ effect: Effect.Effect<A, E, R>,
27
+ ) => Effect.Effect<A, E, Sqlite | R>;
8
28
  }
9
29
 
10
- export const Sqlite = Context.GenericTag<Sqlite>("@services/Sqlite");
30
+ export const Sqlite = Context.GenericTag<Sqlite>("Sqlite");
31
+
32
+ export type SqliteTransactionMode = "exclusive" | "shared" | "last";
33
+
34
+ export class SqliteFactory extends Context.Tag("SqliteFactory")<
35
+ SqliteFactory,
36
+ {
37
+ readonly createSqlite: Effect.Effect<Sqlite, never, Config>;
38
+ }
39
+ >() {
40
+ static Common = Layer.effect(
41
+ SqliteFactory,
42
+ Effect.map(SqliteFactory, (platformSqliteFactory) => ({
43
+ createSqlite: Effect.logTrace("SqliteFactory createSqlite").pipe(
44
+ Effect.zipRight(platformSqliteFactory.createSqlite),
45
+ Effect.map(
46
+ (platformSqlite): Sqlite => ({
47
+ exec: (query) =>
48
+ platformSqlite.exec(query).pipe(
49
+ Effect.tap((result) => {
50
+ maybeParseJson(result.rows);
51
+ }),
52
+ Effect.tap((result) =>
53
+ ["begin", "rollback", "commit"].includes(query.sql)
54
+ ? Effect.logDebug(`SQLiteCommon ${query.sql} transaction`)
55
+ : Effect.logDebug(["SQLiteCommon exec", query, result]),
56
+ ),
57
+ ),
58
+ transaction: (mode) => (effect) => {
59
+ // Shared is for readonly queries.
60
+ if (mode === "shared")
61
+ return platformSqlite.transaction(mode)(effect);
62
+ return Effect.flatMap(Sqlite, (sqlite) =>
63
+ Effect.acquireUseRelease(
64
+ sqlite.exec({ sql: "begin" }),
65
+ () => effect,
66
+ (_, exit) =>
67
+ Exit.isFailure(exit)
68
+ ? sqlite.exec({ sql: "rollback" })
69
+ : sqlite.exec({ sql: "commit" }),
70
+ ),
71
+ ).pipe(platformSqlite.transaction(mode));
72
+ },
73
+ }),
74
+ ),
75
+ ),
76
+ })),
77
+ );
78
+ }
11
79
 
12
80
  export interface SqliteQuery {
13
81
  readonly sql: string;
@@ -49,8 +117,10 @@ export const valuesToSqliteValues = (
49
117
  isJsonObjectOrArray(value) ? JSON.stringify(value) : value,
50
118
  );
51
119
 
52
- export const maybeParseJson = (rows: SqliteRow[]): SqliteRow[] =>
120
+ /** This function mutates for better performance. */
121
+ export const maybeParseJson = (rows: SqliteRow[]): void => {
53
122
  parseArray(rows);
123
+ };
54
124
 
55
125
  const parseArray = <T>(a: T[]): T[] => {
56
126
  for (let i = 0; i < a.length; ++i) a[i] = parse(a[i]) as T;
@@ -104,12 +174,14 @@ export const maybeLogSqliteQueryExecutionTime =
104
174
  (query: SqliteQuery) =>
105
175
  <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> => {
106
176
  if (!query.options?.logQueryExecutionTime) return effect;
107
- return effect.pipe(
108
- Effect.tap(() => Effect.log("QueryExecutionTime")),
109
- // Not using Effect.log because of formating
110
- // eslint-disable-next-line no-console
111
- Effect.tap(() => console.log(query.sql)),
112
- Effect.withLogSpan("duration"),
177
+ return Effect.Do.pipe(
178
+ Effect.let("start", () => performance.now()),
179
+ Effect.bind("result", () => effect),
180
+ Effect.let("elapsed", ({ start }) => performance.now() - start),
181
+ Effect.tap(({ elapsed }) =>
182
+ Console.log(`QueryExecutionTime: ${elapsed}ms`, query),
183
+ ),
184
+ Effect.map(({ result }) => result),
113
185
  );
114
186
  };
115
187
 
@@ -136,21 +208,3 @@ export const drawSqliteQueryPlan = (rows: SqliteQueryPlanRow[]): string =>
136
208
  return `${" ".repeat(indent)}${row.detail}`;
137
209
  })
138
210
  .join("\n");
139
-
140
- export interface SqliteSchema {
141
- readonly tables: ReadonlyArray<Table>;
142
- readonly indexes: ReadonlyArray<Index>;
143
- }
144
-
145
- export interface Table {
146
- readonly name: string;
147
- readonly columns: ReadonlyArray<string>;
148
- }
149
-
150
- export interface Index {
151
- readonly name: string;
152
- readonly sql: string;
153
- }
154
-
155
- export const indexEquivalence: Equivalence<Index> = (self, that) =>
156
- self.name === that.name && self.sql === that.sql;