@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.
Files changed (251) hide show
  1. package/README.md +4 -61
  2. package/dist/src/Array.d.ts +343 -102
  3. package/dist/src/Array.d.ts.map +1 -1
  4. package/dist/src/Array.js +181 -85
  5. package/dist/src/Assert.d.ts +57 -11
  6. package/dist/src/Assert.d.ts.map +1 -1
  7. package/dist/src/Assert.js +47 -11
  8. package/dist/src/BigInt.d.ts +6 -1
  9. package/dist/src/BigInt.d.ts.map +1 -1
  10. package/dist/src/BigInt.js +5 -0
  11. package/dist/src/Brand.d.ts +40 -12
  12. package/dist/src/Brand.d.ts.map +1 -1
  13. package/dist/src/Brand.js +5 -0
  14. package/dist/src/Buffer.d.ts +6 -1
  15. package/dist/src/Buffer.d.ts.map +1 -1
  16. package/dist/src/Buffer.js +7 -2
  17. package/dist/src/Cache.d.ts +10 -2
  18. package/dist/src/Cache.d.ts.map +1 -1
  19. package/dist/src/Cache.js +6 -0
  20. package/dist/src/Callbacks.d.ts +10 -7
  21. package/dist/src/Callbacks.d.ts.map +1 -1
  22. package/dist/src/Callbacks.js +23 -13
  23. package/dist/src/Console.d.ts +361 -69
  24. package/dist/src/Console.d.ts.map +1 -1
  25. package/dist/src/Console.js +217 -117
  26. package/dist/src/Crypto.d.ts +70 -21
  27. package/dist/src/Crypto.d.ts.map +1 -1
  28. package/dist/src/Crypto.js +72 -27
  29. package/dist/src/Eq.d.ts +22 -2
  30. package/dist/src/Eq.d.ts.map +1 -1
  31. package/dist/src/Eq.js +21 -1
  32. package/dist/src/Error.d.ts +23 -12
  33. package/dist/src/Error.d.ts.map +1 -1
  34. package/dist/src/Error.js +27 -11
  35. package/dist/src/Function.d.ts +128 -56
  36. package/dist/src/Function.d.ts.map +1 -1
  37. package/dist/src/Function.js +118 -14
  38. package/dist/src/Identicon.d.ts +5 -0
  39. package/dist/src/Identicon.d.ts.map +1 -1
  40. package/dist/src/Identicon.js +6 -1
  41. package/dist/src/Lookup.d.ts +160 -0
  42. package/dist/src/Lookup.d.ts.map +1 -0
  43. package/dist/src/Lookup.js +192 -0
  44. package/dist/src/Microtask.d.ts +21 -0
  45. package/dist/src/Microtask.d.ts.map +1 -0
  46. package/dist/src/Microtask.js +37 -0
  47. package/dist/src/Number.d.ts +18 -4
  48. package/dist/src/Number.d.ts.map +1 -1
  49. package/dist/src/Number.js +23 -1
  50. package/dist/src/Object.d.ts +110 -5
  51. package/dist/src/Object.d.ts.map +1 -1
  52. package/dist/src/Object.js +108 -5
  53. package/dist/src/Option.d.ts +58 -0
  54. package/dist/src/Option.d.ts.map +1 -0
  55. package/dist/src/Option.js +43 -0
  56. package/dist/src/Order.d.ts +5 -0
  57. package/dist/src/Order.d.ts.map +1 -1
  58. package/dist/src/Order.js +8 -3
  59. package/dist/src/Platform.d.ts +36 -2
  60. package/dist/src/Platform.d.ts.map +1 -1
  61. package/dist/src/Platform.js +10 -6
  62. package/dist/src/Polyfills.d.ts +27 -0
  63. package/dist/src/Polyfills.d.ts.map +1 -0
  64. package/dist/src/Polyfills.js +299 -0
  65. package/dist/src/Random.d.ts +18 -7
  66. package/dist/src/Random.d.ts.map +1 -1
  67. package/dist/src/Random.js +9 -7
  68. package/dist/src/Redacted.d.ts +7 -4
  69. package/dist/src/Redacted.d.ts.map +1 -1
  70. package/dist/src/Redacted.js +5 -0
  71. package/dist/src/Ref.d.ts +31 -16
  72. package/dist/src/Ref.d.ts.map +1 -1
  73. package/dist/src/Ref.js +35 -7
  74. package/dist/src/RefCount.d.ts +62 -0
  75. package/dist/src/RefCount.d.ts.map +1 -0
  76. package/dist/src/RefCount.js +83 -0
  77. package/dist/src/Relation.d.ts +84 -29
  78. package/dist/src/Relation.d.ts.map +1 -1
  79. package/dist/src/Relation.js +83 -105
  80. package/dist/src/Resource.d.ts +263 -0
  81. package/dist/src/Resource.d.ts.map +1 -0
  82. package/dist/src/Resource.js +389 -0
  83. package/dist/src/Result.d.ts +390 -374
  84. package/dist/src/Result.d.ts.map +1 -1
  85. package/dist/src/Result.js +142 -70
  86. package/dist/src/Schedule.d.ts +953 -0
  87. package/dist/src/Schedule.d.ts.map +1 -0
  88. package/dist/src/Schedule.js +1199 -0
  89. package/dist/src/Set.d.ts +181 -0
  90. package/dist/src/Set.d.ts.map +1 -0
  91. package/dist/src/Set.js +137 -0
  92. package/dist/src/Skiplist.d.ts +6 -1
  93. package/dist/src/Skiplist.d.ts.map +1 -1
  94. package/dist/src/Skiplist.js +5 -0
  95. package/dist/src/Sqlite.d.ts +160 -60
  96. package/dist/src/Sqlite.d.ts.map +1 -1
  97. package/dist/src/Sqlite.js +290 -190
  98. package/dist/src/Store.d.ts +36 -28
  99. package/dist/src/Store.d.ts.map +1 -1
  100. package/dist/src/Store.js +55 -15
  101. package/dist/src/String.d.ts +5 -0
  102. package/dist/src/String.d.ts.map +1 -1
  103. package/dist/src/String.js +5 -0
  104. package/dist/src/Task.d.ts +2263 -423
  105. package/dist/src/Task.d.ts.map +1 -1
  106. package/dist/src/Task.js +1488 -348
  107. package/dist/src/Test.d.ts +122 -0
  108. package/dist/src/Test.d.ts.map +1 -0
  109. package/dist/src/Test.js +66 -0
  110. package/dist/src/Time.d.ts +201 -48
  111. package/dist/src/Time.d.ts.map +1 -1
  112. package/dist/src/Time.js +190 -86
  113. package/dist/src/Tracer.d.ts +48 -0
  114. package/dist/src/Tracer.d.ts.map +1 -0
  115. package/dist/src/Tracer.js +6 -0
  116. package/dist/src/Type.d.ts +582 -209
  117. package/dist/src/Type.d.ts.map +1 -1
  118. package/dist/src/Type.js +362 -240
  119. package/dist/src/Types.d.ts +160 -6
  120. package/dist/src/Types.d.ts.map +1 -1
  121. package/dist/src/Types.js +22 -2
  122. package/dist/src/WebSocket.d.ts +113 -58
  123. package/dist/src/WebSocket.d.ts.map +1 -1
  124. package/dist/src/WebSocket.js +210 -118
  125. package/dist/src/Worker.d.ts +245 -60
  126. package/dist/src/Worker.d.ts.map +1 -1
  127. package/dist/src/Worker.js +220 -64
  128. package/dist/src/index.d.ts +25 -3
  129. package/dist/src/index.d.ts.map +1 -1
  130. package/dist/src/index.js +19 -3
  131. package/dist/src/local-first/Db.d.ts +24 -213
  132. package/dist/src/local-first/Db.d.ts.map +1 -1
  133. package/dist/src/local-first/Db.js +622 -293
  134. package/dist/src/local-first/Error.d.ts +12 -0
  135. package/dist/src/local-first/Error.d.ts.map +1 -0
  136. package/dist/src/local-first/Error.js +6 -0
  137. package/dist/src/local-first/Evolu.d.ts +330 -285
  138. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  139. package/dist/src/local-first/Evolu.js +472 -402
  140. package/dist/src/local-first/LocalAuth.d.ts +10 -10
  141. package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
  142. package/dist/src/local-first/LocalAuth.js +6 -1
  143. package/dist/src/local-first/Owner.d.ts +70 -27
  144. package/dist/src/local-first/Owner.d.ts.map +1 -1
  145. package/dist/src/local-first/Owner.js +27 -14
  146. package/dist/src/local-first/Protocol.d.ts +58 -50
  147. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  148. package/dist/src/local-first/Protocol.js +253 -155
  149. package/dist/src/local-first/Query.d.ts +133 -43
  150. package/dist/src/local-first/Query.d.ts.map +1 -1
  151. package/dist/src/local-first/Query.js +139 -90
  152. package/dist/src/local-first/Relay.d.ts +18 -38
  153. package/dist/src/local-first/Relay.d.ts.map +1 -1
  154. package/dist/src/local-first/Relay.js +59 -171
  155. package/dist/src/local-first/Schema.d.ts +139 -144
  156. package/dist/src/local-first/Schema.d.ts.map +1 -1
  157. package/dist/src/local-first/Schema.js +78 -163
  158. package/dist/src/local-first/Shared.d.ts +125 -0
  159. package/dist/src/local-first/Shared.d.ts.map +1 -0
  160. package/dist/src/local-first/Shared.js +569 -0
  161. package/dist/src/local-first/Storage.d.ts +50 -53
  162. package/dist/src/local-first/Storage.d.ts.map +1 -1
  163. package/dist/src/local-first/Storage.js +121 -173
  164. package/dist/src/local-first/Timestamp.d.ts +28 -35
  165. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  166. package/dist/src/local-first/Timestamp.js +24 -25
  167. package/dist/src/local-first/index.d.ts +2 -15
  168. package/dist/src/local-first/index.d.ts.map +1 -1
  169. package/dist/src/local-first/index.js +2 -15
  170. package/package.json +36 -26
  171. package/src/Array.ts +467 -106
  172. package/src/Assert.ts +87 -12
  173. package/src/BigInt.ts +7 -1
  174. package/src/Brand.ts +41 -12
  175. package/src/Buffer.ts +10 -4
  176. package/src/Cache.ts +11 -2
  177. package/src/Callbacks.ts +33 -23
  178. package/src/Console.ts +567 -178
  179. package/src/Crypto.ts +111 -62
  180. package/src/Eq.ts +30 -3
  181. package/src/Error.ts +50 -34
  182. package/src/Function.ts +140 -68
  183. package/src/Identicon.ts +7 -1
  184. package/src/Lookup.ts +415 -0
  185. package/src/Microtask.ts +58 -0
  186. package/src/Number.ts +40 -7
  187. package/src/Object.ts +155 -16
  188. package/src/Option.ts +74 -0
  189. package/src/Order.ts +11 -3
  190. package/src/Platform.ts +43 -7
  191. package/src/Polyfills.ts +465 -0
  192. package/src/Random.ts +25 -11
  193. package/src/Redacted.ts +8 -4
  194. package/src/Ref.ts +72 -21
  195. package/src/RefCount.ts +170 -0
  196. package/src/Relation.ts +188 -136
  197. package/src/Resource.ts +864 -0
  198. package/src/Result.ts +548 -381
  199. package/src/Schedule.ts +1471 -0
  200. package/src/Set.ts +247 -0
  201. package/src/Skiplist.ts +7 -1
  202. package/src/Sqlite.ts +409 -304
  203. package/src/Store.ts +94 -48
  204. package/src/String.ts +6 -0
  205. package/src/Task.ts +3685 -740
  206. package/src/Test.ts +162 -0
  207. package/src/Time.ts +348 -145
  208. package/src/Tracer.ts +54 -0
  209. package/src/Type.ts +1064 -630
  210. package/src/Types.ts +202 -7
  211. package/src/WebSocket.ts +311 -178
  212. package/src/Worker.ts +527 -147
  213. package/src/index.ts +74 -3
  214. package/src/local-first/Db.ts +871 -597
  215. package/src/local-first/Error.ts +17 -0
  216. package/src/local-first/Evolu.ts +884 -823
  217. package/src/local-first/LocalAuth.ts +10 -10
  218. package/src/local-first/Owner.ts +94 -42
  219. package/src/local-first/Protocol.ts +440 -303
  220. package/src/local-first/Query.ts +243 -194
  221. package/src/local-first/Relay.ts +91 -252
  222. package/src/local-first/Schema.ts +284 -394
  223. package/src/local-first/Shared.ts +932 -0
  224. package/src/local-first/Storage.ts +209 -282
  225. package/src/local-first/Timestamp.ts +38 -45
  226. package/src/local-first/index.ts +2 -16
  227. package/LICENSE +0 -21
  228. package/dist/src/Instances.d.ts +0 -34
  229. package/dist/src/Instances.d.ts.map +0 -1
  230. package/dist/src/Instances.js +0 -44
  231. package/dist/src/Resources.d.ts +0 -118
  232. package/dist/src/Resources.d.ts.map +0 -1
  233. package/dist/src/Resources.js +0 -197
  234. package/dist/src/local-first/Platform.d.ts +0 -25
  235. package/dist/src/local-first/Platform.d.ts.map +0 -1
  236. package/dist/src/local-first/Platform.js +0 -1
  237. package/dist/src/local-first/Public.d.ts +0 -22
  238. package/dist/src/local-first/Public.d.ts.map +0 -1
  239. package/dist/src/local-first/Public.js +0 -15
  240. package/dist/src/local-first/PublicKysely.d.ts +0 -148
  241. package/dist/src/local-first/PublicKysely.d.ts.map +0 -1
  242. package/dist/src/local-first/PublicKysely.js +0 -184
  243. package/dist/src/local-first/Sync.d.ts +0 -112
  244. package/dist/src/local-first/Sync.d.ts.map +0 -1
  245. package/dist/src/local-first/Sync.js +0 -529
  246. package/src/Instances.ts +0 -90
  247. package/src/Resources.ts +0 -367
  248. package/src/local-first/Platform.ts +0 -27
  249. package/src/local-first/Public.ts +0 -42
  250. package/src/local-first/PublicKysely.ts +0 -239
  251. package/src/local-first/Sync.ts +0 -960
@@ -0,0 +1,932 @@
1
+ /**
2
+ * Platform-agnostic Evolu SharedWorker.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import {
8
+ emptyArray,
9
+ firstInArray,
10
+ isNonEmptyArray,
11
+ shiftFromArray,
12
+ type NonEmptyReadonlyArray,
13
+ } from "../Array.js";
14
+ import { assert, assertNotDisposed } from "../Assert.js";
15
+ import { createCallbacks } from "../Callbacks.js";
16
+ import type { ConsoleEntry, ConsoleLevel } from "../Console.js";
17
+ import { exhaustiveCheck } from "../Function.js";
18
+ import { structuralLookup, type StructuralLookupKey } from "../Lookup.js";
19
+ import { createRefCountByKey, type RefCountByKey } from "../RefCount.js";
20
+ import {
21
+ createSharedResourceByKey,
22
+ createSharedResourceByKeyWithClaims,
23
+ type BorrowedResource,
24
+ type SharedResourceByKeyWithClaims,
25
+ } from "../Resource.js";
26
+ import { ok, type Result } from "../Result.js";
27
+ import { spaced } from "../Schedule.js";
28
+ import type { NonEmptyReadonlySet } from "../Set.js";
29
+ import {
30
+ repeat,
31
+ unabortable,
32
+ type AbortError,
33
+ type Fiber,
34
+ type Task,
35
+ } from "../Task.js";
36
+ import { createId, type Id, type Name } from "../Type.js";
37
+ import type { Callback, ExtractType } from "../Types.js";
38
+ import type { CreateWebSocketDep, WebSocket } from "../WebSocket.js";
39
+ import type {
40
+ SharedWorker as CommonSharedWorker,
41
+ MessagePort,
42
+ NativeMessagePort,
43
+ SharedWorkerSelf,
44
+ WorkerDeps,
45
+ } from "../Worker.js";
46
+ import type { EvoluError } from "./Error.js";
47
+ import type { Owner, OwnerId, OwnerTransport, SyncOwner } from "./Owner.js";
48
+ import {
49
+ createProtocolMessageForUnsubscribe,
50
+ createProtocolMessageFromCrdtMessages,
51
+ parseProtocolHeader,
52
+ type ApplyProtocolMessageAsClientResult,
53
+ type ProtocolError,
54
+ type ProtocolMessage,
55
+ } from "./Protocol.js";
56
+ import {
57
+ makePatches,
58
+ type Patch,
59
+ type Query,
60
+ type RowsByQueryMap,
61
+ } from "./Query.js";
62
+ import type { MutationChange } from "./Schema.js";
63
+ import type { CrdtMessage } from "./Storage.js";
64
+
65
+ export type SharedWorker = CommonSharedWorker<SharedWorkerInput>;
66
+
67
+ export interface SharedWorkerDep {
68
+ readonly sharedWorker: SharedWorker;
69
+ }
70
+
71
+ export type SharedWorkerDeps = WorkerDeps & CreateWebSocketDep;
72
+
73
+ export type SharedWorkerInput =
74
+ | {
75
+ readonly type: "InitTab";
76
+ readonly consoleLevel: ConsoleLevel;
77
+ readonly port: NativeMessagePort<EvoluTabOutput>;
78
+ }
79
+ | {
80
+ readonly type: "CreateEvolu";
81
+ readonly name: Name;
82
+ readonly evoluPort: NativeMessagePort<EvoluOutput, EvoluInput>;
83
+ readonly dbWorkerPort: NativeMessagePort<DbWorkerInput, DbWorkerOutput>;
84
+ };
85
+
86
+ export type EvoluTabOutput =
87
+ | {
88
+ readonly type: "OnConsoleEntry";
89
+ readonly entry: ConsoleEntry;
90
+ }
91
+ | {
92
+ readonly type: "OnError";
93
+ readonly error: EvoluError;
94
+ };
95
+
96
+ export type EvoluInput =
97
+ | {
98
+ readonly type: "Mutate";
99
+ readonly changes: NonEmptyReadonlyArray<MutationChange>;
100
+ readonly onCompleteIds: ReadonlyArray<Id>;
101
+ readonly subscribedQueries: ReadonlySet<Query>;
102
+ }
103
+ | {
104
+ readonly type: "UseOwner";
105
+ readonly actions: ReadonlyArray<{
106
+ readonly owner: SyncOwner;
107
+ readonly action: "add" | "remove";
108
+ }>;
109
+ }
110
+ | {
111
+ readonly type: "Query";
112
+ readonly queries: NonEmptyReadonlySet<Query>;
113
+ }
114
+ | {
115
+ readonly type: "Export";
116
+ }
117
+ | {
118
+ readonly type: "Dispose";
119
+ };
120
+
121
+ export type EvoluOutput =
122
+ | {
123
+ readonly type: "OnPatchesByQuery";
124
+ readonly patchesByQuery: ReadonlyMap<Query, ReadonlyArray<Patch>>;
125
+ readonly onCompleteIds: ReadonlyArray<Id>;
126
+ }
127
+ | {
128
+ readonly type: "RefreshQueries";
129
+ }
130
+ | {
131
+ readonly type: "OnExport";
132
+ readonly file: Uint8Array<ArrayBuffer>;
133
+ };
134
+
135
+ export const initSharedWorker =
136
+ (
137
+ self: SharedWorkerSelf<SharedWorkerInput>,
138
+ ): Task<AsyncDisposableStack, never, SharedWorkerDeps> =>
139
+ async (run) => {
140
+ const { createMessagePort, consoleStoreOutputEntry, createWebSocket } =
141
+ run.deps;
142
+ const console = run.deps.console.child("SharedWorker");
143
+ await using stack = new AsyncDisposableStack();
144
+ const sharedWorkerReady = Promise.withResolvers<void>();
145
+
146
+ // TODO: Use heartbeat to detect and prune dead ports.
147
+ const tabPorts = new Set<MessagePort<EvoluTabOutput>>();
148
+
149
+ // Buffer console/error outputs until the first tab connects its port.
150
+ const queuedTabOutputs: Array<EvoluTabOutput> = [];
151
+ const postTabOutput = (output: EvoluTabOutput): void => {
152
+ if (tabPorts.size === 0) queuedTabOutputs.push(output);
153
+ else for (const port of tabPorts) port.postMessage(output);
154
+ };
155
+ stack.defer(
156
+ consoleStoreOutputEntry.subscribe(() => {
157
+ const entry = consoleStoreOutputEntry.get();
158
+ if (entry) postTabOutput({ type: "OnConsoleEntry", entry });
159
+ }),
160
+ );
161
+
162
+ // Register ASAP so the worker does not miss connections.
163
+ self.onConnect = (port) => {
164
+ void sharedWorkerReady.promise.then(() => {
165
+ // The underlying port buffers messages until onMessage is assigned.
166
+ port.onMessage = (message) => {
167
+ switch (message.type) {
168
+ case "InitTab": {
169
+ /**
170
+ * One SharedWorker serves multiple tabs. The most recently
171
+ * initialized tab's level wins.
172
+ */
173
+ console.setLevel(message.consoleLevel);
174
+
175
+ const tabPort = createMessagePort<EvoluTabOutput>(message.port);
176
+ tabPorts.add(tabPort);
177
+ if (queuedTabOutputs.length > 0) {
178
+ queuedTabOutputs.forEach(postTabOutput);
179
+ queuedTabOutputs.length = 0;
180
+ }
181
+ break;
182
+ }
183
+
184
+ case "CreateEvolu": {
185
+ void sharedWorkerRunWithSharedEvoluDeps(async (run) => {
186
+ const sharedEvoluResult = await run(
187
+ sharedEvolusByName.acquire(message.name),
188
+ );
189
+ if (!sharedEvoluResult.ok) return sharedEvoluResult;
190
+ sharedEvoluResult.value.createInstance(
191
+ message.evoluPort,
192
+ message.dbWorkerPort,
193
+ () =>
194
+ void sharedWorkerRunWithSharedEvoluDeps(
195
+ sharedEvolusByName.release(message.name),
196
+ ),
197
+ );
198
+ return ok();
199
+ });
200
+ break;
201
+ }
202
+ default:
203
+ console.error("Unknown shared worker input", message);
204
+ }
205
+ };
206
+ });
207
+ };
208
+
209
+ const initSharedWorkerRun = run.create();
210
+
211
+ const transports = stack.use(
212
+ await initSharedWorkerRun.orThrow(
213
+ createSharedResourceByKeyWithClaims<
214
+ WebSocket,
215
+ OwnerTransport,
216
+ OwnerId,
217
+ SharedWorkerDeps,
218
+ StructuralLookupKey
219
+ >(
220
+ (transport) =>
221
+ createWebSocket(transport.url, {
222
+ binaryType: "arraybuffer",
223
+
224
+ onOpen: () => {
225
+ const ownerIds = transports.getClaimsForResource(transport);
226
+ console.debug("transportOpen", {
227
+ url: transport.url,
228
+ ownerIds: [...ownerIds],
229
+ });
230
+
231
+ forEachSharedEvolu((sharedEvolu) => {
232
+ sharedEvolu.requestCreateSyncMessages(ownerIds);
233
+ });
234
+ },
235
+
236
+ onMessage(data) {
237
+ if (!(data instanceof ArrayBuffer)) return;
238
+
239
+ const inputMessage = new globalThis.Uint8Array(data);
240
+ const headerResult = parseProtocolHeader(
241
+ new globalThis.Uint8Array(data),
242
+ );
243
+
244
+ if (!headerResult.ok) {
245
+ console.debug("transportInvalidProtocolMessage", {
246
+ url: transport.url,
247
+ byteLength: inputMessage.byteLength,
248
+ });
249
+
250
+ // TODO: Propagate invalid protocol messages to sync state.
251
+ return;
252
+ }
253
+
254
+ console.debug("transportProtocolMessage", {
255
+ url: transport.url,
256
+ ownerId: headerResult.value.ownerId,
257
+ byteLength: inputMessage.byteLength,
258
+ });
259
+
260
+ forEachSharedEvolu((sharedEvolu) => {
261
+ sharedEvolu.requestApplySyncMessage(
262
+ headerResult.value.ownerId,
263
+ inputMessage,
264
+ );
265
+ });
266
+ },
267
+ }),
268
+ {
269
+ onFirstClaimAdded: (ownerId, webSocket) => {
270
+ if (!webSocket.isOpen()) return;
271
+ forEachSharedEvolu((sharedEvolu) => {
272
+ sharedEvolu.requestCreateSyncMessages(new Set([ownerId]));
273
+ });
274
+ },
275
+
276
+ onLastClaimRemoved: (ownerId, webSocket) => {
277
+ webSocket.send(createProtocolMessageForUnsubscribe(ownerId));
278
+ },
279
+ // Keep sockets alive briefly across short owner churn.
280
+ idleDisposeAfter: "3s",
281
+ resourceLookup: structuralLookup,
282
+ },
283
+ ),
284
+ ),
285
+ );
286
+
287
+ const sharedWorkerRunWithSharedEvoluDeps = initSharedWorkerRun.addDeps({
288
+ transports,
289
+ postTabOutput,
290
+ });
291
+
292
+ const sharedEvolusByName = stack.use(
293
+ await sharedWorkerRunWithSharedEvoluDeps.orThrow(
294
+ createSharedResourceByKey(createSharedEvolu, {
295
+ lookup: structuralLookup,
296
+ }),
297
+ ),
298
+ );
299
+
300
+ const forEachSharedEvolu = (
301
+ callback: Callback<BorrowedResource<SharedEvolu>>,
302
+ ): void => {
303
+ for (const sharedEvolu of sharedEvolusByName
304
+ .snapshot()
305
+ .resourcesByKey.values()) {
306
+ callback(sharedEvolu);
307
+ }
308
+ };
309
+
310
+ sharedWorkerReady.resolve();
311
+ console.info("initSharedWorker");
312
+
313
+ return ok(stack.move());
314
+ };
315
+
316
+ interface SharedEvolu extends AsyncDisposable {
317
+ readonly createInstance: (
318
+ evoluPort: NativeMessagePort<EvoluOutput, EvoluInput>,
319
+ dbWorkerPort: NativeMessagePort<DbWorkerInput, DbWorkerOutput>,
320
+ releaseSharedEvolu: () => void,
321
+ ) => void;
322
+
323
+ readonly requestCreateSyncMessages: (ownerIds: ReadonlySet<OwnerId>) => void;
324
+
325
+ readonly requestApplySyncMessage: (
326
+ ownerId: OwnerId,
327
+ inputMessage: Uint8Array<ArrayBuffer>,
328
+ ) => void;
329
+ }
330
+
331
+ type SharedEvoluDeps = SharedWorkerDeps & PostTabOutputDep & TransportsDep;
332
+
333
+ interface PostTabOutputDep {
334
+ readonly postTabOutput: Callback<EvoluTabOutput>;
335
+ }
336
+
337
+ interface TransportsDep {
338
+ readonly transports: SharedResourceByKeyWithClaims<
339
+ OwnerTransport,
340
+ OwnerId,
341
+ WebSocket,
342
+ SharedWorkerDeps
343
+ >;
344
+ }
345
+
346
+ export interface DbWorkerInput {
347
+ readonly callbackId: Id;
348
+ readonly request:
349
+ | {
350
+ readonly type: "ForEvolu";
351
+ readonly evoluPortId: Id;
352
+ readonly message: ExtractType<
353
+ EvoluInput,
354
+ "Mutate" | "Query" | "Export"
355
+ >;
356
+ }
357
+ | {
358
+ readonly type: "ForSharedWorker";
359
+ readonly message:
360
+ | {
361
+ readonly type: "CreateSyncMessages";
362
+ readonly owners: NonEmptyReadonlyArray<Owner>;
363
+ }
364
+ | {
365
+ readonly type: "ApplySyncMessage";
366
+ readonly owner: Owner;
367
+ readonly inputMessage: Uint8Array<ArrayBuffer>;
368
+ };
369
+ };
370
+ }
371
+
372
+ export type DbWorkerOutput =
373
+ | EvoluTabOutput
374
+ | {
375
+ readonly type: "LeaderAcquired";
376
+ readonly name: Name;
377
+ }
378
+ | {
379
+ readonly type: "OnQueuedResponse";
380
+ readonly callbackId: Id;
381
+ readonly response:
382
+ | {
383
+ readonly type: "ForEvolu";
384
+ readonly evoluPortId: Id;
385
+ readonly message:
386
+ | {
387
+ readonly type: "Mutate";
388
+ readonly messagesByOwnerId: ReadonlyMap<
389
+ OwnerId,
390
+ NonEmptyReadonlyArray<CrdtMessage>
391
+ >;
392
+ readonly rowsByQuery: RowsByQueryMap;
393
+ }
394
+ | {
395
+ readonly type: "Query";
396
+ readonly rowsByQuery: RowsByQueryMap;
397
+ }
398
+ | {
399
+ readonly type: "Export";
400
+ readonly file: Uint8Array<ArrayBuffer>;
401
+ };
402
+ }
403
+ | {
404
+ readonly type: "ForSharedWorker";
405
+ readonly message:
406
+ | {
407
+ readonly type: "CreateSyncMessages";
408
+ readonly protocolMessagesByOwnerId: ReadonlyMap<
409
+ OwnerId,
410
+ ProtocolMessage
411
+ >;
412
+ }
413
+ | {
414
+ readonly type: "ApplySyncMessage";
415
+ readonly ownerId: OwnerId;
416
+ readonly didWriteMessages: boolean;
417
+ readonly result: Result<
418
+ ApplyProtocolMessageAsClientResult,
419
+ ProtocolError | AbortError
420
+ >;
421
+ };
422
+ };
423
+ };
424
+
425
+ export type SyncState = 123;
426
+
427
+ const createSharedEvolu =
428
+ (name: Name): Task<SharedEvolu, never, SharedEvoluDeps> =>
429
+ (run) => {
430
+ const stack = new AsyncDisposableStack();
431
+ const sharedEvoluRun = run.create();
432
+
433
+ const console = run.deps.console.child(name).child("SharedWorker");
434
+ const { createMessagePort, postTabOutput, transports } = run.deps;
435
+
436
+ interface EvoluInstanceState {
437
+ readonly evoluPort: MessagePort<EvoluOutput, EvoluInput>;
438
+ readonly dbWorkerPort: MessagePort<DbWorkerInput, DbWorkerOutput>;
439
+ readonly releaseSharedEvolu: () => void;
440
+ rowsByQuery: RowsByQueryMap;
441
+ readonly usedSyncOwners: RefCountByKey<SyncOwner>;
442
+ }
443
+
444
+ const evoluInstancesByPortId = new Map<Id, EvoluInstanceState>();
445
+ const queue: Array<DbWorkerInput["request"]> = [];
446
+ const callbacks = stack.use(
447
+ createCallbacks<ExtractType<DbWorkerOutput, "OnQueuedResponse">>(
448
+ run.deps,
449
+ ),
450
+ );
451
+
452
+ let leaderDbWorkerPort = null as MessagePort<
453
+ DbWorkerInput,
454
+ DbWorkerOutput
455
+ > | null;
456
+ let queueProcessingFiber: Fiber<void, never, WorkerDeps> | null = null;
457
+
458
+ stack.use(sharedEvoluRun);
459
+ const moved = stack.move();
460
+
461
+ const ensureQueueProcessing = (): void => {
462
+ if (
463
+ queueProcessingFiber ||
464
+ !isNonEmptyArray(queue) ||
465
+ !leaderDbWorkerPort
466
+ ) {
467
+ return;
468
+ }
469
+
470
+ const first = firstInArray(queue);
471
+
472
+ const callbackId = callbacks.register(({ response }) => {
473
+ queueProcessingFiber?.abort();
474
+ queueProcessingFiber = null;
475
+
476
+ switch (response.type) {
477
+ case "ForEvolu": {
478
+ handleResponseForEvolu(response, first);
479
+ break;
480
+ }
481
+
482
+ case "ForSharedWorker":
483
+ handleResponseForSharedWorker(response);
484
+ break;
485
+
486
+ default:
487
+ exhaustiveCheck(response);
488
+ }
489
+
490
+ // Complete the current queue item and continue with the next one.
491
+ shiftFromArray(queue);
492
+ ensureQueueProcessing();
493
+ });
494
+
495
+ /**
496
+ * The leader DbWorker can disappear at any time when its tab closes, so
497
+ * keep resending the current request until some leader answers. The
498
+ * response handler aborts this retry loop and advances the queue.
499
+ */
500
+ queueProcessingFiber = sharedEvoluRun.daemon(
501
+ repeat(() => {
502
+ assert(leaderDbWorkerPort, "Expected a leader DbWorker");
503
+ leaderDbWorkerPort.postMessage({ callbackId, request: first });
504
+ return ok();
505
+ }, spaced("5s")),
506
+ );
507
+ };
508
+
509
+ const handleResponseForEvolu = (
510
+ response: ExtractType<
511
+ ExtractType<DbWorkerOutput, "OnQueuedResponse">["response"],
512
+ "ForEvolu"
513
+ >,
514
+ first: DbWorkerInput["request"],
515
+ ): void => {
516
+ const instance = evoluInstancesByPortId.get(response.evoluPortId);
517
+ const port = instance?.evoluPort;
518
+
519
+ if (!port) return;
520
+
521
+ switch (response.message.type) {
522
+ case "Mutate":
523
+ case "Query": {
524
+ const previousRowsByQuery = instance.rowsByQuery;
525
+ const nextRowsByQuery = new Map(previousRowsByQuery);
526
+ const patchesByQuery = new Map<Query, ReadonlyArray<Patch>>();
527
+
528
+ for (const [query, rows] of response.message.rowsByQuery) {
529
+ nextRowsByQuery.set(query, rows);
530
+ patchesByQuery.set(
531
+ query,
532
+ makePatches(previousRowsByQuery.get(query), rows),
533
+ );
534
+ }
535
+
536
+ instance.rowsByQuery = nextRowsByQuery;
537
+
538
+ instance.evoluPort.postMessage({
539
+ type: "OnPatchesByQuery",
540
+ patchesByQuery,
541
+ onCompleteIds:
542
+ first.message.type === "Mutate"
543
+ ? first.message.onCompleteIds
544
+ : emptyArray,
545
+ });
546
+
547
+ if (response.message.type === "Mutate") {
548
+ const mutateResponse = response.message;
549
+
550
+ for (const [
551
+ otherEvoluPortId,
552
+ otherEvoluInstance,
553
+ ] of evoluInstancesByPortId) {
554
+ if (otherEvoluPortId === response.evoluPortId) continue;
555
+ otherEvoluInstance.evoluPort.postMessage({
556
+ type: "RefreshQueries",
557
+ });
558
+ }
559
+
560
+ sharedEvoluRun<void, never>((run) => {
561
+ const protocolMessagesByOwnerId = new Map<
562
+ OwnerId,
563
+ ProtocolMessage
564
+ >();
565
+
566
+ for (const syncOwner of instance.usedSyncOwners.keys()) {
567
+ const { owner } = syncOwner;
568
+ const messages = mutateResponse.messagesByOwnerId.get(owner.id);
569
+
570
+ // Skip owners this instance does not currently sync for
571
+ // writing. Read-only owners cannot produce protocol
572
+ // messages because they do not have a write key.
573
+ if (!messages || !("writeKey" in owner)) continue;
574
+
575
+ protocolMessagesByOwnerId.set(
576
+ owner.id,
577
+ createProtocolMessageFromCrdtMessages(run.deps)(
578
+ owner,
579
+ messages,
580
+ ),
581
+ );
582
+ }
583
+
584
+ sendProtocolMessagesByOwnerId(protocolMessagesByOwnerId);
585
+
586
+ return ok();
587
+ });
588
+ }
589
+ break;
590
+ }
591
+
592
+ case "Export":
593
+ port.postMessage({ type: "OnExport", file: response.message.file }, [
594
+ response.message.file.buffer,
595
+ ]);
596
+ break;
597
+
598
+ default:
599
+ exhaustiveCheck(response.message);
600
+ }
601
+ };
602
+
603
+ const handleResponseForSharedWorker = (
604
+ response: ExtractType<
605
+ ExtractType<DbWorkerOutput, "OnQueuedResponse">["response"],
606
+ "ForSharedWorker"
607
+ >,
608
+ ): void => {
609
+ switch (response.message.type) {
610
+ case "CreateSyncMessages":
611
+ sendProtocolMessagesByOwnerId(
612
+ response.message.protocolMessagesByOwnerId,
613
+ );
614
+ break;
615
+
616
+ case "ApplySyncMessage":
617
+ if (response.message.didWriteMessages) {
618
+ refreshQueries();
619
+ }
620
+
621
+ if (!response.message.result.ok) {
622
+ if (response.message.result.error.type !== "AbortError") {
623
+ postTabOutput({
624
+ type: "OnError",
625
+ error: response.message.result.error,
626
+ });
627
+ }
628
+ } else {
629
+ switch (response.message.result.value.type) {
630
+ case "Response":
631
+ sendProtocolMessagesByOwnerId(
632
+ new Map([
633
+ [
634
+ response.message.ownerId,
635
+ response.message.result.value.message,
636
+ ],
637
+ ]),
638
+ );
639
+ break;
640
+
641
+ case "Broadcast":
642
+ case "NoResponse":
643
+ break;
644
+
645
+ default:
646
+ exhaustiveCheck(response.message.result.value);
647
+ }
648
+ }
649
+ break;
650
+
651
+ default:
652
+ exhaustiveCheck(response.message);
653
+ }
654
+ };
655
+
656
+ const sendProtocolMessagesByOwnerId = (
657
+ protocolMessagesByOwnerId: ReadonlyMap<OwnerId, ProtocolMessage>,
658
+ ): void => {
659
+ for (const [ownerId, protocolMessage] of protocolMessagesByOwnerId) {
660
+ for (const transport of transports.getResourceKeysForClaim(ownerId)) {
661
+ const webSocket = transports.getResource(transport);
662
+ if (!webSocket?.isOpen()) continue;
663
+
664
+ console.debug("sendProtocolMessage", {
665
+ ownerId,
666
+ url: transport.url,
667
+ byteLength: protocolMessage.byteLength,
668
+ });
669
+
670
+ webSocket.send(protocolMessage);
671
+ }
672
+ }
673
+ };
674
+
675
+ const refreshQueries = (): void => {
676
+ for (const evoluInstance of evoluInstancesByPortId.values()) {
677
+ evoluInstance.evoluPort.postMessage({ type: "RefreshQueries" });
678
+ }
679
+ };
680
+
681
+ const getUsedOwnersById = (
682
+ ownerIds: ReadonlySet<OwnerId>,
683
+ ): ReadonlyMap<OwnerId, Owner> => {
684
+ const ownersById = new Map<OwnerId, Owner>();
685
+
686
+ for (const evoluInstance of evoluInstancesByPortId.values()) {
687
+ for (const { owner } of evoluInstance.usedSyncOwners.keys()) {
688
+ if (!ownerIds.has(owner.id) || !("writeKey" in owner)) continue;
689
+
690
+ ownersById.set(owner.id, owner);
691
+ }
692
+ }
693
+
694
+ return ownersById;
695
+ };
696
+
697
+ const removeAllUsedSyncOwners = (
698
+ evoluInstance: EvoluInstanceState,
699
+ ): Task<void, never, SharedEvoluDeps> =>
700
+ unabortable(async (run) => {
701
+ for (const syncOwner of evoluInstance.usedSyncOwners.keys()) {
702
+ while (evoluInstance.usedSyncOwners.has(syncOwner)) {
703
+ await run(toggleUsedSyncOwner(evoluInstance, syncOwner, "remove"));
704
+ }
705
+ }
706
+ return ok();
707
+ });
708
+
709
+ const toggleUsedSyncOwner = (
710
+ evoluInstance: EvoluInstanceState,
711
+ syncOwner: SyncOwner,
712
+ action: "add" | "remove",
713
+ ): Task<void, never, SharedEvoluDeps> =>
714
+ unabortable(async (run) => {
715
+ if (action === "add") {
716
+ evoluInstance.usedSyncOwners.increment(syncOwner);
717
+ await run(
718
+ transports.addClaim(syncOwner.owner.id, syncOwner.transports),
719
+ );
720
+ } else {
721
+ evoluInstance.usedSyncOwners.decrement(syncOwner);
722
+ await run(
723
+ transports.removeClaim(syncOwner.owner.id, syncOwner.transports),
724
+ );
725
+ }
726
+ return ok();
727
+ });
728
+
729
+ return ok({
730
+ createInstance: (
731
+ nativeEvoluPort: NativeMessagePort<EvoluOutput, EvoluInput>,
732
+ nativeDbWorkerPort: NativeMessagePort<DbWorkerInput, DbWorkerOutput>,
733
+ releaseSharedEvolu: () => void,
734
+ ): void => {
735
+ assertNotDisposed(moved);
736
+
737
+ const evoluPort = createMessagePort<EvoluOutput, EvoluInput>(
738
+ nativeEvoluPort,
739
+ );
740
+ const dbWorkerPort = createMessagePort<DbWorkerInput, DbWorkerOutput>(
741
+ nativeDbWorkerPort,
742
+ );
743
+
744
+ const evoluPortId = createId(run.deps);
745
+
746
+ const evoluInstance: EvoluInstanceState = {
747
+ evoluPort,
748
+ dbWorkerPort,
749
+ releaseSharedEvolu,
750
+ rowsByQuery: new Map(),
751
+ usedSyncOwners: createRefCountByKey<SyncOwner, OwnerId>({
752
+ lookup: ({ owner: { id } }) => id,
753
+ }),
754
+ };
755
+
756
+ evoluInstancesByPortId.set(evoluPortId, evoluInstance);
757
+
758
+ dbWorkerPort.onMessage = (message) => {
759
+ switch (message.type) {
760
+ case "LeaderAcquired": {
761
+ leaderDbWorkerPort = evoluInstance.dbWorkerPort;
762
+ console.info("leaderAcquired");
763
+ ensureQueueProcessing();
764
+ break;
765
+ }
766
+
767
+ case "OnQueuedResponse": {
768
+ callbacks.execute(message.callbackId, message);
769
+ break;
770
+ }
771
+
772
+ case "OnConsoleEntry":
773
+ case "OnError": {
774
+ postTabOutput(message);
775
+ break;
776
+ }
777
+
778
+ default:
779
+ exhaustiveCheck(message);
780
+ }
781
+ };
782
+
783
+ evoluPort.onMessage = (evoluMessage) => {
784
+ switch (evoluMessage.type) {
785
+ case "Query":
786
+ case "Export": {
787
+ queue.push({
788
+ type: "ForEvolu",
789
+ evoluPortId,
790
+ message: evoluMessage,
791
+ });
792
+ ensureQueueProcessing();
793
+ break;
794
+ }
795
+
796
+ case "Mutate": {
797
+ // TODO: Delegate do vsech evolu instances, co to pouzivaji
798
+ queue.push({
799
+ type: "ForEvolu",
800
+ evoluPortId,
801
+ message: evoluMessage,
802
+ });
803
+ ensureQueueProcessing();
804
+ break;
805
+ }
806
+
807
+ case "UseOwner": {
808
+ const evoluInstance = evoluInstancesByPortId.get(evoluPortId);
809
+ if (!evoluInstance) break;
810
+
811
+ for (const { owner, action } of evoluMessage.actions) {
812
+ console.debug("useOwner", {
813
+ evoluPortId,
814
+ action,
815
+ ownerId: owner.owner.id,
816
+ transportUrls: owner.transports.map(({ url }) => url),
817
+ });
818
+
819
+ void sharedEvoluRun(
820
+ toggleUsedSyncOwner(evoluInstance, owner, action),
821
+ );
822
+ }
823
+ break;
824
+ }
825
+
826
+ case "Dispose": {
827
+ const evoluInstance = evoluInstancesByPortId.get(evoluPortId);
828
+ if (!evoluInstance) break;
829
+
830
+ evoluInstancesByPortId.delete(evoluPortId);
831
+ const hadLastPort = evoluInstancesByPortId.size === 0;
832
+ console.info("evoluDispose", {
833
+ name,
834
+ evoluPortId,
835
+ hadLastPort,
836
+ });
837
+
838
+ // Potential plan: keep DbWorker ports in a SharedResource
839
+ // abstraction instead of deleting them eagerly here. DbWorkers use
840
+ // leader election because SQLite WASM needs a single active owner.
841
+ // When the last Evolu instance is disposed, broadcast shutdown to
842
+ // all DbWorkers so the current leader can dispose itself and the
843
+ // followers can clean up consistently.
844
+ sharedEvoluRun<void, never>(async (run) => {
845
+ await run(removeAllUsedSyncOwners(evoluInstance));
846
+ evoluInstance.usedSyncOwners[Symbol.dispose]();
847
+ return ok();
848
+ });
849
+
850
+ // if (hadLastPort) evoluInstance.releaseSharedEvolu();
851
+
852
+ // TODO: Dispose SharedEvolu on the last port.
853
+
854
+ // TODO: Decided what to do with DbWorker but probably dispose it, but
855
+ // https://bugs.webkit.org/show_bug.cgi?id=301520
856
+ break;
857
+ }
858
+
859
+ default:
860
+ exhaustiveCheck(evoluMessage);
861
+ }
862
+ };
863
+ },
864
+
865
+ requestCreateSyncMessages: (ownerIds): void => {
866
+ const ownersToSync = [...getUsedOwnersById(ownerIds).values()];
867
+
868
+ if (!isNonEmptyArray(ownersToSync)) return;
869
+
870
+ console.debug("requestCreateSyncMessages", {
871
+ ownerIds: ownersToSync.map(({ id }) => id),
872
+ });
873
+
874
+ queue.push({
875
+ type: "ForSharedWorker",
876
+ message: {
877
+ type: "CreateSyncMessages",
878
+ owners: ownersToSync,
879
+ },
880
+ });
881
+
882
+ ensureQueueProcessing();
883
+ },
884
+
885
+ requestApplySyncMessage: (ownerId, inputMessage): void => {
886
+ const owner = getUsedOwnersById(new Set([ownerId])).get(ownerId);
887
+
888
+ if (!owner) return;
889
+
890
+ console.debug("requestApplySyncMessage", {
891
+ ownerId,
892
+ byteLength: inputMessage.byteLength,
893
+ });
894
+
895
+ queue.push({
896
+ type: "ForSharedWorker",
897
+ message: {
898
+ type: "ApplySyncMessage",
899
+ owner,
900
+ inputMessage,
901
+ },
902
+ });
903
+
904
+ ensureQueueProcessing();
905
+ },
906
+
907
+ [Symbol.asyncDispose]: () => moved.disposeAsync(),
908
+ });
909
+ };
910
+
911
+ // | (Typed<"reset"> & {
912
+ // readonly onCompleteId: CallbackId;
913
+ // readonly reload: boolean;
914
+ // readonly restore?: {
915
+ // readonly sqliteSchema: SqliteSchema;
916
+ // readonly mnemonic: Mnemonic;
917
+ // };
918
+ // })
919
+ // | (Typed<"ensureSqliteSchema"> & {
920
+ // readonly sqliteSchema: SqliteSchema;
921
+ // })
922
+ // | (Typed<"export"> & {
923
+ // readonly onCompleteId: CallbackId;
924
+ // })
925
+ // | (Typed<"useOwner"> & {
926
+ // readonly use: boolean;
927
+ // readonly owner: SyncOwner;
928
+ // });
929
+ // | (Typed<"onReset"> & {
930
+ // readonly onCompleteId: CallbackId;
931
+ // readonly reload: boolean;
932
+ // })