@agentshouse/mdruntime 0.1.0-alpha.3 → 0.1.0-alpha.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/adapter.d.ts +21 -4
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/connection.d.ts +22 -4
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +657 -78
- package/dist/connection.js.map +1 -1
- package/dist/durability.d.ts +4 -0
- package/dist/durability.d.ts.map +1 -0
- package/dist/durability.js +65 -0
- package/dist/durability.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/log.d.ts +5 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +15 -0
- package/dist/log.js.map +1 -0
- package/dist/memory.js +2 -1
- package/dist/memory.js.map +1 -1
- package/dist/outcome.d.ts +30 -0
- package/dist/outcome.d.ts.map +1 -0
- package/dist/outcome.js +13 -0
- package/dist/outcome.js.map +1 -0
- package/dist/replica.d.ts +1 -0
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +25 -0
- package/dist/replica.js.map +1 -1
- package/dist/status.d.ts +2 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +6 -4
- package/dist/status.js.map +1 -1
- package/dist/store.d.ts +5 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +19 -1
- package/dist/store.js.map +1 -1
- package/package.json +1 -1
package/dist/connection.js
CHANGED
|
@@ -2,10 +2,13 @@ import { query as evaluate } from '@agentshouse/query';
|
|
|
2
2
|
import { ROOM_CONTRACT_VERSION, VERSION_1_CAPABILITIES } from './adapter.js';
|
|
3
3
|
import { bootstrapRoom } from './bootstrap.js';
|
|
4
4
|
import { MD_MODEL, PRESENTED } from './collection.js';
|
|
5
|
+
import { createLog } from './log.js';
|
|
6
|
+
import { queuedOutcome, refused } from './outcome.js';
|
|
5
7
|
import { refreshRoom } from './refresh.js';
|
|
6
8
|
import { buildBatch } from './save.js';
|
|
7
|
-
import { readConfirmed, replicaOf } from './replica.js';
|
|
9
|
+
import { applyReturned, readConfirmed, replicaOf } from './replica.js';
|
|
8
10
|
import { settled, same, saving, STARTING, transferring } from './status.js';
|
|
11
|
+
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
9
12
|
import { openReplicaStore } from './store.js';
|
|
10
13
|
import { unconfirmed } from './transfer.js';
|
|
11
14
|
const DEFAULT_DISCOVERY_INTERVAL_MS = 60_000;
|
|
@@ -17,10 +20,13 @@ export function createMdRuntime(options) {
|
|
|
17
20
|
let store;
|
|
18
21
|
const replicas = new Map();
|
|
19
22
|
const drafts = new Map();
|
|
23
|
+
const outcomes = new Map();
|
|
20
24
|
let limits = [];
|
|
25
|
+
let capabilities = [];
|
|
21
26
|
const rooms = new Map();
|
|
22
27
|
const unreadable = new Set();
|
|
23
28
|
let refusal = null;
|
|
29
|
+
let storageLoss = null;
|
|
24
30
|
let status = STARTING;
|
|
25
31
|
let discoveryPaused = false;
|
|
26
32
|
let timer;
|
|
@@ -31,9 +37,19 @@ export function createMdRuntime(options) {
|
|
|
31
37
|
let queued = false;
|
|
32
38
|
const roomListeners = new Set();
|
|
33
39
|
const refusalListeners = new Set();
|
|
40
|
+
const storageLossListeners = new Set();
|
|
34
41
|
const statusListeners = new Set();
|
|
35
42
|
const querySubscriptions = new Set();
|
|
36
43
|
const queueSubscriptions = new Set();
|
|
44
|
+
const journal = createLog();
|
|
45
|
+
let notifyReady = ()=>{};
|
|
46
|
+
const ready = new Promise((resolve)=>{
|
|
47
|
+
notifyReady = resolve;
|
|
48
|
+
});
|
|
49
|
+
let leader = typeof window === 'undefined';
|
|
50
|
+
let applyingRemote = false;
|
|
51
|
+
let releaseLeadership = ()=>{};
|
|
52
|
+
const channel = typeof window !== 'undefined' ? new BroadcastChannel('mdruntime') : undefined;
|
|
37
53
|
function at(locator, path) {
|
|
38
54
|
return `${locator} ${path}`;
|
|
39
55
|
}
|
|
@@ -42,6 +58,30 @@ export function createMdRuntime(options) {
|
|
|
42
58
|
...replicas.values()
|
|
43
59
|
].some((replica)=>replica.queue.length > 0);
|
|
44
60
|
}
|
|
61
|
+
function attention() {
|
|
62
|
+
return [
|
|
63
|
+
...replicas.values()
|
|
64
|
+
].some((replica)=>{
|
|
65
|
+
const head = replica.queue[0];
|
|
66
|
+
return head !== undefined && refused(outcomes.get(head.id));
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function storedOutcomes(batches) {
|
|
70
|
+
const stored = {};
|
|
71
|
+
for (const batch of batches){
|
|
72
|
+
const outcome = queuedOutcome(outcomes.get(batch.id));
|
|
73
|
+
if (outcome.kind !== 'pending') stored[batch.id] = outcome;
|
|
74
|
+
}
|
|
75
|
+
return stored;
|
|
76
|
+
}
|
|
77
|
+
async function persistQueue(local, locator, batches) {
|
|
78
|
+
await local.writeQueue({
|
|
79
|
+
locator,
|
|
80
|
+
batches,
|
|
81
|
+
outcomes: storedOutcomes(batches)
|
|
82
|
+
});
|
|
83
|
+
announce();
|
|
84
|
+
}
|
|
45
85
|
function listed() {
|
|
46
86
|
return [
|
|
47
87
|
...rooms.values()
|
|
@@ -63,11 +103,32 @@ export function createMdRuntime(options) {
|
|
|
63
103
|
function emitRefusal() {
|
|
64
104
|
for (const listener of refusalListeners)listener(refusal);
|
|
65
105
|
}
|
|
66
|
-
function
|
|
106
|
+
function emitStorageLoss() {
|
|
107
|
+
for (const listener of storageLossListeners)listener(storageLoss);
|
|
108
|
+
}
|
|
109
|
+
function announce(type = 'changed') {
|
|
110
|
+
if (applyingRemote || channel === undefined) return;
|
|
111
|
+
if (type === 'hello') {
|
|
112
|
+
channel.postMessage({
|
|
113
|
+
type: 'hello'
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
channel.postMessage({
|
|
118
|
+
type,
|
|
119
|
+
status
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function adopt(next) {
|
|
67
123
|
if (closed || same(status, next)) return;
|
|
68
124
|
status = next;
|
|
69
125
|
for (const listener of statusListeners)listener(status);
|
|
70
126
|
}
|
|
127
|
+
function moveTo(next) {
|
|
128
|
+
if (closed || same(status, next)) return;
|
|
129
|
+
adopt(next);
|
|
130
|
+
announce();
|
|
131
|
+
}
|
|
71
132
|
function visible(request) {
|
|
72
133
|
return evaluate(queryRooms(), {
|
|
73
134
|
...request,
|
|
@@ -84,13 +145,18 @@ export function createMdRuntime(options) {
|
|
|
84
145
|
subscription.notify(visible(subscription.query));
|
|
85
146
|
}
|
|
86
147
|
}
|
|
87
|
-
function
|
|
88
|
-
|
|
148
|
+
function entriesOf(locator) {
|
|
149
|
+
const replica = replicas.get(locator);
|
|
150
|
+
if (replica === undefined) return [];
|
|
151
|
+
return replica.queue.map((batch)=>Object.freeze({
|
|
152
|
+
batch,
|
|
153
|
+
outcome: queuedOutcome(outcomes.get(batch.id))
|
|
154
|
+
}));
|
|
89
155
|
}
|
|
90
156
|
function emitQueue(locator) {
|
|
91
|
-
const
|
|
157
|
+
const entries = entriesOf(locator);
|
|
92
158
|
for (const subscription of queueSubscriptions){
|
|
93
|
-
if (subscription.locator === locator) subscription.notify(
|
|
159
|
+
if (subscription.locator === locator) subscription.notify(entries);
|
|
94
160
|
}
|
|
95
161
|
}
|
|
96
162
|
function stopTimer() {
|
|
@@ -111,6 +177,7 @@ export function createMdRuntime(options) {
|
|
|
111
177
|
reason,
|
|
112
178
|
reauthorizationOffered: reason === 'revoked-authorization'
|
|
113
179
|
};
|
|
180
|
+
journal.record(`connection refused ${reason}`);
|
|
114
181
|
emitRefusal();
|
|
115
182
|
}
|
|
116
183
|
function list(room, synchronizationAvailable) {
|
|
@@ -138,8 +205,7 @@ export function createMdRuntime(options) {
|
|
|
138
205
|
emitRooms();
|
|
139
206
|
return listedLocators;
|
|
140
207
|
}
|
|
141
|
-
async function
|
|
142
|
-
const opened = await openReplicaStore();
|
|
208
|
+
async function hydrate(opened) {
|
|
143
209
|
const [records, queues, kept, lastCompleted, declared] = await Promise.all([
|
|
144
210
|
opened.all(),
|
|
145
211
|
opened.queues(),
|
|
@@ -147,27 +213,94 @@ export function createMdRuntime(options) {
|
|
|
147
213
|
opened.lastCompleted(),
|
|
148
214
|
opened.providerLimits()
|
|
149
215
|
]);
|
|
150
|
-
store = opened;
|
|
151
216
|
limits = declared;
|
|
217
|
+
drafts.clear();
|
|
152
218
|
for (const draft of kept)drafts.set(at(draft.locator, draft.path), draft.content);
|
|
153
|
-
const
|
|
219
|
+
const queuedBatches = new Map(queues.map((queue)=>[
|
|
154
220
|
queue.locator,
|
|
155
221
|
queue.batches
|
|
156
222
|
]));
|
|
223
|
+
outcomes.clear();
|
|
224
|
+
for (const queue of queues){
|
|
225
|
+
for (const [id, outcome] of Object.entries(queue.outcomes)){
|
|
226
|
+
outcomes.set(id, outcome);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
replicas.clear();
|
|
157
230
|
for (const record of records){
|
|
158
231
|
const confirmed = readConfirmed(record);
|
|
159
232
|
if (confirmed === null) continue;
|
|
160
|
-
replicas.set(record.locator, replicaOf(confirmed,
|
|
161
|
-
|
|
233
|
+
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? []));
|
|
234
|
+
const existing = rooms.get(record.locator);
|
|
235
|
+
if (existing === undefined) list(record, false);
|
|
236
|
+
else {
|
|
237
|
+
rooms.set(record.locator, {
|
|
238
|
+
...existing,
|
|
239
|
+
roomVersion: record.roomVersion
|
|
240
|
+
});
|
|
241
|
+
}
|
|
162
242
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
243
|
+
return {
|
|
244
|
+
lastCompleted,
|
|
245
|
+
locators: new Set([
|
|
246
|
+
...queuedBatches.keys(),
|
|
247
|
+
...replicas.keys()
|
|
248
|
+
])
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
async function readState(opened) {
|
|
252
|
+
const { lastCompleted, locators } = await hydrate(opened);
|
|
167
253
|
if (replicas.size > 0) {
|
|
168
254
|
emitRooms();
|
|
169
255
|
emitVisible();
|
|
170
256
|
}
|
|
257
|
+
for (const locator of locators)emitQueue(locator);
|
|
258
|
+
return lastCompleted;
|
|
259
|
+
}
|
|
260
|
+
async function applyRemote(message) {
|
|
261
|
+
if (closed) return;
|
|
262
|
+
if (message.type === 'hello') {
|
|
263
|
+
if (leader) announce();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
applyingRemote = true;
|
|
267
|
+
try {
|
|
268
|
+
const local = await openLocal();
|
|
269
|
+
const lastCompleted = await readState(local);
|
|
270
|
+
if (message.status !== undefined) adopt(message.status);
|
|
271
|
+
else {
|
|
272
|
+
adopt({
|
|
273
|
+
...status,
|
|
274
|
+
lastCompleted,
|
|
275
|
+
attentionRequired: attention(),
|
|
276
|
+
state: pending() ? 'pending' : status.state
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
} finally{
|
|
280
|
+
applyingRemote = false;
|
|
281
|
+
}
|
|
282
|
+
if (message.type === 'enqueue' && leader) void enqueue();
|
|
283
|
+
}
|
|
284
|
+
async function open() {
|
|
285
|
+
const opened = await openReplicaStore();
|
|
286
|
+
store = opened;
|
|
287
|
+
await acrossTabs('mdruntime-generation', async ()=>{
|
|
288
|
+
const storedGeneration = await opened.generation();
|
|
289
|
+
const loss = detectStorageLoss(storedGeneration);
|
|
290
|
+
const generation = nextGeneration(storedGeneration);
|
|
291
|
+
if (generation !== storedGeneration) await opened.rememberGeneration(generation);
|
|
292
|
+
if (loss !== null) {
|
|
293
|
+
storageLoss = loss;
|
|
294
|
+
emitStorageLoss();
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
const lastCompleted = await readState(opened);
|
|
298
|
+
adopt({
|
|
299
|
+
...status,
|
|
300
|
+
lastCompleted,
|
|
301
|
+
attentionRequired: attention()
|
|
302
|
+
});
|
|
303
|
+
if (!leader) announce('hello');
|
|
171
304
|
return opened;
|
|
172
305
|
}
|
|
173
306
|
function openLocal() {
|
|
@@ -182,60 +315,234 @@ export function createMdRuntime(options) {
|
|
|
182
315
|
mutations = next.then(()=>undefined, ()=>undefined);
|
|
183
316
|
return next;
|
|
184
317
|
}
|
|
318
|
+
function acrossTabs(name, work) {
|
|
319
|
+
if (typeof window === 'undefined') return work();
|
|
320
|
+
return window.navigator.locks.request(name, work);
|
|
321
|
+
}
|
|
185
322
|
function publish(local, room, confirmed) {
|
|
186
323
|
return exclusive(async ()=>{
|
|
187
|
-
await
|
|
188
|
-
|
|
189
|
-
|
|
324
|
+
await acrossTabs('mdruntime-replica', async ()=>{
|
|
325
|
+
await hydrate(local);
|
|
326
|
+
await local.publish(confirmed.record);
|
|
327
|
+
unreadable.delete(room.locator);
|
|
328
|
+
replicas.set(room.locator, replicaOf(confirmed, replicas.get(room.locator)?.queue ?? []));
|
|
329
|
+
});
|
|
190
330
|
list(room, true);
|
|
191
331
|
emitRooms();
|
|
192
332
|
emitVisible();
|
|
333
|
+
announce();
|
|
193
334
|
});
|
|
194
335
|
}
|
|
195
|
-
async function write(locator, changes) {
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
336
|
+
async function write(locator, changes, replacing) {
|
|
337
|
+
const local = await openLocal();
|
|
338
|
+
const result = await acrossTabs('mdruntime-replica', async ()=>{
|
|
339
|
+
await hydrate(local);
|
|
340
|
+
const replica = replicas.get(locator);
|
|
341
|
+
const room = rooms.get(locator);
|
|
342
|
+
if (replica === undefined || room === undefined) {
|
|
343
|
+
throw new Error(`No Local Replica for ${locator}`);
|
|
344
|
+
}
|
|
345
|
+
const built = buildBatch({
|
|
346
|
+
baseRoomVersion: replica.confirmed.record.roomVersion,
|
|
347
|
+
writeGrants: room.writeGrants,
|
|
348
|
+
providerOwnedPaths: room.providerOwnedPaths,
|
|
349
|
+
providerLimits: limits,
|
|
350
|
+
visible: replica.visible,
|
|
351
|
+
changes
|
|
352
|
+
});
|
|
353
|
+
if (built.kind === 'refused') return {
|
|
354
|
+
outcome: 'refused',
|
|
355
|
+
...built.refusal
|
|
356
|
+
};
|
|
357
|
+
const head = replica.queue[0];
|
|
358
|
+
if (replacing !== undefined) {
|
|
359
|
+
if (head === undefined || head.id !== replacing || !refused(outcomes.get(head.id))) {
|
|
360
|
+
return {
|
|
361
|
+
outcome: 'refused',
|
|
362
|
+
rule: 'no-refused-head'
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const batches = replacing === undefined ? [
|
|
367
|
+
...replica.queue,
|
|
368
|
+
built.batch
|
|
369
|
+
] : [
|
|
370
|
+
built.batch,
|
|
371
|
+
...replica.queue.slice(1)
|
|
372
|
+
];
|
|
373
|
+
const consumed = [
|
|
374
|
+
...new Set(changes.map((change)=>change.path))
|
|
375
|
+
];
|
|
376
|
+
try {
|
|
377
|
+
await local.append({
|
|
378
|
+
locator,
|
|
379
|
+
batches,
|
|
380
|
+
outcomes: storedOutcomes(batches)
|
|
381
|
+
}, consumed);
|
|
382
|
+
} catch {
|
|
383
|
+
return {
|
|
384
|
+
outcome: 'refused',
|
|
385
|
+
rule: 'durable-write'
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
if (replacing !== undefined) outcomes.delete(replacing);
|
|
389
|
+
for (const path of consumed)drafts.delete(at(locator, path));
|
|
390
|
+
replicas.set(locator, replicaOf(replica.confirmed, batches));
|
|
391
|
+
emitQueue(locator);
|
|
392
|
+
emitVisible();
|
|
393
|
+
moveTo({
|
|
394
|
+
...saving(status),
|
|
395
|
+
attentionRequired: attention()
|
|
396
|
+
});
|
|
397
|
+
return {
|
|
398
|
+
outcome: 'saved',
|
|
399
|
+
id: built.batch.id
|
|
400
|
+
};
|
|
208
401
|
});
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
402
|
+
if (result.outcome === 'saved') void enqueue();
|
|
403
|
+
return result;
|
|
404
|
+
}
|
|
405
|
+
async function discardHead(locator, identifier) {
|
|
213
406
|
const local = await openLocal();
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
407
|
+
const changed = await acrossTabs('mdruntime-replica', async ()=>{
|
|
408
|
+
await hydrate(local);
|
|
409
|
+
const replica = replicas.get(locator);
|
|
410
|
+
const head = replica?.queue[0];
|
|
411
|
+
if (replica === undefined || head === undefined || head.id !== identifier || !refused(outcomes.get(head.id))) {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
const batches = replica.queue.slice(1);
|
|
415
|
+
outcomes.delete(identifier);
|
|
416
|
+
await persistQueue(local, locator, batches);
|
|
417
|
+
replicas.set(locator, replicaOf(replica.confirmed, batches));
|
|
418
|
+
emitQueue(locator);
|
|
419
|
+
emitVisible();
|
|
420
|
+
moveTo({
|
|
421
|
+
...status,
|
|
422
|
+
state: pending() ? 'pending' : status.state,
|
|
423
|
+
attentionRequired: attention()
|
|
424
|
+
});
|
|
425
|
+
return true;
|
|
426
|
+
});
|
|
427
|
+
if (changed) void enqueue();
|
|
428
|
+
}
|
|
429
|
+
async function readHistory(locator, path, query = {}) {
|
|
430
|
+
if (closed || refusal !== null) return {
|
|
431
|
+
kind: 'transport-uncertainty'
|
|
432
|
+
};
|
|
433
|
+
try {
|
|
434
|
+
await ensureAuthorization();
|
|
435
|
+
} catch {
|
|
436
|
+
return {
|
|
437
|
+
kind: 'transport-uncertainty'
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
if (closed || authorization === undefined || refusal !== null) {
|
|
441
|
+
return {
|
|
442
|
+
kind: 'transport-uncertainty'
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
const transfer = [
|
|
446
|
+
'transfer',
|
|
447
|
+
'history',
|
|
222
448
|
locator,
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
449
|
+
path
|
|
450
|
+
];
|
|
451
|
+
if (query.revision !== undefined) transfer.push(query.revision);
|
|
452
|
+
if (query.pageToken !== undefined) transfer.push(query.pageToken);
|
|
453
|
+
journal.record(transfer.join(' '));
|
|
454
|
+
try {
|
|
455
|
+
const result = await options.adapter.history(authorization, {
|
|
456
|
+
locator,
|
|
457
|
+
path,
|
|
458
|
+
...query.revision === undefined ? {} : {
|
|
459
|
+
revision: query.revision
|
|
460
|
+
},
|
|
461
|
+
...query.pageToken === undefined ? {} : {
|
|
462
|
+
pageToken: query.pageToken
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
journal.record(`outcome history ${result.kind}`);
|
|
466
|
+
if (result.kind === 'revoked-authorization') refuse('revoked-authorization');
|
|
467
|
+
return result;
|
|
468
|
+
} catch {
|
|
469
|
+
journal.record('outcome history transport-uncertainty');
|
|
470
|
+
return {
|
|
471
|
+
kind: 'transport-uncertainty'
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function declaredAction(name) {
|
|
476
|
+
for (const capability of capabilities){
|
|
477
|
+
for (const action of capability.actions ?? []){
|
|
478
|
+
if (action.name === name) return action;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return undefined;
|
|
482
|
+
}
|
|
483
|
+
async function invokeAction(name, input) {
|
|
484
|
+
await ready;
|
|
485
|
+
if (closed || refusal !== null) return {
|
|
486
|
+
kind: 'transport-uncertainty'
|
|
487
|
+
};
|
|
488
|
+
if (declaredAction(name) === undefined) return {
|
|
489
|
+
kind: 'undeclared'
|
|
233
490
|
};
|
|
491
|
+
try {
|
|
492
|
+
await ensureAuthorization();
|
|
493
|
+
} catch {
|
|
494
|
+
return {
|
|
495
|
+
kind: 'transport-uncertainty'
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
if (closed || authorization === undefined || refusal !== null) {
|
|
499
|
+
return {
|
|
500
|
+
kind: 'transport-uncertainty'
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
journal.record(`transfer action ${name}`);
|
|
504
|
+
try {
|
|
505
|
+
const result = await options.adapter.action(authorization, {
|
|
506
|
+
name,
|
|
507
|
+
input
|
|
508
|
+
});
|
|
509
|
+
if (result.kind === 'revoked-authorization') refuse('revoked-authorization');
|
|
510
|
+
journal.record(`outcome action ${result.kind}`);
|
|
511
|
+
if (result.kind === 'rejected') journal.record(`refusal rejected ${result.text}`);
|
|
512
|
+
return result;
|
|
513
|
+
} catch {
|
|
514
|
+
journal.record('outcome action transport-uncertainty');
|
|
515
|
+
return {
|
|
516
|
+
kind: 'transport-uncertainty'
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
async function pullRefresh(authorized, confirmed) {
|
|
521
|
+
try {
|
|
522
|
+
const result = await refreshRoom(options.adapter, authorized, confirmed);
|
|
523
|
+
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
524
|
+
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
525
|
+
return result;
|
|
526
|
+
} catch (error) {
|
|
527
|
+
journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
|
|
528
|
+
throw error;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
async function pullBootstrap(authorized, room) {
|
|
532
|
+
try {
|
|
533
|
+
const result = await bootstrapRoom(options.adapter, authorized, room);
|
|
534
|
+
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
535
|
+
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
536
|
+
return result;
|
|
537
|
+
} catch (error) {
|
|
538
|
+
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
539
|
+
throw error;
|
|
540
|
+
}
|
|
234
541
|
}
|
|
235
542
|
async function advance(room, local, authorized) {
|
|
236
543
|
const held = replicas.get(room.locator);
|
|
237
544
|
if (held !== undefined) {
|
|
238
|
-
const refreshed = await
|
|
545
|
+
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
239
546
|
if (refreshed.kind === 'still-current') {
|
|
240
547
|
if (unreadable.delete(room.locator)) {
|
|
241
548
|
list(room, true);
|
|
@@ -250,62 +557,248 @@ export function createMdRuntime(options) {
|
|
|
250
557
|
}
|
|
251
558
|
if (refreshed.kind !== 'beyond-horizon') return refreshed;
|
|
252
559
|
}
|
|
253
|
-
const bootstrapped = await
|
|
560
|
+
const bootstrapped = await pullBootstrap(authorized, room);
|
|
254
561
|
if (bootstrapped.kind !== 'staged') return bootstrapped;
|
|
255
562
|
if (closed) return unconfirmed;
|
|
256
563
|
await publish(local, room, bootstrapped.confirmed);
|
|
257
564
|
return null;
|
|
258
565
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
566
|
+
function recordRefusal(result) {
|
|
567
|
+
if (result.kind === 'expired') return {
|
|
568
|
+
kind: 'expired'
|
|
569
|
+
};
|
|
570
|
+
if (result.kind === 'conflicted') {
|
|
571
|
+
return {
|
|
572
|
+
kind: 'conflicted',
|
|
573
|
+
roomVersion: result.roomVersion,
|
|
574
|
+
affected: result.affected,
|
|
575
|
+
text: result.text
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
kind: 'rejected',
|
|
580
|
+
affected: result.affected,
|
|
581
|
+
text: result.text
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
async function commitAccept(room, local, submitted, accepted, base, overlay) {
|
|
585
|
+
return acrossTabs('mdruntime-replica', async ()=>{
|
|
586
|
+
await hydrate(local);
|
|
587
|
+
const current = replicas.get(room.locator);
|
|
588
|
+
if (current === undefined || !current.queue.some((batch)=>batch.id === submitted.id)) {
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
const remaining = current.queue.filter((batch)=>batch.id !== submitted.id);
|
|
592
|
+
const applied = overlay ? applyReturned(base, accepted.documents, accepted.revisions, accepted.removals, accepted.roomVersion) : base;
|
|
593
|
+
if (applied === null) return false;
|
|
594
|
+
await local.publish(applied.record);
|
|
595
|
+
await persistQueue(local, room.locator, remaining);
|
|
596
|
+
outcomes.set(submitted.id, {
|
|
597
|
+
kind: 'accepted',
|
|
598
|
+
roomVersion: accepted.roomVersion,
|
|
599
|
+
author: accepted.author,
|
|
600
|
+
time: accepted.time
|
|
601
|
+
});
|
|
602
|
+
unreadable.delete(room.locator);
|
|
603
|
+
replicas.set(room.locator, replicaOf(applied, remaining));
|
|
604
|
+
list(room, true);
|
|
605
|
+
emitRooms();
|
|
606
|
+
emitQueue(room.locator);
|
|
607
|
+
emitVisible();
|
|
608
|
+
journal.record(`outcome ${room.locator} ${submitted.id} accepted`);
|
|
609
|
+
return true;
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
async function submitRoom(room, local, authorized, progress) {
|
|
613
|
+
while(!closed && authorization !== undefined){
|
|
614
|
+
const replica = replicas.get(room.locator);
|
|
615
|
+
const head = replica?.queue[0];
|
|
616
|
+
if (replica === undefined || head === undefined || refused(outcomes.get(head.id))) {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
let result;
|
|
620
|
+
journal.record(`submission ${room.locator} ${head.id}`);
|
|
621
|
+
try {
|
|
622
|
+
result = await options.adapter.mutate(authorized, {
|
|
623
|
+
locator: room.locator,
|
|
624
|
+
changeBatch: head
|
|
625
|
+
});
|
|
626
|
+
} catch {
|
|
262
627
|
progress.complete = false;
|
|
263
|
-
|
|
628
|
+
progress.reachable = false;
|
|
629
|
+
return null;
|
|
264
630
|
}
|
|
265
|
-
|
|
266
|
-
if (
|
|
267
|
-
|
|
268
|
-
if (withheld.kind === 'transport-uncertainty') {
|
|
631
|
+
if (closed) return null;
|
|
632
|
+
if (result.kind === 'transport-uncertainty') {
|
|
633
|
+
progress.complete = false;
|
|
269
634
|
progress.reachable = false;
|
|
270
|
-
|
|
635
|
+
return null;
|
|
271
636
|
}
|
|
272
|
-
if (
|
|
637
|
+
if (result.kind === 'room-not-readable') {
|
|
638
|
+
progress.complete = false;
|
|
273
639
|
unreadable.add(room.locator);
|
|
274
640
|
list(room, false);
|
|
275
641
|
emitRooms();
|
|
276
|
-
|
|
642
|
+
return null;
|
|
277
643
|
}
|
|
278
|
-
if (
|
|
644
|
+
if (result.kind === 'revoked-authorization') {
|
|
279
645
|
refuse('revoked-authorization');
|
|
646
|
+
return 'stop';
|
|
647
|
+
}
|
|
648
|
+
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
649
|
+
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}`);
|
|
650
|
+
if (result.kind !== 'expired') journal.record(`refusal ${result.kind} ${result.text}`);
|
|
651
|
+
await exclusive(async ()=>{
|
|
652
|
+
await acrossTabs('mdruntime-replica', async ()=>{
|
|
653
|
+
await hydrate(local);
|
|
654
|
+
const current = replicas.get(room.locator);
|
|
655
|
+
if (current === undefined || !current.queue.some((batch)=>batch.id === head.id)) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
outcomes.set(head.id, recordRefusal(result));
|
|
659
|
+
await persistQueue(local, room.locator, current.queue);
|
|
660
|
+
emitQueue(room.locator);
|
|
661
|
+
moveTo({
|
|
662
|
+
...status,
|
|
663
|
+
attentionRequired: true
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
const from = replicas.get(room.locator)?.confirmed;
|
|
670
|
+
if (from === undefined) return null;
|
|
671
|
+
const pulled = await pullRefresh(authorized, from);
|
|
672
|
+
if (pulled.kind === 'revoked-authorization') {
|
|
673
|
+
refuse('revoked-authorization');
|
|
674
|
+
return 'stop';
|
|
675
|
+
}
|
|
676
|
+
if (pulled.kind === 'still-current') {
|
|
677
|
+
const committed = await exclusive(()=>commitAccept(room, local, head, result, from, true));
|
|
678
|
+
if (!committed) {
|
|
679
|
+
progress.complete = false;
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
if (pulled.kind === 'advanced') {
|
|
685
|
+
const overlay = pulled.confirmed.record.roomVersion === result.roomVersion;
|
|
686
|
+
const committed = await exclusive(()=>commitAccept(room, local, head, result, pulled.confirmed, overlay));
|
|
687
|
+
if (!committed) {
|
|
688
|
+
progress.complete = false;
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
if (pulled.kind === 'beyond-horizon') {
|
|
694
|
+
const bootstrapped = await pullBootstrap(authorized, room);
|
|
695
|
+
if (bootstrapped.kind === 'revoked-authorization') {
|
|
696
|
+
refuse('revoked-authorization');
|
|
697
|
+
return 'stop';
|
|
698
|
+
}
|
|
699
|
+
if (bootstrapped.kind === 'staged') {
|
|
700
|
+
const overlay = bootstrapped.confirmed.record.roomVersion === result.roomVersion;
|
|
701
|
+
const committed = await exclusive(()=>commitAccept(room, local, head, result, bootstrapped.confirmed, overlay));
|
|
702
|
+
if (!committed) {
|
|
703
|
+
progress.complete = false;
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
progress.complete = false;
|
|
709
|
+
if (bootstrapped.kind === 'transport-uncertainty') {
|
|
710
|
+
progress.reachable = false;
|
|
711
|
+
} else if (bootstrapped.kind === 'room-not-readable') {
|
|
712
|
+
unreadable.add(room.locator);
|
|
713
|
+
list(room, false);
|
|
714
|
+
emitRooms();
|
|
715
|
+
}
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
progress.complete = false;
|
|
719
|
+
if (pulled.kind === 'transport-uncertainty') {
|
|
720
|
+
progress.reachable = false;
|
|
721
|
+
} else if (pulled.kind === 'room-not-readable') {
|
|
722
|
+
unreadable.add(room.locator);
|
|
723
|
+
list(room, false);
|
|
724
|
+
emitRooms();
|
|
725
|
+
}
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
async function synchronize(discovered, local, progress) {
|
|
731
|
+
for (const room of discovered){
|
|
732
|
+
if (closed || authorization === undefined) {
|
|
733
|
+
progress.complete = false;
|
|
280
734
|
return;
|
|
281
735
|
}
|
|
736
|
+
const withheld = await advance(room, local, authorization);
|
|
737
|
+
if (withheld !== null) {
|
|
738
|
+
progress.complete = false;
|
|
739
|
+
if (withheld.kind === 'transport-uncertainty') {
|
|
740
|
+
progress.reachable = false;
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
if (withheld.kind === 'room-not-readable') {
|
|
744
|
+
unreadable.add(room.locator);
|
|
745
|
+
list(room, false);
|
|
746
|
+
emitRooms();
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
if (withheld.kind === 'revoked-authorization') {
|
|
750
|
+
refuse('revoked-authorization');
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
if (await submitRoom(room, local, authorization, progress) === 'stop') return;
|
|
282
756
|
}
|
|
283
757
|
}
|
|
284
758
|
async function cycle(local, progress) {
|
|
285
|
-
if (closed || authorization === undefined)
|
|
759
|
+
if (closed || authorization === undefined) {
|
|
760
|
+
notifyReady();
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
286
763
|
moveTo(transferring(status));
|
|
287
|
-
|
|
288
|
-
|
|
764
|
+
let result;
|
|
765
|
+
try {
|
|
766
|
+
result = await options.adapter.discover(authorization);
|
|
767
|
+
} catch (error) {
|
|
768
|
+
journal.record('connection discover transport-uncertainty');
|
|
769
|
+
notifyReady();
|
|
770
|
+
throw error;
|
|
771
|
+
}
|
|
772
|
+
if (closed) {
|
|
773
|
+
notifyReady();
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
journal.record(`connection discover ${result.kind}`);
|
|
289
777
|
if (result.kind === 'transport-uncertainty') {
|
|
290
778
|
progress.reachable = false;
|
|
779
|
+
notifyReady();
|
|
291
780
|
return;
|
|
292
781
|
}
|
|
293
782
|
if (result.kind === 'revoked-authorization') {
|
|
294
783
|
refuse('revoked-authorization');
|
|
784
|
+
notifyReady();
|
|
295
785
|
return;
|
|
296
786
|
}
|
|
297
787
|
if (!result.supportedVersions.includes(ROOM_CONTRACT_VERSION)) {
|
|
298
788
|
refuse('no-matching-version');
|
|
789
|
+
notifyReady();
|
|
299
790
|
return;
|
|
300
791
|
}
|
|
301
792
|
if (result.mdModel.identity !== MD_MODEL.identity || result.mdModel.version !== MD_MODEL.version) {
|
|
302
793
|
refuse('md-model-version-mismatch');
|
|
794
|
+
notifyReady();
|
|
303
795
|
return;
|
|
304
796
|
}
|
|
305
797
|
const declared = new Set(result.capabilities.map((capability)=>capability.name));
|
|
306
798
|
for (const required of options.requiredGuarantees){
|
|
307
799
|
if (!knownCapabilities.has(required) || !declared.has(required)) {
|
|
308
800
|
refuse('missing-required-guarantee');
|
|
801
|
+
notifyReady();
|
|
309
802
|
return;
|
|
310
803
|
}
|
|
311
804
|
}
|
|
@@ -313,6 +806,8 @@ export function createMdRuntime(options) {
|
|
|
313
806
|
refusal = null;
|
|
314
807
|
emitRefusal();
|
|
315
808
|
}
|
|
809
|
+
capabilities = result.capabilities;
|
|
810
|
+
notifyReady();
|
|
316
811
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits)) {
|
|
317
812
|
limits = result.providerLimits;
|
|
318
813
|
await local.declare(limits);
|
|
@@ -325,10 +820,19 @@ export function createMdRuntime(options) {
|
|
|
325
820
|
}
|
|
326
821
|
async function ensureAuthorization() {
|
|
327
822
|
if (authorization !== undefined) return;
|
|
328
|
-
|
|
823
|
+
try {
|
|
824
|
+
authorization = await options.adapter.authorize();
|
|
825
|
+
journal.record('connection authorize');
|
|
826
|
+
} catch (error) {
|
|
827
|
+
journal.record('connection authorize transport-uncertainty');
|
|
828
|
+
throw error;
|
|
829
|
+
}
|
|
329
830
|
}
|
|
330
831
|
async function connect() {
|
|
331
|
-
if (closed || discoveryPaused)
|
|
832
|
+
if (closed || discoveryPaused) {
|
|
833
|
+
notifyReady();
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
332
836
|
startTimer();
|
|
333
837
|
const progress = {
|
|
334
838
|
complete: false,
|
|
@@ -348,9 +852,11 @@ export function createMdRuntime(options) {
|
|
|
348
852
|
}
|
|
349
853
|
} catch {
|
|
350
854
|
progress.complete = false;
|
|
855
|
+
notifyReady();
|
|
351
856
|
return;
|
|
352
857
|
} finally{
|
|
353
|
-
|
|
858
|
+
notifyReady();
|
|
859
|
+
moveTo(settled(status, progress, completedAt, pending(), attention()));
|
|
354
860
|
}
|
|
355
861
|
}
|
|
356
862
|
async function run() {
|
|
@@ -365,6 +871,10 @@ export function createMdRuntime(options) {
|
|
|
365
871
|
}
|
|
366
872
|
}
|
|
367
873
|
function enqueue() {
|
|
874
|
+
if (!leader) {
|
|
875
|
+
announce('enqueue');
|
|
876
|
+
return chain;
|
|
877
|
+
}
|
|
368
878
|
if (inflight) {
|
|
369
879
|
queued = true;
|
|
370
880
|
return chain;
|
|
@@ -373,21 +883,54 @@ export function createMdRuntime(options) {
|
|
|
373
883
|
chain = chain.then(run, run);
|
|
374
884
|
return chain;
|
|
375
885
|
}
|
|
886
|
+
function startProcessing() {
|
|
887
|
+
if (leader) {
|
|
888
|
+
void enqueue();
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
void window.navigator.locks.request('mdruntime-sync', {
|
|
892
|
+
signal: options.signal
|
|
893
|
+
}, async ()=>{
|
|
894
|
+
if (closed) return;
|
|
895
|
+
leader = true;
|
|
896
|
+
announce();
|
|
897
|
+
void enqueue();
|
|
898
|
+
await new Promise((resolve)=>{
|
|
899
|
+
releaseLeadership = ()=>{
|
|
900
|
+
leader = false;
|
|
901
|
+
resolve();
|
|
902
|
+
};
|
|
903
|
+
});
|
|
904
|
+
}).catch(()=>{});
|
|
905
|
+
}
|
|
376
906
|
function close() {
|
|
377
907
|
closed = true;
|
|
378
908
|
stopTimer();
|
|
909
|
+
releaseLeadership();
|
|
910
|
+
channel?.close();
|
|
379
911
|
store?.close();
|
|
380
912
|
roomListeners.clear();
|
|
381
913
|
refusalListeners.clear();
|
|
914
|
+
storageLossListeners.clear();
|
|
382
915
|
statusListeners.clear();
|
|
383
916
|
querySubscriptions.clear();
|
|
384
917
|
queueSubscriptions.clear();
|
|
385
918
|
}
|
|
919
|
+
channel?.addEventListener('message', (event)=>{
|
|
920
|
+
const data = event.data;
|
|
921
|
+
if (data === null || typeof data !== 'object') return;
|
|
922
|
+
void exclusive(()=>applyRemote(data));
|
|
923
|
+
});
|
|
386
924
|
options.signal?.addEventListener('abort', close, {
|
|
387
925
|
once: true
|
|
388
926
|
});
|
|
389
927
|
if (options.signal?.aborted) close();
|
|
390
|
-
void
|
|
928
|
+
void openLocal().then(()=>{
|
|
929
|
+
if (!leader) notifyReady();
|
|
930
|
+
}, ()=>{
|
|
931
|
+
notifyReady();
|
|
932
|
+
});
|
|
933
|
+
startProcessing();
|
|
391
934
|
return {
|
|
392
935
|
rooms () {
|
|
393
936
|
return listed();
|
|
@@ -420,8 +963,35 @@ export function createMdRuntime(options) {
|
|
|
420
963
|
drafts.delete(at(locator, path));
|
|
421
964
|
}
|
|
422
965
|
},
|
|
423
|
-
save (locator, changes) {
|
|
424
|
-
return exclusive(()=>write(locator, changes));
|
|
966
|
+
save (locator, changes, replacing) {
|
|
967
|
+
return exclusive(()=>write(locator, changes, replacing));
|
|
968
|
+
},
|
|
969
|
+
outcome (identifier) {
|
|
970
|
+
const recorded = outcomes.get(identifier);
|
|
971
|
+
if (recorded !== undefined) return recorded;
|
|
972
|
+
for (const replica of replicas.values()){
|
|
973
|
+
if (replica.queue.some((batch)=>batch.id === identifier)) return {
|
|
974
|
+
kind: 'pending'
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
return null;
|
|
978
|
+
},
|
|
979
|
+
queue: {
|
|
980
|
+
discard (locator, identifier) {
|
|
981
|
+
return exclusive(()=>discardHead(locator, identifier));
|
|
982
|
+
}
|
|
983
|
+
},
|
|
984
|
+
history (locator, path, query) {
|
|
985
|
+
return readHistory(locator, path, query);
|
|
986
|
+
},
|
|
987
|
+
call (name, input) {
|
|
988
|
+
return invokeAction(name, input);
|
|
989
|
+
},
|
|
990
|
+
log () {
|
|
991
|
+
return journal.export();
|
|
992
|
+
},
|
|
993
|
+
storageLoss () {
|
|
994
|
+
return storageLoss;
|
|
425
995
|
},
|
|
426
996
|
observe (topic, second, third) {
|
|
427
997
|
if (topic === 'rooms') {
|
|
@@ -440,6 +1010,14 @@ export function createMdRuntime(options) {
|
|
|
440
1010
|
refusalListeners.delete(notify);
|
|
441
1011
|
};
|
|
442
1012
|
}
|
|
1013
|
+
if (topic === 'storage-loss') {
|
|
1014
|
+
const notify = second;
|
|
1015
|
+
storageLossListeners.add(notify);
|
|
1016
|
+
notify(storageLoss);
|
|
1017
|
+
return ()=>{
|
|
1018
|
+
storageLossListeners.delete(notify);
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
443
1021
|
if (topic === 'sync-status') {
|
|
444
1022
|
const notify = second;
|
|
445
1023
|
statusListeners.add(notify);
|
|
@@ -454,7 +1032,7 @@ export function createMdRuntime(options) {
|
|
|
454
1032
|
notify: third
|
|
455
1033
|
};
|
|
456
1034
|
queueSubscriptions.add(queue);
|
|
457
|
-
queue.notify(
|
|
1035
|
+
queue.notify(entriesOf(queue.locator));
|
|
458
1036
|
return ()=>{
|
|
459
1037
|
queueSubscriptions.delete(queue);
|
|
460
1038
|
};
|
|
@@ -472,6 +1050,7 @@ export function createMdRuntime(options) {
|
|
|
472
1050
|
},
|
|
473
1051
|
async reauthorize () {
|
|
474
1052
|
if (refusal === null || !refusal.reauthorizationOffered) return;
|
|
1053
|
+
journal.record('connection reauthorize');
|
|
475
1054
|
authorization = undefined;
|
|
476
1055
|
discoveryPaused = false;
|
|
477
1056
|
await enqueue();
|