@evolu/common 4.1.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +1 -1
  2. package/dist/src/Config.d.ts +26 -27
  3. package/dist/src/Config.d.ts.map +1 -1
  4. package/dist/src/Config.js +35 -7
  5. package/dist/src/Crdt.d.ts +5 -4
  6. package/dist/src/Crdt.d.ts.map +1 -1
  7. package/dist/src/Crdt.js +27 -24
  8. package/dist/src/Crypto.d.ts +16 -10
  9. package/dist/src/Crypto.d.ts.map +1 -1
  10. package/dist/src/Crypto.js +24 -11
  11. package/dist/src/Db.d.ts +58 -78
  12. package/dist/src/Db.d.ts.map +1 -1
  13. package/dist/src/Db.js +383 -185
  14. package/dist/src/Diff.d.ts +9 -3
  15. package/dist/src/Diff.d.ts.map +1 -1
  16. package/dist/src/Diff.js +12 -11
  17. package/dist/src/Error.d.ts +15 -0
  18. package/dist/src/Error.d.ts.map +1 -0
  19. package/dist/src/Error.js +14 -0
  20. package/dist/src/Evolu.d.ts +139 -91
  21. package/dist/src/Evolu.d.ts.map +1 -1
  22. package/dist/src/Evolu.js +267 -239
  23. package/dist/src/Model.d.ts +52 -0
  24. package/dist/src/Model.d.ts.map +1 -1
  25. package/dist/src/Model.js +47 -5
  26. package/dist/src/Owner.d.ts.map +1 -1
  27. package/dist/src/Owner.js +14 -8
  28. package/dist/src/Platform.d.ts +24 -34
  29. package/dist/src/Platform.d.ts.map +1 -1
  30. package/dist/src/Platform.js +9 -31
  31. package/dist/src/Public.d.ts +3 -5
  32. package/dist/src/Public.d.ts.map +1 -1
  33. package/dist/src/Public.js +2 -3
  34. package/dist/src/Sqlite.d.ts +25 -15
  35. package/dist/src/Sqlite.d.ts.map +1 -1
  36. package/dist/src/Sqlite.js +28 -7
  37. package/dist/src/Store.d.ts.map +1 -1
  38. package/dist/src/Sync.d.ts +70 -0
  39. package/dist/src/Sync.d.ts.map +1 -0
  40. package/dist/src/Sync.js +126 -0
  41. package/dist/src/index.d.ts +2 -5
  42. package/dist/src/index.d.ts.map +1 -1
  43. package/dist/src/index.js +2 -5
  44. package/package.json +12 -10
  45. package/src/Config.ts +82 -39
  46. package/src/Crdt.ts +61 -76
  47. package/src/Crypto.ts +83 -60
  48. package/src/Db.ts +802 -370
  49. package/src/Diff.ts +23 -21
  50. package/src/Error.ts +29 -0
  51. package/src/Evolu.ts +599 -499
  52. package/src/Model.ts +102 -6
  53. package/src/Owner.ts +24 -24
  54. package/src/Platform.ts +33 -81
  55. package/src/Public.ts +3 -5
  56. package/src/Sqlite.ts +81 -27
  57. package/src/Sync.ts +306 -0
  58. package/src/index.ts +2 -5
  59. package/dist/src/DbWorker.d.ts +0 -82
  60. package/dist/src/DbWorker.d.ts.map +0 -1
  61. package/dist/src/DbWorker.js +0 -335
  62. package/dist/src/ErrorStore.d.ts +0 -29
  63. package/dist/src/ErrorStore.d.ts.map +0 -1
  64. package/dist/src/ErrorStore.js +0 -13
  65. package/dist/src/OnCompletes.d.ts +0 -14
  66. package/dist/src/OnCompletes.d.ts.map +0 -1
  67. package/dist/src/OnCompletes.js +0 -25
  68. package/dist/src/SyncWorker.d.ts +0 -80
  69. package/dist/src/SyncWorker.d.ts.map +0 -1
  70. package/dist/src/SyncWorker.js +0 -150
  71. package/dist/src/Types.d.ts +0 -13
  72. package/dist/src/Types.d.ts.map +0 -1
  73. package/dist/src/Types.js +0 -1
  74. package/src/DbWorker.ts +0 -721
  75. package/src/ErrorStore.ts +0 -46
  76. package/src/OnCompletes.ts +0 -51
  77. package/src/SyncWorker.ts +0 -361
  78. 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 * as Function from "effect/Function";
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 Match from "effect/Match";
9
+ import * as ManagedRuntime from "effect/ManagedRuntime";
9
10
  import * as Number from "effect/Number";
10
- import * as ReadonlyArray from "effect/ReadonlyArray";
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, ConfigLive } from "./Config.js";
13
- import { Mnemonic, NanoIdGenerator, NanoIdGeneratorLive } from "./Crypto.js";
15
+ import { Config, createRuntime, defaultConfig } from "./Config.js";
16
+ import { Mnemonic, NanoIdGenerator } from "./Crypto.js";
14
17
  import {
15
- DatabaseSchema,
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
- RowsStore,
22
- RowsStoreLive,
28
+ Table,
29
+ deserializeQuery,
23
30
  emptyRows,
24
31
  queryResultFromRows,
25
- schemaToTables,
26
32
  serializeQuery,
27
33
  } from "./Db.js";
28
- import { DbWorker, DbWorkerOutputOnQuery, Mutation } from "./DbWorker.js";
29
- import { applyPatches } from "./Diff.js";
30
- import { EvoluError, makeErrorStore } from "./ErrorStore.js";
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 { Store, Unsubscribe, makeStore } from "./Store.js";
42
- import { SyncState } from "./SyncWorker.js";
45
+ import { Listener, Unsubscribe, makeStore } from "./Store.js";
46
+ import { SyncState, initialSyncState } from "./Sync.js";
43
47
 
44
- export interface Evolu<S extends DatabaseSchema = DatabaseSchema> {
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: Store<EvoluError | null>["subscribe"];
78
+ readonly subscribeError: (listener: Listener) => Unsubscribe;
55
79
 
56
80
  /** Get {@link EvoluError}. */
57
- readonly getError: Store<EvoluError | null>["getState"];
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: CreateQuery<S>;
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: 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: SubscribedQueries["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: SubscribedQueries["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: Store<Owner | null>["subscribe"];
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: Store<Owner | null>["getState"];
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: Store<SyncState>["subscribe"];
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: Store<SyncState>["getState"];
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: Create<S>;
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: Update<S>;
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: CreateOrUpdate<S>;
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 DatabaseSchema} exist in the
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
- * Evolu syncs on every mutation, tab focus, and network reconnect, so it's
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 sync: () => void;
316
- }
345
+ readonly ensureSchema: (schema: DbSchema) => void;
317
346
 
318
- export const Evolu = Context.GenericTag<Evolu>("@services/Evolu");
319
-
320
- type CreateQuery<S extends DatabaseSchema> = <R extends Row>(
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 QuerySchema<S extends DatabaseSchema> = {
333
- readonly [Table in keyof S]: NullableExceptForIdAndAutomaticColumns<{
334
- readonly [Column in keyof S[Table]]: S[Table][Column];
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 NullableExceptForIdAndAutomaticColumns<T> = {
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
- // https://kysely.dev/docs/recipes/splitting-query-building-and-execution
345
- const kysely = new Kysely.Kysely<QuerySchema<DatabaseSchema>>({
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 S>(
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<S[K], "id" | "createdAt" | "updatedAt" | "isDeleted">>
373
+ Castable<Omit<T[K], "id" | "createdAt" | "updatedAt" | "isDeleted">>
624
374
  >
625
375
  : Mode extends "update"
626
- ? Partial<Castable<Omit<S[K], "id" | "createdAt" | "updatedAt">>> & {
627
- readonly id: S[K]["id"];
376
+ ? Partial<Castable<Omit<T[K], "id" | "createdAt" | "updatedAt">>> & {
377
+ readonly id: T[K]["id"];
628
378
  }
629
379
  : PartialForNullable<
630
- Castable<Omit<S[K], "createdAt" | "updatedAt" | "isDeleted">>
380
+ Castable<Omit<T[K], "createdAt" | "updatedAt" | "isDeleted">>
631
381
  >
632
382
  >,
633
- onComplete?: () => void,
383
+ onComplete?: MutateOnComplete,
634
384
  ) => {
635
- readonly id: S[K]["id"];
385
+ readonly id: T[K]["id"];
636
386
  };
637
387
 
638
- export const Mutate = Context.GenericTag<Mutate>("@services/Mutate");
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
- const MutateLive = Layer.effect(
669
- Mutate,
670
- Effect.gen(function* (_) {
671
- const { nanoid } = yield* _(NanoIdGenerator);
672
- const onCompletes = yield* _(OnCompletes);
673
- const subscribedQueries = yield* _(SubscribedQueries);
674
- const loadingPromises = yield* _(LoadingPromises);
675
- const dbWorker = yield* _(DbWorker);
676
- let mutations: ReadonlyArray<Mutation> = [];
677
-
678
- return Mutate.of((table, { id, ...values }, onComplete) => {
679
- const isInsert = id == null;
680
- if (isInsert) id = Effect.runSync(nanoid) as never;
681
-
682
- const onCompleteId = onComplete
683
- ? onCompletes.add(onComplete).pipe(Effect.runSync)
684
- : null;
685
-
686
- mutations = [
687
- ...mutations,
688
- {
689
- table: table.toString(),
690
- id,
691
- values,
692
- isInsert,
693
- onCompleteId,
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
- if (mutations.length === 1)
698
- queueMicrotask(() => {
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
- return { id };
710
- });
711
- }),
712
- );
713
-
714
- /** EvoluCommon is without side effects, so it's unit-testable. */
715
- const EvoluCommon = Layer.effect(
716
- Evolu,
717
- Effect.gen(function* (_) {
718
- const dbWorker = yield* _(DbWorker);
719
- const errorStore = yield* _(makeErrorStore);
720
- const loadQuery = yield* _(LoadQuery);
721
- const onQuery = yield* _(OnQuery);
722
- const { subscribeQuery, getQuery, getSubscribedQueries } =
723
- yield* _(SubscribedQueries);
724
- const syncStateStore = yield* _(
725
- makeStore<SyncState>({ _tag: "SyncStateInitial" }),
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
- const mutate = yield* _(Mutate);
728
- const ownerStore = yield* _(makeStore<Owner | null>(null));
729
- const loadingPromises = yield* _(LoadingPromises);
730
- const appState = yield* _(AppState);
731
-
732
- dbWorker.onMessage = (output): void =>
733
- Match.value(output).pipe(
734
- Match.tagsExhaustive({
735
- onError: ({ error }) => errorStore.setState(error),
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
- dbWorker.postMessage({
752
- _tag: "init",
753
- config: yield* _(Config),
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 sync = (): void => {
757
- dbWorker.postMessage({ _tag: "sync", queries: getSubscribedQueries() });
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
- appState.init({ onRequestSync: sync });
761
- sync();
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
- return Evolu.of({
758
+ const evolu: Evolu = {
764
759
  subscribeError: errorStore.subscribe,
765
760
  getError: errorStore.getState,
766
761
 
767
- createQuery: makeCreateQuery(),
768
- loadQuery,
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>>(queries: [...Q]) =>
771
- queries.map(loadQuery) as [...QueryResultsPromisesFromQueries<Q>],
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
- getQuery,
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<DatabaseSchema, "create">,
846
+ create: mutate as Mutate<EvoluSchema, "create">,
783
847
  update: mutate,
784
- createOrUpdate: mutate as Mutate<DatabaseSchema, "createOrUpdate">,
848
+ createOrUpdate: mutate as Mutate<EvoluSchema, "createOrUpdate">,
785
849
 
786
- resetOwner: () => dbWorker.postMessage({ _tag: "reset" }),
850
+ resetOwner: () => {
851
+ db.resetOwner().pipe(Effect.zipRight(appStateReset.reset), runFork);
852
+ },
787
853
 
788
- restoreOwner: (mnemonic) =>
789
- dbWorker.postMessage({ _tag: "reset", mnemonic }),
854
+ restoreOwner: (mnemonic) => {
855
+ db.restoreOwner(schema, mnemonic).pipe(
856
+ Effect.zipRight(appStateReset.reset),
857
+ runFork,
858
+ );
859
+ },
790
860
 
791
- ensureSchema: (schema, indexes = []) =>
792
- dbWorker.postMessage({
793
- _tag: "ensureSchema",
794
- tables: schemaToTables(schema),
795
- indexes,
796
- }),
861
+ ensureSchema: (schema) => {
862
+ db.ensureSchema(schema).pipe(runFork);
863
+ },
864
+ };
797
865
 
798
- sync,
799
- });
800
- }),
801
- ).pipe(
802
- Layer.provide(Layer.mergeAll(LoadQueryLive, OnQueryLive, MutateLive)),
803
- Layer.provide(LoadingPromiseLive),
804
- Layer.provide(SubscribedQueriesLive),
805
- Layer.provide(Layer.merge(RowsStoreLive, OnCompletesLive)),
806
- );
807
-
808
- export const EvoluCommonLive = EvoluCommon.pipe(
809
- Layer.provide(NanoIdGeneratorLive),
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
- * The recipe for creating Evolu for a platform and UI library:
814
- *
815
- * 1. Export everything from "@evolu/common/public"
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
- export const makeCreateEvolu =
824
- (EvoluLive: Layer.Layer<Evolu, never, Config>) =>
825
- <From, To extends DatabaseSchema>(
826
- schema: S.Schema<To, From>,
827
- config?: Partial<Config>,
828
- ): Evolu<To> => {
829
- // For https://nextjs.org/docs/architecture/fast-refresh etc.
830
- const evolu = GlobalValue.globalValue("@evolu/common", () =>
831
- Evolu.pipe(
832
- Effect.provide(EvoluLive),
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
- const indexes = config?.indexes?.map(
839
- (index): Index => ({
840
- name: index.toOperationNode().name.name,
841
- sql: index.compile().sql,
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
- evolu.ensureSchema(schema, indexes);
846
- return evolu as Evolu<To>;
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}`);