@evolu/common 1.0.16 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +7 -7
  2. package/dist/src/Config.d.ts +4 -6
  3. package/dist/src/Config.d.ts.map +1 -1
  4. package/dist/src/Config.js +1 -1
  5. package/dist/src/Crdt.d.ts +15 -9
  6. package/dist/src/Crdt.d.ts.map +1 -1
  7. package/dist/src/Crdt.js +15 -14
  8. package/dist/src/Crypto.d.ts +7 -13
  9. package/dist/src/Crypto.d.ts.map +1 -1
  10. package/dist/src/Crypto.js +7 -9
  11. package/dist/src/Db.d.ts +36 -70
  12. package/dist/src/Db.d.ts.map +1 -1
  13. package/dist/src/Db.js +45 -51
  14. package/dist/src/DbWorker.d.ts +13 -14
  15. package/dist/src/DbWorker.d.ts.map +1 -1
  16. package/dist/src/DbWorker.js +56 -57
  17. package/dist/src/Diff.d.ts +3 -1
  18. package/dist/src/Diff.d.ts.map +1 -1
  19. package/dist/src/Diff.js +3 -7
  20. package/dist/src/{Errors.d.ts → ErrorStore.d.ts} +14 -7
  21. package/dist/src/ErrorStore.d.ts.map +1 -0
  22. package/dist/src/{Errors.js → ErrorStore.js} +2 -0
  23. package/dist/src/Evolu.d.ts +316 -58
  24. package/dist/src/Evolu.d.ts.map +1 -1
  25. package/dist/src/Evolu.js +199 -185
  26. package/dist/src/Model.d.ts +47 -57
  27. package/dist/src/Model.d.ts.map +1 -1
  28. package/dist/src/Model.js +30 -35
  29. package/dist/src/OnCompletes.d.ts +11 -0
  30. package/dist/src/OnCompletes.d.ts.map +1 -0
  31. package/dist/src/OnCompletes.js +21 -0
  32. package/dist/src/Owner.d.ts +31 -0
  33. package/dist/src/Owner.d.ts.map +1 -0
  34. package/dist/src/Owner.js +20 -0
  35. package/dist/src/Platform.d.ts +15 -8
  36. package/dist/src/Platform.d.ts.map +1 -1
  37. package/dist/src/Platform.js +5 -5
  38. package/dist/src/Protobuf.d.ts +25 -73
  39. package/dist/src/Protobuf.d.ts.map +1 -1
  40. package/dist/src/Protobuf.js +4 -12
  41. package/dist/src/Public.d.ts +5 -3
  42. package/dist/src/Public.d.ts.map +1 -1
  43. package/dist/src/Public.js +1 -0
  44. package/dist/src/Sqlite.d.ts +8 -11
  45. package/dist/src/Sqlite.d.ts.map +1 -1
  46. package/dist/src/Sqlite.js +7 -7
  47. package/dist/src/Store.d.ts +6 -5
  48. package/dist/src/Store.d.ts.map +1 -1
  49. package/dist/src/Store.js +21 -16
  50. package/dist/src/SyncWorker.d.ts +8 -2
  51. package/dist/src/SyncWorker.d.ts.map +1 -1
  52. package/dist/src/SyncWorker.js +12 -19
  53. package/dist/src/index.d.ts +6 -2
  54. package/dist/src/index.d.ts.map +1 -1
  55. package/dist/src/index.js +6 -2
  56. package/package.json +23 -13
  57. package/src/Config.ts +7 -7
  58. package/src/Crdt.ts +29 -26
  59. package/src/Crypto.ts +14 -19
  60. package/src/Db.ts +113 -166
  61. package/src/DbWorker.ts +110 -140
  62. package/src/Diff.ts +5 -7
  63. package/src/{Errors.ts → ErrorStore.ts} +17 -8
  64. package/src/Evolu.ts +682 -442
  65. package/src/Model.ts +60 -85
  66. package/src/OnCompletes.ts +46 -0
  67. package/src/Owner.ts +65 -0
  68. package/src/Platform.ts +24 -16
  69. package/src/Protobuf.ts +25 -73
  70. package/src/Public.ts +5 -3
  71. package/src/Sqlite.ts +13 -24
  72. package/src/Store.ts +36 -24
  73. package/src/SyncWorker.ts +30 -38
  74. package/src/index.ts +6 -2
  75. 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 { Simplify } from "kysely";
14
- import { Config, ConfigLive } from "./Config.js";
12
+ import * as Kysely from "kysely";
13
+ import { Config } from "./Config.js";
15
14
  import { Time, TimeLive } from "./Crdt.js";
16
- import { Bip39, NanoId } from "./Crypto.js";
15
+ import { Mnemonic, NanoId, NanoIdLive } from "./Crypto.js";
17
16
  import {
18
- CommonColumns,
19
- CreateQuery,
20
- NullableExceptOfId,
21
- Owner,
17
+ Queries,
18
+ Query,
19
+ QueryResult,
20
+ QueryResultsPromisesFromQueries,
21
+ Row,
22
+ RowsStore,
23
+ RowsStoreLive,
22
24
  Schema,
23
- Tables,
24
- makeCreateQuery,
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 "./Errors.js";
35
- import { CastableForMutate, Id, cast } from "./Model.js";
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 { Query, Row } from "./Sqlite.js";
38
- import { Store, StoreListener, StoreUnsubscribe, makeStore } from "./Store.js";
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<S extends Schema> {
42
- readonly subscribeError: ErrorStore["subscribe"];
43
- readonly getError: ErrorStore["getState"];
44
-
45
- readonly subscribeOwner: OwnerStore["subscribe"];
46
- readonly getOwner: OwnerStore["getState"];
47
-
48
- readonly subscribeQuery: QueryStore["subscribe"];
49
- readonly getQuery: QueryStore["getState"];
50
- readonly createQuery: CreateQuery<S>;
51
- readonly loadQuery: QueryStore["loadQuery"];
52
-
53
- readonly subscribeSyncState: (listener: StoreListener) => StoreUnsubscribe;
54
- readonly getSyncState: () => SyncState;
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
- export const Evolu = <S extends Schema>(): Context.Tag<Evolu<S>, Evolu<S>> =>
64
- Context.Tag<Evolu<S>>("evolu/Evolu");
56
+ /** Get {@link EvoluError}. */
57
+ readonly getError: Store<EvoluError | null>["getState"];
65
58
 
66
- type ErrorStore = Store<EvoluError | null>;
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
- type OwnerStore = Store<Owner | null>;
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
- interface QueryStore {
71
- readonly subscribe: (
72
- query: Query | null,
73
- ) => (listener: StoreListener) => StoreUnsubscribe;
74
- readonly getState: (query: Query | null) => ReadonlyArray<Row> | null;
75
- readonly loadQuery: (query: Query) => Promise<ReadonlyArray<Row>>;
76
- readonly onQuery: (output: DbWorkerOutputOnQuery) => void;
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
- const QueryStore = Context.Tag<QueryStore>("evolu/QueryStore");
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
- export type Create<S extends Schema> = <T extends keyof S>(
82
- table: T,
83
- values: Simplify<PartialOnlyForNullable<CastableForMutate<Omit<S[T], "id">>>>,
84
- onComplete?: () => void,
85
- ) => {
86
- readonly id: S[T]["id"];
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
- // https://stackoverflow.com/a/54713648/233902
90
- type PartialOnlyForNullable<
91
- T,
92
- NK extends keyof T = {
93
- [K in keyof T]: null extends T[K] ? K : never;
94
- }[keyof T],
95
- NP = Pick<T, Exclude<keyof T, NK>> & Partial<Pick<T, NK>>,
96
- > = { [K in keyof NP]: NP[K] };
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
- export type Update<S extends Schema> = <T extends keyof S>(
99
- table: T,
100
- values: Simplify<
101
- Partial<
102
- CastableForMutate<Omit<S[T], "id"> & Pick<CommonColumns, "isDeleted">>
103
- > & { readonly id: S[T]["id"] }
104
- >,
105
- onComplete?: () => void,
106
- ) => {
107
- readonly id: S[T]["id"];
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
- type Mutate<S extends Schema> = <
111
- U extends SchemaForMutate<S>,
112
- T extends keyof U,
113
- >(
114
- table: T,
115
- values: Simplify<Partial<CastableForMutate<U[T]>>>,
116
- onComplete?: () => void,
117
- ) => {
118
- readonly id: U[T]["id"];
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
- const Mutate = <S extends Schema>(): Context.Tag<Mutate<S>, Mutate<S>> =>
122
- Context.Tag<Mutate<S>>("evolu/Mutate");
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
- type SchemaForMutate<S extends Schema> = {
125
- readonly [Table in keyof S]: NullableExceptOfId<
126
- {
127
- readonly [Column in keyof S[Table]]: S[Table][Column];
128
- } & Pick<CommonColumns, "isDeleted">
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
- * Use `reset` to delete all local data from the current device.
135
- * After the deletion, Evolu reloads all browser tabs that use Evolu.
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
- readonly reset: () => void;
289
+ update: Mutate<T, "update">;
138
290
 
139
291
  /**
140
- * Use `restore` to restore `Owner` with synced data on a different device.
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 restore: (
143
- mnemonic: string,
144
- ) => Promise<Either.Either<RestoreOwnerError, void>>;
145
- }
296
+ readonly resetOwner: () => void;
146
297
 
147
- const OwnerActions = Context.Tag<OwnerActions>("evolu/OwnerActions");
298
+ /** Restore {@link Owner} with all their synced data. */
299
+ readonly restoreOwner: (mnemonic: Mnemonic) => void;
148
300
 
149
- interface RestoreOwnerError {
150
- readonly _tag: "RestoreOwnerError";
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
- type SubscribedQueries = Map<Query, number>;
307
+ export const Evolu = Context.Tag<Evolu>();
154
308
 
155
- const SubscribedQueries = Context.Tag<SubscribedQueries>(
156
- "evolu/SubscribedQueries",
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 OnCompletes = Map<OnCompleteId, OnComplete>;
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
- const OnCompletes = Context.Tag<OnCompletes>("evolu/OnCompletes");
331
+ type NullableExceptId<T> = {
332
+ readonly [K in keyof T]: K extends "id" ? T[K] : T[K] | null;
333
+ };
162
334
 
163
- type OnComplete = () => void;
335
+ export interface CommonColumns {
336
+ readonly createdAt: SqliteDate;
337
+ readonly updatedAt: SqliteDate;
338
+ readonly isDeleted: SqliteBoolean;
339
+ }
164
340
 
165
- interface LoadingPromises {
166
- readonly getPromise: (query: Query) => {
167
- readonly promise: Promise<ReadonlyArray<Row>>;
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
- readonly resolvePromise: (query: Query, rows: ReadonlyArray<Row>) => void;
171
- readonly releasePromises: (ignoreQueries: ReadonlyArray<Query>) => void;
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>("evolu/LoadingPromises");
385
+ export const LoadingPromises = Context.Tag<LoadingPromises>();
175
386
 
176
- export const loadingPromisesPromiseProp = "rows";
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 LoadingPromisesLive = Layer.effect(
393
+ export const LoadingPromiseLive = Layer.effect(
179
394
  LoadingPromises,
180
- Effect.sync(() => {
181
- const promises = new Map<
182
- Query,
183
- {
184
- readonly promise: Promise<ReadonlyArray<Row>>;
185
- readonly resolve: (rows: ReadonlyArray<Row>) => void;
186
- }
187
- >();
188
-
189
- const getPromise: LoadingPromises["getPromise"] = (query) => {
190
- const item = promises.get(query);
191
- if (item) return { promise: item.promise, isNew: false };
192
- let resolve: (rows: ReadonlyArray<Row>) => void = Function.constVoid;
193
- const promise = new Promise<ReadonlyArray<Row>>((_resolve) => {
194
- resolve = _resolve;
195
- });
196
- promises.set(query, { promise, resolve });
197
- return { promise, isNew: true };
198
- };
199
-
200
- const resolvePromise: LoadingPromises["resolvePromise"] = (query, rows) => {
201
- const item = promises.get(query);
202
- if (!item) return;
203
- // It's similar to what React will do.
204
- void Object.assign(item.promise, { [loadingPromisesPromiseProp]: rows });
205
- item.resolve(rows);
206
- };
207
-
208
- const releasePromises: LoadingPromises["releasePromises"] = (
209
- ignoreQueries,
210
- ) => {
211
- [...promises.keys()].forEach((query) => {
212
- if (!ignoreQueries.includes(query)) promises.delete(query);
213
- });
214
- };
215
-
216
- return { getPromise, resolvePromise, releasePromises };
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
- const QueryStoreLive = Layer.effect(
221
- QueryStore,
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
- const flushSync = yield* _(FlushSync);
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
- const subscribe: QueryStore["subscribe"] = (query) => (listen) => {
233
- if (query == null) return Function.constVoid;
234
- subscribedQueries.set(
235
- query,
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
- const MutateLive = <S extends Schema>(): Layer.Layer<
301
- NanoId | OnCompletes | Time | SubscribedQueries | LoadingPromises | DbWorker,
302
- never,
303
- Mutate<S>
304
- > =>
305
- Layer.effect(
306
- Mutate<S>(),
307
- Effect.gen(function* (_) {
308
- const nanoid = yield* _(NanoId);
309
- const onCompletes = yield* _(OnCompletes);
310
- const time = yield* _(Time);
311
- const subscribedQueries = yield* _(SubscribedQueries);
312
- const loadingPromises = yield* _(LoadingPromises);
313
- const dbWorker = yield* _(DbWorker);
314
-
315
- const queue: Array<MutateItem> = [];
316
-
317
- return Mutate<S>().of((table, { id, ...values }, onComplete) => {
318
- const isInsert = id == null;
319
- if (isInsert) id = Effect.runSync(nanoid.nanoid) as never;
320
-
321
- let onCompleteId = null;
322
- if (onComplete) {
323
- onCompleteId = Effect.runSync(nanoid.nanoid) as OnCompleteId;
324
- onCompletes.set(onCompleteId, onComplete);
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
- queue.push({
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: id as 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
- if (ReadonlyArray.isNonEmptyReadonlyArray(items))
346
- dbWorker.postMessage({ _tag: "mutate", items, queries });
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
- return { id: id as never };
350
- });
351
- }),
352
- );
667
+ return { id };
668
+ });
669
+ }),
670
+ );
353
671
 
354
- const OwnerActionsLive = Layer.effect(
355
- OwnerActions,
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 bip39 = yield* _(Bip39);
359
-
360
- const reset: OwnerActions["reset"] = () => {
361
- dbWorker.postMessage({ _tag: "reset" });
362
- };
363
-
364
- const restore: OwnerActions["restore"] = (mnemonic) =>
365
- bip39.parse(mnemonic).pipe(
366
- Effect.flatMap((mnemonic) =>
367
- Effect.sync(() => {
368
- dbWorker.postMessage({ _tag: "reset", mnemonic });
369
- return Either.right(undefined);
370
- }),
371
- ),
372
- Effect.catchTag("InvalidMnemonicError", () =>
373
- Effect.succeed(
374
- Either.left<RestoreOwnerError>({ _tag: "RestoreOwnerError" }),
375
- ),
376
- ),
377
- Effect.runPromise,
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
- return { reset, restore };
381
- }),
382
- );
709
+ dbWorker.postMessage({
710
+ _tag: "init",
711
+ config: yield* _(Config),
712
+ });
383
713
 
384
- export const EvoluLive = <S extends Schema>(
385
- tables: Tables,
386
- ): Layer.Layer<
387
- DbWorker | Bip39 | Config | FlushSync | NanoId | Time | AppState,
388
- never,
389
- Evolu<S>
390
- > =>
391
- Layer.effect(
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
- const queryStore = Effect.provide(
423
- QueryStore,
424
- Layer.use(QueryStoreLive, Layers),
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
- dbWorker.postMessage({ _tag: "init", config, tables });
727
+ createQuery: makeCreateQuery(),
728
+ loadQuery,
478
729
 
479
- appState.onFocus(() => {
480
- // `queries` to refresh subscribed queries when a tab is changed.
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
- const sync = (): void =>
485
- dbWorker.postMessage({ _tag: "sync", queries: [] });
486
- appState.onReconnect(sync);
487
- sync();
488
-
489
- return Evolu<S>().of({
490
- subscribeError: errorStore.subscribe,
491
- getError: errorStore.getState,
492
-
493
- subscribeOwner: ownerStore.subscribe,
494
- getOwner: ownerStore.getState,
495
-
496
- createQuery: makeCreateQuery<S>(),
497
- subscribeQuery: queryStore.subscribe,
498
- getQuery: queryStore.getState,
499
- loadQuery: queryStore.loadQuery,
500
-
501
- subscribeSyncState: syncStateStore.subscribe,
502
- getSyncState: syncStateStore.getState,
503
-
504
- create: mutate as Create<S>,
505
- update: mutate as Update<S>,
506
- ownerActions,
507
- ensureSchema,
508
- });
509
- }),
510
- );
511
-
512
- export const makeEvoluForPlatform = <S extends Schema>(
513
- PlatformLayer: Layer.Layer<
514
- never,
515
- never,
516
- DbWorker | Bip39 | NanoId | FlushSync | AppState
517
- >,
518
- tables: Tables,
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
+ );