@evolu/common 6.0.1-preview.2 → 6.0.1-preview.21
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 +6 -3
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +7 -4
- 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 +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 +61 -34
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +29 -42
- package/dist/src/Evolu/Db.d.ts +159 -66
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +300 -705
- 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 +200 -115
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +287 -157
- 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 +209 -124
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +118 -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 +256 -211
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +558 -359
- package/dist/src/Evolu/Public.d.ts +6 -8
- package/dist/src/Evolu/Public.d.ts.map +1 -1
- package/dist/src/Evolu/Public.js +2 -3
- package/dist/src/Evolu/PublicKysely.js +3 -3
- package/dist/src/Evolu/Query.d.ts +2 -1
- package/dist/src/Evolu/Query.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.d.ts +72 -4
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +124 -13
- package/dist/src/Evolu/Schema.d.ts +129 -73
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +169 -89
- package/dist/src/Evolu/Storage.d.ts +159 -14
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +42 -32
- 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 +414 -20
- package/dist/src/Evolu/Timestamp.d.ts +35 -27
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +27 -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/Number.d.ts +2 -1
- package/dist/src/Number.d.ts.map +1 -1
- 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 +176 -51
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +30 -241
- package/dist/src/Sqlite.d.ts +21 -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 +474 -327
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +502 -436
- package/dist/src/Types.d.ts +0 -74
- 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 -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 +3 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -3
- package/package.json +14 -13
- package/src/Assert.ts +8 -7
- package/src/Brand.ts +75 -0
- package/src/Buffer.ts +1 -1
- package/src/CallbackRegistry.ts +84 -0
- package/src/Console.ts +91 -11
- package/src/Crypto.ts +94 -79
- package/src/Evolu/Db.ts +528 -1027
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +682 -329
- package/src/Evolu/Internal.ts +0 -2
- package/src/Evolu/LocalAuth.ts +422 -0
- package/src/Evolu/Owner.ts +282 -227
- package/src/Evolu/Platform.ts +9 -9
- package/src/Evolu/Protocol.ts +788 -640
- package/src/Evolu/Public.ts +7 -14
- package/src/Evolu/PublicKysely.ts +3 -3
- package/src/Evolu/Query.ts +2 -1
- package/src/Evolu/Relay.ts +238 -18
- package/src/Evolu/Schema.ts +391 -191
- package/src/Evolu/Storage.ts +275 -55
- package/src/Evolu/Sync.ts +708 -37
- package/src/Evolu/Timestamp.ts +38 -35
- package/src/ManyToManyMap.ts +127 -24
- package/src/Number.ts +2 -6
- package/src/Random.ts +3 -2
- package/src/RefCountedResourceManager.ts +368 -0
- package/src/Result.ts +183 -53
- package/src/Sqlite.ts +59 -23
- package/src/Task.ts +779 -0
- package/src/Time.ts +168 -4
- package/src/Type.ts +708 -660
- package/src/Types.ts +0 -76
- package/src/WebSocket.ts +23 -17
- package/src/Worker.ts +72 -23
- package/src/index.ts +3 -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 -69
- package/dist/src/Evolu/Config.d.ts.map +0 -1
- package/dist/src/Evolu/Config.js +0 -9
- package/dist/src/Evolu/Kysely.d.ts +0 -6
- package/dist/src/Evolu/Kysely.d.ts.map +0 -1
- package/dist/src/Evolu/Kysely.js +0 -21
- package/dist/src/NanoId.d.ts +0 -27
- package/dist/src/NanoId.d.ts.map +0 -1
- package/dist/src/NanoId.js +0 -6
- package/dist/src/Promise.d.ts +0 -180
- package/dist/src/Promise.d.ts.map +0 -1
- package/dist/src/Promise.js +0 -176
- package/src/Callbacks.ts +0 -43
- package/src/Evolu/Config.ts +0 -83
- package/src/Evolu/Kysely.ts +0 -38
- package/src/NanoId.ts +0 -39
- package/src/Promise.ts +0 -295
package/src/Evolu/Evolu.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import { pack } from "msgpackr";
|
|
1
2
|
import { isNonEmptyArray, isNonEmptyReadonlyArray } from "../Array.js";
|
|
2
|
-
import { assertNonEmptyArray } from "../Assert.js";
|
|
3
|
-
import {
|
|
3
|
+
import { assert, assertNonEmptyArray } from "../Assert.js";
|
|
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,
|
|
@@ -44,23 +42,99 @@ import {
|
|
|
44
42
|
SubscribedQueries,
|
|
45
43
|
} from "./Query.js";
|
|
46
44
|
import {
|
|
47
|
-
assertValidEvoluSchema,
|
|
48
45
|
CreateQuery,
|
|
46
|
+
DefaultColumns,
|
|
49
47
|
EvoluSchema,
|
|
48
|
+
evoluSchemaToDbSchema,
|
|
49
|
+
IndexesConfig,
|
|
50
50
|
insertable,
|
|
51
|
+
kysely,
|
|
51
52
|
Mutation,
|
|
53
|
+
MutationChange,
|
|
52
54
|
MutationKind,
|
|
53
55
|
MutationMapping,
|
|
54
56
|
MutationOptions,
|
|
55
57
|
updateable,
|
|
56
58
|
upsertable,
|
|
57
|
-
|
|
58
|
-
ValidMutationSize,
|
|
59
|
-
ValidMutationSizeError,
|
|
59
|
+
ValidateSchema,
|
|
60
60
|
} from "./Schema.js";
|
|
61
|
-
import {
|
|
61
|
+
import { DbChange } from "./Storage.js";
|
|
62
|
+
import { initialSyncState, SyncOwner, SyncState } from "./Sync.js";
|
|
62
63
|
import { TimestampError } from "./Timestamp.js";
|
|
63
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
|
+
|
|
64
138
|
export interface Evolu<S extends EvoluSchema = EvoluSchema> {
|
|
65
139
|
/**
|
|
66
140
|
* Subscribe to {@link EvoluError} changes.
|
|
@@ -88,7 +162,8 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
|
|
|
88
162
|
* All this function does is compile the Kysely query and serialize it into a
|
|
89
163
|
* unique string. Both operations are fast and cheap.
|
|
90
164
|
*
|
|
91
|
-
* For mutations, use {@link Evolu
|
|
165
|
+
* For mutations, use {@link Evolu#insert}, {@link Evolu#update}, or
|
|
166
|
+
* {@link Evolu#upsert}.
|
|
92
167
|
*
|
|
93
168
|
* ### Example
|
|
94
169
|
*
|
|
@@ -108,43 +183,20 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
|
|
|
108
183
|
/**
|
|
109
184
|
* Load {@link Query} and return a promise with {@link QueryRows}.
|
|
110
185
|
*
|
|
111
|
-
*
|
|
112
|
-
* why loading should fail. All data are local, and the query is typed.
|
|
113
|
-
*
|
|
114
|
-
* {@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}.
|
|
115
189
|
*
|
|
116
190
|
* 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
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* With promises passed to children, we can load a query as soon as possible,
|
|
127
|
-
* but we don't have to use the returned promise immediately. That's useful
|
|
128
|
-
* for prefetching, which is generally not necessary for local-first apps but
|
|
129
|
-
* can be if a query takes a long time to load.
|
|
130
|
-
*
|
|
131
|
-
* Caching over mutation is a pattern that every developer should know. As we
|
|
132
|
-
* said, Evolu caches promise until a mutation happens. A query loaded after
|
|
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}.
|
|
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.
|
|
197
|
+
*
|
|
198
|
+
* To subscribe a query for automatic updates, use
|
|
199
|
+
* {@link Evolu#subscribeQuery}.
|
|
148
200
|
*
|
|
149
201
|
* ### Example
|
|
150
202
|
*
|
|
@@ -152,7 +204,7 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
|
|
|
152
204
|
* const allTodos = evolu.createQuery((db) =>
|
|
153
205
|
* db.selectFrom("todo").selectAll(),
|
|
154
206
|
* );
|
|
155
|
-
* evolu.loadQuery(allTodos).then((
|
|
207
|
+
* evolu.loadQuery(allTodos).then((rows) => {
|
|
156
208
|
* console.log(rows);
|
|
157
209
|
* });
|
|
158
210
|
* ```
|
|
@@ -201,86 +253,192 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
|
|
|
201
253
|
readonly getQueryRows: <R extends Row>(query: Query<R>) => QueryRows<R>;
|
|
202
254
|
|
|
203
255
|
/**
|
|
204
|
-
*
|
|
256
|
+
* Promise that resolves to {@link AppOwner} when available.
|
|
205
257
|
*
|
|
206
258
|
* ### Example
|
|
207
259
|
*
|
|
208
260
|
* ```ts
|
|
209
|
-
* const
|
|
210
|
-
* const owner = evolu.getAppOwner();
|
|
211
|
-
* });
|
|
261
|
+
* const owner = await evolu.appOwner;
|
|
212
262
|
* ```
|
|
213
263
|
*/
|
|
214
|
-
readonly
|
|
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;
|
|
215
292
|
|
|
216
293
|
/**
|
|
217
|
-
*
|
|
294
|
+
* Inserts a row into the database and returns a {@link Result} with the new
|
|
295
|
+
* {@link Id}.
|
|
218
296
|
*
|
|
219
|
-
*
|
|
297
|
+
* The first argument is the table name, and the second is an object
|
|
298
|
+
* containing the row data. An optional third argument provides mutation
|
|
299
|
+
* options including an `onComplete` callback and `onlyValidate` flag.
|
|
220
300
|
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
* });
|
|
225
|
-
* ```
|
|
226
|
-
*/
|
|
227
|
-
readonly getAppOwner: () => AppOwner | null;
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Subscribe to {@link SyncState} changes.
|
|
301
|
+
* Returns a Result type - use `.ok` to check if the insertion succeeded, and
|
|
302
|
+
* `.value.id` to access the generated ID on success, or `.error` to handle
|
|
303
|
+
* validation errors.
|
|
231
304
|
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
* const unsubscribe = evolu.subscribeSyncState(() => {
|
|
236
|
-
* const syncState = evolu.getSyncState();
|
|
237
|
-
* });
|
|
238
|
-
* ```
|
|
239
|
-
*/
|
|
240
|
-
readonly subscribeSyncState: StoreSubscribe;
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Get {@link SyncState}.
|
|
305
|
+
* Evolu does not use SQL for mutations to ensure data can be safely and
|
|
306
|
+
* predictably merged without conflicts. Explicit mutations also allow Evolu
|
|
307
|
+
* to automatically add and update {@link DefaultColumns}.
|
|
244
308
|
*
|
|
245
309
|
* ### Example
|
|
246
310
|
*
|
|
247
311
|
* ```ts
|
|
248
|
-
* const
|
|
249
|
-
*
|
|
312
|
+
* const result = evolu.insert("todo", {
|
|
313
|
+
* title: "Learn Evolu",
|
|
314
|
+
* isCompleted: false,
|
|
250
315
|
* });
|
|
251
|
-
* ```
|
|
252
|
-
*/
|
|
253
|
-
readonly getSyncState: () => SyncState;
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Inserts a row.
|
|
257
316
|
*
|
|
258
|
-
*
|
|
317
|
+
* if (result.ok) {
|
|
318
|
+
* console.log("Todo created with ID:", result.value.id);
|
|
319
|
+
* } else {
|
|
320
|
+
* console.error("Validation error:", result.error);
|
|
321
|
+
* }
|
|
259
322
|
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
323
|
+
* // With onComplete callback
|
|
324
|
+
* evolu.insert(
|
|
325
|
+
* "todo",
|
|
326
|
+
* { title: "Another todo" },
|
|
327
|
+
* {
|
|
328
|
+
* onComplete: () => {
|
|
329
|
+
* console.log("Insert completed");
|
|
330
|
+
* },
|
|
331
|
+
* },
|
|
332
|
+
* );
|
|
262
333
|
* ```
|
|
263
334
|
*/
|
|
264
335
|
insert: Mutation<S, "insert">;
|
|
265
336
|
|
|
266
337
|
/**
|
|
267
|
-
* Updates a row
|
|
338
|
+
* Updates a row in the database and returns a {@link Result} with the existing
|
|
339
|
+
* {@link Id}.
|
|
340
|
+
*
|
|
341
|
+
* The first argument is the table name, and the second is an object
|
|
342
|
+
* containing the row data including the required `id` field. An optional
|
|
343
|
+
* third argument provides mutation options including an `onComplete` callback
|
|
344
|
+
* and `onlyValidate` flag.
|
|
345
|
+
*
|
|
346
|
+
* Returns a Result type - use `.ok` to check if the update succeeded, and
|
|
347
|
+
* `.value.id` to access the ID on success, or `.error` to handle validation
|
|
348
|
+
* errors.
|
|
349
|
+
*
|
|
350
|
+
* Evolu does not use SQL for mutations to ensure data can be safely and
|
|
351
|
+
* predictably merged without conflicts. Explicit mutations also allow Evolu
|
|
352
|
+
* to automatically add and update {@link DefaultColumns}.
|
|
268
353
|
*
|
|
269
354
|
* ### Example
|
|
270
355
|
*
|
|
271
356
|
* ```ts
|
|
272
|
-
*
|
|
357
|
+
* const result = evolu.update("todo", {
|
|
358
|
+
* id: todoId,
|
|
359
|
+
* title: "Updated title",
|
|
360
|
+
* isCompleted: true,
|
|
361
|
+
* });
|
|
362
|
+
*
|
|
363
|
+
* if (result.ok) {
|
|
364
|
+
* console.log("Todo updated with ID:", result.value.id);
|
|
365
|
+
* } else {
|
|
366
|
+
* console.error("Validation error:", result.error);
|
|
367
|
+
* }
|
|
368
|
+
*
|
|
369
|
+
* // To delete a row, set isDeleted to true
|
|
370
|
+
* evolu.update("todo", { id: todoId, isDeleted: true });
|
|
371
|
+
*
|
|
372
|
+
* // With onComplete callback
|
|
373
|
+
* evolu.update(
|
|
374
|
+
* "todo",
|
|
375
|
+
* { id: todoId, title: "New title" },
|
|
376
|
+
* {
|
|
377
|
+
* onComplete: () => {
|
|
378
|
+
* console.log("Update completed");
|
|
379
|
+
* },
|
|
380
|
+
* },
|
|
381
|
+
* );
|
|
273
382
|
* ```
|
|
274
383
|
*/
|
|
275
384
|
update: Mutation<S, "update">;
|
|
276
385
|
|
|
277
386
|
/**
|
|
278
|
-
* Upserts a row
|
|
387
|
+
* Upserts a row in the database and returns a {@link Result} with the existing
|
|
388
|
+
* {@link Id}.
|
|
389
|
+
*
|
|
390
|
+
* The first argument is the table name, and the second is an object
|
|
391
|
+
* containing the row data including the required `id` field. An optional
|
|
392
|
+
* third argument provides mutation options including an `onComplete` callback
|
|
393
|
+
* and `onlyValidate` flag.
|
|
394
|
+
*
|
|
395
|
+
* This function allows you to use custom IDs and optionally set `createdAt`,
|
|
396
|
+
* which is useful for external systems, data migrations, or when the same row
|
|
397
|
+
* may already be created on a different device.
|
|
398
|
+
*
|
|
399
|
+
* Returns a Result type - use `.ok` to check if the upsert succeeded, and
|
|
400
|
+
* `.value.id` to access the ID on success, or `.error` to handle validation
|
|
401
|
+
* errors.
|
|
402
|
+
*
|
|
403
|
+
* Evolu does not use SQL for mutations to ensure data can be safely and
|
|
404
|
+
* predictably merged without conflicts. Explicit mutations also allow Evolu
|
|
405
|
+
* to automatically add and update {@link DefaultColumns}.
|
|
279
406
|
*
|
|
280
407
|
* ### Example
|
|
281
408
|
*
|
|
282
409
|
* ```ts
|
|
283
|
-
* //
|
|
410
|
+
* // Use deterministic ID for stable upserts across devices
|
|
411
|
+
* const stableId = createIdFromString("my-todo-1");
|
|
412
|
+
*
|
|
413
|
+
* const result = evolu.upsert("todo", {
|
|
414
|
+
* id: stableId,
|
|
415
|
+
* title: "Learn Evolu",
|
|
416
|
+
* isCompleted: false,
|
|
417
|
+
* });
|
|
418
|
+
*
|
|
419
|
+
* if (result.ok) {
|
|
420
|
+
* console.log("Todo upserted with ID:", result.value.id);
|
|
421
|
+
* } else {
|
|
422
|
+
* console.error("Validation error:", result.error);
|
|
423
|
+
* }
|
|
424
|
+
*
|
|
425
|
+
* // Data migration with custom createdAt
|
|
426
|
+
* evolu.upsert("todo", {
|
|
427
|
+
* id: externalId,
|
|
428
|
+
* title: "Migrated todo",
|
|
429
|
+
* createdAt: new Date("2023-01-01"), // Preserve original timestamp
|
|
430
|
+
* });
|
|
431
|
+
*
|
|
432
|
+
* // With onComplete callback
|
|
433
|
+
* evolu.upsert(
|
|
434
|
+
* "todo",
|
|
435
|
+
* { id: stableId, title: "Updated title" },
|
|
436
|
+
* {
|
|
437
|
+
* onComplete: () => {
|
|
438
|
+
* console.log("Upsert completed");
|
|
439
|
+
* },
|
|
440
|
+
* },
|
|
441
|
+
* );
|
|
284
442
|
* ```
|
|
285
443
|
*/
|
|
286
444
|
upsert: Mutation<S, "upsert">;
|
|
@@ -300,7 +458,7 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
|
|
|
300
458
|
|
|
301
459
|
/**
|
|
302
460
|
* Restore {@link AppOwner} with all their synced data. It uses
|
|
303
|
-
* {@link Evolu
|
|
461
|
+
* {@link Evolu#resetAppOwner}, so be careful.
|
|
304
462
|
*/
|
|
305
463
|
readonly restoreAppOwner: (
|
|
306
464
|
mnemonic: Mnemonic,
|
|
@@ -316,17 +474,60 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
|
|
|
316
474
|
readonly reloadApp: () => void;
|
|
317
475
|
|
|
318
476
|
/** Export SQLite database file as Uint8Array. */
|
|
319
|
-
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;
|
|
320
504
|
}
|
|
321
505
|
|
|
322
|
-
/**
|
|
506
|
+
/** Represents errors that can occur in Evolu. */
|
|
323
507
|
export type EvoluError =
|
|
324
|
-
| TimestampError
|
|
325
508
|
| ProtocolError
|
|
326
|
-
| TransferableError
|
|
327
|
-
| SymmetricCryptoDecryptError
|
|
328
509
|
| ProtocolUnsupportedVersionError
|
|
329
|
-
| 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
|
+
// }
|
|
330
531
|
|
|
331
532
|
interface InternalEvoluInstance<S extends EvoluSchema = EvoluSchema>
|
|
332
533
|
extends Evolu<S> {
|
|
@@ -337,44 +538,13 @@ interface InternalEvoluInstance<S extends EvoluSchema = EvoluSchema>
|
|
|
337
538
|
readonly ensureSchema: (schema: EvoluSchema) => void;
|
|
338
539
|
}
|
|
339
540
|
|
|
340
|
-
export type EvoluDeps =
|
|
341
|
-
|
|
342
|
-
|
|
541
|
+
export type EvoluDeps = ConsoleDep &
|
|
542
|
+
CreateDbWorkerDep &
|
|
543
|
+
LocalAuthDep &
|
|
343
544
|
Partial<FlushSyncDep> &
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
export interface EvoluConfigWithInitialData<S extends EvoluSchema = EvoluSchema>
|
|
348
|
-
extends Config {
|
|
349
|
-
/**
|
|
350
|
-
* Use this option to create initial data (fixtures).
|
|
351
|
-
*
|
|
352
|
-
* ### Example
|
|
353
|
-
*
|
|
354
|
-
* ```ts
|
|
355
|
-
* const evolu = createEvolu(evoluReactWebDeps)(Schema, {
|
|
356
|
-
* initialData: (evolu) => {
|
|
357
|
-
* const todoCategory = evolu.insert("todoCategory", {
|
|
358
|
-
* name: "Not Urgent",
|
|
359
|
-
* });
|
|
360
|
-
*
|
|
361
|
-
* // This is a developer error, which should be fixed immediately.
|
|
362
|
-
* assert(todoCategory.ok, "invalid initial data");
|
|
363
|
-
*
|
|
364
|
-
* evolu.insert("todo", {
|
|
365
|
-
* title: "Try React Suspense",
|
|
366
|
-
* categoryId: todoCategory.value.id,
|
|
367
|
-
* });
|
|
368
|
-
* },
|
|
369
|
-
* });
|
|
370
|
-
* ```
|
|
371
|
-
*/
|
|
372
|
-
initialData?: (evolu: EvoluForInitialData<S>) => void;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
export interface EvoluForInitialData<S extends EvoluSchema = EvoluSchema> {
|
|
376
|
-
insert: Mutation<S, "insert">;
|
|
377
|
-
}
|
|
545
|
+
RandomBytesDep &
|
|
546
|
+
ReloadAppDep &
|
|
547
|
+
TimeDep;
|
|
378
548
|
|
|
379
549
|
// For hot reloading and Evolu multitenancy.
|
|
380
550
|
const evoluInstances = new Map<string, InternalEvoluInstance>();
|
|
@@ -398,7 +568,8 @@ let tabId: Id | null = null;
|
|
|
398
568
|
* and merging.
|
|
399
569
|
* - Automatic schema evolution that updates the underlying database with new
|
|
400
570
|
* columns or tables.
|
|
401
|
-
* - Managing owner data with resetAppOwner and
|
|
571
|
+
* - Managing owner data with {@link Evolu#resetAppOwner} and
|
|
572
|
+
* {@link Evolu#restoreAppOwner}.
|
|
402
573
|
*
|
|
403
574
|
* ### Example
|
|
404
575
|
*
|
|
@@ -431,74 +602,117 @@ let tabId: Id | null = null;
|
|
|
431
602
|
export const createEvolu =
|
|
432
603
|
(deps: EvoluDeps) =>
|
|
433
604
|
<S extends EvoluSchema>(
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
schema: S,
|
|
437
|
-
partialConfig: Partial<EvoluConfigWithInitialData<S>> = {},
|
|
605
|
+
schema: ValidateSchema<S> extends never ? S : ValidateSchema<S>,
|
|
606
|
+
config?: EvoluConfig,
|
|
438
607
|
): Evolu<S> => {
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
let evolu = evoluInstances.get(config.name);
|
|
608
|
+
const name = config?.name ?? defaultDbConfig.name;
|
|
609
|
+
let evolu = evoluInstances.get(name);
|
|
442
610
|
|
|
443
611
|
if (evolu == null) {
|
|
444
|
-
evolu = createEvoluInstance(deps)(schema, config
|
|
445
|
-
evoluInstances.set(
|
|
612
|
+
evolu = createEvoluInstance(deps)(schema as EvoluSchema, config);
|
|
613
|
+
evoluInstances.set(name, evolu);
|
|
446
614
|
} else {
|
|
447
615
|
// Hot reloading. Note that indexes are intentionally omitted.
|
|
448
|
-
evolu.ensureSchema(schema);
|
|
616
|
+
evolu.ensureSchema(schema as EvoluSchema);
|
|
449
617
|
}
|
|
450
618
|
|
|
451
|
-
return evolu as
|
|
619
|
+
return evolu as Evolu<S>;
|
|
452
620
|
};
|
|
453
621
|
|
|
454
622
|
const createEvoluInstance =
|
|
455
623
|
(deps: EvoluDeps) =>
|
|
456
|
-
(
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
deps.console.enabled = evoluConfig.enableLogging ?? false;
|
|
624
|
+
(schema: EvoluSchema, config?: EvoluConfig): InternalEvoluInstance => {
|
|
625
|
+
deps.console.enabled = config?.enableLogging ?? false;
|
|
626
|
+
|
|
627
|
+
const { indexes, reloadUrl = "/", ...partialDbConfig } = config ?? {};
|
|
461
628
|
|
|
462
|
-
|
|
629
|
+
const dbConfig: DbConfig = { ...defaultDbConfig, ...partialDbConfig };
|
|
463
630
|
|
|
464
|
-
|
|
631
|
+
deps.console.log("[evolu]", "createEvoluInstance", {
|
|
632
|
+
name: dbConfig.name,
|
|
633
|
+
});
|
|
465
634
|
|
|
466
635
|
const errorStore = createStore<EvoluError | null>(null);
|
|
467
636
|
const rowsStore = createStore<QueryRowsMap>(new Map());
|
|
468
|
-
|
|
469
|
-
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);
|
|
470
647
|
|
|
471
648
|
const subscribedQueries = createSubscribedQueries(rowsStore);
|
|
472
649
|
const loadingPromises = createLoadingPromises(subscribedQueries);
|
|
473
|
-
const
|
|
650
|
+
const onCompleteRegistry = createCallbackRegistry(deps);
|
|
651
|
+
const exportRegistry =
|
|
652
|
+
createCallbackRegistry<Uint8Array<ArrayBuffer>>(deps);
|
|
474
653
|
|
|
475
|
-
const
|
|
476
|
-
const dbWorker = deps.createDbWorker(config.name);
|
|
654
|
+
const dbWorker = deps.createDbWorker(dbConfig.name);
|
|
477
655
|
|
|
478
656
|
const getTabId = () => {
|
|
479
657
|
tabId ??= createId(deps);
|
|
480
658
|
return tabId;
|
|
481
659
|
};
|
|
482
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).
|
|
483
697
|
dbWorker.onMessage((message) => {
|
|
484
698
|
switch (message.type) {
|
|
485
|
-
case "
|
|
486
|
-
|
|
699
|
+
case "onError": {
|
|
700
|
+
errorStore.set(message.error);
|
|
487
701
|
break;
|
|
488
702
|
}
|
|
489
703
|
|
|
490
|
-
case "
|
|
491
|
-
|
|
704
|
+
case "onGetAppOwner": {
|
|
705
|
+
resolveAppOwner(message.appOwner);
|
|
492
706
|
break;
|
|
493
707
|
}
|
|
494
708
|
|
|
495
|
-
case "
|
|
709
|
+
case "onQueryPatches": {
|
|
496
710
|
if (message.tabId !== getTabId()) return;
|
|
497
711
|
|
|
498
712
|
const state = rowsStore.get();
|
|
499
713
|
const nextState = new Map([
|
|
500
714
|
...state,
|
|
501
|
-
...message.
|
|
715
|
+
...message.queryPatches.map(
|
|
502
716
|
({ query, patches }): [Query, ReadonlyArray<Row>] => [
|
|
503
717
|
query,
|
|
504
718
|
applyPatches(patches, state.get(query) ?? emptyRows),
|
|
@@ -506,7 +720,7 @@ const createEvoluInstance =
|
|
|
506
720
|
),
|
|
507
721
|
]);
|
|
508
722
|
|
|
509
|
-
for (const { query } of message.
|
|
723
|
+
for (const { query } of message.queryPatches) {
|
|
510
724
|
loadingPromises.resolve(query, nextState.get(query) ?? emptyRows);
|
|
511
725
|
}
|
|
512
726
|
|
|
@@ -519,33 +733,111 @@ const createEvoluInstance =
|
|
|
519
733
|
}
|
|
520
734
|
|
|
521
735
|
for (const id of message.onCompleteIds) {
|
|
522
|
-
|
|
736
|
+
onCompleteRegistry.execute(id);
|
|
523
737
|
}
|
|
524
738
|
break;
|
|
525
739
|
}
|
|
526
740
|
|
|
527
|
-
case "
|
|
741
|
+
case "refreshQueries": {
|
|
528
742
|
if (message.tabId && message.tabId === getTabId()) return;
|
|
529
743
|
|
|
530
|
-
loadingPromises.
|
|
531
|
-
|
|
744
|
+
const loadingPromisesQueries = loadingPromises.getQueries();
|
|
745
|
+
loadingPromises.releaseUnsubscribedOnMutation();
|
|
746
|
+
|
|
747
|
+
const queries = [
|
|
748
|
+
// Dedupe
|
|
749
|
+
...new Set([...loadingPromisesQueries, ...subscribedQueries.get()]),
|
|
750
|
+
];
|
|
751
|
+
|
|
532
752
|
if (isNonEmptyReadonlyArray(queries)) {
|
|
533
753
|
dbWorker.postMessage({ type: "query", tabId: getTabId(), queries });
|
|
534
754
|
}
|
|
755
|
+
|
|
535
756
|
break;
|
|
536
757
|
}
|
|
537
758
|
|
|
538
759
|
case "onReset": {
|
|
539
760
|
if (message.reload) {
|
|
540
|
-
|
|
761
|
+
deps.reloadApp(reloadUrl);
|
|
541
762
|
} else {
|
|
542
|
-
|
|
763
|
+
onCompleteRegistry.execute(message.onCompleteId);
|
|
543
764
|
}
|
|
544
765
|
break;
|
|
545
766
|
}
|
|
546
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
|
+
|
|
547
836
|
case "onExport": {
|
|
548
|
-
|
|
837
|
+
exportRegistry.execute(
|
|
838
|
+
message.onCompleteId,
|
|
839
|
+
message.file as Uint8Array<ArrayBuffer>,
|
|
840
|
+
);
|
|
549
841
|
break;
|
|
550
842
|
}
|
|
551
843
|
|
|
@@ -554,10 +846,7 @@ const createEvoluInstance =
|
|
|
554
846
|
}
|
|
555
847
|
});
|
|
556
848
|
|
|
557
|
-
const dbSchema =
|
|
558
|
-
assertValidEvoluSchema(schema),
|
|
559
|
-
indexes,
|
|
560
|
-
);
|
|
849
|
+
const dbSchema = evoluSchemaToDbSchema(schema, indexes);
|
|
561
850
|
|
|
562
851
|
const mutationTypesCache = new Map<
|
|
563
852
|
MutationKind,
|
|
@@ -581,48 +870,19 @@ const createEvoluInstance =
|
|
|
581
870
|
return type;
|
|
582
871
|
};
|
|
583
872
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
/**
|
|
587
|
-
* Note that the initial data function is called even if it is unnecessary
|
|
588
|
-
* (initial data are already in the DB) because we don't want to wait for
|
|
589
|
-
* SQLite's response. Initial data should be small (because they are inlined
|
|
590
|
-
* in the code), so it's ok.
|
|
591
|
-
*/
|
|
592
|
-
if (initialData)
|
|
593
|
-
initialData({
|
|
594
|
-
insert: (table, props) => {
|
|
595
|
-
const Type = getMutationType(table, "insert");
|
|
596
|
-
const id = createId(deps);
|
|
597
|
-
|
|
598
|
-
const result = Type.fromUnknown(props);
|
|
599
|
-
|
|
600
|
-
if (result.ok) {
|
|
601
|
-
initialDataDbChanges.push({
|
|
602
|
-
id,
|
|
603
|
-
table,
|
|
604
|
-
values: result.value,
|
|
605
|
-
} as unknown as DbChange);
|
|
606
|
-
return ok({ id });
|
|
607
|
-
}
|
|
873
|
+
dbWorker.postMessage({ type: "init", config: dbConfig, dbSchema });
|
|
608
874
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
dbWorker.postMessage({
|
|
614
|
-
type: "init",
|
|
615
|
-
config,
|
|
616
|
-
dbSchema,
|
|
617
|
-
initialData: initialDataDbChanges,
|
|
618
|
-
});
|
|
875
|
+
// We can't use `init` to get AppOwner because `init` runs only once per n tabs.
|
|
876
|
+
dbWorker.postMessage({ type: "getAppOwner" });
|
|
619
877
|
|
|
620
878
|
const loadQueryMicrotaskQueue: Array<Query> = [];
|
|
621
879
|
|
|
622
880
|
const mutateMicrotaskQueue: Array<
|
|
623
|
-
[
|
|
881
|
+
[MutationChange | null, MutationOptions["onComplete"] | undefined]
|
|
624
882
|
> = [];
|
|
625
883
|
|
|
884
|
+
const useOwnerMicrotaskQueue: Array<[SyncOwner, boolean, Uint8Array]> = [];
|
|
885
|
+
|
|
626
886
|
const createMutation =
|
|
627
887
|
<Kind extends MutationKind>(kind: Kind) =>
|
|
628
888
|
<TableName extends keyof typeof schema>(
|
|
@@ -638,8 +898,7 @@ const createEvoluInstance =
|
|
|
638
898
|
ObjectType<MutationMapping<(typeof schema)[TableName], Kind>>
|
|
639
899
|
>
|
|
640
900
|
> => {
|
|
641
|
-
const
|
|
642
|
-
const result = Type.fromUnknown(props);
|
|
901
|
+
const result = getMutationType(table, kind).fromUnknown(props);
|
|
643
902
|
|
|
644
903
|
const id =
|
|
645
904
|
kind === "insert"
|
|
@@ -648,51 +907,32 @@ const createEvoluInstance =
|
|
|
648
907
|
|
|
649
908
|
if (options?.onlyValidate !== true) {
|
|
650
909
|
if (!result.ok) {
|
|
651
|
-
//
|
|
652
|
-
|
|
653
|
-
mutateMicrotaskQueue.push([undefined, undefined]);
|
|
910
|
+
// Mark the transaction as invalid by pushing null
|
|
911
|
+
mutateMicrotaskQueue.push([null, undefined]);
|
|
654
912
|
} else {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
// EvoluSchema Types ensure valid types.
|
|
658
|
-
const change = { table, id, values } as unknown as DbChange;
|
|
659
|
-
mutateMicrotaskQueue.push([change, options?.onComplete]);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
if (mutateMicrotaskQueue.length === 1)
|
|
663
|
-
queueMicrotask(() => {
|
|
664
|
-
const changes = [];
|
|
665
|
-
const onCompletes = [];
|
|
913
|
+
const values = { ...result.value };
|
|
914
|
+
delete values.id;
|
|
666
915
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
916
|
+
if (kind === "insert" || kind === "upsert") {
|
|
917
|
+
// Only set createdAt if not provided by user
|
|
918
|
+
if (!("createdAt" in values)) {
|
|
919
|
+
values.createdAt = new Date(deps.time.now()).toISOString();
|
|
670
920
|
}
|
|
921
|
+
}
|
|
671
922
|
|
|
672
|
-
|
|
673
|
-
|
|
923
|
+
const dbChange = { table, id, values };
|
|
924
|
+
assert(
|
|
925
|
+
DbChange.is(dbChange),
|
|
926
|
+
`Failed to create DbChange for table "${dbChange.table}"`,
|
|
927
|
+
);
|
|
674
928
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
return;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
const onCompleteIds = onCompletes.map((onComplete) =>
|
|
682
|
-
callbacks.register(onComplete),
|
|
683
|
-
);
|
|
684
|
-
|
|
685
|
-
loadingPromises.releaseUnsubscribed();
|
|
929
|
+
const mutationChange = { ...dbChange, ownerId: options?.ownerId };
|
|
930
|
+
mutateMicrotaskQueue.push([mutationChange, options?.onComplete]);
|
|
931
|
+
}
|
|
686
932
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
tabId: getTabId(),
|
|
691
|
-
changes,
|
|
692
|
-
onCompleteIds,
|
|
693
|
-
subscribedQueries: subscribedQueries.get(),
|
|
694
|
-
});
|
|
695
|
-
});
|
|
933
|
+
if (mutateMicrotaskQueue.length === 1) {
|
|
934
|
+
queueMicrotask(processMutationQueue);
|
|
935
|
+
}
|
|
696
936
|
}
|
|
697
937
|
|
|
698
938
|
if (result.ok) return ok({ id });
|
|
@@ -706,28 +946,74 @@ const createEvoluInstance =
|
|
|
706
946
|
);
|
|
707
947
|
};
|
|
708
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
|
+
|
|
709
1012
|
const evolu: InternalEvoluInstance = {
|
|
710
1013
|
subscribeError: errorStore.subscribe,
|
|
711
1014
|
getError: errorStore.get,
|
|
712
1015
|
|
|
713
|
-
createQuery
|
|
714
|
-
const compiledQuery = queryCallback(
|
|
715
|
-
kysely as IntentionalNever,
|
|
716
|
-
).compile();
|
|
717
|
-
|
|
718
|
-
if (isSqlMutation(compiledQuery.sql))
|
|
719
|
-
throw new Error(
|
|
720
|
-
"SQL mutation (INSERT, UPDATE, DELETE, etc.) isn't allowed in the Evolu `createQuery` function. Kysely suggests it because there is no read-only Kysely yet, and removing such an API is not possible. For mutations, use Evolu Mutation API.",
|
|
721
|
-
);
|
|
722
|
-
|
|
723
|
-
return serializeQuery({
|
|
724
|
-
sql: compiledQuery.sql as SafeSql,
|
|
725
|
-
parameters: compiledQuery.parameters as NonNullable<
|
|
726
|
-
SqliteQuery["parameters"]
|
|
727
|
-
>,
|
|
728
|
-
...(options && { options }),
|
|
729
|
-
});
|
|
730
|
-
},
|
|
1016
|
+
createQuery,
|
|
731
1017
|
|
|
732
1018
|
loadQuery: <R extends Row>(query: Query<R>): Promise<QueryRows<R>> => {
|
|
733
1019
|
const { promise, isNew } = loadingPromises.get(query);
|
|
@@ -740,6 +1026,7 @@ const createEvoluInstance =
|
|
|
740
1026
|
const queries = [...new Set(loadQueryMicrotaskQueue)];
|
|
741
1027
|
loadQueryMicrotaskQueue.length = 0;
|
|
742
1028
|
assertNonEmptyArray(queries);
|
|
1029
|
+
deps.console.log("[evolu]", "loadQuery", { queries });
|
|
743
1030
|
dbWorker.postMessage({
|
|
744
1031
|
type: "query",
|
|
745
1032
|
tabId: getTabId(),
|
|
@@ -775,23 +1062,19 @@ const createEvoluInstance =
|
|
|
775
1062
|
getQueryRows: <R extends Row>(query: Query<R>): QueryRows<R> =>
|
|
776
1063
|
(rowsStore.get().get(query) ?? emptyRows) as QueryRows<R>,
|
|
777
1064
|
|
|
778
|
-
|
|
779
|
-
getAppOwner: ownerStore.get,
|
|
1065
|
+
appOwner,
|
|
780
1066
|
|
|
781
|
-
|
|
782
|
-
|
|
1067
|
+
// TODO: Update it for the owner-api
|
|
1068
|
+
// subscribeSyncState: syncStore.subscribe,
|
|
1069
|
+
// getSyncState: syncStore.get,
|
|
783
1070
|
|
|
784
1071
|
insert: createMutation("insert"),
|
|
785
1072
|
update: createMutation("update"),
|
|
786
1073
|
upsert: createMutation("upsert"),
|
|
787
1074
|
|
|
788
1075
|
resetAppOwner: (options) => {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
const { promise, resolve } = Promise.withResolvers<void>();
|
|
792
|
-
const onCompleteId = callbacks.register(() => {
|
|
793
|
-
resolve();
|
|
794
|
-
});
|
|
1076
|
+
const { promise, resolve } = Promise.withResolvers<undefined>();
|
|
1077
|
+
const onCompleteId = onCompleteRegistry.register(resolve);
|
|
795
1078
|
dbWorker.postMessage({
|
|
796
1079
|
type: "reset",
|
|
797
1080
|
onCompleteId,
|
|
@@ -801,12 +1084,8 @@ const createEvoluInstance =
|
|
|
801
1084
|
},
|
|
802
1085
|
|
|
803
1086
|
restoreAppOwner: (mnemonic, options) => {
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
const { promise, resolve } = Promise.withResolvers<void>();
|
|
807
|
-
const onCompleteId = callbacks.register(() => {
|
|
808
|
-
resolve();
|
|
809
|
-
});
|
|
1087
|
+
const { promise, resolve } = Promise.withResolvers<undefined>();
|
|
1088
|
+
const onCompleteId = onCompleteRegistry.register(resolve);
|
|
810
1089
|
dbWorker.postMessage({
|
|
811
1090
|
type: "reset",
|
|
812
1091
|
onCompleteId,
|
|
@@ -817,35 +1096,100 @@ const createEvoluInstance =
|
|
|
817
1096
|
},
|
|
818
1097
|
|
|
819
1098
|
reloadApp: () => {
|
|
820
|
-
|
|
1099
|
+
deps.reloadApp(reloadUrl);
|
|
821
1100
|
},
|
|
822
1101
|
|
|
823
1102
|
ensureSchema: (schema) => {
|
|
824
1103
|
mutationTypesCache.clear();
|
|
825
|
-
const
|
|
826
|
-
dbWorker.postMessage({
|
|
827
|
-
type: "ensureDbSchema",
|
|
828
|
-
dbSchema: validEvoluSchemaToDbSchema(validSchema),
|
|
829
|
-
});
|
|
1104
|
+
const dbSchema = evoluSchemaToDbSchema(schema);
|
|
1105
|
+
dbWorker.postMessage({ type: "ensureDbSchema", dbSchema });
|
|
830
1106
|
},
|
|
831
1107
|
|
|
832
1108
|
exportDatabase: () => {
|
|
833
|
-
const { promise, resolve } =
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
});
|
|
1109
|
+
const { promise, resolve } =
|
|
1110
|
+
Promise.withResolvers<Uint8Array<ArrayBuffer>>();
|
|
1111
|
+
const onCompleteId = exportRegistry.register(resolve);
|
|
837
1112
|
dbWorker.postMessage({ type: "export", onCompleteId });
|
|
838
1113
|
return promise;
|
|
839
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
|
+
},
|
|
840
1169
|
};
|
|
841
1170
|
|
|
842
1171
|
return evolu;
|
|
843
1172
|
};
|
|
844
1173
|
|
|
845
|
-
export const
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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
|
+
};
|
|
849
1193
|
|
|
850
1194
|
interface LoadingPromises {
|
|
851
1195
|
get: <R extends Row>(
|
|
@@ -855,13 +1199,32 @@ interface LoadingPromises {
|
|
|
855
1199
|
readonly isNew: boolean;
|
|
856
1200
|
};
|
|
857
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
|
+
*/
|
|
858
1211
|
resolve: (query: Query, rows: ReadonlyArray<Row>) => void;
|
|
859
1212
|
|
|
860
|
-
|
|
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>;
|
|
861
1224
|
}
|
|
862
1225
|
|
|
863
1226
|
interface LoadingPromise {
|
|
864
|
-
/** Promise with props for the
|
|
1227
|
+
/** Promise with props for the React use hook. */
|
|
865
1228
|
promise: Promise<QueryRows> & {
|
|
866
1229
|
status?: "pending" | "fulfilled" | "rejected";
|
|
867
1230
|
value?: QueryRows;
|
|
@@ -876,7 +1239,7 @@ const createLoadingPromises = (
|
|
|
876
1239
|
): LoadingPromises => {
|
|
877
1240
|
const loadingPromiseMap = new Map<Query, LoadingPromise>();
|
|
878
1241
|
|
|
879
|
-
|
|
1242
|
+
return {
|
|
880
1243
|
get: <R extends Row>(
|
|
881
1244
|
query: Query<R>,
|
|
882
1245
|
): {
|
|
@@ -903,16 +1266,13 @@ const createLoadingPromises = (
|
|
|
903
1266
|
if (loadingPromise.promise.status !== "fulfilled") {
|
|
904
1267
|
loadingPromise.resolve(rows);
|
|
905
1268
|
} else {
|
|
906
|
-
// A promise can't be fulfilled 2x, so we need a new one.
|
|
907
1269
|
loadingPromise.promise = Promise.resolve(rows);
|
|
908
1270
|
}
|
|
909
1271
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
* https://github.com/acdlite/rfcs/blob/first-class-promises/text/0000-first-class-support-for-promises.md
|
|
915
|
-
*/
|
|
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
|
|
916
1276
|
void Object.assign(loadingPromise.promise, {
|
|
917
1277
|
status: "fulfilled",
|
|
918
1278
|
value: rows,
|
|
@@ -923,14 +1283,7 @@ const createLoadingPromises = (
|
|
|
923
1283
|
}
|
|
924
1284
|
},
|
|
925
1285
|
|
|
926
|
-
|
|
927
|
-
* We can't delete loading promises in `resolveLoadingPromises` because they
|
|
928
|
-
* must be cached, so repeated calls to `loadQuery` will always return the
|
|
929
|
-
* same promise until the data changes, and we also can't cache them forever
|
|
930
|
-
* because only subscribed queries are automatically updated (reactivity is
|
|
931
|
-
* expensive) hence this function must be called manually on any mutation.
|
|
932
|
-
*/
|
|
933
|
-
releaseUnsubscribed: () => {
|
|
1286
|
+
releaseUnsubscribedOnMutation: () => {
|
|
934
1287
|
[...loadingPromiseMap.entries()]
|
|
935
1288
|
.filter(([query]) => !subscribedQueries.has(query))
|
|
936
1289
|
.forEach(([query, loadingPromise]) => {
|
|
@@ -941,7 +1294,7 @@ const createLoadingPromises = (
|
|
|
941
1294
|
}
|
|
942
1295
|
});
|
|
943
1296
|
},
|
|
944
|
-
};
|
|
945
1297
|
|
|
946
|
-
|
|
1298
|
+
getQueries: () => Array.from(loadingPromiseMap.keys()),
|
|
1299
|
+
};
|
|
947
1300
|
};
|