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