@evolu/common 7.4.0 → 8.0.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -61
- package/dist/src/Array.d.ts +343 -102
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +181 -85
- package/dist/src/Assert.d.ts +57 -11
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +47 -11
- package/dist/src/BigInt.d.ts +6 -1
- package/dist/src/BigInt.d.ts.map +1 -1
- package/dist/src/BigInt.js +5 -0
- package/dist/src/Brand.d.ts +40 -12
- package/dist/src/Brand.d.ts.map +1 -1
- package/dist/src/Brand.js +5 -0
- package/dist/src/Buffer.d.ts +6 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +7 -2
- package/dist/src/Cache.d.ts +10 -2
- package/dist/src/Cache.d.ts.map +1 -1
- package/dist/src/Cache.js +6 -0
- package/dist/src/Callbacks.d.ts +10 -7
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Callbacks.js +23 -13
- package/dist/src/Console.d.ts +361 -69
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +217 -117
- package/dist/src/Crypto.d.ts +70 -21
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +72 -27
- package/dist/src/Eq.d.ts +22 -2
- package/dist/src/Eq.d.ts.map +1 -1
- package/dist/src/Eq.js +21 -1
- package/dist/src/Error.d.ts +23 -12
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +27 -11
- package/dist/src/Function.d.ts +128 -56
- package/dist/src/Function.d.ts.map +1 -1
- package/dist/src/Function.js +118 -14
- package/dist/src/Identicon.d.ts +5 -0
- package/dist/src/Identicon.d.ts.map +1 -1
- package/dist/src/Identicon.js +6 -1
- package/dist/src/Lookup.d.ts +160 -0
- package/dist/src/Lookup.d.ts.map +1 -0
- package/dist/src/Lookup.js +192 -0
- package/dist/src/Microtask.d.ts +21 -0
- package/dist/src/Microtask.d.ts.map +1 -0
- package/dist/src/Microtask.js +37 -0
- package/dist/src/Number.d.ts +18 -4
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +23 -1
- package/dist/src/Object.d.ts +110 -5
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +108 -5
- package/dist/src/Option.d.ts +58 -0
- package/dist/src/Option.d.ts.map +1 -0
- package/dist/src/Option.js +43 -0
- package/dist/src/Order.d.ts +5 -0
- package/dist/src/Order.d.ts.map +1 -1
- package/dist/src/Order.js +8 -3
- package/dist/src/Platform.d.ts +36 -2
- package/dist/src/Platform.d.ts.map +1 -1
- package/dist/src/Platform.js +10 -6
- package/dist/src/Polyfills.d.ts +27 -0
- package/dist/src/Polyfills.d.ts.map +1 -0
- package/dist/src/Polyfills.js +299 -0
- package/dist/src/Random.d.ts +18 -7
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Random.js +9 -7
- package/dist/src/Redacted.d.ts +7 -4
- package/dist/src/Redacted.d.ts.map +1 -1
- package/dist/src/Redacted.js +5 -0
- package/dist/src/Ref.d.ts +31 -16
- package/dist/src/Ref.d.ts.map +1 -1
- package/dist/src/Ref.js +35 -7
- package/dist/src/RefCount.d.ts +62 -0
- package/dist/src/RefCount.d.ts.map +1 -0
- package/dist/src/RefCount.js +83 -0
- package/dist/src/Relation.d.ts +84 -29
- package/dist/src/Relation.d.ts.map +1 -1
- package/dist/src/Relation.js +83 -105
- package/dist/src/Resource.d.ts +263 -0
- package/dist/src/Resource.d.ts.map +1 -0
- package/dist/src/Resource.js +389 -0
- package/dist/src/Result.d.ts +390 -374
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +142 -70
- package/dist/src/Schedule.d.ts +953 -0
- package/dist/src/Schedule.d.ts.map +1 -0
- package/dist/src/Schedule.js +1199 -0
- package/dist/src/Set.d.ts +181 -0
- package/dist/src/Set.d.ts.map +1 -0
- package/dist/src/Set.js +137 -0
- package/dist/src/Skiplist.d.ts +6 -1
- package/dist/src/Skiplist.d.ts.map +1 -1
- package/dist/src/Skiplist.js +5 -0
- package/dist/src/Sqlite.d.ts +160 -60
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +290 -190
- package/dist/src/Store.d.ts +36 -28
- package/dist/src/Store.d.ts.map +1 -1
- package/dist/src/Store.js +55 -15
- package/dist/src/String.d.ts +5 -0
- package/dist/src/String.d.ts.map +1 -1
- package/dist/src/String.js +5 -0
- package/dist/src/Task.d.ts +2263 -423
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +1488 -348
- package/dist/src/Test.d.ts +122 -0
- package/dist/src/Test.d.ts.map +1 -0
- package/dist/src/Test.js +66 -0
- package/dist/src/Time.d.ts +201 -48
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +190 -86
- package/dist/src/Tracer.d.ts +48 -0
- package/dist/src/Tracer.d.ts.map +1 -0
- package/dist/src/Tracer.js +6 -0
- package/dist/src/Type.d.ts +582 -209
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +362 -240
- package/dist/src/Types.d.ts +160 -6
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/Types.js +22 -2
- package/dist/src/WebSocket.d.ts +113 -58
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +210 -118
- package/dist/src/Worker.d.ts +245 -60
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +220 -64
- package/dist/src/index.d.ts +25 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +19 -3
- package/dist/src/local-first/Db.d.ts +24 -213
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +622 -293
- package/dist/src/local-first/Error.d.ts +12 -0
- package/dist/src/local-first/Error.d.ts.map +1 -0
- package/dist/src/local-first/Error.js +6 -0
- package/dist/src/local-first/Evolu.d.ts +330 -285
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +472 -402
- package/dist/src/local-first/LocalAuth.d.ts +10 -10
- package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
- package/dist/src/local-first/LocalAuth.js +6 -1
- package/dist/src/local-first/Owner.d.ts +70 -27
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +27 -14
- package/dist/src/local-first/Protocol.d.ts +58 -50
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +253 -155
- package/dist/src/local-first/Query.d.ts +133 -43
- package/dist/src/local-first/Query.d.ts.map +1 -1
- package/dist/src/local-first/Query.js +139 -90
- package/dist/src/local-first/Relay.d.ts +18 -38
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +59 -171
- package/dist/src/local-first/Schema.d.ts +139 -144
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +78 -163
- package/dist/src/local-first/Shared.d.ts +125 -0
- package/dist/src/local-first/Shared.d.ts.map +1 -0
- package/dist/src/local-first/Shared.js +569 -0
- package/dist/src/local-first/Storage.d.ts +50 -53
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +121 -173
- package/dist/src/local-first/Timestamp.d.ts +28 -35
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +24 -25
- package/dist/src/local-first/index.d.ts +2 -15
- package/dist/src/local-first/index.d.ts.map +1 -1
- package/dist/src/local-first/index.js +2 -15
- package/package.json +36 -26
- package/src/Array.ts +467 -106
- package/src/Assert.ts +87 -12
- package/src/BigInt.ts +7 -1
- package/src/Brand.ts +41 -12
- package/src/Buffer.ts +10 -4
- package/src/Cache.ts +11 -2
- package/src/Callbacks.ts +33 -23
- package/src/Console.ts +567 -178
- package/src/Crypto.ts +111 -62
- package/src/Eq.ts +30 -3
- package/src/Error.ts +50 -34
- package/src/Function.ts +140 -68
- package/src/Identicon.ts +7 -1
- package/src/Lookup.ts +415 -0
- package/src/Microtask.ts +58 -0
- package/src/Number.ts +40 -7
- package/src/Object.ts +155 -16
- package/src/Option.ts +74 -0
- package/src/Order.ts +11 -3
- package/src/Platform.ts +43 -7
- package/src/Polyfills.ts +465 -0
- package/src/Random.ts +25 -11
- package/src/Redacted.ts +8 -4
- package/src/Ref.ts +72 -21
- package/src/RefCount.ts +170 -0
- package/src/Relation.ts +188 -136
- package/src/Resource.ts +864 -0
- package/src/Result.ts +548 -381
- package/src/Schedule.ts +1471 -0
- package/src/Set.ts +247 -0
- package/src/Skiplist.ts +7 -1
- package/src/Sqlite.ts +409 -304
- package/src/Store.ts +94 -48
- package/src/String.ts +6 -0
- package/src/Task.ts +3685 -740
- package/src/Test.ts +162 -0
- package/src/Time.ts +348 -145
- package/src/Tracer.ts +54 -0
- package/src/Type.ts +1064 -630
- package/src/Types.ts +202 -7
- package/src/WebSocket.ts +311 -178
- package/src/Worker.ts +527 -147
- package/src/index.ts +74 -3
- package/src/local-first/Db.ts +871 -597
- package/src/local-first/Error.ts +17 -0
- package/src/local-first/Evolu.ts +884 -823
- package/src/local-first/LocalAuth.ts +10 -10
- package/src/local-first/Owner.ts +94 -42
- package/src/local-first/Protocol.ts +440 -303
- package/src/local-first/Query.ts +243 -194
- package/src/local-first/Relay.ts +91 -252
- package/src/local-first/Schema.ts +284 -394
- package/src/local-first/Shared.ts +932 -0
- package/src/local-first/Storage.ts +209 -282
- package/src/local-first/Timestamp.ts +38 -45
- package/src/local-first/index.ts +2 -16
- package/LICENSE +0 -21
- package/dist/src/Instances.d.ts +0 -34
- package/dist/src/Instances.d.ts.map +0 -1
- package/dist/src/Instances.js +0 -44
- package/dist/src/Resources.d.ts +0 -118
- package/dist/src/Resources.d.ts.map +0 -1
- package/dist/src/Resources.js +0 -197
- package/dist/src/local-first/Platform.d.ts +0 -25
- package/dist/src/local-first/Platform.d.ts.map +0 -1
- package/dist/src/local-first/Platform.js +0 -1
- package/dist/src/local-first/Public.d.ts +0 -22
- package/dist/src/local-first/Public.d.ts.map +0 -1
- package/dist/src/local-first/Public.js +0 -15
- package/dist/src/local-first/PublicKysely.d.ts +0 -148
- package/dist/src/local-first/PublicKysely.d.ts.map +0 -1
- package/dist/src/local-first/PublicKysely.js +0 -184
- package/dist/src/local-first/Sync.d.ts +0 -112
- package/dist/src/local-first/Sync.d.ts.map +0 -1
- package/dist/src/local-first/Sync.js +0 -529
- package/src/Instances.ts +0 -90
- package/src/Resources.ts +0 -367
- package/src/local-first/Platform.ts +0 -27
- package/src/local-first/Public.ts +0 -42
- package/src/local-first/PublicKysely.ts +0 -239
- package/src/local-first/Sync.ts +0 -960
package/src/local-first/Evolu.ts
CHANGED
|
@@ -1,80 +1,199 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Local-first platform.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
2
7
|
import {
|
|
3
|
-
|
|
8
|
+
emptyArray,
|
|
4
9
|
isNonEmptyArray,
|
|
5
|
-
|
|
10
|
+
mapArray,
|
|
11
|
+
type NonEmptyReadonlyArray,
|
|
6
12
|
} from "../Array.js";
|
|
7
|
-
import { assert, assertNonEmptyReadonlyArray } from "../Assert.js";
|
|
8
|
-
import { createCallbacks } from "../Callbacks.js";
|
|
9
|
-
import { ConsoleDep } from "../Console.js";
|
|
10
|
-
import { RandomBytesDep, SymmetricCryptoDecryptError } from "../Crypto.js";
|
|
11
|
-
import { eqArrayNumber } from "../Eq.js";
|
|
12
|
-
import { TransferableError } from "../Error.js";
|
|
13
|
-
import { exhaustiveCheck } from "../Function.js";
|
|
14
|
-
import { createInstances, Instances } from "../Instances.js";
|
|
15
|
-
import { err, ok, Result } from "../Result.js";
|
|
16
13
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
} from "../
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
14
|
+
assert,
|
|
15
|
+
assertNonEmptyReadonlyArray,
|
|
16
|
+
assertNotDisposed,
|
|
17
|
+
} from "../Assert.js";
|
|
18
|
+
import { createCallbacks } from "../Callbacks.js";
|
|
19
|
+
import type { ConsoleDep } from "../Console.js";
|
|
20
|
+
import { createConsole } from "../Console.js";
|
|
21
|
+
import { createUnknownError } from "../Error.js";
|
|
22
|
+
import { exhaustiveCheck, todo } from "../Function.js";
|
|
23
|
+
import { createMicrotaskBatch } from "../Microtask.js";
|
|
24
|
+
import type { FlushSyncDep, ReloadAppDep } from "../Platform.js";
|
|
25
|
+
import { createRefCountByKey } from "../RefCount.js";
|
|
26
|
+
import { err, ok } from "../Result.js";
|
|
27
|
+
import { isNonEmptySet } from "../Set.js";
|
|
28
|
+
import { SqliteBoolean, sqliteBooleanToBoolean } from "../Sqlite.js";
|
|
29
|
+
import type { Listener, ReadonlyStore, Unsubscribe } from "../Store.js";
|
|
30
|
+
import { createStore } from "../Store.js";
|
|
31
|
+
import { type createRun, type Task } from "../Task.js";
|
|
32
|
+
import type { Id, TypeError } from "../Type.js";
|
|
26
33
|
import {
|
|
34
|
+
brand,
|
|
27
35
|
createId,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
InferType,
|
|
32
|
-
Mnemonic,
|
|
33
|
-
ObjectType,
|
|
34
|
-
SimpleName,
|
|
35
|
-
ValidMutationSize,
|
|
36
|
-
ValidMutationSizeError,
|
|
36
|
+
createIdFromString,
|
|
37
|
+
Name,
|
|
38
|
+
UrlSafeString,
|
|
37
39
|
} from "../Type.js";
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
import type { ExtractType } from "../Types.js";
|
|
41
|
+
import type { CreateMessageChannelDep } from "../Worker.js";
|
|
42
|
+
import type { CreateDbWorkerDep } from "./Db.js";
|
|
43
|
+
import type { EvoluError } from "./Error.js";
|
|
44
|
+
import type {
|
|
45
|
+
AppOwner,
|
|
46
|
+
Owner,
|
|
47
|
+
OwnerId,
|
|
48
|
+
OwnerTransport,
|
|
49
|
+
ReadonlyOwner,
|
|
50
|
+
SyncOwner,
|
|
51
|
+
} from "./Owner.js";
|
|
52
|
+
import { createOwnerWebSocketTransport } from "./Owner.js";
|
|
53
|
+
import type {
|
|
47
54
|
Queries,
|
|
48
55
|
QueriesToQueryRowsPromises,
|
|
49
56
|
Query,
|
|
50
57
|
QueryRows,
|
|
51
|
-
QueryRowsMap,
|
|
52
58
|
Row,
|
|
53
|
-
|
|
54
|
-
SubscribedQueries,
|
|
59
|
+
RowsByQueryMap,
|
|
55
60
|
} from "./Query.js";
|
|
56
|
-
import {
|
|
57
|
-
|
|
61
|
+
import { applyPatches } from "./Query.js";
|
|
62
|
+
import type {
|
|
58
63
|
EvoluSchema,
|
|
59
|
-
evoluSchemaToDbSchema,
|
|
60
64
|
IndexesConfig,
|
|
61
|
-
insertable,
|
|
62
|
-
kysely,
|
|
63
65
|
Mutation,
|
|
64
66
|
MutationChange,
|
|
65
|
-
MutationKind,
|
|
66
|
-
MutationMapping,
|
|
67
|
-
MutationOptions,
|
|
68
|
-
SystemColumns,
|
|
69
|
-
updateable,
|
|
70
|
-
upsertable,
|
|
71
67
|
ValidateSchema,
|
|
72
68
|
} from "./Schema.js";
|
|
69
|
+
import { evoluSchemaToSqliteSchema } from "./Schema.js";
|
|
70
|
+
import type {
|
|
71
|
+
DbWorkerInput,
|
|
72
|
+
DbWorkerOutput,
|
|
73
|
+
EvoluInput,
|
|
74
|
+
EvoluOutput,
|
|
75
|
+
EvoluTabOutput,
|
|
76
|
+
SharedWorkerDep,
|
|
77
|
+
} from "./Shared.js";
|
|
73
78
|
import { DbChange } from "./Storage.js";
|
|
74
|
-
import {
|
|
75
|
-
|
|
79
|
+
import type { Timestamp } from "./Timestamp.js";
|
|
80
|
+
|
|
81
|
+
export interface EvoluConfig {
|
|
82
|
+
/**
|
|
83
|
+
* The app name. Evolu is multitenant - it can run multiple instances
|
|
84
|
+
* concurrently. The same app can have multiple instances for different
|
|
85
|
+
* accounts.
|
|
86
|
+
*
|
|
87
|
+
* Evolu derives the final instance name from `appName` and `appOwner` in
|
|
88
|
+
* {@link EvoluConfig}. The derived instance name is used as the SQLite
|
|
89
|
+
* database filename and as the log prefix. This ensures that each
|
|
90
|
+
* {@link Owner} gets a separate local database while preserving a readable app
|
|
91
|
+
* prefix.
|
|
92
|
+
*
|
|
93
|
+
* ### Example
|
|
94
|
+
*
|
|
95
|
+
* ```ts
|
|
96
|
+
* // appName: AppName.orThrow("MyApp")
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
readonly appName: AppName;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* {@link AppOwner} used to create this {@link Evolu} instance.
|
|
103
|
+
*
|
|
104
|
+
* Exposed as {@link Evolu.appOwner}. If `appOwner` is not passed, Evolu
|
|
105
|
+
* creates one.
|
|
106
|
+
*
|
|
107
|
+
* AppOwner controls access to the encrypted local SQLite database. If its
|
|
108
|
+
* secret material (Owner secret / Mnemonic) is not stored safely, data
|
|
109
|
+
* written by that instance is permanently inaccessible.
|
|
110
|
+
*
|
|
111
|
+
* Best onboarding UX is accountless first use: let users try a ready-to-use
|
|
112
|
+
* app, then prompt backup of `evolu.appOwner`.
|
|
113
|
+
*
|
|
114
|
+
* Recommended usage:
|
|
115
|
+
*
|
|
116
|
+
* - Omit `appOwner` for first run, then persist `evolu.appOwner` after user
|
|
117
|
+
* activity and guide the user to back it up.
|
|
118
|
+
* - Pass `appOwner` restored from secure storage (for example, Expo
|
|
119
|
+
* SecureStore, WebAuthn-backed storage, or app-managed account recovery
|
|
120
|
+
* flow).
|
|
121
|
+
*/
|
|
122
|
+
readonly appOwner: AppOwner;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Transport configuration for sync and backup.
|
|
126
|
+
*
|
|
127
|
+
* If not specified, Evolu uses the default Evolu relay. Pass one or more
|
|
128
|
+
* transports to override it with your own relays. Pass an empty array to
|
|
129
|
+
* disable sync, which is useful when sync should be configured later.
|
|
130
|
+
*
|
|
131
|
+
* Empty transports start the instance without sync. In that case,
|
|
132
|
+
* {@link Evolu.useOwner} must be called with explicit non-empty transports to
|
|
133
|
+
* enable sync for any Owner, including the AppOwner.
|
|
134
|
+
*
|
|
135
|
+
* **Redundancy:** The ideal setup uses at least two completely independent
|
|
136
|
+
* relays - for example, a home relay and a geographically separate relay.
|
|
137
|
+
* Data is sent to both relays simultaneously, providing true redundancy
|
|
138
|
+
* similar to using two independent clouds. This eliminates vendor lock-in and
|
|
139
|
+
* ensures your app continues working regardless of circumstances - whether
|
|
140
|
+
* home relay hardware fails or disappears, or a remote relay provider shuts
|
|
141
|
+
* down.
|
|
142
|
+
*
|
|
143
|
+
* Currently supports:
|
|
144
|
+
*
|
|
145
|
+
* - WebSocket: Real-time bidirectional communication with relay servers
|
|
146
|
+
*
|
|
147
|
+
* Use {@link createOwnerWebSocketTransport} to create WebSocket transport
|
|
148
|
+
* configurations with proper URL formatting and {@link OwnerId} inclusion. The
|
|
149
|
+
* {@link OwnerId} in the URL enables relay authentication, allowing relay
|
|
150
|
+
* servers to control access (e.g., for paid tiers or private instances).
|
|
151
|
+
*
|
|
152
|
+
* The default value is:
|
|
153
|
+
*
|
|
154
|
+
* `{ type: "WebSocket", url: "wss://free.evoluhq.com" }`.
|
|
155
|
+
*
|
|
156
|
+
* ### Example
|
|
157
|
+
*
|
|
158
|
+
* ```ts
|
|
159
|
+
* // Single WebSocket relay
|
|
160
|
+
* transports: [{ type: "WebSocket", url: "wss://relay1.example.com" }];
|
|
161
|
+
*
|
|
162
|
+
* // Multiple WebSocket relays for redundancy
|
|
163
|
+
* transports: [
|
|
164
|
+
* { type: "WebSocket", url: "wss://relay1.example.com" },
|
|
165
|
+
* { type: "WebSocket", url: "wss://relay2.example.com" },
|
|
166
|
+
* { type: "WebSocket", url: "wss://relay3.example.com" },
|
|
167
|
+
* ];
|
|
168
|
+
*
|
|
169
|
+
* // Local-only instance (no sync) - useful for device settings or when relay
|
|
170
|
+
* // URL will be provided later (e.g., after authentication), allowing users
|
|
171
|
+
* // to work offline before the app connects
|
|
172
|
+
* transports: [];
|
|
173
|
+
*
|
|
174
|
+
* // Using createOwnerWebSocketTransport helper for relay authentication
|
|
175
|
+
* transports: [
|
|
176
|
+
* createOwnerWebSocketTransport({
|
|
177
|
+
* url: "ws://localhost:4000",
|
|
178
|
+
* ownerId,
|
|
179
|
+
* }),
|
|
180
|
+
* ];
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
readonly transports?: ReadonlyArray<OwnerTransport>;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Keep local data only in memory instead of persisting it on this device.
|
|
187
|
+
* Useful for testing, temporary data, or sensitive data that should not be
|
|
188
|
+
* recoverable from local storage after the process ends.
|
|
189
|
+
*
|
|
190
|
+
* Local data stored in memory is completely destroyed when the process ends.
|
|
191
|
+
* Sync can still persist data remotely when transports are enabled.
|
|
192
|
+
*
|
|
193
|
+
* The default value is: `false`.
|
|
194
|
+
*/
|
|
195
|
+
readonly memoryOnly?: boolean;
|
|
76
196
|
|
|
77
|
-
export interface EvoluConfig extends Partial<DbConfig> {
|
|
78
197
|
/**
|
|
79
198
|
* Use the `indexes` option to define SQLite indexes.
|
|
80
199
|
*
|
|
@@ -98,79 +217,153 @@ export interface EvoluConfig extends Partial<DbConfig> {
|
|
|
98
217
|
readonly indexes?: IndexesConfig;
|
|
99
218
|
|
|
100
219
|
/**
|
|
101
|
-
*
|
|
220
|
+
* Called when this instance's local database is deleted.
|
|
102
221
|
*
|
|
103
|
-
*
|
|
222
|
+
* Apps can use this to update UI immediately because the corresponding
|
|
223
|
+
* {@link Evolu} instance becomes unusable after local database deletion.
|
|
224
|
+
*/
|
|
225
|
+
readonly onDatabaseDeleted?: () => void;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Called when local data for an {@link Owner} is deleted.
|
|
104
229
|
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
230
|
+
* Apps can use this to update UI immediately because that owner stops being
|
|
231
|
+
* used across tabs and instances.
|
|
107
232
|
*/
|
|
108
|
-
readonly
|
|
233
|
+
readonly onOwnerDeleted?: (owner: Owner) => void;
|
|
109
234
|
}
|
|
110
235
|
|
|
111
|
-
|
|
236
|
+
/**
|
|
237
|
+
* Application name.
|
|
238
|
+
*
|
|
239
|
+
* Evolu uses AppName as the base prefix for {@link Evolu.name}. The final
|
|
240
|
+
* instance name is derived per {@link AppOwner} as
|
|
241
|
+
* `${appName}-${createIdFromString(appOwner.id)}`.
|
|
242
|
+
*
|
|
243
|
+
* Uses the same safe alphabet as {@link UrlSafeString} (letters, digits, `-`,
|
|
244
|
+
* `_`) and must be between 1 and 41 characters.
|
|
245
|
+
*/
|
|
246
|
+
export const AppName = /*#__PURE__*/ brand("AppName", UrlSafeString, (value) =>
|
|
247
|
+
value.length >= 1 && value.length <= 41
|
|
248
|
+
? ok(value)
|
|
249
|
+
: err<AppNameError>({ type: "AppName", value }),
|
|
250
|
+
);
|
|
251
|
+
export type AppName = typeof AppName.Type;
|
|
252
|
+
export interface AppNameError extends TypeError<"AppName"> {}
|
|
253
|
+
|
|
254
|
+
export const testAppName = /*#__PURE__*/ AppName.orThrow("AppName");
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Local-first SQL database with typed queries, mutations, and sync.
|
|
258
|
+
*
|
|
259
|
+
* TODO: Better docs.
|
|
260
|
+
*/
|
|
261
|
+
export interface Evolu<
|
|
262
|
+
S extends EvoluSchema = EvoluSchema,
|
|
263
|
+
> extends AsyncDisposable {
|
|
264
|
+
/**
|
|
265
|
+
* Evolu instance name is derived from {@link EvoluConfig.appName} and
|
|
266
|
+
* {@link AppOwner}'s hash.
|
|
267
|
+
*/
|
|
268
|
+
readonly name: Name;
|
|
269
|
+
|
|
270
|
+
/** {@link AppOwner}. */
|
|
271
|
+
readonly appOwner: AppOwner;
|
|
272
|
+
|
|
112
273
|
/**
|
|
113
|
-
*
|
|
274
|
+
* Inserts a row and returns the generated {@link Id}.
|
|
275
|
+
*
|
|
276
|
+
* All non-nullable columns are required, nullable columns are optional, and
|
|
277
|
+
* `id` is omitted because Evolu generates it automatically. This ensures
|
|
278
|
+
* every row has a globally unique, conflict-free identifier without
|
|
279
|
+
* coordination.
|
|
114
280
|
*
|
|
115
281
|
* ### Example
|
|
116
282
|
*
|
|
117
283
|
* ```ts
|
|
118
|
-
* const
|
|
119
|
-
*
|
|
120
|
-
* console.log(error);
|
|
284
|
+
* const { id } = evolu.insert("todo", {
|
|
285
|
+
* title: NonEmptyString100.orThrow("Learn Evolu"),
|
|
121
286
|
* });
|
|
287
|
+
*
|
|
288
|
+
* // With onComplete callback
|
|
289
|
+
* evolu.insert(
|
|
290
|
+
* "todo",
|
|
291
|
+
* { title: NonEmptyString100.orThrow("Another todo") },
|
|
292
|
+
* { onComplete: () => console.log("Insert completed") },
|
|
293
|
+
* );
|
|
122
294
|
* ```
|
|
295
|
+
*
|
|
296
|
+
* @see {@link Mutation}
|
|
123
297
|
*/
|
|
124
|
-
readonly
|
|
125
|
-
|
|
126
|
-
/** Get {@link EvoluError}. */
|
|
127
|
-
readonly getError: () => EvoluError | null;
|
|
298
|
+
readonly insert: Mutation<S, "insert">;
|
|
128
299
|
|
|
129
300
|
/**
|
|
130
|
-
*
|
|
301
|
+
* Updates a row and returns the {@link Id}.
|
|
302
|
+
*
|
|
303
|
+
* Only `id` is required, all other columns are optional.
|
|
131
304
|
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
305
|
+
* ### Example
|
|
306
|
+
*
|
|
307
|
+
* ```ts
|
|
308
|
+
* const { id } = evolu.update("todo", {
|
|
309
|
+
* id: todoId,
|
|
310
|
+
* title: NonEmptyString100.orThrow("Updated title"),
|
|
311
|
+
* });
|
|
134
312
|
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
313
|
+
* // Soft delete
|
|
314
|
+
* evolu.update("todo", { id: todoId, isDeleted: sqliteTrue });
|
|
315
|
+
* ```
|
|
137
316
|
*
|
|
138
|
-
*
|
|
139
|
-
|
|
317
|
+
* @see {@link Mutation}
|
|
318
|
+
*/
|
|
319
|
+
readonly update: Mutation<S, "update">;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Upserts a row and returns the {@link Id}.
|
|
323
|
+
*
|
|
324
|
+
* Like insert, but requires an `id`. Useful for rows with external ID via
|
|
325
|
+
* {@link createIdFromString}. All other non-nullable columns are required,
|
|
326
|
+
* nullable columns are optional.
|
|
327
|
+
*
|
|
328
|
+
* `createdAt` and `updatedAt` cannot be set manually. Evolu derives both from
|
|
329
|
+
* the CRDT {@link Timestamp} — they share the same value, encoded once to
|
|
330
|
+
* avoid redundancy. Timestamps are always generated by Evolu to preserve CRDT
|
|
331
|
+
* consistency guarantees.
|
|
140
332
|
*
|
|
141
333
|
* ### Example
|
|
142
334
|
*
|
|
143
335
|
* ```ts
|
|
144
|
-
* const
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* evolu.createQuery((db) =>
|
|
150
|
-
* db.selectFrom("todo").selectAll().where("id", "=", id),
|
|
151
|
-
* );
|
|
336
|
+
* const stableId = createIdFromString("my-todo-1");
|
|
337
|
+
* const { id } = evolu.upsert("todo", {
|
|
338
|
+
* id: stableId,
|
|
339
|
+
* title: NonEmptyString100.orThrow("Learn Evolu"),
|
|
340
|
+
* });
|
|
152
341
|
* ```
|
|
342
|
+
*
|
|
343
|
+
* @see {@link Mutation}
|
|
153
344
|
*/
|
|
154
|
-
readonly
|
|
345
|
+
readonly upsert: Mutation<S, "upsert">;
|
|
155
346
|
|
|
156
347
|
/**
|
|
157
348
|
* Load {@link Query} and return a promise with {@link QueryRows}.
|
|
158
349
|
*
|
|
159
350
|
* The returned promise always resolves successfully because there is no
|
|
160
|
-
* reason why loading should fail. All data are local, and the query is
|
|
161
|
-
*
|
|
351
|
+
* reason why loading should fail. All data are local, and the query is
|
|
352
|
+
* typed.
|
|
162
353
|
*
|
|
163
|
-
* Loading is batched
|
|
164
|
-
*
|
|
165
|
-
*
|
|
354
|
+
* Loading is batched. Returned promises are cached while pending and can be
|
|
355
|
+
* reused after fulfillment until mutation-driven invalidation, which prevents
|
|
356
|
+
* redundant database queries and supports React Suspense (stable references
|
|
357
|
+
* while pending).
|
|
166
358
|
*
|
|
167
359
|
* To subscribe a query for automatic updates, use
|
|
168
|
-
* {@link Evolu
|
|
360
|
+
* {@link Evolu.subscribeQuery}.
|
|
169
361
|
*
|
|
170
362
|
* ### Example
|
|
171
363
|
*
|
|
172
364
|
* ```ts
|
|
173
|
-
* const
|
|
365
|
+
* const createQuery = createQueryBuilder(Schema);
|
|
366
|
+
* const allTodos = createQuery((db) =>
|
|
174
367
|
* db.selectFrom("todo").selectAll(),
|
|
175
368
|
* );
|
|
176
369
|
* evolu.loadQuery(allTodos).then((rows) => {
|
|
@@ -178,7 +371,9 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> extends Disposable {
|
|
|
178
371
|
* });
|
|
179
372
|
* ```
|
|
180
373
|
*/
|
|
181
|
-
readonly loadQuery: <R extends Row>(
|
|
374
|
+
readonly loadQuery: <R extends Row>(
|
|
375
|
+
query: Query<S, R>,
|
|
376
|
+
) => Promise<QueryRows<R>>;
|
|
182
377
|
|
|
183
378
|
/**
|
|
184
379
|
* Load an array of {@link Query} queries and return an array of
|
|
@@ -191,7 +386,7 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> extends Disposable {
|
|
|
191
386
|
* evolu.loadQueries([allTodos, todoById(1)]);
|
|
192
387
|
* ```
|
|
193
388
|
*/
|
|
194
|
-
readonly loadQueries: <
|
|
389
|
+
readonly loadQueries: <Q extends Queries<S>>(
|
|
195
390
|
queries: [...Q],
|
|
196
391
|
) => [...QueriesToQueryRowsPromises<Q>];
|
|
197
392
|
|
|
@@ -206,7 +401,9 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> extends Disposable {
|
|
|
206
401
|
* });
|
|
207
402
|
* ```
|
|
208
403
|
*/
|
|
209
|
-
readonly subscribeQuery: (
|
|
404
|
+
readonly subscribeQuery: (
|
|
405
|
+
query: Query<S>,
|
|
406
|
+
) => (listener: Listener) => Unsubscribe;
|
|
210
407
|
|
|
211
408
|
/**
|
|
212
409
|
* Get {@link QueryRows}.
|
|
@@ -219,863 +416,727 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> extends Disposable {
|
|
|
219
416
|
* });
|
|
220
417
|
* ```
|
|
221
418
|
*/
|
|
222
|
-
readonly getQueryRows: <R extends Row>(query: Query<R>) => QueryRows<R>;
|
|
419
|
+
readonly getQueryRows: <R extends Row>(query: Query<S, R>) => QueryRows<R>;
|
|
223
420
|
|
|
224
421
|
/**
|
|
225
|
-
*
|
|
422
|
+
* Exports the SQLite database file.
|
|
226
423
|
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
* ### Example
|
|
424
|
+
* Exports are sequential: concurrent calls share one pending export instead
|
|
425
|
+
* of starting parallel exports.
|
|
231
426
|
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
* ```
|
|
427
|
+
* The pending promise rejects if this {@link Evolu} instance is disposed
|
|
428
|
+
* before export completion.
|
|
235
429
|
*/
|
|
236
|
-
readonly
|
|
430
|
+
readonly exportDatabase: () => Promise<Uint8Array<ArrayBuffer>>;
|
|
431
|
+
|
|
432
|
+
// TODO: Add exportHistory.
|
|
237
433
|
|
|
238
434
|
/**
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
* The first argument is the table name, and the second is an object
|
|
243
|
-
* containing the row data. An optional third argument provides mutation
|
|
244
|
-
* options including an `onComplete` callback and `onlyValidate` flag.
|
|
245
|
-
*
|
|
246
|
-
* Returns a Result type - use `.ok` to check if the insertion succeeded, and
|
|
247
|
-
* `.value.id` to access the generated ID on success, or `.error` to handle
|
|
248
|
-
* validation errors.
|
|
435
|
+
* Delete the local SQLite database for this {@link Evolu} instance on the
|
|
436
|
+
* current device.
|
|
249
437
|
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
438
|
+
* **Warning**: This first drops all tables. “The dropped table is completely
|
|
439
|
+
* removed from the database schema and the disk file. The table can not be
|
|
440
|
+
* recovered. All indices and triggers associated with the table are also
|
|
441
|
+
* deleted.” https://sqlite.org/lang_droptable.html
|
|
253
442
|
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
* ```ts
|
|
257
|
-
* const result = evolu.insert("todo", {
|
|
258
|
-
* title: "Learn Evolu",
|
|
259
|
-
* isCompleted: false,
|
|
260
|
-
* });
|
|
261
|
-
*
|
|
262
|
-
* if (result.ok) {
|
|
263
|
-
* console.log("Todo created with ID:", result.value.id);
|
|
264
|
-
* } else {
|
|
265
|
-
* console.error("Validation error:", result.error);
|
|
266
|
-
* }
|
|
443
|
+
* After that, it deletes the SQLite file identified by {@link Evolu.name},
|
|
444
|
+
* permanently forgetting all local data for that instance on this device.
|
|
267
445
|
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
* "todo",
|
|
271
|
-
* { title: "Another todo" },
|
|
272
|
-
* {
|
|
273
|
-
* onComplete: () => {
|
|
274
|
-
* console.log("Insert completed");
|
|
275
|
-
* },
|
|
276
|
-
* },
|
|
277
|
-
* );
|
|
278
|
-
* ```
|
|
446
|
+
* All instances identified by {@link Evolu.name} will be self-disposed. Use
|
|
447
|
+
* {@link EvoluConfig.onDatabaseDeleted} to update app UI when that happens.
|
|
279
448
|
*/
|
|
280
|
-
|
|
449
|
+
readonly deleteDatabase: () => void;
|
|
281
450
|
|
|
282
451
|
/**
|
|
283
|
-
*
|
|
284
|
-
* {@link Id}.
|
|
285
|
-
*
|
|
286
|
-
* The first argument is the table name, and the second is an object
|
|
287
|
-
* containing the row data including the required `id` field. An optional
|
|
288
|
-
* third argument provides mutation options including an `onComplete` callback
|
|
289
|
-
* and `onlyValidate` flag.
|
|
290
|
-
*
|
|
291
|
-
* Returns a Result type - use `.ok` to check if the update succeeded, and
|
|
292
|
-
* `.value.id` to access the ID on success, or `.error` to handle validation
|
|
293
|
-
* errors.
|
|
452
|
+
* Delete all local data for a specific {@link Owner}.
|
|
294
453
|
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
* to automatically update {@link SystemColumns}.
|
|
298
|
-
*
|
|
299
|
-
* ### Example
|
|
300
|
-
*
|
|
301
|
-
* ```ts
|
|
302
|
-
* const result = evolu.update("todo", {
|
|
303
|
-
* id: todoId,
|
|
304
|
-
* title: "Updated title",
|
|
305
|
-
* isCompleted: true,
|
|
306
|
-
* });
|
|
454
|
+
* This deletes all rows whose `ownerId` matches the provided owner and
|
|
455
|
+
* deletes that owner's local history as well.
|
|
307
456
|
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
* } else {
|
|
311
|
-
* console.error("Validation error:", result.error);
|
|
312
|
-
* }
|
|
313
|
-
*
|
|
314
|
-
* // To delete a row, set isDeleted to true
|
|
315
|
-
* evolu.update("todo", { id: todoId, isDeleted: true });
|
|
316
|
-
*
|
|
317
|
-
* // With onComplete callback
|
|
318
|
-
* evolu.update(
|
|
319
|
-
* "todo",
|
|
320
|
-
* { id: todoId, title: "New title" },
|
|
321
|
-
* {
|
|
322
|
-
* onComplete: () => {
|
|
323
|
-
* console.log("Update completed");
|
|
324
|
-
* },
|
|
325
|
-
* },
|
|
326
|
-
* );
|
|
327
|
-
* ```
|
|
457
|
+
* It also stops using that owner across all tabs and instances. Use
|
|
458
|
+
* {@link EvoluConfig.onOwnerDeleted} to update app UI when that happens.
|
|
328
459
|
*/
|
|
329
|
-
|
|
460
|
+
readonly deleteOwner: (owner: Owner) => void;
|
|
330
461
|
|
|
331
462
|
/**
|
|
332
|
-
*
|
|
333
|
-
* {@link Id}.
|
|
463
|
+
* Use an Owner for sync. Returns a {@link UnuseOwner}.
|
|
334
464
|
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
* and `onlyValidate` flag.
|
|
465
|
+
* Using an Owner means syncing it with the provided transports, or with the
|
|
466
|
+
* default transports defined in {@link EvoluConfig} when transports are
|
|
467
|
+
* omitted.
|
|
339
468
|
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
* may already be created on a different device.
|
|
469
|
+
* If {@link EvoluConfig.transports} is an empty array, this method must be
|
|
470
|
+
* called with explicit non-empty transports.
|
|
343
471
|
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
* errors.
|
|
347
|
-
*
|
|
348
|
-
* Evolu does not use SQL for mutations to ensure data can be safely and
|
|
349
|
-
* predictably merged without conflicts. Explicit mutations also allow Evolu
|
|
350
|
-
* to automatically update {@link SystemColumns}.
|
|
472
|
+
* Transports are automatically deduplicated and reference-counted, so
|
|
473
|
+
* multiple Owners using the same transport will share a single connection.
|
|
351
474
|
*
|
|
352
475
|
* ### Example
|
|
353
476
|
*
|
|
354
477
|
* ```ts
|
|
355
|
-
* // Use
|
|
356
|
-
* const
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
* });
|
|
478
|
+
* // Use an Owner (starts syncing).
|
|
479
|
+
* const unuseOwner = evolu.useOwner(shardOwner, [
|
|
480
|
+
* createOwnerWebSocketTransport({
|
|
481
|
+
* url: "ws://localhost:4000",
|
|
482
|
+
* ownerId: shardOwner.id,
|
|
483
|
+
* }),
|
|
484
|
+
* ]);
|
|
363
485
|
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
* } else {
|
|
367
|
-
* console.error("Validation error:", result.error);
|
|
368
|
-
* }
|
|
369
|
-
*
|
|
370
|
-
* // Data migration with custom createdAt
|
|
371
|
-
* evolu.upsert("todo", {
|
|
372
|
-
* id: externalId,
|
|
373
|
-
* title: "Migrated todo",
|
|
374
|
-
* createdAt: new Date("2023-01-01"), // Preserve original timestamp
|
|
375
|
-
* });
|
|
486
|
+
* // Later, stop using the Owner.
|
|
487
|
+
* unuseOwner();
|
|
376
488
|
*
|
|
377
|
-
* //
|
|
378
|
-
* evolu.
|
|
379
|
-
*
|
|
380
|
-
* { id: stableId, title: "Updated title" },
|
|
381
|
-
* {
|
|
382
|
-
* onComplete: () => {
|
|
383
|
-
* console.log("Upsert completed");
|
|
384
|
-
* },
|
|
385
|
-
* },
|
|
386
|
-
* );
|
|
489
|
+
* // Bulk operations.
|
|
490
|
+
* const unuseOwners = owners.map(evolu.useOwner);
|
|
491
|
+
* // Later: for (const unuse of unuseOwners) unuse();
|
|
387
492
|
* ```
|
|
388
493
|
*/
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
* will reload all tabs using Evolu. For native apps, it will restart the
|
|
395
|
-
* app.
|
|
396
|
-
*
|
|
397
|
-
* Reloading can be turned off via options if you want to provide a different
|
|
398
|
-
* UX.
|
|
399
|
-
*/
|
|
400
|
-
readonly resetAppOwner: (options?: {
|
|
401
|
-
readonly reload?: boolean;
|
|
402
|
-
}) => Promise<void>;
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* Restore {@link AppOwner} with all their synced data. It uses
|
|
406
|
-
* {@link Evolu#resetAppOwner}, so be careful.
|
|
407
|
-
*/
|
|
408
|
-
readonly restoreAppOwner: (
|
|
409
|
-
mnemonic: Mnemonic,
|
|
410
|
-
options?: {
|
|
411
|
-
readonly reload?: boolean;
|
|
412
|
-
},
|
|
413
|
-
) => Promise<void>;
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* Reload the app in a platform-specific way. For browsers, this will reload
|
|
417
|
-
* all tabs using Evolu. For native apps, it will restart the app.
|
|
418
|
-
*/
|
|
419
|
-
readonly reloadApp: () => void;
|
|
494
|
+
readonly useOwner: (
|
|
495
|
+
owner: ReadonlyOwner | Owner,
|
|
496
|
+
transports?: NonEmptyReadonlyArray<OwnerTransport>,
|
|
497
|
+
) => UnuseOwner;
|
|
498
|
+
}
|
|
420
499
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
*
|
|
424
|
-
* In the future, it will be possible to import a database and export/import
|
|
425
|
-
* history for 1:1 migrations across owners.
|
|
426
|
-
*/
|
|
427
|
-
readonly exportDatabase: () => Promise<Uint8Array<ArrayBuffer>>;
|
|
500
|
+
/** Function returned by {@link Evolu.useOwner} to stop using an Owner for sync. */
|
|
501
|
+
export type UnuseOwner = () => void;
|
|
428
502
|
|
|
503
|
+
export interface EvoluErrorDep {
|
|
429
504
|
/**
|
|
430
|
-
*
|
|
505
|
+
* {@link ReadonlyStore} of {@link EvoluError} shared by all {@link Evolu}
|
|
506
|
+
* instances created from the same {@link createEvoluDeps} result.
|
|
431
507
|
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
* Transport are automatically deduplicated and reference-counted, so multiple
|
|
436
|
-
* owners using the same transport will share a single connection.
|
|
508
|
+
* Subscribe once to show user-facing messages across all instances. Logging
|
|
509
|
+
* is handled by platform {@link createRun} global error handlers.
|
|
437
510
|
*
|
|
438
511
|
* ### Example
|
|
439
512
|
*
|
|
440
513
|
* ```ts
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
* // Later, stop using the owner.
|
|
445
|
-
* unuseOwner();
|
|
514
|
+
* deps.evoluError.subscribe(() => {
|
|
515
|
+
* const error = deps.evoluError.get();
|
|
516
|
+
* if (!error) return;
|
|
446
517
|
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
518
|
+
* switch (error.type) {
|
|
519
|
+
* case "InvalidComputerClock":
|
|
520
|
+
* // Show guidance specific to the detected error.
|
|
521
|
+
* showMessage(
|
|
522
|
+
* "Your system clock appears incorrect. Please fix it.",
|
|
523
|
+
* );
|
|
524
|
+
* break;
|
|
525
|
+
* default:
|
|
526
|
+
* // Show a generic user message for other operational errors.
|
|
527
|
+
* showMessage("Something went wrong. Please try again.");
|
|
528
|
+
* }
|
|
529
|
+
* });
|
|
450
530
|
* ```
|
|
451
|
-
*
|
|
452
|
-
* @experimental
|
|
453
|
-
*/
|
|
454
|
-
readonly useOwner: (owner: SyncOwner) => UnuseOwner;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/** Function returned by {@link Evolu#useOwner} to stop using an {@link SyncOwner}. */
|
|
458
|
-
export type UnuseOwner = () => void;
|
|
459
|
-
|
|
460
|
-
/** Represents errors that can occur in Evolu. */
|
|
461
|
-
export type EvoluError =
|
|
462
|
-
| ProtocolError
|
|
463
|
-
| SqliteError
|
|
464
|
-
| SymmetricCryptoDecryptError
|
|
465
|
-
| TimestampError
|
|
466
|
-
| TransferableError;
|
|
467
|
-
|
|
468
|
-
interface InternalEvoluInstance<S extends EvoluSchema = EvoluSchema>
|
|
469
|
-
extends Evolu<S> {
|
|
470
|
-
/**
|
|
471
|
-
* Ensure tables and columns defined in {@link EvoluSchema} exist in the
|
|
472
|
-
* database. This function is for hot reloading.
|
|
473
531
|
*/
|
|
474
|
-
readonly
|
|
532
|
+
readonly evoluError: ReadonlyStore<EvoluError | null>;
|
|
475
533
|
}
|
|
476
534
|
|
|
477
|
-
export type EvoluDeps = ConsoleDep &
|
|
478
|
-
CreateDbWorkerDep &
|
|
479
|
-
Partial<FlushSyncDep> &
|
|
480
|
-
RandomBytesDep &
|
|
481
|
-
ReloadAppDep &
|
|
482
|
-
TimeDep;
|
|
483
|
-
|
|
484
|
-
const evoluInstances = createInstances<SimpleName, InternalEvoluInstance>();
|
|
485
|
-
|
|
486
535
|
/**
|
|
487
|
-
*
|
|
488
|
-
*
|
|
536
|
+
* Shared platform dependencies for creating {@link Evolu} instances.
|
|
537
|
+
*
|
|
538
|
+
* Includes platform adapters, the shared {@link EvoluErrorDep.evoluError} store,
|
|
539
|
+
* and disposal for owned resources.
|
|
489
540
|
*/
|
|
490
|
-
|
|
541
|
+
export type EvoluDeps = EvoluPlatformDeps & EvoluErrorDep & Disposable;
|
|
491
542
|
|
|
492
543
|
/**
|
|
493
|
-
*
|
|
494
|
-
* {@link EvoluSchema} and optional {@link EvoluConfig} providing a typed
|
|
495
|
-
* interface for querying, mutating, and syncing your application's data.
|
|
496
|
-
*
|
|
497
|
-
* ### Example
|
|
498
|
-
*
|
|
499
|
-
* ```ts
|
|
500
|
-
* const TodoId = id("Todo");
|
|
501
|
-
* type TodoId = InferType<typeof TodoId>;
|
|
502
|
-
*
|
|
503
|
-
* const TodoCategoryId = id("TodoCategory");
|
|
504
|
-
* type TodoCategoryId = InferType<typeof TodoCategoryId>;
|
|
505
|
-
*
|
|
506
|
-
* const NonEmptyString50 = maxLength(50, NonEmptyString);
|
|
507
|
-
* type NonEmptyString50 = InferType<typeof NonEmptyString50>;
|
|
508
|
-
*
|
|
509
|
-
* const Schema = {
|
|
510
|
-
* todo: {
|
|
511
|
-
* id: TodoId,
|
|
512
|
-
* title: NonEmptyString1000,
|
|
513
|
-
* isCompleted: nullOr(SqliteBoolean),
|
|
514
|
-
* categoryId: nullOr(TodoCategoryId),
|
|
515
|
-
* },
|
|
516
|
-
* todoCategory: {
|
|
517
|
-
* id: TodoCategoryId,
|
|
518
|
-
* name: NonEmptyString50,
|
|
519
|
-
* },
|
|
520
|
-
* };
|
|
544
|
+
* Platform-specific dependencies required to create {@link EvoluDeps}.
|
|
521
545
|
*
|
|
522
|
-
*
|
|
523
|
-
*
|
|
546
|
+
* Provides worker and channel adapters plus optional platform integrations for
|
|
547
|
+
* logging and synchronous UI flush.
|
|
548
|
+
*/
|
|
549
|
+
export type EvoluPlatformDeps = CreateDbWorkerDep &
|
|
550
|
+
CreateMessageChannelDep &
|
|
551
|
+
ReloadAppDep &
|
|
552
|
+
SharedWorkerDep &
|
|
553
|
+
Partial<ConsoleDep> &
|
|
554
|
+
Partial<FlushSyncDep>;
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Creates shared dependencies used by all {@link createEvolu} instances on a
|
|
558
|
+
* platform.
|
|
524
559
|
*
|
|
525
|
-
*
|
|
560
|
+
* Call this once per platform and reuse the returned deps when creating
|
|
561
|
+
* multiple Evolu instances. The returned deps object owns long-lived resources
|
|
562
|
+
* such as worker channels and the shared {@link EvoluErrorDep.evoluError}
|
|
563
|
+
* store.
|
|
526
564
|
*
|
|
527
|
-
*
|
|
528
|
-
* name to enable hot reloading and prevent database corruption from multiple
|
|
529
|
-
* connections. For testing, use unique instance names to ensure proper
|
|
530
|
-
* isolation.
|
|
565
|
+
* Dispose it only during app shutdown.
|
|
531
566
|
*/
|
|
532
|
-
export const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
)
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
deps.console.enabled = config?.enableLogging ?? false;
|
|
551
|
-
|
|
552
|
-
const { indexes, reloadUrl = "/", ...partialDbConfig } = config ?? {};
|
|
553
|
-
|
|
554
|
-
const dbConfig: DbConfig = { ...defaultDbConfig, ...partialDbConfig };
|
|
555
|
-
|
|
556
|
-
deps.console.log("[evolu]", "createEvoluInstance", {
|
|
557
|
-
name: dbConfig.name,
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
const errorStore = createStore<EvoluError | null>(null);
|
|
561
|
-
const rowsStore = createStore<QueryRowsMap>(new Map());
|
|
567
|
+
export const createEvoluDeps = (deps: EvoluPlatformDeps): EvoluDeps => {
|
|
568
|
+
const { createMessageChannel, sharedWorker } = deps;
|
|
569
|
+
const console = deps.console ?? createConsole();
|
|
570
|
+
|
|
571
|
+
const stack = new DisposableStack();
|
|
572
|
+
stack.use(sharedWorker);
|
|
573
|
+
const evoluError = stack.use(createStore<EvoluError | null>(null));
|
|
574
|
+
|
|
575
|
+
const tabChannel = stack.use(createMessageChannel<EvoluTabOutput>());
|
|
576
|
+
tabChannel.port2.onMessage = (message) => {
|
|
577
|
+
switch (message.type) {
|
|
578
|
+
case "OnConsoleEntry":
|
|
579
|
+
console.write(message.entry);
|
|
580
|
+
// Fallback channel for unexpected errors without EvoluError typing.
|
|
581
|
+
if (message.entry.method === "error") {
|
|
582
|
+
evoluError.set(createUnknownError(message.entry.args));
|
|
583
|
+
}
|
|
584
|
+
break;
|
|
562
585
|
|
|
563
|
-
|
|
564
|
-
|
|
586
|
+
case "OnError":
|
|
587
|
+
evoluError.set(message.error);
|
|
588
|
+
// Keep typed errors visible in logs as operational failures.
|
|
589
|
+
console.error(message.error);
|
|
590
|
+
break;
|
|
565
591
|
|
|
566
|
-
|
|
567
|
-
|
|
592
|
+
default:
|
|
593
|
+
exhaustiveCheck(message);
|
|
568
594
|
}
|
|
595
|
+
};
|
|
569
596
|
|
|
570
|
-
|
|
571
|
-
|
|
597
|
+
sharedWorker.port.postMessage(
|
|
598
|
+
{
|
|
599
|
+
type: "InitTab",
|
|
600
|
+
consoleLevel: console.getLevel(),
|
|
601
|
+
port: tabChannel.port1.native,
|
|
602
|
+
},
|
|
603
|
+
[tabChannel.port1.native],
|
|
604
|
+
);
|
|
572
605
|
|
|
573
|
-
|
|
574
|
-
const loadingPromises = createLoadingPromises(subscribedQueries);
|
|
575
|
-
const onCompleteCallbacks = createCallbacks(deps);
|
|
576
|
-
const exportCallbacks = createCallbacks<Uint8Array<ArrayBuffer>>(deps);
|
|
606
|
+
const moved = stack.move();
|
|
577
607
|
|
|
578
|
-
|
|
608
|
+
return {
|
|
609
|
+
...deps,
|
|
610
|
+
evoluError,
|
|
611
|
+
[Symbol.dispose]: () => moved.dispose(),
|
|
612
|
+
};
|
|
613
|
+
};
|
|
579
614
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
615
|
+
/**
|
|
616
|
+
* Creates an {@link Evolu} instance from {@link EvoluSchema} and
|
|
617
|
+
* {@link EvoluConfig}.
|
|
618
|
+
*/
|
|
619
|
+
export const createEvolu =
|
|
620
|
+
<S extends EvoluSchema>(
|
|
621
|
+
schema: ValidateSchema<S> extends never ? S : ValidateSchema<S>,
|
|
622
|
+
config: EvoluConfig,
|
|
623
|
+
): Task<Evolu<S>, never, EvoluPlatformDeps> =>
|
|
624
|
+
async (run) => {
|
|
625
|
+
const {
|
|
626
|
+
appName,
|
|
627
|
+
appOwner,
|
|
628
|
+
memoryOnly = false,
|
|
629
|
+
transports = [{ type: "WebSocket", url: "wss://free.evoluhq.com" }],
|
|
630
|
+
} = config;
|
|
584
631
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
switch (message.type) {
|
|
589
|
-
case "onError": {
|
|
590
|
-
errorStore.set(message.error);
|
|
591
|
-
break;
|
|
592
|
-
}
|
|
632
|
+
const name = Name.orThrow(`${appName}-${createIdFromString(appOwner.id)}`);
|
|
633
|
+
const console = run.deps.console.child(name).child("Evolu");
|
|
634
|
+
console.info("createEvolu");
|
|
593
635
|
|
|
594
|
-
|
|
595
|
-
resolveAppOwner(message.appOwner);
|
|
596
|
-
break;
|
|
597
|
-
}
|
|
636
|
+
await using stack = new AsyncDisposableStack();
|
|
598
637
|
|
|
599
|
-
|
|
600
|
-
|
|
638
|
+
const rowsByQueryMapStore = stack.use(
|
|
639
|
+
createStore<RowsByQueryMap>(new Map()),
|
|
640
|
+
);
|
|
641
|
+
const subscribedQueriesRefCount = stack.use(createRefCountByKey<Query>());
|
|
642
|
+
|
|
643
|
+
interface LoadingPromise {
|
|
644
|
+
/**
|
|
645
|
+
* React tracks `status`/`value`/`reason` on thenables passed to `use`.
|
|
646
|
+
* Evolu mirrors that shape so cached promises can be unwrapped
|
|
647
|
+
* synchronously and to keep promise-cache behavior stable.
|
|
648
|
+
*
|
|
649
|
+
* React source:
|
|
650
|
+
* https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberThenable.js
|
|
651
|
+
*/
|
|
652
|
+
promise: Promise<QueryRows> & {
|
|
653
|
+
status?: "pending" | "fulfilled" | "rejected";
|
|
654
|
+
value?: QueryRows;
|
|
655
|
+
reason?: unknown;
|
|
656
|
+
};
|
|
657
|
+
resolve: (rows: QueryRows) => void;
|
|
658
|
+
releaseOnResolve: boolean;
|
|
659
|
+
}
|
|
601
660
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
661
|
+
/**
|
|
662
|
+
* Settle pending query loads during disposal so awaiting callers and React
|
|
663
|
+
* `use` thenables do not hang forever during teardown.
|
|
664
|
+
*/
|
|
665
|
+
const loadingPromisesByQuery = stack.adopt(
|
|
666
|
+
new Map<Query, LoadingPromise>(),
|
|
667
|
+
(loadingPromisesByQuery) => {
|
|
668
|
+
for (const loadingPromise of loadingPromisesByQuery.values()) {
|
|
669
|
+
if (loadingPromise.promise.status === "fulfilled") continue;
|
|
670
|
+
fulfillLoadingPromise(loadingPromise, emptyArray);
|
|
671
|
+
}
|
|
672
|
+
loadingPromisesByQuery.clear();
|
|
673
|
+
},
|
|
674
|
+
);
|
|
612
675
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
676
|
+
const fulfillLoadingPromise = (
|
|
677
|
+
loadingPromise: LoadingPromise,
|
|
678
|
+
rows: QueryRows,
|
|
679
|
+
): void => {
|
|
680
|
+
/**
|
|
681
|
+
* Pending promises must be resolved in place to preserve identity for
|
|
682
|
+
* current awaiters. Fulfilled promises are replaced with a new resolved
|
|
683
|
+
* promise so future loads see the latest rows.
|
|
684
|
+
*/
|
|
685
|
+
if (loadingPromise.promise.status !== "fulfilled") {
|
|
686
|
+
loadingPromise.resolve(rows);
|
|
687
|
+
} else {
|
|
688
|
+
loadingPromise.promise = Promise.resolve(rows);
|
|
689
|
+
}
|
|
616
690
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
}
|
|
691
|
+
/** See {@link LoadingPromise.promise}. */
|
|
692
|
+
void Object.assign(loadingPromise.promise, {
|
|
693
|
+
status: "fulfilled",
|
|
694
|
+
value: rows,
|
|
695
|
+
});
|
|
696
|
+
};
|
|
624
697
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
698
|
+
/**
|
|
699
|
+
* Mutations and refreshes invalidate query snapshots. Keep loading promises
|
|
700
|
+
* only for actively subscribed queries and release unsubscribed ones.
|
|
701
|
+
*
|
|
702
|
+
* Fulfilled promises can be dropped immediately because no awaiter is
|
|
703
|
+
* waiting on them. Pending promises must stay alive until they resolve so
|
|
704
|
+
* current awaiters keep the same promise identity.
|
|
705
|
+
*/
|
|
706
|
+
const releaseUnsubscribedLoadingPromises = (): void => {
|
|
707
|
+
for (const [query, loadingPromise] of loadingPromisesByQuery) {
|
|
708
|
+
if (subscribedQueriesRefCount.has(query)) continue;
|
|
709
|
+
|
|
710
|
+
if (loadingPromise.promise.status === "fulfilled") {
|
|
711
|
+
loadingPromisesByQuery.delete(query);
|
|
712
|
+
} else {
|
|
713
|
+
loadingPromise.releaseOnResolve = true;
|
|
629
714
|
}
|
|
715
|
+
}
|
|
716
|
+
};
|
|
630
717
|
|
|
631
|
-
|
|
632
|
-
if (message.tabId && message.tabId === getTabId()) return;
|
|
718
|
+
const onMutateCompleteCallbacks = stack.use(createCallbacks(run.deps));
|
|
633
719
|
|
|
634
|
-
|
|
635
|
-
|
|
720
|
+
let exportDatabasePending = null as PromiseWithResolvers<
|
|
721
|
+
Uint8Array<ArrayBuffer>
|
|
722
|
+
> | null;
|
|
636
723
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
724
|
+
stack.defer(() => {
|
|
725
|
+
exportDatabasePending?.reject({ type: "EvoluDisposedError" });
|
|
726
|
+
exportDatabasePending = null;
|
|
727
|
+
});
|
|
641
728
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
729
|
+
const mutateBatch = stack.use(
|
|
730
|
+
createMicrotaskBatch<{
|
|
731
|
+
readonly change: MutationChange;
|
|
732
|
+
readonly onComplete: (() => void) | undefined;
|
|
733
|
+
}>((items) => {
|
|
734
|
+
console.debug("mutateBatch", { changeCount: items.length });
|
|
735
|
+
releaseUnsubscribedLoadingPromises();
|
|
736
|
+
|
|
737
|
+
postMessage({
|
|
738
|
+
type: "Mutate",
|
|
739
|
+
changes: mapArray(items, (item) => item.change),
|
|
740
|
+
onCompleteIds: items.flatMap((item) =>
|
|
741
|
+
item.onComplete
|
|
742
|
+
? [onMutateCompleteCallbacks.register(item.onComplete)]
|
|
743
|
+
: [],
|
|
744
|
+
),
|
|
745
|
+
subscribedQueries: subscribedQueriesRefCount.keys(),
|
|
746
|
+
});
|
|
747
|
+
}),
|
|
748
|
+
);
|
|
645
749
|
|
|
646
|
-
|
|
647
|
-
|
|
750
|
+
const queryBatch = stack.use(
|
|
751
|
+
createMicrotaskBatch<Query>((queries) => {
|
|
752
|
+
const dedupedQueries = new Set(queries);
|
|
753
|
+
assert(
|
|
754
|
+
isNonEmptySet(dedupedQueries),
|
|
755
|
+
"Expected non-empty query batch.",
|
|
756
|
+
);
|
|
757
|
+
console.debug("queryBatch", { queryCount: dedupedQueries.size });
|
|
758
|
+
postMessage({ type: "Query", queries: dedupedQueries });
|
|
759
|
+
}),
|
|
760
|
+
);
|
|
648
761
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
}
|
|
655
|
-
break;
|
|
656
|
-
}
|
|
762
|
+
const useOwnerBatch = stack.use(
|
|
763
|
+
createMicrotaskBatch<
|
|
764
|
+
ExtractType<EvoluInput, "UseOwner">["actions"][number]
|
|
765
|
+
>((actions) => postMessage({ type: "UseOwner", actions })),
|
|
766
|
+
);
|
|
657
767
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
768
|
+
let postMessage: (input: EvoluInput) => void;
|
|
769
|
+
|
|
770
|
+
// Scope worker/channel wiring and keep only postMessage outside.
|
|
771
|
+
{
|
|
772
|
+
const { createDbWorker, createMessageChannel, sharedWorker } = run.deps;
|
|
773
|
+
const dbWorkerChannel = stack.use(
|
|
774
|
+
createMessageChannel<DbWorkerOutput, DbWorkerInput>(),
|
|
775
|
+
);
|
|
776
|
+
const evoluChannel = stack.use(
|
|
777
|
+
createMessageChannel<EvoluInput, EvoluOutput>(),
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
evoluChannel.port1.onMessage = (message) => {
|
|
781
|
+
switch (message.type) {
|
|
782
|
+
case "OnPatchesByQuery": {
|
|
783
|
+
console.debug("onPatchesByQuery", {
|
|
784
|
+
queryCount: message.patchesByQuery.size,
|
|
785
|
+
onCompleteCount: message.onCompleteIds.length,
|
|
786
|
+
});
|
|
787
|
+
const state = rowsByQueryMapStore.get();
|
|
788
|
+
const nextRowsByQueryMap = new Map(state);
|
|
665
789
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
790
|
+
for (const [query, patches] of message.patchesByQuery) {
|
|
791
|
+
nextRowsByQueryMap.set(
|
|
792
|
+
query,
|
|
793
|
+
applyPatches(patches, state.get(query) ?? emptyArray),
|
|
794
|
+
);
|
|
795
|
+
}
|
|
670
796
|
|
|
671
|
-
|
|
797
|
+
for (const query of message.patchesByQuery.keys()) {
|
|
798
|
+
const loadingPromise = loadingPromisesByQuery.get(query);
|
|
799
|
+
if (!loadingPromise) continue;
|
|
672
800
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
Map<string, ValidMutationSize<any>>
|
|
676
|
-
>();
|
|
801
|
+
const rows = nextRowsByQueryMap.get(query);
|
|
802
|
+
assert(rows, "Expected patched query rows to exist.");
|
|
677
803
|
|
|
678
|
-
|
|
679
|
-
const getMutationType = (table: string, kind: MutationKind) => {
|
|
680
|
-
let types = mutationTypesCache.get(kind);
|
|
681
|
-
if (!types) {
|
|
682
|
-
types = new Map();
|
|
683
|
-
mutationTypesCache.set(kind, types);
|
|
684
|
-
}
|
|
685
|
-
let type = types.get(table);
|
|
686
|
-
if (!type) {
|
|
687
|
-
type = { insert: insertable, update: updateable, upsert: upsertable }[
|
|
688
|
-
kind
|
|
689
|
-
](schema[table]);
|
|
690
|
-
types.set(table, type);
|
|
691
|
-
}
|
|
692
|
-
return type;
|
|
693
|
-
};
|
|
804
|
+
fulfillLoadingPromise(loadingPromise, rows);
|
|
694
805
|
|
|
695
|
-
|
|
806
|
+
/**
|
|
807
|
+
* Release promises flagged during mutation when they finish
|
|
808
|
+
* resolving. This keeps in-flight promise identity stable and
|
|
809
|
+
* prevents stale cache entries after completion.
|
|
810
|
+
*/
|
|
811
|
+
if (loadingPromise.releaseOnResolve) {
|
|
812
|
+
loadingPromisesByQuery.delete(query);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
696
815
|
|
|
697
|
-
|
|
698
|
-
|
|
816
|
+
if (run.deps.flushSync && message.onCompleteIds.length > 0) {
|
|
817
|
+
run.deps.flushSync(() => {
|
|
818
|
+
rowsByQueryMapStore.set(nextRowsByQueryMap);
|
|
819
|
+
});
|
|
820
|
+
} else {
|
|
821
|
+
rowsByQueryMapStore.set(nextRowsByQueryMap);
|
|
822
|
+
}
|
|
699
823
|
|
|
700
|
-
|
|
824
|
+
for (const onCompleteId of message.onCompleteIds) {
|
|
825
|
+
onMutateCompleteCallbacks.execute(onCompleteId);
|
|
826
|
+
}
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
701
829
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
> = [];
|
|
830
|
+
case "RefreshQueries": {
|
|
831
|
+
releaseUnsubscribedLoadingPromises();
|
|
705
832
|
|
|
706
|
-
|
|
833
|
+
const queries = new Set<Query>([
|
|
834
|
+
...loadingPromisesByQuery.keys(),
|
|
835
|
+
...subscribedQueriesRefCount.keys(),
|
|
836
|
+
]);
|
|
707
837
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
table: TableName,
|
|
712
|
-
props: InferInput<
|
|
713
|
-
ObjectType<MutationMapping<(typeof schema)[TableName], Kind>>
|
|
714
|
-
>,
|
|
715
|
-
options?: MutationOptions,
|
|
716
|
-
): Result<
|
|
717
|
-
{ readonly id: InferType<(typeof schema)[TableName]["id"]> },
|
|
718
|
-
| ValidMutationSizeError
|
|
719
|
-
| InferErrors<
|
|
720
|
-
ObjectType<MutationMapping<(typeof schema)[TableName], Kind>>
|
|
721
|
-
>
|
|
722
|
-
> => {
|
|
723
|
-
const result = getMutationType(table, kind).fromUnknown(props);
|
|
724
|
-
|
|
725
|
-
const id =
|
|
726
|
-
kind === "insert"
|
|
727
|
-
? createId(deps)
|
|
728
|
-
: (props as unknown as { id: Id }).id;
|
|
729
|
-
|
|
730
|
-
if (options?.onlyValidate !== true) {
|
|
731
|
-
if (!result.ok) {
|
|
732
|
-
// Mark the transaction as invalid by pushing null
|
|
733
|
-
mutateMicrotaskQueue.push([null, undefined]);
|
|
734
|
-
} else {
|
|
735
|
-
const { id: _, isDeleted, ...values } = result.value;
|
|
736
|
-
|
|
737
|
-
const dbChange = {
|
|
738
|
-
table,
|
|
739
|
-
id,
|
|
740
|
-
values,
|
|
741
|
-
isInsert: kind === "insert" || kind === "upsert",
|
|
742
|
-
isDelete: SqliteBoolean.is(isDeleted)
|
|
743
|
-
? sqliteBooleanToBoolean(isDeleted)
|
|
744
|
-
: null,
|
|
745
|
-
};
|
|
838
|
+
if (isNonEmptySet(queries)) postMessage({ type: "Query", queries });
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
746
841
|
|
|
842
|
+
case "OnExport": {
|
|
747
843
|
assert(
|
|
748
|
-
|
|
749
|
-
|
|
844
|
+
exportDatabasePending,
|
|
845
|
+
"OnExport received without pending export.",
|
|
750
846
|
);
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
options?.onComplete,
|
|
755
|
-
]);
|
|
847
|
+
exportDatabasePending.resolve(message.file);
|
|
848
|
+
exportDatabasePending = null;
|
|
849
|
+
break;
|
|
756
850
|
}
|
|
757
851
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
}
|
|
852
|
+
default:
|
|
853
|
+
exhaustiveCheck(message);
|
|
761
854
|
}
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
sharedWorker.port.postMessage(
|
|
858
|
+
{
|
|
859
|
+
type: "CreateEvolu",
|
|
860
|
+
name,
|
|
861
|
+
evoluPort: evoluChannel.port2.native,
|
|
862
|
+
dbWorkerPort: dbWorkerChannel.port2.native,
|
|
863
|
+
},
|
|
864
|
+
[evoluChannel.port2.native, dbWorkerChannel.port2.native],
|
|
865
|
+
);
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* No stack.use because Evolu instances don't dispose DbWorker because
|
|
869
|
+
* it's SharedWorder responsibility. DbWorker can be used by another tab.
|
|
870
|
+
* That's required because SQLite WASM needs a single web worker.
|
|
871
|
+
*/
|
|
872
|
+
createDbWorker().postMessage(
|
|
873
|
+
{
|
|
874
|
+
type: "Init",
|
|
875
|
+
name,
|
|
876
|
+
consoleLevel: console.getLevel(),
|
|
877
|
+
sqliteSchema: evoluSchemaToSqliteSchema(schema, config.indexes),
|
|
878
|
+
encryptionKey: appOwner.encryptionKey,
|
|
879
|
+
memoryOnly,
|
|
880
|
+
port: dbWorkerChannel.port1.native,
|
|
881
|
+
},
|
|
882
|
+
[dbWorkerChannel.port1.native],
|
|
883
|
+
);
|
|
884
|
+
|
|
885
|
+
postMessage = evoluChannel.port1.postMessage;
|
|
886
|
+
|
|
887
|
+
stack.defer(() => {
|
|
888
|
+
postMessage({ type: "Dispose" });
|
|
889
|
+
});
|
|
890
|
+
}
|
|
762
891
|
|
|
763
|
-
|
|
892
|
+
const createMutation =
|
|
893
|
+
<Kind extends "insert" | "update" | "upsert">(
|
|
894
|
+
kind: Kind,
|
|
895
|
+
): Mutation<S, Kind> =>
|
|
896
|
+
(table, values, options) => {
|
|
897
|
+
assertNotDisposed(moved);
|
|
898
|
+
|
|
899
|
+
const {
|
|
900
|
+
id = createId(run.deps),
|
|
901
|
+
isDeleted,
|
|
902
|
+
...changeValues
|
|
903
|
+
} = values as { id?: Id; isDeleted?: unknown; [key: string]: unknown };
|
|
904
|
+
|
|
905
|
+
const dbChange = {
|
|
906
|
+
table,
|
|
907
|
+
id,
|
|
908
|
+
values: changeValues,
|
|
909
|
+
isInsert: kind === "insert" || kind === "upsert",
|
|
910
|
+
isDelete: SqliteBoolean.is(isDeleted)
|
|
911
|
+
? sqliteBooleanToBoolean(isDeleted)
|
|
912
|
+
: null,
|
|
913
|
+
};
|
|
764
914
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
| InferErrors<
|
|
769
|
-
ObjectType<MutationMapping<(typeof schema)[TableName], Kind>>
|
|
770
|
-
>,
|
|
915
|
+
assert(
|
|
916
|
+
DbChange.is(dbChange),
|
|
917
|
+
`Invalid DbChange for table '${String(table)}'.`,
|
|
771
918
|
);
|
|
919
|
+
|
|
920
|
+
mutateBatch.push({
|
|
921
|
+
change: { ...dbChange, ownerId: options?.ownerId ?? appOwner.id },
|
|
922
|
+
onComplete: options?.onComplete,
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
return { id };
|
|
772
926
|
};
|
|
773
927
|
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
|
|
928
|
+
const loadQuery = <R extends Row>(
|
|
929
|
+
query: Query<S, R>,
|
|
930
|
+
): Promise<QueryRows<R>> => {
|
|
931
|
+
assertNotDisposed(moved);
|
|
777
932
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
933
|
+
const loadingPromise = loadingPromisesByQuery.get(query);
|
|
934
|
+
if (loadingPromise) {
|
|
935
|
+
return loadingPromise.promise as Promise<QueryRows<R>>;
|
|
781
936
|
}
|
|
782
937
|
|
|
783
|
-
const
|
|
784
|
-
|
|
938
|
+
const { promise, resolve } = Promise.withResolvers<QueryRows>();
|
|
939
|
+
const typedPromise = promise as LoadingPromise["promise"];
|
|
940
|
+
typedPromise.status = "pending";
|
|
785
941
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
}
|
|
942
|
+
loadingPromisesByQuery.set(query, {
|
|
943
|
+
promise: typedPromise,
|
|
944
|
+
resolve,
|
|
945
|
+
releaseOnResolve: false,
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
queryBatch.push(query);
|
|
791
949
|
|
|
792
|
-
|
|
793
|
-
|
|
950
|
+
return typedPromise as Promise<QueryRows<R>>;
|
|
951
|
+
};
|
|
794
952
|
|
|
795
|
-
|
|
953
|
+
const getQueryRows = <R extends Row>(query: Query<S, R>): QueryRows<R> => {
|
|
954
|
+
assertNotDisposed(moved);
|
|
796
955
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
tabId: getTabId(),
|
|
800
|
-
changes,
|
|
801
|
-
onCompleteIds,
|
|
802
|
-
subscribedQueries: subscribedQueries.get(),
|
|
803
|
-
});
|
|
956
|
+
return (rowsByQueryMapStore.get().get(query) ??
|
|
957
|
+
emptyArray) as QueryRows<R>;
|
|
804
958
|
};
|
|
805
959
|
|
|
806
|
-
const
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
deps.console.log("[evolu]", "loadQuery", { queries });
|
|
823
|
-
dbWorker.postMessage({
|
|
824
|
-
type: "query",
|
|
825
|
-
tabId: getTabId(),
|
|
826
|
-
queries,
|
|
827
|
-
});
|
|
828
|
-
});
|
|
829
|
-
}
|
|
830
|
-
}
|
|
960
|
+
const useOwner = (
|
|
961
|
+
owner: ReadonlyOwner | Owner,
|
|
962
|
+
ownerTransports?: NonEmptyReadonlyArray<OwnerTransport>,
|
|
963
|
+
): UnuseOwner => {
|
|
964
|
+
assertNotDisposed(moved);
|
|
965
|
+
|
|
966
|
+
const effectiveTransports = ownerTransports ?? transports;
|
|
967
|
+
assertNonEmptyReadonlyArray(
|
|
968
|
+
effectiveTransports,
|
|
969
|
+
"useOwner requires explicit non-empty transports when config.transports is empty.",
|
|
970
|
+
);
|
|
971
|
+
|
|
972
|
+
const syncOwner: SyncOwner = {
|
|
973
|
+
owner,
|
|
974
|
+
transports: effectiveTransports,
|
|
975
|
+
};
|
|
831
976
|
|
|
832
|
-
|
|
833
|
-
|
|
977
|
+
useOwnerBatch.push({
|
|
978
|
+
owner: syncOwner,
|
|
979
|
+
action: "add",
|
|
980
|
+
});
|
|
834
981
|
|
|
835
|
-
|
|
836
|
-
queries: [...Q],
|
|
837
|
-
): [...QueriesToQueryRowsPromises<Q>] =>
|
|
838
|
-
queries.map(evolu.loadQuery) as [...QueriesToQueryRowsPromises<Q>],
|
|
982
|
+
let isUsed = true;
|
|
839
983
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
previousRows = rows;
|
|
847
|
-
listener();
|
|
984
|
+
return () => {
|
|
985
|
+
assert(isUsed, "UnuseOwner can be called only once.");
|
|
986
|
+
isUsed = false;
|
|
987
|
+
useOwnerBatch.push({
|
|
988
|
+
owner: syncOwner,
|
|
989
|
+
action: "remove",
|
|
848
990
|
});
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
unsubscribe();
|
|
852
|
-
};
|
|
853
|
-
},
|
|
991
|
+
};
|
|
992
|
+
};
|
|
854
993
|
|
|
855
|
-
|
|
856
|
-
(rowsStore.get().get(query) ?? emptyRows) as QueryRows<R>,
|
|
994
|
+
const moved = stack.move();
|
|
857
995
|
|
|
858
|
-
|
|
996
|
+
if (isNonEmptyArray(transports)) useOwner(appOwner);
|
|
859
997
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
998
|
+
return ok({
|
|
999
|
+
name,
|
|
1000
|
+
appOwner,
|
|
863
1001
|
|
|
864
1002
|
insert: createMutation("insert"),
|
|
865
1003
|
update: createMutation("update"),
|
|
866
1004
|
upsert: createMutation("upsert"),
|
|
867
1005
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
});
|
|
876
|
-
return promise;
|
|
877
|
-
},
|
|
878
|
-
|
|
879
|
-
restoreAppOwner: (mnemonic, options) => {
|
|
880
|
-
const { promise, resolve } = Promise.withResolvers<undefined>();
|
|
881
|
-
const onCompleteId = onCompleteCallbacks.register(resolve);
|
|
882
|
-
dbWorker.postMessage({
|
|
883
|
-
type: "reset",
|
|
884
|
-
onCompleteId,
|
|
885
|
-
reload: options?.reload ?? true,
|
|
886
|
-
restore: { mnemonic, dbSchema },
|
|
887
|
-
});
|
|
888
|
-
return promise;
|
|
889
|
-
},
|
|
890
|
-
|
|
891
|
-
reloadApp: () => {
|
|
892
|
-
deps.reloadApp(reloadUrl);
|
|
893
|
-
},
|
|
894
|
-
|
|
895
|
-
ensureSchema: (schema) => {
|
|
896
|
-
mutationTypesCache.clear();
|
|
897
|
-
const dbSchema = evoluSchemaToDbSchema(schema);
|
|
898
|
-
dbWorker.postMessage({ type: "ensureDbSchema", dbSchema });
|
|
899
|
-
},
|
|
900
|
-
|
|
901
|
-
exportDatabase: () => {
|
|
902
|
-
const { promise, resolve } =
|
|
903
|
-
Promise.withResolvers<Uint8Array<ArrayBuffer>>();
|
|
904
|
-
const onCompleteId = exportCallbacks.register(resolve);
|
|
905
|
-
dbWorker.postMessage({ type: "export", onCompleteId });
|
|
906
|
-
return promise;
|
|
907
|
-
},
|
|
908
|
-
|
|
909
|
-
useOwner: (owner) => {
|
|
910
|
-
const scheduleOwnerQueueProcessing = () => {
|
|
911
|
-
if (useOwnerMicrotaskQueue.length !== 1) return;
|
|
912
|
-
queueMicrotask(() => {
|
|
913
|
-
const queue = [...useOwnerMicrotaskQueue];
|
|
914
|
-
useOwnerMicrotaskQueue.length = 0;
|
|
1006
|
+
loadQuery,
|
|
1007
|
+
loadQueries: <Q extends Queries<S>>(
|
|
1008
|
+
queries: [...Q],
|
|
1009
|
+
): [...QueriesToQueryRowsPromises<Q>] =>
|
|
1010
|
+
queries.map((query) => loadQuery(query)) as [
|
|
1011
|
+
...QueriesToQueryRowsPromises<Q>,
|
|
1012
|
+
],
|
|
915
1013
|
|
|
916
|
-
|
|
917
|
-
|
|
1014
|
+
subscribeQuery: (query) => (listener) => {
|
|
1015
|
+
assertNotDisposed(moved);
|
|
918
1016
|
|
|
919
|
-
|
|
920
|
-
|
|
1017
|
+
subscribedQueriesRefCount.increment(query);
|
|
1018
|
+
let isSubscribed = true;
|
|
921
1019
|
|
|
922
|
-
|
|
923
|
-
queue[i];
|
|
1020
|
+
let previousRows: unknown = null;
|
|
924
1021
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1022
|
+
const unsubscribe = rowsByQueryMapStore.subscribe(() => {
|
|
1023
|
+
const rows = getQueryRows(query);
|
|
1024
|
+
if (previousRows === rows) return;
|
|
1025
|
+
previousRows = rows;
|
|
1026
|
+
listener();
|
|
1027
|
+
});
|
|
928
1028
|
|
|
929
|
-
|
|
1029
|
+
return () => {
|
|
1030
|
+
assert(
|
|
1031
|
+
isSubscribed,
|
|
1032
|
+
"subscribeQuery unsubscribe can be called only once.",
|
|
1033
|
+
);
|
|
1034
|
+
isSubscribed = false;
|
|
930
1035
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
eqArrayNumber(currentOwnerSerialized, otherOwnerSerialized)
|
|
934
|
-
) {
|
|
935
|
-
// Found cancel-out pair, skip both
|
|
936
|
-
skipIndices.add(i).add(j);
|
|
937
|
-
break;
|
|
938
|
-
}
|
|
939
|
-
}
|
|
1036
|
+
previousRows = null;
|
|
1037
|
+
unsubscribe();
|
|
940
1038
|
|
|
941
|
-
|
|
942
|
-
result.push([currentOwner, currentUse, currentOwnerSerialized]);
|
|
943
|
-
}
|
|
944
|
-
}
|
|
1039
|
+
if (moved.disposed) return;
|
|
945
1040
|
|
|
946
|
-
|
|
947
|
-
dbWorker.postMessage({ type: "useOwner", owner, use });
|
|
948
|
-
}
|
|
949
|
-
});
|
|
1041
|
+
subscribedQueriesRefCount.decrement(query);
|
|
950
1042
|
};
|
|
1043
|
+
},
|
|
1044
|
+
getQueryRows,
|
|
951
1045
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
const unuse = () => {
|
|
956
|
-
useOwnerMicrotaskQueue.push([owner, false, pack(owner)]);
|
|
957
|
-
scheduleOwnerQueueProcessing();
|
|
958
|
-
};
|
|
1046
|
+
exportDatabase: () => {
|
|
1047
|
+
assertNotDisposed(moved);
|
|
959
1048
|
|
|
960
|
-
|
|
1049
|
+
if (!exportDatabasePending) {
|
|
1050
|
+
exportDatabasePending =
|
|
1051
|
+
Promise.withResolvers<Uint8Array<ArrayBuffer>>();
|
|
1052
|
+
postMessage({ type: "Export" });
|
|
1053
|
+
}
|
|
1054
|
+
return exportDatabasePending.promise;
|
|
961
1055
|
},
|
|
962
1056
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1057
|
+
deleteDatabase: () => {
|
|
1058
|
+
assertNotDisposed(moved);
|
|
1059
|
+
todo();
|
|
966
1060
|
},
|
|
967
|
-
};
|
|
968
|
-
|
|
969
|
-
return evolu;
|
|
970
|
-
};
|
|
971
|
-
|
|
972
|
-
export const createQuery = <R extends Row>(
|
|
973
|
-
queryCallback: Parameters<CreateQuery<EvoluSchema>>[0],
|
|
974
|
-
options?: Parameters<CreateQuery<EvoluSchema>>[1],
|
|
975
|
-
): Query<R> => {
|
|
976
|
-
const compiledQuery = queryCallback(kysely as IntentionalNever).compile();
|
|
977
|
-
|
|
978
|
-
if (isSqlMutation(compiledQuery.sql))
|
|
979
|
-
throw new Error(
|
|
980
|
-
"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.",
|
|
981
|
-
);
|
|
982
|
-
|
|
983
|
-
return serializeQuery({
|
|
984
|
-
sql: compiledQuery.sql as SafeSql,
|
|
985
|
-
parameters: compiledQuery.parameters as NonNullable<
|
|
986
|
-
SqliteQuery["parameters"]
|
|
987
|
-
>,
|
|
988
|
-
...(options && { options }),
|
|
989
|
-
});
|
|
990
|
-
};
|
|
991
1061
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
readonly isNew: boolean;
|
|
998
|
-
};
|
|
999
|
-
|
|
1000
|
-
resolve: (query: Query, rows: ReadonlyArray<Row>) => void;
|
|
1001
|
-
|
|
1002
|
-
releaseUnsubscribedOnMutation: () => void;
|
|
1062
|
+
deleteOwner: (owner) => {
|
|
1063
|
+
assertNotDisposed(moved);
|
|
1064
|
+
void owner;
|
|
1065
|
+
todo();
|
|
1066
|
+
},
|
|
1003
1067
|
|
|
1004
|
-
|
|
1005
|
-
}
|
|
1068
|
+
useOwner,
|
|
1006
1069
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
reason?: unknown;
|
|
1070
|
+
[Symbol.asyncDispose]: () => {
|
|
1071
|
+
console.info("dispose");
|
|
1072
|
+
return moved.disposeAsync();
|
|
1073
|
+
},
|
|
1074
|
+
} as Evolu<S>);
|
|
1013
1075
|
};
|
|
1014
|
-
resolve: (rows: QueryRows) => void;
|
|
1015
|
-
releaseOnResolve: boolean;
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
const createLoadingPromises = (
|
|
1019
|
-
subscribedQueries: SubscribedQueries,
|
|
1020
|
-
): LoadingPromises => {
|
|
1021
|
-
const loadingPromiseMap = new Map<Query, LoadingPromise>();
|
|
1022
|
-
|
|
1023
|
-
return {
|
|
1024
|
-
get: <R extends Row>(
|
|
1025
|
-
query: Query<R>,
|
|
1026
|
-
): {
|
|
1027
|
-
readonly promise: Promise<QueryRows<R>>;
|
|
1028
|
-
readonly isNew: boolean;
|
|
1029
|
-
} => {
|
|
1030
|
-
let loadingPromise = loadingPromiseMap.get(query);
|
|
1031
|
-
const isNew = !loadingPromise;
|
|
1032
|
-
if (!loadingPromise) {
|
|
1033
|
-
const { promise, resolve } = Promise.withResolvers<QueryRows>();
|
|
1034
|
-
loadingPromise = { resolve, promise, releaseOnResolve: false };
|
|
1035
|
-
loadingPromiseMap.set(query, loadingPromise);
|
|
1036
|
-
}
|
|
1037
|
-
return {
|
|
1038
|
-
promise: loadingPromise.promise as Promise<QueryRows<R>>,
|
|
1039
|
-
isNew,
|
|
1040
|
-
};
|
|
1041
|
-
},
|
|
1042
|
-
|
|
1043
|
-
resolve: (query, rows) => {
|
|
1044
|
-
const loadingPromise = loadingPromiseMap.get(query);
|
|
1045
|
-
if (!loadingPromise) return;
|
|
1046
|
-
|
|
1047
|
-
if (loadingPromise.promise.status !== "fulfilled") {
|
|
1048
|
-
loadingPromise.resolve(rows);
|
|
1049
|
-
} else {
|
|
1050
|
-
loadingPromise.promise = Promise.resolve(rows);
|
|
1051
|
-
}
|
|
1052
1076
|
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
};
|
|
1077
|
+
// case "onReset": {
|
|
1078
|
+
// if (message.reload) {
|
|
1079
|
+
// deps.reloadApp(reloadUrl);
|
|
1080
|
+
// } else {
|
|
1081
|
+
// onCompleteCallbacks.execute(message.onCompleteId);
|
|
1082
|
+
// }
|
|
1083
|
+
// break;
|
|
1084
|
+
// }
|
|
1085
|
+
|
|
1086
|
+
// resetAppOwner: (_options) => {
|
|
1087
|
+
// const { promise, resolve } = Promise.withResolvers<undefined>();
|
|
1088
|
+
// const _onCompleteId = onCompleteCallbacks.register(resolve);
|
|
1089
|
+
// // dbWorker.postMessage({
|
|
1090
|
+
// // type: "reset",
|
|
1091
|
+
// // onCompleteId,
|
|
1092
|
+
// // reload: options?.reload ?? true,
|
|
1093
|
+
// // });
|
|
1094
|
+
// return promise;
|
|
1095
|
+
// },
|
|
1096
|
+
|
|
1097
|
+
// restoreAppOwner: (_mnemonic, _options) => {
|
|
1098
|
+
// const { promise, resolve } = Promise.withResolvers<undefined>();
|
|
1099
|
+
// const _onCompleteId = onCompleteCallbacks.register(resolve);
|
|
1100
|
+
// // dbWorker.postMessage({
|
|
1101
|
+
// // type: "reset",
|
|
1102
|
+
// // onCompleteId,
|
|
1103
|
+
// // reload: options?.reload ?? true,
|
|
1104
|
+
// // restore: { mnemonic, sqliteSchema },
|
|
1105
|
+
// // });
|
|
1106
|
+
// return promise;
|
|
1107
|
+
// },
|
|
1108
|
+
|
|
1109
|
+
// reloadApp: () => {
|
|
1110
|
+
// // TODO:
|
|
1111
|
+
// // deps.reloadApp(reloadUrl);
|
|
1112
|
+
// },
|
|
1113
|
+
|
|
1114
|
+
// ensureSchema: (schema) => {
|
|
1115
|
+
// mutationTypesCache.clear();
|
|
1116
|
+
// const sqliteSchema = evoluSchemaToSqliteSchema(schema);
|
|
1117
|
+
// dbWorker.postMessage({ type: "ensureSqliteSchema", sqliteSchema });
|
|
1118
|
+
// },
|
|
1119
|
+
|
|
1120
|
+
// /**
|
|
1121
|
+
// * Delete {@link AppOwner} and all their data from the current device. After
|
|
1122
|
+
// * the deletion, Evolu will purge the application state. For browsers, this
|
|
1123
|
+
// * will reload all tabs using Evolu. For native apps, it will restart the
|
|
1124
|
+
// * app.
|
|
1125
|
+
// *
|
|
1126
|
+
// * Reloading can be turned off via options if you want to provide a different
|
|
1127
|
+
// * UX.
|
|
1128
|
+
// */
|
|
1129
|
+
// readonly resetAppOwner: (options?: {
|
|
1130
|
+
// readonly reload?: boolean;
|
|
1131
|
+
// }) => Promise<void>;
|
|
1132
|
+
|
|
1133
|
+
// /**
|
|
1134
|
+
// * Restore {@link AppOwner} with all their synced data. It uses
|
|
1135
|
+
// * {@link Evolu.resetAppOwner}, so be careful.
|
|
1136
|
+
// */
|
|
1137
|
+
// readonly restoreAppOwner: (
|
|
1138
|
+
// mnemonic: Mnemonic,
|
|
1139
|
+
// options?: {
|
|
1140
|
+
// readonly reload?: boolean;
|
|
1141
|
+
// },
|
|
1142
|
+
// ) => Promise<void>;
|