@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/Evolu.ts
CHANGED
|
@@ -1,47 +1,71 @@
|
|
|
1
|
+
import * as AST from "@effect/schema/AST";
|
|
1
2
|
import * as S from "@effect/schema/Schema";
|
|
3
|
+
import { make } from "@effect/schema/Schema";
|
|
4
|
+
import * as Arr from "effect/Array";
|
|
2
5
|
import * as Context from "effect/Context";
|
|
3
6
|
import * as Effect from "effect/Effect";
|
|
4
|
-
import
|
|
5
|
-
import { pipe } from "effect/Function";
|
|
6
|
-
import * as GlobalValue from "effect/GlobalValue";
|
|
7
|
+
import { constVoid, flow, pipe } from "effect/Function";
|
|
7
8
|
import * as Layer from "effect/Layer";
|
|
8
|
-
import * as
|
|
9
|
+
import * as ManagedRuntime from "effect/ManagedRuntime";
|
|
9
10
|
import * as Number from "effect/Number";
|
|
10
|
-
import * as
|
|
11
|
+
import * as Option from "effect/Option";
|
|
12
|
+
import * as Predicate from "effect/Predicate";
|
|
13
|
+
import * as Record from "effect/Record";
|
|
11
14
|
import * as Kysely from "kysely";
|
|
12
|
-
import { Config,
|
|
13
|
-
import { Mnemonic, NanoIdGenerator
|
|
15
|
+
import { Config, createRuntime, defaultConfig } from "./Config.js";
|
|
16
|
+
import { Mnemonic, NanoIdGenerator } from "./Crypto.js";
|
|
14
17
|
import {
|
|
15
|
-
|
|
18
|
+
DbFactory,
|
|
19
|
+
DbSchema,
|
|
20
|
+
Index,
|
|
21
|
+
Mutation,
|
|
16
22
|
Queries,
|
|
17
23
|
Query,
|
|
18
24
|
QueryResult,
|
|
19
25
|
QueryResultsPromisesFromQueries,
|
|
26
|
+
QueryRowsMap,
|
|
20
27
|
Row,
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
Table,
|
|
29
|
+
deserializeQuery,
|
|
23
30
|
emptyRows,
|
|
24
31
|
queryResultFromRows,
|
|
25
|
-
schemaToTables,
|
|
26
32
|
serializeQuery,
|
|
27
33
|
} from "./Db.js";
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import { SqliteBoolean, SqliteDate } from "./Model.js";
|
|
32
|
-
import { OnCompletes, OnCompletesLive } from "./OnCompletes.js";
|
|
34
|
+
import { QueryPatches, applyPatches } from "./Diff.js";
|
|
35
|
+
import { EvoluError, makeUnexpectedError } from "./Error.js";
|
|
36
|
+
import { Id, SqliteBoolean, SqliteDate } from "./Model.js";
|
|
33
37
|
import { Owner } from "./Owner.js";
|
|
34
38
|
import { AppState, FlushSync } from "./Platform.js";
|
|
35
39
|
import {
|
|
36
|
-
Index,
|
|
37
40
|
SqliteQuery,
|
|
38
41
|
SqliteQueryOptions,
|
|
42
|
+
Value,
|
|
39
43
|
isSqlMutation,
|
|
40
44
|
} from "./Sqlite.js";
|
|
41
|
-
import {
|
|
42
|
-
import { SyncState } from "./
|
|
45
|
+
import { Listener, Unsubscribe, makeStore } from "./Store.js";
|
|
46
|
+
import { SyncState, initialSyncState } from "./Sync.js";
|
|
43
47
|
|
|
44
|
-
|
|
48
|
+
/**
|
|
49
|
+
* The Evolu interface provides a type-safe SQL query building and state
|
|
50
|
+
* management defined by a database schema. It leverages Kysely for creating SQL
|
|
51
|
+
* queries in TypeScript, enabling operations such as data querying, loading,
|
|
52
|
+
* subscription to data changes, and mutations (create, update, createOrUpdate).
|
|
53
|
+
* It also includes functionalities for error handling, syncing state
|
|
54
|
+
* management, and owner data manipulation. Specifically, Evolu allows:
|
|
55
|
+
*
|
|
56
|
+
* - Subscribing to and getting errors via subscribeError and getError.
|
|
57
|
+
* - Creating type-safe SQL queries with createQuery, leveraging Kysely's
|
|
58
|
+
* capabilities.
|
|
59
|
+
* - Loading queries and subscribing to query result changes using loadQuery,
|
|
60
|
+
* loadQueries, subscribeQuery, and getQuery.
|
|
61
|
+
* - Subscribing to and getting the owner's information and sync state changes.
|
|
62
|
+
* - Performing mutations on the database with create, update, and createOrUpdate
|
|
63
|
+
* methods, which include automatic management of common columns like
|
|
64
|
+
* createdAt, updatedAt, and isDeleted.
|
|
65
|
+
* - Managing owner data with resetOwner and restoreOwner.
|
|
66
|
+
* - Ensuring the database schema's integrity with ensureSchema.
|
|
67
|
+
*/
|
|
68
|
+
export interface Evolu<T extends EvoluSchema = EvoluSchema> {
|
|
45
69
|
/**
|
|
46
70
|
* Subscribe to {@link EvoluError} changes.
|
|
47
71
|
*
|
|
@@ -51,10 +75,10 @@ export interface Evolu<S extends DatabaseSchema = DatabaseSchema> {
|
|
|
51
75
|
* console.log(error);
|
|
52
76
|
* });
|
|
53
77
|
*/
|
|
54
|
-
readonly subscribeError:
|
|
78
|
+
readonly subscribeError: (listener: Listener) => Unsubscribe;
|
|
55
79
|
|
|
56
80
|
/** Get {@link EvoluError}. */
|
|
57
|
-
readonly getError:
|
|
81
|
+
readonly getError: () => EvoluError | null;
|
|
58
82
|
|
|
59
83
|
/**
|
|
60
84
|
* Create type-safe SQL {@link Query}.
|
|
@@ -74,7 +98,19 @@ export interface Evolu<S extends DatabaseSchema = DatabaseSchema> {
|
|
|
74
98
|
* db.selectFrom("todo").selectAll().where("id", "=", id),
|
|
75
99
|
* );
|
|
76
100
|
*/
|
|
77
|
-
readonly createQuery:
|
|
101
|
+
readonly createQuery: <R extends Row>(
|
|
102
|
+
queryCallback: (
|
|
103
|
+
db: Pick<
|
|
104
|
+
Kysely.Kysely<{
|
|
105
|
+
[Table in keyof T]: NullableExceptIdCreatedAtUpdatedAt<{
|
|
106
|
+
[Column in keyof T[Table]]: T[Table][Column];
|
|
107
|
+
}>;
|
|
108
|
+
}>,
|
|
109
|
+
"selectFrom" | "fn" | "with" | "withRecursive"
|
|
110
|
+
>,
|
|
111
|
+
) => Kysely.SelectQueryBuilder<any, any, R>,
|
|
112
|
+
options?: SqliteQueryOptions,
|
|
113
|
+
) => Query<R>;
|
|
78
114
|
|
|
79
115
|
/**
|
|
80
116
|
* Load {@link Query} and return a promise with {@link QueryResult}.
|
|
@@ -125,7 +161,9 @@ export interface Evolu<S extends DatabaseSchema = DatabaseSchema> {
|
|
|
125
161
|
* console.log(rows);
|
|
126
162
|
* });
|
|
127
163
|
*/
|
|
128
|
-
readonly loadQuery:
|
|
164
|
+
readonly loadQuery: <R extends Row>(
|
|
165
|
+
query: Query<R>,
|
|
166
|
+
) => Promise<QueryResult<R>>;
|
|
129
167
|
|
|
130
168
|
/**
|
|
131
169
|
* Load an array of {@link Query} queries and return an array of
|
|
@@ -147,7 +185,9 @@ export interface Evolu<S extends DatabaseSchema = DatabaseSchema> {
|
|
|
147
185
|
* const { rows } = evolu.getQuery(allTodos);
|
|
148
186
|
* });
|
|
149
187
|
*/
|
|
150
|
-
readonly subscribeQuery:
|
|
188
|
+
readonly subscribeQuery: (
|
|
189
|
+
query: Query,
|
|
190
|
+
) => (listener: Listener) => Unsubscribe;
|
|
151
191
|
|
|
152
192
|
/**
|
|
153
193
|
* Get {@link Query} {@link QueryResult}.
|
|
@@ -157,7 +197,7 @@ export interface Evolu<S extends DatabaseSchema = DatabaseSchema> {
|
|
|
157
197
|
* const { rows } = evolu.getQuery(allTodos);
|
|
158
198
|
* });
|
|
159
199
|
*/
|
|
160
|
-
readonly getQuery:
|
|
200
|
+
readonly getQuery: <R extends Row>(query: Query<R>) => QueryResult<R>;
|
|
161
201
|
|
|
162
202
|
/**
|
|
163
203
|
* Subscribe to {@link Owner} changes.
|
|
@@ -167,7 +207,7 @@ export interface Evolu<S extends DatabaseSchema = DatabaseSchema> {
|
|
|
167
207
|
* const owner = evolu.getOwner();
|
|
168
208
|
* });
|
|
169
209
|
*/
|
|
170
|
-
readonly subscribeOwner:
|
|
210
|
+
readonly subscribeOwner: (listener: Listener) => Unsubscribe;
|
|
171
211
|
|
|
172
212
|
/**
|
|
173
213
|
* Get {@link Owner}.
|
|
@@ -177,7 +217,7 @@ export interface Evolu<S extends DatabaseSchema = DatabaseSchema> {
|
|
|
177
217
|
* const owner = evolu.getOwner();
|
|
178
218
|
* });
|
|
179
219
|
*/
|
|
180
|
-
readonly getOwner:
|
|
220
|
+
readonly getOwner: () => Owner | null;
|
|
181
221
|
|
|
182
222
|
/**
|
|
183
223
|
* Subscribe to {@link SyncState} changes.
|
|
@@ -187,7 +227,7 @@ export interface Evolu<S extends DatabaseSchema = DatabaseSchema> {
|
|
|
187
227
|
* const syncState = evolu.getSyncState();
|
|
188
228
|
* });
|
|
189
229
|
*/
|
|
190
|
-
readonly subscribeSyncState:
|
|
230
|
+
readonly subscribeSyncState: (listener: Listener) => Unsubscribe;
|
|
191
231
|
|
|
192
232
|
/**
|
|
193
233
|
* Get {@link SyncState}.
|
|
@@ -197,7 +237,7 @@ export interface Evolu<S extends DatabaseSchema = DatabaseSchema> {
|
|
|
197
237
|
* const syncState = evolu.getSyncState();
|
|
198
238
|
* });
|
|
199
239
|
*/
|
|
200
|
-
readonly getSyncState:
|
|
240
|
+
readonly getSyncState: () => SyncState;
|
|
201
241
|
|
|
202
242
|
/**
|
|
203
243
|
* Create a row in the database and returns a new ID. The first argument is
|
|
@@ -225,7 +265,7 @@ export interface Evolu<S extends DatabaseSchema = DatabaseSchema> {
|
|
|
225
265
|
* // onComplete callback
|
|
226
266
|
* });
|
|
227
267
|
*/
|
|
228
|
-
create:
|
|
268
|
+
create: Mutate<T, "create">;
|
|
229
269
|
|
|
230
270
|
/**
|
|
231
271
|
* Update a row in the database and return the existing ID. The first argument
|
|
@@ -253,7 +293,7 @@ export interface Evolu<S extends DatabaseSchema = DatabaseSchema> {
|
|
|
253
293
|
* // To delete a row, set `isDeleted` to true.
|
|
254
294
|
* evolu.update("todo", { id, isDeleted: true });
|
|
255
295
|
*/
|
|
256
|
-
update:
|
|
296
|
+
update: Mutate<T, "update">;
|
|
257
297
|
|
|
258
298
|
/**
|
|
259
299
|
* Create or update a row in the database and return the existing ID. The
|
|
@@ -284,7 +324,7 @@ export interface Evolu<S extends DatabaseSchema = DatabaseSchema> {
|
|
|
284
324
|
*
|
|
285
325
|
* evolu.createOrUpdate("todo", { id, title });
|
|
286
326
|
*/
|
|
287
|
-
createOrUpdate:
|
|
327
|
+
createOrUpdate: Mutate<T, "createOrUpdate">;
|
|
288
328
|
|
|
289
329
|
/**
|
|
290
330
|
* Delete {@link Owner} and all their data from the current device. After the
|
|
@@ -297,345 +337,55 @@ export interface Evolu<S extends DatabaseSchema = DatabaseSchema> {
|
|
|
297
337
|
readonly restoreOwner: (mnemonic: Mnemonic) => void;
|
|
298
338
|
|
|
299
339
|
/**
|
|
300
|
-
* Ensure tables and columns defined in {@link
|
|
340
|
+
* Ensure tables and columns defined in {@link EvoluSchema} exist in the
|
|
301
341
|
* database.
|
|
302
|
-
*/
|
|
303
|
-
readonly ensureSchema: <From, To extends S>(
|
|
304
|
-
schema: S.Schema<To, From>,
|
|
305
|
-
indexes?: ReadonlyArray<Index>,
|
|
306
|
-
) => void;
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Force sync with Evolu Server.
|
|
310
342
|
*
|
|
311
|
-
*
|
|
312
|
-
* generally not required to sync manually, but if you need it, you can do
|
|
313
|
-
* it.
|
|
343
|
+
* This function is for hot/live reloading.
|
|
314
344
|
*/
|
|
315
|
-
readonly
|
|
316
|
-
}
|
|
345
|
+
readonly ensureSchema: (schema: DbSchema) => void;
|
|
317
346
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
queryCallback: QueryCallback<S, R>,
|
|
322
|
-
options?: SqliteQueryOptions,
|
|
323
|
-
) => Query<R>;
|
|
324
|
-
|
|
325
|
-
type QueryCallback<S extends DatabaseSchema, R extends Row> = (
|
|
326
|
-
db: Pick<
|
|
327
|
-
Kysely.Kysely<QuerySchema<S>>,
|
|
328
|
-
"selectFrom" | "fn" | "with" | "withRecursive"
|
|
329
|
-
>,
|
|
330
|
-
) => Kysely.SelectQueryBuilder<any, any, R>;
|
|
347
|
+
// TODO:
|
|
348
|
+
// readonly exportSqliteFile: () => Promise<Uint8Array>
|
|
349
|
+
}
|
|
331
350
|
|
|
332
|
-
type
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
351
|
+
/** A type to define tables, columns, and column types. */
|
|
352
|
+
export type EvoluSchema = Record.ReadonlyRecord<
|
|
353
|
+
string,
|
|
354
|
+
Record.ReadonlyRecord<string, Value> & {
|
|
355
|
+
readonly id: Id;
|
|
356
|
+
}
|
|
357
|
+
>;
|
|
337
358
|
|
|
338
|
-
type
|
|
359
|
+
type NullableExceptIdCreatedAtUpdatedAt<T> = {
|
|
339
360
|
readonly [K in keyof T]: K extends "id" | "createdAt" | "updatedAt"
|
|
340
361
|
? T[K]
|
|
341
362
|
: T[K] | null;
|
|
342
363
|
};
|
|
343
364
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
dialect: {
|
|
347
|
-
createAdapter: (): Kysely.DialectAdapter => new Kysely.SqliteAdapter(),
|
|
348
|
-
createDriver: (): Kysely.Driver => new Kysely.DummyDriver(),
|
|
349
|
-
createIntrospector(): Kysely.DatabaseIntrospector {
|
|
350
|
-
throw "Not implemeneted";
|
|
351
|
-
},
|
|
352
|
-
createQueryCompiler: (): Kysely.QueryCompiler =>
|
|
353
|
-
new Kysely.SqliteQueryCompiler(),
|
|
354
|
-
},
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
export const createIndex = kysely.schema.createIndex.bind(kysely.schema);
|
|
358
|
-
|
|
359
|
-
export const makeCreateQuery =
|
|
360
|
-
<S extends DatabaseSchema = DatabaseSchema>(): CreateQuery<S> =>
|
|
361
|
-
<R extends Row>(
|
|
362
|
-
queryCallback: QueryCallback<S, R>,
|
|
363
|
-
options?: SqliteQueryOptions,
|
|
364
|
-
) =>
|
|
365
|
-
pipe(
|
|
366
|
-
queryCallback(kysely as Kysely.Kysely<QuerySchema<S>>).compile(),
|
|
367
|
-
(compiledQuery): SqliteQuery => {
|
|
368
|
-
if (isSqlMutation(compiledQuery.sql))
|
|
369
|
-
throw new Error(
|
|
370
|
-
"SQL mutation (INSERT, UPDATE, DELETE, etc.) isn't allowed in the Evolu `createQuery` function. Kysely suggests it because there is no read-only Kysely yet, and removing such an API is not possible. For mutations, use Evolu mutation API.",
|
|
371
|
-
);
|
|
372
|
-
const parameters = compiledQuery.parameters as NonNullable<
|
|
373
|
-
SqliteQuery["parameters"]
|
|
374
|
-
>;
|
|
375
|
-
return {
|
|
376
|
-
sql: compiledQuery.sql,
|
|
377
|
-
parameters,
|
|
378
|
-
...(options && { options }),
|
|
379
|
-
};
|
|
380
|
-
},
|
|
381
|
-
(query) => serializeQuery<R>(query),
|
|
382
|
-
);
|
|
383
|
-
|
|
384
|
-
export interface LoadingPromises {
|
|
385
|
-
readonly get: <R extends Row>(
|
|
386
|
-
query: Query<R>,
|
|
387
|
-
) => {
|
|
388
|
-
readonly promise: LoadingPromise<R>;
|
|
389
|
-
readonly isNew: boolean;
|
|
390
|
-
};
|
|
391
|
-
|
|
392
|
-
readonly resolve: <R extends Row>(
|
|
393
|
-
query: Query<R>,
|
|
394
|
-
rows: ReadonlyArray<R>,
|
|
395
|
-
) => void;
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* Release all unsubscribed queries on mutation because only subscribed
|
|
399
|
-
* queries are automatically updated.
|
|
400
|
-
*/
|
|
401
|
-
readonly release: () => void;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
export const LoadingPromises = Context.GenericTag<LoadingPromises>(
|
|
405
|
-
"@services/LoadingPromises",
|
|
406
|
-
);
|
|
407
|
-
|
|
408
|
-
export type LoadingPromise<R extends Row> = Promise<QueryResult<R>> & {
|
|
409
|
-
status?: "pending" | "fulfilled" | "rejected";
|
|
410
|
-
value?: QueryResult<R>;
|
|
411
|
-
reason?: unknown;
|
|
412
|
-
};
|
|
413
|
-
|
|
414
|
-
export const LoadingPromiseLive = Layer.effect(
|
|
415
|
-
LoadingPromises,
|
|
416
|
-
Effect.gen(function* (_) {
|
|
417
|
-
interface LoadingPromiseWithResolve<R extends Row> {
|
|
418
|
-
promise: LoadingPromise<R>;
|
|
419
|
-
readonly resolve: Resolve<R>;
|
|
420
|
-
releaseOnResolve: boolean;
|
|
421
|
-
}
|
|
422
|
-
type Resolve<R extends Row> = (rows: QueryResult<R>) => void;
|
|
423
|
-
|
|
424
|
-
const subscribedQueries = yield* _(SubscribedQueries);
|
|
425
|
-
const promises = new Map<Query, LoadingPromiseWithResolve<Row>>();
|
|
426
|
-
|
|
427
|
-
return LoadingPromises.of({
|
|
428
|
-
get<R extends Row>(query: Query<R>) {
|
|
429
|
-
let isNew = false;
|
|
430
|
-
let promiseWithResolve = promises.get(query);
|
|
431
|
-
|
|
432
|
-
if (!promiseWithResolve) {
|
|
433
|
-
isNew = true;
|
|
434
|
-
let resolve: Resolve<Row> = Function.constVoid;
|
|
435
|
-
const promise: LoadingPromise<Row> = new Promise((_resolve) => {
|
|
436
|
-
resolve = _resolve;
|
|
437
|
-
});
|
|
438
|
-
promiseWithResolve = { promise, resolve, releaseOnResolve: false };
|
|
439
|
-
promises.set(query, promiseWithResolve);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
return {
|
|
443
|
-
promise: promiseWithResolve.promise as LoadingPromise<R>,
|
|
444
|
-
isNew,
|
|
445
|
-
};
|
|
446
|
-
},
|
|
447
|
-
|
|
448
|
-
resolve(query, rows) {
|
|
449
|
-
const promiseWithResolve = promises.get(query);
|
|
450
|
-
if (!promiseWithResolve) return;
|
|
451
|
-
const result = queryResultFromRows(rows);
|
|
452
|
-
if (promiseWithResolve.promise.status !== "fulfilled")
|
|
453
|
-
promiseWithResolve.resolve(result);
|
|
454
|
-
else promiseWithResolve.promise = Promise.resolve(result);
|
|
455
|
-
setPromiseAsResolved(promiseWithResolve.promise)(result);
|
|
456
|
-
if (promiseWithResolve.releaseOnResolve) promises.delete(query);
|
|
457
|
-
},
|
|
458
|
-
|
|
459
|
-
release() {
|
|
460
|
-
const keep = subscribedQueries.getSubscribedQueries();
|
|
461
|
-
promises.forEach((promiseWithResolve, query) => {
|
|
462
|
-
if (keep.includes(query)) return;
|
|
463
|
-
if (promiseWithResolve.promise.status === "fulfilled")
|
|
464
|
-
promises.delete(query);
|
|
465
|
-
else promiseWithResolve.releaseOnResolve = true;
|
|
466
|
-
});
|
|
467
|
-
},
|
|
468
|
-
});
|
|
469
|
-
}),
|
|
470
|
-
);
|
|
471
|
-
|
|
472
|
-
// "For example, a data framework can set the status and value fields on a promise
|
|
473
|
-
// preemptively, before passing to React, so that React can unwrap it without waiting
|
|
474
|
-
// a microtask."
|
|
475
|
-
// https://github.com/acdlite/rfcs/blob/first-class-promises/text/0000-first-class-support-for-promises.md
|
|
476
|
-
const setPromiseAsResolved =
|
|
477
|
-
<T>(promise: Promise<T>) =>
|
|
478
|
-
(value: unknown): void => {
|
|
479
|
-
Object.assign(promise, { status: "fulfilled", value });
|
|
480
|
-
};
|
|
481
|
-
|
|
482
|
-
type LoadQuery = <R extends Row>(query: Query<R>) => Promise<QueryResult<R>>;
|
|
483
|
-
|
|
484
|
-
const LoadQuery = Context.GenericTag<LoadQuery>("@services/LoadQuery");
|
|
485
|
-
|
|
486
|
-
const LoadQueryLive = Layer.effect(
|
|
487
|
-
LoadQuery,
|
|
488
|
-
Effect.gen(function* (_) {
|
|
489
|
-
const loadingPromises = yield* _(LoadingPromises);
|
|
490
|
-
const dbWorker = yield* _(DbWorker);
|
|
491
|
-
let queries: ReadonlyArray<Query> = [];
|
|
492
|
-
|
|
493
|
-
return LoadQuery.of((query) => {
|
|
494
|
-
const { promise, isNew } = loadingPromises.get(query);
|
|
495
|
-
if (isNew) queries = [...queries, query];
|
|
496
|
-
if (queries.length === 1) {
|
|
497
|
-
queueMicrotask(() => {
|
|
498
|
-
if (ReadonlyArray.isNonEmptyReadonlyArray(queries))
|
|
499
|
-
dbWorker.postMessage({ _tag: "query", queries });
|
|
500
|
-
queries = [];
|
|
501
|
-
});
|
|
502
|
-
}
|
|
503
|
-
return promise;
|
|
504
|
-
});
|
|
505
|
-
}),
|
|
506
|
-
);
|
|
507
|
-
|
|
508
|
-
type OnQuery = (
|
|
509
|
-
dbWorkerOutputOnQuery: DbWorkerOutputOnQuery,
|
|
510
|
-
) => Effect.Effect<void>;
|
|
511
|
-
|
|
512
|
-
const OnQuery = Context.GenericTag<OnQuery>("@services/OnQuery");
|
|
513
|
-
|
|
514
|
-
const OnQueryLive = Layer.effect(
|
|
515
|
-
OnQuery,
|
|
516
|
-
Effect.gen(function* (_) {
|
|
517
|
-
const rowsStore = yield* _(RowsStore);
|
|
518
|
-
const loadingPromises = yield* _(LoadingPromises);
|
|
519
|
-
const flushSync = yield* _(FlushSync);
|
|
520
|
-
const onCompletes = yield* _(OnCompletes);
|
|
521
|
-
|
|
522
|
-
return OnQuery.of(({ queriesPatches, onCompleteIds }) =>
|
|
523
|
-
Effect.gen(function* (_) {
|
|
524
|
-
const currentState = rowsStore.getState();
|
|
525
|
-
const nextState = pipe(
|
|
526
|
-
queriesPatches,
|
|
527
|
-
ReadonlyArray.map(
|
|
528
|
-
({ query, patches }) =>
|
|
529
|
-
[
|
|
530
|
-
query,
|
|
531
|
-
applyPatches(patches)(currentState.get(query) || emptyRows()),
|
|
532
|
-
] as const,
|
|
533
|
-
),
|
|
534
|
-
(map) => new Map([...currentState, ...map]),
|
|
535
|
-
);
|
|
536
|
-
|
|
537
|
-
queriesPatches.forEach(({ query }) => {
|
|
538
|
-
loadingPromises.resolve(query, nextState.get(query) || emptyRows());
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
// No mutation is using onComplete, so we don't need flushSync.
|
|
542
|
-
if (onCompleteIds.length === 0) {
|
|
543
|
-
yield* _(rowsStore.setState(nextState));
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
flushSync(() => rowsStore.setState(nextState).pipe(Effect.runSync));
|
|
548
|
-
yield* _(onCompletes.complete(onCompleteIds));
|
|
549
|
-
}),
|
|
550
|
-
);
|
|
551
|
-
}),
|
|
552
|
-
);
|
|
553
|
-
|
|
554
|
-
export interface SubscribedQueries {
|
|
555
|
-
readonly subscribeQuery: (query: Query) => Store<Row>["subscribe"];
|
|
556
|
-
readonly getQuery: <R extends Row>(query: Query<R>) => QueryResult<R>;
|
|
557
|
-
readonly getSubscribedQueries: () => Queries;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
export const SubscribedQueries = Context.GenericTag<SubscribedQueries>(
|
|
561
|
-
"@services/SubscribedQueries",
|
|
562
|
-
);
|
|
563
|
-
|
|
564
|
-
export const SubscribedQueriesLive = Layer.effect(
|
|
565
|
-
SubscribedQueries,
|
|
566
|
-
Effect.gen(function* (_) {
|
|
567
|
-
const rowsStore = yield* _(RowsStore);
|
|
568
|
-
const subscribedQueries = new Map<Query, number>();
|
|
569
|
-
|
|
570
|
-
return SubscribedQueries.of({
|
|
571
|
-
subscribeQuery:
|
|
572
|
-
(query) =>
|
|
573
|
-
(listener): Unsubscribe => {
|
|
574
|
-
subscribedQueries.set(
|
|
575
|
-
query,
|
|
576
|
-
Number.increment(subscribedQueries.get(query) ?? 0),
|
|
577
|
-
);
|
|
578
|
-
const unsubscribe = rowsStore.subscribe(listener);
|
|
579
|
-
|
|
580
|
-
return () => {
|
|
581
|
-
const count = subscribedQueries.get(query);
|
|
582
|
-
if (count != null && count > 1)
|
|
583
|
-
subscribedQueries.set(query, Number.decrement(count));
|
|
584
|
-
else subscribedQueries.delete(query);
|
|
585
|
-
unsubscribe();
|
|
586
|
-
};
|
|
587
|
-
},
|
|
588
|
-
|
|
589
|
-
getQuery: <R extends Row>(query: Query<R>): QueryResult<R> =>
|
|
590
|
-
queryResultFromRows(
|
|
591
|
-
rowsStore.getState().get(query) || emptyRows(),
|
|
592
|
-
) as QueryResult<R>,
|
|
593
|
-
|
|
594
|
-
getSubscribedQueries: () =>
|
|
595
|
-
ReadonlyArray.fromIterable(subscribedQueries.keys()),
|
|
596
|
-
});
|
|
597
|
-
}),
|
|
598
|
-
);
|
|
599
|
-
|
|
600
|
-
export type Create<S extends DatabaseSchema = DatabaseSchema> = Mutate<
|
|
601
|
-
S,
|
|
602
|
-
"create"
|
|
603
|
-
>;
|
|
604
|
-
|
|
605
|
-
export type Update<S extends DatabaseSchema = DatabaseSchema> = Mutate<
|
|
606
|
-
S,
|
|
607
|
-
"update"
|
|
608
|
-
>;
|
|
609
|
-
|
|
610
|
-
export type CreateOrUpdate<S extends DatabaseSchema = DatabaseSchema> = Mutate<
|
|
611
|
-
S,
|
|
612
|
-
"createOrUpdate"
|
|
613
|
-
>;
|
|
614
|
-
|
|
615
|
-
export type Mutate<
|
|
616
|
-
S extends DatabaseSchema = DatabaseSchema,
|
|
365
|
+
type Mutate<
|
|
366
|
+
T extends EvoluSchema = EvoluSchema,
|
|
617
367
|
Mode extends "create" | "update" | "createOrUpdate" = "update",
|
|
618
|
-
> = <K extends keyof
|
|
368
|
+
> = <K extends keyof T>(
|
|
619
369
|
table: K,
|
|
620
370
|
values: Kysely.Simplify<
|
|
621
371
|
Mode extends "create"
|
|
622
372
|
? PartialForNullable<
|
|
623
|
-
Castable<Omit<
|
|
373
|
+
Castable<Omit<T[K], "id" | "createdAt" | "updatedAt" | "isDeleted">>
|
|
624
374
|
>
|
|
625
375
|
: Mode extends "update"
|
|
626
|
-
? Partial<Castable<Omit<
|
|
627
|
-
readonly id:
|
|
376
|
+
? Partial<Castable<Omit<T[K], "id" | "createdAt" | "updatedAt">>> & {
|
|
377
|
+
readonly id: T[K]["id"];
|
|
628
378
|
}
|
|
629
379
|
: PartialForNullable<
|
|
630
|
-
Castable<Omit<
|
|
380
|
+
Castable<Omit<T[K], "createdAt" | "updatedAt" | "isDeleted">>
|
|
631
381
|
>
|
|
632
382
|
>,
|
|
633
|
-
onComplete?:
|
|
383
|
+
onComplete?: MutateOnComplete,
|
|
634
384
|
) => {
|
|
635
|
-
readonly id:
|
|
385
|
+
readonly id: T[K]["id"];
|
|
636
386
|
};
|
|
637
387
|
|
|
638
|
-
|
|
388
|
+
type MutateOnComplete = () => void;
|
|
639
389
|
|
|
640
390
|
// https://stackoverflow.com/a/54713648/233902
|
|
641
391
|
type PartialForNullable<
|
|
@@ -649,9 +399,6 @@ type PartialForNullable<
|
|
|
649
399
|
/**
|
|
650
400
|
* SQLite doesn't support Date nor Boolean types, so Evolu emulates them with
|
|
651
401
|
* {@link SqliteBoolean} and {@link SqliteDate}.
|
|
652
|
-
*
|
|
653
|
-
* For {@link SqliteBoolean}, you can use JavaScript boolean. For
|
|
654
|
-
* {@link SqliteDate}, you can use JavaScript Date.
|
|
655
402
|
*/
|
|
656
403
|
type Castable<T> = {
|
|
657
404
|
readonly [K in keyof T]: T[K] extends SqliteBoolean
|
|
@@ -665,113 +412,430 @@ type Castable<T> = {
|
|
|
665
412
|
: T[K];
|
|
666
413
|
};
|
|
667
414
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
415
|
+
export class EvoluFactory extends Context.Tag("EvoluFactory")<
|
|
416
|
+
EvoluFactory,
|
|
417
|
+
{
|
|
418
|
+
/**
|
|
419
|
+
* Create Evolu from the database schema.
|
|
420
|
+
*
|
|
421
|
+
* Tables with a name prefixed with `_` are local-only, which means they are
|
|
422
|
+
* never synced. It's useful for device-specific or temporal data.
|
|
423
|
+
*
|
|
424
|
+
* @example
|
|
425
|
+
* import * as S from "@effect/schema/Schema";
|
|
426
|
+
* import * as E from "@evolu/react";
|
|
427
|
+
* // The same API for different platforms
|
|
428
|
+
* // import * as E from "@evolu/react-native";
|
|
429
|
+
* // import * as E from "@evolu/common-web";
|
|
430
|
+
*
|
|
431
|
+
* const TodoId = E.id("Todo");
|
|
432
|
+
* type TodoId = S.Schema.Type<typeof TodoId>;
|
|
433
|
+
*
|
|
434
|
+
* const TodoTable = E.table({
|
|
435
|
+
* id: TodoId,
|
|
436
|
+
* title: E.NonEmptyString1000,
|
|
437
|
+
* });
|
|
438
|
+
* type TodoTable = S.Schema.Type<typeof TodoTable>;
|
|
439
|
+
*
|
|
440
|
+
* const Database = E.database({
|
|
441
|
+
* todo: TodoTable,
|
|
442
|
+
*
|
|
443
|
+
* // Prefix `_` makes the table local-only (it will not sync)
|
|
444
|
+
* _todo: TodoTable,
|
|
445
|
+
* });
|
|
446
|
+
* type Database = S.Schema.Type<typeof Database>;
|
|
447
|
+
*
|
|
448
|
+
* const evolu = E.createEvolu(Database);
|
|
449
|
+
*/
|
|
450
|
+
readonly createEvolu: <T extends EvoluSchema, I>(
|
|
451
|
+
schema: S.Schema<T, I>,
|
|
452
|
+
config?: Partial<EvoluConfig<T>>,
|
|
453
|
+
) => Evolu<T>;
|
|
454
|
+
}
|
|
455
|
+
>() {
|
|
456
|
+
static Common = Layer.effect(
|
|
457
|
+
EvoluFactory,
|
|
458
|
+
Effect.gen(function* () {
|
|
459
|
+
const flushSync = yield* Effect.map(
|
|
460
|
+
Effect.serviceOption(FlushSync),
|
|
461
|
+
Option.getOrElse<FlushSync>(() => (callback) => callback()),
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
const context = Context.empty().pipe(
|
|
465
|
+
Context.add(DbFactory, yield* DbFactory),
|
|
466
|
+
Context.add(NanoIdGenerator, yield* NanoIdGenerator),
|
|
467
|
+
Context.add(FlushSync, flushSync),
|
|
468
|
+
Context.add(AppState, yield* AppState),
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
// For hot/live reloading and future Evolu dynamic import.
|
|
472
|
+
const instances = new Map<string, Evolu>();
|
|
473
|
+
|
|
474
|
+
return EvoluFactory.of({
|
|
475
|
+
createEvolu: <T extends EvoluSchema, I>(
|
|
476
|
+
schema: S.Schema<T, I>,
|
|
477
|
+
{ indexes, initialData, ...config }: Partial<EvoluConfig<T>> = {},
|
|
478
|
+
): Evolu<T> => {
|
|
479
|
+
const runtime = createRuntime(config);
|
|
480
|
+
const name = config?.name || defaultConfig.name;
|
|
481
|
+
const dbSchema: DbSchema = {
|
|
482
|
+
tables: schemaToTables(schema),
|
|
483
|
+
indexes: indexes || [],
|
|
484
|
+
};
|
|
485
|
+
let evolu = instances.get(name);
|
|
486
|
+
if (evolu == null) {
|
|
487
|
+
evolu = createEvolu(
|
|
488
|
+
dbSchema,
|
|
489
|
+
runtime,
|
|
490
|
+
initialData as EvoluConfig["initialData"],
|
|
491
|
+
).pipe(Effect.provide(context), runtime.runSync);
|
|
492
|
+
instances.set(name, evolu);
|
|
493
|
+
} else {
|
|
494
|
+
evolu.ensureSchema(dbSchema);
|
|
495
|
+
}
|
|
496
|
+
return evolu as Evolu<T>;
|
|
694
497
|
},
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
if (ReadonlyArray.isNonEmptyReadonlyArray(mutations))
|
|
700
|
-
dbWorker.postMessage({
|
|
701
|
-
_tag: "mutate",
|
|
702
|
-
mutations,
|
|
703
|
-
queries: subscribedQueries.getSubscribedQueries(),
|
|
704
|
-
});
|
|
705
|
-
loadingPromises.release();
|
|
706
|
-
mutations = [];
|
|
707
|
-
});
|
|
498
|
+
});
|
|
499
|
+
}),
|
|
500
|
+
);
|
|
501
|
+
}
|
|
708
502
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
503
|
+
export interface EvoluConfig<T extends EvoluSchema = EvoluSchema>
|
|
504
|
+
extends Config {
|
|
505
|
+
/**
|
|
506
|
+
* Use the `indexes` property to define SQLite indexes.
|
|
507
|
+
*
|
|
508
|
+
* Table and column names are not typed because Kysely doesn't support it.
|
|
509
|
+
*
|
|
510
|
+
* https://medium.com/@JasonWyatt/squeezing-performance-from-sqlite-indexes-indexes-c4e175f3c346
|
|
511
|
+
*
|
|
512
|
+
* @example
|
|
513
|
+
* const indexes = [
|
|
514
|
+
* createIndex("indexTodoCreatedAt").on("todo").column("createdAt"),
|
|
515
|
+
*
|
|
516
|
+
* createIndex("indexTodoCategoryCreatedAt")
|
|
517
|
+
* .on("todoCategory")
|
|
518
|
+
* .column("createdAt"),
|
|
519
|
+
* ];
|
|
520
|
+
*/
|
|
521
|
+
indexes: ReadonlyArray<Index>;
|
|
522
|
+
|
|
523
|
+
/** Use this option to create initial data (fixtures). */
|
|
524
|
+
initialData: (evolu: EvoluForInitialData<T>) => void;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const schemaToTables = (schema: S.Schema<any>) =>
|
|
528
|
+
pipe(
|
|
529
|
+
getPropertySignatures(schema),
|
|
530
|
+
Record.toEntries,
|
|
531
|
+
Arr.map(
|
|
532
|
+
([name, schema]): Table => ({
|
|
533
|
+
name,
|
|
534
|
+
columns: Object.keys(getPropertySignatures(schema)),
|
|
535
|
+
}),
|
|
536
|
+
),
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
// TODO: Simplify.
|
|
540
|
+
// https://discord.com/channels/795981131316985866/1218626687546294386/1218796529725476935
|
|
541
|
+
const getPropertySignatures = <I extends { [K in keyof A]: any }, A>(
|
|
542
|
+
schema: S.Schema<A, I>,
|
|
543
|
+
): { [K in keyof A]: S.Schema<A[K], I[K]> } => {
|
|
544
|
+
const out: Record<PropertyKey, S.Schema<any>> = {};
|
|
545
|
+
const propertySignatures = AST.getPropertySignatures(schema.ast);
|
|
546
|
+
for (let i = 0; i < propertySignatures.length; i++) {
|
|
547
|
+
const propertySignature = propertySignatures[i];
|
|
548
|
+
out[propertySignature.name] = make(propertySignature.type);
|
|
549
|
+
}
|
|
550
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
551
|
+
return out as any;
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const createEvolu = (
|
|
555
|
+
schema: DbSchema,
|
|
556
|
+
runtime: ManagedRuntime.ManagedRuntime<Config, never>,
|
|
557
|
+
initialData?: EvoluConfig["initialData"],
|
|
558
|
+
) =>
|
|
559
|
+
Effect.gen(function* () {
|
|
560
|
+
yield* Effect.logTrace("EvoluFactory createEvolu");
|
|
561
|
+
const config = yield* Config;
|
|
562
|
+
const dbFactory = yield* DbFactory;
|
|
563
|
+
const appState = yield* AppState;
|
|
564
|
+
const nanoIdGenerator = yield* NanoIdGenerator;
|
|
565
|
+
const flushSync = yield* FlushSync;
|
|
566
|
+
|
|
567
|
+
const db = yield* dbFactory.createDb;
|
|
568
|
+
const errorStore = yield* makeStore<EvoluError | null>(null);
|
|
569
|
+
const ownerStore = yield* makeStore<Owner | null>(null);
|
|
570
|
+
const rowsStore = yield* makeStore<QueryRowsMap>(new Map());
|
|
571
|
+
const syncStateStore = yield* makeStore<SyncState>(initialSyncState);
|
|
572
|
+
|
|
573
|
+
const loadingPromises = new Map<Query, LoadingPromise>();
|
|
574
|
+
const subscribedQueries = new Map<Query, number>();
|
|
575
|
+
|
|
576
|
+
const handleAllErrors = <T>(effect: Effect.Effect<T, EvoluError, Config>) =>
|
|
577
|
+
effect.pipe(
|
|
578
|
+
Effect.catchAllDefect((error) =>
|
|
579
|
+
Effect.fail(makeUnexpectedError(error)),
|
|
580
|
+
),
|
|
581
|
+
Effect.tapError(Effect.logError),
|
|
582
|
+
Effect.tapError(errorStore.setState),
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
const runFork = flow(handleAllErrors, runtime.runFork);
|
|
586
|
+
const runSync = flow(handleAllErrors, runtime.runSync);
|
|
587
|
+
// const runPromise = flow(handleAllErrors, runtime.runPromise);
|
|
588
|
+
|
|
589
|
+
const initialDataAsMutations = yield* Effect.provideService(
|
|
590
|
+
initialDataToMutations(initialData),
|
|
591
|
+
NanoIdGenerator,
|
|
592
|
+
nanoIdGenerator,
|
|
726
593
|
);
|
|
727
|
-
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
onQuery,
|
|
737
|
-
onOwner: ({ owner }) => ownerStore.setState(owner),
|
|
738
|
-
onSyncState: ({ state }) => syncStateStore.setState(state),
|
|
739
|
-
onReceive: () =>
|
|
740
|
-
Effect.sync(() => {
|
|
741
|
-
loadingPromises.release();
|
|
742
|
-
const queries = getSubscribedQueries();
|
|
743
|
-
if (ReadonlyArray.isNonEmptyReadonlyArray(queries))
|
|
744
|
-
dbWorker.postMessage({ _tag: "query", queries });
|
|
745
|
-
}),
|
|
746
|
-
onResetOrRestore: () => appState.reset,
|
|
747
|
-
}),
|
|
748
|
-
Effect.runSync,
|
|
594
|
+
|
|
595
|
+
const handleDbError = (error: EvoluError) => {
|
|
596
|
+
Effect.fail(error).pipe(runFork);
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
const handleSyncStateChange = (state: SyncState) => {
|
|
600
|
+
Effect.logDebug(["Evolu handleSyncStateChange", { state }]).pipe(
|
|
601
|
+
Effect.zipRight(syncStateStore.setState(state)),
|
|
602
|
+
runFork,
|
|
749
603
|
);
|
|
604
|
+
};
|
|
750
605
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
606
|
+
const handleDbReceive = () => {
|
|
607
|
+
Effect.gen(function* () {
|
|
608
|
+
yield* Effect.logTrace("Evolu handleDbReceive");
|
|
609
|
+
releaseUnsubscribedLoadingPromises();
|
|
610
|
+
const queries = [...subscribedQueries.keys()];
|
|
611
|
+
if (queries.length > 0) {
|
|
612
|
+
yield* Effect.flatMap(db.loadQueries(queries), handlePatches());
|
|
613
|
+
}
|
|
614
|
+
}).pipe(runFork);
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
const sync =
|
|
618
|
+
({ refreshQueries }: { refreshQueries: boolean }) =>
|
|
619
|
+
() => {
|
|
620
|
+
Effect.flatMap(
|
|
621
|
+
db.sync(refreshQueries ? [...subscribedQueries.keys()] : []),
|
|
622
|
+
handlePatches(),
|
|
623
|
+
).pipe(runFork);
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
db.init(
|
|
627
|
+
schema,
|
|
628
|
+
initialDataAsMutations,
|
|
629
|
+
handleDbError,
|
|
630
|
+
handleSyncStateChange,
|
|
631
|
+
handleDbReceive,
|
|
632
|
+
).pipe(
|
|
633
|
+
Effect.tap(sync({ refreshQueries: false })),
|
|
634
|
+
Effect.flatMap(ownerStore.setState),
|
|
635
|
+
Effect.catchTag("NotSupportedPlatformError", () => Effect.void), // no-op
|
|
636
|
+
runFork,
|
|
637
|
+
);
|
|
638
|
+
|
|
639
|
+
const appStateReset = yield* appState.init({
|
|
640
|
+
onRequestSync: sync({ refreshQueries: true }),
|
|
641
|
+
reloadUrl: config.reloadUrl,
|
|
754
642
|
});
|
|
755
643
|
|
|
756
|
-
const
|
|
757
|
-
|
|
644
|
+
const handlePatches =
|
|
645
|
+
(options?: {
|
|
646
|
+
/**
|
|
647
|
+
* The flushSync is for onComplete handlers only. For example, with
|
|
648
|
+
* React, when we want to focus on a node created by a mutation, we must
|
|
649
|
+
* ensure all DOM changes are flushed synchronously.
|
|
650
|
+
*/
|
|
651
|
+
readonly flushSync: boolean;
|
|
652
|
+
}) =>
|
|
653
|
+
(patches: ReadonlyArray<QueryPatches>) =>
|
|
654
|
+
Effect.logDebug(["Evolu handlePatches", { patches }]).pipe(
|
|
655
|
+
Effect.zipRight(rowsStoreStateFromPatches(patches)),
|
|
656
|
+
Effect.tap((nextState) =>
|
|
657
|
+
Effect.forEach(patches, ({ query }) =>
|
|
658
|
+
resolveLoadingPromises(
|
|
659
|
+
query,
|
|
660
|
+
nextState.get(query) || emptyRows(),
|
|
661
|
+
),
|
|
662
|
+
),
|
|
663
|
+
),
|
|
664
|
+
Effect.tap((nextState) => {
|
|
665
|
+
if (options?.flushSync) {
|
|
666
|
+
flushSync(() => {
|
|
667
|
+
rowsStore.setState(nextState).pipe(runSync);
|
|
668
|
+
});
|
|
669
|
+
} else {
|
|
670
|
+
rowsStore.setState(nextState).pipe(runSync);
|
|
671
|
+
}
|
|
672
|
+
}),
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
const rowsStoreStateFromPatches = (patches: ReadonlyArray<QueryPatches>) =>
|
|
676
|
+
Effect.sync((): QueryRowsMap => {
|
|
677
|
+
const rowsStoreState = rowsStore.getState();
|
|
678
|
+
if (patches.length === 0) return rowsStoreState;
|
|
679
|
+
const queriesRows = Arr.map(
|
|
680
|
+
patches,
|
|
681
|
+
({ query, patches }): [Query, ReadonlyArray<Row>] => [
|
|
682
|
+
query,
|
|
683
|
+
applyPatches(patches, rowsStoreState.get(query) || emptyRows()),
|
|
684
|
+
],
|
|
685
|
+
);
|
|
686
|
+
return new Map([...rowsStoreState, ...queriesRows]);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
const resolveLoadingPromises = (query: Query, rows: ReadonlyArray<Row>) =>
|
|
690
|
+
Effect.sync(() => {
|
|
691
|
+
const loadingPromise = loadingPromises.get(query);
|
|
692
|
+
if (!loadingPromise) return;
|
|
693
|
+
const result = queryResultFromRows(rows);
|
|
694
|
+
if (loadingPromise.promise.status !== "fulfilled") {
|
|
695
|
+
loadingPromise.resolve(result);
|
|
696
|
+
} else {
|
|
697
|
+
// A promise can't be fulfilled 2x, so we need a new one.
|
|
698
|
+
loadingPromise.promise = Promise.resolve(result);
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* "For example, a data framework can set the status and value fields on
|
|
702
|
+
* a promise preemptively, before passing to React, so that React can
|
|
703
|
+
* unwrap it without waiting a microtask."
|
|
704
|
+
* https://github.com/acdlite/rfcs/blob/first-class-promises/text/0000-first-class-support-for-promises.md
|
|
705
|
+
*/
|
|
706
|
+
Object.assign(loadingPromise, { status: "fulfilled", value: result });
|
|
707
|
+
if (loadingPromise.releaseOnResolve) {
|
|
708
|
+
loadingPromises.delete(query);
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* We can't delete loading promises in `resolveLoadingPromises` because they
|
|
714
|
+
* must be cached, so repeated calls to `loadQuery` will always return the
|
|
715
|
+
* same promise until the data changes, and we also can't cache them forever
|
|
716
|
+
* because only subscribed queries are automatically updated (reactivity is
|
|
717
|
+
* expensive) hence this function must be called manually on any mutation.
|
|
718
|
+
*/
|
|
719
|
+
const releaseUnsubscribedLoadingPromises = () => {
|
|
720
|
+
[...loadingPromises.entries()]
|
|
721
|
+
.filter(([query]) => !subscribedQueries.has(query))
|
|
722
|
+
.forEach(([query, loadingPromise]) => {
|
|
723
|
+
if (loadingPromise.promise.status === "fulfilled") {
|
|
724
|
+
loadingPromises.delete(query);
|
|
725
|
+
} else {
|
|
726
|
+
loadingPromise.releaseOnResolve = true;
|
|
727
|
+
}
|
|
728
|
+
});
|
|
758
729
|
};
|
|
759
730
|
|
|
760
|
-
|
|
761
|
-
|
|
731
|
+
const mutate = ((): Mutate => {
|
|
732
|
+
let queue: ReadonlyArray<[Mutation, MutateOnComplete | undefined]> = [];
|
|
733
|
+
return (table, { id, ...values }, onComplete) => {
|
|
734
|
+
Effect.logDebug(["Evolu mutate", { table, id, values }]).pipe(runSync);
|
|
735
|
+
const isInsert = id == null;
|
|
736
|
+
if (isInsert) id = nanoIdGenerator.rowId.pipe(runSync);
|
|
737
|
+
queue = [...queue, [{ table, id, values, isInsert }, onComplete]];
|
|
738
|
+
if (queue.length === 1)
|
|
739
|
+
queueMicrotask(() => {
|
|
740
|
+
const [mutations, onCompletes] = Arr.unzip(queue);
|
|
741
|
+
queue = [];
|
|
742
|
+
const onCompletesDef = onCompletes.filter(Predicate.isNotUndefined);
|
|
743
|
+
releaseUnsubscribedLoadingPromises();
|
|
744
|
+
db.mutate(mutations, [...subscribedQueries.keys()]).pipe(
|
|
745
|
+
Effect.flatMap(
|
|
746
|
+
handlePatches({ flushSync: onCompletesDef.length > 0 }),
|
|
747
|
+
),
|
|
748
|
+
Effect.tap(() => {
|
|
749
|
+
onCompletesDef.forEach((onComplete) => onComplete());
|
|
750
|
+
}),
|
|
751
|
+
runFork,
|
|
752
|
+
);
|
|
753
|
+
});
|
|
754
|
+
return { id };
|
|
755
|
+
};
|
|
756
|
+
})();
|
|
762
757
|
|
|
763
|
-
|
|
758
|
+
const evolu: Evolu = {
|
|
764
759
|
subscribeError: errorStore.subscribe,
|
|
765
760
|
getError: errorStore.getState,
|
|
766
761
|
|
|
767
|
-
createQuery:
|
|
768
|
-
|
|
762
|
+
createQuery: (queryCallback, options) =>
|
|
763
|
+
pipe(
|
|
764
|
+
queryCallback(kysely).compile(),
|
|
765
|
+
(compiledQuery): SqliteQuery => {
|
|
766
|
+
if (isSqlMutation(compiledQuery.sql))
|
|
767
|
+
throw new Error(
|
|
768
|
+
"SQL mutation (INSERT, UPDATE, DELETE, etc.) isn't allowed in the Evolu `createQuery` function. Kysely suggests it because there is no read-only Kysely yet, and removing such an API is not possible. For mutations, use Evolu mutation API.",
|
|
769
|
+
);
|
|
770
|
+
const parameters = compiledQuery.parameters as NonNullable<
|
|
771
|
+
SqliteQuery["parameters"]
|
|
772
|
+
>;
|
|
773
|
+
return {
|
|
774
|
+
sql: compiledQuery.sql,
|
|
775
|
+
parameters,
|
|
776
|
+
...(options && { options }),
|
|
777
|
+
};
|
|
778
|
+
},
|
|
779
|
+
(query) => serializeQuery(query),
|
|
780
|
+
),
|
|
781
|
+
|
|
782
|
+
loadQuery: (() => {
|
|
783
|
+
let queue: ReadonlyArray<Query> = [];
|
|
784
|
+
return <R extends Row>(query: Query<R>): Promise<QueryResult<R>> => {
|
|
785
|
+
Effect.logDebug([
|
|
786
|
+
"Evolu loadQuery",
|
|
787
|
+
{ query: deserializeQuery(query) },
|
|
788
|
+
]).pipe(runSync);
|
|
789
|
+
let loadingPromise = loadingPromises.get(query);
|
|
790
|
+
if (!loadingPromise) {
|
|
791
|
+
let resolve: LoadingPromise["resolve"] = constVoid;
|
|
792
|
+
const promise: LoadingPromise["promise"] = new Promise(
|
|
793
|
+
(_resolve) => {
|
|
794
|
+
resolve = _resolve;
|
|
795
|
+
},
|
|
796
|
+
);
|
|
797
|
+
loadingPromise = { resolve, promise, releaseOnResolve: false };
|
|
798
|
+
loadingPromises.set(query, loadingPromise);
|
|
799
|
+
queue = [...queue, query];
|
|
800
|
+
if (queue.length === 1) {
|
|
801
|
+
queueMicrotask(() => {
|
|
802
|
+
db.loadQueries(Arr.dedupe(queue)).pipe(
|
|
803
|
+
Effect.flatMap(handlePatches()),
|
|
804
|
+
runFork,
|
|
805
|
+
);
|
|
806
|
+
queue = [];
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return loadingPromise.promise as Promise<QueryResult<R>>;
|
|
811
|
+
};
|
|
812
|
+
})(),
|
|
769
813
|
|
|
770
|
-
loadQueries: <R extends Row, Q extends Queries<R>>(
|
|
771
|
-
queries
|
|
814
|
+
loadQueries: <R extends Row, Q extends Queries<R>>(
|
|
815
|
+
queries: [...Q],
|
|
816
|
+
): [...QueryResultsPromisesFromQueries<Q>] =>
|
|
817
|
+
queries.map(evolu.loadQuery) as [...QueryResultsPromisesFromQueries<Q>],
|
|
772
818
|
|
|
773
|
-
subscribeQuery
|
|
774
|
-
|
|
819
|
+
subscribeQuery: (query) => (listener) => {
|
|
820
|
+
subscribedQueries.set(
|
|
821
|
+
query,
|
|
822
|
+
Number.increment(subscribedQueries.get(query) ?? 0),
|
|
823
|
+
);
|
|
824
|
+
const unsubscribe = rowsStore.subscribe(listener);
|
|
825
|
+
|
|
826
|
+
return () => {
|
|
827
|
+
const count = subscribedQueries.get(query);
|
|
828
|
+
if (count != null && count > 1)
|
|
829
|
+
subscribedQueries.set(query, Number.decrement(count));
|
|
830
|
+
else subscribedQueries.delete(query);
|
|
831
|
+
unsubscribe();
|
|
832
|
+
};
|
|
833
|
+
},
|
|
834
|
+
|
|
835
|
+
getQuery: <R extends Row>(query: Query<R>): QueryResult<R> =>
|
|
836
|
+
queryResultFromRows(
|
|
837
|
+
rowsStore.getState().get(query) || emptyRows(),
|
|
838
|
+
) as QueryResult<R>,
|
|
775
839
|
|
|
776
840
|
subscribeOwner: ownerStore.subscribe,
|
|
777
841
|
getOwner: ownerStore.getState,
|
|
@@ -779,69 +843,105 @@ const EvoluCommon = Layer.effect(
|
|
|
779
843
|
subscribeSyncState: syncStateStore.subscribe,
|
|
780
844
|
getSyncState: syncStateStore.getState,
|
|
781
845
|
|
|
782
|
-
create: mutate as Mutate<
|
|
846
|
+
create: mutate as Mutate<EvoluSchema, "create">,
|
|
783
847
|
update: mutate,
|
|
784
|
-
createOrUpdate: mutate as Mutate<
|
|
848
|
+
createOrUpdate: mutate as Mutate<EvoluSchema, "createOrUpdate">,
|
|
785
849
|
|
|
786
|
-
resetOwner: () =>
|
|
850
|
+
resetOwner: () => {
|
|
851
|
+
db.resetOwner().pipe(Effect.zipRight(appStateReset.reset), runFork);
|
|
852
|
+
},
|
|
787
853
|
|
|
788
|
-
restoreOwner: (mnemonic) =>
|
|
789
|
-
|
|
854
|
+
restoreOwner: (mnemonic) => {
|
|
855
|
+
db.restoreOwner(schema, mnemonic).pipe(
|
|
856
|
+
Effect.zipRight(appStateReset.reset),
|
|
857
|
+
runFork,
|
|
858
|
+
);
|
|
859
|
+
},
|
|
790
860
|
|
|
791
|
-
ensureSchema: (schema
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
indexes,
|
|
796
|
-
}),
|
|
861
|
+
ensureSchema: (schema) => {
|
|
862
|
+
db.ensureSchema(schema).pipe(runFork);
|
|
863
|
+
},
|
|
864
|
+
};
|
|
797
865
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
)
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
866
|
+
return evolu;
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
const initialDataToMutations = (
|
|
870
|
+
initialData: EvoluConfig["initialData"] = constVoid,
|
|
871
|
+
) =>
|
|
872
|
+
Effect.map(NanoIdGenerator, (nanoIdGenerator) => {
|
|
873
|
+
const mutations: Mutation[] = [];
|
|
874
|
+
const mutate: Mutate = (table, { id, ...values }) => {
|
|
875
|
+
if (id == null) id = nanoIdGenerator.rowId.pipe(Effect.runSync) as never;
|
|
876
|
+
mutations.push({ isInsert: true, id, table: table as string, values });
|
|
877
|
+
return { id };
|
|
878
|
+
};
|
|
879
|
+
const evolu: EvoluForInitialData = {
|
|
880
|
+
create: mutate as Mutate<EvoluSchema, "create">,
|
|
881
|
+
createOrUpdate: mutate as Mutate<EvoluSchema, "createOrUpdate">,
|
|
882
|
+
};
|
|
883
|
+
initialData(evolu);
|
|
884
|
+
return mutations;
|
|
885
|
+
});
|
|
811
886
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
* 2. Export platform-specific parseMnemonic. If the platform supports lazy import,
|
|
817
|
-
* use it because dictionaries have a few hundred KBs.
|
|
818
|
-
* 3. Export `createEvolu` for a platform. The TS docs must be copy-pasted, and
|
|
819
|
-
* remember to update the import.
|
|
820
|
-
* 4. Export UI library API code.
|
|
821
|
-
*/
|
|
887
|
+
interface EvoluForInitialData<T extends EvoluSchema = EvoluSchema> {
|
|
888
|
+
create: Mutate<T, "create">;
|
|
889
|
+
createOrUpdate: Mutate<T, "createOrUpdate">;
|
|
890
|
+
}
|
|
822
891
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
<
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
Effect.provide(ConfigLive(config)),
|
|
834
|
-
Effect.runSync,
|
|
835
|
-
),
|
|
836
|
-
);
|
|
892
|
+
interface LoadingPromise {
|
|
893
|
+
/** Promise with props for the upcoming React use hook. */
|
|
894
|
+
promise: Promise<QueryResult> & {
|
|
895
|
+
status?: "pending" | "fulfilled" | "rejected";
|
|
896
|
+
value?: QueryResult;
|
|
897
|
+
reason?: unknown;
|
|
898
|
+
};
|
|
899
|
+
resolve: (rows: QueryResult) => void;
|
|
900
|
+
releaseOnResolve: boolean;
|
|
901
|
+
}
|
|
837
902
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
)
|
|
903
|
+
// https://kysely.dev/docs/recipes/splitting-query-building-and-execution
|
|
904
|
+
const kysely = new Kysely.Kysely({
|
|
905
|
+
dialect: {
|
|
906
|
+
createAdapter: () => new Kysely.SqliteAdapter(),
|
|
907
|
+
createDriver: () => new Kysely.DummyDriver(),
|
|
908
|
+
createIntrospector() {
|
|
909
|
+
throw "Not implemeneted";
|
|
910
|
+
},
|
|
911
|
+
createQueryCompiler: () => new Kysely.SqliteQueryCompiler(),
|
|
912
|
+
},
|
|
913
|
+
});
|
|
844
914
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
915
|
+
const createIndex = kysely.schema.createIndex.bind(kysely.schema);
|
|
916
|
+
type CreateIndex = typeof createIndex;
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Create SQLite indexes.
|
|
920
|
+
*
|
|
921
|
+
* See https://www.evolu.dev/docs/indexes
|
|
922
|
+
*
|
|
923
|
+
* @example
|
|
924
|
+
* const indexes = createIndexes((create) => [
|
|
925
|
+
* create("indexTodoCreatedAt").on("todo").column("createdAt"),
|
|
926
|
+
* create("indexTodoCategoryCreatedAt")
|
|
927
|
+
* .on("todoCategory")
|
|
928
|
+
* .column("createdAt"),
|
|
929
|
+
* ]);
|
|
930
|
+
*/
|
|
931
|
+
export const createIndexes = (
|
|
932
|
+
callback: (
|
|
933
|
+
create: CreateIndex,
|
|
934
|
+
) => ReadonlyArray<Kysely.CreateIndexBuilder<any>>,
|
|
935
|
+
): ReadonlyArray<Index> =>
|
|
936
|
+
callback(createIndex).map(
|
|
937
|
+
(index): Index => ({
|
|
938
|
+
name: index.toOperationNode().name.name,
|
|
939
|
+
sql: index.compile().sql,
|
|
940
|
+
}),
|
|
941
|
+
);
|
|
942
|
+
|
|
943
|
+
/** Create a namespaced lock name. */
|
|
944
|
+
export const getLockName = (
|
|
945
|
+
name: string,
|
|
946
|
+
): Effect.Effect<string, never, Config> =>
|
|
947
|
+
Effect.map(Config, (config) => `evolu:${config.name}:${name}`);
|