@evolu/common 8.0.0-next.3 → 8.0.0-next.5
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/Assert.d.ts +10 -5
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +10 -5
- package/dist/src/Error.d.ts +5 -3
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +16 -0
- package/dist/src/Identicon.js +4 -4
- package/dist/src/LockManager.d.ts +74 -0
- package/dist/src/LockManager.d.ts.map +1 -0
- package/dist/src/LockManager.js +103 -0
- package/dist/src/Microtask.d.ts.map +1 -1
- package/dist/src/Microtask.js +90 -26
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/RefCount.d.ts.map +1 -1
- package/dist/src/RefCount.js +153 -68
- package/dist/src/Resource.d.ts +6 -6
- package/dist/src/Resource.d.ts.map +1 -1
- package/dist/src/Resource.js +211 -162
- package/dist/src/Schedule.d.ts +5 -5
- package/dist/src/Schedule.d.ts.map +1 -1
- package/dist/src/Schedule.js +53 -44
- package/dist/src/Sqlite.d.ts +2 -0
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +66 -41
- package/dist/src/Task.d.ts +99 -123
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +273 -256
- package/dist/src/Test.d.ts +14 -15
- package/dist/src/Test.d.ts.map +1 -1
- package/dist/src/Time.d.ts +5 -0
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +5 -0
- package/dist/src/Type.d.ts +7 -0
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +7 -0
- package/dist/src/Types.d.ts +0 -8
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +7 -6
- package/dist/src/Worker.d.ts +42 -8
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +292 -112
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/local-first/Db.d.ts +5 -4
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +43 -37
- package/dist/src/local-first/Evolu.d.ts +3 -2
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +90 -80
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Shared.d.ts +65 -54
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +390 -376
- package/package.json +1 -1
- package/src/Array.ts +8 -8
- package/src/Assert.ts +11 -6
- package/src/Error.ts +5 -4
- package/src/Identicon.ts +4 -4
- package/src/LockManager.ts +181 -0
- package/src/Microtask.ts +17 -11
- package/src/Object.ts +3 -3
- package/src/RefCount.ts +25 -21
- package/src/Resource.ts +63 -56
- package/src/Schedule.ts +70 -48
- package/src/Set.ts +4 -4
- package/src/Sqlite.ts +43 -31
- package/src/Task.ts +392 -430
- package/src/Test.ts +14 -15
- package/src/Time.ts +7 -0
- package/src/Type.ts +9 -0
- package/src/Types.ts +0 -9
- package/src/WebSocket.ts +7 -6
- package/src/Worker.ts +195 -53
- package/src/index.ts +1 -0
- package/src/local-first/Db.ts +86 -61
- package/src/local-first/Evolu.ts +74 -68
- package/src/local-first/Owner.ts +1 -1
- package/src/local-first/Query.ts +1 -1
- package/src/local-first/Shared.ts +485 -476
|
@@ -11,10 +11,12 @@ import {
|
|
|
11
11
|
shiftFromArray,
|
|
12
12
|
type NonEmptyReadonlyArray,
|
|
13
13
|
} from "../Array.js";
|
|
14
|
-
import { assert, assertNotDisposed } from "../Assert.js";
|
|
14
|
+
import { assert, assertNotAborted, assertNotDisposed } from "../Assert.js";
|
|
15
|
+
import type { Brand } from "../Brand.js";
|
|
15
16
|
import { createCallbacks } from "../Callbacks.js";
|
|
16
17
|
import type { ConsoleEntry, ConsoleLevel } from "../Console.js";
|
|
17
|
-
import {
|
|
18
|
+
import type { EncryptionKey } from "../Crypto.js";
|
|
19
|
+
import { acquireLeaderLock, type LockManagerDep } from "../LockManager.js";
|
|
18
20
|
import { structuralLookup, type StructuralLookupKey } from "../Lookup.js";
|
|
19
21
|
import { createRefCountByKey, type RefCountByKey } from "../RefCount.js";
|
|
20
22
|
import {
|
|
@@ -24,25 +26,29 @@ import {
|
|
|
24
26
|
type SharedResourceByKeyWithClaims,
|
|
25
27
|
} from "../Resource.js";
|
|
26
28
|
import { ok, type Result } from "../Result.js";
|
|
27
|
-
import { spaced } from "../Schedule.js";
|
|
28
29
|
import type { NonEmptyReadonlySet } from "../Set.js";
|
|
30
|
+
import type { SqliteSchema } from "../Sqlite.js";
|
|
31
|
+
import { createStore, type Store } from "../Store.js";
|
|
29
32
|
import {
|
|
30
|
-
|
|
33
|
+
AbortError,
|
|
34
|
+
createMutex,
|
|
31
35
|
unabortable,
|
|
32
|
-
type
|
|
33
|
-
type Fiber,
|
|
36
|
+
type Mutex,
|
|
34
37
|
type Task,
|
|
35
38
|
} from "../Task.js";
|
|
36
|
-
import {
|
|
39
|
+
import { type Id, type Name, type Typed } from "../Type.js";
|
|
37
40
|
import type { Callback, ExtractType } from "../Types.js";
|
|
38
41
|
import type { CreateWebSocketDep, WebSocket } from "../WebSocket.js";
|
|
39
42
|
import type {
|
|
40
43
|
SharedWorker as CommonSharedWorker,
|
|
44
|
+
CreateBroadcastChannelDep,
|
|
45
|
+
CreateMessageChannelDep,
|
|
41
46
|
MessagePort,
|
|
42
47
|
NativeMessagePort,
|
|
43
48
|
SharedWorkerSelf,
|
|
44
49
|
WorkerDeps,
|
|
45
50
|
} from "../Worker.js";
|
|
51
|
+
import type { DbWorkerInit } from "./Db.js";
|
|
46
52
|
import type { EvoluError } from "./Error.js";
|
|
47
53
|
import type { Owner, OwnerId, OwnerTransport, SyncOwner } from "./Owner.js";
|
|
48
54
|
import {
|
|
@@ -57,42 +63,52 @@ import {
|
|
|
57
63
|
makePatches,
|
|
58
64
|
type Patch,
|
|
59
65
|
type Query,
|
|
66
|
+
type Row,
|
|
60
67
|
type RowsByQueryMap,
|
|
61
68
|
} from "./Query.js";
|
|
62
69
|
import type { MutationChange } from "./Schema.js";
|
|
63
70
|
import type { CrdtMessage } from "./Storage.js";
|
|
64
71
|
|
|
65
|
-
export type SharedWorker = CommonSharedWorker<
|
|
72
|
+
export type SharedWorker = CommonSharedWorker<
|
|
73
|
+
SharedWorkerInput,
|
|
74
|
+
SharedWorkerOutput
|
|
75
|
+
>;
|
|
66
76
|
|
|
67
77
|
export interface SharedWorkerDep {
|
|
68
78
|
readonly sharedWorker: SharedWorker;
|
|
69
79
|
}
|
|
70
80
|
|
|
71
|
-
export type SharedWorkerDeps = WorkerDeps & CreateWebSocketDep;
|
|
72
|
-
|
|
73
81
|
export type SharedWorkerInput =
|
|
74
82
|
| {
|
|
75
|
-
readonly type: "
|
|
83
|
+
readonly type: "AnnounceTabLeader";
|
|
76
84
|
readonly consoleLevel: ConsoleLevel;
|
|
77
|
-
readonly port: NativeMessagePort<EvoluTabOutput>;
|
|
78
85
|
}
|
|
79
86
|
| {
|
|
80
87
|
readonly type: "CreateEvolu";
|
|
81
88
|
readonly name: Name;
|
|
89
|
+
readonly id: EvoluInstanceId;
|
|
90
|
+
readonly consoleLevel: ConsoleLevel;
|
|
91
|
+
readonly sqliteSchema: SqliteSchema;
|
|
92
|
+
readonly encryptionKey: EncryptionKey;
|
|
93
|
+
readonly memoryOnly: boolean;
|
|
82
94
|
readonly evoluPort: NativeMessagePort<EvoluOutput, EvoluInput>;
|
|
83
|
-
readonly dbWorkerPort: NativeMessagePort<DbWorkerInput, DbWorkerOutput>;
|
|
84
95
|
};
|
|
85
96
|
|
|
86
|
-
export type
|
|
97
|
+
export type SharedWorkerOutput = DbWorkerInit;
|
|
98
|
+
|
|
99
|
+
export type ConsoleEntryOrError =
|
|
87
100
|
| {
|
|
88
|
-
readonly type: "
|
|
101
|
+
readonly type: "ConsoleEntry";
|
|
89
102
|
readonly entry: ConsoleEntry;
|
|
90
103
|
}
|
|
91
104
|
| {
|
|
92
|
-
readonly type: "
|
|
105
|
+
readonly type: "Error";
|
|
93
106
|
readonly error: EvoluError;
|
|
94
107
|
};
|
|
95
108
|
|
|
109
|
+
export const consoleEntryOrErrorBroadcastChannelName =
|
|
110
|
+
"evolu:console-entry-or-error";
|
|
111
|
+
|
|
96
112
|
export type EvoluInput =
|
|
97
113
|
| {
|
|
98
114
|
readonly type: "Mutate";
|
|
@@ -113,9 +129,6 @@ export type EvoluInput =
|
|
|
113
129
|
}
|
|
114
130
|
| {
|
|
115
131
|
readonly type: "Export";
|
|
116
|
-
}
|
|
117
|
-
| {
|
|
118
|
-
readonly type: "Dispose";
|
|
119
132
|
};
|
|
120
133
|
|
|
121
134
|
export type EvoluOutput =
|
|
@@ -132,32 +145,158 @@ export type EvoluOutput =
|
|
|
132
145
|
readonly file: Uint8Array<ArrayBuffer>;
|
|
133
146
|
};
|
|
134
147
|
|
|
148
|
+
export type DbWorkerInput =
|
|
149
|
+
| (Typed<"Request"> & {
|
|
150
|
+
readonly callbackId: Id;
|
|
151
|
+
readonly request: DbWorkerRequest;
|
|
152
|
+
})
|
|
153
|
+
| Typed<"Dispose">;
|
|
154
|
+
|
|
155
|
+
export type DbWorkerRequest =
|
|
156
|
+
| {
|
|
157
|
+
readonly type: "ForEvolu";
|
|
158
|
+
readonly id: EvoluInstanceId;
|
|
159
|
+
readonly message: ExtractType<EvoluInput, "Mutate" | "Query" | "Export">;
|
|
160
|
+
}
|
|
161
|
+
| {
|
|
162
|
+
readonly type: "ForSharedWorker";
|
|
163
|
+
readonly message:
|
|
164
|
+
| {
|
|
165
|
+
readonly type: "CreateSyncMessages";
|
|
166
|
+
readonly owners: NonEmptyReadonlyArray<Owner>;
|
|
167
|
+
}
|
|
168
|
+
| {
|
|
169
|
+
readonly type: "ApplySyncMessage";
|
|
170
|
+
readonly owner: Owner;
|
|
171
|
+
readonly inputMessage: Uint8Array;
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export type DbWorkerOutput =
|
|
176
|
+
| {
|
|
177
|
+
readonly type: "LeaderAcquired";
|
|
178
|
+
readonly name: Name;
|
|
179
|
+
}
|
|
180
|
+
| {
|
|
181
|
+
readonly type: "OnQueuedResponse";
|
|
182
|
+
readonly callbackId: Id;
|
|
183
|
+
readonly response: DbWorkerQueuedResponse;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export type DbWorkerQueuedResponse =
|
|
187
|
+
| {
|
|
188
|
+
readonly type: "ForEvolu";
|
|
189
|
+
readonly id: EvoluInstanceId;
|
|
190
|
+
readonly message:
|
|
191
|
+
| {
|
|
192
|
+
readonly type: "Mutate";
|
|
193
|
+
readonly messagesByOwnerId: ReadonlyMap<
|
|
194
|
+
OwnerId,
|
|
195
|
+
NonEmptyReadonlyArray<CrdtMessage>
|
|
196
|
+
>;
|
|
197
|
+
readonly rowsByQuery: RowsByQueryMap;
|
|
198
|
+
}
|
|
199
|
+
| {
|
|
200
|
+
readonly type: "Query";
|
|
201
|
+
readonly rowsByQuery: RowsByQueryMap;
|
|
202
|
+
}
|
|
203
|
+
| {
|
|
204
|
+
readonly type: "Export";
|
|
205
|
+
readonly file: Uint8Array<ArrayBuffer>;
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
| {
|
|
209
|
+
readonly type: "ForSharedWorker";
|
|
210
|
+
readonly message:
|
|
211
|
+
| {
|
|
212
|
+
readonly type: "CreateSyncMessages";
|
|
213
|
+
readonly protocolMessagesByOwnerId: ReadonlyMap<
|
|
214
|
+
OwnerId,
|
|
215
|
+
ProtocolMessage
|
|
216
|
+
>;
|
|
217
|
+
}
|
|
218
|
+
| {
|
|
219
|
+
readonly type: "ApplySyncMessage";
|
|
220
|
+
readonly ownerId: OwnerId;
|
|
221
|
+
readonly didWriteMessages: boolean;
|
|
222
|
+
readonly result: Result<
|
|
223
|
+
ApplyProtocolMessageAsClientResult,
|
|
224
|
+
ProtocolError | AbortError
|
|
225
|
+
>;
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
export type SharedWorkerDeps = WorkerDeps &
|
|
230
|
+
CreateBroadcastChannelDep &
|
|
231
|
+
CreateMessageChannelDep &
|
|
232
|
+
CreateWebSocketDep &
|
|
233
|
+
LockManagerDep;
|
|
234
|
+
|
|
235
|
+
interface EvoluTenant extends AsyncDisposable {
|
|
236
|
+
readonly addInstance: (
|
|
237
|
+
message: ExtractType<SharedWorkerInput, "CreateEvolu">,
|
|
238
|
+
onDisposed: () => void,
|
|
239
|
+
) => void;
|
|
240
|
+
|
|
241
|
+
readonly requestCreateSyncMessages: (ownerIds: ReadonlySet<OwnerId>) => void;
|
|
242
|
+
|
|
243
|
+
readonly requestApplySyncMessage: (
|
|
244
|
+
ownerId: OwnerId,
|
|
245
|
+
inputMessage: Uint8Array,
|
|
246
|
+
) => void;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
type EvoluTenantDeps = SharedWorkerDeps &
|
|
250
|
+
PostConsoleEntryOrErrorDep &
|
|
251
|
+
TabLeaderPortStoreDep &
|
|
252
|
+
TransportsDep;
|
|
253
|
+
|
|
254
|
+
interface PostConsoleEntryOrErrorDep {
|
|
255
|
+
readonly postConsoleEntryOrError: Callback<ConsoleEntryOrError>;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
interface TabLeaderPortStoreDep {
|
|
259
|
+
readonly tabLeaderPortStore: Store<TabLeaderPort | null>;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
type TabLeaderPort = Pick<MessagePort<DbWorkerInit>, "postMessage">;
|
|
263
|
+
|
|
264
|
+
interface TransportsDep {
|
|
265
|
+
readonly transports: SharedResourceByKeyWithClaims<
|
|
266
|
+
OwnerTransport,
|
|
267
|
+
OwnerId,
|
|
268
|
+
WebSocket,
|
|
269
|
+
SharedWorkerDeps
|
|
270
|
+
>;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export type EvoluInstanceId = Id & Brand<"EvoluInstance">;
|
|
274
|
+
|
|
275
|
+
export type SyncState = 123;
|
|
276
|
+
|
|
135
277
|
export const initSharedWorker =
|
|
136
278
|
(
|
|
137
|
-
self: SharedWorkerSelf<SharedWorkerInput>,
|
|
279
|
+
self: SharedWorkerSelf<SharedWorkerInput, SharedWorkerOutput>,
|
|
138
280
|
): Task<AsyncDisposableStack, never, SharedWorkerDeps> =>
|
|
139
281
|
async (run) => {
|
|
140
|
-
const {
|
|
141
|
-
|
|
142
|
-
const console = run.deps.console.child("SharedWorker");
|
|
143
|
-
await using stack = new AsyncDisposableStack();
|
|
144
|
-
const sharedWorkerReady = Promise.withResolvers<void>();
|
|
282
|
+
const { deps } = run;
|
|
283
|
+
const console = deps.console.child("SharedWorker");
|
|
145
284
|
|
|
146
|
-
|
|
147
|
-
const tabPorts = new Set<MessagePort<EvoluTabOutput>>();
|
|
285
|
+
await using disposer = new AsyncDisposableStack();
|
|
148
286
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
consoleStoreOutputEntry.subscribe(() => {
|
|
157
|
-
const entry = consoleStoreOutputEntry.get();
|
|
158
|
-
if (entry) postTabOutput({ type: "OnConsoleEntry", entry });
|
|
159
|
-
}),
|
|
287
|
+
const tabLeaderPortStore = disposer.use(
|
|
288
|
+
createStore<TabLeaderPort | null>(null),
|
|
289
|
+
);
|
|
290
|
+
const consoleEntryOrErrorBroadcastChannel = disposer.use(
|
|
291
|
+
deps.createBroadcastChannel<ConsoleEntryOrError>(
|
|
292
|
+
consoleEntryOrErrorBroadcastChannelName,
|
|
293
|
+
),
|
|
160
294
|
);
|
|
295
|
+
const postConsoleEntryOrError = (output: ConsoleEntryOrError): void => {
|
|
296
|
+
consoleEntryOrErrorBroadcastChannel.postMessage(output);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const sharedWorkerReady = Promise.withResolvers<void>();
|
|
161
300
|
|
|
162
301
|
// Register ASAP so the worker does not miss connections.
|
|
163
302
|
self.onConnect = (port) => {
|
|
@@ -165,35 +304,23 @@ export const initSharedWorker =
|
|
|
165
304
|
// The underlying port buffers messages until onMessage is assigned.
|
|
166
305
|
port.onMessage = (message) => {
|
|
167
306
|
switch (message.type) {
|
|
168
|
-
case "
|
|
169
|
-
/**
|
|
170
|
-
* One SharedWorker serves multiple tabs. The most recently
|
|
171
|
-
* initialized tab's level wins.
|
|
172
|
-
*/
|
|
307
|
+
case "AnnounceTabLeader": {
|
|
173
308
|
console.setLevel(message.consoleLevel);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
tabPorts.add(tabPort);
|
|
177
|
-
if (queuedTabOutputs.length > 0) {
|
|
178
|
-
queuedTabOutputs.forEach(postTabOutput);
|
|
179
|
-
queuedTabOutputs.length = 0;
|
|
180
|
-
}
|
|
309
|
+
tabLeaderPortStore.set(port);
|
|
310
|
+
console.info("tabLeaderAnnounced");
|
|
181
311
|
break;
|
|
182
312
|
}
|
|
183
313
|
|
|
184
314
|
case "CreateEvolu": {
|
|
185
|
-
void
|
|
186
|
-
const
|
|
187
|
-
|
|
315
|
+
void sharedWorkerRun(async (run) => {
|
|
316
|
+
const tenant = await run(
|
|
317
|
+
unabortable(tenantsByName.acquire(message)),
|
|
188
318
|
);
|
|
189
|
-
if (!
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
message
|
|
193
|
-
() =>
|
|
194
|
-
void sharedWorkerRunWithSharedEvoluDeps(
|
|
195
|
-
sharedEvolusByName.release(message.name),
|
|
196
|
-
),
|
|
319
|
+
if (!tenant.ok) return tenant;
|
|
320
|
+
|
|
321
|
+
tenant.value.addInstance(
|
|
322
|
+
message,
|
|
323
|
+
() => void sharedWorkerRun(tenantsByName.release(message)),
|
|
197
324
|
);
|
|
198
325
|
return ok();
|
|
199
326
|
});
|
|
@@ -206,10 +333,15 @@ export const initSharedWorker =
|
|
|
206
333
|
});
|
|
207
334
|
};
|
|
208
335
|
|
|
209
|
-
|
|
336
|
+
disposer.defer(
|
|
337
|
+
deps.consoleStoreOutputEntry.subscribe(() => {
|
|
338
|
+
const entry = deps.consoleStoreOutputEntry.get();
|
|
339
|
+
if (entry) postConsoleEntryOrError({ type: "ConsoleEntry", entry });
|
|
340
|
+
}),
|
|
341
|
+
);
|
|
210
342
|
|
|
211
|
-
const transports =
|
|
212
|
-
await
|
|
343
|
+
const transports = disposer.use(
|
|
344
|
+
await run.orThrow(
|
|
213
345
|
createSharedResourceByKeyWithClaims<
|
|
214
346
|
WebSocket,
|
|
215
347
|
OwnerTransport,
|
|
@@ -218,7 +350,7 @@ export const initSharedWorker =
|
|
|
218
350
|
StructuralLookupKey
|
|
219
351
|
>(
|
|
220
352
|
(transport) =>
|
|
221
|
-
createWebSocket(transport.url, {
|
|
353
|
+
deps.createWebSocket(transport.url, {
|
|
222
354
|
binaryType: "arraybuffer",
|
|
223
355
|
|
|
224
356
|
onOpen: () => {
|
|
@@ -228,39 +360,35 @@ export const initSharedWorker =
|
|
|
228
360
|
ownerIds: [...ownerIds],
|
|
229
361
|
});
|
|
230
362
|
|
|
231
|
-
|
|
232
|
-
|
|
363
|
+
forEachTenant((tenant) => {
|
|
364
|
+
tenant.requestCreateSyncMessages(ownerIds);
|
|
233
365
|
});
|
|
234
366
|
},
|
|
235
367
|
|
|
236
368
|
onMessage(data) {
|
|
237
369
|
if (!(data instanceof ArrayBuffer)) return;
|
|
238
370
|
|
|
239
|
-
const
|
|
240
|
-
const headerResult = parseProtocolHeader(
|
|
241
|
-
new globalThis.Uint8Array(data),
|
|
242
|
-
);
|
|
371
|
+
const message = new Uint8Array(data);
|
|
372
|
+
const headerResult = parseProtocolHeader(message);
|
|
243
373
|
|
|
244
374
|
if (!headerResult.ok) {
|
|
245
375
|
console.debug("transportInvalidProtocolMessage", {
|
|
246
376
|
url: transport.url,
|
|
247
|
-
byteLength:
|
|
377
|
+
byteLength: message.byteLength,
|
|
248
378
|
});
|
|
249
|
-
|
|
250
|
-
// TODO: Propagate invalid protocol messages to sync state.
|
|
251
379
|
return;
|
|
252
380
|
}
|
|
253
381
|
|
|
254
382
|
console.debug("transportProtocolMessage", {
|
|
255
383
|
url: transport.url,
|
|
256
384
|
ownerId: headerResult.value.ownerId,
|
|
257
|
-
byteLength:
|
|
385
|
+
byteLength: message.byteLength,
|
|
258
386
|
});
|
|
259
387
|
|
|
260
|
-
|
|
261
|
-
|
|
388
|
+
forEachTenant((tenant) => {
|
|
389
|
+
tenant.requestApplySyncMessage(
|
|
262
390
|
headerResult.value.ownerId,
|
|
263
|
-
|
|
391
|
+
message,
|
|
264
392
|
);
|
|
265
393
|
});
|
|
266
394
|
},
|
|
@@ -268,8 +396,8 @@ export const initSharedWorker =
|
|
|
268
396
|
{
|
|
269
397
|
onFirstClaimAdded: (ownerId, webSocket) => {
|
|
270
398
|
if (!webSocket.isOpen()) return;
|
|
271
|
-
|
|
272
|
-
|
|
399
|
+
forEachTenant((tenant) => {
|
|
400
|
+
tenant.requestCreateSyncMessages(new Set([ownerId]));
|
|
273
401
|
});
|
|
274
402
|
},
|
|
275
403
|
|
|
@@ -284,258 +412,209 @@ export const initSharedWorker =
|
|
|
284
412
|
),
|
|
285
413
|
);
|
|
286
414
|
|
|
287
|
-
const
|
|
415
|
+
const sharedWorkerRun = run.create({
|
|
416
|
+
...deps,
|
|
417
|
+
postConsoleEntryOrError,
|
|
418
|
+
tabLeaderPortStore,
|
|
288
419
|
transports,
|
|
289
|
-
postTabOutput,
|
|
290
420
|
});
|
|
291
421
|
|
|
292
|
-
const
|
|
293
|
-
await
|
|
294
|
-
createSharedResourceByKey(
|
|
295
|
-
|
|
422
|
+
const tenantsByName = disposer.use(
|
|
423
|
+
await sharedWorkerRun.orThrow(
|
|
424
|
+
createSharedResourceByKey(createEvoluTenant, {
|
|
425
|
+
idleDisposeAfter: "3s",
|
|
426
|
+
lookup: (message) => message.name,
|
|
296
427
|
}),
|
|
297
428
|
),
|
|
298
429
|
);
|
|
299
430
|
|
|
300
|
-
|
|
301
|
-
|
|
431
|
+
sharedWorkerReady.resolve();
|
|
432
|
+
|
|
433
|
+
const forEachTenant = (
|
|
434
|
+
callback: Callback<BorrowedResource<EvoluTenant>>,
|
|
302
435
|
): void => {
|
|
303
|
-
for (const
|
|
304
|
-
|
|
305
|
-
.resourcesByKey.values()) {
|
|
306
|
-
callback(sharedEvolu);
|
|
436
|
+
for (const tenant of tenantsByName.snapshot().resourcesByKey.values()) {
|
|
437
|
+
callback(tenant);
|
|
307
438
|
}
|
|
308
439
|
};
|
|
309
440
|
|
|
310
|
-
|
|
311
|
-
console.info("initSharedWorker");
|
|
312
|
-
|
|
313
|
-
return ok(stack.move());
|
|
441
|
+
return ok(disposer.move());
|
|
314
442
|
};
|
|
315
443
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
) =>
|
|
329
|
-
|
|
444
|
+
const createEvoluTenant =
|
|
445
|
+
({
|
|
446
|
+
name,
|
|
447
|
+
consoleLevel,
|
|
448
|
+
sqliteSchema,
|
|
449
|
+
encryptionKey,
|
|
450
|
+
memoryOnly,
|
|
451
|
+
}: ExtractType<SharedWorkerInput, "CreateEvolu">): Task<
|
|
452
|
+
EvoluTenant,
|
|
453
|
+
never,
|
|
454
|
+
EvoluTenantDeps
|
|
455
|
+
> =>
|
|
456
|
+
async (run) => {
|
|
457
|
+
await using disposer = new AsyncDisposableStack();
|
|
458
|
+
const tenantRun = disposer.use(run.create());
|
|
330
459
|
|
|
331
|
-
|
|
460
|
+
const { deps } = run;
|
|
461
|
+
const console = deps.console.child(name).child("SharedWorker");
|
|
332
462
|
|
|
333
|
-
interface
|
|
334
|
-
|
|
335
|
-
|
|
463
|
+
interface EvoluInstance extends AsyncDisposable {
|
|
464
|
+
readonly id: EvoluInstanceId;
|
|
465
|
+
readonly onDisposed: () => void;
|
|
466
|
+
readonly port: MessagePort<EvoluOutput, EvoluInput>;
|
|
467
|
+
readonly useOwnerMutex: Mutex;
|
|
468
|
+
readonly usedSyncOwners: RefCountByKey<SyncOwner>;
|
|
469
|
+
rowsByQuery: Map<Query, ReadonlyArray<Row>>;
|
|
470
|
+
}
|
|
336
471
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
472
|
+
const instancesById = disposer.adopt(
|
|
473
|
+
new Map<EvoluInstanceId, EvoluInstance>(),
|
|
474
|
+
async (instancesById) => {
|
|
475
|
+
await using disposer = new AsyncDisposableStack();
|
|
476
|
+
for (const instance of [...instancesById.values()]) {
|
|
477
|
+
disposer.use(instance);
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
);
|
|
345
481
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
482
|
+
let dbWorkerPort = null as MessagePort<
|
|
483
|
+
DbWorkerInput,
|
|
484
|
+
DbWorkerOutput
|
|
485
|
+
> | null;
|
|
486
|
+
const dbWorkerInited = Promise.withResolvers<void>();
|
|
487
|
+
|
|
488
|
+
const initDbWorker = (): void => {
|
|
489
|
+
const tabLeaderPort = deps.tabLeaderPortStore.get();
|
|
490
|
+
assert(tabLeaderPort, "Expected tab leader port.");
|
|
491
|
+
|
|
492
|
+
const dbWorkerChannel = deps.createMessageChannel<
|
|
493
|
+
DbWorkerOutput,
|
|
494
|
+
DbWorkerInput
|
|
495
|
+
>();
|
|
496
|
+
const currentDbWorkerPort = dbWorkerChannel.port2;
|
|
497
|
+
|
|
498
|
+
currentDbWorkerPort.onMessage = (message) => {
|
|
499
|
+
switch (message.type) {
|
|
500
|
+
case "LeaderAcquired": {
|
|
501
|
+
assert(
|
|
502
|
+
dbWorkerPort !== currentDbWorkerPort,
|
|
503
|
+
"Expected a new DbWorker port.",
|
|
504
|
+
);
|
|
505
|
+
dbWorkerPort?.[Symbol.dispose]();
|
|
506
|
+
dbWorkerPort = currentDbWorkerPort;
|
|
507
|
+
queueRequestInFlight = false;
|
|
508
|
+
console.info("leaderAcquired");
|
|
509
|
+
dbWorkerInited.resolve();
|
|
510
|
+
runQueue();
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
case "OnQueuedResponse": {
|
|
514
|
+
callbacks.execute(message.callbackId, message);
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
369
518
|
};
|
|
370
|
-
}
|
|
371
519
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
};
|
|
520
|
+
tabLeaderPort.postMessage(
|
|
521
|
+
{
|
|
522
|
+
type: "DbWorkerInit",
|
|
523
|
+
name,
|
|
524
|
+
consoleLevel,
|
|
525
|
+
sqliteSchema,
|
|
526
|
+
encryptionKey,
|
|
527
|
+
memoryOnly,
|
|
528
|
+
port: dbWorkerChannel.port1.native,
|
|
529
|
+
},
|
|
530
|
+
[dbWorkerChannel.port1.native],
|
|
531
|
+
);
|
|
423
532
|
};
|
|
424
533
|
|
|
425
|
-
|
|
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(
|
|
534
|
+
const queue: Array<DbWorkerRequest> = [];
|
|
535
|
+
const callbacks = disposer.use(
|
|
447
536
|
createCallbacks<ExtractType<DbWorkerOutput, "OnQueuedResponse">>(
|
|
448
537
|
run.deps,
|
|
449
538
|
),
|
|
450
539
|
);
|
|
540
|
+
let queueRequestInFlight = false;
|
|
451
541
|
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
) {
|
|
542
|
+
const runQueue = (): void => {
|
|
543
|
+
if (queueRequestInFlight || !isNonEmptyArray(queue) || !dbWorkerPort) {
|
|
467
544
|
return;
|
|
468
545
|
}
|
|
469
546
|
|
|
470
|
-
const
|
|
547
|
+
const request = firstInArray(queue);
|
|
471
548
|
|
|
472
549
|
const callbackId = callbacks.register(({ response }) => {
|
|
473
|
-
queueProcessingFiber?.abort();
|
|
474
|
-
queueProcessingFiber = null;
|
|
475
|
-
|
|
476
550
|
switch (response.type) {
|
|
477
551
|
case "ForEvolu": {
|
|
478
|
-
handleResponseForEvolu(response,
|
|
552
|
+
handleResponseForEvolu(response, request);
|
|
479
553
|
break;
|
|
480
554
|
}
|
|
481
555
|
|
|
482
556
|
case "ForSharedWorker":
|
|
483
557
|
handleResponseForSharedWorker(response);
|
|
484
558
|
break;
|
|
485
|
-
|
|
486
|
-
default:
|
|
487
|
-
exhaustiveCheck(response);
|
|
488
559
|
}
|
|
489
560
|
|
|
490
561
|
// Complete the current queue item and continue with the next one.
|
|
491
562
|
shiftFromArray(queue);
|
|
492
|
-
|
|
563
|
+
queueRequestInFlight = false;
|
|
564
|
+
runQueue();
|
|
493
565
|
});
|
|
494
566
|
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
);
|
|
567
|
+
queueRequestInFlight = true;
|
|
568
|
+
dbWorkerPort.postMessage({ type: "Request", callbackId, request });
|
|
507
569
|
};
|
|
508
570
|
|
|
571
|
+
disposer.defer(async () => {
|
|
572
|
+
dbWorkerPort?.postMessage({ type: "Dispose" });
|
|
573
|
+
dbWorkerPort = null;
|
|
574
|
+
queueRequestInFlight = false;
|
|
575
|
+
|
|
576
|
+
// The DbWorker holds this tenant leader lock while it is alive. Tenant
|
|
577
|
+
// disposal sends Dispose, then acquires the same lock to wait until the
|
|
578
|
+
// DbWorker releases it: either because Dispose was delivered or because
|
|
579
|
+
// the hosting tab closed. The wait is unabortable because tenant disposal
|
|
580
|
+
// must finish even after tenantRun receives an abort request.
|
|
581
|
+
const lock = await tenantRun(acquireLeaderLock(name));
|
|
582
|
+
// AbortError here means tenantRun was already disposed before this
|
|
583
|
+
// cleanup could start, violating the disposal ordering above.
|
|
584
|
+
assertNotAborted(lock);
|
|
585
|
+
await using _ = lock.value;
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
disposer.defer(deps.tabLeaderPortStore.subscribe(initDbWorker));
|
|
589
|
+
|
|
590
|
+
initDbWorker();
|
|
591
|
+
await dbWorkerInited.promise;
|
|
592
|
+
const disposables = disposer.move();
|
|
593
|
+
|
|
509
594
|
const handleResponseForEvolu = (
|
|
510
|
-
response: ExtractType<
|
|
511
|
-
|
|
512
|
-
"ForEvolu"
|
|
513
|
-
>,
|
|
514
|
-
first: DbWorkerInput["request"],
|
|
595
|
+
response: ExtractType<DbWorkerQueuedResponse, "ForEvolu">,
|
|
596
|
+
first: DbWorkerRequest,
|
|
515
597
|
): void => {
|
|
516
|
-
const instance =
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
if (!port) return;
|
|
598
|
+
const instance = instancesById.get(response.id);
|
|
599
|
+
if (!instance) return;
|
|
520
600
|
|
|
521
601
|
switch (response.message.type) {
|
|
522
602
|
case "Mutate":
|
|
523
603
|
case "Query": {
|
|
524
|
-
const
|
|
525
|
-
const nextRowsByQuery = new Map(previousRowsByQuery);
|
|
604
|
+
const nextRowsByQuery = new Map(instance.rowsByQuery);
|
|
526
605
|
const patchesByQuery = new Map<Query, ReadonlyArray<Patch>>();
|
|
527
606
|
|
|
528
607
|
for (const [query, rows] of response.message.rowsByQuery) {
|
|
529
608
|
nextRowsByQuery.set(query, rows);
|
|
530
609
|
patchesByQuery.set(
|
|
531
610
|
query,
|
|
532
|
-
makePatches(
|
|
611
|
+
makePatches(instance.rowsByQuery.get(query), rows),
|
|
533
612
|
);
|
|
534
613
|
}
|
|
535
614
|
|
|
536
615
|
instance.rowsByQuery = nextRowsByQuery;
|
|
537
616
|
|
|
538
|
-
instance.
|
|
617
|
+
instance.port.postMessage({
|
|
539
618
|
type: "OnPatchesByQuery",
|
|
540
619
|
patchesByQuery,
|
|
541
620
|
onCompleteIds:
|
|
@@ -545,66 +624,52 @@ const createSharedEvolu =
|
|
|
545
624
|
});
|
|
546
625
|
|
|
547
626
|
if (response.message.type === "Mutate") {
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
otherEvoluPortId,
|
|
552
|
-
otherEvoluInstance,
|
|
553
|
-
] of evoluInstancesByPortId) {
|
|
554
|
-
if (otherEvoluPortId === response.evoluPortId) continue;
|
|
555
|
-
otherEvoluInstance.evoluPort.postMessage({
|
|
627
|
+
for (const [instanceId, instance] of instancesById) {
|
|
628
|
+
if (instanceId === response.id) continue;
|
|
629
|
+
instance.port.postMessage({
|
|
556
630
|
type: "RefreshQueries",
|
|
557
631
|
});
|
|
558
632
|
}
|
|
559
633
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
sendProtocolMessagesByOwnerId(protocolMessagesByOwnerId);
|
|
634
|
+
const protocolMessagesByOwnerId = new Map<
|
|
635
|
+
OwnerId,
|
|
636
|
+
ProtocolMessage
|
|
637
|
+
>();
|
|
638
|
+
|
|
639
|
+
for (const syncOwner of instance.usedSyncOwners.keys()) {
|
|
640
|
+
const { owner } = syncOwner;
|
|
641
|
+
const messages = response.message.messagesByOwnerId.get(owner.id);
|
|
642
|
+
|
|
643
|
+
// Skip owners this instance does not currently sync for
|
|
644
|
+
// writing. Read-only owners cannot produce protocol
|
|
645
|
+
// messages because they do not have a write key.
|
|
646
|
+
if (!messages || !("writeKey" in owner)) continue;
|
|
647
|
+
|
|
648
|
+
protocolMessagesByOwnerId.set(
|
|
649
|
+
owner.id,
|
|
650
|
+
createProtocolMessageFromCrdtMessages(run.deps)(
|
|
651
|
+
owner,
|
|
652
|
+
messages,
|
|
653
|
+
),
|
|
654
|
+
);
|
|
655
|
+
}
|
|
585
656
|
|
|
586
|
-
|
|
587
|
-
});
|
|
657
|
+
sendProtocolMessagesByOwnerId(protocolMessagesByOwnerId);
|
|
588
658
|
}
|
|
589
659
|
break;
|
|
590
660
|
}
|
|
591
661
|
|
|
592
662
|
case "Export":
|
|
593
|
-
port.postMessage(
|
|
594
|
-
response.message.file
|
|
595
|
-
|
|
663
|
+
instance.port.postMessage(
|
|
664
|
+
{ type: "OnExport", file: response.message.file },
|
|
665
|
+
[response.message.file.buffer],
|
|
666
|
+
);
|
|
596
667
|
break;
|
|
597
|
-
|
|
598
|
-
default:
|
|
599
|
-
exhaustiveCheck(response.message);
|
|
600
668
|
}
|
|
601
669
|
};
|
|
602
670
|
|
|
603
671
|
const handleResponseForSharedWorker = (
|
|
604
|
-
response: ExtractType<
|
|
605
|
-
ExtractType<DbWorkerOutput, "OnQueuedResponse">["response"],
|
|
606
|
-
"ForSharedWorker"
|
|
607
|
-
>,
|
|
672
|
+
response: ExtractType<DbWorkerQueuedResponse, "ForSharedWorker">,
|
|
608
673
|
): void => {
|
|
609
674
|
switch (response.message.type) {
|
|
610
675
|
case "CreateSyncMessages":
|
|
@@ -615,13 +680,15 @@ const createSharedEvolu =
|
|
|
615
680
|
|
|
616
681
|
case "ApplySyncMessage":
|
|
617
682
|
if (response.message.didWriteMessages) {
|
|
618
|
-
|
|
683
|
+
for (const instance of instancesById.values()) {
|
|
684
|
+
instance.port.postMessage({ type: "RefreshQueries" });
|
|
685
|
+
}
|
|
619
686
|
}
|
|
620
687
|
|
|
621
688
|
if (!response.message.result.ok) {
|
|
622
689
|
if (response.message.result.error.type !== "AbortError") {
|
|
623
|
-
|
|
624
|
-
type: "
|
|
690
|
+
deps.postConsoleEntryOrError({
|
|
691
|
+
type: "Error",
|
|
625
692
|
error: response.message.result.error,
|
|
626
693
|
});
|
|
627
694
|
}
|
|
@@ -641,15 +708,9 @@ const createSharedEvolu =
|
|
|
641
708
|
case "Broadcast":
|
|
642
709
|
case "NoResponse":
|
|
643
710
|
break;
|
|
644
|
-
|
|
645
|
-
default:
|
|
646
|
-
exhaustiveCheck(response.message.result.value);
|
|
647
711
|
}
|
|
648
712
|
}
|
|
649
713
|
break;
|
|
650
|
-
|
|
651
|
-
default:
|
|
652
|
-
exhaustiveCheck(response.message);
|
|
653
714
|
}
|
|
654
715
|
};
|
|
655
716
|
|
|
@@ -657,8 +718,10 @@ const createSharedEvolu =
|
|
|
657
718
|
protocolMessagesByOwnerId: ReadonlyMap<OwnerId, ProtocolMessage>,
|
|
658
719
|
): void => {
|
|
659
720
|
for (const [ownerId, protocolMessage] of protocolMessagesByOwnerId) {
|
|
660
|
-
for (const transport of transports.getResourceKeysForClaim(
|
|
661
|
-
|
|
721
|
+
for (const transport of deps.transports.getResourceKeysForClaim(
|
|
722
|
+
ownerId,
|
|
723
|
+
)) {
|
|
724
|
+
const webSocket = deps.transports.getResource(transport);
|
|
662
725
|
if (!webSocket?.isOpen()) continue;
|
|
663
726
|
|
|
664
727
|
console.debug("sendProtocolMessage", {
|
|
@@ -672,192 +735,131 @@ const createSharedEvolu =
|
|
|
672
735
|
}
|
|
673
736
|
};
|
|
674
737
|
|
|
675
|
-
const refreshQueries = (): void => {
|
|
676
|
-
for (const evoluInstance of evoluInstancesByPortId.values()) {
|
|
677
|
-
evoluInstance.evoluPort.postMessage({ type: "RefreshQueries" });
|
|
678
|
-
}
|
|
679
|
-
};
|
|
680
|
-
|
|
681
738
|
const getUsedOwnersById = (
|
|
682
739
|
ownerIds: ReadonlySet<OwnerId>,
|
|
683
740
|
): ReadonlyMap<OwnerId, Owner> => {
|
|
684
741
|
const ownersById = new Map<OwnerId, Owner>();
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
for (const { owner } of evoluInstance.usedSyncOwners.keys()) {
|
|
742
|
+
for (const instance of instancesById.values()) {
|
|
743
|
+
for (const { owner } of instance.usedSyncOwners.keys()) {
|
|
688
744
|
if (!ownerIds.has(owner.id) || !("writeKey" in owner)) continue;
|
|
689
|
-
|
|
690
745
|
ownersById.set(owner.id, owner);
|
|
691
746
|
}
|
|
692
747
|
}
|
|
693
|
-
|
|
694
748
|
return ownersById;
|
|
695
749
|
};
|
|
696
750
|
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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) => {
|
|
751
|
+
const toggleSyncOwner =
|
|
752
|
+
(
|
|
753
|
+
instance: EvoluInstance,
|
|
754
|
+
syncOwner: SyncOwner,
|
|
755
|
+
action: "add" | "remove",
|
|
756
|
+
): Task<void, never, EvoluTenantDeps> =>
|
|
757
|
+
async (run) => {
|
|
715
758
|
if (action === "add") {
|
|
716
|
-
|
|
717
|
-
await run(
|
|
718
|
-
transports.addClaim(syncOwner.owner.id, syncOwner.transports),
|
|
759
|
+
instance.usedSyncOwners.increment(syncOwner);
|
|
760
|
+
return await run(
|
|
761
|
+
deps.transports.addClaim(syncOwner.owner.id, syncOwner.transports),
|
|
719
762
|
);
|
|
720
763
|
} else {
|
|
721
|
-
|
|
722
|
-
await run(
|
|
723
|
-
transports.removeClaim(
|
|
764
|
+
instance.usedSyncOwners.decrement(syncOwner);
|
|
765
|
+
return await run(
|
|
766
|
+
deps.transports.removeClaim(
|
|
767
|
+
syncOwner.owner.id,
|
|
768
|
+
syncOwner.transports,
|
|
769
|
+
),
|
|
724
770
|
);
|
|
725
771
|
}
|
|
726
|
-
|
|
727
|
-
});
|
|
772
|
+
};
|
|
728
773
|
|
|
729
774
|
return ok({
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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(),
|
|
775
|
+
addInstance: (message, onDisposed) => {
|
|
776
|
+
assertNotDisposed(disposables);
|
|
777
|
+
|
|
778
|
+
const instance: EvoluInstance = {
|
|
779
|
+
id: message.id,
|
|
780
|
+
port: deps.createMessagePort<EvoluOutput, EvoluInput>(
|
|
781
|
+
message.evoluPort,
|
|
782
|
+
),
|
|
783
|
+
onDisposed,
|
|
784
|
+
rowsByQuery: new Map<Query, ReadonlyArray<Row>>(),
|
|
785
|
+
useOwnerMutex: createMutex(),
|
|
751
786
|
usedSyncOwners: createRefCountByKey<SyncOwner, OwnerId>({
|
|
752
|
-
lookup: (
|
|
787
|
+
lookup: (syncOwner) => syncOwner.owner.id,
|
|
753
788
|
}),
|
|
789
|
+
[Symbol.asyncDispose]: () => disposer.disposeAsync(),
|
|
754
790
|
};
|
|
755
791
|
|
|
756
|
-
|
|
792
|
+
instancesById.set(instance.id, instance);
|
|
757
793
|
|
|
758
|
-
|
|
759
|
-
switch (message.type) {
|
|
760
|
-
case "LeaderAcquired": {
|
|
761
|
-
leaderDbWorkerPort = evoluInstance.dbWorkerPort;
|
|
762
|
-
console.info("leaderAcquired");
|
|
763
|
-
ensureQueueProcessing();
|
|
764
|
-
break;
|
|
765
|
-
}
|
|
794
|
+
const disposer = new AsyncDisposableStack();
|
|
766
795
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
}
|
|
796
|
+
disposer.defer(instance.onDisposed);
|
|
797
|
+
disposer.use(instance.usedSyncOwners);
|
|
798
|
+
disposer.use(instance.useOwnerMutex);
|
|
771
799
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
800
|
+
disposer.defer(async () => {
|
|
801
|
+
await tenantRun(
|
|
802
|
+
instance.useOwnerMutex.withLock(async (run) => {
|
|
803
|
+
for (const syncOwner of instance.usedSyncOwners.keys()) {
|
|
804
|
+
while (instance.usedSyncOwners.has(syncOwner)) {
|
|
805
|
+
await run(toggleSyncOwner(instance, syncOwner, "remove"));
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return ok();
|
|
809
|
+
}),
|
|
810
|
+
);
|
|
811
|
+
});
|
|
777
812
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
}
|
|
781
|
-
};
|
|
813
|
+
disposer.defer(() => {
|
|
814
|
+
instancesById.delete(instance.id);
|
|
815
|
+
console.info("evoluDispose", { name, id: instance.id });
|
|
816
|
+
});
|
|
817
|
+
disposer.use(instance.port);
|
|
818
|
+
|
|
819
|
+
// The main-thread Evolu instance holds this per-instance leader lock
|
|
820
|
+
// while it is alive. Acquiring the same lock here means the main
|
|
821
|
+
// thread instance was disposed or its tab closed, so the tenant-side
|
|
822
|
+
// instance must dispose itself.
|
|
823
|
+
void tenantRun(acquireLeaderLock(message.id)).then((lock) => {
|
|
824
|
+
if (!lock.ok) return;
|
|
825
|
+
|
|
826
|
+
disposer.use(lock.value);
|
|
827
|
+
return disposer.disposeAsync();
|
|
828
|
+
});
|
|
782
829
|
|
|
783
|
-
|
|
784
|
-
switch (
|
|
830
|
+
instance.port.onMessage = (message) => {
|
|
831
|
+
switch (message.type) {
|
|
785
832
|
case "Query":
|
|
786
833
|
case "Export": {
|
|
787
|
-
queue.push({
|
|
788
|
-
|
|
789
|
-
evoluPortId,
|
|
790
|
-
message: evoluMessage,
|
|
791
|
-
});
|
|
792
|
-
ensureQueueProcessing();
|
|
834
|
+
queue.push({ type: "ForEvolu", id: instance.id, message });
|
|
835
|
+
runQueue();
|
|
793
836
|
break;
|
|
794
837
|
}
|
|
795
|
-
|
|
796
838
|
case "Mutate": {
|
|
797
839
|
// TODO: Delegate do vsech evolu instances, co to pouzivaji
|
|
798
|
-
queue.push({
|
|
799
|
-
|
|
800
|
-
evoluPortId,
|
|
801
|
-
message: evoluMessage,
|
|
802
|
-
});
|
|
803
|
-
ensureQueueProcessing();
|
|
840
|
+
queue.push({ type: "ForEvolu", id: instance.id, message });
|
|
841
|
+
runQueue();
|
|
804
842
|
break;
|
|
805
843
|
}
|
|
806
|
-
|
|
807
844
|
case "UseOwner": {
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
845
|
+
void tenantRun(
|
|
846
|
+
instance.useOwnerMutex.withLock(async (run) => {
|
|
847
|
+
for (const { owner, action } of message.actions) {
|
|
848
|
+
console.debug("useOwner", {
|
|
849
|
+
id: instance.id,
|
|
850
|
+
action,
|
|
851
|
+
ownerId: owner.owner.id,
|
|
852
|
+
transportUrls: owner.transports.map(({ url }) => url),
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
await run(toggleSyncOwner(instance, owner, action));
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
return ok();
|
|
859
|
+
}),
|
|
860
|
+
);
|
|
823
861
|
break;
|
|
824
862
|
}
|
|
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
863
|
}
|
|
862
864
|
};
|
|
863
865
|
},
|
|
@@ -879,12 +881,11 @@ const createSharedEvolu =
|
|
|
879
881
|
},
|
|
880
882
|
});
|
|
881
883
|
|
|
882
|
-
|
|
884
|
+
runQueue();
|
|
883
885
|
},
|
|
884
886
|
|
|
885
887
|
requestApplySyncMessage: (ownerId, inputMessage): void => {
|
|
886
888
|
const owner = getUsedOwnersById(new Set([ownerId])).get(ownerId);
|
|
887
|
-
|
|
888
889
|
if (!owner) return;
|
|
889
890
|
|
|
890
891
|
console.debug("requestApplySyncMessage", {
|
|
@@ -901,10 +902,10 @@ const createSharedEvolu =
|
|
|
901
902
|
},
|
|
902
903
|
});
|
|
903
904
|
|
|
904
|
-
|
|
905
|
+
runQueue();
|
|
905
906
|
},
|
|
906
907
|
|
|
907
|
-
[Symbol.asyncDispose]: () =>
|
|
908
|
+
[Symbol.asyncDispose]: () => disposables.disposeAsync(),
|
|
908
909
|
});
|
|
909
910
|
};
|
|
910
911
|
|
|
@@ -930,3 +931,11 @@ const createSharedEvolu =
|
|
|
930
931
|
// readonly onCompleteId: CallbackId;
|
|
931
932
|
// readonly reload: boolean;
|
|
932
933
|
// })
|
|
934
|
+
|
|
935
|
+
// TODO: Remaining SharedWorker lifetime issues.
|
|
936
|
+
// - Investigate heartbeat for ports and where liveness tracking is needed.
|
|
937
|
+
// - Sync state for monitoring.
|
|
938
|
+
// - Propagate invalid protocol messages to sync state.
|
|
939
|
+
// - Make DbWorker logically stateless by making all requests idempotent.
|
|
940
|
+
// - Investigate WebKit bug 301520 relevance to DbWorker disposal. (again)
|
|
941
|
+
// https://bugs.webkit.org/show_bug.cgi?id=301520
|