@evolu/common 5.4.7 → 6.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.
- package/README.md +30 -34
- package/dist/src/Array.d.ts +17 -0
- package/dist/src/Array.d.ts.map +1 -0
- package/dist/src/Array.js +12 -0
- package/dist/src/Assert.d.ts +68 -0
- package/dist/src/Assert.d.ts.map +1 -0
- package/dist/src/Assert.js +77 -0
- package/dist/src/BigInt.d.ts +20 -0
- package/dist/src/BigInt.d.ts.map +1 -0
- package/dist/src/BigInt.js +18 -0
- package/dist/src/Buffer.d.ts +92 -0
- package/dist/src/Buffer.d.ts.map +1 -0
- package/dist/src/Buffer.js +62 -0
- package/dist/src/Callbacks.d.ts +20 -0
- package/dist/src/Callbacks.d.ts.map +1 -0
- package/dist/src/Callbacks.js +18 -0
- package/dist/src/Console.d.ts +78 -0
- package/dist/src/Console.d.ts.map +1 -0
- package/dist/src/Console.js +103 -0
- package/dist/src/Crypto.d.ts +72 -39
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +89 -54
- package/dist/src/Eq.d.ts +97 -0
- package/dist/src/Eq.d.ts.map +1 -0
- package/dist/src/Eq.js +167 -0
- package/dist/src/Error.d.ts +14 -10
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +43 -11
- package/dist/src/Evolu/Config.d.ts +69 -0
- package/dist/src/Evolu/Config.d.ts.map +1 -0
- package/dist/src/Evolu/Config.js +9 -0
- package/dist/src/Evolu/Db.d.ts +126 -0
- package/dist/src/Evolu/Db.d.ts.map +1 -0
- package/dist/src/Evolu/Db.js +774 -0
- package/dist/src/Evolu/Diff.d.ts +43 -0
- package/dist/src/Evolu/Diff.d.ts.map +1 -0
- package/dist/src/Evolu/Diff.js +95 -0
- package/dist/src/Evolu/Evolu.d.ts +334 -0
- package/dist/src/Evolu/Evolu.d.ts.map +1 -0
- package/dist/src/Evolu/Evolu.js +434 -0
- package/dist/src/Evolu/Internal.d.ts +26 -0
- package/dist/src/Evolu/Internal.d.ts.map +1 -0
- package/dist/src/Evolu/Internal.js +25 -0
- package/dist/src/Evolu/Kysely.d.ts +6 -0
- package/dist/src/Evolu/Kysely.d.ts.map +1 -0
- package/dist/src/Evolu/Kysely.js +21 -0
- package/dist/src/Evolu/Owner.d.ts +155 -0
- package/dist/src/Evolu/Owner.d.ts.map +1 -0
- package/dist/src/Evolu/Owner.js +126 -0
- package/dist/src/Evolu/Platform.d.ts +23 -0
- package/dist/src/Evolu/Platform.d.ts.map +1 -0
- package/dist/src/Evolu/Platform.js +1 -0
- package/dist/src/Evolu/Protocol.d.ts +401 -0
- package/dist/src/Evolu/Protocol.d.ts.map +1 -0
- package/dist/src/Evolu/Protocol.js +1151 -0
- package/dist/src/Evolu/Public.d.ts +18 -0
- package/dist/src/Evolu/Public.d.ts.map +1 -0
- package/dist/src/Evolu/Public.js +11 -0
- package/dist/src/Evolu/PublicKysely.d.ts +148 -0
- package/dist/src/Evolu/PublicKysely.d.ts.map +1 -0
- package/dist/src/Evolu/PublicKysely.js +185 -0
- package/dist/src/Evolu/Query.d.ts +63 -0
- package/dist/src/Evolu/Query.d.ts.map +1 -0
- package/dist/src/Evolu/Query.js +61 -0
- package/dist/src/Evolu/Relay.d.ts +13 -0
- package/dist/src/Evolu/Relay.d.ts.map +1 -0
- package/dist/src/Evolu/Relay.js +109 -0
- package/dist/src/Evolu/Schema.d.ts +201 -0
- package/dist/src/Evolu/Schema.d.ts.map +1 -0
- package/dist/src/Evolu/Schema.js +150 -0
- package/dist/src/Evolu/Storage.d.ts +49 -0
- package/dist/src/Evolu/Storage.d.ts.map +1 -0
- package/dist/src/Evolu/Storage.js +1111 -0
- package/dist/src/Evolu/Sync.d.ts +59 -0
- package/dist/src/Evolu/Sync.d.ts.map +1 -0
- package/dist/src/Evolu/Sync.js +29 -0
- package/dist/src/Evolu/Timestamp.d.ts +106 -0
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -0
- package/dist/src/Evolu/Timestamp.js +179 -0
- package/dist/src/Function.d.ts +54 -0
- package/dist/src/Function.d.ts.map +1 -0
- package/dist/src/Function.js +38 -0
- package/dist/src/ManyToManyMap.d.ts +26 -0
- package/dist/src/ManyToManyMap.d.ts.map +1 -0
- package/dist/src/ManyToManyMap.js +92 -0
- package/dist/src/NanoId.d.ts +27 -0
- package/dist/src/NanoId.d.ts.map +1 -0
- package/dist/src/NanoId.js +6 -0
- package/dist/src/Number.d.ts +42 -0
- package/dist/src/Number.d.ts.map +1 -0
- package/dist/src/Number.js +55 -0
- package/dist/src/Object.d.ts +35 -0
- package/dist/src/Object.d.ts.map +1 -0
- package/dist/src/Object.js +36 -0
- package/dist/src/Order.d.ts +90 -0
- package/dist/src/Order.d.ts.map +1 -0
- package/dist/src/Order.js +85 -0
- package/dist/src/Promise.d.ts +180 -0
- package/dist/src/Promise.d.ts.map +1 -0
- package/dist/src/Promise.js +176 -0
- package/dist/src/Random.d.ts +52 -0
- package/dist/src/Random.d.ts.map +1 -0
- package/dist/src/Random.js +29 -0
- package/dist/src/Ref.d.ts +40 -0
- package/dist/src/Ref.d.ts.map +1 -0
- package/dist/src/Ref.js +13 -0
- package/dist/src/Result.d.ts +421 -0
- package/dist/src/Result.d.ts.map +1 -0
- package/dist/src/Result.js +357 -0
- package/dist/src/Skiplist.d.ts +23 -0
- package/dist/src/Skiplist.d.ts.map +1 -0
- package/dist/src/Skiplist.js +58 -0
- package/dist/src/Sqlite.d.ts +116 -52
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +183 -67
- package/dist/src/Store.d.ts +45 -8
- package/dist/src/Store.d.ts.map +1 -1
- package/dist/src/Store.js +33 -17
- package/dist/src/String.d.ts +2 -0
- package/dist/src/String.d.ts.map +1 -0
- package/dist/src/String.js +14 -0
- package/dist/src/Time.d.ts +20 -0
- package/dist/src/Time.d.ts.map +1 -0
- package/dist/src/Time.js +25 -0
- package/dist/src/Type.d.ts +1937 -0
- package/dist/src/Type.d.ts.map +1 -0
- package/dist/src/Type.js +2002 -0
- package/dist/src/Types.d.ts +188 -0
- package/dist/src/Types.d.ts.map +1 -0
- package/dist/src/Types.js +6 -0
- package/dist/src/WebSocket.d.ts +112 -0
- package/dist/src/WebSocket.d.ts.map +1 -0
- package/dist/src/WebSocket.js +139 -0
- package/dist/src/Worker.d.ts +44 -0
- package/dist/src/Worker.d.ts.map +1 -0
- package/dist/src/Worker.js +66 -0
- package/dist/src/index.d.ts +24 -11
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +24 -11
- package/package.json +29 -38
- package/src/Array.ts +39 -0
- package/src/Assert.ts +116 -0
- package/src/BigInt.ts +29 -0
- package/src/Buffer.ts +175 -0
- package/src/Callbacks.ts +43 -0
- package/src/Console.ts +159 -0
- package/src/Crypto.ts +169 -115
- package/src/Eq.ts +204 -0
- package/src/Error.ts +57 -20
- package/src/Evolu/Config.ts +83 -0
- package/src/Evolu/Db.ts +1275 -0
- package/src/Evolu/Diff.ts +142 -0
- package/src/Evolu/Evolu.ts +947 -0
- package/src/Evolu/Internal.ts +26 -0
- package/src/Evolu/Kysely.ts +38 -0
- package/src/Evolu/Owner.ts +296 -0
- package/src/Evolu/Platform.ts +27 -0
- package/src/Evolu/Protocol.ts +1857 -0
- package/src/Evolu/Public.ts +43 -0
- package/src/Evolu/PublicKysely.ts +240 -0
- package/src/Evolu/Query.ts +167 -0
- package/src/Evolu/Relay.ts +142 -0
- package/src/Evolu/Schema.ts +417 -0
- package/src/Evolu/Storage.ts +1281 -0
- package/src/Evolu/Sync.ts +105 -0
- package/src/Evolu/Timestamp.ts +311 -0
- package/src/Function.ts +58 -0
- package/src/ManyToManyMap.ts +140 -0
- package/src/NanoId.ts +39 -0
- package/src/Number.ts +90 -0
- package/src/Object.ts +64 -0
- package/src/Order.ts +113 -0
- package/src/Promise.ts +295 -0
- package/src/Random.ts +68 -0
- package/src/Ref.ts +63 -0
- package/src/Result.ts +453 -0
- package/src/Skiplist.ts +102 -0
- package/src/Sqlite.ts +366 -153
- package/src/Store.ts +79 -36
- package/src/String.ts +10 -0
- package/src/Time.ts +36 -0
- package/src/Type.ts +3978 -0
- package/src/Types.ts +209 -0
- package/src/WebSocket.ts +273 -0
- package/src/Worker.ts +129 -0
- package/src/index.ts +24 -11
- package/dist/src/Config.d.ts +0 -56
- package/dist/src/Config.d.ts.map +0 -1
- package/dist/src/Config.js +0 -39
- package/dist/src/Crdt.d.ts +0 -89
- package/dist/src/Crdt.d.ts.map +0 -1
- package/dist/src/Crdt.js +0 -181
- package/dist/src/Db.d.ts +0 -107
- package/dist/src/Db.d.ts.map +0 -1
- package/dist/src/Db.js +0 -443
- package/dist/src/Diff.d.ts +0 -27
- package/dist/src/Diff.d.ts.map +0 -1
- package/dist/src/Diff.js +0 -84
- package/dist/src/Evolu.d.ts +0 -426
- package/dist/src/Evolu.d.ts.map +0 -1
- package/dist/src/Evolu.js +0 -333
- package/dist/src/Model.d.ts +0 -141
- package/dist/src/Model.d.ts.map +0 -1
- package/dist/src/Model.js +0 -125
- package/dist/src/Murmurhash.d.ts +0 -2
- package/dist/src/Murmurhash.d.ts.map +0 -1
- package/dist/src/Murmurhash.js +0 -60
- package/dist/src/Owner.d.ts +0 -33
- package/dist/src/Owner.d.ts.map +0 -1
- package/dist/src/Owner.js +0 -26
- package/dist/src/Platform.d.ts +0 -37
- package/dist/src/Platform.d.ts.map +0 -1
- package/dist/src/Platform.js +0 -11
- package/dist/src/Protobuf.d.ts +0 -81
- package/dist/src/Protobuf.d.ts.map +0 -1
- package/dist/src/Protobuf.js +0 -92
- package/dist/src/Public.d.ts +0 -13
- package/dist/src/Public.d.ts.map +0 -1
- package/dist/src/Public.js +0 -6
- package/dist/src/Socket.d.ts +0 -8
- package/dist/src/Socket.d.ts.map +0 -1
- package/dist/src/Socket.js +0 -51
- package/dist/src/Sql.d.ts +0 -12
- package/dist/src/Sql.d.ts.map +0 -1
- package/dist/src/Sql.js +0 -30
- package/dist/src/Sync.d.ts +0 -70
- package/dist/src/Sync.d.ts.map +0 -1
- package/dist/src/Sync.js +0 -127
- package/src/Config.ts +0 -119
- package/src/Crdt.ts +0 -361
- package/src/Db.ts +0 -955
- package/src/Diff.ts +0 -114
- package/src/Evolu.ts +0 -1016
- package/src/Model.ts +0 -233
- package/src/Murmurhash.ts +0 -70
- package/src/Owner.ts +0 -69
- package/src/Platform.ts +0 -47
- package/src/Protobuf.ts +0 -155
- package/src/Public.ts +0 -12
- package/src/Socket.ts +0 -83
- package/src/Sql.ts +0 -41
- package/src/Sync.ts +0 -315
package/src/Evolu.ts
DELETED
|
@@ -1,1016 +0,0 @@
|
|
|
1
|
-
import * as AST from "@effect/schema/AST";
|
|
2
|
-
import * as S from "@effect/schema/Schema";
|
|
3
|
-
import { make } from "@effect/schema/Schema";
|
|
4
|
-
import * as Arr from "effect/Array";
|
|
5
|
-
import * as Context from "effect/Context";
|
|
6
|
-
import * as Effect from "effect/Effect";
|
|
7
|
-
import { constVoid, flow, pipe } from "effect/Function";
|
|
8
|
-
import * as Layer from "effect/Layer";
|
|
9
|
-
import * as ManagedRuntime from "effect/ManagedRuntime";
|
|
10
|
-
import * as Number from "effect/Number";
|
|
11
|
-
import * as Option from "effect/Option";
|
|
12
|
-
import * as Predicate from "effect/Predicate";
|
|
13
|
-
import * as Record from "effect/Record";
|
|
14
|
-
import * as Kysely from "kysely";
|
|
15
|
-
import { Config, createRuntime, defaultConfig } from "./Config.js";
|
|
16
|
-
import { TimestampString } from "./Crdt.js";
|
|
17
|
-
import { Mnemonic, NanoIdGenerator } from "./Crypto.js";
|
|
18
|
-
import { createSocket } from "./Socket.js";
|
|
19
|
-
import {
|
|
20
|
-
DbFactory,
|
|
21
|
-
DbSchema,
|
|
22
|
-
Index,
|
|
23
|
-
Mutation,
|
|
24
|
-
Queries,
|
|
25
|
-
Query,
|
|
26
|
-
QueryResult,
|
|
27
|
-
QueryResultsPromisesFromQueries,
|
|
28
|
-
QueryRowsMap,
|
|
29
|
-
Row,
|
|
30
|
-
Table,
|
|
31
|
-
deserializeQuery,
|
|
32
|
-
emptyRows,
|
|
33
|
-
queryResultFromRows,
|
|
34
|
-
serializeQuery,
|
|
35
|
-
} from "./Db.js";
|
|
36
|
-
import { QueryPatches, applyPatches } from "./Diff.js";
|
|
37
|
-
import { EvoluError, makeUnexpectedError } from "./Error.js";
|
|
38
|
-
import { Id, SqliteBoolean, SqliteDate } from "./Model.js";
|
|
39
|
-
import { Owner } from "./Owner.js";
|
|
40
|
-
import { AppState, FlushSync } from "./Platform.js";
|
|
41
|
-
import {
|
|
42
|
-
SqliteQuery,
|
|
43
|
-
SqliteQueryOptions,
|
|
44
|
-
Value,
|
|
45
|
-
isSqlMutation,
|
|
46
|
-
} from "./Sqlite.js";
|
|
47
|
-
import { Listener, Unsubscribe, makeStore } from "./Store.js";
|
|
48
|
-
import { SyncState, initialSyncState } from "./Sync.js";
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* The Evolu interface provides a type-safe SQL query building and state
|
|
52
|
-
* management defined by a database schema. It leverages Kysely for creating SQL
|
|
53
|
-
* queries in TypeScript, enabling operations such as data querying, loading,
|
|
54
|
-
* subscription to data changes, and mutations (create, update, createOrUpdate).
|
|
55
|
-
* It also includes functionalities for error handling, syncing state
|
|
56
|
-
* management, and owner data manipulation. Specifically, Evolu allows:
|
|
57
|
-
*
|
|
58
|
-
* - Subscribing to and getting errors via subscribeError and getError.
|
|
59
|
-
* - Creating type-safe SQL queries with createQuery, leveraging Kysely's
|
|
60
|
-
* capabilities.
|
|
61
|
-
* - Loading queries and subscribing to query result changes using loadQuery,
|
|
62
|
-
* loadQueries, subscribeQuery, and getQuery.
|
|
63
|
-
* - Subscribing to and getting the owner's information and sync state changes.
|
|
64
|
-
* - Performing mutations on the database with create, update, and createOrUpdate
|
|
65
|
-
* methods, which include automatic management of common columns like
|
|
66
|
-
* createdAt, updatedAt, and isDeleted.
|
|
67
|
-
* - Managing owner data with resetOwner and restoreOwner.
|
|
68
|
-
* - Ensuring the database schema's integrity with ensureSchema.
|
|
69
|
-
*/
|
|
70
|
-
export interface Evolu<T extends EvoluSchema = EvoluSchema> {
|
|
71
|
-
readonly socket: Promise<WebSocket | undefined>;
|
|
72
|
-
/**
|
|
73
|
-
* Subscribe to {@link EvoluError} changes.
|
|
74
|
-
*
|
|
75
|
-
* @example
|
|
76
|
-
* const unsubscribe = evolu.subscribeError(() => {
|
|
77
|
-
* const error = evolu.getError();
|
|
78
|
-
* console.log(error);
|
|
79
|
-
* });
|
|
80
|
-
*/
|
|
81
|
-
readonly subscribeError: (listener: Listener) => Unsubscribe;
|
|
82
|
-
|
|
83
|
-
/** Get {@link EvoluError}. */
|
|
84
|
-
readonly getError: () => EvoluError | null;
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Create type-safe SQL {@link Query}.
|
|
88
|
-
*
|
|
89
|
-
* Evolu uses Kysely - the type-safe SQL query builder for TypeScript. See
|
|
90
|
-
* https://kysely.dev.
|
|
91
|
-
*
|
|
92
|
-
* For mutations, use {@link create} and {@link update}.
|
|
93
|
-
*
|
|
94
|
-
* @example
|
|
95
|
-
* const allTodos = evolu.createQuery((db) =>
|
|
96
|
-
* db.selectFrom("todo").selectAll(),
|
|
97
|
-
* );
|
|
98
|
-
*
|
|
99
|
-
* const todoById = (id: TodoId) =>
|
|
100
|
-
* evolu.createQuery((db) =>
|
|
101
|
-
* db.selectFrom("todo").selectAll().where("id", "=", id),
|
|
102
|
-
* );
|
|
103
|
-
*/
|
|
104
|
-
readonly createQuery: <R extends Row>(
|
|
105
|
-
queryCallback: (
|
|
106
|
-
db: Pick<
|
|
107
|
-
Kysely.Kysely<
|
|
108
|
-
{
|
|
109
|
-
[Table in keyof T]: NullableExceptIdCreatedAtUpdatedAt<{
|
|
110
|
-
[Column in keyof T[Table]]: T[Table][Column];
|
|
111
|
-
}>;
|
|
112
|
-
} & {
|
|
113
|
-
readonly evolu_message: {
|
|
114
|
-
readonly timestamp: TimestampString;
|
|
115
|
-
readonly table: keyof T;
|
|
116
|
-
readonly row: Id;
|
|
117
|
-
readonly column: string;
|
|
118
|
-
readonly value: Value;
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
>,
|
|
122
|
-
"selectFrom" | "fn" | "with" | "withRecursive"
|
|
123
|
-
>,
|
|
124
|
-
) => Kysely.SelectQueryBuilder<any, any, R>,
|
|
125
|
-
options?: SqliteQueryOptions,
|
|
126
|
-
) => Query<R>;
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Load {@link Query} and return a promise with {@link QueryResult}.
|
|
130
|
-
*
|
|
131
|
-
* A returned promise always resolves successfully because there is no reason
|
|
132
|
-
* why loading should fail. All data are local, and the query is typed. A
|
|
133
|
-
* serious unexpected Evolu error shall be handled with
|
|
134
|
-
* {@link subscribeError}.
|
|
135
|
-
*
|
|
136
|
-
* Loading is batched, and returned promises are cached, so there is no need
|
|
137
|
-
* for an additional cache. Evolu's internal cache is invalidated on
|
|
138
|
-
* mutation.
|
|
139
|
-
*
|
|
140
|
-
* The returned promise is enriched with special status and value properties
|
|
141
|
-
* for the upcoming React `use` Hook, but other UI libraries can also leverage
|
|
142
|
-
* them. Speaking of React, there are two essential React Suspense-related
|
|
143
|
-
* patterns that every developer should be aware of—passing promises to
|
|
144
|
-
* children and caching over mutations.
|
|
145
|
-
*
|
|
146
|
-
* With promises passed to children, we can load a query as soon as possible,
|
|
147
|
-
* but we don't have to use the returned promise immediately. That's useful
|
|
148
|
-
* for prefetching, which is generally not necessary for local-first apps but
|
|
149
|
-
* can be if a query takes a long time to load.
|
|
150
|
-
*
|
|
151
|
-
* Caching over mutation is a pattern that every developer should know. As we
|
|
152
|
-
* said, Evolu caches promise until a mutation happens. A query loaded after
|
|
153
|
-
* that will return a new pending promise. That's okay for general usage but
|
|
154
|
-
* not for UI with React Suspense because a mutation would suspend rerendered
|
|
155
|
-
* queries on a page, and that's not a good UX.
|
|
156
|
-
*
|
|
157
|
-
* We call this pattern "caching over mutation" because it has no globally
|
|
158
|
-
* accepted name yet. React RFC for React Cache does not exist yet.
|
|
159
|
-
*
|
|
160
|
-
* For better UX, a query must be subscribed for updates. This way, instead of
|
|
161
|
-
* Suspense flashes, the user sees new data immediately because Evolu replaces
|
|
162
|
-
* cached promises with fresh, already resolved new ones.
|
|
163
|
-
*
|
|
164
|
-
* If you are curious why Evolu does not do that for all queries by default,
|
|
165
|
-
* the answer is simple: performance. Tracking changes is costly and
|
|
166
|
-
* meaningful only for visible (hence subscribed) queries anyway. To subscribe
|
|
167
|
-
* to a query, use {@link subscribeQuery}.
|
|
168
|
-
*
|
|
169
|
-
* @example
|
|
170
|
-
* const allTodos = evolu.createQuery((db) =>
|
|
171
|
-
* db.selectFrom("todo").selectAll(),
|
|
172
|
-
* );
|
|
173
|
-
* evolu.loadQuery(allTodos).then(({ rows }) => {
|
|
174
|
-
* console.log(rows);
|
|
175
|
-
* });
|
|
176
|
-
*/
|
|
177
|
-
readonly loadQuery: <R extends Row>(
|
|
178
|
-
query: Query<R>,
|
|
179
|
-
) => Promise<QueryResult<R>>;
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Load an array of {@link Query} queries and return an array of
|
|
183
|
-
* {@link QueryResult} promises. It's like `queries.map(loadQuery)` but with
|
|
184
|
-
* proper types for returned promises.
|
|
185
|
-
*
|
|
186
|
-
* @example
|
|
187
|
-
* evolu.loadQueries([allTodos, todoById(1)]);
|
|
188
|
-
*/
|
|
189
|
-
readonly loadQueries: <R extends Row, Q extends Queries<R>>(
|
|
190
|
-
queries: [...Q],
|
|
191
|
-
) => [...QueryResultsPromisesFromQueries<Q>];
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Subscribe to {@link Query} {@link QueryResult} changes.
|
|
195
|
-
*
|
|
196
|
-
* @example
|
|
197
|
-
* const unsubscribe = evolu.subscribeQuery(allTodos)(() => {
|
|
198
|
-
* const { rows } = evolu.getQuery(allTodos);
|
|
199
|
-
* });
|
|
200
|
-
*/
|
|
201
|
-
readonly subscribeQuery: (
|
|
202
|
-
query: Query,
|
|
203
|
-
) => (listener: Listener) => Unsubscribe;
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Get {@link Query} {@link QueryResult}.
|
|
207
|
-
*
|
|
208
|
-
* @example
|
|
209
|
-
* const unsubscribe = evolu.subscribeQuery(allTodos)(() => {
|
|
210
|
-
* const { rows } = evolu.getQuery(allTodos);
|
|
211
|
-
* });
|
|
212
|
-
*/
|
|
213
|
-
readonly getQuery: <R extends Row>(query: Query<R>) => QueryResult<R>;
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Subscribe to {@link Owner} changes.
|
|
217
|
-
*
|
|
218
|
-
* @example
|
|
219
|
-
* const unsubscribe = evolu.subscribeOwner(() => {
|
|
220
|
-
* const owner = evolu.getOwner();
|
|
221
|
-
* });
|
|
222
|
-
*/
|
|
223
|
-
readonly subscribeOwner: (listener: Listener) => Unsubscribe;
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Get {@link Owner}.
|
|
227
|
-
*
|
|
228
|
-
* @example
|
|
229
|
-
* const unsubscribe = evolu.subscribeOwner(() => {
|
|
230
|
-
* const owner = evolu.getOwner();
|
|
231
|
-
* });
|
|
232
|
-
*/
|
|
233
|
-
readonly getOwner: () => Owner | null;
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Subscribe to {@link SyncState} changes.
|
|
237
|
-
*
|
|
238
|
-
* @example
|
|
239
|
-
* const unsubscribe = evolu.subscribeSyncState(() => {
|
|
240
|
-
* const syncState = evolu.getSyncState();
|
|
241
|
-
* });
|
|
242
|
-
*/
|
|
243
|
-
readonly subscribeSyncState: (listener: Listener) => Unsubscribe;
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Get {@link SyncState}.
|
|
247
|
-
*
|
|
248
|
-
* @example
|
|
249
|
-
* const unsubscribe = evolu.subscribeSyncState(() => {
|
|
250
|
-
* const syncState = evolu.getSyncState();
|
|
251
|
-
* });
|
|
252
|
-
*/
|
|
253
|
-
readonly getSyncState: () => SyncState;
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Create a row in the database and returns a new ID. The first argument is
|
|
257
|
-
* the table name, and the second is an object.
|
|
258
|
-
*
|
|
259
|
-
* The third optional argument, the onComplete callback, is generally
|
|
260
|
-
* unnecessary because creating a row cannot fail. Still, UI libraries can use
|
|
261
|
-
* it to ensure the DOM is updated if we want to manipulate it, for example,
|
|
262
|
-
* to focus an element.
|
|
263
|
-
*
|
|
264
|
-
* Evolu does not use SQL for mutations to ensure data can be safely and
|
|
265
|
-
* predictably merged without conflicts.
|
|
266
|
-
*
|
|
267
|
-
* Explicit mutations also allow Evolu to automatically add and update a few
|
|
268
|
-
* useful columns common to all tables. Those columns are: `createdAt`,
|
|
269
|
-
* `updatedAt`, and `isDeleted`.
|
|
270
|
-
*
|
|
271
|
-
* @example
|
|
272
|
-
* import * as S from "@effect/schema/Schema";
|
|
273
|
-
*
|
|
274
|
-
* // Evolu uses the Schema to enforce domain model.
|
|
275
|
-
* const title = S.decodeSync(Evolu.NonEmptyString1000)("A title");
|
|
276
|
-
*
|
|
277
|
-
* const { id } = evolu.create("todo", { title }, () => {
|
|
278
|
-
* // onComplete callback
|
|
279
|
-
* });
|
|
280
|
-
*/
|
|
281
|
-
create: Mutate<T, "create">;
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Update a row in the database and return the existing ID. The first argument
|
|
285
|
-
* is the table name, and the second is an object.
|
|
286
|
-
*
|
|
287
|
-
* The third optional argument, the onComplete callback, is generally
|
|
288
|
-
* unnecessary because updating a row cannot fail. Still, UI libraries can use
|
|
289
|
-
* it to ensure the DOM is updated if we want to manipulate it, for example,
|
|
290
|
-
* to focus an element.
|
|
291
|
-
*
|
|
292
|
-
* Evolu does not use SQL for mutations to ensure data can be safely and
|
|
293
|
-
* predictably merged without conflicts.
|
|
294
|
-
*
|
|
295
|
-
* Explicit mutations also allow Evolu to automatically add and update a few
|
|
296
|
-
* useful columns common to all tables. Those columns are: `createdAt`,
|
|
297
|
-
* `updatedAt`, and `isDeleted`.
|
|
298
|
-
*
|
|
299
|
-
* @example
|
|
300
|
-
* import * as S from "@effect/schema/Schema";
|
|
301
|
-
*
|
|
302
|
-
* // Evolu uses the Schema to enforce domain model.
|
|
303
|
-
* const title = S.decodeSync(Evolu.NonEmptyString1000)("A title");
|
|
304
|
-
* evolu.update("todo", { id, title });
|
|
305
|
-
*
|
|
306
|
-
* // To delete a row, set `isDeleted` to true.
|
|
307
|
-
* evolu.update("todo", { id, isDeleted: true });
|
|
308
|
-
*/
|
|
309
|
-
update: Mutate<T, "update">;
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Create or update a row in the database and return the existing ID. The
|
|
313
|
-
* first argument is the table name, and the second is an object.
|
|
314
|
-
*
|
|
315
|
-
* This function is useful when we already have an `id` and want to create a
|
|
316
|
-
* new row or update an existing one.
|
|
317
|
-
*
|
|
318
|
-
* The third optional argument, the onComplete callback, is generally
|
|
319
|
-
* unnecessary because updating a row cannot fail. Still, UI libraries can use
|
|
320
|
-
* it to ensure the DOM is updated if we want to manipulate it, for example,
|
|
321
|
-
* to focus an element.
|
|
322
|
-
*
|
|
323
|
-
* Evolu does not use SQL for mutations to ensure data can be safely and
|
|
324
|
-
* predictably merged without conflicts.
|
|
325
|
-
*
|
|
326
|
-
* Explicit mutations also allow Evolu to automatically add and update a few
|
|
327
|
-
* useful columns common to all tables. Those columns are: `createdAt`,
|
|
328
|
-
* `updatedAt`, and `isDeleted`.
|
|
329
|
-
*
|
|
330
|
-
* @example
|
|
331
|
-
* import * as S from "@effect/schema/Schema";
|
|
332
|
-
* import { Id } from "@evolu/react";
|
|
333
|
-
*
|
|
334
|
-
* // Id can be stable.
|
|
335
|
-
* // 2024-02-0800000000000
|
|
336
|
-
* const id = S.decodeSync(Id)(date.toString().padEnd(21, "0")) as TodoId;
|
|
337
|
-
*
|
|
338
|
-
* evolu.createOrUpdate("todo", { id, title });
|
|
339
|
-
*/
|
|
340
|
-
createOrUpdate: Mutate<T, "createOrUpdate">;
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Delete {@link Owner} and all their data from the current device. After the
|
|
344
|
-
* deletion, Evolu will purge the application state. For browsers, this will
|
|
345
|
-
* reload all tabs using Evolu. For native apps, it will restart the app.
|
|
346
|
-
*
|
|
347
|
-
* Reloading can be turned off via options if you want to provide a different
|
|
348
|
-
* UX.
|
|
349
|
-
*/
|
|
350
|
-
readonly resetOwner: (options?: {
|
|
351
|
-
readonly reload: boolean;
|
|
352
|
-
}) => Promise<void>;
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Restore {@link Owner} with all their synced data. It uses {@link resetOwner},
|
|
356
|
-
* so be careful.
|
|
357
|
-
*/
|
|
358
|
-
readonly restoreOwner: (
|
|
359
|
-
mnemonic: Mnemonic,
|
|
360
|
-
options?: {
|
|
361
|
-
readonly reload: boolean;
|
|
362
|
-
},
|
|
363
|
-
) => Promise<void>;
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Reload the app in a platform-specific way. For browsers, this will reload
|
|
367
|
-
* all tabs using Evolu. For native apps, it will restart the app.
|
|
368
|
-
*/
|
|
369
|
-
readonly reloadApp: () => void;
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Ensure tables and columns defined in {@link EvoluSchema} exist in the
|
|
373
|
-
* database.
|
|
374
|
-
*
|
|
375
|
-
* This function is for hot/live reloading.
|
|
376
|
-
*/
|
|
377
|
-
readonly ensureSchema: (schema: DbSchema) => void;
|
|
378
|
-
|
|
379
|
-
/** Export SQLite database as Uint8Array. */
|
|
380
|
-
readonly exportDatabase: () => Promise<Uint8Array>;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/** A type to define tables, columns, and column types. */
|
|
384
|
-
export type EvoluSchema = Record.ReadonlyRecord<
|
|
385
|
-
string,
|
|
386
|
-
Record.ReadonlyRecord<string, Value> & {
|
|
387
|
-
readonly id: Id;
|
|
388
|
-
}
|
|
389
|
-
>;
|
|
390
|
-
|
|
391
|
-
type NullableExceptIdCreatedAtUpdatedAt<T> = {
|
|
392
|
-
readonly [K in keyof T]: K extends "id" | "createdAt" | "updatedAt"
|
|
393
|
-
? T[K]
|
|
394
|
-
: T[K] | null;
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
type Mutate<
|
|
398
|
-
T extends EvoluSchema = EvoluSchema,
|
|
399
|
-
Mode extends "create" | "update" | "createOrUpdate" = "update",
|
|
400
|
-
> = <K extends keyof T>(
|
|
401
|
-
table: K,
|
|
402
|
-
values: Kysely.Simplify<
|
|
403
|
-
Mode extends "create"
|
|
404
|
-
? PartialForNullable<
|
|
405
|
-
Castable<Omit<T[K], "id" | "createdAt" | "updatedAt" | "isDeleted">>
|
|
406
|
-
>
|
|
407
|
-
: Mode extends "update"
|
|
408
|
-
? Partial<Castable<Omit<T[K], "id" | "createdAt" | "updatedAt">>> & {
|
|
409
|
-
readonly id: T[K]["id"];
|
|
410
|
-
}
|
|
411
|
-
: PartialForNullable<
|
|
412
|
-
Castable<Omit<T[K], "createdAt" | "updatedAt" | "isDeleted">>
|
|
413
|
-
>
|
|
414
|
-
>,
|
|
415
|
-
onComplete?: MutateOnComplete,
|
|
416
|
-
) => {
|
|
417
|
-
readonly id: T[K]["id"];
|
|
418
|
-
};
|
|
419
|
-
|
|
420
|
-
type MutateOnComplete = () => void;
|
|
421
|
-
|
|
422
|
-
// https://stackoverflow.com/a/54713648/233902
|
|
423
|
-
type PartialForNullable<
|
|
424
|
-
T,
|
|
425
|
-
NK extends keyof T = {
|
|
426
|
-
[K in keyof T]: null extends T[K] ? K : never;
|
|
427
|
-
}[keyof T],
|
|
428
|
-
NP = Pick<T, Exclude<keyof T, NK>> & Partial<Pick<T, NK>>,
|
|
429
|
-
> = { [K in keyof NP]: NP[K] };
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* SQLite doesn't support Date nor Boolean types, so Evolu emulates them with
|
|
433
|
-
* {@link SqliteBoolean} and {@link SqliteDate}.
|
|
434
|
-
*/
|
|
435
|
-
type Castable<T> = {
|
|
436
|
-
readonly [K in keyof T]: T[K] extends SqliteBoolean
|
|
437
|
-
? boolean | SqliteBoolean
|
|
438
|
-
: T[K] extends null | SqliteBoolean
|
|
439
|
-
? null | boolean | SqliteBoolean
|
|
440
|
-
: T[K] extends SqliteDate
|
|
441
|
-
? Date | SqliteDate
|
|
442
|
-
: T[K] extends null | SqliteDate
|
|
443
|
-
? null | Date | SqliteDate
|
|
444
|
-
: T[K];
|
|
445
|
-
};
|
|
446
|
-
|
|
447
|
-
export class EvoluFactory extends Context.Tag("EvoluFactory")<
|
|
448
|
-
EvoluFactory,
|
|
449
|
-
{
|
|
450
|
-
/**
|
|
451
|
-
* Create Evolu from the database schema.
|
|
452
|
-
*
|
|
453
|
-
* Tables with a name prefixed with `_` are local-only, which means they are
|
|
454
|
-
* never synced. It's useful for device-specific or temporal data.
|
|
455
|
-
*
|
|
456
|
-
* @example
|
|
457
|
-
* import * as S from "@effect/schema/Schema";
|
|
458
|
-
* import * as E from "@evolu/react";
|
|
459
|
-
* // The same API for different platforms
|
|
460
|
-
* // import * as E from "@evolu/react-native";
|
|
461
|
-
* // import * as E from "@evolu/common-web";
|
|
462
|
-
*
|
|
463
|
-
* const TodoId = E.id("Todo");
|
|
464
|
-
* type TodoId = typeof TodoId.Type;
|
|
465
|
-
*
|
|
466
|
-
* const TodoTable = E.table({
|
|
467
|
-
* id: TodoId,
|
|
468
|
-
* title: E.NonEmptyString1000,
|
|
469
|
-
* });
|
|
470
|
-
* type TodoTable = typeof TodoTable.Type;
|
|
471
|
-
*
|
|
472
|
-
* const Database = E.database({
|
|
473
|
-
* todo: TodoTable,
|
|
474
|
-
*
|
|
475
|
-
* // Prefix `_` makes the table local-only (it will not sync)
|
|
476
|
-
* _todo: TodoTable,
|
|
477
|
-
* });
|
|
478
|
-
* type Database = typeof Database.Type;
|
|
479
|
-
*
|
|
480
|
-
* const evolu = E.createEvolu(Database);
|
|
481
|
-
*/
|
|
482
|
-
readonly createEvolu: <T extends EvoluSchema, I>(
|
|
483
|
-
schema: S.Schema<T, I>,
|
|
484
|
-
config?: Partial<EvoluConfig<T>>,
|
|
485
|
-
) => Evolu<T>;
|
|
486
|
-
}
|
|
487
|
-
>() {
|
|
488
|
-
static Common = Layer.effect(
|
|
489
|
-
EvoluFactory,
|
|
490
|
-
Effect.gen(function* () {
|
|
491
|
-
const flushSync = yield* Effect.map(
|
|
492
|
-
Effect.serviceOption(FlushSync),
|
|
493
|
-
Option.getOrElse<FlushSync>(() => (callback) => callback()),
|
|
494
|
-
);
|
|
495
|
-
|
|
496
|
-
const context = Context.empty().pipe(
|
|
497
|
-
Context.add(DbFactory, yield* DbFactory),
|
|
498
|
-
Context.add(NanoIdGenerator, yield* NanoIdGenerator),
|
|
499
|
-
Context.add(FlushSync, flushSync),
|
|
500
|
-
Context.add(AppState, yield* AppState),
|
|
501
|
-
);
|
|
502
|
-
|
|
503
|
-
// For hot/live reloading and future Evolu dynamic import.
|
|
504
|
-
const instances = new Map<string, Evolu>();
|
|
505
|
-
|
|
506
|
-
return EvoluFactory.of({
|
|
507
|
-
createEvolu: <T extends EvoluSchema, I>(
|
|
508
|
-
schema: S.Schema<T, I>,
|
|
509
|
-
{
|
|
510
|
-
indexes,
|
|
511
|
-
initialData,
|
|
512
|
-
mnemonic,
|
|
513
|
-
...config
|
|
514
|
-
}: Partial<EvoluConfig<T>> = {},
|
|
515
|
-
): Evolu<T> => {
|
|
516
|
-
const runtime = createRuntime(config);
|
|
517
|
-
const name = config?.name || defaultConfig.name;
|
|
518
|
-
const dbSchema: DbSchema = {
|
|
519
|
-
tables: schemaToTables(schema),
|
|
520
|
-
indexes: indexes || [],
|
|
521
|
-
};
|
|
522
|
-
let evolu = instances.get(name);
|
|
523
|
-
if (evolu == null) {
|
|
524
|
-
evolu = createEvolu(
|
|
525
|
-
dbSchema,
|
|
526
|
-
runtime,
|
|
527
|
-
initialData as EvoluConfig["initialData"],
|
|
528
|
-
mnemonic,
|
|
529
|
-
).pipe(Effect.provide(context), runtime.runSync);
|
|
530
|
-
instances.set(name, evolu);
|
|
531
|
-
} else {
|
|
532
|
-
evolu.ensureSchema(dbSchema);
|
|
533
|
-
}
|
|
534
|
-
return evolu as Evolu<T>;
|
|
535
|
-
},
|
|
536
|
-
});
|
|
537
|
-
}),
|
|
538
|
-
);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
export interface EvoluConfig<T extends EvoluSchema = EvoluSchema>
|
|
542
|
-
extends Config {
|
|
543
|
-
/**
|
|
544
|
-
* Use the `indexes` option to define SQLite indexes.
|
|
545
|
-
*
|
|
546
|
-
* Table and column names are not typed because Kysely doesn't support it.
|
|
547
|
-
*
|
|
548
|
-
* https://medium.com/@JasonWyatt/squeezing-performance-from-sqlite-indexes-indexes-c4e175f3c346
|
|
549
|
-
*
|
|
550
|
-
* @example
|
|
551
|
-
* const indexes = [
|
|
552
|
-
* createIndex("indexTodoCreatedAt").on("todo").column("createdAt"),
|
|
553
|
-
*
|
|
554
|
-
* createIndex("indexTodoCategoryCreatedAt")
|
|
555
|
-
* .on("todoCategory")
|
|
556
|
-
* .column("createdAt"),
|
|
557
|
-
* ];
|
|
558
|
-
*/
|
|
559
|
-
indexes: ReadonlyArray<Index>;
|
|
560
|
-
|
|
561
|
-
/** Use this option to create initial data (fixtures). */
|
|
562
|
-
initialData: (evolu: EvoluForInitialData<T>) => void;
|
|
563
|
-
|
|
564
|
-
/**
|
|
565
|
-
* Use this option to create Evolu with the specified mnemonic. If omitted,
|
|
566
|
-
* the mnemonic will be autogenerated. That should be the default behavior
|
|
567
|
-
* until special UX requirements are needed (e.g., multitenancy).
|
|
568
|
-
*/
|
|
569
|
-
mnemonic: Mnemonic;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
const schemaToTables = (schema: S.Schema<any>) =>
|
|
573
|
-
pipe(
|
|
574
|
-
getPropertySignatures(schema),
|
|
575
|
-
Record.toEntries,
|
|
576
|
-
Arr.map(
|
|
577
|
-
([name, schema]): Table => ({
|
|
578
|
-
name,
|
|
579
|
-
columns: Object.keys(getPropertySignatures(schema)),
|
|
580
|
-
}),
|
|
581
|
-
),
|
|
582
|
-
);
|
|
583
|
-
|
|
584
|
-
// TODO: Simplify.
|
|
585
|
-
// https://discord.com/channels/795981131316985866/1218626687546294386/1218796529725476935
|
|
586
|
-
const getPropertySignatures = <I extends { [K in keyof A]: any }, A>(
|
|
587
|
-
schema: S.Schema<A, I>,
|
|
588
|
-
): { [K in keyof A]: S.Schema<A[K], I[K]> } => {
|
|
589
|
-
const out: Record<PropertyKey, S.Schema<any>> = {};
|
|
590
|
-
const propertySignatures = AST.getPropertySignatures(schema.ast);
|
|
591
|
-
for (let i = 0; i < propertySignatures.length; i++) {
|
|
592
|
-
const propertySignature = propertySignatures[i];
|
|
593
|
-
out[propertySignature.name] = make(propertySignature.type);
|
|
594
|
-
}
|
|
595
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
596
|
-
return out as any;
|
|
597
|
-
};
|
|
598
|
-
|
|
599
|
-
const createEvolu = (
|
|
600
|
-
schema: DbSchema,
|
|
601
|
-
runtime: ManagedRuntime.ManagedRuntime<Config, never>,
|
|
602
|
-
initialData: EvoluConfig["initialData"],
|
|
603
|
-
mnemonic: Mnemonic | undefined,
|
|
604
|
-
) =>
|
|
605
|
-
Effect.gen(function* () {
|
|
606
|
-
yield* Effect.logTrace("EvoluFactory createEvolu");
|
|
607
|
-
const config = yield* Config;
|
|
608
|
-
const dbFactory = yield* DbFactory;
|
|
609
|
-
const appState = yield* AppState;
|
|
610
|
-
const nanoIdGenerator = yield* NanoIdGenerator;
|
|
611
|
-
const flushSync = yield* FlushSync;
|
|
612
|
-
|
|
613
|
-
const db = yield* dbFactory.createDb;
|
|
614
|
-
const errorStore = yield* makeStore<EvoluError | null>(null);
|
|
615
|
-
const ownerStore = yield* makeStore<Owner | null>(null);
|
|
616
|
-
const rowsStore = yield* makeStore<QueryRowsMap>(new Map());
|
|
617
|
-
const syncStateStore = yield* makeStore<SyncState>(initialSyncState);
|
|
618
|
-
|
|
619
|
-
const loadingPromises = new Map<Query, LoadingPromise>();
|
|
620
|
-
const subscribedQueries = new Map<Query, number>();
|
|
621
|
-
|
|
622
|
-
const handleAllErrors = <T>(effect: Effect.Effect<T, EvoluError, Config>) =>
|
|
623
|
-
effect.pipe(
|
|
624
|
-
Effect.catchAllDefect((error) =>
|
|
625
|
-
Effect.fail(makeUnexpectedError(error)),
|
|
626
|
-
),
|
|
627
|
-
Effect.tapError(Effect.logError),
|
|
628
|
-
Effect.tapError(errorStore.setState),
|
|
629
|
-
);
|
|
630
|
-
|
|
631
|
-
const runFork = flow(handleAllErrors, runtime.runFork);
|
|
632
|
-
const runSync = flow(handleAllErrors, runtime.runSync);
|
|
633
|
-
const runPromise = flow(handleAllErrors, runtime.runPromise);
|
|
634
|
-
|
|
635
|
-
const initialDataAsMutations = yield* Effect.provideService(
|
|
636
|
-
initialDataToMutations(initialData),
|
|
637
|
-
NanoIdGenerator,
|
|
638
|
-
nanoIdGenerator,
|
|
639
|
-
);
|
|
640
|
-
|
|
641
|
-
const handleDbError = (error: EvoluError) => {
|
|
642
|
-
Effect.fail(error).pipe(runFork);
|
|
643
|
-
};
|
|
644
|
-
|
|
645
|
-
const handleSyncStateChange = (state: SyncState) => {
|
|
646
|
-
Effect.logDebug(["Evolu handleSyncStateChange", { state }]).pipe(
|
|
647
|
-
Effect.zipRight(syncStateStore.setState(state)),
|
|
648
|
-
runFork,
|
|
649
|
-
);
|
|
650
|
-
};
|
|
651
|
-
|
|
652
|
-
const handleDbReceive = () => {
|
|
653
|
-
Effect.gen(function* () {
|
|
654
|
-
yield* Effect.logTrace("Evolu handleDbReceive");
|
|
655
|
-
releaseUnsubscribedLoadingPromises();
|
|
656
|
-
const queries = [...subscribedQueries.keys()];
|
|
657
|
-
if (queries.length > 0) {
|
|
658
|
-
yield* Effect.flatMap(db.loadQueries(queries), handlePatches());
|
|
659
|
-
}
|
|
660
|
-
}).pipe(runFork);
|
|
661
|
-
};
|
|
662
|
-
|
|
663
|
-
const sync =
|
|
664
|
-
({ refreshQueries }: { refreshQueries: boolean }) =>
|
|
665
|
-
() => {
|
|
666
|
-
Effect.flatMap(
|
|
667
|
-
db.sync(refreshQueries ? [...subscribedQueries.keys()] : []),
|
|
668
|
-
handlePatches(),
|
|
669
|
-
).pipe(runFork);
|
|
670
|
-
};
|
|
671
|
-
|
|
672
|
-
db.init(
|
|
673
|
-
schema,
|
|
674
|
-
initialDataAsMutations,
|
|
675
|
-
handleDbError,
|
|
676
|
-
handleSyncStateChange,
|
|
677
|
-
handleDbReceive,
|
|
678
|
-
mnemonic,
|
|
679
|
-
).pipe(
|
|
680
|
-
Effect.tap(sync({ refreshQueries: false })),
|
|
681
|
-
Effect.flatMap(ownerStore.setState),
|
|
682
|
-
Effect.catchTag("NotSupportedPlatformError", () => Effect.void), // no-op
|
|
683
|
-
runFork,
|
|
684
|
-
);
|
|
685
|
-
let socketResolved = false;
|
|
686
|
-
const socket: Promise<WebSocket | undefined> = new Promise((resolve) => {
|
|
687
|
-
ownerStore.subscribe(() => {
|
|
688
|
-
const owner = ownerStore.getState() as Owner;
|
|
689
|
-
if (!socketResolved && typeof owner?.id === "string") {
|
|
690
|
-
socketResolved = true;
|
|
691
|
-
resolve(createSocket(sync, config, owner));
|
|
692
|
-
}
|
|
693
|
-
});
|
|
694
|
-
});
|
|
695
|
-
|
|
696
|
-
const appStateReset = yield* appState.init({
|
|
697
|
-
onRequestSync: sync({ refreshQueries: true }),
|
|
698
|
-
reloadUrl: config.reloadUrl,
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
const handlePatches =
|
|
702
|
-
(options?: {
|
|
703
|
-
/**
|
|
704
|
-
* The flushSync is for onComplete handlers only. For example, with
|
|
705
|
-
* React, when we want to focus on a node created by a mutation, we must
|
|
706
|
-
* ensure all DOM changes are flushed synchronously.
|
|
707
|
-
*/
|
|
708
|
-
readonly flushSync: boolean;
|
|
709
|
-
}) =>
|
|
710
|
-
(patches: ReadonlyArray<QueryPatches>) =>
|
|
711
|
-
Effect.logDebug(["Evolu handlePatches", { patches }]).pipe(
|
|
712
|
-
Effect.zipRight(rowsStoreStateFromPatches(patches)),
|
|
713
|
-
Effect.tap((nextState) =>
|
|
714
|
-
Effect.forEach(patches, ({ query }) =>
|
|
715
|
-
resolveLoadingPromises(
|
|
716
|
-
query,
|
|
717
|
-
nextState.get(query) || emptyRows(),
|
|
718
|
-
),
|
|
719
|
-
),
|
|
720
|
-
),
|
|
721
|
-
Effect.tap((nextState) => {
|
|
722
|
-
if (options?.flushSync) {
|
|
723
|
-
flushSync(() => {
|
|
724
|
-
rowsStore.setState(nextState).pipe(runSync);
|
|
725
|
-
});
|
|
726
|
-
} else {
|
|
727
|
-
rowsStore.setState(nextState).pipe(runSync);
|
|
728
|
-
}
|
|
729
|
-
}),
|
|
730
|
-
);
|
|
731
|
-
|
|
732
|
-
const rowsStoreStateFromPatches = (patches: ReadonlyArray<QueryPatches>) =>
|
|
733
|
-
Effect.sync((): QueryRowsMap => {
|
|
734
|
-
const rowsStoreState = rowsStore.getState();
|
|
735
|
-
if (patches.length === 0) return rowsStoreState;
|
|
736
|
-
const queriesRows = Arr.map(
|
|
737
|
-
patches,
|
|
738
|
-
({ query, patches }): [Query, ReadonlyArray<Row>] => [
|
|
739
|
-
query,
|
|
740
|
-
applyPatches(patches, rowsStoreState.get(query) || emptyRows()),
|
|
741
|
-
],
|
|
742
|
-
);
|
|
743
|
-
return new Map([...rowsStoreState, ...queriesRows]);
|
|
744
|
-
});
|
|
745
|
-
|
|
746
|
-
const resolveLoadingPromises = (query: Query, rows: ReadonlyArray<Row>) =>
|
|
747
|
-
Effect.sync(() => {
|
|
748
|
-
const loadingPromise = loadingPromises.get(query);
|
|
749
|
-
if (!loadingPromise) return;
|
|
750
|
-
const result = queryResultFromRows(rows);
|
|
751
|
-
if (loadingPromise.promise.status !== "fulfilled") {
|
|
752
|
-
loadingPromise.resolve(result);
|
|
753
|
-
} else {
|
|
754
|
-
// A promise can't be fulfilled 2x, so we need a new one.
|
|
755
|
-
loadingPromise.promise = Promise.resolve(result);
|
|
756
|
-
}
|
|
757
|
-
/**
|
|
758
|
-
* "For example, a data framework can set the status and value fields on
|
|
759
|
-
* a promise preemptively, before passing to React, so that React can
|
|
760
|
-
* unwrap it without waiting a microtask."
|
|
761
|
-
* https://github.com/acdlite/rfcs/blob/first-class-promises/text/0000-first-class-support-for-promises.md
|
|
762
|
-
*/
|
|
763
|
-
Object.assign(loadingPromise.promise, {
|
|
764
|
-
status: "fulfilled",
|
|
765
|
-
value: result,
|
|
766
|
-
});
|
|
767
|
-
if (loadingPromise.releaseOnResolve) {
|
|
768
|
-
loadingPromises.delete(query);
|
|
769
|
-
}
|
|
770
|
-
});
|
|
771
|
-
|
|
772
|
-
/**
|
|
773
|
-
* We can't delete loading promises in `resolveLoadingPromises` because they
|
|
774
|
-
* must be cached, so repeated calls to `loadQuery` will always return the
|
|
775
|
-
* same promise until the data changes, and we also can't cache them forever
|
|
776
|
-
* because only subscribed queries are automatically updated (reactivity is
|
|
777
|
-
* expensive) hence this function must be called manually on any mutation.
|
|
778
|
-
*/
|
|
779
|
-
const releaseUnsubscribedLoadingPromises = () => {
|
|
780
|
-
[...loadingPromises.entries()]
|
|
781
|
-
.filter(([query]) => !subscribedQueries.has(query))
|
|
782
|
-
.forEach(([query, loadingPromise]) => {
|
|
783
|
-
if (loadingPromise.promise.status === "fulfilled") {
|
|
784
|
-
loadingPromises.delete(query);
|
|
785
|
-
} else {
|
|
786
|
-
loadingPromise.releaseOnResolve = true;
|
|
787
|
-
}
|
|
788
|
-
});
|
|
789
|
-
};
|
|
790
|
-
|
|
791
|
-
const mutate = ((): Mutate => {
|
|
792
|
-
let queue: ReadonlyArray<[Mutation, MutateOnComplete | undefined]> = [];
|
|
793
|
-
return (table, { id, ...values }, onComplete) => {
|
|
794
|
-
Effect.logDebug(["Evolu mutate", { table, id, values }]).pipe(runSync);
|
|
795
|
-
const isInsert = id == null;
|
|
796
|
-
if (isInsert) id = nanoIdGenerator.rowId.pipe(runSync);
|
|
797
|
-
queue = [...queue, [{ table, id, values, isInsert }, onComplete]];
|
|
798
|
-
if (queue.length === 1)
|
|
799
|
-
queueMicrotask(() => {
|
|
800
|
-
const [mutations, onCompletes] = Arr.unzip(queue);
|
|
801
|
-
queue = [];
|
|
802
|
-
const onCompletesDef = onCompletes.filter(Predicate.isNotUndefined);
|
|
803
|
-
releaseUnsubscribedLoadingPromises();
|
|
804
|
-
db.mutate(mutations, [...subscribedQueries.keys()]).pipe(
|
|
805
|
-
Effect.flatMap(
|
|
806
|
-
handlePatches({ flushSync: onCompletesDef.length > 0 }),
|
|
807
|
-
),
|
|
808
|
-
Effect.tap(() => {
|
|
809
|
-
onCompletesDef.forEach((onComplete) => onComplete());
|
|
810
|
-
}),
|
|
811
|
-
runFork,
|
|
812
|
-
);
|
|
813
|
-
});
|
|
814
|
-
return { id };
|
|
815
|
-
};
|
|
816
|
-
})();
|
|
817
|
-
|
|
818
|
-
const evolu: Evolu = {
|
|
819
|
-
socket,
|
|
820
|
-
subscribeError: errorStore.subscribe,
|
|
821
|
-
getError: errorStore.getState,
|
|
822
|
-
|
|
823
|
-
createQuery: (queryCallback, options) =>
|
|
824
|
-
pipe(
|
|
825
|
-
queryCallback(kysely as never).compile(),
|
|
826
|
-
(compiledQuery): SqliteQuery => {
|
|
827
|
-
if (isSqlMutation(compiledQuery.sql))
|
|
828
|
-
throw new Error(
|
|
829
|
-
"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.",
|
|
830
|
-
);
|
|
831
|
-
const parameters = compiledQuery.parameters as NonNullable<
|
|
832
|
-
SqliteQuery["parameters"]
|
|
833
|
-
>;
|
|
834
|
-
return {
|
|
835
|
-
sql: compiledQuery.sql,
|
|
836
|
-
parameters,
|
|
837
|
-
...(options && { options }),
|
|
838
|
-
};
|
|
839
|
-
},
|
|
840
|
-
(query) => serializeQuery(query),
|
|
841
|
-
),
|
|
842
|
-
|
|
843
|
-
loadQuery: (() => {
|
|
844
|
-
let queue: ReadonlyArray<Query> = [];
|
|
845
|
-
return <R extends Row>(query: Query<R>): Promise<QueryResult<R>> => {
|
|
846
|
-
Effect.logDebug([
|
|
847
|
-
"Evolu loadQuery",
|
|
848
|
-
{ query: deserializeQuery(query) },
|
|
849
|
-
]).pipe(runSync);
|
|
850
|
-
let loadingPromise = loadingPromises.get(query);
|
|
851
|
-
if (!loadingPromise) {
|
|
852
|
-
let resolve: LoadingPromise["resolve"] = constVoid;
|
|
853
|
-
const promise: LoadingPromise["promise"] = new Promise(
|
|
854
|
-
(_resolve) => {
|
|
855
|
-
resolve = _resolve;
|
|
856
|
-
},
|
|
857
|
-
);
|
|
858
|
-
loadingPromise = { resolve, promise, releaseOnResolve: false };
|
|
859
|
-
loadingPromises.set(query, loadingPromise);
|
|
860
|
-
queue = [...queue, query];
|
|
861
|
-
if (queue.length === 1) {
|
|
862
|
-
queueMicrotask(() => {
|
|
863
|
-
db.loadQueries(Arr.dedupe(queue)).pipe(
|
|
864
|
-
Effect.flatMap(handlePatches()),
|
|
865
|
-
runFork,
|
|
866
|
-
);
|
|
867
|
-
queue = [];
|
|
868
|
-
});
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
return loadingPromise.promise as Promise<QueryResult<R>>;
|
|
872
|
-
};
|
|
873
|
-
})(),
|
|
874
|
-
|
|
875
|
-
loadQueries: <R extends Row, Q extends Queries<R>>(
|
|
876
|
-
queries: [...Q],
|
|
877
|
-
): [...QueryResultsPromisesFromQueries<Q>] =>
|
|
878
|
-
queries.map(evolu.loadQuery) as [...QueryResultsPromisesFromQueries<Q>],
|
|
879
|
-
|
|
880
|
-
subscribeQuery: (query) => (listener) => {
|
|
881
|
-
subscribedQueries.set(
|
|
882
|
-
query,
|
|
883
|
-
Number.increment(subscribedQueries.get(query) ?? 0),
|
|
884
|
-
);
|
|
885
|
-
const unsubscribe = rowsStore.subscribe(listener);
|
|
886
|
-
|
|
887
|
-
return () => {
|
|
888
|
-
const count = subscribedQueries.get(query);
|
|
889
|
-
if (count != null && count > 1)
|
|
890
|
-
subscribedQueries.set(query, Number.decrement(count));
|
|
891
|
-
else subscribedQueries.delete(query);
|
|
892
|
-
unsubscribe();
|
|
893
|
-
};
|
|
894
|
-
},
|
|
895
|
-
|
|
896
|
-
getQuery: <R extends Row>(query: Query<R>): QueryResult<R> =>
|
|
897
|
-
queryResultFromRows(
|
|
898
|
-
rowsStore.getState().get(query) || emptyRows(),
|
|
899
|
-
) as QueryResult<R>,
|
|
900
|
-
|
|
901
|
-
subscribeOwner: ownerStore.subscribe,
|
|
902
|
-
getOwner: ownerStore.getState,
|
|
903
|
-
|
|
904
|
-
subscribeSyncState: syncStateStore.subscribe,
|
|
905
|
-
getSyncState: syncStateStore.getState,
|
|
906
|
-
|
|
907
|
-
create: mutate as Mutate<EvoluSchema, "create">,
|
|
908
|
-
update: mutate,
|
|
909
|
-
createOrUpdate: mutate as Mutate<EvoluSchema, "createOrUpdate">,
|
|
910
|
-
|
|
911
|
-
resetOwner: (options) =>
|
|
912
|
-
Effect.gen(function* () {
|
|
913
|
-
yield* db.resetOwner();
|
|
914
|
-
if (options?.reload !== false) yield* appStateReset.reset;
|
|
915
|
-
}).pipe(runPromise),
|
|
916
|
-
|
|
917
|
-
restoreOwner: (mnemonic, options) =>
|
|
918
|
-
Effect.gen(function* () {
|
|
919
|
-
yield* db.restoreOwner(schema, mnemonic);
|
|
920
|
-
if (options?.reload !== false) yield* appStateReset.reset;
|
|
921
|
-
}).pipe(runPromise),
|
|
922
|
-
|
|
923
|
-
reloadApp: () => {
|
|
924
|
-
appStateReset.reset.pipe(runFork);
|
|
925
|
-
},
|
|
926
|
-
|
|
927
|
-
ensureSchema: (schema) => {
|
|
928
|
-
db.ensureSchema(schema).pipe(runFork);
|
|
929
|
-
},
|
|
930
|
-
|
|
931
|
-
exportDatabase: () => db.exportDatabase().pipe(runPromise),
|
|
932
|
-
};
|
|
933
|
-
|
|
934
|
-
return evolu;
|
|
935
|
-
});
|
|
936
|
-
|
|
937
|
-
const initialDataToMutations = (
|
|
938
|
-
initialData: EvoluConfig["initialData"] = constVoid,
|
|
939
|
-
) =>
|
|
940
|
-
Effect.map(NanoIdGenerator, (nanoIdGenerator) => {
|
|
941
|
-
const mutations: Mutation[] = [];
|
|
942
|
-
const mutate: Mutate = (table, { id, ...values }) => {
|
|
943
|
-
if (id == null) id = nanoIdGenerator.rowId.pipe(Effect.runSync) as never;
|
|
944
|
-
mutations.push({ isInsert: true, id, table: table as string, values });
|
|
945
|
-
return { id };
|
|
946
|
-
};
|
|
947
|
-
const evolu: EvoluForInitialData = {
|
|
948
|
-
create: mutate as Mutate<EvoluSchema, "create">,
|
|
949
|
-
createOrUpdate: mutate as Mutate<EvoluSchema, "createOrUpdate">,
|
|
950
|
-
};
|
|
951
|
-
initialData(evolu);
|
|
952
|
-
return mutations;
|
|
953
|
-
});
|
|
954
|
-
|
|
955
|
-
interface EvoluForInitialData<T extends EvoluSchema = EvoluSchema> {
|
|
956
|
-
create: Mutate<T, "create">;
|
|
957
|
-
createOrUpdate: Mutate<T, "createOrUpdate">;
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
interface LoadingPromise {
|
|
961
|
-
/** Promise with props for the upcoming React use hook. */
|
|
962
|
-
promise: Promise<QueryResult> & {
|
|
963
|
-
status?: "pending" | "fulfilled" | "rejected";
|
|
964
|
-
value?: QueryResult;
|
|
965
|
-
reason?: unknown;
|
|
966
|
-
};
|
|
967
|
-
resolve: (rows: QueryResult) => void;
|
|
968
|
-
releaseOnResolve: boolean;
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
// https://kysely.dev/docs/recipes/splitting-query-building-and-execution
|
|
972
|
-
const kysely = new Kysely.Kysely({
|
|
973
|
-
dialect: {
|
|
974
|
-
createAdapter: () => new Kysely.SqliteAdapter(),
|
|
975
|
-
createDriver: () => new Kysely.DummyDriver(),
|
|
976
|
-
createIntrospector() {
|
|
977
|
-
// eslint-disable-next-line @typescript-eslint/only-throw-error
|
|
978
|
-
throw "Not implemeneted";
|
|
979
|
-
},
|
|
980
|
-
createQueryCompiler: () => new Kysely.SqliteQueryCompiler(),
|
|
981
|
-
},
|
|
982
|
-
});
|
|
983
|
-
|
|
984
|
-
const createIndex = kysely.schema.createIndex.bind(kysely.schema);
|
|
985
|
-
type CreateIndex = typeof createIndex;
|
|
986
|
-
|
|
987
|
-
/**
|
|
988
|
-
* Create SQLite indexes.
|
|
989
|
-
*
|
|
990
|
-
* See https://www.evolu.dev/docs/indexes
|
|
991
|
-
*
|
|
992
|
-
* @example
|
|
993
|
-
* const indexes = createIndexes((create) => [
|
|
994
|
-
* create("indexTodoCreatedAt").on("todo").column("createdAt"),
|
|
995
|
-
* create("indexTodoCategoryCreatedAt")
|
|
996
|
-
* .on("todoCategory")
|
|
997
|
-
* .column("createdAt"),
|
|
998
|
-
* ]);
|
|
999
|
-
*/
|
|
1000
|
-
export const createIndexes = (
|
|
1001
|
-
callback: (
|
|
1002
|
-
create: CreateIndex,
|
|
1003
|
-
) => ReadonlyArray<Kysely.CreateIndexBuilder<any>>,
|
|
1004
|
-
): ReadonlyArray<Index> =>
|
|
1005
|
-
callback(createIndex).map(
|
|
1006
|
-
(index): Index => ({
|
|
1007
|
-
name: index.toOperationNode().name.name,
|
|
1008
|
-
sql: index.compile().sql,
|
|
1009
|
-
}),
|
|
1010
|
-
);
|
|
1011
|
-
|
|
1012
|
-
/** Create a namespaced lock name. */
|
|
1013
|
-
export const getLockName = (
|
|
1014
|
-
name: string,
|
|
1015
|
-
): Effect.Effect<string, never, Config> =>
|
|
1016
|
-
Effect.map(Config, (config) => `evolu:${config.name}:${name}`);
|