@agentshouse/mdruntime 0.1.0-c8f3163f32f93 → 0.1.0-c93103f5357ff
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 +262 -262
- 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 +13 -7
- package/dist/adapter.d.ts.map +1 -1
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +4 -0
- package/dist/change-set.d.ts +2 -0
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +36 -10
- 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/message.d.ts.map +1 -1
- package/dist/chat/message.js +2 -2
- 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 +10 -6
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +11 -2
- package/dist/collection.d.ts +17 -3
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +230 -19
- package/dist/connection.d.ts +23 -7
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +1031 -183
- package/dist/conversation.d.ts +10 -5
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +201 -95
- package/dist/durability.d.ts +7 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/entry.d.ts +2 -2
- package/dist/entry.d.ts.map +1 -1
- package/dist/entry.js +3 -3
- package/dist/host.d.ts +16 -2
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +82 -8
- package/dist/identity.d.ts +2 -0
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +6 -0
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/memory.d.ts +3 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +14 -2
- package/dist/model.d.ts +4 -2
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +10 -1
- package/dist/observation.d.ts +5 -2
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +22 -11
- package/dist/overlay.d.ts +2 -0
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +50 -55
- package/dist/query.d.ts +35 -3
- 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 +16 -4
- package/dist/react.d.ts +7 -4
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +6 -4
- package/dist/read-side.d.ts +13 -6
- package/dist/read-side.d.ts.map +1 -1
- package/dist/read-side.js +285 -83
- package/dist/readiness.d.ts +53 -0
- package/dist/readiness.d.ts.map +1 -0
- package/dist/readiness.js +33 -0
- package/dist/recovery.d.ts +25 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +197 -0
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +17 -10
- package/dist/replica.d.ts +1 -1
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +57 -20
- 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 +5 -13
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +5 -13
- package/dist/save.d.ts +5 -0
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +36 -13
- package/dist/search.d.ts +36 -6
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +132 -46
- package/dist/seed.d.ts +16 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +61 -0
- package/dist/shared.d.ts +11 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +368 -0
- package/dist/status.d.ts +4 -0
- package/dist/status.d.ts.map +1 -1
- package/dist/store.d.ts +21 -5
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +157 -53
- package/dist/turn.d.ts +7 -0
- package/dist/turn.d.ts.map +1 -0
- package/dist/turn.js +45 -0
- package/dist/wire.d.ts +136 -32
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +48 -31
- package/package.json +2 -2
package/dist/connection.js
CHANGED
|
@@ -1,31 +1,45 @@
|
|
|
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';
|
|
9
|
-
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
10
|
+
import { changeSet, entering, mergeChangeSets } from './change-set.js';
|
|
10
11
|
import { createConversationStore, } from './conversation.js';
|
|
11
12
|
import { queuedFile } from './change.js';
|
|
12
|
-
import { validRoot } from './collection.js';
|
|
13
|
+
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
17
|
import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
|
|
18
|
+
import { createRecovery } from './recovery.js';
|
|
17
19
|
import { refreshRoom } from './refresh.js';
|
|
18
20
|
import { buildBatch, joined, limitedIn, requeued } from './save.js';
|
|
19
21
|
import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
|
|
20
22
|
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
21
23
|
import { same, saving, STARTING, transferring, } from './status.js';
|
|
22
24
|
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
25
|
+
import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
|
|
26
|
+
import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
|
|
23
27
|
import { FencedMap } from './fence.js';
|
|
24
|
-
import {
|
|
28
|
+
import { tally } from './shared.js';
|
|
29
|
+
import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
|
|
30
|
+
import { identityTime, roomDraftRevision } from './identity.js';
|
|
25
31
|
import { beneath, normalize } from './paths.js';
|
|
26
32
|
import { openRuntimeStore, } from './store.js';
|
|
27
33
|
import { unconfirmed } from './transfer.js';
|
|
28
34
|
import { governs, holds, narrowed, planReach, } from './reach.js';
|
|
35
|
+
function roomsAlone(plan) {
|
|
36
|
+
return (plan.reach === null &&
|
|
37
|
+
plan.root.types === null &&
|
|
38
|
+
plan.root.where === null &&
|
|
39
|
+
plan.root.pathPrefix === null &&
|
|
40
|
+
plan.hop === null &&
|
|
41
|
+
plan.backlinks === null);
|
|
42
|
+
}
|
|
29
43
|
function within(root, path) {
|
|
30
44
|
return root === '' || beneath(root, path);
|
|
31
45
|
}
|
|
@@ -42,6 +56,29 @@ function declaredSelection(room) {
|
|
|
42
56
|
const OPENING_LANE = 'opening';
|
|
43
57
|
const CONVERSATION_LANE = 'conversations';
|
|
44
58
|
const ROOM_READS = 256;
|
|
59
|
+
const ROOM_ATTEMPTS = 16;
|
|
60
|
+
const outside = new Map();
|
|
61
|
+
function outsideOf(roots) {
|
|
62
|
+
const key = JSON.stringify(roots);
|
|
63
|
+
const held = outside.get(key);
|
|
64
|
+
if (held !== undefined)
|
|
65
|
+
return held;
|
|
66
|
+
const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
|
|
67
|
+
const made = {
|
|
68
|
+
documents: (path) => (markdownPath(path) ? counted(path) : 0),
|
|
69
|
+
binaries: counted,
|
|
70
|
+
};
|
|
71
|
+
outside.set(key, made);
|
|
72
|
+
return made;
|
|
73
|
+
}
|
|
74
|
+
function abandonedOn(signal) {
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
if (signal.aborted)
|
|
77
|
+
resolve(null);
|
|
78
|
+
else
|
|
79
|
+
signal.addEventListener('abort', () => resolve(null), { once: true });
|
|
80
|
+
});
|
|
81
|
+
}
|
|
45
82
|
function roomLane(locator) {
|
|
46
83
|
return `room ${locator}`;
|
|
47
84
|
}
|
|
@@ -59,6 +96,8 @@ export function composeRuntime(options) {
|
|
|
59
96
|
const replicas = new FencedMap(isFenced);
|
|
60
97
|
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
61
98
|
const pendings = new Map();
|
|
99
|
+
const seeds = new Map();
|
|
100
|
+
const seedWatchers = new Map();
|
|
62
101
|
const roomDrafts = new FencedMap(isFenced);
|
|
63
102
|
const presentations = new Map();
|
|
64
103
|
let limits = [];
|
|
@@ -98,7 +137,6 @@ export function composeRuntime(options) {
|
|
|
98
137
|
let status = STARTING;
|
|
99
138
|
let shown = STARTING;
|
|
100
139
|
let discoveryPaused = false;
|
|
101
|
-
let resetTimer;
|
|
102
140
|
let closed = false;
|
|
103
141
|
let stopping = false;
|
|
104
142
|
let withdrawStream;
|
|
@@ -106,7 +144,10 @@ export function composeRuntime(options) {
|
|
|
106
144
|
let underway = null;
|
|
107
145
|
let streamed = null;
|
|
108
146
|
let handed = false;
|
|
109
|
-
const
|
|
147
|
+
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
148
|
+
const repairing = new Set();
|
|
149
|
+
const repairable = new Map();
|
|
150
|
+
let outcomeRetention = null;
|
|
110
151
|
const answering = new Map();
|
|
111
152
|
let complete = false;
|
|
112
153
|
let recorded = false;
|
|
@@ -136,25 +177,41 @@ export function composeRuntime(options) {
|
|
|
136
177
|
const serviceModeListeners = new Set();
|
|
137
178
|
let member = null;
|
|
138
179
|
const memberListeners = new Set();
|
|
139
|
-
const
|
|
140
|
-
|
|
180
|
+
const prepared = new Set();
|
|
181
|
+
const projecting = new Set();
|
|
182
|
+
const unpreparedDemand = new Set();
|
|
183
|
+
const unindexable = new Set();
|
|
184
|
+
let preparing = false;
|
|
185
|
+
let demand = WHOLE_DEMAND;
|
|
186
|
+
let narrowing = null;
|
|
187
|
+
let targets = {};
|
|
188
|
+
let persistence = options.shell.persistence;
|
|
189
|
+
let capacity = null;
|
|
190
|
+
const paused = new Set();
|
|
191
|
+
let rechecking = null;
|
|
192
|
+
const readinessWatches = new Set();
|
|
193
|
+
let readinessQueued = false;
|
|
194
|
+
const catching = new Set();
|
|
141
195
|
const transfers = new FencedMap(isFenced);
|
|
142
196
|
const conversations = createConversationStore({
|
|
143
197
|
agents: declaredAgents,
|
|
144
|
-
documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
|
|
198
|
+
documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
|
|
145
199
|
call: (name, input) => invokeAction(name, input),
|
|
146
200
|
durably,
|
|
147
201
|
stream: () => options.adapter.conversations,
|
|
148
202
|
publish: publishChangeSet,
|
|
149
203
|
online: () => !status.providerUnreachable && refusal === null,
|
|
150
204
|
closed: () => closed,
|
|
205
|
+
unanswered: resendLater,
|
|
206
|
+
expired: (operation) => pastHorizon(operation),
|
|
151
207
|
});
|
|
152
208
|
const searchIndex = createSearchIndex({
|
|
153
|
-
|
|
209
|
+
room: (room) => readSide.room(room),
|
|
154
210
|
document: (room, path) => readSide.document(room, path),
|
|
155
|
-
messages: () => conversations.messages(),
|
|
156
|
-
message: (conversation, identity) => conversations.message(conversation, identity),
|
|
157
211
|
});
|
|
212
|
+
const roomsToIndex = new Set();
|
|
213
|
+
const conversationsToIndex = new Set();
|
|
214
|
+
let indexing = false;
|
|
158
215
|
const journal = createLog();
|
|
159
216
|
let notifyReady = () => { };
|
|
160
217
|
const ready = new Promise((resolve) => {
|
|
@@ -233,9 +290,16 @@ export function composeRuntime(options) {
|
|
|
233
290
|
returned: structuredClone(record.returned),
|
|
234
291
|
});
|
|
235
292
|
}
|
|
293
|
+
for (const [room, refusal] of sourceRefusals)
|
|
294
|
+
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
236
295
|
attentionRequired.sort((left, right) => {
|
|
237
296
|
if (left.room !== right.room)
|
|
238
297
|
return left.room < right.room ? -1 : 1;
|
|
298
|
+
if ('refusal' in left || 'refusal' in right) {
|
|
299
|
+
if ('refusal' in left && 'refusal' in right)
|
|
300
|
+
return 0;
|
|
301
|
+
return 'refusal' in left ? -1 : 1;
|
|
302
|
+
}
|
|
239
303
|
if (left.path !== right.path)
|
|
240
304
|
return left.path < right.path ? -1 : 1;
|
|
241
305
|
return 0;
|
|
@@ -254,11 +318,9 @@ export function composeRuntime(options) {
|
|
|
254
318
|
if (replica === undefined)
|
|
255
319
|
return 0;
|
|
256
320
|
const roots = replica.confirmed.record.typedSelection?.roots ?? [];
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
];
|
|
261
|
-
return paths.filter((path) => !roots.some((root) => beneath(root, path))).length;
|
|
321
|
+
const weighed = outsideOf(roots);
|
|
322
|
+
return (tally(replica.visible.documents, weighed.documents) +
|
|
323
|
+
tally(replica.visible.binaries, weighed.binaries));
|
|
262
324
|
}
|
|
263
325
|
function listed() {
|
|
264
326
|
return structuredClone([...rooms.values()].map((room) => {
|
|
@@ -285,15 +347,19 @@ export function composeRuntime(options) {
|
|
|
285
347
|
return { listed: roomsListed, rooms: held };
|
|
286
348
|
}
|
|
287
349
|
function emitRooms() {
|
|
350
|
+
readSide.recover();
|
|
351
|
+
queueReadiness();
|
|
288
352
|
const value = listed();
|
|
289
353
|
for (const listener of roomListeners)
|
|
290
354
|
listener(value);
|
|
291
355
|
}
|
|
292
356
|
function emitRefusal() {
|
|
357
|
+
queueReadiness();
|
|
293
358
|
for (const listener of refusalListeners)
|
|
294
359
|
listener(refusal);
|
|
295
360
|
}
|
|
296
361
|
function emitStorageLoss() {
|
|
362
|
+
queueReadiness();
|
|
297
363
|
for (const listener of storageLossListeners)
|
|
298
364
|
listener(storageLoss);
|
|
299
365
|
}
|
|
@@ -310,7 +376,8 @@ export function composeRuntime(options) {
|
|
|
310
376
|
function show() {
|
|
311
377
|
if (closed)
|
|
312
378
|
return;
|
|
313
|
-
|
|
379
|
+
queueReadiness();
|
|
380
|
+
const next = status.state === 'synced' && demand.conversations && !conversations.current()
|
|
314
381
|
? { ...status, state: 'syncing' }
|
|
315
382
|
: status;
|
|
316
383
|
if (same(shown, next))
|
|
@@ -339,42 +406,511 @@ export function composeRuntime(options) {
|
|
|
339
406
|
}
|
|
340
407
|
}
|
|
341
408
|
function applyChangeSet(set) {
|
|
342
|
-
const held =
|
|
343
|
-
|
|
344
|
-
|
|
409
|
+
const held = {
|
|
410
|
+
...set,
|
|
411
|
+
documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
|
|
412
|
+
};
|
|
345
413
|
readSide.apply(held);
|
|
346
414
|
searchIndex.changed(held);
|
|
347
415
|
}
|
|
348
|
-
function
|
|
349
|
-
|
|
416
|
+
async function referenced(locator) {
|
|
417
|
+
for (;;) {
|
|
418
|
+
const record = replicas.get(locator)?.confirmed.record;
|
|
419
|
+
if (record === undefined || closed)
|
|
420
|
+
return;
|
|
421
|
+
const held = { text: record.documents, binaries: record.binaries };
|
|
422
|
+
const composition = compositionOf(record);
|
|
423
|
+
if (referencesIndexed(held, composition))
|
|
424
|
+
return;
|
|
425
|
+
await indexReferrers(held, composition);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
async function prepareReplica(locator) {
|
|
429
|
+
await referenced(locator);
|
|
430
|
+
const replica = replicas.get(locator);
|
|
431
|
+
if (replica === undefined || closed || prepared.has(locator))
|
|
350
432
|
return;
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
433
|
+
const visible = replica.visible;
|
|
434
|
+
const paths = [
|
|
435
|
+
...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
|
|
436
|
+
];
|
|
437
|
+
const entered = [];
|
|
438
|
+
projecting.add(locator);
|
|
439
|
+
try {
|
|
440
|
+
await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
|
|
441
|
+
const current = replicas.get(locator);
|
|
442
|
+
if (current === undefined || closed)
|
|
443
|
+
return;
|
|
444
|
+
const set = entering(locator, current, chunk);
|
|
445
|
+
readSide.apply(set);
|
|
446
|
+
entered.push(set);
|
|
447
|
+
});
|
|
448
|
+
if (!replicas.has(locator) || closed)
|
|
449
|
+
return;
|
|
450
|
+
prepared.add(locator);
|
|
361
451
|
}
|
|
362
|
-
|
|
452
|
+
finally {
|
|
453
|
+
projecting.delete(locator);
|
|
454
|
+
}
|
|
455
|
+
conversations.changed(mergeChangeSets(entered));
|
|
456
|
+
readSide.recover();
|
|
457
|
+
queueReadiness();
|
|
458
|
+
}
|
|
459
|
+
function unprepared() {
|
|
460
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
|
|
461
|
+
}
|
|
462
|
+
function unbuilt(locator, error) {
|
|
463
|
+
unindexable.add(locator);
|
|
464
|
+
journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
|
|
465
|
+
queueReadiness();
|
|
466
|
+
}
|
|
467
|
+
function prepare(locators = []) {
|
|
468
|
+
for (const locator of locators)
|
|
469
|
+
unpreparedDemand.add(locator);
|
|
470
|
+
if (preparing)
|
|
363
471
|
return;
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
472
|
+
preparing = true;
|
|
473
|
+
void (async () => {
|
|
474
|
+
try {
|
|
475
|
+
for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
|
|
476
|
+
await yielded();
|
|
477
|
+
if (closed)
|
|
478
|
+
return;
|
|
479
|
+
unpreparedDemand.delete(waiting);
|
|
480
|
+
await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
finally {
|
|
484
|
+
preparing = false;
|
|
485
|
+
}
|
|
486
|
+
})();
|
|
367
487
|
}
|
|
368
488
|
function reachedRooms(plan) {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
489
|
+
const reach = plan.reach;
|
|
490
|
+
const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
491
|
+
const named = plan.rooms ??
|
|
492
|
+
(reached === null
|
|
493
|
+
? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
|
|
494
|
+
: [...reached]);
|
|
495
|
+
return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
|
|
496
|
+
}
|
|
497
|
+
function awaited(locator) {
|
|
498
|
+
return (!roomsListed ||
|
|
499
|
+
(rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
|
|
500
|
+
}
|
|
501
|
+
function covered(plan) {
|
|
502
|
+
const locators = reachedRooms(plan);
|
|
503
|
+
if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
|
|
504
|
+
return { pending: [] };
|
|
505
|
+
}
|
|
506
|
+
const pending = [];
|
|
507
|
+
const coverage = [];
|
|
508
|
+
const overlay = [];
|
|
509
|
+
for (const locator of locators) {
|
|
510
|
+
const replica = replicas.get(locator);
|
|
511
|
+
if (replica === undefined) {
|
|
512
|
+
if (awaited(locator))
|
|
513
|
+
pending.push({ room: locator, awaits: 'acquisition' });
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
if (!prepared.has(locator)) {
|
|
517
|
+
pending.push({ room: locator, awaits: 'preparation' });
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
coverage.push({
|
|
521
|
+
room: locator,
|
|
522
|
+
generation: replica.confirmed.record.transfer,
|
|
523
|
+
current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
|
|
524
|
+
});
|
|
525
|
+
for (const batch of replica.queue)
|
|
526
|
+
overlay.push(batch.id);
|
|
527
|
+
}
|
|
528
|
+
if (pending.length > 0)
|
|
529
|
+
return { pending };
|
|
530
|
+
return { coverage: { rooms: coverage, indexes: [], overlay } };
|
|
531
|
+
}
|
|
532
|
+
function awaitPreparation(cover) {
|
|
533
|
+
if (!('pending' in cover))
|
|
534
|
+
return;
|
|
535
|
+
const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
|
|
536
|
+
if (waiting.length > 0)
|
|
537
|
+
prepare(waiting.map(({ room }) => room));
|
|
538
|
+
}
|
|
539
|
+
function acquires(room) {
|
|
540
|
+
return (demanded(demand, room) ||
|
|
541
|
+
(replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
|
|
542
|
+
queuesWithoutReplica.has(room.locator));
|
|
543
|
+
}
|
|
544
|
+
function acquiredLocators() {
|
|
545
|
+
return [...discovered.values()]
|
|
546
|
+
.filter(acquires)
|
|
547
|
+
.map((room) => room.locator)
|
|
548
|
+
.sort();
|
|
549
|
+
}
|
|
550
|
+
function heldDemand(acquired) {
|
|
551
|
+
if (demand.whole)
|
|
552
|
+
return null;
|
|
553
|
+
return { rooms: acquired.map((room) => room.locator), conversations: demand.conversations };
|
|
554
|
+
}
|
|
555
|
+
function applyDemand(next) {
|
|
556
|
+
const before = acquiredLocators();
|
|
557
|
+
const conversed = demand.conversations;
|
|
558
|
+
demand = next;
|
|
559
|
+
const after = acquiredLocators();
|
|
560
|
+
queueReadiness();
|
|
561
|
+
if (!conversed && next.conversations)
|
|
562
|
+
scheduleCurrency();
|
|
563
|
+
if (JSON.stringify(before) === JSON.stringify(after) && conversed === next.conversations)
|
|
564
|
+
return;
|
|
565
|
+
journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
|
|
566
|
+
if (options.adapter.roomStream === undefined)
|
|
567
|
+
void sync('unstreamed');
|
|
568
|
+
else if (socketOpen)
|
|
569
|
+
options.adapter.roomStream.reopen();
|
|
570
|
+
}
|
|
571
|
+
function acquire(next) {
|
|
572
|
+
if (closed)
|
|
573
|
+
return;
|
|
574
|
+
if (narrowing !== null)
|
|
575
|
+
clearTimeout(narrowing);
|
|
576
|
+
narrowing = null;
|
|
577
|
+
const widening = wider(next, demand);
|
|
578
|
+
if (!widening && !narrower(next, demand))
|
|
579
|
+
return;
|
|
580
|
+
if (widening || !roomsListed) {
|
|
581
|
+
applyDemand(next);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
// A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
|
|
585
|
+
narrowing = setTimeout(() => {
|
|
586
|
+
narrowing = null;
|
|
587
|
+
applyDemand(next);
|
|
588
|
+
}, 5_000);
|
|
589
|
+
}
|
|
590
|
+
function requiredRooms(reach) {
|
|
591
|
+
if (reach === null)
|
|
592
|
+
return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
|
|
593
|
+
const named = [...new Set(reach.rooms)].sort();
|
|
594
|
+
if (roomsListed)
|
|
595
|
+
return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
|
|
596
|
+
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
597
|
+
}
|
|
598
|
+
function roomState(locator, search) {
|
|
599
|
+
if (sourceRefusals.has(locator))
|
|
600
|
+
return 'refused';
|
|
601
|
+
const listedRoom = discovered.get(locator);
|
|
602
|
+
if (roomsListed &&
|
|
603
|
+
(listedRoom === undefined ||
|
|
604
|
+
listedRoom.replica.admission !== 'admitted' ||
|
|
605
|
+
unreadable.has(locator)))
|
|
606
|
+
return 'unadmitted';
|
|
607
|
+
if (!replicas.has(locator))
|
|
608
|
+
return 'acquiring';
|
|
609
|
+
if (!prepared.has(locator))
|
|
610
|
+
return 'preparing';
|
|
611
|
+
if (search && !searchIndex.rooms().has(locator))
|
|
612
|
+
return 'indexing';
|
|
613
|
+
return 'complete';
|
|
614
|
+
}
|
|
615
|
+
function conversationCoverage(reach, search) {
|
|
616
|
+
const declared = reach === null || reach.conversations;
|
|
617
|
+
if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
|
|
618
|
+
return 'undeclared';
|
|
619
|
+
if (!conversations.known() || !conversations.current())
|
|
620
|
+
return 'acquiring';
|
|
621
|
+
if (!search)
|
|
622
|
+
return 'complete';
|
|
623
|
+
const unindexed = conversations
|
|
624
|
+
.searchable()
|
|
625
|
+
.filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
|
|
626
|
+
if (unindexed.length === 0)
|
|
627
|
+
return 'complete';
|
|
628
|
+
index([], unindexed);
|
|
629
|
+
return 'indexing';
|
|
630
|
+
}
|
|
631
|
+
function connectionOf() {
|
|
632
|
+
if (socketOpen)
|
|
633
|
+
return 'connected';
|
|
634
|
+
if (status.providerUnreachable)
|
|
635
|
+
return 'unreachable';
|
|
636
|
+
return options.adapter.roomStream === undefined && status.lastCompleted !== null
|
|
637
|
+
? 'connected'
|
|
638
|
+
: 'connecting';
|
|
639
|
+
}
|
|
640
|
+
function refusedIn(locators) {
|
|
641
|
+
return locators.flatMap((room) => {
|
|
642
|
+
const held = sourceRefusals.get(room);
|
|
643
|
+
if (held === undefined)
|
|
644
|
+
return [];
|
|
645
|
+
const { file, location } = held.fields;
|
|
646
|
+
return [
|
|
647
|
+
{
|
|
648
|
+
room,
|
|
649
|
+
file: typeof file === 'string' ? file : '',
|
|
650
|
+
rule: held.code,
|
|
651
|
+
location: typeof location === 'string' ? location : null,
|
|
652
|
+
},
|
|
653
|
+
];
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
function readinessFor(port, reach) {
|
|
657
|
+
const required = requiredRooms(reach);
|
|
658
|
+
const search = port.search === 'required';
|
|
659
|
+
const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
|
|
660
|
+
const building = (state) => coverage
|
|
661
|
+
.filter((room) => room.state === state && !unindexable.has(room.room))
|
|
662
|
+
.map(({ room }) => room);
|
|
663
|
+
const preparing = building('preparing');
|
|
664
|
+
if (preparing.length > 0)
|
|
665
|
+
prepare(preparing);
|
|
666
|
+
const indexing = building('indexing');
|
|
667
|
+
if (indexing.length > 0)
|
|
668
|
+
index(indexing, []);
|
|
669
|
+
return readinessOf({
|
|
670
|
+
code: port.code,
|
|
671
|
+
coverage: {
|
|
672
|
+
listed: required.listed,
|
|
673
|
+
rooms: coverage,
|
|
674
|
+
conversations: conversationCoverage(reach, search),
|
|
675
|
+
search: search ? 'required' : 'unused',
|
|
676
|
+
},
|
|
677
|
+
persistence,
|
|
678
|
+
connection: connectionOf(),
|
|
679
|
+
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
680
|
+
capacity: capacity !== null &&
|
|
681
|
+
coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
|
|
682
|
+
? capacity
|
|
683
|
+
: port.capacity,
|
|
684
|
+
refused: refusedIn(required.rooms),
|
|
685
|
+
lost: storageLoss !== null,
|
|
686
|
+
established: targets[port.target]?.established === true,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
function establish(name) {
|
|
690
|
+
targets = { ...targets, [name]: { established: true } };
|
|
691
|
+
queueReadiness();
|
|
692
|
+
void durably((local) => local.keepTargets(targets)).catch(() => {
|
|
693
|
+
journal.record(`target record failed ${name}`);
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
function deliverReadiness(watch) {
|
|
697
|
+
if (!readinessWatches.has(watch))
|
|
698
|
+
return;
|
|
699
|
+
const readiness = readinessFor(watch.port, watch.reach);
|
|
700
|
+
if (readiness.ready && targets[watch.port.target]?.established !== true) {
|
|
701
|
+
establish(watch.port.target);
|
|
702
|
+
}
|
|
703
|
+
const shown = JSON.stringify(readiness);
|
|
704
|
+
if (shown === watch.last)
|
|
705
|
+
return;
|
|
706
|
+
watch.last = shown;
|
|
707
|
+
watch.deliver(structuredClone(readiness));
|
|
708
|
+
}
|
|
709
|
+
function queueReadiness() {
|
|
710
|
+
if (readinessQueued || closed || readinessWatches.size === 0)
|
|
711
|
+
return;
|
|
712
|
+
readinessQueued = true;
|
|
713
|
+
queueMicrotask(() => {
|
|
714
|
+
readinessQueued = false;
|
|
715
|
+
if (closed)
|
|
716
|
+
return;
|
|
717
|
+
for (const watch of [...readinessWatches])
|
|
718
|
+
deliverReadiness(watch);
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
function watchReadiness(port, reach, deliver) {
|
|
722
|
+
const watch = { port, reach, deliver, last: null };
|
|
723
|
+
readinessWatches.add(watch);
|
|
724
|
+
void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
|
|
725
|
+
return () => {
|
|
726
|
+
readinessWatches.delete(watch);
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
function layerBytes(layer) {
|
|
730
|
+
let bytes = 0;
|
|
731
|
+
for (const [path, text] of Object.entries(layer.documents))
|
|
732
|
+
bytes += (path.length + text.length) * 2;
|
|
733
|
+
return bytes;
|
|
734
|
+
}
|
|
735
|
+
function grownBy(next, previous) {
|
|
736
|
+
if (previous === undefined)
|
|
737
|
+
return layerBytes(next);
|
|
738
|
+
const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
|
|
739
|
+
return layerBytes({ ...next, documents });
|
|
740
|
+
}
|
|
741
|
+
function exceeded(error) {
|
|
742
|
+
return (typeof error === 'object' &&
|
|
743
|
+
error !== null &&
|
|
744
|
+
error.name === 'QuotaExceededError');
|
|
745
|
+
}
|
|
746
|
+
async function estimated() {
|
|
747
|
+
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
748
|
+
return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
|
|
749
|
+
}
|
|
750
|
+
function pause(locator, cause) {
|
|
751
|
+
paused.add(locator);
|
|
752
|
+
capacity = cause;
|
|
753
|
+
journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
|
|
754
|
+
queueReadiness();
|
|
755
|
+
if (rechecking !== null)
|
|
756
|
+
return;
|
|
757
|
+
// A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
|
|
758
|
+
rechecking = setInterval(() => {
|
|
759
|
+
void recheck();
|
|
760
|
+
}, 60_000);
|
|
761
|
+
}
|
|
762
|
+
async function recheck() {
|
|
763
|
+
const held = capacity;
|
|
764
|
+
if (held === null || closed)
|
|
765
|
+
return;
|
|
766
|
+
const available = await estimated().catch(() => null);
|
|
767
|
+
if (available === null || available < (held.needed ?? 0))
|
|
768
|
+
return;
|
|
769
|
+
if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
|
|
770
|
+
return;
|
|
771
|
+
resume();
|
|
772
|
+
}
|
|
773
|
+
function resume() {
|
|
774
|
+
if (rechecking !== null)
|
|
775
|
+
clearInterval(rechecking);
|
|
776
|
+
rechecking = null;
|
|
777
|
+
if (paused.size === 0 && capacity === null)
|
|
778
|
+
return;
|
|
779
|
+
paused.clear();
|
|
780
|
+
capacity = null;
|
|
781
|
+
journal.record('capacity resumed');
|
|
782
|
+
queueReadiness();
|
|
783
|
+
if (!closed && authorization !== undefined)
|
|
784
|
+
void sync('unstreamed');
|
|
785
|
+
}
|
|
786
|
+
async function reclaimed(local) {
|
|
787
|
+
const unreferenced = [...transfers.keys()].filter((locator) => {
|
|
788
|
+
const room = discovered.get(locator);
|
|
789
|
+
return room === undefined || !acquires(room);
|
|
790
|
+
});
|
|
791
|
+
for (const locator of unreferenced) {
|
|
792
|
+
await local.abandon(locator);
|
|
793
|
+
transfers.delete(locator);
|
|
794
|
+
}
|
|
795
|
+
if (unreferenced.length > 0)
|
|
796
|
+
journal.record(`capacity reclaimed ${unreferenced.length}`);
|
|
797
|
+
return unreferenced.length > 0;
|
|
798
|
+
}
|
|
799
|
+
async function admitted(local, locator, needed) {
|
|
800
|
+
if (paused.has(locator))
|
|
801
|
+
return false;
|
|
802
|
+
let available = await estimated().catch(() => null);
|
|
803
|
+
if (available === null || available >= needed)
|
|
804
|
+
return true;
|
|
805
|
+
if (await reclaimed(local))
|
|
806
|
+
available = await estimated().catch(() => null);
|
|
807
|
+
if (available === null || available >= needed)
|
|
808
|
+
return true;
|
|
809
|
+
pause(locator, { cause: 'insufficient-space', needed, available });
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
async function exceededOn(locator, bytes) {
|
|
813
|
+
const available = await estimated().catch(() => null);
|
|
814
|
+
pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
|
|
815
|
+
}
|
|
816
|
+
function seedRoomHeld(room, version) {
|
|
817
|
+
return prepared.has(room) && reachedBy(room, version);
|
|
818
|
+
}
|
|
819
|
+
function seedHeld(at) {
|
|
820
|
+
return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
|
|
821
|
+
}
|
|
822
|
+
function seedApplies(plan) {
|
|
823
|
+
const seed = seeds.get(plan.key);
|
|
824
|
+
if (seed === undefined)
|
|
372
825
|
return null;
|
|
373
|
-
|
|
826
|
+
if (!seedHeld(seed.at))
|
|
827
|
+
return seed;
|
|
828
|
+
seeds.delete(plan.key);
|
|
829
|
+
return null;
|
|
830
|
+
}
|
|
831
|
+
function seedBehind(plan, cover) {
|
|
832
|
+
const seed = seedApplies(plan);
|
|
833
|
+
if (seed === null || 'pending' in cover)
|
|
834
|
+
return cover;
|
|
835
|
+
return {
|
|
836
|
+
pending: Object.entries(seed.at)
|
|
837
|
+
.filter(([room, version]) => !seedRoomHeld(room, version))
|
|
838
|
+
.map(([room]) => ({ room, awaits: 'acquisition' })),
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
function seed(entry) {
|
|
842
|
+
if (seedHeld(entry.at))
|
|
843
|
+
return;
|
|
844
|
+
for (const { query, rows } of entry.queries) {
|
|
845
|
+
const key = queryKey(query);
|
|
846
|
+
seeds.set(key, { rows, at: entry.at });
|
|
847
|
+
for (const shown of seedWatchers.get(key) ?? [])
|
|
848
|
+
shown();
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
async function rowsQuery(plan) {
|
|
852
|
+
const before = covered(plan);
|
|
853
|
+
awaitPreparation(before);
|
|
854
|
+
const seeded = seedApplies(plan);
|
|
855
|
+
if (seeded !== null)
|
|
856
|
+
return seededResult(seeded).result;
|
|
857
|
+
if ('pending' in before)
|
|
858
|
+
return before;
|
|
859
|
+
const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
|
|
860
|
+
awaitPreparation(cover);
|
|
861
|
+
if ('pending' in cover)
|
|
862
|
+
return cover;
|
|
863
|
+
return 'groups' in rows
|
|
864
|
+
? { groups: rows.groups, coverage: cover.coverage }
|
|
865
|
+
: { documents: rows.documents, coverage: cover.coverage };
|
|
866
|
+
}
|
|
867
|
+
async function indexConversation(conversation) {
|
|
868
|
+
searchIndex.indexMessages(conversation, [], false);
|
|
869
|
+
let after = null;
|
|
870
|
+
for (;;) {
|
|
871
|
+
const batch = await durably((local) => local.messagesAfter(conversation, after));
|
|
872
|
+
if (closed)
|
|
873
|
+
return;
|
|
874
|
+
searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
|
|
875
|
+
if (batch.length === 0)
|
|
876
|
+
return;
|
|
877
|
+
after = batch[batch.length - 1].identity;
|
|
878
|
+
}
|
|
374
879
|
}
|
|
375
|
-
function
|
|
376
|
-
|
|
377
|
-
|
|
880
|
+
function indexNext() {
|
|
881
|
+
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
|
|
882
|
+
if (room !== undefined) {
|
|
883
|
+
roomsToIndex.delete(room);
|
|
884
|
+
return () => prepared.has(room)
|
|
885
|
+
? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
|
|
886
|
+
: undefined;
|
|
887
|
+
}
|
|
888
|
+
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
889
|
+
if (conversation === undefined)
|
|
890
|
+
return null;
|
|
891
|
+
conversationsToIndex.delete(conversation);
|
|
892
|
+
return () => indexConversation(conversation);
|
|
893
|
+
}
|
|
894
|
+
function index(rooms, named) {
|
|
895
|
+
for (const room of rooms)
|
|
896
|
+
roomsToIndex.add(room);
|
|
897
|
+
for (const conversation of named)
|
|
898
|
+
conversationsToIndex.add(conversation);
|
|
899
|
+
if (indexing)
|
|
900
|
+
return;
|
|
901
|
+
indexing = true;
|
|
902
|
+
void (async () => {
|
|
903
|
+
try {
|
|
904
|
+
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
905
|
+
await yielded();
|
|
906
|
+
await unit();
|
|
907
|
+
queueReadiness();
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
finally {
|
|
911
|
+
indexing = false;
|
|
912
|
+
}
|
|
913
|
+
})().catch(() => { });
|
|
378
914
|
}
|
|
379
915
|
function publishChangeSet(set) {
|
|
380
916
|
applyChangeSet(set);
|
|
@@ -391,34 +927,7 @@ export function composeRuntime(options) {
|
|
|
391
927
|
emitPresentation(app);
|
|
392
928
|
conversations.changed(set);
|
|
393
929
|
}
|
|
394
|
-
function stopTimer() {
|
|
395
|
-
if (resetTimer === undefined)
|
|
396
|
-
return;
|
|
397
|
-
clearTimeout(resetTimer);
|
|
398
|
-
resetTimer = undefined;
|
|
399
|
-
}
|
|
400
|
-
function armTimer() {
|
|
401
|
-
stopTimer();
|
|
402
|
-
if (closed || stopping)
|
|
403
|
-
return;
|
|
404
|
-
let earliest = null;
|
|
405
|
-
for (const { submission } of pendings.values()) {
|
|
406
|
-
if (submission.state !== 'reset')
|
|
407
|
-
continue;
|
|
408
|
-
const at = Date.parse(submission.reset);
|
|
409
|
-
if (earliest === null || at < earliest)
|
|
410
|
-
earliest = at;
|
|
411
|
-
}
|
|
412
|
-
if (earliest === null)
|
|
413
|
-
return;
|
|
414
|
-
// A Save House held back goes again only once the reset its answer named has passed.
|
|
415
|
-
resetTimer = setTimeout(() => {
|
|
416
|
-
resetTimer = undefined;
|
|
417
|
-
submitAll();
|
|
418
|
-
}, Math.max(0, earliest - Date.now()));
|
|
419
|
-
}
|
|
420
930
|
async function refuse(reason, provider = null) {
|
|
421
|
-
stopTimer();
|
|
422
931
|
discoveryPaused = true;
|
|
423
932
|
try {
|
|
424
933
|
if (reason === 'revoked-authorization') {
|
|
@@ -503,6 +1012,9 @@ export function composeRuntime(options) {
|
|
|
503
1012
|
if (fence !== undefined && fence.erased && fence.at < started)
|
|
504
1013
|
fences.delete(room.locator);
|
|
505
1014
|
}
|
|
1015
|
+
await loseRooms(found
|
|
1016
|
+
.filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
|
|
1017
|
+
.map((room) => room.locator));
|
|
506
1018
|
const listed = found.filter((room) => !fences.has(room.locator));
|
|
507
1019
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
508
1020
|
for (const locator of listedLocators)
|
|
@@ -527,6 +1039,12 @@ export function composeRuntime(options) {
|
|
|
527
1039
|
emitRooms();
|
|
528
1040
|
return listed;
|
|
529
1041
|
}
|
|
1042
|
+
function heldLocally(locator) {
|
|
1043
|
+
return (replicas.has(locator) ||
|
|
1044
|
+
roomDrafts.has(locator) ||
|
|
1045
|
+
transfers.has(locator) ||
|
|
1046
|
+
queuesWithoutReplica.has(locator));
|
|
1047
|
+
}
|
|
530
1048
|
function unlistedHeld(listed) {
|
|
531
1049
|
return [
|
|
532
1050
|
...new Set([
|
|
@@ -563,10 +1081,14 @@ export function composeRuntime(options) {
|
|
|
563
1081
|
rooms.delete(locator);
|
|
564
1082
|
roomDrafts.delete(locator);
|
|
565
1083
|
transfers.delete(locator);
|
|
1084
|
+
catching.delete(locator);
|
|
566
1085
|
discovered.delete(locator);
|
|
567
1086
|
announced.delete(locator);
|
|
568
1087
|
unreadable.delete(locator);
|
|
569
1088
|
stale.delete(locator);
|
|
1089
|
+
repairable.delete(locator);
|
|
1090
|
+
unindexable.delete(locator);
|
|
1091
|
+
paused.delete(locator);
|
|
570
1092
|
scopedRefusals.delete(locator);
|
|
571
1093
|
sourceRefusals.delete(locator);
|
|
572
1094
|
declarationRefusals.delete(locator);
|
|
@@ -582,7 +1104,9 @@ export function composeRuntime(options) {
|
|
|
582
1104
|
return fresh;
|
|
583
1105
|
publishChangeSet(mergeChangeSets(removed));
|
|
584
1106
|
for (const locator of fresh)
|
|
585
|
-
|
|
1107
|
+
prepared.delete(locator);
|
|
1108
|
+
searchIndex.forget(fresh);
|
|
1109
|
+
readSide.recover();
|
|
586
1110
|
moveTo({ ...status, ...attention() });
|
|
587
1111
|
return fresh;
|
|
588
1112
|
}
|
|
@@ -618,7 +1142,7 @@ export function composeRuntime(options) {
|
|
|
618
1142
|
return model;
|
|
619
1143
|
}
|
|
620
1144
|
async function hydrate(opened) {
|
|
621
|
-
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
|
|
1145
|
+
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
|
|
622
1146
|
opened.all(),
|
|
623
1147
|
opened.queues(),
|
|
624
1148
|
opened.roomDrafts(),
|
|
@@ -630,7 +1154,9 @@ export function composeRuntime(options) {
|
|
|
630
1154
|
opened.capabilities(),
|
|
631
1155
|
opened.member(),
|
|
632
1156
|
opened.transfers(),
|
|
1157
|
+
opened.targets(),
|
|
633
1158
|
]);
|
|
1159
|
+
targets = heldTargets;
|
|
634
1160
|
transfers.clear();
|
|
635
1161
|
for (const record of staged)
|
|
636
1162
|
transfers.set(record.locator, record);
|
|
@@ -692,6 +1218,7 @@ export function composeRuntime(options) {
|
|
|
692
1218
|
async function readState(opened) {
|
|
693
1219
|
const { lastCompleted, locators, read } = await hydrate(opened);
|
|
694
1220
|
applyChangeSet(read);
|
|
1221
|
+
prepare();
|
|
695
1222
|
if (replicas.size > 0)
|
|
696
1223
|
emitRooms();
|
|
697
1224
|
if (member !== null)
|
|
@@ -807,17 +1334,19 @@ export function composeRuntime(options) {
|
|
|
807
1334
|
moveTo({ ...status, ...attention() });
|
|
808
1335
|
throw error;
|
|
809
1336
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1337
|
+
return () => {
|
|
1338
|
+
queuesWithoutReplica.delete(locator);
|
|
1339
|
+
for (const { batch, returned } of unqueued) {
|
|
1340
|
+
const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
|
|
1341
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
1342
|
+
}
|
|
1343
|
+
replicas.set(locator, replicaOf(confirmed, kept, before));
|
|
1344
|
+
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
1345
|
+
room: settling.room,
|
|
1346
|
+
queue: kept.length !== previousQueue.length,
|
|
1347
|
+
drafts: [...written.keys()],
|
|
1348
|
+
});
|
|
1349
|
+
};
|
|
821
1350
|
}
|
|
822
1351
|
function adoptModels(locator, record) {
|
|
823
1352
|
for (const [attached, model] of [
|
|
@@ -853,8 +1382,13 @@ export function composeRuntime(options) {
|
|
|
853
1382
|
function publish(local, room, confirmed, landing, live) {
|
|
854
1383
|
const { base, stored } = landing;
|
|
855
1384
|
return exclusive(async () => {
|
|
856
|
-
if (
|
|
1385
|
+
if (stored !== undefined) {
|
|
1386
|
+
await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
|
|
1387
|
+
}
|
|
1388
|
+
if (!live()) {
|
|
1389
|
+
caughtUpTo(room.locator);
|
|
857
1390
|
return true;
|
|
1391
|
+
}
|
|
858
1392
|
const prior = replicas.get(room.locator);
|
|
859
1393
|
if (base !== undefined &&
|
|
860
1394
|
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
@@ -868,17 +1402,33 @@ export function composeRuntime(options) {
|
|
|
868
1402
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
869
1403
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
870
1404
|
});
|
|
871
|
-
|
|
872
|
-
|
|
1405
|
+
let settled;
|
|
1406
|
+
try {
|
|
1407
|
+
settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
|
|
1408
|
+
}
|
|
1409
|
+
catch (error) {
|
|
1410
|
+
if (!exceeded(error))
|
|
1411
|
+
throw error;
|
|
1412
|
+
await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
|
|
1413
|
+
caughtUpTo(room.locator);
|
|
1414
|
+
return true;
|
|
1415
|
+
}
|
|
1416
|
+
const set = settled();
|
|
1417
|
+
if (stored !== undefined) {
|
|
873
1418
|
transfers.delete(room.locator);
|
|
1419
|
+
catching.delete(room.locator);
|
|
1420
|
+
}
|
|
874
1421
|
for (const batch of accepted)
|
|
875
1422
|
pendings.delete(batch.id);
|
|
876
1423
|
stale.delete(room.locator);
|
|
1424
|
+
unindexable.delete(room.locator);
|
|
877
1425
|
if (unreadable.delete(room.locator))
|
|
878
1426
|
scopedRefusals.delete(room.locator);
|
|
879
1427
|
sourceRefusals.delete(room.locator);
|
|
880
1428
|
declarationRefusals.delete(room.locator);
|
|
881
1429
|
list(room, true);
|
|
1430
|
+
if (prior === undefined)
|
|
1431
|
+
prepared.add(room.locator);
|
|
882
1432
|
publishChangeSet(set);
|
|
883
1433
|
moveTo({ ...status, ...attention() });
|
|
884
1434
|
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
@@ -887,6 +1437,7 @@ export function composeRuntime(options) {
|
|
|
887
1437
|
}
|
|
888
1438
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
889
1439
|
const local = await openLocal();
|
|
1440
|
+
await referenced(locator);
|
|
890
1441
|
const replica = replicas.get(locator);
|
|
891
1442
|
const room = rooms.get(locator);
|
|
892
1443
|
if (replica === undefined || room === undefined) {
|
|
@@ -934,6 +1485,11 @@ export function composeRuntime(options) {
|
|
|
934
1485
|
changes,
|
|
935
1486
|
bases: parts.flatMap(({ bases }) => bases),
|
|
936
1487
|
writtenBy,
|
|
1488
|
+
clean: {
|
|
1489
|
+
text: replica.confirmed.record.documents,
|
|
1490
|
+
binaries: replica.confirmed.record.binaries,
|
|
1491
|
+
composition: compositionOf(replica.confirmed.record),
|
|
1492
|
+
},
|
|
937
1493
|
};
|
|
938
1494
|
const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
|
|
939
1495
|
if (checked?.kind === 'refused')
|
|
@@ -1274,8 +1830,20 @@ export function composeRuntime(options) {
|
|
|
1274
1830
|
stage: (record, page) => exclusive(async () => {
|
|
1275
1831
|
if (!live())
|
|
1276
1832
|
return false;
|
|
1277
|
-
|
|
1833
|
+
const bytes = layerBytes(page);
|
|
1834
|
+
if (!(await admitted(local, record.locator, bytes)))
|
|
1835
|
+
return false;
|
|
1836
|
+
try {
|
|
1837
|
+
await local.stage(record, page);
|
|
1838
|
+
}
|
|
1839
|
+
catch (error) {
|
|
1840
|
+
if (!exceeded(error))
|
|
1841
|
+
throw error;
|
|
1842
|
+
await exceededOn(record.locator, bytes);
|
|
1843
|
+
return false;
|
|
1844
|
+
}
|
|
1278
1845
|
transfers.set(record.locator, record);
|
|
1846
|
+
readSide.recover();
|
|
1279
1847
|
return true;
|
|
1280
1848
|
}),
|
|
1281
1849
|
abandon: (locator) => exclusive(async () => {
|
|
@@ -1283,6 +1851,7 @@ export function composeRuntime(options) {
|
|
|
1283
1851
|
return;
|
|
1284
1852
|
await local.abandon(locator);
|
|
1285
1853
|
transfers.delete(locator);
|
|
1854
|
+
readSide.recover();
|
|
1286
1855
|
}),
|
|
1287
1856
|
};
|
|
1288
1857
|
}
|
|
@@ -1295,6 +1864,10 @@ export function composeRuntime(options) {
|
|
|
1295
1864
|
async function pullBootstrap(authorized, room, local, live) {
|
|
1296
1865
|
if (!live())
|
|
1297
1866
|
return unconfirmed;
|
|
1867
|
+
if (replicas.has(room.locator)) {
|
|
1868
|
+
catching.add(room.locator);
|
|
1869
|
+
readSide.recover();
|
|
1870
|
+
}
|
|
1298
1871
|
try {
|
|
1299
1872
|
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
1300
1873
|
collection: modelOf(room.locator),
|
|
@@ -1302,14 +1875,24 @@ export function composeRuntime(options) {
|
|
|
1302
1875
|
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
1303
1876
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
1304
1877
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1878
|
+
if (result.kind !== 'staged')
|
|
1879
|
+
caughtUpTo(room.locator);
|
|
1305
1880
|
return result;
|
|
1306
1881
|
}
|
|
1307
1882
|
catch (error) {
|
|
1308
1883
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
1884
|
+
caughtUpTo(room.locator);
|
|
1309
1885
|
throw error;
|
|
1310
1886
|
}
|
|
1311
1887
|
}
|
|
1888
|
+
function caughtUpTo(locator) {
|
|
1889
|
+
if (catching.delete(locator))
|
|
1890
|
+
readSide.recover();
|
|
1891
|
+
}
|
|
1312
1892
|
async function advance(room, local, authorized, live) {
|
|
1893
|
+
await referenced(room.locator);
|
|
1894
|
+
if (!live() || paused.has(room.locator))
|
|
1895
|
+
return unconfirmed;
|
|
1313
1896
|
const held = replicas.get(room.locator);
|
|
1314
1897
|
if (held !== undefined) {
|
|
1315
1898
|
const refreshed = await pullRefresh(authorized, held.confirmed, live);
|
|
@@ -1340,6 +1923,7 @@ export function composeRuntime(options) {
|
|
|
1340
1923
|
return refreshed;
|
|
1341
1924
|
if (opened !== null) {
|
|
1342
1925
|
owed.add(room.locator);
|
|
1926
|
+
readSide.recover();
|
|
1343
1927
|
return unconfirmed;
|
|
1344
1928
|
}
|
|
1345
1929
|
}
|
|
@@ -1352,10 +1936,19 @@ export function composeRuntime(options) {
|
|
|
1352
1936
|
return null;
|
|
1353
1937
|
}
|
|
1354
1938
|
async function withhold(withheld, room, local, progress) {
|
|
1939
|
+
if (withheld.kind === 'unconfirmed')
|
|
1940
|
+
return;
|
|
1355
1941
|
if (withheld.kind === 'refused')
|
|
1356
1942
|
stale.delete(room.locator);
|
|
1357
1943
|
else
|
|
1358
1944
|
stale.add(room.locator);
|
|
1945
|
+
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
1946
|
+
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
1947
|
+
repairable.set(room.locator, reset);
|
|
1948
|
+
repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
|
|
1949
|
+
}
|
|
1950
|
+
else
|
|
1951
|
+
repairable.delete(room.locator);
|
|
1359
1952
|
if (withheld.kind === 'scoped-refusal') {
|
|
1360
1953
|
await scoped(withheld, room, local);
|
|
1361
1954
|
}
|
|
@@ -1391,6 +1984,7 @@ export function composeRuntime(options) {
|
|
|
1391
1984
|
unreadable.add(room.locator);
|
|
1392
1985
|
list(room, false);
|
|
1393
1986
|
emitRooms();
|
|
1987
|
+
moveTo({ ...status, ...attention() });
|
|
1394
1988
|
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
1395
1989
|
}
|
|
1396
1990
|
function leaving(batch, result) {
|
|
@@ -1412,6 +2006,7 @@ export function composeRuntime(options) {
|
|
|
1412
2006
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1413
2007
|
}
|
|
1414
2008
|
async function adoptAccepted(room, local, submitted, accepted) {
|
|
2009
|
+
await referenced(room.locator);
|
|
1415
2010
|
if (!continuing())
|
|
1416
2011
|
return unconfirmed;
|
|
1417
2012
|
const current = replicas.get(room.locator);
|
|
@@ -1433,11 +2028,12 @@ export function composeRuntime(options) {
|
|
|
1433
2028
|
emitQueue(room.locator);
|
|
1434
2029
|
return applied;
|
|
1435
2030
|
}
|
|
1436
|
-
const
|
|
2031
|
+
const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
1437
2032
|
pendings.delete(submitted.id);
|
|
1438
2033
|
unreadable.delete(room.locator);
|
|
1439
2034
|
sourceRefusals.delete(room.locator);
|
|
1440
2035
|
declarationRefusals.delete(room.locator);
|
|
2036
|
+
const set = settled();
|
|
1441
2037
|
list(room, true);
|
|
1442
2038
|
publishChangeSet(set);
|
|
1443
2039
|
moveTo({ ...status, ...attention() });
|
|
@@ -1451,14 +2047,14 @@ export function composeRuntime(options) {
|
|
|
1451
2047
|
refusedSource(room, failed);
|
|
1452
2048
|
return false;
|
|
1453
2049
|
}
|
|
1454
|
-
async function submitRoom(room, local, authorized, progress) {
|
|
1455
|
-
while (continuing() && authorization !== undefined) {
|
|
2050
|
+
async function submitRoom(room, local, authorized, progress, signal) {
|
|
2051
|
+
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
1456
2052
|
const replica = replicas.get(room.locator);
|
|
1457
2053
|
const head = replica?.queue[0];
|
|
1458
2054
|
if (replica === undefined || head === undefined)
|
|
1459
|
-
return;
|
|
2055
|
+
return 'done';
|
|
1460
2056
|
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1461
|
-
return;
|
|
2057
|
+
return 'done';
|
|
1462
2058
|
const state = pendings.get(head.id);
|
|
1463
2059
|
const { submission, lost: earlier } = state ?? {
|
|
1464
2060
|
submission: { state: 'waiting' },
|
|
@@ -1466,24 +2062,30 @@ export function composeRuntime(options) {
|
|
|
1466
2062
|
};
|
|
1467
2063
|
if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
|
|
1468
2064
|
progress.complete = false;
|
|
1469
|
-
return;
|
|
2065
|
+
return { at: Date.parse(submission.reset) };
|
|
1470
2066
|
}
|
|
1471
2067
|
if (writesHeld()) {
|
|
1472
2068
|
progress.complete = false;
|
|
1473
2069
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1474
|
-
return;
|
|
2070
|
+
return 'done';
|
|
1475
2071
|
}
|
|
1476
2072
|
const lost = earlier || submission.state === 'submitted';
|
|
2073
|
+
if (lost && pastHorizon(head.id)) {
|
|
2074
|
+
await discardPending(room, local, signal);
|
|
2075
|
+
return 'done';
|
|
2076
|
+
}
|
|
1477
2077
|
let result;
|
|
1478
2078
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
1479
2079
|
await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
|
|
1480
2080
|
if (discoveryPaused || fences.has(room.locator))
|
|
1481
|
-
return;
|
|
1482
|
-
if (writesHeld()) {
|
|
2081
|
+
return 'done';
|
|
2082
|
+
if (signal.aborted || writesHeld()) {
|
|
1483
2083
|
await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
|
|
2084
|
+
if (signal.aborted)
|
|
2085
|
+
return 'done';
|
|
1484
2086
|
progress.complete = false;
|
|
1485
2087
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1486
|
-
return;
|
|
2088
|
+
return 'done';
|
|
1487
2089
|
}
|
|
1488
2090
|
const opensBefore = openSignals;
|
|
1489
2091
|
const answered = awaitingAnswer(room.locator);
|
|
@@ -1499,32 +2101,36 @@ export function composeRuntime(options) {
|
|
|
1499
2101
|
progress.complete = false;
|
|
1500
2102
|
if (continuing())
|
|
1501
2103
|
progress.reachable = false;
|
|
1502
|
-
return;
|
|
2104
|
+
return 'again';
|
|
2105
|
+
}
|
|
2106
|
+
if (signal.aborted) {
|
|
2107
|
+
answered();
|
|
2108
|
+
return 'done';
|
|
1503
2109
|
}
|
|
1504
2110
|
if (result.kind !== 'accepted' || !continuing())
|
|
1505
2111
|
answered();
|
|
1506
2112
|
if (!continuing())
|
|
1507
|
-
return;
|
|
2113
|
+
return 'done';
|
|
1508
2114
|
if (result.kind === 'transport-uncertainty') {
|
|
1509
2115
|
progress.complete = false;
|
|
1510
2116
|
progress.reachable = false;
|
|
1511
|
-
return;
|
|
2117
|
+
return 'again';
|
|
1512
2118
|
}
|
|
1513
2119
|
if (result.kind === 'room-not-readable') {
|
|
1514
2120
|
progress.complete = false;
|
|
1515
2121
|
unreadable.add(room.locator);
|
|
1516
2122
|
list(room, false);
|
|
1517
2123
|
emitRooms();
|
|
1518
|
-
return;
|
|
2124
|
+
return 'done';
|
|
1519
2125
|
}
|
|
1520
2126
|
if (result.kind === 'revoked-authorization') {
|
|
1521
2127
|
await refuse('revoked-authorization');
|
|
1522
|
-
return;
|
|
2128
|
+
return 'done';
|
|
1523
2129
|
}
|
|
1524
2130
|
if (result.kind === 'scoped-refusal') {
|
|
1525
2131
|
await scoped(result, room, local);
|
|
1526
2132
|
progress.complete = false;
|
|
1527
|
-
return;
|
|
2133
|
+
return 'done';
|
|
1528
2134
|
}
|
|
1529
2135
|
if (result.kind === 'write-halted') {
|
|
1530
2136
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
@@ -1533,16 +2139,24 @@ export function composeRuntime(options) {
|
|
|
1533
2139
|
continue;
|
|
1534
2140
|
haltedRefusal = true;
|
|
1535
2141
|
progress.complete = false;
|
|
1536
|
-
return;
|
|
2142
|
+
return 'done';
|
|
1537
2143
|
}
|
|
1538
2144
|
if (result.kind === 'conflicted' ||
|
|
1539
2145
|
result.kind === 'rejected' ||
|
|
1540
2146
|
result.kind === 'expired' ||
|
|
1541
2147
|
result.kind === 'retryable-refusal') {
|
|
2148
|
+
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
2149
|
+
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
2150
|
+
await discardPending(room, local, signal);
|
|
2151
|
+
return 'done';
|
|
2152
|
+
}
|
|
1542
2153
|
if (result.kind === 'expired') {
|
|
2154
|
+
await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
|
|
1543
2155
|
const withheld = await advance(room, local, authorized, continuing);
|
|
1544
|
-
if (withheld !== null)
|
|
1545
|
-
|
|
2156
|
+
if (withheld !== null) {
|
|
2157
|
+
await withhold(withheld, room, local, progress);
|
|
2158
|
+
return 'done';
|
|
2159
|
+
}
|
|
1546
2160
|
}
|
|
1547
2161
|
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
|
|
1548
2162
|
(result.kind === 'conflicted' && result.roomVersion !== undefined
|
|
@@ -1555,8 +2169,7 @@ export function composeRuntime(options) {
|
|
|
1555
2169
|
const reset = result.reset;
|
|
1556
2170
|
progress.complete = false;
|
|
1557
2171
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1558
|
-
|
|
1559
|
-
return;
|
|
2172
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
1560
2173
|
}
|
|
1561
2174
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
1562
2175
|
const left = leaving(head, result);
|
|
@@ -1567,28 +2180,73 @@ export function composeRuntime(options) {
|
|
|
1567
2180
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
1568
2181
|
return;
|
|
1569
2182
|
}
|
|
1570
|
-
const
|
|
2183
|
+
const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
1571
2184
|
leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
|
|
1572
2185
|
(result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
|
|
1573
2186
|
? { ...left, returned: null }
|
|
1574
2187
|
: left,
|
|
1575
2188
|
});
|
|
1576
|
-
publishChangeSet(
|
|
2189
|
+
publishChangeSet(settled());
|
|
1577
2190
|
moveTo({ ...status, ...attention() });
|
|
1578
2191
|
});
|
|
1579
2192
|
if (result.kind === 'expired')
|
|
1580
2193
|
continue;
|
|
1581
2194
|
const withheld = await advance(room, local, authorized, continuing);
|
|
1582
|
-
if (withheld !== null)
|
|
1583
|
-
|
|
2195
|
+
if (withheld !== null) {
|
|
2196
|
+
await withhold(withheld, room, local, progress);
|
|
2197
|
+
return 'done';
|
|
2198
|
+
}
|
|
1584
2199
|
continue;
|
|
1585
2200
|
}
|
|
1586
2201
|
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1587
2202
|
if (!kept) {
|
|
1588
2203
|
progress.complete = false;
|
|
1589
|
-
return;
|
|
2204
|
+
return 'done';
|
|
1590
2205
|
}
|
|
1591
2206
|
}
|
|
2207
|
+
return 'done';
|
|
2208
|
+
}
|
|
2209
|
+
function pastHorizon(identity) {
|
|
2210
|
+
return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
|
|
2211
|
+
}
|
|
2212
|
+
async function discardPending(room, local, signal) {
|
|
2213
|
+
const { locator } = room;
|
|
2214
|
+
const discarded = await exclusive(async () => {
|
|
2215
|
+
if (fences.has(locator))
|
|
2216
|
+
return false;
|
|
2217
|
+
const before = replicas.get(locator);
|
|
2218
|
+
const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
2219
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
2220
|
+
lastFenced.set(locator, ++fencings);
|
|
2221
|
+
try {
|
|
2222
|
+
await local.discard(locator);
|
|
2223
|
+
}
|
|
2224
|
+
finally {
|
|
2225
|
+
fences.delete(locator);
|
|
2226
|
+
}
|
|
2227
|
+
for (const batch of queue)
|
|
2228
|
+
pendings.delete(batch.id);
|
|
2229
|
+
queuesWithoutReplica.delete(locator);
|
|
2230
|
+
const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
|
|
2231
|
+
if (after !== null)
|
|
2232
|
+
replicas.set(locator, after);
|
|
2233
|
+
journal.record(`room discarded ${locator}`);
|
|
2234
|
+
publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
|
|
2235
|
+
moveTo({ ...status, ...attention() });
|
|
2236
|
+
return true;
|
|
2237
|
+
});
|
|
2238
|
+
if (!discarded)
|
|
2239
|
+
return;
|
|
2240
|
+
const progress = { complete: true, reachable: true };
|
|
2241
|
+
await roomTask(locator, async (live) => {
|
|
2242
|
+
await Promise.race([
|
|
2243
|
+
reload(room, local, progress, () => live() && !signal.aborted),
|
|
2244
|
+
abandonedOn(signal),
|
|
2245
|
+
]);
|
|
2246
|
+
});
|
|
2247
|
+
if (signal.aborted)
|
|
2248
|
+
return;
|
|
2249
|
+
await rest(progress);
|
|
1592
2250
|
}
|
|
1593
2251
|
function awaitingAnswer(locator) {
|
|
1594
2252
|
let release = () => { };
|
|
@@ -1668,6 +2326,11 @@ export function composeRuntime(options) {
|
|
|
1668
2326
|
});
|
|
1669
2327
|
}
|
|
1670
2328
|
async function scoped(result, room, local) {
|
|
2329
|
+
if ((result.scope === 'content' || result.scope === 'sync') && !result.retryable) {
|
|
2330
|
+
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
2331
|
+
await loseRooms([room.locator]);
|
|
2332
|
+
return;
|
|
2333
|
+
}
|
|
1671
2334
|
scopedRefusals.set(room.locator, {
|
|
1672
2335
|
code: result.code,
|
|
1673
2336
|
fields: result.fields,
|
|
@@ -1714,15 +2377,16 @@ export function composeRuntime(options) {
|
|
|
1714
2377
|
});
|
|
1715
2378
|
}
|
|
1716
2379
|
async function synchronize(listed, local, progress, attempt, live) {
|
|
1717
|
-
const
|
|
2380
|
+
const acquired = listed.filter(acquires);
|
|
2381
|
+
const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
|
|
1718
2382
|
if (!live())
|
|
1719
2383
|
return;
|
|
1720
|
-
const ready =
|
|
2384
|
+
const ready = acquired.filter((_, index) => {
|
|
1721
2385
|
const outcome = prepared[index];
|
|
1722
2386
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1723
2387
|
});
|
|
1724
2388
|
if (attempt === 'opened') {
|
|
1725
|
-
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
2389
|
+
options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
|
|
1726
2390
|
handed = true;
|
|
1727
2391
|
}
|
|
1728
2392
|
discovering?.release();
|
|
@@ -1750,12 +2414,7 @@ export function composeRuntime(options) {
|
|
|
1750
2414
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1751
2415
|
if (discoveryPaused || authorization === undefined)
|
|
1752
2416
|
return false;
|
|
1753
|
-
|
|
1754
|
-
if (replicas.has(room.locator))
|
|
1755
|
-
progress.complete = false;
|
|
1756
|
-
return false;
|
|
1757
|
-
}
|
|
1758
|
-
return true;
|
|
2417
|
+
return room.replica.admission === 'admitted';
|
|
1759
2418
|
}
|
|
1760
2419
|
async function caughtUp(room, local, progress, live) {
|
|
1761
2420
|
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
@@ -1789,17 +2448,22 @@ export function composeRuntime(options) {
|
|
|
1789
2448
|
await caughtUp(room, local, progress, live);
|
|
1790
2449
|
}
|
|
1791
2450
|
async function reload(room, local, progress, live) {
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
2451
|
+
try {
|
|
2452
|
+
if (!live() || authorization === undefined)
|
|
2453
|
+
return;
|
|
2454
|
+
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
2455
|
+
if (!live())
|
|
2456
|
+
return;
|
|
2457
|
+
if (bootstrapped.kind !== 'staged') {
|
|
2458
|
+
await withhold(bootstrapped, room, local, progress);
|
|
2459
|
+
return;
|
|
2460
|
+
}
|
|
2461
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
2462
|
+
await caughtUp(room, local, progress, live);
|
|
2463
|
+
}
|
|
2464
|
+
finally {
|
|
2465
|
+
caughtUpTo(room.locator);
|
|
1800
2466
|
}
|
|
1801
|
-
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
1802
|
-
await caughtUp(room, local, progress, live);
|
|
1803
2467
|
}
|
|
1804
2468
|
async function discoverRooms(local, progress, attempt, live) {
|
|
1805
2469
|
if (closed || authorization === undefined) {
|
|
@@ -1875,6 +2539,7 @@ export function composeRuntime(options) {
|
|
|
1875
2539
|
}
|
|
1876
2540
|
const previousCapabilities = capabilities;
|
|
1877
2541
|
capabilities = result.capabilities;
|
|
2542
|
+
outcomeRetention = result.retention.outcome;
|
|
1878
2543
|
notifyReady();
|
|
1879
2544
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1880
2545
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
@@ -2004,6 +2669,8 @@ export function composeRuntime(options) {
|
|
|
2004
2669
|
const progress = underway?.generation === generation ? underway.progress : null;
|
|
2005
2670
|
const cycle = progress ?? { complete: true, reachable: true };
|
|
2006
2671
|
const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
|
|
2672
|
+
if (opened === generation)
|
|
2673
|
+
opened = null;
|
|
2007
2674
|
return task(OPENING_LANE, async (live) => {
|
|
2008
2675
|
underway = null;
|
|
2009
2676
|
if (!live())
|
|
@@ -2016,8 +2683,6 @@ export function composeRuntime(options) {
|
|
|
2016
2683
|
await roomsCarried(expired);
|
|
2017
2684
|
if (!live())
|
|
2018
2685
|
return;
|
|
2019
|
-
if (opened === generation)
|
|
2020
|
-
opened = null;
|
|
2021
2686
|
const replaced = new Set(owed);
|
|
2022
2687
|
owed.clear();
|
|
2023
2688
|
try {
|
|
@@ -2025,6 +2690,9 @@ export function composeRuntime(options) {
|
|
|
2025
2690
|
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
2026
2691
|
const reloading = held.filter((room) => !expired.includes(room.locator) &&
|
|
2027
2692
|
(replaced.has(room.locator) || revoked(room.locator)));
|
|
2693
|
+
for (const room of reloading)
|
|
2694
|
+
catching.add(room.locator);
|
|
2695
|
+
readSide.recover();
|
|
2028
2696
|
const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
|
|
2029
2697
|
const reloaded = [...(await reloads), ...(await replacing)];
|
|
2030
2698
|
if (progress.reachable) {
|
|
@@ -2069,38 +2737,94 @@ export function composeRuntime(options) {
|
|
|
2069
2737
|
function submitLater(locator) {
|
|
2070
2738
|
if (localSaves)
|
|
2071
2739
|
return;
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2740
|
+
void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
|
|
2741
|
+
}
|
|
2742
|
+
async function submitPass(locator, signal) {
|
|
2743
|
+
const room = discovered.get(locator);
|
|
2744
|
+
if (closed || stopping || discoveryPaused || room === undefined)
|
|
2745
|
+
return 'done';
|
|
2746
|
+
const progress = { complete: true, reachable: true };
|
|
2747
|
+
let next;
|
|
2748
|
+
try {
|
|
2749
|
+
await ensureAuthorization();
|
|
2750
|
+
if (authorization === undefined)
|
|
2751
|
+
return 'done';
|
|
2752
|
+
next = await submitRoom(room, await openLocal(), authorization, progress, signal);
|
|
2753
|
+
}
|
|
2754
|
+
catch {
|
|
2755
|
+
progress.reachable = false;
|
|
2756
|
+
next = 'again';
|
|
2757
|
+
}
|
|
2758
|
+
if (signal.aborted)
|
|
2759
|
+
return 'done';
|
|
2760
|
+
await rest(progress);
|
|
2761
|
+
return next === 'again' && !socketOpen ? 'done' : next;
|
|
2762
|
+
}
|
|
2763
|
+
function repairLater(locator, when) {
|
|
2764
|
+
if (!socketOpen || repairing.has(locator))
|
|
2075
2765
|
return;
|
|
2766
|
+
void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
|
|
2767
|
+
}
|
|
2768
|
+
async function repairPass(locator, signal) {
|
|
2769
|
+
const room = discovered.get(locator);
|
|
2770
|
+
if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
|
|
2771
|
+
return 'done';
|
|
2772
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2773
|
+
return 'done';
|
|
2774
|
+
const progress = { complete: true, reachable: true };
|
|
2775
|
+
repairing.add(locator);
|
|
2776
|
+
try {
|
|
2777
|
+
const local = await openLocal();
|
|
2778
|
+
await roomTask(locator, async (live) => {
|
|
2779
|
+
const current = () => live() && !signal.aborted;
|
|
2780
|
+
if (!current() || authorization === undefined)
|
|
2781
|
+
return;
|
|
2782
|
+
const withheld = await Promise.race([
|
|
2783
|
+
advance(room, local, authorization, current),
|
|
2784
|
+
abandonedOn(signal),
|
|
2785
|
+
]);
|
|
2786
|
+
if (withheld !== null && current())
|
|
2787
|
+
await withhold(withheld, room, local, progress);
|
|
2788
|
+
});
|
|
2076
2789
|
}
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2790
|
+
catch {
|
|
2791
|
+
stale.add(locator);
|
|
2792
|
+
progress.reachable = false;
|
|
2793
|
+
}
|
|
2794
|
+
finally {
|
|
2795
|
+
repairing.delete(locator);
|
|
2796
|
+
}
|
|
2797
|
+
if (signal.aborted)
|
|
2798
|
+
return 'done';
|
|
2799
|
+
await rest(progress);
|
|
2800
|
+
if (!stale.has(locator)) {
|
|
2801
|
+
if ((replicas.get(locator)?.queue.length ?? 0) > 0)
|
|
2802
|
+
submitLater(locator);
|
|
2803
|
+
return 'done';
|
|
2804
|
+
}
|
|
2805
|
+
if (!repairable.has(locator))
|
|
2806
|
+
return 'done';
|
|
2807
|
+
const reset = repairable.get(locator);
|
|
2808
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
2809
|
+
}
|
|
2810
|
+
function resendLater() {
|
|
2811
|
+
if (!socketOpen)
|
|
2812
|
+
return;
|
|
2813
|
+
void recovery.attempt('resend', resendPass, { again: true });
|
|
2814
|
+
}
|
|
2815
|
+
async function resendPass(signal) {
|
|
2816
|
+
if (!socketOpen || closed || stopping || discoveryPaused)
|
|
2817
|
+
return 'done';
|
|
2818
|
+
const reading = { complete: true, reachable: true };
|
|
2819
|
+
try {
|
|
2820
|
+
await conversations.resend(reading);
|
|
2821
|
+
}
|
|
2822
|
+
catch {
|
|
2823
|
+
reading.reachable = false;
|
|
2824
|
+
}
|
|
2825
|
+
if (signal.aborted)
|
|
2826
|
+
return 'done';
|
|
2827
|
+
return reading.reachable && reading.complete ? 'done' : 'again';
|
|
2104
2828
|
}
|
|
2105
2829
|
function submitAll() {
|
|
2106
2830
|
for (const [locator, replica] of replicas) {
|
|
@@ -2115,15 +2839,21 @@ export function composeRuntime(options) {
|
|
|
2115
2839
|
return;
|
|
2116
2840
|
}
|
|
2117
2841
|
submitAll();
|
|
2842
|
+
for (const locator of stale)
|
|
2843
|
+
if (repairable.has(locator))
|
|
2844
|
+
repairLater(locator, {});
|
|
2118
2845
|
await conversations.reconcile();
|
|
2119
2846
|
if (status.providerUnreachable)
|
|
2120
2847
|
conversations.reconnected();
|
|
2121
2848
|
const reading = { complete: true, reachable: true };
|
|
2122
|
-
|
|
2849
|
+
if (demand.conversations)
|
|
2850
|
+
await conversations.catchUp(reading);
|
|
2123
2851
|
if (progress.reachable && reading.reachable)
|
|
2124
2852
|
await conversations.resend(reading);
|
|
2125
|
-
if (!reading.reachable)
|
|
2853
|
+
if (!reading.reachable) {
|
|
2126
2854
|
progress.reachable = false;
|
|
2855
|
+
resendLater();
|
|
2856
|
+
}
|
|
2127
2857
|
}
|
|
2128
2858
|
catch {
|
|
2129
2859
|
progress.complete = false;
|
|
@@ -2180,7 +2910,7 @@ export function composeRuntime(options) {
|
|
|
2180
2910
|
return;
|
|
2181
2911
|
const room = discovered.get(locator);
|
|
2182
2912
|
const held = replicas.get(locator);
|
|
2183
|
-
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2913
|
+
if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
|
|
2184
2914
|
return;
|
|
2185
2915
|
if (held !== undefined &&
|
|
2186
2916
|
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
@@ -2232,6 +2962,8 @@ export function composeRuntime(options) {
|
|
|
2232
2962
|
if (event.kind === 'opened') {
|
|
2233
2963
|
socketOpen = true;
|
|
2234
2964
|
generation += 1;
|
|
2965
|
+
conversations.reopened();
|
|
2966
|
+
recovery.resume();
|
|
2235
2967
|
void sync('opened');
|
|
2236
2968
|
}
|
|
2237
2969
|
else if (event.kind === 'closed')
|
|
@@ -2255,6 +2987,7 @@ export function composeRuntime(options) {
|
|
|
2255
2987
|
haltedRefusal = false;
|
|
2256
2988
|
if (mode !== serviceMode) {
|
|
2257
2989
|
serviceMode = mode;
|
|
2990
|
+
queueReadiness();
|
|
2258
2991
|
journal.record(`service mode ${mode}`);
|
|
2259
2992
|
for (const listener of serviceModeListeners)
|
|
2260
2993
|
listener(mode);
|
|
@@ -2267,6 +3000,8 @@ export function composeRuntime(options) {
|
|
|
2267
3000
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
2268
3001
|
return;
|
|
2269
3002
|
withdrawConversations = stream.observe((event) => {
|
|
3003
|
+
if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
|
|
3004
|
+
queueReadiness();
|
|
2270
3005
|
if (conversations.receive(event)) {
|
|
2271
3006
|
void ready.then(scheduleCurrency);
|
|
2272
3007
|
return;
|
|
@@ -2289,7 +3024,7 @@ export function composeRuntime(options) {
|
|
|
2289
3024
|
if (!live())
|
|
2290
3025
|
return;
|
|
2291
3026
|
await conversations.reconcile();
|
|
2292
|
-
if (conversations.current()) {
|
|
3027
|
+
if (conversations.current() || !demand.conversations) {
|
|
2293
3028
|
show();
|
|
2294
3029
|
return;
|
|
2295
3030
|
}
|
|
@@ -2307,13 +3042,18 @@ export function composeRuntime(options) {
|
|
|
2307
3042
|
if (closed)
|
|
2308
3043
|
return;
|
|
2309
3044
|
closed = true;
|
|
3045
|
+
if (narrowing !== null)
|
|
3046
|
+
clearTimeout(narrowing);
|
|
3047
|
+
if (rechecking !== null)
|
|
3048
|
+
clearInterval(rechecking);
|
|
3049
|
+
readinessWatches.clear();
|
|
2310
3050
|
withdrawStream?.();
|
|
2311
3051
|
withdrawStream = undefined;
|
|
2312
3052
|
withdrawConversations?.();
|
|
2313
3053
|
withdrawConversations = undefined;
|
|
2314
3054
|
withdrawServiceMode?.();
|
|
2315
3055
|
conversations.close();
|
|
2316
|
-
|
|
3056
|
+
void recovery.retire();
|
|
2317
3057
|
store?.close();
|
|
2318
3058
|
void opening?.then((opened) => {
|
|
2319
3059
|
opened.close();
|
|
@@ -2331,9 +3071,8 @@ export function composeRuntime(options) {
|
|
|
2331
3071
|
}
|
|
2332
3072
|
async function stop() {
|
|
2333
3073
|
stopping = true;
|
|
2334
|
-
|
|
3074
|
+
await recovery.retire();
|
|
2335
3075
|
await Promise.all(working);
|
|
2336
|
-
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
2337
3076
|
await mutations;
|
|
2338
3077
|
close();
|
|
2339
3078
|
}
|
|
@@ -2355,17 +3094,101 @@ export function composeRuntime(options) {
|
|
|
2355
3094
|
return planOf(input, roomFacts(reach), planReach(reach));
|
|
2356
3095
|
}
|
|
2357
3096
|
function watchPlan(plan, deliver) {
|
|
2358
|
-
|
|
2359
|
-
|
|
3097
|
+
let seeded = false;
|
|
3098
|
+
let pending = true;
|
|
3099
|
+
const shown = () => {
|
|
3100
|
+
const seed = seeds.get(plan.key);
|
|
3101
|
+
if (seed === undefined || seeded || !pending)
|
|
3102
|
+
return;
|
|
3103
|
+
seeded = true;
|
|
3104
|
+
deliver(seededResult(seed));
|
|
3105
|
+
};
|
|
3106
|
+
const watchers = seedWatchers.get(plan.key) ?? new Set();
|
|
3107
|
+
seedWatchers.set(plan.key, watchers);
|
|
3108
|
+
watchers.add(shown);
|
|
3109
|
+
const stop = readSide.watch(plan, identityOf(plan), () => {
|
|
3110
|
+
const cover = covered(plan);
|
|
3111
|
+
awaitPreparation(cover);
|
|
3112
|
+
return seedBehind(plan, cover);
|
|
3113
|
+
}, (message) => {
|
|
3114
|
+
const answered = message;
|
|
3115
|
+
pending = whole(answered) && 'pending' in answered.result;
|
|
3116
|
+
if (pending && seeds.has(plan.key)) {
|
|
3117
|
+
shown();
|
|
3118
|
+
return;
|
|
3119
|
+
}
|
|
3120
|
+
seeded = false;
|
|
3121
|
+
deliver(message);
|
|
3122
|
+
});
|
|
3123
|
+
return () => {
|
|
3124
|
+
watchers.delete(shown);
|
|
3125
|
+
if (watchers.size === 0)
|
|
3126
|
+
seedWatchers.delete(plan.key);
|
|
3127
|
+
stop();
|
|
3128
|
+
};
|
|
2360
3129
|
}
|
|
2361
3130
|
async function searched(input, reach) {
|
|
2362
3131
|
const plan = searchPlanOf(input);
|
|
2363
|
-
const
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
3132
|
+
const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
|
|
3133
|
+
for (;;) {
|
|
3134
|
+
const cover = covered(filter);
|
|
3135
|
+
awaitPreparation(cover);
|
|
3136
|
+
const pending = 'pending' in cover ? [...cover.pending] : [];
|
|
3137
|
+
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
3138
|
+
const held = covered({
|
|
3139
|
+
...filter,
|
|
3140
|
+
rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
|
|
3141
|
+
});
|
|
3142
|
+
const covering = 'pending' in held ? [] : held.coverage.rooms;
|
|
3143
|
+
const unindexed = covering
|
|
3144
|
+
.filter(({ room }) => !searchIndex.rooms().has(room))
|
|
3145
|
+
.map(({ room }) => room);
|
|
3146
|
+
for (const room of unindexed)
|
|
3147
|
+
pending.push({ room, awaits: 'index' });
|
|
3148
|
+
const listed = conversations.searchable();
|
|
3149
|
+
const named = reach !== null && !reach.conversations
|
|
3150
|
+
? []
|
|
3151
|
+
: (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
|
|
3152
|
+
const waiting = named.filter((one) => !searchIndex.conversations().has(one));
|
|
3153
|
+
for (const conversation of waiting)
|
|
3154
|
+
pending.push({ conversation, awaits: 'index' });
|
|
3155
|
+
if (unindexed.length > 0 || waiting.length > 0)
|
|
3156
|
+
index(unindexed, waiting);
|
|
3157
|
+
const complete = pending.length === 0 && !('pending' in cover);
|
|
3158
|
+
const answering = complete
|
|
3159
|
+
? filter
|
|
3160
|
+
: {
|
|
3161
|
+
...filter,
|
|
3162
|
+
rooms: covering
|
|
3163
|
+
.filter(({ room }) => searchIndex.rooms().has(room))
|
|
3164
|
+
.map(({ room }) => room),
|
|
3165
|
+
};
|
|
3166
|
+
const answered = covered(answering);
|
|
3167
|
+
if ('pending' in answered)
|
|
3168
|
+
continue;
|
|
3169
|
+
const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
|
|
3170
|
+
const seen = searchIndex.changes();
|
|
3171
|
+
await searchIndex.caughtUp();
|
|
3172
|
+
const selected = roomsAlone(answering) ? null : await rowsQuery(answering);
|
|
3173
|
+
if (selected !== null && 'pending' in selected)
|
|
3174
|
+
continue;
|
|
3175
|
+
if (searchIndex.changes() !== seen ||
|
|
3176
|
+
JSON.stringify(covered(answering)) !== JSON.stringify(answered))
|
|
3177
|
+
continue;
|
|
3178
|
+
const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms === null ? null : new Set(answering.rooms), plan.collection
|
|
3179
|
+
? new Map(answered.coverage.rooms.map(({ room }) => [
|
|
3180
|
+
room,
|
|
3181
|
+
replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
|
|
3182
|
+
]))
|
|
3183
|
+
: null);
|
|
3184
|
+
const coverage = {
|
|
3185
|
+
rooms: answered.coverage.rooms,
|
|
3186
|
+
conversations: searchedConversations,
|
|
3187
|
+
index: searchIndex.generation(),
|
|
3188
|
+
overlay: answered.coverage.overlay,
|
|
3189
|
+
};
|
|
3190
|
+
return complete ? { ...hits, coverage } : { pending, ...hits, coverage };
|
|
3191
|
+
}
|
|
2369
3192
|
}
|
|
2370
3193
|
const watch = (topic, input, deliver) => {
|
|
2371
3194
|
if (topic === 'room-drafts') {
|
|
@@ -2411,6 +3234,9 @@ export function composeRuntime(options) {
|
|
|
2411
3234
|
presentationListeners.delete(listener);
|
|
2412
3235
|
};
|
|
2413
3236
|
}
|
|
3237
|
+
if (topic === 'readiness') {
|
|
3238
|
+
return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
|
|
3239
|
+
}
|
|
2414
3240
|
if (topic === 'rooms') {
|
|
2415
3241
|
roomListeners.add(deliver);
|
|
2416
3242
|
deliver(listed());
|
|
@@ -2577,7 +3403,13 @@ export function composeRuntime(options) {
|
|
|
2577
3403
|
return journal.export();
|
|
2578
3404
|
},
|
|
2579
3405
|
persistence() {
|
|
2580
|
-
return
|
|
3406
|
+
return persistence === 'granted';
|
|
3407
|
+
},
|
|
3408
|
+
async retry() {
|
|
3409
|
+
journal.record('retry');
|
|
3410
|
+
unindexable.clear();
|
|
3411
|
+
queueReadiness();
|
|
3412
|
+
resume();
|
|
2581
3413
|
},
|
|
2582
3414
|
observe: observing(watch),
|
|
2583
3415
|
async reauthorize() {
|
|
@@ -2604,6 +3436,22 @@ export function composeRuntime(options) {
|
|
|
2604
3436
|
query: (plan) => rowsQuery(plan),
|
|
2605
3437
|
watchPlan,
|
|
2606
3438
|
search: searched,
|
|
3439
|
+
watchReadiness,
|
|
3440
|
+
};
|
|
3441
|
+
return {
|
|
3442
|
+
md,
|
|
3443
|
+
seed,
|
|
3444
|
+
stop,
|
|
3445
|
+
lose: loseRooms,
|
|
3446
|
+
watch,
|
|
3447
|
+
acquire,
|
|
3448
|
+
persisted(answer) {
|
|
3449
|
+
if (answer === persistence)
|
|
3450
|
+
return;
|
|
3451
|
+
persistence = answer;
|
|
3452
|
+
journal.record(`persistence ${answer}`);
|
|
3453
|
+
queueReadiness();
|
|
3454
|
+
},
|
|
3455
|
+
within: (reach, port) => narrowed(reachable, reach, port),
|
|
2607
3456
|
};
|
|
2608
|
-
return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2609
3457
|
}
|