@agentshouse/mdruntime 0.1.0-c8ca66ac9e99d → 0.1.0-c8f3163f32f93
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/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/provider.d.ts.map +1 -1
- package/dist/chat/provider.js +25 -50
- 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 +332 -144
- 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/overlay.d.ts.map +1 -1
- package/dist/overlay.js +8 -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({
|
|
@@ -476,14 +497,24 @@ export function composeRuntime(options) {
|
|
|
476
497
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
477
498
|
});
|
|
478
499
|
}
|
|
479
|
-
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));
|
|
480
507
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
481
508
|
for (const locator of listedLocators)
|
|
482
509
|
scopedRefusals.delete(locator);
|
|
483
510
|
discovered.clear();
|
|
484
511
|
for (const room of listed)
|
|
485
512
|
discovered.set(room.locator, room);
|
|
486
|
-
|
|
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);
|
|
487
518
|
for (const room of listed) {
|
|
488
519
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
489
520
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -494,6 +525,7 @@ export function composeRuntime(options) {
|
|
|
494
525
|
}
|
|
495
526
|
roomsListed = true;
|
|
496
527
|
emitRooms();
|
|
528
|
+
return listed;
|
|
497
529
|
}
|
|
498
530
|
function unlistedHeld(listed) {
|
|
499
531
|
return [
|
|
@@ -505,41 +537,67 @@ export function composeRuntime(options) {
|
|
|
505
537
|
]),
|
|
506
538
|
].filter((locator) => !listed.has(locator));
|
|
507
539
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
|
|
515
|
-
room: true,
|
|
516
|
-
queue: true,
|
|
517
|
-
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
518
|
-
}));
|
|
519
|
-
for (const locator of unlisted) {
|
|
520
|
-
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
521
|
-
pendings.delete(batch.id);
|
|
522
|
-
replicas.delete(locator);
|
|
523
|
-
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
|
|
524
546
|
rooms.delete(locator);
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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);
|
|
539
578
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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));
|
|
543
601
|
}
|
|
544
602
|
function restored(key, persisted) {
|
|
545
603
|
const prior = models.get(key);
|
|
@@ -644,8 +702,15 @@ export function composeRuntime(options) {
|
|
|
644
702
|
return lastCompleted;
|
|
645
703
|
}
|
|
646
704
|
async function open() {
|
|
647
|
-
|
|
705
|
+
if (closed)
|
|
706
|
+
throw new Error('runtime closed');
|
|
707
|
+
const opened = await openRuntimeStore(isFenced);
|
|
648
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
|
+
}
|
|
649
714
|
const kept = options.shell.generation;
|
|
650
715
|
const storedGeneration = await opened.generation();
|
|
651
716
|
const loss = detectStorageLoss(storedGeneration, kept);
|
|
@@ -659,6 +724,8 @@ export function composeRuntime(options) {
|
|
|
659
724
|
emitStorageLoss();
|
|
660
725
|
}
|
|
661
726
|
const lastCompleted = await readState(opened);
|
|
727
|
+
if (interrupted.length > 0)
|
|
728
|
+
await erased(opened, interrupted);
|
|
662
729
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
663
730
|
return opened;
|
|
664
731
|
}
|
|
@@ -783,10 +850,10 @@ export function composeRuntime(options) {
|
|
|
783
850
|
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
784
851
|
};
|
|
785
852
|
}
|
|
786
|
-
function publish(local, room, confirmed, landing
|
|
853
|
+
function publish(local, room, confirmed, landing, live) {
|
|
787
854
|
const { base, stored } = landing;
|
|
788
855
|
return exclusive(async () => {
|
|
789
|
-
if (!
|
|
856
|
+
if (!live())
|
|
790
857
|
return true;
|
|
791
858
|
const prior = replicas.get(room.locator);
|
|
792
859
|
if (base !== undefined &&
|
|
@@ -814,6 +881,7 @@ export function composeRuntime(options) {
|
|
|
814
881
|
list(room, true);
|
|
815
882
|
publishChangeSet(set);
|
|
816
883
|
moveTo({ ...status, ...attention() });
|
|
884
|
+
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
817
885
|
return true;
|
|
818
886
|
});
|
|
819
887
|
}
|
|
@@ -936,6 +1004,8 @@ export function composeRuntime(options) {
|
|
|
936
1004
|
catch {
|
|
937
1005
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
938
1006
|
}
|
|
1007
|
+
if (fences.has(locator))
|
|
1008
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
939
1009
|
for (const origin of withdrawn.keys())
|
|
940
1010
|
pendings.delete(origin);
|
|
941
1011
|
for (const [id, drafts] of drafted) {
|
|
@@ -965,6 +1035,8 @@ export function composeRuntime(options) {
|
|
|
965
1035
|
return;
|
|
966
1036
|
}
|
|
967
1037
|
await local.submit(locator, identifier, submission, lost);
|
|
1038
|
+
if (fences.has(locator))
|
|
1039
|
+
return;
|
|
968
1040
|
pendings.set(identifier, { ...state, submission, lost });
|
|
969
1041
|
const replica = replicas.get(locator);
|
|
970
1042
|
if (replica !== undefined)
|
|
@@ -975,6 +1047,8 @@ export function composeRuntime(options) {
|
|
|
975
1047
|
if (!continuing())
|
|
976
1048
|
throw new Error('connection refused');
|
|
977
1049
|
await local.keepUpload(locator, identifier, upload);
|
|
1050
|
+
if (fences.has(locator))
|
|
1051
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
978
1052
|
pendings.set(identifier, { ...pendings.get(identifier), upload });
|
|
979
1053
|
if (!continuing())
|
|
980
1054
|
throw new Error('connection refused');
|
|
@@ -990,6 +1064,7 @@ export function composeRuntime(options) {
|
|
|
990
1064
|
}
|
|
991
1065
|
if (closed || authorization === undefined || refusal !== null)
|
|
992
1066
|
return UNCERTAIN;
|
|
1067
|
+
const since = fencings;
|
|
993
1068
|
const transfer = ['transfer', 'history', locator, path];
|
|
994
1069
|
if (query.revision !== undefined)
|
|
995
1070
|
transfer.push(query.revision);
|
|
@@ -1004,6 +1079,8 @@ export function composeRuntime(options) {
|
|
|
1004
1079
|
...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
|
|
1005
1080
|
});
|
|
1006
1081
|
journal.record(`outcome history ${result.kind}`);
|
|
1082
|
+
if (fencedSince(locator, since))
|
|
1083
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
1007
1084
|
if (result.kind === 'scoped-refusal') {
|
|
1008
1085
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
1009
1086
|
if (room !== undefined)
|
|
@@ -1026,6 +1103,12 @@ export function composeRuntime(options) {
|
|
|
1026
1103
|
}
|
|
1027
1104
|
return null;
|
|
1028
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
|
+
}
|
|
1029
1112
|
async function readBinary(request) {
|
|
1030
1113
|
if (closed || refusal !== null)
|
|
1031
1114
|
return UNCERTAIN;
|
|
@@ -1037,12 +1120,16 @@ export function composeRuntime(options) {
|
|
|
1037
1120
|
}
|
|
1038
1121
|
if (closed || authorization === undefined || refusal !== null)
|
|
1039
1122
|
return UNCERTAIN;
|
|
1123
|
+
const since = fencings;
|
|
1040
1124
|
journal.record(request.kind === 'path'
|
|
1041
1125
|
? `transfer binary ${request.locator} ${request.path}`
|
|
1042
1126
|
: `transfer binary ${request.fileRef}`);
|
|
1043
1127
|
try {
|
|
1044
1128
|
const result = await options.adapter.binary(authorization, request);
|
|
1045
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 });
|
|
1046
1133
|
if (result.kind === 'scoped-refusal') {
|
|
1047
1134
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
1048
1135
|
if (room !== undefined)
|
|
@@ -1166,11 +1253,11 @@ export function composeRuntime(options) {
|
|
|
1166
1253
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
1167
1254
|
!options.adapter.reached(locator, roomVersion, written));
|
|
1168
1255
|
}
|
|
1169
|
-
async function pullRefresh(authorized, confirmed) {
|
|
1170
|
-
if (!
|
|
1256
|
+
async function pullRefresh(authorized, confirmed, live) {
|
|
1257
|
+
if (!live())
|
|
1171
1258
|
return unconfirmed;
|
|
1172
1259
|
try {
|
|
1173
|
-
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);
|
|
1174
1261
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
1175
1262
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
1176
1263
|
return result;
|
|
@@ -1180,33 +1267,39 @@ export function composeRuntime(options) {
|
|
|
1180
1267
|
throw error;
|
|
1181
1268
|
}
|
|
1182
1269
|
}
|
|
1183
|
-
function staging(local) {
|
|
1270
|
+
function staging(local, live) {
|
|
1184
1271
|
return {
|
|
1185
1272
|
held: (locator) => transfers.get(locator) ?? null,
|
|
1186
1273
|
staged: (record) => local.staged(record),
|
|
1187
1274
|
stage: (record, page) => exclusive(async () => {
|
|
1188
|
-
if (!
|
|
1275
|
+
if (!live())
|
|
1189
1276
|
return false;
|
|
1190
1277
|
await local.stage(record, page);
|
|
1191
1278
|
transfers.set(record.locator, record);
|
|
1192
1279
|
return true;
|
|
1193
1280
|
}),
|
|
1194
1281
|
abandon: (locator) => exclusive(async () => {
|
|
1195
|
-
if (!
|
|
1282
|
+
if (!live())
|
|
1196
1283
|
return;
|
|
1197
1284
|
await local.abandon(locator);
|
|
1198
1285
|
transfers.delete(locator);
|
|
1199
1286
|
}),
|
|
1200
1287
|
};
|
|
1201
1288
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
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())
|
|
1204
1297
|
return unconfirmed;
|
|
1205
1298
|
try {
|
|
1206
1299
|
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
1207
1300
|
collection: modelOf(room.locator),
|
|
1208
1301
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
1209
|
-
}, (models, held) => remodel(room.locator, models, held),
|
|
1302
|
+
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
1210
1303
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
1211
1304
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1212
1305
|
return result;
|
|
@@ -1216,11 +1309,11 @@ export function composeRuntime(options) {
|
|
|
1216
1309
|
throw error;
|
|
1217
1310
|
}
|
|
1218
1311
|
}
|
|
1219
|
-
async function advance(room, local, authorized) {
|
|
1312
|
+
async function advance(room, local, authorized, live) {
|
|
1220
1313
|
const held = replicas.get(room.locator);
|
|
1221
1314
|
if (held !== undefined) {
|
|
1222
|
-
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
1223
|
-
if (!
|
|
1315
|
+
const refreshed = await pullRefresh(authorized, held.confirmed, live);
|
|
1316
|
+
if (!live())
|
|
1224
1317
|
return unconfirmed;
|
|
1225
1318
|
if (refreshed.kind === 'still-current') {
|
|
1226
1319
|
stale.delete(room.locator);
|
|
@@ -1234,24 +1327,28 @@ export function composeRuntime(options) {
|
|
|
1234
1327
|
return null;
|
|
1235
1328
|
}
|
|
1236
1329
|
if (refreshed.kind === 'advanced') {
|
|
1237
|
-
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
|
|
1330
|
+
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
|
|
1238
1331
|
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1239
1332
|
const reached = now !== undefined &&
|
|
1240
1333
|
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
1241
|
-
return reached ? null : advance(room, local, authorized);
|
|
1334
|
+
return reached ? null : advance(room, local, authorized, live);
|
|
1242
1335
|
}
|
|
1243
1336
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1244
1337
|
return null;
|
|
1245
1338
|
}
|
|
1246
1339
|
if (refreshed.kind !== 'beyond-horizon')
|
|
1247
1340
|
return refreshed;
|
|
1341
|
+
if (opened !== null) {
|
|
1342
|
+
owed.add(room.locator);
|
|
1343
|
+
return unconfirmed;
|
|
1344
|
+
}
|
|
1248
1345
|
}
|
|
1249
|
-
const bootstrapped = await pullBootstrap(authorized, room, local);
|
|
1250
|
-
if (!
|
|
1346
|
+
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
1347
|
+
if (!live())
|
|
1251
1348
|
return unconfirmed;
|
|
1252
1349
|
if (bootstrapped.kind !== 'staged')
|
|
1253
1350
|
return bootstrapped;
|
|
1254
|
-
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
1351
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
1255
1352
|
return null;
|
|
1256
1353
|
}
|
|
1257
1354
|
async function withhold(withheld, room, local, progress) {
|
|
@@ -1331,7 +1428,7 @@ export function composeRuntime(options) {
|
|
|
1331
1428
|
if (applied.kind !== 'staged') {
|
|
1332
1429
|
const state = pendings.get(submitted.id);
|
|
1333
1430
|
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1334
|
-
if (state !== undefined)
|
|
1431
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
1335
1432
|
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1336
1433
|
emitQueue(room.locator);
|
|
1337
1434
|
return applied;
|
|
@@ -1380,7 +1477,7 @@ export function composeRuntime(options) {
|
|
|
1380
1477
|
let result;
|
|
1381
1478
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
1382
1479
|
await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
|
|
1383
|
-
if (discoveryPaused)
|
|
1480
|
+
if (discoveryPaused || fences.has(room.locator))
|
|
1384
1481
|
return;
|
|
1385
1482
|
if (writesHeld()) {
|
|
1386
1483
|
await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
|
|
@@ -1443,7 +1540,7 @@ export function composeRuntime(options) {
|
|
|
1443
1540
|
result.kind === 'expired' ||
|
|
1444
1541
|
result.kind === 'retryable-refusal') {
|
|
1445
1542
|
if (result.kind === 'expired') {
|
|
1446
|
-
const withheld = await advance(room, local, authorized);
|
|
1543
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
1447
1544
|
if (withheld !== null)
|
|
1448
1545
|
return withhold(withheld, room, local, progress);
|
|
1449
1546
|
}
|
|
@@ -1481,7 +1578,7 @@ export function composeRuntime(options) {
|
|
|
1481
1578
|
});
|
|
1482
1579
|
if (result.kind === 'expired')
|
|
1483
1580
|
continue;
|
|
1484
|
-
const withheld = await advance(room, local, authorized);
|
|
1581
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
1485
1582
|
if (withheld !== null)
|
|
1486
1583
|
return withhold(withheld, room, local, progress);
|
|
1487
1584
|
continue;
|
|
@@ -1498,9 +1595,9 @@ export function composeRuntime(options) {
|
|
|
1498
1595
|
const awaited = new Promise((resolve) => {
|
|
1499
1596
|
release = resolve;
|
|
1500
1597
|
});
|
|
1501
|
-
answering.set(locator, awaited);
|
|
1598
|
+
answering.set(locator, { generation, answered: awaited });
|
|
1502
1599
|
return () => {
|
|
1503
|
-
if (answering.get(locator) === awaited)
|
|
1600
|
+
if (answering.get(locator)?.answered === awaited)
|
|
1504
1601
|
answering.delete(locator);
|
|
1505
1602
|
release();
|
|
1506
1603
|
};
|
|
@@ -1616,8 +1713,10 @@ export function composeRuntime(options) {
|
|
|
1616
1713
|
];
|
|
1617
1714
|
});
|
|
1618
1715
|
}
|
|
1619
|
-
async function synchronize(listed, local, progress, attempt) {
|
|
1716
|
+
async function synchronize(listed, local, progress, attempt, live) {
|
|
1620
1717
|
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
|
|
1718
|
+
if (!live())
|
|
1719
|
+
return;
|
|
1621
1720
|
const ready = listed.filter((_, index) => {
|
|
1622
1721
|
const outcome = prepared[index];
|
|
1623
1722
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
@@ -1626,9 +1725,10 @@ export function composeRuntime(options) {
|
|
|
1626
1725
|
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
1627
1726
|
handed = true;
|
|
1628
1727
|
}
|
|
1728
|
+
discovering?.release();
|
|
1629
1729
|
const loaded = await Promise.allSettled(ready
|
|
1630
1730
|
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1631
|
-
.map((room) => loadRoom(room, local, progress)));
|
|
1731
|
+
.map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
|
|
1632
1732
|
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1633
1733
|
if (failed !== undefined)
|
|
1634
1734
|
throw failed.reason;
|
|
@@ -1657,14 +1757,14 @@ export function composeRuntime(options) {
|
|
|
1657
1757
|
}
|
|
1658
1758
|
return true;
|
|
1659
1759
|
}
|
|
1660
|
-
async function caughtUp(room, local, progress) {
|
|
1760
|
+
async function caughtUp(room, local, progress, live) {
|
|
1661
1761
|
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1662
|
-
if (held === undefined || authorization === undefined)
|
|
1762
|
+
if (held === undefined || authorization === undefined || !live())
|
|
1663
1763
|
return;
|
|
1664
1764
|
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
1665
1765
|
if (!behind)
|
|
1666
1766
|
return;
|
|
1667
|
-
const withheld = await advance(room, local, authorization);
|
|
1767
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1668
1768
|
if (withheld !== null)
|
|
1669
1769
|
await withhold(withheld, room, local, progress);
|
|
1670
1770
|
}
|
|
@@ -1674,39 +1774,41 @@ export function composeRuntime(options) {
|
|
|
1674
1774
|
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1675
1775
|
});
|
|
1676
1776
|
}
|
|
1677
|
-
async function loadRoom(room, local, progress) {
|
|
1678
|
-
if (
|
|
1777
|
+
async function loadRoom(room, local, progress, live) {
|
|
1778
|
+
if (!live() || authorization === undefined)
|
|
1679
1779
|
return;
|
|
1680
1780
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1681
|
-
await reload(room, local, progress);
|
|
1781
|
+
await reload(room, local, progress, live);
|
|
1682
1782
|
return;
|
|
1683
1783
|
}
|
|
1684
|
-
const withheld = await advance(room, local, authorization);
|
|
1784
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1685
1785
|
if (withheld !== null) {
|
|
1686
1786
|
await withhold(withheld, room, local, progress);
|
|
1687
1787
|
return;
|
|
1688
1788
|
}
|
|
1689
|
-
await caughtUp(room, local, progress);
|
|
1789
|
+
await caughtUp(room, local, progress, live);
|
|
1690
1790
|
}
|
|
1691
|
-
async function reload(room, local, progress) {
|
|
1692
|
-
if (
|
|
1791
|
+
async function reload(room, local, progress, live) {
|
|
1792
|
+
if (!live() || authorization === undefined)
|
|
1693
1793
|
return;
|
|
1694
|
-
const bootstrapped = await pullBootstrap(authorization, room, local);
|
|
1695
|
-
if (!
|
|
1794
|
+
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
1795
|
+
if (!live())
|
|
1696
1796
|
return;
|
|
1697
1797
|
if (bootstrapped.kind !== 'staged') {
|
|
1698
1798
|
await withhold(bootstrapped, room, local, progress);
|
|
1699
1799
|
return;
|
|
1700
1800
|
}
|
|
1701
|
-
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
1702
|
-
await caughtUp(room, local, progress);
|
|
1801
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
1802
|
+
await caughtUp(room, local, progress, live);
|
|
1703
1803
|
}
|
|
1704
|
-
async function discoverRooms(local, progress, attempt) {
|
|
1804
|
+
async function discoverRooms(local, progress, attempt, live) {
|
|
1705
1805
|
if (closed || authorization === undefined) {
|
|
1706
1806
|
notifyReady();
|
|
1707
1807
|
return;
|
|
1708
1808
|
}
|
|
1709
1809
|
moveTo(transferring(status));
|
|
1810
|
+
discoveries += 1;
|
|
1811
|
+
const started = discoveries;
|
|
1710
1812
|
let result;
|
|
1711
1813
|
try {
|
|
1712
1814
|
result = await options.adapter.discover(authorization);
|
|
@@ -1714,7 +1816,8 @@ export function composeRuntime(options) {
|
|
|
1714
1816
|
catch {
|
|
1715
1817
|
result = { kind: 'transport-uncertainty' };
|
|
1716
1818
|
}
|
|
1717
|
-
if (
|
|
1819
|
+
if (!live()) {
|
|
1820
|
+
progress.complete = false;
|
|
1718
1821
|
notifyReady();
|
|
1719
1822
|
return;
|
|
1720
1823
|
}
|
|
@@ -1784,8 +1887,7 @@ export function composeRuntime(options) {
|
|
|
1784
1887
|
member = signedIn;
|
|
1785
1888
|
emitMember();
|
|
1786
1889
|
}
|
|
1787
|
-
await applyDiscovery(result.rooms, local);
|
|
1788
|
-
await synchronize(result.rooms, local, progress, attempt);
|
|
1890
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
1789
1891
|
}
|
|
1790
1892
|
async function ensureAuthorization() {
|
|
1791
1893
|
if (authorization !== undefined)
|
|
@@ -1799,20 +1901,50 @@ export function composeRuntime(options) {
|
|
|
1799
1901
|
throw error;
|
|
1800
1902
|
}
|
|
1801
1903
|
}
|
|
1802
|
-
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) {
|
|
1803
1924
|
if (stopping)
|
|
1804
|
-
return
|
|
1805
|
-
|
|
1806
|
-
|
|
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;
|
|
1807
1936
|
}
|
|
1808
1937
|
function sync(attempt) {
|
|
1809
|
-
return task(async () => {
|
|
1810
|
-
if (
|
|
1938
|
+
return task(OPENING_LANE, async (live) => {
|
|
1939
|
+
if (!live()) {
|
|
1811
1940
|
notifyReady();
|
|
1812
1941
|
return;
|
|
1813
1942
|
}
|
|
1814
1943
|
const progress = { complete: true, reachable: true };
|
|
1815
|
-
|
|
1944
|
+
const gate = discover();
|
|
1945
|
+
if (attempt === 'opened')
|
|
1946
|
+
opened = generation;
|
|
1947
|
+
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
1816
1948
|
if (attempt === 'opened')
|
|
1817
1949
|
streamed = progress;
|
|
1818
1950
|
complete = false;
|
|
@@ -1821,12 +1953,12 @@ export function composeRuntime(options) {
|
|
|
1821
1953
|
handed = false;
|
|
1822
1954
|
try {
|
|
1823
1955
|
const local = await openLocal();
|
|
1824
|
-
if (
|
|
1956
|
+
if (!live())
|
|
1825
1957
|
return;
|
|
1826
1958
|
await ensureAuthorization();
|
|
1827
|
-
if (
|
|
1959
|
+
if (!live())
|
|
1828
1960
|
return;
|
|
1829
|
-
await discoverRooms(local, progress, attempt);
|
|
1961
|
+
await discoverRooms(local, progress, attempt, live);
|
|
1830
1962
|
}
|
|
1831
1963
|
catch {
|
|
1832
1964
|
progress.complete = false;
|
|
@@ -1834,17 +1966,21 @@ export function composeRuntime(options) {
|
|
|
1834
1966
|
progress.reachable = false;
|
|
1835
1967
|
}
|
|
1836
1968
|
finally {
|
|
1969
|
+
gate.release();
|
|
1837
1970
|
notifyReady();
|
|
1838
1971
|
}
|
|
1972
|
+
if (!live())
|
|
1973
|
+
return;
|
|
1839
1974
|
if (attempt !== 'opened') {
|
|
1840
|
-
await finishOpening(progress);
|
|
1975
|
+
await finishOpening(progress, live);
|
|
1841
1976
|
return;
|
|
1842
1977
|
}
|
|
1843
1978
|
if (handed || closed || progress.reachable)
|
|
1844
1979
|
return;
|
|
1845
1980
|
underway = null;
|
|
1981
|
+
opened = null;
|
|
1846
1982
|
journal.record('connection reopen');
|
|
1847
|
-
await rest(progress);
|
|
1983
|
+
await rest(progress, live);
|
|
1848
1984
|
options.adapter.roomStream?.reopen();
|
|
1849
1985
|
});
|
|
1850
1986
|
}
|
|
@@ -1865,24 +2001,38 @@ export function composeRuntime(options) {
|
|
|
1865
2001
|
});
|
|
1866
2002
|
}
|
|
1867
2003
|
function applyOpening(expired) {
|
|
1868
|
-
|
|
1869
|
-
|
|
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) => {
|
|
1870
2008
|
underway = null;
|
|
1871
|
-
if (
|
|
2009
|
+
if (!live())
|
|
1872
2010
|
return;
|
|
1873
2011
|
if (progress === null) {
|
|
1874
|
-
await
|
|
2012
|
+
await replacing;
|
|
2013
|
+
await rest(cycle, live);
|
|
1875
2014
|
return;
|
|
1876
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();
|
|
1877
2023
|
try {
|
|
1878
2024
|
const local = await openLocal();
|
|
1879
2025
|
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
1880
|
-
const reloading = held.filter((room) => expired.includes(room.locator)
|
|
1881
|
-
|
|
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)];
|
|
1882
2030
|
if (progress.reachable) {
|
|
1883
|
-
for (const room of held)
|
|
1884
|
-
if (!reloading.includes(room))
|
|
2031
|
+
for (const room of held) {
|
|
2032
|
+
if (!expired.includes(room.locator) && !reloading.includes(room)) {
|
|
1885
2033
|
await confirmModels(room, local);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
1886
2036
|
}
|
|
1887
2037
|
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1888
2038
|
if (failed !== undefined)
|
|
@@ -1892,21 +2042,21 @@ export function composeRuntime(options) {
|
|
|
1892
2042
|
progress.complete = false;
|
|
1893
2043
|
progress.reachable = false;
|
|
1894
2044
|
}
|
|
1895
|
-
|
|
2045
|
+
if (live())
|
|
2046
|
+
await finishOpening(progress, live);
|
|
1896
2047
|
});
|
|
1897
2048
|
}
|
|
1898
|
-
async function
|
|
1899
|
-
const progress = { complete: true, reachable: true };
|
|
2049
|
+
async function reloadNow(replaced, progress) {
|
|
1900
2050
|
try {
|
|
1901
2051
|
const local = await openLocal();
|
|
1902
|
-
await Promise.allSettled([...discovered.values()]
|
|
1903
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
1904
|
-
.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))));
|
|
1905
2055
|
}
|
|
1906
2056
|
catch {
|
|
1907
2057
|
progress.reachable = false;
|
|
2058
|
+
return [];
|
|
1908
2059
|
}
|
|
1909
|
-
await rest(progress);
|
|
1910
2060
|
}
|
|
1911
2061
|
async function confirmModels(room, local) {
|
|
1912
2062
|
for (const attached of ['collection', 'typed-selection']) {
|
|
@@ -1958,9 +2108,9 @@ export function composeRuntime(options) {
|
|
|
1958
2108
|
submitLater(locator);
|
|
1959
2109
|
}
|
|
1960
2110
|
}
|
|
1961
|
-
async function finishOpening(progress) {
|
|
2111
|
+
async function finishOpening(progress, live) {
|
|
1962
2112
|
try {
|
|
1963
|
-
if (
|
|
2113
|
+
if (!live()) {
|
|
1964
2114
|
progress.complete = false;
|
|
1965
2115
|
return;
|
|
1966
2116
|
}
|
|
@@ -1970,6 +2120,8 @@ export function composeRuntime(options) {
|
|
|
1970
2120
|
conversations.reconnected();
|
|
1971
2121
|
const reading = { complete: true, reachable: true };
|
|
1972
2122
|
await conversations.catchUp(reading);
|
|
2123
|
+
if (progress.reachable && reading.reachable)
|
|
2124
|
+
await conversations.resend(reading);
|
|
1973
2125
|
if (!reading.reachable)
|
|
1974
2126
|
progress.reachable = false;
|
|
1975
2127
|
}
|
|
@@ -1977,12 +2129,14 @@ export function composeRuntime(options) {
|
|
|
1977
2129
|
progress.complete = false;
|
|
1978
2130
|
}
|
|
1979
2131
|
finally {
|
|
1980
|
-
|
|
1981
|
-
|
|
2132
|
+
if (live()) {
|
|
2133
|
+
complete = progress.complete;
|
|
2134
|
+
await rest(progress, live);
|
|
2135
|
+
}
|
|
1982
2136
|
}
|
|
1983
2137
|
}
|
|
1984
|
-
async function rest(progress) {
|
|
1985
|
-
if (
|
|
2138
|
+
async function rest(progress, live = () => !closed) {
|
|
2139
|
+
if (!live())
|
|
1986
2140
|
return;
|
|
1987
2141
|
let lastCompleted = status.lastCompleted;
|
|
1988
2142
|
let synced = complete && stale.size === 0 && !pending();
|
|
@@ -1990,6 +2144,8 @@ export function composeRuntime(options) {
|
|
|
1990
2144
|
const completedAt = new Date().toISOString();
|
|
1991
2145
|
try {
|
|
1992
2146
|
await (await openLocal()).complete(completedAt);
|
|
2147
|
+
if (!live())
|
|
2148
|
+
return;
|
|
1993
2149
|
recorded = true;
|
|
1994
2150
|
lastCompleted = completedAt;
|
|
1995
2151
|
}
|
|
@@ -2004,11 +2160,23 @@ export function composeRuntime(options) {
|
|
|
2004
2160
|
...attention(),
|
|
2005
2161
|
});
|
|
2006
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
|
+
}
|
|
2007
2171
|
function applyRoom(locator, roomVersion) {
|
|
2008
2172
|
announced.set(locator, roomVersion);
|
|
2009
|
-
return task(async () => {
|
|
2010
|
-
|
|
2011
|
-
|
|
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)
|
|
2012
2180
|
return;
|
|
2013
2181
|
const room = discovered.get(locator);
|
|
2014
2182
|
const held = replicas.get(locator);
|
|
@@ -2019,25 +2187,41 @@ export function composeRuntime(options) {
|
|
|
2019
2187
|
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
2020
2188
|
return;
|
|
2021
2189
|
}
|
|
2022
|
-
const during = underway;
|
|
2190
|
+
const during = underway?.generation === generation ? underway.progress : null;
|
|
2023
2191
|
const progress = during ?? { complete: true, reachable: true };
|
|
2024
2192
|
if (during === null)
|
|
2025
2193
|
moveTo(transferring(status));
|
|
2026
2194
|
try {
|
|
2027
2195
|
const local = await openLocal();
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
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
|
+
});
|
|
2031
2203
|
}
|
|
2032
2204
|
catch {
|
|
2033
2205
|
stale.add(locator);
|
|
2034
2206
|
}
|
|
2035
2207
|
finally {
|
|
2036
2208
|
if (during === null)
|
|
2037
|
-
await rest(progress);
|
|
2209
|
+
await rest(progress, live);
|
|
2038
2210
|
}
|
|
2039
2211
|
});
|
|
2040
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
|
+
};
|
|
2041
2225
|
function observeStream() {
|
|
2042
2226
|
const stream = options.adapter.roomStream;
|
|
2043
2227
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -2047,6 +2231,7 @@ export function composeRuntime(options) {
|
|
|
2047
2231
|
return;
|
|
2048
2232
|
if (event.kind === 'opened') {
|
|
2049
2233
|
socketOpen = true;
|
|
2234
|
+
generation += 1;
|
|
2050
2235
|
void sync('opened');
|
|
2051
2236
|
}
|
|
2052
2237
|
else if (event.kind === 'closed')
|
|
@@ -2055,7 +2240,7 @@ export function composeRuntime(options) {
|
|
|
2055
2240
|
void applyOpening(event.expired);
|
|
2056
2241
|
else
|
|
2057
2242
|
void applyRoom(event.locator, event.roomVersion);
|
|
2058
|
-
});
|
|
2243
|
+
}, receipts);
|
|
2059
2244
|
}
|
|
2060
2245
|
function writesHeld() {
|
|
2061
2246
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -2093,14 +2278,15 @@ export function composeRuntime(options) {
|
|
|
2093
2278
|
});
|
|
2094
2279
|
}
|
|
2095
2280
|
function scheduleCurrency() {
|
|
2096
|
-
if (currencyQueued || closed || stopping)
|
|
2281
|
+
if (currencyQueued === generation || closed || stopping)
|
|
2097
2282
|
return;
|
|
2098
|
-
currencyQueued =
|
|
2099
|
-
void task(runCurrency);
|
|
2283
|
+
currencyQueued = generation;
|
|
2284
|
+
void task(CONVERSATION_LANE, runCurrency);
|
|
2100
2285
|
}
|
|
2101
|
-
async function runCurrency() {
|
|
2102
|
-
currencyQueued
|
|
2103
|
-
|
|
2286
|
+
async function runCurrency(live) {
|
|
2287
|
+
if (currencyQueued === generation)
|
|
2288
|
+
currencyQueued = null;
|
|
2289
|
+
if (!live())
|
|
2104
2290
|
return;
|
|
2105
2291
|
await conversations.reconcile();
|
|
2106
2292
|
if (conversations.current()) {
|
|
@@ -2115,7 +2301,7 @@ export function composeRuntime(options) {
|
|
|
2115
2301
|
catch {
|
|
2116
2302
|
progress.reachable = false;
|
|
2117
2303
|
}
|
|
2118
|
-
await rest(progress);
|
|
2304
|
+
await rest(progress, live);
|
|
2119
2305
|
}
|
|
2120
2306
|
function close() {
|
|
2121
2307
|
if (closed)
|
|
@@ -2146,7 +2332,7 @@ export function composeRuntime(options) {
|
|
|
2146
2332
|
async function stop() {
|
|
2147
2333
|
stopping = true;
|
|
2148
2334
|
stopTimer();
|
|
2149
|
-
await
|
|
2335
|
+
await Promise.all(working);
|
|
2150
2336
|
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
2151
2337
|
await mutations;
|
|
2152
2338
|
close();
|
|
@@ -2398,14 +2584,16 @@ export function composeRuntime(options) {
|
|
|
2398
2584
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2399
2585
|
return;
|
|
2400
2586
|
journal.record('connection reauthorize');
|
|
2401
|
-
await
|
|
2587
|
+
await Promise.all([...lanes.values()]
|
|
2588
|
+
.filter((held) => held.generation === generation)
|
|
2589
|
+
.map((held) => held.chain));
|
|
2402
2590
|
await mutations;
|
|
2403
2591
|
authorization = undefined;
|
|
2404
2592
|
discoveryPaused = false;
|
|
2405
2593
|
if (options.adapter.roomStream === undefined)
|
|
2406
2594
|
await sync('unstreamed');
|
|
2407
2595
|
else
|
|
2408
|
-
await task(ensureAuthorization);
|
|
2596
|
+
await task(OPENING_LANE, ensureAuthorization);
|
|
2409
2597
|
},
|
|
2410
2598
|
};
|
|
2411
2599
|
const reachable = {
|
|
@@ -2417,5 +2605,5 @@ export function composeRuntime(options) {
|
|
|
2417
2605
|
watchPlan,
|
|
2418
2606
|
search: searched,
|
|
2419
2607
|
};
|
|
2420
|
-
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2608
|
+
return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2421
2609
|
}
|