@evolu/common 1.0.17 → 2.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 +7 -7
- package/dist/src/Config.d.ts +4 -6
- package/dist/src/Config.d.ts.map +1 -1
- package/dist/src/Config.js +1 -1
- package/dist/src/Crdt.d.ts +15 -9
- package/dist/src/Crdt.d.ts.map +1 -1
- package/dist/src/Crdt.js +15 -14
- package/dist/src/Crypto.d.ts +7 -13
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +7 -9
- package/dist/src/Db.d.ts +36 -70
- package/dist/src/Db.d.ts.map +1 -1
- package/dist/src/Db.js +45 -51
- package/dist/src/DbWorker.d.ts +13 -14
- package/dist/src/DbWorker.d.ts.map +1 -1
- package/dist/src/DbWorker.js +56 -57
- package/dist/src/Diff.d.ts +3 -1
- package/dist/src/Diff.d.ts.map +1 -1
- package/dist/src/Diff.js +3 -7
- package/dist/src/{Errors.d.ts → ErrorStore.d.ts} +14 -7
- package/dist/src/ErrorStore.d.ts.map +1 -0
- package/dist/src/{Errors.js → ErrorStore.js} +2 -0
- package/dist/src/Evolu.d.ts +316 -58
- package/dist/src/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu.js +199 -185
- package/dist/src/Model.d.ts +47 -57
- package/dist/src/Model.d.ts.map +1 -1
- package/dist/src/Model.js +30 -35
- package/dist/src/OnCompletes.d.ts +11 -0
- package/dist/src/OnCompletes.d.ts.map +1 -0
- package/dist/src/OnCompletes.js +21 -0
- package/dist/src/Owner.d.ts +31 -0
- package/dist/src/Owner.d.ts.map +1 -0
- package/dist/src/Owner.js +20 -0
- package/dist/src/Platform.d.ts +16 -12
- package/dist/src/Platform.d.ts.map +1 -1
- package/dist/src/Platform.js +18 -12
- package/dist/src/Protobuf.d.ts +25 -73
- package/dist/src/Protobuf.d.ts.map +1 -1
- package/dist/src/Protobuf.js +4 -12
- package/dist/src/Public.d.ts +5 -3
- package/dist/src/Public.d.ts.map +1 -1
- package/dist/src/Public.js +1 -0
- package/dist/src/Sqlite.d.ts +8 -11
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +7 -7
- package/dist/src/Store.d.ts +6 -5
- package/dist/src/Store.d.ts.map +1 -1
- package/dist/src/Store.js +21 -16
- package/dist/src/SyncWorker.d.ts +8 -2
- package/dist/src/SyncWorker.d.ts.map +1 -1
- package/dist/src/SyncWorker.js +12 -19
- package/dist/src/index.d.ts +6 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +6 -2
- package/package.json +23 -13
- package/src/Config.ts +7 -7
- package/src/Crdt.ts +29 -26
- package/src/Crypto.ts +14 -19
- package/src/Db.ts +113 -166
- package/src/DbWorker.ts +110 -140
- package/src/Diff.ts +5 -7
- package/src/{Errors.ts → ErrorStore.ts} +17 -8
- package/src/Evolu.ts +682 -442
- package/src/Model.ts +60 -85
- package/src/OnCompletes.ts +46 -0
- package/src/Owner.ts +65 -0
- package/src/Platform.ts +38 -25
- package/src/Protobuf.ts +25 -73
- package/src/Public.ts +5 -3
- package/src/Sqlite.ts +13 -24
- package/src/Store.ts +36 -24
- package/src/SyncWorker.ts +30 -38
- package/src/index.ts +6 -2
- package/dist/src/Errors.d.ts.map +0 -1
package/src/Evolu.ts
CHANGED
|
@@ -1,527 +1,767 @@
|
|
|
1
|
+
import * as S from "@effect/schema/Schema";
|
|
1
2
|
import {
|
|
2
3
|
Context,
|
|
3
4
|
Effect,
|
|
4
|
-
Either,
|
|
5
5
|
Function,
|
|
6
6
|
Layer,
|
|
7
|
+
Match,
|
|
7
8
|
Number,
|
|
8
|
-
Option,
|
|
9
9
|
ReadonlyArray,
|
|
10
|
-
absurd,
|
|
11
10
|
pipe,
|
|
12
11
|
} from "effect";
|
|
13
|
-
import
|
|
14
|
-
import { Config
|
|
12
|
+
import * as Kysely from "kysely";
|
|
13
|
+
import { Config } from "./Config.js";
|
|
15
14
|
import { Time, TimeLive } from "./Crdt.js";
|
|
16
|
-
import {
|
|
15
|
+
import { Mnemonic, NanoId, NanoIdLive } from "./Crypto.js";
|
|
17
16
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
Queries,
|
|
18
|
+
Query,
|
|
19
|
+
QueryResult,
|
|
20
|
+
QueryResultsPromisesFromQueries,
|
|
21
|
+
Row,
|
|
22
|
+
RowsStore,
|
|
23
|
+
RowsStoreLive,
|
|
22
24
|
Schema,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
emptyRows,
|
|
26
|
+
queryResultFromRows,
|
|
27
|
+
schemaToTables,
|
|
28
|
+
serializeQuery,
|
|
25
29
|
} from "./Db.js";
|
|
26
|
-
import {
|
|
27
|
-
DbWorker,
|
|
28
|
-
DbWorkerOutputOnQuery,
|
|
29
|
-
MutateItem,
|
|
30
|
-
OnCompleteId,
|
|
31
|
-
RowsCacheMap,
|
|
32
|
-
} from "./DbWorker.js";
|
|
30
|
+
import { DbWorker, DbWorkerOutputOnQuery, Mutation } from "./DbWorker.js";
|
|
33
31
|
import { applyPatches } from "./Diff.js";
|
|
34
|
-
import { EvoluError } from "./
|
|
35
|
-
import {
|
|
32
|
+
import { EvoluError, makeErrorStore } from "./ErrorStore.js";
|
|
33
|
+
import { SqliteBoolean, SqliteDate, cast } from "./Model.js";
|
|
34
|
+
import { OnCompletes, OnCompletesLive } from "./OnCompletes.js";
|
|
35
|
+
import { Owner } from "./Owner.js";
|
|
36
36
|
import { AppState, FlushSync } from "./Platform.js";
|
|
37
|
-
import {
|
|
38
|
-
import { Store,
|
|
37
|
+
import { SqliteQuery } from "./Sqlite.js";
|
|
38
|
+
import { Store, Unsubscribe, makeStore } from "./Store.js";
|
|
39
39
|
import { SyncState } from "./SyncWorker.js";
|
|
40
40
|
|
|
41
|
-
export interface Evolu<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
readonly
|
|
55
|
-
|
|
56
|
-
readonly create: Create<S>;
|
|
57
|
-
readonly update: Update<S>;
|
|
58
|
-
|
|
59
|
-
readonly ownerActions: OwnerActions;
|
|
60
|
-
readonly ensureSchema: (tables: Tables) => void;
|
|
61
|
-
}
|
|
41
|
+
export interface Evolu<T extends Schema = Schema> {
|
|
42
|
+
/**
|
|
43
|
+
* Subscribe to {@link EvoluError} changes.
|
|
44
|
+
*
|
|
45
|
+
* ### Example
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* const unsubscribe = evolu.subscribeError(() => {
|
|
49
|
+
* const error = evolu.getError();
|
|
50
|
+
* console.log(error);
|
|
51
|
+
* });
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
readonly subscribeError: Store<EvoluError | null>["subscribe"];
|
|
62
55
|
|
|
63
|
-
|
|
64
|
-
|
|
56
|
+
/** Get {@link EvoluError}. */
|
|
57
|
+
readonly getError: Store<EvoluError | null>["getState"];
|
|
65
58
|
|
|
66
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Create type-safe SQL {@link Query}.
|
|
61
|
+
*
|
|
62
|
+
* Evolu uses Kysely - the type-safe SQL query builder for TypeScript. See
|
|
63
|
+
* https://kysely.dev.
|
|
64
|
+
*
|
|
65
|
+
* For mutations, use {@link create} and {@link update}.
|
|
66
|
+
*
|
|
67
|
+
* ### Example
|
|
68
|
+
*
|
|
69
|
+
* ```ts
|
|
70
|
+
* const allTodos = evolu.createQuery((db) =>
|
|
71
|
+
* db.selectFrom("todo").selectAll(),
|
|
72
|
+
* );
|
|
73
|
+
*
|
|
74
|
+
* const todoById = (id: TodoId) =>
|
|
75
|
+
* evolu.createQuery((db) =>
|
|
76
|
+
* db.selectFrom("todo").selectAll().where("id", "=", id),
|
|
77
|
+
* );
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
readonly createQuery: CreateQuery<T>;
|
|
67
81
|
|
|
68
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Load {@link Query} and return a promise with {@link QueryResult}.
|
|
84
|
+
*
|
|
85
|
+
* A returned promise always resolves successfully because there is no reason
|
|
86
|
+
* why loading should fail. All data are local, and the query is typed. A
|
|
87
|
+
* serious unexpected Evolu error shall be handled with
|
|
88
|
+
* {@link subscribeError}.
|
|
89
|
+
*
|
|
90
|
+
* Loading is batched, and returned promises are cached, so there is no need
|
|
91
|
+
* for an additional cache. Evolu's internal cache is invalidated on
|
|
92
|
+
* mutation.
|
|
93
|
+
*
|
|
94
|
+
* The returned promise is enriched with special status and value properties
|
|
95
|
+
* for the upcoming React `use` Hook, but other UI libraries can also leverage
|
|
96
|
+
* them. Speaking of React, there are two essential React Suspense-related
|
|
97
|
+
* patterns that every developer should be aware of—passing promises to
|
|
98
|
+
* children and caching over mutations.
|
|
99
|
+
*
|
|
100
|
+
* With promises passed to children, we can load a query as soon as possible,
|
|
101
|
+
* but we don't have to use the returned promise immediately. That's useful
|
|
102
|
+
* for prefetching, which is generally not necessary for local-first apps but
|
|
103
|
+
* can be if a query takes a long time to load.
|
|
104
|
+
*
|
|
105
|
+
* Caching over mutation is a pattern that every developer should know. As we
|
|
106
|
+
* said, Evolu caches promise until a mutation happens. A query loaded after
|
|
107
|
+
* that will return a new pending promise. That's okay for general usage but
|
|
108
|
+
* not for UI with React Suspense because a mutation would suspend rerendered
|
|
109
|
+
* queries on a page, and that's not a good UX.
|
|
110
|
+
*
|
|
111
|
+
* We call this pattern "caching over mutation" because it has no globally
|
|
112
|
+
* accepted name yet. React RFC for React Cache does not exist yet.
|
|
113
|
+
*
|
|
114
|
+
* For better UX, a query must be subscribed for updates. This way, instead of
|
|
115
|
+
* Suspense flashes, the user sees new data immediately because Evolu replaces
|
|
116
|
+
* cached promises with fresh, already resolved new ones.
|
|
117
|
+
*
|
|
118
|
+
* If you are curious why Evolu does not do that for all queries by default,
|
|
119
|
+
* the answer is simple: performance. Tracking changes is costly and
|
|
120
|
+
* meaningful only for visible (hence subscribed) queries anyway. To subscribe
|
|
121
|
+
* to a query, use {@link subscribeQuery}.
|
|
122
|
+
*
|
|
123
|
+
* ### Example
|
|
124
|
+
*
|
|
125
|
+
* ```ts
|
|
126
|
+
* const allTodos = evolu.createQuery((db) =>
|
|
127
|
+
* db.selectFrom("todo").selectAll(),
|
|
128
|
+
* );
|
|
129
|
+
* evolu.loadQuery(allTodos).then(({ rows }) => {
|
|
130
|
+
* console.log(rows);
|
|
131
|
+
* });
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
readonly loadQuery: LoadQuery;
|
|
69
135
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Load an array of {@link Query} queries and return an array of
|
|
138
|
+
* {@link QueryResult} promises. It's like `queries.map(loadQuery)` but with
|
|
139
|
+
* proper types for returned promises.
|
|
140
|
+
*
|
|
141
|
+
* ### Example
|
|
142
|
+
*
|
|
143
|
+
* ```ts
|
|
144
|
+
* evolu.loadQueries([allTodos, todoById(1)]);
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
readonly loadQueries: <R extends Row, Q extends Queries<R>>(
|
|
148
|
+
queries: [...Q],
|
|
149
|
+
) => [...QueryResultsPromisesFromQueries<Q>];
|
|
78
150
|
|
|
79
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Subscribe to {@link Query} {@link QueryResult} changes.
|
|
153
|
+
*
|
|
154
|
+
* ### Example
|
|
155
|
+
*
|
|
156
|
+
* ```ts
|
|
157
|
+
* const unsubscribe = evolu.subscribeQuery(allTodos)(() => {
|
|
158
|
+
* const { rows } = evolu.getQuery(allTodos);
|
|
159
|
+
* });
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
readonly subscribeQuery: SubscribedQueries["subscribeQuery"];
|
|
80
163
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Get {@link Query} {@link QueryResult}.
|
|
166
|
+
*
|
|
167
|
+
* ### Example
|
|
168
|
+
*
|
|
169
|
+
* ```ts
|
|
170
|
+
* const unsubscribe = evolu.subscribeQuery(allTodos)(() => {
|
|
171
|
+
* const { rows } = evolu.getQuery(allTodos);
|
|
172
|
+
* });
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
readonly getQuery: SubscribedQueries["getQuery"];
|
|
88
176
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Subscribe to {@link Owner} changes.
|
|
179
|
+
*
|
|
180
|
+
* ### Example
|
|
181
|
+
*
|
|
182
|
+
* ```ts
|
|
183
|
+
* const unsubscribe = evolu.subscribeOwner(() => {
|
|
184
|
+
* const owner = evolu.getOwner();
|
|
185
|
+
* });
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
readonly subscribeOwner: Store<Owner | null>["subscribe"];
|
|
97
189
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
|
|
190
|
+
/**
|
|
191
|
+
* Get {@link Owner}.
|
|
192
|
+
*
|
|
193
|
+
* ### Example
|
|
194
|
+
*
|
|
195
|
+
* ```ts
|
|
196
|
+
* const unsubscribe = evolu.subscribeOwner(() => {
|
|
197
|
+
* const owner = evolu.getOwner();
|
|
198
|
+
* });
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
readonly getOwner: Store<Owner | null>["getState"];
|
|
109
202
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Subscribe to {@link SyncState} changes.
|
|
205
|
+
*
|
|
206
|
+
* ### Example
|
|
207
|
+
*
|
|
208
|
+
* ```ts
|
|
209
|
+
* const unsubscribe = evolu.subscribeSyncState(() => {
|
|
210
|
+
* const syncState = evolu.getSyncState();
|
|
211
|
+
* });
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
214
|
+
readonly subscribeSyncState: Store<SyncState>["subscribe"];
|
|
120
215
|
|
|
121
|
-
|
|
122
|
-
|
|
216
|
+
/**
|
|
217
|
+
* Get {@link SyncState}.
|
|
218
|
+
*
|
|
219
|
+
* ### Example
|
|
220
|
+
*
|
|
221
|
+
* ```ts
|
|
222
|
+
* const unsubscribe = evolu.subscribeSyncState(() => {
|
|
223
|
+
* const syncState = evolu.getSyncState();
|
|
224
|
+
* });
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
readonly getSyncState: Store<SyncState>["getState"];
|
|
123
228
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
229
|
+
/**
|
|
230
|
+
* The function creates a row in the database and returns a new ID. The first
|
|
231
|
+
* argument is the table name, and the second is an object.
|
|
232
|
+
*
|
|
233
|
+
* The third optional argument, the onComplete callback, is generally
|
|
234
|
+
* unnecessary because creating a row cannot fail. Still, UI libraries can use
|
|
235
|
+
* it to ensure the DOM is updated if we want to manipulate it, for example,
|
|
236
|
+
* to focus an element.
|
|
237
|
+
*
|
|
238
|
+
* Evolu does not use SQL for mutations to ensure data can be safely and
|
|
239
|
+
* predictably merged without conflicts.
|
|
240
|
+
*
|
|
241
|
+
* Explicit mutations also allow Evolu to automatically add and update a few
|
|
242
|
+
* useful columns common to all tables. Those columns are: `createdAt`,
|
|
243
|
+
* `updatedAt`, and `isDeleted`.
|
|
244
|
+
*
|
|
245
|
+
* ### Example
|
|
246
|
+
*
|
|
247
|
+
* ```ts
|
|
248
|
+
* import * as S from "@effect/schema/Schema";
|
|
249
|
+
*
|
|
250
|
+
* // Evolu uses the Schema to enforce domain model.
|
|
251
|
+
* const title = S.parseSync(Evolu.NonEmptyString1000)("A title");
|
|
252
|
+
*
|
|
253
|
+
* const { id } = evolu.create("todo", { title }, () => {
|
|
254
|
+
* // onComplete callback
|
|
255
|
+
* });
|
|
256
|
+
* ```
|
|
257
|
+
*/
|
|
258
|
+
create: Mutate<T, "create">;
|
|
131
259
|
|
|
132
|
-
export interface OwnerActions {
|
|
133
260
|
/**
|
|
134
|
-
*
|
|
135
|
-
*
|
|
261
|
+
* The function updates a row in the database and returns the existing ID. The
|
|
262
|
+
* first argument is the table name, and the second is an object.
|
|
263
|
+
*
|
|
264
|
+
* The third optional argument, the onComplete callback, is generally
|
|
265
|
+
* unnecessary because creating a row cannot fail. Still, UI libraries can use
|
|
266
|
+
* it to ensure the DOM is updated if we want to manipulate it, for example,
|
|
267
|
+
* to focus an element.
|
|
268
|
+
*
|
|
269
|
+
* Evolu does not use SQL for mutations to ensure data can be safely and
|
|
270
|
+
* predictably merged without conflicts.
|
|
271
|
+
*
|
|
272
|
+
* Explicit mutations also allow Evolu to automatically add and update a few
|
|
273
|
+
* useful columns common to all tables. Those columns are: `createdAt`,
|
|
274
|
+
* `updatedAt`, and `isDeleted`.
|
|
275
|
+
*
|
|
276
|
+
* ### Example
|
|
277
|
+
*
|
|
278
|
+
* ```ts
|
|
279
|
+
* import * as S from "@effect/schema/Schema";
|
|
280
|
+
*
|
|
281
|
+
* // Evolu uses the Schema to enforce domain model.
|
|
282
|
+
* const title = S.parseSync(Evolu.NonEmptyString1000)("A title");
|
|
283
|
+
* evolu.update("todo", { id, title });
|
|
284
|
+
*
|
|
285
|
+
* // To delete a row, set `isDeleted` to true.
|
|
286
|
+
* evolu.update("todo", { id, isDeleted: true });
|
|
287
|
+
* ```
|
|
136
288
|
*/
|
|
137
|
-
|
|
289
|
+
update: Mutate<T, "update">;
|
|
138
290
|
|
|
139
291
|
/**
|
|
140
|
-
*
|
|
292
|
+
* Delete {@link Owner} and all their data from the current device. After the
|
|
293
|
+
* deletion, Evolu will purge the application state. For browsers, this will
|
|
294
|
+
* reload all tabs using Evolu. For native apps, it will restart the app.
|
|
141
295
|
*/
|
|
142
|
-
readonly
|
|
143
|
-
mnemonic: string,
|
|
144
|
-
) => Promise<Either.Either<RestoreOwnerError, void>>;
|
|
145
|
-
}
|
|
296
|
+
readonly resetOwner: () => void;
|
|
146
297
|
|
|
147
|
-
|
|
298
|
+
/** Restore {@link Owner} with all their synced data. */
|
|
299
|
+
readonly restoreOwner: (mnemonic: Mnemonic) => void;
|
|
148
300
|
|
|
149
|
-
|
|
150
|
-
readonly
|
|
301
|
+
/** Ensure tables and columns defined in {@link Schema} exist in the database. */
|
|
302
|
+
readonly ensureSchema: <From, To extends T>(
|
|
303
|
+
schema: S.Schema<From, To>,
|
|
304
|
+
) => void;
|
|
151
305
|
}
|
|
152
306
|
|
|
153
|
-
|
|
307
|
+
export const Evolu = Context.Tag<Evolu>();
|
|
154
308
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
)
|
|
309
|
+
type CreateQuery<S extends Schema> = <R extends Row>(
|
|
310
|
+
queryCallback: QueryCallback<S, R>,
|
|
311
|
+
) => Query<R>;
|
|
312
|
+
|
|
313
|
+
type QueryCallback<S extends Schema, R extends Row> = (
|
|
314
|
+
db: Pick<
|
|
315
|
+
Kysely.Kysely<QuerySchema<S>>,
|
|
316
|
+
| "selectFrom"
|
|
317
|
+
// TODO: Test.
|
|
318
|
+
// | "selectNoFrom"
|
|
319
|
+
| "fn"
|
|
320
|
+
>,
|
|
321
|
+
) => Kysely.SelectQueryBuilder<any, any, R>;
|
|
158
322
|
|
|
159
|
-
type
|
|
323
|
+
type QuerySchema<S extends Schema> = {
|
|
324
|
+
readonly [Table in keyof S]: NullableExceptId<
|
|
325
|
+
{
|
|
326
|
+
readonly [Column in keyof S[Table]]: S[Table][Column];
|
|
327
|
+
} & CommonColumns
|
|
328
|
+
>;
|
|
329
|
+
};
|
|
160
330
|
|
|
161
|
-
|
|
331
|
+
type NullableExceptId<T> = {
|
|
332
|
+
readonly [K in keyof T]: K extends "id" ? T[K] : T[K] | null;
|
|
333
|
+
};
|
|
162
334
|
|
|
163
|
-
|
|
335
|
+
export interface CommonColumns {
|
|
336
|
+
readonly createdAt: SqliteDate;
|
|
337
|
+
readonly updatedAt: SqliteDate;
|
|
338
|
+
readonly isDeleted: SqliteBoolean;
|
|
339
|
+
}
|
|
164
340
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
341
|
+
const kysely = new Kysely.Kysely<QuerySchema<Schema>>({
|
|
342
|
+
dialect: {
|
|
343
|
+
createAdapter: (): Kysely.DialectAdapter => new Kysely.SqliteAdapter(),
|
|
344
|
+
createDriver: (): Kysely.Driver => new Kysely.DummyDriver(),
|
|
345
|
+
createIntrospector(): Kysely.DatabaseIntrospector {
|
|
346
|
+
throw "Not implemeneted";
|
|
347
|
+
},
|
|
348
|
+
createQueryCompiler: (): Kysely.QueryCompiler =>
|
|
349
|
+
new Kysely.SqliteQueryCompiler(),
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
export const makeCreateQuery =
|
|
354
|
+
<S extends Schema = Schema>(): CreateQuery<S> =>
|
|
355
|
+
<R extends Row>(queryCallback: QueryCallback<S, R>) =>
|
|
356
|
+
pipe(
|
|
357
|
+
queryCallback(kysely as Kysely.Kysely<QuerySchema<S>>).compile(),
|
|
358
|
+
({ sql, parameters }): SqliteQuery => ({
|
|
359
|
+
sql,
|
|
360
|
+
parameters: parameters as SqliteQuery["parameters"],
|
|
361
|
+
}),
|
|
362
|
+
(query) => serializeQuery<R>(query),
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
export interface LoadingPromises {
|
|
366
|
+
readonly get: <R extends Row>(
|
|
367
|
+
query: Query<R>,
|
|
368
|
+
) => {
|
|
369
|
+
readonly promise: LoadingPromise<R>;
|
|
168
370
|
readonly isNew: boolean;
|
|
169
371
|
};
|
|
170
|
-
|
|
171
|
-
readonly
|
|
372
|
+
|
|
373
|
+
readonly resolve: <R extends Row>(
|
|
374
|
+
query: Query<R>,
|
|
375
|
+
rows: ReadonlyArray<R>,
|
|
376
|
+
) => void;
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Release all unsubscribed queries on mutation because only subscribed
|
|
380
|
+
* queries are automatically updated.
|
|
381
|
+
*/
|
|
382
|
+
readonly release: () => void;
|
|
172
383
|
}
|
|
173
384
|
|
|
174
|
-
const LoadingPromises = Context.Tag<LoadingPromises>(
|
|
385
|
+
export const LoadingPromises = Context.Tag<LoadingPromises>();
|
|
175
386
|
|
|
176
|
-
export
|
|
387
|
+
export type LoadingPromise<R extends Row> = Promise<QueryResult<R>> & {
|
|
388
|
+
status?: "pending" | "fulfilled" | "rejected";
|
|
389
|
+
value?: QueryResult<R>;
|
|
390
|
+
reason?: unknown;
|
|
391
|
+
};
|
|
177
392
|
|
|
178
|
-
const
|
|
393
|
+
export const LoadingPromiseLive = Layer.effect(
|
|
179
394
|
LoadingPromises,
|
|
180
|
-
Effect.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
395
|
+
Effect.gen(function* (_) {
|
|
396
|
+
interface LoadingPromiseWithResolve<R extends Row> {
|
|
397
|
+
promise: LoadingPromise<R>;
|
|
398
|
+
readonly resolve: Resolve<R>;
|
|
399
|
+
releaseOnResolve: boolean;
|
|
400
|
+
}
|
|
401
|
+
type Resolve<R extends Row> = (rows: QueryResult<R>) => void;
|
|
402
|
+
|
|
403
|
+
const subscribedQueries = yield* _(SubscribedQueries);
|
|
404
|
+
const promises = new Map<Query, LoadingPromiseWithResolve<Row>>();
|
|
405
|
+
|
|
406
|
+
return LoadingPromises.of({
|
|
407
|
+
get<R extends Row>(query: Query<R>) {
|
|
408
|
+
let isNew = false;
|
|
409
|
+
let promiseWithResolve = promises.get(query);
|
|
410
|
+
|
|
411
|
+
if (!promiseWithResolve) {
|
|
412
|
+
isNew = true;
|
|
413
|
+
let resolve: Resolve<Row> = Function.constVoid;
|
|
414
|
+
const promise: LoadingPromise<Row> = new Promise((_resolve) => {
|
|
415
|
+
resolve = _resolve;
|
|
416
|
+
});
|
|
417
|
+
promiseWithResolve = { promise, resolve, releaseOnResolve: false };
|
|
418
|
+
promises.set(query, promiseWithResolve);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
promise: promiseWithResolve.promise as LoadingPromise<R>,
|
|
423
|
+
isNew,
|
|
424
|
+
};
|
|
425
|
+
},
|
|
426
|
+
|
|
427
|
+
resolve(query, rows) {
|
|
428
|
+
const promiseWithResolve = promises.get(query);
|
|
429
|
+
if (!promiseWithResolve) return;
|
|
430
|
+
const result = queryResultFromRows(rows);
|
|
431
|
+
if (promiseWithResolve.promise.status !== "fulfilled")
|
|
432
|
+
promiseWithResolve.resolve(result);
|
|
433
|
+
else promiseWithResolve.promise = Promise.resolve(result);
|
|
434
|
+
setPromiseAsResolved(promiseWithResolve.promise)(result);
|
|
435
|
+
if (promiseWithResolve.releaseOnResolve) promises.delete(query);
|
|
436
|
+
},
|
|
437
|
+
|
|
438
|
+
release() {
|
|
439
|
+
const keep = subscribedQueries.getSubscribedQueries();
|
|
440
|
+
promises.forEach((promiseWithResolve, query) => {
|
|
441
|
+
if (keep.includes(query)) return;
|
|
442
|
+
if (promiseWithResolve.promise.status === "fulfilled")
|
|
443
|
+
promises.delete(query);
|
|
444
|
+
else promiseWithResolve.releaseOnResolve = true;
|
|
445
|
+
});
|
|
446
|
+
},
|
|
447
|
+
});
|
|
217
448
|
}),
|
|
218
449
|
);
|
|
219
450
|
|
|
220
|
-
|
|
221
|
-
|
|
451
|
+
// "For example, a data framework can set the status and value fields on a promise
|
|
452
|
+
// preemptively, before passing to React, so that React can unwrap it without waiting
|
|
453
|
+
// a microtask."
|
|
454
|
+
// https://github.com/acdlite/rfcs/blob/first-class-promises/text/0000-first-class-support-for-promises.md
|
|
455
|
+
const setPromiseAsResolved =
|
|
456
|
+
<T>(promise: Promise<T>) =>
|
|
457
|
+
(value: unknown): void => {
|
|
458
|
+
Object.assign(promise, { status: "fulfilled", value });
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
type LoadQuery = <R extends Row>(query: Query<R>) => Promise<QueryResult<R>>;
|
|
462
|
+
|
|
463
|
+
const LoadQuery = Context.Tag<LoadQuery>();
|
|
464
|
+
|
|
465
|
+
const LoadQueryLive = Layer.effect(
|
|
466
|
+
LoadQuery,
|
|
222
467
|
Effect.gen(function* (_) {
|
|
223
|
-
const subscribedQueries = yield* _(SubscribedQueries);
|
|
224
468
|
const loadingPromises = yield* _(LoadingPromises);
|
|
225
469
|
const dbWorker = yield* _(DbWorker);
|
|
226
|
-
|
|
227
|
-
const onCompletes = yield* _(OnCompletes);
|
|
228
|
-
|
|
229
|
-
const rowsCacheStore = makeStore<RowsCacheMap>(new Map());
|
|
230
|
-
const queue = new Set<Query>();
|
|
470
|
+
let queries: ReadonlyArray<Query> = [];
|
|
231
471
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
Number.increment(subscribedQueries.get(query) ?? 0),
|
|
237
|
-
);
|
|
238
|
-
const unsubscribe = rowsCacheStore.subscribe(listen);
|
|
239
|
-
return () => {
|
|
240
|
-
// `as number`, because React mount/unmount are symmetric.
|
|
241
|
-
const count = subscribedQueries.get(query) as number;
|
|
242
|
-
if (count > 1) subscribedQueries.set(query, Number.decrement(count));
|
|
243
|
-
else subscribedQueries.delete(query);
|
|
244
|
-
unsubscribe();
|
|
245
|
-
};
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
const getState: QueryStore["getState"] = (query) =>
|
|
249
|
-
(query && rowsCacheStore.getState().get(query)) || null;
|
|
250
|
-
|
|
251
|
-
const loadQuery: QueryStore["loadQuery"] = (query) => {
|
|
252
|
-
const { promise, isNew } = loadingPromises.getPromise(query);
|
|
253
|
-
if (isNew) queue.add(query);
|
|
254
|
-
if (queue.size === 1) {
|
|
472
|
+
return LoadQuery.of((query) => {
|
|
473
|
+
const { promise, isNew } = loadingPromises.get(query);
|
|
474
|
+
if (isNew) queries = [...queries, query];
|
|
475
|
+
if (queries.length === 1) {
|
|
255
476
|
queueMicrotask(() => {
|
|
256
|
-
const queries = [...queue];
|
|
257
|
-
queue.clear();
|
|
258
477
|
if (ReadonlyArray.isNonEmptyReadonlyArray(queries))
|
|
259
478
|
dbWorker.postMessage({ _tag: "query", queries });
|
|
479
|
+
queries = [];
|
|
260
480
|
});
|
|
261
481
|
}
|
|
262
482
|
return promise;
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
const onQuery: QueryStore["onQuery"] = ({
|
|
266
|
-
queriesPatches,
|
|
267
|
-
onCompleteIds,
|
|
268
|
-
}) => {
|
|
269
|
-
const state = rowsCacheStore.getState();
|
|
270
|
-
const nextState = pipe(
|
|
271
|
-
queriesPatches,
|
|
272
|
-
ReadonlyArray.map(
|
|
273
|
-
({ query, patches }) =>
|
|
274
|
-
[query, applyPatches(patches)(state.get(query) || [])] as const,
|
|
275
|
-
),
|
|
276
|
-
(a): RowsCacheMap => new Map([...state, ...a]),
|
|
277
|
-
);
|
|
278
|
-
// Resolve all Promises belonging to queries.
|
|
279
|
-
queriesPatches.forEach(({ query }) => {
|
|
280
|
-
const rows = nextState.get(query) || [];
|
|
281
|
-
loadingPromises.resolvePromise(query, rows);
|
|
282
|
-
});
|
|
283
|
-
// No mutation is using onComplete, so we don't need flushSync.
|
|
284
|
-
if (onCompleteIds.length === 0) {
|
|
285
|
-
rowsCacheStore.setState(nextState);
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
flushSync(() => rowsCacheStore.setState(nextState));
|
|
289
|
-
ReadonlyArray.filterMap(onCompleteIds, (id) => {
|
|
290
|
-
const onComplete = onCompletes.get(id);
|
|
291
|
-
onCompletes.delete(id);
|
|
292
|
-
return Option.fromNullable(onComplete);
|
|
293
|
-
}).forEach((onComplete) => onComplete());
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
return { subscribe, getState, loadQuery, onQuery };
|
|
483
|
+
});
|
|
297
484
|
}),
|
|
298
485
|
);
|
|
299
486
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
>
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
487
|
+
type OnQuery = (
|
|
488
|
+
dbWorkerOutputOnQuery: DbWorkerOutputOnQuery,
|
|
489
|
+
) => Effect.Effect<never, never, void>;
|
|
490
|
+
|
|
491
|
+
const OnQuery = Context.Tag<OnQuery>();
|
|
492
|
+
|
|
493
|
+
const OnQueryLive = Layer.effect(
|
|
494
|
+
OnQuery,
|
|
495
|
+
Effect.gen(function* (_) {
|
|
496
|
+
const rowsStore = yield* _(RowsStore);
|
|
497
|
+
const loadingPromises = yield* _(LoadingPromises);
|
|
498
|
+
const flushSync = yield* _(FlushSync);
|
|
499
|
+
const onCompletes = yield* _(OnCompletes);
|
|
500
|
+
|
|
501
|
+
return OnQuery.of(({ queriesPatches, onCompleteIds }) =>
|
|
502
|
+
Effect.gen(function* (_) {
|
|
503
|
+
const currentState = rowsStore.getState();
|
|
504
|
+
const nextState = pipe(
|
|
505
|
+
queriesPatches,
|
|
506
|
+
ReadonlyArray.map(
|
|
507
|
+
({ query, patches }) =>
|
|
508
|
+
[
|
|
509
|
+
query,
|
|
510
|
+
applyPatches(patches)(currentState.get(query) || emptyRows()),
|
|
511
|
+
] as const,
|
|
512
|
+
),
|
|
513
|
+
(map) => new Map([...currentState, ...map]),
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
queriesPatches.forEach(({ query }) => {
|
|
517
|
+
loadingPromises.resolve(query, nextState.get(query) || emptyRows());
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// No mutation is using onComplete, so we don't need flushSync.
|
|
521
|
+
if (onCompleteIds.length === 0) {
|
|
522
|
+
yield* _(rowsStore.setState(nextState));
|
|
523
|
+
return;
|
|
325
524
|
}
|
|
326
525
|
|
|
327
|
-
|
|
526
|
+
flushSync(() => rowsStore.setState(nextState).pipe(Effect.runSync));
|
|
527
|
+
yield* _(onCompletes.complete(onCompleteIds));
|
|
528
|
+
}),
|
|
529
|
+
);
|
|
530
|
+
}),
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
export interface SubscribedQueries {
|
|
534
|
+
readonly subscribeQuery: (query: Query) => Store<Row>["subscribe"];
|
|
535
|
+
readonly getQuery: <R extends Row>(query: Query<R>) => QueryResult<R>;
|
|
536
|
+
readonly getSubscribedQueries: () => Queries;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export const SubscribedQueries = Context.Tag<SubscribedQueries>();
|
|
540
|
+
|
|
541
|
+
export const SubscribedQueriesLive = Layer.effect(
|
|
542
|
+
SubscribedQueries,
|
|
543
|
+
Effect.gen(function* (_) {
|
|
544
|
+
const rowsStore = yield* _(RowsStore);
|
|
545
|
+
const subscribedQueries = new Map<Query, number>();
|
|
546
|
+
|
|
547
|
+
return SubscribedQueries.of({
|
|
548
|
+
subscribeQuery:
|
|
549
|
+
(query) =>
|
|
550
|
+
(listener): Unsubscribe => {
|
|
551
|
+
subscribedQueries.set(
|
|
552
|
+
query,
|
|
553
|
+
Number.increment(subscribedQueries.get(query) ?? 0),
|
|
554
|
+
);
|
|
555
|
+
const unsubscribe = rowsStore.subscribe(listener);
|
|
556
|
+
|
|
557
|
+
return () => {
|
|
558
|
+
const count = subscribedQueries.get(query);
|
|
559
|
+
if (count != null && count > 1)
|
|
560
|
+
subscribedQueries.set(query, Number.decrement(count));
|
|
561
|
+
else subscribedQueries.delete(query);
|
|
562
|
+
unsubscribe();
|
|
563
|
+
};
|
|
564
|
+
},
|
|
565
|
+
|
|
566
|
+
getQuery: <R extends Row>(query: Query<R>): QueryResult<R> =>
|
|
567
|
+
queryResultFromRows(
|
|
568
|
+
rowsStore.getState().get(query) || emptyRows(),
|
|
569
|
+
) as QueryResult<R>,
|
|
570
|
+
|
|
571
|
+
getSubscribedQueries: () =>
|
|
572
|
+
ReadonlyArray.fromIterable(subscribedQueries.keys()),
|
|
573
|
+
});
|
|
574
|
+
}),
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
type Mutate<
|
|
578
|
+
S extends Schema = Schema,
|
|
579
|
+
Mode extends "create" | "update" = "update",
|
|
580
|
+
> = <K extends keyof S>(
|
|
581
|
+
table: K,
|
|
582
|
+
values: Kysely.Simplify<
|
|
583
|
+
Mode extends "create"
|
|
584
|
+
? PartialForNullable<Castable<Omit<S[K], "id">>>
|
|
585
|
+
: Partial<
|
|
586
|
+
Castable<Omit<S[K], "id"> & Pick<CommonColumns, "isDeleted">>
|
|
587
|
+
> & { readonly id: S[K]["id"] }
|
|
588
|
+
>,
|
|
589
|
+
onComplete?: () => void,
|
|
590
|
+
) => {
|
|
591
|
+
readonly id: S[K]["id"];
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
const Mutate = Context.Tag<Mutate>();
|
|
595
|
+
|
|
596
|
+
// https://stackoverflow.com/a/54713648/233902
|
|
597
|
+
type PartialForNullable<
|
|
598
|
+
T,
|
|
599
|
+
NK extends keyof T = {
|
|
600
|
+
[K in keyof T]: null extends T[K] ? K : never;
|
|
601
|
+
}[keyof T],
|
|
602
|
+
NP = Pick<T, Exclude<keyof T, NK>> & Partial<Pick<T, NK>>,
|
|
603
|
+
> = { [K in keyof NP]: NP[K] };
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* SQLite doesn't support Date nor Boolean types, so Evolu emulates them with
|
|
607
|
+
* {@link SqliteBoolean} and {@link SqliteDate}.
|
|
608
|
+
*
|
|
609
|
+
* For {@link SqliteBoolean}, you can use JavaScript boolean. For
|
|
610
|
+
* {@link SqliteDate}, you can use JavaScript Date.
|
|
611
|
+
*/
|
|
612
|
+
type Castable<T> = {
|
|
613
|
+
readonly [K in keyof T]: T[K] extends SqliteBoolean
|
|
614
|
+
? boolean | SqliteBoolean
|
|
615
|
+
: T[K] extends null | SqliteBoolean
|
|
616
|
+
? null | boolean | SqliteBoolean
|
|
617
|
+
: T[K] extends SqliteDate
|
|
618
|
+
? Date | SqliteDate
|
|
619
|
+
: T[K] extends null | SqliteDate
|
|
620
|
+
? null | Date | SqliteDate
|
|
621
|
+
: T[K];
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
const MutateLive = Layer.effect(
|
|
625
|
+
Mutate,
|
|
626
|
+
Effect.gen(function* (_) {
|
|
627
|
+
const nanoid = yield* _(NanoId);
|
|
628
|
+
const onCompletes = yield* _(OnCompletes);
|
|
629
|
+
const time = yield* _(Time);
|
|
630
|
+
const subscribedQueries = yield* _(SubscribedQueries);
|
|
631
|
+
const loadingPromises = yield* _(LoadingPromises);
|
|
632
|
+
const dbWorker = yield* _(DbWorker);
|
|
633
|
+
let mutations: ReadonlyArray<Mutation> = [];
|
|
634
|
+
|
|
635
|
+
return Mutate.of((table, { id, ...values }, onComplete) => {
|
|
636
|
+
const isInsert = id == null;
|
|
637
|
+
if (isInsert) id = Effect.runSync(nanoid.nanoid) as never;
|
|
638
|
+
|
|
639
|
+
const onCompleteId = onComplete
|
|
640
|
+
? onCompletes.add(onComplete).pipe(Effect.runSync)
|
|
641
|
+
: null;
|
|
642
|
+
|
|
643
|
+
mutations = [
|
|
644
|
+
...mutations,
|
|
645
|
+
{
|
|
328
646
|
table: table.toString(),
|
|
329
|
-
id
|
|
647
|
+
id,
|
|
330
648
|
values,
|
|
331
649
|
isInsert,
|
|
332
650
|
now: cast(new Date(Effect.runSync(time.now))),
|
|
333
651
|
onCompleteId,
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
if (queue.length === 1)
|
|
337
|
-
queueMicrotask(() => {
|
|
338
|
-
const items = [...queue];
|
|
339
|
-
queue.length = 0;
|
|
340
|
-
|
|
341
|
-
const queries = Array.from(subscribedQueries.keys());
|
|
342
|
-
// Just wipe-out LoadingPromises unused queries.
|
|
343
|
-
loadingPromises.releasePromises(queries);
|
|
652
|
+
},
|
|
653
|
+
];
|
|
344
654
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
655
|
+
if (mutations.length === 1)
|
|
656
|
+
queueMicrotask(() => {
|
|
657
|
+
if (ReadonlyArray.isNonEmptyReadonlyArray(mutations))
|
|
658
|
+
dbWorker.postMessage({
|
|
659
|
+
_tag: "mutate",
|
|
660
|
+
mutations,
|
|
661
|
+
queries: subscribedQueries.getSubscribedQueries(),
|
|
662
|
+
});
|
|
663
|
+
loadingPromises.release();
|
|
664
|
+
mutations = [];
|
|
665
|
+
});
|
|
348
666
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
667
|
+
return { id };
|
|
668
|
+
});
|
|
669
|
+
}),
|
|
670
|
+
);
|
|
353
671
|
|
|
354
|
-
|
|
355
|
-
|
|
672
|
+
/** EvoluCommon is without side effects, so it's unit-testable. */
|
|
673
|
+
const EvoluCommon = Layer.effect(
|
|
674
|
+
Evolu,
|
|
356
675
|
Effect.gen(function* (_) {
|
|
357
676
|
const dbWorker = yield* _(DbWorker);
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
677
|
+
const errorStore = yield* _(makeErrorStore);
|
|
678
|
+
const loadQuery = yield* _(LoadQuery);
|
|
679
|
+
const onQuery = yield* _(OnQuery);
|
|
680
|
+
const { subscribeQuery, getQuery, getSubscribedQueries } =
|
|
681
|
+
yield* _(SubscribedQueries);
|
|
682
|
+
const syncStateStore = yield* _(
|
|
683
|
+
makeStore<SyncState>({ _tag: "SyncStateInitial" }),
|
|
684
|
+
);
|
|
685
|
+
const mutate = yield* _(Mutate);
|
|
686
|
+
const ownerStore = yield* _(makeStore<Owner | null>(null));
|
|
687
|
+
const loadingPromises = yield* _(LoadingPromises);
|
|
688
|
+
const appState = yield* _(AppState);
|
|
689
|
+
|
|
690
|
+
dbWorker.onMessage = (output): void =>
|
|
691
|
+
Match.value(output).pipe(
|
|
692
|
+
Match.tagsExhaustive({
|
|
693
|
+
onError: ({ error }) => errorStore.setState(error),
|
|
694
|
+
onQuery,
|
|
695
|
+
onOwner: ({ owner }) => ownerStore.setState(owner),
|
|
696
|
+
onSyncState: ({ state }) => syncStateStore.setState(state),
|
|
697
|
+
onReceive: () =>
|
|
698
|
+
Effect.sync(() => {
|
|
699
|
+
loadingPromises.release();
|
|
700
|
+
const queries = getSubscribedQueries();
|
|
701
|
+
if (ReadonlyArray.isNonEmptyReadonlyArray(queries))
|
|
702
|
+
dbWorker.postMessage({ _tag: "query", queries });
|
|
703
|
+
}),
|
|
704
|
+
onResetOrRestore: () => appState.reset,
|
|
705
|
+
}),
|
|
706
|
+
Effect.runSync,
|
|
378
707
|
);
|
|
379
708
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
)
|
|
709
|
+
dbWorker.postMessage({
|
|
710
|
+
_tag: "init",
|
|
711
|
+
config: yield* _(Config),
|
|
712
|
+
});
|
|
383
713
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
Evolu<S>(),
|
|
393
|
-
Effect.gen(function* (_) {
|
|
394
|
-
const dbWorker = yield* _(DbWorker);
|
|
395
|
-
const appState = yield* _(AppState);
|
|
396
|
-
const config = yield* _(Config);
|
|
397
|
-
|
|
398
|
-
const subscribedQueries: SubscribedQueries = new Map();
|
|
399
|
-
const onCompletes: OnCompletes = new Map();
|
|
400
|
-
const loadingPromises = Effect.provide(
|
|
401
|
-
LoadingPromises,
|
|
402
|
-
LoadingPromisesLive,
|
|
403
|
-
).pipe(Effect.runSync);
|
|
404
|
-
|
|
405
|
-
const errorStore = makeStore<EvoluError | null>(null);
|
|
406
|
-
const ownerStore = makeStore<Owner | null>(null);
|
|
407
|
-
const syncStateStore = makeStore<SyncState>({
|
|
408
|
-
_tag: "SyncStateInitial",
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
const Layers = Layer.mergeAll(
|
|
412
|
-
Layer.succeed(DbWorker, dbWorker),
|
|
413
|
-
Layer.succeed(SubscribedQueries, subscribedQueries),
|
|
414
|
-
Layer.succeed(OnCompletes, onCompletes),
|
|
415
|
-
Layer.succeed(LoadingPromises, loadingPromises),
|
|
416
|
-
Layer.succeed(FlushSync, yield* _(FlushSync)),
|
|
417
|
-
Layer.succeed(NanoId, yield* _(NanoId)),
|
|
418
|
-
Layer.succeed(Time, yield* _(Time)),
|
|
419
|
-
Layer.succeed(Bip39, yield* _(Bip39)),
|
|
420
|
-
);
|
|
714
|
+
appState.init({
|
|
715
|
+
onFocus: () => {
|
|
716
|
+
dbWorker.postMessage({ _tag: "sync", queries: getSubscribedQueries() });
|
|
717
|
+
},
|
|
718
|
+
onReconnect: () => {
|
|
719
|
+
dbWorker.postMessage({ _tag: "sync", queries: [] });
|
|
720
|
+
},
|
|
721
|
+
});
|
|
421
722
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
).pipe(Effect.runSync);
|
|
426
|
-
|
|
427
|
-
const mutate = Effect.provide(
|
|
428
|
-
Mutate<S>(),
|
|
429
|
-
Layer.use(MutateLive<S>(), Layers),
|
|
430
|
-
).pipe(Effect.runSync);
|
|
431
|
-
|
|
432
|
-
const ownerActions = Effect.provide(
|
|
433
|
-
OwnerActions,
|
|
434
|
-
Layer.use(OwnerActionsLive, Layers),
|
|
435
|
-
).pipe(Effect.runSync);
|
|
436
|
-
|
|
437
|
-
const ensureSchema: Evolu<S>["ensureSchema"] = (tables) => {
|
|
438
|
-
dbWorker.postMessage({ _tag: "ensureSchema", tables });
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
const getSubscribedQueries = (): ReadonlyArray<Query> =>
|
|
442
|
-
Array.from(subscribedQueries.keys());
|
|
443
|
-
|
|
444
|
-
dbWorker.onMessage = (output): void => {
|
|
445
|
-
// console.log(output);
|
|
446
|
-
switch (output._tag) {
|
|
447
|
-
case "onError":
|
|
448
|
-
if (process.env.NODE_ENV === "development")
|
|
449
|
-
// JSON.stringify, because Expo console needs strings.
|
|
450
|
-
// eslint-disable-next-line no-console
|
|
451
|
-
console.warn(JSON.stringify(output.error, null, 2));
|
|
452
|
-
errorStore.setState(output.error);
|
|
453
|
-
break;
|
|
454
|
-
case "onOwner":
|
|
455
|
-
ownerStore.setState(output.owner);
|
|
456
|
-
break;
|
|
457
|
-
case "onQuery":
|
|
458
|
-
queryStore.onQuery(output);
|
|
459
|
-
break;
|
|
460
|
-
case "onReceive": {
|
|
461
|
-
const queries = getSubscribedQueries();
|
|
462
|
-
if (ReadonlyArray.isNonEmptyReadonlyArray(queries))
|
|
463
|
-
dbWorker.postMessage({ _tag: "query", queries });
|
|
464
|
-
break;
|
|
465
|
-
}
|
|
466
|
-
case "onResetOrRestore":
|
|
467
|
-
Effect.runSync(appState.reset);
|
|
468
|
-
break;
|
|
469
|
-
case "onSyncState":
|
|
470
|
-
syncStateStore.setState(output.state);
|
|
471
|
-
break;
|
|
472
|
-
default:
|
|
473
|
-
absurd(output);
|
|
474
|
-
}
|
|
475
|
-
};
|
|
723
|
+
return Evolu.of({
|
|
724
|
+
subscribeError: errorStore.subscribe,
|
|
725
|
+
getError: errorStore.getState,
|
|
476
726
|
|
|
477
|
-
|
|
727
|
+
createQuery: makeCreateQuery(),
|
|
728
|
+
loadQuery,
|
|
478
729
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
dbWorker.postMessage({ _tag: "sync", queries: getSubscribedQueries() });
|
|
482
|
-
});
|
|
730
|
+
loadQueries: <R extends Row, Q extends Queries<R>>(queries: [...Q]) =>
|
|
731
|
+
queries.map(loadQuery) as [...QueryResultsPromisesFromQueries<Q>],
|
|
483
732
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
)
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
config?: Partial<Config>,
|
|
520
|
-
): Evolu<S> =>
|
|
521
|
-
Effect.provide(
|
|
522
|
-
Evolu<S>(),
|
|
523
|
-
Layer.use(
|
|
524
|
-
EvoluLive<S>(tables),
|
|
525
|
-
Layer.mergeAll(PlatformLayer, ConfigLive(config), TimeLive),
|
|
526
|
-
),
|
|
527
|
-
).pipe(Effect.runSync);
|
|
733
|
+
subscribeQuery,
|
|
734
|
+
getQuery,
|
|
735
|
+
|
|
736
|
+
subscribeOwner: ownerStore.subscribe,
|
|
737
|
+
getOwner: ownerStore.getState,
|
|
738
|
+
|
|
739
|
+
subscribeSyncState: syncStateStore.subscribe,
|
|
740
|
+
getSyncState: syncStateStore.getState,
|
|
741
|
+
|
|
742
|
+
create: mutate as Mutate<Schema, "create">,
|
|
743
|
+
update: mutate,
|
|
744
|
+
|
|
745
|
+
resetOwner: () => dbWorker.postMessage({ _tag: "reset" }),
|
|
746
|
+
|
|
747
|
+
restoreOwner: (mnemonic) =>
|
|
748
|
+
dbWorker.postMessage({ _tag: "reset", mnemonic }),
|
|
749
|
+
|
|
750
|
+
ensureSchema: (schema) =>
|
|
751
|
+
dbWorker.postMessage({
|
|
752
|
+
_tag: "ensureSchema",
|
|
753
|
+
tables: schemaToTables(schema),
|
|
754
|
+
}),
|
|
755
|
+
});
|
|
756
|
+
}),
|
|
757
|
+
).pipe(
|
|
758
|
+
Layer.use(Layer.mergeAll(LoadQueryLive, OnQueryLive, MutateLive)),
|
|
759
|
+
Layer.use(LoadingPromiseLive),
|
|
760
|
+
Layer.use(SubscribedQueriesLive),
|
|
761
|
+
Layer.use(Layer.merge(RowsStoreLive, OnCompletesLive)),
|
|
762
|
+
);
|
|
763
|
+
|
|
764
|
+
/** EvoluCommonLive has only platform independent side-effects. */
|
|
765
|
+
export const EvoluCommonLive = EvoluCommon.pipe(
|
|
766
|
+
Layer.use(Layer.merge(TimeLive, NanoIdLive)),
|
|
767
|
+
);
|