@agentshouse/mdruntime 0.1.0-c009335e08279 → 0.1.0-c30ed2b8e04d5
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/AGENTS.md +21 -12
- package/README.md +4 -5
- package/dist/adapter.d.ts +56 -41
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +1 -9
- package/dist/attached-model.d.ts +11 -0
- package/dist/attached-model.d.ts.map +1 -0
- package/dist/attached-model.js +34 -0
- package/dist/bootstrap.d.ts +5 -3
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +2 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +3 -10
- package/dist/connection.d.ts +4 -8
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +485 -502
- package/dist/conversation.d.ts +6 -6
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +93 -37
- package/dist/host.d.ts +7 -1
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +34 -31
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/memory.d.ts +10 -5
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +66 -26
- package/dist/model.d.ts +1 -5
- package/dist/model.d.ts.map +1 -1
- package/dist/observation.d.ts +2 -0
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +24 -14
- package/dist/query.d.ts +7 -2
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +4 -3
- package/dist/reach.d.ts +40 -0
- package/dist/reach.d.ts.map +1 -0
- package/dist/reach.js +277 -0
- package/dist/read-side.d.ts +4 -0
- package/dist/read-side.d.ts.map +1 -1
- package/dist/read-side.js +16 -10
- package/dist/refresh.d.ts +2 -1
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +7 -3
- package/dist/replica.d.ts +4 -4
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +12 -13
- package/dist/room-draft.d.ts +3 -0
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +6 -4
- package/dist/save.d.ts +0 -2
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +2 -4
- package/dist/store.d.ts +0 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/wire.d.ts +50 -16
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +49 -59
- package/package.json +2 -2
package/dist/connection.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { attachedModel } from './attached-model.js';
|
|
2
2
|
import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
|
|
3
|
-
import {
|
|
3
|
+
import { observing } from './observation.js';
|
|
4
4
|
import { filterOf, identityOf, planOf, } from './query.js';
|
|
5
5
|
import { createReadSide } from './read-side.js';
|
|
6
6
|
import { createSearchIndex, searchPlanOf, } from './search.js';
|
|
7
|
-
import { ROOM_CONTRACT_VERSION, VERSION_1_CAPABILITIES, } from './adapter.js';
|
|
8
7
|
import { bootstrapRoom } from './bootstrap.js';
|
|
9
8
|
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
10
9
|
import { createConversationStore, } from './conversation.js';
|
|
11
|
-
import {
|
|
10
|
+
import { parseCollection, validRoot } from './collection.js';
|
|
12
11
|
import { createLog } from './log.js';
|
|
13
12
|
import { refreshRoom } from './refresh.js';
|
|
14
13
|
import { buildBatch } from './save.js';
|
|
@@ -20,37 +19,21 @@ import { roomDraftRevision } from './identity.js';
|
|
|
20
19
|
import { beneath, normalize } from './paths.js';
|
|
21
20
|
import { openRuntimeStore, } from './store.js';
|
|
22
21
|
import { unconfirmed } from './transfer.js';
|
|
23
|
-
|
|
24
|
-
const DEFAULT_DISCOVERY_INTERVAL_MS = 60_000;
|
|
25
|
-
const EMPTY_SCHEMA = { verdict: 'accept', model: { types: {} } };
|
|
26
|
-
const knownCapabilities = new Set(VERSION_1_CAPABILITIES);
|
|
22
|
+
import { governs, holds, narrowed, planReach, } from './reach.js';
|
|
27
23
|
function within(root, path) {
|
|
28
24
|
return root === '' || beneath(root, path);
|
|
29
25
|
}
|
|
30
|
-
function declaredIdentity(model) {
|
|
31
|
-
if (model === undefined)
|
|
32
|
-
return false;
|
|
33
|
-
if (model === null)
|
|
34
|
-
return true;
|
|
35
|
-
return (typeof model.identity === 'string' &&
|
|
36
|
-
model.identity.length > 0 &&
|
|
37
|
-
typeof model.version === 'string' &&
|
|
38
|
-
model.version.length > 0);
|
|
39
|
-
}
|
|
40
26
|
function declaredSelection(room) {
|
|
41
27
|
const selection = room.typedSelection;
|
|
42
28
|
if (selection === undefined)
|
|
43
29
|
return true;
|
|
44
30
|
if (!Array.isArray(selection.roots) || selection.roots.length === 0)
|
|
45
31
|
return false;
|
|
46
|
-
if (!declaredIdentity(selection.attachedModel))
|
|
47
|
-
return false;
|
|
48
32
|
return selection.roots.every((root, index) => validRoot(root) &&
|
|
49
33
|
root !== '' &&
|
|
50
34
|
selection.roots.every((other, at) => at === index || !within(other, root)));
|
|
51
35
|
}
|
|
52
36
|
export function composeRuntime(options) {
|
|
53
|
-
const period = options.discoveryIntervalMs ?? DEFAULT_DISCOVERY_INTERVAL_MS;
|
|
54
37
|
let authorization = options.authorization;
|
|
55
38
|
let opening;
|
|
56
39
|
let store;
|
|
@@ -58,20 +41,13 @@ export function composeRuntime(options) {
|
|
|
58
41
|
const pendings = new Map();
|
|
59
42
|
const roomDrafts = new Map();
|
|
60
43
|
const presentations = new Map();
|
|
61
|
-
const wanted = new Map();
|
|
62
44
|
let limits = [];
|
|
63
45
|
let capabilities = [];
|
|
64
|
-
function rewriting() {
|
|
65
|
-
return capabilities.some((capability) => capability.name === 'move-reference-rewrites')
|
|
66
|
-
? 'provider'
|
|
67
|
-
: 'runtime';
|
|
68
|
-
}
|
|
69
46
|
const rooms = new Map();
|
|
70
47
|
const unreadable = new Set();
|
|
71
48
|
const models = new Map();
|
|
72
49
|
const liveModels = new Map();
|
|
73
50
|
const invalidModels = new Set();
|
|
74
|
-
const modelEpoch = new Map();
|
|
75
51
|
const discovered = new Map();
|
|
76
52
|
let roomsListed = false;
|
|
77
53
|
const scopedRefusals = new Map();
|
|
@@ -100,16 +76,19 @@ export function composeRuntime(options) {
|
|
|
100
76
|
let status = STARTING;
|
|
101
77
|
let shown = STARTING;
|
|
102
78
|
let discoveryPaused = false;
|
|
103
|
-
let
|
|
79
|
+
let resetTimer;
|
|
104
80
|
let closed = false;
|
|
105
81
|
let stopping = false;
|
|
106
|
-
let
|
|
107
|
-
|
|
108
|
-
|
|
82
|
+
let withdrawStream;
|
|
83
|
+
const announced = new Map();
|
|
84
|
+
let underway = null;
|
|
85
|
+
const submitting = new Map();
|
|
86
|
+
const answering = new Map();
|
|
87
|
+
let complete = false;
|
|
88
|
+
let recorded = false;
|
|
89
|
+
const stale = new Set();
|
|
109
90
|
let chain = Promise.resolve();
|
|
110
91
|
let mutations = Promise.resolve();
|
|
111
|
-
let inflight = false;
|
|
112
|
-
let queued = false;
|
|
113
92
|
const roomListeners = new Set();
|
|
114
93
|
const refusalListeners = new Set();
|
|
115
94
|
const storageLossListeners = new Set();
|
|
@@ -120,6 +99,7 @@ export function composeRuntime(options) {
|
|
|
120
99
|
const readSide = createReadSide();
|
|
121
100
|
let withdrawConversations;
|
|
122
101
|
let currencyQueued = false;
|
|
102
|
+
let socketOpen = false;
|
|
123
103
|
let serviceMode = 'open';
|
|
124
104
|
let haltedRefusal = false;
|
|
125
105
|
let openSignals = 0;
|
|
@@ -234,11 +214,14 @@ export function composeRuntime(options) {
|
|
|
234
214
|
function listed() {
|
|
235
215
|
return structuredClone([...rooms.values()]);
|
|
236
216
|
}
|
|
237
|
-
function roomFacts() {
|
|
238
|
-
const held = [...rooms.values()]
|
|
217
|
+
function roomFacts(reach = null) {
|
|
218
|
+
const held = [...rooms.values()]
|
|
219
|
+
.filter((room) => holds(reach, room.locator))
|
|
220
|
+
.map((room) => {
|
|
221
|
+
const governed = governs(reach, room);
|
|
239
222
|
const attached = [
|
|
240
|
-
modelOf(room.locator),
|
|
241
|
-
...(
|
|
223
|
+
...(governed.collection ? [modelOf(room.locator)] : []),
|
|
224
|
+
...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
|
|
242
225
|
];
|
|
243
226
|
return {
|
|
244
227
|
locator: room.locator,
|
|
@@ -322,17 +305,30 @@ export function composeRuntime(options) {
|
|
|
322
305
|
}
|
|
323
306
|
}
|
|
324
307
|
function stopTimer() {
|
|
325
|
-
if (
|
|
308
|
+
if (resetTimer === undefined)
|
|
326
309
|
return;
|
|
327
|
-
|
|
328
|
-
|
|
310
|
+
clearTimeout(resetTimer);
|
|
311
|
+
resetTimer = undefined;
|
|
329
312
|
}
|
|
330
|
-
function
|
|
331
|
-
|
|
313
|
+
function armTimer() {
|
|
314
|
+
stopTimer();
|
|
315
|
+
if (closed || stopping)
|
|
316
|
+
return;
|
|
317
|
+
let earliest = null;
|
|
318
|
+
for (const { submission } of pendings.values()) {
|
|
319
|
+
if (submission.state !== 'reset')
|
|
320
|
+
continue;
|
|
321
|
+
const at = Date.parse(submission.reset);
|
|
322
|
+
if (earliest === null || at < earliest)
|
|
323
|
+
earliest = at;
|
|
324
|
+
}
|
|
325
|
+
if (earliest === null)
|
|
332
326
|
return;
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
327
|
+
// A Save House held back goes again only once the reset its answer named has passed.
|
|
328
|
+
resetTimer = setTimeout(() => {
|
|
329
|
+
resetTimer = undefined;
|
|
330
|
+
submitAll();
|
|
331
|
+
}, Math.max(0, earliest - Date.now()));
|
|
336
332
|
}
|
|
337
333
|
async function refuse(reason, provider = null) {
|
|
338
334
|
stopTimer();
|
|
@@ -358,7 +354,6 @@ export function composeRuntime(options) {
|
|
|
358
354
|
}
|
|
359
355
|
for (const key of keys) {
|
|
360
356
|
invalidModels.add(key);
|
|
361
|
-
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
362
357
|
models.set(key, revoked);
|
|
363
358
|
liveModels.delete(key);
|
|
364
359
|
}
|
|
@@ -373,7 +368,7 @@ export function composeRuntime(options) {
|
|
|
373
368
|
};
|
|
374
369
|
const read = readConfirmed(record, before.confirmed);
|
|
375
370
|
replicas.set(locator, read.kind === 'staged'
|
|
376
|
-
? replicaOf(read.confirmed, before.queue,
|
|
371
|
+
? replicaOf(read.confirmed, before.queue, before)
|
|
377
372
|
: { ...before, confirmed: { ...before.confirmed, record } });
|
|
378
373
|
}
|
|
379
374
|
rooms.set(locator, {
|
|
@@ -406,13 +401,11 @@ export function composeRuntime(options) {
|
|
|
406
401
|
locator: room.locator,
|
|
407
402
|
operations: room.operations,
|
|
408
403
|
replica: room.replica,
|
|
409
|
-
attachedModel: room.attachedModel,
|
|
410
404
|
model: modelTypes(modelOf(room.locator)),
|
|
411
405
|
typedSelection: selection === null
|
|
412
406
|
? null
|
|
413
407
|
: {
|
|
414
408
|
roots: selection.roots,
|
|
415
|
-
attachedModel: selection.attachedModel,
|
|
416
409
|
model: modelTypes(modelOf(room.locator, 'typed-selection')),
|
|
417
410
|
},
|
|
418
411
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
@@ -429,9 +422,6 @@ export function composeRuntime(options) {
|
|
|
429
422
|
discovered.clear();
|
|
430
423
|
for (const room of listed)
|
|
431
424
|
discovered.set(room.locator, room);
|
|
432
|
-
for (const locator of wanted.keys())
|
|
433
|
-
if (!listedLocators.has(locator))
|
|
434
|
-
wanted.delete(locator);
|
|
435
425
|
await dropUnlisted(listedLocators, local);
|
|
436
426
|
for (const room of listed) {
|
|
437
427
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
@@ -465,6 +455,7 @@ export function composeRuntime(options) {
|
|
|
465
455
|
rooms.delete(locator);
|
|
466
456
|
roomDrafts.delete(locator);
|
|
467
457
|
unreadable.delete(locator);
|
|
458
|
+
stale.delete(locator);
|
|
468
459
|
scopedRefusals.delete(locator);
|
|
469
460
|
sourceRefusals.delete(locator);
|
|
470
461
|
declarationRefusals.delete(locator);
|
|
@@ -473,7 +464,6 @@ export function composeRuntime(options) {
|
|
|
473
464
|
models.delete(key);
|
|
474
465
|
liveModels.delete(key);
|
|
475
466
|
invalidModels.delete(key);
|
|
476
|
-
modelEpoch.delete(key);
|
|
477
467
|
}
|
|
478
468
|
journal.record(`room unlisted ${locator}`);
|
|
479
469
|
}
|
|
@@ -486,7 +476,6 @@ export function composeRuntime(options) {
|
|
|
486
476
|
if (prior !== undefined &&
|
|
487
477
|
prior.status !== 'unavailable' &&
|
|
488
478
|
(persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
|
|
489
|
-
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
490
479
|
invalidModels.add(key);
|
|
491
480
|
}
|
|
492
481
|
const model = persisted.status === 'unavailable' ||
|
|
@@ -541,7 +530,7 @@ export function composeRuntime(options) {
|
|
|
541
530
|
record: retyped,
|
|
542
531
|
collection: parseCollection(retyped.documents, compositionOf(retyped)),
|
|
543
532
|
};
|
|
544
|
-
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [],
|
|
533
|
+
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
|
|
545
534
|
const existing = rooms.get(record.locator);
|
|
546
535
|
if (existing === undefined)
|
|
547
536
|
list(record, false);
|
|
@@ -552,13 +541,11 @@ export function composeRuntime(options) {
|
|
|
552
541
|
attachments: record.attachments,
|
|
553
542
|
operations: record.operations,
|
|
554
543
|
replica: record.replica,
|
|
555
|
-
attachedModel: record.attachedModel,
|
|
556
544
|
model: modelTypes(model),
|
|
557
545
|
typedSelection: record.typedSelection === null
|
|
558
546
|
? null
|
|
559
547
|
: {
|
|
560
548
|
roots: record.typedSelection.roots,
|
|
561
|
-
attachedModel: record.typedSelection.attachedModel,
|
|
562
549
|
model: modelTypes(selectionModel),
|
|
563
550
|
},
|
|
564
551
|
});
|
|
@@ -626,7 +613,7 @@ export function composeRuntime(options) {
|
|
|
626
613
|
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
627
614
|
}
|
|
628
615
|
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
629
|
-
const withdrawn = withdrawals(confirmed, queue, withdrawable
|
|
616
|
+
const withdrawn = withdrawals(confirmed, queue, withdrawable);
|
|
630
617
|
const kept = queue.filter((batch) => !withdrawn.includes(batch));
|
|
631
618
|
const returning = [
|
|
632
619
|
...withdrawn.map((batch) => ({ batch, returned: { kind: 'withdrawn' } })).reverse(),
|
|
@@ -662,42 +649,73 @@ export function composeRuntime(options) {
|
|
|
662
649
|
rememberRoomDraft(record);
|
|
663
650
|
for (const batch of withdrawn)
|
|
664
651
|
journal.record(`outcome ${locator} ${batch.id} withdrawn`);
|
|
665
|
-
replicas.set(locator, replicaOf(confirmed, kept,
|
|
652
|
+
replicas.set(locator, replicaOf(confirmed, kept, before));
|
|
666
653
|
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
667
654
|
room: settling.room,
|
|
668
655
|
queue: kept.length !== (before?.queue.length ?? 0),
|
|
669
656
|
drafts: [...written.keys()],
|
|
670
657
|
});
|
|
671
658
|
}
|
|
672
|
-
function
|
|
659
|
+
function adoptModels(locator, record) {
|
|
660
|
+
for (const [attached, model] of [
|
|
661
|
+
['collection', record.model],
|
|
662
|
+
['typed-selection', record.selectionModel],
|
|
663
|
+
]) {
|
|
664
|
+
const key = modelKey(locator, attached);
|
|
665
|
+
models.set(key, model);
|
|
666
|
+
invalidModels.delete(key);
|
|
667
|
+
if (model.status === 'current')
|
|
668
|
+
liveModels.set(key, JSON.stringify(model.snapshot));
|
|
669
|
+
else if (model.status === 'unavailable')
|
|
670
|
+
liveModels.delete(key);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
function remodel(locator, answers, held) {
|
|
674
|
+
const taken = (answer, previous, attached) => {
|
|
675
|
+
if (answer === undefined)
|
|
676
|
+
return previous;
|
|
677
|
+
const { state, refused } = attachedModel(answer, previous);
|
|
678
|
+
if (refused !== null) {
|
|
679
|
+
const where = refused.location === undefined ? '' : ` ${refused.location}`;
|
|
680
|
+
journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
|
|
681
|
+
}
|
|
682
|
+
models.set(modelKey(locator, attached), state);
|
|
683
|
+
return state;
|
|
684
|
+
};
|
|
685
|
+
return {
|
|
686
|
+
collection: taken(answers?.collection, held.collection, 'collection'),
|
|
687
|
+
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
function publish(local, room, confirmed, base) {
|
|
673
691
|
return exclusive(async () => {
|
|
674
692
|
if (!continuing())
|
|
675
|
-
return;
|
|
693
|
+
return true;
|
|
676
694
|
const prior = replicas.get(room.locator);
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
throw new Error(`Cannot retype staged Room ${room.locator}`);
|
|
695
|
+
if (base !== undefined &&
|
|
696
|
+
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
697
|
+
prior.confirmed.record.revisions !== base.record.revisions)) {
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
700
|
+
adoptModels(room.locator, confirmed.record);
|
|
684
701
|
const queue = prior?.queue ?? [];
|
|
685
|
-
const roomVersion =
|
|
702
|
+
const roomVersion = confirmed.record.roomVersion;
|
|
686
703
|
const accepted = queue.filter((batch) => {
|
|
687
704
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
688
705
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
689
706
|
});
|
|
690
|
-
const set = await settle(local, room.locator, prior,
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
707
|
+
const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
|
|
708
|
+
for (const batch of accepted)
|
|
709
|
+
pendings.delete(batch.id);
|
|
710
|
+
stale.delete(room.locator);
|
|
711
|
+
if (unreadable.delete(room.locator))
|
|
712
|
+
scopedRefusals.delete(room.locator);
|
|
696
713
|
sourceRefusals.delete(room.locator);
|
|
697
714
|
declarationRefusals.delete(room.locator);
|
|
698
715
|
list(room, true);
|
|
699
716
|
publishChangeSet(set);
|
|
700
717
|
moveTo({ ...status, ...attention() });
|
|
718
|
+
return true;
|
|
701
719
|
});
|
|
702
720
|
}
|
|
703
721
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
@@ -726,7 +744,6 @@ export function composeRuntime(options) {
|
|
|
726
744
|
operations: room.operations,
|
|
727
745
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
728
746
|
providerLimits: limits,
|
|
729
|
-
rewriting: rewriting(),
|
|
730
747
|
visible: replica.visible,
|
|
731
748
|
changes: parts.flatMap(({ changes }) => changes),
|
|
732
749
|
bases: parts.flatMap(({ bases }) => bases),
|
|
@@ -750,15 +767,16 @@ export function composeRuntime(options) {
|
|
|
750
767
|
return { outcome: 'refused', rule: 'durable-write' };
|
|
751
768
|
}
|
|
752
769
|
pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
|
|
770
|
+
recorded = false;
|
|
753
771
|
for (const path of consumed)
|
|
754
772
|
forgetRoomDraft(locator, path);
|
|
755
|
-
replicas.set(locator, replicaOf(replica.confirmed, batches,
|
|
773
|
+
replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
|
|
756
774
|
publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
|
|
757
775
|
queue: true,
|
|
758
776
|
drafts: consumed,
|
|
759
777
|
}));
|
|
760
778
|
moveTo({ ...saving(status), ...attention() });
|
|
761
|
-
|
|
779
|
+
submitLater(locator);
|
|
762
780
|
return { outcome: 'saved', identifier: built.batch.id };
|
|
763
781
|
}
|
|
764
782
|
async function mark(local, locator, identifier, submission, lost) {
|
|
@@ -857,8 +875,6 @@ export function composeRuntime(options) {
|
|
|
857
875
|
journal.record(`transfer action ${name}`);
|
|
858
876
|
try {
|
|
859
877
|
const result = await options.adapter.action(authorization, { name, input });
|
|
860
|
-
if (result.kind === 'action' && result.at !== undefined)
|
|
861
|
-
want(result.at);
|
|
862
878
|
if (result.kind === 'scoped-refusal') {
|
|
863
879
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
864
880
|
if (room !== undefined)
|
|
@@ -890,32 +906,11 @@ export function composeRuntime(options) {
|
|
|
890
906
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
891
907
|
!options.adapter.reached(locator, roomVersion, written));
|
|
892
908
|
}
|
|
893
|
-
function
|
|
894
|
-
if (reachedBy(locator, roomVersion))
|
|
895
|
-
return;
|
|
896
|
-
const held = wanted.get(locator);
|
|
897
|
-
if (held === undefined || options.adapter.reached(locator, roomVersion, held)) {
|
|
898
|
-
wanted.set(locator, roomVersion);
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
function reachOf(locator) {
|
|
902
|
-
let target = wanted.get(locator);
|
|
903
|
-
const versions = (replicas.get(locator)?.queue ?? []).flatMap((batch) => {
|
|
904
|
-
const accepted = pendings.get(batch.id)?.accepted;
|
|
905
|
-
return accepted === undefined ? [] : [accepted];
|
|
906
|
-
});
|
|
907
|
-
for (const version of versions) {
|
|
908
|
-
if (target === undefined || options.adapter.reached(locator, version, target)) {
|
|
909
|
-
target = version;
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
return target === undefined || reachedBy(locator, target) ? undefined : target;
|
|
913
|
-
}
|
|
914
|
-
async function pullRefresh(authorized, confirmed, minPosition) {
|
|
909
|
+
async function pullRefresh(authorized, confirmed) {
|
|
915
910
|
if (!continuing())
|
|
916
911
|
return unconfirmed;
|
|
917
912
|
try {
|
|
918
|
-
const result = await refreshRoom(options.adapter, authorized, confirmed,
|
|
913
|
+
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
|
|
919
914
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
920
915
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
921
916
|
return result;
|
|
@@ -932,7 +927,7 @@ export function composeRuntime(options) {
|
|
|
932
927
|
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
933
928
|
collection: modelOf(room.locator),
|
|
934
929
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
935
|
-
}, continuing);
|
|
930
|
+
}, (models, held) => remodel(room.locator, models, held), continuing);
|
|
936
931
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
937
932
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
938
933
|
return result;
|
|
@@ -945,20 +940,28 @@ export function composeRuntime(options) {
|
|
|
945
940
|
async function advance(room, local, authorized) {
|
|
946
941
|
const held = replicas.get(room.locator);
|
|
947
942
|
if (held !== undefined) {
|
|
948
|
-
const refreshed = await pullRefresh(authorized, held.confirmed
|
|
943
|
+
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
949
944
|
if (!continuing())
|
|
950
945
|
return unconfirmed;
|
|
951
946
|
if (refreshed.kind === 'still-current') {
|
|
952
947
|
if (sourceRefusals.has(room.locator))
|
|
953
948
|
return unconfirmed;
|
|
949
|
+
stale.delete(room.locator);
|
|
954
950
|
if (unreadable.delete(room.locator)) {
|
|
951
|
+
scopedRefusals.delete(room.locator);
|
|
955
952
|
list(room, true);
|
|
956
953
|
emitRooms();
|
|
957
954
|
}
|
|
958
955
|
return null;
|
|
959
956
|
}
|
|
960
957
|
if (refreshed.kind === 'advanced') {
|
|
961
|
-
await publish(local, room, refreshed.confirmed)
|
|
958
|
+
if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
|
|
959
|
+
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
960
|
+
const reached = now !== undefined &&
|
|
961
|
+
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
962
|
+
return reached ? null : advance(room, local, authorized);
|
|
963
|
+
}
|
|
964
|
+
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
962
965
|
return null;
|
|
963
966
|
}
|
|
964
967
|
if (refreshed.kind !== 'beyond-horizon')
|
|
@@ -973,7 +976,7 @@ export function composeRuntime(options) {
|
|
|
973
976
|
return null;
|
|
974
977
|
}
|
|
975
978
|
async function withhold(withheld, room, local, progress) {
|
|
976
|
-
|
|
979
|
+
stale.add(room.locator);
|
|
977
980
|
if (withheld.kind === 'scoped-refusal') {
|
|
978
981
|
await scoped(withheld, room, local);
|
|
979
982
|
}
|
|
@@ -1020,44 +1023,30 @@ export function composeRuntime(options) {
|
|
|
1020
1023
|
(accepted.author === undefined ? '' : ` author ${accepted.author}`) +
|
|
1021
1024
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1022
1025
|
}
|
|
1023
|
-
async function
|
|
1024
|
-
await exclusive(async () => {
|
|
1025
|
-
if (!continuing())
|
|
1026
|
-
return;
|
|
1027
|
-
const current = replicas.get(room.locator);
|
|
1028
|
-
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
1029
|
-
return;
|
|
1030
|
-
}
|
|
1031
|
-
const state = pendings.get(submitted.id);
|
|
1032
|
-
if (state === undefined)
|
|
1033
|
-
return;
|
|
1034
|
-
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1035
|
-
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1036
|
-
logAccepted(room.locator, submitted, accepted);
|
|
1037
|
-
emitQueue(room.locator);
|
|
1038
|
-
});
|
|
1039
|
-
}
|
|
1040
|
-
async function commitAccept(room, local, submitted, accepted, base, overlay) {
|
|
1026
|
+
async function adoptAccepted(room, local, submitted, accepted) {
|
|
1041
1027
|
if (!continuing())
|
|
1042
1028
|
return unconfirmed;
|
|
1043
1029
|
const current = replicas.get(room.locator);
|
|
1044
1030
|
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
1045
1031
|
return null;
|
|
1046
1032
|
}
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1033
|
+
const held = current.confirmed.record.roomVersion;
|
|
1034
|
+
logAccepted(room.locator, submitted, accepted);
|
|
1035
|
+
const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
1036
|
+
? { kind: 'staged', confirmed: current.confirmed }
|
|
1037
|
+
: applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
1038
|
+
? accepted.roomVersion
|
|
1039
|
+
: held, accepted.roomVersion);
|
|
1040
|
+
if (applied.kind !== 'staged') {
|
|
1041
|
+
const state = pendings.get(submitted.id);
|
|
1042
|
+
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1043
|
+
if (state !== undefined)
|
|
1044
|
+
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1045
|
+
emitQueue(room.locator);
|
|
1057
1046
|
return applied;
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1047
|
+
}
|
|
1048
|
+
const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
1049
|
+
pendings.delete(submitted.id);
|
|
1061
1050
|
unreadable.delete(room.locator);
|
|
1062
1051
|
sourceRefusals.delete(room.locator);
|
|
1063
1052
|
declarationRefusals.delete(room.locator);
|
|
@@ -1066,17 +1055,8 @@ export function composeRuntime(options) {
|
|
|
1066
1055
|
moveTo({ ...status, ...attention() });
|
|
1067
1056
|
return null;
|
|
1068
1057
|
}
|
|
1069
|
-
async function
|
|
1070
|
-
const
|
|
1071
|
-
if (options.adapter.reached(room.locator, roomVersion, accepted.roomVersion)) {
|
|
1072
|
-
return committed(room, local, submitted, accepted, pulled, roomVersion === accepted.roomVersion);
|
|
1073
|
-
}
|
|
1074
|
-
if (fresh && continuing())
|
|
1075
|
-
await publish(local, room, pulled);
|
|
1076
|
-
return false;
|
|
1077
|
-
}
|
|
1078
|
-
async function committed(room, local, submitted, accepted, base, overlay) {
|
|
1079
|
-
const failed = await exclusive(() => commitAccept(room, local, submitted, accepted, base, overlay));
|
|
1058
|
+
async function adopted(room, local, submitted, accepted) {
|
|
1059
|
+
const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
|
|
1080
1060
|
if (failed === null)
|
|
1081
1061
|
return true;
|
|
1082
1062
|
if (failed.kind === 'refused' && continuing())
|
|
@@ -1089,10 +1069,8 @@ export function composeRuntime(options) {
|
|
|
1089
1069
|
const head = replica?.queue[0];
|
|
1090
1070
|
if (replica === undefined || head === undefined)
|
|
1091
1071
|
return;
|
|
1092
|
-
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1093
|
-
progress.complete = false;
|
|
1072
|
+
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1094
1073
|
return;
|
|
1095
|
-
}
|
|
1096
1074
|
const state = pendings.get(head.id);
|
|
1097
1075
|
const { submission, lost: earlier } = state ?? {
|
|
1098
1076
|
submission: { state: 'waiting' },
|
|
@@ -1120,6 +1098,7 @@ export function composeRuntime(options) {
|
|
|
1120
1098
|
return;
|
|
1121
1099
|
}
|
|
1122
1100
|
const opensBefore = openSignals;
|
|
1101
|
+
const answered = awaitingAnswer(room.locator);
|
|
1123
1102
|
try {
|
|
1124
1103
|
result = await options.adapter.mutate(authorized, {
|
|
1125
1104
|
locator: room.locator,
|
|
@@ -1128,11 +1107,14 @@ export function composeRuntime(options) {
|
|
|
1128
1107
|
}, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
|
|
1129
1108
|
}
|
|
1130
1109
|
catch {
|
|
1110
|
+
answered();
|
|
1131
1111
|
progress.complete = false;
|
|
1132
1112
|
if (continuing())
|
|
1133
1113
|
progress.reachable = false;
|
|
1134
1114
|
return;
|
|
1135
1115
|
}
|
|
1116
|
+
if (result.kind !== 'accepted' || !continuing())
|
|
1117
|
+
answered();
|
|
1136
1118
|
if (!continuing())
|
|
1137
1119
|
return;
|
|
1138
1120
|
if (result.kind === 'transport-uncertainty') {
|
|
@@ -1185,6 +1167,7 @@ export function composeRuntime(options) {
|
|
|
1185
1167
|
const reset = result.reset;
|
|
1186
1168
|
progress.complete = false;
|
|
1187
1169
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1170
|
+
armTimer();
|
|
1188
1171
|
return;
|
|
1189
1172
|
}
|
|
1190
1173
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
@@ -1211,95 +1194,33 @@ export function composeRuntime(options) {
|
|
|
1211
1194
|
return withhold(withheld, room, local, progress);
|
|
1212
1195
|
continue;
|
|
1213
1196
|
}
|
|
1214
|
-
await
|
|
1215
|
-
|
|
1216
|
-
if (from === undefined)
|
|
1217
|
-
return;
|
|
1218
|
-
const pulled = await pullRefresh(authorized, from, reachOf(room.locator));
|
|
1219
|
-
if (!continuing())
|
|
1220
|
-
return;
|
|
1221
|
-
if (pulled.kind === 'revoked-authorization') {
|
|
1222
|
-
await refuse('revoked-authorization');
|
|
1223
|
-
return;
|
|
1224
|
-
}
|
|
1225
|
-
if (pulled.kind === 'scoped-refusal') {
|
|
1226
|
-
await scoped(pulled, room, local);
|
|
1197
|
+
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1198
|
+
if (!kept) {
|
|
1227
1199
|
progress.complete = false;
|
|
1228
1200
|
return;
|
|
1229
1201
|
}
|
|
1230
|
-
if (pulled.kind === 'still-current' || pulled.kind === 'advanced') {
|
|
1231
|
-
const fresh = pulled.kind === 'advanced';
|
|
1232
|
-
if (!(await accepting(room, local, head, result, fresh ? pulled.confirmed : from, fresh))) {
|
|
1233
|
-
progress.complete = false;
|
|
1234
|
-
return;
|
|
1235
|
-
}
|
|
1236
|
-
continue;
|
|
1237
|
-
}
|
|
1238
|
-
if (pulled.kind === 'beyond-horizon') {
|
|
1239
|
-
const bootstrapped = await pullBootstrap(authorized, room);
|
|
1240
|
-
if (!continuing())
|
|
1241
|
-
return;
|
|
1242
|
-
if (bootstrapped.kind === 'revoked-authorization') {
|
|
1243
|
-
await refuse('revoked-authorization');
|
|
1244
|
-
return;
|
|
1245
|
-
}
|
|
1246
|
-
if (bootstrapped.kind === 'scoped-refusal') {
|
|
1247
|
-
await scoped(bootstrapped, room, local);
|
|
1248
|
-
progress.complete = false;
|
|
1249
|
-
return;
|
|
1250
|
-
}
|
|
1251
|
-
if (bootstrapped.kind === 'staged') {
|
|
1252
|
-
if (!(await accepting(room, local, head, result, bootstrapped.confirmed, true))) {
|
|
1253
|
-
progress.complete = false;
|
|
1254
|
-
return;
|
|
1255
|
-
}
|
|
1256
|
-
continue;
|
|
1257
|
-
}
|
|
1258
|
-
progress.complete = false;
|
|
1259
|
-
if (bootstrapped.kind === 'transport-uncertainty') {
|
|
1260
|
-
progress.reachable = false;
|
|
1261
|
-
}
|
|
1262
|
-
else if (bootstrapped.kind === 'room-not-readable') {
|
|
1263
|
-
unreadable.add(room.locator);
|
|
1264
|
-
list(room, false);
|
|
1265
|
-
emitRooms();
|
|
1266
|
-
}
|
|
1267
|
-
else if (bootstrapped.kind === 'refused') {
|
|
1268
|
-
refusedSource(room, bootstrapped);
|
|
1269
|
-
}
|
|
1270
|
-
return;
|
|
1271
|
-
}
|
|
1272
|
-
progress.complete = false;
|
|
1273
|
-
if (pulled.kind === 'transport-uncertainty') {
|
|
1274
|
-
progress.reachable = false;
|
|
1275
|
-
}
|
|
1276
|
-
else if (pulled.kind === 'room-not-readable') {
|
|
1277
|
-
unreadable.add(room.locator);
|
|
1278
|
-
list(room, false);
|
|
1279
|
-
emitRooms();
|
|
1280
|
-
}
|
|
1281
|
-
else if (pulled.kind === 'retryable-refusal') {
|
|
1282
|
-
journal.record(`refusal ${pulled.kind} ${pulled.code}`);
|
|
1283
|
-
}
|
|
1284
|
-
else if (pulled.kind === 'refused') {
|
|
1285
|
-
refusedSource(room, pulled);
|
|
1286
|
-
}
|
|
1287
|
-
return;
|
|
1288
1202
|
}
|
|
1289
|
-
|
|
1203
|
+
}
|
|
1204
|
+
function awaitingAnswer(locator) {
|
|
1205
|
+
let release = () => { };
|
|
1206
|
+
const awaited = new Promise((resolve) => {
|
|
1207
|
+
release = resolve;
|
|
1208
|
+
});
|
|
1209
|
+
answering.set(locator, awaited);
|
|
1210
|
+
return () => {
|
|
1211
|
+
if (answering.get(locator) === awaited)
|
|
1212
|
+
answering.delete(locator);
|
|
1213
|
+
release();
|
|
1214
|
+
};
|
|
1290
1215
|
}
|
|
1291
1216
|
async function retainModel(local, room, attached, next) {
|
|
1292
1217
|
const key = modelKey(room.locator, attached);
|
|
1293
|
-
if (next.status === 'unavailable')
|
|
1294
|
-
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
1295
|
-
const epoch = modelEpoch.get(key) ?? 0;
|
|
1296
1218
|
const previous = modelOf(room.locator, attached);
|
|
1297
1219
|
const heldBefore = replicas.get(room.locator)?.confirmed.record;
|
|
1298
1220
|
if (JSON.stringify(previous) === JSON.stringify(next) &&
|
|
1299
1221
|
(heldBefore === undefined ||
|
|
1300
1222
|
(JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
|
|
1301
1223
|
JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
|
|
1302
|
-
JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) &&
|
|
1303
1224
|
JSON.stringify(heldBefore.typedSelection) ===
|
|
1304
1225
|
JSON.stringify(room.typedSelection ?? null))))
|
|
1305
1226
|
return;
|
|
@@ -1310,15 +1231,12 @@ export function composeRuntime(options) {
|
|
|
1310
1231
|
await exclusive(async () => {
|
|
1311
1232
|
if (!continuing())
|
|
1312
1233
|
return;
|
|
1313
|
-
if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0))
|
|
1314
|
-
return;
|
|
1315
1234
|
const held = replicas.get(room.locator);
|
|
1316
1235
|
if (held !== undefined) {
|
|
1317
1236
|
const declared = {
|
|
1318
1237
|
...held.confirmed.record,
|
|
1319
1238
|
operations: room.operations,
|
|
1320
1239
|
replica: room.replica,
|
|
1321
|
-
attachedModel: room.attachedModel,
|
|
1322
1240
|
typedSelection: room.typedSelection ?? null,
|
|
1323
1241
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
1324
1242
|
};
|
|
@@ -1338,7 +1256,7 @@ export function composeRuntime(options) {
|
|
|
1338
1256
|
queue: held.queue,
|
|
1339
1257
|
});
|
|
1340
1258
|
if (read.kind === 'staged') {
|
|
1341
|
-
replicas.set(room.locator, replicaOf(read.confirmed, held.queue,
|
|
1259
|
+
replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
|
|
1342
1260
|
if (declarationRefusals.delete(room.locator)) {
|
|
1343
1261
|
sourceRefusals.delete(room.locator);
|
|
1344
1262
|
unreadable.delete(room.locator);
|
|
@@ -1362,77 +1280,6 @@ export function composeRuntime(options) {
|
|
|
1362
1280
|
publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
|
|
1363
1281
|
});
|
|
1364
1282
|
}
|
|
1365
|
-
async function acquireModel(local, room, authorized, selection) {
|
|
1366
|
-
const attached = selection === null ? 'collection' : 'typed-selection';
|
|
1367
|
-
const key = modelKey(room.locator, attached);
|
|
1368
|
-
const declared = selection === null ? room.attachedModel : selection.attachedModel;
|
|
1369
|
-
let previous = modelOf(room.locator, attached);
|
|
1370
|
-
if (previous.status !== 'unavailable' &&
|
|
1371
|
-
(declared === null || !sameModel(previous.snapshot, declared))) {
|
|
1372
|
-
previous = { status: 'unavailable', reason: 'version-changed' };
|
|
1373
|
-
await retainModel(local, room, attached, previous);
|
|
1374
|
-
if (discoveryPaused)
|
|
1375
|
-
return;
|
|
1376
|
-
}
|
|
1377
|
-
const epoch = modelEpoch.get(key) ?? 0;
|
|
1378
|
-
let answer;
|
|
1379
|
-
try {
|
|
1380
|
-
answer = await options.adapter.model(authorized, selection === null
|
|
1381
|
-
? { locator: room.locator, attached: 'collection', model: declared }
|
|
1382
|
-
: {
|
|
1383
|
-
locator: room.locator,
|
|
1384
|
-
attached: 'typed-selection',
|
|
1385
|
-
roots: selection.roots,
|
|
1386
|
-
model: declared,
|
|
1387
|
-
});
|
|
1388
|
-
}
|
|
1389
|
-
catch {
|
|
1390
|
-
answer = { kind: 'transport-uncertainty' };
|
|
1391
|
-
}
|
|
1392
|
-
if (discoveryPaused || epoch !== (modelEpoch.get(key) ?? 0))
|
|
1393
|
-
return;
|
|
1394
|
-
if (answer.kind === 'revoked-authorization') {
|
|
1395
|
-
await refuse('revoked-authorization');
|
|
1396
|
-
return;
|
|
1397
|
-
}
|
|
1398
|
-
if (answer.kind === 'transport-uncertainty') {
|
|
1399
|
-
await retainModel(local, room, attached, cached(previous));
|
|
1400
|
-
return;
|
|
1401
|
-
}
|
|
1402
|
-
if (answer.kind === 'model-unavailable') {
|
|
1403
|
-
await retainModel(local, room, attached, {
|
|
1404
|
-
status: 'unavailable',
|
|
1405
|
-
reason: answer.reason,
|
|
1406
|
-
});
|
|
1407
|
-
return;
|
|
1408
|
-
}
|
|
1409
|
-
if (declared === null || !sameModel(answer.snapshot, declared)) {
|
|
1410
|
-
await retainModel(local, room, attached, {
|
|
1411
|
-
status: 'unavailable',
|
|
1412
|
-
reason: 'version-changed',
|
|
1413
|
-
});
|
|
1414
|
-
return;
|
|
1415
|
-
}
|
|
1416
|
-
if (previous.status !== 'unavailable' &&
|
|
1417
|
-
sameModel(previous.snapshot, answer.snapshot) &&
|
|
1418
|
-
previous.snapshot.schema !== answer.snapshot.schema) {
|
|
1419
|
-
await retainModel(local, room, attached, { status: 'unavailable', reason: 'invalid' });
|
|
1420
|
-
return;
|
|
1421
|
-
}
|
|
1422
|
-
const verified = answer.snapshot.schema === null ? EMPTY_SCHEMA : verify(answer.snapshot.schema);
|
|
1423
|
-
if (verified.verdict === 'refuse') {
|
|
1424
|
-
const where = verified.location === undefined ? '' : ` ${verified.location}`;
|
|
1425
|
-
journal.record(`model invalid ${room.locator} ${attached} ${answer.snapshot.identity} ${verified.rule}${where}`);
|
|
1426
|
-
}
|
|
1427
|
-
await retainModel(local, room, attached, verified.verdict === 'accept'
|
|
1428
|
-
? {
|
|
1429
|
-
status: 'current',
|
|
1430
|
-
snapshot: structuredClone(answer.snapshot),
|
|
1431
|
-
verifiedWith: MD_MODEL,
|
|
1432
|
-
types: verified.model.types,
|
|
1433
|
-
}
|
|
1434
|
-
: { status: 'unavailable', reason: 'invalid' });
|
|
1435
|
-
}
|
|
1436
1283
|
async function scoped(result, room, local) {
|
|
1437
1284
|
scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
|
|
1438
1285
|
let scopedAway = room;
|
|
@@ -1460,14 +1307,34 @@ export function composeRuntime(options) {
|
|
|
1460
1307
|
emitRooms();
|
|
1461
1308
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
1462
1309
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1310
|
+
function heldRooms(listed) {
|
|
1311
|
+
return listed.flatMap((room) => {
|
|
1312
|
+
const record = replicas.get(room.locator)?.confirmed.record;
|
|
1313
|
+
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1314
|
+
return [];
|
|
1315
|
+
return [
|
|
1316
|
+
{
|
|
1317
|
+
locator: room.locator,
|
|
1318
|
+
roomVersion: record.roomVersion,
|
|
1319
|
+
documents: record.documents,
|
|
1320
|
+
revisions: record.revisions,
|
|
1321
|
+
attachments: record.attachments,
|
|
1322
|
+
},
|
|
1323
|
+
];
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
async function synchronize(listed, local, progress, attempt) {
|
|
1327
|
+
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
|
|
1328
|
+
const ready = listed.filter((_, index) => {
|
|
1466
1329
|
const outcome = prepared[index];
|
|
1467
1330
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1468
1331
|
});
|
|
1469
|
-
|
|
1470
|
-
|
|
1332
|
+
if (attempt === 'opened')
|
|
1333
|
+
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
1334
|
+
const loaded = await Promise.allSettled(ready
|
|
1335
|
+
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1336
|
+
.map((room) => loadRoom(room, local, progress)));
|
|
1337
|
+
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1471
1338
|
if (failed !== undefined)
|
|
1472
1339
|
throw failed.reason;
|
|
1473
1340
|
}
|
|
@@ -1484,11 +1351,8 @@ export function composeRuntime(options) {
|
|
|
1484
1351
|
progress.complete = false;
|
|
1485
1352
|
return false;
|
|
1486
1353
|
}
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
acquireModel(local, room, authorization, null),
|
|
1490
|
-
...(selection === undefined ? [] : [acquireModel(local, room, authorization, selection)]),
|
|
1491
|
-
]);
|
|
1354
|
+
if (prior !== undefined)
|
|
1355
|
+
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1492
1356
|
if (discoveryPaused || authorization === undefined)
|
|
1493
1357
|
return false;
|
|
1494
1358
|
if (room.replica.admission !== 'admitted') {
|
|
@@ -1496,24 +1360,53 @@ export function composeRuntime(options) {
|
|
|
1496
1360
|
progress.complete = false;
|
|
1497
1361
|
return false;
|
|
1498
1362
|
}
|
|
1499
|
-
const held = replicas.get(room.locator);
|
|
1500
|
-
if (held !== undefined && held.confirmed.record.replica.selection !== room.replica.selection) {
|
|
1501
|
-
progress.complete = false;
|
|
1502
|
-
return false;
|
|
1503
|
-
}
|
|
1504
1363
|
return true;
|
|
1505
1364
|
}
|
|
1506
|
-
async function
|
|
1365
|
+
async function caughtUp(room, local, progress) {
|
|
1366
|
+
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1367
|
+
if (held === undefined || authorization === undefined)
|
|
1368
|
+
return;
|
|
1369
|
+
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
1370
|
+
if (!behind)
|
|
1371
|
+
return;
|
|
1372
|
+
const withheld = await advance(room, local, authorization);
|
|
1373
|
+
if (withheld !== null)
|
|
1374
|
+
await withhold(withheld, room, local, progress);
|
|
1375
|
+
}
|
|
1376
|
+
function revoked(locator) {
|
|
1377
|
+
return ['collection', 'typed-selection'].some((attached) => {
|
|
1378
|
+
const model = modelOf(locator, attached);
|
|
1379
|
+
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
async function loadRoom(room, local, progress) {
|
|
1507
1383
|
if (discoveryPaused || authorization === undefined)
|
|
1508
1384
|
return;
|
|
1385
|
+
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1386
|
+
await reload(room, local, progress);
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1509
1389
|
const withheld = await advance(room, local, authorization);
|
|
1510
1390
|
if (withheld !== null) {
|
|
1511
1391
|
await withhold(withheld, room, local, progress);
|
|
1512
1392
|
return;
|
|
1513
1393
|
}
|
|
1514
|
-
await
|
|
1394
|
+
await caughtUp(room, local, progress);
|
|
1515
1395
|
}
|
|
1516
|
-
async function
|
|
1396
|
+
async function reload(room, local, progress) {
|
|
1397
|
+
if (discoveryPaused || authorization === undefined)
|
|
1398
|
+
return;
|
|
1399
|
+
const bootstrapped = await pullBootstrap(authorization, room);
|
|
1400
|
+
if (!continuing())
|
|
1401
|
+
return;
|
|
1402
|
+
if (bootstrapped.kind !== 'staged') {
|
|
1403
|
+
await withhold(bootstrapped, room, local, progress);
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
await publish(local, room, bootstrapped.confirmed);
|
|
1407
|
+
await caughtUp(room, local, progress);
|
|
1408
|
+
}
|
|
1409
|
+
async function discoverRooms(local, progress, attempt) {
|
|
1517
1410
|
if (closed || authorization === undefined) {
|
|
1518
1411
|
notifyReady();
|
|
1519
1412
|
return;
|
|
@@ -1539,6 +1432,10 @@ export function composeRuntime(options) {
|
|
|
1539
1432
|
}
|
|
1540
1433
|
}
|
|
1541
1434
|
progress.reachable = false;
|
|
1435
|
+
progress.complete = false;
|
|
1436
|
+
if (attempt === 'opened') {
|
|
1437
|
+
options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
|
|
1438
|
+
}
|
|
1542
1439
|
notifyReady();
|
|
1543
1440
|
return;
|
|
1544
1441
|
}
|
|
@@ -1553,17 +1450,6 @@ export function composeRuntime(options) {
|
|
|
1553
1450
|
notifyReady();
|
|
1554
1451
|
return;
|
|
1555
1452
|
}
|
|
1556
|
-
if (!result.supportedVersions.includes(ROOM_CONTRACT_VERSION)) {
|
|
1557
|
-
await refuse('no-matching-version');
|
|
1558
|
-
notifyReady();
|
|
1559
|
-
return;
|
|
1560
|
-
}
|
|
1561
|
-
if (result.mdModel.identity !== MD_MODEL.identity ||
|
|
1562
|
-
result.mdModel.version !== MD_MODEL.version) {
|
|
1563
|
-
await refuse('md-model-version-mismatch');
|
|
1564
|
-
notifyReady();
|
|
1565
|
-
return;
|
|
1566
|
-
}
|
|
1567
1453
|
for (const room of result.rooms) {
|
|
1568
1454
|
if (room.operations === undefined ||
|
|
1569
1455
|
!['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
@@ -1571,8 +1457,7 @@ export function composeRuntime(options) {
|
|
|
1571
1457
|
room.replica === undefined ||
|
|
1572
1458
|
typeof room.replica.selection !== 'string' ||
|
|
1573
1459
|
room.replica.selection.length === 0 ||
|
|
1574
|
-
!['admitted', 'unavailable'].includes(room.replica.admission)
|
|
1575
|
-
!declaredIdentity(room.attachedModel)) {
|
|
1460
|
+
!['admitted', 'unavailable'].includes(room.replica.admission)) {
|
|
1576
1461
|
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
1577
1462
|
await refuse('missing-required-guarantee');
|
|
1578
1463
|
notifyReady();
|
|
@@ -1585,14 +1470,6 @@ export function composeRuntime(options) {
|
|
|
1585
1470
|
return;
|
|
1586
1471
|
}
|
|
1587
1472
|
}
|
|
1588
|
-
const declared = new Set(result.capabilities.map((capability) => capability.name));
|
|
1589
|
-
for (const required of options.requiredGuarantees) {
|
|
1590
|
-
if (!knownCapabilities.has(required) || !declared.has(required)) {
|
|
1591
|
-
await refuse('missing-required-guarantee');
|
|
1592
|
-
notifyReady();
|
|
1593
|
-
return;
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
1473
|
if (refusal !== null) {
|
|
1597
1474
|
refusal = null;
|
|
1598
1475
|
emitRefusal();
|
|
@@ -1600,26 +1477,13 @@ export function composeRuntime(options) {
|
|
|
1600
1477
|
const previousCapabilities = capabilities;
|
|
1601
1478
|
capabilities = result.capabilities;
|
|
1602
1479
|
notifyReady();
|
|
1603
|
-
if (declared.has('room-version-notifications'))
|
|
1604
|
-
observeRoomVersions();
|
|
1605
1480
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1606
1481
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
1607
1482
|
limits = result.providerLimits;
|
|
1608
1483
|
await local.declare(limits, capabilities);
|
|
1609
1484
|
}
|
|
1610
1485
|
await applyDiscovery(result.rooms, local);
|
|
1611
|
-
|
|
1612
|
-
.filter((room) => room.replica.admission === 'admitted')
|
|
1613
|
-
.map((room) => room.locator)
|
|
1614
|
-
.sort());
|
|
1615
|
-
progress.complete = true;
|
|
1616
|
-
await synchronize(result.rooms, local, progress);
|
|
1617
|
-
if (closed || discoveryPaused)
|
|
1618
|
-
return;
|
|
1619
|
-
await conversations.reconcile();
|
|
1620
|
-
if (status.providerUnreachable)
|
|
1621
|
-
conversations.reconnected();
|
|
1622
|
-
await conversations.catchUp(progress);
|
|
1486
|
+
await synchronize(result.rooms, local, progress, attempt);
|
|
1623
1487
|
}
|
|
1624
1488
|
async function ensureAuthorization() {
|
|
1625
1489
|
if (authorization !== undefined)
|
|
@@ -1633,64 +1497,246 @@ export function composeRuntime(options) {
|
|
|
1633
1497
|
throw error;
|
|
1634
1498
|
}
|
|
1635
1499
|
}
|
|
1636
|
-
|
|
1637
|
-
if (
|
|
1638
|
-
|
|
1500
|
+
function task(work) {
|
|
1501
|
+
if (stopping)
|
|
1502
|
+
return chain;
|
|
1503
|
+
chain = chain.then(work, work).catch(() => undefined);
|
|
1504
|
+
return chain;
|
|
1505
|
+
}
|
|
1506
|
+
function sync(attempt) {
|
|
1507
|
+
return task(async () => {
|
|
1508
|
+
if (closed || stopping || discoveryPaused) {
|
|
1509
|
+
notifyReady();
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
const progress = { complete: true, reachable: true };
|
|
1513
|
+
underway = attempt === 'opened' ? progress : null;
|
|
1514
|
+
complete = false;
|
|
1515
|
+
recorded = false;
|
|
1516
|
+
moveTo(transferring(status));
|
|
1517
|
+
try {
|
|
1518
|
+
const local = await openLocal();
|
|
1519
|
+
if (closed)
|
|
1520
|
+
return;
|
|
1521
|
+
await ensureAuthorization();
|
|
1522
|
+
if (closed)
|
|
1523
|
+
return;
|
|
1524
|
+
await discoverRooms(local, progress, attempt);
|
|
1525
|
+
}
|
|
1526
|
+
catch {
|
|
1527
|
+
progress.complete = false;
|
|
1528
|
+
if (attempt === 'opened') {
|
|
1529
|
+
options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
finally {
|
|
1533
|
+
notifyReady();
|
|
1534
|
+
}
|
|
1535
|
+
if (attempt === 'opened')
|
|
1536
|
+
return;
|
|
1537
|
+
await finishOpening(progress);
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
function closedSocket(revoked) {
|
|
1541
|
+
const opened = socketOpen;
|
|
1542
|
+
socketOpen = false;
|
|
1543
|
+
if (revoked) {
|
|
1544
|
+
void refuse('revoked-authorization');
|
|
1639
1545
|
return;
|
|
1640
1546
|
}
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
if (closed)
|
|
1547
|
+
if (opened || status.providerUnreachable)
|
|
1548
|
+
return;
|
|
1549
|
+
moveTo({ ...status, providerUnreachable: true });
|
|
1550
|
+
}
|
|
1551
|
+
function applyOpening(expired) {
|
|
1552
|
+
return task(async () => {
|
|
1553
|
+
const progress = underway;
|
|
1554
|
+
underway = null;
|
|
1555
|
+
if (closed || stopping)
|
|
1650
1556
|
return;
|
|
1651
|
-
|
|
1652
|
-
|
|
1557
|
+
if (progress === null) {
|
|
1558
|
+
await reloadExpired(expired);
|
|
1653
1559
|
return;
|
|
1654
|
-
if (progress.complete) {
|
|
1655
|
-
completedAt = new Date().toISOString();
|
|
1656
|
-
await local.complete(completedAt);
|
|
1657
1560
|
}
|
|
1561
|
+
try {
|
|
1562
|
+
const local = await openLocal();
|
|
1563
|
+
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
1564
|
+
const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
|
|
1565
|
+
const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
|
|
1566
|
+
if (progress.reachable) {
|
|
1567
|
+
for (const room of held)
|
|
1568
|
+
if (!reloading.includes(room))
|
|
1569
|
+
await confirmModels(room, local);
|
|
1570
|
+
}
|
|
1571
|
+
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1572
|
+
if (failed !== undefined)
|
|
1573
|
+
throw failed.reason;
|
|
1574
|
+
}
|
|
1575
|
+
catch {
|
|
1576
|
+
progress.complete = false;
|
|
1577
|
+
progress.reachable = false;
|
|
1578
|
+
}
|
|
1579
|
+
await finishOpening(progress);
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
async function reloadExpired(expired) {
|
|
1583
|
+
const progress = { complete: true, reachable: true };
|
|
1584
|
+
try {
|
|
1585
|
+
const local = await openLocal();
|
|
1586
|
+
await Promise.allSettled([...discovered.values()]
|
|
1587
|
+
.filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
|
|
1588
|
+
.map((room) => reload(room, local, progress)));
|
|
1658
1589
|
}
|
|
1659
1590
|
catch {
|
|
1660
|
-
progress.
|
|
1661
|
-
|
|
1591
|
+
progress.reachable = false;
|
|
1592
|
+
}
|
|
1593
|
+
await rest(progress);
|
|
1594
|
+
}
|
|
1595
|
+
async function confirmModels(room, local) {
|
|
1596
|
+
for (const attached of ['collection', 'typed-selection']) {
|
|
1597
|
+
const model = modelOf(room.locator, attached);
|
|
1598
|
+
if (model.status === 'cached') {
|
|
1599
|
+
await retainModel(local, room, attached, { ...model, status: 'current' });
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
function submitLater(locator) {
|
|
1604
|
+
const running = submitting.get(locator);
|
|
1605
|
+
if (running !== undefined) {
|
|
1606
|
+
running.again = true;
|
|
1662
1607
|
return;
|
|
1663
1608
|
}
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1609
|
+
const run = { again: false, done: Promise.resolve() };
|
|
1610
|
+
submitting.set(locator, run);
|
|
1611
|
+
run.done = (async () => {
|
|
1612
|
+
try {
|
|
1613
|
+
do {
|
|
1614
|
+
run.again = false;
|
|
1615
|
+
const room = discovered.get(locator);
|
|
1616
|
+
if (closed || stopping || discoveryPaused || room === undefined)
|
|
1617
|
+
return;
|
|
1618
|
+
const progress = { complete: true, reachable: true };
|
|
1619
|
+
try {
|
|
1620
|
+
await ensureAuthorization();
|
|
1621
|
+
if (authorization === undefined)
|
|
1622
|
+
return;
|
|
1623
|
+
await submitRoom(room, await openLocal(), authorization, progress);
|
|
1624
|
+
}
|
|
1625
|
+
catch {
|
|
1626
|
+
progress.reachable = false;
|
|
1627
|
+
}
|
|
1628
|
+
armTimer();
|
|
1629
|
+
await rest(progress);
|
|
1630
|
+
} while (run.again);
|
|
1631
|
+
}
|
|
1632
|
+
finally {
|
|
1633
|
+
submitting.delete(locator);
|
|
1634
|
+
}
|
|
1635
|
+
})();
|
|
1636
|
+
}
|
|
1637
|
+
function submitAll() {
|
|
1638
|
+
for (const [locator, replica] of replicas) {
|
|
1639
|
+
if (replica.queue.length > 0)
|
|
1640
|
+
submitLater(locator);
|
|
1667
1641
|
}
|
|
1668
1642
|
}
|
|
1669
|
-
async function
|
|
1643
|
+
async function finishOpening(progress) {
|
|
1670
1644
|
try {
|
|
1671
|
-
|
|
1645
|
+
if (closed || discoveryPaused) {
|
|
1646
|
+
progress.complete = false;
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
submitAll();
|
|
1650
|
+
await conversations.reconcile();
|
|
1651
|
+
if (status.providerUnreachable)
|
|
1652
|
+
conversations.reconnected();
|
|
1653
|
+
const reading = { complete: true, reachable: true };
|
|
1654
|
+
await conversations.catchUp(reading);
|
|
1655
|
+
if (!reading.reachable)
|
|
1656
|
+
progress.reachable = false;
|
|
1657
|
+
}
|
|
1658
|
+
catch {
|
|
1659
|
+
progress.complete = false;
|
|
1672
1660
|
}
|
|
1673
1661
|
finally {
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
queued = false;
|
|
1677
|
-
void enqueue();
|
|
1678
|
-
}
|
|
1662
|
+
complete = progress.complete;
|
|
1663
|
+
await rest(progress);
|
|
1679
1664
|
}
|
|
1680
1665
|
}
|
|
1681
|
-
function
|
|
1682
|
-
|
|
1683
|
-
if (path === undefined || withdrawNotifications !== undefined)
|
|
1666
|
+
async function rest(progress) {
|
|
1667
|
+
if (closed)
|
|
1684
1668
|
return;
|
|
1685
|
-
|
|
1686
|
-
|
|
1669
|
+
let lastCompleted = status.lastCompleted;
|
|
1670
|
+
let synced = complete && stale.size === 0 && !pending();
|
|
1671
|
+
if (synced && !recorded) {
|
|
1672
|
+
const completedAt = new Date().toISOString();
|
|
1673
|
+
try {
|
|
1674
|
+
await (await openLocal()).complete(completedAt);
|
|
1675
|
+
recorded = true;
|
|
1676
|
+
lastCompleted = completedAt;
|
|
1677
|
+
}
|
|
1678
|
+
catch {
|
|
1679
|
+
synced = false;
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
moveTo({
|
|
1683
|
+
state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
|
|
1684
|
+
lastCompleted,
|
|
1685
|
+
providerUnreachable: !progress.reachable,
|
|
1686
|
+
...attention(),
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
function applyRoom(locator, roomVersion) {
|
|
1690
|
+
announced.set(locator, roomVersion);
|
|
1691
|
+
return task(async () => {
|
|
1692
|
+
await answering.get(locator);
|
|
1693
|
+
if (closed || stopping || discoveryPaused || authorization === undefined)
|
|
1687
1694
|
return;
|
|
1688
|
-
|
|
1689
|
-
|
|
1695
|
+
const room = discovered.get(locator);
|
|
1696
|
+
const held = replicas.get(locator);
|
|
1697
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
1698
|
+
return;
|
|
1699
|
+
if (held !== undefined &&
|
|
1700
|
+
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
1701
|
+
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
const during = underway;
|
|
1705
|
+
const progress = during ?? { complete: true, reachable: true };
|
|
1706
|
+
if (during === null)
|
|
1707
|
+
moveTo(transferring(status));
|
|
1708
|
+
try {
|
|
1709
|
+
const local = await openLocal();
|
|
1710
|
+
const withheld = await advance(room, local, authorization);
|
|
1711
|
+
if (withheld !== null)
|
|
1712
|
+
await withhold(withheld, room, local, progress);
|
|
1713
|
+
}
|
|
1714
|
+
catch {
|
|
1715
|
+
stale.add(locator);
|
|
1716
|
+
}
|
|
1717
|
+
finally {
|
|
1718
|
+
if (during === null)
|
|
1719
|
+
await rest(progress);
|
|
1720
|
+
}
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
function observeStream() {
|
|
1724
|
+
const stream = options.adapter.roomStream;
|
|
1725
|
+
if (stream === undefined || withdrawStream !== undefined)
|
|
1726
|
+
return;
|
|
1727
|
+
withdrawStream = stream.observe((event) => {
|
|
1728
|
+
if (closed || stopping)
|
|
1690
1729
|
return;
|
|
1730
|
+
if (event.kind === 'opened') {
|
|
1731
|
+
socketOpen = true;
|
|
1732
|
+
void sync('opened');
|
|
1691
1733
|
}
|
|
1692
|
-
|
|
1693
|
-
|
|
1734
|
+
else if (event.kind === 'closed')
|
|
1735
|
+
closedSocket(event.revoked);
|
|
1736
|
+
else if (event.kind === 'opening')
|
|
1737
|
+
void applyOpening(event.expired);
|
|
1738
|
+
else
|
|
1739
|
+
void applyRoom(event.locator, event.roomVersion);
|
|
1694
1740
|
});
|
|
1695
1741
|
}
|
|
1696
1742
|
function writesHeld() {
|
|
@@ -1711,7 +1757,7 @@ export function composeRuntime(options) {
|
|
|
1711
1757
|
listener(mode);
|
|
1712
1758
|
}
|
|
1713
1759
|
if (releasing)
|
|
1714
|
-
|
|
1760
|
+
submitAll();
|
|
1715
1761
|
}
|
|
1716
1762
|
function observeConversations() {
|
|
1717
1763
|
const stream = options.adapter.conversations;
|
|
@@ -1729,7 +1775,7 @@ export function composeRuntime(options) {
|
|
|
1729
1775
|
if (currencyQueued || closed || stopping)
|
|
1730
1776
|
return;
|
|
1731
1777
|
currencyQueued = true;
|
|
1732
|
-
|
|
1778
|
+
void task(runCurrency);
|
|
1733
1779
|
}
|
|
1734
1780
|
async function runCurrency() {
|
|
1735
1781
|
currencyQueued = false;
|
|
@@ -1755,95 +1801,12 @@ export function composeRuntime(options) {
|
|
|
1755
1801
|
}
|
|
1756
1802
|
}
|
|
1757
1803
|
}
|
|
1758
|
-
function handSelected(locators) {
|
|
1759
|
-
const path = options.adapter.roomVersions;
|
|
1760
|
-
if (path === undefined || withdrawNotifications === undefined)
|
|
1761
|
-
return;
|
|
1762
|
-
if (handed !== undefined && JSON.stringify(locators) === JSON.stringify(handed))
|
|
1763
|
-
return;
|
|
1764
|
-
handed = locators;
|
|
1765
|
-
path.select(locators);
|
|
1766
|
-
}
|
|
1767
|
-
async function runNotified() {
|
|
1768
|
-
const announced = [...notices];
|
|
1769
|
-
notices.clear();
|
|
1770
|
-
if (announced.length === 0 || closed || stopping || discoveryPaused)
|
|
1771
|
-
return;
|
|
1772
|
-
const resting = status;
|
|
1773
|
-
const progress = { complete: false, reachable: !resting.providerUnreachable };
|
|
1774
|
-
moveTo(transferring(resting));
|
|
1775
|
-
try {
|
|
1776
|
-
const local = await openLocal();
|
|
1777
|
-
for (const [locator, roomVersion] of announced) {
|
|
1778
|
-
if (closed || discoveryPaused || authorization === undefined)
|
|
1779
|
-
return;
|
|
1780
|
-
if (handed?.includes(locator) !== true)
|
|
1781
|
-
continue;
|
|
1782
|
-
const room = discovered.get(locator);
|
|
1783
|
-
const held = replicas.get(locator);
|
|
1784
|
-
if (room === undefined)
|
|
1785
|
-
continue;
|
|
1786
|
-
if (sourceRefusals.has(locator)) {
|
|
1787
|
-
void enqueue();
|
|
1788
|
-
continue;
|
|
1789
|
-
}
|
|
1790
|
-
if (held === undefined)
|
|
1791
|
-
continue;
|
|
1792
|
-
if (held.confirmed.record.roomVersion === roomVersion)
|
|
1793
|
-
continue;
|
|
1794
|
-
const withheld = await advance(room, local, authorization);
|
|
1795
|
-
if (withheld === null)
|
|
1796
|
-
continue;
|
|
1797
|
-
if (withheld.kind === 'refused') {
|
|
1798
|
-
void enqueue();
|
|
1799
|
-
continue;
|
|
1800
|
-
}
|
|
1801
|
-
if (withheld.kind === 'scoped-refusal') {
|
|
1802
|
-
await scoped(withheld, room, local);
|
|
1803
|
-
continue;
|
|
1804
|
-
}
|
|
1805
|
-
if (withheld.kind === 'transport-uncertainty') {
|
|
1806
|
-
progress.reachable = false;
|
|
1807
|
-
continue;
|
|
1808
|
-
}
|
|
1809
|
-
if (withheld.kind === 'room-not-readable') {
|
|
1810
|
-
unreadable.add(locator);
|
|
1811
|
-
list(room, false);
|
|
1812
|
-
emitRooms();
|
|
1813
|
-
continue;
|
|
1814
|
-
}
|
|
1815
|
-
if (withheld.kind === 'revoked-authorization') {
|
|
1816
|
-
await refuse('revoked-authorization');
|
|
1817
|
-
return;
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
catch {
|
|
1822
|
-
progress.reachable = false;
|
|
1823
|
-
}
|
|
1824
|
-
finally {
|
|
1825
|
-
if (!closed) {
|
|
1826
|
-
moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
|
|
1827
|
-
}
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
function enqueue() {
|
|
1831
|
-
if (stopping)
|
|
1832
|
-
return chain;
|
|
1833
|
-
if (inflight) {
|
|
1834
|
-
queued = true;
|
|
1835
|
-
return chain;
|
|
1836
|
-
}
|
|
1837
|
-
inflight = true;
|
|
1838
|
-
chain = chain.then(run, run);
|
|
1839
|
-
return chain;
|
|
1840
|
-
}
|
|
1841
1804
|
function close() {
|
|
1842
1805
|
if (closed)
|
|
1843
1806
|
return;
|
|
1844
1807
|
closed = true;
|
|
1845
|
-
|
|
1846
|
-
|
|
1808
|
+
withdrawStream?.();
|
|
1809
|
+
withdrawStream = undefined;
|
|
1847
1810
|
withdrawConversations?.();
|
|
1848
1811
|
withdrawConversations = undefined;
|
|
1849
1812
|
withdrawServiceMode?.();
|
|
@@ -1867,6 +1830,7 @@ export function composeRuntime(options) {
|
|
|
1867
1830
|
stopping = true;
|
|
1868
1831
|
stopTimer();
|
|
1869
1832
|
await chain.catch(() => undefined);
|
|
1833
|
+
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
1870
1834
|
await mutations;
|
|
1871
1835
|
close();
|
|
1872
1836
|
}
|
|
@@ -1875,11 +1839,29 @@ export function composeRuntime(options) {
|
|
|
1875
1839
|
options.signal?.addEventListener('abort', close, { once: true });
|
|
1876
1840
|
if (options.signal?.aborted)
|
|
1877
1841
|
close();
|
|
1878
|
-
|
|
1842
|
+
observeStream();
|
|
1843
|
+
void openLocal().then(() => {
|
|
1844
|
+
if (capabilities.length > 0)
|
|
1845
|
+
notifyReady();
|
|
1846
|
+
}, () => {
|
|
1879
1847
|
notifyReady();
|
|
1880
1848
|
});
|
|
1881
|
-
|
|
1882
|
-
|
|
1849
|
+
if (options.adapter.roomStream === undefined)
|
|
1850
|
+
void sync('unstreamed');
|
|
1851
|
+
function planned(input, reach) {
|
|
1852
|
+
return planOf(input, roomFacts(reach), planReach(reach));
|
|
1853
|
+
}
|
|
1854
|
+
function watchPlan(plan, deliver) {
|
|
1855
|
+
return readSide.watch(plan, identityOf(plan), deliver);
|
|
1856
|
+
}
|
|
1857
|
+
async function searched(input, reach) {
|
|
1858
|
+
const plan = searchPlanOf(input);
|
|
1859
|
+
const selected = plan.documents === undefined && reach === null
|
|
1860
|
+
? null
|
|
1861
|
+
: (await readSide.query(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
|
|
1862
|
+
return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
|
|
1863
|
+
}
|
|
1864
|
+
const watch = (topic, input, deliver) => {
|
|
1883
1865
|
if (topic === 'room-drafts') {
|
|
1884
1866
|
const listener = { notify: deliver, ready: false };
|
|
1885
1867
|
roomDraftListeners.add(listener);
|
|
@@ -1962,9 +1944,8 @@ export function composeRuntime(options) {
|
|
|
1962
1944
|
queueSubscriptions.delete(queue);
|
|
1963
1945
|
};
|
|
1964
1946
|
}
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
}
|
|
1947
|
+
return watchPlan(planned(input[0], null), deliver);
|
|
1948
|
+
};
|
|
1968
1949
|
const md = {
|
|
1969
1950
|
loaded() {
|
|
1970
1951
|
return openLocal().then(() => undefined);
|
|
@@ -1972,15 +1953,8 @@ export function composeRuntime(options) {
|
|
|
1972
1953
|
rooms() {
|
|
1973
1954
|
return listed();
|
|
1974
1955
|
},
|
|
1975
|
-
query: ((input) => readSide.query(
|
|
1976
|
-
|
|
1977
|
-
const plan = searchPlanOf(input);
|
|
1978
|
-
const selected = plan.documents === undefined
|
|
1979
|
-
? null
|
|
1980
|
-
: (await readSide.query(filterOf(plan.documents, roomFacts())))
|
|
1981
|
-
.documents;
|
|
1982
|
-
return searchIndex.search(plan, selected);
|
|
1983
|
-
},
|
|
1956
|
+
query: ((input) => readSide.query(planned(input, null))),
|
|
1957
|
+
search: (input) => searched(input, null),
|
|
1984
1958
|
types(locator) {
|
|
1985
1959
|
return {
|
|
1986
1960
|
collection: modelTypes(modelOf(locator)),
|
|
@@ -2087,9 +2061,7 @@ export function composeRuntime(options) {
|
|
|
2087
2061
|
persistence() {
|
|
2088
2062
|
return options.shell.persistentStorage;
|
|
2089
2063
|
},
|
|
2090
|
-
observe: (
|
|
2091
|
-
deliver(structuredClone(message));
|
|
2092
|
-
}), topic === 'query')),
|
|
2064
|
+
observe: observing(watch),
|
|
2093
2065
|
async reauthorize() {
|
|
2094
2066
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2095
2067
|
return;
|
|
@@ -2098,8 +2070,19 @@ export function composeRuntime(options) {
|
|
|
2098
2070
|
await mutations;
|
|
2099
2071
|
authorization = undefined;
|
|
2100
2072
|
discoveryPaused = false;
|
|
2101
|
-
|
|
2073
|
+
if (options.adapter.roomStream === undefined)
|
|
2074
|
+
await sync('unstreamed');
|
|
2075
|
+
else
|
|
2076
|
+
await task(ensureAuthorization);
|
|
2102
2077
|
},
|
|
2103
2078
|
};
|
|
2104
|
-
|
|
2079
|
+
const reachable = {
|
|
2080
|
+
md,
|
|
2081
|
+
watch,
|
|
2082
|
+
plan: planned,
|
|
2083
|
+
query: (plan) => readSide.query(plan),
|
|
2084
|
+
watchPlan,
|
|
2085
|
+
search: searched,
|
|
2086
|
+
};
|
|
2087
|
+
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2105
2088
|
}
|