@evolu/common 8.10.0 → 8.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/dist/src/Bytes.d.ts +39 -2
  2. package/dist/src/Bytes.d.ts.map +1 -1
  3. package/dist/src/Bytes.js +50 -2
  4. package/dist/src/Config.d.ts +22 -22
  5. package/dist/src/Config.d.ts.map +1 -1
  6. package/dist/src/Console.d.ts +62 -7
  7. package/dist/src/Console.d.ts.map +1 -1
  8. package/dist/src/Console.js +20 -4
  9. package/dist/src/Crypto.d.ts +76 -4
  10. package/dist/src/Crypto.d.ts.map +1 -1
  11. package/dist/src/Crypto.js +55 -4
  12. package/dist/src/Error.d.ts +45 -0
  13. package/dist/src/Error.d.ts.map +1 -1
  14. package/dist/src/Error.js +69 -0
  15. package/dist/src/Fs.d.ts +92 -18
  16. package/dist/src/Fs.d.ts.map +1 -1
  17. package/dist/src/Fs.js +2 -0
  18. package/dist/src/Identicon.d.ts +2 -2
  19. package/dist/src/Identicon.js +2 -2
  20. package/dist/src/LeakDetector.d.ts +22 -3
  21. package/dist/src/LeakDetector.d.ts.map +1 -1
  22. package/dist/src/LeakDetector.js +12 -2
  23. package/dist/src/LockManager.d.ts +8 -0
  24. package/dist/src/LockManager.d.ts.map +1 -1
  25. package/dist/src/LockManager.js +6 -0
  26. package/dist/src/Object.d.ts.map +1 -1
  27. package/dist/src/Object.js +5 -0
  28. package/dist/src/Platform.d.ts +47 -7
  29. package/dist/src/Platform.d.ts.map +1 -1
  30. package/dist/src/Platform.js +24 -5
  31. package/dist/src/Random.d.ts +25 -2
  32. package/dist/src/Random.d.ts.map +1 -1
  33. package/dist/src/Random.js +14 -2
  34. package/dist/src/Resource.d.ts +156 -1
  35. package/dist/src/Resource.d.ts.map +1 -1
  36. package/dist/src/Resource.js +201 -72
  37. package/dist/src/Schedule.d.ts +11 -10
  38. package/dist/src/Schedule.d.ts.map +1 -1
  39. package/dist/src/Schedule.js +1 -1
  40. package/dist/src/Sqlite.d.ts +132 -16
  41. package/dist/src/Sqlite.d.ts.map +1 -1
  42. package/dist/src/Sqlite.js +63 -9
  43. package/dist/src/Task.d.ts +15 -4
  44. package/dist/src/Task.d.ts.map +1 -1
  45. package/dist/src/Task.js +41 -15
  46. package/dist/src/Test.d.ts +9 -0
  47. package/dist/src/Test.d.ts.map +1 -1
  48. package/dist/src/Test.js +4 -0
  49. package/dist/src/Time.d.ts +106 -9
  50. package/dist/src/Time.d.ts.map +1 -1
  51. package/dist/src/Time.js +55 -4
  52. package/dist/src/Type.d.ts +1455 -1310
  53. package/dist/src/Type.d.ts.map +1 -1
  54. package/dist/src/Type.js +1274 -517
  55. package/dist/src/WebSocket.d.ts +164 -13
  56. package/dist/src/WebSocket.d.ts.map +1 -1
  57. package/dist/src/WebSocket.js +133 -24
  58. package/dist/src/Worker.d.ts +90 -8
  59. package/dist/src/Worker.d.ts.map +1 -1
  60. package/dist/src/Worker.js +28 -2
  61. package/dist/src/index.d.ts +6 -7
  62. package/dist/src/index.d.ts.map +1 -1
  63. package/dist/src/index.js +2 -3
  64. package/dist/src/local-first/Db.d.ts +52 -3
  65. package/dist/src/local-first/Db.d.ts.map +1 -1
  66. package/dist/src/local-first/Db.js +412 -137
  67. package/dist/src/local-first/Evolu.d.ts +412 -213
  68. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  69. package/dist/src/local-first/Evolu.js +181 -18
  70. package/dist/src/local-first/Owner.d.ts +13 -30
  71. package/dist/src/local-first/Owner.d.ts.map +1 -1
  72. package/dist/src/local-first/Owner.js +13 -30
  73. package/dist/src/local-first/Protocol.d.ts +106 -19
  74. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  75. package/dist/src/local-first/Protocol.js +162 -60
  76. package/dist/src/local-first/Query.d.ts +8 -15
  77. package/dist/src/local-first/Query.d.ts.map +1 -1
  78. package/dist/src/local-first/Relay.d.ts.map +1 -1
  79. package/dist/src/local-first/Relay.js +4 -2
  80. package/dist/src/local-first/Schema.d.ts +346 -23
  81. package/dist/src/local-first/Schema.d.ts.map +1 -1
  82. package/dist/src/local-first/Schema.js +214 -17
  83. package/dist/src/local-first/Shared.d.ts +537 -22
  84. package/dist/src/local-first/Shared.d.ts.map +1 -1
  85. package/dist/src/local-first/Shared.js +1437 -234
  86. package/dist/src/local-first/Storage.d.ts +195 -17
  87. package/dist/src/local-first/Storage.d.ts.map +1 -1
  88. package/dist/src/local-first/Storage.js +85 -22
  89. package/dist/src/local-first/Timestamp.d.ts +392 -41
  90. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  91. package/dist/src/local-first/Timestamp.js +403 -81
  92. package/dist/src/local-first/index.d.ts +0 -1
  93. package/dist/src/local-first/index.d.ts.map +1 -1
  94. package/dist/src/local-first/index.js +0 -1
  95. package/package.json +1 -1
  96. package/src/Assert.test.ts +2 -5
  97. package/src/Bytes.test.ts +27 -0
  98. package/src/Bytes.ts +58 -2
  99. package/src/Config.test.ts +2 -6
  100. package/src/Config.ts +133 -133
  101. package/src/Console.ts +62 -7
  102. package/src/Crypto.ts +76 -4
  103. package/src/Eq.test.ts +2 -3
  104. package/src/Error.test.ts +76 -3
  105. package/src/Error.ts +71 -0
  106. package/src/Fs.ts +92 -18
  107. package/src/Identicon.ts +2 -2
  108. package/src/LeakDetector.ts +22 -3
  109. package/src/LockManager.ts +8 -0
  110. package/src/Object.test.ts +27 -12
  111. package/src/Object.ts +5 -0
  112. package/src/Platform.ts +50 -8
  113. package/src/Random.ts +25 -2
  114. package/src/Resource.test.ts +837 -0
  115. package/src/Resource.ts +235 -15
  116. package/src/Schedule.test.ts +50 -12
  117. package/src/Schedule.ts +24 -14
  118. package/src/Sqlite.ts +137 -17
  119. package/src/Task.test.ts +189 -8
  120. package/src/Task.ts +56 -17
  121. package/src/Test.ts +9 -0
  122. package/src/Time.ts +106 -9
  123. package/src/Type.test.ts +946 -1028
  124. package/src/Type.ts +4195 -3136
  125. package/src/Types.test.ts +4 -14
  126. package/src/WebSocket.ts +313 -40
  127. package/src/Worker.ts +90 -8
  128. package/src/index.ts +20 -6
  129. package/src/local-first/Db.ts +644 -339
  130. package/src/local-first/Evolu.test.ts +994 -22
  131. package/src/local-first/Evolu.ts +625 -232
  132. package/src/local-first/Owner.ts +13 -30
  133. package/src/local-first/Protocol.test.ts +634 -10
  134. package/src/local-first/Protocol.ts +255 -109
  135. package/src/local-first/Query.ts +8 -15
  136. package/src/local-first/Relay.ts +4 -2
  137. package/src/local-first/Schema.test.ts +143 -0
  138. package/src/local-first/Schema.ts +376 -26
  139. package/src/local-first/Shared.test.ts +7731 -559
  140. package/src/local-first/Shared.ts +2036 -267
  141. package/src/local-first/Storage.ts +224 -36
  142. package/src/local-first/Timestamp.test.ts +344 -70
  143. package/src/local-first/Timestamp.ts +434 -118
  144. package/src/local-first/index.ts +0 -1
  145. package/dist/src/local-first/Error.d.ts +0 -12
  146. package/dist/src/local-first/Error.d.ts.map +0 -1
  147. package/dist/src/local-first/Error.js +0 -6
  148. package/dist/src/local-first/LocalAuth.d.ts +0 -150
  149. package/dist/src/local-first/LocalAuth.d.ts.map +0 -1
  150. package/dist/src/local-first/LocalAuth.js +0 -179
  151. package/src/local-first/Error.ts +0 -17
  152. 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 type { Id, Name, Typed } from "../Type.ts";
16
- import type { CreateWebSocketDep } from "../WebSocket.ts";
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 "./Error.ts";
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 { MutationChange } from "./Schema.ts";
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 callbackId: Id;
80
- readonly request: DbWorkerRequest;
81
- }) | Typed<"Dispose">;
82
- export type DbWorkerRequest = {
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 callbackId: Id;
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
- export type SharedWorkerDeps = WorkerDeps & CreateBroadcastChannelDep & CreateMessageChannelDep & CreateWebSocketDep & LockManagerDep;
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
- export type SyncState = 123;
136
- /** Initializes the platform-agnostic Evolu SharedWorker. */
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