@evolu/common 8.10.0 → 8.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/Bytes.d.ts +39 -2
- package/dist/src/Bytes.d.ts.map +1 -1
- package/dist/src/Bytes.js +50 -2
- package/dist/src/Config.d.ts +22 -22
- package/dist/src/Config.d.ts.map +1 -1
- package/dist/src/Console.d.ts +62 -7
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +20 -4
- package/dist/src/Crypto.d.ts +76 -4
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +55 -4
- package/dist/src/Error.d.ts +45 -0
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +69 -0
- package/dist/src/Fs.d.ts +92 -18
- package/dist/src/Fs.d.ts.map +1 -1
- package/dist/src/Fs.js +2 -0
- package/dist/src/Identicon.d.ts +2 -2
- package/dist/src/Identicon.js +2 -2
- package/dist/src/LeakDetector.d.ts +22 -3
- package/dist/src/LeakDetector.d.ts.map +1 -1
- package/dist/src/LeakDetector.js +12 -2
- package/dist/src/LockManager.d.ts +8 -0
- package/dist/src/LockManager.d.ts.map +1 -1
- package/dist/src/LockManager.js +6 -0
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +5 -0
- package/dist/src/Platform.d.ts +47 -7
- package/dist/src/Platform.d.ts.map +1 -1
- package/dist/src/Platform.js +24 -5
- package/dist/src/Random.d.ts +25 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Random.js +14 -2
- package/dist/src/Resource.d.ts +156 -1
- package/dist/src/Resource.d.ts.map +1 -1
- package/dist/src/Resource.js +201 -72
- package/dist/src/Schedule.d.ts +11 -10
- package/dist/src/Schedule.d.ts.map +1 -1
- package/dist/src/Schedule.js +1 -1
- package/dist/src/Sqlite.d.ts +132 -16
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +63 -9
- package/dist/src/Task.d.ts +15 -4
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +41 -15
- package/dist/src/Test.d.ts +9 -0
- package/dist/src/Test.d.ts.map +1 -1
- package/dist/src/Test.js +4 -0
- package/dist/src/Time.d.ts +106 -9
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +55 -4
- package/dist/src/Type.d.ts +1455 -1310
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +1274 -517
- package/dist/src/WebSocket.d.ts +164 -13
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +133 -24
- package/dist/src/Worker.d.ts +90 -8
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +28 -2
- package/dist/src/index.d.ts +6 -7
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -3
- package/dist/src/local-first/Db.d.ts +52 -3
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +412 -137
- package/dist/src/local-first/Evolu.d.ts +412 -213
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +181 -18
- package/dist/src/local-first/Owner.d.ts +13 -30
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +13 -30
- package/dist/src/local-first/Protocol.d.ts +106 -19
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +162 -60
- package/dist/src/local-first/Query.d.ts +8 -15
- package/dist/src/local-first/Query.d.ts.map +1 -1
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +4 -2
- package/dist/src/local-first/Schema.d.ts +346 -23
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +214 -17
- package/dist/src/local-first/Shared.d.ts +537 -22
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +1437 -234
- package/dist/src/local-first/Storage.d.ts +195 -17
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +85 -22
- package/dist/src/local-first/Timestamp.d.ts +392 -41
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +403 -81
- package/dist/src/local-first/index.d.ts +0 -1
- package/dist/src/local-first/index.d.ts.map +1 -1
- package/dist/src/local-first/index.js +0 -1
- package/package.json +1 -1
- package/src/Assert.test.ts +2 -5
- package/src/Bytes.test.ts +27 -0
- package/src/Bytes.ts +58 -2
- package/src/Config.test.ts +2 -6
- package/src/Config.ts +133 -133
- package/src/Console.ts +62 -7
- package/src/Crypto.ts +76 -4
- package/src/Eq.test.ts +2 -3
- package/src/Error.test.ts +76 -3
- package/src/Error.ts +71 -0
- package/src/Fs.ts +92 -18
- package/src/Identicon.ts +2 -2
- package/src/LeakDetector.ts +22 -3
- package/src/LockManager.ts +8 -0
- package/src/Object.test.ts +27 -12
- package/src/Object.ts +5 -0
- package/src/Platform.ts +50 -8
- package/src/Random.ts +25 -2
- package/src/Resource.test.ts +837 -0
- package/src/Resource.ts +235 -15
- package/src/Schedule.test.ts +50 -12
- package/src/Schedule.ts +24 -14
- package/src/Sqlite.ts +137 -17
- package/src/Task.test.ts +189 -8
- package/src/Task.ts +56 -17
- package/src/Test.ts +9 -0
- package/src/Time.ts +106 -9
- package/src/Type.test.ts +946 -1028
- package/src/Type.ts +4195 -3136
- package/src/Types.test.ts +4 -14
- package/src/WebSocket.ts +313 -40
- package/src/Worker.ts +90 -8
- package/src/index.ts +20 -6
- package/src/local-first/Db.ts +644 -339
- package/src/local-first/Evolu.test.ts +994 -22
- package/src/local-first/Evolu.ts +625 -232
- package/src/local-first/Owner.ts +13 -30
- package/src/local-first/Protocol.test.ts +634 -10
- package/src/local-first/Protocol.ts +255 -109
- package/src/local-first/Query.ts +8 -15
- package/src/local-first/Relay.ts +4 -2
- package/src/local-first/Schema.test.ts +143 -0
- package/src/local-first/Schema.ts +376 -26
- package/src/local-first/Shared.test.ts +7731 -559
- package/src/local-first/Shared.ts +2036 -267
- package/src/local-first/Storage.ts +224 -36
- package/src/local-first/Timestamp.test.ts +344 -70
- package/src/local-first/Timestamp.ts +434 -118
- package/src/local-first/index.ts +0 -1
- package/dist/src/local-first/Error.d.ts +0 -12
- package/dist/src/local-first/Error.d.ts.map +0 -1
- package/dist/src/local-first/Error.js +0 -6
- package/dist/src/local-first/LocalAuth.d.ts +0 -150
- package/dist/src/local-first/LocalAuth.d.ts.map +0 -1
- package/dist/src/local-first/LocalAuth.js +0 -179
- package/src/local-first/Error.ts +0 -17
- package/src/local-first/LocalAuth.ts +0 -457
package/src/local-first/Evolu.ts
CHANGED
|
@@ -15,9 +15,12 @@ import {
|
|
|
15
15
|
assertNonEmptyReadonlyArray,
|
|
16
16
|
assertNotUndefined,
|
|
17
17
|
} from "../Assert.ts";
|
|
18
|
+
import { createBuffer } from "../Bytes.ts";
|
|
18
19
|
import { createCallbacks } from "../Callbacks.ts";
|
|
19
20
|
import type { ConsoleDep } from "../Console.ts";
|
|
20
21
|
import { createConsole } from "../Console.ts";
|
|
22
|
+
import type { DecryptWithXChaCha20Poly1305Error } from "../Crypto.ts";
|
|
23
|
+
import type { UnknownError } from "../Error.ts";
|
|
21
24
|
import { createUnknownError } from "../Error.ts";
|
|
22
25
|
import { disposable, exhaustiveCheck, todo } from "../Function.ts";
|
|
23
26
|
import {
|
|
@@ -26,11 +29,20 @@ import {
|
|
|
26
29
|
type LockManagerDep,
|
|
27
30
|
} from "../LockManager.ts";
|
|
28
31
|
import { createMicrotaskBatch } from "../Microtask.ts";
|
|
32
|
+
import {
|
|
33
|
+
createMutableRecord,
|
|
34
|
+
objectToEntries,
|
|
35
|
+
type ReadonlyRecord,
|
|
36
|
+
} from "../Object.ts";
|
|
29
37
|
import type { FlushSyncDep, ReloadAppDep } from "../Platform.ts";
|
|
30
38
|
import { createRefCountByKey } from "../RefCount.ts";
|
|
31
39
|
import { err, ok } from "../Result.ts";
|
|
32
40
|
import { isNonEmptySet } from "../Set.ts";
|
|
33
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
SqliteBoolean,
|
|
43
|
+
sqliteBooleanToBoolean,
|
|
44
|
+
type SqliteValue,
|
|
45
|
+
} from "../Sqlite.ts";
|
|
34
46
|
import type { Listener, ReadonlyStore, Unsubscribe } from "../Store.ts";
|
|
35
47
|
import { createStore } from "../Store.ts";
|
|
36
48
|
import type { Task } from "../Task.ts";
|
|
@@ -39,17 +51,18 @@ import {
|
|
|
39
51
|
createId,
|
|
40
52
|
createIdFromString,
|
|
41
53
|
type ExtractTyped,
|
|
42
|
-
|
|
54
|
+
Id,
|
|
43
55
|
Name,
|
|
56
|
+
PositiveInt,
|
|
44
57
|
type TypeError,
|
|
45
58
|
UrlSafeString,
|
|
46
59
|
} from "../Type.ts";
|
|
47
60
|
import type {
|
|
61
|
+
BroadcastChannel,
|
|
48
62
|
CreateBroadcastChannelDep,
|
|
49
63
|
CreateMessageChannelDep,
|
|
50
64
|
} from "../Worker.ts";
|
|
51
|
-
import type { CreateDbWorkerDep } from "./Db.ts";
|
|
52
|
-
import type { EvoluError } from "./Error.ts";
|
|
65
|
+
import type { CreateDbWorkerDep, UnsupportedDbVersionError } from "./Db.ts";
|
|
53
66
|
import type {
|
|
54
67
|
AppOwner,
|
|
55
68
|
Owner,
|
|
@@ -59,6 +72,12 @@ import type {
|
|
|
59
72
|
SyncOwner,
|
|
60
73
|
} from "./Owner.ts";
|
|
61
74
|
import { createOwnerWebSocketTransport } from "./Owner.ts";
|
|
75
|
+
import {
|
|
76
|
+
encodeDbChange,
|
|
77
|
+
type ProtocolError,
|
|
78
|
+
type ProtocolQuotaError,
|
|
79
|
+
type ProtocolVersionError,
|
|
80
|
+
} from "./Protocol.ts";
|
|
62
81
|
import type {
|
|
63
82
|
Queries,
|
|
64
83
|
QueriesToQueryRowsPromises,
|
|
@@ -73,30 +92,50 @@ import type {
|
|
|
73
92
|
IndexesConfig,
|
|
74
93
|
Mutation,
|
|
75
94
|
MutationChange,
|
|
95
|
+
MutationOptions,
|
|
96
|
+
MutationValues,
|
|
76
97
|
ValidateSchema,
|
|
77
98
|
} from "./Schema.ts";
|
|
78
|
-
import { evoluSchemaToSqliteSchema } from "./Schema.ts";
|
|
99
|
+
import { evoluSchemaToSqliteSchema, isLocalOnlyTable } from "./Schema.ts";
|
|
79
100
|
import type {
|
|
80
101
|
ConsoleEntryOrError,
|
|
81
102
|
EvoluInput,
|
|
82
103
|
EvoluOutput,
|
|
104
|
+
OtherBuildRunningError,
|
|
83
105
|
SharedWorkerDep,
|
|
106
|
+
SyncState,
|
|
107
|
+
syncStateToOwnerSyncStates,
|
|
84
108
|
} from "./Shared.ts";
|
|
85
109
|
import { consoleEntryOrErrorBroadcastChannelName } from "./Shared.ts";
|
|
86
|
-
import { DbChange } from "./Storage.ts";
|
|
87
|
-
import type
|
|
110
|
+
import { DbChange, type StorageQuotaError } from "./Storage.ts";
|
|
111
|
+
import { createTimestamp, type Timestamp } from "./Timestamp.ts";
|
|
88
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Configuration for {@link createEvolu}.
|
|
115
|
+
*
|
|
116
|
+
* @group Configuration
|
|
117
|
+
*/
|
|
89
118
|
export interface EvoluConfig {
|
|
90
119
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
120
|
+
* An application name used in logs and local database names.
|
|
121
|
+
*
|
|
122
|
+
* Evolu combines `appName` with the {@link AppOwner} identity to identify the
|
|
123
|
+
* local database. Changing either opens a different database. Keep `appName`
|
|
124
|
+
* stable across ordinary application updates.
|
|
125
|
+
*
|
|
126
|
+
* Instances that share an `appName` and AppOwner open the same database and
|
|
127
|
+
* must use the same schema. Evolu applies the schema of the first instance
|
|
128
|
+
* and later instances join it.
|
|
94
129
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
130
|
+
* A different app name lets you create a separate local replica for the same
|
|
131
|
+
* AppOwner—for example, to test a different SQLite implementation or index
|
|
132
|
+
* configuration, or rebuild a replica for debugging while preserving the
|
|
133
|
+
* existing database.
|
|
134
|
+
*
|
|
135
|
+
* Evolu supports running these databases concurrently. Their local separation
|
|
136
|
+
* does not isolate synchronization: replicas synchronizing the same owners
|
|
137
|
+
* through the same relay can still exchange changes. Changing `appName`
|
|
138
|
+
* neither migrates existing local data nor isolates incompatible schemas.
|
|
100
139
|
*
|
|
101
140
|
* ### Example
|
|
102
141
|
*
|
|
@@ -132,6 +171,73 @@ export interface EvoluConfig {
|
|
|
132
171
|
*/
|
|
133
172
|
readonly appOwner: AppOwner;
|
|
134
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Keep the database in memory instead of persisting it on this device.
|
|
176
|
+
*
|
|
177
|
+
* This option controls device persistence independently of synchronization.
|
|
178
|
+
* When synchronization is enabled, data can still be synchronized and
|
|
179
|
+
* persisted remotely.
|
|
180
|
+
*
|
|
181
|
+
* Useful for testing or temporary sessions. Data that exists only in this
|
|
182
|
+
* database is lost when the database closes.
|
|
183
|
+
*
|
|
184
|
+
* The default value is: `false`.
|
|
185
|
+
*/
|
|
186
|
+
readonly memoryOnly?: boolean;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Use the `indexes` option to define SQLite indexes.
|
|
190
|
+
*
|
|
191
|
+
* Table and column names are not typed because Kysely doesn't support it.
|
|
192
|
+
*
|
|
193
|
+
* https://medium.com/@JasonWyatt/squeezing-performance-from-sqlite-indexes-indexes-c4e175f3c346
|
|
194
|
+
*
|
|
195
|
+
* ### Example
|
|
196
|
+
*
|
|
197
|
+
* ```ts
|
|
198
|
+
* import {
|
|
199
|
+
* createEvolu,
|
|
200
|
+
* id,
|
|
201
|
+
* testAppName,
|
|
202
|
+
* testAppOwner,
|
|
203
|
+
* } from "@evolu/common";
|
|
204
|
+
*
|
|
205
|
+
* const Schema = {
|
|
206
|
+
* todo: { id: id("Todo") },
|
|
207
|
+
* todoCategory: { id: id("TodoCategory") },
|
|
208
|
+
* };
|
|
209
|
+
*
|
|
210
|
+
* const _createTodoEvolu = createEvolu(Schema, {
|
|
211
|
+
* appName: testAppName,
|
|
212
|
+
* appOwner: testAppOwner,
|
|
213
|
+
* transports: [],
|
|
214
|
+
* indexes: (create) => [
|
|
215
|
+
* create("todoCreatedAt").on("todo").column("createdAt"),
|
|
216
|
+
* create("todoCategoryCreatedAt")
|
|
217
|
+
* .on("todoCategory")
|
|
218
|
+
* .column("createdAt"),
|
|
219
|
+
* ],
|
|
220
|
+
* });
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
readonly indexes?: IndexesConfig;
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Called when this instance's local database is deleted.
|
|
227
|
+
*
|
|
228
|
+
* Apps can use this to update UI immediately because the corresponding
|
|
229
|
+
* {@link Evolu} instance becomes unusable after local database deletion.
|
|
230
|
+
*/
|
|
231
|
+
readonly onDatabaseDeleted?: () => void;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Called when local data for an {@link Owner} is deleted.
|
|
235
|
+
*
|
|
236
|
+
* Apps can use this to update UI immediately because that owner stops being
|
|
237
|
+
* used across tabs and instances.
|
|
238
|
+
*/
|
|
239
|
+
readonly onOwnerDeleted?: (owner: Owner) => void;
|
|
240
|
+
|
|
135
241
|
/**
|
|
136
242
|
* Transport configuration for sync and backup.
|
|
137
243
|
*
|
|
@@ -169,18 +275,11 @@ export interface EvoluConfig {
|
|
|
169
275
|
* ```ts
|
|
170
276
|
* import {
|
|
171
277
|
* assertEqual,
|
|
172
|
-
* createAppOwner,
|
|
173
278
|
* createOwnerWebSocketTransport,
|
|
174
|
-
*
|
|
175
|
-
* createRandomBytes,
|
|
279
|
+
* testAppOwner,
|
|
176
280
|
* type OwnerTransport,
|
|
177
281
|
* } from "@evolu/common";
|
|
178
282
|
*
|
|
179
|
-
* // Create once, persist the mnemonic securely, and restore it on later runs.
|
|
180
|
-
* const appOwner = createAppOwner(
|
|
181
|
-
* createOwnerSecret({ randomBytes: createRandomBytes() }),
|
|
182
|
-
* );
|
|
183
|
-
*
|
|
184
283
|
* // Use one relay.
|
|
185
284
|
* const _singleRelay = [
|
|
186
285
|
* { type: "WebSocket", url: "wss://relay1.example.com" },
|
|
@@ -201,91 +300,17 @@ export interface EvoluConfig {
|
|
|
201
300
|
* const authenticatedRelay = [
|
|
202
301
|
* createOwnerWebSocketTransport({
|
|
203
302
|
* url: "wss://relay.example.com",
|
|
204
|
-
* ownerId:
|
|
303
|
+
* ownerId: testAppOwner.id,
|
|
205
304
|
* }),
|
|
206
305
|
* ];
|
|
207
306
|
*
|
|
208
307
|
* assertEqual(
|
|
209
308
|
* authenticatedRelay[0]?.url,
|
|
210
|
-
* `wss://relay.example.com?ownerId=${
|
|
309
|
+
* `wss://relay.example.com?ownerId=${testAppOwner.id}`,
|
|
211
310
|
* );
|
|
212
311
|
* ```
|
|
213
312
|
*/
|
|
214
313
|
readonly transports?: ReadonlyArray<OwnerTransport>;
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Keep local data only in memory instead of persisting it on this device.
|
|
218
|
-
* Useful for testing, temporary data, or sensitive data that should not be
|
|
219
|
-
* recoverable from local storage after the process ends.
|
|
220
|
-
*
|
|
221
|
-
* Local data stored in memory is completely destroyed when the process ends.
|
|
222
|
-
* Sync can still persist data remotely when transports are enabled.
|
|
223
|
-
*
|
|
224
|
-
* The default value is: `false`.
|
|
225
|
-
*/
|
|
226
|
-
readonly memoryOnly?: boolean;
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Use the `indexes` option to define SQLite indexes.
|
|
230
|
-
*
|
|
231
|
-
* Table and column names are not typed because Kysely doesn't support it.
|
|
232
|
-
*
|
|
233
|
-
* https://medium.com/@JasonWyatt/squeezing-performance-from-sqlite-indexes-indexes-c4e175f3c346
|
|
234
|
-
*
|
|
235
|
-
* ### Example
|
|
236
|
-
*
|
|
237
|
-
* ```ts
|
|
238
|
-
* import {
|
|
239
|
-
* AppName,
|
|
240
|
-
* assertTrue,
|
|
241
|
-
* createAppOwner,
|
|
242
|
-
* createEvolu,
|
|
243
|
-
* createOwnerSecret,
|
|
244
|
-
* createRandomBytes,
|
|
245
|
-
* id,
|
|
246
|
-
* } from "@evolu/common";
|
|
247
|
-
*
|
|
248
|
-
* const Schema = {
|
|
249
|
-
* todo: { id: id("Todo") },
|
|
250
|
-
* todoCategory: { id: id("TodoCategory") },
|
|
251
|
-
* };
|
|
252
|
-
* // Create once, persist the mnemonic securely, and restore it on later runs.
|
|
253
|
-
* const appOwner = createAppOwner(
|
|
254
|
-
* createOwnerSecret({ randomBytes: createRandomBytes() }),
|
|
255
|
-
* );
|
|
256
|
-
*
|
|
257
|
-
* const createTodoEvolu = createEvolu(Schema, {
|
|
258
|
-
* appName: AppName.orThrow("IndexedTodos"),
|
|
259
|
-
* appOwner,
|
|
260
|
-
* transports: [],
|
|
261
|
-
* indexes: (create) => [
|
|
262
|
-
* create("todoCreatedAt").on("todo").column("createdAt"),
|
|
263
|
-
* create("todoCategoryCreatedAt")
|
|
264
|
-
* .on("todoCategory")
|
|
265
|
-
* .column("createdAt"),
|
|
266
|
-
* ],
|
|
267
|
-
* });
|
|
268
|
-
*
|
|
269
|
-
* assertTrue(typeof createTodoEvolu === "function");
|
|
270
|
-
* ```
|
|
271
|
-
*/
|
|
272
|
-
readonly indexes?: IndexesConfig;
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Called when this instance's local database is deleted.
|
|
276
|
-
*
|
|
277
|
-
* Apps can use this to update UI immediately because the corresponding
|
|
278
|
-
* {@link Evolu} instance becomes unusable after local database deletion.
|
|
279
|
-
*/
|
|
280
|
-
readonly onDatabaseDeleted?: () => void;
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Called when local data for an {@link Owner} is deleted.
|
|
284
|
-
*
|
|
285
|
-
* Apps can use this to update UI immediately because that owner stops being
|
|
286
|
-
* used across tabs and instances.
|
|
287
|
-
*/
|
|
288
|
-
readonly onOwnerDeleted?: (owner: Owner) => void;
|
|
289
314
|
}
|
|
290
315
|
|
|
291
316
|
/**
|
|
@@ -297,6 +322,8 @@ export interface EvoluConfig {
|
|
|
297
322
|
*
|
|
298
323
|
* Uses the same safe alphabet as {@link UrlSafeString} (letters, digits, `-`,
|
|
299
324
|
* `_`) and must be between 1 and 41 characters.
|
|
325
|
+
*
|
|
326
|
+
* @group Configuration
|
|
300
327
|
*/
|
|
301
328
|
export const AppName = /*#__PURE__*/ brand(
|
|
302
329
|
"AppName",
|
|
@@ -309,16 +336,35 @@ export const AppName = /*#__PURE__*/ brand(
|
|
|
309
336
|
`The value ${JSON.stringify(error.value)} is not between 1 and 41 characters.`,
|
|
310
337
|
);
|
|
311
338
|
export type AppName = typeof AppName.Output;
|
|
339
|
+
/**
|
|
340
|
+
* Error produced when a value is not a valid {@link AppName}.
|
|
341
|
+
*
|
|
342
|
+
* @group Configuration
|
|
343
|
+
*/
|
|
312
344
|
export interface AppNameError extends TypeError<"AppName"> {
|
|
313
345
|
readonly value: UrlSafeString;
|
|
314
346
|
}
|
|
315
347
|
|
|
348
|
+
/**
|
|
349
|
+
* Stable valid {@link AppName} for tests and examples.
|
|
350
|
+
*
|
|
351
|
+
* @group Testing
|
|
352
|
+
*/
|
|
316
353
|
export const testAppName = /*#__PURE__*/ AppName.orThrow("AppName");
|
|
317
354
|
|
|
318
355
|
/**
|
|
319
|
-
*
|
|
356
|
+
* A local-first SQL database.
|
|
357
|
+
*
|
|
358
|
+
* Stores application data in SQLite on the device, so reads and writes work
|
|
359
|
+
* offline. Provides typed queries, mutations, and reactive subscriptions, with
|
|
360
|
+
* synchronization between devices. Persistent SQLite is encrypted with the
|
|
361
|
+
* {@link AppOwner} key, and synchronized data is end-to-end encrypted.
|
|
362
|
+
*
|
|
363
|
+
* Tables whose names start with `_` stay local, even when the instance
|
|
364
|
+
* synchronizes other data. Use them for device-local data such as application
|
|
365
|
+
* settings or an app-owner registry.
|
|
320
366
|
*
|
|
321
|
-
*
|
|
367
|
+
* @group Core
|
|
322
368
|
*/
|
|
323
369
|
export interface Evolu<
|
|
324
370
|
S extends EvoluSchema = EvoluSchema,
|
|
@@ -340,32 +386,29 @@ export interface Evolu<
|
|
|
340
386
|
* every row has a globally unique, conflict-free identifier without
|
|
341
387
|
* coordination.
|
|
342
388
|
*
|
|
343
|
-
* Pass `onComplete` when follow-up work must wait until the mutation
|
|
344
|
-
*
|
|
389
|
+
* Pass `onComplete` when follow-up work must wait until the mutation is
|
|
390
|
+
* stored and subscribed queries reflect it. It never runs when the database
|
|
391
|
+
* is unavailable; see {@link Evolu.loadQuery}. A stored change is not always
|
|
392
|
+
* visible; see {@link MutationOptions.onComplete}.
|
|
345
393
|
*
|
|
346
394
|
* ### Example
|
|
347
395
|
*
|
|
348
396
|
* ```ts
|
|
349
397
|
* import {
|
|
350
398
|
* assertType,
|
|
351
|
-
*
|
|
399
|
+
* type TestEvoluSchema,
|
|
352
400
|
* NonEmptyTrimmedString100,
|
|
353
401
|
* type Evolu,
|
|
402
|
+
* type TestTodoId,
|
|
354
403
|
* } from "@evolu/common";
|
|
355
404
|
*
|
|
356
|
-
* const
|
|
357
|
-
* type TodoId = typeof TodoId.Output;
|
|
358
|
-
* const Schema = {
|
|
359
|
-
* todo: { id: TodoId, title: NonEmptyTrimmedString100 },
|
|
360
|
-
* };
|
|
361
|
-
*
|
|
362
|
-
* const insertTodo = (evolu: Evolu<typeof Schema>) =>
|
|
405
|
+
* const insertTodo = (evolu: Evolu<TestEvoluSchema>) =>
|
|
363
406
|
* evolu.insert("todo", {
|
|
364
407
|
* title: NonEmptyTrimmedString100.orThrow("Learn Evolu"),
|
|
365
408
|
* }).id;
|
|
366
409
|
*
|
|
367
410
|
* const insertTodoAndNotify = (
|
|
368
|
-
* evolu: Evolu<
|
|
411
|
+
* evolu: Evolu<TestEvoluSchema>,
|
|
369
412
|
* onComplete: () => void,
|
|
370
413
|
* ) =>
|
|
371
414
|
* evolu.insert(
|
|
@@ -379,7 +422,7 @@ export interface Evolu<
|
|
|
379
422
|
* ReturnType<typeof insertTodo>,
|
|
380
423
|
* ReturnType<typeof insertTodoAndNotify>,
|
|
381
424
|
* ],
|
|
382
|
-
* [
|
|
425
|
+
* [TestTodoId, TestTodoId]
|
|
383
426
|
* >();
|
|
384
427
|
* ```
|
|
385
428
|
*
|
|
@@ -397,30 +440,30 @@ export interface Evolu<
|
|
|
397
440
|
* ```ts
|
|
398
441
|
* import {
|
|
399
442
|
* assertType,
|
|
400
|
-
*
|
|
443
|
+
* type TestEvoluSchema,
|
|
401
444
|
* NonEmptyTrimmedString100,
|
|
402
445
|
* sqliteTrue,
|
|
403
446
|
* type Evolu,
|
|
447
|
+
* type TestTodoId,
|
|
404
448
|
* } from "@evolu/common";
|
|
405
449
|
*
|
|
406
|
-
* const
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
* };
|
|
411
|
-
*
|
|
412
|
-
* const renameTodo = (evolu: Evolu<typeof Schema>, todoId: TodoId) =>
|
|
450
|
+
* const renameTodo = (
|
|
451
|
+
* evolu: Evolu<TestEvoluSchema>,
|
|
452
|
+
* todoId: TestTodoId,
|
|
453
|
+
* ) =>
|
|
413
454
|
* evolu.update("todo", {
|
|
414
455
|
* id: todoId,
|
|
415
456
|
* title: NonEmptyTrimmedString100.orThrow("Updated title"),
|
|
416
457
|
* }).id;
|
|
417
458
|
*
|
|
418
|
-
* const softDeleteTodo = (
|
|
419
|
-
* evolu
|
|
459
|
+
* const softDeleteTodo = (
|
|
460
|
+
* evolu: Evolu<TestEvoluSchema>,
|
|
461
|
+
* todoId: TestTodoId,
|
|
462
|
+
* ) => evolu.update("todo", { id: todoId, isDeleted: sqliteTrue }).id;
|
|
420
463
|
*
|
|
421
464
|
* assertType<
|
|
422
465
|
* [ReturnType<typeof renameTodo>, ReturnType<typeof softDeleteTodo>],
|
|
423
|
-
* [
|
|
466
|
+
* [TestTodoId, TestTodoId]
|
|
424
467
|
* >();
|
|
425
468
|
* ```
|
|
426
469
|
*
|
|
@@ -446,41 +489,77 @@ export interface Evolu<
|
|
|
446
489
|
* import {
|
|
447
490
|
* assertType,
|
|
448
491
|
* createIdFromString,
|
|
449
|
-
*
|
|
492
|
+
* type TestEvoluSchema,
|
|
450
493
|
* NonEmptyTrimmedString100,
|
|
451
494
|
* type Evolu,
|
|
495
|
+
* TestTodoId,
|
|
452
496
|
* } from "@evolu/common";
|
|
453
497
|
*
|
|
454
|
-
* const
|
|
455
|
-
*
|
|
456
|
-
* const Schema = {
|
|
457
|
-
* todo: { id: TodoId, title: NonEmptyTrimmedString100 },
|
|
458
|
-
* };
|
|
459
|
-
* const stableId = TodoId.orThrow(createIdFromString("my-todo-1"));
|
|
460
|
-
* const upsertTodo = (evolu: Evolu<typeof Schema>) =>
|
|
498
|
+
* const stableId = TestTodoId.orThrow(createIdFromString("my-todo-1"));
|
|
499
|
+
* const upsertTodo = (evolu: Evolu<TestEvoluSchema>) =>
|
|
461
500
|
* evolu.upsert("todo", {
|
|
462
501
|
* id: stableId,
|
|
463
502
|
* title: NonEmptyTrimmedString100.orThrow("Learn Evolu"),
|
|
464
503
|
* }).id;
|
|
465
504
|
*
|
|
466
|
-
* assertType<ReturnType<typeof upsertTodo>,
|
|
505
|
+
* assertType<ReturnType<typeof upsertTodo>, TestTodoId>();
|
|
467
506
|
* ```
|
|
468
507
|
*
|
|
469
508
|
* @see {@link Mutation}
|
|
470
509
|
*/
|
|
471
510
|
readonly upsert: Mutation<S, "upsert">;
|
|
472
511
|
|
|
512
|
+
/**
|
|
513
|
+
* Returns the size of a mutation of `table` with `values`, measured as
|
|
514
|
+
* {@link maxMutationSize} describes.
|
|
515
|
+
*
|
|
516
|
+
* Use it to check values that column Types do not bound before mutating, or
|
|
517
|
+
* to show how much of the limit a mutation uses. It accepts any of the
|
|
518
|
+
* table's columns, so the values of an insert, update, or upsert fit, and it
|
|
519
|
+
* ignores `id` and `isDeleted`, which are not columns. Mutations of
|
|
520
|
+
* local-only tables are measured too, although they are exempt from the
|
|
521
|
+
* limit.
|
|
522
|
+
*
|
|
523
|
+
* ### Example
|
|
524
|
+
*
|
|
525
|
+
* ```ts
|
|
526
|
+
* import {
|
|
527
|
+
* assertType,
|
|
528
|
+
* type Evolu,
|
|
529
|
+
* maxMutationSize,
|
|
530
|
+
* type NonEmptyTrimmedString100,
|
|
531
|
+
* type TestEvoluSchema,
|
|
532
|
+
* } from "@evolu/common";
|
|
533
|
+
*
|
|
534
|
+
* const fitsTodo = (
|
|
535
|
+
* evolu: Evolu<TestEvoluSchema>,
|
|
536
|
+
* title: NonEmptyTrimmedString100,
|
|
537
|
+
* ) => evolu.getMutationSize("todo", { title }) <= maxMutationSize;
|
|
538
|
+
*
|
|
539
|
+
* assertType<ReturnType<typeof fitsTodo>, boolean>();
|
|
540
|
+
* ```
|
|
541
|
+
*/
|
|
542
|
+
readonly getMutationSize: <TableName extends keyof S>(
|
|
543
|
+
table: TableName,
|
|
544
|
+
values: Partial<MutationValues<S[TableName], "update">>,
|
|
545
|
+
) => PositiveInt;
|
|
546
|
+
|
|
473
547
|
/**
|
|
474
548
|
* Load {@link Query} and return a promise with {@link QueryRows}.
|
|
475
549
|
*
|
|
476
|
-
* The returned promise always resolves successfully because
|
|
477
|
-
*
|
|
478
|
-
*
|
|
550
|
+
* The returned promise always resolves successfully because all data are
|
|
551
|
+
* local and the query is typed. If the database refuses startup, unanswered
|
|
552
|
+
* loads stay pending until disposal resolves them with empty rows. Observe
|
|
553
|
+
* {@link EvoluErrorDep.evoluError} to display the refusal independently of
|
|
554
|
+
* query loading.
|
|
479
555
|
*
|
|
480
556
|
* Loading is batched. Returned promises are cached while pending and can be
|
|
481
|
-
* reused after fulfillment
|
|
482
|
-
*
|
|
483
|
-
*
|
|
557
|
+
* reused after fulfillment, which prevents redundant database queries and
|
|
558
|
+
* supports React Suspense (stable references while pending). A mutation or
|
|
559
|
+
* incoming sync invalidates the cache of an unsubscribed query, so its next
|
|
560
|
+
* load reads again. A subscribed query keeps its cached rows and the
|
|
561
|
+
* subscription refreshes them, so a load can return rows that a pending
|
|
562
|
+
* refresh is about to replace.
|
|
484
563
|
*
|
|
485
564
|
* To subscribe a query for automatic updates, use
|
|
486
565
|
* {@link Evolu.subscribeQuery}.
|
|
@@ -491,20 +570,17 @@ export interface Evolu<
|
|
|
491
570
|
* import {
|
|
492
571
|
* assertType,
|
|
493
572
|
* createQueryBuilder,
|
|
494
|
-
*
|
|
495
|
-
*
|
|
573
|
+
* testEvoluSchema,
|
|
574
|
+
* type TestEvoluSchema,
|
|
496
575
|
* type Evolu,
|
|
497
576
|
* type QueryRows,
|
|
498
577
|
* } from "@evolu/common";
|
|
499
578
|
*
|
|
500
|
-
* const
|
|
501
|
-
* todo: { id: id("Todo"), title: NonEmptyTrimmedString100 },
|
|
502
|
-
* };
|
|
503
|
-
* const createQuery = createQueryBuilder(Schema);
|
|
579
|
+
* const createQuery = createQueryBuilder(testEvoluSchema);
|
|
504
580
|
* const allTodos = createQuery((db) =>
|
|
505
581
|
* db.selectFrom("todo").selectAll(),
|
|
506
582
|
* );
|
|
507
|
-
* const loadTodos = async (evolu: Evolu<
|
|
583
|
+
* const loadTodos = async (evolu: Evolu<TestEvoluSchema>) => {
|
|
508
584
|
* const rows = await evolu.loadQuery(allTodos);
|
|
509
585
|
* return rows;
|
|
510
586
|
* };
|
|
@@ -529,31 +605,22 @@ export interface Evolu<
|
|
|
529
605
|
* ```ts
|
|
530
606
|
* import {
|
|
531
607
|
* assertType,
|
|
532
|
-
* createIdFromString,
|
|
533
608
|
* createQueryBuilder,
|
|
534
|
-
*
|
|
535
|
-
*
|
|
609
|
+
* testEvoluSchema,
|
|
610
|
+
* type TestEvoluSchema,
|
|
611
|
+
* testTodoId,
|
|
536
612
|
* type Evolu,
|
|
537
613
|
* type QueryRows,
|
|
538
614
|
* } from "@evolu/common";
|
|
539
615
|
*
|
|
540
|
-
* const
|
|
541
|
-
* type TodoId = typeof TodoId.Output;
|
|
542
|
-
* const Schema = {
|
|
543
|
-
* todo: { id: TodoId, title: NonEmptyTrimmedString100 },
|
|
544
|
-
* };
|
|
545
|
-
* const createQuery = createQueryBuilder(Schema);
|
|
616
|
+
* const createQuery = createQueryBuilder(testEvoluSchema);
|
|
546
617
|
* const allTodos = createQuery((db) =>
|
|
547
618
|
* db.selectFrom("todo").select(["id", "title"]),
|
|
548
619
|
* );
|
|
549
|
-
* const
|
|
550
|
-
*
|
|
551
|
-
* db.selectFrom("todo").select("title").where("id", "=", todoId),
|
|
552
|
-
* );
|
|
553
|
-
* const firstTodo = todoById(
|
|
554
|
-
* TodoId.orThrow(createIdFromString("first-todo")),
|
|
620
|
+
* const firstTodo = createQuery((db) =>
|
|
621
|
+
* db.selectFrom("todo").select("title").where("id", "=", testTodoId),
|
|
555
622
|
* );
|
|
556
|
-
* const loadTodoQueries = (evolu: Evolu<
|
|
623
|
+
* const loadTodoQueries = (evolu: Evolu<TestEvoluSchema>) =>
|
|
557
624
|
* evolu.loadQueries([allTodos, firstTodo]);
|
|
558
625
|
*
|
|
559
626
|
* assertType<
|
|
@@ -572,28 +639,28 @@ export interface Evolu<
|
|
|
572
639
|
/**
|
|
573
640
|
* Subscribe to {@link Query} {@link QueryRows} changes.
|
|
574
641
|
*
|
|
642
|
+
* Cached rows invalidated before subscription are refreshed. Subscribing
|
|
643
|
+
* alone does not load a query that has no cached rows.
|
|
644
|
+
*
|
|
575
645
|
* ### Example
|
|
576
646
|
*
|
|
577
647
|
* ```ts
|
|
578
648
|
* import {
|
|
579
649
|
* assertType,
|
|
580
650
|
* createQueryBuilder,
|
|
581
|
-
*
|
|
582
|
-
*
|
|
651
|
+
* testEvoluSchema,
|
|
652
|
+
* type TestEvoluSchema,
|
|
583
653
|
* type Evolu,
|
|
584
654
|
* type QueryRows,
|
|
585
655
|
* type Unsubscribe,
|
|
586
656
|
* } from "@evolu/common";
|
|
587
657
|
*
|
|
588
|
-
* const
|
|
589
|
-
* todo: { id: id("Todo"), title: NonEmptyTrimmedString100 },
|
|
590
|
-
* };
|
|
591
|
-
* const createQuery = createQueryBuilder(Schema);
|
|
658
|
+
* const createQuery = createQueryBuilder(testEvoluSchema);
|
|
592
659
|
* const allTodos = createQuery((db) =>
|
|
593
660
|
* db.selectFrom("todo").select("title"),
|
|
594
661
|
* );
|
|
595
662
|
* const subscribeToTodos = (
|
|
596
|
-
* evolu: Evolu<
|
|
663
|
+
* evolu: Evolu<TestEvoluSchema>,
|
|
597
664
|
* onRows: (rows: QueryRows<typeof allTodos.Row>) => void,
|
|
598
665
|
* ) =>
|
|
599
666
|
* evolu.subscribeQuery(allTodos)(() => {
|
|
@@ -616,20 +683,17 @@ export interface Evolu<
|
|
|
616
683
|
* import {
|
|
617
684
|
* assertType,
|
|
618
685
|
* createQueryBuilder,
|
|
619
|
-
*
|
|
620
|
-
*
|
|
686
|
+
* testEvoluSchema,
|
|
687
|
+
* type TestEvoluSchema,
|
|
621
688
|
* type Evolu,
|
|
622
689
|
* type QueryRows,
|
|
623
690
|
* } from "@evolu/common";
|
|
624
691
|
*
|
|
625
|
-
* const
|
|
626
|
-
* todo: { id: id("Todo"), title: NonEmptyTrimmedString100 },
|
|
627
|
-
* };
|
|
628
|
-
* const createQuery = createQueryBuilder(Schema);
|
|
692
|
+
* const createQuery = createQueryBuilder(testEvoluSchema);
|
|
629
693
|
* const allTodos = createQuery((db) =>
|
|
630
694
|
* db.selectFrom("todo").select("title"),
|
|
631
695
|
* );
|
|
632
|
-
* const getTodos = (evolu: Evolu<
|
|
696
|
+
* const getTodos = (evolu: Evolu<TestEvoluSchema>) =>
|
|
633
697
|
* evolu.getQueryRows(allTodos);
|
|
634
698
|
*
|
|
635
699
|
* assertType<
|
|
@@ -647,7 +711,8 @@ export interface Evolu<
|
|
|
647
711
|
* of starting parallel exports.
|
|
648
712
|
*
|
|
649
713
|
* The pending promise rejects if this {@link Evolu} instance is disposed
|
|
650
|
-
* before export completion.
|
|
714
|
+
* before export completion. If the database refuses startup, export stays
|
|
715
|
+
* pending until disposal; see {@link Evolu.loadQuery}.
|
|
651
716
|
*/
|
|
652
717
|
readonly exportDatabase: () => Promise<Uint8Array<ArrayBuffer>>;
|
|
653
718
|
|
|
@@ -703,21 +768,15 @@ export interface Evolu<
|
|
|
703
768
|
* ```ts
|
|
704
769
|
* import {
|
|
705
770
|
* assertEqual,
|
|
706
|
-
* createAppOwner,
|
|
707
771
|
* createOwnerWebSocketTransport,
|
|
708
|
-
* createOwnerSecret,
|
|
709
|
-
* createRandomBytes,
|
|
710
772
|
* deriveShardOwner,
|
|
773
|
+
* testAppOwner,
|
|
711
774
|
* type Evolu,
|
|
712
775
|
* type ReadonlyOwner,
|
|
713
776
|
* type UnuseOwner,
|
|
714
777
|
* } from "@evolu/common";
|
|
715
778
|
*
|
|
716
|
-
*
|
|
717
|
-
* const appOwner = createAppOwner(
|
|
718
|
-
* createOwnerSecret({ randomBytes: createRandomBytes() }),
|
|
719
|
-
* );
|
|
720
|
-
* const shardOwner = deriveShardOwner(appOwner, ["todos", 1]);
|
|
779
|
+
* const shardOwner = deriveShardOwner(testAppOwner, ["todos", 1]);
|
|
721
780
|
* const shardTransport = createOwnerWebSocketTransport({
|
|
722
781
|
* url: "wss://relay.example.com",
|
|
723
782
|
* ownerId: shardOwner.id,
|
|
@@ -753,17 +812,168 @@ export interface Evolu<
|
|
|
753
812
|
owner: ReadonlyOwner | Owner,
|
|
754
813
|
transports?: NonEmptyReadonlyArray<OwnerTransport>,
|
|
755
814
|
) => UnuseOwner;
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Requests a new synchronization round for an active {@link OwnerId}.
|
|
818
|
+
*
|
|
819
|
+
* Reconciles locally stored changes, including writes previously rejected
|
|
820
|
+
* with {@link ProtocolQuotaError}, through the owner's active transports. Call
|
|
821
|
+
* this after the relay provider confirms additional quota is available.
|
|
822
|
+
* Existing connections and {@link Evolu.useOwner} registrations are retained,
|
|
823
|
+
* including registrations shared by multiple instances or tabs.
|
|
824
|
+
*
|
|
825
|
+
* The owner must have an active writable registration in this database.
|
|
826
|
+
* Unregistered or read-only owners are ignored. Requests are skipped while
|
|
827
|
+
* all of the owner's transports are closed; they synchronize when they
|
|
828
|
+
* reopen. Disposal drops locally buffered requests. Calling a disposed
|
|
829
|
+
* instance throws, like other Evolu operations.
|
|
830
|
+
*
|
|
831
|
+
* Returns immediately, without waiting for synchronization to complete.
|
|
832
|
+
* Errors are reported through {@link EvoluErrorDep.evoluError}.
|
|
833
|
+
*
|
|
834
|
+
* ### Example
|
|
835
|
+
*
|
|
836
|
+
* ```ts
|
|
837
|
+
* import { assertType, type Evolu, type OwnerId } from "@evolu/common";
|
|
838
|
+
*
|
|
839
|
+
* // Call after successfully increasing the affected owner's relay quota.
|
|
840
|
+
* const onQuotaIncreased = (evolu: Evolu, ownerId: OwnerId) => {
|
|
841
|
+
* evolu.requestSync(ownerId);
|
|
842
|
+
* };
|
|
843
|
+
*
|
|
844
|
+
* assertType<
|
|
845
|
+
* typeof onQuotaIncreased,
|
|
846
|
+
* (evolu: Evolu, ownerId: OwnerId) => void
|
|
847
|
+
* >();
|
|
848
|
+
* ```
|
|
849
|
+
*/
|
|
850
|
+
readonly requestSync: (ownerId: OwnerId) => void;
|
|
756
851
|
}
|
|
757
852
|
|
|
758
|
-
/**
|
|
853
|
+
/**
|
|
854
|
+
* Function returned by {@link Evolu.useOwner} to stop using an Owner for sync.
|
|
855
|
+
*
|
|
856
|
+
* @group Core
|
|
857
|
+
*/
|
|
759
858
|
export type UnuseOwner = () => void;
|
|
760
859
|
|
|
860
|
+
/**
|
|
861
|
+
* Represents errors that can occur in {@link Evolu}.
|
|
862
|
+
*
|
|
863
|
+
* Apps show them from {@link EvoluErrorDep.evoluError}.
|
|
864
|
+
*
|
|
865
|
+
* An error that leaves the app unusable deserves a modal dialog, which moves
|
|
866
|
+
* focus into itself and restores it when closed. Any other error is a status
|
|
867
|
+
* message: show it in a region with `role="alert"`, which screen readers
|
|
868
|
+
* announce without moving focus, so the user keeps working and a background tab
|
|
869
|
+
* shows it when the user returns. Avoid `alert()`, which blocks the page, once
|
|
870
|
+
* in every open tab.
|
|
871
|
+
*
|
|
872
|
+
* - {@link UnsupportedDbVersionError} blocks the app: the local data needs a newer
|
|
873
|
+
* version of it. Ask the user to update the app or close all its tabs.
|
|
874
|
+
* - {@link OtherBuildRunningError} blocks the app while it lasts: another version
|
|
875
|
+
* of the app holds the local data. Ask the user to close the app's other
|
|
876
|
+
* tabs. It clears when the wait ends.
|
|
877
|
+
* - {@link ProtocolError} does not block the app: sync with a relay failed for an
|
|
878
|
+
* owner, because the relay rejected or failed a request, or sent data that
|
|
879
|
+
* could not be decoded or verified; sync state shows the affected routes. A
|
|
880
|
+
* {@link ProtocolQuotaError} needs more relay quota, then
|
|
881
|
+
* {@link Evolu.requestSync}; a {@link ProtocolVersionError} needs an app or
|
|
882
|
+
* relay update.
|
|
883
|
+
* - {@link StorageQuotaError} does not block the app: a storage or billing quota
|
|
884
|
+
* was exceeded, so a batch of an owner's changes was not stored. The built-in
|
|
885
|
+
* client storage does not report it yet; a relay's quota arrives as
|
|
886
|
+
* {@link ProtocolQuotaError}.
|
|
887
|
+
* - {@link DecryptWithXChaCha20Poly1305Error} does not block the app: changes
|
|
888
|
+
* received for an owner could not be decrypted, so none of their batch was
|
|
889
|
+
* stored.
|
|
890
|
+
* - {@link UnknownError} does not block the app: Evolu logged an unexpected
|
|
891
|
+
* failure. Show a generic message.
|
|
892
|
+
*
|
|
893
|
+
* @group Core
|
|
894
|
+
*/
|
|
895
|
+
export type EvoluError =
|
|
896
|
+
| DecryptWithXChaCha20Poly1305Error
|
|
897
|
+
| OtherBuildRunningError
|
|
898
|
+
| ProtocolError
|
|
899
|
+
| StorageQuotaError
|
|
900
|
+
| UnknownError
|
|
901
|
+
| UnsupportedDbVersionError;
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* The largest {@link Mutation}, in bytes.
|
|
905
|
+
*
|
|
906
|
+
* A mutation's size is its change as encoded for sync: the table name, the ID,
|
|
907
|
+
* and every column name and value, with a few bytes of overhead. A string takes
|
|
908
|
+
* at most three bytes per UTF-16 code unit. Every mutation within the limit
|
|
909
|
+
* fits one protocol message, so it can always sync. A larger mutation throws
|
|
910
|
+
* and is not saved. Mutations of local-only tables are exempt, because they
|
|
911
|
+
* never sync. Measure a mutation in advance with {@link Evolu.getMutationSize}.
|
|
912
|
+
*
|
|
913
|
+
* @group Core
|
|
914
|
+
*/
|
|
915
|
+
export const maxMutationSize: PositiveInt =
|
|
916
|
+
/*#__PURE__*/ PositiveInt.orThrow(640_000);
|
|
917
|
+
|
|
918
|
+
/** Measures a mutation as {@link maxMutationSize} describes. */
|
|
919
|
+
const measureMutation = (
|
|
920
|
+
table: string,
|
|
921
|
+
values: ReadonlyRecord<string, SqliteValue | undefined>,
|
|
922
|
+
): PositiveInt => {
|
|
923
|
+
const columns = createMutableRecord<string, SqliteValue>();
|
|
924
|
+
for (const [column, value] of objectToEntries(values)) {
|
|
925
|
+
if (value !== undefined && column !== "id" && column !== "isDeleted") {
|
|
926
|
+
columns[column] = value;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Measuring with the protocol's encoding keeps the limit and the message size
|
|
931
|
+
// from disagreeing. A separate formula would save the tab about 3 KB
|
|
932
|
+
// compressed, but it would be a second encoding to keep in sync, and it
|
|
933
|
+
// would have to overcount.
|
|
934
|
+
const buffer = createBuffer();
|
|
935
|
+
encodeDbChange(buffer, {
|
|
936
|
+
// Every timestamp and ID takes 16 bytes, and the flags always take one.
|
|
937
|
+
timestamp: createTimestamp(),
|
|
938
|
+
change: DbChange.orThrow({
|
|
939
|
+
table,
|
|
940
|
+
id: mutationSizeId,
|
|
941
|
+
values: columns,
|
|
942
|
+
isInsert: true,
|
|
943
|
+
isDelete: null,
|
|
944
|
+
}),
|
|
945
|
+
});
|
|
946
|
+
return buffer.getLength() as PositiveInt;
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
const mutationSizeId = /*#__PURE__*/ Id.orThrow("A".repeat(22));
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Dependency wrapper for the shared {@link EvoluError} store.
|
|
953
|
+
*
|
|
954
|
+
* @group Construction
|
|
955
|
+
*/
|
|
761
956
|
export interface EvoluErrorDep {
|
|
762
957
|
/**
|
|
763
958
|
* {@link ReadonlyStore} of {@link EvoluError} shared by all {@link Evolu}
|
|
764
959
|
* instances created from the same {@link createEvoluDeps} result.
|
|
765
960
|
*
|
|
961
|
+
* Starts at `null` and otherwise holds the latest reported error until the
|
|
962
|
+
* first {@link UnsupportedDbVersionError}. That refusal remains for the
|
|
963
|
+
* lifetime of these dependencies, even if a tenant is disposed and recreated.
|
|
964
|
+
* Later errors are still logged but do not replace it or notify this store's
|
|
965
|
+
* subscribers. Fresh dependencies start with a fresh error store. An
|
|
966
|
+
* {@link OtherBuildRunningError} reports a wait, so the store returns to
|
|
967
|
+
* `null` when the wait ends, unless another error replaced it.
|
|
968
|
+
*
|
|
766
969
|
* Subscribe once to show user-facing error messages across all instances.
|
|
970
|
+
* While a refused database's tenant remains alive, the SharedWorker sends the
|
|
971
|
+
* refusal to each tab once, including tabs that connect later, and starts no
|
|
972
|
+
* replacement database workers. After all instances release that tenant and
|
|
973
|
+
* it is disposed when idle, creating another instance retries startup and may
|
|
974
|
+
* send the refusal again. On the web, a refused tab first reloads once
|
|
975
|
+
* instead; see {@link UnsupportedDbVersionError}. Show that blocking message
|
|
976
|
+
* outside any query-loading boundary, so pending queries do not hide it.
|
|
767
977
|
*
|
|
768
978
|
* ### Example
|
|
769
979
|
*
|
|
@@ -771,60 +981,113 @@ export interface EvoluErrorDep {
|
|
|
771
981
|
* import {
|
|
772
982
|
* assertEqual,
|
|
773
983
|
* createStore,
|
|
774
|
-
* Millis,
|
|
775
984
|
* type EvoluError,
|
|
776
985
|
* } from "@evolu/common";
|
|
777
986
|
* import type { EvoluErrorDep } from "@evolu/common/local-first";
|
|
778
987
|
*
|
|
988
|
+
* // The message for the current error, or null for none.
|
|
989
|
+
* const errorMessage = (error: EvoluError | null): string | null => {
|
|
990
|
+
* if (!error) return null;
|
|
991
|
+
* // oxlint-disable-next-line typescript/switch-exhaustiveness-check -- The default handles every other EvoluError.
|
|
992
|
+
* switch (error.type) {
|
|
993
|
+
* case "UnsupportedDbVersionError":
|
|
994
|
+
* return "Your data requires a newer version of this app. Please update it.";
|
|
995
|
+
* case "OtherBuildRunningError":
|
|
996
|
+
* return "This app is open in another tab with a different version. Close that tab to continue.";
|
|
997
|
+
* default:
|
|
998
|
+
* return "Something went wrong. Please try again.";
|
|
999
|
+
* }
|
|
1000
|
+
* };
|
|
1001
|
+
*
|
|
779
1002
|
* // Stand-in for run.deps.evoluError from createEvoluDeps.
|
|
780
1003
|
* using evoluError = createStore<EvoluError | null>(null);
|
|
781
1004
|
* const deps = { evoluError } satisfies EvoluErrorDep;
|
|
782
|
-
*
|
|
783
|
-
* const
|
|
784
|
-
* displayedMessage = message;
|
|
785
|
-
* };
|
|
1005
|
+
* // What the app showed over time; null hides the message.
|
|
1006
|
+
* const shown: Array<string | null> = [];
|
|
786
1007
|
*
|
|
787
1008
|
* deps.evoluError.subscribe(() => {
|
|
788
|
-
*
|
|
789
|
-
* if (!error) return;
|
|
790
|
-
*
|
|
791
|
-
* // oxlint-disable-next-line typescript/switch-exhaustiveness-check -- The default intentionally handles every other EvoluError.
|
|
792
|
-
* switch (error.type) {
|
|
793
|
-
* case "TimestampDriftError":
|
|
794
|
-
* // Show guidance specific to the detected error.
|
|
795
|
-
* showMessage(
|
|
796
|
-
* "Your system clock appears incorrect. Please fix it.",
|
|
797
|
-
* );
|
|
798
|
-
* break;
|
|
799
|
-
* default:
|
|
800
|
-
* // Show a generic user message for other operational errors.
|
|
801
|
-
* showMessage("Something went wrong. Please try again.");
|
|
802
|
-
* }
|
|
1009
|
+
* shown.push(errorMessage(deps.evoluError.get()));
|
|
803
1010
|
* });
|
|
804
1011
|
*
|
|
805
|
-
*
|
|
806
|
-
*
|
|
807
|
-
*
|
|
808
|
-
*
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
*
|
|
812
|
-
*
|
|
813
|
-
* );
|
|
1012
|
+
* // Another version keeps this tab waiting, then the wait ends.
|
|
1013
|
+
* deps.evoluError.set({ type: "OtherBuildRunningError" });
|
|
1014
|
+
* deps.evoluError.set(null);
|
|
1015
|
+
*
|
|
1016
|
+
* assertEqual(shown, [
|
|
1017
|
+
* "This app is open in another tab with a different version. Close that tab to continue.",
|
|
1018
|
+
* null,
|
|
1019
|
+
* ]);
|
|
814
1020
|
* ```
|
|
815
1021
|
*/
|
|
816
1022
|
readonly evoluError: ReadonlyStore<EvoluError | null>;
|
|
817
1023
|
}
|
|
818
1024
|
|
|
1025
|
+
/**
|
|
1026
|
+
* Dependency wrapper for the shared {@link SyncState} store.
|
|
1027
|
+
*
|
|
1028
|
+
* @group Construction
|
|
1029
|
+
*/
|
|
1030
|
+
export interface SyncStateDep {
|
|
1031
|
+
/**
|
|
1032
|
+
* {@link ReadonlyStore} of the latest {@link SyncState} shared by all
|
|
1033
|
+
* {@link Evolu} instances, or null before the shared worker sends its first
|
|
1034
|
+
* snapshot. Derive what to show from it, such as one indicator per relay, or
|
|
1035
|
+
* use {@link syncStateToOwnerSyncStates} for one state per owner.
|
|
1036
|
+
*
|
|
1037
|
+
* ### Example
|
|
1038
|
+
*
|
|
1039
|
+
* ```ts
|
|
1040
|
+
* import {
|
|
1041
|
+
* assertEqual,
|
|
1042
|
+
* createId,
|
|
1043
|
+
* createStore,
|
|
1044
|
+
* testCreateDeps,
|
|
1045
|
+
* } from "@evolu/common";
|
|
1046
|
+
* import type {
|
|
1047
|
+
* SyncState,
|
|
1048
|
+
* SyncStateDep,
|
|
1049
|
+
* } from "@evolu/common/local-first";
|
|
1050
|
+
*
|
|
1051
|
+
* const openRelayLabels = (deps: SyncStateDep): ReadonlyArray<string> =>
|
|
1052
|
+
* (deps.syncState.get()?.transports ?? [])
|
|
1053
|
+
* .filter(({ readyState }) => readyState === "open")
|
|
1054
|
+
* .map(({ label }) => label);
|
|
1055
|
+
*
|
|
1056
|
+
* using syncState = createStore<SyncState | null>(null);
|
|
1057
|
+
* assertEqual(openRelayLabels({ syncState }), []);
|
|
1058
|
+
*
|
|
1059
|
+
* const deps = testCreateDeps();
|
|
1060
|
+
* syncState.set({
|
|
1061
|
+
* transports: [
|
|
1062
|
+
* {
|
|
1063
|
+
* id: createId<"SyncTransport">(deps),
|
|
1064
|
+
* label: "wss://relay.example",
|
|
1065
|
+
* readyState: "open",
|
|
1066
|
+
* openedAt: null,
|
|
1067
|
+
* closedAt: null,
|
|
1068
|
+
* error: null,
|
|
1069
|
+
* },
|
|
1070
|
+
* ],
|
|
1071
|
+
* tenants: [],
|
|
1072
|
+
* });
|
|
1073
|
+
* assertEqual(openRelayLabels({ syncState }), ["wss://relay.example"]);
|
|
1074
|
+
* ```
|
|
1075
|
+
*/
|
|
1076
|
+
readonly syncState: ReadonlyStore<SyncState | null>;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
819
1079
|
/**
|
|
820
1080
|
* Shared platform dependencies for creating {@link Evolu} instances.
|
|
821
1081
|
*
|
|
822
1082
|
* Includes platform adapters, the shared {@link EvoluErrorDep.evoluError} store,
|
|
823
1083
|
* and disposal for owned resources.
|
|
1084
|
+
*
|
|
1085
|
+
* @group Construction
|
|
824
1086
|
*/
|
|
825
1087
|
export type EvoluDeps = EvoluPlatformDeps &
|
|
826
1088
|
ConsoleDep &
|
|
827
1089
|
EvoluErrorDep &
|
|
1090
|
+
SyncStateDep &
|
|
828
1091
|
Disposable;
|
|
829
1092
|
|
|
830
1093
|
/**
|
|
@@ -832,6 +1095,8 @@ export type EvoluDeps = EvoluPlatformDeps &
|
|
|
832
1095
|
*
|
|
833
1096
|
* Provides worker and channel adapters plus optional platform integrations for
|
|
834
1097
|
* logging and synchronous UI flush.
|
|
1098
|
+
*
|
|
1099
|
+
* @group Construction
|
|
835
1100
|
*/
|
|
836
1101
|
export type EvoluPlatformDeps = CreateDbWorkerDep &
|
|
837
1102
|
CreateBroadcastChannelDep &
|
|
@@ -848,36 +1113,50 @@ export type EvoluPlatformDeps = CreateDbWorkerDep &
|
|
|
848
1113
|
*
|
|
849
1114
|
* Call this once per platform and reuse the returned deps when creating
|
|
850
1115
|
* multiple Evolu instances. The returned deps object owns long-lived resources
|
|
851
|
-
* such as worker channels and the shared {@link EvoluErrorDep.evoluError}
|
|
852
|
-
*
|
|
1116
|
+
* such as worker channels and the shared {@link EvoluErrorDep.evoluError} and
|
|
1117
|
+
* {@link SyncStateDep.syncState} stores.
|
|
853
1118
|
*
|
|
854
1119
|
* Dispose it only during app shutdown.
|
|
1120
|
+
*
|
|
1121
|
+
* @group Construction
|
|
855
1122
|
*/
|
|
856
1123
|
export const createEvoluDeps = (deps: EvoluPlatformDeps): EvoluDeps => {
|
|
857
1124
|
const { createBroadcastChannel, sharedWorker } = deps;
|
|
858
1125
|
const console = deps.console ?? createConsole();
|
|
1126
|
+
// Opened when the worker connects.
|
|
1127
|
+
let syncStateBroadcastChannel: BroadcastChannel<SyncState> | null = null;
|
|
1128
|
+
let tabLeaderElection: Disposable | null = null;
|
|
859
1129
|
|
|
860
1130
|
using disposer = new DisposableStack();
|
|
861
1131
|
disposer.use(sharedWorker);
|
|
862
1132
|
const evoluError = disposer.use(createStore<EvoluError | null>(null));
|
|
1133
|
+
const syncState = disposer.use(createStore<SyncState | null>(null));
|
|
1134
|
+
disposer.defer(() => {
|
|
1135
|
+
syncStateBroadcastChannel?.[Symbol.dispose]();
|
|
1136
|
+
});
|
|
863
1137
|
|
|
864
1138
|
const consoleEntryOrErrorBroadcastChannel = disposer.use(
|
|
865
1139
|
createBroadcastChannel<ConsoleEntryOrError>(
|
|
866
1140
|
consoleEntryOrErrorBroadcastChannelName,
|
|
867
1141
|
),
|
|
868
1142
|
);
|
|
1143
|
+
const setEvoluError = (error: EvoluError): void => {
|
|
1144
|
+
if (evoluError.get()?.type === "UnsupportedDbVersionError") return;
|
|
1145
|
+
evoluError.set(error);
|
|
1146
|
+
};
|
|
1147
|
+
|
|
869
1148
|
consoleEntryOrErrorBroadcastChannel.onMessage = (message) => {
|
|
870
1149
|
switch (message.type) {
|
|
871
1150
|
case "ConsoleEntry":
|
|
872
1151
|
console.write(message.entry);
|
|
873
1152
|
// Fallback channel for unexpected errors without EvoluError typing.
|
|
874
1153
|
if (message.entry.method === "error") {
|
|
875
|
-
|
|
1154
|
+
setEvoluError(createUnknownError(message.entry.args));
|
|
876
1155
|
}
|
|
877
1156
|
break;
|
|
878
1157
|
|
|
879
1158
|
case "Error":
|
|
880
|
-
|
|
1159
|
+
setEvoluError(message.error);
|
|
881
1160
|
// Keep typed errors visible in logs as operational failures.
|
|
882
1161
|
console.error(message.error);
|
|
883
1162
|
break;
|
|
@@ -888,23 +1167,69 @@ export const createEvoluDeps = (deps: EvoluPlatformDeps): EvoluDeps => {
|
|
|
888
1167
|
};
|
|
889
1168
|
|
|
890
1169
|
sharedWorker.port.onMessage = (message) => {
|
|
891
|
-
|
|
1170
|
+
switch (message.type) {
|
|
1171
|
+
case "DbWorkerInit":
|
|
1172
|
+
deps.createDbWorker().postMessage(message, [message.port]);
|
|
1173
|
+
break;
|
|
1174
|
+
|
|
1175
|
+
case "Error":
|
|
1176
|
+
// Sent to this tab only: its database refused startup, or its worker
|
|
1177
|
+
// still waits for another build.
|
|
1178
|
+
setEvoluError(message.error);
|
|
1179
|
+
console.error(message.error);
|
|
1180
|
+
break;
|
|
1181
|
+
|
|
1182
|
+
case "Waiting":
|
|
1183
|
+
case "StorageUnavailable":
|
|
1184
|
+
// Platform adapters act on these; see Builds and Storage in the Shared
|
|
1185
|
+
// module.
|
|
1186
|
+
break;
|
|
1187
|
+
|
|
1188
|
+
case "Connected": {
|
|
1189
|
+
assert(!syncStateBroadcastChannel, "The shared worker connects once.");
|
|
1190
|
+
// The wait the error reported is over.
|
|
1191
|
+
if (evoluError.get()?.type === "OtherBuildRunningError") {
|
|
1192
|
+
evoluError.set(null);
|
|
1193
|
+
}
|
|
1194
|
+
// The worker is the channel's only sender, and one sender's messages
|
|
1195
|
+
// arrive in order, so the last one is current.
|
|
1196
|
+
syncStateBroadcastChannel = createBroadcastChannel<SyncState>(
|
|
1197
|
+
message.syncStateChannelName,
|
|
1198
|
+
);
|
|
1199
|
+
syncStateBroadcastChannel.onMessage = (state) => {
|
|
1200
|
+
syncState.set(state);
|
|
1201
|
+
};
|
|
1202
|
+
// Asking only after listening misses no snapshot.
|
|
1203
|
+
sharedWorker.port.postMessage({ type: "RequestSyncState" });
|
|
1204
|
+
// Only this worker's tabs compete, so a tab of another worker never
|
|
1205
|
+
// hosts its DbWorkers.
|
|
1206
|
+
tabLeaderElection = acquireLeaderLockCallback(deps)(
|
|
1207
|
+
`tab-${message.workerId}`,
|
|
1208
|
+
() => {
|
|
1209
|
+
sharedWorker.port.postMessage({
|
|
1210
|
+
type: "AnnounceTabLeader",
|
|
1211
|
+
consoleLevel: console.getLevel(),
|
|
1212
|
+
});
|
|
1213
|
+
},
|
|
1214
|
+
);
|
|
1215
|
+
break;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
default:
|
|
1219
|
+
exhaustiveCheck(message);
|
|
1220
|
+
}
|
|
892
1221
|
};
|
|
893
1222
|
|
|
894
|
-
disposer.
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
type: "AnnounceTabLeader",
|
|
898
|
-
consoleLevel: console.getLevel(),
|
|
899
|
-
});
|
|
900
|
-
}),
|
|
901
|
-
);
|
|
1223
|
+
disposer.defer(() => {
|
|
1224
|
+
tabLeaderElection?.[Symbol.dispose]();
|
|
1225
|
+
});
|
|
902
1226
|
|
|
903
1227
|
return disposable<EvoluDeps>(
|
|
904
1228
|
{
|
|
905
1229
|
...deps,
|
|
906
1230
|
console,
|
|
907
1231
|
evoluError,
|
|
1232
|
+
syncState,
|
|
908
1233
|
},
|
|
909
1234
|
disposer,
|
|
910
1235
|
);
|
|
@@ -913,6 +1238,8 @@ export const createEvoluDeps = (deps: EvoluPlatformDeps): EvoluDeps => {
|
|
|
913
1238
|
/**
|
|
914
1239
|
* Creates an {@link Evolu} instance from {@link EvoluSchema} and
|
|
915
1240
|
* {@link EvoluConfig}.
|
|
1241
|
+
*
|
|
1242
|
+
* @group Construction
|
|
916
1243
|
*/
|
|
917
1244
|
export const createEvolu =
|
|
918
1245
|
<S extends EvoluSchema>(
|
|
@@ -1091,10 +1418,7 @@ export const createEvolu =
|
|
|
1091
1418
|
case "RefreshQueries": {
|
|
1092
1419
|
releaseUnsubscribedLoadingPromises();
|
|
1093
1420
|
|
|
1094
|
-
const queries = new Set<Query>(
|
|
1095
|
-
...loadingPromisesByQuery.keys(),
|
|
1096
|
-
...subscribedQueriesRefCount.keys(),
|
|
1097
|
-
]);
|
|
1421
|
+
const queries = new Set<Query>(subscribedQueriesRefCount.keys());
|
|
1098
1422
|
|
|
1099
1423
|
if (isNonEmptySet(queries)) postMessage({ type: "Query", queries });
|
|
1100
1424
|
break;
|
|
@@ -1202,6 +1526,41 @@ export const createEvolu =
|
|
|
1202
1526
|
`Invalid DbChange for table '${String(table)}'.`,
|
|
1203
1527
|
);
|
|
1204
1528
|
|
|
1529
|
+
// Copy binary values, so what is saved is what was passed: the caller
|
|
1530
|
+
// can change a Uint8Array before the batch is sent, a view of a
|
|
1531
|
+
// resizable buffer can grow, and a view of a shared buffer cannot be
|
|
1532
|
+
// sent to a SharedWorker at all. Node's Buffer passes validation and
|
|
1533
|
+
// its slice shares memory, so only the constructor copies reliably.
|
|
1534
|
+
for (const [column, value] of objectToEntries(dbChange.values)) {
|
|
1535
|
+
if (typeof value === "object" && value !== null) {
|
|
1536
|
+
changeValues[column] = new Uint8Array(value);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
// A change over maxMutationSize could never sync, and a shared worker
|
|
1541
|
+
// hosting two databases would stop all work of that database. Correct
|
|
1542
|
+
// apps bound column Types, so it is a programmer error and throws like
|
|
1543
|
+
// the DbChange check above, before batching: it gets no timestamp, the
|
|
1544
|
+
// database worker never sees it, and the code after the call does not
|
|
1545
|
+
// run, so a form keeps its input and no row refers to the missing one.
|
|
1546
|
+
//
|
|
1547
|
+
// Rejected: reporting it through evoluError, which lets that code run
|
|
1548
|
+
// as if the mutation was saved; checking in the database worker, which
|
|
1549
|
+
// gets it already batched and cannot reach the call site; quarantine,
|
|
1550
|
+
// which holds changes stored for sync; and skipping it in sync, which
|
|
1551
|
+
// keeps range fingerprints disagreeing. Received changes are not
|
|
1552
|
+
// checked, and oversized changes stored before this limit are not
|
|
1553
|
+
// recovered, which needs a history-aware design.
|
|
1554
|
+
//
|
|
1555
|
+
// Local-only tables never sync, so they have no limit.
|
|
1556
|
+
if (!isLocalOnlyTable(dbChange.table)) {
|
|
1557
|
+
const size = measureMutation(dbChange.table, dbChange.values);
|
|
1558
|
+
assert(
|
|
1559
|
+
size <= maxMutationSize,
|
|
1560
|
+
`The mutation of table '${dbChange.table}' is ${size} bytes, over maxMutationSize (${maxMutationSize}). Bound the Types of its columns or check it with evolu.getMutationSize.`,
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1205
1564
|
mutateBatch.push({
|
|
1206
1565
|
change: { ...dbChange, ownerId: options?.ownerId ?? appOwner.id },
|
|
1207
1566
|
onComplete: options?.onComplete,
|
|
@@ -1290,6 +1649,12 @@ export const createEvolu =
|
|
|
1290
1649
|
update: createMutation("update"),
|
|
1291
1650
|
upsert: createMutation("upsert"),
|
|
1292
1651
|
|
|
1652
|
+
getMutationSize: (table, values) =>
|
|
1653
|
+
measureMutation(
|
|
1654
|
+
String(table),
|
|
1655
|
+
values as ReadonlyRecord<string, SqliteValue | undefined>,
|
|
1656
|
+
),
|
|
1657
|
+
|
|
1293
1658
|
loadQuery,
|
|
1294
1659
|
loadQueries: <Q extends Queries<S>>(
|
|
1295
1660
|
queries: [...Q],
|
|
@@ -1311,6 +1676,27 @@ export const createEvolu =
|
|
|
1311
1676
|
listener();
|
|
1312
1677
|
});
|
|
1313
1678
|
|
|
1679
|
+
// Invalidation can happen after loading but before a framework
|
|
1680
|
+
// subscribes. Register first so subsequent invalidations also
|
|
1681
|
+
// refresh this query.
|
|
1682
|
+
const loadingPromise = loadingPromisesByQuery.get(query);
|
|
1683
|
+
if (loadingPromise?.releaseOnResolve) {
|
|
1684
|
+
// Retain the pending promise for its awaiters, but queue a read
|
|
1685
|
+
// after the mutation or sync that invalidated its result.
|
|
1686
|
+
loadingPromise.releaseOnResolve = false;
|
|
1687
|
+
queryBatch.push(query);
|
|
1688
|
+
} else if (!loadingPromise) {
|
|
1689
|
+
const rows = rowsByQueryMapStore.get().get(query);
|
|
1690
|
+
if (rows) {
|
|
1691
|
+
// Queue a read, but keep cached rows available to React use()
|
|
1692
|
+
// so a render before the worker responds does not suspend.
|
|
1693
|
+
void loadQuery(query);
|
|
1694
|
+
const entry = loadingPromisesByQuery.get(query);
|
|
1695
|
+
assertNotUndefined(entry);
|
|
1696
|
+
fulfillLoadingPromise(entry, rows);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1314
1700
|
return () => {
|
|
1315
1701
|
assert(
|
|
1316
1702
|
isSubscribed,
|
|
@@ -1347,6 +1733,13 @@ export const createEvolu =
|
|
|
1347
1733
|
},
|
|
1348
1734
|
|
|
1349
1735
|
useOwner,
|
|
1736
|
+
requestSync: (ownerId) => {
|
|
1737
|
+
// Do not let the forced mutation flush overtake pending owner actions.
|
|
1738
|
+
useOwnerBatch.flushNow();
|
|
1739
|
+
// Queue preceding mutations before the reconciliation request.
|
|
1740
|
+
mutateBatch.flushNow();
|
|
1741
|
+
useOwnerBatch.push({ action: "sync", ownerId });
|
|
1742
|
+
},
|
|
1350
1743
|
},
|
|
1351
1744
|
disposer,
|
|
1352
1745
|
),
|