@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.
- package/README.md +1 -1
- package/dist/src/Config.d.ts +27 -30
- package/dist/src/Config.d.ts.map +1 -1
- package/dist/src/Config.js +32 -5
- 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 +368 -170
- 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 -16
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +28 -20
- 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 +77 -36
- package/src/Crdt.ts +61 -76
- package/src/Crypto.ts +83 -60
- package/src/Db.ts +782 -352
- 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 -44
- 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,158 +162,227 @@ 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
|
-
|
|
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
|
+
);
|
|
221
186
|
|
|
222
|
-
const
|
|
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
|
+
);
|
|
223
209
|
|
|
224
|
-
|
|
225
|
-
|
|
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
|
+
}),
|
|
226
242
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
243
|
+
loadQueries: (queries) =>
|
|
244
|
+
Effect.logDebug(["Db loadQueries", { queries }]).pipe(
|
|
245
|
+
Effect.zipRight(loadQueries(queries, queryRowsRef)),
|
|
246
|
+
afterInit({ transaction: "shared" }),
|
|
247
|
+
),
|
|
231
248
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
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;
|
|
238
254
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
255
|
+
const [toSyncMutations, localOnlyMutations] = Arr.partition(
|
|
256
|
+
mutations,
|
|
257
|
+
/** Table name starting with '_' is local only (not synced). */
|
|
258
|
+
(mutation) => mutation.table.startsWith("_"),
|
|
259
|
+
);
|
|
242
260
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
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: `
|
|
@@ -369,9 +392,9 @@ select
|
|
|
369
392
|
from
|
|
370
393
|
sqlite_master
|
|
371
394
|
join pragma_table_info(sqlite_master.name) as table_info
|
|
372
|
-
`.trim(),
|
|
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,14 +404,14 @@ from
|
|
|
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
417
|
select
|
|
@@ -401,8 +424,8 @@ where
|
|
|
401
424
|
name not like 'index_evolu_%'
|
|
402
425
|
`.trim(),
|
|
403
426
|
}),
|
|
404
|
-
|
|
405
|
-
|
|
427
|
+
(result) =>
|
|
428
|
+
Arr.map(
|
|
406
429
|
result.rows,
|
|
407
430
|
(row): Index => ({
|
|
408
431
|
name: row.name as string,
|
|
@@ -414,42 +437,37 @@ where
|
|
|
414
437
|
sql: (row.sql as string).replace("CREATE INDEX", "create index"),
|
|
415
438
|
}),
|
|
416
439
|
),
|
|
417
|
-
),
|
|
418
440
|
);
|
|
419
441
|
|
|
420
442
|
return { tables, indexes };
|
|
421
443
|
},
|
|
422
444
|
);
|
|
423
445
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
Effect.gen(function* (_) {
|
|
428
|
-
const sqlite = yield* _(Sqlite);
|
|
429
|
-
const currentSchema = yield* _(getSchema);
|
|
430
|
-
|
|
446
|
+
const ensureSchema = (newSchema: DbSchema) => (currentSchema: DbSchema) =>
|
|
447
|
+
Effect.gen(function* () {
|
|
448
|
+
yield* Effect.logTrace("Db ensureSchema");
|
|
431
449
|
const sql: string[] = [];
|
|
432
450
|
|
|
433
|
-
|
|
451
|
+
newSchema.tables.forEach((table) => {
|
|
434
452
|
const currentTable = currentSchema.tables.find(
|
|
435
453
|
(t) => t.name === table.name,
|
|
436
454
|
);
|
|
437
455
|
if (!currentTable) {
|
|
438
456
|
sql.push(
|
|
439
457
|
`
|
|
440
|
-
create table ${table.name} (
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
);`.trim(),
|
|
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(),
|
|
450
468
|
);
|
|
451
469
|
} else {
|
|
452
|
-
|
|
470
|
+
Arr.differenceWith(String.Equivalence)(
|
|
453
471
|
table.columns,
|
|
454
472
|
currentTable.columns,
|
|
455
473
|
).forEach((newColumn) => {
|
|
@@ -461,53 +479,465 @@ create table ${table.name} (
|
|
|
461
479
|
});
|
|
462
480
|
|
|
463
481
|
// Remove old indexes.
|
|
464
|
-
|
|
482
|
+
Arr.differenceWith(indexEquivalence)(
|
|
465
483
|
currentSchema.indexes,
|
|
466
|
-
|
|
484
|
+
Arr.intersectionWith(indexEquivalence)(
|
|
467
485
|
currentSchema.indexes,
|
|
468
|
-
|
|
486
|
+
newSchema.indexes,
|
|
469
487
|
),
|
|
470
488
|
).forEach((indexToDrop) => {
|
|
471
489
|
sql.push(`drop index "${indexToDrop.name}";`);
|
|
472
490
|
});
|
|
473
491
|
|
|
474
492
|
// Add new indexes.
|
|
475
|
-
|
|
476
|
-
|
|
493
|
+
Arr.differenceWith(indexEquivalence)(
|
|
494
|
+
newSchema.indexes,
|
|
477
495
|
currentSchema.indexes,
|
|
478
496
|
).forEach((newIndex) => {
|
|
479
497
|
sql.push(`${newIndex.sql};`);
|
|
480
498
|
});
|
|
499
|
+
if (sql.length > 0) {
|
|
500
|
+
const sqlite = yield* Sqlite;
|
|
501
|
+
yield* sqlite.exec({ sql: sql.join("\n") });
|
|
502
|
+
}
|
|
503
|
+
});
|
|
481
504
|
|
|
482
|
-
|
|
483
|
-
|
|
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),
|
|
484
530
|
sqlite.exec({
|
|
485
|
-
|
|
531
|
+
...Sql.insertOwner,
|
|
532
|
+
parameters: [
|
|
533
|
+
owner.id,
|
|
534
|
+
owner.mnemonic,
|
|
535
|
+
owner.encryptionKey,
|
|
536
|
+
timestampToString(initialTimestampString),
|
|
537
|
+
merkleTreeToString(initialMerkleTree),
|
|
538
|
+
],
|
|
486
539
|
}),
|
|
487
|
-
)
|
|
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;
|
|
488
563
|
});
|
|
489
564
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
// https://sqlite.org/lang_droptable.html
|
|
499
|
-
.map((table) => `drop table "${table.name}";`)
|
|
500
|
-
.join("");
|
|
501
|
-
yield* _(sqlite.exec({ sql }));
|
|
502
|
-
},
|
|
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
|
+
})),
|
|
503
573
|
);
|
|
504
574
|
|
|
505
|
-
|
|
506
|
-
|
|
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(
|
|
609
|
+
sqlite.exec({
|
|
610
|
+
...Sql.selectLastTimestampForTableRowColumn,
|
|
611
|
+
parameters: [message.table, message.row, message.column, 1],
|
|
612
|
+
}),
|
|
613
|
+
({ rows }) =>
|
|
614
|
+
rows.length > 0 ? (rows[0].timestamp as TimestampString) : null,
|
|
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;
|
|
639
|
+
});
|
|
640
|
+
|
|
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
|
+
);
|
|
507
673
|
|
|
508
|
-
|
|
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
|
+
});
|
|
509
681
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
)
|
|
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
|
+
});
|
|
703
|
+
|
|
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
|
+
};
|