@evolu/common 6.0.1-preview.19 → 6.0.1-preview.20
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/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +1 -1
- package/dist/src/Buffer.d.ts +1 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +1 -1
- package/dist/src/CallbackRegistry.d.ts +53 -0
- package/dist/src/CallbackRegistry.d.ts.map +1 -0
- package/dist/src/CallbackRegistry.js +25 -0
- 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 +48 -37
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +27 -50
- package/dist/src/Evolu/Db.d.ts +138 -66
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +248 -645
- 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 +79 -116
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +275 -132
- 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 +144 -0
- package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
- package/dist/src/Evolu/LocalAuth.js +171 -0
- package/dist/src/Evolu/Owner.d.ts +129 -83
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +80 -89
- 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 +114 -191
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +409 -416
- 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/Relay.d.ts +1 -2
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +11 -9
- package/dist/src/Evolu/Schema.d.ts +88 -27
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +141 -24
- package/dist/src/Evolu/Storage.d.ts +158 -14
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +32 -32
- package/dist/src/Evolu/Sync.d.ts +77 -13
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +453 -20
- package/dist/src/Evolu/Timestamp.d.ts +29 -27
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +20 -18
- package/dist/src/ManyToManyMap.d.ts +74 -10
- package/dist/src/ManyToManyMap.d.ts.map +1 -1
- package/dist/src/ManyToManyMap.js +41 -6
- package/dist/src/Random.d.ts +3 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/RefCountedResourceManager.d.ts +119 -0
- package/dist/src/RefCountedResourceManager.d.ts.map +1 -0
- package/dist/src/RefCountedResourceManager.js +197 -0
- package/dist/src/Result.d.ts +144 -22
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +5 -2
- package/dist/src/Sqlite.d.ts +20 -4
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +50 -8
- package/dist/src/Task.d.ts +511 -0
- package/dist/src/Task.d.ts.map +1 -0
- package/dist/src/Task.js +410 -0
- package/dist/src/Time.d.ts +59 -0
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +87 -4
- package/dist/src/Type.d.ts +431 -341
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +458 -466
- package/dist/src/WebSocket.d.ts +5 -2
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +12 -13
- 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 +2 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -3
- package/package.json +7 -7
- package/src/Assert.ts +2 -4
- package/src/Buffer.ts +1 -1
- package/src/CallbackRegistry.ts +84 -0
- package/src/Console.ts +91 -11
- package/src/Crypto.ts +78 -91
- package/src/Evolu/Db.ts +455 -947
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +545 -307
- package/src/Evolu/Internal.ts +0 -2
- package/src/Evolu/LocalAuth.ts +422 -0
- package/src/Evolu/Owner.ts +191 -131
- package/src/Evolu/Platform.ts +9 -9
- package/src/Evolu/Protocol.ts +536 -653
- package/src/Evolu/Public.ts +7 -9
- package/src/Evolu/PublicKysely.ts +3 -3
- package/src/Evolu/Relay.ts +17 -12
- package/src/Evolu/Schema.ts +271 -66
- package/src/Evolu/Storage.ts +263 -55
- package/src/Evolu/Sync.ts +758 -37
- package/src/Evolu/Timestamp.ts +30 -35
- package/src/ManyToManyMap.ts +127 -24
- package/src/Random.ts +3 -2
- package/src/RefCountedResourceManager.ts +368 -0
- package/src/Result.ts +149 -23
- package/src/Sqlite.ts +59 -24
- package/src/Task.ts +779 -0
- package/src/Time.ts +168 -4
- package/src/Type.ts +657 -695
- package/src/WebSocket.ts +23 -17
- package/src/Worker.ts +72 -23
- package/src/index.ts +2 -3
- package/dist/src/Callbacks.d.ts +0 -20
- package/dist/src/Callbacks.d.ts.map +0 -1
- package/dist/src/Callbacks.js +0 -18
- package/dist/src/Evolu/Config.d.ts +0 -82
- 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/Callbacks.ts +0 -43
- package/src/Evolu/Config.ts +0 -97
- 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,12 @@
|
|
|
1
|
+
import { pack } from "msgpackr";
|
|
1
2
|
import { isNonEmptyArray, isNonEmptyReadonlyArray } from "../Array.js";
|
|
2
3
|
import { assert, assertNonEmptyArray } from "../Assert.js";
|
|
3
|
-
import {
|
|
4
|
+
import { createCallbackRegistry } from "../CallbackRegistry.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 { NanoIdLibDep } from "../NanoId.js";
|
|
9
10
|
import { err, ok, Result } from "../Result.js";
|
|
10
11
|
import { isSqlMutation, SafeSql, SqliteError, SqliteQuery } from "../Sqlite.js";
|
|
11
12
|
import { createStore, StoreSubscribe } from "../Store.js";
|
|
@@ -18,19 +19,16 @@ import {
|
|
|
18
19
|
InferType,
|
|
19
20
|
Mnemonic,
|
|
20
21
|
ObjectType,
|
|
22
|
+
ValidMutationSize,
|
|
23
|
+
ValidMutationSizeError,
|
|
21
24
|
} from "../Type.js";
|
|
22
25
|
import { IntentionalNever } from "../Types.js";
|
|
23
|
-
import {
|
|
24
|
-
import { CreateDbWorkerDep } from "./Db.js";
|
|
26
|
+
import { CreateDbWorkerDep, DbConfig, defaultDbConfig } from "./Db.js";
|
|
25
27
|
import { applyPatches } from "./Diff.js";
|
|
26
|
-
import {
|
|
28
|
+
import { LocalAuthDep } from "./LocalAuth.js";
|
|
27
29
|
import { AppOwner } from "./Owner.js";
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
DbChange,
|
|
31
|
-
ProtocolError,
|
|
32
|
-
ProtocolUnsupportedVersionError,
|
|
33
|
-
} from "./Protocol.js";
|
|
30
|
+
import { FlushSyncDep, ReloadAppDep } from "./Platform.js";
|
|
31
|
+
import { ProtocolError, ProtocolUnsupportedVersionError } from "./Protocol.js";
|
|
34
32
|
import {
|
|
35
33
|
createSubscribedQueries,
|
|
36
34
|
emptyRows,
|
|
@@ -48,20 +46,95 @@ import {
|
|
|
48
46
|
DefaultColumns,
|
|
49
47
|
EvoluSchema,
|
|
50
48
|
evoluSchemaToDbSchema,
|
|
49
|
+
IndexesConfig,
|
|
51
50
|
insertable,
|
|
51
|
+
kysely,
|
|
52
52
|
Mutation,
|
|
53
|
+
MutationChange,
|
|
53
54
|
MutationKind,
|
|
54
55
|
MutationMapping,
|
|
55
56
|
MutationOptions,
|
|
56
57
|
updateable,
|
|
57
58
|
upsertable,
|
|
58
59
|
ValidateSchema,
|
|
59
|
-
ValidMutationSize,
|
|
60
|
-
ValidMutationSizeError,
|
|
61
60
|
} from "./Schema.js";
|
|
62
|
-
import {
|
|
61
|
+
import { DbChange } from "./Storage.js";
|
|
62
|
+
import { initialSyncState, SyncOwner, SyncState } from "./Sync.js";
|
|
63
63
|
import { TimestampError } from "./Timestamp.js";
|
|
64
64
|
|
|
65
|
+
export interface EvoluConfig extends Partial<DbConfig> {
|
|
66
|
+
/**
|
|
67
|
+
* Use the `indexes` option to define SQLite indexes.
|
|
68
|
+
*
|
|
69
|
+
* Table and column names are not typed because Kysely doesn't support it.
|
|
70
|
+
*
|
|
71
|
+
* https://medium.com/@JasonWyatt/squeezing-performance-from-sqlite-indexes-indexes-c4e175f3c346
|
|
72
|
+
*
|
|
73
|
+
* ### Example
|
|
74
|
+
*
|
|
75
|
+
* ```ts
|
|
76
|
+
* const evolu = createEvolu(evoluReactDeps)(Schema, {
|
|
77
|
+
* indexes: (create) => [
|
|
78
|
+
* create("todoCreatedAt").on("todo").column("createdAt"),
|
|
79
|
+
* create("todoCategoryCreatedAt")
|
|
80
|
+
* .on("todoCategory")
|
|
81
|
+
* .column("createdAt"),
|
|
82
|
+
* ],
|
|
83
|
+
* });
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
readonly indexes?: IndexesConfig;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* URL to reload browser tabs after reset or restore.
|
|
90
|
+
*
|
|
91
|
+
* The default value is `/`.
|
|
92
|
+
*/
|
|
93
|
+
readonly reloadUrl?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// /**
|
|
97
|
+
// * Validated database change with schema-typed values.
|
|
98
|
+
// *
|
|
99
|
+
// * This is a tagged union where the tag is the table name and the values are
|
|
100
|
+
// * updateable (validated against the schema). This represents the content of a
|
|
101
|
+
// * {@link CrdtMessage} without the timestamp, which is sufficient for business
|
|
102
|
+
// * logic validation in {@link EvoluConfig.onMessage}.
|
|
103
|
+
// */
|
|
104
|
+
// export type ValidatedDbChange<S extends EvoluSchema> = {
|
|
105
|
+
// [Table in keyof S]: {
|
|
106
|
+
// readonly table: Table;
|
|
107
|
+
// readonly id: Id;
|
|
108
|
+
// readonly values: Updateable<S[Table]> & { readonly createdAt?: DateIso };
|
|
109
|
+
// };
|
|
110
|
+
// }[keyof S];
|
|
111
|
+
|
|
112
|
+
// /**
|
|
113
|
+
// * Local-only mutation interface for use within {@link EvoluConfig.onMessage}
|
|
114
|
+
// * callback.
|
|
115
|
+
// *
|
|
116
|
+
// * Provides type-safe mutation methods that only accept tables with names
|
|
117
|
+
// * starting with underscore (local-only tables). All methods require fully
|
|
118
|
+
// * validated branded values. No validation is performed as TypeScript ensures
|
|
119
|
+
// * type correctness.
|
|
120
|
+
// */
|
|
121
|
+
// export interface LocalOnly<S extends EvoluSchema> {
|
|
122
|
+
// readonly insert: <T extends keyof S & `_${string}`>(
|
|
123
|
+
// table: T,
|
|
124
|
+
// values: InferType<ObjectType<InsertableProps<S[T]>>>,
|
|
125
|
+
// ) => InferType<S[T]["id"]>;
|
|
126
|
+
|
|
127
|
+
// readonly update: <T extends keyof S & `_${string}`>(
|
|
128
|
+
// table: T,
|
|
129
|
+
// values: InferType<ObjectType<UpdateableProps<S[T]>>>,
|
|
130
|
+
// ) => void;
|
|
131
|
+
|
|
132
|
+
// readonly upsert: <T extends keyof S & `_${string}`>(
|
|
133
|
+
// table: T,
|
|
134
|
+
// values: InferType<ObjectType<UpsertableProps<S[T]>>>,
|
|
135
|
+
// ) => void;
|
|
136
|
+
// }
|
|
137
|
+
|
|
65
138
|
export interface Evolu<S extends EvoluSchema = EvoluSchema> {
|
|
66
139
|
/**
|
|
67
140
|
* Subscribe to {@link EvoluError} changes.
|
|
@@ -89,7 +162,8 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
|
|
|
89
162
|
* All this function does is compile the Kysely query and serialize it into a
|
|
90
163
|
* unique string. Both operations are fast and cheap.
|
|
91
164
|
*
|
|
92
|
-
* For mutations, use {@link Evolu#insert}
|
|
165
|
+
* For mutations, use {@link Evolu#insert}, {@link Evolu#update}, or
|
|
166
|
+
* {@link Evolu#upsert}.
|
|
93
167
|
*
|
|
94
168
|
* ### Example
|
|
95
169
|
*
|
|
@@ -109,43 +183,20 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
|
|
|
109
183
|
/**
|
|
110
184
|
* Load {@link Query} and return a promise with {@link QueryRows}.
|
|
111
185
|
*
|
|
112
|
-
*
|
|
113
|
-
* why loading should fail. All data are local, and the query is typed.
|
|
114
|
-
*
|
|
115
|
-
* {@link Evolu#subscribeError}.
|
|
186
|
+
* The returned promise always resolves successfully because there is no
|
|
187
|
+
* reason why loading should fail. All data are local, and the query is typed.
|
|
188
|
+
* Unexpected errors are handled with {@link Evolu#subscribeError}.
|
|
116
189
|
*
|
|
117
190
|
* Loading is batched, and returned promises are cached, so there is no need
|
|
118
|
-
* for an additional cache. Evolu's internal cache is invalidated on
|
|
119
|
-
*
|
|
191
|
+
* for an additional cache. Evolu's internal cache is invalidated on mutation.
|
|
192
|
+
* Unsubscribed queries are removed from the cache, so loading them again will
|
|
193
|
+
* return a new pending promise. Subscribed queries remain in the cache to
|
|
194
|
+
* prevent unnecessary Suspense boundaries from activating. Their promises are
|
|
195
|
+
* replaced with `Promise.resolve(rows)`, allowing React to synchronously
|
|
196
|
+
* unwrap the updated data without suspending.
|
|
120
197
|
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* them. Speaking of React, there are two essential React Suspense-related
|
|
124
|
-
* patterns that every developer should be aware of—passing promises to
|
|
125
|
-
* children and caching over mutations.
|
|
126
|
-
*
|
|
127
|
-
* With promises passed to children, we can load a query as soon as possible,
|
|
128
|
-
* but we don't have to use the returned promise immediately. That's useful
|
|
129
|
-
* for prefetching, which is generally not necessary for local-first apps but
|
|
130
|
-
* can be if a query takes a long time to load.
|
|
131
|
-
*
|
|
132
|
-
* Caching over mutation is a pattern that every developer should know. As we
|
|
133
|
-
* said, Evolu caches promise until a mutation happens. A query loaded after
|
|
134
|
-
* that will return a new pending promise. That's okay for general usage but
|
|
135
|
-
* not for UI with React Suspense because a mutation would suspend rerendered
|
|
136
|
-
* queries on a page, and that's not a good UX.
|
|
137
|
-
*
|
|
138
|
-
* We call this pattern "caching over mutation" because it has no globally
|
|
139
|
-
* accepted name yet. React RFC for React Cache does not exist yet.
|
|
140
|
-
*
|
|
141
|
-
* For better UX, a query must be subscribed for updates. This way, instead of
|
|
142
|
-
* Suspense flashes, the user sees new data immediately because Evolu replaces
|
|
143
|
-
* cached promises with fresh, already resolved new ones.
|
|
144
|
-
*
|
|
145
|
-
* If you are curious why Evolu does not do that for all queries by default,
|
|
146
|
-
* the answer is simple: performance. Tracking changes is costly and
|
|
147
|
-
* meaningful only for visible (hence subscribed) queries anyway. To subscribe
|
|
148
|
-
* to a query, use {@link Evolu#subscribeQuery}.
|
|
198
|
+
* To subscribe a query for automatic updates, use
|
|
199
|
+
* {@link Evolu#subscribeQuery}.
|
|
149
200
|
*
|
|
150
201
|
* ### Example
|
|
151
202
|
*
|
|
@@ -153,7 +204,7 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
|
|
|
153
204
|
* const allTodos = evolu.createQuery((db) =>
|
|
154
205
|
* db.selectFrom("todo").selectAll(),
|
|
155
206
|
* );
|
|
156
|
-
* evolu.loadQuery(allTodos).then((
|
|
207
|
+
* evolu.loadQuery(allTodos).then((rows) => {
|
|
157
208
|
* console.log(rows);
|
|
158
209
|
* });
|
|
159
210
|
* ```
|
|
@@ -202,56 +253,42 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
|
|
|
202
253
|
readonly getQueryRows: <R extends Row>(query: Query<R>) => QueryRows<R>;
|
|
203
254
|
|
|
204
255
|
/**
|
|
205
|
-
*
|
|
256
|
+
* Promise that resolves to {@link AppOwner} when available.
|
|
206
257
|
*
|
|
207
258
|
* ### Example
|
|
208
259
|
*
|
|
209
260
|
* ```ts
|
|
210
|
-
* const
|
|
211
|
-
* const owner = evolu.getAppOwner();
|
|
212
|
-
* });
|
|
261
|
+
* const owner = await evolu.appOwner;
|
|
213
262
|
* ```
|
|
214
263
|
*/
|
|
215
|
-
readonly
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Get {@link SyncState}.
|
|
245
|
-
*
|
|
246
|
-
* ### Example
|
|
247
|
-
*
|
|
248
|
-
* ```ts
|
|
249
|
-
* const unsubscribe = evolu.subscribeSyncState(() => {
|
|
250
|
-
* const syncState = evolu.getSyncState();
|
|
251
|
-
* });
|
|
252
|
-
* ```
|
|
253
|
-
*/
|
|
254
|
-
readonly getSyncState: () => SyncState;
|
|
264
|
+
readonly appOwner: Promise<AppOwner>;
|
|
265
|
+
|
|
266
|
+
// TODO: Update it for the owners
|
|
267
|
+
// /**
|
|
268
|
+
// * Subscribe to {@link SyncState} changes.
|
|
269
|
+
// *
|
|
270
|
+
// * ### Example
|
|
271
|
+
// *
|
|
272
|
+
// * ```ts
|
|
273
|
+
// * const unsubscribe = evolu.subscribeSyncState(() => {
|
|
274
|
+
// * const syncState = evolu.getSyncState();
|
|
275
|
+
// * });
|
|
276
|
+
// * ```
|
|
277
|
+
// */
|
|
278
|
+
// readonly subscribeSyncState: StoreSubscribe;
|
|
279
|
+
|
|
280
|
+
// /**
|
|
281
|
+
// * Get {@link SyncState}.
|
|
282
|
+
// *
|
|
283
|
+
// * ### Example
|
|
284
|
+
// *
|
|
285
|
+
// * ```ts
|
|
286
|
+
// * const unsubscribe = evolu.subscribeSyncState(() => {
|
|
287
|
+
// * const syncState = evolu.getSyncState();
|
|
288
|
+
// * });
|
|
289
|
+
// * ```
|
|
290
|
+
// */
|
|
291
|
+
// readonly getSyncState: () => SyncState;
|
|
255
292
|
|
|
256
293
|
/**
|
|
257
294
|
* Inserts a row into the database and returns a {@link Result} with the new
|
|
@@ -437,17 +474,60 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
|
|
|
437
474
|
readonly reloadApp: () => void;
|
|
438
475
|
|
|
439
476
|
/** Export SQLite database file as Uint8Array. */
|
|
440
|
-
readonly exportDatabase: () => Promise<Uint8Array
|
|
477
|
+
readonly exportDatabase: () => Promise<Uint8Array<ArrayBuffer>>;
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Use an owner. Using an owner means syncing it and subscribing to
|
|
481
|
+
* broadcasted changes. Returns a function to stop using the owner.
|
|
482
|
+
*
|
|
483
|
+
* Transport connections are automatically deduplicated and reference-counted,
|
|
484
|
+
* so multiple owners using the same transport will share a single
|
|
485
|
+
* connection.
|
|
486
|
+
*
|
|
487
|
+
* ### Example
|
|
488
|
+
*
|
|
489
|
+
* ```ts
|
|
490
|
+
* // Use an owner (starts syncing and subscribing to changes).
|
|
491
|
+
* const unuse = evolu.useOwner(shardOwner);
|
|
492
|
+
*
|
|
493
|
+
* // Later, stop using the owner.
|
|
494
|
+
* unuse();
|
|
495
|
+
*
|
|
496
|
+
* // Bulk operations.
|
|
497
|
+
* const unuses = owners.map((owner) => evolu.useOwner(owner));
|
|
498
|
+
* // Later: unuses.forEach(unuse => unuse());
|
|
499
|
+
* ```
|
|
500
|
+
*
|
|
501
|
+
* @experimental
|
|
502
|
+
*/
|
|
503
|
+
readonly useOwner: (owner: SyncOwner) => () => void;
|
|
441
504
|
}
|
|
442
505
|
|
|
443
|
-
/**
|
|
506
|
+
/** Represents errors that can occur in Evolu. */
|
|
444
507
|
export type EvoluError =
|
|
445
|
-
| TimestampError
|
|
446
508
|
| ProtocolError
|
|
447
|
-
| TransferableError
|
|
448
|
-
| SymmetricCryptoDecryptError
|
|
449
509
|
| ProtocolUnsupportedVersionError
|
|
450
|
-
| SqliteError
|
|
510
|
+
| SqliteError
|
|
511
|
+
| SymmetricCryptoDecryptError
|
|
512
|
+
| TimestampError
|
|
513
|
+
| TransferableError;
|
|
514
|
+
|
|
515
|
+
// /**
|
|
516
|
+
// * Error reported when a message is invalid or rejected during processing.
|
|
517
|
+
// *
|
|
518
|
+
// * This error should never happen because a properly written app should ensure
|
|
519
|
+
// * data correctness, but it can occur for two reasons:
|
|
520
|
+
// *
|
|
521
|
+
// * 1. An attack from someone who modified app code
|
|
522
|
+
// * 2. A bug by the developer
|
|
523
|
+
// *
|
|
524
|
+
// * Both cases are useful to report for debugging and security monitoring.
|
|
525
|
+
// */
|
|
526
|
+
// export interface OnMessageError {
|
|
527
|
+
// readonly type: "OnMessageError";
|
|
528
|
+
// readonly invalidChanges: ReadonlyArray<DbChange>;
|
|
529
|
+
// readonly rejectedChanges: ReadonlyArray<DbChange>;
|
|
530
|
+
// }
|
|
451
531
|
|
|
452
532
|
interface InternalEvoluInstance<S extends EvoluSchema = EvoluSchema>
|
|
453
533
|
extends Evolu<S> {
|
|
@@ -458,44 +538,13 @@ interface InternalEvoluInstance<S extends EvoluSchema = EvoluSchema>
|
|
|
458
538
|
readonly ensureSchema: (schema: EvoluSchema) => void;
|
|
459
539
|
}
|
|
460
540
|
|
|
461
|
-
export type EvoluDeps =
|
|
462
|
-
|
|
463
|
-
|
|
541
|
+
export type EvoluDeps = ConsoleDep &
|
|
542
|
+
CreateDbWorkerDep &
|
|
543
|
+
LocalAuthDep &
|
|
464
544
|
Partial<FlushSyncDep> &
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
export interface EvoluConfigWithFunctions extends Config {
|
|
469
|
-
/**
|
|
470
|
-
* Callback invoked when the database is initialized. Use `isFirst` to perform
|
|
471
|
-
* one-time setup like initial data seeding.
|
|
472
|
-
*
|
|
473
|
-
* ### Example
|
|
474
|
-
*
|
|
475
|
-
* ```ts
|
|
476
|
-
* const evolu = createEvolu(evoluReactWebDeps)(Schema, {
|
|
477
|
-
* onInit: ({ appOwner, isFirst }) => {
|
|
478
|
-
* if (isFirst) {
|
|
479
|
-
* const todoCategoryId = getOrThrow(
|
|
480
|
-
* evolu.insert("todoCategory", {
|
|
481
|
-
* name: "Not Urgent",
|
|
482
|
-
* }),
|
|
483
|
-
* );
|
|
484
|
-
*
|
|
485
|
-
* evolu.insert("todo", {
|
|
486
|
-
* title: "Try React Suspense",
|
|
487
|
-
* categoryId: todoCategoryId.id,
|
|
488
|
-
* });
|
|
489
|
-
* }
|
|
490
|
-
* },
|
|
491
|
-
* });
|
|
492
|
-
* ```
|
|
493
|
-
*/
|
|
494
|
-
readonly onInit?: (params: {
|
|
495
|
-
readonly appOwner: AppOwner;
|
|
496
|
-
readonly isFirst: boolean;
|
|
497
|
-
}) => void;
|
|
498
|
-
}
|
|
545
|
+
RandomBytesDep &
|
|
546
|
+
ReloadAppDep &
|
|
547
|
+
TimeDep;
|
|
499
548
|
|
|
500
549
|
// For hot reloading and Evolu multitenancy.
|
|
501
550
|
const evoluInstances = new Map<string, InternalEvoluInstance>();
|
|
@@ -519,7 +568,8 @@ let tabId: Id | null = null;
|
|
|
519
568
|
* and merging.
|
|
520
569
|
* - Automatic schema evolution that updates the underlying database with new
|
|
521
570
|
* columns or tables.
|
|
522
|
-
* - Managing owner data with resetAppOwner and
|
|
571
|
+
* - Managing owner data with {@link Evolu#resetAppOwner} and
|
|
572
|
+
* {@link Evolu#restoreAppOwner}.
|
|
523
573
|
*
|
|
524
574
|
* ### Example
|
|
525
575
|
*
|
|
@@ -553,78 +603,116 @@ export const createEvolu =
|
|
|
553
603
|
(deps: EvoluDeps) =>
|
|
554
604
|
<S extends EvoluSchema>(
|
|
555
605
|
schema: ValidateSchema<S> extends never ? S : ValidateSchema<S>,
|
|
556
|
-
|
|
606
|
+
config?: EvoluConfig,
|
|
557
607
|
): Evolu<S> => {
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
let evolu = evoluInstances.get(config.name);
|
|
608
|
+
const name = config?.name ?? defaultDbConfig.name;
|
|
609
|
+
let evolu = evoluInstances.get(name);
|
|
561
610
|
|
|
562
611
|
if (evolu == null) {
|
|
563
|
-
evolu = createEvoluInstance(deps)(
|
|
564
|
-
|
|
565
|
-
config as IntentionalNever,
|
|
566
|
-
);
|
|
567
|
-
evoluInstances.set(config.name, evolu);
|
|
612
|
+
evolu = createEvoluInstance(deps)(schema as EvoluSchema, config);
|
|
613
|
+
evoluInstances.set(name, evolu);
|
|
568
614
|
} else {
|
|
569
615
|
// Hot reloading. Note that indexes are intentionally omitted.
|
|
570
616
|
evolu.ensureSchema(schema as EvoluSchema);
|
|
571
617
|
}
|
|
572
618
|
|
|
573
|
-
return evolu as
|
|
619
|
+
return evolu as Evolu<S>;
|
|
574
620
|
};
|
|
575
621
|
|
|
576
622
|
const createEvoluInstance =
|
|
577
623
|
(deps: EvoluDeps) =>
|
|
578
|
-
(
|
|
579
|
-
|
|
580
|
-
evoluConfig: EvoluConfigWithFunctions,
|
|
581
|
-
): InternalEvoluInstance => {
|
|
582
|
-
deps.console.enabled = evoluConfig.enableLogging ?? false;
|
|
624
|
+
(schema: EvoluSchema, config?: EvoluConfig): InternalEvoluInstance => {
|
|
625
|
+
deps.console.enabled = config?.enableLogging ?? false;
|
|
583
626
|
|
|
584
|
-
|
|
627
|
+
const { indexes, reloadUrl = "/", ...partialDbConfig } = config ?? {};
|
|
585
628
|
|
|
586
|
-
const {
|
|
629
|
+
const dbConfig: DbConfig = { ...defaultDbConfig, ...partialDbConfig };
|
|
630
|
+
|
|
631
|
+
deps.console.log("[evolu]", "createEvoluInstance", {
|
|
632
|
+
name: dbConfig.name,
|
|
633
|
+
});
|
|
587
634
|
|
|
588
635
|
const errorStore = createStore<EvoluError | null>(null);
|
|
589
636
|
const rowsStore = createStore<QueryRowsMap>(new Map());
|
|
590
|
-
|
|
591
|
-
const
|
|
637
|
+
|
|
638
|
+
const { promise: appOwner, resolve: resolveAppOwner } =
|
|
639
|
+
Promise.withResolvers<AppOwner>();
|
|
640
|
+
|
|
641
|
+
if (config?.externalAppOwner) {
|
|
642
|
+
resolveAppOwner(config.externalAppOwner);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// TODO: Update it for the owner-api
|
|
646
|
+
const _syncStore = createStore<SyncState>(initialSyncState);
|
|
592
647
|
|
|
593
648
|
const subscribedQueries = createSubscribedQueries(rowsStore);
|
|
594
649
|
const loadingPromises = createLoadingPromises(subscribedQueries);
|
|
595
|
-
const
|
|
650
|
+
const onCompleteRegistry = createCallbackRegistry(deps);
|
|
651
|
+
const exportRegistry =
|
|
652
|
+
createCallbackRegistry<Uint8Array<ArrayBuffer>>(deps);
|
|
596
653
|
|
|
597
|
-
const
|
|
598
|
-
const dbWorker = deps.createDbWorker(config.name);
|
|
654
|
+
const dbWorker = deps.createDbWorker(dbConfig.name);
|
|
599
655
|
|
|
600
656
|
const getTabId = () => {
|
|
601
657
|
tabId ??= createId(deps);
|
|
602
658
|
return tabId;
|
|
603
659
|
};
|
|
604
660
|
|
|
661
|
+
// const createLocalOnly = (
|
|
662
|
+
// localMutations: Array<MutationChange>,
|
|
663
|
+
// defaultOwnerId: OwnerId | undefined,
|
|
664
|
+
// ): LocalOnly<EvoluSchema> => ({
|
|
665
|
+
// insert: (table, values) => {
|
|
666
|
+
// const id = createId(deps);
|
|
667
|
+
// localMutations.push({
|
|
668
|
+
// table,
|
|
669
|
+
// id,
|
|
670
|
+
// values,
|
|
671
|
+
// ownerId: defaultOwnerId,
|
|
672
|
+
// });
|
|
673
|
+
// return id;
|
|
674
|
+
// },
|
|
675
|
+
// update: (table, values) => {
|
|
676
|
+
// const { id, ...rest } = values;
|
|
677
|
+
// localMutations.push({
|
|
678
|
+
// table,
|
|
679
|
+
// id: id as Id,
|
|
680
|
+
// values: rest,
|
|
681
|
+
// ownerId: defaultOwnerId,
|
|
682
|
+
// });
|
|
683
|
+
// },
|
|
684
|
+
// upsert: (table, values) => {
|
|
685
|
+
// const { id, ...rest } = values as Record<string, unknown> & { id: Id };
|
|
686
|
+
// localMutations.push({
|
|
687
|
+
// table,
|
|
688
|
+
// id: id,
|
|
689
|
+
// values: rest as MutationChange["values"],
|
|
690
|
+
// ownerId: defaultOwnerId,
|
|
691
|
+
// });
|
|
692
|
+
// },
|
|
693
|
+
// });
|
|
694
|
+
|
|
695
|
+
// Worker responses are delivered to all tabs. Each case must handle this
|
|
696
|
+
// properly (e.g., AppOwner promise resolves only once, tabId filtering).
|
|
605
697
|
dbWorker.onMessage((message) => {
|
|
606
698
|
switch (message.type) {
|
|
607
|
-
case "
|
|
608
|
-
|
|
609
|
-
onInit?.({
|
|
610
|
-
appOwner: message.appOwner,
|
|
611
|
-
isFirst: message.isFirst,
|
|
612
|
-
});
|
|
699
|
+
case "onError": {
|
|
700
|
+
errorStore.set(message.error);
|
|
613
701
|
break;
|
|
614
702
|
}
|
|
615
703
|
|
|
616
|
-
case "
|
|
617
|
-
|
|
704
|
+
case "onGetAppOwner": {
|
|
705
|
+
resolveAppOwner(message.appOwner);
|
|
618
706
|
break;
|
|
619
707
|
}
|
|
620
708
|
|
|
621
|
-
case "
|
|
709
|
+
case "onQueryPatches": {
|
|
622
710
|
if (message.tabId !== getTabId()) return;
|
|
623
711
|
|
|
624
712
|
const state = rowsStore.get();
|
|
625
713
|
const nextState = new Map([
|
|
626
714
|
...state,
|
|
627
|
-
...message.
|
|
715
|
+
...message.queryPatches.map(
|
|
628
716
|
({ query, patches }): [Query, ReadonlyArray<Row>] => [
|
|
629
717
|
query,
|
|
630
718
|
applyPatches(patches, state.get(query) ?? emptyRows),
|
|
@@ -632,7 +720,7 @@ const createEvoluInstance =
|
|
|
632
720
|
),
|
|
633
721
|
]);
|
|
634
722
|
|
|
635
|
-
for (const { query } of message.
|
|
723
|
+
for (const { query } of message.queryPatches) {
|
|
636
724
|
loadingPromises.resolve(query, nextState.get(query) ?? emptyRows);
|
|
637
725
|
}
|
|
638
726
|
|
|
@@ -645,33 +733,111 @@ const createEvoluInstance =
|
|
|
645
733
|
}
|
|
646
734
|
|
|
647
735
|
for (const id of message.onCompleteIds) {
|
|
648
|
-
|
|
736
|
+
onCompleteRegistry.execute(id);
|
|
649
737
|
}
|
|
650
738
|
break;
|
|
651
739
|
}
|
|
652
740
|
|
|
653
|
-
case "
|
|
741
|
+
case "refreshQueries": {
|
|
654
742
|
if (message.tabId && message.tabId === getTabId()) return;
|
|
655
743
|
|
|
656
|
-
loadingPromises.
|
|
657
|
-
|
|
744
|
+
const loadingPromisesQueries = loadingPromises.getQueries();
|
|
745
|
+
loadingPromises.releaseUnsubscribedOnMutation();
|
|
746
|
+
|
|
747
|
+
const queries = [
|
|
748
|
+
// Dedupe
|
|
749
|
+
...new Set([...loadingPromisesQueries, ...subscribedQueries.get()]),
|
|
750
|
+
];
|
|
751
|
+
|
|
658
752
|
if (isNonEmptyReadonlyArray(queries)) {
|
|
659
753
|
dbWorker.postMessage({ type: "query", tabId: getTabId(), queries });
|
|
660
754
|
}
|
|
755
|
+
|
|
661
756
|
break;
|
|
662
757
|
}
|
|
663
758
|
|
|
664
759
|
case "onReset": {
|
|
665
760
|
if (message.reload) {
|
|
666
|
-
|
|
761
|
+
deps.reloadApp(reloadUrl);
|
|
667
762
|
} else {
|
|
668
|
-
|
|
763
|
+
onCompleteRegistry.execute(message.onCompleteId);
|
|
669
764
|
}
|
|
670
765
|
break;
|
|
671
766
|
}
|
|
672
767
|
|
|
768
|
+
// case "processNewMessages": {
|
|
769
|
+
// void requestIdleTask(
|
|
770
|
+
// toTask(async () => {
|
|
771
|
+
// const approved: Array<Timestamp> = [];
|
|
772
|
+
// const invalidChanges: Array<DbChange> = [];
|
|
773
|
+
// const rejectedChanges: Array<DbChange> = [];
|
|
774
|
+
// const localMutations: Array<MutationChange> = [];
|
|
775
|
+
|
|
776
|
+
// for (const crdtMessage of message.messages) {
|
|
777
|
+
// let isApproved = true;
|
|
778
|
+
// let isValid = true;
|
|
779
|
+
|
|
780
|
+
// const table = crdtMessage.change.table;
|
|
781
|
+
// if (table in schema) {
|
|
782
|
+
// const { createdAt, ...values } = crdtMessage.change.values;
|
|
783
|
+
// isValid =
|
|
784
|
+
// (createdAt ? DateIso.is(createdAt) : true) &&
|
|
785
|
+
// getMutationType(table, "update").is({
|
|
786
|
+
// id: crdtMessage.change.id,
|
|
787
|
+
// ...values,
|
|
788
|
+
// });
|
|
789
|
+
// } else {
|
|
790
|
+
// isValid = false;
|
|
791
|
+
// }
|
|
792
|
+
|
|
793
|
+
// if (!isValid) {
|
|
794
|
+
// isApproved = false;
|
|
795
|
+
// invalidChanges.push(crdtMessage.change);
|
|
796
|
+
// } else if (onMessage) {
|
|
797
|
+
// // At this point, we've validated that the message conforms to the
|
|
798
|
+
// // schema, so the typed callback can safely process it.
|
|
799
|
+
// isApproved = await onMessage(crdtMessage.change, {
|
|
800
|
+
// ownerId: message.ownerId,
|
|
801
|
+
// localOnly: createLocalOnly(localMutations, message.ownerId),
|
|
802
|
+
// });
|
|
803
|
+
// if (!isApproved) {
|
|
804
|
+
// rejectedChanges.push(crdtMessage.change);
|
|
805
|
+
// }
|
|
806
|
+
// }
|
|
807
|
+
|
|
808
|
+
// if (isApproved) {
|
|
809
|
+
// approved.push(crdtMessage.timestamp);
|
|
810
|
+
// }
|
|
811
|
+
// }
|
|
812
|
+
|
|
813
|
+
// // Report OnMessageError if there were any invalid or rejected changes
|
|
814
|
+
// if (invalidChanges.length > 0 || rejectedChanges.length > 0) {
|
|
815
|
+
// const onMessageError: OnMessageError = {
|
|
816
|
+
// type: "OnMessageError",
|
|
817
|
+
// invalidChanges,
|
|
818
|
+
// rejectedChanges,
|
|
819
|
+
// };
|
|
820
|
+
// errorStore.set(onMessageError);
|
|
821
|
+
// }
|
|
822
|
+
|
|
823
|
+
// dbWorker.postMessage({
|
|
824
|
+
// type: "onProcessNewMessages",
|
|
825
|
+
// onCompleteId: message.onCompleteId,
|
|
826
|
+
// approved,
|
|
827
|
+
// localMutations,
|
|
828
|
+
// });
|
|
829
|
+
|
|
830
|
+
// return ok();
|
|
831
|
+
// }),
|
|
832
|
+
// )();
|
|
833
|
+
// break;
|
|
834
|
+
// }
|
|
835
|
+
|
|
673
836
|
case "onExport": {
|
|
674
|
-
|
|
837
|
+
exportRegistry.execute(
|
|
838
|
+
message.onCompleteId,
|
|
839
|
+
message.file as Uint8Array<ArrayBuffer>,
|
|
840
|
+
);
|
|
675
841
|
break;
|
|
676
842
|
}
|
|
677
843
|
|
|
@@ -704,18 +870,19 @@ const createEvoluInstance =
|
|
|
704
870
|
return type;
|
|
705
871
|
};
|
|
706
872
|
|
|
707
|
-
dbWorker.postMessage({
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
});
|
|
873
|
+
dbWorker.postMessage({ type: "init", config: dbConfig, dbSchema });
|
|
874
|
+
|
|
875
|
+
// We can't use `init` to get AppOwner because `init` runs only once per n tabs.
|
|
876
|
+
dbWorker.postMessage({ type: "getAppOwner" });
|
|
712
877
|
|
|
713
878
|
const loadQueryMicrotaskQueue: Array<Query> = [];
|
|
714
879
|
|
|
715
880
|
const mutateMicrotaskQueue: Array<
|
|
716
|
-
[
|
|
881
|
+
[MutationChange | null, MutationOptions["onComplete"] | undefined]
|
|
717
882
|
> = [];
|
|
718
883
|
|
|
884
|
+
const useOwnerMicrotaskQueue: Array<[SyncOwner, boolean, Uint8Array]> = [];
|
|
885
|
+
|
|
719
886
|
const createMutation =
|
|
720
887
|
<Kind extends MutationKind>(kind: Kind) =>
|
|
721
888
|
<TableName extends keyof typeof schema>(
|
|
@@ -731,8 +898,7 @@ const createEvoluInstance =
|
|
|
731
898
|
ObjectType<MutationMapping<(typeof schema)[TableName], Kind>>
|
|
732
899
|
>
|
|
733
900
|
> => {
|
|
734
|
-
const
|
|
735
|
-
const result = Type.fromUnknown(props);
|
|
901
|
+
const result = getMutationType(table, kind).fromUnknown(props);
|
|
736
902
|
|
|
737
903
|
const id =
|
|
738
904
|
kind === "insert"
|
|
@@ -741,13 +907,12 @@ const createEvoluInstance =
|
|
|
741
907
|
|
|
742
908
|
if (options?.onlyValidate !== true) {
|
|
743
909
|
if (!result.ok) {
|
|
744
|
-
//
|
|
745
|
-
|
|
746
|
-
mutateMicrotaskQueue.push([undefined, undefined]);
|
|
910
|
+
// Mark the transaction as invalid by pushing null
|
|
911
|
+
mutateMicrotaskQueue.push([null, undefined]);
|
|
747
912
|
} else {
|
|
748
913
|
const values = { ...result.value };
|
|
749
|
-
|
|
750
914
|
delete values.id;
|
|
915
|
+
|
|
751
916
|
if (kind === "insert" || kind === "upsert") {
|
|
752
917
|
// Only set createdAt if not provided by user
|
|
753
918
|
if (!("createdAt" in values)) {
|
|
@@ -755,50 +920,19 @@ const createEvoluInstance =
|
|
|
755
920
|
}
|
|
756
921
|
}
|
|
757
922
|
|
|
758
|
-
const dbChange = {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
assertValidDbChange(dbChange);
|
|
923
|
+
const dbChange = { table, id, values };
|
|
924
|
+
assert(
|
|
925
|
+
DbChange.is(dbChange),
|
|
926
|
+
`Failed to create DbChange for table "${dbChange.table}"`,
|
|
927
|
+
);
|
|
764
928
|
|
|
765
|
-
|
|
929
|
+
const mutationChange = { ...dbChange, ownerId: options?.ownerId };
|
|
930
|
+
mutateMicrotaskQueue.push([mutationChange, options?.onComplete]);
|
|
766
931
|
}
|
|
767
932
|
|
|
768
|
-
if (mutateMicrotaskQueue.length === 1)
|
|
769
|
-
queueMicrotask(
|
|
770
|
-
|
|
771
|
-
const onCompletes = [];
|
|
772
|
-
|
|
773
|
-
for (const [change, onComplete] of mutateMicrotaskQueue) {
|
|
774
|
-
if (change) changes.push(change);
|
|
775
|
-
if (onComplete) onCompletes.push(onComplete);
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
const mutateMicrotaskQueueLength = mutateMicrotaskQueue.length;
|
|
779
|
-
mutateMicrotaskQueue.length = 0;
|
|
780
|
-
|
|
781
|
-
// Don't mutate anything if there was a validation error.
|
|
782
|
-
// All mutations within a queue are considered to be a transaction.
|
|
783
|
-
if (changes.length !== mutateMicrotaskQueueLength) {
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
const onCompleteIds = onCompletes.map((onComplete) =>
|
|
788
|
-
callbacks.register(onComplete),
|
|
789
|
-
);
|
|
790
|
-
|
|
791
|
-
loadingPromises.releaseUnsubscribed();
|
|
792
|
-
|
|
793
|
-
if (isNonEmptyArray(changes))
|
|
794
|
-
dbWorker.postMessage({
|
|
795
|
-
type: "mutate",
|
|
796
|
-
tabId: getTabId(),
|
|
797
|
-
changes,
|
|
798
|
-
onCompleteIds,
|
|
799
|
-
subscribedQueries: subscribedQueries.get(),
|
|
800
|
-
});
|
|
801
|
-
});
|
|
933
|
+
if (mutateMicrotaskQueue.length === 1) {
|
|
934
|
+
queueMicrotask(processMutationQueue);
|
|
935
|
+
}
|
|
802
936
|
}
|
|
803
937
|
|
|
804
938
|
if (result.ok) return ok({ id });
|
|
@@ -812,28 +946,74 @@ const createEvoluInstance =
|
|
|
812
946
|
);
|
|
813
947
|
};
|
|
814
948
|
|
|
949
|
+
const processMutationQueue = () => {
|
|
950
|
+
const changes: Array<MutationChange> = [];
|
|
951
|
+
const onCompleteCallbacks = [];
|
|
952
|
+
|
|
953
|
+
for (const [change, onComplete] of mutateMicrotaskQueue) {
|
|
954
|
+
if (change !== null) changes.push(change);
|
|
955
|
+
if (onComplete) onCompleteCallbacks.push(onComplete);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const queueLength = mutateMicrotaskQueue.length;
|
|
959
|
+
mutateMicrotaskQueue.length = 0;
|
|
960
|
+
|
|
961
|
+
// Don't process any mutations if there was a validation error.
|
|
962
|
+
// All mutations within a queue run as a single transaction.
|
|
963
|
+
if (changes.length !== queueLength) {
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const onCompleteIds = onCompleteCallbacks.map(
|
|
968
|
+
onCompleteRegistry.register,
|
|
969
|
+
);
|
|
970
|
+
|
|
971
|
+
loadingPromises.releaseUnsubscribedOnMutation();
|
|
972
|
+
|
|
973
|
+
if (!isNonEmptyArray(changes)) return;
|
|
974
|
+
|
|
975
|
+
// if (onMessage) {
|
|
976
|
+
// const rejectedChanges: Array<DbChange> = [];
|
|
977
|
+
// const localMutations: Array<MutationChange> = [];
|
|
978
|
+
|
|
979
|
+
// for (const change of changes) {
|
|
980
|
+
// const localOnly = createLocalOnly(localMutations, change.ownerId);
|
|
981
|
+
|
|
982
|
+
// const isApproved = await onMessage(change, {
|
|
983
|
+
// ownerId: change.ownerId,
|
|
984
|
+
// localOnly,
|
|
985
|
+
// });
|
|
986
|
+
// if (!isApproved) {
|
|
987
|
+
// rejectedChanges.push(change);
|
|
988
|
+
// }
|
|
989
|
+
// }
|
|
990
|
+
|
|
991
|
+
// if (rejectedChanges.length > 0) {
|
|
992
|
+
// errorStore.set({
|
|
993
|
+
// type: "OnMessageError",
|
|
994
|
+
// invalidChanges: [],
|
|
995
|
+
// rejectedChanges,
|
|
996
|
+
// });
|
|
997
|
+
// return;
|
|
998
|
+
// }
|
|
999
|
+
|
|
1000
|
+
// changes.push(...localMutations);
|
|
1001
|
+
// }
|
|
1002
|
+
|
|
1003
|
+
dbWorker.postMessage({
|
|
1004
|
+
type: "mutate",
|
|
1005
|
+
tabId: getTabId(),
|
|
1006
|
+
changes,
|
|
1007
|
+
onCompleteIds,
|
|
1008
|
+
subscribedQueries: subscribedQueries.get(),
|
|
1009
|
+
});
|
|
1010
|
+
};
|
|
1011
|
+
|
|
815
1012
|
const evolu: InternalEvoluInstance = {
|
|
816
1013
|
subscribeError: errorStore.subscribe,
|
|
817
1014
|
getError: errorStore.get,
|
|
818
1015
|
|
|
819
|
-
createQuery
|
|
820
|
-
const compiledQuery = queryCallback(
|
|
821
|
-
kysely as IntentionalNever,
|
|
822
|
-
).compile();
|
|
823
|
-
|
|
824
|
-
if (isSqlMutation(compiledQuery.sql))
|
|
825
|
-
throw new Error(
|
|
826
|
-
"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.",
|
|
827
|
-
);
|
|
828
|
-
|
|
829
|
-
return serializeQuery({
|
|
830
|
-
sql: compiledQuery.sql as SafeSql,
|
|
831
|
-
parameters: compiledQuery.parameters as NonNullable<
|
|
832
|
-
SqliteQuery["parameters"]
|
|
833
|
-
>,
|
|
834
|
-
...(options && { options }),
|
|
835
|
-
});
|
|
836
|
-
},
|
|
1016
|
+
createQuery,
|
|
837
1017
|
|
|
838
1018
|
loadQuery: <R extends Row>(query: Query<R>): Promise<QueryRows<R>> => {
|
|
839
1019
|
const { promise, isNew } = loadingPromises.get(query);
|
|
@@ -846,6 +1026,7 @@ const createEvoluInstance =
|
|
|
846
1026
|
const queries = [...new Set(loadQueryMicrotaskQueue)];
|
|
847
1027
|
loadQueryMicrotaskQueue.length = 0;
|
|
848
1028
|
assertNonEmptyArray(queries);
|
|
1029
|
+
deps.console.log("[evolu]", "loadQuery", { queries });
|
|
849
1030
|
dbWorker.postMessage({
|
|
850
1031
|
type: "query",
|
|
851
1032
|
tabId: getTabId(),
|
|
@@ -881,23 +1062,19 @@ const createEvoluInstance =
|
|
|
881
1062
|
getQueryRows: <R extends Row>(query: Query<R>): QueryRows<R> =>
|
|
882
1063
|
(rowsStore.get().get(query) ?? emptyRows) as QueryRows<R>,
|
|
883
1064
|
|
|
884
|
-
|
|
885
|
-
getAppOwner: appOwnerStore.get,
|
|
1065
|
+
appOwner,
|
|
886
1066
|
|
|
887
|
-
|
|
888
|
-
|
|
1067
|
+
// TODO: Update it for the owner-api
|
|
1068
|
+
// subscribeSyncState: syncStore.subscribe,
|
|
1069
|
+
// getSyncState: syncStore.get,
|
|
889
1070
|
|
|
890
1071
|
insert: createMutation("insert"),
|
|
891
1072
|
update: createMutation("update"),
|
|
892
1073
|
upsert: createMutation("upsert"),
|
|
893
1074
|
|
|
894
1075
|
resetAppOwner: (options) => {
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
const { promise, resolve } = Promise.withResolvers<void>();
|
|
898
|
-
const onCompleteId = callbacks.register(() => {
|
|
899
|
-
resolve();
|
|
900
|
-
});
|
|
1076
|
+
const { promise, resolve } = Promise.withResolvers<undefined>();
|
|
1077
|
+
const onCompleteId = onCompleteRegistry.register(resolve);
|
|
901
1078
|
dbWorker.postMessage({
|
|
902
1079
|
type: "reset",
|
|
903
1080
|
onCompleteId,
|
|
@@ -907,13 +1084,8 @@ const createEvoluInstance =
|
|
|
907
1084
|
},
|
|
908
1085
|
|
|
909
1086
|
restoreAppOwner: (mnemonic, options) => {
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
const { promise, resolve } = Promise.withResolvers<void>();
|
|
913
|
-
const onCompleteId = callbacks.register(() => {
|
|
914
|
-
resolve();
|
|
915
|
-
});
|
|
916
|
-
|
|
1087
|
+
const { promise, resolve } = Promise.withResolvers<undefined>();
|
|
1088
|
+
const onCompleteId = onCompleteRegistry.register(resolve);
|
|
917
1089
|
dbWorker.postMessage({
|
|
918
1090
|
type: "reset",
|
|
919
1091
|
onCompleteId,
|
|
@@ -924,7 +1096,7 @@ const createEvoluInstance =
|
|
|
924
1096
|
},
|
|
925
1097
|
|
|
926
1098
|
reloadApp: () => {
|
|
927
|
-
|
|
1099
|
+
deps.reloadApp(reloadUrl);
|
|
928
1100
|
},
|
|
929
1101
|
|
|
930
1102
|
ensureSchema: (schema) => {
|
|
@@ -934,22 +1106,90 @@ const createEvoluInstance =
|
|
|
934
1106
|
},
|
|
935
1107
|
|
|
936
1108
|
exportDatabase: () => {
|
|
937
|
-
const { promise, resolve } =
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
});
|
|
1109
|
+
const { promise, resolve } =
|
|
1110
|
+
Promise.withResolvers<Uint8Array<ArrayBuffer>>();
|
|
1111
|
+
const onCompleteId = exportRegistry.register(resolve);
|
|
941
1112
|
dbWorker.postMessage({ type: "export", onCompleteId });
|
|
942
1113
|
return promise;
|
|
943
1114
|
},
|
|
1115
|
+
|
|
1116
|
+
useOwner: (owner) => {
|
|
1117
|
+
const scheduleOwnerQueueProcessing = () => {
|
|
1118
|
+
if (useOwnerMicrotaskQueue.length !== 1) return;
|
|
1119
|
+
queueMicrotask(() => {
|
|
1120
|
+
const queue = [...useOwnerMicrotaskQueue];
|
|
1121
|
+
useOwnerMicrotaskQueue.length = 0;
|
|
1122
|
+
|
|
1123
|
+
const result: Array<[SyncOwner, boolean, Uint8Array]> = [];
|
|
1124
|
+
const skipIndices = new Set<number>();
|
|
1125
|
+
|
|
1126
|
+
for (let i = 0; i < queue.length; i++) {
|
|
1127
|
+
if (skipIndices.has(i)) continue;
|
|
1128
|
+
|
|
1129
|
+
const [currentOwner, currentUse, currentOwnerSerialized] =
|
|
1130
|
+
queue[i];
|
|
1131
|
+
|
|
1132
|
+
// Look for opposite action with same owner
|
|
1133
|
+
for (let j = i + 1; j < queue.length; j++) {
|
|
1134
|
+
if (skipIndices.has(j)) continue;
|
|
1135
|
+
|
|
1136
|
+
const [, otherUse, otherOwnerSerialized] = queue[j];
|
|
1137
|
+
|
|
1138
|
+
if (
|
|
1139
|
+
currentUse !== otherUse &&
|
|
1140
|
+
eqArrayNumber(currentOwnerSerialized, otherOwnerSerialized)
|
|
1141
|
+
) {
|
|
1142
|
+
// Found cancel-out pair, skip both
|
|
1143
|
+
skipIndices.add(i).add(j);
|
|
1144
|
+
break;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
if (!skipIndices.has(i)) {
|
|
1149
|
+
result.push([currentOwner, currentUse, currentOwnerSerialized]);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
for (const [owner, use] of result) {
|
|
1154
|
+
dbWorker.postMessage({ type: "useOwner", owner, use });
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
useOwnerMicrotaskQueue.push([owner, true, pack(owner)]);
|
|
1160
|
+
scheduleOwnerQueueProcessing();
|
|
1161
|
+
|
|
1162
|
+
const unuse = () => {
|
|
1163
|
+
useOwnerMicrotaskQueue.push([owner, false, pack(owner)]);
|
|
1164
|
+
scheduleOwnerQueueProcessing();
|
|
1165
|
+
};
|
|
1166
|
+
|
|
1167
|
+
return unuse;
|
|
1168
|
+
},
|
|
944
1169
|
};
|
|
945
1170
|
|
|
946
1171
|
return evolu;
|
|
947
1172
|
};
|
|
948
1173
|
|
|
949
|
-
export const
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1174
|
+
export const createQuery = <R extends Row>(
|
|
1175
|
+
queryCallback: Parameters<CreateQuery<EvoluSchema>>[0],
|
|
1176
|
+
options?: Parameters<CreateQuery<EvoluSchema>>[1],
|
|
1177
|
+
): Query<R> => {
|
|
1178
|
+
const compiledQuery = queryCallback(kysely as IntentionalNever).compile();
|
|
1179
|
+
|
|
1180
|
+
if (isSqlMutation(compiledQuery.sql))
|
|
1181
|
+
throw new Error(
|
|
1182
|
+
"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.",
|
|
1183
|
+
);
|
|
1184
|
+
|
|
1185
|
+
return serializeQuery({
|
|
1186
|
+
sql: compiledQuery.sql as SafeSql,
|
|
1187
|
+
parameters: compiledQuery.parameters as NonNullable<
|
|
1188
|
+
SqliteQuery["parameters"]
|
|
1189
|
+
>,
|
|
1190
|
+
...(options && { options }),
|
|
1191
|
+
});
|
|
1192
|
+
};
|
|
953
1193
|
|
|
954
1194
|
interface LoadingPromises {
|
|
955
1195
|
get: <R extends Row>(
|
|
@@ -959,13 +1199,32 @@ interface LoadingPromises {
|
|
|
959
1199
|
readonly isNew: boolean;
|
|
960
1200
|
};
|
|
961
1201
|
|
|
1202
|
+
/**
|
|
1203
|
+
* Resolve a cached promise with updated rows.
|
|
1204
|
+
*
|
|
1205
|
+
* If the promise is not yet fulfilled, it will be resolved normally. If
|
|
1206
|
+
* already fulfilled (subscribed query updated after mutation), the promise
|
|
1207
|
+
* property is replaced with a new `Promise.resolve(rows)` while keeping the
|
|
1208
|
+
* same cached object reference. The promise is not removed from the cache
|
|
1209
|
+
* because React Suspense requires repeated calls to return the same promise.
|
|
1210
|
+
*/
|
|
962
1211
|
resolve: (query: Query, rows: ReadonlyArray<Row>) => void;
|
|
963
1212
|
|
|
964
|
-
|
|
1213
|
+
/**
|
|
1214
|
+
* Release unsubscribed queries from the cache.
|
|
1215
|
+
*
|
|
1216
|
+
* Loading promises can't be released in `resolve` because they must be cached
|
|
1217
|
+
* for React Suspense, but they also can't be cached forever because only
|
|
1218
|
+
* subscribed queries are automatically updated (reactivity is expensive
|
|
1219
|
+
* because it's implemented via refetching subscribed queries).
|
|
1220
|
+
*/
|
|
1221
|
+
releaseUnsubscribedOnMutation: () => void;
|
|
1222
|
+
|
|
1223
|
+
getQueries: () => ReadonlyArray<Query>;
|
|
965
1224
|
}
|
|
966
1225
|
|
|
967
1226
|
interface LoadingPromise {
|
|
968
|
-
/** Promise with props for the
|
|
1227
|
+
/** Promise with props for the React use hook. */
|
|
969
1228
|
promise: Promise<QueryRows> & {
|
|
970
1229
|
status?: "pending" | "fulfilled" | "rejected";
|
|
971
1230
|
value?: QueryRows;
|
|
@@ -980,7 +1239,7 @@ const createLoadingPromises = (
|
|
|
980
1239
|
): LoadingPromises => {
|
|
981
1240
|
const loadingPromiseMap = new Map<Query, LoadingPromise>();
|
|
982
1241
|
|
|
983
|
-
|
|
1242
|
+
return {
|
|
984
1243
|
get: <R extends Row>(
|
|
985
1244
|
query: Query<R>,
|
|
986
1245
|
): {
|
|
@@ -1007,16 +1266,13 @@ const createLoadingPromises = (
|
|
|
1007
1266
|
if (loadingPromise.promise.status !== "fulfilled") {
|
|
1008
1267
|
loadingPromise.resolve(rows);
|
|
1009
1268
|
} else {
|
|
1010
|
-
// A promise can't be fulfilled 2x, so we need a new one.
|
|
1011
1269
|
loadingPromise.promise = Promise.resolve(rows);
|
|
1012
1270
|
}
|
|
1013
1271
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
* https://github.com/acdlite/rfcs/blob/first-class-promises/text/0000-first-class-support-for-promises.md
|
|
1019
|
-
*/
|
|
1272
|
+
// Set status and value fields for React's `use` Hook to unwrap synchronously.
|
|
1273
|
+
// While undocumented in React docs, React still uses these properties internally,
|
|
1274
|
+
// and Evolu's own promise caching logic depends on checking `promise.status`.
|
|
1275
|
+
// https://github.com/acdlite/rfcs/blob/first-class-promises/text/0000-first-class-support-for-promises.md
|
|
1020
1276
|
void Object.assign(loadingPromise.promise, {
|
|
1021
1277
|
status: "fulfilled",
|
|
1022
1278
|
value: rows,
|
|
@@ -1027,14 +1283,7 @@ const createLoadingPromises = (
|
|
|
1027
1283
|
}
|
|
1028
1284
|
},
|
|
1029
1285
|
|
|
1030
|
-
|
|
1031
|
-
* We can't delete loading promises in `resolveLoadingPromises` because they
|
|
1032
|
-
* must be cached, so repeated calls to `loadQuery` will always return the
|
|
1033
|
-
* same promise until the data changes, and we also can't cache them forever
|
|
1034
|
-
* because only subscribed queries are automatically updated (reactivity is
|
|
1035
|
-
* expensive) hence this function must be called manually on any mutation.
|
|
1036
|
-
*/
|
|
1037
|
-
releaseUnsubscribed: () => {
|
|
1286
|
+
releaseUnsubscribedOnMutation: () => {
|
|
1038
1287
|
[...loadingPromiseMap.entries()]
|
|
1039
1288
|
.filter(([query]) => !subscribedQueries.has(query))
|
|
1040
1289
|
.forEach(([query, loadingPromise]) => {
|
|
@@ -1045,18 +1294,7 @@ const createLoadingPromises = (
|
|
|
1045
1294
|
}
|
|
1046
1295
|
});
|
|
1047
1296
|
},
|
|
1048
|
-
};
|
|
1049
1297
|
|
|
1050
|
-
|
|
1051
|
-
};
|
|
1052
|
-
|
|
1053
|
-
const assertValidDbChange: (dbChange: {
|
|
1054
|
-
table: string;
|
|
1055
|
-
id: Id;
|
|
1056
|
-
values: unknown;
|
|
1057
|
-
}) => asserts dbChange is DbChange = (dbChange) => {
|
|
1058
|
-
assert(
|
|
1059
|
-
DbChange.is(dbChange),
|
|
1060
|
-
`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.`,
|
|
1061
|
-
);
|
|
1298
|
+
getQueries: () => Array.from(loadingPromiseMap.keys()),
|
|
1299
|
+
};
|
|
1062
1300
|
};
|