@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/Db.ts
CHANGED
|
@@ -1,206 +1,160 @@
|
|
|
1
|
-
import * as AST from "@effect/schema/AST";
|
|
2
1
|
import * as S from "@effect/schema/Schema";
|
|
3
|
-
import
|
|
2
|
+
import * as Arr from "effect/Array";
|
|
4
3
|
import * as Brand from "effect/Brand";
|
|
4
|
+
import * as Console from "effect/Console";
|
|
5
5
|
import * as Context from "effect/Context";
|
|
6
|
+
import * as Deferred from "effect/Deferred";
|
|
6
7
|
import * as Effect from "effect/Effect";
|
|
7
|
-
import
|
|
8
|
-
import { pipe } from "effect/Function";
|
|
9
|
-
import * as Layer from "effect/Layer";
|
|
8
|
+
import { Equivalence } from "effect/Equivalence";
|
|
9
|
+
import { constVoid, pipe } from "effect/Function";
|
|
10
10
|
import * as Option from "effect/Option";
|
|
11
11
|
import * as Predicate from "effect/Predicate";
|
|
12
|
-
import * as
|
|
13
|
-
import * as
|
|
12
|
+
import * as Record from "effect/Record";
|
|
13
|
+
import * as Ref from "effect/Ref";
|
|
14
|
+
import * as Schedule from "effect/Schedule";
|
|
14
15
|
import * as String from "effect/String";
|
|
15
|
-
import * as
|
|
16
|
+
import * as SynchronizedRef from "effect/SynchronizedRef";
|
|
16
17
|
import * as Kysely from "kysely";
|
|
18
|
+
import { Config } from "./Config.js";
|
|
17
19
|
import {
|
|
20
|
+
MerkleTree,
|
|
21
|
+
Millis,
|
|
22
|
+
Time,
|
|
23
|
+
Timestamp,
|
|
24
|
+
TimestampCounterOverflowError,
|
|
25
|
+
TimestampDriftError,
|
|
26
|
+
TimestampString,
|
|
27
|
+
TimestampTimeOutOfRangeError,
|
|
28
|
+
diffMerkleTrees,
|
|
18
29
|
initialMerkleTree,
|
|
30
|
+
insertIntoMerkleTree,
|
|
19
31
|
makeInitialTimestamp,
|
|
32
|
+
makeSyncTimestamp,
|
|
20
33
|
merkleTreeToString,
|
|
34
|
+
receiveTimestamp,
|
|
35
|
+
sendTimestamp,
|
|
21
36
|
timestampToString,
|
|
37
|
+
unsafeTimestampFromString,
|
|
22
38
|
} from "./Crdt.js";
|
|
23
39
|
import { Bip39, Mnemonic, NanoIdGenerator } from "./Crypto.js";
|
|
24
|
-
import {
|
|
25
|
-
import { Id, SqliteBoolean, SqliteDate } from "./Model.js";
|
|
26
|
-
import { Owner, makeOwner } from "./Owner.js";
|
|
40
|
+
import { QueryPatches, makePatches } from "./Diff.js";
|
|
27
41
|
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
} from "./
|
|
42
|
+
EvoluError,
|
|
43
|
+
ensureTransferableError,
|
|
44
|
+
makeUnexpectedError,
|
|
45
|
+
} from "./Error.js";
|
|
46
|
+
import { Id, cast } from "./Model.js";
|
|
47
|
+
import { Owner, OwnerId, makeOwner } from "./Owner.js";
|
|
48
|
+
import { SyncLock } from "./Platform.js";
|
|
49
|
+
import * as Sql from "./Sql.js";
|
|
33
50
|
import {
|
|
34
|
-
Index,
|
|
35
|
-
indexEquivalence,
|
|
36
51
|
JsonObjectOrArray,
|
|
37
52
|
Sqlite,
|
|
53
|
+
SqliteExecResult,
|
|
54
|
+
SqliteFactory,
|
|
38
55
|
SqliteQuery,
|
|
39
56
|
SqliteQueryOptions,
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
SqliteQueryPlanRow,
|
|
58
|
+
SqliteTransactionMode,
|
|
42
59
|
Value,
|
|
60
|
+
drawSqliteQueryPlan,
|
|
43
61
|
isJsonObjectOrArray,
|
|
44
62
|
} from "./Sqlite.js";
|
|
45
|
-
import {
|
|
63
|
+
import {
|
|
64
|
+
Message,
|
|
65
|
+
NewMessage,
|
|
66
|
+
Sync,
|
|
67
|
+
SyncData,
|
|
68
|
+
SyncFactory,
|
|
69
|
+
SyncResult,
|
|
70
|
+
SyncState,
|
|
71
|
+
} from "./Sync.js";
|
|
72
|
+
|
|
73
|
+
export interface Db {
|
|
74
|
+
readonly init: (
|
|
75
|
+
schema: DbSchema,
|
|
76
|
+
initialData: ReadonlyArray<Mutation>,
|
|
77
|
+
onError: Callbacks["onError"],
|
|
78
|
+
onSyncStateChange: Callbacks["onSyncStateChange"],
|
|
79
|
+
onReceive: Callbacks["onReceive"],
|
|
80
|
+
) => Effect.Effect<
|
|
81
|
+
Owner,
|
|
82
|
+
| NotSupportedPlatformError
|
|
83
|
+
| TimestampTimeOutOfRangeError
|
|
84
|
+
| TimestampDriftError
|
|
85
|
+
| TimestampCounterOverflowError,
|
|
86
|
+
Config
|
|
87
|
+
>;
|
|
88
|
+
|
|
89
|
+
readonly loadQueries: (
|
|
90
|
+
queries: ReadonlyArray<Query>,
|
|
91
|
+
) => Effect.Effect<ReadonlyArray<QueryPatches>>;
|
|
92
|
+
|
|
93
|
+
readonly mutate: (
|
|
94
|
+
mutations: ReadonlyArray<Mutation>,
|
|
95
|
+
queriesToRefresh: ReadonlyArray<Query>,
|
|
96
|
+
) => Effect.Effect<
|
|
97
|
+
ReadonlyArray<QueryPatches>,
|
|
98
|
+
| TimestampTimeOutOfRangeError
|
|
99
|
+
| TimestampDriftError
|
|
100
|
+
| TimestampCounterOverflowError,
|
|
101
|
+
Config
|
|
102
|
+
>;
|
|
103
|
+
|
|
104
|
+
readonly resetOwner: () => Effect.Effect<void>;
|
|
105
|
+
|
|
106
|
+
readonly restoreOwner: (
|
|
107
|
+
schema: DbSchema,
|
|
108
|
+
mnemonic: Mnemonic,
|
|
109
|
+
) => Effect.Effect<void>;
|
|
110
|
+
|
|
111
|
+
readonly ensureSchema: (schema: DbSchema) => Effect.Effect<void>;
|
|
112
|
+
|
|
113
|
+
readonly sync: (
|
|
114
|
+
queriesToRefresh: ReadonlyArray<Query>,
|
|
115
|
+
) => Effect.Effect<ReadonlyArray<QueryPatches>, never, Config>;
|
|
116
|
+
}
|
|
46
117
|
|
|
47
|
-
export
|
|
118
|
+
export interface DbSchema {
|
|
119
|
+
readonly tables: ReadonlyArray<Table>;
|
|
120
|
+
readonly indexes: ReadonlyArray<Index>;
|
|
121
|
+
}
|
|
48
122
|
|
|
49
|
-
|
|
50
|
-
readonly
|
|
51
|
-
|
|
123
|
+
export interface Table {
|
|
124
|
+
readonly name: string;
|
|
125
|
+
readonly columns: ReadonlyArray<string>;
|
|
126
|
+
}
|
|
52
127
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
* Supported types are null, string, number, Uint8Array, JSON Object, and JSON
|
|
57
|
-
* Array. Use SqliteDate for dates and SqliteBoolean for booleans.
|
|
58
|
-
*
|
|
59
|
-
* Reserved columns are createdAt, updatedAt, isDeleted. Those columns are added
|
|
60
|
-
* by default.
|
|
61
|
-
*
|
|
62
|
-
* @example
|
|
63
|
-
* const TodoId = id("Todo");
|
|
64
|
-
* type TodoId = S.Schema.Type<typeof TodoId>;
|
|
65
|
-
*
|
|
66
|
-
* const TodoTable = table({
|
|
67
|
-
* id: TodoId,
|
|
68
|
-
* title: NonEmptyString1000,
|
|
69
|
-
* isCompleted: S.nullable(SqliteBoolean),
|
|
70
|
-
* });
|
|
71
|
-
* type TodoTable = S.Schema.Type<typeof TodoTable>;
|
|
72
|
-
*/
|
|
73
|
-
export const table = <Fields extends TableFields>(
|
|
74
|
-
fields: Fields,
|
|
75
|
-
// Because Schema is invariant, we have to do validation like this.
|
|
76
|
-
): ValidateFieldsTypes<Fields> extends true
|
|
77
|
-
? ValidateFieldsNames<Fields> extends true
|
|
78
|
-
? ValidateFieldsHasId<Fields> extends true
|
|
79
|
-
? S.Schema<
|
|
80
|
-
Types.Simplify<
|
|
81
|
-
S.Struct.Type<Fields> & S.Schema.Type<typeof ReservedColumns>
|
|
82
|
-
>,
|
|
83
|
-
Types.Simplify<
|
|
84
|
-
S.Struct.Encoded<Fields> & S.Schema.Encoded<typeof ReservedColumns>
|
|
85
|
-
>
|
|
86
|
-
>
|
|
87
|
-
: EvoluTypeError<"table() called without id column.">
|
|
88
|
-
: EvoluTypeError<"table() called with a reserved column. Reserved columns are createdAt, updatedAt, isDeleted. Those columns are added by default.">
|
|
89
|
-
: 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."> =>
|
|
90
|
-
S.struct(fields).pipe(S.extend(ReservedColumns)) as never;
|
|
91
|
-
|
|
92
|
-
const ReservedColumns = S.struct({
|
|
93
|
-
createdAt: SqliteDate,
|
|
94
|
-
updatedAt: SqliteDate,
|
|
95
|
-
isDeleted: SqliteBoolean,
|
|
128
|
+
export const Index = S.Struct({
|
|
129
|
+
name: S.String,
|
|
130
|
+
sql: S.String,
|
|
96
131
|
});
|
|
132
|
+
export type Index = S.Schema.Type<typeof Index>;
|
|
97
133
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
? A extends Value
|
|
108
|
-
? true
|
|
109
|
-
: false
|
|
110
|
-
: never
|
|
111
|
-
: never
|
|
112
|
-
: never;
|
|
113
|
-
|
|
114
|
-
type ValidateFieldsNames<Fields extends TableFields> =
|
|
115
|
-
keyof Fields extends infer K
|
|
116
|
-
? K extends keyof Fields
|
|
117
|
-
? K extends "createdAt" | "updatedAt" | "isDeleted"
|
|
118
|
-
? false
|
|
119
|
-
: true
|
|
120
|
-
: never
|
|
121
|
-
: never;
|
|
122
|
-
|
|
123
|
-
type ValidateFieldsHasId<Fields extends TableFields> = "id" extends keyof Fields
|
|
124
|
-
? true
|
|
125
|
-
: false;
|
|
134
|
+
export interface Mutation {
|
|
135
|
+
readonly table: string;
|
|
136
|
+
readonly id: Id;
|
|
137
|
+
readonly values: Record.ReadonlyRecord<
|
|
138
|
+
string,
|
|
139
|
+
Value | Date | boolean | undefined
|
|
140
|
+
>;
|
|
141
|
+
readonly isInsert: boolean;
|
|
142
|
+
}
|
|
126
143
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
* Tables with a name prefixed with _ are local-only, which means they are not
|
|
131
|
-
* synced. Local-only tables are useful for device-specific or temporal data.
|
|
132
|
-
*
|
|
133
|
-
* @example
|
|
134
|
-
* const Database = database({
|
|
135
|
-
* // A local-only table.
|
|
136
|
-
* _todo: TodoTable,
|
|
137
|
-
* todo: TodoTable,
|
|
138
|
-
* todoCategory: TodoCategoryTable,
|
|
139
|
-
* });
|
|
140
|
-
* type Database = S.Schema.Type<typeof Database>;
|
|
141
|
-
*/
|
|
142
|
-
export const database = S.struct;
|
|
144
|
+
export interface NotSupportedPlatformError {
|
|
145
|
+
readonly _tag: "NotSupportedPlatformError";
|
|
146
|
+
}
|
|
143
147
|
|
|
144
148
|
// https://blog.beraliv.dev/2021-05-07-opaque-type-in-typescript
|
|
145
149
|
declare const __queryBrand: unique symbol;
|
|
146
150
|
|
|
147
151
|
/**
|
|
148
|
-
*
|
|
149
|
-
*
|
|
152
|
+
* Query is SQL query serialized as a string with a branded type representing a
|
|
153
|
+
* row it returns.
|
|
150
154
|
*/
|
|
151
155
|
export type Query<R extends Row = Row> = string &
|
|
152
156
|
Brand.Brand<"Query"> & { readonly [__queryBrand]: R };
|
|
153
157
|
|
|
154
|
-
export type Queries<R extends Row = Row> = ReadonlyArray<Query<R>>;
|
|
155
|
-
|
|
156
|
-
interface SerializedSqliteQuery {
|
|
157
|
-
readonly sql: string;
|
|
158
|
-
readonly parameters: (
|
|
159
|
-
| null
|
|
160
|
-
| string
|
|
161
|
-
| number
|
|
162
|
-
| Array<number>
|
|
163
|
-
| { json: JsonObjectOrArray }
|
|
164
|
-
)[];
|
|
165
|
-
readonly options?: SqliteQueryOptions;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// We use queries as keys, hence JSON.stringify.
|
|
169
|
-
export const serializeQuery = <R extends Row>({
|
|
170
|
-
sql,
|
|
171
|
-
parameters = [],
|
|
172
|
-
options,
|
|
173
|
-
}: SqliteQuery): Query<R> => {
|
|
174
|
-
const query: SerializedSqliteQuery = {
|
|
175
|
-
sql,
|
|
176
|
-
parameters: parameters.map((p) =>
|
|
177
|
-
Predicate.isUint8Array(p)
|
|
178
|
-
? Array.from(p)
|
|
179
|
-
: isJsonObjectOrArray(p)
|
|
180
|
-
? { json: p }
|
|
181
|
-
: p,
|
|
182
|
-
),
|
|
183
|
-
...(options && { options }),
|
|
184
|
-
};
|
|
185
|
-
return JSON.stringify(query) as Query<R>;
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
export const deserializeQuery = <R extends Row>(
|
|
189
|
-
query: Query<R>,
|
|
190
|
-
): SqliteQuery => {
|
|
191
|
-
const serializedSqliteQuery = JSON.parse(query) as SerializedSqliteQuery;
|
|
192
|
-
return {
|
|
193
|
-
...serializedSqliteQuery,
|
|
194
|
-
parameters: serializedSqliteQuery.parameters.map((p) =>
|
|
195
|
-
Array.isArray(p)
|
|
196
|
-
? new Uint8Array(p)
|
|
197
|
-
: typeof p === "object" && p != null
|
|
198
|
-
? p.json
|
|
199
|
-
: p,
|
|
200
|
-
),
|
|
201
|
-
};
|
|
202
|
-
};
|
|
203
|
-
|
|
204
158
|
export type Row = {
|
|
205
159
|
[key: string]:
|
|
206
160
|
| Value
|
|
@@ -208,170 +162,239 @@ export type Row = {
|
|
|
208
162
|
| ReadonlyArray<Row>; // for jsonArrayFrom from kysely/helpers/sqlite
|
|
209
163
|
};
|
|
210
164
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
165
|
+
export class DbFactory extends Context.Tag("DbFactory")<
|
|
166
|
+
DbFactory,
|
|
167
|
+
{
|
|
168
|
+
readonly createDb: Effect.Effect<Db>;
|
|
169
|
+
}
|
|
170
|
+
>() {}
|
|
171
|
+
|
|
172
|
+
export const createDb: Effect.Effect<
|
|
173
|
+
Db,
|
|
174
|
+
never,
|
|
175
|
+
SqliteFactory | Bip39 | NanoIdGenerator | Time | SyncFactory | SyncLock
|
|
176
|
+
> = Effect.gen(function* () {
|
|
177
|
+
const { createSqlite } = yield* SqliteFactory;
|
|
178
|
+
const { createSync } = yield* SyncFactory;
|
|
179
|
+
|
|
180
|
+
const initContext = Context.empty().pipe(
|
|
181
|
+
Context.add(Bip39, yield* Bip39),
|
|
182
|
+
Context.add(NanoIdGenerator, yield* NanoIdGenerator),
|
|
183
|
+
Context.add(Time, yield* Time),
|
|
184
|
+
Context.add(SyncLock, yield* SyncLock),
|
|
185
|
+
);
|
|
226
186
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
187
|
+
const afterInitContext =
|
|
188
|
+
yield* Deferred.make<
|
|
189
|
+
Context.Context<
|
|
190
|
+
| Bip39
|
|
191
|
+
| NanoIdGenerator
|
|
192
|
+
| Time
|
|
193
|
+
| SyncLock
|
|
194
|
+
| Sqlite
|
|
195
|
+
| Owner
|
|
196
|
+
| Sync
|
|
197
|
+
| Callbacks
|
|
198
|
+
>
|
|
199
|
+
>();
|
|
200
|
+
|
|
201
|
+
const afterInit =
|
|
202
|
+
(options: { readonly transaction: SqliteTransactionMode }) =>
|
|
203
|
+
<A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
204
|
+
Effect.flatMap(Deferred.await(afterInitContext), (context) =>
|
|
205
|
+
Effect.flatMap(Sqlite, (sqlite) =>
|
|
206
|
+
sqlite.transaction(options.transaction)(effect),
|
|
207
|
+
).pipe(Effect.provide(context)),
|
|
208
|
+
);
|
|
231
209
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
210
|
+
const queryRowsRef = yield* SynchronizedRef.make<QueryRowsMap>(new Map());
|
|
211
|
+
|
|
212
|
+
const db: Db = {
|
|
213
|
+
init: (schema, initialData, onError, onSyncStateChange, onReceive) =>
|
|
214
|
+
Effect.gen(function* () {
|
|
215
|
+
yield* Effect.logDebug(["Db init", { schema }]);
|
|
216
|
+
const sqlite = yield* createSqlite;
|
|
217
|
+
const contextWithSqlite = Context.add(initContext, Sqlite, sqlite);
|
|
218
|
+
const owner = yield* getSchema.pipe(
|
|
219
|
+
Effect.tap(ensureSchema(schema)),
|
|
220
|
+
Effect.flatMap((currentSchema) => {
|
|
221
|
+
if (currentSchema.tables.map((t) => t.name).includes("evolu_owner"))
|
|
222
|
+
return readOwner;
|
|
223
|
+
return createOwner().pipe(Effect.tap(applyMutations(initialData)));
|
|
224
|
+
}),
|
|
225
|
+
sqlite.transaction("exclusive"),
|
|
226
|
+
Effect.provide(contextWithSqlite),
|
|
227
|
+
);
|
|
228
|
+
const sync = yield* createSync.pipe(Effect.provide(initContext));
|
|
229
|
+
yield* sync.init(owner);
|
|
230
|
+
Deferred.unsafeDone(
|
|
231
|
+
afterInitContext,
|
|
232
|
+
Effect.succeed(
|
|
233
|
+
contextWithSqlite.pipe(
|
|
234
|
+
Context.add(Owner, owner),
|
|
235
|
+
Context.add(Sync, sync),
|
|
236
|
+
Context.add(Callbacks, { onError, onSyncStateChange, onReceive }),
|
|
237
|
+
),
|
|
238
|
+
),
|
|
239
|
+
);
|
|
240
|
+
return owner;
|
|
241
|
+
}),
|
|
238
242
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
243
|
+
loadQueries: (queries) =>
|
|
244
|
+
Effect.logDebug(["Db loadQueries", { queries }]).pipe(
|
|
245
|
+
Effect.zipRight(loadQueries(queries, queryRowsRef)),
|
|
246
|
+
afterInit({ transaction: "shared" }),
|
|
247
|
+
),
|
|
242
248
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
};
|
|
249
|
+
mutate: (mutations, queriesToRefresh) =>
|
|
250
|
+
Effect.gen(function* () {
|
|
251
|
+
yield* Effect.logDebug(["Db mutate", { mutations, queriesToRefresh }]);
|
|
252
|
+
const time = yield* Time;
|
|
253
|
+
const sqlite = yield* Sqlite;
|
|
254
|
+
|
|
255
|
+
const [toSyncMutations, localOnlyMutations] = Arr.partition(
|
|
256
|
+
mutations,
|
|
257
|
+
/** Table name starting with '_' is local only (not synced). */
|
|
258
|
+
(mutation) => mutation.table.startsWith("_"),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
for (const mutation of localOnlyMutations) {
|
|
262
|
+
const isDeleteMutation = mutationToNewMessages(mutation).some(
|
|
263
|
+
({ column, value }) => column === "isDeleted" && value === 1,
|
|
264
|
+
);
|
|
265
|
+
if (isDeleteMutation) {
|
|
266
|
+
yield* sqlite.exec({
|
|
267
|
+
sql: `delete from "${mutation.table}" where "id" = ?;`,
|
|
268
|
+
parameters: [mutation.id],
|
|
269
|
+
});
|
|
270
|
+
} else {
|
|
271
|
+
const messages = mutationToNewMessages(mutation);
|
|
272
|
+
for (const message of messages) {
|
|
273
|
+
const now = yield* time.now;
|
|
274
|
+
yield* upsertValueIntoTableRowColumn(message, messages, now);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (toSyncMutations.length > 0) {
|
|
279
|
+
yield* Effect.tap(applyMutations(toSyncMutations), forkSync);
|
|
280
|
+
}
|
|
281
|
+
return yield* loadQueries(queriesToRefresh, queryRowsRef);
|
|
282
|
+
}).pipe(afterInit({ transaction: "exclusive" })),
|
|
283
|
+
|
|
284
|
+
resetOwner: () =>
|
|
285
|
+
Effect.logTrace("Db resetOwner").pipe(
|
|
286
|
+
Effect.tap(dropAllTables),
|
|
287
|
+
afterInit({ transaction: "last" }),
|
|
288
|
+
),
|
|
246
289
|
|
|
247
|
-
|
|
290
|
+
restoreOwner: (schema, mnemonic) =>
|
|
291
|
+
Effect.logTrace("Db restoreOwner").pipe(
|
|
292
|
+
Effect.tap(dropAllTables),
|
|
293
|
+
Effect.tap(Effect.flatMap(getSchema, ensureSchema(schema))),
|
|
294
|
+
Effect.tap(createOwner(mnemonic)),
|
|
295
|
+
afterInit({ transaction: "last" }),
|
|
296
|
+
),
|
|
248
297
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
queryResult = { rows, row: rows[0] };
|
|
255
|
-
queryResultCache.set(rows, queryResult);
|
|
256
|
-
}
|
|
257
|
-
return queryResult as QueryResult<R>;
|
|
258
|
-
};
|
|
298
|
+
ensureSchema: (schema) =>
|
|
299
|
+
getSchema.pipe(
|
|
300
|
+
Effect.flatMap(ensureSchema(schema)),
|
|
301
|
+
afterInit({ transaction: "exclusive" }),
|
|
302
|
+
),
|
|
259
303
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const propertySignatures = AST.getPropertySignatures(schema.ast);
|
|
267
|
-
for (let i = 0; i < propertySignatures.length; i++) {
|
|
268
|
-
const propertySignature = propertySignatures[i];
|
|
269
|
-
out[propertySignature.name] = make(propertySignature.type);
|
|
270
|
-
}
|
|
271
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
272
|
-
return out as any;
|
|
273
|
-
};
|
|
304
|
+
sync: (queriesToRefresh) =>
|
|
305
|
+
Effect.logDebug(["Db sync", { queriesToRefresh }]).pipe(
|
|
306
|
+
Effect.zipRight(forkSync()),
|
|
307
|
+
Effect.zipRight(loadQueries(queriesToRefresh, queryRowsRef)),
|
|
308
|
+
afterInit({ transaction: "shared" }),
|
|
309
|
+
),
|
|
274
310
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
columns: Object.keys(getPropertySignatures(schema)),
|
|
283
|
-
}),
|
|
284
|
-
),
|
|
285
|
-
);
|
|
311
|
+
// TODO:
|
|
312
|
+
// dispose: () =>
|
|
313
|
+
// Effect.logTrace("Db dispose").pipe(
|
|
314
|
+
// Effect.tap(Scope.close(scope, Exit.succeed("Db disposed"))),
|
|
315
|
+
// afterInit({ transaction: "last" }),
|
|
316
|
+
// ),
|
|
317
|
+
};
|
|
286
318
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
): Effect.Effect<A, E, Sqlite | R> =>
|
|
290
|
-
Effect.flatMap(Sqlite, (sqlite) =>
|
|
291
|
-
Effect.acquireUseRelease(
|
|
292
|
-
sqlite.exec({ sql: "begin" }),
|
|
293
|
-
() => effect,
|
|
294
|
-
(_, exit) =>
|
|
295
|
-
Exit.isFailure(exit)
|
|
296
|
-
? sqlite.exec({ sql: "rollback" })
|
|
297
|
-
: sqlite.exec({ sql: "end" }),
|
|
298
|
-
),
|
|
299
|
-
);
|
|
319
|
+
return db;
|
|
320
|
+
});
|
|
300
321
|
|
|
301
|
-
export interface
|
|
302
|
-
readonly
|
|
322
|
+
export interface Callbacks {
|
|
323
|
+
readonly onError: (error: EvoluError) => void;
|
|
324
|
+
readonly onSyncStateChange: (state: SyncState) => void;
|
|
325
|
+
readonly onReceive: () => void;
|
|
303
326
|
}
|
|
304
327
|
|
|
305
|
-
export const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
328
|
+
export const Callbacks = Context.GenericTag<Callbacks>("Callbacks");
|
|
329
|
+
|
|
330
|
+
export type QueryRowsMap = ReadonlyMap<Query, ReadonlyArray<Row>>;
|
|
331
|
+
|
|
332
|
+
const loadQueries = (
|
|
333
|
+
queries: ReadonlyArray<Query>,
|
|
334
|
+
queryRowsRef: SynchronizedRef.SynchronizedRef<QueryRowsMap>,
|
|
335
|
+
) =>
|
|
336
|
+
Effect.gen(function* () {
|
|
337
|
+
const sqlite = yield* Sqlite;
|
|
338
|
+
const previousState = yield* SynchronizedRef.getAndUpdateEffect(
|
|
339
|
+
queryRowsRef,
|
|
340
|
+
(previousState) =>
|
|
341
|
+
Effect.map(
|
|
342
|
+
Effect.forEach(queries, (query) => {
|
|
343
|
+
const sqliteQuery = deserializeQuery(query);
|
|
344
|
+
return sqlite.exec(sqliteQuery).pipe(
|
|
345
|
+
Effect.tap(maybeExplainQueryPlan(sqliteQuery)),
|
|
346
|
+
Effect.map(({ rows }) => [query, rows] as const),
|
|
347
|
+
);
|
|
322
348
|
}),
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
);
|
|
326
|
-
|
|
327
|
-
export const lazyInit = (
|
|
328
|
-
mnemonic?: Mnemonic,
|
|
329
|
-
): Effect.Effect<Owner, never, Sqlite | Bip39 | NanoIdGenerator> =>
|
|
330
|
-
Effect.gen(function* (_) {
|
|
331
|
-
const [owner, sqlite, initialTimestampString] = yield* _(
|
|
332
|
-
Effect.all([makeOwner(mnemonic), Sqlite, makeInitialTimestamp], {
|
|
333
|
-
concurrency: "unbounded",
|
|
334
|
-
}),
|
|
349
|
+
(queriesRows) => new Map([...previousState, ...queriesRows]),
|
|
350
|
+
),
|
|
335
351
|
);
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
owner.id,
|
|
346
|
-
owner.mnemonic,
|
|
347
|
-
owner.encryptionKey,
|
|
348
|
-
timestampToString(initialTimestampString),
|
|
349
|
-
merkleTreeToString(initialMerkleTree),
|
|
350
|
-
],
|
|
351
|
-
}),
|
|
352
|
-
]),
|
|
352
|
+
const currentState = yield* SynchronizedRef.get(queryRowsRef);
|
|
353
|
+
return queries.map(
|
|
354
|
+
(query): QueryPatches => ({
|
|
355
|
+
query,
|
|
356
|
+
patches: makePatches(
|
|
357
|
+
previousState.get(query),
|
|
358
|
+
currentState.get(query) || [],
|
|
359
|
+
),
|
|
360
|
+
}),
|
|
353
361
|
);
|
|
354
|
-
|
|
355
|
-
return owner;
|
|
356
362
|
});
|
|
357
363
|
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
364
|
+
const maybeExplainQueryPlan = (sqliteQuery: SqliteQuery) => {
|
|
365
|
+
if (!sqliteQuery.options?.logExplainQueryPlan) return Effect.void;
|
|
366
|
+
return Sqlite.pipe(
|
|
367
|
+
Effect.flatMap((sqlite) =>
|
|
368
|
+
sqlite.exec({
|
|
369
|
+
...sqliteQuery,
|
|
370
|
+
sql: `EXPLAIN QUERY PLAN ${sqliteQuery.sql}`,
|
|
371
|
+
}),
|
|
372
|
+
),
|
|
373
|
+
Effect.tap(Console.log("ExplainQueryPlan", sqliteQuery)),
|
|
374
|
+
Effect.tap(({ rows }) =>
|
|
375
|
+
Console.log(drawSqliteQueryPlan(rows as SqliteQueryPlanRow[])),
|
|
376
|
+
),
|
|
377
|
+
Effect.map(constVoid),
|
|
378
|
+
);
|
|
379
|
+
};
|
|
361
380
|
|
|
362
|
-
|
|
381
|
+
const getSchema: Effect.Effect<DbSchema, never, Sqlite> = Effect.gen(
|
|
382
|
+
function* (_) {
|
|
383
|
+
yield* Effect.logTrace("Db getSchema");
|
|
384
|
+
const sqlite = yield* Sqlite;
|
|
385
|
+
const tables = yield* Effect.map(
|
|
363
386
|
sqlite.exec({
|
|
364
387
|
// https://til.simonwillison.net/sqlite/list-all-columns-in-a-database
|
|
365
388
|
sql: `
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
389
|
+
select
|
|
390
|
+
sqlite_master.name as tableName,
|
|
391
|
+
table_info.name as columnName
|
|
392
|
+
from
|
|
393
|
+
sqlite_master
|
|
394
|
+
join pragma_table_info(sqlite_master.name) as table_info
|
|
395
|
+
`.trim(),
|
|
373
396
|
}),
|
|
374
|
-
|
|
397
|
+
({ rows }) => {
|
|
375
398
|
const map = new Map<string, string[]>();
|
|
376
399
|
rows.forEach((row) => {
|
|
377
400
|
const { tableName, columnName } = row as {
|
|
@@ -381,27 +404,28 @@ const getSchema: Effect.Effect<SqliteSchema, never, Sqlite> = Effect.gen(
|
|
|
381
404
|
if (!map.has(tableName)) map.set(tableName, []);
|
|
382
405
|
map.get(tableName)?.push(columnName);
|
|
383
406
|
});
|
|
384
|
-
return Array.from(map, ([name, columns]) => ({
|
|
407
|
+
return globalThis.Array.from(map, ([name, columns]) => ({
|
|
385
408
|
name,
|
|
386
409
|
columns,
|
|
387
410
|
}));
|
|
388
|
-
}
|
|
411
|
+
},
|
|
389
412
|
);
|
|
390
413
|
|
|
391
|
-
const indexes = yield*
|
|
414
|
+
const indexes = yield* Effect.map(
|
|
392
415
|
sqlite.exec({
|
|
393
416
|
sql: `
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
417
|
+
select
|
|
418
|
+
name, sql
|
|
419
|
+
from
|
|
420
|
+
sqlite_master
|
|
421
|
+
where
|
|
422
|
+
type='index' and
|
|
423
|
+
name not like 'sqlite_%' and
|
|
424
|
+
name not like 'index_evolu_%'
|
|
425
|
+
`.trim(),
|
|
402
426
|
}),
|
|
403
|
-
|
|
404
|
-
|
|
427
|
+
(result) =>
|
|
428
|
+
Arr.map(
|
|
405
429
|
result.rows,
|
|
406
430
|
(row): Index => ({
|
|
407
431
|
name: row.name as string,
|
|
@@ -413,41 +437,37 @@ const getSchema: Effect.Effect<SqliteSchema, never, Sqlite> = Effect.gen(
|
|
|
413
437
|
sql: (row.sql as string).replace("CREATE INDEX", "create index"),
|
|
414
438
|
}),
|
|
415
439
|
),
|
|
416
|
-
),
|
|
417
440
|
);
|
|
418
441
|
|
|
419
442
|
return { tables, indexes };
|
|
420
443
|
},
|
|
421
444
|
);
|
|
422
445
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
Effect.gen(function* (_) {
|
|
427
|
-
const sqlite = yield* _(Sqlite);
|
|
428
|
-
const currentSchema = yield* _(getSchema);
|
|
429
|
-
|
|
446
|
+
const ensureSchema = (newSchema: DbSchema) => (currentSchema: DbSchema) =>
|
|
447
|
+
Effect.gen(function* () {
|
|
448
|
+
yield* Effect.logTrace("Db ensureSchema");
|
|
430
449
|
const sql: string[] = [];
|
|
431
450
|
|
|
432
|
-
|
|
451
|
+
newSchema.tables.forEach((table) => {
|
|
433
452
|
const currentTable = currentSchema.tables.find(
|
|
434
453
|
(t) => t.name === table.name,
|
|
435
454
|
);
|
|
436
455
|
if (!currentTable) {
|
|
437
|
-
sql.push(
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
456
|
+
sql.push(
|
|
457
|
+
`
|
|
458
|
+
create table ${table.name} (
|
|
459
|
+
"id" text primary key,
|
|
460
|
+
${table.columns
|
|
461
|
+
.filter((c) => c !== "id")
|
|
462
|
+
// "A column with affinity BLOB does not prefer one storage class over another
|
|
463
|
+
// and no attempt is made to coerce data from one storage class into another."
|
|
464
|
+
// https://www.sqlite.org/datatype3.html
|
|
465
|
+
.map((name) => `"${name}" blob`)
|
|
466
|
+
.join(", ")}
|
|
467
|
+
);`.trim(),
|
|
468
|
+
);
|
|
449
469
|
} else {
|
|
450
|
-
|
|
470
|
+
Arr.differenceWith(String.Equivalence)(
|
|
451
471
|
table.columns,
|
|
452
472
|
currentTable.columns,
|
|
453
473
|
).forEach((newColumn) => {
|
|
@@ -459,53 +479,465 @@ export const ensureSchema = (
|
|
|
459
479
|
});
|
|
460
480
|
|
|
461
481
|
// Remove old indexes.
|
|
462
|
-
|
|
482
|
+
Arr.differenceWith(indexEquivalence)(
|
|
463
483
|
currentSchema.indexes,
|
|
464
|
-
|
|
484
|
+
Arr.intersectionWith(indexEquivalence)(
|
|
465
485
|
currentSchema.indexes,
|
|
466
|
-
|
|
486
|
+
newSchema.indexes,
|
|
467
487
|
),
|
|
468
488
|
).forEach((indexToDrop) => {
|
|
469
489
|
sql.push(`drop index "${indexToDrop.name}";`);
|
|
470
490
|
});
|
|
471
491
|
|
|
472
492
|
// Add new indexes.
|
|
473
|
-
|
|
474
|
-
|
|
493
|
+
Arr.differenceWith(indexEquivalence)(
|
|
494
|
+
newSchema.indexes,
|
|
475
495
|
currentSchema.indexes,
|
|
476
496
|
).forEach((newIndex) => {
|
|
477
497
|
sql.push(`${newIndex.sql};`);
|
|
478
498
|
});
|
|
499
|
+
if (sql.length > 0) {
|
|
500
|
+
const sqlite = yield* Sqlite;
|
|
501
|
+
yield* sqlite.exec({ sql: sql.join("\n") });
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
const indexEquivalence: Equivalence<Index> = (self, that) =>
|
|
506
|
+
self.name === that.name && self.sql === that.sql;
|
|
507
|
+
|
|
508
|
+
const readOwner = Effect.logTrace("Db readOwner").pipe(
|
|
509
|
+
Effect.zipRight(Sqlite),
|
|
510
|
+
Effect.flatMap((sqlite) => sqlite.exec(Sql.selectOwner)),
|
|
511
|
+
Effect.map(
|
|
512
|
+
({ rows: [row] }): Owner => ({
|
|
513
|
+
id: row.id as OwnerId,
|
|
514
|
+
mnemonic: row.mnemonic as Mnemonic,
|
|
515
|
+
encryptionKey: row.encryptionKey as Uint8Array,
|
|
516
|
+
}),
|
|
517
|
+
),
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
const createOwner = (mnemonic?: Mnemonic) =>
|
|
521
|
+
Effect.logTrace("Db createOwner").pipe(
|
|
522
|
+
Effect.zipRight(
|
|
523
|
+
Effect.all([makeOwner(mnemonic), Sqlite, makeInitialTimestamp]),
|
|
524
|
+
),
|
|
525
|
+
Effect.tap(([owner, sqlite, initialTimestampString]) =>
|
|
526
|
+
Effect.all([
|
|
527
|
+
sqlite.exec(Sql.createMessageTable),
|
|
528
|
+
sqlite.exec(Sql.createMessageTableIndex),
|
|
529
|
+
sqlite.exec(Sql.createOwnerTable),
|
|
530
|
+
sqlite.exec({
|
|
531
|
+
...Sql.insertOwner,
|
|
532
|
+
parameters: [
|
|
533
|
+
owner.id,
|
|
534
|
+
owner.mnemonic,
|
|
535
|
+
owner.encryptionKey,
|
|
536
|
+
timestampToString(initialTimestampString),
|
|
537
|
+
merkleTreeToString(initialMerkleTree),
|
|
538
|
+
],
|
|
539
|
+
}),
|
|
540
|
+
]),
|
|
541
|
+
),
|
|
542
|
+
Effect.map(([owner]) => owner),
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
const applyMutations = (mutations: ReadonlyArray<Mutation>) =>
|
|
546
|
+
Effect.gen(function* (_) {
|
|
547
|
+
const { timestamp, merkleTree } = yield* readTimestampAndMerkleTree;
|
|
548
|
+
const [nextTimestamp, messages] = yield* Effect.mapAccum(
|
|
549
|
+
mutations.flatMap(mutationToNewMessages),
|
|
550
|
+
timestamp,
|
|
551
|
+
(currentTimestamp, newMessage) =>
|
|
552
|
+
Effect.map(sendTimestamp(currentTimestamp), (nextTimestamp) => {
|
|
553
|
+
const message: Message = {
|
|
554
|
+
...newMessage,
|
|
555
|
+
timestamp: timestampToString(nextTimestamp),
|
|
556
|
+
};
|
|
557
|
+
return [nextTimestamp, message];
|
|
558
|
+
}),
|
|
559
|
+
);
|
|
560
|
+
const nextMerkleTree = yield* applyMessages(merkleTree, messages);
|
|
561
|
+
yield* writeTimestampAndMerkleTree(nextTimestamp, nextMerkleTree);
|
|
562
|
+
return messages;
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
const readTimestampAndMerkleTree = Sqlite.pipe(
|
|
566
|
+
Effect.flatMap((sqlite) =>
|
|
567
|
+
sqlite.exec(Sql.selectOwnerTimestampAndMerkleTree),
|
|
568
|
+
),
|
|
569
|
+
Effect.map(({ rows: [{ timestamp, merkleTree }] }) => ({
|
|
570
|
+
timestamp: unsafeTimestampFromString(timestamp as TimestampString),
|
|
571
|
+
merkleTree: merkleTree as MerkleTree,
|
|
572
|
+
})),
|
|
573
|
+
);
|
|
479
574
|
|
|
480
|
-
|
|
481
|
-
|
|
575
|
+
const mutationToNewMessages = (mutation: Mutation) =>
|
|
576
|
+
pipe(
|
|
577
|
+
Object.entries(mutation.values),
|
|
578
|
+
Arr.filterMap(([column, value]) =>
|
|
579
|
+
// The value can be undefined if exactOptionalPropertyTypes isn't true.
|
|
580
|
+
// Don't insert nulls because null is the default value.
|
|
581
|
+
value === undefined || (mutation.isInsert && value == null)
|
|
582
|
+
? Option.none()
|
|
583
|
+
: Option.some([column, value] as const),
|
|
584
|
+
),
|
|
585
|
+
Arr.map(
|
|
586
|
+
([column, value]): NewMessage => ({
|
|
587
|
+
table: mutation.table,
|
|
588
|
+
row: mutation.id,
|
|
589
|
+
column,
|
|
590
|
+
value:
|
|
591
|
+
typeof value === "boolean"
|
|
592
|
+
? cast(value)
|
|
593
|
+
: value instanceof Date
|
|
594
|
+
? cast(value)
|
|
595
|
+
: value,
|
|
596
|
+
}),
|
|
597
|
+
),
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
const applyMessages = (
|
|
601
|
+
merkleTree: MerkleTree,
|
|
602
|
+
messages: ReadonlyArray<Message>,
|
|
603
|
+
): Effect.Effect<MerkleTree, never, Sqlite> =>
|
|
604
|
+
Effect.gen(function* () {
|
|
605
|
+
const sqlite = yield* Sqlite;
|
|
606
|
+
for (const message of messages) {
|
|
607
|
+
const messageTimestamp = unsafeTimestampFromString(message.timestamp);
|
|
608
|
+
const lastTimestamp = yield* Effect.map(
|
|
482
609
|
sqlite.exec({
|
|
483
|
-
|
|
610
|
+
...Sql.selectLastTimestampForTableRowColumn,
|
|
611
|
+
parameters: [message.table, message.row, message.column, 1],
|
|
484
612
|
}),
|
|
613
|
+
({ rows }) =>
|
|
614
|
+
rows.length > 0 ? (rows[0].timestamp as TimestampString) : null,
|
|
485
615
|
);
|
|
616
|
+
if (lastTimestamp == null || lastTimestamp < message.timestamp) {
|
|
617
|
+
yield* upsertValueIntoTableRowColumn(
|
|
618
|
+
message,
|
|
619
|
+
messages,
|
|
620
|
+
messageTimestamp.millis,
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
if (lastTimestamp == null || lastTimestamp !== message.timestamp) {
|
|
624
|
+
const { changes } = yield* sqlite.exec({
|
|
625
|
+
...Sql.insertIntoMessagesIfNew,
|
|
626
|
+
parameters: [
|
|
627
|
+
message.timestamp,
|
|
628
|
+
message.table,
|
|
629
|
+
message.row,
|
|
630
|
+
message.column,
|
|
631
|
+
message.value,
|
|
632
|
+
],
|
|
633
|
+
});
|
|
634
|
+
if (changes === 1)
|
|
635
|
+
merkleTree = insertIntoMerkleTree(merkleTree, messageTimestamp);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return merkleTree;
|
|
486
639
|
});
|
|
487
640
|
|
|
488
|
-
export const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
)
|
|
641
|
+
export const upsertValueIntoTableRowColumn = (
|
|
642
|
+
message: NewMessage,
|
|
643
|
+
messages: ReadonlyArray<NewMessage>,
|
|
644
|
+
millis: Millis,
|
|
645
|
+
): Effect.Effect<SqliteExecResult, never, Sqlite> =>
|
|
646
|
+
Sqlite.pipe(
|
|
647
|
+
Effect.map((sqlite) => {
|
|
648
|
+
const now = cast(new Date(millis));
|
|
649
|
+
return sqlite.exec({
|
|
650
|
+
sql: `
|
|
651
|
+
insert into
|
|
652
|
+
"${message.table}" ("id", "${message.column}", "createdAt", "updatedAt")
|
|
653
|
+
values
|
|
654
|
+
(?, ?, ?, ?)
|
|
655
|
+
on conflict do update set
|
|
656
|
+
"${message.column}" = ?,
|
|
657
|
+
"updatedAt" = ?
|
|
658
|
+
`.trim(),
|
|
659
|
+
parameters: [message.row, message.value, now, now, message.value, now],
|
|
660
|
+
});
|
|
661
|
+
}),
|
|
662
|
+
Effect.flatMap((insert) =>
|
|
663
|
+
Effect.catchSomeDefect(insert, (error) =>
|
|
664
|
+
S.is(SqliteNoSuchTableOrColumnError)(error)
|
|
665
|
+
? Option.some(
|
|
666
|
+
// If one message fails, we ensure schema for all messages.
|
|
667
|
+
ensureSchemaByNewMessages(messages).pipe(Effect.zipRight(insert)),
|
|
668
|
+
)
|
|
669
|
+
: Option.none(),
|
|
670
|
+
),
|
|
671
|
+
),
|
|
672
|
+
);
|
|
502
673
|
|
|
503
|
-
|
|
504
|
-
|
|
674
|
+
const SqliteNoSuchTableOrColumnError = S.Struct({
|
|
675
|
+
message: S.Union(
|
|
676
|
+
S.String.pipe(S.includes("no such table")),
|
|
677
|
+
S.String.pipe(S.includes("no such column")),
|
|
678
|
+
S.String.pipe(S.includes("has no column")),
|
|
679
|
+
),
|
|
680
|
+
});
|
|
505
681
|
|
|
506
|
-
|
|
682
|
+
const ensureSchemaByNewMessages = (messages: ReadonlyArray<NewMessage>) =>
|
|
683
|
+
Effect.gen(function* () {
|
|
684
|
+
const tablesMap = new Map<string, Table>();
|
|
685
|
+
messages.forEach((message) => {
|
|
686
|
+
const table = tablesMap.get(message.table);
|
|
687
|
+
if (table == null) {
|
|
688
|
+
tablesMap.set(message.table, {
|
|
689
|
+
name: message.table,
|
|
690
|
+
columns: [message.column, "createdAt", "updatedAt"],
|
|
691
|
+
});
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (table.columns.includes(message.column)) return;
|
|
695
|
+
tablesMap.set(message.table, {
|
|
696
|
+
name: message.table,
|
|
697
|
+
columns: table.columns.concat(message.column),
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
const tables = Arr.fromIterable(tablesMap.values());
|
|
701
|
+
yield* Effect.flatMap(getSchema, ensureSchema({ tables, indexes: [] }));
|
|
702
|
+
});
|
|
507
703
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
)
|
|
704
|
+
const writeTimestampAndMerkleTree = (
|
|
705
|
+
timestamp: Timestamp,
|
|
706
|
+
merkleTree: MerkleTree,
|
|
707
|
+
) =>
|
|
708
|
+
Effect.flatMap(Sqlite, (sqlite) =>
|
|
709
|
+
sqlite.exec({
|
|
710
|
+
...Sql.updateOwnerTimestampAndMerkleTree,
|
|
711
|
+
parameters: [
|
|
712
|
+
merkleTreeToString(merkleTree),
|
|
713
|
+
timestampToString(timestamp),
|
|
714
|
+
],
|
|
715
|
+
}),
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
const dropAllTables = Effect.gen(function* () {
|
|
719
|
+
yield* Effect.logTrace("Db dropAllTables");
|
|
720
|
+
const sqlite = yield* Sqlite;
|
|
721
|
+
const schema = yield* getSchema;
|
|
722
|
+
yield* Effect.forEach(schema.tables, (table) =>
|
|
723
|
+
// The dropped table is completely removed from the database schema and
|
|
724
|
+
// the disk file. The table can not be recovered.
|
|
725
|
+
// All indices and triggers associated with the table are also deleted.
|
|
726
|
+
// https://sqlite.org/lang_droptable.html
|
|
727
|
+
sqlite.exec({ sql: `drop table "${table.name}"` }),
|
|
728
|
+
);
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
const forkSync = (messages: ReadonlyArray<Message> = []) =>
|
|
732
|
+
SyncLock.pipe(
|
|
733
|
+
Effect.flatMap((syncLock) => syncLock.tryAcquire),
|
|
734
|
+
Effect.matchEffect({
|
|
735
|
+
onFailure: () => Effect.void,
|
|
736
|
+
onSuccess: () => syncLoop(messages),
|
|
737
|
+
}),
|
|
738
|
+
Effect.scoped,
|
|
739
|
+
Effect.forkDaemon,
|
|
740
|
+
);
|
|
741
|
+
|
|
742
|
+
const syncLoop = (
|
|
743
|
+
messages: ReadonlyArray<Message> = [],
|
|
744
|
+
): Effect.Effect<void, never, Sync | Callbacks | Time | Sqlite | Config> =>
|
|
745
|
+
Effect.gen(function* (_) {
|
|
746
|
+
const sqlite = yield* Sqlite;
|
|
747
|
+
const sync = yield* Sync;
|
|
748
|
+
const callbacks = yield* Callbacks;
|
|
749
|
+
const time = yield* Time;
|
|
750
|
+
|
|
751
|
+
const syncDataRef = yield* readTimestampAndMerkleTree.pipe(
|
|
752
|
+
sqlite.transaction("shared"),
|
|
753
|
+
Effect.map((a): SyncData => ({ ...a, messages })),
|
|
754
|
+
Effect.flatMap(Ref.make),
|
|
755
|
+
);
|
|
756
|
+
|
|
757
|
+
callbacks.onSyncStateChange({ _tag: "SyncStateIsSyncing" });
|
|
758
|
+
|
|
759
|
+
yield* Ref.get(syncDataRef).pipe(
|
|
760
|
+
Effect.flatMap(sync.sync),
|
|
761
|
+
Effect.flatMap(handleSyncResult),
|
|
762
|
+
Effect.repeat({
|
|
763
|
+
// TODO: Schedule.jittered(Schedule.exponential("10 millis"))
|
|
764
|
+
schedule: Schedule.recurs(50),
|
|
765
|
+
until: Option.match({
|
|
766
|
+
onNone: () =>
|
|
767
|
+
Effect.gen(function* () {
|
|
768
|
+
callbacks.onSyncStateChange({
|
|
769
|
+
_tag: "SyncStateIsSynced",
|
|
770
|
+
time: yield* time.now,
|
|
771
|
+
});
|
|
772
|
+
return true;
|
|
773
|
+
}),
|
|
774
|
+
onSome: (syncData) =>
|
|
775
|
+
Effect.gen(function* () {
|
|
776
|
+
yield* Ref.set(syncDataRef, syncData);
|
|
777
|
+
return false;
|
|
778
|
+
}),
|
|
779
|
+
}),
|
|
780
|
+
}),
|
|
781
|
+
Effect.catchAllDefect((error) =>
|
|
782
|
+
// Db can run in a Web Worker, so we must ensure transferable error.
|
|
783
|
+
Effect.fail(makeUnexpectedError(ensureTransferableError(error))),
|
|
784
|
+
),
|
|
785
|
+
Effect.catchTag("SyncStateIsNotSynced", (state) =>
|
|
786
|
+
Effect.succeed(callbacks.onSyncStateChange(state)),
|
|
787
|
+
),
|
|
788
|
+
Effect.catchAll((error) => Effect.succeed(callbacks.onError(error))),
|
|
789
|
+
);
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
const handleSyncResult = (result: SyncResult) =>
|
|
793
|
+
Effect.flatMap(Sqlite, (sqlite) =>
|
|
794
|
+
Effect.gen(function* (_) {
|
|
795
|
+
const { onReceive } = yield* Callbacks;
|
|
796
|
+
const current = yield* readTimestampAndMerkleTree;
|
|
797
|
+
const nextTimestamp = yield* Effect.reduce(
|
|
798
|
+
result.messages,
|
|
799
|
+
current.timestamp,
|
|
800
|
+
(local, message) =>
|
|
801
|
+
receiveTimestamp({
|
|
802
|
+
local,
|
|
803
|
+
remote: unsafeTimestampFromString(message.timestamp),
|
|
804
|
+
}),
|
|
805
|
+
);
|
|
806
|
+
const nextMerkleTree = yield* applyMessages(
|
|
807
|
+
current.merkleTree,
|
|
808
|
+
result.messages,
|
|
809
|
+
);
|
|
810
|
+
if (result.messages.length > 0) {
|
|
811
|
+
yield* writeTimestampAndMerkleTree(nextTimestamp, nextMerkleTree);
|
|
812
|
+
onReceive();
|
|
813
|
+
}
|
|
814
|
+
return yield* diffMerkleTrees(result.merkleTree, nextMerkleTree).pipe(
|
|
815
|
+
Effect.flatMap((diff) =>
|
|
816
|
+
sqlite.exec({
|
|
817
|
+
...Sql.selectMessagesToSync,
|
|
818
|
+
parameters: [timestampToString(makeSyncTimestamp(diff))],
|
|
819
|
+
}),
|
|
820
|
+
),
|
|
821
|
+
Effect.map(
|
|
822
|
+
({ rows }): SyncData => ({
|
|
823
|
+
messages: rows as unknown as ReadonlyArray<Message>,
|
|
824
|
+
merkleTree: nextMerkleTree,
|
|
825
|
+
timestamp: nextTimestamp,
|
|
826
|
+
}),
|
|
827
|
+
),
|
|
828
|
+
Effect.option,
|
|
829
|
+
);
|
|
830
|
+
}).pipe(sqlite.transaction("exclusive")),
|
|
831
|
+
);
|
|
832
|
+
|
|
833
|
+
interface SerializedSqliteQuery {
|
|
834
|
+
readonly sql: string;
|
|
835
|
+
readonly parameters: (
|
|
836
|
+
| null
|
|
837
|
+
| string
|
|
838
|
+
| number
|
|
839
|
+
| Array<number>
|
|
840
|
+
| { json: JsonObjectOrArray }
|
|
841
|
+
)[];
|
|
842
|
+
readonly options?: SqliteQueryOptions;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// We use queries as keys, hence JSON.stringify.
|
|
846
|
+
export const serializeQuery = <R extends Row>({
|
|
847
|
+
sql,
|
|
848
|
+
parameters = [],
|
|
849
|
+
options,
|
|
850
|
+
}: SqliteQuery): Query<R> => {
|
|
851
|
+
const query: SerializedSqliteQuery = {
|
|
852
|
+
sql,
|
|
853
|
+
parameters: parameters.map((p) =>
|
|
854
|
+
Predicate.isUint8Array(p)
|
|
855
|
+
? Arr.fromIterable(p)
|
|
856
|
+
: isJsonObjectOrArray(p)
|
|
857
|
+
? { json: p }
|
|
858
|
+
: p,
|
|
859
|
+
),
|
|
860
|
+
...(options && { options }),
|
|
861
|
+
};
|
|
862
|
+
return JSON.stringify(query) as Query<R>;
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
export const deserializeQuery = <R extends Row>(
|
|
866
|
+
query: Query<R>,
|
|
867
|
+
): SqliteQuery => {
|
|
868
|
+
const serializedSqliteQuery = JSON.parse(query) as SerializedSqliteQuery;
|
|
869
|
+
return {
|
|
870
|
+
...serializedSqliteQuery,
|
|
871
|
+
parameters: serializedSqliteQuery.parameters.map((p) =>
|
|
872
|
+
Arr.isArray(p)
|
|
873
|
+
? new Uint8Array(p)
|
|
874
|
+
: typeof p === "object" && p != null
|
|
875
|
+
? p.json
|
|
876
|
+
: p,
|
|
877
|
+
),
|
|
878
|
+
};
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Extract {@link Row} from {@link Query} instance.
|
|
883
|
+
*
|
|
884
|
+
* @example
|
|
885
|
+
* const allTodos = evolu.createQuery((db) =>
|
|
886
|
+
* db.selectFrom("todo").selectAll(),
|
|
887
|
+
* );
|
|
888
|
+
* type AllTodosRow = ExtractRow<typeof allTodos>;
|
|
889
|
+
*/
|
|
890
|
+
export type ExtractRow<T extends Query> = T extends Query<infer R> ? R : never;
|
|
891
|
+
|
|
892
|
+
// To preserve identity.
|
|
893
|
+
const _emptyRows: ReadonlyArray<Row> = [];
|
|
894
|
+
export const emptyRows = <R extends Row>(): ReadonlyArray<R> =>
|
|
895
|
+
_emptyRows as ReadonlyArray<R>;
|
|
896
|
+
|
|
897
|
+
/** An object with rows and row properties. */
|
|
898
|
+
export interface QueryResult<R extends Row = Row> {
|
|
899
|
+
/** An array containing all the rows returned by the query. */
|
|
900
|
+
readonly rows: ReadonlyArray<Readonly<Kysely.Simplify<R>>>;
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* The first row returned by the query, or null if no rows were returned. This
|
|
904
|
+
* property is useful for queries that are expected to return a single row.
|
|
905
|
+
*/
|
|
906
|
+
readonly row: Readonly<Kysely.Simplify<R>> | null;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
export type Queries<R extends Row = Row> = ReadonlyArray<Query<R>>;
|
|
910
|
+
|
|
911
|
+
export type QueryResultsFromQueries<Q extends Queries> = {
|
|
912
|
+
[P in keyof Q]: Q[P] extends Query<infer R> ? QueryResult<R> : never;
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
export type QueryResultsPromisesFromQueries<Q extends Queries> = {
|
|
916
|
+
[P in keyof Q]: Q[P] extends Query<infer R> ? Promise<QueryResult<R>> : never;
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
// To preserve identity.
|
|
920
|
+
const queryResultCache = new WeakMap<ReadonlyArray<Row>, QueryResult<Row>>();
|
|
921
|
+
export const queryResultFromRows = <R extends Row>(
|
|
922
|
+
rows: ReadonlyArray<R>,
|
|
923
|
+
): QueryResult<R> => {
|
|
924
|
+
let queryResult = queryResultCache.get(rows);
|
|
925
|
+
if (queryResult == null) {
|
|
926
|
+
queryResult = { rows, row: rows[0] };
|
|
927
|
+
queryResultCache.set(rows, queryResult);
|
|
928
|
+
}
|
|
929
|
+
return queryResult as QueryResult<R>;
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
export const notSupportedPlatformWorker: Db = {
|
|
933
|
+
init: () =>
|
|
934
|
+
Effect.fail<NotSupportedPlatformError>({
|
|
935
|
+
_tag: "NotSupportedPlatformError",
|
|
936
|
+
}),
|
|
937
|
+
loadQueries: () => Effect.succeed([]),
|
|
938
|
+
mutate: () => Effect.succeed([]),
|
|
939
|
+
resetOwner: () => Effect.void,
|
|
940
|
+
restoreOwner: () => Effect.void,
|
|
941
|
+
ensureSchema: () => Effect.void,
|
|
942
|
+
sync: () => Effect.succeed([]),
|
|
943
|
+
};
|