@agentshouse/mdruntime 0.1.0-cb0eb0d87ff49 → 0.1.0-cb2e390ee7b89
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 +275 -285
- 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/earlier.d.ts +10 -0
- package/dist/chat/earlier.d.ts.map +1 -0
- package/dist/chat/earlier.js +54 -0
- 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 +42 -5
- 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 +34 -19
- package/dist/chat/question.d.ts +1 -1
- package/dist/chat/question.d.ts.map +1 -1
- package/dist/chat/question.js +7 -18
- 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/chat/style.js +1 -1
- package/dist/client.d.ts +8 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +16 -5
- package/dist/connection.d.ts +19 -3
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +741 -100
- package/dist/conversation.d.ts +7 -0
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +202 -39
- 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 +85 -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/observation.d.ts +2 -0
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +9 -3
- 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 +24 -1
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +32 -5
- package/dist/reach.d.ts +6 -3
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +18 -3
- 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 +169 -47
- 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 +5 -1
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +30 -5
- 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/save.d.ts +1 -0
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +17 -1
- package/dist/search.d.ts +28 -7
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +93 -13
- package/dist/seed.d.ts +16 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +75 -0
- package/dist/status.d.ts +4 -0
- package/dist/status.d.ts.map +1 -1
- package/dist/store.d.ts +26 -4
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +92 -25
- package/dist/windows.d.ts +18 -0
- package/dist/windows.d.ts.map +1 -0
- package/dist/windows.js +80 -0
- package/dist/wire.d.ts +144 -32
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +50 -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, plain, 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
|
-
import { buildBatch, joined, limitedIn, requeued } from './save.js';
|
|
20
|
-
import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership,
|
|
21
|
+
import { buildBatch, joined, limitedIn, queuedDocuments, requeued, } from './save.js';
|
|
22
|
+
import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, standing, } 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,15 @@ 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.paths === null &&
|
|
42
|
+
plan.hop === null &&
|
|
43
|
+
plan.backlinks === null);
|
|
44
|
+
}
|
|
32
45
|
function within(root, path) {
|
|
33
46
|
return root === '' || beneath(root, path);
|
|
34
47
|
}
|
|
@@ -46,6 +59,10 @@ const OPENING_LANE = 'opening';
|
|
|
46
59
|
const CONVERSATION_LANE = 'conversations';
|
|
47
60
|
const ROOM_READS = 256;
|
|
48
61
|
const ROOM_ATTEMPTS = 16;
|
|
62
|
+
function operationOf(input) {
|
|
63
|
+
const operation = input?.operation_id;
|
|
64
|
+
return typeof operation === 'string' ? operation : null;
|
|
65
|
+
}
|
|
49
66
|
const outside = new Map();
|
|
50
67
|
function outsideOf(roots) {
|
|
51
68
|
const key = JSON.stringify(roots);
|
|
@@ -85,6 +102,8 @@ export function composeRuntime(options) {
|
|
|
85
102
|
const replicas = new FencedMap(isFenced);
|
|
86
103
|
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
87
104
|
const pendings = new Map();
|
|
105
|
+
const seeds = new Map();
|
|
106
|
+
const seedWatchers = new Map();
|
|
88
107
|
const roomDrafts = new FencedMap(isFenced);
|
|
89
108
|
const presentations = new Map();
|
|
90
109
|
let limits = [];
|
|
@@ -97,6 +116,7 @@ export function composeRuntime(options) {
|
|
|
97
116
|
const liveModels = new FencedMap(isModelFenced);
|
|
98
117
|
const invalidModels = new Set();
|
|
99
118
|
const discovered = new Map();
|
|
119
|
+
const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
|
|
100
120
|
let roomsListed = false;
|
|
101
121
|
const scopedRefusals = new Map();
|
|
102
122
|
const sourceRefusals = new Map();
|
|
@@ -133,6 +153,7 @@ export function composeRuntime(options) {
|
|
|
133
153
|
let handed = false;
|
|
134
154
|
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
135
155
|
const repairing = new Set();
|
|
156
|
+
const calling = new Set();
|
|
136
157
|
const repairable = new Map();
|
|
137
158
|
let outcomeRetention = null;
|
|
138
159
|
const answering = new Map();
|
|
@@ -166,8 +187,18 @@ export function composeRuntime(options) {
|
|
|
166
187
|
const memberListeners = new Set();
|
|
167
188
|
const prepared = new Set();
|
|
168
189
|
const projecting = new Set();
|
|
169
|
-
const
|
|
190
|
+
const unpreparedDemand = new Set();
|
|
191
|
+
const unindexable = new Set();
|
|
170
192
|
let preparing = false;
|
|
193
|
+
let demand = WHOLE_DEMAND;
|
|
194
|
+
let narrowing = null;
|
|
195
|
+
let targets = {};
|
|
196
|
+
let persistence = options.shell.persistence;
|
|
197
|
+
let capacity = null;
|
|
198
|
+
const paused = new Set();
|
|
199
|
+
let rechecking = null;
|
|
200
|
+
const readinessWatches = new Set();
|
|
201
|
+
let readinessQueued = false;
|
|
171
202
|
const catching = new Set();
|
|
172
203
|
const transfers = new FencedMap(isFenced);
|
|
173
204
|
const conversations = createConversationStore({
|
|
@@ -243,6 +274,15 @@ export function composeRuntime(options) {
|
|
|
243
274
|
const value = presentations.get(app);
|
|
244
275
|
return value === undefined ? null : structuredClone(value);
|
|
245
276
|
}
|
|
277
|
+
function keepPresentation(app, next) {
|
|
278
|
+
return exclusive(async () => {
|
|
279
|
+
const local = await openLocal();
|
|
280
|
+
const stored = next(presentations.get(app));
|
|
281
|
+
await local.present({ app, value: stored });
|
|
282
|
+
presentations.set(app, stored);
|
|
283
|
+
publishChangeSet(changeSet('', null, null, { presentation: app }));
|
|
284
|
+
});
|
|
285
|
+
}
|
|
246
286
|
function emitPresentation(app) {
|
|
247
287
|
if (closed)
|
|
248
288
|
return;
|
|
@@ -267,9 +307,16 @@ export function composeRuntime(options) {
|
|
|
267
307
|
returned: structuredClone(record.returned),
|
|
268
308
|
});
|
|
269
309
|
}
|
|
310
|
+
for (const [room, refusal] of sourceRefusals)
|
|
311
|
+
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
270
312
|
attentionRequired.sort((left, right) => {
|
|
271
313
|
if (left.room !== right.room)
|
|
272
314
|
return left.room < right.room ? -1 : 1;
|
|
315
|
+
if ('refusal' in left || 'refusal' in right) {
|
|
316
|
+
if ('refusal' in left && 'refusal' in right)
|
|
317
|
+
return 0;
|
|
318
|
+
return 'refusal' in left ? -1 : 1;
|
|
319
|
+
}
|
|
273
320
|
if (left.path !== right.path)
|
|
274
321
|
return left.path < right.path ? -1 : 1;
|
|
275
322
|
return 0;
|
|
@@ -318,15 +365,18 @@ export function composeRuntime(options) {
|
|
|
318
365
|
}
|
|
319
366
|
function emitRooms() {
|
|
320
367
|
readSide.recover();
|
|
368
|
+
queueReadiness();
|
|
321
369
|
const value = listed();
|
|
322
370
|
for (const listener of roomListeners)
|
|
323
371
|
listener(value);
|
|
324
372
|
}
|
|
325
373
|
function emitRefusal() {
|
|
374
|
+
queueReadiness();
|
|
326
375
|
for (const listener of refusalListeners)
|
|
327
376
|
listener(refusal);
|
|
328
377
|
}
|
|
329
378
|
function emitStorageLoss() {
|
|
379
|
+
queueReadiness();
|
|
330
380
|
for (const listener of storageLossListeners)
|
|
331
381
|
listener(storageLoss);
|
|
332
382
|
}
|
|
@@ -343,7 +393,8 @@ export function composeRuntime(options) {
|
|
|
343
393
|
function show() {
|
|
344
394
|
if (closed)
|
|
345
395
|
return;
|
|
346
|
-
|
|
396
|
+
queueReadiness();
|
|
397
|
+
const next = status.state === 'synced' && demand.conversations && !conversations.current()
|
|
347
398
|
? { ...status, state: 'syncing' }
|
|
348
399
|
: status;
|
|
349
400
|
if (same(shown, next))
|
|
@@ -420,13 +471,19 @@ export function composeRuntime(options) {
|
|
|
420
471
|
}
|
|
421
472
|
conversations.changed(mergeChangeSets(entered));
|
|
422
473
|
readSide.recover();
|
|
474
|
+
queueReadiness();
|
|
423
475
|
}
|
|
424
476
|
function unprepared() {
|
|
425
|
-
return [...
|
|
477
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
|
|
478
|
+
}
|
|
479
|
+
function unbuilt(locator, error) {
|
|
480
|
+
unindexable.add(locator);
|
|
481
|
+
journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
|
|
482
|
+
queueReadiness();
|
|
426
483
|
}
|
|
427
484
|
function prepare(locators = []) {
|
|
428
485
|
for (const locator of locators)
|
|
429
|
-
|
|
486
|
+
unpreparedDemand.add(locator);
|
|
430
487
|
if (preparing)
|
|
431
488
|
return;
|
|
432
489
|
preparing = true;
|
|
@@ -436,8 +493,8 @@ export function composeRuntime(options) {
|
|
|
436
493
|
await yielded();
|
|
437
494
|
if (closed)
|
|
438
495
|
return;
|
|
439
|
-
|
|
440
|
-
await prepareReplica(waiting);
|
|
496
|
+
unpreparedDemand.delete(waiting);
|
|
497
|
+
await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
|
|
441
498
|
}
|
|
442
499
|
}
|
|
443
500
|
finally {
|
|
@@ -455,7 +512,8 @@ export function composeRuntime(options) {
|
|
|
455
512
|
return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
|
|
456
513
|
}
|
|
457
514
|
function awaited(locator) {
|
|
458
|
-
return
|
|
515
|
+
return (!roomsListed ||
|
|
516
|
+
(rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
|
|
459
517
|
}
|
|
460
518
|
function covered(plan) {
|
|
461
519
|
const locators = reachedRooms(plan);
|
|
@@ -495,18 +553,339 @@ export function composeRuntime(options) {
|
|
|
495
553
|
if (waiting.length > 0)
|
|
496
554
|
prepare(waiting.map(({ room }) => room));
|
|
497
555
|
}
|
|
556
|
+
function acquires(room) {
|
|
557
|
+
return (demanded(demand, room) ||
|
|
558
|
+
(replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
|
|
559
|
+
queuesWithoutReplica.has(room.locator));
|
|
560
|
+
}
|
|
561
|
+
function acquiredLocators() {
|
|
562
|
+
return [...discovered.values()]
|
|
563
|
+
.filter(acquires)
|
|
564
|
+
.map((room) => room.locator)
|
|
565
|
+
.sort();
|
|
566
|
+
}
|
|
567
|
+
function heldLocators(acquired) {
|
|
568
|
+
return demand.whole ? acquired : [...new Set([...acquired, ...demand.rooms])].sort();
|
|
569
|
+
}
|
|
570
|
+
function heldDemand(acquired) {
|
|
571
|
+
if (demand.whole)
|
|
572
|
+
return null;
|
|
573
|
+
return {
|
|
574
|
+
rooms: heldLocators(acquired.map((room) => room.locator)),
|
|
575
|
+
conversations: demand.conversations,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
function applyDemand(next) {
|
|
579
|
+
const before = heldLocators(acquiredLocators());
|
|
580
|
+
const { whole, conversations: conversed } = demand;
|
|
581
|
+
demand = next;
|
|
582
|
+
const after = heldLocators(acquiredLocators());
|
|
583
|
+
queueReadiness();
|
|
584
|
+
if (!conversed && next.conversations)
|
|
585
|
+
scheduleCurrency();
|
|
586
|
+
if (JSON.stringify(before) === JSON.stringify(after) &&
|
|
587
|
+
whole === next.whole &&
|
|
588
|
+
conversed === next.conversations)
|
|
589
|
+
return;
|
|
590
|
+
journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
|
|
591
|
+
if (options.adapter.roomStream === undefined)
|
|
592
|
+
void sync('unstreamed');
|
|
593
|
+
else if (socketOpen)
|
|
594
|
+
options.adapter.roomStream.reopen();
|
|
595
|
+
}
|
|
596
|
+
function acquire(next) {
|
|
597
|
+
if (closed)
|
|
598
|
+
return;
|
|
599
|
+
if (narrowing !== null)
|
|
600
|
+
clearTimeout(narrowing);
|
|
601
|
+
narrowing = null;
|
|
602
|
+
const widening = wider(next, demand);
|
|
603
|
+
if (!widening && !narrower(next, demand))
|
|
604
|
+
return;
|
|
605
|
+
if (widening || !roomsListed) {
|
|
606
|
+
applyDemand(next);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
// A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
|
|
610
|
+
narrowing = setTimeout(() => {
|
|
611
|
+
narrowing = null;
|
|
612
|
+
applyDemand(next);
|
|
613
|
+
}, 5_000);
|
|
614
|
+
}
|
|
615
|
+
function requiredRooms(reach) {
|
|
616
|
+
if (reach === null)
|
|
617
|
+
return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
|
|
618
|
+
const named = [...new Set(reach.rooms)].sort();
|
|
619
|
+
if (roomsListed)
|
|
620
|
+
return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
|
|
621
|
+
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
622
|
+
}
|
|
623
|
+
function roomState(locator, search) {
|
|
624
|
+
if (sourceRefusals.has(locator))
|
|
625
|
+
return 'refused';
|
|
626
|
+
const listedRoom = discovered.get(locator);
|
|
627
|
+
if (roomsListed &&
|
|
628
|
+
(listedRoom === undefined ||
|
|
629
|
+
listedRoom.replica.admission !== 'admitted' ||
|
|
630
|
+
unreadable.has(locator)))
|
|
631
|
+
return 'unadmitted';
|
|
632
|
+
if (!replicas.has(locator))
|
|
633
|
+
return 'acquiring';
|
|
634
|
+
if (!prepared.has(locator))
|
|
635
|
+
return 'preparing';
|
|
636
|
+
if (search && !searchIndex.rooms().has(locator))
|
|
637
|
+
return 'indexing';
|
|
638
|
+
return 'complete';
|
|
639
|
+
}
|
|
640
|
+
function conversationCoverage(reach, search) {
|
|
641
|
+
const declared = reach === null || reach.conversations;
|
|
642
|
+
if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
|
|
643
|
+
return 'undeclared';
|
|
644
|
+
if (!conversations.known() || !conversations.current())
|
|
645
|
+
return 'acquiring';
|
|
646
|
+
if (!search)
|
|
647
|
+
return 'complete';
|
|
648
|
+
const unindexed = conversations
|
|
649
|
+
.searchable()
|
|
650
|
+
.filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
|
|
651
|
+
if (unindexed.length === 0)
|
|
652
|
+
return 'complete';
|
|
653
|
+
index([], unindexed);
|
|
654
|
+
return 'indexing';
|
|
655
|
+
}
|
|
656
|
+
function connectionOf() {
|
|
657
|
+
if (socketOpen)
|
|
658
|
+
return 'connected';
|
|
659
|
+
if (status.providerUnreachable)
|
|
660
|
+
return 'unreachable';
|
|
661
|
+
return options.adapter.roomStream === undefined && status.lastCompleted !== null
|
|
662
|
+
? 'connected'
|
|
663
|
+
: 'connecting';
|
|
664
|
+
}
|
|
665
|
+
function refusedIn(locators) {
|
|
666
|
+
return locators.flatMap((room) => {
|
|
667
|
+
const held = sourceRefusals.get(room);
|
|
668
|
+
if (held === undefined)
|
|
669
|
+
return [];
|
|
670
|
+
const { file, location } = held.fields;
|
|
671
|
+
return [
|
|
672
|
+
{
|
|
673
|
+
room,
|
|
674
|
+
file: typeof file === 'string' ? file : '',
|
|
675
|
+
rule: held.code,
|
|
676
|
+
location: typeof location === 'string' ? location : null,
|
|
677
|
+
},
|
|
678
|
+
];
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
function readinessFor(port, reach) {
|
|
682
|
+
const required = requiredRooms(reach);
|
|
683
|
+
const search = port.search === 'required';
|
|
684
|
+
const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
|
|
685
|
+
const building = (state) => coverage
|
|
686
|
+
.filter((room) => room.state === state && !unindexable.has(room.room))
|
|
687
|
+
.map(({ room }) => room);
|
|
688
|
+
const preparing = building('preparing');
|
|
689
|
+
if (preparing.length > 0)
|
|
690
|
+
prepare(preparing);
|
|
691
|
+
const indexing = building('indexing');
|
|
692
|
+
if (indexing.length > 0)
|
|
693
|
+
index(indexing, []);
|
|
694
|
+
return readinessOf({
|
|
695
|
+
code: port.code,
|
|
696
|
+
coverage: {
|
|
697
|
+
listed: required.listed,
|
|
698
|
+
rooms: coverage,
|
|
699
|
+
conversations: conversationCoverage(reach, search),
|
|
700
|
+
search: search ? 'required' : 'unused',
|
|
701
|
+
},
|
|
702
|
+
persistence,
|
|
703
|
+
connection: connectionOf(),
|
|
704
|
+
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
705
|
+
capacity: capacity !== null &&
|
|
706
|
+
coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
|
|
707
|
+
? capacity
|
|
708
|
+
: port.capacity,
|
|
709
|
+
refused: refusedIn(required.rooms),
|
|
710
|
+
lost: storageLoss !== null,
|
|
711
|
+
established: targets[port.target]?.established === true,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
function establish(name) {
|
|
715
|
+
targets = { ...targets, [name]: { established: true } };
|
|
716
|
+
queueReadiness();
|
|
717
|
+
void durably((local) => local.keepTargets(targets)).catch(() => {
|
|
718
|
+
journal.record(`target record failed ${name}`);
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
function deliverReadiness(watch) {
|
|
722
|
+
if (!readinessWatches.has(watch))
|
|
723
|
+
return;
|
|
724
|
+
const readiness = readinessFor(watch.port, watch.reach);
|
|
725
|
+
if (readiness.ready && targets[watch.port.target]?.established !== true) {
|
|
726
|
+
establish(watch.port.target);
|
|
727
|
+
}
|
|
728
|
+
const shown = JSON.stringify(readiness);
|
|
729
|
+
if (shown === watch.last)
|
|
730
|
+
return;
|
|
731
|
+
watch.last = shown;
|
|
732
|
+
watch.deliver(structuredClone(readiness));
|
|
733
|
+
}
|
|
734
|
+
function queueReadiness() {
|
|
735
|
+
if (readinessQueued || closed || readinessWatches.size === 0)
|
|
736
|
+
return;
|
|
737
|
+
readinessQueued = true;
|
|
738
|
+
queueMicrotask(() => {
|
|
739
|
+
readinessQueued = false;
|
|
740
|
+
if (closed)
|
|
741
|
+
return;
|
|
742
|
+
for (const watch of [...readinessWatches])
|
|
743
|
+
deliverReadiness(watch);
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
function watchReadiness(port, reach, deliver) {
|
|
747
|
+
const watch = { port, reach, deliver, last: null };
|
|
748
|
+
readinessWatches.add(watch);
|
|
749
|
+
void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
|
|
750
|
+
return () => {
|
|
751
|
+
readinessWatches.delete(watch);
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
function layerBytes(layer) {
|
|
755
|
+
let bytes = 0;
|
|
756
|
+
for (const [path, text] of Object.entries(layer.documents))
|
|
757
|
+
bytes += (path.length + text.length) * 2;
|
|
758
|
+
return bytes;
|
|
759
|
+
}
|
|
760
|
+
function grownBy(next, previous) {
|
|
761
|
+
if (previous === undefined)
|
|
762
|
+
return layerBytes(next);
|
|
763
|
+
const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
|
|
764
|
+
return layerBytes({ ...next, documents });
|
|
765
|
+
}
|
|
766
|
+
function exceeded(error) {
|
|
767
|
+
return (typeof error === 'object' &&
|
|
768
|
+
error !== null &&
|
|
769
|
+
error.name === 'QuotaExceededError');
|
|
770
|
+
}
|
|
771
|
+
async function estimated() {
|
|
772
|
+
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
773
|
+
return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
|
|
774
|
+
}
|
|
775
|
+
function pause(locator, cause) {
|
|
776
|
+
paused.add(locator);
|
|
777
|
+
capacity = cause;
|
|
778
|
+
journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
|
|
779
|
+
queueReadiness();
|
|
780
|
+
if (rechecking !== null)
|
|
781
|
+
return;
|
|
782
|
+
// A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
|
|
783
|
+
rechecking = setInterval(() => {
|
|
784
|
+
void recheck();
|
|
785
|
+
}, 60_000);
|
|
786
|
+
}
|
|
787
|
+
async function recheck() {
|
|
788
|
+
const held = capacity;
|
|
789
|
+
if (held === null || closed)
|
|
790
|
+
return;
|
|
791
|
+
const available = await estimated().catch(() => null);
|
|
792
|
+
if (available === null || available < (held.needed ?? 0))
|
|
793
|
+
return;
|
|
794
|
+
if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
|
|
795
|
+
return;
|
|
796
|
+
resume();
|
|
797
|
+
}
|
|
798
|
+
function resume() {
|
|
799
|
+
if (rechecking !== null)
|
|
800
|
+
clearInterval(rechecking);
|
|
801
|
+
rechecking = null;
|
|
802
|
+
if (paused.size === 0 && capacity === null)
|
|
803
|
+
return;
|
|
804
|
+
paused.clear();
|
|
805
|
+
capacity = null;
|
|
806
|
+
journal.record('capacity resumed');
|
|
807
|
+
queueReadiness();
|
|
808
|
+
if (!closed && authorization !== undefined)
|
|
809
|
+
void sync('unstreamed');
|
|
810
|
+
}
|
|
811
|
+
async function reclaimed(local) {
|
|
812
|
+
const unreferenced = [...transfers.keys()].filter((locator) => {
|
|
813
|
+
const room = discovered.get(locator);
|
|
814
|
+
return room === undefined || !acquires(room);
|
|
815
|
+
});
|
|
816
|
+
for (const locator of unreferenced) {
|
|
817
|
+
await local.abandon(locator);
|
|
818
|
+
transfers.delete(locator);
|
|
819
|
+
}
|
|
820
|
+
if (unreferenced.length > 0)
|
|
821
|
+
journal.record(`capacity reclaimed ${unreferenced.length}`);
|
|
822
|
+
return unreferenced.length > 0;
|
|
823
|
+
}
|
|
824
|
+
async function admitted(local, locator, needed) {
|
|
825
|
+
if (paused.has(locator))
|
|
826
|
+
return false;
|
|
827
|
+
let available = await estimated().catch(() => null);
|
|
828
|
+
if (available === null || available >= needed)
|
|
829
|
+
return true;
|
|
830
|
+
if (await reclaimed(local))
|
|
831
|
+
available = await estimated().catch(() => null);
|
|
832
|
+
if (available === null || available >= needed)
|
|
833
|
+
return true;
|
|
834
|
+
pause(locator, { cause: 'insufficient-space', needed, available });
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
async function exceededOn(locator, bytes) {
|
|
838
|
+
const available = await estimated().catch(() => null);
|
|
839
|
+
pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
|
|
840
|
+
}
|
|
841
|
+
function seedRoomHeld(room, version) {
|
|
842
|
+
return prepared.has(room) && reachedBy(room, version);
|
|
843
|
+
}
|
|
844
|
+
function seedHeld(at) {
|
|
845
|
+
return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
|
|
846
|
+
}
|
|
847
|
+
function seedApplies(plan) {
|
|
848
|
+
const seed = seeds.get(plan.key);
|
|
849
|
+
if (seed === undefined)
|
|
850
|
+
return null;
|
|
851
|
+
if (!seedHeld(seed.at))
|
|
852
|
+
return seed;
|
|
853
|
+
seeds.delete(plan.key);
|
|
854
|
+
return null;
|
|
855
|
+
}
|
|
856
|
+
function seedBehind(plan, cover) {
|
|
857
|
+
const seed = seedApplies(plan);
|
|
858
|
+
if (seed === null || 'pending' in cover)
|
|
859
|
+
return cover;
|
|
860
|
+
return {
|
|
861
|
+
pending: Object.entries(seed.at)
|
|
862
|
+
.filter(([room, version]) => !seedRoomHeld(room, version))
|
|
863
|
+
.map(([room]) => ({ room, awaits: 'acquisition' })),
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
function seed(entry) {
|
|
867
|
+
if (seedHeld(entry.at))
|
|
868
|
+
return;
|
|
869
|
+
for (const { query, rows } of entry.queries) {
|
|
870
|
+
const key = queryKey(query);
|
|
871
|
+
seeds.set(key, { rows, at: entry.at });
|
|
872
|
+
for (const shown of seedWatchers.get(key) ?? [])
|
|
873
|
+
shown();
|
|
874
|
+
}
|
|
875
|
+
}
|
|
498
876
|
async function rowsQuery(plan) {
|
|
499
877
|
const before = covered(plan);
|
|
500
878
|
awaitPreparation(before);
|
|
879
|
+
const seeded = seedApplies(plan);
|
|
880
|
+
if (seeded !== null)
|
|
881
|
+
return seededResult(seeded).result;
|
|
501
882
|
if ('pending' in before)
|
|
502
883
|
return before;
|
|
503
884
|
const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
|
|
504
885
|
awaitPreparation(cover);
|
|
505
886
|
if ('pending' in cover)
|
|
506
887
|
return cover;
|
|
507
|
-
return
|
|
508
|
-
? { groups: rows.groups, coverage: cover.coverage }
|
|
509
|
-
: { documents: rows.documents, coverage: cover.coverage };
|
|
888
|
+
return { ...rows, coverage: cover.coverage };
|
|
510
889
|
}
|
|
511
890
|
async function indexConversation(conversation) {
|
|
512
891
|
searchIndex.indexMessages(conversation, [], false);
|
|
@@ -522,10 +901,12 @@ export function composeRuntime(options) {
|
|
|
522
901
|
}
|
|
523
902
|
}
|
|
524
903
|
function indexNext() {
|
|
525
|
-
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator));
|
|
904
|
+
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
|
|
526
905
|
if (room !== undefined) {
|
|
527
906
|
roomsToIndex.delete(room);
|
|
528
|
-
return () =>
|
|
907
|
+
return () => prepared.has(room)
|
|
908
|
+
? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
|
|
909
|
+
: undefined;
|
|
529
910
|
}
|
|
530
911
|
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
531
912
|
if (conversation === undefined)
|
|
@@ -546,6 +927,7 @@ export function composeRuntime(options) {
|
|
|
546
927
|
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
547
928
|
await yielded();
|
|
548
929
|
await unit();
|
|
930
|
+
queueReadiness();
|
|
549
931
|
}
|
|
550
932
|
}
|
|
551
933
|
finally {
|
|
@@ -653,6 +1035,9 @@ export function composeRuntime(options) {
|
|
|
653
1035
|
if (fence !== undefined && fence.erased && fence.at < started)
|
|
654
1036
|
fences.delete(room.locator);
|
|
655
1037
|
}
|
|
1038
|
+
await loseRooms(found
|
|
1039
|
+
.filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
|
|
1040
|
+
.map((room) => room.locator));
|
|
656
1041
|
const listed = found.filter((room) => !fences.has(room.locator));
|
|
657
1042
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
658
1043
|
for (const locator of listedLocators)
|
|
@@ -677,6 +1062,12 @@ export function composeRuntime(options) {
|
|
|
677
1062
|
emitRooms();
|
|
678
1063
|
return listed;
|
|
679
1064
|
}
|
|
1065
|
+
function heldLocally(locator) {
|
|
1066
|
+
return (replicas.has(locator) ||
|
|
1067
|
+
roomDrafts.has(locator) ||
|
|
1068
|
+
transfers.has(locator) ||
|
|
1069
|
+
queuesWithoutReplica.has(locator));
|
|
1070
|
+
}
|
|
680
1071
|
function unlistedHeld(listed) {
|
|
681
1072
|
return [
|
|
682
1073
|
...new Set([
|
|
@@ -719,6 +1110,8 @@ export function composeRuntime(options) {
|
|
|
719
1110
|
unreadable.delete(locator);
|
|
720
1111
|
stale.delete(locator);
|
|
721
1112
|
repairable.delete(locator);
|
|
1113
|
+
unindexable.delete(locator);
|
|
1114
|
+
paused.delete(locator);
|
|
722
1115
|
scopedRefusals.delete(locator);
|
|
723
1116
|
sourceRefusals.delete(locator);
|
|
724
1117
|
declarationRefusals.delete(locator);
|
|
@@ -772,7 +1165,7 @@ export function composeRuntime(options) {
|
|
|
772
1165
|
return model;
|
|
773
1166
|
}
|
|
774
1167
|
async function hydrate(opened) {
|
|
775
|
-
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
|
|
1168
|
+
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
|
|
776
1169
|
opened.all(),
|
|
777
1170
|
opened.queues(),
|
|
778
1171
|
opened.roomDrafts(),
|
|
@@ -784,7 +1177,9 @@ export function composeRuntime(options) {
|
|
|
784
1177
|
opened.capabilities(),
|
|
785
1178
|
opened.member(),
|
|
786
1179
|
opened.transfers(),
|
|
1180
|
+
opened.targets(),
|
|
787
1181
|
]);
|
|
1182
|
+
targets = heldTargets;
|
|
788
1183
|
transfers.clear();
|
|
789
1184
|
for (const record of staged)
|
|
790
1185
|
transfers.set(record.locator, record);
|
|
@@ -908,8 +1303,9 @@ export function composeRuntime(options) {
|
|
|
908
1303
|
}
|
|
909
1304
|
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
910
1305
|
const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
911
|
-
const withdrawn =
|
|
912
|
-
const
|
|
1306
|
+
const { withdrawn, kept } = standing(confirmed, queue, withdrawable, localSaves);
|
|
1307
|
+
const founded = kept.filter((batch) => !queue.includes(batch));
|
|
1308
|
+
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
913
1309
|
const unqueued = withdrawn.map((batch) => ({
|
|
914
1310
|
batch,
|
|
915
1311
|
returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
|
|
@@ -933,9 +1329,10 @@ export function composeRuntime(options) {
|
|
|
933
1329
|
});
|
|
934
1330
|
}
|
|
935
1331
|
}
|
|
936
|
-
await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
|
|
1332
|
+
await local.keepAuthored({ locator, queue: kept, founded, roomDrafts: [...written.values()] });
|
|
1333
|
+
const staying = new Set(kept.map((batch) => batch.id));
|
|
937
1334
|
for (const batch of previousQueue) {
|
|
938
|
-
if (!
|
|
1335
|
+
if (!staying.has(batch.id))
|
|
939
1336
|
pendings.delete(batch.id);
|
|
940
1337
|
}
|
|
941
1338
|
for (const record of written.values())
|
|
@@ -956,7 +1353,7 @@ export function composeRuntime(options) {
|
|
|
956
1353
|
else
|
|
957
1354
|
queuesWithoutReplica.delete(locator);
|
|
958
1355
|
publishChangeSet(changeSet(locator, before ?? null, held, {
|
|
959
|
-
queue:
|
|
1356
|
+
queue: queueChanged,
|
|
960
1357
|
drafts: [...written.keys()],
|
|
961
1358
|
}));
|
|
962
1359
|
moveTo({ ...status, ...attention() });
|
|
@@ -971,7 +1368,7 @@ export function composeRuntime(options) {
|
|
|
971
1368
|
replicas.set(locator, replicaOf(confirmed, kept, before));
|
|
972
1369
|
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
973
1370
|
room: settling.room,
|
|
974
|
-
queue:
|
|
1371
|
+
queue: queueChanged,
|
|
975
1372
|
drafts: [...written.keys()],
|
|
976
1373
|
});
|
|
977
1374
|
};
|
|
@@ -1030,7 +1427,18 @@ export function composeRuntime(options) {
|
|
|
1030
1427
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
1031
1428
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
1032
1429
|
});
|
|
1033
|
-
|
|
1430
|
+
let settled;
|
|
1431
|
+
try {
|
|
1432
|
+
settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
|
|
1433
|
+
}
|
|
1434
|
+
catch (error) {
|
|
1435
|
+
if (!exceeded(error))
|
|
1436
|
+
throw error;
|
|
1437
|
+
await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
|
|
1438
|
+
caughtUpTo(room.locator);
|
|
1439
|
+
return true;
|
|
1440
|
+
}
|
|
1441
|
+
const set = settled();
|
|
1034
1442
|
if (stored !== undefined) {
|
|
1035
1443
|
transfers.delete(room.locator);
|
|
1036
1444
|
catching.delete(room.locator);
|
|
@@ -1038,6 +1446,7 @@ export function composeRuntime(options) {
|
|
|
1038
1446
|
for (const batch of accepted)
|
|
1039
1447
|
pendings.delete(batch.id);
|
|
1040
1448
|
stale.delete(room.locator);
|
|
1449
|
+
unindexable.delete(room.locator);
|
|
1041
1450
|
if (unreadable.delete(room.locator))
|
|
1042
1451
|
scopedRefusals.delete(room.locator);
|
|
1043
1452
|
sourceRefusals.delete(room.locator);
|
|
@@ -1100,6 +1509,7 @@ export function composeRuntime(options) {
|
|
|
1100
1509
|
visible: replica.visible,
|
|
1101
1510
|
changes,
|
|
1102
1511
|
bases: parts.flatMap(({ bases }) => bases),
|
|
1512
|
+
queued: queuedDocuments(replica.queue),
|
|
1103
1513
|
writtenBy,
|
|
1104
1514
|
clean: {
|
|
1105
1515
|
text: replica.confirmed.record.documents,
|
|
@@ -1107,7 +1517,9 @@ export function composeRuntime(options) {
|
|
|
1107
1517
|
composition: compositionOf(replica.confirmed.record),
|
|
1108
1518
|
},
|
|
1109
1519
|
};
|
|
1110
|
-
const checked = withdrawing
|
|
1520
|
+
const checked = withdrawing
|
|
1521
|
+
? buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
|
|
1522
|
+
: null;
|
|
1111
1523
|
if (checked?.kind === 'refused')
|
|
1112
1524
|
return { outcome: 'refused', ...checked.refusal };
|
|
1113
1525
|
const owners = [...requeue.owners].flatMap((origin) => {
|
|
@@ -1130,6 +1542,7 @@ export function composeRuntime(options) {
|
|
|
1130
1542
|
...input,
|
|
1131
1543
|
visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
|
|
1132
1544
|
changes: requeue.changes,
|
|
1545
|
+
queued: queuedDocuments(requeue.queue),
|
|
1133
1546
|
});
|
|
1134
1547
|
if (built?.kind === 'refused')
|
|
1135
1548
|
return { outcome: 'refused', ...built.refusal };
|
|
@@ -1281,7 +1694,7 @@ export function composeRuntime(options) {
|
|
|
1281
1694
|
const scope = options.adapter.referrerScope(request.referrer);
|
|
1282
1695
|
return scope?.kind === 'room' ? scope.locator : null;
|
|
1283
1696
|
}
|
|
1284
|
-
async function
|
|
1697
|
+
async function transferBinary(request, transfer, answer) {
|
|
1285
1698
|
if (closed || refusal !== null)
|
|
1286
1699
|
return UNCERTAIN;
|
|
1287
1700
|
try {
|
|
@@ -1297,40 +1710,41 @@ export function composeRuntime(options) {
|
|
|
1297
1710
|
? `transfer binary ${request.locator} ${request.path}`
|
|
1298
1711
|
: `transfer binary ${request.fileRef}`);
|
|
1299
1712
|
try {
|
|
1300
|
-
const result = await
|
|
1301
|
-
|
|
1713
|
+
const result = await transfer(authorization);
|
|
1714
|
+
const settled = result;
|
|
1715
|
+
journal.record(`outcome binary ${settled.kind}`);
|
|
1302
1716
|
const locator = binaryRoom(request);
|
|
1303
1717
|
if (locator !== null && fencedSince(locator, since))
|
|
1304
1718
|
return failure({ kind: 'room-not-readable', locator });
|
|
1305
|
-
if (
|
|
1306
|
-
const room = replicas.get(
|
|
1719
|
+
if (settled.kind === 'scoped-refusal') {
|
|
1720
|
+
const room = replicas.get(settled.locator)?.confirmed.record;
|
|
1307
1721
|
if (room !== undefined)
|
|
1308
|
-
await scoped(
|
|
1722
|
+
await scoped(settled, room, await openLocal());
|
|
1309
1723
|
}
|
|
1310
|
-
if (
|
|
1724
|
+
if (settled.kind === 'revoked-authorization')
|
|
1311
1725
|
await refuse('revoked-authorization');
|
|
1312
|
-
return
|
|
1726
|
+
return answer(result);
|
|
1313
1727
|
}
|
|
1314
1728
|
catch {
|
|
1315
1729
|
journal.record('outcome binary transport-uncertainty');
|
|
1316
1730
|
return UNCERTAIN;
|
|
1317
1731
|
}
|
|
1318
1732
|
}
|
|
1319
|
-
|
|
1320
|
-
|
|
1733
|
+
function readBinary(request) {
|
|
1734
|
+
return transferBinary(request, (held) => options.adapter.binary(held, request), binaryAnswer);
|
|
1735
|
+
}
|
|
1736
|
+
function binaryPath(locator, path, revision) {
|
|
1321
1737
|
if (revision !== undefined)
|
|
1322
|
-
return
|
|
1738
|
+
return { kind: 'path', locator, path, revision };
|
|
1323
1739
|
const visible = replicas.get(locator)?.visible;
|
|
1324
1740
|
const held = visible?.binaries[path];
|
|
1325
1741
|
if (visible === undefined || held === undefined)
|
|
1326
|
-
return
|
|
1327
|
-
if (held.state === 'pending')
|
|
1328
|
-
|
|
1329
|
-
return blob === null ? absentBinary(path) : { kind: 'binary', blob };
|
|
1330
|
-
}
|
|
1742
|
+
return null;
|
|
1743
|
+
if (held.state === 'pending')
|
|
1744
|
+
return { kind: 'pending', localId: held.localId };
|
|
1331
1745
|
const record = binaryRecord(path, held, visible.revisions[path]);
|
|
1332
1746
|
if (record.state !== 'accepted')
|
|
1333
|
-
return
|
|
1747
|
+
return null;
|
|
1334
1748
|
const replica = replicas.get(locator);
|
|
1335
1749
|
const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
|
|
1336
1750
|
const holds = (at) => {
|
|
@@ -1345,13 +1759,31 @@ export function composeRuntime(options) {
|
|
|
1345
1759
|
if (change.kind === 'move' && change.to === confirmed)
|
|
1346
1760
|
confirmed = change.path;
|
|
1347
1761
|
}
|
|
1348
|
-
return
|
|
1762
|
+
return {
|
|
1349
1763
|
kind: 'path',
|
|
1350
1764
|
locator,
|
|
1351
1765
|
path: confirmed,
|
|
1352
1766
|
revision: record.revision,
|
|
1353
1767
|
fileRef: record.fileRef,
|
|
1354
|
-
}
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
async function readBinaryPath(locator, path, revision) {
|
|
1771
|
+
await ready;
|
|
1772
|
+
const asked = binaryPath(locator, path, revision);
|
|
1773
|
+
if (asked === null)
|
|
1774
|
+
return absentBinary(path);
|
|
1775
|
+
if (asked.kind === 'pending') {
|
|
1776
|
+
const blob = queuedBlob(locator, path, asked.localId);
|
|
1777
|
+
return blob === null ? absentBinary(path) : { kind: 'binary', blob };
|
|
1778
|
+
}
|
|
1779
|
+
return readBinary(asked);
|
|
1780
|
+
}
|
|
1781
|
+
async function linkBinaryPath(locator, path, revision) {
|
|
1782
|
+
await ready;
|
|
1783
|
+
const asked = binaryPath(locator, path, revision);
|
|
1784
|
+
if (asked === null || asked.kind === 'pending')
|
|
1785
|
+
return absentBinary(path);
|
|
1786
|
+
return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
|
|
1355
1787
|
}
|
|
1356
1788
|
async function readBinaryReference(reference) {
|
|
1357
1789
|
await ready;
|
|
@@ -1377,7 +1809,7 @@ export function composeRuntime(options) {
|
|
|
1377
1809
|
}
|
|
1378
1810
|
return undefined;
|
|
1379
1811
|
}
|
|
1380
|
-
async function invokeAction(name, input) {
|
|
1812
|
+
async function invokeAction(name, input, dispatching) {
|
|
1381
1813
|
await ready;
|
|
1382
1814
|
if (closed || refusal !== null)
|
|
1383
1815
|
return UNCERTAIN;
|
|
@@ -1391,6 +1823,7 @@ export function composeRuntime(options) {
|
|
|
1391
1823
|
}
|
|
1392
1824
|
if (closed || authorization === undefined || refusal !== null)
|
|
1393
1825
|
return UNCERTAIN;
|
|
1826
|
+
await dispatching?.();
|
|
1394
1827
|
journal.record(`transfer action ${name}`);
|
|
1395
1828
|
try {
|
|
1396
1829
|
const result = await options.adapter.action(authorization, { name, input });
|
|
@@ -1411,6 +1844,60 @@ export function composeRuntime(options) {
|
|
|
1411
1844
|
return UNCERTAIN;
|
|
1412
1845
|
}
|
|
1413
1846
|
}
|
|
1847
|
+
function unsettled(result) {
|
|
1848
|
+
return result.kind !== 'action' && result.retryable;
|
|
1849
|
+
}
|
|
1850
|
+
async function recoveredAction(name, input) {
|
|
1851
|
+
const operation = operationOf(input);
|
|
1852
|
+
if (operation === null)
|
|
1853
|
+
return invokeAction(name, input);
|
|
1854
|
+
const kept = async () => (await openLocal()).keepCall({ operation, name, input });
|
|
1855
|
+
calling.add(operation);
|
|
1856
|
+
try {
|
|
1857
|
+
let result = await invokeAction(name, input, kept);
|
|
1858
|
+
if (unsettled(result)) {
|
|
1859
|
+
await recovery.attempt(`action ${operation}`, async () => {
|
|
1860
|
+
if (closed || refusal !== null)
|
|
1861
|
+
return 'done';
|
|
1862
|
+
if (!socketOpen)
|
|
1863
|
+
return 'again';
|
|
1864
|
+
result = await invokeAction(name, input, kept);
|
|
1865
|
+
return unsettled(result) ? 'again' : 'done';
|
|
1866
|
+
}, { again: true });
|
|
1867
|
+
}
|
|
1868
|
+
if (!unsettled(result))
|
|
1869
|
+
await (await openLocal()).dropCall(operation);
|
|
1870
|
+
return result;
|
|
1871
|
+
}
|
|
1872
|
+
finally {
|
|
1873
|
+
calling.delete(operation);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
async function resendCalls(signal) {
|
|
1877
|
+
if (!socketOpen || closed || stopping)
|
|
1878
|
+
return 'done';
|
|
1879
|
+
const local = await openLocal();
|
|
1880
|
+
for (const held of await local.calls()) {
|
|
1881
|
+
if (calling.has(held.operation))
|
|
1882
|
+
continue;
|
|
1883
|
+
if (closed || signal.aborted)
|
|
1884
|
+
return 'done';
|
|
1885
|
+
if (pastHorizon(held.operation)) {
|
|
1886
|
+
await local.dropCall(held.operation);
|
|
1887
|
+
continue;
|
|
1888
|
+
}
|
|
1889
|
+
calling.add(held.operation);
|
|
1890
|
+
try {
|
|
1891
|
+
if (unsettled(await invokeAction(held.name, held.input)))
|
|
1892
|
+
return 'again';
|
|
1893
|
+
await local.dropCall(held.operation);
|
|
1894
|
+
}
|
|
1895
|
+
finally {
|
|
1896
|
+
calling.delete(held.operation);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
return 'done';
|
|
1900
|
+
}
|
|
1414
1901
|
function continuing() {
|
|
1415
1902
|
return !closed && !discoveryPaused;
|
|
1416
1903
|
}
|
|
@@ -1428,8 +1915,9 @@ export function composeRuntime(options) {
|
|
|
1428
1915
|
async function pullRefresh(authorized, confirmed, live) {
|
|
1429
1916
|
if (!live())
|
|
1430
1917
|
return unconfirmed;
|
|
1918
|
+
const turn = generation;
|
|
1431
1919
|
try {
|
|
1432
|
-
const result = await refreshRoom(
|
|
1920
|
+
const result = await refreshRoom(reads, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
|
|
1433
1921
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
1434
1922
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
1435
1923
|
return result;
|
|
@@ -1438,6 +1926,9 @@ export function composeRuntime(options) {
|
|
|
1438
1926
|
journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
|
|
1439
1927
|
throw error;
|
|
1440
1928
|
}
|
|
1929
|
+
finally {
|
|
1930
|
+
reads.finished(confirmed.record.locator, turn);
|
|
1931
|
+
}
|
|
1441
1932
|
}
|
|
1442
1933
|
function staging(local, live) {
|
|
1443
1934
|
return {
|
|
@@ -1446,7 +1937,18 @@ export function composeRuntime(options) {
|
|
|
1446
1937
|
stage: (record, page) => exclusive(async () => {
|
|
1447
1938
|
if (!live())
|
|
1448
1939
|
return false;
|
|
1449
|
-
|
|
1940
|
+
const bytes = layerBytes(page);
|
|
1941
|
+
if (!(await admitted(local, record.locator, bytes)))
|
|
1942
|
+
return false;
|
|
1943
|
+
try {
|
|
1944
|
+
await local.stage(record, page);
|
|
1945
|
+
}
|
|
1946
|
+
catch (error) {
|
|
1947
|
+
if (!exceeded(error))
|
|
1948
|
+
throw error;
|
|
1949
|
+
await exceededOn(record.locator, bytes);
|
|
1950
|
+
return false;
|
|
1951
|
+
}
|
|
1450
1952
|
transfers.set(record.locator, record);
|
|
1451
1953
|
readSide.recover();
|
|
1452
1954
|
return true;
|
|
@@ -1469,12 +1971,13 @@ export function composeRuntime(options) {
|
|
|
1469
1971
|
async function pullBootstrap(authorized, room, local, live) {
|
|
1470
1972
|
if (!live())
|
|
1471
1973
|
return unconfirmed;
|
|
1974
|
+
const turn = generation;
|
|
1472
1975
|
if (replicas.has(room.locator)) {
|
|
1473
1976
|
catching.add(room.locator);
|
|
1474
1977
|
readSide.recover();
|
|
1475
1978
|
}
|
|
1476
1979
|
try {
|
|
1477
|
-
const result = await bootstrapRoom(
|
|
1980
|
+
const result = await bootstrapRoom(reads, authorized, room, {
|
|
1478
1981
|
collection: modelOf(room.locator),
|
|
1479
1982
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
1480
1983
|
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
@@ -1489,14 +1992,30 @@ export function composeRuntime(options) {
|
|
|
1489
1992
|
caughtUpTo(room.locator);
|
|
1490
1993
|
throw error;
|
|
1491
1994
|
}
|
|
1995
|
+
finally {
|
|
1996
|
+
reads.finished(room.locator, turn);
|
|
1997
|
+
}
|
|
1492
1998
|
}
|
|
1493
1999
|
function caughtUpTo(locator) {
|
|
1494
2000
|
if (catching.delete(locator))
|
|
1495
2001
|
readSide.recover();
|
|
1496
2002
|
}
|
|
2003
|
+
function behind(room) {
|
|
2004
|
+
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
2005
|
+
return (held !== undefined &&
|
|
2006
|
+
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
2007
|
+
}
|
|
1497
2008
|
async function advance(room, local, authorized, live) {
|
|
2009
|
+
const carried = reads.carried(room.locator);
|
|
2010
|
+
const withheld = await advanceOnce(room, local, authorized, live);
|
|
2011
|
+
if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
|
|
2012
|
+
return withheld;
|
|
2013
|
+
}
|
|
2014
|
+
return advanceOnce(room, local, authorized, live);
|
|
2015
|
+
}
|
|
2016
|
+
async function advanceOnce(room, local, authorized, live) {
|
|
1498
2017
|
await referenced(room.locator);
|
|
1499
|
-
if (!live())
|
|
2018
|
+
if (!live() || paused.has(room.locator))
|
|
1500
2019
|
return unconfirmed;
|
|
1501
2020
|
const held = replicas.get(room.locator);
|
|
1502
2021
|
if (held !== undefined) {
|
|
@@ -1589,6 +2108,7 @@ export function composeRuntime(options) {
|
|
|
1589
2108
|
unreadable.add(room.locator);
|
|
1590
2109
|
list(room, false);
|
|
1591
2110
|
emitRooms();
|
|
2111
|
+
moveTo({ ...status, ...attention() });
|
|
1592
2112
|
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
1593
2113
|
}
|
|
1594
2114
|
function leaving(batch, result) {
|
|
@@ -1930,6 +2450,11 @@ export function composeRuntime(options) {
|
|
|
1930
2450
|
});
|
|
1931
2451
|
}
|
|
1932
2452
|
async function scoped(result, room, local) {
|
|
2453
|
+
if ((result.scope === 'content' || result.scope === 'sync') && !result.retryable) {
|
|
2454
|
+
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
2455
|
+
await loseRooms([room.locator]);
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
1933
2458
|
scopedRefusals.set(room.locator, {
|
|
1934
2459
|
code: result.code,
|
|
1935
2460
|
fields: result.fields,
|
|
@@ -1976,15 +2501,16 @@ export function composeRuntime(options) {
|
|
|
1976
2501
|
});
|
|
1977
2502
|
}
|
|
1978
2503
|
async function synchronize(listed, local, progress, attempt, live) {
|
|
1979
|
-
const
|
|
2504
|
+
const acquired = listed.filter(acquires);
|
|
2505
|
+
const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
|
|
1980
2506
|
if (!live())
|
|
1981
2507
|
return;
|
|
1982
|
-
const ready =
|
|
2508
|
+
const ready = acquired.filter((_, index) => {
|
|
1983
2509
|
const outcome = prepared[index];
|
|
1984
2510
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1985
2511
|
});
|
|
1986
2512
|
if (attempt === 'opened') {
|
|
1987
|
-
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
2513
|
+
options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
|
|
1988
2514
|
handed = true;
|
|
1989
2515
|
}
|
|
1990
2516
|
discovering?.release();
|
|
@@ -2012,19 +2538,10 @@ export function composeRuntime(options) {
|
|
|
2012
2538
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
2013
2539
|
if (discoveryPaused || authorization === undefined)
|
|
2014
2540
|
return false;
|
|
2015
|
-
|
|
2016
|
-
if (replicas.has(room.locator))
|
|
2017
|
-
progress.complete = false;
|
|
2018
|
-
return false;
|
|
2019
|
-
}
|
|
2020
|
-
return true;
|
|
2541
|
+
return room.replica.admission === 'admitted';
|
|
2021
2542
|
}
|
|
2022
2543
|
async function caughtUp(room, local, progress, live) {
|
|
2023
|
-
|
|
2024
|
-
if (held === undefined || authorization === undefined || !live())
|
|
2025
|
-
return;
|
|
2026
|
-
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
2027
|
-
if (!behind)
|
|
2544
|
+
if (authorization === undefined || !live() || !behind(room))
|
|
2028
2545
|
return;
|
|
2029
2546
|
const withheld = await advance(room, local, authorization, live);
|
|
2030
2547
|
if (withheld !== null)
|
|
@@ -2252,9 +2769,43 @@ export function composeRuntime(options) {
|
|
|
2252
2769
|
options.adapter.roomStream?.reopen();
|
|
2253
2770
|
});
|
|
2254
2771
|
}
|
|
2255
|
-
function
|
|
2772
|
+
function rediscover() {
|
|
2773
|
+
return task(OPENING_LANE, async (live) => {
|
|
2774
|
+
if (!live() || !socketOpen)
|
|
2775
|
+
return;
|
|
2776
|
+
const progress = { complete: true, reachable: true };
|
|
2777
|
+
const gate = discover();
|
|
2778
|
+
moveTo(transferring(status));
|
|
2779
|
+
try {
|
|
2780
|
+
const local = await openLocal();
|
|
2781
|
+
if (!live())
|
|
2782
|
+
return;
|
|
2783
|
+
await ensureAuthorization();
|
|
2784
|
+
if (!live())
|
|
2785
|
+
return;
|
|
2786
|
+
await discoverRooms(local, progress, 'authorized', live);
|
|
2787
|
+
}
|
|
2788
|
+
catch {
|
|
2789
|
+
progress.complete = false;
|
|
2790
|
+
}
|
|
2791
|
+
finally {
|
|
2792
|
+
gate.release();
|
|
2793
|
+
notifyReady();
|
|
2794
|
+
}
|
|
2795
|
+
if (!live())
|
|
2796
|
+
return;
|
|
2797
|
+
if (!progress.complete)
|
|
2798
|
+
complete = false;
|
|
2799
|
+
await rest(progress, live);
|
|
2800
|
+
});
|
|
2801
|
+
}
|
|
2802
|
+
function closedSocket(revoked, lost) {
|
|
2256
2803
|
const opened = socketOpen;
|
|
2257
2804
|
socketOpen = false;
|
|
2805
|
+
if (lost) {
|
|
2806
|
+
reads.lost();
|
|
2807
|
+
conversations.lost();
|
|
2808
|
+
}
|
|
2258
2809
|
if (revoked) {
|
|
2259
2810
|
void refuse('revoked-authorization');
|
|
2260
2811
|
return;
|
|
@@ -2272,6 +2823,8 @@ export function composeRuntime(options) {
|
|
|
2272
2823
|
const progress = underway?.generation === generation ? underway.progress : null;
|
|
2273
2824
|
const cycle = progress ?? { complete: true, reachable: true };
|
|
2274
2825
|
const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
|
|
2826
|
+
if (opened === generation)
|
|
2827
|
+
opened = null;
|
|
2275
2828
|
return task(OPENING_LANE, async (live) => {
|
|
2276
2829
|
underway = null;
|
|
2277
2830
|
if (!live())
|
|
@@ -2284,8 +2837,6 @@ export function composeRuntime(options) {
|
|
|
2284
2837
|
await roomsCarried(expired);
|
|
2285
2838
|
if (!live())
|
|
2286
2839
|
return;
|
|
2287
|
-
if (opened === generation)
|
|
2288
|
-
opened = null;
|
|
2289
2840
|
const replaced = new Set(owed);
|
|
2290
2841
|
owed.clear();
|
|
2291
2842
|
try {
|
|
@@ -2441,6 +2992,7 @@ export function composeRuntime(options) {
|
|
|
2441
2992
|
progress.complete = false;
|
|
2442
2993
|
return;
|
|
2443
2994
|
}
|
|
2995
|
+
reads.applied();
|
|
2444
2996
|
submitAll();
|
|
2445
2997
|
for (const locator of stale)
|
|
2446
2998
|
if (repairable.has(locator))
|
|
@@ -2449,9 +3001,12 @@ export function composeRuntime(options) {
|
|
|
2449
3001
|
if (status.providerUnreachable)
|
|
2450
3002
|
conversations.reconnected();
|
|
2451
3003
|
const reading = { complete: true, reachable: true };
|
|
2452
|
-
|
|
3004
|
+
if (demand.conversations)
|
|
3005
|
+
await conversations.catchUp(reading);
|
|
2453
3006
|
if (progress.reachable && reading.reachable)
|
|
2454
3007
|
await conversations.resend(reading);
|
|
3008
|
+
if (progress.reachable)
|
|
3009
|
+
void recovery.attempt('calls', resendCalls);
|
|
2455
3010
|
if (!reading.reachable) {
|
|
2456
3011
|
progress.reachable = false;
|
|
2457
3012
|
resendLater();
|
|
@@ -2512,7 +3067,7 @@ export function composeRuntime(options) {
|
|
|
2512
3067
|
return;
|
|
2513
3068
|
const room = discovered.get(locator);
|
|
2514
3069
|
const held = replicas.get(locator);
|
|
2515
|
-
if (room === undefined || room.replica.admission !== 'admitted')
|
|
3070
|
+
if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
|
|
2516
3071
|
return;
|
|
2517
3072
|
if (held !== undefined &&
|
|
2518
3073
|
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
@@ -2564,14 +3119,17 @@ export function composeRuntime(options) {
|
|
|
2564
3119
|
if (event.kind === 'opened') {
|
|
2565
3120
|
socketOpen = true;
|
|
2566
3121
|
generation += 1;
|
|
3122
|
+
reads.opened();
|
|
2567
3123
|
conversations.reopened();
|
|
2568
3124
|
recovery.resume();
|
|
2569
3125
|
void sync('opened');
|
|
2570
3126
|
}
|
|
2571
3127
|
else if (event.kind === 'closed')
|
|
2572
|
-
closedSocket(event.revoked);
|
|
3128
|
+
closedSocket(event.revoked, event.lost);
|
|
2573
3129
|
else if (event.kind === 'opening')
|
|
2574
3130
|
void applyOpening(event.expired);
|
|
3131
|
+
else if (event.kind === 'authority-changed')
|
|
3132
|
+
void rediscover();
|
|
2575
3133
|
else
|
|
2576
3134
|
void applyRoom(event.locator, event.roomVersion);
|
|
2577
3135
|
}, receipts);
|
|
@@ -2589,6 +3147,7 @@ export function composeRuntime(options) {
|
|
|
2589
3147
|
haltedRefusal = false;
|
|
2590
3148
|
if (mode !== serviceMode) {
|
|
2591
3149
|
serviceMode = mode;
|
|
3150
|
+
queueReadiness();
|
|
2592
3151
|
journal.record(`service mode ${mode}`);
|
|
2593
3152
|
for (const listener of serviceModeListeners)
|
|
2594
3153
|
listener(mode);
|
|
@@ -2601,6 +3160,8 @@ export function composeRuntime(options) {
|
|
|
2601
3160
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
2602
3161
|
return;
|
|
2603
3162
|
withdrawConversations = stream.observe((event) => {
|
|
3163
|
+
if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
|
|
3164
|
+
queueReadiness();
|
|
2604
3165
|
if (conversations.receive(event)) {
|
|
2605
3166
|
void ready.then(scheduleCurrency);
|
|
2606
3167
|
return;
|
|
@@ -2623,7 +3184,7 @@ export function composeRuntime(options) {
|
|
|
2623
3184
|
if (!live())
|
|
2624
3185
|
return;
|
|
2625
3186
|
await conversations.reconcile();
|
|
2626
|
-
if (conversations.current()) {
|
|
3187
|
+
if (conversations.current() || !demand.conversations) {
|
|
2627
3188
|
show();
|
|
2628
3189
|
return;
|
|
2629
3190
|
}
|
|
@@ -2641,6 +3202,11 @@ export function composeRuntime(options) {
|
|
|
2641
3202
|
if (closed)
|
|
2642
3203
|
return;
|
|
2643
3204
|
closed = true;
|
|
3205
|
+
if (narrowing !== null)
|
|
3206
|
+
clearTimeout(narrowing);
|
|
3207
|
+
if (rechecking !== null)
|
|
3208
|
+
clearInterval(rechecking);
|
|
3209
|
+
readinessWatches.clear();
|
|
2644
3210
|
withdrawStream?.();
|
|
2645
3211
|
withdrawStream = undefined;
|
|
2646
3212
|
withdrawConversations?.();
|
|
@@ -2688,11 +3254,38 @@ export function composeRuntime(options) {
|
|
|
2688
3254
|
return planOf(input, roomFacts(reach), planReach(reach));
|
|
2689
3255
|
}
|
|
2690
3256
|
function watchPlan(plan, deliver) {
|
|
2691
|
-
|
|
3257
|
+
let seeded = false;
|
|
3258
|
+
let pending = true;
|
|
3259
|
+
const shown = () => {
|
|
3260
|
+
const seed = seeds.get(plan.key);
|
|
3261
|
+
if (seed === undefined || seeded || !pending)
|
|
3262
|
+
return;
|
|
3263
|
+
seeded = true;
|
|
3264
|
+
deliver(seededResult(seed));
|
|
3265
|
+
};
|
|
3266
|
+
const watchers = seedWatchers.get(plan.key) ?? new Set();
|
|
3267
|
+
seedWatchers.set(plan.key, watchers);
|
|
3268
|
+
watchers.add(shown);
|
|
3269
|
+
const stop = readSide.watch(plan, identityOf(plan), () => {
|
|
2692
3270
|
const cover = covered(plan);
|
|
2693
3271
|
awaitPreparation(cover);
|
|
2694
|
-
return cover;
|
|
2695
|
-
},
|
|
3272
|
+
return seedBehind(plan, cover);
|
|
3273
|
+
}, (message) => {
|
|
3274
|
+
const answered = message;
|
|
3275
|
+
pending = whole(answered) && 'pending' in answered.result;
|
|
3276
|
+
if (pending && seeds.has(plan.key)) {
|
|
3277
|
+
shown();
|
|
3278
|
+
return;
|
|
3279
|
+
}
|
|
3280
|
+
seeded = false;
|
|
3281
|
+
deliver(message);
|
|
3282
|
+
});
|
|
3283
|
+
return () => {
|
|
3284
|
+
watchers.delete(shown);
|
|
3285
|
+
if (watchers.size === 0)
|
|
3286
|
+
seedWatchers.delete(plan.key);
|
|
3287
|
+
stop();
|
|
3288
|
+
};
|
|
2696
3289
|
}
|
|
2697
3290
|
async function searched(input, reach) {
|
|
2698
3291
|
const plan = searchPlanOf(input);
|
|
@@ -2701,40 +3294,62 @@ export function composeRuntime(options) {
|
|
|
2701
3294
|
const cover = covered(filter);
|
|
2702
3295
|
awaitPreparation(cover);
|
|
2703
3296
|
const pending = 'pending' in cover ? [...cover.pending] : [];
|
|
2704
|
-
const
|
|
3297
|
+
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
3298
|
+
const held = covered({
|
|
3299
|
+
...filter,
|
|
3300
|
+
rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
|
|
3301
|
+
});
|
|
3302
|
+
const covering = 'pending' in held ? [] : held.coverage.rooms;
|
|
2705
3303
|
const unindexed = covering
|
|
2706
3304
|
.filter(({ room }) => !searchIndex.rooms().has(room))
|
|
2707
3305
|
.map(({ room }) => room);
|
|
2708
3306
|
for (const room of unindexed)
|
|
2709
3307
|
pending.push({ room, awaits: 'index' });
|
|
2710
|
-
const
|
|
3308
|
+
const listed = conversations.searchable();
|
|
2711
3309
|
const named = reach !== null && !reach.conversations
|
|
2712
3310
|
? []
|
|
2713
|
-
: (plan.conversations ??
|
|
3311
|
+
: (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
|
|
2714
3312
|
const waiting = named.filter((one) => !searchIndex.conversations().has(one));
|
|
2715
3313
|
for (const conversation of waiting)
|
|
2716
3314
|
pending.push({ conversation, awaits: 'index' });
|
|
2717
3315
|
if (unindexed.length > 0 || waiting.length > 0)
|
|
2718
3316
|
index(unindexed, waiting);
|
|
2719
|
-
|
|
2720
|
-
|
|
3317
|
+
const complete = pending.length === 0 && !('pending' in cover);
|
|
3318
|
+
const answering = complete
|
|
3319
|
+
? filter
|
|
3320
|
+
: {
|
|
3321
|
+
...filter,
|
|
3322
|
+
rooms: covering
|
|
3323
|
+
.filter(({ room }) => searchIndex.rooms().has(room))
|
|
3324
|
+
.map(({ room }) => room),
|
|
3325
|
+
};
|
|
3326
|
+
const answered = covered(answering);
|
|
3327
|
+
if ('pending' in answered)
|
|
3328
|
+
continue;
|
|
3329
|
+
const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
|
|
2721
3330
|
const seen = searchIndex.changes();
|
|
2722
3331
|
await searchIndex.caughtUp();
|
|
2723
|
-
const selected =
|
|
3332
|
+
const selected = roomsAlone(answering) ? null : await rowsQuery(answering);
|
|
2724
3333
|
if (selected !== null && 'pending' in selected)
|
|
2725
|
-
|
|
3334
|
+
continue;
|
|
2726
3335
|
if (searchIndex.changes() !== seen ||
|
|
2727
|
-
JSON.stringify(covered(
|
|
3336
|
+
JSON.stringify(covered(answering)) !== JSON.stringify(answered))
|
|
2728
3337
|
continue;
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
3338
|
+
const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms === null ? null : new Set(answering.rooms), plan.collection
|
|
3339
|
+
? new Map(answered.coverage.rooms.map(({ room }) => [
|
|
3340
|
+
room,
|
|
3341
|
+
replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
|
|
3342
|
+
]))
|
|
3343
|
+
: null);
|
|
3344
|
+
const coverage = {
|
|
3345
|
+
rooms: answered.coverage.rooms,
|
|
3346
|
+
conversations: searchedConversations,
|
|
3347
|
+
index: searchIndex.generation(),
|
|
3348
|
+
overlay: answered.coverage.overlay,
|
|
2737
3349
|
};
|
|
3350
|
+
return complete
|
|
3351
|
+
? { ...hits, coverage }
|
|
3352
|
+
: { pending, documents: hits.documents, messages: hits.messages, coverage };
|
|
2738
3353
|
}
|
|
2739
3354
|
}
|
|
2740
3355
|
const watch = (topic, input, deliver) => {
|
|
@@ -2781,6 +3396,9 @@ export function composeRuntime(options) {
|
|
|
2781
3396
|
presentationListeners.delete(listener);
|
|
2782
3397
|
};
|
|
2783
3398
|
}
|
|
3399
|
+
if (topic === 'readiness') {
|
|
3400
|
+
return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
|
|
3401
|
+
}
|
|
2784
3402
|
if (topic === 'rooms') {
|
|
2785
3403
|
roomListeners.add(deliver);
|
|
2786
3404
|
deliver(listed());
|
|
@@ -2916,13 +3534,13 @@ export function composeRuntime(options) {
|
|
|
2916
3534
|
});
|
|
2917
3535
|
},
|
|
2918
3536
|
write(app, value) {
|
|
2919
|
-
return
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
3537
|
+
return keepPresentation(app, () => structuredClone(value));
|
|
3538
|
+
},
|
|
3539
|
+
edit(app, members) {
|
|
3540
|
+
return keepPresentation(app, (held) => Object.fromEntries([
|
|
3541
|
+
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(members, member)),
|
|
3542
|
+
...Object.entries(structuredClone(members)).filter(([, value]) => value !== null),
|
|
3543
|
+
]));
|
|
2926
3544
|
},
|
|
2927
3545
|
},
|
|
2928
3546
|
save(room, named, writtenBy) {
|
|
@@ -2933,10 +3551,11 @@ export function composeRuntime(options) {
|
|
|
2933
3551
|
},
|
|
2934
3552
|
binaryFile: {
|
|
2935
3553
|
read: readBinaryPath,
|
|
3554
|
+
link: linkBinaryPath,
|
|
2936
3555
|
readReference: readBinaryReference,
|
|
2937
3556
|
},
|
|
2938
3557
|
call(name, input) {
|
|
2939
|
-
return
|
|
3558
|
+
return recoveredAction(name, input);
|
|
2940
3559
|
},
|
|
2941
3560
|
reached(locator, roomVersion, path) {
|
|
2942
3561
|
return path === undefined
|
|
@@ -2947,7 +3566,13 @@ export function composeRuntime(options) {
|
|
|
2947
3566
|
return journal.export();
|
|
2948
3567
|
},
|
|
2949
3568
|
persistence() {
|
|
2950
|
-
return
|
|
3569
|
+
return persistence === 'granted';
|
|
3570
|
+
},
|
|
3571
|
+
async retry() {
|
|
3572
|
+
journal.record('retry');
|
|
3573
|
+
unindexable.clear();
|
|
3574
|
+
queueReadiness();
|
|
3575
|
+
resume();
|
|
2951
3576
|
},
|
|
2952
3577
|
observe: observing(watch),
|
|
2953
3578
|
async reauthorize() {
|
|
@@ -2974,6 +3599,22 @@ export function composeRuntime(options) {
|
|
|
2974
3599
|
query: (plan) => rowsQuery(plan),
|
|
2975
3600
|
watchPlan,
|
|
2976
3601
|
search: searched,
|
|
3602
|
+
watchReadiness,
|
|
3603
|
+
};
|
|
3604
|
+
return {
|
|
3605
|
+
md,
|
|
3606
|
+
seed,
|
|
3607
|
+
stop,
|
|
3608
|
+
lose: loseRooms,
|
|
3609
|
+
watch,
|
|
3610
|
+
acquire,
|
|
3611
|
+
persisted(answer) {
|
|
3612
|
+
if (answer === persistence)
|
|
3613
|
+
return;
|
|
3614
|
+
persistence = answer;
|
|
3615
|
+
journal.record(`persistence ${answer}`);
|
|
3616
|
+
queueReadiness();
|
|
3617
|
+
},
|
|
3618
|
+
within: (reach, port) => narrowed(reachable, reach, port),
|
|
2977
3619
|
};
|
|
2978
|
-
return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2979
3620
|
}
|