@agentshouse/mdruntime 0.1.0-ca9afacd5aebd → 0.1.0-cb0eb0d87ff49
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 +114 -123
- package/dist/adapter.d.ts +26 -3
- package/dist/adapter.d.ts.map +1 -1
- package/dist/attempt.d.ts.map +1 -1
- package/dist/attempt.js +14 -1
- package/dist/bootstrap.d.ts +2 -2
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +28 -6
- 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/chat/acts.d.ts +3 -4
- package/dist/chat/acts.d.ts.map +1 -1
- package/dist/chat/acts.js +10 -10
- package/dist/chat/list.d.ts +6 -0
- package/dist/chat/list.d.ts.map +1 -1
- package/dist/chat/list.js +6 -3
- package/dist/chat/markdown.d.ts +2 -1
- package/dist/chat/markdown.d.ts.map +1 -1
- package/dist/chat/markdown.js +7 -6
- package/dist/chat/message.d.ts.map +1 -1
- package/dist/chat/message.js +2 -2
- package/dist/chat/provider.d.ts.map +1 -1
- package/dist/chat/provider.js +25 -50
- package/dist/chat/ready.d.ts.map +1 -1
- package/dist/chat/ready.js +2 -2
- package/dist/chat/style.js +3 -3
- package/dist/client.d.ts +7 -7
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1 -1
- 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 +8 -5
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +858 -301
- package/dist/conversation.d.ts +12 -7
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +258 -130
- package/dist/entry.d.ts +17 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +156 -0
- package/dist/fence.d.ts +6 -0
- package/dist/fence.d.ts.map +1 -0
- package/dist/fence.js +10 -0
- package/dist/host.d.ts +4 -2
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +9 -5
- 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/index.js +1 -0
- package/dist/memory.d.ts +1 -0
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +10 -1
- 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/outcome.d.ts +4 -0
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +1 -0
- package/dist/overlay.d.ts +1 -0
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +33 -54
- package/dist/query.d.ts +28 -3
- package/dist/query.d.ts.map +1 -1
- package/dist/reach.js +2 -2
- package/dist/react.d.ts +3 -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/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 +49 -32
- 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 +6 -2
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +42 -18
- package/dist/search.d.ts +28 -6
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +109 -44
- package/dist/shared.d.ts +11 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +368 -0
- package/dist/store.d.ts +35 -10
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +275 -74
- package/dist/turn.d.ts +7 -0
- package/dist/turn.d.ts.map +1 -0
- package/dist/turn.js +45 -0
- package/package.json +12 -8
package/dist/connection.js
CHANGED
|
@@ -6,21 +6,25 @@ 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
|
-
import { absentBinary, binaryAnswer, calledResult, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
|
|
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';
|
|
23
|
-
import {
|
|
24
|
+
import { FencedMap } from './fence.js';
|
|
25
|
+
import { tally } from './shared.js';
|
|
26
|
+
import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
|
|
27
|
+
import { identityTime, roomDraftRevision } from './identity.js';
|
|
24
28
|
import { beneath, normalize } from './paths.js';
|
|
25
29
|
import { openRuntimeStore, } from './store.js';
|
|
26
30
|
import { unconfirmed } from './transfer.js';
|
|
@@ -38,23 +42,59 @@ function declaredSelection(room) {
|
|
|
38
42
|
root !== '' &&
|
|
39
43
|
selection.roots.every((other, at) => at === index || !within(other, root)));
|
|
40
44
|
}
|
|
45
|
+
const OPENING_LANE = 'opening';
|
|
46
|
+
const CONVERSATION_LANE = 'conversations';
|
|
47
|
+
const ROOM_READS = 256;
|
|
48
|
+
const ROOM_ATTEMPTS = 16;
|
|
49
|
+
const outside = new Map();
|
|
50
|
+
function outsideOf(roots) {
|
|
51
|
+
const key = JSON.stringify(roots);
|
|
52
|
+
const held = outside.get(key);
|
|
53
|
+
if (held !== undefined)
|
|
54
|
+
return held;
|
|
55
|
+
const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
|
|
56
|
+
const made = {
|
|
57
|
+
documents: (path) => (markdownPath(path) ? counted(path) : 0),
|
|
58
|
+
binaries: counted,
|
|
59
|
+
};
|
|
60
|
+
outside.set(key, made);
|
|
61
|
+
return made;
|
|
62
|
+
}
|
|
63
|
+
function abandonedOn(signal) {
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
if (signal.aborted)
|
|
66
|
+
resolve(null);
|
|
67
|
+
else
|
|
68
|
+
signal.addEventListener('abort', () => resolve(null), { once: true });
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function roomLane(locator) {
|
|
72
|
+
return `room ${locator}`;
|
|
73
|
+
}
|
|
41
74
|
export function composeRuntime(options) {
|
|
42
75
|
const localSaves = options.adapter.localSaves === true;
|
|
43
76
|
let authorization = options.authorization;
|
|
44
77
|
let opening;
|
|
45
78
|
let store;
|
|
46
|
-
const
|
|
47
|
-
const
|
|
79
|
+
const fences = new Map();
|
|
80
|
+
const isFenced = (locator) => fences.has(locator);
|
|
81
|
+
let fencings = 0;
|
|
82
|
+
const lastFenced = new Map();
|
|
83
|
+
const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
|
|
84
|
+
let discoveries = 0;
|
|
85
|
+
const replicas = new FencedMap(isFenced);
|
|
86
|
+
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
48
87
|
const pendings = new Map();
|
|
49
|
-
const roomDrafts = new
|
|
88
|
+
const roomDrafts = new FencedMap(isFenced);
|
|
50
89
|
const presentations = new Map();
|
|
51
90
|
let limits = [];
|
|
52
91
|
let capabilities = [];
|
|
53
|
-
const rooms = new
|
|
92
|
+
const rooms = new FencedMap(isFenced);
|
|
54
93
|
const counted = new Map();
|
|
55
94
|
const unreadable = new Set();
|
|
56
|
-
const
|
|
57
|
-
const
|
|
95
|
+
const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
|
|
96
|
+
const models = new FencedMap(isModelFenced);
|
|
97
|
+
const liveModels = new FencedMap(isModelFenced);
|
|
58
98
|
const invalidModels = new Set();
|
|
59
99
|
const discovered = new Map();
|
|
60
100
|
let roomsListed = false;
|
|
@@ -84,7 +124,6 @@ export function composeRuntime(options) {
|
|
|
84
124
|
let status = STARTING;
|
|
85
125
|
let shown = STARTING;
|
|
86
126
|
let discoveryPaused = false;
|
|
87
|
-
let resetTimer;
|
|
88
127
|
let closed = false;
|
|
89
128
|
let stopping = false;
|
|
90
129
|
let withdrawStream;
|
|
@@ -92,12 +131,21 @@ export function composeRuntime(options) {
|
|
|
92
131
|
let underway = null;
|
|
93
132
|
let streamed = null;
|
|
94
133
|
let handed = false;
|
|
95
|
-
const
|
|
134
|
+
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
135
|
+
const repairing = new Set();
|
|
136
|
+
const repairable = new Map();
|
|
137
|
+
let outcomeRetention = null;
|
|
96
138
|
const answering = new Map();
|
|
97
139
|
let complete = false;
|
|
98
140
|
let recorded = false;
|
|
99
141
|
const stale = new Set();
|
|
100
|
-
let
|
|
142
|
+
let generation = 0;
|
|
143
|
+
let discovering = null;
|
|
144
|
+
let opened = null;
|
|
145
|
+
const owed = new Set();
|
|
146
|
+
const lanes = new Map();
|
|
147
|
+
let roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
148
|
+
const working = new Set();
|
|
101
149
|
let mutations = Promise.resolve();
|
|
102
150
|
const roomListeners = new Set();
|
|
103
151
|
const refusalListeners = new Set();
|
|
@@ -108,7 +156,7 @@ export function composeRuntime(options) {
|
|
|
108
156
|
const presentationListeners = new Set();
|
|
109
157
|
const readSide = createReadSide();
|
|
110
158
|
let withdrawConversations;
|
|
111
|
-
let currencyQueued =
|
|
159
|
+
let currencyQueued = null;
|
|
112
160
|
let socketOpen = false;
|
|
113
161
|
let serviceMode = 'open';
|
|
114
162
|
let haltedRefusal = false;
|
|
@@ -116,24 +164,31 @@ export function composeRuntime(options) {
|
|
|
116
164
|
const serviceModeListeners = new Set();
|
|
117
165
|
let member = null;
|
|
118
166
|
const memberListeners = new Set();
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
const
|
|
167
|
+
const prepared = new Set();
|
|
168
|
+
const projecting = new Set();
|
|
169
|
+
const demanded = new Set();
|
|
170
|
+
let preparing = false;
|
|
171
|
+
const catching = new Set();
|
|
172
|
+
const transfers = new FencedMap(isFenced);
|
|
122
173
|
const conversations = createConversationStore({
|
|
123
174
|
agents: declaredAgents,
|
|
124
|
-
documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
|
|
175
|
+
documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
|
|
125
176
|
call: (name, input) => invokeAction(name, input),
|
|
126
177
|
durably,
|
|
127
178
|
stream: () => options.adapter.conversations,
|
|
128
179
|
publish: publishChangeSet,
|
|
180
|
+
online: () => !status.providerUnreachable && refusal === null,
|
|
129
181
|
closed: () => closed,
|
|
182
|
+
unanswered: resendLater,
|
|
183
|
+
expired: (operation) => pastHorizon(operation),
|
|
130
184
|
});
|
|
131
185
|
const searchIndex = createSearchIndex({
|
|
132
|
-
|
|
186
|
+
room: (room) => readSide.room(room),
|
|
133
187
|
document: (room, path) => readSide.document(room, path),
|
|
134
|
-
messages: () => conversations.messages(),
|
|
135
|
-
message: (conversation, identity) => conversations.message(conversation, identity),
|
|
136
188
|
});
|
|
189
|
+
const roomsToIndex = new Set();
|
|
190
|
+
const conversationsToIndex = new Set();
|
|
191
|
+
let indexing = false;
|
|
137
192
|
const journal = createLog();
|
|
138
193
|
let notifyReady = () => { };
|
|
139
194
|
const ready = new Promise((resolve) => {
|
|
@@ -233,12 +288,9 @@ export function composeRuntime(options) {
|
|
|
233
288
|
if (replica === undefined)
|
|
234
289
|
return 0;
|
|
235
290
|
const roots = replica.confirmed.record.typedSelection?.roots ?? [];
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
count += 1;
|
|
240
|
-
}
|
|
241
|
-
return count;
|
|
291
|
+
const weighed = outsideOf(roots);
|
|
292
|
+
return (tally(replica.visible.documents, weighed.documents) +
|
|
293
|
+
tally(replica.visible.binaries, weighed.binaries));
|
|
242
294
|
}
|
|
243
295
|
function listed() {
|
|
244
296
|
return structuredClone([...rooms.values()].map((room) => {
|
|
@@ -265,6 +317,7 @@ export function composeRuntime(options) {
|
|
|
265
317
|
return { listed: roomsListed, rooms: held };
|
|
266
318
|
}
|
|
267
319
|
function emitRooms() {
|
|
320
|
+
readSide.recover();
|
|
268
321
|
const value = listed();
|
|
269
322
|
for (const listener of roomListeners)
|
|
270
323
|
listener(value);
|
|
@@ -319,42 +372,186 @@ export function composeRuntime(options) {
|
|
|
319
372
|
}
|
|
320
373
|
}
|
|
321
374
|
function applyChangeSet(set) {
|
|
322
|
-
const held =
|
|
323
|
-
|
|
324
|
-
|
|
375
|
+
const held = {
|
|
376
|
+
...set,
|
|
377
|
+
documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
|
|
378
|
+
};
|
|
325
379
|
readSide.apply(held);
|
|
326
380
|
searchIndex.changed(held);
|
|
327
381
|
}
|
|
328
|
-
function
|
|
329
|
-
|
|
382
|
+
async function referenced(locator) {
|
|
383
|
+
for (;;) {
|
|
384
|
+
const record = replicas.get(locator)?.confirmed.record;
|
|
385
|
+
if (record === undefined || closed)
|
|
386
|
+
return;
|
|
387
|
+
const held = { text: record.documents, binaries: record.binaries };
|
|
388
|
+
const composition = compositionOf(record);
|
|
389
|
+
if (referencesIndexed(held, composition))
|
|
390
|
+
return;
|
|
391
|
+
await indexReferrers(held, composition);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
async function prepareReplica(locator) {
|
|
395
|
+
await referenced(locator);
|
|
396
|
+
const replica = replicas.get(locator);
|
|
397
|
+
if (replica === undefined || closed || prepared.has(locator))
|
|
330
398
|
return;
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
399
|
+
const visible = replica.visible;
|
|
400
|
+
const paths = [
|
|
401
|
+
...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
|
|
402
|
+
];
|
|
403
|
+
const entered = [];
|
|
404
|
+
projecting.add(locator);
|
|
405
|
+
try {
|
|
406
|
+
await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
|
|
407
|
+
const current = replicas.get(locator);
|
|
408
|
+
if (current === undefined || closed)
|
|
409
|
+
return;
|
|
410
|
+
const set = entering(locator, current, chunk);
|
|
411
|
+
readSide.apply(set);
|
|
412
|
+
entered.push(set);
|
|
413
|
+
});
|
|
414
|
+
if (!replicas.has(locator) || closed)
|
|
415
|
+
return;
|
|
416
|
+
prepared.add(locator);
|
|
341
417
|
}
|
|
342
|
-
|
|
418
|
+
finally {
|
|
419
|
+
projecting.delete(locator);
|
|
420
|
+
}
|
|
421
|
+
conversations.changed(mergeChangeSets(entered));
|
|
422
|
+
readSide.recover();
|
|
423
|
+
}
|
|
424
|
+
function unprepared() {
|
|
425
|
+
return [...demanded, ...replicas.keys()].find((locator) => !prepared.has(locator));
|
|
426
|
+
}
|
|
427
|
+
function prepare(locators = []) {
|
|
428
|
+
for (const locator of locators)
|
|
429
|
+
demanded.add(locator);
|
|
430
|
+
if (preparing)
|
|
343
431
|
return;
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
432
|
+
preparing = true;
|
|
433
|
+
void (async () => {
|
|
434
|
+
try {
|
|
435
|
+
for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
|
|
436
|
+
await yielded();
|
|
437
|
+
if (closed)
|
|
438
|
+
return;
|
|
439
|
+
demanded.delete(waiting);
|
|
440
|
+
await prepareReplica(waiting);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
finally {
|
|
444
|
+
preparing = false;
|
|
445
|
+
}
|
|
446
|
+
})();
|
|
347
447
|
}
|
|
348
448
|
function reachedRooms(plan) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
449
|
+
const reach = plan.reach;
|
|
450
|
+
const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
451
|
+
const named = plan.rooms ??
|
|
452
|
+
(reached === null
|
|
453
|
+
? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
|
|
454
|
+
: [...reached]);
|
|
455
|
+
return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
|
|
456
|
+
}
|
|
457
|
+
function awaited(locator) {
|
|
458
|
+
return rooms.has(locator) || !roomsListed;
|
|
459
|
+
}
|
|
460
|
+
function covered(plan) {
|
|
461
|
+
const locators = reachedRooms(plan);
|
|
462
|
+
if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
|
|
463
|
+
return { pending: [] };
|
|
464
|
+
}
|
|
465
|
+
const pending = [];
|
|
466
|
+
const coverage = [];
|
|
467
|
+
const overlay = [];
|
|
468
|
+
for (const locator of locators) {
|
|
469
|
+
const replica = replicas.get(locator);
|
|
470
|
+
if (replica === undefined) {
|
|
471
|
+
if (awaited(locator))
|
|
472
|
+
pending.push({ room: locator, awaits: 'acquisition' });
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
if (!prepared.has(locator)) {
|
|
476
|
+
pending.push({ room: locator, awaits: 'preparation' });
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
coverage.push({
|
|
480
|
+
room: locator,
|
|
481
|
+
generation: replica.confirmed.record.transfer,
|
|
482
|
+
current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
|
|
483
|
+
});
|
|
484
|
+
for (const batch of replica.queue)
|
|
485
|
+
overlay.push(batch.id);
|
|
486
|
+
}
|
|
487
|
+
if (pending.length > 0)
|
|
488
|
+
return { pending };
|
|
489
|
+
return { coverage: { rooms: coverage, indexes: [], overlay } };
|
|
354
490
|
}
|
|
355
|
-
function
|
|
356
|
-
|
|
357
|
-
|
|
491
|
+
function awaitPreparation(cover) {
|
|
492
|
+
if (!('pending' in cover))
|
|
493
|
+
return;
|
|
494
|
+
const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
|
|
495
|
+
if (waiting.length > 0)
|
|
496
|
+
prepare(waiting.map(({ room }) => room));
|
|
497
|
+
}
|
|
498
|
+
async function rowsQuery(plan) {
|
|
499
|
+
const before = covered(plan);
|
|
500
|
+
awaitPreparation(before);
|
|
501
|
+
if ('pending' in before)
|
|
502
|
+
return before;
|
|
503
|
+
const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
|
|
504
|
+
awaitPreparation(cover);
|
|
505
|
+
if ('pending' in cover)
|
|
506
|
+
return cover;
|
|
507
|
+
return 'groups' in rows
|
|
508
|
+
? { groups: rows.groups, coverage: cover.coverage }
|
|
509
|
+
: { documents: rows.documents, coverage: cover.coverage };
|
|
510
|
+
}
|
|
511
|
+
async function indexConversation(conversation) {
|
|
512
|
+
searchIndex.indexMessages(conversation, [], false);
|
|
513
|
+
let after = null;
|
|
514
|
+
for (;;) {
|
|
515
|
+
const batch = await durably((local) => local.messagesAfter(conversation, after));
|
|
516
|
+
if (closed)
|
|
517
|
+
return;
|
|
518
|
+
searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
|
|
519
|
+
if (batch.length === 0)
|
|
520
|
+
return;
|
|
521
|
+
after = batch[batch.length - 1].identity;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
function indexNext() {
|
|
525
|
+
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator));
|
|
526
|
+
if (room !== undefined) {
|
|
527
|
+
roomsToIndex.delete(room);
|
|
528
|
+
return () => (prepared.has(room) ? searchIndex.indexRoom(room) : undefined);
|
|
529
|
+
}
|
|
530
|
+
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
531
|
+
if (conversation === undefined)
|
|
532
|
+
return null;
|
|
533
|
+
conversationsToIndex.delete(conversation);
|
|
534
|
+
return () => indexConversation(conversation);
|
|
535
|
+
}
|
|
536
|
+
function index(rooms, named) {
|
|
537
|
+
for (const room of rooms)
|
|
538
|
+
roomsToIndex.add(room);
|
|
539
|
+
for (const conversation of named)
|
|
540
|
+
conversationsToIndex.add(conversation);
|
|
541
|
+
if (indexing)
|
|
542
|
+
return;
|
|
543
|
+
indexing = true;
|
|
544
|
+
void (async () => {
|
|
545
|
+
try {
|
|
546
|
+
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
547
|
+
await yielded();
|
|
548
|
+
await unit();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
finally {
|
|
552
|
+
indexing = false;
|
|
553
|
+
}
|
|
554
|
+
})().catch(() => { });
|
|
358
555
|
}
|
|
359
556
|
function publishChangeSet(set) {
|
|
360
557
|
applyChangeSet(set);
|
|
@@ -371,34 +568,7 @@ export function composeRuntime(options) {
|
|
|
371
568
|
emitPresentation(app);
|
|
372
569
|
conversations.changed(set);
|
|
373
570
|
}
|
|
374
|
-
function stopTimer() {
|
|
375
|
-
if (resetTimer === undefined)
|
|
376
|
-
return;
|
|
377
|
-
clearTimeout(resetTimer);
|
|
378
|
-
resetTimer = undefined;
|
|
379
|
-
}
|
|
380
|
-
function armTimer() {
|
|
381
|
-
stopTimer();
|
|
382
|
-
if (closed || stopping)
|
|
383
|
-
return;
|
|
384
|
-
let earliest = null;
|
|
385
|
-
for (const { submission } of pendings.values()) {
|
|
386
|
-
if (submission.state !== 'reset')
|
|
387
|
-
continue;
|
|
388
|
-
const at = Date.parse(submission.reset);
|
|
389
|
-
if (earliest === null || at < earliest)
|
|
390
|
-
earliest = at;
|
|
391
|
-
}
|
|
392
|
-
if (earliest === null)
|
|
393
|
-
return;
|
|
394
|
-
// A Save House held back goes again only once the reset its answer named has passed.
|
|
395
|
-
resetTimer = setTimeout(() => {
|
|
396
|
-
resetTimer = undefined;
|
|
397
|
-
submitAll();
|
|
398
|
-
}, Math.max(0, earliest - Date.now()));
|
|
399
|
-
}
|
|
400
571
|
async function refuse(reason, provider = null) {
|
|
401
|
-
stopTimer();
|
|
402
572
|
discoveryPaused = true;
|
|
403
573
|
try {
|
|
404
574
|
if (reason === 'revoked-authorization') {
|
|
@@ -477,14 +647,24 @@ export function composeRuntime(options) {
|
|
|
477
647
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
478
648
|
});
|
|
479
649
|
}
|
|
480
|
-
async function applyDiscovery(
|
|
650
|
+
async function applyDiscovery(found, started) {
|
|
651
|
+
for (const room of found) {
|
|
652
|
+
const fence = fences.get(room.locator);
|
|
653
|
+
if (fence !== undefined && fence.erased && fence.at < started)
|
|
654
|
+
fences.delete(room.locator);
|
|
655
|
+
}
|
|
656
|
+
const listed = found.filter((room) => !fences.has(room.locator));
|
|
481
657
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
482
658
|
for (const locator of listedLocators)
|
|
483
659
|
scopedRefusals.delete(locator);
|
|
484
660
|
discovered.clear();
|
|
485
661
|
for (const room of listed)
|
|
486
662
|
discovered.set(room.locator, room);
|
|
487
|
-
|
|
663
|
+
const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
|
|
664
|
+
if (options.adapter.exhaustiveDiscovery === true)
|
|
665
|
+
await loseRooms(unlisted);
|
|
666
|
+
else
|
|
667
|
+
unlistedKept(unlisted);
|
|
488
668
|
for (const room of listed) {
|
|
489
669
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
490
670
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -495,6 +675,7 @@ export function composeRuntime(options) {
|
|
|
495
675
|
}
|
|
496
676
|
roomsListed = true;
|
|
497
677
|
emitRooms();
|
|
678
|
+
return listed;
|
|
498
679
|
}
|
|
499
680
|
function unlistedHeld(listed) {
|
|
500
681
|
return [
|
|
@@ -506,41 +687,71 @@ export function composeRuntime(options) {
|
|
|
506
687
|
]),
|
|
507
688
|
].filter((locator) => !listed.has(locator));
|
|
508
689
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
|
|
516
|
-
room: true,
|
|
517
|
-
queue: true,
|
|
518
|
-
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
519
|
-
}));
|
|
520
|
-
for (const locator of unlisted) {
|
|
521
|
-
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
522
|
-
pendings.delete(batch.id);
|
|
523
|
-
replicas.delete(locator);
|
|
524
|
-
queuesWithoutReplica.delete(locator);
|
|
690
|
+
function unlistedKept(unlisted) {
|
|
691
|
+
for (const locator of unlisted) {
|
|
692
|
+
const held = replicas.get(locator)?.confirmed.record;
|
|
693
|
+
if (held !== undefined)
|
|
694
|
+
list(held, false);
|
|
695
|
+
else
|
|
525
696
|
rooms.delete(locator);
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
function fence(locators) {
|
|
700
|
+
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
701
|
+
const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
|
|
702
|
+
room: true,
|
|
703
|
+
queue: true,
|
|
704
|
+
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
705
|
+
}));
|
|
706
|
+
for (const locator of fresh) {
|
|
707
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
708
|
+
lastFenced.set(locator, ++fencings);
|
|
709
|
+
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
710
|
+
pendings.delete(batch.id);
|
|
711
|
+
replicas.delete(locator);
|
|
712
|
+
queuesWithoutReplica.delete(locator);
|
|
713
|
+
rooms.delete(locator);
|
|
714
|
+
roomDrafts.delete(locator);
|
|
715
|
+
transfers.delete(locator);
|
|
716
|
+
catching.delete(locator);
|
|
717
|
+
discovered.delete(locator);
|
|
718
|
+
announced.delete(locator);
|
|
719
|
+
unreadable.delete(locator);
|
|
720
|
+
stale.delete(locator);
|
|
721
|
+
repairable.delete(locator);
|
|
722
|
+
scopedRefusals.delete(locator);
|
|
723
|
+
sourceRefusals.delete(locator);
|
|
724
|
+
declarationRefusals.delete(locator);
|
|
725
|
+
for (const attached of ['collection', 'typed-selection']) {
|
|
726
|
+
const key = modelKey(locator, attached);
|
|
727
|
+
models.delete(key);
|
|
728
|
+
liveModels.delete(key);
|
|
729
|
+
invalidModels.delete(key);
|
|
540
730
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
731
|
+
journal.record(`room lost ${locator}`);
|
|
732
|
+
}
|
|
733
|
+
if (fresh.length === 0)
|
|
734
|
+
return fresh;
|
|
735
|
+
publishChangeSet(mergeChangeSets(removed));
|
|
736
|
+
for (const locator of fresh)
|
|
737
|
+
prepared.delete(locator);
|
|
738
|
+
searchIndex.forget(fresh);
|
|
739
|
+
readSide.recover();
|
|
740
|
+
moveTo({ ...status, ...attention() });
|
|
741
|
+
return fresh;
|
|
742
|
+
}
|
|
743
|
+
async function erased(local, locators) {
|
|
744
|
+
await local.erase(locators);
|
|
745
|
+
for (const locator of locators) {
|
|
746
|
+
const held = fences.get(locator);
|
|
747
|
+
if (held !== undefined)
|
|
748
|
+
held.erased = true;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
async function loseRooms(locators) {
|
|
752
|
+
const fresh = fence(locators);
|
|
753
|
+
if (fresh.length > 0)
|
|
754
|
+
await durably((local) => erased(local, fresh));
|
|
544
755
|
}
|
|
545
756
|
function restored(key, persisted) {
|
|
546
757
|
const prior = models.get(key);
|
|
@@ -635,6 +846,7 @@ export function composeRuntime(options) {
|
|
|
635
846
|
async function readState(opened) {
|
|
636
847
|
const { lastCompleted, locators, read } = await hydrate(opened);
|
|
637
848
|
applyChangeSet(read);
|
|
849
|
+
prepare();
|
|
638
850
|
if (replicas.size > 0)
|
|
639
851
|
emitRooms();
|
|
640
852
|
if (member !== null)
|
|
@@ -645,8 +857,15 @@ export function composeRuntime(options) {
|
|
|
645
857
|
return lastCompleted;
|
|
646
858
|
}
|
|
647
859
|
async function open() {
|
|
648
|
-
|
|
860
|
+
if (closed)
|
|
861
|
+
throw new Error('runtime closed');
|
|
862
|
+
const opened = await openRuntimeStore(isFenced);
|
|
649
863
|
store = opened;
|
|
864
|
+
const interrupted = await opened.fences();
|
|
865
|
+
for (const locator of interrupted) {
|
|
866
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
867
|
+
lastFenced.set(locator, ++fencings);
|
|
868
|
+
}
|
|
650
869
|
const kept = options.shell.generation;
|
|
651
870
|
const storedGeneration = await opened.generation();
|
|
652
871
|
const loss = detectStorageLoss(storedGeneration, kept);
|
|
@@ -660,6 +879,8 @@ export function composeRuntime(options) {
|
|
|
660
879
|
emitStorageLoss();
|
|
661
880
|
}
|
|
662
881
|
const lastCompleted = await readState(opened);
|
|
882
|
+
if (interrupted.length > 0)
|
|
883
|
+
await erased(opened, interrupted);
|
|
663
884
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
664
885
|
return opened;
|
|
665
886
|
}
|
|
@@ -741,17 +962,19 @@ export function composeRuntime(options) {
|
|
|
741
962
|
moveTo({ ...status, ...attention() });
|
|
742
963
|
throw error;
|
|
743
964
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
965
|
+
return () => {
|
|
966
|
+
queuesWithoutReplica.delete(locator);
|
|
967
|
+
for (const { batch, returned } of unqueued) {
|
|
968
|
+
const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
|
|
969
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
970
|
+
}
|
|
971
|
+
replicas.set(locator, replicaOf(confirmed, kept, before));
|
|
972
|
+
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
973
|
+
room: settling.room,
|
|
974
|
+
queue: kept.length !== previousQueue.length,
|
|
975
|
+
drafts: [...written.keys()],
|
|
976
|
+
});
|
|
977
|
+
};
|
|
755
978
|
}
|
|
756
979
|
function adoptModels(locator, record) {
|
|
757
980
|
for (const [attached, model] of [
|
|
@@ -784,11 +1007,16 @@ export function composeRuntime(options) {
|
|
|
784
1007
|
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
785
1008
|
};
|
|
786
1009
|
}
|
|
787
|
-
function publish(local, room, confirmed, landing
|
|
1010
|
+
function publish(local, room, confirmed, landing, live) {
|
|
788
1011
|
const { base, stored } = landing;
|
|
789
1012
|
return exclusive(async () => {
|
|
790
|
-
if (
|
|
1013
|
+
if (stored !== undefined) {
|
|
1014
|
+
await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
|
|
1015
|
+
}
|
|
1016
|
+
if (!live()) {
|
|
1017
|
+
caughtUpTo(room.locator);
|
|
791
1018
|
return true;
|
|
1019
|
+
}
|
|
792
1020
|
const prior = replicas.get(room.locator);
|
|
793
1021
|
if (base !== undefined &&
|
|
794
1022
|
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
@@ -802,9 +1030,11 @@ export function composeRuntime(options) {
|
|
|
802
1030
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
803
1031
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
804
1032
|
});
|
|
805
|
-
const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
|
|
806
|
-
if (stored !== undefined)
|
|
1033
|
+
const set = (await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) }))();
|
|
1034
|
+
if (stored !== undefined) {
|
|
807
1035
|
transfers.delete(room.locator);
|
|
1036
|
+
catching.delete(room.locator);
|
|
1037
|
+
}
|
|
808
1038
|
for (const batch of accepted)
|
|
809
1039
|
pendings.delete(batch.id);
|
|
810
1040
|
stale.delete(room.locator);
|
|
@@ -813,13 +1043,17 @@ export function composeRuntime(options) {
|
|
|
813
1043
|
sourceRefusals.delete(room.locator);
|
|
814
1044
|
declarationRefusals.delete(room.locator);
|
|
815
1045
|
list(room, true);
|
|
1046
|
+
if (prior === undefined)
|
|
1047
|
+
prepared.add(room.locator);
|
|
816
1048
|
publishChangeSet(set);
|
|
817
1049
|
moveTo({ ...status, ...attention() });
|
|
1050
|
+
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
818
1051
|
return true;
|
|
819
1052
|
});
|
|
820
1053
|
}
|
|
821
1054
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
822
1055
|
const local = await openLocal();
|
|
1056
|
+
await referenced(locator);
|
|
823
1057
|
const replica = replicas.get(locator);
|
|
824
1058
|
const room = rooms.get(locator);
|
|
825
1059
|
if (replica === undefined || room === undefined) {
|
|
@@ -867,6 +1101,11 @@ export function composeRuntime(options) {
|
|
|
867
1101
|
changes,
|
|
868
1102
|
bases: parts.flatMap(({ bases }) => bases),
|
|
869
1103
|
writtenBy,
|
|
1104
|
+
clean: {
|
|
1105
|
+
text: replica.confirmed.record.documents,
|
|
1106
|
+
binaries: replica.confirmed.record.binaries,
|
|
1107
|
+
composition: compositionOf(replica.confirmed.record),
|
|
1108
|
+
},
|
|
870
1109
|
};
|
|
871
1110
|
const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
|
|
872
1111
|
if (checked?.kind === 'refused')
|
|
@@ -937,6 +1176,8 @@ export function composeRuntime(options) {
|
|
|
937
1176
|
catch {
|
|
938
1177
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
939
1178
|
}
|
|
1179
|
+
if (fences.has(locator))
|
|
1180
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
940
1181
|
for (const origin of withdrawn.keys())
|
|
941
1182
|
pendings.delete(origin);
|
|
942
1183
|
for (const [id, drafts] of drafted) {
|
|
@@ -966,6 +1207,8 @@ export function composeRuntime(options) {
|
|
|
966
1207
|
return;
|
|
967
1208
|
}
|
|
968
1209
|
await local.submit(locator, identifier, submission, lost);
|
|
1210
|
+
if (fences.has(locator))
|
|
1211
|
+
return;
|
|
969
1212
|
pendings.set(identifier, { ...state, submission, lost });
|
|
970
1213
|
const replica = replicas.get(locator);
|
|
971
1214
|
if (replica !== undefined)
|
|
@@ -976,6 +1219,8 @@ export function composeRuntime(options) {
|
|
|
976
1219
|
if (!continuing())
|
|
977
1220
|
throw new Error('connection refused');
|
|
978
1221
|
await local.keepUpload(locator, identifier, upload);
|
|
1222
|
+
if (fences.has(locator))
|
|
1223
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
979
1224
|
pendings.set(identifier, { ...pendings.get(identifier), upload });
|
|
980
1225
|
if (!continuing())
|
|
981
1226
|
throw new Error('connection refused');
|
|
@@ -991,6 +1236,7 @@ export function composeRuntime(options) {
|
|
|
991
1236
|
}
|
|
992
1237
|
if (closed || authorization === undefined || refusal !== null)
|
|
993
1238
|
return UNCERTAIN;
|
|
1239
|
+
const since = fencings;
|
|
994
1240
|
const transfer = ['transfer', 'history', locator, path];
|
|
995
1241
|
if (query.revision !== undefined)
|
|
996
1242
|
transfer.push(query.revision);
|
|
@@ -1005,6 +1251,8 @@ export function composeRuntime(options) {
|
|
|
1005
1251
|
...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
|
|
1006
1252
|
});
|
|
1007
1253
|
journal.record(`outcome history ${result.kind}`);
|
|
1254
|
+
if (fencedSince(locator, since))
|
|
1255
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
1008
1256
|
if (result.kind === 'scoped-refusal') {
|
|
1009
1257
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
1010
1258
|
if (room !== undefined)
|
|
@@ -1027,6 +1275,12 @@ export function composeRuntime(options) {
|
|
|
1027
1275
|
}
|
|
1028
1276
|
return null;
|
|
1029
1277
|
}
|
|
1278
|
+
function binaryRoom(request) {
|
|
1279
|
+
if (request.kind === 'path')
|
|
1280
|
+
return request.locator;
|
|
1281
|
+
const scope = options.adapter.referrerScope(request.referrer);
|
|
1282
|
+
return scope?.kind === 'room' ? scope.locator : null;
|
|
1283
|
+
}
|
|
1030
1284
|
async function readBinary(request) {
|
|
1031
1285
|
if (closed || refusal !== null)
|
|
1032
1286
|
return UNCERTAIN;
|
|
@@ -1038,12 +1292,16 @@ export function composeRuntime(options) {
|
|
|
1038
1292
|
}
|
|
1039
1293
|
if (closed || authorization === undefined || refusal !== null)
|
|
1040
1294
|
return UNCERTAIN;
|
|
1295
|
+
const since = fencings;
|
|
1041
1296
|
journal.record(request.kind === 'path'
|
|
1042
1297
|
? `transfer binary ${request.locator} ${request.path}`
|
|
1043
1298
|
: `transfer binary ${request.fileRef}`);
|
|
1044
1299
|
try {
|
|
1045
1300
|
const result = await options.adapter.binary(authorization, request);
|
|
1046
1301
|
journal.record(`outcome binary ${result.kind}`);
|
|
1302
|
+
const locator = binaryRoom(request);
|
|
1303
|
+
if (locator !== null && fencedSince(locator, since))
|
|
1304
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
1047
1305
|
if (result.kind === 'scoped-refusal') {
|
|
1048
1306
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
1049
1307
|
if (room !== undefined)
|
|
@@ -1167,11 +1425,11 @@ export function composeRuntime(options) {
|
|
|
1167
1425
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
1168
1426
|
!options.adapter.reached(locator, roomVersion, written));
|
|
1169
1427
|
}
|
|
1170
|
-
async function pullRefresh(authorized, confirmed) {
|
|
1171
|
-
if (!
|
|
1428
|
+
async function pullRefresh(authorized, confirmed, live) {
|
|
1429
|
+
if (!live())
|
|
1172
1430
|
return unconfirmed;
|
|
1173
1431
|
try {
|
|
1174
|
-
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held),
|
|
1432
|
+
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
|
|
1175
1433
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
1176
1434
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
1177
1435
|
return result;
|
|
@@ -1181,47 +1439,69 @@ export function composeRuntime(options) {
|
|
|
1181
1439
|
throw error;
|
|
1182
1440
|
}
|
|
1183
1441
|
}
|
|
1184
|
-
function staging(local) {
|
|
1442
|
+
function staging(local, live) {
|
|
1185
1443
|
return {
|
|
1186
1444
|
held: (locator) => transfers.get(locator) ?? null,
|
|
1187
1445
|
staged: (record) => local.staged(record),
|
|
1188
1446
|
stage: (record, page) => exclusive(async () => {
|
|
1189
|
-
if (!
|
|
1447
|
+
if (!live())
|
|
1190
1448
|
return false;
|
|
1191
1449
|
await local.stage(record, page);
|
|
1192
1450
|
transfers.set(record.locator, record);
|
|
1451
|
+
readSide.recover();
|
|
1193
1452
|
return true;
|
|
1194
1453
|
}),
|
|
1195
1454
|
abandon: (locator) => exclusive(async () => {
|
|
1196
|
-
if (!
|
|
1455
|
+
if (!live())
|
|
1197
1456
|
return;
|
|
1198
1457
|
await local.abandon(locator);
|
|
1199
1458
|
transfers.delete(locator);
|
|
1459
|
+
readSide.recover();
|
|
1200
1460
|
}),
|
|
1201
1461
|
};
|
|
1202
1462
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1463
|
+
function reusable(room) {
|
|
1464
|
+
const record = replicas.get(room.locator)?.confirmed.record;
|
|
1465
|
+
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1466
|
+
return null;
|
|
1467
|
+
return { documents: record.documents, revisions: record.revisions };
|
|
1468
|
+
}
|
|
1469
|
+
async function pullBootstrap(authorized, room, local, live) {
|
|
1470
|
+
if (!live())
|
|
1205
1471
|
return unconfirmed;
|
|
1472
|
+
if (replicas.has(room.locator)) {
|
|
1473
|
+
catching.add(room.locator);
|
|
1474
|
+
readSide.recover();
|
|
1475
|
+
}
|
|
1206
1476
|
try {
|
|
1207
1477
|
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
1208
1478
|
collection: modelOf(room.locator),
|
|
1209
1479
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
1210
|
-
}, (models, held) => remodel(room.locator, models, held),
|
|
1480
|
+
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
1211
1481
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
1212
1482
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1483
|
+
if (result.kind !== 'staged')
|
|
1484
|
+
caughtUpTo(room.locator);
|
|
1213
1485
|
return result;
|
|
1214
1486
|
}
|
|
1215
1487
|
catch (error) {
|
|
1216
1488
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
1489
|
+
caughtUpTo(room.locator);
|
|
1217
1490
|
throw error;
|
|
1218
1491
|
}
|
|
1219
1492
|
}
|
|
1220
|
-
|
|
1493
|
+
function caughtUpTo(locator) {
|
|
1494
|
+
if (catching.delete(locator))
|
|
1495
|
+
readSide.recover();
|
|
1496
|
+
}
|
|
1497
|
+
async function advance(room, local, authorized, live) {
|
|
1498
|
+
await referenced(room.locator);
|
|
1499
|
+
if (!live())
|
|
1500
|
+
return unconfirmed;
|
|
1221
1501
|
const held = replicas.get(room.locator);
|
|
1222
1502
|
if (held !== undefined) {
|
|
1223
|
-
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
1224
|
-
if (!
|
|
1503
|
+
const refreshed = await pullRefresh(authorized, held.confirmed, live);
|
|
1504
|
+
if (!live())
|
|
1225
1505
|
return unconfirmed;
|
|
1226
1506
|
if (refreshed.kind === 'still-current') {
|
|
1227
1507
|
stale.delete(room.locator);
|
|
@@ -1235,31 +1515,45 @@ export function composeRuntime(options) {
|
|
|
1235
1515
|
return null;
|
|
1236
1516
|
}
|
|
1237
1517
|
if (refreshed.kind === 'advanced') {
|
|
1238
|
-
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
|
|
1518
|
+
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
|
|
1239
1519
|
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1240
1520
|
const reached = now !== undefined &&
|
|
1241
1521
|
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
1242
|
-
return reached ? null : advance(room, local, authorized);
|
|
1522
|
+
return reached ? null : advance(room, local, authorized, live);
|
|
1243
1523
|
}
|
|
1244
1524
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1245
1525
|
return null;
|
|
1246
1526
|
}
|
|
1247
1527
|
if (refreshed.kind !== 'beyond-horizon')
|
|
1248
1528
|
return refreshed;
|
|
1529
|
+
if (opened !== null) {
|
|
1530
|
+
owed.add(room.locator);
|
|
1531
|
+
readSide.recover();
|
|
1532
|
+
return unconfirmed;
|
|
1533
|
+
}
|
|
1249
1534
|
}
|
|
1250
|
-
const bootstrapped = await pullBootstrap(authorized, room, local);
|
|
1251
|
-
if (!
|
|
1535
|
+
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
1536
|
+
if (!live())
|
|
1252
1537
|
return unconfirmed;
|
|
1253
1538
|
if (bootstrapped.kind !== 'staged')
|
|
1254
1539
|
return bootstrapped;
|
|
1255
|
-
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
1540
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
1256
1541
|
return null;
|
|
1257
1542
|
}
|
|
1258
1543
|
async function withhold(withheld, room, local, progress) {
|
|
1544
|
+
if (withheld.kind === 'unconfirmed')
|
|
1545
|
+
return;
|
|
1259
1546
|
if (withheld.kind === 'refused')
|
|
1260
1547
|
stale.delete(room.locator);
|
|
1261
1548
|
else
|
|
1262
1549
|
stale.add(room.locator);
|
|
1550
|
+
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
1551
|
+
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
1552
|
+
repairable.set(room.locator, reset);
|
|
1553
|
+
repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
|
|
1554
|
+
}
|
|
1555
|
+
else
|
|
1556
|
+
repairable.delete(room.locator);
|
|
1263
1557
|
if (withheld.kind === 'scoped-refusal') {
|
|
1264
1558
|
await scoped(withheld, room, local);
|
|
1265
1559
|
}
|
|
@@ -1316,6 +1610,7 @@ export function composeRuntime(options) {
|
|
|
1316
1610
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1317
1611
|
}
|
|
1318
1612
|
async function adoptAccepted(room, local, submitted, accepted) {
|
|
1613
|
+
await referenced(room.locator);
|
|
1319
1614
|
if (!continuing())
|
|
1320
1615
|
return unconfirmed;
|
|
1321
1616
|
const current = replicas.get(room.locator);
|
|
@@ -1332,16 +1627,17 @@ export function composeRuntime(options) {
|
|
|
1332
1627
|
if (applied.kind !== 'staged') {
|
|
1333
1628
|
const state = pendings.get(submitted.id);
|
|
1334
1629
|
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1335
|
-
if (state !== undefined)
|
|
1630
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
1336
1631
|
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1337
1632
|
emitQueue(room.locator);
|
|
1338
1633
|
return applied;
|
|
1339
1634
|
}
|
|
1340
|
-
const
|
|
1635
|
+
const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
1341
1636
|
pendings.delete(submitted.id);
|
|
1342
1637
|
unreadable.delete(room.locator);
|
|
1343
1638
|
sourceRefusals.delete(room.locator);
|
|
1344
1639
|
declarationRefusals.delete(room.locator);
|
|
1640
|
+
const set = settled();
|
|
1345
1641
|
list(room, true);
|
|
1346
1642
|
publishChangeSet(set);
|
|
1347
1643
|
moveTo({ ...status, ...attention() });
|
|
@@ -1355,14 +1651,14 @@ export function composeRuntime(options) {
|
|
|
1355
1651
|
refusedSource(room, failed);
|
|
1356
1652
|
return false;
|
|
1357
1653
|
}
|
|
1358
|
-
async function submitRoom(room, local, authorized, progress) {
|
|
1359
|
-
while (continuing() && authorization !== undefined) {
|
|
1654
|
+
async function submitRoom(room, local, authorized, progress, signal) {
|
|
1655
|
+
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
1360
1656
|
const replica = replicas.get(room.locator);
|
|
1361
1657
|
const head = replica?.queue[0];
|
|
1362
1658
|
if (replica === undefined || head === undefined)
|
|
1363
|
-
return;
|
|
1659
|
+
return 'done';
|
|
1364
1660
|
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1365
|
-
return;
|
|
1661
|
+
return 'done';
|
|
1366
1662
|
const state = pendings.get(head.id);
|
|
1367
1663
|
const { submission, lost: earlier } = state ?? {
|
|
1368
1664
|
submission: { state: 'waiting' },
|
|
@@ -1370,24 +1666,30 @@ export function composeRuntime(options) {
|
|
|
1370
1666
|
};
|
|
1371
1667
|
if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
|
|
1372
1668
|
progress.complete = false;
|
|
1373
|
-
return;
|
|
1669
|
+
return { at: Date.parse(submission.reset) };
|
|
1374
1670
|
}
|
|
1375
1671
|
if (writesHeld()) {
|
|
1376
1672
|
progress.complete = false;
|
|
1377
1673
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1378
|
-
return;
|
|
1674
|
+
return 'done';
|
|
1379
1675
|
}
|
|
1380
1676
|
const lost = earlier || submission.state === 'submitted';
|
|
1677
|
+
if (lost && pastHorizon(head.id)) {
|
|
1678
|
+
await discardPending(room, local, signal);
|
|
1679
|
+
return 'done';
|
|
1680
|
+
}
|
|
1381
1681
|
let result;
|
|
1382
1682
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
1383
1683
|
await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
|
|
1384
|
-
if (discoveryPaused)
|
|
1385
|
-
return;
|
|
1386
|
-
if (writesHeld()) {
|
|
1684
|
+
if (discoveryPaused || fences.has(room.locator))
|
|
1685
|
+
return 'done';
|
|
1686
|
+
if (signal.aborted || writesHeld()) {
|
|
1387
1687
|
await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
|
|
1688
|
+
if (signal.aborted)
|
|
1689
|
+
return 'done';
|
|
1388
1690
|
progress.complete = false;
|
|
1389
1691
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1390
|
-
return;
|
|
1692
|
+
return 'done';
|
|
1391
1693
|
}
|
|
1392
1694
|
const opensBefore = openSignals;
|
|
1393
1695
|
const answered = awaitingAnswer(room.locator);
|
|
@@ -1403,32 +1705,36 @@ export function composeRuntime(options) {
|
|
|
1403
1705
|
progress.complete = false;
|
|
1404
1706
|
if (continuing())
|
|
1405
1707
|
progress.reachable = false;
|
|
1406
|
-
return;
|
|
1708
|
+
return 'again';
|
|
1709
|
+
}
|
|
1710
|
+
if (signal.aborted) {
|
|
1711
|
+
answered();
|
|
1712
|
+
return 'done';
|
|
1407
1713
|
}
|
|
1408
1714
|
if (result.kind !== 'accepted' || !continuing())
|
|
1409
1715
|
answered();
|
|
1410
1716
|
if (!continuing())
|
|
1411
|
-
return;
|
|
1717
|
+
return 'done';
|
|
1412
1718
|
if (result.kind === 'transport-uncertainty') {
|
|
1413
1719
|
progress.complete = false;
|
|
1414
1720
|
progress.reachable = false;
|
|
1415
|
-
return;
|
|
1721
|
+
return 'again';
|
|
1416
1722
|
}
|
|
1417
1723
|
if (result.kind === 'room-not-readable') {
|
|
1418
1724
|
progress.complete = false;
|
|
1419
1725
|
unreadable.add(room.locator);
|
|
1420
1726
|
list(room, false);
|
|
1421
1727
|
emitRooms();
|
|
1422
|
-
return;
|
|
1728
|
+
return 'done';
|
|
1423
1729
|
}
|
|
1424
1730
|
if (result.kind === 'revoked-authorization') {
|
|
1425
1731
|
await refuse('revoked-authorization');
|
|
1426
|
-
return;
|
|
1732
|
+
return 'done';
|
|
1427
1733
|
}
|
|
1428
1734
|
if (result.kind === 'scoped-refusal') {
|
|
1429
1735
|
await scoped(result, room, local);
|
|
1430
1736
|
progress.complete = false;
|
|
1431
|
-
return;
|
|
1737
|
+
return 'done';
|
|
1432
1738
|
}
|
|
1433
1739
|
if (result.kind === 'write-halted') {
|
|
1434
1740
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
@@ -1437,16 +1743,24 @@ export function composeRuntime(options) {
|
|
|
1437
1743
|
continue;
|
|
1438
1744
|
haltedRefusal = true;
|
|
1439
1745
|
progress.complete = false;
|
|
1440
|
-
return;
|
|
1746
|
+
return 'done';
|
|
1441
1747
|
}
|
|
1442
1748
|
if (result.kind === 'conflicted' ||
|
|
1443
1749
|
result.kind === 'rejected' ||
|
|
1444
1750
|
result.kind === 'expired' ||
|
|
1445
1751
|
result.kind === 'retryable-refusal') {
|
|
1752
|
+
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
1753
|
+
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
1754
|
+
await discardPending(room, local, signal);
|
|
1755
|
+
return 'done';
|
|
1756
|
+
}
|
|
1446
1757
|
if (result.kind === 'expired') {
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1758
|
+
await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
|
|
1759
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
1760
|
+
if (withheld !== null) {
|
|
1761
|
+
await withhold(withheld, room, local, progress);
|
|
1762
|
+
return 'done';
|
|
1763
|
+
}
|
|
1450
1764
|
}
|
|
1451
1765
|
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
|
|
1452
1766
|
(result.kind === 'conflicted' && result.roomVersion !== undefined
|
|
@@ -1459,8 +1773,7 @@ export function composeRuntime(options) {
|
|
|
1459
1773
|
const reset = result.reset;
|
|
1460
1774
|
progress.complete = false;
|
|
1461
1775
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1462
|
-
|
|
1463
|
-
return;
|
|
1776
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
1464
1777
|
}
|
|
1465
1778
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
1466
1779
|
const left = leaving(head, result);
|
|
@@ -1471,37 +1784,82 @@ export function composeRuntime(options) {
|
|
|
1471
1784
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
1472
1785
|
return;
|
|
1473
1786
|
}
|
|
1474
|
-
const
|
|
1787
|
+
const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
1475
1788
|
leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
|
|
1476
1789
|
(result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
|
|
1477
1790
|
? { ...left, returned: null }
|
|
1478
1791
|
: left,
|
|
1479
1792
|
});
|
|
1480
|
-
publishChangeSet(
|
|
1793
|
+
publishChangeSet(settled());
|
|
1481
1794
|
moveTo({ ...status, ...attention() });
|
|
1482
1795
|
});
|
|
1483
1796
|
if (result.kind === 'expired')
|
|
1484
1797
|
continue;
|
|
1485
|
-
const withheld = await advance(room, local, authorized);
|
|
1486
|
-
if (withheld !== null)
|
|
1487
|
-
|
|
1798
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
1799
|
+
if (withheld !== null) {
|
|
1800
|
+
await withhold(withheld, room, local, progress);
|
|
1801
|
+
return 'done';
|
|
1802
|
+
}
|
|
1488
1803
|
continue;
|
|
1489
1804
|
}
|
|
1490
1805
|
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1491
1806
|
if (!kept) {
|
|
1492
1807
|
progress.complete = false;
|
|
1493
|
-
return;
|
|
1808
|
+
return 'done';
|
|
1494
1809
|
}
|
|
1495
1810
|
}
|
|
1811
|
+
return 'done';
|
|
1812
|
+
}
|
|
1813
|
+
function pastHorizon(identity) {
|
|
1814
|
+
return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
|
|
1815
|
+
}
|
|
1816
|
+
async function discardPending(room, local, signal) {
|
|
1817
|
+
const { locator } = room;
|
|
1818
|
+
const discarded = await exclusive(async () => {
|
|
1819
|
+
if (fences.has(locator))
|
|
1820
|
+
return false;
|
|
1821
|
+
const before = replicas.get(locator);
|
|
1822
|
+
const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
1823
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
1824
|
+
lastFenced.set(locator, ++fencings);
|
|
1825
|
+
try {
|
|
1826
|
+
await local.discard(locator);
|
|
1827
|
+
}
|
|
1828
|
+
finally {
|
|
1829
|
+
fences.delete(locator);
|
|
1830
|
+
}
|
|
1831
|
+
for (const batch of queue)
|
|
1832
|
+
pendings.delete(batch.id);
|
|
1833
|
+
queuesWithoutReplica.delete(locator);
|
|
1834
|
+
const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
|
|
1835
|
+
if (after !== null)
|
|
1836
|
+
replicas.set(locator, after);
|
|
1837
|
+
journal.record(`room discarded ${locator}`);
|
|
1838
|
+
publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
|
|
1839
|
+
moveTo({ ...status, ...attention() });
|
|
1840
|
+
return true;
|
|
1841
|
+
});
|
|
1842
|
+
if (!discarded)
|
|
1843
|
+
return;
|
|
1844
|
+
const progress = { complete: true, reachable: true };
|
|
1845
|
+
await roomTask(locator, async (live) => {
|
|
1846
|
+
await Promise.race([
|
|
1847
|
+
reload(room, local, progress, () => live() && !signal.aborted),
|
|
1848
|
+
abandonedOn(signal),
|
|
1849
|
+
]);
|
|
1850
|
+
});
|
|
1851
|
+
if (signal.aborted)
|
|
1852
|
+
return;
|
|
1853
|
+
await rest(progress);
|
|
1496
1854
|
}
|
|
1497
1855
|
function awaitingAnswer(locator) {
|
|
1498
1856
|
let release = () => { };
|
|
1499
1857
|
const awaited = new Promise((resolve) => {
|
|
1500
1858
|
release = resolve;
|
|
1501
1859
|
});
|
|
1502
|
-
answering.set(locator, awaited);
|
|
1860
|
+
answering.set(locator, { generation, answered: awaited });
|
|
1503
1861
|
return () => {
|
|
1504
|
-
if (answering.get(locator) === awaited)
|
|
1862
|
+
if (answering.get(locator)?.answered === awaited)
|
|
1505
1863
|
answering.delete(locator);
|
|
1506
1864
|
release();
|
|
1507
1865
|
};
|
|
@@ -1617,8 +1975,10 @@ export function composeRuntime(options) {
|
|
|
1617
1975
|
];
|
|
1618
1976
|
});
|
|
1619
1977
|
}
|
|
1620
|
-
async function synchronize(listed, local, progress, attempt) {
|
|
1978
|
+
async function synchronize(listed, local, progress, attempt, live) {
|
|
1621
1979
|
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
|
|
1980
|
+
if (!live())
|
|
1981
|
+
return;
|
|
1622
1982
|
const ready = listed.filter((_, index) => {
|
|
1623
1983
|
const outcome = prepared[index];
|
|
1624
1984
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
@@ -1627,9 +1987,10 @@ export function composeRuntime(options) {
|
|
|
1627
1987
|
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
1628
1988
|
handed = true;
|
|
1629
1989
|
}
|
|
1990
|
+
discovering?.release();
|
|
1630
1991
|
const loaded = await Promise.allSettled(ready
|
|
1631
1992
|
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1632
|
-
.map((room) => loadRoom(room, local, progress)));
|
|
1993
|
+
.map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
|
|
1633
1994
|
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1634
1995
|
if (failed !== undefined)
|
|
1635
1996
|
throw failed.reason;
|
|
@@ -1658,14 +2019,14 @@ export function composeRuntime(options) {
|
|
|
1658
2019
|
}
|
|
1659
2020
|
return true;
|
|
1660
2021
|
}
|
|
1661
|
-
async function caughtUp(room, local, progress) {
|
|
2022
|
+
async function caughtUp(room, local, progress, live) {
|
|
1662
2023
|
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1663
|
-
if (held === undefined || authorization === undefined)
|
|
2024
|
+
if (held === undefined || authorization === undefined || !live())
|
|
1664
2025
|
return;
|
|
1665
2026
|
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
1666
2027
|
if (!behind)
|
|
1667
2028
|
return;
|
|
1668
|
-
const withheld = await advance(room, local, authorization);
|
|
2029
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1669
2030
|
if (withheld !== null)
|
|
1670
2031
|
await withhold(withheld, room, local, progress);
|
|
1671
2032
|
}
|
|
@@ -1675,39 +2036,46 @@ export function composeRuntime(options) {
|
|
|
1675
2036
|
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1676
2037
|
});
|
|
1677
2038
|
}
|
|
1678
|
-
async function loadRoom(room, local, progress) {
|
|
1679
|
-
if (
|
|
2039
|
+
async function loadRoom(room, local, progress, live) {
|
|
2040
|
+
if (!live() || authorization === undefined)
|
|
1680
2041
|
return;
|
|
1681
2042
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1682
|
-
await reload(room, local, progress);
|
|
2043
|
+
await reload(room, local, progress, live);
|
|
1683
2044
|
return;
|
|
1684
2045
|
}
|
|
1685
|
-
const withheld = await advance(room, local, authorization);
|
|
2046
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1686
2047
|
if (withheld !== null) {
|
|
1687
2048
|
await withhold(withheld, room, local, progress);
|
|
1688
2049
|
return;
|
|
1689
2050
|
}
|
|
1690
|
-
await caughtUp(room, local, progress);
|
|
2051
|
+
await caughtUp(room, local, progress, live);
|
|
1691
2052
|
}
|
|
1692
|
-
async function reload(room, local, progress) {
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
2053
|
+
async function reload(room, local, progress, live) {
|
|
2054
|
+
try {
|
|
2055
|
+
if (!live() || authorization === undefined)
|
|
2056
|
+
return;
|
|
2057
|
+
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
2058
|
+
if (!live())
|
|
2059
|
+
return;
|
|
2060
|
+
if (bootstrapped.kind !== 'staged') {
|
|
2061
|
+
await withhold(bootstrapped, room, local, progress);
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
2065
|
+
await caughtUp(room, local, progress, live);
|
|
2066
|
+
}
|
|
2067
|
+
finally {
|
|
2068
|
+
caughtUpTo(room.locator);
|
|
1701
2069
|
}
|
|
1702
|
-
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
1703
|
-
await caughtUp(room, local, progress);
|
|
1704
2070
|
}
|
|
1705
|
-
async function discoverRooms(local, progress, attempt) {
|
|
2071
|
+
async function discoverRooms(local, progress, attempt, live) {
|
|
1706
2072
|
if (closed || authorization === undefined) {
|
|
1707
2073
|
notifyReady();
|
|
1708
2074
|
return;
|
|
1709
2075
|
}
|
|
1710
2076
|
moveTo(transferring(status));
|
|
2077
|
+
discoveries += 1;
|
|
2078
|
+
const started = discoveries;
|
|
1711
2079
|
let result;
|
|
1712
2080
|
try {
|
|
1713
2081
|
result = await options.adapter.discover(authorization);
|
|
@@ -1715,7 +2083,8 @@ export function composeRuntime(options) {
|
|
|
1715
2083
|
catch {
|
|
1716
2084
|
result = { kind: 'transport-uncertainty' };
|
|
1717
2085
|
}
|
|
1718
|
-
if (
|
|
2086
|
+
if (!live()) {
|
|
2087
|
+
progress.complete = false;
|
|
1719
2088
|
notifyReady();
|
|
1720
2089
|
return;
|
|
1721
2090
|
}
|
|
@@ -1773,6 +2142,7 @@ export function composeRuntime(options) {
|
|
|
1773
2142
|
}
|
|
1774
2143
|
const previousCapabilities = capabilities;
|
|
1775
2144
|
capabilities = result.capabilities;
|
|
2145
|
+
outcomeRetention = result.retention.outcome;
|
|
1776
2146
|
notifyReady();
|
|
1777
2147
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1778
2148
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
@@ -1785,8 +2155,7 @@ export function composeRuntime(options) {
|
|
|
1785
2155
|
member = signedIn;
|
|
1786
2156
|
emitMember();
|
|
1787
2157
|
}
|
|
1788
|
-
await applyDiscovery(result.rooms, local);
|
|
1789
|
-
await synchronize(result.rooms, local, progress, attempt);
|
|
2158
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
1790
2159
|
}
|
|
1791
2160
|
async function ensureAuthorization() {
|
|
1792
2161
|
if (authorization !== undefined)
|
|
@@ -1800,20 +2169,50 @@ export function composeRuntime(options) {
|
|
|
1800
2169
|
throw error;
|
|
1801
2170
|
}
|
|
1802
2171
|
}
|
|
1803
|
-
function
|
|
2172
|
+
async function admittedRead(work) {
|
|
2173
|
+
if (roomReads.generation !== generation) {
|
|
2174
|
+
roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
2175
|
+
}
|
|
2176
|
+
const reads = roomReads;
|
|
2177
|
+
while (reads.free === 0)
|
|
2178
|
+
await new Promise((admit) => reads.waiting.push(admit));
|
|
2179
|
+
reads.free -= 1;
|
|
2180
|
+
try {
|
|
2181
|
+
await work();
|
|
2182
|
+
}
|
|
2183
|
+
finally {
|
|
2184
|
+
reads.free += 1;
|
|
2185
|
+
reads.waiting.shift()?.();
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
function roomTask(locator, work) {
|
|
2189
|
+
return task(roomLane(locator), (live) => admittedRead(() => work(live)));
|
|
2190
|
+
}
|
|
2191
|
+
function task(lane, work) {
|
|
1804
2192
|
if (stopping)
|
|
1805
|
-
return
|
|
1806
|
-
|
|
1807
|
-
|
|
2193
|
+
return Promise.resolve();
|
|
2194
|
+
const held = lanes.get(lane);
|
|
2195
|
+
const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
|
|
2196
|
+
const turn = current.generation;
|
|
2197
|
+
const live = () => turn === generation && continuing();
|
|
2198
|
+
const run = current.chain.then(() => work(live)).catch(() => undefined);
|
|
2199
|
+
current.chain = run;
|
|
2200
|
+
lanes.set(lane, current);
|
|
2201
|
+
working.add(run);
|
|
2202
|
+
void run.finally(() => working.delete(run));
|
|
2203
|
+
return run;
|
|
1808
2204
|
}
|
|
1809
2205
|
function sync(attempt) {
|
|
1810
|
-
return task(async () => {
|
|
1811
|
-
if (
|
|
2206
|
+
return task(OPENING_LANE, async (live) => {
|
|
2207
|
+
if (!live()) {
|
|
1812
2208
|
notifyReady();
|
|
1813
2209
|
return;
|
|
1814
2210
|
}
|
|
1815
2211
|
const progress = { complete: true, reachable: true };
|
|
1816
|
-
|
|
2212
|
+
const gate = discover();
|
|
2213
|
+
if (attempt === 'opened')
|
|
2214
|
+
opened = generation;
|
|
2215
|
+
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
1817
2216
|
if (attempt === 'opened')
|
|
1818
2217
|
streamed = progress;
|
|
1819
2218
|
complete = false;
|
|
@@ -1822,12 +2221,12 @@ export function composeRuntime(options) {
|
|
|
1822
2221
|
handed = false;
|
|
1823
2222
|
try {
|
|
1824
2223
|
const local = await openLocal();
|
|
1825
|
-
if (
|
|
2224
|
+
if (!live())
|
|
1826
2225
|
return;
|
|
1827
2226
|
await ensureAuthorization();
|
|
1828
|
-
if (
|
|
2227
|
+
if (!live())
|
|
1829
2228
|
return;
|
|
1830
|
-
await discoverRooms(local, progress, attempt);
|
|
2229
|
+
await discoverRooms(local, progress, attempt, live);
|
|
1831
2230
|
}
|
|
1832
2231
|
catch {
|
|
1833
2232
|
progress.complete = false;
|
|
@@ -1835,17 +2234,21 @@ export function composeRuntime(options) {
|
|
|
1835
2234
|
progress.reachable = false;
|
|
1836
2235
|
}
|
|
1837
2236
|
finally {
|
|
2237
|
+
gate.release();
|
|
1838
2238
|
notifyReady();
|
|
1839
2239
|
}
|
|
2240
|
+
if (!live())
|
|
2241
|
+
return;
|
|
1840
2242
|
if (attempt !== 'opened') {
|
|
1841
|
-
await finishOpening(progress);
|
|
2243
|
+
await finishOpening(progress, live);
|
|
1842
2244
|
return;
|
|
1843
2245
|
}
|
|
1844
2246
|
if (handed || closed || progress.reachable)
|
|
1845
2247
|
return;
|
|
1846
2248
|
underway = null;
|
|
2249
|
+
opened = null;
|
|
1847
2250
|
journal.record('connection reopen');
|
|
1848
|
-
await rest(progress);
|
|
2251
|
+
await rest(progress, live);
|
|
1849
2252
|
options.adapter.roomStream?.reopen();
|
|
1850
2253
|
});
|
|
1851
2254
|
}
|
|
@@ -1866,24 +2269,41 @@ export function composeRuntime(options) {
|
|
|
1866
2269
|
});
|
|
1867
2270
|
}
|
|
1868
2271
|
function applyOpening(expired) {
|
|
1869
|
-
|
|
1870
|
-
|
|
2272
|
+
const progress = underway?.generation === generation ? underway.progress : null;
|
|
2273
|
+
const cycle = progress ?? { complete: true, reachable: true };
|
|
2274
|
+
const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
|
|
2275
|
+
return task(OPENING_LANE, async (live) => {
|
|
1871
2276
|
underway = null;
|
|
1872
|
-
if (
|
|
2277
|
+
if (!live())
|
|
1873
2278
|
return;
|
|
1874
2279
|
if (progress === null) {
|
|
1875
|
-
await
|
|
2280
|
+
await replacing;
|
|
2281
|
+
await rest(cycle, live);
|
|
1876
2282
|
return;
|
|
1877
2283
|
}
|
|
2284
|
+
await roomsCarried(expired);
|
|
2285
|
+
if (!live())
|
|
2286
|
+
return;
|
|
2287
|
+
if (opened === generation)
|
|
2288
|
+
opened = null;
|
|
2289
|
+
const replaced = new Set(owed);
|
|
2290
|
+
owed.clear();
|
|
1878
2291
|
try {
|
|
1879
2292
|
const local = await openLocal();
|
|
1880
2293
|
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
1881
|
-
const reloading = held.filter((room) => expired.includes(room.locator)
|
|
1882
|
-
|
|
2294
|
+
const reloading = held.filter((room) => !expired.includes(room.locator) &&
|
|
2295
|
+
(replaced.has(room.locator) || revoked(room.locator)));
|
|
2296
|
+
for (const room of reloading)
|
|
2297
|
+
catching.add(room.locator);
|
|
2298
|
+
readSide.recover();
|
|
2299
|
+
const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
|
|
2300
|
+
const reloaded = [...(await reloads), ...(await replacing)];
|
|
1883
2301
|
if (progress.reachable) {
|
|
1884
|
-
for (const room of held)
|
|
1885
|
-
if (!reloading.includes(room))
|
|
2302
|
+
for (const room of held) {
|
|
2303
|
+
if (!expired.includes(room.locator) && !reloading.includes(room)) {
|
|
1886
2304
|
await confirmModels(room, local);
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
1887
2307
|
}
|
|
1888
2308
|
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1889
2309
|
if (failed !== undefined)
|
|
@@ -1893,21 +2313,21 @@ export function composeRuntime(options) {
|
|
|
1893
2313
|
progress.complete = false;
|
|
1894
2314
|
progress.reachable = false;
|
|
1895
2315
|
}
|
|
1896
|
-
|
|
2316
|
+
if (live())
|
|
2317
|
+
await finishOpening(progress, live);
|
|
1897
2318
|
});
|
|
1898
2319
|
}
|
|
1899
|
-
async function
|
|
1900
|
-
const progress = { complete: true, reachable: true };
|
|
2320
|
+
async function reloadNow(replaced, progress) {
|
|
1901
2321
|
try {
|
|
1902
2322
|
const local = await openLocal();
|
|
1903
|
-
await Promise.allSettled([...discovered.values()]
|
|
1904
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
1905
|
-
.map((room) => reload(room, local, progress)));
|
|
2323
|
+
return await Promise.allSettled([...discovered.values()]
|
|
2324
|
+
.filter((room) => replicas.has(room.locator) && replaced(room))
|
|
2325
|
+
.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
1906
2326
|
}
|
|
1907
2327
|
catch {
|
|
1908
2328
|
progress.reachable = false;
|
|
2329
|
+
return [];
|
|
1909
2330
|
}
|
|
1910
|
-
await rest(progress);
|
|
1911
2331
|
}
|
|
1912
2332
|
async function confirmModels(room, local) {
|
|
1913
2333
|
for (const attached of ['collection', 'typed-selection']) {
|
|
@@ -1920,38 +2340,94 @@ export function composeRuntime(options) {
|
|
|
1920
2340
|
function submitLater(locator) {
|
|
1921
2341
|
if (localSaves)
|
|
1922
2342
|
return;
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
2343
|
+
void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
|
|
2344
|
+
}
|
|
2345
|
+
async function submitPass(locator, signal) {
|
|
2346
|
+
const room = discovered.get(locator);
|
|
2347
|
+
if (closed || stopping || discoveryPaused || room === undefined)
|
|
2348
|
+
return 'done';
|
|
2349
|
+
const progress = { complete: true, reachable: true };
|
|
2350
|
+
let next;
|
|
2351
|
+
try {
|
|
2352
|
+
await ensureAuthorization();
|
|
2353
|
+
if (authorization === undefined)
|
|
2354
|
+
return 'done';
|
|
2355
|
+
next = await submitRoom(room, await openLocal(), authorization, progress, signal);
|
|
2356
|
+
}
|
|
2357
|
+
catch {
|
|
2358
|
+
progress.reachable = false;
|
|
2359
|
+
next = 'again';
|
|
2360
|
+
}
|
|
2361
|
+
if (signal.aborted)
|
|
2362
|
+
return 'done';
|
|
2363
|
+
await rest(progress);
|
|
2364
|
+
return next === 'again' && !socketOpen ? 'done' : next;
|
|
2365
|
+
}
|
|
2366
|
+
function repairLater(locator, when) {
|
|
2367
|
+
if (!socketOpen || repairing.has(locator))
|
|
1926
2368
|
return;
|
|
2369
|
+
void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
|
|
2370
|
+
}
|
|
2371
|
+
async function repairPass(locator, signal) {
|
|
2372
|
+
const room = discovered.get(locator);
|
|
2373
|
+
if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
|
|
2374
|
+
return 'done';
|
|
2375
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2376
|
+
return 'done';
|
|
2377
|
+
const progress = { complete: true, reachable: true };
|
|
2378
|
+
repairing.add(locator);
|
|
2379
|
+
try {
|
|
2380
|
+
const local = await openLocal();
|
|
2381
|
+
await roomTask(locator, async (live) => {
|
|
2382
|
+
const current = () => live() && !signal.aborted;
|
|
2383
|
+
if (!current() || authorization === undefined)
|
|
2384
|
+
return;
|
|
2385
|
+
const withheld = await Promise.race([
|
|
2386
|
+
advance(room, local, authorization, current),
|
|
2387
|
+
abandonedOn(signal),
|
|
2388
|
+
]);
|
|
2389
|
+
if (withheld !== null && current())
|
|
2390
|
+
await withhold(withheld, room, local, progress);
|
|
2391
|
+
});
|
|
1927
2392
|
}
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
2393
|
+
catch {
|
|
2394
|
+
stale.add(locator);
|
|
2395
|
+
progress.reachable = false;
|
|
2396
|
+
}
|
|
2397
|
+
finally {
|
|
2398
|
+
repairing.delete(locator);
|
|
2399
|
+
}
|
|
2400
|
+
if (signal.aborted)
|
|
2401
|
+
return 'done';
|
|
2402
|
+
await rest(progress);
|
|
2403
|
+
if (!stale.has(locator)) {
|
|
2404
|
+
if ((replicas.get(locator)?.queue.length ?? 0) > 0)
|
|
2405
|
+
submitLater(locator);
|
|
2406
|
+
return 'done';
|
|
2407
|
+
}
|
|
2408
|
+
if (!repairable.has(locator))
|
|
2409
|
+
return 'done';
|
|
2410
|
+
const reset = repairable.get(locator);
|
|
2411
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
2412
|
+
}
|
|
2413
|
+
function resendLater() {
|
|
2414
|
+
if (!socketOpen)
|
|
2415
|
+
return;
|
|
2416
|
+
void recovery.attempt('resend', resendPass, { again: true });
|
|
2417
|
+
}
|
|
2418
|
+
async function resendPass(signal) {
|
|
2419
|
+
if (!socketOpen || closed || stopping || discoveryPaused)
|
|
2420
|
+
return 'done';
|
|
2421
|
+
const reading = { complete: true, reachable: true };
|
|
2422
|
+
try {
|
|
2423
|
+
await conversations.resend(reading);
|
|
2424
|
+
}
|
|
2425
|
+
catch {
|
|
2426
|
+
reading.reachable = false;
|
|
2427
|
+
}
|
|
2428
|
+
if (signal.aborted)
|
|
2429
|
+
return 'done';
|
|
2430
|
+
return reading.reachable && reading.complete ? 'done' : 'again';
|
|
1955
2431
|
}
|
|
1956
2432
|
function submitAll() {
|
|
1957
2433
|
for (const [locator, replica] of replicas) {
|
|
@@ -1959,31 +2435,40 @@ export function composeRuntime(options) {
|
|
|
1959
2435
|
submitLater(locator);
|
|
1960
2436
|
}
|
|
1961
2437
|
}
|
|
1962
|
-
async function finishOpening(progress) {
|
|
2438
|
+
async function finishOpening(progress, live) {
|
|
1963
2439
|
try {
|
|
1964
|
-
if (
|
|
2440
|
+
if (!live()) {
|
|
1965
2441
|
progress.complete = false;
|
|
1966
2442
|
return;
|
|
1967
2443
|
}
|
|
1968
2444
|
submitAll();
|
|
2445
|
+
for (const locator of stale)
|
|
2446
|
+
if (repairable.has(locator))
|
|
2447
|
+
repairLater(locator, {});
|
|
1969
2448
|
await conversations.reconcile();
|
|
1970
2449
|
if (status.providerUnreachable)
|
|
1971
2450
|
conversations.reconnected();
|
|
1972
2451
|
const reading = { complete: true, reachable: true };
|
|
1973
2452
|
await conversations.catchUp(reading);
|
|
1974
|
-
if (
|
|
2453
|
+
if (progress.reachable && reading.reachable)
|
|
2454
|
+
await conversations.resend(reading);
|
|
2455
|
+
if (!reading.reachable) {
|
|
1975
2456
|
progress.reachable = false;
|
|
2457
|
+
resendLater();
|
|
2458
|
+
}
|
|
1976
2459
|
}
|
|
1977
2460
|
catch {
|
|
1978
2461
|
progress.complete = false;
|
|
1979
2462
|
}
|
|
1980
2463
|
finally {
|
|
1981
|
-
|
|
1982
|
-
|
|
2464
|
+
if (live()) {
|
|
2465
|
+
complete = progress.complete;
|
|
2466
|
+
await rest(progress, live);
|
|
2467
|
+
}
|
|
1983
2468
|
}
|
|
1984
2469
|
}
|
|
1985
|
-
async function rest(progress) {
|
|
1986
|
-
if (
|
|
2470
|
+
async function rest(progress, live = () => !closed) {
|
|
2471
|
+
if (!live())
|
|
1987
2472
|
return;
|
|
1988
2473
|
let lastCompleted = status.lastCompleted;
|
|
1989
2474
|
let synced = complete && stale.size === 0 && !pending();
|
|
@@ -1991,6 +2476,8 @@ export function composeRuntime(options) {
|
|
|
1991
2476
|
const completedAt = new Date().toISOString();
|
|
1992
2477
|
try {
|
|
1993
2478
|
await (await openLocal()).complete(completedAt);
|
|
2479
|
+
if (!live())
|
|
2480
|
+
return;
|
|
1994
2481
|
recorded = true;
|
|
1995
2482
|
lastCompleted = completedAt;
|
|
1996
2483
|
}
|
|
@@ -2005,11 +2492,23 @@ export function composeRuntime(options) {
|
|
|
2005
2492
|
...attention(),
|
|
2006
2493
|
});
|
|
2007
2494
|
}
|
|
2495
|
+
function discover() {
|
|
2496
|
+
let release = () => { };
|
|
2497
|
+
const done = new Promise((resolve) => {
|
|
2498
|
+
release = resolve;
|
|
2499
|
+
});
|
|
2500
|
+
discovering = { generation, done, release };
|
|
2501
|
+
return discovering;
|
|
2502
|
+
}
|
|
2008
2503
|
function applyRoom(locator, roomVersion) {
|
|
2009
2504
|
announced.set(locator, roomVersion);
|
|
2010
|
-
return task(async () => {
|
|
2011
|
-
|
|
2012
|
-
|
|
2505
|
+
return task(roomLane(locator), async (live) => {
|
|
2506
|
+
if (discovering?.generation === generation)
|
|
2507
|
+
await discovering.done;
|
|
2508
|
+
const saving = answering.get(locator);
|
|
2509
|
+
if (saving?.generation === generation)
|
|
2510
|
+
await saving.answered;
|
|
2511
|
+
if (!live() || authorization === undefined)
|
|
2013
2512
|
return;
|
|
2014
2513
|
const room = discovered.get(locator);
|
|
2015
2514
|
const held = replicas.get(locator);
|
|
@@ -2020,25 +2519,41 @@ export function composeRuntime(options) {
|
|
|
2020
2519
|
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
2021
2520
|
return;
|
|
2022
2521
|
}
|
|
2023
|
-
const during = underway;
|
|
2522
|
+
const during = underway?.generation === generation ? underway.progress : null;
|
|
2024
2523
|
const progress = during ?? { complete: true, reachable: true };
|
|
2025
2524
|
if (during === null)
|
|
2026
2525
|
moveTo(transferring(status));
|
|
2027
2526
|
try {
|
|
2028
2527
|
const local = await openLocal();
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2528
|
+
await admittedRead(async () => {
|
|
2529
|
+
if (!live() || authorization === undefined)
|
|
2530
|
+
return;
|
|
2531
|
+
const withheld = await advance(room, local, authorization, live);
|
|
2532
|
+
if (withheld !== null)
|
|
2533
|
+
await withhold(withheld, room, local, progress);
|
|
2534
|
+
});
|
|
2032
2535
|
}
|
|
2033
2536
|
catch {
|
|
2034
2537
|
stale.add(locator);
|
|
2035
2538
|
}
|
|
2036
2539
|
finally {
|
|
2037
2540
|
if (during === null)
|
|
2038
|
-
await rest(progress);
|
|
2541
|
+
await rest(progress, live);
|
|
2039
2542
|
}
|
|
2040
2543
|
});
|
|
2041
2544
|
}
|
|
2545
|
+
function roomsCarried(replacing) {
|
|
2546
|
+
const replaced = new Set(replacing.map(roomLane));
|
|
2547
|
+
return Promise.all([...lanes]
|
|
2548
|
+
.filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
|
|
2549
|
+
.map(([, held]) => held.chain));
|
|
2550
|
+
}
|
|
2551
|
+
const receipts = {
|
|
2552
|
+
heads: async () => (await openLocal()).receivedHeads(),
|
|
2553
|
+
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
2554
|
+
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
2555
|
+
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
2556
|
+
};
|
|
2042
2557
|
function observeStream() {
|
|
2043
2558
|
const stream = options.adapter.roomStream;
|
|
2044
2559
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -2048,6 +2563,9 @@ export function composeRuntime(options) {
|
|
|
2048
2563
|
return;
|
|
2049
2564
|
if (event.kind === 'opened') {
|
|
2050
2565
|
socketOpen = true;
|
|
2566
|
+
generation += 1;
|
|
2567
|
+
conversations.reopened();
|
|
2568
|
+
recovery.resume();
|
|
2051
2569
|
void sync('opened');
|
|
2052
2570
|
}
|
|
2053
2571
|
else if (event.kind === 'closed')
|
|
@@ -2056,7 +2574,7 @@ export function composeRuntime(options) {
|
|
|
2056
2574
|
void applyOpening(event.expired);
|
|
2057
2575
|
else
|
|
2058
2576
|
void applyRoom(event.locator, event.roomVersion);
|
|
2059
|
-
});
|
|
2577
|
+
}, receipts);
|
|
2060
2578
|
}
|
|
2061
2579
|
function writesHeld() {
|
|
2062
2580
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -2094,14 +2612,15 @@ export function composeRuntime(options) {
|
|
|
2094
2612
|
});
|
|
2095
2613
|
}
|
|
2096
2614
|
function scheduleCurrency() {
|
|
2097
|
-
if (currencyQueued || closed || stopping)
|
|
2615
|
+
if (currencyQueued === generation || closed || stopping)
|
|
2098
2616
|
return;
|
|
2099
|
-
currencyQueued =
|
|
2100
|
-
void task(runCurrency);
|
|
2617
|
+
currencyQueued = generation;
|
|
2618
|
+
void task(CONVERSATION_LANE, runCurrency);
|
|
2101
2619
|
}
|
|
2102
|
-
async function runCurrency() {
|
|
2103
|
-
currencyQueued
|
|
2104
|
-
|
|
2620
|
+
async function runCurrency(live) {
|
|
2621
|
+
if (currencyQueued === generation)
|
|
2622
|
+
currencyQueued = null;
|
|
2623
|
+
if (!live())
|
|
2105
2624
|
return;
|
|
2106
2625
|
await conversations.reconcile();
|
|
2107
2626
|
if (conversations.current()) {
|
|
@@ -2116,7 +2635,7 @@ export function composeRuntime(options) {
|
|
|
2116
2635
|
catch {
|
|
2117
2636
|
progress.reachable = false;
|
|
2118
2637
|
}
|
|
2119
|
-
await rest(progress);
|
|
2638
|
+
await rest(progress, live);
|
|
2120
2639
|
}
|
|
2121
2640
|
function close() {
|
|
2122
2641
|
if (closed)
|
|
@@ -2128,7 +2647,7 @@ export function composeRuntime(options) {
|
|
|
2128
2647
|
withdrawConversations = undefined;
|
|
2129
2648
|
withdrawServiceMode?.();
|
|
2130
2649
|
conversations.close();
|
|
2131
|
-
|
|
2650
|
+
void recovery.retire();
|
|
2132
2651
|
store?.close();
|
|
2133
2652
|
void opening?.then((opened) => {
|
|
2134
2653
|
opened.close();
|
|
@@ -2146,9 +2665,8 @@ export function composeRuntime(options) {
|
|
|
2146
2665
|
}
|
|
2147
2666
|
async function stop() {
|
|
2148
2667
|
stopping = true;
|
|
2149
|
-
|
|
2150
|
-
await
|
|
2151
|
-
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
2668
|
+
await recovery.retire();
|
|
2669
|
+
await Promise.all(working);
|
|
2152
2670
|
await mutations;
|
|
2153
2671
|
close();
|
|
2154
2672
|
}
|
|
@@ -2170,17 +2688,54 @@ export function composeRuntime(options) {
|
|
|
2170
2688
|
return planOf(input, roomFacts(reach), planReach(reach));
|
|
2171
2689
|
}
|
|
2172
2690
|
function watchPlan(plan, deliver) {
|
|
2173
|
-
|
|
2174
|
-
|
|
2691
|
+
return readSide.watch(plan, identityOf(plan), () => {
|
|
2692
|
+
const cover = covered(plan);
|
|
2693
|
+
awaitPreparation(cover);
|
|
2694
|
+
return cover;
|
|
2695
|
+
}, deliver);
|
|
2175
2696
|
}
|
|
2176
2697
|
async function searched(input, reach) {
|
|
2177
2698
|
const plan = searchPlanOf(input);
|
|
2178
|
-
const
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2699
|
+
const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
|
|
2700
|
+
for (;;) {
|
|
2701
|
+
const cover = covered(filter);
|
|
2702
|
+
awaitPreparation(cover);
|
|
2703
|
+
const pending = 'pending' in cover ? [...cover.pending] : [];
|
|
2704
|
+
const covering = 'pending' in cover ? [] : cover.coverage.rooms;
|
|
2705
|
+
const unindexed = covering
|
|
2706
|
+
.filter(({ room }) => !searchIndex.rooms().has(room))
|
|
2707
|
+
.map(({ room }) => room);
|
|
2708
|
+
for (const room of unindexed)
|
|
2709
|
+
pending.push({ room, awaits: 'index' });
|
|
2710
|
+
const held = conversations.searchable();
|
|
2711
|
+
const named = reach !== null && !reach.conversations
|
|
2712
|
+
? []
|
|
2713
|
+
: (plan.conversations ?? held).filter((one) => held.includes(one) && conversations.acquired(one));
|
|
2714
|
+
const waiting = named.filter((one) => !searchIndex.conversations().has(one));
|
|
2715
|
+
for (const conversation of waiting)
|
|
2716
|
+
pending.push({ conversation, awaits: 'index' });
|
|
2717
|
+
if (unindexed.length > 0 || waiting.length > 0)
|
|
2718
|
+
index(unindexed, waiting);
|
|
2719
|
+
if (pending.length > 0 || 'pending' in cover)
|
|
2720
|
+
return { pending };
|
|
2721
|
+
const seen = searchIndex.changes();
|
|
2722
|
+
await searchIndex.caughtUp();
|
|
2723
|
+
const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(filter);
|
|
2724
|
+
if (selected !== null && 'pending' in selected)
|
|
2725
|
+
return selected;
|
|
2726
|
+
if (searchIndex.changes() !== seen ||
|
|
2727
|
+
JSON.stringify(covered(filter)) !== JSON.stringify(cover))
|
|
2728
|
+
continue;
|
|
2729
|
+
return {
|
|
2730
|
+
...searchIndex.search(plan, selected === null ? null : selected.documents, named),
|
|
2731
|
+
coverage: {
|
|
2732
|
+
rooms: covering,
|
|
2733
|
+
conversations: named,
|
|
2734
|
+
index: searchIndex.generation(),
|
|
2735
|
+
overlay: cover.coverage.overlay,
|
|
2736
|
+
},
|
|
2737
|
+
};
|
|
2738
|
+
}
|
|
2184
2739
|
}
|
|
2185
2740
|
const watch = (topic, input, deliver) => {
|
|
2186
2741
|
if (topic === 'room-drafts') {
|
|
@@ -2399,14 +2954,16 @@ export function composeRuntime(options) {
|
|
|
2399
2954
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2400
2955
|
return;
|
|
2401
2956
|
journal.record('connection reauthorize');
|
|
2402
|
-
await
|
|
2957
|
+
await Promise.all([...lanes.values()]
|
|
2958
|
+
.filter((held) => held.generation === generation)
|
|
2959
|
+
.map((held) => held.chain));
|
|
2403
2960
|
await mutations;
|
|
2404
2961
|
authorization = undefined;
|
|
2405
2962
|
discoveryPaused = false;
|
|
2406
2963
|
if (options.adapter.roomStream === undefined)
|
|
2407
2964
|
await sync('unstreamed');
|
|
2408
2965
|
else
|
|
2409
|
-
await task(ensureAuthorization);
|
|
2966
|
+
await task(OPENING_LANE, ensureAuthorization);
|
|
2410
2967
|
},
|
|
2411
2968
|
};
|
|
2412
2969
|
const reachable = {
|
|
@@ -2418,5 +2975,5 @@ export function composeRuntime(options) {
|
|
|
2418
2975
|
watchPlan,
|
|
2419
2976
|
search: searched,
|
|
2420
2977
|
};
|
|
2421
|
-
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2978
|
+
return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2422
2979
|
}
|