@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.
- package/README.md +1 -1
- package/dist/src/Config.d.ts +26 -27
- package/dist/src/Config.d.ts.map +1 -1
- package/dist/src/Config.js +35 -7
- package/dist/src/Crdt.d.ts +5 -4
- package/dist/src/Crdt.d.ts.map +1 -1
- package/dist/src/Crdt.js +27 -24
- package/dist/src/Crypto.d.ts +16 -10
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +24 -11
- package/dist/src/Db.d.ts +58 -78
- package/dist/src/Db.d.ts.map +1 -1
- package/dist/src/Db.js +383 -185
- package/dist/src/Diff.d.ts +9 -3
- package/dist/src/Diff.d.ts.map +1 -1
- package/dist/src/Diff.js +12 -11
- package/dist/src/Error.d.ts +15 -0
- package/dist/src/Error.d.ts.map +1 -0
- package/dist/src/Error.js +14 -0
- package/dist/src/Evolu.d.ts +139 -91
- package/dist/src/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu.js +267 -239
- package/dist/src/Model.d.ts +52 -0
- package/dist/src/Model.d.ts.map +1 -1
- package/dist/src/Model.js +47 -5
- package/dist/src/Owner.d.ts.map +1 -1
- package/dist/src/Owner.js +14 -8
- package/dist/src/Platform.d.ts +24 -34
- package/dist/src/Platform.d.ts.map +1 -1
- package/dist/src/Platform.js +9 -31
- package/dist/src/Public.d.ts +3 -5
- package/dist/src/Public.d.ts.map +1 -1
- package/dist/src/Public.js +2 -3
- package/dist/src/Sqlite.d.ts +25 -15
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +28 -7
- package/dist/src/Store.d.ts.map +1 -1
- package/dist/src/Sync.d.ts +70 -0
- package/dist/src/Sync.d.ts.map +1 -0
- package/dist/src/Sync.js +126 -0
- package/dist/src/index.d.ts +2 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -5
- package/package.json +12 -10
- package/src/Config.ts +82 -39
- package/src/Crdt.ts +61 -76
- package/src/Crypto.ts +83 -60
- package/src/Db.ts +802 -370
- package/src/Diff.ts +23 -21
- package/src/Error.ts +29 -0
- package/src/Evolu.ts +599 -499
- package/src/Model.ts +102 -6
- package/src/Owner.ts +24 -24
- package/src/Platform.ts +33 -81
- package/src/Public.ts +3 -5
- package/src/Sqlite.ts +81 -27
- package/src/Sync.ts +306 -0
- package/src/index.ts +2 -5
- package/dist/src/DbWorker.d.ts +0 -82
- package/dist/src/DbWorker.d.ts.map +0 -1
- package/dist/src/DbWorker.js +0 -335
- package/dist/src/ErrorStore.d.ts +0 -29
- package/dist/src/ErrorStore.d.ts.map +0 -1
- package/dist/src/ErrorStore.js +0 -13
- package/dist/src/OnCompletes.d.ts +0 -14
- package/dist/src/OnCompletes.d.ts.map +0 -1
- package/dist/src/OnCompletes.js +0 -25
- package/dist/src/SyncWorker.d.ts +0 -80
- package/dist/src/SyncWorker.d.ts.map +0 -1
- package/dist/src/SyncWorker.js +0 -150
- package/dist/src/Types.d.ts +0 -13
- package/dist/src/Types.d.ts.map +0 -1
- package/dist/src/Types.js +0 -1
- package/src/DbWorker.ts +0 -721
- package/src/ErrorStore.ts +0 -46
- package/src/OnCompletes.ts +0 -51
- package/src/SyncWorker.ts +0 -361
- 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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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>("
|
|
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*
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
|
3
|
+
import * as Scope from "effect/Scope";
|
|
4
|
+
import { Config } from "./Config.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
* FlushSync
|
|
7
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
Effect.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
72
|
-
readonly
|
|
41
|
+
export interface SyncLockRelease {
|
|
42
|
+
readonly release: Effect.Effect<void>;
|
|
73
43
|
}
|
|
74
44
|
|
|
75
|
-
export
|
|
76
|
-
readonly
|
|
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
|
|
9
|
-
export {
|
|
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 {
|
|
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
|
|
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>("
|
|
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
|
-
|
|
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
|
|
108
|
-
Effect.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
Effect.tap(() =>
|
|
112
|
-
|
|
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;
|