@agentshouse/mdruntime 0.1.0-cfb0d45b48da0 → 0.1.0-cfb2ae0107054
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 +5 -5
- package/dist/adapter.d.ts +24 -2
- package/dist/adapter.d.ts.map +1 -1
- package/dist/attempt.d.ts.map +1 -1
- package/dist/attempt.js +14 -1
- package/dist/bootstrap.d.ts +2 -2
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +28 -6
- package/dist/chat/acts.d.ts +3 -4
- package/dist/chat/acts.d.ts.map +1 -1
- package/dist/chat/acts.js +10 -10
- package/dist/chat/list.d.ts +6 -0
- package/dist/chat/list.d.ts.map +1 -1
- package/dist/chat/list.js +6 -3
- package/dist/chat/markdown.d.ts +2 -1
- package/dist/chat/markdown.d.ts.map +1 -1
- package/dist/chat/markdown.js +7 -6
- package/dist/chat/provider.d.ts.map +1 -1
- package/dist/chat/provider.js +25 -50
- package/dist/chat/ready.d.ts.map +1 -1
- package/dist/chat/ready.js +2 -2
- package/dist/chat/style.js +3 -3
- package/dist/client.d.ts +2 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1 -1
- package/dist/connection.d.ts +1 -0
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +337 -150
- package/dist/conversation.d.ts +4 -2
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +143 -58
- package/dist/entry.d.ts +17 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +156 -0
- package/dist/fence.d.ts +6 -0
- package/dist/fence.d.ts.map +1 -0
- package/dist/fence.js +10 -0
- package/dist/host.d.ts +2 -1
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +5 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/memory.d.ts +1 -0
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +9 -0
- package/dist/outcome.d.ts +4 -0
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +1 -0
- package/dist/reach.js +1 -1
- package/dist/save.d.ts +1 -2
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +6 -5
- package/dist/store.d.ts +22 -7
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +125 -36
- package/package.json +5 -1
package/dist/connection.js
CHANGED
|
@@ -13,13 +13,14 @@ import { validRoot } from './collection.js';
|
|
|
13
13
|
import { runtimeError } from './failure.js';
|
|
14
14
|
import { createLog } from './log.js';
|
|
15
15
|
import { binaryRecord } from './binary.js';
|
|
16
|
-
import { absentBinary, binaryAnswer, calledResult, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
|
|
16
|
+
import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
|
|
17
17
|
import { refreshRoom } from './refresh.js';
|
|
18
18
|
import { buildBatch, joined, limitedIn, requeued } from './save.js';
|
|
19
19
|
import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
|
|
20
20
|
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
21
21
|
import { same, saving, STARTING, transferring, } from './status.js';
|
|
22
22
|
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
23
|
+
import { FencedMap } from './fence.js';
|
|
23
24
|
import { roomDraftRevision } from './identity.js';
|
|
24
25
|
import { beneath, normalize } from './paths.js';
|
|
25
26
|
import { openRuntimeStore, } from './store.js';
|
|
@@ -38,23 +39,36 @@ function declaredSelection(room) {
|
|
|
38
39
|
root !== '' &&
|
|
39
40
|
selection.roots.every((other, at) => at === index || !within(other, root)));
|
|
40
41
|
}
|
|
42
|
+
const OPENING_LANE = 'opening';
|
|
43
|
+
const CONVERSATION_LANE = 'conversations';
|
|
44
|
+
const ROOM_READS = 256;
|
|
45
|
+
function roomLane(locator) {
|
|
46
|
+
return `room ${locator}`;
|
|
47
|
+
}
|
|
41
48
|
export function composeRuntime(options) {
|
|
42
49
|
const localSaves = options.adapter.localSaves === true;
|
|
43
50
|
let authorization = options.authorization;
|
|
44
51
|
let opening;
|
|
45
52
|
let store;
|
|
46
|
-
const
|
|
47
|
-
const
|
|
53
|
+
const fences = new Map();
|
|
54
|
+
const isFenced = (locator) => fences.has(locator);
|
|
55
|
+
let fencings = 0;
|
|
56
|
+
const lastFenced = new Map();
|
|
57
|
+
const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
|
|
58
|
+
let discoveries = 0;
|
|
59
|
+
const replicas = new FencedMap(isFenced);
|
|
60
|
+
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
48
61
|
const pendings = new Map();
|
|
49
|
-
const roomDrafts = new
|
|
62
|
+
const roomDrafts = new FencedMap(isFenced);
|
|
50
63
|
const presentations = new Map();
|
|
51
64
|
let limits = [];
|
|
52
65
|
let capabilities = [];
|
|
53
|
-
const rooms = new
|
|
66
|
+
const rooms = new FencedMap(isFenced);
|
|
54
67
|
const counted = new Map();
|
|
55
68
|
const unreadable = new Set();
|
|
56
|
-
const
|
|
57
|
-
const
|
|
69
|
+
const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
|
|
70
|
+
const models = new FencedMap(isModelFenced);
|
|
71
|
+
const liveModels = new FencedMap(isModelFenced);
|
|
58
72
|
const invalidModels = new Set();
|
|
59
73
|
const discovered = new Map();
|
|
60
74
|
let roomsListed = false;
|
|
@@ -97,7 +111,13 @@ export function composeRuntime(options) {
|
|
|
97
111
|
let complete = false;
|
|
98
112
|
let recorded = false;
|
|
99
113
|
const stale = new Set();
|
|
100
|
-
let
|
|
114
|
+
let generation = 0;
|
|
115
|
+
let discovering = null;
|
|
116
|
+
let opened = null;
|
|
117
|
+
const owed = new Set();
|
|
118
|
+
const lanes = new Map();
|
|
119
|
+
let roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
120
|
+
const working = new Set();
|
|
101
121
|
let mutations = Promise.resolve();
|
|
102
122
|
const roomListeners = new Set();
|
|
103
123
|
const refusalListeners = new Set();
|
|
@@ -108,7 +128,7 @@ export function composeRuntime(options) {
|
|
|
108
128
|
const presentationListeners = new Set();
|
|
109
129
|
const readSide = createReadSide();
|
|
110
130
|
let withdrawConversations;
|
|
111
|
-
let currencyQueued =
|
|
131
|
+
let currencyQueued = null;
|
|
112
132
|
let socketOpen = false;
|
|
113
133
|
let serviceMode = 'open';
|
|
114
134
|
let haltedRefusal = false;
|
|
@@ -118,7 +138,7 @@ export function composeRuntime(options) {
|
|
|
118
138
|
const memberListeners = new Set();
|
|
119
139
|
const materialized = new Set();
|
|
120
140
|
let everyRoom = false;
|
|
121
|
-
const transfers = new
|
|
141
|
+
const transfers = new FencedMap(isFenced);
|
|
122
142
|
const conversations = createConversationStore({
|
|
123
143
|
agents: declaredAgents,
|
|
124
144
|
documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
|
|
@@ -126,6 +146,7 @@ export function composeRuntime(options) {
|
|
|
126
146
|
durably,
|
|
127
147
|
stream: () => options.adapter.conversations,
|
|
128
148
|
publish: publishChangeSet,
|
|
149
|
+
online: () => !status.providerUnreachable && refusal === null,
|
|
129
150
|
closed: () => closed,
|
|
130
151
|
});
|
|
131
152
|
const searchIndex = createSearchIndex({
|
|
@@ -233,12 +254,11 @@ export function composeRuntime(options) {
|
|
|
233
254
|
if (replica === undefined)
|
|
234
255
|
return 0;
|
|
235
256
|
const roots = replica.confirmed.record.typedSelection?.roots ?? [];
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return count;
|
|
257
|
+
const paths = [
|
|
258
|
+
...Object.keys(replica.visible.documents).filter(markdownPath),
|
|
259
|
+
...Object.keys(replica.visible.binaries),
|
|
260
|
+
];
|
|
261
|
+
return paths.filter((path) => !roots.some((root) => beneath(root, path))).length;
|
|
242
262
|
}
|
|
243
263
|
function listed() {
|
|
244
264
|
return structuredClone([...rooms.values()].map((room) => {
|
|
@@ -477,14 +497,24 @@ export function composeRuntime(options) {
|
|
|
477
497
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
478
498
|
});
|
|
479
499
|
}
|
|
480
|
-
async function applyDiscovery(
|
|
500
|
+
async function applyDiscovery(found, started) {
|
|
501
|
+
for (const room of found) {
|
|
502
|
+
const fence = fences.get(room.locator);
|
|
503
|
+
if (fence !== undefined && fence.erased && fence.at < started)
|
|
504
|
+
fences.delete(room.locator);
|
|
505
|
+
}
|
|
506
|
+
const listed = found.filter((room) => !fences.has(room.locator));
|
|
481
507
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
482
508
|
for (const locator of listedLocators)
|
|
483
509
|
scopedRefusals.delete(locator);
|
|
484
510
|
discovered.clear();
|
|
485
511
|
for (const room of listed)
|
|
486
512
|
discovered.set(room.locator, room);
|
|
487
|
-
|
|
513
|
+
const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
|
|
514
|
+
if (options.adapter.exhaustiveDiscovery === true)
|
|
515
|
+
await loseRooms(unlisted);
|
|
516
|
+
else
|
|
517
|
+
unlistedKept(unlisted);
|
|
488
518
|
for (const room of listed) {
|
|
489
519
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
490
520
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -495,6 +525,7 @@ export function composeRuntime(options) {
|
|
|
495
525
|
}
|
|
496
526
|
roomsListed = true;
|
|
497
527
|
emitRooms();
|
|
528
|
+
return listed;
|
|
498
529
|
}
|
|
499
530
|
function unlistedHeld(listed) {
|
|
500
531
|
return [
|
|
@@ -506,41 +537,67 @@ export function composeRuntime(options) {
|
|
|
506
537
|
]),
|
|
507
538
|
].filter((locator) => !listed.has(locator));
|
|
508
539
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
|
|
516
|
-
room: true,
|
|
517
|
-
queue: true,
|
|
518
|
-
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
519
|
-
}));
|
|
520
|
-
for (const locator of unlisted) {
|
|
521
|
-
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
522
|
-
pendings.delete(batch.id);
|
|
523
|
-
replicas.delete(locator);
|
|
524
|
-
queuesWithoutReplica.delete(locator);
|
|
540
|
+
function unlistedKept(unlisted) {
|
|
541
|
+
for (const locator of unlisted) {
|
|
542
|
+
const held = replicas.get(locator)?.confirmed.record;
|
|
543
|
+
if (held !== undefined)
|
|
544
|
+
list(held, false);
|
|
545
|
+
else
|
|
525
546
|
rooms.delete(locator);
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
function fence(locators) {
|
|
550
|
+
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
551
|
+
const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
|
|
552
|
+
room: true,
|
|
553
|
+
queue: true,
|
|
554
|
+
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
555
|
+
}));
|
|
556
|
+
for (const locator of fresh) {
|
|
557
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
558
|
+
lastFenced.set(locator, ++fencings);
|
|
559
|
+
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
560
|
+
pendings.delete(batch.id);
|
|
561
|
+
replicas.delete(locator);
|
|
562
|
+
queuesWithoutReplica.delete(locator);
|
|
563
|
+
rooms.delete(locator);
|
|
564
|
+
roomDrafts.delete(locator);
|
|
565
|
+
transfers.delete(locator);
|
|
566
|
+
discovered.delete(locator);
|
|
567
|
+
announced.delete(locator);
|
|
568
|
+
unreadable.delete(locator);
|
|
569
|
+
stale.delete(locator);
|
|
570
|
+
scopedRefusals.delete(locator);
|
|
571
|
+
sourceRefusals.delete(locator);
|
|
572
|
+
declarationRefusals.delete(locator);
|
|
573
|
+
for (const attached of ['collection', 'typed-selection']) {
|
|
574
|
+
const key = modelKey(locator, attached);
|
|
575
|
+
models.delete(key);
|
|
576
|
+
liveModels.delete(key);
|
|
577
|
+
invalidModels.delete(key);
|
|
540
578
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
579
|
+
journal.record(`room lost ${locator}`);
|
|
580
|
+
}
|
|
581
|
+
if (fresh.length === 0)
|
|
582
|
+
return fresh;
|
|
583
|
+
publishChangeSet(mergeChangeSets(removed));
|
|
584
|
+
for (const locator of fresh)
|
|
585
|
+
materialized.delete(locator);
|
|
586
|
+
moveTo({ ...status, ...attention() });
|
|
587
|
+
return fresh;
|
|
588
|
+
}
|
|
589
|
+
async function erased(local, locators) {
|
|
590
|
+
await local.erase(locators);
|
|
591
|
+
for (const locator of locators) {
|
|
592
|
+
const held = fences.get(locator);
|
|
593
|
+
if (held !== undefined)
|
|
594
|
+
held.erased = true;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
async function loseRooms(locators) {
|
|
598
|
+
const fresh = fence(locators);
|
|
599
|
+
if (fresh.length > 0)
|
|
600
|
+
await durably((local) => erased(local, fresh));
|
|
544
601
|
}
|
|
545
602
|
function restored(key, persisted) {
|
|
546
603
|
const prior = models.get(key);
|
|
@@ -645,8 +702,15 @@ export function composeRuntime(options) {
|
|
|
645
702
|
return lastCompleted;
|
|
646
703
|
}
|
|
647
704
|
async function open() {
|
|
648
|
-
|
|
705
|
+
if (closed)
|
|
706
|
+
throw new Error('runtime closed');
|
|
707
|
+
const opened = await openRuntimeStore(isFenced);
|
|
649
708
|
store = opened;
|
|
709
|
+
const interrupted = await opened.fences();
|
|
710
|
+
for (const locator of interrupted) {
|
|
711
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
712
|
+
lastFenced.set(locator, ++fencings);
|
|
713
|
+
}
|
|
650
714
|
const kept = options.shell.generation;
|
|
651
715
|
const storedGeneration = await opened.generation();
|
|
652
716
|
const loss = detectStorageLoss(storedGeneration, kept);
|
|
@@ -660,6 +724,8 @@ export function composeRuntime(options) {
|
|
|
660
724
|
emitStorageLoss();
|
|
661
725
|
}
|
|
662
726
|
const lastCompleted = await readState(opened);
|
|
727
|
+
if (interrupted.length > 0)
|
|
728
|
+
await erased(opened, interrupted);
|
|
663
729
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
664
730
|
return opened;
|
|
665
731
|
}
|
|
@@ -784,10 +850,10 @@ export function composeRuntime(options) {
|
|
|
784
850
|
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
785
851
|
};
|
|
786
852
|
}
|
|
787
|
-
function publish(local, room, confirmed, landing
|
|
853
|
+
function publish(local, room, confirmed, landing, live) {
|
|
788
854
|
const { base, stored } = landing;
|
|
789
855
|
return exclusive(async () => {
|
|
790
|
-
if (!
|
|
856
|
+
if (!live())
|
|
791
857
|
return true;
|
|
792
858
|
const prior = replicas.get(room.locator);
|
|
793
859
|
if (base !== undefined &&
|
|
@@ -815,6 +881,7 @@ export function composeRuntime(options) {
|
|
|
815
881
|
list(room, true);
|
|
816
882
|
publishChangeSet(set);
|
|
817
883
|
moveTo({ ...status, ...attention() });
|
|
884
|
+
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
818
885
|
return true;
|
|
819
886
|
});
|
|
820
887
|
}
|
|
@@ -937,6 +1004,8 @@ export function composeRuntime(options) {
|
|
|
937
1004
|
catch {
|
|
938
1005
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
939
1006
|
}
|
|
1007
|
+
if (fences.has(locator))
|
|
1008
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
940
1009
|
for (const origin of withdrawn.keys())
|
|
941
1010
|
pendings.delete(origin);
|
|
942
1011
|
for (const [id, drafts] of drafted) {
|
|
@@ -966,6 +1035,8 @@ export function composeRuntime(options) {
|
|
|
966
1035
|
return;
|
|
967
1036
|
}
|
|
968
1037
|
await local.submit(locator, identifier, submission, lost);
|
|
1038
|
+
if (fences.has(locator))
|
|
1039
|
+
return;
|
|
969
1040
|
pendings.set(identifier, { ...state, submission, lost });
|
|
970
1041
|
const replica = replicas.get(locator);
|
|
971
1042
|
if (replica !== undefined)
|
|
@@ -976,6 +1047,8 @@ export function composeRuntime(options) {
|
|
|
976
1047
|
if (!continuing())
|
|
977
1048
|
throw new Error('connection refused');
|
|
978
1049
|
await local.keepUpload(locator, identifier, upload);
|
|
1050
|
+
if (fences.has(locator))
|
|
1051
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
979
1052
|
pendings.set(identifier, { ...pendings.get(identifier), upload });
|
|
980
1053
|
if (!continuing())
|
|
981
1054
|
throw new Error('connection refused');
|
|
@@ -991,6 +1064,7 @@ export function composeRuntime(options) {
|
|
|
991
1064
|
}
|
|
992
1065
|
if (closed || authorization === undefined || refusal !== null)
|
|
993
1066
|
return UNCERTAIN;
|
|
1067
|
+
const since = fencings;
|
|
994
1068
|
const transfer = ['transfer', 'history', locator, path];
|
|
995
1069
|
if (query.revision !== undefined)
|
|
996
1070
|
transfer.push(query.revision);
|
|
@@ -1005,6 +1079,8 @@ export function composeRuntime(options) {
|
|
|
1005
1079
|
...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
|
|
1006
1080
|
});
|
|
1007
1081
|
journal.record(`outcome history ${result.kind}`);
|
|
1082
|
+
if (fencedSince(locator, since))
|
|
1083
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
1008
1084
|
if (result.kind === 'scoped-refusal') {
|
|
1009
1085
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
1010
1086
|
if (room !== undefined)
|
|
@@ -1027,6 +1103,12 @@ export function composeRuntime(options) {
|
|
|
1027
1103
|
}
|
|
1028
1104
|
return null;
|
|
1029
1105
|
}
|
|
1106
|
+
function binaryRoom(request) {
|
|
1107
|
+
if (request.kind === 'path')
|
|
1108
|
+
return request.locator;
|
|
1109
|
+
const scope = options.adapter.referrerScope(request.referrer);
|
|
1110
|
+
return scope?.kind === 'room' ? scope.locator : null;
|
|
1111
|
+
}
|
|
1030
1112
|
async function readBinary(request) {
|
|
1031
1113
|
if (closed || refusal !== null)
|
|
1032
1114
|
return UNCERTAIN;
|
|
@@ -1038,12 +1120,16 @@ export function composeRuntime(options) {
|
|
|
1038
1120
|
}
|
|
1039
1121
|
if (closed || authorization === undefined || refusal !== null)
|
|
1040
1122
|
return UNCERTAIN;
|
|
1123
|
+
const since = fencings;
|
|
1041
1124
|
journal.record(request.kind === 'path'
|
|
1042
1125
|
? `transfer binary ${request.locator} ${request.path}`
|
|
1043
1126
|
: `transfer binary ${request.fileRef}`);
|
|
1044
1127
|
try {
|
|
1045
1128
|
const result = await options.adapter.binary(authorization, request);
|
|
1046
1129
|
journal.record(`outcome binary ${result.kind}`);
|
|
1130
|
+
const locator = binaryRoom(request);
|
|
1131
|
+
if (locator !== null && fencedSince(locator, since))
|
|
1132
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
1047
1133
|
if (result.kind === 'scoped-refusal') {
|
|
1048
1134
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
1049
1135
|
if (room !== undefined)
|
|
@@ -1167,11 +1253,11 @@ export function composeRuntime(options) {
|
|
|
1167
1253
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
1168
1254
|
!options.adapter.reached(locator, roomVersion, written));
|
|
1169
1255
|
}
|
|
1170
|
-
async function pullRefresh(authorized, confirmed) {
|
|
1171
|
-
if (!
|
|
1256
|
+
async function pullRefresh(authorized, confirmed, live) {
|
|
1257
|
+
if (!live())
|
|
1172
1258
|
return unconfirmed;
|
|
1173
1259
|
try {
|
|
1174
|
-
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held),
|
|
1260
|
+
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
|
|
1175
1261
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
1176
1262
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
1177
1263
|
return result;
|
|
@@ -1181,33 +1267,39 @@ export function composeRuntime(options) {
|
|
|
1181
1267
|
throw error;
|
|
1182
1268
|
}
|
|
1183
1269
|
}
|
|
1184
|
-
function staging(local) {
|
|
1270
|
+
function staging(local, live) {
|
|
1185
1271
|
return {
|
|
1186
1272
|
held: (locator) => transfers.get(locator) ?? null,
|
|
1187
1273
|
staged: (record) => local.staged(record),
|
|
1188
1274
|
stage: (record, page) => exclusive(async () => {
|
|
1189
|
-
if (!
|
|
1275
|
+
if (!live())
|
|
1190
1276
|
return false;
|
|
1191
1277
|
await local.stage(record, page);
|
|
1192
1278
|
transfers.set(record.locator, record);
|
|
1193
1279
|
return true;
|
|
1194
1280
|
}),
|
|
1195
1281
|
abandon: (locator) => exclusive(async () => {
|
|
1196
|
-
if (!
|
|
1282
|
+
if (!live())
|
|
1197
1283
|
return;
|
|
1198
1284
|
await local.abandon(locator);
|
|
1199
1285
|
transfers.delete(locator);
|
|
1200
1286
|
}),
|
|
1201
1287
|
};
|
|
1202
1288
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1289
|
+
function reusable(room) {
|
|
1290
|
+
const record = replicas.get(room.locator)?.confirmed.record;
|
|
1291
|
+
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1292
|
+
return null;
|
|
1293
|
+
return { documents: record.documents, revisions: record.revisions };
|
|
1294
|
+
}
|
|
1295
|
+
async function pullBootstrap(authorized, room, local, live) {
|
|
1296
|
+
if (!live())
|
|
1205
1297
|
return unconfirmed;
|
|
1206
1298
|
try {
|
|
1207
1299
|
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
1208
1300
|
collection: modelOf(room.locator),
|
|
1209
1301
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
1210
|
-
}, (models, held) => remodel(room.locator, models, held),
|
|
1302
|
+
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
1211
1303
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
1212
1304
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1213
1305
|
return result;
|
|
@@ -1217,11 +1309,11 @@ export function composeRuntime(options) {
|
|
|
1217
1309
|
throw error;
|
|
1218
1310
|
}
|
|
1219
1311
|
}
|
|
1220
|
-
async function advance(room, local, authorized) {
|
|
1312
|
+
async function advance(room, local, authorized, live) {
|
|
1221
1313
|
const held = replicas.get(room.locator);
|
|
1222
1314
|
if (held !== undefined) {
|
|
1223
|
-
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
1224
|
-
if (!
|
|
1315
|
+
const refreshed = await pullRefresh(authorized, held.confirmed, live);
|
|
1316
|
+
if (!live())
|
|
1225
1317
|
return unconfirmed;
|
|
1226
1318
|
if (refreshed.kind === 'still-current') {
|
|
1227
1319
|
stale.delete(room.locator);
|
|
@@ -1235,24 +1327,28 @@ export function composeRuntime(options) {
|
|
|
1235
1327
|
return null;
|
|
1236
1328
|
}
|
|
1237
1329
|
if (refreshed.kind === 'advanced') {
|
|
1238
|
-
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
|
|
1330
|
+
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
|
|
1239
1331
|
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1240
1332
|
const reached = now !== undefined &&
|
|
1241
1333
|
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
1242
|
-
return reached ? null : advance(room, local, authorized);
|
|
1334
|
+
return reached ? null : advance(room, local, authorized, live);
|
|
1243
1335
|
}
|
|
1244
1336
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1245
1337
|
return null;
|
|
1246
1338
|
}
|
|
1247
1339
|
if (refreshed.kind !== 'beyond-horizon')
|
|
1248
1340
|
return refreshed;
|
|
1341
|
+
if (opened !== null) {
|
|
1342
|
+
owed.add(room.locator);
|
|
1343
|
+
return unconfirmed;
|
|
1344
|
+
}
|
|
1249
1345
|
}
|
|
1250
|
-
const bootstrapped = await pullBootstrap(authorized, room, local);
|
|
1251
|
-
if (!
|
|
1346
|
+
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
1347
|
+
if (!live())
|
|
1252
1348
|
return unconfirmed;
|
|
1253
1349
|
if (bootstrapped.kind !== 'staged')
|
|
1254
1350
|
return bootstrapped;
|
|
1255
|
-
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
1351
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
1256
1352
|
return null;
|
|
1257
1353
|
}
|
|
1258
1354
|
async function withhold(withheld, room, local, progress) {
|
|
@@ -1332,7 +1428,7 @@ export function composeRuntime(options) {
|
|
|
1332
1428
|
if (applied.kind !== 'staged') {
|
|
1333
1429
|
const state = pendings.get(submitted.id);
|
|
1334
1430
|
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1335
|
-
if (state !== undefined)
|
|
1431
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
1336
1432
|
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1337
1433
|
emitQueue(room.locator);
|
|
1338
1434
|
return applied;
|
|
@@ -1381,7 +1477,7 @@ export function composeRuntime(options) {
|
|
|
1381
1477
|
let result;
|
|
1382
1478
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
1383
1479
|
await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
|
|
1384
|
-
if (discoveryPaused)
|
|
1480
|
+
if (discoveryPaused || fences.has(room.locator))
|
|
1385
1481
|
return;
|
|
1386
1482
|
if (writesHeld()) {
|
|
1387
1483
|
await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
|
|
@@ -1444,7 +1540,7 @@ export function composeRuntime(options) {
|
|
|
1444
1540
|
result.kind === 'expired' ||
|
|
1445
1541
|
result.kind === 'retryable-refusal') {
|
|
1446
1542
|
if (result.kind === 'expired') {
|
|
1447
|
-
const withheld = await advance(room, local, authorized);
|
|
1543
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
1448
1544
|
if (withheld !== null)
|
|
1449
1545
|
return withhold(withheld, room, local, progress);
|
|
1450
1546
|
}
|
|
@@ -1482,7 +1578,7 @@ export function composeRuntime(options) {
|
|
|
1482
1578
|
});
|
|
1483
1579
|
if (result.kind === 'expired')
|
|
1484
1580
|
continue;
|
|
1485
|
-
const withheld = await advance(room, local, authorized);
|
|
1581
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
1486
1582
|
if (withheld !== null)
|
|
1487
1583
|
return withhold(withheld, room, local, progress);
|
|
1488
1584
|
continue;
|
|
@@ -1499,9 +1595,9 @@ export function composeRuntime(options) {
|
|
|
1499
1595
|
const awaited = new Promise((resolve) => {
|
|
1500
1596
|
release = resolve;
|
|
1501
1597
|
});
|
|
1502
|
-
answering.set(locator, awaited);
|
|
1598
|
+
answering.set(locator, { generation, answered: awaited });
|
|
1503
1599
|
return () => {
|
|
1504
|
-
if (answering.get(locator) === awaited)
|
|
1600
|
+
if (answering.get(locator)?.answered === awaited)
|
|
1505
1601
|
answering.delete(locator);
|
|
1506
1602
|
release();
|
|
1507
1603
|
};
|
|
@@ -1617,8 +1713,10 @@ export function composeRuntime(options) {
|
|
|
1617
1713
|
];
|
|
1618
1714
|
});
|
|
1619
1715
|
}
|
|
1620
|
-
async function synchronize(listed, local, progress, attempt) {
|
|
1716
|
+
async function synchronize(listed, local, progress, attempt, live) {
|
|
1621
1717
|
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
|
|
1718
|
+
if (!live())
|
|
1719
|
+
return;
|
|
1622
1720
|
const ready = listed.filter((_, index) => {
|
|
1623
1721
|
const outcome = prepared[index];
|
|
1624
1722
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
@@ -1627,9 +1725,10 @@ export function composeRuntime(options) {
|
|
|
1627
1725
|
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
1628
1726
|
handed = true;
|
|
1629
1727
|
}
|
|
1728
|
+
discovering?.release();
|
|
1630
1729
|
const loaded = await Promise.allSettled(ready
|
|
1631
1730
|
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1632
|
-
.map((room) => loadRoom(room, local, progress)));
|
|
1731
|
+
.map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
|
|
1633
1732
|
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1634
1733
|
if (failed !== undefined)
|
|
1635
1734
|
throw failed.reason;
|
|
@@ -1658,14 +1757,14 @@ export function composeRuntime(options) {
|
|
|
1658
1757
|
}
|
|
1659
1758
|
return true;
|
|
1660
1759
|
}
|
|
1661
|
-
async function caughtUp(room, local, progress) {
|
|
1760
|
+
async function caughtUp(room, local, progress, live) {
|
|
1662
1761
|
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1663
|
-
if (held === undefined || authorization === undefined)
|
|
1762
|
+
if (held === undefined || authorization === undefined || !live())
|
|
1664
1763
|
return;
|
|
1665
1764
|
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
1666
1765
|
if (!behind)
|
|
1667
1766
|
return;
|
|
1668
|
-
const withheld = await advance(room, local, authorization);
|
|
1767
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1669
1768
|
if (withheld !== null)
|
|
1670
1769
|
await withhold(withheld, room, local, progress);
|
|
1671
1770
|
}
|
|
@@ -1675,39 +1774,41 @@ export function composeRuntime(options) {
|
|
|
1675
1774
|
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1676
1775
|
});
|
|
1677
1776
|
}
|
|
1678
|
-
async function loadRoom(room, local, progress) {
|
|
1679
|
-
if (
|
|
1777
|
+
async function loadRoom(room, local, progress, live) {
|
|
1778
|
+
if (!live() || authorization === undefined)
|
|
1680
1779
|
return;
|
|
1681
1780
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1682
|
-
await reload(room, local, progress);
|
|
1781
|
+
await reload(room, local, progress, live);
|
|
1683
1782
|
return;
|
|
1684
1783
|
}
|
|
1685
|
-
const withheld = await advance(room, local, authorization);
|
|
1784
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1686
1785
|
if (withheld !== null) {
|
|
1687
1786
|
await withhold(withheld, room, local, progress);
|
|
1688
1787
|
return;
|
|
1689
1788
|
}
|
|
1690
|
-
await caughtUp(room, local, progress);
|
|
1789
|
+
await caughtUp(room, local, progress, live);
|
|
1691
1790
|
}
|
|
1692
|
-
async function reload(room, local, progress) {
|
|
1693
|
-
if (
|
|
1791
|
+
async function reload(room, local, progress, live) {
|
|
1792
|
+
if (!live() || authorization === undefined)
|
|
1694
1793
|
return;
|
|
1695
|
-
const bootstrapped = await pullBootstrap(authorization, room, local);
|
|
1696
|
-
if (!
|
|
1794
|
+
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
1795
|
+
if (!live())
|
|
1697
1796
|
return;
|
|
1698
1797
|
if (bootstrapped.kind !== 'staged') {
|
|
1699
1798
|
await withhold(bootstrapped, room, local, progress);
|
|
1700
1799
|
return;
|
|
1701
1800
|
}
|
|
1702
|
-
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
1703
|
-
await caughtUp(room, local, progress);
|
|
1801
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
1802
|
+
await caughtUp(room, local, progress, live);
|
|
1704
1803
|
}
|
|
1705
|
-
async function discoverRooms(local, progress, attempt) {
|
|
1804
|
+
async function discoverRooms(local, progress, attempt, live) {
|
|
1706
1805
|
if (closed || authorization === undefined) {
|
|
1707
1806
|
notifyReady();
|
|
1708
1807
|
return;
|
|
1709
1808
|
}
|
|
1710
1809
|
moveTo(transferring(status));
|
|
1810
|
+
discoveries += 1;
|
|
1811
|
+
const started = discoveries;
|
|
1711
1812
|
let result;
|
|
1712
1813
|
try {
|
|
1713
1814
|
result = await options.adapter.discover(authorization);
|
|
@@ -1715,7 +1816,8 @@ export function composeRuntime(options) {
|
|
|
1715
1816
|
catch {
|
|
1716
1817
|
result = { kind: 'transport-uncertainty' };
|
|
1717
1818
|
}
|
|
1718
|
-
if (
|
|
1819
|
+
if (!live()) {
|
|
1820
|
+
progress.complete = false;
|
|
1719
1821
|
notifyReady();
|
|
1720
1822
|
return;
|
|
1721
1823
|
}
|
|
@@ -1785,8 +1887,7 @@ export function composeRuntime(options) {
|
|
|
1785
1887
|
member = signedIn;
|
|
1786
1888
|
emitMember();
|
|
1787
1889
|
}
|
|
1788
|
-
await applyDiscovery(result.rooms, local);
|
|
1789
|
-
await synchronize(result.rooms, local, progress, attempt);
|
|
1890
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
1790
1891
|
}
|
|
1791
1892
|
async function ensureAuthorization() {
|
|
1792
1893
|
if (authorization !== undefined)
|
|
@@ -1800,20 +1901,50 @@ export function composeRuntime(options) {
|
|
|
1800
1901
|
throw error;
|
|
1801
1902
|
}
|
|
1802
1903
|
}
|
|
1803
|
-
function
|
|
1904
|
+
async function admittedRead(work) {
|
|
1905
|
+
if (roomReads.generation !== generation) {
|
|
1906
|
+
roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
1907
|
+
}
|
|
1908
|
+
const reads = roomReads;
|
|
1909
|
+
while (reads.free === 0)
|
|
1910
|
+
await new Promise((admit) => reads.waiting.push(admit));
|
|
1911
|
+
reads.free -= 1;
|
|
1912
|
+
try {
|
|
1913
|
+
await work();
|
|
1914
|
+
}
|
|
1915
|
+
finally {
|
|
1916
|
+
reads.free += 1;
|
|
1917
|
+
reads.waiting.shift()?.();
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
function roomTask(locator, work) {
|
|
1921
|
+
return task(roomLane(locator), (live) => admittedRead(() => work(live)));
|
|
1922
|
+
}
|
|
1923
|
+
function task(lane, work) {
|
|
1804
1924
|
if (stopping)
|
|
1805
|
-
return
|
|
1806
|
-
|
|
1807
|
-
|
|
1925
|
+
return Promise.resolve();
|
|
1926
|
+
const held = lanes.get(lane);
|
|
1927
|
+
const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
|
|
1928
|
+
const turn = current.generation;
|
|
1929
|
+
const live = () => turn === generation && continuing();
|
|
1930
|
+
const run = current.chain.then(() => work(live)).catch(() => undefined);
|
|
1931
|
+
current.chain = run;
|
|
1932
|
+
lanes.set(lane, current);
|
|
1933
|
+
working.add(run);
|
|
1934
|
+
void run.finally(() => working.delete(run));
|
|
1935
|
+
return run;
|
|
1808
1936
|
}
|
|
1809
1937
|
function sync(attempt) {
|
|
1810
|
-
return task(async () => {
|
|
1811
|
-
if (
|
|
1938
|
+
return task(OPENING_LANE, async (live) => {
|
|
1939
|
+
if (!live()) {
|
|
1812
1940
|
notifyReady();
|
|
1813
1941
|
return;
|
|
1814
1942
|
}
|
|
1815
1943
|
const progress = { complete: true, reachable: true };
|
|
1816
|
-
|
|
1944
|
+
const gate = discover();
|
|
1945
|
+
if (attempt === 'opened')
|
|
1946
|
+
opened = generation;
|
|
1947
|
+
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
1817
1948
|
if (attempt === 'opened')
|
|
1818
1949
|
streamed = progress;
|
|
1819
1950
|
complete = false;
|
|
@@ -1822,12 +1953,12 @@ export function composeRuntime(options) {
|
|
|
1822
1953
|
handed = false;
|
|
1823
1954
|
try {
|
|
1824
1955
|
const local = await openLocal();
|
|
1825
|
-
if (
|
|
1956
|
+
if (!live())
|
|
1826
1957
|
return;
|
|
1827
1958
|
await ensureAuthorization();
|
|
1828
|
-
if (
|
|
1959
|
+
if (!live())
|
|
1829
1960
|
return;
|
|
1830
|
-
await discoverRooms(local, progress, attempt);
|
|
1961
|
+
await discoverRooms(local, progress, attempt, live);
|
|
1831
1962
|
}
|
|
1832
1963
|
catch {
|
|
1833
1964
|
progress.complete = false;
|
|
@@ -1835,17 +1966,21 @@ export function composeRuntime(options) {
|
|
|
1835
1966
|
progress.reachable = false;
|
|
1836
1967
|
}
|
|
1837
1968
|
finally {
|
|
1969
|
+
gate.release();
|
|
1838
1970
|
notifyReady();
|
|
1839
1971
|
}
|
|
1972
|
+
if (!live())
|
|
1973
|
+
return;
|
|
1840
1974
|
if (attempt !== 'opened') {
|
|
1841
|
-
await finishOpening(progress);
|
|
1975
|
+
await finishOpening(progress, live);
|
|
1842
1976
|
return;
|
|
1843
1977
|
}
|
|
1844
1978
|
if (handed || closed || progress.reachable)
|
|
1845
1979
|
return;
|
|
1846
1980
|
underway = null;
|
|
1981
|
+
opened = null;
|
|
1847
1982
|
journal.record('connection reopen');
|
|
1848
|
-
await rest(progress);
|
|
1983
|
+
await rest(progress, live);
|
|
1849
1984
|
options.adapter.roomStream?.reopen();
|
|
1850
1985
|
});
|
|
1851
1986
|
}
|
|
@@ -1866,24 +2001,38 @@ export function composeRuntime(options) {
|
|
|
1866
2001
|
});
|
|
1867
2002
|
}
|
|
1868
2003
|
function applyOpening(expired) {
|
|
1869
|
-
|
|
1870
|
-
|
|
2004
|
+
const progress = underway?.generation === generation ? underway.progress : null;
|
|
2005
|
+
const cycle = progress ?? { complete: true, reachable: true };
|
|
2006
|
+
const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
|
|
2007
|
+
return task(OPENING_LANE, async (live) => {
|
|
1871
2008
|
underway = null;
|
|
1872
|
-
if (
|
|
2009
|
+
if (!live())
|
|
1873
2010
|
return;
|
|
1874
2011
|
if (progress === null) {
|
|
1875
|
-
await
|
|
2012
|
+
await replacing;
|
|
2013
|
+
await rest(cycle, live);
|
|
1876
2014
|
return;
|
|
1877
2015
|
}
|
|
2016
|
+
await roomsCarried(expired);
|
|
2017
|
+
if (!live())
|
|
2018
|
+
return;
|
|
2019
|
+
if (opened === generation)
|
|
2020
|
+
opened = null;
|
|
2021
|
+
const replaced = new Set(owed);
|
|
2022
|
+
owed.clear();
|
|
1878
2023
|
try {
|
|
1879
2024
|
const local = await openLocal();
|
|
1880
2025
|
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
1881
|
-
const reloading = held.filter((room) => expired.includes(room.locator)
|
|
1882
|
-
|
|
2026
|
+
const reloading = held.filter((room) => !expired.includes(room.locator) &&
|
|
2027
|
+
(replaced.has(room.locator) || revoked(room.locator)));
|
|
2028
|
+
const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
|
|
2029
|
+
const reloaded = [...(await reloads), ...(await replacing)];
|
|
1883
2030
|
if (progress.reachable) {
|
|
1884
|
-
for (const room of held)
|
|
1885
|
-
if (!reloading.includes(room))
|
|
2031
|
+
for (const room of held) {
|
|
2032
|
+
if (!expired.includes(room.locator) && !reloading.includes(room)) {
|
|
1886
2033
|
await confirmModels(room, local);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
1887
2036
|
}
|
|
1888
2037
|
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1889
2038
|
if (failed !== undefined)
|
|
@@ -1893,21 +2042,21 @@ export function composeRuntime(options) {
|
|
|
1893
2042
|
progress.complete = false;
|
|
1894
2043
|
progress.reachable = false;
|
|
1895
2044
|
}
|
|
1896
|
-
|
|
2045
|
+
if (live())
|
|
2046
|
+
await finishOpening(progress, live);
|
|
1897
2047
|
});
|
|
1898
2048
|
}
|
|
1899
|
-
async function
|
|
1900
|
-
const progress = { complete: true, reachable: true };
|
|
2049
|
+
async function reloadNow(replaced, progress) {
|
|
1901
2050
|
try {
|
|
1902
2051
|
const local = await openLocal();
|
|
1903
|
-
await Promise.allSettled([...discovered.values()]
|
|
1904
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
1905
|
-
.map((room) => reload(room, local, progress)));
|
|
2052
|
+
return await Promise.allSettled([...discovered.values()]
|
|
2053
|
+
.filter((room) => replicas.has(room.locator) && replaced(room))
|
|
2054
|
+
.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
1906
2055
|
}
|
|
1907
2056
|
catch {
|
|
1908
2057
|
progress.reachable = false;
|
|
2058
|
+
return [];
|
|
1909
2059
|
}
|
|
1910
|
-
await rest(progress);
|
|
1911
2060
|
}
|
|
1912
2061
|
async function confirmModels(room, local) {
|
|
1913
2062
|
for (const attached of ['collection', 'typed-selection']) {
|
|
@@ -1959,9 +2108,9 @@ export function composeRuntime(options) {
|
|
|
1959
2108
|
submitLater(locator);
|
|
1960
2109
|
}
|
|
1961
2110
|
}
|
|
1962
|
-
async function finishOpening(progress) {
|
|
2111
|
+
async function finishOpening(progress, live) {
|
|
1963
2112
|
try {
|
|
1964
|
-
if (
|
|
2113
|
+
if (!live()) {
|
|
1965
2114
|
progress.complete = false;
|
|
1966
2115
|
return;
|
|
1967
2116
|
}
|
|
@@ -1971,6 +2120,8 @@ export function composeRuntime(options) {
|
|
|
1971
2120
|
conversations.reconnected();
|
|
1972
2121
|
const reading = { complete: true, reachable: true };
|
|
1973
2122
|
await conversations.catchUp(reading);
|
|
2123
|
+
if (progress.reachable && reading.reachable)
|
|
2124
|
+
await conversations.resend(reading);
|
|
1974
2125
|
if (!reading.reachable)
|
|
1975
2126
|
progress.reachable = false;
|
|
1976
2127
|
}
|
|
@@ -1978,12 +2129,14 @@ export function composeRuntime(options) {
|
|
|
1978
2129
|
progress.complete = false;
|
|
1979
2130
|
}
|
|
1980
2131
|
finally {
|
|
1981
|
-
|
|
1982
|
-
|
|
2132
|
+
if (live()) {
|
|
2133
|
+
complete = progress.complete;
|
|
2134
|
+
await rest(progress, live);
|
|
2135
|
+
}
|
|
1983
2136
|
}
|
|
1984
2137
|
}
|
|
1985
|
-
async function rest(progress) {
|
|
1986
|
-
if (
|
|
2138
|
+
async function rest(progress, live = () => !closed) {
|
|
2139
|
+
if (!live())
|
|
1987
2140
|
return;
|
|
1988
2141
|
let lastCompleted = status.lastCompleted;
|
|
1989
2142
|
let synced = complete && stale.size === 0 && !pending();
|
|
@@ -1991,6 +2144,8 @@ export function composeRuntime(options) {
|
|
|
1991
2144
|
const completedAt = new Date().toISOString();
|
|
1992
2145
|
try {
|
|
1993
2146
|
await (await openLocal()).complete(completedAt);
|
|
2147
|
+
if (!live())
|
|
2148
|
+
return;
|
|
1994
2149
|
recorded = true;
|
|
1995
2150
|
lastCompleted = completedAt;
|
|
1996
2151
|
}
|
|
@@ -2005,11 +2160,23 @@ export function composeRuntime(options) {
|
|
|
2005
2160
|
...attention(),
|
|
2006
2161
|
});
|
|
2007
2162
|
}
|
|
2163
|
+
function discover() {
|
|
2164
|
+
let release = () => { };
|
|
2165
|
+
const done = new Promise((resolve) => {
|
|
2166
|
+
release = resolve;
|
|
2167
|
+
});
|
|
2168
|
+
discovering = { generation, done, release };
|
|
2169
|
+
return discovering;
|
|
2170
|
+
}
|
|
2008
2171
|
function applyRoom(locator, roomVersion) {
|
|
2009
2172
|
announced.set(locator, roomVersion);
|
|
2010
|
-
return task(async () => {
|
|
2011
|
-
|
|
2012
|
-
|
|
2173
|
+
return task(roomLane(locator), async (live) => {
|
|
2174
|
+
if (discovering?.generation === generation)
|
|
2175
|
+
await discovering.done;
|
|
2176
|
+
const saving = answering.get(locator);
|
|
2177
|
+
if (saving?.generation === generation)
|
|
2178
|
+
await saving.answered;
|
|
2179
|
+
if (!live() || authorization === undefined)
|
|
2013
2180
|
return;
|
|
2014
2181
|
const room = discovered.get(locator);
|
|
2015
2182
|
const held = replicas.get(locator);
|
|
@@ -2020,25 +2187,41 @@ export function composeRuntime(options) {
|
|
|
2020
2187
|
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
2021
2188
|
return;
|
|
2022
2189
|
}
|
|
2023
|
-
const during = underway;
|
|
2190
|
+
const during = underway?.generation === generation ? underway.progress : null;
|
|
2024
2191
|
const progress = during ?? { complete: true, reachable: true };
|
|
2025
2192
|
if (during === null)
|
|
2026
2193
|
moveTo(transferring(status));
|
|
2027
2194
|
try {
|
|
2028
2195
|
const local = await openLocal();
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2196
|
+
await admittedRead(async () => {
|
|
2197
|
+
if (!live() || authorization === undefined)
|
|
2198
|
+
return;
|
|
2199
|
+
const withheld = await advance(room, local, authorization, live);
|
|
2200
|
+
if (withheld !== null)
|
|
2201
|
+
await withhold(withheld, room, local, progress);
|
|
2202
|
+
});
|
|
2032
2203
|
}
|
|
2033
2204
|
catch {
|
|
2034
2205
|
stale.add(locator);
|
|
2035
2206
|
}
|
|
2036
2207
|
finally {
|
|
2037
2208
|
if (during === null)
|
|
2038
|
-
await rest(progress);
|
|
2209
|
+
await rest(progress, live);
|
|
2039
2210
|
}
|
|
2040
2211
|
});
|
|
2041
2212
|
}
|
|
2213
|
+
function roomsCarried(replacing) {
|
|
2214
|
+
const replaced = new Set(replacing.map(roomLane));
|
|
2215
|
+
return Promise.all([...lanes]
|
|
2216
|
+
.filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
|
|
2217
|
+
.map(([, held]) => held.chain));
|
|
2218
|
+
}
|
|
2219
|
+
const receipts = {
|
|
2220
|
+
heads: async () => (await openLocal()).receivedHeads(),
|
|
2221
|
+
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
2222
|
+
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
2223
|
+
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
2224
|
+
};
|
|
2042
2225
|
function observeStream() {
|
|
2043
2226
|
const stream = options.adapter.roomStream;
|
|
2044
2227
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -2048,6 +2231,7 @@ export function composeRuntime(options) {
|
|
|
2048
2231
|
return;
|
|
2049
2232
|
if (event.kind === 'opened') {
|
|
2050
2233
|
socketOpen = true;
|
|
2234
|
+
generation += 1;
|
|
2051
2235
|
void sync('opened');
|
|
2052
2236
|
}
|
|
2053
2237
|
else if (event.kind === 'closed')
|
|
@@ -2056,7 +2240,7 @@ export function composeRuntime(options) {
|
|
|
2056
2240
|
void applyOpening(event.expired);
|
|
2057
2241
|
else
|
|
2058
2242
|
void applyRoom(event.locator, event.roomVersion);
|
|
2059
|
-
});
|
|
2243
|
+
}, receipts);
|
|
2060
2244
|
}
|
|
2061
2245
|
function writesHeld() {
|
|
2062
2246
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -2094,14 +2278,15 @@ export function composeRuntime(options) {
|
|
|
2094
2278
|
});
|
|
2095
2279
|
}
|
|
2096
2280
|
function scheduleCurrency() {
|
|
2097
|
-
if (currencyQueued || closed || stopping)
|
|
2281
|
+
if (currencyQueued === generation || closed || stopping)
|
|
2098
2282
|
return;
|
|
2099
|
-
currencyQueued =
|
|
2100
|
-
void task(runCurrency);
|
|
2283
|
+
currencyQueued = generation;
|
|
2284
|
+
void task(CONVERSATION_LANE, runCurrency);
|
|
2101
2285
|
}
|
|
2102
|
-
async function runCurrency() {
|
|
2103
|
-
currencyQueued
|
|
2104
|
-
|
|
2286
|
+
async function runCurrency(live) {
|
|
2287
|
+
if (currencyQueued === generation)
|
|
2288
|
+
currencyQueued = null;
|
|
2289
|
+
if (!live())
|
|
2105
2290
|
return;
|
|
2106
2291
|
await conversations.reconcile();
|
|
2107
2292
|
if (conversations.current()) {
|
|
@@ -2116,7 +2301,7 @@ export function composeRuntime(options) {
|
|
|
2116
2301
|
catch {
|
|
2117
2302
|
progress.reachable = false;
|
|
2118
2303
|
}
|
|
2119
|
-
await rest(progress);
|
|
2304
|
+
await rest(progress, live);
|
|
2120
2305
|
}
|
|
2121
2306
|
function close() {
|
|
2122
2307
|
if (closed)
|
|
@@ -2147,7 +2332,7 @@ export function composeRuntime(options) {
|
|
|
2147
2332
|
async function stop() {
|
|
2148
2333
|
stopping = true;
|
|
2149
2334
|
stopTimer();
|
|
2150
|
-
await
|
|
2335
|
+
await Promise.all(working);
|
|
2151
2336
|
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
2152
2337
|
await mutations;
|
|
2153
2338
|
close();
|
|
@@ -2399,14 +2584,16 @@ export function composeRuntime(options) {
|
|
|
2399
2584
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2400
2585
|
return;
|
|
2401
2586
|
journal.record('connection reauthorize');
|
|
2402
|
-
await
|
|
2587
|
+
await Promise.all([...lanes.values()]
|
|
2588
|
+
.filter((held) => held.generation === generation)
|
|
2589
|
+
.map((held) => held.chain));
|
|
2403
2590
|
await mutations;
|
|
2404
2591
|
authorization = undefined;
|
|
2405
2592
|
discoveryPaused = false;
|
|
2406
2593
|
if (options.adapter.roomStream === undefined)
|
|
2407
2594
|
await sync('unstreamed');
|
|
2408
2595
|
else
|
|
2409
|
-
await task(ensureAuthorization);
|
|
2596
|
+
await task(OPENING_LANE, ensureAuthorization);
|
|
2410
2597
|
},
|
|
2411
2598
|
};
|
|
2412
2599
|
const reachable = {
|
|
@@ -2418,5 +2605,5 @@ export function composeRuntime(options) {
|
|
|
2418
2605
|
watchPlan,
|
|
2419
2606
|
search: searched,
|
|
2420
2607
|
};
|
|
2421
|
-
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2608
|
+
return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2422
2609
|
}
|