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