@agentshouse/mdruntime 0.1.0-cfb2ae0107054 → 0.1.0-cfcbb238a5fa0
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 +14 -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 +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 +1065 -192
- package/dist/conversation.d.ts +11 -5
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +241 -100
- 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 +7 -2
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +20 -7
- 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 +58 -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/reads.d.ts +11 -0
- package/dist/reads.d.ts.map +1 -0
- package/dist/reads.js +98 -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 +4 -4
- 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,46 @@
|
|
|
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';
|
|
19
|
+
import { doorReads } from './reads.js';
|
|
17
20
|
import { refreshRoom } from './refresh.js';
|
|
18
21
|
import { buildBatch, joined, limitedIn, requeued } from './save.js';
|
|
19
22
|
import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
|
|
20
23
|
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
21
24
|
import { same, saving, STARTING, transferring, } from './status.js';
|
|
22
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';
|
|
23
28
|
import { FencedMap } from './fence.js';
|
|
24
|
-
import {
|
|
29
|
+
import { tally } from './shared.js';
|
|
30
|
+
import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
|
|
31
|
+
import { identityTime, roomDraftRevision } from './identity.js';
|
|
25
32
|
import { beneath, normalize } from './paths.js';
|
|
26
33
|
import { openRuntimeStore, } from './store.js';
|
|
27
34
|
import { unconfirmed } from './transfer.js';
|
|
28
35
|
import { governs, holds, narrowed, planReach, } from './reach.js';
|
|
36
|
+
function roomsAlone(plan) {
|
|
37
|
+
return (plan.reach === null &&
|
|
38
|
+
plan.root.types === null &&
|
|
39
|
+
plan.root.where === null &&
|
|
40
|
+
plan.root.pathPrefix === null &&
|
|
41
|
+
plan.hop === null &&
|
|
42
|
+
plan.backlinks === null);
|
|
43
|
+
}
|
|
29
44
|
function within(root, path) {
|
|
30
45
|
return root === '' || beneath(root, path);
|
|
31
46
|
}
|
|
@@ -42,6 +57,29 @@ function declaredSelection(room) {
|
|
|
42
57
|
const OPENING_LANE = 'opening';
|
|
43
58
|
const CONVERSATION_LANE = 'conversations';
|
|
44
59
|
const ROOM_READS = 256;
|
|
60
|
+
const ROOM_ATTEMPTS = 16;
|
|
61
|
+
const outside = new Map();
|
|
62
|
+
function outsideOf(roots) {
|
|
63
|
+
const key = JSON.stringify(roots);
|
|
64
|
+
const held = outside.get(key);
|
|
65
|
+
if (held !== undefined)
|
|
66
|
+
return held;
|
|
67
|
+
const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
|
|
68
|
+
const made = {
|
|
69
|
+
documents: (path) => (markdownPath(path) ? counted(path) : 0),
|
|
70
|
+
binaries: counted,
|
|
71
|
+
};
|
|
72
|
+
outside.set(key, made);
|
|
73
|
+
return made;
|
|
74
|
+
}
|
|
75
|
+
function abandonedOn(signal) {
|
|
76
|
+
return new Promise((resolve) => {
|
|
77
|
+
if (signal.aborted)
|
|
78
|
+
resolve(null);
|
|
79
|
+
else
|
|
80
|
+
signal.addEventListener('abort', () => resolve(null), { once: true });
|
|
81
|
+
});
|
|
82
|
+
}
|
|
45
83
|
function roomLane(locator) {
|
|
46
84
|
return `room ${locator}`;
|
|
47
85
|
}
|
|
@@ -59,6 +97,8 @@ export function composeRuntime(options) {
|
|
|
59
97
|
const replicas = new FencedMap(isFenced);
|
|
60
98
|
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
61
99
|
const pendings = new Map();
|
|
100
|
+
const seeds = new Map();
|
|
101
|
+
const seedWatchers = new Map();
|
|
62
102
|
const roomDrafts = new FencedMap(isFenced);
|
|
63
103
|
const presentations = new Map();
|
|
64
104
|
let limits = [];
|
|
@@ -71,6 +111,7 @@ export function composeRuntime(options) {
|
|
|
71
111
|
const liveModels = new FencedMap(isModelFenced);
|
|
72
112
|
const invalidModels = new Set();
|
|
73
113
|
const discovered = new Map();
|
|
114
|
+
const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
|
|
74
115
|
let roomsListed = false;
|
|
75
116
|
const scopedRefusals = new Map();
|
|
76
117
|
const sourceRefusals = new Map();
|
|
@@ -98,7 +139,6 @@ export function composeRuntime(options) {
|
|
|
98
139
|
let status = STARTING;
|
|
99
140
|
let shown = STARTING;
|
|
100
141
|
let discoveryPaused = false;
|
|
101
|
-
let resetTimer;
|
|
102
142
|
let closed = false;
|
|
103
143
|
let stopping = false;
|
|
104
144
|
let withdrawStream;
|
|
@@ -106,7 +146,10 @@ export function composeRuntime(options) {
|
|
|
106
146
|
let underway = null;
|
|
107
147
|
let streamed = null;
|
|
108
148
|
let handed = false;
|
|
109
|
-
const
|
|
149
|
+
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
150
|
+
const repairing = new Set();
|
|
151
|
+
const repairable = new Map();
|
|
152
|
+
let outcomeRetention = null;
|
|
110
153
|
const answering = new Map();
|
|
111
154
|
let complete = false;
|
|
112
155
|
let recorded = false;
|
|
@@ -136,25 +179,41 @@ export function composeRuntime(options) {
|
|
|
136
179
|
const serviceModeListeners = new Set();
|
|
137
180
|
let member = null;
|
|
138
181
|
const memberListeners = new Set();
|
|
139
|
-
const
|
|
140
|
-
|
|
182
|
+
const prepared = new Set();
|
|
183
|
+
const projecting = new Set();
|
|
184
|
+
const unpreparedDemand = new Set();
|
|
185
|
+
const unindexable = new Set();
|
|
186
|
+
let preparing = false;
|
|
187
|
+
let demand = WHOLE_DEMAND;
|
|
188
|
+
let narrowing = null;
|
|
189
|
+
let targets = {};
|
|
190
|
+
let persistence = options.shell.persistence;
|
|
191
|
+
let capacity = null;
|
|
192
|
+
const paused = new Set();
|
|
193
|
+
let rechecking = null;
|
|
194
|
+
const readinessWatches = new Set();
|
|
195
|
+
let readinessQueued = false;
|
|
196
|
+
const catching = new Set();
|
|
141
197
|
const transfers = new FencedMap(isFenced);
|
|
142
198
|
const conversations = createConversationStore({
|
|
143
199
|
agents: declaredAgents,
|
|
144
|
-
documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
|
|
200
|
+
documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
|
|
145
201
|
call: (name, input) => invokeAction(name, input),
|
|
146
202
|
durably,
|
|
147
203
|
stream: () => options.adapter.conversations,
|
|
148
204
|
publish: publishChangeSet,
|
|
149
205
|
online: () => !status.providerUnreachable && refusal === null,
|
|
150
206
|
closed: () => closed,
|
|
207
|
+
unanswered: resendLater,
|
|
208
|
+
expired: (operation) => pastHorizon(operation),
|
|
151
209
|
});
|
|
152
210
|
const searchIndex = createSearchIndex({
|
|
153
|
-
|
|
211
|
+
room: (room) => readSide.room(room),
|
|
154
212
|
document: (room, path) => readSide.document(room, path),
|
|
155
|
-
messages: () => conversations.messages(),
|
|
156
|
-
message: (conversation, identity) => conversations.message(conversation, identity),
|
|
157
213
|
});
|
|
214
|
+
const roomsToIndex = new Set();
|
|
215
|
+
const conversationsToIndex = new Set();
|
|
216
|
+
let indexing = false;
|
|
158
217
|
const journal = createLog();
|
|
159
218
|
let notifyReady = () => { };
|
|
160
219
|
const ready = new Promise((resolve) => {
|
|
@@ -233,9 +292,16 @@ export function composeRuntime(options) {
|
|
|
233
292
|
returned: structuredClone(record.returned),
|
|
234
293
|
});
|
|
235
294
|
}
|
|
295
|
+
for (const [room, refusal] of sourceRefusals)
|
|
296
|
+
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
236
297
|
attentionRequired.sort((left, right) => {
|
|
237
298
|
if (left.room !== right.room)
|
|
238
299
|
return left.room < right.room ? -1 : 1;
|
|
300
|
+
if ('refusal' in left || 'refusal' in right) {
|
|
301
|
+
if ('refusal' in left && 'refusal' in right)
|
|
302
|
+
return 0;
|
|
303
|
+
return 'refusal' in left ? -1 : 1;
|
|
304
|
+
}
|
|
239
305
|
if (left.path !== right.path)
|
|
240
306
|
return left.path < right.path ? -1 : 1;
|
|
241
307
|
return 0;
|
|
@@ -254,11 +320,9 @@ export function composeRuntime(options) {
|
|
|
254
320
|
if (replica === undefined)
|
|
255
321
|
return 0;
|
|
256
322
|
const roots = replica.confirmed.record.typedSelection?.roots ?? [];
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
];
|
|
261
|
-
return paths.filter((path) => !roots.some((root) => beneath(root, path))).length;
|
|
323
|
+
const weighed = outsideOf(roots);
|
|
324
|
+
return (tally(replica.visible.documents, weighed.documents) +
|
|
325
|
+
tally(replica.visible.binaries, weighed.binaries));
|
|
262
326
|
}
|
|
263
327
|
function listed() {
|
|
264
328
|
return structuredClone([...rooms.values()].map((room) => {
|
|
@@ -285,15 +349,19 @@ export function composeRuntime(options) {
|
|
|
285
349
|
return { listed: roomsListed, rooms: held };
|
|
286
350
|
}
|
|
287
351
|
function emitRooms() {
|
|
352
|
+
readSide.recover();
|
|
353
|
+
queueReadiness();
|
|
288
354
|
const value = listed();
|
|
289
355
|
for (const listener of roomListeners)
|
|
290
356
|
listener(value);
|
|
291
357
|
}
|
|
292
358
|
function emitRefusal() {
|
|
359
|
+
queueReadiness();
|
|
293
360
|
for (const listener of refusalListeners)
|
|
294
361
|
listener(refusal);
|
|
295
362
|
}
|
|
296
363
|
function emitStorageLoss() {
|
|
364
|
+
queueReadiness();
|
|
297
365
|
for (const listener of storageLossListeners)
|
|
298
366
|
listener(storageLoss);
|
|
299
367
|
}
|
|
@@ -310,7 +378,8 @@ export function composeRuntime(options) {
|
|
|
310
378
|
function show() {
|
|
311
379
|
if (closed)
|
|
312
380
|
return;
|
|
313
|
-
|
|
381
|
+
queueReadiness();
|
|
382
|
+
const next = status.state === 'synced' && demand.conversations && !conversations.current()
|
|
314
383
|
? { ...status, state: 'syncing' }
|
|
315
384
|
: status;
|
|
316
385
|
if (same(shown, next))
|
|
@@ -339,42 +408,511 @@ export function composeRuntime(options) {
|
|
|
339
408
|
}
|
|
340
409
|
}
|
|
341
410
|
function applyChangeSet(set) {
|
|
342
|
-
const held =
|
|
343
|
-
|
|
344
|
-
|
|
411
|
+
const held = {
|
|
412
|
+
...set,
|
|
413
|
+
documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
|
|
414
|
+
};
|
|
345
415
|
readSide.apply(held);
|
|
346
416
|
searchIndex.changed(held);
|
|
347
417
|
}
|
|
348
|
-
function
|
|
349
|
-
|
|
418
|
+
async function referenced(locator) {
|
|
419
|
+
for (;;) {
|
|
420
|
+
const record = replicas.get(locator)?.confirmed.record;
|
|
421
|
+
if (record === undefined || closed)
|
|
422
|
+
return;
|
|
423
|
+
const held = { text: record.documents, binaries: record.binaries };
|
|
424
|
+
const composition = compositionOf(record);
|
|
425
|
+
if (referencesIndexed(held, composition))
|
|
426
|
+
return;
|
|
427
|
+
await indexReferrers(held, composition);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async function prepareReplica(locator) {
|
|
431
|
+
await referenced(locator);
|
|
432
|
+
const replica = replicas.get(locator);
|
|
433
|
+
if (replica === undefined || closed || prepared.has(locator))
|
|
350
434
|
return;
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
435
|
+
const visible = replica.visible;
|
|
436
|
+
const paths = [
|
|
437
|
+
...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
|
|
438
|
+
];
|
|
439
|
+
const entered = [];
|
|
440
|
+
projecting.add(locator);
|
|
441
|
+
try {
|
|
442
|
+
await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
|
|
443
|
+
const current = replicas.get(locator);
|
|
444
|
+
if (current === undefined || closed)
|
|
445
|
+
return;
|
|
446
|
+
const set = entering(locator, current, chunk);
|
|
447
|
+
readSide.apply(set);
|
|
448
|
+
entered.push(set);
|
|
449
|
+
});
|
|
450
|
+
if (!replicas.has(locator) || closed)
|
|
451
|
+
return;
|
|
452
|
+
prepared.add(locator);
|
|
361
453
|
}
|
|
362
|
-
|
|
454
|
+
finally {
|
|
455
|
+
projecting.delete(locator);
|
|
456
|
+
}
|
|
457
|
+
conversations.changed(mergeChangeSets(entered));
|
|
458
|
+
readSide.recover();
|
|
459
|
+
queueReadiness();
|
|
460
|
+
}
|
|
461
|
+
function unprepared() {
|
|
462
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
|
|
463
|
+
}
|
|
464
|
+
function unbuilt(locator, error) {
|
|
465
|
+
unindexable.add(locator);
|
|
466
|
+
journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
|
|
467
|
+
queueReadiness();
|
|
468
|
+
}
|
|
469
|
+
function prepare(locators = []) {
|
|
470
|
+
for (const locator of locators)
|
|
471
|
+
unpreparedDemand.add(locator);
|
|
472
|
+
if (preparing)
|
|
363
473
|
return;
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
474
|
+
preparing = true;
|
|
475
|
+
void (async () => {
|
|
476
|
+
try {
|
|
477
|
+
for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
|
|
478
|
+
await yielded();
|
|
479
|
+
if (closed)
|
|
480
|
+
return;
|
|
481
|
+
unpreparedDemand.delete(waiting);
|
|
482
|
+
await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
finally {
|
|
486
|
+
preparing = false;
|
|
487
|
+
}
|
|
488
|
+
})();
|
|
367
489
|
}
|
|
368
490
|
function reachedRooms(plan) {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
491
|
+
const reach = plan.reach;
|
|
492
|
+
const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
493
|
+
const named = plan.rooms ??
|
|
494
|
+
(reached === null
|
|
495
|
+
? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
|
|
496
|
+
: [...reached]);
|
|
497
|
+
return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
|
|
498
|
+
}
|
|
499
|
+
function awaited(locator) {
|
|
500
|
+
return (!roomsListed ||
|
|
501
|
+
(rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
|
|
502
|
+
}
|
|
503
|
+
function covered(plan) {
|
|
504
|
+
const locators = reachedRooms(plan);
|
|
505
|
+
if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
|
|
506
|
+
return { pending: [] };
|
|
507
|
+
}
|
|
508
|
+
const pending = [];
|
|
509
|
+
const coverage = [];
|
|
510
|
+
const overlay = [];
|
|
511
|
+
for (const locator of locators) {
|
|
512
|
+
const replica = replicas.get(locator);
|
|
513
|
+
if (replica === undefined) {
|
|
514
|
+
if (awaited(locator))
|
|
515
|
+
pending.push({ room: locator, awaits: 'acquisition' });
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
if (!prepared.has(locator)) {
|
|
519
|
+
pending.push({ room: locator, awaits: 'preparation' });
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
coverage.push({
|
|
523
|
+
room: locator,
|
|
524
|
+
generation: replica.confirmed.record.transfer,
|
|
525
|
+
current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
|
|
526
|
+
});
|
|
527
|
+
for (const batch of replica.queue)
|
|
528
|
+
overlay.push(batch.id);
|
|
529
|
+
}
|
|
530
|
+
if (pending.length > 0)
|
|
531
|
+
return { pending };
|
|
532
|
+
return { coverage: { rooms: coverage, indexes: [], overlay } };
|
|
533
|
+
}
|
|
534
|
+
function awaitPreparation(cover) {
|
|
535
|
+
if (!('pending' in cover))
|
|
536
|
+
return;
|
|
537
|
+
const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
|
|
538
|
+
if (waiting.length > 0)
|
|
539
|
+
prepare(waiting.map(({ room }) => room));
|
|
540
|
+
}
|
|
541
|
+
function acquires(room) {
|
|
542
|
+
return (demanded(demand, room) ||
|
|
543
|
+
(replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
|
|
544
|
+
queuesWithoutReplica.has(room.locator));
|
|
545
|
+
}
|
|
546
|
+
function acquiredLocators() {
|
|
547
|
+
return [...discovered.values()]
|
|
548
|
+
.filter(acquires)
|
|
549
|
+
.map((room) => room.locator)
|
|
550
|
+
.sort();
|
|
551
|
+
}
|
|
552
|
+
function heldDemand(acquired) {
|
|
553
|
+
if (demand.whole)
|
|
554
|
+
return null;
|
|
555
|
+
return { rooms: acquired.map((room) => room.locator), conversations: demand.conversations };
|
|
556
|
+
}
|
|
557
|
+
function applyDemand(next) {
|
|
558
|
+
const before = acquiredLocators();
|
|
559
|
+
const conversed = demand.conversations;
|
|
560
|
+
demand = next;
|
|
561
|
+
const after = acquiredLocators();
|
|
562
|
+
queueReadiness();
|
|
563
|
+
if (!conversed && next.conversations)
|
|
564
|
+
scheduleCurrency();
|
|
565
|
+
if (JSON.stringify(before) === JSON.stringify(after) && conversed === next.conversations)
|
|
566
|
+
return;
|
|
567
|
+
journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
|
|
568
|
+
if (options.adapter.roomStream === undefined)
|
|
569
|
+
void sync('unstreamed');
|
|
570
|
+
else if (socketOpen)
|
|
571
|
+
options.adapter.roomStream.reopen();
|
|
572
|
+
}
|
|
573
|
+
function acquire(next) {
|
|
574
|
+
if (closed)
|
|
575
|
+
return;
|
|
576
|
+
if (narrowing !== null)
|
|
577
|
+
clearTimeout(narrowing);
|
|
578
|
+
narrowing = null;
|
|
579
|
+
const widening = wider(next, demand);
|
|
580
|
+
if (!widening && !narrower(next, demand))
|
|
581
|
+
return;
|
|
582
|
+
if (widening || !roomsListed) {
|
|
583
|
+
applyDemand(next);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
// A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
|
|
587
|
+
narrowing = setTimeout(() => {
|
|
588
|
+
narrowing = null;
|
|
589
|
+
applyDemand(next);
|
|
590
|
+
}, 5_000);
|
|
591
|
+
}
|
|
592
|
+
function requiredRooms(reach) {
|
|
593
|
+
if (reach === null)
|
|
594
|
+
return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
|
|
595
|
+
const named = [...new Set(reach.rooms)].sort();
|
|
596
|
+
if (roomsListed)
|
|
597
|
+
return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
|
|
598
|
+
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
599
|
+
}
|
|
600
|
+
function roomState(locator, search) {
|
|
601
|
+
if (sourceRefusals.has(locator))
|
|
602
|
+
return 'refused';
|
|
603
|
+
const listedRoom = discovered.get(locator);
|
|
604
|
+
if (roomsListed &&
|
|
605
|
+
(listedRoom === undefined ||
|
|
606
|
+
listedRoom.replica.admission !== 'admitted' ||
|
|
607
|
+
unreadable.has(locator)))
|
|
608
|
+
return 'unadmitted';
|
|
609
|
+
if (!replicas.has(locator))
|
|
610
|
+
return 'acquiring';
|
|
611
|
+
if (!prepared.has(locator))
|
|
612
|
+
return 'preparing';
|
|
613
|
+
if (search && !searchIndex.rooms().has(locator))
|
|
614
|
+
return 'indexing';
|
|
615
|
+
return 'complete';
|
|
616
|
+
}
|
|
617
|
+
function conversationCoverage(reach, search) {
|
|
618
|
+
const declared = reach === null || reach.conversations;
|
|
619
|
+
if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
|
|
620
|
+
return 'undeclared';
|
|
621
|
+
if (!conversations.known() || !conversations.current())
|
|
622
|
+
return 'acquiring';
|
|
623
|
+
if (!search)
|
|
624
|
+
return 'complete';
|
|
625
|
+
const unindexed = conversations
|
|
626
|
+
.searchable()
|
|
627
|
+
.filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
|
|
628
|
+
if (unindexed.length === 0)
|
|
629
|
+
return 'complete';
|
|
630
|
+
index([], unindexed);
|
|
631
|
+
return 'indexing';
|
|
632
|
+
}
|
|
633
|
+
function connectionOf() {
|
|
634
|
+
if (socketOpen)
|
|
635
|
+
return 'connected';
|
|
636
|
+
if (status.providerUnreachable)
|
|
637
|
+
return 'unreachable';
|
|
638
|
+
return options.adapter.roomStream === undefined && status.lastCompleted !== null
|
|
639
|
+
? 'connected'
|
|
640
|
+
: 'connecting';
|
|
641
|
+
}
|
|
642
|
+
function refusedIn(locators) {
|
|
643
|
+
return locators.flatMap((room) => {
|
|
644
|
+
const held = sourceRefusals.get(room);
|
|
645
|
+
if (held === undefined)
|
|
646
|
+
return [];
|
|
647
|
+
const { file, location } = held.fields;
|
|
648
|
+
return [
|
|
649
|
+
{
|
|
650
|
+
room,
|
|
651
|
+
file: typeof file === 'string' ? file : '',
|
|
652
|
+
rule: held.code,
|
|
653
|
+
location: typeof location === 'string' ? location : null,
|
|
654
|
+
},
|
|
655
|
+
];
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
function readinessFor(port, reach) {
|
|
659
|
+
const required = requiredRooms(reach);
|
|
660
|
+
const search = port.search === 'required';
|
|
661
|
+
const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
|
|
662
|
+
const building = (state) => coverage
|
|
663
|
+
.filter((room) => room.state === state && !unindexable.has(room.room))
|
|
664
|
+
.map(({ room }) => room);
|
|
665
|
+
const preparing = building('preparing');
|
|
666
|
+
if (preparing.length > 0)
|
|
667
|
+
prepare(preparing);
|
|
668
|
+
const indexing = building('indexing');
|
|
669
|
+
if (indexing.length > 0)
|
|
670
|
+
index(indexing, []);
|
|
671
|
+
return readinessOf({
|
|
672
|
+
code: port.code,
|
|
673
|
+
coverage: {
|
|
674
|
+
listed: required.listed,
|
|
675
|
+
rooms: coverage,
|
|
676
|
+
conversations: conversationCoverage(reach, search),
|
|
677
|
+
search: search ? 'required' : 'unused',
|
|
678
|
+
},
|
|
679
|
+
persistence,
|
|
680
|
+
connection: connectionOf(),
|
|
681
|
+
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
682
|
+
capacity: capacity !== null &&
|
|
683
|
+
coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
|
|
684
|
+
? capacity
|
|
685
|
+
: port.capacity,
|
|
686
|
+
refused: refusedIn(required.rooms),
|
|
687
|
+
lost: storageLoss !== null,
|
|
688
|
+
established: targets[port.target]?.established === true,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
function establish(name) {
|
|
692
|
+
targets = { ...targets, [name]: { established: true } };
|
|
693
|
+
queueReadiness();
|
|
694
|
+
void durably((local) => local.keepTargets(targets)).catch(() => {
|
|
695
|
+
journal.record(`target record failed ${name}`);
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
function deliverReadiness(watch) {
|
|
699
|
+
if (!readinessWatches.has(watch))
|
|
700
|
+
return;
|
|
701
|
+
const readiness = readinessFor(watch.port, watch.reach);
|
|
702
|
+
if (readiness.ready && targets[watch.port.target]?.established !== true) {
|
|
703
|
+
establish(watch.port.target);
|
|
704
|
+
}
|
|
705
|
+
const shown = JSON.stringify(readiness);
|
|
706
|
+
if (shown === watch.last)
|
|
707
|
+
return;
|
|
708
|
+
watch.last = shown;
|
|
709
|
+
watch.deliver(structuredClone(readiness));
|
|
710
|
+
}
|
|
711
|
+
function queueReadiness() {
|
|
712
|
+
if (readinessQueued || closed || readinessWatches.size === 0)
|
|
713
|
+
return;
|
|
714
|
+
readinessQueued = true;
|
|
715
|
+
queueMicrotask(() => {
|
|
716
|
+
readinessQueued = false;
|
|
717
|
+
if (closed)
|
|
718
|
+
return;
|
|
719
|
+
for (const watch of [...readinessWatches])
|
|
720
|
+
deliverReadiness(watch);
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
function watchReadiness(port, reach, deliver) {
|
|
724
|
+
const watch = { port, reach, deliver, last: null };
|
|
725
|
+
readinessWatches.add(watch);
|
|
726
|
+
void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
|
|
727
|
+
return () => {
|
|
728
|
+
readinessWatches.delete(watch);
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
function layerBytes(layer) {
|
|
732
|
+
let bytes = 0;
|
|
733
|
+
for (const [path, text] of Object.entries(layer.documents))
|
|
734
|
+
bytes += (path.length + text.length) * 2;
|
|
735
|
+
return bytes;
|
|
736
|
+
}
|
|
737
|
+
function grownBy(next, previous) {
|
|
738
|
+
if (previous === undefined)
|
|
739
|
+
return layerBytes(next);
|
|
740
|
+
const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
|
|
741
|
+
return layerBytes({ ...next, documents });
|
|
742
|
+
}
|
|
743
|
+
function exceeded(error) {
|
|
744
|
+
return (typeof error === 'object' &&
|
|
745
|
+
error !== null &&
|
|
746
|
+
error.name === 'QuotaExceededError');
|
|
747
|
+
}
|
|
748
|
+
async function estimated() {
|
|
749
|
+
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
750
|
+
return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
|
|
751
|
+
}
|
|
752
|
+
function pause(locator, cause) {
|
|
753
|
+
paused.add(locator);
|
|
754
|
+
capacity = cause;
|
|
755
|
+
journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
|
|
756
|
+
queueReadiness();
|
|
757
|
+
if (rechecking !== null)
|
|
758
|
+
return;
|
|
759
|
+
// A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
|
|
760
|
+
rechecking = setInterval(() => {
|
|
761
|
+
void recheck();
|
|
762
|
+
}, 60_000);
|
|
763
|
+
}
|
|
764
|
+
async function recheck() {
|
|
765
|
+
const held = capacity;
|
|
766
|
+
if (held === null || closed)
|
|
767
|
+
return;
|
|
768
|
+
const available = await estimated().catch(() => null);
|
|
769
|
+
if (available === null || available < (held.needed ?? 0))
|
|
770
|
+
return;
|
|
771
|
+
if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
|
|
772
|
+
return;
|
|
773
|
+
resume();
|
|
774
|
+
}
|
|
775
|
+
function resume() {
|
|
776
|
+
if (rechecking !== null)
|
|
777
|
+
clearInterval(rechecking);
|
|
778
|
+
rechecking = null;
|
|
779
|
+
if (paused.size === 0 && capacity === null)
|
|
780
|
+
return;
|
|
781
|
+
paused.clear();
|
|
782
|
+
capacity = null;
|
|
783
|
+
journal.record('capacity resumed');
|
|
784
|
+
queueReadiness();
|
|
785
|
+
if (!closed && authorization !== undefined)
|
|
786
|
+
void sync('unstreamed');
|
|
787
|
+
}
|
|
788
|
+
async function reclaimed(local) {
|
|
789
|
+
const unreferenced = [...transfers.keys()].filter((locator) => {
|
|
790
|
+
const room = discovered.get(locator);
|
|
791
|
+
return room === undefined || !acquires(room);
|
|
792
|
+
});
|
|
793
|
+
for (const locator of unreferenced) {
|
|
794
|
+
await local.abandon(locator);
|
|
795
|
+
transfers.delete(locator);
|
|
796
|
+
}
|
|
797
|
+
if (unreferenced.length > 0)
|
|
798
|
+
journal.record(`capacity reclaimed ${unreferenced.length}`);
|
|
799
|
+
return unreferenced.length > 0;
|
|
800
|
+
}
|
|
801
|
+
async function admitted(local, locator, needed) {
|
|
802
|
+
if (paused.has(locator))
|
|
803
|
+
return false;
|
|
804
|
+
let available = await estimated().catch(() => null);
|
|
805
|
+
if (available === null || available >= needed)
|
|
806
|
+
return true;
|
|
807
|
+
if (await reclaimed(local))
|
|
808
|
+
available = await estimated().catch(() => null);
|
|
809
|
+
if (available === null || available >= needed)
|
|
810
|
+
return true;
|
|
811
|
+
pause(locator, { cause: 'insufficient-space', needed, available });
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
async function exceededOn(locator, bytes) {
|
|
815
|
+
const available = await estimated().catch(() => null);
|
|
816
|
+
pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
|
|
817
|
+
}
|
|
818
|
+
function seedRoomHeld(room, version) {
|
|
819
|
+
return prepared.has(room) && reachedBy(room, version);
|
|
820
|
+
}
|
|
821
|
+
function seedHeld(at) {
|
|
822
|
+
return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
|
|
823
|
+
}
|
|
824
|
+
function seedApplies(plan) {
|
|
825
|
+
const seed = seeds.get(plan.key);
|
|
826
|
+
if (seed === undefined)
|
|
372
827
|
return null;
|
|
373
|
-
|
|
828
|
+
if (!seedHeld(seed.at))
|
|
829
|
+
return seed;
|
|
830
|
+
seeds.delete(plan.key);
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
function seedBehind(plan, cover) {
|
|
834
|
+
const seed = seedApplies(plan);
|
|
835
|
+
if (seed === null || 'pending' in cover)
|
|
836
|
+
return cover;
|
|
837
|
+
return {
|
|
838
|
+
pending: Object.entries(seed.at)
|
|
839
|
+
.filter(([room, version]) => !seedRoomHeld(room, version))
|
|
840
|
+
.map(([room]) => ({ room, awaits: 'acquisition' })),
|
|
841
|
+
};
|
|
374
842
|
}
|
|
375
|
-
function
|
|
376
|
-
|
|
377
|
-
|
|
843
|
+
function seed(entry) {
|
|
844
|
+
if (seedHeld(entry.at))
|
|
845
|
+
return;
|
|
846
|
+
for (const { query, rows } of entry.queries) {
|
|
847
|
+
const key = queryKey(query);
|
|
848
|
+
seeds.set(key, { rows, at: entry.at });
|
|
849
|
+
for (const shown of seedWatchers.get(key) ?? [])
|
|
850
|
+
shown();
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
async function rowsQuery(plan) {
|
|
854
|
+
const before = covered(plan);
|
|
855
|
+
awaitPreparation(before);
|
|
856
|
+
const seeded = seedApplies(plan);
|
|
857
|
+
if (seeded !== null)
|
|
858
|
+
return seededResult(seeded).result;
|
|
859
|
+
if ('pending' in before)
|
|
860
|
+
return before;
|
|
861
|
+
const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
|
|
862
|
+
awaitPreparation(cover);
|
|
863
|
+
if ('pending' in cover)
|
|
864
|
+
return cover;
|
|
865
|
+
return 'groups' in rows
|
|
866
|
+
? { groups: rows.groups, coverage: cover.coverage }
|
|
867
|
+
: { documents: rows.documents, coverage: cover.coverage };
|
|
868
|
+
}
|
|
869
|
+
async function indexConversation(conversation) {
|
|
870
|
+
searchIndex.indexMessages(conversation, [], false);
|
|
871
|
+
let after = null;
|
|
872
|
+
for (;;) {
|
|
873
|
+
const batch = await durably((local) => local.messagesAfter(conversation, after));
|
|
874
|
+
if (closed)
|
|
875
|
+
return;
|
|
876
|
+
searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
|
|
877
|
+
if (batch.length === 0)
|
|
878
|
+
return;
|
|
879
|
+
after = batch[batch.length - 1].identity;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
function indexNext() {
|
|
883
|
+
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
|
|
884
|
+
if (room !== undefined) {
|
|
885
|
+
roomsToIndex.delete(room);
|
|
886
|
+
return () => prepared.has(room)
|
|
887
|
+
? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
|
|
888
|
+
: undefined;
|
|
889
|
+
}
|
|
890
|
+
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
891
|
+
if (conversation === undefined)
|
|
892
|
+
return null;
|
|
893
|
+
conversationsToIndex.delete(conversation);
|
|
894
|
+
return () => indexConversation(conversation);
|
|
895
|
+
}
|
|
896
|
+
function index(rooms, named) {
|
|
897
|
+
for (const room of rooms)
|
|
898
|
+
roomsToIndex.add(room);
|
|
899
|
+
for (const conversation of named)
|
|
900
|
+
conversationsToIndex.add(conversation);
|
|
901
|
+
if (indexing)
|
|
902
|
+
return;
|
|
903
|
+
indexing = true;
|
|
904
|
+
void (async () => {
|
|
905
|
+
try {
|
|
906
|
+
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
907
|
+
await yielded();
|
|
908
|
+
await unit();
|
|
909
|
+
queueReadiness();
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
finally {
|
|
913
|
+
indexing = false;
|
|
914
|
+
}
|
|
915
|
+
})().catch(() => { });
|
|
378
916
|
}
|
|
379
917
|
function publishChangeSet(set) {
|
|
380
918
|
applyChangeSet(set);
|
|
@@ -391,34 +929,7 @@ export function composeRuntime(options) {
|
|
|
391
929
|
emitPresentation(app);
|
|
392
930
|
conversations.changed(set);
|
|
393
931
|
}
|
|
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
932
|
async function refuse(reason, provider = null) {
|
|
421
|
-
stopTimer();
|
|
422
933
|
discoveryPaused = true;
|
|
423
934
|
try {
|
|
424
935
|
if (reason === 'revoked-authorization') {
|
|
@@ -503,6 +1014,9 @@ export function composeRuntime(options) {
|
|
|
503
1014
|
if (fence !== undefined && fence.erased && fence.at < started)
|
|
504
1015
|
fences.delete(room.locator);
|
|
505
1016
|
}
|
|
1017
|
+
await loseRooms(found
|
|
1018
|
+
.filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
|
|
1019
|
+
.map((room) => room.locator));
|
|
506
1020
|
const listed = found.filter((room) => !fences.has(room.locator));
|
|
507
1021
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
508
1022
|
for (const locator of listedLocators)
|
|
@@ -527,6 +1041,12 @@ export function composeRuntime(options) {
|
|
|
527
1041
|
emitRooms();
|
|
528
1042
|
return listed;
|
|
529
1043
|
}
|
|
1044
|
+
function heldLocally(locator) {
|
|
1045
|
+
return (replicas.has(locator) ||
|
|
1046
|
+
roomDrafts.has(locator) ||
|
|
1047
|
+
transfers.has(locator) ||
|
|
1048
|
+
queuesWithoutReplica.has(locator));
|
|
1049
|
+
}
|
|
530
1050
|
function unlistedHeld(listed) {
|
|
531
1051
|
return [
|
|
532
1052
|
...new Set([
|
|
@@ -563,10 +1083,14 @@ export function composeRuntime(options) {
|
|
|
563
1083
|
rooms.delete(locator);
|
|
564
1084
|
roomDrafts.delete(locator);
|
|
565
1085
|
transfers.delete(locator);
|
|
1086
|
+
catching.delete(locator);
|
|
566
1087
|
discovered.delete(locator);
|
|
567
1088
|
announced.delete(locator);
|
|
568
1089
|
unreadable.delete(locator);
|
|
569
1090
|
stale.delete(locator);
|
|
1091
|
+
repairable.delete(locator);
|
|
1092
|
+
unindexable.delete(locator);
|
|
1093
|
+
paused.delete(locator);
|
|
570
1094
|
scopedRefusals.delete(locator);
|
|
571
1095
|
sourceRefusals.delete(locator);
|
|
572
1096
|
declarationRefusals.delete(locator);
|
|
@@ -582,7 +1106,9 @@ export function composeRuntime(options) {
|
|
|
582
1106
|
return fresh;
|
|
583
1107
|
publishChangeSet(mergeChangeSets(removed));
|
|
584
1108
|
for (const locator of fresh)
|
|
585
|
-
|
|
1109
|
+
prepared.delete(locator);
|
|
1110
|
+
searchIndex.forget(fresh);
|
|
1111
|
+
readSide.recover();
|
|
586
1112
|
moveTo({ ...status, ...attention() });
|
|
587
1113
|
return fresh;
|
|
588
1114
|
}
|
|
@@ -618,7 +1144,7 @@ export function composeRuntime(options) {
|
|
|
618
1144
|
return model;
|
|
619
1145
|
}
|
|
620
1146
|
async function hydrate(opened) {
|
|
621
|
-
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
|
|
1147
|
+
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
|
|
622
1148
|
opened.all(),
|
|
623
1149
|
opened.queues(),
|
|
624
1150
|
opened.roomDrafts(),
|
|
@@ -630,7 +1156,9 @@ export function composeRuntime(options) {
|
|
|
630
1156
|
opened.capabilities(),
|
|
631
1157
|
opened.member(),
|
|
632
1158
|
opened.transfers(),
|
|
1159
|
+
opened.targets(),
|
|
633
1160
|
]);
|
|
1161
|
+
targets = heldTargets;
|
|
634
1162
|
transfers.clear();
|
|
635
1163
|
for (const record of staged)
|
|
636
1164
|
transfers.set(record.locator, record);
|
|
@@ -692,6 +1220,7 @@ export function composeRuntime(options) {
|
|
|
692
1220
|
async function readState(opened) {
|
|
693
1221
|
const { lastCompleted, locators, read } = await hydrate(opened);
|
|
694
1222
|
applyChangeSet(read);
|
|
1223
|
+
prepare();
|
|
695
1224
|
if (replicas.size > 0)
|
|
696
1225
|
emitRooms();
|
|
697
1226
|
if (member !== null)
|
|
@@ -807,17 +1336,19 @@ export function composeRuntime(options) {
|
|
|
807
1336
|
moveTo({ ...status, ...attention() });
|
|
808
1337
|
throw error;
|
|
809
1338
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1339
|
+
return () => {
|
|
1340
|
+
queuesWithoutReplica.delete(locator);
|
|
1341
|
+
for (const { batch, returned } of unqueued) {
|
|
1342
|
+
const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
|
|
1343
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
1344
|
+
}
|
|
1345
|
+
replicas.set(locator, replicaOf(confirmed, kept, before));
|
|
1346
|
+
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
1347
|
+
room: settling.room,
|
|
1348
|
+
queue: kept.length !== previousQueue.length,
|
|
1349
|
+
drafts: [...written.keys()],
|
|
1350
|
+
});
|
|
1351
|
+
};
|
|
821
1352
|
}
|
|
822
1353
|
function adoptModels(locator, record) {
|
|
823
1354
|
for (const [attached, model] of [
|
|
@@ -853,8 +1384,13 @@ export function composeRuntime(options) {
|
|
|
853
1384
|
function publish(local, room, confirmed, landing, live) {
|
|
854
1385
|
const { base, stored } = landing;
|
|
855
1386
|
return exclusive(async () => {
|
|
856
|
-
if (
|
|
1387
|
+
if (stored !== undefined) {
|
|
1388
|
+
await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
|
|
1389
|
+
}
|
|
1390
|
+
if (!live()) {
|
|
1391
|
+
caughtUpTo(room.locator);
|
|
857
1392
|
return true;
|
|
1393
|
+
}
|
|
858
1394
|
const prior = replicas.get(room.locator);
|
|
859
1395
|
if (base !== undefined &&
|
|
860
1396
|
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
@@ -868,17 +1404,33 @@ export function composeRuntime(options) {
|
|
|
868
1404
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
869
1405
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
870
1406
|
});
|
|
871
|
-
|
|
872
|
-
|
|
1407
|
+
let settled;
|
|
1408
|
+
try {
|
|
1409
|
+
settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
|
|
1410
|
+
}
|
|
1411
|
+
catch (error) {
|
|
1412
|
+
if (!exceeded(error))
|
|
1413
|
+
throw error;
|
|
1414
|
+
await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
|
|
1415
|
+
caughtUpTo(room.locator);
|
|
1416
|
+
return true;
|
|
1417
|
+
}
|
|
1418
|
+
const set = settled();
|
|
1419
|
+
if (stored !== undefined) {
|
|
873
1420
|
transfers.delete(room.locator);
|
|
1421
|
+
catching.delete(room.locator);
|
|
1422
|
+
}
|
|
874
1423
|
for (const batch of accepted)
|
|
875
1424
|
pendings.delete(batch.id);
|
|
876
1425
|
stale.delete(room.locator);
|
|
1426
|
+
unindexable.delete(room.locator);
|
|
877
1427
|
if (unreadable.delete(room.locator))
|
|
878
1428
|
scopedRefusals.delete(room.locator);
|
|
879
1429
|
sourceRefusals.delete(room.locator);
|
|
880
1430
|
declarationRefusals.delete(room.locator);
|
|
881
1431
|
list(room, true);
|
|
1432
|
+
if (prior === undefined)
|
|
1433
|
+
prepared.add(room.locator);
|
|
882
1434
|
publishChangeSet(set);
|
|
883
1435
|
moveTo({ ...status, ...attention() });
|
|
884
1436
|
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
@@ -887,6 +1439,7 @@ export function composeRuntime(options) {
|
|
|
887
1439
|
}
|
|
888
1440
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
889
1441
|
const local = await openLocal();
|
|
1442
|
+
await referenced(locator);
|
|
890
1443
|
const replica = replicas.get(locator);
|
|
891
1444
|
const room = rooms.get(locator);
|
|
892
1445
|
if (replica === undefined || room === undefined) {
|
|
@@ -934,6 +1487,11 @@ export function composeRuntime(options) {
|
|
|
934
1487
|
changes,
|
|
935
1488
|
bases: parts.flatMap(({ bases }) => bases),
|
|
936
1489
|
writtenBy,
|
|
1490
|
+
clean: {
|
|
1491
|
+
text: replica.confirmed.record.documents,
|
|
1492
|
+
binaries: replica.confirmed.record.binaries,
|
|
1493
|
+
composition: compositionOf(replica.confirmed.record),
|
|
1494
|
+
},
|
|
937
1495
|
};
|
|
938
1496
|
const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
|
|
939
1497
|
if (checked?.kind === 'refused')
|
|
@@ -1256,8 +1814,9 @@ export function composeRuntime(options) {
|
|
|
1256
1814
|
async function pullRefresh(authorized, confirmed, live) {
|
|
1257
1815
|
if (!live())
|
|
1258
1816
|
return unconfirmed;
|
|
1817
|
+
const turn = generation;
|
|
1259
1818
|
try {
|
|
1260
|
-
const result = await refreshRoom(
|
|
1819
|
+
const result = await refreshRoom(reads, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
|
|
1261
1820
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
1262
1821
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
1263
1822
|
return result;
|
|
@@ -1266,6 +1825,9 @@ export function composeRuntime(options) {
|
|
|
1266
1825
|
journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
|
|
1267
1826
|
throw error;
|
|
1268
1827
|
}
|
|
1828
|
+
finally {
|
|
1829
|
+
reads.finished(confirmed.record.locator, turn);
|
|
1830
|
+
}
|
|
1269
1831
|
}
|
|
1270
1832
|
function staging(local, live) {
|
|
1271
1833
|
return {
|
|
@@ -1274,8 +1836,20 @@ export function composeRuntime(options) {
|
|
|
1274
1836
|
stage: (record, page) => exclusive(async () => {
|
|
1275
1837
|
if (!live())
|
|
1276
1838
|
return false;
|
|
1277
|
-
|
|
1839
|
+
const bytes = layerBytes(page);
|
|
1840
|
+
if (!(await admitted(local, record.locator, bytes)))
|
|
1841
|
+
return false;
|
|
1842
|
+
try {
|
|
1843
|
+
await local.stage(record, page);
|
|
1844
|
+
}
|
|
1845
|
+
catch (error) {
|
|
1846
|
+
if (!exceeded(error))
|
|
1847
|
+
throw error;
|
|
1848
|
+
await exceededOn(record.locator, bytes);
|
|
1849
|
+
return false;
|
|
1850
|
+
}
|
|
1278
1851
|
transfers.set(record.locator, record);
|
|
1852
|
+
readSide.recover();
|
|
1279
1853
|
return true;
|
|
1280
1854
|
}),
|
|
1281
1855
|
abandon: (locator) => exclusive(async () => {
|
|
@@ -1283,6 +1857,7 @@ export function composeRuntime(options) {
|
|
|
1283
1857
|
return;
|
|
1284
1858
|
await local.abandon(locator);
|
|
1285
1859
|
transfers.delete(locator);
|
|
1860
|
+
readSide.recover();
|
|
1286
1861
|
}),
|
|
1287
1862
|
};
|
|
1288
1863
|
}
|
|
@@ -1295,21 +1870,52 @@ export function composeRuntime(options) {
|
|
|
1295
1870
|
async function pullBootstrap(authorized, room, local, live) {
|
|
1296
1871
|
if (!live())
|
|
1297
1872
|
return unconfirmed;
|
|
1873
|
+
const turn = generation;
|
|
1874
|
+
if (replicas.has(room.locator)) {
|
|
1875
|
+
catching.add(room.locator);
|
|
1876
|
+
readSide.recover();
|
|
1877
|
+
}
|
|
1298
1878
|
try {
|
|
1299
|
-
const result = await bootstrapRoom(
|
|
1879
|
+
const result = await bootstrapRoom(reads, authorized, room, {
|
|
1300
1880
|
collection: modelOf(room.locator),
|
|
1301
1881
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
1302
1882
|
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
1303
1883
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
1304
1884
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1885
|
+
if (result.kind !== 'staged')
|
|
1886
|
+
caughtUpTo(room.locator);
|
|
1305
1887
|
return result;
|
|
1306
1888
|
}
|
|
1307
1889
|
catch (error) {
|
|
1308
1890
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
1891
|
+
caughtUpTo(room.locator);
|
|
1309
1892
|
throw error;
|
|
1310
1893
|
}
|
|
1894
|
+
finally {
|
|
1895
|
+
reads.finished(room.locator, turn);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
function caughtUpTo(locator) {
|
|
1899
|
+
if (catching.delete(locator))
|
|
1900
|
+
readSide.recover();
|
|
1901
|
+
}
|
|
1902
|
+
function behind(room) {
|
|
1903
|
+
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1904
|
+
return (held !== undefined &&
|
|
1905
|
+
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
1311
1906
|
}
|
|
1312
1907
|
async function advance(room, local, authorized, live) {
|
|
1908
|
+
const carried = reads.carried(room.locator);
|
|
1909
|
+
const withheld = await advanceOnce(room, local, authorized, live);
|
|
1910
|
+
if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
|
|
1911
|
+
return withheld;
|
|
1912
|
+
}
|
|
1913
|
+
return advanceOnce(room, local, authorized, live);
|
|
1914
|
+
}
|
|
1915
|
+
async function advanceOnce(room, local, authorized, live) {
|
|
1916
|
+
await referenced(room.locator);
|
|
1917
|
+
if (!live() || paused.has(room.locator))
|
|
1918
|
+
return unconfirmed;
|
|
1313
1919
|
const held = replicas.get(room.locator);
|
|
1314
1920
|
if (held !== undefined) {
|
|
1315
1921
|
const refreshed = await pullRefresh(authorized, held.confirmed, live);
|
|
@@ -1340,6 +1946,7 @@ export function composeRuntime(options) {
|
|
|
1340
1946
|
return refreshed;
|
|
1341
1947
|
if (opened !== null) {
|
|
1342
1948
|
owed.add(room.locator);
|
|
1949
|
+
readSide.recover();
|
|
1343
1950
|
return unconfirmed;
|
|
1344
1951
|
}
|
|
1345
1952
|
}
|
|
@@ -1352,10 +1959,19 @@ export function composeRuntime(options) {
|
|
|
1352
1959
|
return null;
|
|
1353
1960
|
}
|
|
1354
1961
|
async function withhold(withheld, room, local, progress) {
|
|
1962
|
+
if (withheld.kind === 'unconfirmed')
|
|
1963
|
+
return;
|
|
1355
1964
|
if (withheld.kind === 'refused')
|
|
1356
1965
|
stale.delete(room.locator);
|
|
1357
1966
|
else
|
|
1358
1967
|
stale.add(room.locator);
|
|
1968
|
+
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
1969
|
+
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
1970
|
+
repairable.set(room.locator, reset);
|
|
1971
|
+
repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
|
|
1972
|
+
}
|
|
1973
|
+
else
|
|
1974
|
+
repairable.delete(room.locator);
|
|
1359
1975
|
if (withheld.kind === 'scoped-refusal') {
|
|
1360
1976
|
await scoped(withheld, room, local);
|
|
1361
1977
|
}
|
|
@@ -1391,6 +2007,7 @@ export function composeRuntime(options) {
|
|
|
1391
2007
|
unreadable.add(room.locator);
|
|
1392
2008
|
list(room, false);
|
|
1393
2009
|
emitRooms();
|
|
2010
|
+
moveTo({ ...status, ...attention() });
|
|
1394
2011
|
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
1395
2012
|
}
|
|
1396
2013
|
function leaving(batch, result) {
|
|
@@ -1412,6 +2029,7 @@ export function composeRuntime(options) {
|
|
|
1412
2029
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1413
2030
|
}
|
|
1414
2031
|
async function adoptAccepted(room, local, submitted, accepted) {
|
|
2032
|
+
await referenced(room.locator);
|
|
1415
2033
|
if (!continuing())
|
|
1416
2034
|
return unconfirmed;
|
|
1417
2035
|
const current = replicas.get(room.locator);
|
|
@@ -1433,11 +2051,12 @@ export function composeRuntime(options) {
|
|
|
1433
2051
|
emitQueue(room.locator);
|
|
1434
2052
|
return applied;
|
|
1435
2053
|
}
|
|
1436
|
-
const
|
|
2054
|
+
const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
1437
2055
|
pendings.delete(submitted.id);
|
|
1438
2056
|
unreadable.delete(room.locator);
|
|
1439
2057
|
sourceRefusals.delete(room.locator);
|
|
1440
2058
|
declarationRefusals.delete(room.locator);
|
|
2059
|
+
const set = settled();
|
|
1441
2060
|
list(room, true);
|
|
1442
2061
|
publishChangeSet(set);
|
|
1443
2062
|
moveTo({ ...status, ...attention() });
|
|
@@ -1451,14 +2070,14 @@ export function composeRuntime(options) {
|
|
|
1451
2070
|
refusedSource(room, failed);
|
|
1452
2071
|
return false;
|
|
1453
2072
|
}
|
|
1454
|
-
async function submitRoom(room, local, authorized, progress) {
|
|
1455
|
-
while (continuing() && authorization !== undefined) {
|
|
2073
|
+
async function submitRoom(room, local, authorized, progress, signal) {
|
|
2074
|
+
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
1456
2075
|
const replica = replicas.get(room.locator);
|
|
1457
2076
|
const head = replica?.queue[0];
|
|
1458
2077
|
if (replica === undefined || head === undefined)
|
|
1459
|
-
return;
|
|
2078
|
+
return 'done';
|
|
1460
2079
|
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1461
|
-
return;
|
|
2080
|
+
return 'done';
|
|
1462
2081
|
const state = pendings.get(head.id);
|
|
1463
2082
|
const { submission, lost: earlier } = state ?? {
|
|
1464
2083
|
submission: { state: 'waiting' },
|
|
@@ -1466,24 +2085,30 @@ export function composeRuntime(options) {
|
|
|
1466
2085
|
};
|
|
1467
2086
|
if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
|
|
1468
2087
|
progress.complete = false;
|
|
1469
|
-
return;
|
|
2088
|
+
return { at: Date.parse(submission.reset) };
|
|
1470
2089
|
}
|
|
1471
2090
|
if (writesHeld()) {
|
|
1472
2091
|
progress.complete = false;
|
|
1473
2092
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1474
|
-
return;
|
|
2093
|
+
return 'done';
|
|
1475
2094
|
}
|
|
1476
2095
|
const lost = earlier || submission.state === 'submitted';
|
|
2096
|
+
if (lost && pastHorizon(head.id)) {
|
|
2097
|
+
await discardPending(room, local, signal);
|
|
2098
|
+
return 'done';
|
|
2099
|
+
}
|
|
1477
2100
|
let result;
|
|
1478
2101
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
1479
2102
|
await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
|
|
1480
2103
|
if (discoveryPaused || fences.has(room.locator))
|
|
1481
|
-
return;
|
|
1482
|
-
if (writesHeld()) {
|
|
2104
|
+
return 'done';
|
|
2105
|
+
if (signal.aborted || writesHeld()) {
|
|
1483
2106
|
await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
|
|
2107
|
+
if (signal.aborted)
|
|
2108
|
+
return 'done';
|
|
1484
2109
|
progress.complete = false;
|
|
1485
2110
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1486
|
-
return;
|
|
2111
|
+
return 'done';
|
|
1487
2112
|
}
|
|
1488
2113
|
const opensBefore = openSignals;
|
|
1489
2114
|
const answered = awaitingAnswer(room.locator);
|
|
@@ -1499,32 +2124,36 @@ export function composeRuntime(options) {
|
|
|
1499
2124
|
progress.complete = false;
|
|
1500
2125
|
if (continuing())
|
|
1501
2126
|
progress.reachable = false;
|
|
1502
|
-
return;
|
|
2127
|
+
return 'again';
|
|
2128
|
+
}
|
|
2129
|
+
if (signal.aborted) {
|
|
2130
|
+
answered();
|
|
2131
|
+
return 'done';
|
|
1503
2132
|
}
|
|
1504
2133
|
if (result.kind !== 'accepted' || !continuing())
|
|
1505
2134
|
answered();
|
|
1506
2135
|
if (!continuing())
|
|
1507
|
-
return;
|
|
2136
|
+
return 'done';
|
|
1508
2137
|
if (result.kind === 'transport-uncertainty') {
|
|
1509
2138
|
progress.complete = false;
|
|
1510
2139
|
progress.reachable = false;
|
|
1511
|
-
return;
|
|
2140
|
+
return 'again';
|
|
1512
2141
|
}
|
|
1513
2142
|
if (result.kind === 'room-not-readable') {
|
|
1514
2143
|
progress.complete = false;
|
|
1515
2144
|
unreadable.add(room.locator);
|
|
1516
2145
|
list(room, false);
|
|
1517
2146
|
emitRooms();
|
|
1518
|
-
return;
|
|
2147
|
+
return 'done';
|
|
1519
2148
|
}
|
|
1520
2149
|
if (result.kind === 'revoked-authorization') {
|
|
1521
2150
|
await refuse('revoked-authorization');
|
|
1522
|
-
return;
|
|
2151
|
+
return 'done';
|
|
1523
2152
|
}
|
|
1524
2153
|
if (result.kind === 'scoped-refusal') {
|
|
1525
2154
|
await scoped(result, room, local);
|
|
1526
2155
|
progress.complete = false;
|
|
1527
|
-
return;
|
|
2156
|
+
return 'done';
|
|
1528
2157
|
}
|
|
1529
2158
|
if (result.kind === 'write-halted') {
|
|
1530
2159
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
@@ -1533,16 +2162,24 @@ export function composeRuntime(options) {
|
|
|
1533
2162
|
continue;
|
|
1534
2163
|
haltedRefusal = true;
|
|
1535
2164
|
progress.complete = false;
|
|
1536
|
-
return;
|
|
2165
|
+
return 'done';
|
|
1537
2166
|
}
|
|
1538
2167
|
if (result.kind === 'conflicted' ||
|
|
1539
2168
|
result.kind === 'rejected' ||
|
|
1540
2169
|
result.kind === 'expired' ||
|
|
1541
2170
|
result.kind === 'retryable-refusal') {
|
|
2171
|
+
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
2172
|
+
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
2173
|
+
await discardPending(room, local, signal);
|
|
2174
|
+
return 'done';
|
|
2175
|
+
}
|
|
1542
2176
|
if (result.kind === 'expired') {
|
|
2177
|
+
await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
|
|
1543
2178
|
const withheld = await advance(room, local, authorized, continuing);
|
|
1544
|
-
if (withheld !== null)
|
|
1545
|
-
|
|
2179
|
+
if (withheld !== null) {
|
|
2180
|
+
await withhold(withheld, room, local, progress);
|
|
2181
|
+
return 'done';
|
|
2182
|
+
}
|
|
1546
2183
|
}
|
|
1547
2184
|
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
|
|
1548
2185
|
(result.kind === 'conflicted' && result.roomVersion !== undefined
|
|
@@ -1555,8 +2192,7 @@ export function composeRuntime(options) {
|
|
|
1555
2192
|
const reset = result.reset;
|
|
1556
2193
|
progress.complete = false;
|
|
1557
2194
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1558
|
-
|
|
1559
|
-
return;
|
|
2195
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
1560
2196
|
}
|
|
1561
2197
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
1562
2198
|
const left = leaving(head, result);
|
|
@@ -1567,28 +2203,73 @@ export function composeRuntime(options) {
|
|
|
1567
2203
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
1568
2204
|
return;
|
|
1569
2205
|
}
|
|
1570
|
-
const
|
|
2206
|
+
const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
1571
2207
|
leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
|
|
1572
2208
|
(result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
|
|
1573
2209
|
? { ...left, returned: null }
|
|
1574
2210
|
: left,
|
|
1575
2211
|
});
|
|
1576
|
-
publishChangeSet(
|
|
2212
|
+
publishChangeSet(settled());
|
|
1577
2213
|
moveTo({ ...status, ...attention() });
|
|
1578
2214
|
});
|
|
1579
2215
|
if (result.kind === 'expired')
|
|
1580
2216
|
continue;
|
|
1581
2217
|
const withheld = await advance(room, local, authorized, continuing);
|
|
1582
|
-
if (withheld !== null)
|
|
1583
|
-
|
|
2218
|
+
if (withheld !== null) {
|
|
2219
|
+
await withhold(withheld, room, local, progress);
|
|
2220
|
+
return 'done';
|
|
2221
|
+
}
|
|
1584
2222
|
continue;
|
|
1585
2223
|
}
|
|
1586
2224
|
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1587
2225
|
if (!kept) {
|
|
1588
2226
|
progress.complete = false;
|
|
1589
|
-
return;
|
|
2227
|
+
return 'done';
|
|
1590
2228
|
}
|
|
1591
2229
|
}
|
|
2230
|
+
return 'done';
|
|
2231
|
+
}
|
|
2232
|
+
function pastHorizon(identity) {
|
|
2233
|
+
return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
|
|
2234
|
+
}
|
|
2235
|
+
async function discardPending(room, local, signal) {
|
|
2236
|
+
const { locator } = room;
|
|
2237
|
+
const discarded = await exclusive(async () => {
|
|
2238
|
+
if (fences.has(locator))
|
|
2239
|
+
return false;
|
|
2240
|
+
const before = replicas.get(locator);
|
|
2241
|
+
const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
2242
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
2243
|
+
lastFenced.set(locator, ++fencings);
|
|
2244
|
+
try {
|
|
2245
|
+
await local.discard(locator);
|
|
2246
|
+
}
|
|
2247
|
+
finally {
|
|
2248
|
+
fences.delete(locator);
|
|
2249
|
+
}
|
|
2250
|
+
for (const batch of queue)
|
|
2251
|
+
pendings.delete(batch.id);
|
|
2252
|
+
queuesWithoutReplica.delete(locator);
|
|
2253
|
+
const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
|
|
2254
|
+
if (after !== null)
|
|
2255
|
+
replicas.set(locator, after);
|
|
2256
|
+
journal.record(`room discarded ${locator}`);
|
|
2257
|
+
publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
|
|
2258
|
+
moveTo({ ...status, ...attention() });
|
|
2259
|
+
return true;
|
|
2260
|
+
});
|
|
2261
|
+
if (!discarded)
|
|
2262
|
+
return;
|
|
2263
|
+
const progress = { complete: true, reachable: true };
|
|
2264
|
+
await roomTask(locator, async (live) => {
|
|
2265
|
+
await Promise.race([
|
|
2266
|
+
reload(room, local, progress, () => live() && !signal.aborted),
|
|
2267
|
+
abandonedOn(signal),
|
|
2268
|
+
]);
|
|
2269
|
+
});
|
|
2270
|
+
if (signal.aborted)
|
|
2271
|
+
return;
|
|
2272
|
+
await rest(progress);
|
|
1592
2273
|
}
|
|
1593
2274
|
function awaitingAnswer(locator) {
|
|
1594
2275
|
let release = () => { };
|
|
@@ -1668,6 +2349,11 @@ export function composeRuntime(options) {
|
|
|
1668
2349
|
});
|
|
1669
2350
|
}
|
|
1670
2351
|
async function scoped(result, room, local) {
|
|
2352
|
+
if ((result.scope === 'content' || result.scope === 'sync') && !result.retryable) {
|
|
2353
|
+
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
2354
|
+
await loseRooms([room.locator]);
|
|
2355
|
+
return;
|
|
2356
|
+
}
|
|
1671
2357
|
scopedRefusals.set(room.locator, {
|
|
1672
2358
|
code: result.code,
|
|
1673
2359
|
fields: result.fields,
|
|
@@ -1714,15 +2400,16 @@ export function composeRuntime(options) {
|
|
|
1714
2400
|
});
|
|
1715
2401
|
}
|
|
1716
2402
|
async function synchronize(listed, local, progress, attempt, live) {
|
|
1717
|
-
const
|
|
2403
|
+
const acquired = listed.filter(acquires);
|
|
2404
|
+
const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
|
|
1718
2405
|
if (!live())
|
|
1719
2406
|
return;
|
|
1720
|
-
const ready =
|
|
2407
|
+
const ready = acquired.filter((_, index) => {
|
|
1721
2408
|
const outcome = prepared[index];
|
|
1722
2409
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1723
2410
|
});
|
|
1724
2411
|
if (attempt === 'opened') {
|
|
1725
|
-
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
2412
|
+
options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
|
|
1726
2413
|
handed = true;
|
|
1727
2414
|
}
|
|
1728
2415
|
discovering?.release();
|
|
@@ -1750,19 +2437,10 @@ export function composeRuntime(options) {
|
|
|
1750
2437
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1751
2438
|
if (discoveryPaused || authorization === undefined)
|
|
1752
2439
|
return false;
|
|
1753
|
-
|
|
1754
|
-
if (replicas.has(room.locator))
|
|
1755
|
-
progress.complete = false;
|
|
1756
|
-
return false;
|
|
1757
|
-
}
|
|
1758
|
-
return true;
|
|
2440
|
+
return room.replica.admission === 'admitted';
|
|
1759
2441
|
}
|
|
1760
2442
|
async function caughtUp(room, local, progress, live) {
|
|
1761
|
-
|
|
1762
|
-
if (held === undefined || authorization === undefined || !live())
|
|
1763
|
-
return;
|
|
1764
|
-
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
1765
|
-
if (!behind)
|
|
2443
|
+
if (authorization === undefined || !live() || !behind(room))
|
|
1766
2444
|
return;
|
|
1767
2445
|
const withheld = await advance(room, local, authorization, live);
|
|
1768
2446
|
if (withheld !== null)
|
|
@@ -1789,17 +2467,22 @@ export function composeRuntime(options) {
|
|
|
1789
2467
|
await caughtUp(room, local, progress, live);
|
|
1790
2468
|
}
|
|
1791
2469
|
async function reload(room, local, progress, live) {
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
2470
|
+
try {
|
|
2471
|
+
if (!live() || authorization === undefined)
|
|
2472
|
+
return;
|
|
2473
|
+
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
2474
|
+
if (!live())
|
|
2475
|
+
return;
|
|
2476
|
+
if (bootstrapped.kind !== 'staged') {
|
|
2477
|
+
await withhold(bootstrapped, room, local, progress);
|
|
2478
|
+
return;
|
|
2479
|
+
}
|
|
2480
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
2481
|
+
await caughtUp(room, local, progress, live);
|
|
2482
|
+
}
|
|
2483
|
+
finally {
|
|
2484
|
+
caughtUpTo(room.locator);
|
|
1800
2485
|
}
|
|
1801
|
-
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
1802
|
-
await caughtUp(room, local, progress, live);
|
|
1803
2486
|
}
|
|
1804
2487
|
async function discoverRooms(local, progress, attempt, live) {
|
|
1805
2488
|
if (closed || authorization === undefined) {
|
|
@@ -1875,6 +2558,7 @@ export function composeRuntime(options) {
|
|
|
1875
2558
|
}
|
|
1876
2559
|
const previousCapabilities = capabilities;
|
|
1877
2560
|
capabilities = result.capabilities;
|
|
2561
|
+
outcomeRetention = result.retention.outcome;
|
|
1878
2562
|
notifyReady();
|
|
1879
2563
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1880
2564
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
@@ -1984,9 +2668,13 @@ export function composeRuntime(options) {
|
|
|
1984
2668
|
options.adapter.roomStream?.reopen();
|
|
1985
2669
|
});
|
|
1986
2670
|
}
|
|
1987
|
-
function closedSocket(revoked) {
|
|
2671
|
+
function closedSocket(revoked, lost) {
|
|
1988
2672
|
const opened = socketOpen;
|
|
1989
2673
|
socketOpen = false;
|
|
2674
|
+
if (lost) {
|
|
2675
|
+
reads.lost();
|
|
2676
|
+
conversations.lost();
|
|
2677
|
+
}
|
|
1990
2678
|
if (revoked) {
|
|
1991
2679
|
void refuse('revoked-authorization');
|
|
1992
2680
|
return;
|
|
@@ -2004,6 +2692,8 @@ export function composeRuntime(options) {
|
|
|
2004
2692
|
const progress = underway?.generation === generation ? underway.progress : null;
|
|
2005
2693
|
const cycle = progress ?? { complete: true, reachable: true };
|
|
2006
2694
|
const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
|
|
2695
|
+
if (opened === generation)
|
|
2696
|
+
opened = null;
|
|
2007
2697
|
return task(OPENING_LANE, async (live) => {
|
|
2008
2698
|
underway = null;
|
|
2009
2699
|
if (!live())
|
|
@@ -2016,8 +2706,6 @@ export function composeRuntime(options) {
|
|
|
2016
2706
|
await roomsCarried(expired);
|
|
2017
2707
|
if (!live())
|
|
2018
2708
|
return;
|
|
2019
|
-
if (opened === generation)
|
|
2020
|
-
opened = null;
|
|
2021
2709
|
const replaced = new Set(owed);
|
|
2022
2710
|
owed.clear();
|
|
2023
2711
|
try {
|
|
@@ -2025,6 +2713,9 @@ export function composeRuntime(options) {
|
|
|
2025
2713
|
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
2026
2714
|
const reloading = held.filter((room) => !expired.includes(room.locator) &&
|
|
2027
2715
|
(replaced.has(room.locator) || revoked(room.locator)));
|
|
2716
|
+
for (const room of reloading)
|
|
2717
|
+
catching.add(room.locator);
|
|
2718
|
+
readSide.recover();
|
|
2028
2719
|
const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
|
|
2029
2720
|
const reloaded = [...(await reloads), ...(await replacing)];
|
|
2030
2721
|
if (progress.reachable) {
|
|
@@ -2069,38 +2760,94 @@ export function composeRuntime(options) {
|
|
|
2069
2760
|
function submitLater(locator) {
|
|
2070
2761
|
if (localSaves)
|
|
2071
2762
|
return;
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2763
|
+
void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
|
|
2764
|
+
}
|
|
2765
|
+
async function submitPass(locator, signal) {
|
|
2766
|
+
const room = discovered.get(locator);
|
|
2767
|
+
if (closed || stopping || discoveryPaused || room === undefined)
|
|
2768
|
+
return 'done';
|
|
2769
|
+
const progress = { complete: true, reachable: true };
|
|
2770
|
+
let next;
|
|
2771
|
+
try {
|
|
2772
|
+
await ensureAuthorization();
|
|
2773
|
+
if (authorization === undefined)
|
|
2774
|
+
return 'done';
|
|
2775
|
+
next = await submitRoom(room, await openLocal(), authorization, progress, signal);
|
|
2776
|
+
}
|
|
2777
|
+
catch {
|
|
2778
|
+
progress.reachable = false;
|
|
2779
|
+
next = 'again';
|
|
2780
|
+
}
|
|
2781
|
+
if (signal.aborted)
|
|
2782
|
+
return 'done';
|
|
2783
|
+
await rest(progress);
|
|
2784
|
+
return next === 'again' && !socketOpen ? 'done' : next;
|
|
2785
|
+
}
|
|
2786
|
+
function repairLater(locator, when) {
|
|
2787
|
+
if (!socketOpen || repairing.has(locator))
|
|
2075
2788
|
return;
|
|
2789
|
+
void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
|
|
2790
|
+
}
|
|
2791
|
+
async function repairPass(locator, signal) {
|
|
2792
|
+
const room = discovered.get(locator);
|
|
2793
|
+
if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
|
|
2794
|
+
return 'done';
|
|
2795
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2796
|
+
return 'done';
|
|
2797
|
+
const progress = { complete: true, reachable: true };
|
|
2798
|
+
repairing.add(locator);
|
|
2799
|
+
try {
|
|
2800
|
+
const local = await openLocal();
|
|
2801
|
+
await roomTask(locator, async (live) => {
|
|
2802
|
+
const current = () => live() && !signal.aborted;
|
|
2803
|
+
if (!current() || authorization === undefined)
|
|
2804
|
+
return;
|
|
2805
|
+
const withheld = await Promise.race([
|
|
2806
|
+
advance(room, local, authorization, current),
|
|
2807
|
+
abandonedOn(signal),
|
|
2808
|
+
]);
|
|
2809
|
+
if (withheld !== null && current())
|
|
2810
|
+
await withhold(withheld, room, local, progress);
|
|
2811
|
+
});
|
|
2076
2812
|
}
|
|
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
|
-
|
|
2813
|
+
catch {
|
|
2814
|
+
stale.add(locator);
|
|
2815
|
+
progress.reachable = false;
|
|
2816
|
+
}
|
|
2817
|
+
finally {
|
|
2818
|
+
repairing.delete(locator);
|
|
2819
|
+
}
|
|
2820
|
+
if (signal.aborted)
|
|
2821
|
+
return 'done';
|
|
2822
|
+
await rest(progress);
|
|
2823
|
+
if (!stale.has(locator)) {
|
|
2824
|
+
if ((replicas.get(locator)?.queue.length ?? 0) > 0)
|
|
2825
|
+
submitLater(locator);
|
|
2826
|
+
return 'done';
|
|
2827
|
+
}
|
|
2828
|
+
if (!repairable.has(locator))
|
|
2829
|
+
return 'done';
|
|
2830
|
+
const reset = repairable.get(locator);
|
|
2831
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
2832
|
+
}
|
|
2833
|
+
function resendLater() {
|
|
2834
|
+
if (!socketOpen)
|
|
2835
|
+
return;
|
|
2836
|
+
void recovery.attempt('resend', resendPass, { again: true });
|
|
2837
|
+
}
|
|
2838
|
+
async function resendPass(signal) {
|
|
2839
|
+
if (!socketOpen || closed || stopping || discoveryPaused)
|
|
2840
|
+
return 'done';
|
|
2841
|
+
const reading = { complete: true, reachable: true };
|
|
2842
|
+
try {
|
|
2843
|
+
await conversations.resend(reading);
|
|
2844
|
+
}
|
|
2845
|
+
catch {
|
|
2846
|
+
reading.reachable = false;
|
|
2847
|
+
}
|
|
2848
|
+
if (signal.aborted)
|
|
2849
|
+
return 'done';
|
|
2850
|
+
return reading.reachable && reading.complete ? 'done' : 'again';
|
|
2104
2851
|
}
|
|
2105
2852
|
function submitAll() {
|
|
2106
2853
|
for (const [locator, replica] of replicas) {
|
|
@@ -2114,16 +2861,23 @@ export function composeRuntime(options) {
|
|
|
2114
2861
|
progress.complete = false;
|
|
2115
2862
|
return;
|
|
2116
2863
|
}
|
|
2864
|
+
reads.applied();
|
|
2117
2865
|
submitAll();
|
|
2866
|
+
for (const locator of stale)
|
|
2867
|
+
if (repairable.has(locator))
|
|
2868
|
+
repairLater(locator, {});
|
|
2118
2869
|
await conversations.reconcile();
|
|
2119
2870
|
if (status.providerUnreachable)
|
|
2120
2871
|
conversations.reconnected();
|
|
2121
2872
|
const reading = { complete: true, reachable: true };
|
|
2122
|
-
|
|
2873
|
+
if (demand.conversations)
|
|
2874
|
+
await conversations.catchUp(reading);
|
|
2123
2875
|
if (progress.reachable && reading.reachable)
|
|
2124
2876
|
await conversations.resend(reading);
|
|
2125
|
-
if (!reading.reachable)
|
|
2877
|
+
if (!reading.reachable) {
|
|
2126
2878
|
progress.reachable = false;
|
|
2879
|
+
resendLater();
|
|
2880
|
+
}
|
|
2127
2881
|
}
|
|
2128
2882
|
catch {
|
|
2129
2883
|
progress.complete = false;
|
|
@@ -2180,7 +2934,7 @@ export function composeRuntime(options) {
|
|
|
2180
2934
|
return;
|
|
2181
2935
|
const room = discovered.get(locator);
|
|
2182
2936
|
const held = replicas.get(locator);
|
|
2183
|
-
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2937
|
+
if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
|
|
2184
2938
|
return;
|
|
2185
2939
|
if (held !== undefined &&
|
|
2186
2940
|
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
@@ -2232,10 +2986,13 @@ export function composeRuntime(options) {
|
|
|
2232
2986
|
if (event.kind === 'opened') {
|
|
2233
2987
|
socketOpen = true;
|
|
2234
2988
|
generation += 1;
|
|
2989
|
+
reads.opened();
|
|
2990
|
+
conversations.reopened();
|
|
2991
|
+
recovery.resume();
|
|
2235
2992
|
void sync('opened');
|
|
2236
2993
|
}
|
|
2237
2994
|
else if (event.kind === 'closed')
|
|
2238
|
-
closedSocket(event.revoked);
|
|
2995
|
+
closedSocket(event.revoked, event.lost);
|
|
2239
2996
|
else if (event.kind === 'opening')
|
|
2240
2997
|
void applyOpening(event.expired);
|
|
2241
2998
|
else
|
|
@@ -2255,6 +3012,7 @@ export function composeRuntime(options) {
|
|
|
2255
3012
|
haltedRefusal = false;
|
|
2256
3013
|
if (mode !== serviceMode) {
|
|
2257
3014
|
serviceMode = mode;
|
|
3015
|
+
queueReadiness();
|
|
2258
3016
|
journal.record(`service mode ${mode}`);
|
|
2259
3017
|
for (const listener of serviceModeListeners)
|
|
2260
3018
|
listener(mode);
|
|
@@ -2267,6 +3025,8 @@ export function composeRuntime(options) {
|
|
|
2267
3025
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
2268
3026
|
return;
|
|
2269
3027
|
withdrawConversations = stream.observe((event) => {
|
|
3028
|
+
if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
|
|
3029
|
+
queueReadiness();
|
|
2270
3030
|
if (conversations.receive(event)) {
|
|
2271
3031
|
void ready.then(scheduleCurrency);
|
|
2272
3032
|
return;
|
|
@@ -2289,7 +3049,7 @@ export function composeRuntime(options) {
|
|
|
2289
3049
|
if (!live())
|
|
2290
3050
|
return;
|
|
2291
3051
|
await conversations.reconcile();
|
|
2292
|
-
if (conversations.current()) {
|
|
3052
|
+
if (conversations.current() || !demand.conversations) {
|
|
2293
3053
|
show();
|
|
2294
3054
|
return;
|
|
2295
3055
|
}
|
|
@@ -2307,13 +3067,18 @@ export function composeRuntime(options) {
|
|
|
2307
3067
|
if (closed)
|
|
2308
3068
|
return;
|
|
2309
3069
|
closed = true;
|
|
3070
|
+
if (narrowing !== null)
|
|
3071
|
+
clearTimeout(narrowing);
|
|
3072
|
+
if (rechecking !== null)
|
|
3073
|
+
clearInterval(rechecking);
|
|
3074
|
+
readinessWatches.clear();
|
|
2310
3075
|
withdrawStream?.();
|
|
2311
3076
|
withdrawStream = undefined;
|
|
2312
3077
|
withdrawConversations?.();
|
|
2313
3078
|
withdrawConversations = undefined;
|
|
2314
3079
|
withdrawServiceMode?.();
|
|
2315
3080
|
conversations.close();
|
|
2316
|
-
|
|
3081
|
+
void recovery.retire();
|
|
2317
3082
|
store?.close();
|
|
2318
3083
|
void opening?.then((opened) => {
|
|
2319
3084
|
opened.close();
|
|
@@ -2331,9 +3096,8 @@ export function composeRuntime(options) {
|
|
|
2331
3096
|
}
|
|
2332
3097
|
async function stop() {
|
|
2333
3098
|
stopping = true;
|
|
2334
|
-
|
|
3099
|
+
await recovery.retire();
|
|
2335
3100
|
await Promise.all(working);
|
|
2336
|
-
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
2337
3101
|
await mutations;
|
|
2338
3102
|
close();
|
|
2339
3103
|
}
|
|
@@ -2355,17 +3119,101 @@ export function composeRuntime(options) {
|
|
|
2355
3119
|
return planOf(input, roomFacts(reach), planReach(reach));
|
|
2356
3120
|
}
|
|
2357
3121
|
function watchPlan(plan, deliver) {
|
|
2358
|
-
|
|
2359
|
-
|
|
3122
|
+
let seeded = false;
|
|
3123
|
+
let pending = true;
|
|
3124
|
+
const shown = () => {
|
|
3125
|
+
const seed = seeds.get(plan.key);
|
|
3126
|
+
if (seed === undefined || seeded || !pending)
|
|
3127
|
+
return;
|
|
3128
|
+
seeded = true;
|
|
3129
|
+
deliver(seededResult(seed));
|
|
3130
|
+
};
|
|
3131
|
+
const watchers = seedWatchers.get(plan.key) ?? new Set();
|
|
3132
|
+
seedWatchers.set(plan.key, watchers);
|
|
3133
|
+
watchers.add(shown);
|
|
3134
|
+
const stop = readSide.watch(plan, identityOf(plan), () => {
|
|
3135
|
+
const cover = covered(plan);
|
|
3136
|
+
awaitPreparation(cover);
|
|
3137
|
+
return seedBehind(plan, cover);
|
|
3138
|
+
}, (message) => {
|
|
3139
|
+
const answered = message;
|
|
3140
|
+
pending = whole(answered) && 'pending' in answered.result;
|
|
3141
|
+
if (pending && seeds.has(plan.key)) {
|
|
3142
|
+
shown();
|
|
3143
|
+
return;
|
|
3144
|
+
}
|
|
3145
|
+
seeded = false;
|
|
3146
|
+
deliver(message);
|
|
3147
|
+
});
|
|
3148
|
+
return () => {
|
|
3149
|
+
watchers.delete(shown);
|
|
3150
|
+
if (watchers.size === 0)
|
|
3151
|
+
seedWatchers.delete(plan.key);
|
|
3152
|
+
stop();
|
|
3153
|
+
};
|
|
2360
3154
|
}
|
|
2361
3155
|
async function searched(input, reach) {
|
|
2362
3156
|
const plan = searchPlanOf(input);
|
|
2363
|
-
const
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
3157
|
+
const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
|
|
3158
|
+
for (;;) {
|
|
3159
|
+
const cover = covered(filter);
|
|
3160
|
+
awaitPreparation(cover);
|
|
3161
|
+
const pending = 'pending' in cover ? [...cover.pending] : [];
|
|
3162
|
+
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
3163
|
+
const held = covered({
|
|
3164
|
+
...filter,
|
|
3165
|
+
rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
|
|
3166
|
+
});
|
|
3167
|
+
const covering = 'pending' in held ? [] : held.coverage.rooms;
|
|
3168
|
+
const unindexed = covering
|
|
3169
|
+
.filter(({ room }) => !searchIndex.rooms().has(room))
|
|
3170
|
+
.map(({ room }) => room);
|
|
3171
|
+
for (const room of unindexed)
|
|
3172
|
+
pending.push({ room, awaits: 'index' });
|
|
3173
|
+
const listed = conversations.searchable();
|
|
3174
|
+
const named = reach !== null && !reach.conversations
|
|
3175
|
+
? []
|
|
3176
|
+
: (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
|
|
3177
|
+
const waiting = named.filter((one) => !searchIndex.conversations().has(one));
|
|
3178
|
+
for (const conversation of waiting)
|
|
3179
|
+
pending.push({ conversation, awaits: 'index' });
|
|
3180
|
+
if (unindexed.length > 0 || waiting.length > 0)
|
|
3181
|
+
index(unindexed, waiting);
|
|
3182
|
+
const complete = pending.length === 0 && !('pending' in cover);
|
|
3183
|
+
const answering = complete
|
|
3184
|
+
? filter
|
|
3185
|
+
: {
|
|
3186
|
+
...filter,
|
|
3187
|
+
rooms: covering
|
|
3188
|
+
.filter(({ room }) => searchIndex.rooms().has(room))
|
|
3189
|
+
.map(({ room }) => room),
|
|
3190
|
+
};
|
|
3191
|
+
const answered = covered(answering);
|
|
3192
|
+
if ('pending' in answered)
|
|
3193
|
+
continue;
|
|
3194
|
+
const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
|
|
3195
|
+
const seen = searchIndex.changes();
|
|
3196
|
+
await searchIndex.caughtUp();
|
|
3197
|
+
const selected = roomsAlone(answering) ? null : await rowsQuery(answering);
|
|
3198
|
+
if (selected !== null && 'pending' in selected)
|
|
3199
|
+
continue;
|
|
3200
|
+
if (searchIndex.changes() !== seen ||
|
|
3201
|
+
JSON.stringify(covered(answering)) !== JSON.stringify(answered))
|
|
3202
|
+
continue;
|
|
3203
|
+
const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms === null ? null : new Set(answering.rooms), plan.collection
|
|
3204
|
+
? new Map(answered.coverage.rooms.map(({ room }) => [
|
|
3205
|
+
room,
|
|
3206
|
+
replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
|
|
3207
|
+
]))
|
|
3208
|
+
: null);
|
|
3209
|
+
const coverage = {
|
|
3210
|
+
rooms: answered.coverage.rooms,
|
|
3211
|
+
conversations: searchedConversations,
|
|
3212
|
+
index: searchIndex.generation(),
|
|
3213
|
+
overlay: answered.coverage.overlay,
|
|
3214
|
+
};
|
|
3215
|
+
return complete ? { ...hits, coverage } : { pending, ...hits, coverage };
|
|
3216
|
+
}
|
|
2369
3217
|
}
|
|
2370
3218
|
const watch = (topic, input, deliver) => {
|
|
2371
3219
|
if (topic === 'room-drafts') {
|
|
@@ -2411,6 +3259,9 @@ export function composeRuntime(options) {
|
|
|
2411
3259
|
presentationListeners.delete(listener);
|
|
2412
3260
|
};
|
|
2413
3261
|
}
|
|
3262
|
+
if (topic === 'readiness') {
|
|
3263
|
+
return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
|
|
3264
|
+
}
|
|
2414
3265
|
if (topic === 'rooms') {
|
|
2415
3266
|
roomListeners.add(deliver);
|
|
2416
3267
|
deliver(listed());
|
|
@@ -2577,7 +3428,13 @@ export function composeRuntime(options) {
|
|
|
2577
3428
|
return journal.export();
|
|
2578
3429
|
},
|
|
2579
3430
|
persistence() {
|
|
2580
|
-
return
|
|
3431
|
+
return persistence === 'granted';
|
|
3432
|
+
},
|
|
3433
|
+
async retry() {
|
|
3434
|
+
journal.record('retry');
|
|
3435
|
+
unindexable.clear();
|
|
3436
|
+
queueReadiness();
|
|
3437
|
+
resume();
|
|
2581
3438
|
},
|
|
2582
3439
|
observe: observing(watch),
|
|
2583
3440
|
async reauthorize() {
|
|
@@ -2604,6 +3461,22 @@ export function composeRuntime(options) {
|
|
|
2604
3461
|
query: (plan) => rowsQuery(plan),
|
|
2605
3462
|
watchPlan,
|
|
2606
3463
|
search: searched,
|
|
3464
|
+
watchReadiness,
|
|
3465
|
+
};
|
|
3466
|
+
return {
|
|
3467
|
+
md,
|
|
3468
|
+
seed,
|
|
3469
|
+
stop,
|
|
3470
|
+
lose: loseRooms,
|
|
3471
|
+
watch,
|
|
3472
|
+
acquire,
|
|
3473
|
+
persisted(answer) {
|
|
3474
|
+
if (answer === persistence)
|
|
3475
|
+
return;
|
|
3476
|
+
persistence = answer;
|
|
3477
|
+
journal.record(`persistence ${answer}`);
|
|
3478
|
+
queueReadiness();
|
|
3479
|
+
},
|
|
3480
|
+
within: (reach, port) => narrowed(reachable, reach, port),
|
|
2607
3481
|
};
|
|
2608
|
-
return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2609
3482
|
}
|