@agentshouse/mdruntime 0.1.0-ca14c49c06d90 → 0.1.0-ca4dc698bd0c4
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 +260 -259
- 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 +6 -1
- package/dist/adapter.d.ts.map +1 -1
- package/dist/change-set.d.ts +2 -0
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +34 -9
- package/dist/client.d.ts +8 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1 -0
- 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 +20 -7
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +926 -175
- package/dist/conversation.d.ts +8 -5
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +128 -83
- 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 +11 -2
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +33 -3
- 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 +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/memory.d.ts +2 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +7 -2
- package/dist/model.d.ts +1 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +9 -0
- 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 +1 -0
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +25 -54
- package/dist/query.d.ts +28 -3
- package/dist/query.d.ts.map +1 -1
- package/dist/reach.d.ts +6 -2
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +16 -4
- package/dist/react.d.ts +5 -3
- package/dist/react.d.ts.map +1 -1
- package/dist/read-side.d.ts +13 -6
- package/dist/read-side.d.ts.map +1 -1
- package/dist/read-side.js +282 -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 +11 -10
- package/dist/replica.d.ts +1 -1
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +47 -19
- package/dist/rows.d.ts +4 -13
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +2 -12
- 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 +28 -6
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +108 -45
- 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 +18 -3
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +147 -49
- 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 +2 -0
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +2 -0
- package/package.json +2 -2
package/dist/connection.js
CHANGED
|
@@ -6,22 +6,27 @@ import { createReadSide } from './read-side.js';
|
|
|
6
6
|
import { markdownPath, roomOfKey } from './rows.js';
|
|
7
7
|
import { createSearchIndex, searchPlanOf, } from './search.js';
|
|
8
8
|
import { bootstrapRoom } from './bootstrap.js';
|
|
9
|
-
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
9
|
+
import { changeSet, entering, mergeChangeSets } from './change-set.js';
|
|
10
10
|
import { createConversationStore, } from './conversation.js';
|
|
11
11
|
import { queuedFile } from './change.js';
|
|
12
|
-
import { validRoot } from './collection.js';
|
|
12
|
+
import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
|
|
13
13
|
import { runtimeError } from './failure.js';
|
|
14
14
|
import { createLog } from './log.js';
|
|
15
15
|
import { binaryRecord } from './binary.js';
|
|
16
16
|
import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
|
|
17
|
+
import { createRecovery } from './recovery.js';
|
|
17
18
|
import { refreshRoom } from './refresh.js';
|
|
18
19
|
import { buildBatch, joined, limitedIn, requeued } from './save.js';
|
|
19
20
|
import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
|
|
20
21
|
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
21
22
|
import { same, saving, STARTING, transferring, } from './status.js';
|
|
22
23
|
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
24
|
+
import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
|
|
25
|
+
import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
|
|
23
26
|
import { FencedMap } from './fence.js';
|
|
24
|
-
import {
|
|
27
|
+
import { tally } from './shared.js';
|
|
28
|
+
import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
|
|
29
|
+
import { identityTime, roomDraftRevision } from './identity.js';
|
|
25
30
|
import { beneath, normalize } from './paths.js';
|
|
26
31
|
import { openRuntimeStore, } from './store.js';
|
|
27
32
|
import { unconfirmed } from './transfer.js';
|
|
@@ -42,6 +47,29 @@ function declaredSelection(room) {
|
|
|
42
47
|
const OPENING_LANE = 'opening';
|
|
43
48
|
const CONVERSATION_LANE = 'conversations';
|
|
44
49
|
const ROOM_READS = 256;
|
|
50
|
+
const ROOM_ATTEMPTS = 16;
|
|
51
|
+
const outside = new Map();
|
|
52
|
+
function outsideOf(roots) {
|
|
53
|
+
const key = JSON.stringify(roots);
|
|
54
|
+
const held = outside.get(key);
|
|
55
|
+
if (held !== undefined)
|
|
56
|
+
return held;
|
|
57
|
+
const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
|
|
58
|
+
const made = {
|
|
59
|
+
documents: (path) => (markdownPath(path) ? counted(path) : 0),
|
|
60
|
+
binaries: counted,
|
|
61
|
+
};
|
|
62
|
+
outside.set(key, made);
|
|
63
|
+
return made;
|
|
64
|
+
}
|
|
65
|
+
function abandonedOn(signal) {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
if (signal.aborted)
|
|
68
|
+
resolve(null);
|
|
69
|
+
else
|
|
70
|
+
signal.addEventListener('abort', () => resolve(null), { once: true });
|
|
71
|
+
});
|
|
72
|
+
}
|
|
45
73
|
function roomLane(locator) {
|
|
46
74
|
return `room ${locator}`;
|
|
47
75
|
}
|
|
@@ -98,7 +126,6 @@ export function composeRuntime(options) {
|
|
|
98
126
|
let status = STARTING;
|
|
99
127
|
let shown = STARTING;
|
|
100
128
|
let discoveryPaused = false;
|
|
101
|
-
let resetTimer;
|
|
102
129
|
let closed = false;
|
|
103
130
|
let stopping = false;
|
|
104
131
|
let withdrawStream;
|
|
@@ -106,7 +133,10 @@ export function composeRuntime(options) {
|
|
|
106
133
|
let underway = null;
|
|
107
134
|
let streamed = null;
|
|
108
135
|
let handed = false;
|
|
109
|
-
const
|
|
136
|
+
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
137
|
+
const repairing = new Set();
|
|
138
|
+
const repairable = new Map();
|
|
139
|
+
let outcomeRetention = null;
|
|
110
140
|
const answering = new Map();
|
|
111
141
|
let complete = false;
|
|
112
142
|
let recorded = false;
|
|
@@ -136,25 +166,41 @@ export function composeRuntime(options) {
|
|
|
136
166
|
const serviceModeListeners = new Set();
|
|
137
167
|
let member = null;
|
|
138
168
|
const memberListeners = new Set();
|
|
139
|
-
const
|
|
140
|
-
|
|
169
|
+
const prepared = new Set();
|
|
170
|
+
const projecting = new Set();
|
|
171
|
+
const unpreparedDemand = new Set();
|
|
172
|
+
const unindexable = new Set();
|
|
173
|
+
let preparing = false;
|
|
174
|
+
let demand = WHOLE_DEMAND;
|
|
175
|
+
let narrowing = null;
|
|
176
|
+
let targets = {};
|
|
177
|
+
let persistence = options.shell.persistence;
|
|
178
|
+
let capacity = null;
|
|
179
|
+
const paused = new Set();
|
|
180
|
+
let rechecking = null;
|
|
181
|
+
const readinessWatches = new Set();
|
|
182
|
+
let readinessQueued = false;
|
|
183
|
+
const catching = new Set();
|
|
141
184
|
const transfers = new FencedMap(isFenced);
|
|
142
185
|
const conversations = createConversationStore({
|
|
143
186
|
agents: declaredAgents,
|
|
144
|
-
documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
|
|
187
|
+
documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
|
|
145
188
|
call: (name, input) => invokeAction(name, input),
|
|
146
189
|
durably,
|
|
147
190
|
stream: () => options.adapter.conversations,
|
|
148
191
|
publish: publishChangeSet,
|
|
149
192
|
online: () => !status.providerUnreachable && refusal === null,
|
|
150
193
|
closed: () => closed,
|
|
194
|
+
unanswered: resendLater,
|
|
195
|
+
expired: (operation) => pastHorizon(operation),
|
|
151
196
|
});
|
|
152
197
|
const searchIndex = createSearchIndex({
|
|
153
|
-
|
|
198
|
+
room: (room) => readSide.room(room),
|
|
154
199
|
document: (room, path) => readSide.document(room, path),
|
|
155
|
-
messages: () => conversations.messages(),
|
|
156
|
-
message: (conversation, identity) => conversations.message(conversation, identity),
|
|
157
200
|
});
|
|
201
|
+
const roomsToIndex = new Set();
|
|
202
|
+
const conversationsToIndex = new Set();
|
|
203
|
+
let indexing = false;
|
|
158
204
|
const journal = createLog();
|
|
159
205
|
let notifyReady = () => { };
|
|
160
206
|
const ready = new Promise((resolve) => {
|
|
@@ -233,9 +279,16 @@ export function composeRuntime(options) {
|
|
|
233
279
|
returned: structuredClone(record.returned),
|
|
234
280
|
});
|
|
235
281
|
}
|
|
282
|
+
for (const [room, refusal] of sourceRefusals)
|
|
283
|
+
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
236
284
|
attentionRequired.sort((left, right) => {
|
|
237
285
|
if (left.room !== right.room)
|
|
238
286
|
return left.room < right.room ? -1 : 1;
|
|
287
|
+
if ('refusal' in left || 'refusal' in right) {
|
|
288
|
+
if ('refusal' in left && 'refusal' in right)
|
|
289
|
+
return 0;
|
|
290
|
+
return 'refusal' in left ? -1 : 1;
|
|
291
|
+
}
|
|
239
292
|
if (left.path !== right.path)
|
|
240
293
|
return left.path < right.path ? -1 : 1;
|
|
241
294
|
return 0;
|
|
@@ -254,11 +307,9 @@ export function composeRuntime(options) {
|
|
|
254
307
|
if (replica === undefined)
|
|
255
308
|
return 0;
|
|
256
309
|
const roots = replica.confirmed.record.typedSelection?.roots ?? [];
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
];
|
|
261
|
-
return paths.filter((path) => !roots.some((root) => beneath(root, path))).length;
|
|
310
|
+
const weighed = outsideOf(roots);
|
|
311
|
+
return (tally(replica.visible.documents, weighed.documents) +
|
|
312
|
+
tally(replica.visible.binaries, weighed.binaries));
|
|
262
313
|
}
|
|
263
314
|
function listed() {
|
|
264
315
|
return structuredClone([...rooms.values()].map((room) => {
|
|
@@ -285,15 +336,19 @@ export function composeRuntime(options) {
|
|
|
285
336
|
return { listed: roomsListed, rooms: held };
|
|
286
337
|
}
|
|
287
338
|
function emitRooms() {
|
|
339
|
+
readSide.recover();
|
|
340
|
+
queueReadiness();
|
|
288
341
|
const value = listed();
|
|
289
342
|
for (const listener of roomListeners)
|
|
290
343
|
listener(value);
|
|
291
344
|
}
|
|
292
345
|
function emitRefusal() {
|
|
346
|
+
queueReadiness();
|
|
293
347
|
for (const listener of refusalListeners)
|
|
294
348
|
listener(refusal);
|
|
295
349
|
}
|
|
296
350
|
function emitStorageLoss() {
|
|
351
|
+
queueReadiness();
|
|
297
352
|
for (const listener of storageLossListeners)
|
|
298
353
|
listener(storageLoss);
|
|
299
354
|
}
|
|
@@ -310,7 +365,8 @@ export function composeRuntime(options) {
|
|
|
310
365
|
function show() {
|
|
311
366
|
if (closed)
|
|
312
367
|
return;
|
|
313
|
-
|
|
368
|
+
queueReadiness();
|
|
369
|
+
const next = status.state === 'synced' && demand.conversations && !conversations.current()
|
|
314
370
|
? { ...status, state: 'syncing' }
|
|
315
371
|
: status;
|
|
316
372
|
if (same(shown, next))
|
|
@@ -339,42 +395,481 @@ export function composeRuntime(options) {
|
|
|
339
395
|
}
|
|
340
396
|
}
|
|
341
397
|
function applyChangeSet(set) {
|
|
342
|
-
const held =
|
|
343
|
-
|
|
344
|
-
|
|
398
|
+
const held = {
|
|
399
|
+
...set,
|
|
400
|
+
documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
|
|
401
|
+
};
|
|
345
402
|
readSide.apply(held);
|
|
346
403
|
searchIndex.changed(held);
|
|
347
404
|
}
|
|
348
|
-
function
|
|
349
|
-
|
|
405
|
+
async function referenced(locator) {
|
|
406
|
+
for (;;) {
|
|
407
|
+
const record = replicas.get(locator)?.confirmed.record;
|
|
408
|
+
if (record === undefined || closed)
|
|
409
|
+
return;
|
|
410
|
+
const held = { text: record.documents, binaries: record.binaries };
|
|
411
|
+
const composition = compositionOf(record);
|
|
412
|
+
if (referencesIndexed(held, composition))
|
|
413
|
+
return;
|
|
414
|
+
await indexReferrers(held, composition);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
async function prepareReplica(locator) {
|
|
418
|
+
await referenced(locator);
|
|
419
|
+
const replica = replicas.get(locator);
|
|
420
|
+
if (replica === undefined || closed || prepared.has(locator))
|
|
350
421
|
return;
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
422
|
+
const visible = replica.visible;
|
|
423
|
+
const paths = [
|
|
424
|
+
...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
|
|
425
|
+
];
|
|
426
|
+
const entered = [];
|
|
427
|
+
projecting.add(locator);
|
|
428
|
+
try {
|
|
429
|
+
await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
|
|
430
|
+
const current = replicas.get(locator);
|
|
431
|
+
if (current === undefined || closed)
|
|
432
|
+
return;
|
|
433
|
+
const set = entering(locator, current, chunk);
|
|
434
|
+
readSide.apply(set);
|
|
435
|
+
entered.push(set);
|
|
436
|
+
});
|
|
437
|
+
if (!replicas.has(locator) || closed)
|
|
438
|
+
return;
|
|
439
|
+
prepared.add(locator);
|
|
440
|
+
}
|
|
441
|
+
finally {
|
|
442
|
+
projecting.delete(locator);
|
|
361
443
|
}
|
|
362
|
-
|
|
444
|
+
conversations.changed(mergeChangeSets(entered));
|
|
445
|
+
readSide.recover();
|
|
446
|
+
queueReadiness();
|
|
447
|
+
}
|
|
448
|
+
function unprepared() {
|
|
449
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
|
|
450
|
+
}
|
|
451
|
+
function unbuilt(locator, error) {
|
|
452
|
+
unindexable.add(locator);
|
|
453
|
+
journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
|
|
454
|
+
queueReadiness();
|
|
455
|
+
}
|
|
456
|
+
function prepare(locators = []) {
|
|
457
|
+
for (const locator of locators)
|
|
458
|
+
unpreparedDemand.add(locator);
|
|
459
|
+
if (preparing)
|
|
363
460
|
return;
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
461
|
+
preparing = true;
|
|
462
|
+
void (async () => {
|
|
463
|
+
try {
|
|
464
|
+
for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
|
|
465
|
+
await yielded();
|
|
466
|
+
if (closed)
|
|
467
|
+
return;
|
|
468
|
+
unpreparedDemand.delete(waiting);
|
|
469
|
+
await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
finally {
|
|
473
|
+
preparing = false;
|
|
474
|
+
}
|
|
475
|
+
})();
|
|
367
476
|
}
|
|
368
477
|
function reachedRooms(plan) {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
478
|
+
const reach = plan.reach;
|
|
479
|
+
const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
480
|
+
const named = plan.rooms ??
|
|
481
|
+
(reached === null
|
|
482
|
+
? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
|
|
483
|
+
: [...reached]);
|
|
484
|
+
return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
|
|
485
|
+
}
|
|
486
|
+
function awaited(locator) {
|
|
487
|
+
return rooms.has(locator) || !roomsListed;
|
|
488
|
+
}
|
|
489
|
+
function covered(plan) {
|
|
490
|
+
const locators = reachedRooms(plan);
|
|
491
|
+
if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
|
|
492
|
+
return { pending: [] };
|
|
493
|
+
}
|
|
494
|
+
const pending = [];
|
|
495
|
+
const coverage = [];
|
|
496
|
+
const overlay = [];
|
|
497
|
+
for (const locator of locators) {
|
|
498
|
+
const replica = replicas.get(locator);
|
|
499
|
+
if (replica === undefined) {
|
|
500
|
+
if (awaited(locator))
|
|
501
|
+
pending.push({ room: locator, awaits: 'acquisition' });
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (!prepared.has(locator)) {
|
|
505
|
+
pending.push({ room: locator, awaits: 'preparation' });
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
coverage.push({
|
|
509
|
+
room: locator,
|
|
510
|
+
generation: replica.confirmed.record.transfer,
|
|
511
|
+
current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
|
|
512
|
+
});
|
|
513
|
+
for (const batch of replica.queue)
|
|
514
|
+
overlay.push(batch.id);
|
|
515
|
+
}
|
|
516
|
+
if (pending.length > 0)
|
|
517
|
+
return { pending };
|
|
518
|
+
return { coverage: { rooms: coverage, indexes: [], overlay } };
|
|
519
|
+
}
|
|
520
|
+
function awaitPreparation(cover) {
|
|
521
|
+
if (!('pending' in cover))
|
|
522
|
+
return;
|
|
523
|
+
const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
|
|
524
|
+
if (waiting.length > 0)
|
|
525
|
+
prepare(waiting.map(({ room }) => room));
|
|
526
|
+
}
|
|
527
|
+
function acquires(room) {
|
|
528
|
+
return (demanded(demand, room) ||
|
|
529
|
+
(replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
|
|
530
|
+
queuesWithoutReplica.has(room.locator));
|
|
531
|
+
}
|
|
532
|
+
function acquiredLocators() {
|
|
533
|
+
return [...discovered.values()]
|
|
534
|
+
.filter(acquires)
|
|
535
|
+
.map((room) => room.locator)
|
|
536
|
+
.sort();
|
|
537
|
+
}
|
|
538
|
+
function heldDemand(acquired) {
|
|
539
|
+
if (demand.whole)
|
|
372
540
|
return null;
|
|
373
|
-
return
|
|
541
|
+
return { rooms: acquired.map((room) => room.locator), conversations: demand.conversations };
|
|
542
|
+
}
|
|
543
|
+
function applyDemand(next) {
|
|
544
|
+
const before = acquiredLocators();
|
|
545
|
+
const conversed = demand.conversations;
|
|
546
|
+
demand = next;
|
|
547
|
+
const after = acquiredLocators();
|
|
548
|
+
queueReadiness();
|
|
549
|
+
if (!conversed && next.conversations)
|
|
550
|
+
scheduleCurrency();
|
|
551
|
+
if (JSON.stringify(before) === JSON.stringify(after) && conversed === next.conversations)
|
|
552
|
+
return;
|
|
553
|
+
journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
|
|
554
|
+
if (options.adapter.roomStream === undefined)
|
|
555
|
+
void sync('unstreamed');
|
|
556
|
+
else if (socketOpen)
|
|
557
|
+
options.adapter.roomStream.reopen();
|
|
558
|
+
}
|
|
559
|
+
function acquire(next) {
|
|
560
|
+
if (closed)
|
|
561
|
+
return;
|
|
562
|
+
if (narrowing !== null)
|
|
563
|
+
clearTimeout(narrowing);
|
|
564
|
+
narrowing = null;
|
|
565
|
+
const widening = wider(next, demand);
|
|
566
|
+
if (!widening && !narrower(next, demand))
|
|
567
|
+
return;
|
|
568
|
+
if (widening || !roomsListed) {
|
|
569
|
+
applyDemand(next);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
// A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
|
|
573
|
+
narrowing = setTimeout(() => {
|
|
574
|
+
narrowing = null;
|
|
575
|
+
applyDemand(next);
|
|
576
|
+
}, 5_000);
|
|
577
|
+
}
|
|
578
|
+
function requiredRooms(reach) {
|
|
579
|
+
if (reach === null)
|
|
580
|
+
return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
|
|
581
|
+
const named = [...new Set(reach.rooms)].sort();
|
|
582
|
+
if (roomsListed)
|
|
583
|
+
return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
|
|
584
|
+
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
585
|
+
}
|
|
586
|
+
function roomState(locator, search) {
|
|
587
|
+
if (sourceRefusals.has(locator))
|
|
588
|
+
return 'refused';
|
|
589
|
+
const listedRoom = discovered.get(locator);
|
|
590
|
+
if (roomsListed &&
|
|
591
|
+
(listedRoom === undefined ||
|
|
592
|
+
listedRoom.replica.admission !== 'admitted' ||
|
|
593
|
+
unreadable.has(locator)))
|
|
594
|
+
return 'unadmitted';
|
|
595
|
+
if (!replicas.has(locator))
|
|
596
|
+
return 'acquiring';
|
|
597
|
+
if (!prepared.has(locator))
|
|
598
|
+
return 'preparing';
|
|
599
|
+
if (search && !searchIndex.rooms().has(locator))
|
|
600
|
+
return 'indexing';
|
|
601
|
+
return 'complete';
|
|
602
|
+
}
|
|
603
|
+
function conversationCoverage(reach, search) {
|
|
604
|
+
const declared = reach === null || reach.conversations;
|
|
605
|
+
if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
|
|
606
|
+
return 'undeclared';
|
|
607
|
+
if (!conversations.known() || !conversations.current())
|
|
608
|
+
return 'acquiring';
|
|
609
|
+
if (!search)
|
|
610
|
+
return 'complete';
|
|
611
|
+
const unindexed = conversations
|
|
612
|
+
.searchable()
|
|
613
|
+
.filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
|
|
614
|
+
if (unindexed.length === 0)
|
|
615
|
+
return 'complete';
|
|
616
|
+
index([], unindexed);
|
|
617
|
+
return 'indexing';
|
|
618
|
+
}
|
|
619
|
+
function connectionOf() {
|
|
620
|
+
if (socketOpen)
|
|
621
|
+
return 'connected';
|
|
622
|
+
if (status.providerUnreachable)
|
|
623
|
+
return 'unreachable';
|
|
624
|
+
return options.adapter.roomStream === undefined && status.lastCompleted !== null
|
|
625
|
+
? 'connected'
|
|
626
|
+
: 'connecting';
|
|
627
|
+
}
|
|
628
|
+
function refusedIn(locators) {
|
|
629
|
+
return locators.flatMap((room) => {
|
|
630
|
+
const held = sourceRefusals.get(room);
|
|
631
|
+
if (held === undefined)
|
|
632
|
+
return [];
|
|
633
|
+
const { file, location } = held.fields;
|
|
634
|
+
return [
|
|
635
|
+
{
|
|
636
|
+
room,
|
|
637
|
+
file: typeof file === 'string' ? file : '',
|
|
638
|
+
rule: held.code,
|
|
639
|
+
location: typeof location === 'string' ? location : null,
|
|
640
|
+
},
|
|
641
|
+
];
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
function readinessFor(port, reach) {
|
|
645
|
+
const required = requiredRooms(reach);
|
|
646
|
+
const target = targets[port.target];
|
|
647
|
+
const search = target?.search === true;
|
|
648
|
+
const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
|
|
649
|
+
const building = (state) => coverage
|
|
650
|
+
.filter((room) => room.state === state && !unindexable.has(room.room))
|
|
651
|
+
.map(({ room }) => room);
|
|
652
|
+
const preparing = building('preparing');
|
|
653
|
+
if (preparing.length > 0)
|
|
654
|
+
prepare(preparing);
|
|
655
|
+
const indexing = building('indexing');
|
|
656
|
+
if (indexing.length > 0)
|
|
657
|
+
index(indexing, []);
|
|
658
|
+
return readinessOf({
|
|
659
|
+
code: port.code,
|
|
660
|
+
coverage: {
|
|
661
|
+
listed: required.listed,
|
|
662
|
+
rooms: coverage,
|
|
663
|
+
conversations: conversationCoverage(reach, search),
|
|
664
|
+
search: search ? 'required' : 'unused',
|
|
665
|
+
},
|
|
666
|
+
persistence,
|
|
667
|
+
connection: connectionOf(),
|
|
668
|
+
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
669
|
+
capacity: capacity !== null &&
|
|
670
|
+
coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
|
|
671
|
+
? capacity
|
|
672
|
+
: port.capacity,
|
|
673
|
+
refused: refusedIn(required.rooms),
|
|
674
|
+
lost: storageLoss !== null,
|
|
675
|
+
established: target?.established === true,
|
|
676
|
+
});
|
|
374
677
|
}
|
|
375
|
-
function
|
|
376
|
-
|
|
377
|
-
|
|
678
|
+
function keepTarget(name, change) {
|
|
679
|
+
const held = targets[name] ?? { established: false, search: false };
|
|
680
|
+
const next = { ...held, ...change };
|
|
681
|
+
if (next.established === held.established && next.search === held.search)
|
|
682
|
+
return;
|
|
683
|
+
targets = { ...targets, [name]: next };
|
|
684
|
+
queueReadiness();
|
|
685
|
+
void durably((local) => local.keepTargets(targets)).catch(() => {
|
|
686
|
+
journal.record(`target record failed ${name}`);
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
function deliverReadiness(watch) {
|
|
690
|
+
if (!readinessWatches.has(watch))
|
|
691
|
+
return;
|
|
692
|
+
const readiness = readinessFor(watch.port, watch.reach);
|
|
693
|
+
if (readiness.ready && targets[watch.port.target]?.established !== true) {
|
|
694
|
+
keepTarget(watch.port.target, { established: true });
|
|
695
|
+
}
|
|
696
|
+
const shown = JSON.stringify(readiness);
|
|
697
|
+
if (shown === watch.last)
|
|
698
|
+
return;
|
|
699
|
+
watch.last = shown;
|
|
700
|
+
watch.deliver(structuredClone(readiness));
|
|
701
|
+
}
|
|
702
|
+
function queueReadiness() {
|
|
703
|
+
if (readinessQueued || closed || readinessWatches.size === 0)
|
|
704
|
+
return;
|
|
705
|
+
readinessQueued = true;
|
|
706
|
+
queueMicrotask(() => {
|
|
707
|
+
readinessQueued = false;
|
|
708
|
+
if (closed)
|
|
709
|
+
return;
|
|
710
|
+
for (const watch of [...readinessWatches])
|
|
711
|
+
deliverReadiness(watch);
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
function watchReadiness(port, reach, deliver) {
|
|
715
|
+
const watch = { port, reach, deliver, last: null };
|
|
716
|
+
readinessWatches.add(watch);
|
|
717
|
+
void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
|
|
718
|
+
return () => {
|
|
719
|
+
readinessWatches.delete(watch);
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
function searching(target) {
|
|
723
|
+
if (targets[target]?.search !== true)
|
|
724
|
+
keepTarget(target, { search: true });
|
|
725
|
+
}
|
|
726
|
+
function layerBytes(layer) {
|
|
727
|
+
let bytes = 0;
|
|
728
|
+
for (const [path, text] of Object.entries(layer.documents))
|
|
729
|
+
bytes += (path.length + text.length) * 2;
|
|
730
|
+
return bytes;
|
|
731
|
+
}
|
|
732
|
+
function grownBy(next, previous) {
|
|
733
|
+
if (previous === undefined)
|
|
734
|
+
return layerBytes(next);
|
|
735
|
+
const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
|
|
736
|
+
return layerBytes({ ...next, documents });
|
|
737
|
+
}
|
|
738
|
+
function exceeded(error) {
|
|
739
|
+
return (typeof error === 'object' &&
|
|
740
|
+
error !== null &&
|
|
741
|
+
error.name === 'QuotaExceededError');
|
|
742
|
+
}
|
|
743
|
+
async function estimated() {
|
|
744
|
+
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
745
|
+
return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
|
|
746
|
+
}
|
|
747
|
+
function pause(locator, cause) {
|
|
748
|
+
paused.add(locator);
|
|
749
|
+
capacity = cause;
|
|
750
|
+
journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
|
|
751
|
+
queueReadiness();
|
|
752
|
+
if (rechecking !== null)
|
|
753
|
+
return;
|
|
754
|
+
// A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
|
|
755
|
+
rechecking = setInterval(() => {
|
|
756
|
+
void recheck();
|
|
757
|
+
}, 60_000);
|
|
758
|
+
}
|
|
759
|
+
async function recheck() {
|
|
760
|
+
const held = capacity;
|
|
761
|
+
if (held === null || closed)
|
|
762
|
+
return;
|
|
763
|
+
const available = await estimated().catch(() => null);
|
|
764
|
+
if (available === null || available < (held.needed ?? 0))
|
|
765
|
+
return;
|
|
766
|
+
if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
|
|
767
|
+
return;
|
|
768
|
+
resume();
|
|
769
|
+
}
|
|
770
|
+
function resume() {
|
|
771
|
+
if (rechecking !== null)
|
|
772
|
+
clearInterval(rechecking);
|
|
773
|
+
rechecking = null;
|
|
774
|
+
if (paused.size === 0 && capacity === null)
|
|
775
|
+
return;
|
|
776
|
+
paused.clear();
|
|
777
|
+
capacity = null;
|
|
778
|
+
journal.record('capacity resumed');
|
|
779
|
+
queueReadiness();
|
|
780
|
+
if (!closed && authorization !== undefined)
|
|
781
|
+
void sync('unstreamed');
|
|
782
|
+
}
|
|
783
|
+
async function reclaimed(local) {
|
|
784
|
+
const unreferenced = [...transfers.keys()].filter((locator) => {
|
|
785
|
+
const room = discovered.get(locator);
|
|
786
|
+
return room === undefined || !acquires(room);
|
|
787
|
+
});
|
|
788
|
+
for (const locator of unreferenced) {
|
|
789
|
+
await local.abandon(locator);
|
|
790
|
+
transfers.delete(locator);
|
|
791
|
+
}
|
|
792
|
+
if (unreferenced.length > 0)
|
|
793
|
+
journal.record(`capacity reclaimed ${unreferenced.length}`);
|
|
794
|
+
return unreferenced.length > 0;
|
|
795
|
+
}
|
|
796
|
+
async function admitted(local, locator, needed) {
|
|
797
|
+
if (paused.has(locator))
|
|
798
|
+
return false;
|
|
799
|
+
let available = await estimated().catch(() => null);
|
|
800
|
+
if (available === null || available >= needed)
|
|
801
|
+
return true;
|
|
802
|
+
if (await reclaimed(local))
|
|
803
|
+
available = await estimated().catch(() => null);
|
|
804
|
+
if (available === null || available >= needed)
|
|
805
|
+
return true;
|
|
806
|
+
pause(locator, { cause: 'insufficient-space', needed, available });
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
async function exceededOn(locator, bytes) {
|
|
810
|
+
const available = await estimated().catch(() => null);
|
|
811
|
+
pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
|
|
812
|
+
}
|
|
813
|
+
async function rowsQuery(plan) {
|
|
814
|
+
const before = covered(plan);
|
|
815
|
+
awaitPreparation(before);
|
|
816
|
+
if ('pending' in before)
|
|
817
|
+
return before;
|
|
818
|
+
const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
|
|
819
|
+
awaitPreparation(cover);
|
|
820
|
+
if ('pending' in cover)
|
|
821
|
+
return cover;
|
|
822
|
+
return 'groups' in rows
|
|
823
|
+
? { groups: rows.groups, coverage: cover.coverage }
|
|
824
|
+
: { documents: rows.documents, coverage: cover.coverage };
|
|
825
|
+
}
|
|
826
|
+
async function indexConversation(conversation) {
|
|
827
|
+
searchIndex.indexMessages(conversation, [], false);
|
|
828
|
+
let after = null;
|
|
829
|
+
for (;;) {
|
|
830
|
+
const batch = await durably((local) => local.messagesAfter(conversation, after));
|
|
831
|
+
if (closed)
|
|
832
|
+
return;
|
|
833
|
+
searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
|
|
834
|
+
if (batch.length === 0)
|
|
835
|
+
return;
|
|
836
|
+
after = batch[batch.length - 1].identity;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
function indexNext() {
|
|
840
|
+
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
|
|
841
|
+
if (room !== undefined) {
|
|
842
|
+
roomsToIndex.delete(room);
|
|
843
|
+
return () => prepared.has(room)
|
|
844
|
+
? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
|
|
845
|
+
: undefined;
|
|
846
|
+
}
|
|
847
|
+
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
848
|
+
if (conversation === undefined)
|
|
849
|
+
return null;
|
|
850
|
+
conversationsToIndex.delete(conversation);
|
|
851
|
+
return () => indexConversation(conversation);
|
|
852
|
+
}
|
|
853
|
+
function index(rooms, named) {
|
|
854
|
+
for (const room of rooms)
|
|
855
|
+
roomsToIndex.add(room);
|
|
856
|
+
for (const conversation of named)
|
|
857
|
+
conversationsToIndex.add(conversation);
|
|
858
|
+
if (indexing)
|
|
859
|
+
return;
|
|
860
|
+
indexing = true;
|
|
861
|
+
void (async () => {
|
|
862
|
+
try {
|
|
863
|
+
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
864
|
+
await yielded();
|
|
865
|
+
await unit();
|
|
866
|
+
queueReadiness();
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
finally {
|
|
870
|
+
indexing = false;
|
|
871
|
+
}
|
|
872
|
+
})().catch(() => { });
|
|
378
873
|
}
|
|
379
874
|
function publishChangeSet(set) {
|
|
380
875
|
applyChangeSet(set);
|
|
@@ -391,34 +886,7 @@ export function composeRuntime(options) {
|
|
|
391
886
|
emitPresentation(app);
|
|
392
887
|
conversations.changed(set);
|
|
393
888
|
}
|
|
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
889
|
async function refuse(reason, provider = null) {
|
|
421
|
-
stopTimer();
|
|
422
890
|
discoveryPaused = true;
|
|
423
891
|
try {
|
|
424
892
|
if (reason === 'revoked-authorization') {
|
|
@@ -563,10 +1031,14 @@ export function composeRuntime(options) {
|
|
|
563
1031
|
rooms.delete(locator);
|
|
564
1032
|
roomDrafts.delete(locator);
|
|
565
1033
|
transfers.delete(locator);
|
|
1034
|
+
catching.delete(locator);
|
|
566
1035
|
discovered.delete(locator);
|
|
567
1036
|
announced.delete(locator);
|
|
568
1037
|
unreadable.delete(locator);
|
|
569
1038
|
stale.delete(locator);
|
|
1039
|
+
repairable.delete(locator);
|
|
1040
|
+
unindexable.delete(locator);
|
|
1041
|
+
paused.delete(locator);
|
|
570
1042
|
scopedRefusals.delete(locator);
|
|
571
1043
|
sourceRefusals.delete(locator);
|
|
572
1044
|
declarationRefusals.delete(locator);
|
|
@@ -582,7 +1054,9 @@ export function composeRuntime(options) {
|
|
|
582
1054
|
return fresh;
|
|
583
1055
|
publishChangeSet(mergeChangeSets(removed));
|
|
584
1056
|
for (const locator of fresh)
|
|
585
|
-
|
|
1057
|
+
prepared.delete(locator);
|
|
1058
|
+
searchIndex.forget(fresh);
|
|
1059
|
+
readSide.recover();
|
|
586
1060
|
moveTo({ ...status, ...attention() });
|
|
587
1061
|
return fresh;
|
|
588
1062
|
}
|
|
@@ -618,7 +1092,7 @@ export function composeRuntime(options) {
|
|
|
618
1092
|
return model;
|
|
619
1093
|
}
|
|
620
1094
|
async function hydrate(opened) {
|
|
621
|
-
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
|
|
1095
|
+
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
|
|
622
1096
|
opened.all(),
|
|
623
1097
|
opened.queues(),
|
|
624
1098
|
opened.roomDrafts(),
|
|
@@ -630,7 +1104,9 @@ export function composeRuntime(options) {
|
|
|
630
1104
|
opened.capabilities(),
|
|
631
1105
|
opened.member(),
|
|
632
1106
|
opened.transfers(),
|
|
1107
|
+
opened.targets(),
|
|
633
1108
|
]);
|
|
1109
|
+
targets = heldTargets;
|
|
634
1110
|
transfers.clear();
|
|
635
1111
|
for (const record of staged)
|
|
636
1112
|
transfers.set(record.locator, record);
|
|
@@ -692,6 +1168,7 @@ export function composeRuntime(options) {
|
|
|
692
1168
|
async function readState(opened) {
|
|
693
1169
|
const { lastCompleted, locators, read } = await hydrate(opened);
|
|
694
1170
|
applyChangeSet(read);
|
|
1171
|
+
prepare();
|
|
695
1172
|
if (replicas.size > 0)
|
|
696
1173
|
emitRooms();
|
|
697
1174
|
if (member !== null)
|
|
@@ -807,17 +1284,19 @@ export function composeRuntime(options) {
|
|
|
807
1284
|
moveTo({ ...status, ...attention() });
|
|
808
1285
|
throw error;
|
|
809
1286
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1287
|
+
return () => {
|
|
1288
|
+
queuesWithoutReplica.delete(locator);
|
|
1289
|
+
for (const { batch, returned } of unqueued) {
|
|
1290
|
+
const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
|
|
1291
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
1292
|
+
}
|
|
1293
|
+
replicas.set(locator, replicaOf(confirmed, kept, before));
|
|
1294
|
+
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
1295
|
+
room: settling.room,
|
|
1296
|
+
queue: kept.length !== previousQueue.length,
|
|
1297
|
+
drafts: [...written.keys()],
|
|
1298
|
+
});
|
|
1299
|
+
};
|
|
821
1300
|
}
|
|
822
1301
|
function adoptModels(locator, record) {
|
|
823
1302
|
for (const [attached, model] of [
|
|
@@ -853,8 +1332,13 @@ export function composeRuntime(options) {
|
|
|
853
1332
|
function publish(local, room, confirmed, landing, live) {
|
|
854
1333
|
const { base, stored } = landing;
|
|
855
1334
|
return exclusive(async () => {
|
|
856
|
-
if (
|
|
1335
|
+
if (stored !== undefined) {
|
|
1336
|
+
await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
|
|
1337
|
+
}
|
|
1338
|
+
if (!live()) {
|
|
1339
|
+
caughtUpTo(room.locator);
|
|
857
1340
|
return true;
|
|
1341
|
+
}
|
|
858
1342
|
const prior = replicas.get(room.locator);
|
|
859
1343
|
if (base !== undefined &&
|
|
860
1344
|
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
@@ -868,17 +1352,33 @@ export function composeRuntime(options) {
|
|
|
868
1352
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
869
1353
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
870
1354
|
});
|
|
871
|
-
|
|
872
|
-
|
|
1355
|
+
let settled;
|
|
1356
|
+
try {
|
|
1357
|
+
settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
|
|
1358
|
+
}
|
|
1359
|
+
catch (error) {
|
|
1360
|
+
if (!exceeded(error))
|
|
1361
|
+
throw error;
|
|
1362
|
+
await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
|
|
1363
|
+
caughtUpTo(room.locator);
|
|
1364
|
+
return true;
|
|
1365
|
+
}
|
|
1366
|
+
const set = settled();
|
|
1367
|
+
if (stored !== undefined) {
|
|
873
1368
|
transfers.delete(room.locator);
|
|
1369
|
+
catching.delete(room.locator);
|
|
1370
|
+
}
|
|
874
1371
|
for (const batch of accepted)
|
|
875
1372
|
pendings.delete(batch.id);
|
|
876
1373
|
stale.delete(room.locator);
|
|
1374
|
+
unindexable.delete(room.locator);
|
|
877
1375
|
if (unreadable.delete(room.locator))
|
|
878
1376
|
scopedRefusals.delete(room.locator);
|
|
879
1377
|
sourceRefusals.delete(room.locator);
|
|
880
1378
|
declarationRefusals.delete(room.locator);
|
|
881
1379
|
list(room, true);
|
|
1380
|
+
if (prior === undefined)
|
|
1381
|
+
prepared.add(room.locator);
|
|
882
1382
|
publishChangeSet(set);
|
|
883
1383
|
moveTo({ ...status, ...attention() });
|
|
884
1384
|
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
@@ -887,6 +1387,7 @@ export function composeRuntime(options) {
|
|
|
887
1387
|
}
|
|
888
1388
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
889
1389
|
const local = await openLocal();
|
|
1390
|
+
await referenced(locator);
|
|
890
1391
|
const replica = replicas.get(locator);
|
|
891
1392
|
const room = rooms.get(locator);
|
|
892
1393
|
if (replica === undefined || room === undefined) {
|
|
@@ -934,6 +1435,11 @@ export function composeRuntime(options) {
|
|
|
934
1435
|
changes,
|
|
935
1436
|
bases: parts.flatMap(({ bases }) => bases),
|
|
936
1437
|
writtenBy,
|
|
1438
|
+
clean: {
|
|
1439
|
+
text: replica.confirmed.record.documents,
|
|
1440
|
+
binaries: replica.confirmed.record.binaries,
|
|
1441
|
+
composition: compositionOf(replica.confirmed.record),
|
|
1442
|
+
},
|
|
937
1443
|
};
|
|
938
1444
|
const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
|
|
939
1445
|
if (checked?.kind === 'refused')
|
|
@@ -1274,8 +1780,20 @@ export function composeRuntime(options) {
|
|
|
1274
1780
|
stage: (record, page) => exclusive(async () => {
|
|
1275
1781
|
if (!live())
|
|
1276
1782
|
return false;
|
|
1277
|
-
|
|
1783
|
+
const bytes = layerBytes(page);
|
|
1784
|
+
if (!(await admitted(local, record.locator, bytes)))
|
|
1785
|
+
return false;
|
|
1786
|
+
try {
|
|
1787
|
+
await local.stage(record, page);
|
|
1788
|
+
}
|
|
1789
|
+
catch (error) {
|
|
1790
|
+
if (!exceeded(error))
|
|
1791
|
+
throw error;
|
|
1792
|
+
await exceededOn(record.locator, bytes);
|
|
1793
|
+
return false;
|
|
1794
|
+
}
|
|
1278
1795
|
transfers.set(record.locator, record);
|
|
1796
|
+
readSide.recover();
|
|
1279
1797
|
return true;
|
|
1280
1798
|
}),
|
|
1281
1799
|
abandon: (locator) => exclusive(async () => {
|
|
@@ -1283,6 +1801,7 @@ export function composeRuntime(options) {
|
|
|
1283
1801
|
return;
|
|
1284
1802
|
await local.abandon(locator);
|
|
1285
1803
|
transfers.delete(locator);
|
|
1804
|
+
readSide.recover();
|
|
1286
1805
|
}),
|
|
1287
1806
|
};
|
|
1288
1807
|
}
|
|
@@ -1295,6 +1814,10 @@ export function composeRuntime(options) {
|
|
|
1295
1814
|
async function pullBootstrap(authorized, room, local, live) {
|
|
1296
1815
|
if (!live())
|
|
1297
1816
|
return unconfirmed;
|
|
1817
|
+
if (replicas.has(room.locator)) {
|
|
1818
|
+
catching.add(room.locator);
|
|
1819
|
+
readSide.recover();
|
|
1820
|
+
}
|
|
1298
1821
|
try {
|
|
1299
1822
|
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
1300
1823
|
collection: modelOf(room.locator),
|
|
@@ -1302,14 +1825,24 @@ export function composeRuntime(options) {
|
|
|
1302
1825
|
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
1303
1826
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
1304
1827
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1828
|
+
if (result.kind !== 'staged')
|
|
1829
|
+
caughtUpTo(room.locator);
|
|
1305
1830
|
return result;
|
|
1306
1831
|
}
|
|
1307
1832
|
catch (error) {
|
|
1308
1833
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
1834
|
+
caughtUpTo(room.locator);
|
|
1309
1835
|
throw error;
|
|
1310
1836
|
}
|
|
1311
1837
|
}
|
|
1838
|
+
function caughtUpTo(locator) {
|
|
1839
|
+
if (catching.delete(locator))
|
|
1840
|
+
readSide.recover();
|
|
1841
|
+
}
|
|
1312
1842
|
async function advance(room, local, authorized, live) {
|
|
1843
|
+
await referenced(room.locator);
|
|
1844
|
+
if (!live() || paused.has(room.locator))
|
|
1845
|
+
return unconfirmed;
|
|
1313
1846
|
const held = replicas.get(room.locator);
|
|
1314
1847
|
if (held !== undefined) {
|
|
1315
1848
|
const refreshed = await pullRefresh(authorized, held.confirmed, live);
|
|
@@ -1340,6 +1873,7 @@ export function composeRuntime(options) {
|
|
|
1340
1873
|
return refreshed;
|
|
1341
1874
|
if (opened !== null) {
|
|
1342
1875
|
owed.add(room.locator);
|
|
1876
|
+
readSide.recover();
|
|
1343
1877
|
return unconfirmed;
|
|
1344
1878
|
}
|
|
1345
1879
|
}
|
|
@@ -1352,10 +1886,19 @@ export function composeRuntime(options) {
|
|
|
1352
1886
|
return null;
|
|
1353
1887
|
}
|
|
1354
1888
|
async function withhold(withheld, room, local, progress) {
|
|
1889
|
+
if (withheld.kind === 'unconfirmed')
|
|
1890
|
+
return;
|
|
1355
1891
|
if (withheld.kind === 'refused')
|
|
1356
1892
|
stale.delete(room.locator);
|
|
1357
1893
|
else
|
|
1358
1894
|
stale.add(room.locator);
|
|
1895
|
+
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
1896
|
+
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
1897
|
+
repairable.set(room.locator, reset);
|
|
1898
|
+
repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
|
|
1899
|
+
}
|
|
1900
|
+
else
|
|
1901
|
+
repairable.delete(room.locator);
|
|
1359
1902
|
if (withheld.kind === 'scoped-refusal') {
|
|
1360
1903
|
await scoped(withheld, room, local);
|
|
1361
1904
|
}
|
|
@@ -1391,6 +1934,7 @@ export function composeRuntime(options) {
|
|
|
1391
1934
|
unreadable.add(room.locator);
|
|
1392
1935
|
list(room, false);
|
|
1393
1936
|
emitRooms();
|
|
1937
|
+
moveTo({ ...status, ...attention() });
|
|
1394
1938
|
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
1395
1939
|
}
|
|
1396
1940
|
function leaving(batch, result) {
|
|
@@ -1412,6 +1956,7 @@ export function composeRuntime(options) {
|
|
|
1412
1956
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1413
1957
|
}
|
|
1414
1958
|
async function adoptAccepted(room, local, submitted, accepted) {
|
|
1959
|
+
await referenced(room.locator);
|
|
1415
1960
|
if (!continuing())
|
|
1416
1961
|
return unconfirmed;
|
|
1417
1962
|
const current = replicas.get(room.locator);
|
|
@@ -1433,11 +1978,12 @@ export function composeRuntime(options) {
|
|
|
1433
1978
|
emitQueue(room.locator);
|
|
1434
1979
|
return applied;
|
|
1435
1980
|
}
|
|
1436
|
-
const
|
|
1981
|
+
const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
1437
1982
|
pendings.delete(submitted.id);
|
|
1438
1983
|
unreadable.delete(room.locator);
|
|
1439
1984
|
sourceRefusals.delete(room.locator);
|
|
1440
1985
|
declarationRefusals.delete(room.locator);
|
|
1986
|
+
const set = settled();
|
|
1441
1987
|
list(room, true);
|
|
1442
1988
|
publishChangeSet(set);
|
|
1443
1989
|
moveTo({ ...status, ...attention() });
|
|
@@ -1451,14 +1997,14 @@ export function composeRuntime(options) {
|
|
|
1451
1997
|
refusedSource(room, failed);
|
|
1452
1998
|
return false;
|
|
1453
1999
|
}
|
|
1454
|
-
async function submitRoom(room, local, authorized, progress) {
|
|
1455
|
-
while (continuing() && authorization !== undefined) {
|
|
2000
|
+
async function submitRoom(room, local, authorized, progress, signal) {
|
|
2001
|
+
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
1456
2002
|
const replica = replicas.get(room.locator);
|
|
1457
2003
|
const head = replica?.queue[0];
|
|
1458
2004
|
if (replica === undefined || head === undefined)
|
|
1459
|
-
return;
|
|
2005
|
+
return 'done';
|
|
1460
2006
|
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1461
|
-
return;
|
|
2007
|
+
return 'done';
|
|
1462
2008
|
const state = pendings.get(head.id);
|
|
1463
2009
|
const { submission, lost: earlier } = state ?? {
|
|
1464
2010
|
submission: { state: 'waiting' },
|
|
@@ -1466,24 +2012,30 @@ export function composeRuntime(options) {
|
|
|
1466
2012
|
};
|
|
1467
2013
|
if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
|
|
1468
2014
|
progress.complete = false;
|
|
1469
|
-
return;
|
|
2015
|
+
return { at: Date.parse(submission.reset) };
|
|
1470
2016
|
}
|
|
1471
2017
|
if (writesHeld()) {
|
|
1472
2018
|
progress.complete = false;
|
|
1473
2019
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1474
|
-
return;
|
|
2020
|
+
return 'done';
|
|
1475
2021
|
}
|
|
1476
2022
|
const lost = earlier || submission.state === 'submitted';
|
|
2023
|
+
if (lost && pastHorizon(head.id)) {
|
|
2024
|
+
await discardPending(room, local, signal);
|
|
2025
|
+
return 'done';
|
|
2026
|
+
}
|
|
1477
2027
|
let result;
|
|
1478
2028
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
1479
2029
|
await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
|
|
1480
2030
|
if (discoveryPaused || fences.has(room.locator))
|
|
1481
|
-
return;
|
|
1482
|
-
if (writesHeld()) {
|
|
2031
|
+
return 'done';
|
|
2032
|
+
if (signal.aborted || writesHeld()) {
|
|
1483
2033
|
await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
|
|
2034
|
+
if (signal.aborted)
|
|
2035
|
+
return 'done';
|
|
1484
2036
|
progress.complete = false;
|
|
1485
2037
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1486
|
-
return;
|
|
2038
|
+
return 'done';
|
|
1487
2039
|
}
|
|
1488
2040
|
const opensBefore = openSignals;
|
|
1489
2041
|
const answered = awaitingAnswer(room.locator);
|
|
@@ -1499,32 +2051,36 @@ export function composeRuntime(options) {
|
|
|
1499
2051
|
progress.complete = false;
|
|
1500
2052
|
if (continuing())
|
|
1501
2053
|
progress.reachable = false;
|
|
1502
|
-
return;
|
|
2054
|
+
return 'again';
|
|
2055
|
+
}
|
|
2056
|
+
if (signal.aborted) {
|
|
2057
|
+
answered();
|
|
2058
|
+
return 'done';
|
|
1503
2059
|
}
|
|
1504
2060
|
if (result.kind !== 'accepted' || !continuing())
|
|
1505
2061
|
answered();
|
|
1506
2062
|
if (!continuing())
|
|
1507
|
-
return;
|
|
2063
|
+
return 'done';
|
|
1508
2064
|
if (result.kind === 'transport-uncertainty') {
|
|
1509
2065
|
progress.complete = false;
|
|
1510
2066
|
progress.reachable = false;
|
|
1511
|
-
return;
|
|
2067
|
+
return 'again';
|
|
1512
2068
|
}
|
|
1513
2069
|
if (result.kind === 'room-not-readable') {
|
|
1514
2070
|
progress.complete = false;
|
|
1515
2071
|
unreadable.add(room.locator);
|
|
1516
2072
|
list(room, false);
|
|
1517
2073
|
emitRooms();
|
|
1518
|
-
return;
|
|
2074
|
+
return 'done';
|
|
1519
2075
|
}
|
|
1520
2076
|
if (result.kind === 'revoked-authorization') {
|
|
1521
2077
|
await refuse('revoked-authorization');
|
|
1522
|
-
return;
|
|
2078
|
+
return 'done';
|
|
1523
2079
|
}
|
|
1524
2080
|
if (result.kind === 'scoped-refusal') {
|
|
1525
2081
|
await scoped(result, room, local);
|
|
1526
2082
|
progress.complete = false;
|
|
1527
|
-
return;
|
|
2083
|
+
return 'done';
|
|
1528
2084
|
}
|
|
1529
2085
|
if (result.kind === 'write-halted') {
|
|
1530
2086
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
@@ -1533,16 +2089,24 @@ export function composeRuntime(options) {
|
|
|
1533
2089
|
continue;
|
|
1534
2090
|
haltedRefusal = true;
|
|
1535
2091
|
progress.complete = false;
|
|
1536
|
-
return;
|
|
2092
|
+
return 'done';
|
|
1537
2093
|
}
|
|
1538
2094
|
if (result.kind === 'conflicted' ||
|
|
1539
2095
|
result.kind === 'rejected' ||
|
|
1540
2096
|
result.kind === 'expired' ||
|
|
1541
2097
|
result.kind === 'retryable-refusal') {
|
|
2098
|
+
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
2099
|
+
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
2100
|
+
await discardPending(room, local, signal);
|
|
2101
|
+
return 'done';
|
|
2102
|
+
}
|
|
1542
2103
|
if (result.kind === 'expired') {
|
|
2104
|
+
await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
|
|
1543
2105
|
const withheld = await advance(room, local, authorized, continuing);
|
|
1544
|
-
if (withheld !== null)
|
|
1545
|
-
|
|
2106
|
+
if (withheld !== null) {
|
|
2107
|
+
await withhold(withheld, room, local, progress);
|
|
2108
|
+
return 'done';
|
|
2109
|
+
}
|
|
1546
2110
|
}
|
|
1547
2111
|
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
|
|
1548
2112
|
(result.kind === 'conflicted' && result.roomVersion !== undefined
|
|
@@ -1555,8 +2119,7 @@ export function composeRuntime(options) {
|
|
|
1555
2119
|
const reset = result.reset;
|
|
1556
2120
|
progress.complete = false;
|
|
1557
2121
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1558
|
-
|
|
1559
|
-
return;
|
|
2122
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
1560
2123
|
}
|
|
1561
2124
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
1562
2125
|
const left = leaving(head, result);
|
|
@@ -1567,28 +2130,73 @@ export function composeRuntime(options) {
|
|
|
1567
2130
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
1568
2131
|
return;
|
|
1569
2132
|
}
|
|
1570
|
-
const
|
|
2133
|
+
const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
1571
2134
|
leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
|
|
1572
2135
|
(result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
|
|
1573
2136
|
? { ...left, returned: null }
|
|
1574
2137
|
: left,
|
|
1575
2138
|
});
|
|
1576
|
-
publishChangeSet(
|
|
2139
|
+
publishChangeSet(settled());
|
|
1577
2140
|
moveTo({ ...status, ...attention() });
|
|
1578
2141
|
});
|
|
1579
2142
|
if (result.kind === 'expired')
|
|
1580
2143
|
continue;
|
|
1581
2144
|
const withheld = await advance(room, local, authorized, continuing);
|
|
1582
|
-
if (withheld !== null)
|
|
1583
|
-
|
|
2145
|
+
if (withheld !== null) {
|
|
2146
|
+
await withhold(withheld, room, local, progress);
|
|
2147
|
+
return 'done';
|
|
2148
|
+
}
|
|
1584
2149
|
continue;
|
|
1585
2150
|
}
|
|
1586
2151
|
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1587
2152
|
if (!kept) {
|
|
1588
2153
|
progress.complete = false;
|
|
1589
|
-
return;
|
|
2154
|
+
return 'done';
|
|
1590
2155
|
}
|
|
1591
2156
|
}
|
|
2157
|
+
return 'done';
|
|
2158
|
+
}
|
|
2159
|
+
function pastHorizon(identity) {
|
|
2160
|
+
return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
|
|
2161
|
+
}
|
|
2162
|
+
async function discardPending(room, local, signal) {
|
|
2163
|
+
const { locator } = room;
|
|
2164
|
+
const discarded = await exclusive(async () => {
|
|
2165
|
+
if (fences.has(locator))
|
|
2166
|
+
return false;
|
|
2167
|
+
const before = replicas.get(locator);
|
|
2168
|
+
const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
2169
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
2170
|
+
lastFenced.set(locator, ++fencings);
|
|
2171
|
+
try {
|
|
2172
|
+
await local.discard(locator);
|
|
2173
|
+
}
|
|
2174
|
+
finally {
|
|
2175
|
+
fences.delete(locator);
|
|
2176
|
+
}
|
|
2177
|
+
for (const batch of queue)
|
|
2178
|
+
pendings.delete(batch.id);
|
|
2179
|
+
queuesWithoutReplica.delete(locator);
|
|
2180
|
+
const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
|
|
2181
|
+
if (after !== null)
|
|
2182
|
+
replicas.set(locator, after);
|
|
2183
|
+
journal.record(`room discarded ${locator}`);
|
|
2184
|
+
publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
|
|
2185
|
+
moveTo({ ...status, ...attention() });
|
|
2186
|
+
return true;
|
|
2187
|
+
});
|
|
2188
|
+
if (!discarded)
|
|
2189
|
+
return;
|
|
2190
|
+
const progress = { complete: true, reachable: true };
|
|
2191
|
+
await roomTask(locator, async (live) => {
|
|
2192
|
+
await Promise.race([
|
|
2193
|
+
reload(room, local, progress, () => live() && !signal.aborted),
|
|
2194
|
+
abandonedOn(signal),
|
|
2195
|
+
]);
|
|
2196
|
+
});
|
|
2197
|
+
if (signal.aborted)
|
|
2198
|
+
return;
|
|
2199
|
+
await rest(progress);
|
|
1592
2200
|
}
|
|
1593
2201
|
function awaitingAnswer(locator) {
|
|
1594
2202
|
let release = () => { };
|
|
@@ -1714,15 +2322,16 @@ export function composeRuntime(options) {
|
|
|
1714
2322
|
});
|
|
1715
2323
|
}
|
|
1716
2324
|
async function synchronize(listed, local, progress, attempt, live) {
|
|
1717
|
-
const
|
|
2325
|
+
const acquired = listed.filter(acquires);
|
|
2326
|
+
const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
|
|
1718
2327
|
if (!live())
|
|
1719
2328
|
return;
|
|
1720
|
-
const ready =
|
|
2329
|
+
const ready = acquired.filter((_, index) => {
|
|
1721
2330
|
const outcome = prepared[index];
|
|
1722
2331
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1723
2332
|
});
|
|
1724
2333
|
if (attempt === 'opened') {
|
|
1725
|
-
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
2334
|
+
options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
|
|
1726
2335
|
handed = true;
|
|
1727
2336
|
}
|
|
1728
2337
|
discovering?.release();
|
|
@@ -1789,17 +2398,22 @@ export function composeRuntime(options) {
|
|
|
1789
2398
|
await caughtUp(room, local, progress, live);
|
|
1790
2399
|
}
|
|
1791
2400
|
async function reload(room, local, progress, live) {
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
2401
|
+
try {
|
|
2402
|
+
if (!live() || authorization === undefined)
|
|
2403
|
+
return;
|
|
2404
|
+
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
2405
|
+
if (!live())
|
|
2406
|
+
return;
|
|
2407
|
+
if (bootstrapped.kind !== 'staged') {
|
|
2408
|
+
await withhold(bootstrapped, room, local, progress);
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
2412
|
+
await caughtUp(room, local, progress, live);
|
|
2413
|
+
}
|
|
2414
|
+
finally {
|
|
2415
|
+
caughtUpTo(room.locator);
|
|
1800
2416
|
}
|
|
1801
|
-
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
1802
|
-
await caughtUp(room, local, progress, live);
|
|
1803
2417
|
}
|
|
1804
2418
|
async function discoverRooms(local, progress, attempt, live) {
|
|
1805
2419
|
if (closed || authorization === undefined) {
|
|
@@ -1875,6 +2489,7 @@ export function composeRuntime(options) {
|
|
|
1875
2489
|
}
|
|
1876
2490
|
const previousCapabilities = capabilities;
|
|
1877
2491
|
capabilities = result.capabilities;
|
|
2492
|
+
outcomeRetention = result.retention.outcome;
|
|
1878
2493
|
notifyReady();
|
|
1879
2494
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1880
2495
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
@@ -2004,6 +2619,8 @@ export function composeRuntime(options) {
|
|
|
2004
2619
|
const progress = underway?.generation === generation ? underway.progress : null;
|
|
2005
2620
|
const cycle = progress ?? { complete: true, reachable: true };
|
|
2006
2621
|
const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
|
|
2622
|
+
if (opened === generation)
|
|
2623
|
+
opened = null;
|
|
2007
2624
|
return task(OPENING_LANE, async (live) => {
|
|
2008
2625
|
underway = null;
|
|
2009
2626
|
if (!live())
|
|
@@ -2016,8 +2633,6 @@ export function composeRuntime(options) {
|
|
|
2016
2633
|
await roomsCarried(expired);
|
|
2017
2634
|
if (!live())
|
|
2018
2635
|
return;
|
|
2019
|
-
if (opened === generation)
|
|
2020
|
-
opened = null;
|
|
2021
2636
|
const replaced = new Set(owed);
|
|
2022
2637
|
owed.clear();
|
|
2023
2638
|
try {
|
|
@@ -2025,6 +2640,9 @@ export function composeRuntime(options) {
|
|
|
2025
2640
|
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
2026
2641
|
const reloading = held.filter((room) => !expired.includes(room.locator) &&
|
|
2027
2642
|
(replaced.has(room.locator) || revoked(room.locator)));
|
|
2643
|
+
for (const room of reloading)
|
|
2644
|
+
catching.add(room.locator);
|
|
2645
|
+
readSide.recover();
|
|
2028
2646
|
const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
|
|
2029
2647
|
const reloaded = [...(await reloads), ...(await replacing)];
|
|
2030
2648
|
if (progress.reachable) {
|
|
@@ -2069,38 +2687,94 @@ export function composeRuntime(options) {
|
|
|
2069
2687
|
function submitLater(locator) {
|
|
2070
2688
|
if (localSaves)
|
|
2071
2689
|
return;
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2690
|
+
void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
|
|
2691
|
+
}
|
|
2692
|
+
async function submitPass(locator, signal) {
|
|
2693
|
+
const room = discovered.get(locator);
|
|
2694
|
+
if (closed || stopping || discoveryPaused || room === undefined)
|
|
2695
|
+
return 'done';
|
|
2696
|
+
const progress = { complete: true, reachable: true };
|
|
2697
|
+
let next;
|
|
2698
|
+
try {
|
|
2699
|
+
await ensureAuthorization();
|
|
2700
|
+
if (authorization === undefined)
|
|
2701
|
+
return 'done';
|
|
2702
|
+
next = await submitRoom(room, await openLocal(), authorization, progress, signal);
|
|
2703
|
+
}
|
|
2704
|
+
catch {
|
|
2705
|
+
progress.reachable = false;
|
|
2706
|
+
next = 'again';
|
|
2707
|
+
}
|
|
2708
|
+
if (signal.aborted)
|
|
2709
|
+
return 'done';
|
|
2710
|
+
await rest(progress);
|
|
2711
|
+
return next === 'again' && !socketOpen ? 'done' : next;
|
|
2712
|
+
}
|
|
2713
|
+
function repairLater(locator, when) {
|
|
2714
|
+
if (!socketOpen || repairing.has(locator))
|
|
2075
2715
|
return;
|
|
2716
|
+
void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
|
|
2717
|
+
}
|
|
2718
|
+
async function repairPass(locator, signal) {
|
|
2719
|
+
const room = discovered.get(locator);
|
|
2720
|
+
if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
|
|
2721
|
+
return 'done';
|
|
2722
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2723
|
+
return 'done';
|
|
2724
|
+
const progress = { complete: true, reachable: true };
|
|
2725
|
+
repairing.add(locator);
|
|
2726
|
+
try {
|
|
2727
|
+
const local = await openLocal();
|
|
2728
|
+
await roomTask(locator, async (live) => {
|
|
2729
|
+
const current = () => live() && !signal.aborted;
|
|
2730
|
+
if (!current() || authorization === undefined)
|
|
2731
|
+
return;
|
|
2732
|
+
const withheld = await Promise.race([
|
|
2733
|
+
advance(room, local, authorization, current),
|
|
2734
|
+
abandonedOn(signal),
|
|
2735
|
+
]);
|
|
2736
|
+
if (withheld !== null && current())
|
|
2737
|
+
await withhold(withheld, room, local, progress);
|
|
2738
|
+
});
|
|
2076
2739
|
}
|
|
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
|
-
|
|
2740
|
+
catch {
|
|
2741
|
+
stale.add(locator);
|
|
2742
|
+
progress.reachable = false;
|
|
2743
|
+
}
|
|
2744
|
+
finally {
|
|
2745
|
+
repairing.delete(locator);
|
|
2746
|
+
}
|
|
2747
|
+
if (signal.aborted)
|
|
2748
|
+
return 'done';
|
|
2749
|
+
await rest(progress);
|
|
2750
|
+
if (!stale.has(locator)) {
|
|
2751
|
+
if ((replicas.get(locator)?.queue.length ?? 0) > 0)
|
|
2752
|
+
submitLater(locator);
|
|
2753
|
+
return 'done';
|
|
2754
|
+
}
|
|
2755
|
+
if (!repairable.has(locator))
|
|
2756
|
+
return 'done';
|
|
2757
|
+
const reset = repairable.get(locator);
|
|
2758
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
2759
|
+
}
|
|
2760
|
+
function resendLater() {
|
|
2761
|
+
if (!socketOpen)
|
|
2762
|
+
return;
|
|
2763
|
+
void recovery.attempt('resend', resendPass, { again: true });
|
|
2764
|
+
}
|
|
2765
|
+
async function resendPass(signal) {
|
|
2766
|
+
if (!socketOpen || closed || stopping || discoveryPaused)
|
|
2767
|
+
return 'done';
|
|
2768
|
+
const reading = { complete: true, reachable: true };
|
|
2769
|
+
try {
|
|
2770
|
+
await conversations.resend(reading);
|
|
2771
|
+
}
|
|
2772
|
+
catch {
|
|
2773
|
+
reading.reachable = false;
|
|
2774
|
+
}
|
|
2775
|
+
if (signal.aborted)
|
|
2776
|
+
return 'done';
|
|
2777
|
+
return reading.reachable && reading.complete ? 'done' : 'again';
|
|
2104
2778
|
}
|
|
2105
2779
|
function submitAll() {
|
|
2106
2780
|
for (const [locator, replica] of replicas) {
|
|
@@ -2115,15 +2789,21 @@ export function composeRuntime(options) {
|
|
|
2115
2789
|
return;
|
|
2116
2790
|
}
|
|
2117
2791
|
submitAll();
|
|
2792
|
+
for (const locator of stale)
|
|
2793
|
+
if (repairable.has(locator))
|
|
2794
|
+
repairLater(locator, {});
|
|
2118
2795
|
await conversations.reconcile();
|
|
2119
2796
|
if (status.providerUnreachable)
|
|
2120
2797
|
conversations.reconnected();
|
|
2121
2798
|
const reading = { complete: true, reachable: true };
|
|
2122
|
-
|
|
2799
|
+
if (demand.conversations)
|
|
2800
|
+
await conversations.catchUp(reading);
|
|
2123
2801
|
if (progress.reachable && reading.reachable)
|
|
2124
2802
|
await conversations.resend(reading);
|
|
2125
|
-
if (!reading.reachable)
|
|
2803
|
+
if (!reading.reachable) {
|
|
2126
2804
|
progress.reachable = false;
|
|
2805
|
+
resendLater();
|
|
2806
|
+
}
|
|
2127
2807
|
}
|
|
2128
2808
|
catch {
|
|
2129
2809
|
progress.complete = false;
|
|
@@ -2180,7 +2860,7 @@ export function composeRuntime(options) {
|
|
|
2180
2860
|
return;
|
|
2181
2861
|
const room = discovered.get(locator);
|
|
2182
2862
|
const held = replicas.get(locator);
|
|
2183
|
-
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2863
|
+
if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
|
|
2184
2864
|
return;
|
|
2185
2865
|
if (held !== undefined &&
|
|
2186
2866
|
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
@@ -2232,6 +2912,8 @@ export function composeRuntime(options) {
|
|
|
2232
2912
|
if (event.kind === 'opened') {
|
|
2233
2913
|
socketOpen = true;
|
|
2234
2914
|
generation += 1;
|
|
2915
|
+
conversations.reopened();
|
|
2916
|
+
recovery.resume();
|
|
2235
2917
|
void sync('opened');
|
|
2236
2918
|
}
|
|
2237
2919
|
else if (event.kind === 'closed')
|
|
@@ -2255,6 +2937,7 @@ export function composeRuntime(options) {
|
|
|
2255
2937
|
haltedRefusal = false;
|
|
2256
2938
|
if (mode !== serviceMode) {
|
|
2257
2939
|
serviceMode = mode;
|
|
2940
|
+
queueReadiness();
|
|
2258
2941
|
journal.record(`service mode ${mode}`);
|
|
2259
2942
|
for (const listener of serviceModeListeners)
|
|
2260
2943
|
listener(mode);
|
|
@@ -2267,6 +2950,8 @@ export function composeRuntime(options) {
|
|
|
2267
2950
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
2268
2951
|
return;
|
|
2269
2952
|
withdrawConversations = stream.observe((event) => {
|
|
2953
|
+
if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
|
|
2954
|
+
queueReadiness();
|
|
2270
2955
|
if (conversations.receive(event)) {
|
|
2271
2956
|
void ready.then(scheduleCurrency);
|
|
2272
2957
|
return;
|
|
@@ -2289,7 +2974,7 @@ export function composeRuntime(options) {
|
|
|
2289
2974
|
if (!live())
|
|
2290
2975
|
return;
|
|
2291
2976
|
await conversations.reconcile();
|
|
2292
|
-
if (conversations.current()) {
|
|
2977
|
+
if (conversations.current() || !demand.conversations) {
|
|
2293
2978
|
show();
|
|
2294
2979
|
return;
|
|
2295
2980
|
}
|
|
@@ -2307,13 +2992,18 @@ export function composeRuntime(options) {
|
|
|
2307
2992
|
if (closed)
|
|
2308
2993
|
return;
|
|
2309
2994
|
closed = true;
|
|
2995
|
+
if (narrowing !== null)
|
|
2996
|
+
clearTimeout(narrowing);
|
|
2997
|
+
if (rechecking !== null)
|
|
2998
|
+
clearInterval(rechecking);
|
|
2999
|
+
readinessWatches.clear();
|
|
2310
3000
|
withdrawStream?.();
|
|
2311
3001
|
withdrawStream = undefined;
|
|
2312
3002
|
withdrawConversations?.();
|
|
2313
3003
|
withdrawConversations = undefined;
|
|
2314
3004
|
withdrawServiceMode?.();
|
|
2315
3005
|
conversations.close();
|
|
2316
|
-
|
|
3006
|
+
void recovery.retire();
|
|
2317
3007
|
store?.close();
|
|
2318
3008
|
void opening?.then((opened) => {
|
|
2319
3009
|
opened.close();
|
|
@@ -2331,9 +3021,8 @@ export function composeRuntime(options) {
|
|
|
2331
3021
|
}
|
|
2332
3022
|
async function stop() {
|
|
2333
3023
|
stopping = true;
|
|
2334
|
-
|
|
3024
|
+
await recovery.retire();
|
|
2335
3025
|
await Promise.all(working);
|
|
2336
|
-
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
2337
3026
|
await mutations;
|
|
2338
3027
|
close();
|
|
2339
3028
|
}
|
|
@@ -2355,17 +3044,54 @@ export function composeRuntime(options) {
|
|
|
2355
3044
|
return planOf(input, roomFacts(reach), planReach(reach));
|
|
2356
3045
|
}
|
|
2357
3046
|
function watchPlan(plan, deliver) {
|
|
2358
|
-
|
|
2359
|
-
|
|
3047
|
+
return readSide.watch(plan, identityOf(plan), () => {
|
|
3048
|
+
const cover = covered(plan);
|
|
3049
|
+
awaitPreparation(cover);
|
|
3050
|
+
return cover;
|
|
3051
|
+
}, deliver);
|
|
2360
3052
|
}
|
|
2361
3053
|
async function searched(input, reach) {
|
|
2362
3054
|
const plan = searchPlanOf(input);
|
|
2363
|
-
const
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
3055
|
+
const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
|
|
3056
|
+
for (;;) {
|
|
3057
|
+
const cover = covered(filter);
|
|
3058
|
+
awaitPreparation(cover);
|
|
3059
|
+
const pending = 'pending' in cover ? [...cover.pending] : [];
|
|
3060
|
+
const covering = 'pending' in cover ? [] : cover.coverage.rooms;
|
|
3061
|
+
const unindexed = covering
|
|
3062
|
+
.filter(({ room }) => !searchIndex.rooms().has(room))
|
|
3063
|
+
.map(({ room }) => room);
|
|
3064
|
+
for (const room of unindexed)
|
|
3065
|
+
pending.push({ room, awaits: 'index' });
|
|
3066
|
+
const held = conversations.searchable();
|
|
3067
|
+
const named = reach !== null && !reach.conversations
|
|
3068
|
+
? []
|
|
3069
|
+
: (plan.conversations ?? held).filter((one) => held.includes(one) && conversations.acquired(one));
|
|
3070
|
+
const waiting = named.filter((one) => !searchIndex.conversations().has(one));
|
|
3071
|
+
for (const conversation of waiting)
|
|
3072
|
+
pending.push({ conversation, awaits: 'index' });
|
|
3073
|
+
if (unindexed.length > 0 || waiting.length > 0)
|
|
3074
|
+
index(unindexed, waiting);
|
|
3075
|
+
if (pending.length > 0 || 'pending' in cover)
|
|
3076
|
+
return { pending };
|
|
3077
|
+
const seen = searchIndex.changes();
|
|
3078
|
+
await searchIndex.caughtUp();
|
|
3079
|
+
const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(filter);
|
|
3080
|
+
if (selected !== null && 'pending' in selected)
|
|
3081
|
+
return selected;
|
|
3082
|
+
if (searchIndex.changes() !== seen ||
|
|
3083
|
+
JSON.stringify(covered(filter)) !== JSON.stringify(cover))
|
|
3084
|
+
continue;
|
|
3085
|
+
return {
|
|
3086
|
+
...searchIndex.search(plan, selected === null ? null : selected.documents, named),
|
|
3087
|
+
coverage: {
|
|
3088
|
+
rooms: covering,
|
|
3089
|
+
conversations: named,
|
|
3090
|
+
index: searchIndex.generation(),
|
|
3091
|
+
overlay: cover.coverage.overlay,
|
|
3092
|
+
},
|
|
3093
|
+
};
|
|
3094
|
+
}
|
|
2369
3095
|
}
|
|
2370
3096
|
const watch = (topic, input, deliver) => {
|
|
2371
3097
|
if (topic === 'room-drafts') {
|
|
@@ -2411,6 +3137,9 @@ export function composeRuntime(options) {
|
|
|
2411
3137
|
presentationListeners.delete(listener);
|
|
2412
3138
|
};
|
|
2413
3139
|
}
|
|
3140
|
+
if (topic === 'readiness') {
|
|
3141
|
+
return watchReadiness({ target: '', code: 'stored', capacity: null }, null, deliver);
|
|
3142
|
+
}
|
|
2414
3143
|
if (topic === 'rooms') {
|
|
2415
3144
|
roomListeners.add(deliver);
|
|
2416
3145
|
deliver(listed());
|
|
@@ -2577,7 +3306,13 @@ export function composeRuntime(options) {
|
|
|
2577
3306
|
return journal.export();
|
|
2578
3307
|
},
|
|
2579
3308
|
persistence() {
|
|
2580
|
-
return
|
|
3309
|
+
return persistence === 'granted';
|
|
3310
|
+
},
|
|
3311
|
+
async retry() {
|
|
3312
|
+
journal.record('retry');
|
|
3313
|
+
unindexable.clear();
|
|
3314
|
+
queueReadiness();
|
|
3315
|
+
resume();
|
|
2581
3316
|
},
|
|
2582
3317
|
observe: observing(watch),
|
|
2583
3318
|
async reauthorize() {
|
|
@@ -2604,6 +3339,22 @@ export function composeRuntime(options) {
|
|
|
2604
3339
|
query: (plan) => rowsQuery(plan),
|
|
2605
3340
|
watchPlan,
|
|
2606
3341
|
search: searched,
|
|
3342
|
+
searching,
|
|
3343
|
+
watchReadiness,
|
|
3344
|
+
};
|
|
3345
|
+
return {
|
|
3346
|
+
md,
|
|
3347
|
+
stop,
|
|
3348
|
+
lose: loseRooms,
|
|
3349
|
+
watch,
|
|
3350
|
+
acquire,
|
|
3351
|
+
persisted(answer) {
|
|
3352
|
+
if (answer === persistence)
|
|
3353
|
+
return;
|
|
3354
|
+
persistence = answer;
|
|
3355
|
+
journal.record(`persistence ${answer}`);
|
|
3356
|
+
queueReadiness();
|
|
3357
|
+
},
|
|
3358
|
+
within: (reach, port) => narrowed(reachable, reach, port),
|
|
2607
3359
|
};
|
|
2608
|
-
return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2609
3360
|
}
|