@agentshouse/mdruntime 0.1.0-cd50a4607cb7d → 0.1.0-cd76160689224
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 +118 -115
- package/dist/acquisition.d.ts +14 -0
- package/dist/acquisition.d.ts.map +1 -0
- package/dist/acquisition.js +24 -0
- package/dist/adapter.d.ts +25 -7
- package/dist/adapter.d.ts.map +1 -1
- package/dist/bootstrap.d.ts +2 -1
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +4 -0
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +2 -1
- package/dist/chat/kit.d.ts +2 -1
- package/dist/chat/kit.d.ts.map +1 -1
- package/dist/chat/kit.js +3 -3
- package/dist/chat/list.d.ts +2 -1
- package/dist/chat/list.d.ts.map +1 -1
- package/dist/chat/list.js +4 -3
- package/dist/chat/messages.d.ts +1 -0
- package/dist/chat/messages.d.ts.map +1 -1
- package/dist/chat/messages.js +4 -0
- package/dist/chat/provider.d.ts +1 -1
- package/dist/chat/provider.d.ts.map +1 -1
- package/dist/chat/provider.js +26 -17
- package/dist/chat/ready.d.ts +1 -0
- package/dist/chat/ready.d.ts.map +1 -1
- package/dist/chat/ready.js +2 -2
- package/dist/client.d.ts +7 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +12 -2
- package/dist/connection.d.ts +18 -3
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +696 -79
- package/dist/conversation.d.ts +3 -0
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +115 -17
- package/dist/durability.d.ts +7 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/entry.d.ts.map +1 -1
- package/dist/entry.js +1 -1
- package/dist/host.d.ts +15 -2
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +83 -8
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/memory.d.ts +8 -2
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +26 -6
- package/dist/model.d.ts +3 -2
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +1 -1
- package/dist/outcome.d.ts +8 -2
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +3 -0
- package/dist/overlay.d.ts +1 -0
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +27 -3
- package/dist/query.d.ts +8 -1
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +8 -2
- package/dist/reach.d.ts +6 -3
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +17 -2
- package/dist/react.d.ts +4 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +6 -4
- package/dist/read-side.d.ts.map +1 -1
- package/dist/read-side.js +3 -0
- package/dist/readiness.d.ts +53 -0
- package/dist/readiness.d.ts.map +1 -0
- package/dist/readiness.js +33 -0
- package/dist/reads.d.ts +11 -0
- package/dist/reads.d.ts.map +1 -0
- package/dist/reads.js +98 -0
- package/dist/refresh.d.ts +4 -4
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +6 -0
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +11 -2
- package/dist/room-draft.d.ts +13 -1
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +2 -2
- package/dist/rows.d.ts +2 -1
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +3 -1
- package/dist/search.d.ts +14 -6
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +33 -12
- package/dist/seed.d.ts +16 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +61 -0
- package/dist/store.d.ts +19 -3
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +59 -14
- package/dist/wire.d.ts +140 -32
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +49 -31
- package/package.json +2 -2
package/dist/connection.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { attachedModel } from './attached-model.js';
|
|
2
2
|
import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
|
|
3
|
-
import { observing } from './observation.js';
|
|
4
|
-
import { filterOf, identityOf, planOf, } from './query.js';
|
|
3
|
+
import { observing, whole } from './observation.js';
|
|
4
|
+
import { filterOf, identityOf, planOf, queryKey, } from './query.js';
|
|
5
5
|
import { createReadSide } from './read-side.js';
|
|
6
|
+
import { seededResult } from './seed.js';
|
|
6
7
|
import { markdownPath, roomOfKey } from './rows.js';
|
|
7
8
|
import { createSearchIndex, searchPlanOf, } from './search.js';
|
|
8
9
|
import { bootstrapRoom } from './bootstrap.js';
|
|
@@ -13,14 +14,17 @@ import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
|
|
|
13
14
|
import { runtimeError } from './failure.js';
|
|
14
15
|
import { createLog } from './log.js';
|
|
15
16
|
import { binaryRecord } from './binary.js';
|
|
16
|
-
import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
|
|
17
|
+
import { absentBinary, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
|
|
17
18
|
import { createRecovery } from './recovery.js';
|
|
19
|
+
import { doorReads } from './reads.js';
|
|
18
20
|
import { refreshRoom } from './refresh.js';
|
|
19
21
|
import { buildBatch, joined, limitedIn, requeued } from './save.js';
|
|
20
22
|
import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
|
|
21
23
|
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
22
24
|
import { same, saving, STARTING, transferring, } from './status.js';
|
|
23
25
|
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
26
|
+
import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
|
|
27
|
+
import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
|
|
24
28
|
import { FencedMap } from './fence.js';
|
|
25
29
|
import { tally } from './shared.js';
|
|
26
30
|
import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
|
|
@@ -29,6 +33,14 @@ import { beneath, normalize } from './paths.js';
|
|
|
29
33
|
import { openRuntimeStore, } from './store.js';
|
|
30
34
|
import { unconfirmed } from './transfer.js';
|
|
31
35
|
import { governs, holds, narrowed, planReach, } from './reach.js';
|
|
36
|
+
function roomsAlone(plan) {
|
|
37
|
+
return (plan.reach === null &&
|
|
38
|
+
plan.root.types === null &&
|
|
39
|
+
plan.root.where === null &&
|
|
40
|
+
plan.root.pathPrefix === null &&
|
|
41
|
+
plan.hop === null &&
|
|
42
|
+
plan.backlinks === null);
|
|
43
|
+
}
|
|
32
44
|
function within(root, path) {
|
|
33
45
|
return root === '' || beneath(root, path);
|
|
34
46
|
}
|
|
@@ -46,6 +58,10 @@ const OPENING_LANE = 'opening';
|
|
|
46
58
|
const CONVERSATION_LANE = 'conversations';
|
|
47
59
|
const ROOM_READS = 256;
|
|
48
60
|
const ROOM_ATTEMPTS = 16;
|
|
61
|
+
function operationOf(input) {
|
|
62
|
+
const operation = input?.operation_id;
|
|
63
|
+
return typeof operation === 'string' ? operation : null;
|
|
64
|
+
}
|
|
49
65
|
const outside = new Map();
|
|
50
66
|
function outsideOf(roots) {
|
|
51
67
|
const key = JSON.stringify(roots);
|
|
@@ -85,6 +101,8 @@ export function composeRuntime(options) {
|
|
|
85
101
|
const replicas = new FencedMap(isFenced);
|
|
86
102
|
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
87
103
|
const pendings = new Map();
|
|
104
|
+
const seeds = new Map();
|
|
105
|
+
const seedWatchers = new Map();
|
|
88
106
|
const roomDrafts = new FencedMap(isFenced);
|
|
89
107
|
const presentations = new Map();
|
|
90
108
|
let limits = [];
|
|
@@ -97,6 +115,7 @@ export function composeRuntime(options) {
|
|
|
97
115
|
const liveModels = new FencedMap(isModelFenced);
|
|
98
116
|
const invalidModels = new Set();
|
|
99
117
|
const discovered = new Map();
|
|
118
|
+
const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
|
|
100
119
|
let roomsListed = false;
|
|
101
120
|
const scopedRefusals = new Map();
|
|
102
121
|
const sourceRefusals = new Map();
|
|
@@ -133,6 +152,7 @@ export function composeRuntime(options) {
|
|
|
133
152
|
let handed = false;
|
|
134
153
|
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
135
154
|
const repairing = new Set();
|
|
155
|
+
const calling = new Set();
|
|
136
156
|
const repairable = new Map();
|
|
137
157
|
let outcomeRetention = null;
|
|
138
158
|
const answering = new Map();
|
|
@@ -166,8 +186,18 @@ export function composeRuntime(options) {
|
|
|
166
186
|
const memberListeners = new Set();
|
|
167
187
|
const prepared = new Set();
|
|
168
188
|
const projecting = new Set();
|
|
169
|
-
const
|
|
189
|
+
const unpreparedDemand = new Set();
|
|
190
|
+
const unindexable = new Set();
|
|
170
191
|
let preparing = false;
|
|
192
|
+
let demand = WHOLE_DEMAND;
|
|
193
|
+
let narrowing = null;
|
|
194
|
+
let targets = {};
|
|
195
|
+
let persistence = options.shell.persistence;
|
|
196
|
+
let capacity = null;
|
|
197
|
+
const paused = new Set();
|
|
198
|
+
let rechecking = null;
|
|
199
|
+
const readinessWatches = new Set();
|
|
200
|
+
let readinessQueued = false;
|
|
171
201
|
const catching = new Set();
|
|
172
202
|
const transfers = new FencedMap(isFenced);
|
|
173
203
|
const conversations = createConversationStore({
|
|
@@ -325,15 +355,18 @@ export function composeRuntime(options) {
|
|
|
325
355
|
}
|
|
326
356
|
function emitRooms() {
|
|
327
357
|
readSide.recover();
|
|
358
|
+
queueReadiness();
|
|
328
359
|
const value = listed();
|
|
329
360
|
for (const listener of roomListeners)
|
|
330
361
|
listener(value);
|
|
331
362
|
}
|
|
332
363
|
function emitRefusal() {
|
|
364
|
+
queueReadiness();
|
|
333
365
|
for (const listener of refusalListeners)
|
|
334
366
|
listener(refusal);
|
|
335
367
|
}
|
|
336
368
|
function emitStorageLoss() {
|
|
369
|
+
queueReadiness();
|
|
337
370
|
for (const listener of storageLossListeners)
|
|
338
371
|
listener(storageLoss);
|
|
339
372
|
}
|
|
@@ -350,7 +383,8 @@ export function composeRuntime(options) {
|
|
|
350
383
|
function show() {
|
|
351
384
|
if (closed)
|
|
352
385
|
return;
|
|
353
|
-
|
|
386
|
+
queueReadiness();
|
|
387
|
+
const next = status.state === 'synced' && demand.conversations && !conversations.current()
|
|
354
388
|
? { ...status, state: 'syncing' }
|
|
355
389
|
: status;
|
|
356
390
|
if (same(shown, next))
|
|
@@ -427,13 +461,19 @@ export function composeRuntime(options) {
|
|
|
427
461
|
}
|
|
428
462
|
conversations.changed(mergeChangeSets(entered));
|
|
429
463
|
readSide.recover();
|
|
464
|
+
queueReadiness();
|
|
430
465
|
}
|
|
431
466
|
function unprepared() {
|
|
432
|
-
return [...
|
|
467
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
|
|
468
|
+
}
|
|
469
|
+
function unbuilt(locator, error) {
|
|
470
|
+
unindexable.add(locator);
|
|
471
|
+
journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
|
|
472
|
+
queueReadiness();
|
|
433
473
|
}
|
|
434
474
|
function prepare(locators = []) {
|
|
435
475
|
for (const locator of locators)
|
|
436
|
-
|
|
476
|
+
unpreparedDemand.add(locator);
|
|
437
477
|
if (preparing)
|
|
438
478
|
return;
|
|
439
479
|
preparing = true;
|
|
@@ -443,8 +483,8 @@ export function composeRuntime(options) {
|
|
|
443
483
|
await yielded();
|
|
444
484
|
if (closed)
|
|
445
485
|
return;
|
|
446
|
-
|
|
447
|
-
await prepareReplica(waiting);
|
|
486
|
+
unpreparedDemand.delete(waiting);
|
|
487
|
+
await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
|
|
448
488
|
}
|
|
449
489
|
}
|
|
450
490
|
finally {
|
|
@@ -462,7 +502,8 @@ export function composeRuntime(options) {
|
|
|
462
502
|
return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
|
|
463
503
|
}
|
|
464
504
|
function awaited(locator) {
|
|
465
|
-
return
|
|
505
|
+
return (!roomsListed ||
|
|
506
|
+
(rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
|
|
466
507
|
}
|
|
467
508
|
function covered(plan) {
|
|
468
509
|
const locators = reachedRooms(plan);
|
|
@@ -502,9 +543,332 @@ export function composeRuntime(options) {
|
|
|
502
543
|
if (waiting.length > 0)
|
|
503
544
|
prepare(waiting.map(({ room }) => room));
|
|
504
545
|
}
|
|
546
|
+
function acquires(room) {
|
|
547
|
+
return (demanded(demand, room) ||
|
|
548
|
+
(replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
|
|
549
|
+
queuesWithoutReplica.has(room.locator));
|
|
550
|
+
}
|
|
551
|
+
function acquiredLocators() {
|
|
552
|
+
return [...discovered.values()]
|
|
553
|
+
.filter(acquires)
|
|
554
|
+
.map((room) => room.locator)
|
|
555
|
+
.sort();
|
|
556
|
+
}
|
|
557
|
+
function heldLocators(acquired) {
|
|
558
|
+
return demand.whole ? acquired : [...new Set([...acquired, ...demand.rooms])].sort();
|
|
559
|
+
}
|
|
560
|
+
function heldDemand(acquired) {
|
|
561
|
+
if (demand.whole)
|
|
562
|
+
return null;
|
|
563
|
+
return {
|
|
564
|
+
rooms: heldLocators(acquired.map((room) => room.locator)),
|
|
565
|
+
conversations: demand.conversations,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function applyDemand(next) {
|
|
569
|
+
const before = heldLocators(acquiredLocators());
|
|
570
|
+
const { whole, conversations: conversed } = demand;
|
|
571
|
+
demand = next;
|
|
572
|
+
const after = heldLocators(acquiredLocators());
|
|
573
|
+
queueReadiness();
|
|
574
|
+
if (!conversed && next.conversations)
|
|
575
|
+
scheduleCurrency();
|
|
576
|
+
if (JSON.stringify(before) === JSON.stringify(after) &&
|
|
577
|
+
whole === next.whole &&
|
|
578
|
+
conversed === next.conversations)
|
|
579
|
+
return;
|
|
580
|
+
journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
|
|
581
|
+
if (options.adapter.roomStream === undefined)
|
|
582
|
+
void sync('unstreamed');
|
|
583
|
+
else if (socketOpen)
|
|
584
|
+
options.adapter.roomStream.reopen();
|
|
585
|
+
}
|
|
586
|
+
function acquire(next) {
|
|
587
|
+
if (closed)
|
|
588
|
+
return;
|
|
589
|
+
if (narrowing !== null)
|
|
590
|
+
clearTimeout(narrowing);
|
|
591
|
+
narrowing = null;
|
|
592
|
+
const widening = wider(next, demand);
|
|
593
|
+
if (!widening && !narrower(next, demand))
|
|
594
|
+
return;
|
|
595
|
+
if (widening || !roomsListed) {
|
|
596
|
+
applyDemand(next);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
// A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
|
|
600
|
+
narrowing = setTimeout(() => {
|
|
601
|
+
narrowing = null;
|
|
602
|
+
applyDemand(next);
|
|
603
|
+
}, 5_000);
|
|
604
|
+
}
|
|
605
|
+
function requiredRooms(reach) {
|
|
606
|
+
if (reach === null)
|
|
607
|
+
return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
|
|
608
|
+
const named = [...new Set(reach.rooms)].sort();
|
|
609
|
+
if (roomsListed)
|
|
610
|
+
return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
|
|
611
|
+
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
612
|
+
}
|
|
613
|
+
function roomState(locator, search) {
|
|
614
|
+
if (sourceRefusals.has(locator))
|
|
615
|
+
return 'refused';
|
|
616
|
+
const listedRoom = discovered.get(locator);
|
|
617
|
+
if (roomsListed &&
|
|
618
|
+
(listedRoom === undefined ||
|
|
619
|
+
listedRoom.replica.admission !== 'admitted' ||
|
|
620
|
+
unreadable.has(locator)))
|
|
621
|
+
return 'unadmitted';
|
|
622
|
+
if (!replicas.has(locator))
|
|
623
|
+
return 'acquiring';
|
|
624
|
+
if (!prepared.has(locator))
|
|
625
|
+
return 'preparing';
|
|
626
|
+
if (search && !searchIndex.rooms().has(locator))
|
|
627
|
+
return 'indexing';
|
|
628
|
+
return 'complete';
|
|
629
|
+
}
|
|
630
|
+
function conversationCoverage(reach, search) {
|
|
631
|
+
const declared = reach === null || reach.conversations;
|
|
632
|
+
if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
|
|
633
|
+
return 'undeclared';
|
|
634
|
+
if (!conversations.known() || !conversations.current())
|
|
635
|
+
return 'acquiring';
|
|
636
|
+
if (!search)
|
|
637
|
+
return 'complete';
|
|
638
|
+
const unindexed = conversations
|
|
639
|
+
.searchable()
|
|
640
|
+
.filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
|
|
641
|
+
if (unindexed.length === 0)
|
|
642
|
+
return 'complete';
|
|
643
|
+
index([], unindexed);
|
|
644
|
+
return 'indexing';
|
|
645
|
+
}
|
|
646
|
+
function connectionOf() {
|
|
647
|
+
if (socketOpen)
|
|
648
|
+
return 'connected';
|
|
649
|
+
if (status.providerUnreachable)
|
|
650
|
+
return 'unreachable';
|
|
651
|
+
return options.adapter.roomStream === undefined && status.lastCompleted !== null
|
|
652
|
+
? 'connected'
|
|
653
|
+
: 'connecting';
|
|
654
|
+
}
|
|
655
|
+
function refusedIn(locators) {
|
|
656
|
+
return locators.flatMap((room) => {
|
|
657
|
+
const held = sourceRefusals.get(room);
|
|
658
|
+
if (held === undefined)
|
|
659
|
+
return [];
|
|
660
|
+
const { file, location } = held.fields;
|
|
661
|
+
return [
|
|
662
|
+
{
|
|
663
|
+
room,
|
|
664
|
+
file: typeof file === 'string' ? file : '',
|
|
665
|
+
rule: held.code,
|
|
666
|
+
location: typeof location === 'string' ? location : null,
|
|
667
|
+
},
|
|
668
|
+
];
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
function readinessFor(port, reach) {
|
|
672
|
+
const required = requiredRooms(reach);
|
|
673
|
+
const search = port.search === 'required';
|
|
674
|
+
const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
|
|
675
|
+
const building = (state) => coverage
|
|
676
|
+
.filter((room) => room.state === state && !unindexable.has(room.room))
|
|
677
|
+
.map(({ room }) => room);
|
|
678
|
+
const preparing = building('preparing');
|
|
679
|
+
if (preparing.length > 0)
|
|
680
|
+
prepare(preparing);
|
|
681
|
+
const indexing = building('indexing');
|
|
682
|
+
if (indexing.length > 0)
|
|
683
|
+
index(indexing, []);
|
|
684
|
+
return readinessOf({
|
|
685
|
+
code: port.code,
|
|
686
|
+
coverage: {
|
|
687
|
+
listed: required.listed,
|
|
688
|
+
rooms: coverage,
|
|
689
|
+
conversations: conversationCoverage(reach, search),
|
|
690
|
+
search: search ? 'required' : 'unused',
|
|
691
|
+
},
|
|
692
|
+
persistence,
|
|
693
|
+
connection: connectionOf(),
|
|
694
|
+
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
695
|
+
capacity: capacity !== null &&
|
|
696
|
+
coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
|
|
697
|
+
? capacity
|
|
698
|
+
: port.capacity,
|
|
699
|
+
refused: refusedIn(required.rooms),
|
|
700
|
+
lost: storageLoss !== null,
|
|
701
|
+
established: targets[port.target]?.established === true,
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
function establish(name) {
|
|
705
|
+
targets = { ...targets, [name]: { established: true } };
|
|
706
|
+
queueReadiness();
|
|
707
|
+
void durably((local) => local.keepTargets(targets)).catch(() => {
|
|
708
|
+
journal.record(`target record failed ${name}`);
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
function deliverReadiness(watch) {
|
|
712
|
+
if (!readinessWatches.has(watch))
|
|
713
|
+
return;
|
|
714
|
+
const readiness = readinessFor(watch.port, watch.reach);
|
|
715
|
+
if (readiness.ready && targets[watch.port.target]?.established !== true) {
|
|
716
|
+
establish(watch.port.target);
|
|
717
|
+
}
|
|
718
|
+
const shown = JSON.stringify(readiness);
|
|
719
|
+
if (shown === watch.last)
|
|
720
|
+
return;
|
|
721
|
+
watch.last = shown;
|
|
722
|
+
watch.deliver(structuredClone(readiness));
|
|
723
|
+
}
|
|
724
|
+
function queueReadiness() {
|
|
725
|
+
if (readinessQueued || closed || readinessWatches.size === 0)
|
|
726
|
+
return;
|
|
727
|
+
readinessQueued = true;
|
|
728
|
+
queueMicrotask(() => {
|
|
729
|
+
readinessQueued = false;
|
|
730
|
+
if (closed)
|
|
731
|
+
return;
|
|
732
|
+
for (const watch of [...readinessWatches])
|
|
733
|
+
deliverReadiness(watch);
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
function watchReadiness(port, reach, deliver) {
|
|
737
|
+
const watch = { port, reach, deliver, last: null };
|
|
738
|
+
readinessWatches.add(watch);
|
|
739
|
+
void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
|
|
740
|
+
return () => {
|
|
741
|
+
readinessWatches.delete(watch);
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
function layerBytes(layer) {
|
|
745
|
+
let bytes = 0;
|
|
746
|
+
for (const [path, text] of Object.entries(layer.documents))
|
|
747
|
+
bytes += (path.length + text.length) * 2;
|
|
748
|
+
return bytes;
|
|
749
|
+
}
|
|
750
|
+
function grownBy(next, previous) {
|
|
751
|
+
if (previous === undefined)
|
|
752
|
+
return layerBytes(next);
|
|
753
|
+
const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
|
|
754
|
+
return layerBytes({ ...next, documents });
|
|
755
|
+
}
|
|
756
|
+
function exceeded(error) {
|
|
757
|
+
return (typeof error === 'object' &&
|
|
758
|
+
error !== null &&
|
|
759
|
+
error.name === 'QuotaExceededError');
|
|
760
|
+
}
|
|
761
|
+
async function estimated() {
|
|
762
|
+
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
763
|
+
return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
|
|
764
|
+
}
|
|
765
|
+
function pause(locator, cause) {
|
|
766
|
+
paused.add(locator);
|
|
767
|
+
capacity = cause;
|
|
768
|
+
journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
|
|
769
|
+
queueReadiness();
|
|
770
|
+
if (rechecking !== null)
|
|
771
|
+
return;
|
|
772
|
+
// A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
|
|
773
|
+
rechecking = setInterval(() => {
|
|
774
|
+
void recheck();
|
|
775
|
+
}, 60_000);
|
|
776
|
+
}
|
|
777
|
+
async function recheck() {
|
|
778
|
+
const held = capacity;
|
|
779
|
+
if (held === null || closed)
|
|
780
|
+
return;
|
|
781
|
+
const available = await estimated().catch(() => null);
|
|
782
|
+
if (available === null || available < (held.needed ?? 0))
|
|
783
|
+
return;
|
|
784
|
+
if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
|
|
785
|
+
return;
|
|
786
|
+
resume();
|
|
787
|
+
}
|
|
788
|
+
function resume() {
|
|
789
|
+
if (rechecking !== null)
|
|
790
|
+
clearInterval(rechecking);
|
|
791
|
+
rechecking = null;
|
|
792
|
+
if (paused.size === 0 && capacity === null)
|
|
793
|
+
return;
|
|
794
|
+
paused.clear();
|
|
795
|
+
capacity = null;
|
|
796
|
+
journal.record('capacity resumed');
|
|
797
|
+
queueReadiness();
|
|
798
|
+
if (!closed && authorization !== undefined)
|
|
799
|
+
void sync('unstreamed');
|
|
800
|
+
}
|
|
801
|
+
async function reclaimed(local) {
|
|
802
|
+
const unreferenced = [...transfers.keys()].filter((locator) => {
|
|
803
|
+
const room = discovered.get(locator);
|
|
804
|
+
return room === undefined || !acquires(room);
|
|
805
|
+
});
|
|
806
|
+
for (const locator of unreferenced) {
|
|
807
|
+
await local.abandon(locator);
|
|
808
|
+
transfers.delete(locator);
|
|
809
|
+
}
|
|
810
|
+
if (unreferenced.length > 0)
|
|
811
|
+
journal.record(`capacity reclaimed ${unreferenced.length}`);
|
|
812
|
+
return unreferenced.length > 0;
|
|
813
|
+
}
|
|
814
|
+
async function admitted(local, locator, needed) {
|
|
815
|
+
if (paused.has(locator))
|
|
816
|
+
return false;
|
|
817
|
+
let available = await estimated().catch(() => null);
|
|
818
|
+
if (available === null || available >= needed)
|
|
819
|
+
return true;
|
|
820
|
+
if (await reclaimed(local))
|
|
821
|
+
available = await estimated().catch(() => null);
|
|
822
|
+
if (available === null || available >= needed)
|
|
823
|
+
return true;
|
|
824
|
+
pause(locator, { cause: 'insufficient-space', needed, available });
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
827
|
+
async function exceededOn(locator, bytes) {
|
|
828
|
+
const available = await estimated().catch(() => null);
|
|
829
|
+
pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
|
|
830
|
+
}
|
|
831
|
+
function seedRoomHeld(room, version) {
|
|
832
|
+
return prepared.has(room) && reachedBy(room, version);
|
|
833
|
+
}
|
|
834
|
+
function seedHeld(at) {
|
|
835
|
+
return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
|
|
836
|
+
}
|
|
837
|
+
function seedApplies(plan) {
|
|
838
|
+
const seed = seeds.get(plan.key);
|
|
839
|
+
if (seed === undefined)
|
|
840
|
+
return null;
|
|
841
|
+
if (!seedHeld(seed.at))
|
|
842
|
+
return seed;
|
|
843
|
+
seeds.delete(plan.key);
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
846
|
+
function seedBehind(plan, cover) {
|
|
847
|
+
const seed = seedApplies(plan);
|
|
848
|
+
if (seed === null || 'pending' in cover)
|
|
849
|
+
return cover;
|
|
850
|
+
return {
|
|
851
|
+
pending: Object.entries(seed.at)
|
|
852
|
+
.filter(([room, version]) => !seedRoomHeld(room, version))
|
|
853
|
+
.map(([room]) => ({ room, awaits: 'acquisition' })),
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
function seed(entry) {
|
|
857
|
+
if (seedHeld(entry.at))
|
|
858
|
+
return;
|
|
859
|
+
for (const { query, rows } of entry.queries) {
|
|
860
|
+
const key = queryKey(query);
|
|
861
|
+
seeds.set(key, { rows, at: entry.at });
|
|
862
|
+
for (const shown of seedWatchers.get(key) ?? [])
|
|
863
|
+
shown();
|
|
864
|
+
}
|
|
865
|
+
}
|
|
505
866
|
async function rowsQuery(plan) {
|
|
506
867
|
const before = covered(plan);
|
|
507
868
|
awaitPreparation(before);
|
|
869
|
+
const seeded = seedApplies(plan);
|
|
870
|
+
if (seeded !== null)
|
|
871
|
+
return seededResult(seeded).result;
|
|
508
872
|
if ('pending' in before)
|
|
509
873
|
return before;
|
|
510
874
|
const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
|
|
@@ -529,10 +893,12 @@ export function composeRuntime(options) {
|
|
|
529
893
|
}
|
|
530
894
|
}
|
|
531
895
|
function indexNext() {
|
|
532
|
-
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator));
|
|
896
|
+
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
|
|
533
897
|
if (room !== undefined) {
|
|
534
898
|
roomsToIndex.delete(room);
|
|
535
|
-
return () =>
|
|
899
|
+
return () => prepared.has(room)
|
|
900
|
+
? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
|
|
901
|
+
: undefined;
|
|
536
902
|
}
|
|
537
903
|
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
538
904
|
if (conversation === undefined)
|
|
@@ -553,6 +919,7 @@ export function composeRuntime(options) {
|
|
|
553
919
|
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
554
920
|
await yielded();
|
|
555
921
|
await unit();
|
|
922
|
+
queueReadiness();
|
|
556
923
|
}
|
|
557
924
|
}
|
|
558
925
|
finally {
|
|
@@ -660,6 +1027,9 @@ export function composeRuntime(options) {
|
|
|
660
1027
|
if (fence !== undefined && fence.erased && fence.at < started)
|
|
661
1028
|
fences.delete(room.locator);
|
|
662
1029
|
}
|
|
1030
|
+
await loseRooms(found
|
|
1031
|
+
.filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
|
|
1032
|
+
.map((room) => room.locator));
|
|
663
1033
|
const listed = found.filter((room) => !fences.has(room.locator));
|
|
664
1034
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
665
1035
|
for (const locator of listedLocators)
|
|
@@ -684,6 +1054,12 @@ export function composeRuntime(options) {
|
|
|
684
1054
|
emitRooms();
|
|
685
1055
|
return listed;
|
|
686
1056
|
}
|
|
1057
|
+
function heldLocally(locator) {
|
|
1058
|
+
return (replicas.has(locator) ||
|
|
1059
|
+
roomDrafts.has(locator) ||
|
|
1060
|
+
transfers.has(locator) ||
|
|
1061
|
+
queuesWithoutReplica.has(locator));
|
|
1062
|
+
}
|
|
687
1063
|
function unlistedHeld(listed) {
|
|
688
1064
|
return [
|
|
689
1065
|
...new Set([
|
|
@@ -726,6 +1102,8 @@ export function composeRuntime(options) {
|
|
|
726
1102
|
unreadable.delete(locator);
|
|
727
1103
|
stale.delete(locator);
|
|
728
1104
|
repairable.delete(locator);
|
|
1105
|
+
unindexable.delete(locator);
|
|
1106
|
+
paused.delete(locator);
|
|
729
1107
|
scopedRefusals.delete(locator);
|
|
730
1108
|
sourceRefusals.delete(locator);
|
|
731
1109
|
declarationRefusals.delete(locator);
|
|
@@ -779,7 +1157,7 @@ export function composeRuntime(options) {
|
|
|
779
1157
|
return model;
|
|
780
1158
|
}
|
|
781
1159
|
async function hydrate(opened) {
|
|
782
|
-
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
|
|
1160
|
+
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
|
|
783
1161
|
opened.all(),
|
|
784
1162
|
opened.queues(),
|
|
785
1163
|
opened.roomDrafts(),
|
|
@@ -791,7 +1169,9 @@ export function composeRuntime(options) {
|
|
|
791
1169
|
opened.capabilities(),
|
|
792
1170
|
opened.member(),
|
|
793
1171
|
opened.transfers(),
|
|
1172
|
+
opened.targets(),
|
|
794
1173
|
]);
|
|
1174
|
+
targets = heldTargets;
|
|
795
1175
|
transfers.clear();
|
|
796
1176
|
for (const record of staged)
|
|
797
1177
|
transfers.set(record.locator, record);
|
|
@@ -1037,7 +1417,18 @@ export function composeRuntime(options) {
|
|
|
1037
1417
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
1038
1418
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
1039
1419
|
});
|
|
1040
|
-
|
|
1420
|
+
let settled;
|
|
1421
|
+
try {
|
|
1422
|
+
settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
|
|
1423
|
+
}
|
|
1424
|
+
catch (error) {
|
|
1425
|
+
if (!exceeded(error))
|
|
1426
|
+
throw error;
|
|
1427
|
+
await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
|
|
1428
|
+
caughtUpTo(room.locator);
|
|
1429
|
+
return true;
|
|
1430
|
+
}
|
|
1431
|
+
const set = settled();
|
|
1041
1432
|
if (stored !== undefined) {
|
|
1042
1433
|
transfers.delete(room.locator);
|
|
1043
1434
|
catching.delete(room.locator);
|
|
@@ -1045,6 +1436,7 @@ export function composeRuntime(options) {
|
|
|
1045
1436
|
for (const batch of accepted)
|
|
1046
1437
|
pendings.delete(batch.id);
|
|
1047
1438
|
stale.delete(room.locator);
|
|
1439
|
+
unindexable.delete(room.locator);
|
|
1048
1440
|
if (unreadable.delete(room.locator))
|
|
1049
1441
|
scopedRefusals.delete(room.locator);
|
|
1050
1442
|
sourceRefusals.delete(room.locator);
|
|
@@ -1288,7 +1680,7 @@ export function composeRuntime(options) {
|
|
|
1288
1680
|
const scope = options.adapter.referrerScope(request.referrer);
|
|
1289
1681
|
return scope?.kind === 'room' ? scope.locator : null;
|
|
1290
1682
|
}
|
|
1291
|
-
async function
|
|
1683
|
+
async function transferBinary(request, transfer, answer) {
|
|
1292
1684
|
if (closed || refusal !== null)
|
|
1293
1685
|
return UNCERTAIN;
|
|
1294
1686
|
try {
|
|
@@ -1304,40 +1696,41 @@ export function composeRuntime(options) {
|
|
|
1304
1696
|
? `transfer binary ${request.locator} ${request.path}`
|
|
1305
1697
|
: `transfer binary ${request.fileRef}`);
|
|
1306
1698
|
try {
|
|
1307
|
-
const result = await
|
|
1308
|
-
|
|
1699
|
+
const result = await transfer(authorization);
|
|
1700
|
+
const settled = result;
|
|
1701
|
+
journal.record(`outcome binary ${settled.kind}`);
|
|
1309
1702
|
const locator = binaryRoom(request);
|
|
1310
1703
|
if (locator !== null && fencedSince(locator, since))
|
|
1311
1704
|
return failure({ kind: 'room-not-readable', locator });
|
|
1312
|
-
if (
|
|
1313
|
-
const room = replicas.get(
|
|
1705
|
+
if (settled.kind === 'scoped-refusal') {
|
|
1706
|
+
const room = replicas.get(settled.locator)?.confirmed.record;
|
|
1314
1707
|
if (room !== undefined)
|
|
1315
|
-
await scoped(
|
|
1708
|
+
await scoped(settled, room, await openLocal());
|
|
1316
1709
|
}
|
|
1317
|
-
if (
|
|
1710
|
+
if (settled.kind === 'revoked-authorization')
|
|
1318
1711
|
await refuse('revoked-authorization');
|
|
1319
|
-
return
|
|
1712
|
+
return answer(result);
|
|
1320
1713
|
}
|
|
1321
1714
|
catch {
|
|
1322
1715
|
journal.record('outcome binary transport-uncertainty');
|
|
1323
1716
|
return UNCERTAIN;
|
|
1324
1717
|
}
|
|
1325
1718
|
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1719
|
+
function readBinary(request) {
|
|
1720
|
+
return transferBinary(request, (held) => options.adapter.binary(held, request), binaryAnswer);
|
|
1721
|
+
}
|
|
1722
|
+
function binaryPath(locator, path, revision) {
|
|
1328
1723
|
if (revision !== undefined)
|
|
1329
|
-
return
|
|
1724
|
+
return { kind: 'path', locator, path, revision };
|
|
1330
1725
|
const visible = replicas.get(locator)?.visible;
|
|
1331
1726
|
const held = visible?.binaries[path];
|
|
1332
1727
|
if (visible === undefined || held === undefined)
|
|
1333
|
-
return
|
|
1334
|
-
if (held.state === 'pending')
|
|
1335
|
-
|
|
1336
|
-
return blob === null ? absentBinary(path) : { kind: 'binary', blob };
|
|
1337
|
-
}
|
|
1728
|
+
return null;
|
|
1729
|
+
if (held.state === 'pending')
|
|
1730
|
+
return { kind: 'pending', localId: held.localId };
|
|
1338
1731
|
const record = binaryRecord(path, held, visible.revisions[path]);
|
|
1339
1732
|
if (record.state !== 'accepted')
|
|
1340
|
-
return
|
|
1733
|
+
return null;
|
|
1341
1734
|
const replica = replicas.get(locator);
|
|
1342
1735
|
const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
|
|
1343
1736
|
const holds = (at) => {
|
|
@@ -1352,13 +1745,31 @@ export function composeRuntime(options) {
|
|
|
1352
1745
|
if (change.kind === 'move' && change.to === confirmed)
|
|
1353
1746
|
confirmed = change.path;
|
|
1354
1747
|
}
|
|
1355
|
-
return
|
|
1748
|
+
return {
|
|
1356
1749
|
kind: 'path',
|
|
1357
1750
|
locator,
|
|
1358
1751
|
path: confirmed,
|
|
1359
1752
|
revision: record.revision,
|
|
1360
1753
|
fileRef: record.fileRef,
|
|
1361
|
-
}
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
async function readBinaryPath(locator, path, revision) {
|
|
1757
|
+
await ready;
|
|
1758
|
+
const asked = binaryPath(locator, path, revision);
|
|
1759
|
+
if (asked === null)
|
|
1760
|
+
return absentBinary(path);
|
|
1761
|
+
if (asked.kind === 'pending') {
|
|
1762
|
+
const blob = queuedBlob(locator, path, asked.localId);
|
|
1763
|
+
return blob === null ? absentBinary(path) : { kind: 'binary', blob };
|
|
1764
|
+
}
|
|
1765
|
+
return readBinary(asked);
|
|
1766
|
+
}
|
|
1767
|
+
async function linkBinaryPath(locator, path, revision) {
|
|
1768
|
+
await ready;
|
|
1769
|
+
const asked = binaryPath(locator, path, revision);
|
|
1770
|
+
if (asked === null || asked.kind === 'pending')
|
|
1771
|
+
return absentBinary(path);
|
|
1772
|
+
return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
|
|
1362
1773
|
}
|
|
1363
1774
|
async function readBinaryReference(reference) {
|
|
1364
1775
|
await ready;
|
|
@@ -1384,7 +1795,7 @@ export function composeRuntime(options) {
|
|
|
1384
1795
|
}
|
|
1385
1796
|
return undefined;
|
|
1386
1797
|
}
|
|
1387
|
-
async function invokeAction(name, input) {
|
|
1798
|
+
async function invokeAction(name, input, dispatching) {
|
|
1388
1799
|
await ready;
|
|
1389
1800
|
if (closed || refusal !== null)
|
|
1390
1801
|
return UNCERTAIN;
|
|
@@ -1398,6 +1809,7 @@ export function composeRuntime(options) {
|
|
|
1398
1809
|
}
|
|
1399
1810
|
if (closed || authorization === undefined || refusal !== null)
|
|
1400
1811
|
return UNCERTAIN;
|
|
1812
|
+
await dispatching?.();
|
|
1401
1813
|
journal.record(`transfer action ${name}`);
|
|
1402
1814
|
try {
|
|
1403
1815
|
const result = await options.adapter.action(authorization, { name, input });
|
|
@@ -1418,6 +1830,60 @@ export function composeRuntime(options) {
|
|
|
1418
1830
|
return UNCERTAIN;
|
|
1419
1831
|
}
|
|
1420
1832
|
}
|
|
1833
|
+
function unsettled(result) {
|
|
1834
|
+
return result.kind !== 'action' && result.retryable;
|
|
1835
|
+
}
|
|
1836
|
+
async function recoveredAction(name, input) {
|
|
1837
|
+
const operation = operationOf(input);
|
|
1838
|
+
if (operation === null)
|
|
1839
|
+
return invokeAction(name, input);
|
|
1840
|
+
const kept = async () => (await openLocal()).keepCall({ operation, name, input });
|
|
1841
|
+
calling.add(operation);
|
|
1842
|
+
try {
|
|
1843
|
+
let result = await invokeAction(name, input, kept);
|
|
1844
|
+
if (unsettled(result)) {
|
|
1845
|
+
await recovery.attempt(`action ${operation}`, async () => {
|
|
1846
|
+
if (closed || refusal !== null)
|
|
1847
|
+
return 'done';
|
|
1848
|
+
if (!socketOpen)
|
|
1849
|
+
return 'again';
|
|
1850
|
+
result = await invokeAction(name, input, kept);
|
|
1851
|
+
return unsettled(result) ? 'again' : 'done';
|
|
1852
|
+
}, { again: true });
|
|
1853
|
+
}
|
|
1854
|
+
if (!unsettled(result))
|
|
1855
|
+
await (await openLocal()).dropCall(operation);
|
|
1856
|
+
return result;
|
|
1857
|
+
}
|
|
1858
|
+
finally {
|
|
1859
|
+
calling.delete(operation);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
async function resendCalls(signal) {
|
|
1863
|
+
if (!socketOpen || closed || stopping)
|
|
1864
|
+
return 'done';
|
|
1865
|
+
const local = await openLocal();
|
|
1866
|
+
for (const held of await local.calls()) {
|
|
1867
|
+
if (calling.has(held.operation))
|
|
1868
|
+
continue;
|
|
1869
|
+
if (closed || signal.aborted)
|
|
1870
|
+
return 'done';
|
|
1871
|
+
if (pastHorizon(held.operation)) {
|
|
1872
|
+
await local.dropCall(held.operation);
|
|
1873
|
+
continue;
|
|
1874
|
+
}
|
|
1875
|
+
calling.add(held.operation);
|
|
1876
|
+
try {
|
|
1877
|
+
if (unsettled(await invokeAction(held.name, held.input)))
|
|
1878
|
+
return 'again';
|
|
1879
|
+
await local.dropCall(held.operation);
|
|
1880
|
+
}
|
|
1881
|
+
finally {
|
|
1882
|
+
calling.delete(held.operation);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
return 'done';
|
|
1886
|
+
}
|
|
1421
1887
|
function continuing() {
|
|
1422
1888
|
return !closed && !discoveryPaused;
|
|
1423
1889
|
}
|
|
@@ -1435,8 +1901,9 @@ export function composeRuntime(options) {
|
|
|
1435
1901
|
async function pullRefresh(authorized, confirmed, live) {
|
|
1436
1902
|
if (!live())
|
|
1437
1903
|
return unconfirmed;
|
|
1904
|
+
const turn = generation;
|
|
1438
1905
|
try {
|
|
1439
|
-
const result = await refreshRoom(
|
|
1906
|
+
const result = await refreshRoom(reads, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
|
|
1440
1907
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
1441
1908
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
1442
1909
|
return result;
|
|
@@ -1445,6 +1912,9 @@ export function composeRuntime(options) {
|
|
|
1445
1912
|
journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
|
|
1446
1913
|
throw error;
|
|
1447
1914
|
}
|
|
1915
|
+
finally {
|
|
1916
|
+
reads.finished(confirmed.record.locator, turn);
|
|
1917
|
+
}
|
|
1448
1918
|
}
|
|
1449
1919
|
function staging(local, live) {
|
|
1450
1920
|
return {
|
|
@@ -1453,7 +1923,18 @@ export function composeRuntime(options) {
|
|
|
1453
1923
|
stage: (record, page) => exclusive(async () => {
|
|
1454
1924
|
if (!live())
|
|
1455
1925
|
return false;
|
|
1456
|
-
|
|
1926
|
+
const bytes = layerBytes(page);
|
|
1927
|
+
if (!(await admitted(local, record.locator, bytes)))
|
|
1928
|
+
return false;
|
|
1929
|
+
try {
|
|
1930
|
+
await local.stage(record, page);
|
|
1931
|
+
}
|
|
1932
|
+
catch (error) {
|
|
1933
|
+
if (!exceeded(error))
|
|
1934
|
+
throw error;
|
|
1935
|
+
await exceededOn(record.locator, bytes);
|
|
1936
|
+
return false;
|
|
1937
|
+
}
|
|
1457
1938
|
transfers.set(record.locator, record);
|
|
1458
1939
|
readSide.recover();
|
|
1459
1940
|
return true;
|
|
@@ -1476,12 +1957,13 @@ export function composeRuntime(options) {
|
|
|
1476
1957
|
async function pullBootstrap(authorized, room, local, live) {
|
|
1477
1958
|
if (!live())
|
|
1478
1959
|
return unconfirmed;
|
|
1960
|
+
const turn = generation;
|
|
1479
1961
|
if (replicas.has(room.locator)) {
|
|
1480
1962
|
catching.add(room.locator);
|
|
1481
1963
|
readSide.recover();
|
|
1482
1964
|
}
|
|
1483
1965
|
try {
|
|
1484
|
-
const result = await bootstrapRoom(
|
|
1966
|
+
const result = await bootstrapRoom(reads, authorized, room, {
|
|
1485
1967
|
collection: modelOf(room.locator),
|
|
1486
1968
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
1487
1969
|
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
@@ -1496,14 +1978,30 @@ export function composeRuntime(options) {
|
|
|
1496
1978
|
caughtUpTo(room.locator);
|
|
1497
1979
|
throw error;
|
|
1498
1980
|
}
|
|
1981
|
+
finally {
|
|
1982
|
+
reads.finished(room.locator, turn);
|
|
1983
|
+
}
|
|
1499
1984
|
}
|
|
1500
1985
|
function caughtUpTo(locator) {
|
|
1501
1986
|
if (catching.delete(locator))
|
|
1502
1987
|
readSide.recover();
|
|
1503
1988
|
}
|
|
1989
|
+
function behind(room) {
|
|
1990
|
+
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1991
|
+
return (held !== undefined &&
|
|
1992
|
+
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
1993
|
+
}
|
|
1504
1994
|
async function advance(room, local, authorized, live) {
|
|
1995
|
+
const carried = reads.carried(room.locator);
|
|
1996
|
+
const withheld = await advanceOnce(room, local, authorized, live);
|
|
1997
|
+
if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
|
|
1998
|
+
return withheld;
|
|
1999
|
+
}
|
|
2000
|
+
return advanceOnce(room, local, authorized, live);
|
|
2001
|
+
}
|
|
2002
|
+
async function advanceOnce(room, local, authorized, live) {
|
|
1505
2003
|
await referenced(room.locator);
|
|
1506
|
-
if (!live())
|
|
2004
|
+
if (!live() || paused.has(room.locator))
|
|
1507
2005
|
return unconfirmed;
|
|
1508
2006
|
const held = replicas.get(room.locator);
|
|
1509
2007
|
if (held !== undefined) {
|
|
@@ -1938,6 +2436,11 @@ export function composeRuntime(options) {
|
|
|
1938
2436
|
});
|
|
1939
2437
|
}
|
|
1940
2438
|
async function scoped(result, room, local) {
|
|
2439
|
+
if ((result.scope === 'content' || result.scope === 'sync') && !result.retryable) {
|
|
2440
|
+
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
2441
|
+
await loseRooms([room.locator]);
|
|
2442
|
+
return;
|
|
2443
|
+
}
|
|
1941
2444
|
scopedRefusals.set(room.locator, {
|
|
1942
2445
|
code: result.code,
|
|
1943
2446
|
fields: result.fields,
|
|
@@ -1984,15 +2487,16 @@ export function composeRuntime(options) {
|
|
|
1984
2487
|
});
|
|
1985
2488
|
}
|
|
1986
2489
|
async function synchronize(listed, local, progress, attempt, live) {
|
|
1987
|
-
const
|
|
2490
|
+
const acquired = listed.filter(acquires);
|
|
2491
|
+
const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
|
|
1988
2492
|
if (!live())
|
|
1989
2493
|
return;
|
|
1990
|
-
const ready =
|
|
2494
|
+
const ready = acquired.filter((_, index) => {
|
|
1991
2495
|
const outcome = prepared[index];
|
|
1992
2496
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1993
2497
|
});
|
|
1994
2498
|
if (attempt === 'opened') {
|
|
1995
|
-
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
2499
|
+
options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
|
|
1996
2500
|
handed = true;
|
|
1997
2501
|
}
|
|
1998
2502
|
discovering?.release();
|
|
@@ -2020,19 +2524,10 @@ export function composeRuntime(options) {
|
|
|
2020
2524
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
2021
2525
|
if (discoveryPaused || authorization === undefined)
|
|
2022
2526
|
return false;
|
|
2023
|
-
|
|
2024
|
-
if (replicas.has(room.locator))
|
|
2025
|
-
progress.complete = false;
|
|
2026
|
-
return false;
|
|
2027
|
-
}
|
|
2028
|
-
return true;
|
|
2527
|
+
return room.replica.admission === 'admitted';
|
|
2029
2528
|
}
|
|
2030
2529
|
async function caughtUp(room, local, progress, live) {
|
|
2031
|
-
|
|
2032
|
-
if (held === undefined || authorization === undefined || !live())
|
|
2033
|
-
return;
|
|
2034
|
-
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
2035
|
-
if (!behind)
|
|
2530
|
+
if (authorization === undefined || !live() || !behind(room))
|
|
2036
2531
|
return;
|
|
2037
2532
|
const withheld = await advance(room, local, authorization, live);
|
|
2038
2533
|
if (withheld !== null)
|
|
@@ -2260,9 +2755,43 @@ export function composeRuntime(options) {
|
|
|
2260
2755
|
options.adapter.roomStream?.reopen();
|
|
2261
2756
|
});
|
|
2262
2757
|
}
|
|
2263
|
-
function
|
|
2758
|
+
function rediscover() {
|
|
2759
|
+
return task(OPENING_LANE, async (live) => {
|
|
2760
|
+
if (!live() || !socketOpen)
|
|
2761
|
+
return;
|
|
2762
|
+
const progress = { complete: true, reachable: true };
|
|
2763
|
+
const gate = discover();
|
|
2764
|
+
moveTo(transferring(status));
|
|
2765
|
+
try {
|
|
2766
|
+
const local = await openLocal();
|
|
2767
|
+
if (!live())
|
|
2768
|
+
return;
|
|
2769
|
+
await ensureAuthorization();
|
|
2770
|
+
if (!live())
|
|
2771
|
+
return;
|
|
2772
|
+
await discoverRooms(local, progress, 'authorized', live);
|
|
2773
|
+
}
|
|
2774
|
+
catch {
|
|
2775
|
+
progress.complete = false;
|
|
2776
|
+
}
|
|
2777
|
+
finally {
|
|
2778
|
+
gate.release();
|
|
2779
|
+
notifyReady();
|
|
2780
|
+
}
|
|
2781
|
+
if (!live())
|
|
2782
|
+
return;
|
|
2783
|
+
if (!progress.complete)
|
|
2784
|
+
complete = false;
|
|
2785
|
+
await rest(progress, live);
|
|
2786
|
+
});
|
|
2787
|
+
}
|
|
2788
|
+
function closedSocket(revoked, lost) {
|
|
2264
2789
|
const opened = socketOpen;
|
|
2265
2790
|
socketOpen = false;
|
|
2791
|
+
if (lost) {
|
|
2792
|
+
reads.lost();
|
|
2793
|
+
conversations.lost();
|
|
2794
|
+
}
|
|
2266
2795
|
if (revoked) {
|
|
2267
2796
|
void refuse('revoked-authorization');
|
|
2268
2797
|
return;
|
|
@@ -2449,6 +2978,7 @@ export function composeRuntime(options) {
|
|
|
2449
2978
|
progress.complete = false;
|
|
2450
2979
|
return;
|
|
2451
2980
|
}
|
|
2981
|
+
reads.applied();
|
|
2452
2982
|
submitAll();
|
|
2453
2983
|
for (const locator of stale)
|
|
2454
2984
|
if (repairable.has(locator))
|
|
@@ -2457,9 +2987,12 @@ export function composeRuntime(options) {
|
|
|
2457
2987
|
if (status.providerUnreachable)
|
|
2458
2988
|
conversations.reconnected();
|
|
2459
2989
|
const reading = { complete: true, reachable: true };
|
|
2460
|
-
|
|
2990
|
+
if (demand.conversations)
|
|
2991
|
+
await conversations.catchUp(reading);
|
|
2461
2992
|
if (progress.reachable && reading.reachable)
|
|
2462
2993
|
await conversations.resend(reading);
|
|
2994
|
+
if (progress.reachable)
|
|
2995
|
+
void recovery.attempt('calls', resendCalls);
|
|
2463
2996
|
if (!reading.reachable) {
|
|
2464
2997
|
progress.reachable = false;
|
|
2465
2998
|
resendLater();
|
|
@@ -2520,7 +3053,7 @@ export function composeRuntime(options) {
|
|
|
2520
3053
|
return;
|
|
2521
3054
|
const room = discovered.get(locator);
|
|
2522
3055
|
const held = replicas.get(locator);
|
|
2523
|
-
if (room === undefined || room.replica.admission !== 'admitted')
|
|
3056
|
+
if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
|
|
2524
3057
|
return;
|
|
2525
3058
|
if (held !== undefined &&
|
|
2526
3059
|
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
@@ -2572,14 +3105,17 @@ export function composeRuntime(options) {
|
|
|
2572
3105
|
if (event.kind === 'opened') {
|
|
2573
3106
|
socketOpen = true;
|
|
2574
3107
|
generation += 1;
|
|
3108
|
+
reads.opened();
|
|
2575
3109
|
conversations.reopened();
|
|
2576
3110
|
recovery.resume();
|
|
2577
3111
|
void sync('opened');
|
|
2578
3112
|
}
|
|
2579
3113
|
else if (event.kind === 'closed')
|
|
2580
|
-
closedSocket(event.revoked);
|
|
3114
|
+
closedSocket(event.revoked, event.lost);
|
|
2581
3115
|
else if (event.kind === 'opening')
|
|
2582
3116
|
void applyOpening(event.expired);
|
|
3117
|
+
else if (event.kind === 'authority-changed')
|
|
3118
|
+
void rediscover();
|
|
2583
3119
|
else
|
|
2584
3120
|
void applyRoom(event.locator, event.roomVersion);
|
|
2585
3121
|
}, receipts);
|
|
@@ -2597,6 +3133,7 @@ export function composeRuntime(options) {
|
|
|
2597
3133
|
haltedRefusal = false;
|
|
2598
3134
|
if (mode !== serviceMode) {
|
|
2599
3135
|
serviceMode = mode;
|
|
3136
|
+
queueReadiness();
|
|
2600
3137
|
journal.record(`service mode ${mode}`);
|
|
2601
3138
|
for (const listener of serviceModeListeners)
|
|
2602
3139
|
listener(mode);
|
|
@@ -2609,6 +3146,8 @@ export function composeRuntime(options) {
|
|
|
2609
3146
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
2610
3147
|
return;
|
|
2611
3148
|
withdrawConversations = stream.observe((event) => {
|
|
3149
|
+
if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
|
|
3150
|
+
queueReadiness();
|
|
2612
3151
|
if (conversations.receive(event)) {
|
|
2613
3152
|
void ready.then(scheduleCurrency);
|
|
2614
3153
|
return;
|
|
@@ -2631,7 +3170,7 @@ export function composeRuntime(options) {
|
|
|
2631
3170
|
if (!live())
|
|
2632
3171
|
return;
|
|
2633
3172
|
await conversations.reconcile();
|
|
2634
|
-
if (conversations.current()) {
|
|
3173
|
+
if (conversations.current() || !demand.conversations) {
|
|
2635
3174
|
show();
|
|
2636
3175
|
return;
|
|
2637
3176
|
}
|
|
@@ -2649,6 +3188,11 @@ export function composeRuntime(options) {
|
|
|
2649
3188
|
if (closed)
|
|
2650
3189
|
return;
|
|
2651
3190
|
closed = true;
|
|
3191
|
+
if (narrowing !== null)
|
|
3192
|
+
clearTimeout(narrowing);
|
|
3193
|
+
if (rechecking !== null)
|
|
3194
|
+
clearInterval(rechecking);
|
|
3195
|
+
readinessWatches.clear();
|
|
2652
3196
|
withdrawStream?.();
|
|
2653
3197
|
withdrawStream = undefined;
|
|
2654
3198
|
withdrawConversations?.();
|
|
@@ -2696,11 +3240,38 @@ export function composeRuntime(options) {
|
|
|
2696
3240
|
return planOf(input, roomFacts(reach), planReach(reach));
|
|
2697
3241
|
}
|
|
2698
3242
|
function watchPlan(plan, deliver) {
|
|
2699
|
-
|
|
3243
|
+
let seeded = false;
|
|
3244
|
+
let pending = true;
|
|
3245
|
+
const shown = () => {
|
|
3246
|
+
const seed = seeds.get(plan.key);
|
|
3247
|
+
if (seed === undefined || seeded || !pending)
|
|
3248
|
+
return;
|
|
3249
|
+
seeded = true;
|
|
3250
|
+
deliver(seededResult(seed));
|
|
3251
|
+
};
|
|
3252
|
+
const watchers = seedWatchers.get(plan.key) ?? new Set();
|
|
3253
|
+
seedWatchers.set(plan.key, watchers);
|
|
3254
|
+
watchers.add(shown);
|
|
3255
|
+
const stop = readSide.watch(plan, identityOf(plan), () => {
|
|
2700
3256
|
const cover = covered(plan);
|
|
2701
3257
|
awaitPreparation(cover);
|
|
2702
|
-
return cover;
|
|
2703
|
-
},
|
|
3258
|
+
return seedBehind(plan, cover);
|
|
3259
|
+
}, (message) => {
|
|
3260
|
+
const answered = message;
|
|
3261
|
+
pending = whole(answered) && 'pending' in answered.result;
|
|
3262
|
+
if (pending && seeds.has(plan.key)) {
|
|
3263
|
+
shown();
|
|
3264
|
+
return;
|
|
3265
|
+
}
|
|
3266
|
+
seeded = false;
|
|
3267
|
+
deliver(message);
|
|
3268
|
+
});
|
|
3269
|
+
return () => {
|
|
3270
|
+
watchers.delete(shown);
|
|
3271
|
+
if (watchers.size === 0)
|
|
3272
|
+
seedWatchers.delete(plan.key);
|
|
3273
|
+
stop();
|
|
3274
|
+
};
|
|
2704
3275
|
}
|
|
2705
3276
|
async function searched(input, reach) {
|
|
2706
3277
|
const plan = searchPlanOf(input);
|
|
@@ -2709,40 +3280,60 @@ export function composeRuntime(options) {
|
|
|
2709
3280
|
const cover = covered(filter);
|
|
2710
3281
|
awaitPreparation(cover);
|
|
2711
3282
|
const pending = 'pending' in cover ? [...cover.pending] : [];
|
|
2712
|
-
const
|
|
3283
|
+
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
3284
|
+
const held = covered({
|
|
3285
|
+
...filter,
|
|
3286
|
+
rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
|
|
3287
|
+
});
|
|
3288
|
+
const covering = 'pending' in held ? [] : held.coverage.rooms;
|
|
2713
3289
|
const unindexed = covering
|
|
2714
3290
|
.filter(({ room }) => !searchIndex.rooms().has(room))
|
|
2715
3291
|
.map(({ room }) => room);
|
|
2716
3292
|
for (const room of unindexed)
|
|
2717
3293
|
pending.push({ room, awaits: 'index' });
|
|
2718
|
-
const
|
|
3294
|
+
const listed = conversations.searchable();
|
|
2719
3295
|
const named = reach !== null && !reach.conversations
|
|
2720
3296
|
? []
|
|
2721
|
-
: (plan.conversations ??
|
|
3297
|
+
: (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
|
|
2722
3298
|
const waiting = named.filter((one) => !searchIndex.conversations().has(one));
|
|
2723
3299
|
for (const conversation of waiting)
|
|
2724
3300
|
pending.push({ conversation, awaits: 'index' });
|
|
2725
3301
|
if (unindexed.length > 0 || waiting.length > 0)
|
|
2726
3302
|
index(unindexed, waiting);
|
|
2727
|
-
|
|
2728
|
-
|
|
3303
|
+
const complete = pending.length === 0 && !('pending' in cover);
|
|
3304
|
+
const answering = complete
|
|
3305
|
+
? filter
|
|
3306
|
+
: {
|
|
3307
|
+
...filter,
|
|
3308
|
+
rooms: covering
|
|
3309
|
+
.filter(({ room }) => searchIndex.rooms().has(room))
|
|
3310
|
+
.map(({ room }) => room),
|
|
3311
|
+
};
|
|
3312
|
+
const answered = covered(answering);
|
|
3313
|
+
if ('pending' in answered)
|
|
3314
|
+
continue;
|
|
3315
|
+
const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
|
|
2729
3316
|
const seen = searchIndex.changes();
|
|
2730
3317
|
await searchIndex.caughtUp();
|
|
2731
|
-
const selected =
|
|
3318
|
+
const selected = roomsAlone(answering) ? null : await rowsQuery(answering);
|
|
2732
3319
|
if (selected !== null && 'pending' in selected)
|
|
2733
|
-
|
|
3320
|
+
continue;
|
|
2734
3321
|
if (searchIndex.changes() !== seen ||
|
|
2735
|
-
JSON.stringify(covered(
|
|
3322
|
+
JSON.stringify(covered(answering)) !== JSON.stringify(answered))
|
|
2736
3323
|
continue;
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
3324
|
+
const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms === null ? null : new Set(answering.rooms), plan.collection
|
|
3325
|
+
? new Map(answered.coverage.rooms.map(({ room }) => [
|
|
3326
|
+
room,
|
|
3327
|
+
replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
|
|
3328
|
+
]))
|
|
3329
|
+
: null);
|
|
3330
|
+
const coverage = {
|
|
3331
|
+
rooms: answered.coverage.rooms,
|
|
3332
|
+
conversations: searchedConversations,
|
|
3333
|
+
index: searchIndex.generation(),
|
|
3334
|
+
overlay: answered.coverage.overlay,
|
|
2745
3335
|
};
|
|
3336
|
+
return complete ? { ...hits, coverage } : { pending, ...hits, coverage };
|
|
2746
3337
|
}
|
|
2747
3338
|
}
|
|
2748
3339
|
const watch = (topic, input, deliver) => {
|
|
@@ -2789,6 +3380,9 @@ export function composeRuntime(options) {
|
|
|
2789
3380
|
presentationListeners.delete(listener);
|
|
2790
3381
|
};
|
|
2791
3382
|
}
|
|
3383
|
+
if (topic === 'readiness') {
|
|
3384
|
+
return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
|
|
3385
|
+
}
|
|
2792
3386
|
if (topic === 'rooms') {
|
|
2793
3387
|
roomListeners.add(deliver);
|
|
2794
3388
|
deliver(listed());
|
|
@@ -2941,10 +3535,11 @@ export function composeRuntime(options) {
|
|
|
2941
3535
|
},
|
|
2942
3536
|
binaryFile: {
|
|
2943
3537
|
read: readBinaryPath,
|
|
3538
|
+
link: linkBinaryPath,
|
|
2944
3539
|
readReference: readBinaryReference,
|
|
2945
3540
|
},
|
|
2946
3541
|
call(name, input) {
|
|
2947
|
-
return
|
|
3542
|
+
return recoveredAction(name, input);
|
|
2948
3543
|
},
|
|
2949
3544
|
reached(locator, roomVersion, path) {
|
|
2950
3545
|
return path === undefined
|
|
@@ -2955,7 +3550,13 @@ export function composeRuntime(options) {
|
|
|
2955
3550
|
return journal.export();
|
|
2956
3551
|
},
|
|
2957
3552
|
persistence() {
|
|
2958
|
-
return
|
|
3553
|
+
return persistence === 'granted';
|
|
3554
|
+
},
|
|
3555
|
+
async retry() {
|
|
3556
|
+
journal.record('retry');
|
|
3557
|
+
unindexable.clear();
|
|
3558
|
+
queueReadiness();
|
|
3559
|
+
resume();
|
|
2959
3560
|
},
|
|
2960
3561
|
observe: observing(watch),
|
|
2961
3562
|
async reauthorize() {
|
|
@@ -2982,6 +3583,22 @@ export function composeRuntime(options) {
|
|
|
2982
3583
|
query: (plan) => rowsQuery(plan),
|
|
2983
3584
|
watchPlan,
|
|
2984
3585
|
search: searched,
|
|
3586
|
+
watchReadiness,
|
|
3587
|
+
};
|
|
3588
|
+
return {
|
|
3589
|
+
md,
|
|
3590
|
+
seed,
|
|
3591
|
+
stop,
|
|
3592
|
+
lose: loseRooms,
|
|
3593
|
+
watch,
|
|
3594
|
+
acquire,
|
|
3595
|
+
persisted(answer) {
|
|
3596
|
+
if (answer === persistence)
|
|
3597
|
+
return;
|
|
3598
|
+
persistence = answer;
|
|
3599
|
+
journal.record(`persistence ${answer}`);
|
|
3600
|
+
queueReadiness();
|
|
3601
|
+
},
|
|
3602
|
+
within: (reach, port) => narrowed(reachable, reach, port),
|
|
2985
3603
|
};
|
|
2986
|
-
return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2987
3604
|
}
|