@evolu/common 6.0.1-preview.3 → 6.0.1-preview.31
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/dist/src/Array.d.ts +69 -5
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +64 -5
- package/dist/src/Assert.d.ts +6 -16
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +6 -18
- package/dist/src/Brand.d.ts +75 -0
- package/dist/src/Brand.d.ts.map +1 -0
- package/dist/src/Brand.js +1 -0
- package/dist/src/Buffer.d.ts +1 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +8 -7
- package/dist/src/Cache.d.ts +44 -0
- package/dist/src/Cache.d.ts.map +1 -0
- package/dist/src/Cache.js +52 -0
- package/dist/src/Callbacks.d.ts +45 -12
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Callbacks.js +14 -7
- package/dist/src/Console.d.ts +31 -6
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +72 -9
- package/dist/src/Crypto.d.ts +61 -34
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +32 -45
- package/dist/src/Evolu/Db.d.ts +161 -65
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +286 -694
- package/dist/src/Evolu/Diff.d.ts +3 -3
- package/dist/src/Evolu/Diff.d.ts.map +1 -1
- package/dist/src/Evolu/Diff.js +7 -5
- package/dist/src/Evolu/Evolu.d.ts +208 -133
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +188 -183
- package/dist/src/Evolu/Internal.d.ts +0 -2
- package/dist/src/Evolu/Internal.d.ts.map +1 -1
- package/dist/src/Evolu/Internal.js +0 -2
- package/dist/src/Evolu/LocalAuth.d.ts +150 -0
- package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
- package/dist/src/Evolu/LocalAuth.js +174 -0
- package/dist/src/Evolu/Owner.d.ts +273 -120
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +130 -104
- package/dist/src/Evolu/Platform.d.ts +9 -7
- package/dist/src/Evolu/Platform.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.d.ts +277 -232
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +603 -378
- package/dist/src/Evolu/Public.d.ts +6 -8
- package/dist/src/Evolu/Public.d.ts.map +1 -1
- package/dist/src/Evolu/Public.js +2 -3
- package/dist/src/Evolu/PublicKysely.js +3 -3
- package/dist/src/Evolu/Query.d.ts +2 -1
- package/dist/src/Evolu/Query.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.d.ts +92 -7
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +238 -76
- package/dist/src/Evolu/Schema.d.ts +129 -73
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +169 -89
- package/dist/src/Evolu/Storage.d.ts +240 -26
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +189 -91
- package/dist/src/Evolu/Sync.d.ts +67 -13
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +441 -20
- package/dist/src/Evolu/Timestamp.d.ts +85 -27
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +77 -18
- package/dist/src/Identicon.d.ts +35 -0
- package/dist/src/Identicon.d.ts.map +1 -0
- package/dist/src/Identicon.js +143 -0
- package/dist/src/Instances.d.ts +34 -0
- package/dist/src/Instances.d.ts.map +1 -0
- package/dist/src/Instances.js +44 -0
- package/dist/src/ManyToManyMap.d.ts +71 -10
- package/dist/src/ManyToManyMap.d.ts.map +1 -1
- package/dist/src/ManyToManyMap.js +41 -6
- package/dist/src/Number.d.ts +4 -3
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +5 -4
- package/dist/src/Platform.d.ts +20 -0
- package/dist/src/Platform.d.ts.map +1 -0
- package/dist/src/Platform.js +22 -0
- package/dist/src/Random.d.ts +3 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Resources.d.ts +118 -0
- package/dist/src/Resources.d.ts.map +1 -0
- package/dist/src/Resources.js +197 -0
- package/dist/src/Result.d.ts +184 -52
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +30 -241
- package/dist/src/Skiplist.js +2 -1
- package/dist/src/Sqlite.d.ts +63 -5
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +110 -9
- package/dist/src/Task.d.ts +586 -0
- package/dist/src/Task.d.ts.map +1 -0
- package/dist/src/Task.js +469 -0
- package/dist/src/Time.d.ts +66 -1
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +99 -5
- package/dist/src/Type.d.ts +622 -340
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +666 -464
- package/dist/src/Types.d.ts +1 -75
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/WebSocket.d.ts +5 -2
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +12 -18
- package/dist/src/Worker.d.ts +39 -11
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +22 -4
- package/dist/src/index.d.ts +7 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -2
- package/package.json +14 -13
- package/src/Array.ts +90 -11
- package/src/Assert.ts +6 -24
- package/src/Brand.ts +75 -0
- package/src/Buffer.ts +7 -7
- package/src/Cache.ts +85 -0
- package/src/Callbacks.ts +62 -22
- package/src/Console.ts +91 -11
- package/src/Crypto.ts +97 -82
- package/src/Evolu/Db.ts +517 -1020
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +464 -355
- package/src/Evolu/Internal.ts +0 -2
- package/src/Evolu/LocalAuth.ts +463 -0
- package/src/Evolu/Owner.ts +355 -228
- package/src/Evolu/Platform.ts +9 -9
- package/src/Evolu/Protocol.ts +859 -676
- package/src/Evolu/Public.ts +7 -14
- package/src/Evolu/PublicKysely.ts +3 -3
- package/src/Evolu/Query.ts +2 -1
- package/src/Evolu/Relay.ts +437 -93
- package/src/Evolu/Schema.ts +391 -191
- package/src/Evolu/Storage.ts +532 -135
- package/src/Evolu/Sync.ts +766 -37
- package/src/Evolu/Timestamp.ts +88 -35
- package/src/Identicon.ts +197 -0
- package/src/Instances.ts +90 -0
- package/src/ManyToManyMap.ts +124 -24
- package/src/Number.ts +6 -10
- package/src/Platform.ts +26 -0
- package/src/Random.ts +3 -2
- package/src/Resources.ts +367 -0
- package/src/Result.ts +191 -54
- package/src/Skiplist.ts +1 -1
- package/src/Sqlite.ts +122 -17
- package/src/Task.ts +901 -0
- package/src/Time.ts +180 -5
- package/src/Type.ts +1084 -727
- package/src/Types.ts +1 -77
- package/src/WebSocket.ts +27 -25
- package/src/Worker.ts +72 -23
- package/src/index.ts +7 -2
- package/dist/src/Evolu/Config.d.ts +0 -69
- package/dist/src/Evolu/Config.d.ts.map +0 -1
- package/dist/src/Evolu/Config.js +0 -9
- package/dist/src/Evolu/Kysely.d.ts +0 -6
- package/dist/src/Evolu/Kysely.d.ts.map +0 -1
- package/dist/src/Evolu/Kysely.js +0 -21
- package/dist/src/NanoId.d.ts +0 -27
- package/dist/src/NanoId.d.ts.map +0 -1
- package/dist/src/NanoId.js +0 -6
- package/dist/src/Promise.d.ts +0 -180
- package/dist/src/Promise.d.ts.map +0 -1
- package/dist/src/Promise.js +0 -176
- package/src/Evolu/Config.ts +0 -83
- package/src/Evolu/Kysely.ts +0 -38
- package/src/NanoId.ts +0 -39
- package/src/Promise.ts +0 -295
package/src/Evolu/Evolu.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import { pack } from "msgpackr";
|
|
1
2
|
import { isNonEmptyArray, isNonEmptyReadonlyArray } from "../Array.js";
|
|
2
|
-
import { assertNonEmptyArray } from "../Assert.js";
|
|
3
|
+
import { assert, assertNonEmptyArray } from "../Assert.js";
|
|
3
4
|
import { createCallbacks } from "../Callbacks.js";
|
|
4
5
|
import { ConsoleDep } from "../Console.js";
|
|
5
|
-
import { SymmetricCryptoDecryptError } from "../Crypto.js";
|
|
6
|
+
import { RandomBytesDep, SymmetricCryptoDecryptError } from "../Crypto.js";
|
|
7
|
+
import { eqArrayNumber } from "../Eq.js";
|
|
6
8
|
import { TransferableError } from "../Error.js";
|
|
7
9
|
import { exhaustiveCheck } from "../Function.js";
|
|
8
|
-
import {
|
|
10
|
+
import { createInstances, Instances } from "../Instances.js";
|
|
9
11
|
import { err, ok, Result } from "../Result.js";
|
|
10
12
|
import { isSqlMutation, SafeSql, SqliteError, SqliteQuery } from "../Sqlite.js";
|
|
11
13
|
import { createStore, StoreSubscribe } from "../Store.js";
|
|
@@ -18,19 +20,16 @@ import {
|
|
|
18
20
|
InferType,
|
|
19
21
|
Mnemonic,
|
|
20
22
|
ObjectType,
|
|
23
|
+
SimpleName,
|
|
24
|
+
ValidMutationSize,
|
|
25
|
+
ValidMutationSizeError,
|
|
21
26
|
} from "../Type.js";
|
|
22
27
|
import { IntentionalNever } from "../Types.js";
|
|
23
|
-
import {
|
|
24
|
-
import { CreateDbWorkerDep } from "./Db.js";
|
|
28
|
+
import { CreateDbWorkerDep, DbConfig, defaultDbConfig } from "./Db.js";
|
|
25
29
|
import { applyPatches } from "./Diff.js";
|
|
26
|
-
import { kysely } from "./Kysely.js";
|
|
27
30
|
import { AppOwner } from "./Owner.js";
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
DbChange,
|
|
31
|
-
ProtocolError,
|
|
32
|
-
ProtocolUnsupportedVersionError,
|
|
33
|
-
} from "./Protocol.js";
|
|
31
|
+
import { FlushSyncDep, ReloadAppDep } from "./Platform.js";
|
|
32
|
+
import { ProtocolError } from "./Protocol.js";
|
|
34
33
|
import {
|
|
35
34
|
createSubscribedQueries,
|
|
36
35
|
emptyRows,
|
|
@@ -44,24 +43,58 @@ import {
|
|
|
44
43
|
SubscribedQueries,
|
|
45
44
|
} from "./Query.js";
|
|
46
45
|
import {
|
|
47
|
-
assertValidEvoluSchema,
|
|
48
46
|
CreateQuery,
|
|
47
|
+
DefaultColumns,
|
|
49
48
|
EvoluSchema,
|
|
49
|
+
evoluSchemaToDbSchema,
|
|
50
|
+
IndexesConfig,
|
|
50
51
|
insertable,
|
|
52
|
+
kysely,
|
|
51
53
|
Mutation,
|
|
54
|
+
MutationChange,
|
|
52
55
|
MutationKind,
|
|
53
56
|
MutationMapping,
|
|
54
57
|
MutationOptions,
|
|
55
58
|
updateable,
|
|
56
59
|
upsertable,
|
|
57
|
-
|
|
58
|
-
ValidMutationSize,
|
|
59
|
-
ValidMutationSizeError,
|
|
60
|
+
ValidateSchema,
|
|
60
61
|
} from "./Schema.js";
|
|
61
|
-
import {
|
|
62
|
+
import { DbChange } from "./Storage.js";
|
|
63
|
+
import { initialSyncState, SyncOwner, SyncState } from "./Sync.js";
|
|
62
64
|
import { TimestampError } from "./Timestamp.js";
|
|
63
65
|
|
|
64
|
-
export interface
|
|
66
|
+
export interface EvoluConfig extends Partial<DbConfig> {
|
|
67
|
+
/**
|
|
68
|
+
* Use the `indexes` option to define SQLite indexes.
|
|
69
|
+
*
|
|
70
|
+
* Table and column names are not typed because Kysely doesn't support it.
|
|
71
|
+
*
|
|
72
|
+
* https://medium.com/@JasonWyatt/squeezing-performance-from-sqlite-indexes-indexes-c4e175f3c346
|
|
73
|
+
*
|
|
74
|
+
* ### Example
|
|
75
|
+
*
|
|
76
|
+
* ```ts
|
|
77
|
+
* const evolu = createEvolu(evoluReactDeps)(Schema, {
|
|
78
|
+
* indexes: (create) => [
|
|
79
|
+
* create("todoCreatedAt").on("todo").column("createdAt"),
|
|
80
|
+
* create("todoCategoryCreatedAt")
|
|
81
|
+
* .on("todoCategory")
|
|
82
|
+
* .column("createdAt"),
|
|
83
|
+
* ],
|
|
84
|
+
* });
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
readonly indexes?: IndexesConfig;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* URL to reload browser tabs after reset or restore.
|
|
91
|
+
*
|
|
92
|
+
* The default value is `/`.
|
|
93
|
+
*/
|
|
94
|
+
readonly reloadUrl?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface Evolu<S extends EvoluSchema = EvoluSchema> extends Disposable {
|
|
65
98
|
/**
|
|
66
99
|
* Subscribe to {@link EvoluError} changes.
|
|
67
100
|
*
|
|
@@ -88,7 +121,8 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
|
|
|
88
121
|
* All this function does is compile the Kysely query and serialize it into a
|
|
89
122
|
* unique string. Both operations are fast and cheap.
|
|
90
123
|
*
|
|
91
|
-
* For mutations, use {@link Evolu
|
|
124
|
+
* For mutations, use {@link Evolu#insert}, {@link Evolu#update}, or
|
|
125
|
+
* {@link Evolu#upsert}.
|
|
92
126
|
*
|
|
93
127
|
* ### Example
|
|
94
128
|
*
|
|
@@ -108,43 +142,16 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
|
|
|
108
142
|
/**
|
|
109
143
|
* Load {@link Query} and return a promise with {@link QueryRows}.
|
|
110
144
|
*
|
|
111
|
-
*
|
|
112
|
-
* why loading should fail. All data are local, and the query is typed.
|
|
113
|
-
*
|
|
114
|
-
* {@link Evolu.subscribeError}.
|
|
115
|
-
*
|
|
116
|
-
* Loading is batched, and returned promises are cached, so there is no need
|
|
117
|
-
* for an additional cache. Evolu's internal cache is invalidated on
|
|
118
|
-
* mutation.
|
|
119
|
-
*
|
|
120
|
-
* The returned promise is enriched with special status and value properties
|
|
121
|
-
* for the upcoming React `use` Hook, but other UI libraries can also leverage
|
|
122
|
-
* them. Speaking of React, there are two essential React Suspense-related
|
|
123
|
-
* patterns that every developer should be aware of—passing promises to
|
|
124
|
-
* children and caching over mutations.
|
|
125
|
-
*
|
|
126
|
-
* With promises passed to children, we can load a query as soon as possible,
|
|
127
|
-
* but we don't have to use the returned promise immediately. That's useful
|
|
128
|
-
* for prefetching, which is generally not necessary for local-first apps but
|
|
129
|
-
* can be if a query takes a long time to load.
|
|
145
|
+
* The returned promise always resolves successfully because there is no
|
|
146
|
+
* reason why loading should fail. All data are local, and the query is typed.
|
|
147
|
+
* Unexpected errors are handled with {@link Evolu#subscribeError}.
|
|
130
148
|
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* not for UI with React Suspense because a mutation would suspend rerendered
|
|
135
|
-
* queries on a page, and that's not a good UX.
|
|
149
|
+
* Loading is batched, and returned promises are cached until resolved to
|
|
150
|
+
* prevent redundant database queries and to support React Suspense (which
|
|
151
|
+
* requires stable promise references while pending).
|
|
136
152
|
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* For better UX, a query must be subscribed for updates. This way, instead of
|
|
141
|
-
* Suspense flashes, the user sees new data immediately because Evolu replaces
|
|
142
|
-
* cached promises with fresh, already resolved new ones.
|
|
143
|
-
*
|
|
144
|
-
* If you are curious why Evolu does not do that for all queries by default,
|
|
145
|
-
* the answer is simple: performance. Tracking changes is costly and
|
|
146
|
-
* meaningful only for visible (hence subscribed) queries anyway. To subscribe
|
|
147
|
-
* to a query, use {@link Evolu.subscribeQuery}.
|
|
153
|
+
* To subscribe a query for automatic updates, use
|
|
154
|
+
* {@link Evolu#subscribeQuery}.
|
|
148
155
|
*
|
|
149
156
|
* ### Example
|
|
150
157
|
*
|
|
@@ -152,7 +159,7 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
|
|
|
152
159
|
* const allTodos = evolu.createQuery((db) =>
|
|
153
160
|
* db.selectFrom("todo").selectAll(),
|
|
154
161
|
* );
|
|
155
|
-
* evolu.loadQuery(allTodos).then((
|
|
162
|
+
* evolu.loadQuery(allTodos).then((rows) => {
|
|
156
163
|
* console.log(rows);
|
|
157
164
|
* });
|
|
158
165
|
* ```
|
|
@@ -201,86 +208,168 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
|
|
|
201
208
|
readonly getQueryRows: <R extends Row>(query: Query<R>) => QueryRows<R>;
|
|
202
209
|
|
|
203
210
|
/**
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
* ### Example
|
|
211
|
+
* Promise that resolves to {@link AppOwner} when available.
|
|
207
212
|
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
* const owner = evolu.getAppOwner();
|
|
211
|
-
* });
|
|
212
|
-
* ```
|
|
213
|
-
*/
|
|
214
|
-
readonly subscribeAppOwner: StoreSubscribe;
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Get {@link AppOwner}.
|
|
213
|
+
* Note: With web-only deps, this promise will not resolve during SSR because
|
|
214
|
+
* there is no AppOwner on the server.
|
|
218
215
|
*
|
|
219
216
|
* ### Example
|
|
220
217
|
*
|
|
221
218
|
* ```ts
|
|
222
|
-
* const
|
|
223
|
-
* const owner = evolu.getAppOwner();
|
|
224
|
-
* });
|
|
219
|
+
* const owner = await evolu.appOwner;
|
|
225
220
|
* ```
|
|
226
221
|
*/
|
|
227
|
-
readonly
|
|
222
|
+
readonly appOwner: Promise<AppOwner>;
|
|
228
223
|
|
|
229
224
|
/**
|
|
230
|
-
*
|
|
225
|
+
* Inserts a row into the database and returns a {@link Result} with the new
|
|
226
|
+
* {@link Id}.
|
|
231
227
|
*
|
|
232
|
-
*
|
|
228
|
+
* The first argument is the table name, and the second is an object
|
|
229
|
+
* containing the row data. An optional third argument provides mutation
|
|
230
|
+
* options including an `onComplete` callback and `onlyValidate` flag.
|
|
233
231
|
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Get {@link SyncState}.
|
|
232
|
+
* Returns a Result type - use `.ok` to check if the insertion succeeded, and
|
|
233
|
+
* `.value.id` to access the generated ID on success, or `.error` to handle
|
|
234
|
+
* validation errors.
|
|
235
|
+
*
|
|
236
|
+
* Evolu does not use SQL for mutations to ensure data can be safely and
|
|
237
|
+
* predictably merged without conflicts. Explicit mutations also allow Evolu
|
|
238
|
+
* to automatically add and update {@link DefaultColumns}.
|
|
244
239
|
*
|
|
245
240
|
* ### Example
|
|
246
241
|
*
|
|
247
242
|
* ```ts
|
|
248
|
-
* const
|
|
249
|
-
*
|
|
243
|
+
* const result = evolu.insert("todo", {
|
|
244
|
+
* title: "Learn Evolu",
|
|
245
|
+
* isCompleted: false,
|
|
250
246
|
* });
|
|
251
|
-
* ```
|
|
252
|
-
*/
|
|
253
|
-
readonly getSyncState: () => SyncState;
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Inserts a row.
|
|
257
247
|
*
|
|
258
|
-
*
|
|
248
|
+
* if (result.ok) {
|
|
249
|
+
* console.log("Todo created with ID:", result.value.id);
|
|
250
|
+
* } else {
|
|
251
|
+
* console.error("Validation error:", result.error);
|
|
252
|
+
* }
|
|
259
253
|
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
254
|
+
* // With onComplete callback
|
|
255
|
+
* evolu.insert(
|
|
256
|
+
* "todo",
|
|
257
|
+
* { title: "Another todo" },
|
|
258
|
+
* {
|
|
259
|
+
* onComplete: () => {
|
|
260
|
+
* console.log("Insert completed");
|
|
261
|
+
* },
|
|
262
|
+
* },
|
|
263
|
+
* );
|
|
262
264
|
* ```
|
|
263
265
|
*/
|
|
264
266
|
insert: Mutation<S, "insert">;
|
|
265
267
|
|
|
266
268
|
/**
|
|
267
|
-
* Updates a row
|
|
269
|
+
* Updates a row in the database and returns a {@link Result} with the existing
|
|
270
|
+
* {@link Id}.
|
|
271
|
+
*
|
|
272
|
+
* The first argument is the table name, and the second is an object
|
|
273
|
+
* containing the row data including the required `id` field. An optional
|
|
274
|
+
* third argument provides mutation options including an `onComplete` callback
|
|
275
|
+
* and `onlyValidate` flag.
|
|
276
|
+
*
|
|
277
|
+
* Returns a Result type - use `.ok` to check if the update succeeded, and
|
|
278
|
+
* `.value.id` to access the ID on success, or `.error` to handle validation
|
|
279
|
+
* errors.
|
|
280
|
+
*
|
|
281
|
+
* Evolu does not use SQL for mutations to ensure data can be safely and
|
|
282
|
+
* predictably merged without conflicts. Explicit mutations also allow Evolu
|
|
283
|
+
* to automatically add and update {@link DefaultColumns}.
|
|
268
284
|
*
|
|
269
285
|
* ### Example
|
|
270
286
|
*
|
|
271
287
|
* ```ts
|
|
272
|
-
*
|
|
288
|
+
* const result = evolu.update("todo", {
|
|
289
|
+
* id: todoId,
|
|
290
|
+
* title: "Updated title",
|
|
291
|
+
* isCompleted: true,
|
|
292
|
+
* });
|
|
293
|
+
*
|
|
294
|
+
* if (result.ok) {
|
|
295
|
+
* console.log("Todo updated with ID:", result.value.id);
|
|
296
|
+
* } else {
|
|
297
|
+
* console.error("Validation error:", result.error);
|
|
298
|
+
* }
|
|
299
|
+
*
|
|
300
|
+
* // To delete a row, set isDeleted to true
|
|
301
|
+
* evolu.update("todo", { id: todoId, isDeleted: true });
|
|
302
|
+
*
|
|
303
|
+
* // With onComplete callback
|
|
304
|
+
* evolu.update(
|
|
305
|
+
* "todo",
|
|
306
|
+
* { id: todoId, title: "New title" },
|
|
307
|
+
* {
|
|
308
|
+
* onComplete: () => {
|
|
309
|
+
* console.log("Update completed");
|
|
310
|
+
* },
|
|
311
|
+
* },
|
|
312
|
+
* );
|
|
273
313
|
* ```
|
|
274
314
|
*/
|
|
275
315
|
update: Mutation<S, "update">;
|
|
276
316
|
|
|
277
317
|
/**
|
|
278
|
-
* Upserts a row
|
|
318
|
+
* Upserts a row in the database and returns a {@link Result} with the existing
|
|
319
|
+
* {@link Id}.
|
|
320
|
+
*
|
|
321
|
+
* The first argument is the table name, and the second is an object
|
|
322
|
+
* containing the row data including the required `id` field. An optional
|
|
323
|
+
* third argument provides mutation options including an `onComplete` callback
|
|
324
|
+
* and `onlyValidate` flag.
|
|
325
|
+
*
|
|
326
|
+
* This function allows you to use custom IDs and optionally set `createdAt`,
|
|
327
|
+
* which is useful for external systems, data migrations, or when the same row
|
|
328
|
+
* may already be created on a different device.
|
|
329
|
+
*
|
|
330
|
+
* Returns a Result type - use `.ok` to check if the upsert succeeded, and
|
|
331
|
+
* `.value.id` to access the ID on success, or `.error` to handle validation
|
|
332
|
+
* errors.
|
|
333
|
+
*
|
|
334
|
+
* Evolu does not use SQL for mutations to ensure data can be safely and
|
|
335
|
+
* predictably merged without conflicts. Explicit mutations also allow Evolu
|
|
336
|
+
* to automatically add and update {@link DefaultColumns}.
|
|
279
337
|
*
|
|
280
338
|
* ### Example
|
|
281
339
|
*
|
|
282
340
|
* ```ts
|
|
283
|
-
* //
|
|
341
|
+
* // Use deterministic ID for stable upserts across devices
|
|
342
|
+
* const stableId = createIdFromString("my-todo-1");
|
|
343
|
+
*
|
|
344
|
+
* const result = evolu.upsert("todo", {
|
|
345
|
+
* id: stableId,
|
|
346
|
+
* title: "Learn Evolu",
|
|
347
|
+
* isCompleted: false,
|
|
348
|
+
* });
|
|
349
|
+
*
|
|
350
|
+
* if (result.ok) {
|
|
351
|
+
* console.log("Todo upserted with ID:", result.value.id);
|
|
352
|
+
* } else {
|
|
353
|
+
* console.error("Validation error:", result.error);
|
|
354
|
+
* }
|
|
355
|
+
*
|
|
356
|
+
* // Data migration with custom createdAt
|
|
357
|
+
* evolu.upsert("todo", {
|
|
358
|
+
* id: externalId,
|
|
359
|
+
* title: "Migrated todo",
|
|
360
|
+
* createdAt: new Date("2023-01-01"), // Preserve original timestamp
|
|
361
|
+
* });
|
|
362
|
+
*
|
|
363
|
+
* // With onComplete callback
|
|
364
|
+
* evolu.upsert(
|
|
365
|
+
* "todo",
|
|
366
|
+
* { id: stableId, title: "Updated title" },
|
|
367
|
+
* {
|
|
368
|
+
* onComplete: () => {
|
|
369
|
+
* console.log("Upsert completed");
|
|
370
|
+
* },
|
|
371
|
+
* },
|
|
372
|
+
* );
|
|
284
373
|
* ```
|
|
285
374
|
*/
|
|
286
375
|
upsert: Mutation<S, "upsert">;
|
|
@@ -300,7 +389,7 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
|
|
|
300
389
|
|
|
301
390
|
/**
|
|
302
391
|
* Restore {@link AppOwner} with all their synced data. It uses
|
|
303
|
-
* {@link Evolu
|
|
392
|
+
* {@link Evolu#resetAppOwner}, so be careful.
|
|
304
393
|
*/
|
|
305
394
|
readonly restoreAppOwner: (
|
|
306
395
|
mnemonic: Mnemonic,
|
|
@@ -316,17 +405,42 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
|
|
|
316
405
|
readonly reloadApp: () => void;
|
|
317
406
|
|
|
318
407
|
/** Export SQLite database file as Uint8Array. */
|
|
319
|
-
readonly exportDatabase: () => Promise<Uint8Array
|
|
408
|
+
readonly exportDatabase: () => Promise<Uint8Array<ArrayBuffer>>;
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Use an owner. Using an owner means syncing it and subscribing to
|
|
412
|
+
* broadcasted changes. Returns a function to stop using the owner.
|
|
413
|
+
*
|
|
414
|
+
* Transport connections are automatically deduplicated and reference-counted,
|
|
415
|
+
* so multiple owners using the same transport will share a single
|
|
416
|
+
* connection.
|
|
417
|
+
*
|
|
418
|
+
* ### Example
|
|
419
|
+
*
|
|
420
|
+
* ```ts
|
|
421
|
+
* // Use an owner (starts syncing and subscribing to changes).
|
|
422
|
+
* const unuse = evolu.useOwner(shardOwner);
|
|
423
|
+
*
|
|
424
|
+
* // Later, stop using the owner.
|
|
425
|
+
* unuse();
|
|
426
|
+
*
|
|
427
|
+
* // Bulk operations.
|
|
428
|
+
* const unuses = owners.map((owner) => evolu.useOwner(owner));
|
|
429
|
+
* // Later: unuses.forEach(unuse => unuse());
|
|
430
|
+
* ```
|
|
431
|
+
*
|
|
432
|
+
* @experimental
|
|
433
|
+
*/
|
|
434
|
+
readonly useOwner: (owner: SyncOwner) => () => void;
|
|
320
435
|
}
|
|
321
436
|
|
|
322
|
-
/**
|
|
437
|
+
/** Represents errors that can occur in Evolu. */
|
|
323
438
|
export type EvoluError =
|
|
324
|
-
| TimestampError
|
|
325
439
|
| ProtocolError
|
|
326
|
-
|
|
|
440
|
+
| SqliteError
|
|
327
441
|
| SymmetricCryptoDecryptError
|
|
328
|
-
|
|
|
329
|
-
|
|
|
442
|
+
| TimestampError
|
|
443
|
+
| TransferableError;
|
|
330
444
|
|
|
331
445
|
interface InternalEvoluInstance<S extends EvoluSchema = EvoluSchema>
|
|
332
446
|
extends Evolu<S> {
|
|
@@ -337,68 +451,25 @@ interface InternalEvoluInstance<S extends EvoluSchema = EvoluSchema>
|
|
|
337
451
|
readonly ensureSchema: (schema: EvoluSchema) => void;
|
|
338
452
|
}
|
|
339
453
|
|
|
340
|
-
export type EvoluDeps =
|
|
341
|
-
|
|
342
|
-
NanoIdLibDep &
|
|
454
|
+
export type EvoluDeps = ConsoleDep &
|
|
455
|
+
CreateDbWorkerDep &
|
|
343
456
|
Partial<FlushSyncDep> &
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
export interface EvoluConfigWithInitialData<S extends EvoluSchema = EvoluSchema>
|
|
348
|
-
extends Config {
|
|
349
|
-
/**
|
|
350
|
-
* Use this option to create initial data (fixtures).
|
|
351
|
-
*
|
|
352
|
-
* ### Example
|
|
353
|
-
*
|
|
354
|
-
* ```ts
|
|
355
|
-
* const evolu = createEvolu(evoluReactWebDeps)(Schema, {
|
|
356
|
-
* initialData: (evolu) => {
|
|
357
|
-
* const todoCategory = evolu.insert("todoCategory", {
|
|
358
|
-
* name: "Not Urgent",
|
|
359
|
-
* });
|
|
360
|
-
*
|
|
361
|
-
* // This is a developer error, which should be fixed immediately.
|
|
362
|
-
* assert(todoCategory.ok, "invalid initial data");
|
|
363
|
-
*
|
|
364
|
-
* evolu.insert("todo", {
|
|
365
|
-
* title: "Try React Suspense",
|
|
366
|
-
* categoryId: todoCategory.value.id,
|
|
367
|
-
* });
|
|
368
|
-
* },
|
|
369
|
-
* });
|
|
370
|
-
* ```
|
|
371
|
-
*/
|
|
372
|
-
initialData?: (evolu: EvoluForInitialData<S>) => void;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
export interface EvoluForInitialData<S extends EvoluSchema = EvoluSchema> {
|
|
376
|
-
insert: Mutation<S, "insert">;
|
|
377
|
-
}
|
|
457
|
+
RandomBytesDep &
|
|
458
|
+
ReloadAppDep &
|
|
459
|
+
TimeDep;
|
|
378
460
|
|
|
379
|
-
|
|
380
|
-
const evoluInstances = new Map<string, InternalEvoluInstance>();
|
|
461
|
+
const evoluInstances = createInstances<SimpleName, InternalEvoluInstance>();
|
|
381
462
|
|
|
463
|
+
/**
|
|
464
|
+
* Unique identifier for the current browser tab or app instance, lazily
|
|
465
|
+
* initialized on first use to distinguish between multiple tabs.
|
|
466
|
+
*/
|
|
382
467
|
let tabId: Id | null = null;
|
|
383
468
|
|
|
384
469
|
/**
|
|
385
|
-
* Creates an {@link Evolu} instance configured with the specified
|
|
386
|
-
* {@link EvoluSchema} and optional
|
|
387
|
-
*
|
|
388
|
-
* This function returns a configured Evolu instance, providing a typed
|
|
389
|
-
* interface for querying, mutating, and syncing your application's data. The
|
|
390
|
-
* returned instance includes:
|
|
391
|
-
*
|
|
392
|
-
* - Subscription methods for receiving updates on queries, the owner, errors, and
|
|
393
|
-
* sync state.
|
|
394
|
-
* - Methods for creating, updating, or deleting rows in a type-safe manner.
|
|
395
|
-
* - Methods for querying data using Evolu's typed SQL queries, leveraging Kysely
|
|
396
|
-
* under the hood.
|
|
397
|
-
* - Built-in support for local-first and offline-first data with automatic sync
|
|
398
|
-
* and merging.
|
|
399
|
-
* - Automatic schema evolution that updates the underlying database with new
|
|
400
|
-
* columns or tables.
|
|
401
|
-
* - Managing owner data with resetAppOwner and restoreAppOwner.
|
|
470
|
+
* Creates an {@link Evolu} instance for a platform configured with the specified
|
|
471
|
+
* {@link EvoluSchema} and optional {@link EvoluConfig} providing a typed
|
|
472
|
+
* interface for querying, mutating, and syncing your application's data.
|
|
402
473
|
*
|
|
403
474
|
* ### Example
|
|
404
475
|
*
|
|
@@ -427,78 +498,88 @@ let tabId: Id | null = null;
|
|
|
427
498
|
*
|
|
428
499
|
* const evolu = createEvolu(evoluReactDeps)(Schema);
|
|
429
500
|
* ```
|
|
501
|
+
*
|
|
502
|
+
* ### Instance Caching
|
|
503
|
+
*
|
|
504
|
+
* `createEvolu` caches instances using {@link Instances} by {@link EvoluConfig}
|
|
505
|
+
* name to enable hot reloading and prevent database corruption from multiple
|
|
506
|
+
* connections. For testing, use unique instance names to ensure proper
|
|
507
|
+
* isolation.
|
|
430
508
|
*/
|
|
431
509
|
export const createEvolu =
|
|
432
510
|
(deps: EvoluDeps) =>
|
|
433
511
|
<S extends EvoluSchema>(
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
evoluInstances.set(config.name, evolu);
|
|
446
|
-
} else {
|
|
447
|
-
// Hot reloading. Note that indexes are intentionally omitted.
|
|
448
|
-
evolu.ensureSchema(schema);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
return evolu as IntentionalNever;
|
|
452
|
-
};
|
|
512
|
+
schema: ValidateSchema<S> extends never ? S : ValidateSchema<S>,
|
|
513
|
+
config?: EvoluConfig,
|
|
514
|
+
): Evolu<S> =>
|
|
515
|
+
evoluInstances.ensure(
|
|
516
|
+
config?.name ?? defaultDbConfig.name,
|
|
517
|
+
() => createEvoluInstance(deps)(schema as EvoluSchema, config),
|
|
518
|
+
(evolu) => {
|
|
519
|
+
// Hot reloading. Note that indexes are intentionally omitted.
|
|
520
|
+
evolu.ensureSchema(schema as EvoluSchema);
|
|
521
|
+
},
|
|
522
|
+
) as Evolu<S>;
|
|
453
523
|
|
|
454
524
|
const createEvoluInstance =
|
|
455
525
|
(deps: EvoluDeps) =>
|
|
456
|
-
(
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
deps.console.enabled = evoluConfig.enableLogging ?? false;
|
|
526
|
+
(schema: EvoluSchema, config?: EvoluConfig): InternalEvoluInstance => {
|
|
527
|
+
deps.console.enabled = config?.enableLogging ?? false;
|
|
528
|
+
|
|
529
|
+
const { indexes, reloadUrl = "/", ...partialDbConfig } = config ?? {};
|
|
461
530
|
|
|
462
|
-
|
|
531
|
+
const dbConfig: DbConfig = { ...defaultDbConfig, ...partialDbConfig };
|
|
463
532
|
|
|
464
|
-
|
|
533
|
+
deps.console.log("[evolu]", "createEvoluInstance", {
|
|
534
|
+
name: dbConfig.name,
|
|
535
|
+
});
|
|
465
536
|
|
|
466
537
|
const errorStore = createStore<EvoluError | null>(null);
|
|
467
538
|
const rowsStore = createStore<QueryRowsMap>(new Map());
|
|
468
|
-
|
|
469
|
-
const
|
|
539
|
+
|
|
540
|
+
const { promise: appOwner, resolve: resolveAppOwner } =
|
|
541
|
+
Promise.withResolvers<AppOwner>();
|
|
542
|
+
|
|
543
|
+
if (config?.externalAppOwner) {
|
|
544
|
+
resolveAppOwner(config.externalAppOwner);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// TODO: Update it for the owner-api
|
|
548
|
+
const _syncStore = createStore<SyncState>(initialSyncState);
|
|
470
549
|
|
|
471
550
|
const subscribedQueries = createSubscribedQueries(rowsStore);
|
|
472
551
|
const loadingPromises = createLoadingPromises(subscribedQueries);
|
|
473
|
-
const
|
|
552
|
+
const onCompleteCallbacks = createCallbacks(deps);
|
|
553
|
+
const exportCallbacks = createCallbacks<Uint8Array<ArrayBuffer>>(deps);
|
|
474
554
|
|
|
475
|
-
const
|
|
476
|
-
const dbWorker = deps.createDbWorker(config.name);
|
|
555
|
+
const dbWorker = deps.createDbWorker(dbConfig.name);
|
|
477
556
|
|
|
478
557
|
const getTabId = () => {
|
|
479
558
|
tabId ??= createId(deps);
|
|
480
559
|
return tabId;
|
|
481
560
|
};
|
|
482
561
|
|
|
562
|
+
// Worker responses are delivered to all tabs. Each case must handle this
|
|
563
|
+
// properly (e.g., AppOwner promise resolves only once, tabId filtering).
|
|
483
564
|
dbWorker.onMessage((message) => {
|
|
484
565
|
switch (message.type) {
|
|
485
|
-
case "
|
|
486
|
-
|
|
566
|
+
case "onError": {
|
|
567
|
+
errorStore.set(message.error);
|
|
487
568
|
break;
|
|
488
569
|
}
|
|
489
570
|
|
|
490
|
-
case "
|
|
491
|
-
|
|
571
|
+
case "onGetAppOwner": {
|
|
572
|
+
resolveAppOwner(message.appOwner);
|
|
492
573
|
break;
|
|
493
574
|
}
|
|
494
575
|
|
|
495
|
-
case "
|
|
576
|
+
case "onQueryPatches": {
|
|
496
577
|
if (message.tabId !== getTabId()) return;
|
|
497
578
|
|
|
498
579
|
const state = rowsStore.get();
|
|
499
580
|
const nextState = new Map([
|
|
500
581
|
...state,
|
|
501
|
-
...message.
|
|
582
|
+
...message.queryPatches.map(
|
|
502
583
|
({ query, patches }): [Query, ReadonlyArray<Row>] => [
|
|
503
584
|
query,
|
|
504
585
|
applyPatches(patches, state.get(query) ?? emptyRows),
|
|
@@ -506,7 +587,7 @@ const createEvoluInstance =
|
|
|
506
587
|
),
|
|
507
588
|
]);
|
|
508
589
|
|
|
509
|
-
for (const { query } of message.
|
|
590
|
+
for (const { query } of message.queryPatches) {
|
|
510
591
|
loadingPromises.resolve(query, nextState.get(query) ?? emptyRows);
|
|
511
592
|
}
|
|
512
593
|
|
|
@@ -519,33 +600,43 @@ const createEvoluInstance =
|
|
|
519
600
|
}
|
|
520
601
|
|
|
521
602
|
for (const id of message.onCompleteIds) {
|
|
522
|
-
|
|
603
|
+
onCompleteCallbacks.execute(id);
|
|
523
604
|
}
|
|
524
605
|
break;
|
|
525
606
|
}
|
|
526
607
|
|
|
527
|
-
case "
|
|
608
|
+
case "refreshQueries": {
|
|
528
609
|
if (message.tabId && message.tabId === getTabId()) return;
|
|
529
610
|
|
|
530
|
-
loadingPromises.
|
|
531
|
-
|
|
611
|
+
const loadingPromisesQueries = loadingPromises.getQueries();
|
|
612
|
+
loadingPromises.releaseUnsubscribedOnMutation();
|
|
613
|
+
|
|
614
|
+
const queries = [
|
|
615
|
+
// Dedupe
|
|
616
|
+
...new Set([...loadingPromisesQueries, ...subscribedQueries.get()]),
|
|
617
|
+
];
|
|
618
|
+
|
|
532
619
|
if (isNonEmptyReadonlyArray(queries)) {
|
|
533
620
|
dbWorker.postMessage({ type: "query", tabId: getTabId(), queries });
|
|
534
621
|
}
|
|
622
|
+
|
|
535
623
|
break;
|
|
536
624
|
}
|
|
537
625
|
|
|
538
626
|
case "onReset": {
|
|
539
627
|
if (message.reload) {
|
|
540
|
-
|
|
628
|
+
deps.reloadApp(reloadUrl);
|
|
541
629
|
} else {
|
|
542
|
-
|
|
630
|
+
onCompleteCallbacks.execute(message.onCompleteId);
|
|
543
631
|
}
|
|
544
632
|
break;
|
|
545
633
|
}
|
|
546
634
|
|
|
547
635
|
case "onExport": {
|
|
548
|
-
|
|
636
|
+
exportCallbacks.execute(
|
|
637
|
+
message.onCompleteId,
|
|
638
|
+
message.file as Uint8Array<ArrayBuffer>,
|
|
639
|
+
);
|
|
549
640
|
break;
|
|
550
641
|
}
|
|
551
642
|
|
|
@@ -554,10 +645,7 @@ const createEvoluInstance =
|
|
|
554
645
|
}
|
|
555
646
|
});
|
|
556
647
|
|
|
557
|
-
const dbSchema =
|
|
558
|
-
assertValidEvoluSchema(schema),
|
|
559
|
-
indexes,
|
|
560
|
-
);
|
|
648
|
+
const dbSchema = evoluSchemaToDbSchema(schema, indexes);
|
|
561
649
|
|
|
562
650
|
const mutationTypesCache = new Map<
|
|
563
651
|
MutationKind,
|
|
@@ -581,48 +669,19 @@ const createEvoluInstance =
|
|
|
581
669
|
return type;
|
|
582
670
|
};
|
|
583
671
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
/**
|
|
587
|
-
* Note that the initial data function is called even if it is unnecessary
|
|
588
|
-
* (initial data are already in the DB) because we don't want to wait for
|
|
589
|
-
* SQLite's response. Initial data should be small (because they are inlined
|
|
590
|
-
* in the code), so it's ok.
|
|
591
|
-
*/
|
|
592
|
-
if (initialData)
|
|
593
|
-
initialData({
|
|
594
|
-
insert: (table, props) => {
|
|
595
|
-
const Type = getMutationType(table, "insert");
|
|
596
|
-
const id = createId(deps);
|
|
597
|
-
|
|
598
|
-
const result = Type.fromUnknown(props);
|
|
599
|
-
|
|
600
|
-
if (result.ok) {
|
|
601
|
-
initialDataDbChanges.push({
|
|
602
|
-
id,
|
|
603
|
-
table,
|
|
604
|
-
values: result.value,
|
|
605
|
-
} as unknown as DbChange);
|
|
606
|
-
return ok({ id });
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
return result;
|
|
610
|
-
},
|
|
611
|
-
});
|
|
672
|
+
dbWorker.postMessage({ type: "init", config: dbConfig, dbSchema });
|
|
612
673
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
config,
|
|
616
|
-
dbSchema,
|
|
617
|
-
initialData: initialDataDbChanges,
|
|
618
|
-
});
|
|
674
|
+
// We can't use `init` to get AppOwner because `init` runs only once per n tabs.
|
|
675
|
+
dbWorker.postMessage({ type: "getAppOwner" });
|
|
619
676
|
|
|
620
677
|
const loadQueryMicrotaskQueue: Array<Query> = [];
|
|
621
678
|
|
|
622
679
|
const mutateMicrotaskQueue: Array<
|
|
623
|
-
[
|
|
680
|
+
[MutationChange | null, MutationOptions["onComplete"] | undefined]
|
|
624
681
|
> = [];
|
|
625
682
|
|
|
683
|
+
const useOwnerMicrotaskQueue: Array<[SyncOwner, boolean, Uint8Array]> = [];
|
|
684
|
+
|
|
626
685
|
const createMutation =
|
|
627
686
|
<Kind extends MutationKind>(kind: Kind) =>
|
|
628
687
|
<TableName extends keyof typeof schema>(
|
|
@@ -638,8 +697,7 @@ const createEvoluInstance =
|
|
|
638
697
|
ObjectType<MutationMapping<(typeof schema)[TableName], Kind>>
|
|
639
698
|
>
|
|
640
699
|
> => {
|
|
641
|
-
const
|
|
642
|
-
const result = Type.fromUnknown(props);
|
|
700
|
+
const result = getMutationType(table, kind).fromUnknown(props);
|
|
643
701
|
|
|
644
702
|
const id =
|
|
645
703
|
kind === "insert"
|
|
@@ -648,51 +706,32 @@ const createEvoluInstance =
|
|
|
648
706
|
|
|
649
707
|
if (options?.onlyValidate !== true) {
|
|
650
708
|
if (!result.ok) {
|
|
651
|
-
//
|
|
652
|
-
|
|
653
|
-
mutateMicrotaskQueue.push([undefined, undefined]);
|
|
709
|
+
// Mark the transaction as invalid by pushing null
|
|
710
|
+
mutateMicrotaskQueue.push([null, undefined]);
|
|
654
711
|
} else {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
// EvoluSchema Types ensure valid types.
|
|
658
|
-
const change = { table, id, values } as unknown as DbChange;
|
|
659
|
-
mutateMicrotaskQueue.push([change, options?.onComplete]);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
if (mutateMicrotaskQueue.length === 1)
|
|
663
|
-
queueMicrotask(() => {
|
|
664
|
-
const changes = [];
|
|
665
|
-
const onCompletes = [];
|
|
712
|
+
const values = { ...result.value };
|
|
713
|
+
delete values.id;
|
|
666
714
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
715
|
+
if (kind === "insert" || kind === "upsert") {
|
|
716
|
+
// Only set createdAt if not provided by user
|
|
717
|
+
if (!("createdAt" in values)) {
|
|
718
|
+
values.createdAt = new Date(deps.time.now()).toISOString();
|
|
670
719
|
}
|
|
720
|
+
}
|
|
671
721
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
if (changes.length !== mutateMicrotaskQueueLength) {
|
|
678
|
-
return;
|
|
679
|
-
}
|
|
722
|
+
const dbChange = { table, id, values };
|
|
723
|
+
assert(
|
|
724
|
+
DbChange.is(dbChange),
|
|
725
|
+
`Failed to create DbChange for table "${dbChange.table}"`,
|
|
726
|
+
);
|
|
680
727
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
loadingPromises.releaseUnsubscribed();
|
|
728
|
+
const mutationChange = { ...dbChange, ownerId: options?.ownerId };
|
|
729
|
+
mutateMicrotaskQueue.push([mutationChange, options?.onComplete]);
|
|
730
|
+
}
|
|
686
731
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
tabId: getTabId(),
|
|
691
|
-
changes,
|
|
692
|
-
onCompleteIds,
|
|
693
|
-
subscribedQueries: subscribedQueries.get(),
|
|
694
|
-
});
|
|
695
|
-
});
|
|
732
|
+
if (mutateMicrotaskQueue.length === 1) {
|
|
733
|
+
queueMicrotask(processMutationQueue);
|
|
734
|
+
}
|
|
696
735
|
}
|
|
697
736
|
|
|
698
737
|
if (result.ok) return ok({ id });
|
|
@@ -706,28 +745,43 @@ const createEvoluInstance =
|
|
|
706
745
|
);
|
|
707
746
|
};
|
|
708
747
|
|
|
748
|
+
const processMutationQueue = () => {
|
|
749
|
+
const changes: Array<MutationChange> = [];
|
|
750
|
+
const onCompletes = [];
|
|
751
|
+
|
|
752
|
+
for (const [change, onComplete] of mutateMicrotaskQueue) {
|
|
753
|
+
if (change !== null) changes.push(change);
|
|
754
|
+
if (onComplete) onCompletes.push(onComplete);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const queueLength = mutateMicrotaskQueue.length;
|
|
758
|
+
mutateMicrotaskQueue.length = 0;
|
|
759
|
+
|
|
760
|
+
// Don't process any mutations if there was a validation error.
|
|
761
|
+
// All mutations within a queue run as a single transaction.
|
|
762
|
+
if (changes.length !== queueLength) {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const onCompleteIds = onCompletes.map(onCompleteCallbacks.register);
|
|
767
|
+
loadingPromises.releaseUnsubscribedOnMutation();
|
|
768
|
+
|
|
769
|
+
if (!isNonEmptyArray(changes)) return;
|
|
770
|
+
|
|
771
|
+
dbWorker.postMessage({
|
|
772
|
+
type: "mutate",
|
|
773
|
+
tabId: getTabId(),
|
|
774
|
+
changes,
|
|
775
|
+
onCompleteIds,
|
|
776
|
+
subscribedQueries: subscribedQueries.get(),
|
|
777
|
+
});
|
|
778
|
+
};
|
|
779
|
+
|
|
709
780
|
const evolu: InternalEvoluInstance = {
|
|
710
781
|
subscribeError: errorStore.subscribe,
|
|
711
782
|
getError: errorStore.get,
|
|
712
783
|
|
|
713
|
-
createQuery
|
|
714
|
-
const compiledQuery = queryCallback(
|
|
715
|
-
kysely as IntentionalNever,
|
|
716
|
-
).compile();
|
|
717
|
-
|
|
718
|
-
if (isSqlMutation(compiledQuery.sql))
|
|
719
|
-
throw new Error(
|
|
720
|
-
"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.",
|
|
721
|
-
);
|
|
722
|
-
|
|
723
|
-
return serializeQuery({
|
|
724
|
-
sql: compiledQuery.sql as SafeSql,
|
|
725
|
-
parameters: compiledQuery.parameters as NonNullable<
|
|
726
|
-
SqliteQuery["parameters"]
|
|
727
|
-
>,
|
|
728
|
-
...(options && { options }),
|
|
729
|
-
});
|
|
730
|
-
},
|
|
784
|
+
createQuery,
|
|
731
785
|
|
|
732
786
|
loadQuery: <R extends Row>(query: Query<R>): Promise<QueryRows<R>> => {
|
|
733
787
|
const { promise, isNew } = loadingPromises.get(query);
|
|
@@ -740,6 +794,7 @@ const createEvoluInstance =
|
|
|
740
794
|
const queries = [...new Set(loadQueryMicrotaskQueue)];
|
|
741
795
|
loadQueryMicrotaskQueue.length = 0;
|
|
742
796
|
assertNonEmptyArray(queries);
|
|
797
|
+
deps.console.log("[evolu]", "loadQuery", { queries });
|
|
743
798
|
dbWorker.postMessage({
|
|
744
799
|
type: "query",
|
|
745
800
|
tabId: getTabId(),
|
|
@@ -775,23 +830,19 @@ const createEvoluInstance =
|
|
|
775
830
|
getQueryRows: <R extends Row>(query: Query<R>): QueryRows<R> =>
|
|
776
831
|
(rowsStore.get().get(query) ?? emptyRows) as QueryRows<R>,
|
|
777
832
|
|
|
778
|
-
|
|
779
|
-
getAppOwner: ownerStore.get,
|
|
833
|
+
appOwner,
|
|
780
834
|
|
|
781
|
-
|
|
782
|
-
|
|
835
|
+
// TODO: Update it for the owner-api
|
|
836
|
+
// subscribeSyncState: syncStore.subscribe,
|
|
837
|
+
// getSyncState: syncStore.get,
|
|
783
838
|
|
|
784
839
|
insert: createMutation("insert"),
|
|
785
840
|
update: createMutation("update"),
|
|
786
841
|
upsert: createMutation("upsert"),
|
|
787
842
|
|
|
788
843
|
resetAppOwner: (options) => {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
const { promise, resolve } = Promise.withResolvers<void>();
|
|
792
|
-
const onCompleteId = callbacks.register(() => {
|
|
793
|
-
resolve();
|
|
794
|
-
});
|
|
844
|
+
const { promise, resolve } = Promise.withResolvers<undefined>();
|
|
845
|
+
const onCompleteId = onCompleteCallbacks.register(resolve);
|
|
795
846
|
dbWorker.postMessage({
|
|
796
847
|
type: "reset",
|
|
797
848
|
onCompleteId,
|
|
@@ -801,12 +852,8 @@ const createEvoluInstance =
|
|
|
801
852
|
},
|
|
802
853
|
|
|
803
854
|
restoreAppOwner: (mnemonic, options) => {
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
const { promise, resolve } = Promise.withResolvers<void>();
|
|
807
|
-
const onCompleteId = callbacks.register(() => {
|
|
808
|
-
resolve();
|
|
809
|
-
});
|
|
855
|
+
const { promise, resolve } = Promise.withResolvers<undefined>();
|
|
856
|
+
const onCompleteId = onCompleteCallbacks.register(resolve);
|
|
810
857
|
dbWorker.postMessage({
|
|
811
858
|
type: "reset",
|
|
812
859
|
onCompleteId,
|
|
@@ -817,35 +864,105 @@ const createEvoluInstance =
|
|
|
817
864
|
},
|
|
818
865
|
|
|
819
866
|
reloadApp: () => {
|
|
820
|
-
|
|
867
|
+
deps.reloadApp(reloadUrl);
|
|
821
868
|
},
|
|
822
869
|
|
|
823
870
|
ensureSchema: (schema) => {
|
|
824
871
|
mutationTypesCache.clear();
|
|
825
|
-
const
|
|
826
|
-
dbWorker.postMessage({
|
|
827
|
-
type: "ensureDbSchema",
|
|
828
|
-
dbSchema: validEvoluSchemaToDbSchema(validSchema),
|
|
829
|
-
});
|
|
872
|
+
const dbSchema = evoluSchemaToDbSchema(schema);
|
|
873
|
+
dbWorker.postMessage({ type: "ensureDbSchema", dbSchema });
|
|
830
874
|
},
|
|
831
875
|
|
|
832
876
|
exportDatabase: () => {
|
|
833
|
-
const { promise, resolve } =
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
});
|
|
877
|
+
const { promise, resolve } =
|
|
878
|
+
Promise.withResolvers<Uint8Array<ArrayBuffer>>();
|
|
879
|
+
const onCompleteId = exportCallbacks.register(resolve);
|
|
837
880
|
dbWorker.postMessage({ type: "export", onCompleteId });
|
|
838
881
|
return promise;
|
|
839
882
|
},
|
|
883
|
+
|
|
884
|
+
useOwner: (owner) => {
|
|
885
|
+
const scheduleOwnerQueueProcessing = () => {
|
|
886
|
+
if (useOwnerMicrotaskQueue.length !== 1) return;
|
|
887
|
+
queueMicrotask(() => {
|
|
888
|
+
const queue = [...useOwnerMicrotaskQueue];
|
|
889
|
+
useOwnerMicrotaskQueue.length = 0;
|
|
890
|
+
|
|
891
|
+
const result: Array<[SyncOwner, boolean, Uint8Array]> = [];
|
|
892
|
+
const skipIndices = new Set<number>();
|
|
893
|
+
|
|
894
|
+
for (let i = 0; i < queue.length; i++) {
|
|
895
|
+
if (skipIndices.has(i)) continue;
|
|
896
|
+
|
|
897
|
+
const [currentOwner, currentUse, currentOwnerSerialized] =
|
|
898
|
+
queue[i];
|
|
899
|
+
|
|
900
|
+
// Look for opposite action with same owner
|
|
901
|
+
for (let j = i + 1; j < queue.length; j++) {
|
|
902
|
+
if (skipIndices.has(j)) continue;
|
|
903
|
+
|
|
904
|
+
const [, otherUse, otherOwnerSerialized] = queue[j];
|
|
905
|
+
|
|
906
|
+
if (
|
|
907
|
+
currentUse !== otherUse &&
|
|
908
|
+
eqArrayNumber(currentOwnerSerialized, otherOwnerSerialized)
|
|
909
|
+
) {
|
|
910
|
+
// Found cancel-out pair, skip both
|
|
911
|
+
skipIndices.add(i).add(j);
|
|
912
|
+
break;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
if (!skipIndices.has(i)) {
|
|
917
|
+
result.push([currentOwner, currentUse, currentOwnerSerialized]);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
for (const [owner, use] of result) {
|
|
922
|
+
dbWorker.postMessage({ type: "useOwner", owner, use });
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
useOwnerMicrotaskQueue.push([owner, true, pack(owner)]);
|
|
928
|
+
scheduleOwnerQueueProcessing();
|
|
929
|
+
|
|
930
|
+
const unuse = () => {
|
|
931
|
+
useOwnerMicrotaskQueue.push([owner, false, pack(owner)]);
|
|
932
|
+
scheduleOwnerQueueProcessing();
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
return unuse;
|
|
936
|
+
},
|
|
937
|
+
|
|
938
|
+
/** Disposal is not implemented yet. */
|
|
939
|
+
[Symbol.dispose]: () => {
|
|
940
|
+
throw new Error("Evolu instance disposal is not yet implemented");
|
|
941
|
+
},
|
|
840
942
|
};
|
|
841
943
|
|
|
842
944
|
return evolu;
|
|
843
945
|
};
|
|
844
946
|
|
|
845
|
-
export const
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
947
|
+
export const createQuery = <R extends Row>(
|
|
948
|
+
queryCallback: Parameters<CreateQuery<EvoluSchema>>[0],
|
|
949
|
+
options?: Parameters<CreateQuery<EvoluSchema>>[1],
|
|
950
|
+
): Query<R> => {
|
|
951
|
+
const compiledQuery = queryCallback(kysely as IntentionalNever).compile();
|
|
952
|
+
|
|
953
|
+
if (isSqlMutation(compiledQuery.sql))
|
|
954
|
+
throw new Error(
|
|
955
|
+
"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.",
|
|
956
|
+
);
|
|
957
|
+
|
|
958
|
+
return serializeQuery({
|
|
959
|
+
sql: compiledQuery.sql as SafeSql,
|
|
960
|
+
parameters: compiledQuery.parameters as NonNullable<
|
|
961
|
+
SqliteQuery["parameters"]
|
|
962
|
+
>,
|
|
963
|
+
...(options && { options }),
|
|
964
|
+
});
|
|
965
|
+
};
|
|
849
966
|
|
|
850
967
|
interface LoadingPromises {
|
|
851
968
|
get: <R extends Row>(
|
|
@@ -857,11 +974,13 @@ interface LoadingPromises {
|
|
|
857
974
|
|
|
858
975
|
resolve: (query: Query, rows: ReadonlyArray<Row>) => void;
|
|
859
976
|
|
|
860
|
-
|
|
977
|
+
releaseUnsubscribedOnMutation: () => void;
|
|
978
|
+
|
|
979
|
+
getQueries: () => ReadonlyArray<Query>;
|
|
861
980
|
}
|
|
862
981
|
|
|
863
982
|
interface LoadingPromise {
|
|
864
|
-
/** Promise with props for the
|
|
983
|
+
/** Promise with props for the React use hook. */
|
|
865
984
|
promise: Promise<QueryRows> & {
|
|
866
985
|
status?: "pending" | "fulfilled" | "rejected";
|
|
867
986
|
value?: QueryRows;
|
|
@@ -876,7 +995,7 @@ const createLoadingPromises = (
|
|
|
876
995
|
): LoadingPromises => {
|
|
877
996
|
const loadingPromiseMap = new Map<Query, LoadingPromise>();
|
|
878
997
|
|
|
879
|
-
|
|
998
|
+
return {
|
|
880
999
|
get: <R extends Row>(
|
|
881
1000
|
query: Query<R>,
|
|
882
1001
|
): {
|
|
@@ -903,16 +1022,13 @@ const createLoadingPromises = (
|
|
|
903
1022
|
if (loadingPromise.promise.status !== "fulfilled") {
|
|
904
1023
|
loadingPromise.resolve(rows);
|
|
905
1024
|
} else {
|
|
906
|
-
// A promise can't be fulfilled 2x, so we need a new one.
|
|
907
1025
|
loadingPromise.promise = Promise.resolve(rows);
|
|
908
1026
|
}
|
|
909
1027
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
* https://github.com/acdlite/rfcs/blob/first-class-promises/text/0000-first-class-support-for-promises.md
|
|
915
|
-
*/
|
|
1028
|
+
// Set status and value fields for React's `use` Hook to unwrap synchronously.
|
|
1029
|
+
// While undocumented in React docs, React still uses these properties internally,
|
|
1030
|
+
// and Evolu's own promise caching logic depends on checking `promise.status`.
|
|
1031
|
+
// https://github.com/acdlite/rfcs/blob/first-class-promises/text/0000-first-class-support-for-promises.md
|
|
916
1032
|
void Object.assign(loadingPromise.promise, {
|
|
917
1033
|
status: "fulfilled",
|
|
918
1034
|
value: rows,
|
|
@@ -923,14 +1039,7 @@ const createLoadingPromises = (
|
|
|
923
1039
|
}
|
|
924
1040
|
},
|
|
925
1041
|
|
|
926
|
-
|
|
927
|
-
* We can't delete loading promises in `resolveLoadingPromises` because they
|
|
928
|
-
* must be cached, so repeated calls to `loadQuery` will always return the
|
|
929
|
-
* same promise until the data changes, and we also can't cache them forever
|
|
930
|
-
* because only subscribed queries are automatically updated (reactivity is
|
|
931
|
-
* expensive) hence this function must be called manually on any mutation.
|
|
932
|
-
*/
|
|
933
|
-
releaseUnsubscribed: () => {
|
|
1042
|
+
releaseUnsubscribedOnMutation: () => {
|
|
934
1043
|
[...loadingPromiseMap.entries()]
|
|
935
1044
|
.filter(([query]) => !subscribedQueries.has(query))
|
|
936
1045
|
.forEach(([query, loadingPromise]) => {
|
|
@@ -941,7 +1050,7 @@ const createLoadingPromises = (
|
|
|
941
1050
|
}
|
|
942
1051
|
});
|
|
943
1052
|
},
|
|
944
|
-
};
|
|
945
1053
|
|
|
946
|
-
|
|
1054
|
+
getQueries: () => Array.from(loadingPromiseMap.keys()),
|
|
1055
|
+
};
|
|
947
1056
|
};
|