@evolu/common 8.10.0 → 8.11.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/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 +336 -211
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +102 -15
- 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 +94 -16
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +118 -38
- 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/Schema.d.ts +335 -21
- 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 +192 -14
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +81 -20
- 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/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 +15 -6
- package/src/local-first/Db.ts +644 -339
- package/src/local-first/Evolu.test.ts +686 -21
- package/src/local-first/Evolu.ts +450 -228
- package/src/local-first/Owner.ts +13 -30
- package/src/local-first/Protocol.test.ts +617 -10
- package/src/local-first/Protocol.ts +196 -72
- package/src/local-first/Query.ts +8 -15
- package/src/local-first/Schema.test.ts +143 -0
- package/src/local-first/Schema.ts +363 -24
- package/src/local-first/Shared.test.ts +7731 -559
- package/src/local-first/Shared.ts +2036 -267
- package/src/local-first/Storage.ts +218 -32
- 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
|
@@ -1,6 +1,213 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Platform-agnostic Evolu SharedWorker.
|
|
3
3
|
*
|
|
4
|
+
* ## Builds
|
|
5
|
+
*
|
|
6
|
+
* Tabs of different app builds can be open at once, for example an old tab
|
|
7
|
+
* during a deploy. Bundlers derive the worker script's URL from its content, so
|
|
8
|
+
* every build whose worker code differs gets its own SharedWorker, and all of
|
|
9
|
+
* them open the same databases. Only one may use them at a time: a request
|
|
10
|
+
* retried after another worker wrote from the same stored clock can reuse that
|
|
11
|
+
* write's timestamps and then is skipped as already stored.
|
|
12
|
+
*
|
|
13
|
+
* The worker therefore takes an origin-wide lock before it answers any tab and
|
|
14
|
+
* holds it for its lifetime. A worker of another build waits, with its tabs'
|
|
15
|
+
* messages buffered, until every tab of the first one is closed or reloaded and
|
|
16
|
+
* the browser ends it. The lock is the one that earlier releases take in their
|
|
17
|
+
* leader tab, so they are excluded too. An earlier release's worker can outlive
|
|
18
|
+
* its leader tab and resume once the lock is free; it computes new timestamps
|
|
19
|
+
* when it retries, so it cannot reuse another worker's. A tab of an earlier
|
|
20
|
+
* release that opens while a worker of this release runs gets no response to
|
|
21
|
+
* its database requests until it reloads, and until then it also blocks workers
|
|
22
|
+
* that start after this one ends.
|
|
23
|
+
*
|
|
24
|
+
* On the web, the wait is usually short, because tabs of the running build
|
|
25
|
+
* reload to load the build the server now serves:
|
|
26
|
+
*
|
|
27
|
+
* 1. A worker tells the tabs that connect before it holds the lock that they wait,
|
|
28
|
+
* and such a tab announces the worker with {@link BuildWaiting}. It announces
|
|
29
|
+
* again when a tab that connects asks with {@link BuildWaitingRequest}, so a
|
|
30
|
+
* tab that started or connected after the first announcement learns of it
|
|
31
|
+
* too.
|
|
32
|
+
* 2. A tab connected to another worker reloads with {@link ReloadApp}: at once if
|
|
33
|
+
* the user is not in it, otherwise once they leave it, so a tab never
|
|
34
|
+
* reloads while the user works in it. Focus can leave a page from a frame
|
|
35
|
+
* without a window event, so such a tab checks once a second whether it
|
|
36
|
+
* still has focus. A tab that still waits itself keeps the announcements it
|
|
37
|
+
* receives and handles them once it connects, because the lock can pass to
|
|
38
|
+
* its worker first.
|
|
39
|
+
* 3. A page that such a reload loaded never announces, and a tab reloads at most
|
|
40
|
+
* once for each waiting worker, so two builds cannot keep reloading each
|
|
41
|
+
* other, even when a reload loads the running build again. A tab without
|
|
42
|
+
* session storage cannot record its reloads, so it does not reload.
|
|
43
|
+
*
|
|
44
|
+
* Only the web platform does this, because only there do builds coexist. A
|
|
45
|
+
* reload loses UI state the app did not persist, so apps keep drafts in
|
|
46
|
+
* local-only tables. A write a background tab has in flight can be lost, as
|
|
47
|
+
* when a tab crashes.
|
|
48
|
+
*
|
|
49
|
+
* The wait lasts while a tab of the running build does not reload: a tab of an
|
|
50
|
+
* earlier release, a tab Safari has frozen, a tab without session storage, or a
|
|
51
|
+
* tab whose reload loads the running build again, for example because another
|
|
52
|
+
* Evolu app shares the origin or a cache still serves the old build. A worker
|
|
53
|
+
* still waiting after three seconds reports {@link OtherBuildRunningError} to
|
|
54
|
+
* its tabs. Every build broadcasts console entries and errors on
|
|
55
|
+
* {@link consoleEntryOrErrorBroadcastChannelName}, so a waiting tab also prints
|
|
56
|
+
* the running build's output and reports its errors as its own `evoluError`.
|
|
57
|
+
*
|
|
58
|
+
* The tabs of one worker elect the host of its DbWorkers among themselves, with
|
|
59
|
+
* a lock scoped to the worker, so a tab of another worker never hosts them.
|
|
60
|
+
* Builds that differ only in DbWorker code share one SharedWorker, and a tab of
|
|
61
|
+
* either can host its DbWorkers.
|
|
62
|
+
*
|
|
63
|
+
* Safari suspends, rather than ends, a worker whose tabs are all in its
|
|
64
|
+
* back-forward cache, and the suspended worker keeps the lock. There, a waiting
|
|
65
|
+
* build also waits until Safari drops those pages, or until the user goes back
|
|
66
|
+
* to one of them, which reloads it on the web.
|
|
67
|
+
*
|
|
68
|
+
* ## Storage
|
|
69
|
+
*
|
|
70
|
+
* A platform that can lack persistent storage, as a browser does in Safari's
|
|
71
|
+
* Private Browsing, provides {@link PersistentStorageDep}. The worker checks it
|
|
72
|
+
* once, after it takes the build lock and before any DbWorker starts. Without
|
|
73
|
+
* persistent storage, every DbWorker it starts keeps its database in memory,
|
|
74
|
+
* replacements included, and each tab that connects is told with
|
|
75
|
+
* `StorageUnavailable`. The decision holds for the worker's lifetime, so all
|
|
76
|
+
* its tabs see one mode, and the next worker checks again.
|
|
77
|
+
*
|
|
78
|
+
* The check cannot tell a private session from a storage failure, but memory
|
|
79
|
+
* loses nothing that refusing to start would have kept, and the persistent
|
|
80
|
+
* database stays untouched. Each DbWorker keeps its own memory, so when the tab
|
|
81
|
+
* hosting it closes, or on the web navigates away, data that exists only
|
|
82
|
+
* locally or has not synced yet is lost, even though its replacement starts in
|
|
83
|
+
* memory too.
|
|
84
|
+
*
|
|
85
|
+
* ## Synchronization routing
|
|
86
|
+
*
|
|
87
|
+
* WebSocket transports are shared resources keyed by their configuration and
|
|
88
|
+
* claimed by owner ID, so every tenant (one named local database) using an
|
|
89
|
+
* owner shares that owner's sockets, whichever tenant claimed them. An incoming
|
|
90
|
+
* frame is offered to every tenant with a writable registration for its owner,
|
|
91
|
+
* and each tenant reconciles independently: the protocol exchange is stateless
|
|
92
|
+
* per message and message writes are idempotent, so a response that answered
|
|
93
|
+
* another tenant's request is still a valid reconciliation step.
|
|
94
|
+
*
|
|
95
|
+
* Traffic goes only where it is needed. A continuation returns to the transport
|
|
96
|
+
* that produced the response, and a round started by a socket opening or by a
|
|
97
|
+
* tenant's first use of a transport for an owner goes through that transport.
|
|
98
|
+
* Explicit synchronization requests and mutation uploads go to every open
|
|
99
|
+
* transport claimed for the owner. A write uploads through the database's
|
|
100
|
+
* writable registrations for its owner, whichever instance made it, even one
|
|
101
|
+
* disposed before the database worker answered. When a relay frame stores new
|
|
102
|
+
* messages, the tenant requests a round through each other transport claimed
|
|
103
|
+
* for the owner, so data learned from one relay reaches the others. A closed
|
|
104
|
+
* transport reconciles when it opens, and a replacement leader reconciles every
|
|
105
|
+
* transport again, because a response reporting stored messages may have been
|
|
106
|
+
* lost.
|
|
107
|
+
*
|
|
108
|
+
* Relays omit the sending socket when broadcasting an upload, so the uploader
|
|
109
|
+
* also delivers it as local Broadcast frames to every other tenant with
|
|
110
|
+
* writable access to the owner, even while sockets are closed. A copy keeps the
|
|
111
|
+
* uploader's target, and a recipient that stores new continuation messages
|
|
112
|
+
* reconciles them through the transports outside that target. Local delivery
|
|
113
|
+
* forwards uploads, including historical messages sent during reconciliation,
|
|
114
|
+
* but does not reconcile local database histories with each other. That waits
|
|
115
|
+
* until replication scopes and retention semantics are defined.
|
|
116
|
+
*
|
|
117
|
+
* ## Sync state
|
|
118
|
+
*
|
|
119
|
+
* The shared worker publishes one plain snapshot, {@link SyncState}, of every
|
|
120
|
+
* transport it manages and every database and owner registration it holds, with
|
|
121
|
+
* one route per writable registration and transport, as specified below.
|
|
122
|
+
* {@link syncStateToOwnerSyncStates} derives one state per database and owner.
|
|
123
|
+
*
|
|
124
|
+
* Each worker broadcasts snapshots on its own channel, whose name a connecting
|
|
125
|
+
* tab receives through its port, so a tab never hears another worker, such as
|
|
126
|
+
* one of a different app version, and
|
|
127
|
+
* {@link SyncStateDep.syncState | deps.syncState} keeps the last snapshot. The
|
|
128
|
+
* worker publishes after every change it observes; a transition without an
|
|
129
|
+
* event, such as a closed socket starting to reconnect, appears with the next
|
|
130
|
+
* snapshot. The snapshot lives in worker memory only, so a new worker starts
|
|
131
|
+
* empty.
|
|
132
|
+
*
|
|
133
|
+
* ## Synchronization completion
|
|
134
|
+
*
|
|
135
|
+
* A protocol frame carries the owner ID and the message type but nothing that
|
|
136
|
+
* correlates it with a request, and a relay answers a converged round, an
|
|
137
|
+
* upload that fits one frame, and an Unsubscribe alike with a header-only
|
|
138
|
+
* Response. Every tenant with a writable registration applies every frame for
|
|
139
|
+
* its owner, so a tenant cannot tell which response answered its own request.
|
|
140
|
+
*
|
|
141
|
+
* Completion is therefore counted, not attributed. The shared worker counts
|
|
142
|
+
* outstanding requests per owner and socket: every Request sent on an open
|
|
143
|
+
* socket, including an Unsubscribe, increments the count; every Response,
|
|
144
|
+
* including a relay's version-mismatch reply, which has no message type,
|
|
145
|
+
* decrements it; and an opening socket resets it, because requests on the
|
|
146
|
+
* previous connection are never answered. A route, one tenant's use of one
|
|
147
|
+
* owner through one transport, is complete when:
|
|
148
|
+
*
|
|
149
|
+
* - The tenant has not refused startup and the socket is open.
|
|
150
|
+
* - The count is zero.
|
|
151
|
+
* - The tenant has no apply for the owner queued for that transport or for every
|
|
152
|
+
* transport, because a frame is applied asynchronously, so the count reads
|
|
153
|
+
* zero in the middle of a chain.
|
|
154
|
+
* - The tenant has no replicated write for the owner queued, because its upload
|
|
155
|
+
* is sent only after the database worker answers it.
|
|
156
|
+
* - The tenant has sent a round through the transport since the last event that
|
|
157
|
+
* requires one: its first use of the transport for the owner, the socket
|
|
158
|
+
* opening, an explicit request, a replacement leader, or storing messages
|
|
159
|
+
* from another transport. No failed or aborted result has arrived on the
|
|
160
|
+
* route since.
|
|
161
|
+
*
|
|
162
|
+
* A reconciliation chain ends only with a converged result, a failure, an
|
|
163
|
+
* abort, a dropped frame, or a continuation that finds the socket closed, and
|
|
164
|
+
* relay errors reach every applying tenant, so these conditions mean every
|
|
165
|
+
* chain, including this tenant's, converged. A local Broadcast from a sibling
|
|
166
|
+
* tenant holds every route of its owner until it is applied, because it arrives
|
|
167
|
+
* without a request of its own; one that fails to apply requires a round
|
|
168
|
+
* through every transport.
|
|
169
|
+
*
|
|
170
|
+
* A failed result on a route requests one round through it. Any further failure
|
|
171
|
+
* before the route completes waits for an explicit request or a reopen, so a
|
|
172
|
+
* persistent failure cannot loop; a converged reply in between does not end the
|
|
173
|
+
* wait, because it may answer another tenant's round on the shared socket. An
|
|
174
|
+
* aborted apply leaves its routes incomplete without a retry. An exception
|
|
175
|
+
* while the database worker creates a round is logged there and fails the
|
|
176
|
+
* round's routes with `SyncFailed` without a retry; other unexpected SQLite
|
|
177
|
+
* exceptions remain unsupported and can panic the database worker. A frame the
|
|
178
|
+
* relay silently drops, such as invalid data, leaves the count above zero until
|
|
179
|
+
* the liveness rule below replaces the socket.
|
|
180
|
+
*
|
|
181
|
+
* ### Liveness
|
|
182
|
+
*
|
|
183
|
+
* An open socket can be dead without a close event, when a NAT drops an idle
|
|
184
|
+
* mapping or the path fails while nothing is sent. The relay pings every
|
|
185
|
+
* connection and terminates one from which nothing has arrived since the
|
|
186
|
+
* previous ping, which also keeps NAT mappings alive. The shared worker
|
|
187
|
+
* reconnects a socket when a request for an owner has been outstanding for
|
|
188
|
+
* ninety seconds, enough for a 1 MB frame at about 90 kbit/s, with no Response
|
|
189
|
+
* for that owner since. Frames for other owners do not count: they prove the
|
|
190
|
+
* socket alive, not that the request was received.
|
|
191
|
+
*
|
|
192
|
+
* A slow link looks like a dead one, because the browser reports no transfer
|
|
193
|
+
* progress and a large frame saves nothing until it arrives whole. Each timeout
|
|
194
|
+
* therefore doubles the transport's timeout, up to twenty-four minutes: a
|
|
195
|
+
* connection that reopens but times out again is more likely slow than dead.
|
|
196
|
+
* The timeout belongs to the transport, not to an owner, because a small reply
|
|
197
|
+
* for one owner can wait behind another owner's large frame on the socket. A
|
|
198
|
+
* grown timeout lasts while a request is outstanding on the socket or a
|
|
199
|
+
* database that has not refused startup has an incomplete route through it,
|
|
200
|
+
* including one waiting after a failure, and ends with the transport. A reply's
|
|
201
|
+
* speed proves nothing, because a recovery on a slow link starts with small
|
|
202
|
+
* replies that arrive quickly. The reopen resets the counts and starts the open
|
|
203
|
+
* rounds, so a dropped frame delays a route instead of stranding it.
|
|
204
|
+
*
|
|
205
|
+
* The shared worker sends nothing while idle, so a path that dies then may go
|
|
206
|
+
* unnoticed until its next request; until then, changes from other devices stop
|
|
207
|
+
* arriving while routes still read complete. A periodic empty request would
|
|
208
|
+
* notice it without waiting for the app to send. That is deferred, because
|
|
209
|
+
* relay pings already keep NAT mappings alive, the common cause.
|
|
210
|
+
*
|
|
4
211
|
* @module
|
|
5
212
|
*/
|
|
6
213
|
import { type NonEmptyReadonlyArray } from "../Array.ts";
|
|
@@ -12,16 +219,18 @@ import { type Result } from "../Result.ts";
|
|
|
12
219
|
import type { NonEmptyReadonlySet } from "../Set.ts";
|
|
13
220
|
import type { SqliteSchema } from "../Sqlite.ts";
|
|
14
221
|
import { AbortError, type Task } from "../Task.ts";
|
|
15
|
-
import
|
|
16
|
-
import type
|
|
222
|
+
import { type Millis } from "../Time.ts";
|
|
223
|
+
import { type ExtractTyped, type Id, type InferType, type LiteralType, type Name, type ObjectType, type Typed } from "../Type.ts";
|
|
224
|
+
import type { CreateWebSocketDep, WebSocketError, WebSocketReadyState } from "../WebSocket.ts";
|
|
17
225
|
import type { SharedWorker as CommonSharedWorker, CreateBroadcastChannelDep, CreateMessageChannelDep, NativeMessagePort, SharedWorkerSelf, WorkerDeps } from "../Worker.ts";
|
|
18
|
-
import type { DbWorkerInit } from "./Db.ts";
|
|
19
|
-
import type { EvoluError } from "./
|
|
226
|
+
import type { DbWorkerInit, UnsupportedDbVersionError } from "./Db.ts";
|
|
227
|
+
import type { EvoluError } from "./Evolu.ts";
|
|
20
228
|
import type { Owner, OwnerId, SyncOwner } from "./Owner.ts";
|
|
21
229
|
import { type ApplyProtocolMessageAsClientResult, type ProtocolError, type ProtocolMessage } from "./Protocol.ts";
|
|
22
230
|
import { type Patch, type Query, type RowsByQueryMap } from "./Query.ts";
|
|
23
|
-
import type
|
|
24
|
-
import type { CrdtMessage } from "./Storage.ts";
|
|
231
|
+
import { type MutationChange } from "./Schema.ts";
|
|
232
|
+
import type { CrdtMessage, StorageWriteMessagesError } from "./Storage.ts";
|
|
233
|
+
import { type Timestamp } from "./Timestamp.ts";
|
|
25
234
|
export type SharedWorker = CommonSharedWorker<SharedWorkerInput, SharedWorkerOutput>;
|
|
26
235
|
export interface SharedWorkerDep {
|
|
27
236
|
readonly sharedWorker: SharedWorker;
|
|
@@ -29,6 +238,12 @@ export interface SharedWorkerDep {
|
|
|
29
238
|
export type SharedWorkerInput = {
|
|
30
239
|
readonly type: "AnnounceTabLeader";
|
|
31
240
|
readonly consoleLevel: ConsoleLevel;
|
|
241
|
+
} | {
|
|
242
|
+
/**
|
|
243
|
+
* Asks the worker to broadcast a {@link SyncState} on its channel, which
|
|
244
|
+
* the tab opened after receiving its name.
|
|
245
|
+
*/
|
|
246
|
+
readonly type: "RequestSyncState";
|
|
32
247
|
} | {
|
|
33
248
|
readonly type: "CreateEvolu";
|
|
34
249
|
readonly name: Name;
|
|
@@ -39,7 +254,38 @@ export type SharedWorkerInput = {
|
|
|
39
254
|
readonly memoryOnly: boolean;
|
|
40
255
|
readonly evoluPort: NativeMessagePort<EvoluOutput, EvoluInput>;
|
|
41
256
|
};
|
|
42
|
-
export type SharedWorkerOutput = DbWorkerInit
|
|
257
|
+
export type SharedWorkerOutput = DbWorkerInit | {
|
|
258
|
+
/**
|
|
259
|
+
* Sent to one tab only: its database refused startup, or another build
|
|
260
|
+
* keeps this worker waiting.
|
|
261
|
+
*/
|
|
262
|
+
readonly type: "Error";
|
|
263
|
+
readonly error: UnsupportedDbVersionError | OtherBuildRunningError;
|
|
264
|
+
} | {
|
|
265
|
+
/**
|
|
266
|
+
* Sent to a tab that connects while the worker waits for the build lock;
|
|
267
|
+
* see Builds. `Connected` follows once the worker holds it.
|
|
268
|
+
*/
|
|
269
|
+
readonly type: "Waiting";
|
|
270
|
+
readonly workerId: SharedWorkerId;
|
|
271
|
+
} | {
|
|
272
|
+
/**
|
|
273
|
+
* Sent to a connecting tab once the worker holds the build lock; see
|
|
274
|
+
* Builds in this module's documentation. The tab elects the host of the
|
|
275
|
+
* worker's DbWorkers among the worker's tabs, scoped by `workerId`, and
|
|
276
|
+
* listens for {@link SyncState} on `syncStateChannelName`.
|
|
277
|
+
*/
|
|
278
|
+
readonly type: "Connected";
|
|
279
|
+
readonly workerId: SharedWorkerId;
|
|
280
|
+
readonly syncStateChannelName: string;
|
|
281
|
+
} | {
|
|
282
|
+
/**
|
|
283
|
+
* Sent to a connecting tab after `Connected` when the platform offers no
|
|
284
|
+
* persistent storage, so the worker keeps every database in memory; see
|
|
285
|
+
* Storage in this module's documentation.
|
|
286
|
+
*/
|
|
287
|
+
readonly type: "StorageUnavailable";
|
|
288
|
+
};
|
|
43
289
|
export type ConsoleEntryOrError = {
|
|
44
290
|
readonly type: "ConsoleEntry";
|
|
45
291
|
readonly entry: ConsoleEntry;
|
|
@@ -48,6 +294,236 @@ export type ConsoleEntryOrError = {
|
|
|
48
294
|
readonly error: EvoluError;
|
|
49
295
|
};
|
|
50
296
|
export declare const consoleEntryOrErrorBroadcastChannelName = "evolu:console-entry-or-error";
|
|
297
|
+
/** Identifies one running SharedWorker instance. */
|
|
298
|
+
export declare const SharedWorkerId: import("../Type.ts").TableId<"SharedWorker">;
|
|
299
|
+
export type SharedWorkerId = typeof SharedWorkerId.Output;
|
|
300
|
+
/**
|
|
301
|
+
* The channel on which a tab announces its waiting worker with
|
|
302
|
+
* {@link BuildWaiting}; see Builds. Builds of different releases share it, so
|
|
303
|
+
* its name and messages never change.
|
|
304
|
+
*/
|
|
305
|
+
export declare const buildsBroadcastChannelName = "evolu:builds";
|
|
306
|
+
/**
|
|
307
|
+
* Posted on {@link buildsBroadcastChannelName} by a tab whose worker waits for
|
|
308
|
+
* the build lock, unless an automatic reload loaded the page, when it starts
|
|
309
|
+
* waiting and for each {@link BuildWaitingRequest}. A tab connected to another
|
|
310
|
+
* worker reloads for it; see Builds.
|
|
311
|
+
*/
|
|
312
|
+
export declare const BuildWaiting: ObjectType<{
|
|
313
|
+
readonly type: LiteralType<"BuildWaiting">;
|
|
314
|
+
readonly workerId: typeof SharedWorkerId;
|
|
315
|
+
}>;
|
|
316
|
+
export interface BuildWaiting extends InferType<typeof BuildWaiting> {
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Posted on {@link buildsBroadcastChannelName} by a tab once it connects, asking
|
|
320
|
+
* tabs whose worker waits to post {@link BuildWaiting} again; see Builds.
|
|
321
|
+
*/
|
|
322
|
+
export declare const BuildWaitingRequest: ObjectType<{
|
|
323
|
+
readonly type: LiteralType<"BuildWaitingRequest">;
|
|
324
|
+
}>;
|
|
325
|
+
export interface BuildWaitingRequest extends InferType<typeof BuildWaitingRequest> {
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Another build of the app holds the local databases, and this tab waits until
|
|
329
|
+
* every tab of that build is closed or reloaded; see Builds.
|
|
330
|
+
*
|
|
331
|
+
* Tabs of the other build usually reload by themselves, so this is reported
|
|
332
|
+
* only when the wait lasts, for example because the user is still in a tab of
|
|
333
|
+
* the other build, or that tab runs an earlier release or is frozen by Safari.
|
|
334
|
+
* Apps can ask the user to close the app's other tabs. It is cleared once the
|
|
335
|
+
* wait ends.
|
|
336
|
+
*/
|
|
337
|
+
export interface OtherBuildRunningError extends Typed<"OtherBuildRunningError"> {
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* A snapshot of the transports and databases the shared worker manages.
|
|
341
|
+
*
|
|
342
|
+
* See the Sync state section of this module's documentation.
|
|
343
|
+
*/
|
|
344
|
+
export interface SyncState {
|
|
345
|
+
readonly transports: ReadonlyArray<SyncTransport>;
|
|
346
|
+
readonly tenants: ReadonlyArray<SyncTenant>;
|
|
347
|
+
}
|
|
348
|
+
/** One WebSocket, shared by every owner and database claiming it. */
|
|
349
|
+
export interface SyncTransport {
|
|
350
|
+
/** Opaque and stable for the transport's lifetime, across socket replacements. */
|
|
351
|
+
readonly id: SyncTransportId;
|
|
352
|
+
/** The URL without its query, which carries the owner ID. */
|
|
353
|
+
readonly label: string;
|
|
354
|
+
readonly readyState: WebSocketReadyState;
|
|
355
|
+
/** When the connection last opened, or null before its first open. */
|
|
356
|
+
readonly openedAt: Millis | null;
|
|
357
|
+
/** When the connection last closed, or null before its first close. */
|
|
358
|
+
readonly closedAt: Millis | null;
|
|
359
|
+
/**
|
|
360
|
+
* The last error, retained after a successful reconnect; null if none. Errors
|
|
361
|
+
* while reconnecting are routine.
|
|
362
|
+
*/
|
|
363
|
+
readonly error: SyncTransportError | null;
|
|
364
|
+
}
|
|
365
|
+
export type SyncTransportId = Id & Brand<"SyncTransport">;
|
|
366
|
+
export interface SyncTransportError {
|
|
367
|
+
readonly type: WebSocketError["type"];
|
|
368
|
+
readonly at: Millis;
|
|
369
|
+
}
|
|
370
|
+
/** One named local database and the owners it registered. */
|
|
371
|
+
export interface SyncTenant {
|
|
372
|
+
readonly name: Name;
|
|
373
|
+
/** The database refused startup, so nothing it holds synchronizes. */
|
|
374
|
+
readonly refused: boolean;
|
|
375
|
+
readonly owners: ReadonlyArray<SyncTenantOwner>;
|
|
376
|
+
}
|
|
377
|
+
export interface SyncTenantOwner {
|
|
378
|
+
readonly ownerId: OwnerId;
|
|
379
|
+
/** A readonly registration holds transports but never synchronizes. */
|
|
380
|
+
readonly writable: boolean;
|
|
381
|
+
/** Every transport claimed for the owner, by any database. */
|
|
382
|
+
readonly transportIds: ReadonlyArray<SyncTransportId>;
|
|
383
|
+
/** One route per transport for a writable owner; none for a readonly one. */
|
|
384
|
+
readonly routes: ReadonlyArray<SyncRoute>;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* One database's use of one owner through one transport. See the
|
|
388
|
+
* Synchronization completion section of this module's documentation.
|
|
389
|
+
*/
|
|
390
|
+
export interface SyncRoute {
|
|
391
|
+
readonly transportId: SyncTransportId;
|
|
392
|
+
/** Whether the database is reconciled with the relay for the owner. */
|
|
393
|
+
readonly complete: boolean;
|
|
394
|
+
/** When the route last became complete, or null. */
|
|
395
|
+
readonly completeAt: Millis | null;
|
|
396
|
+
/** When this database last sent a request through the route, or null. */
|
|
397
|
+
readonly lastSentAt: Millis | null;
|
|
398
|
+
/**
|
|
399
|
+
* When processing a frame from the route last finished, successfully or with
|
|
400
|
+
* a failure, or null. Aborted processing does not update this timestamp.
|
|
401
|
+
*/
|
|
402
|
+
readonly lastReceivedAt: Millis | null;
|
|
403
|
+
/** The last failed result on the route; cleared when the route completes. */
|
|
404
|
+
readonly error: SyncRouteError | null;
|
|
405
|
+
}
|
|
406
|
+
export interface SyncRouteError {
|
|
407
|
+
readonly type: SyncRouteErrorType;
|
|
408
|
+
readonly at: Millis;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* A {@link ProtocolError} or the original {@link StorageWriteMessagesError} type
|
|
412
|
+
* for a rejected write. `WriteFailed` means a `writeMessages` call that threw,
|
|
413
|
+
* logged by the protocol, and `SyncFailed` means a logged failure while
|
|
414
|
+
* creating a round or reconciling ranges.
|
|
415
|
+
*/
|
|
416
|
+
export type SyncRouteErrorType = ProtocolError["type"] | StorageWriteMessagesError["type"] | "WriteFailed" | "SyncFailed";
|
|
417
|
+
/**
|
|
418
|
+
* One owner's standing with its relays in one database, derived from
|
|
419
|
+
* {@link SyncState} by {@link syncStateToOwnerSyncStates}.
|
|
420
|
+
*/
|
|
421
|
+
export interface OwnerSyncState {
|
|
422
|
+
readonly name: Name;
|
|
423
|
+
readonly ownerId: OwnerId;
|
|
424
|
+
readonly status: OwnerSyncStatus;
|
|
425
|
+
/** When a route of the owner last became complete, or null. */
|
|
426
|
+
readonly syncedAt: Millis | null;
|
|
427
|
+
/** The newest route error of the owner, or null. */
|
|
428
|
+
readonly error: SyncRouteError | null;
|
|
429
|
+
/** Each relay of the owner, in the order of its routes. */
|
|
430
|
+
readonly relays: ReadonlyArray<RelaySyncState>;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* The status of an {@link OwnerSyncState}: the first of `error`, `syncing`,
|
|
434
|
+
* `synced`, and `offline` that one of its relays has, or `initial` before the
|
|
435
|
+
* owner has a relay. Work in progress on an open transport shows before a relay
|
|
436
|
+
* that is already up to date.
|
|
437
|
+
*/
|
|
438
|
+
export type OwnerSyncStatus = "initial" | RelaySyncStatus;
|
|
439
|
+
/**
|
|
440
|
+
* One relay of an {@link OwnerSyncState}: a transport and the database's route
|
|
441
|
+
* through it.
|
|
442
|
+
*/
|
|
443
|
+
export interface RelaySyncState {
|
|
444
|
+
readonly transport: SyncTransport;
|
|
445
|
+
readonly route: SyncRoute;
|
|
446
|
+
readonly status: RelaySyncStatus;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* The status of a {@link RelaySyncState}: `error` when its route failed and has
|
|
450
|
+
* not completed since, `synced` when the route is complete, `syncing` while the
|
|
451
|
+
* transport is open, and `offline` otherwise.
|
|
452
|
+
*/
|
|
453
|
+
export type RelaySyncStatus = "syncing" | "synced" | "offline" | "error";
|
|
454
|
+
/**
|
|
455
|
+
* Folds the routes of every writable owner registration of every running
|
|
456
|
+
* database in a {@link SyncState} into one {@link OwnerSyncState} per database
|
|
457
|
+
* and owner, which pairs each route with its transport as a
|
|
458
|
+
* {@link RelaySyncState}. A database that refused startup and a readonly
|
|
459
|
+
* registration synchronize nothing, so they are left out.
|
|
460
|
+
*
|
|
461
|
+
* ### Example
|
|
462
|
+
*
|
|
463
|
+
* ```ts
|
|
464
|
+
* import {
|
|
465
|
+
* assertEqual,
|
|
466
|
+
* createId,
|
|
467
|
+
* Millis,
|
|
468
|
+
* testCreateDeps,
|
|
469
|
+
* testName,
|
|
470
|
+
* } from "@evolu/common";
|
|
471
|
+
* import {
|
|
472
|
+
* syncStateToOwnerSyncStates,
|
|
473
|
+
* testAppOwner,
|
|
474
|
+
* type SyncRoute,
|
|
475
|
+
* type SyncState,
|
|
476
|
+
* type SyncTransport,
|
|
477
|
+
* } from "@evolu/common/local-first";
|
|
478
|
+
*
|
|
479
|
+
* const deps = testCreateDeps();
|
|
480
|
+
* const transport: SyncTransport = {
|
|
481
|
+
* id: createId<"SyncTransport">(deps),
|
|
482
|
+
* label: "wss://relay.example",
|
|
483
|
+
* readyState: "open",
|
|
484
|
+
* openedAt: null,
|
|
485
|
+
* closedAt: null,
|
|
486
|
+
* error: null,
|
|
487
|
+
* };
|
|
488
|
+
* const route: SyncRoute = {
|
|
489
|
+
* transportId: transport.id,
|
|
490
|
+
* complete: true,
|
|
491
|
+
* completeAt: Millis.orThrow(1000),
|
|
492
|
+
* lastSentAt: Millis.orThrow(900),
|
|
493
|
+
* lastReceivedAt: Millis.orThrow(1000),
|
|
494
|
+
* error: null,
|
|
495
|
+
* };
|
|
496
|
+
* const state: SyncState = {
|
|
497
|
+
* transports: [transport],
|
|
498
|
+
* tenants: [
|
|
499
|
+
* {
|
|
500
|
+
* name: testName,
|
|
501
|
+
* refused: false,
|
|
502
|
+
* owners: [
|
|
503
|
+
* {
|
|
504
|
+
* ownerId: testAppOwner.id,
|
|
505
|
+
* writable: true,
|
|
506
|
+
* transportIds: [transport.id],
|
|
507
|
+
* routes: [route],
|
|
508
|
+
* },
|
|
509
|
+
* ],
|
|
510
|
+
* },
|
|
511
|
+
* ],
|
|
512
|
+
* };
|
|
513
|
+
*
|
|
514
|
+
* assertEqual(syncStateToOwnerSyncStates(state), [
|
|
515
|
+
* {
|
|
516
|
+
* name: testName,
|
|
517
|
+
* ownerId: testAppOwner.id,
|
|
518
|
+
* status: "synced",
|
|
519
|
+
* syncedAt: Millis.orThrow(1000),
|
|
520
|
+
* error: null,
|
|
521
|
+
* relays: [{ transport, route, status: "synced" }],
|
|
522
|
+
* },
|
|
523
|
+
* ]);
|
|
524
|
+
* ```
|
|
525
|
+
*/
|
|
526
|
+
export declare const syncStateToOwnerSyncStates: (state: SyncState) => ReadonlyArray<OwnerSyncState>;
|
|
51
527
|
export type EvoluInput = {
|
|
52
528
|
readonly type: "Mutate";
|
|
53
529
|
readonly changes: NonEmptyReadonlyArray<MutationChange>;
|
|
@@ -58,6 +534,9 @@ export type EvoluInput = {
|
|
|
58
534
|
readonly actions: ReadonlyArray<{
|
|
59
535
|
readonly owner: SyncOwner;
|
|
60
536
|
readonly action: "add" | "remove";
|
|
537
|
+
} | {
|
|
538
|
+
readonly ownerId: OwnerId;
|
|
539
|
+
readonly action: "sync";
|
|
61
540
|
}>;
|
|
62
541
|
} | {
|
|
63
542
|
readonly type: "Query";
|
|
@@ -76,32 +555,50 @@ export type EvoluOutput = {
|
|
|
76
555
|
readonly file: Uint8Array<ArrayBuffer>;
|
|
77
556
|
};
|
|
78
557
|
export type DbWorkerInput = (Typed<"Request"> & {
|
|
79
|
-
readonly
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
558
|
+
readonly attemptId: Id;
|
|
559
|
+
} & ({
|
|
560
|
+
readonly request: DbWorkerWriteRequest;
|
|
561
|
+
readonly clock: Timestamp;
|
|
562
|
+
readonly now: Millis;
|
|
563
|
+
} | {
|
|
564
|
+
readonly request: DbWorkerReadRequest;
|
|
565
|
+
})) | Typed<"Dispose">;
|
|
566
|
+
export type DbWorkerRequest = DbWorkerWriteRequest | DbWorkerReadRequest;
|
|
567
|
+
export type DbWorkerWriteRequest = {
|
|
83
568
|
readonly type: "ForEvolu";
|
|
84
569
|
readonly id: EvoluInstanceId;
|
|
85
|
-
readonly message:
|
|
86
|
-
readonly [Message in EvoluInput as Message["type"]]: Message;
|
|
87
|
-
}["Mutate" | "Query" | "Export"];
|
|
570
|
+
readonly message: ExtractTyped<EvoluInput, "Mutate">;
|
|
88
571
|
} | {
|
|
89
572
|
readonly type: "ForSharedWorker";
|
|
90
573
|
readonly message: {
|
|
91
|
-
readonly type: "CreateSyncMessages";
|
|
92
|
-
readonly owners: NonEmptyReadonlyArray<Owner>;
|
|
93
|
-
} | {
|
|
94
574
|
readonly type: "ApplySyncMessage";
|
|
95
575
|
readonly owner: Owner;
|
|
96
576
|
readonly inputMessage: Uint8Array;
|
|
97
577
|
};
|
|
98
578
|
};
|
|
579
|
+
export type DbWorkerReadRequest = {
|
|
580
|
+
readonly type: "ForEvolu";
|
|
581
|
+
readonly id: EvoluInstanceId;
|
|
582
|
+
readonly message: ExtractTyped<EvoluInput, "Query" | "Export">;
|
|
583
|
+
} | {
|
|
584
|
+
readonly type: "ForSharedWorker";
|
|
585
|
+
readonly message: {
|
|
586
|
+
readonly type: "CreateSyncMessages";
|
|
587
|
+
readonly owners: NonEmptyReadonlyArray<Owner>;
|
|
588
|
+
};
|
|
589
|
+
};
|
|
99
590
|
export type DbWorkerOutput = {
|
|
100
591
|
readonly type: "LeaderAcquired";
|
|
101
592
|
readonly name: Name;
|
|
593
|
+
readonly clock: Timestamp;
|
|
594
|
+
} | {
|
|
595
|
+
/** Startup was refused; the worker is releasing its resources. */
|
|
596
|
+
readonly type: "LeaderRefused";
|
|
597
|
+
readonly name: Name;
|
|
598
|
+
readonly error: UnsupportedDbVersionError;
|
|
102
599
|
} | {
|
|
103
600
|
readonly type: "OnQueuedResponse";
|
|
104
|
-
readonly
|
|
601
|
+
readonly attemptId: Id;
|
|
105
602
|
readonly response: DbWorkerQueuedResponse;
|
|
106
603
|
};
|
|
107
604
|
export type DbWorkerQueuedResponse = {
|
|
@@ -109,6 +606,7 @@ export type DbWorkerQueuedResponse = {
|
|
|
109
606
|
readonly id: EvoluInstanceId;
|
|
110
607
|
readonly message: {
|
|
111
608
|
readonly type: "Mutate";
|
|
609
|
+
readonly clock: Timestamp;
|
|
112
610
|
readonly messagesByOwnerId: ReadonlyMap<OwnerId, NonEmptyReadonlyArray<CrdtMessage>>;
|
|
113
611
|
readonly rowsByQuery: RowsByQueryMap;
|
|
114
612
|
} | {
|
|
@@ -123,16 +621,33 @@ export type DbWorkerQueuedResponse = {
|
|
|
123
621
|
readonly message: {
|
|
124
622
|
readonly type: "CreateSyncMessages";
|
|
125
623
|
readonly protocolMessagesByOwnerId: ReadonlyMap<OwnerId, ProtocolMessage>;
|
|
624
|
+
/** Owners whose message creation threw; the DbWorker logged it. */
|
|
625
|
+
readonly failedOwnerIds: ReadonlySet<OwnerId>;
|
|
126
626
|
} | {
|
|
127
627
|
readonly type: "ApplySyncMessage";
|
|
628
|
+
readonly clock: Timestamp;
|
|
128
629
|
readonly ownerId: OwnerId;
|
|
129
630
|
readonly didWriteMessages: boolean;
|
|
130
|
-
readonly result: Result<ApplyProtocolMessageAsClientResult, ProtocolError | AbortError>;
|
|
631
|
+
readonly result: Result<ApplyProtocolMessageAsClientResult, ProtocolError | StorageWriteMessagesError | AbortError>;
|
|
131
632
|
};
|
|
132
633
|
};
|
|
133
|
-
|
|
634
|
+
/**
|
|
635
|
+
* Tells whether the platform can store databases persistently.
|
|
636
|
+
*
|
|
637
|
+
* Only a platform that can lack persistent storage provides it, as a browser
|
|
638
|
+
* does in Safari's Private Browsing; see Storage in the Shared module.
|
|
639
|
+
*/
|
|
640
|
+
export interface PersistentStorageDep {
|
|
641
|
+
readonly isPersistentStorageAvailable: () => Promise<boolean>;
|
|
642
|
+
}
|
|
643
|
+
export type SharedWorkerDeps = WorkerDeps & CreateBroadcastChannelDep & CreateMessageChannelDep & CreateWebSocketDep & LockManagerDep & Partial<PersistentStorageDep>;
|
|
134
644
|
export type EvoluInstanceId = Id & Brand<"EvoluInstance">;
|
|
135
|
-
|
|
136
|
-
|
|
645
|
+
/**
|
|
646
|
+
* Initializes the platform-agnostic Evolu SharedWorker.
|
|
647
|
+
*
|
|
648
|
+
* The worker holds the build lock until it is disposed, so a platform connects
|
|
649
|
+
* every `createEvoluDeps` call in a JS runtime to one worker, as React Native
|
|
650
|
+
* does; see Builds.
|
|
651
|
+
*/
|
|
137
652
|
export declare const initSharedWorker: (self: SharedWorkerSelf<SharedWorkerInput, SharedWorkerOutput>) => Task<AsyncDisposableStack, never, SharedWorkerDeps>;
|
|
138
653
|
//# sourceMappingURL=Shared.d.ts.map
|