@agentshouse/mdruntime 0.1.0-cd3ee807e46ae → 0.1.0-cd50a4607cb7d
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 +247 -269
- package/dist/adapter.d.ts +25 -2
- 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 +866 -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 +3 -3
- 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 +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 +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/status.d.ts +4 -0
- package/dist/status.d.ts.map +1 -1
- 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 +6 -2
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) => {
|
|
@@ -212,9 +267,16 @@ export function composeRuntime(options) {
|
|
|
212
267
|
returned: structuredClone(record.returned),
|
|
213
268
|
});
|
|
214
269
|
}
|
|
270
|
+
for (const [room, refusal] of sourceRefusals)
|
|
271
|
+
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
215
272
|
attentionRequired.sort((left, right) => {
|
|
216
273
|
if (left.room !== right.room)
|
|
217
274
|
return left.room < right.room ? -1 : 1;
|
|
275
|
+
if ('refusal' in left || 'refusal' in right) {
|
|
276
|
+
if ('refusal' in left && 'refusal' in right)
|
|
277
|
+
return 0;
|
|
278
|
+
return 'refusal' in left ? -1 : 1;
|
|
279
|
+
}
|
|
218
280
|
if (left.path !== right.path)
|
|
219
281
|
return left.path < right.path ? -1 : 1;
|
|
220
282
|
return 0;
|
|
@@ -233,12 +295,9 @@ export function composeRuntime(options) {
|
|
|
233
295
|
if (replica === undefined)
|
|
234
296
|
return 0;
|
|
235
297
|
const roots = replica.confirmed.record.typedSelection?.roots ?? [];
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
count += 1;
|
|
240
|
-
}
|
|
241
|
-
return count;
|
|
298
|
+
const weighed = outsideOf(roots);
|
|
299
|
+
return (tally(replica.visible.documents, weighed.documents) +
|
|
300
|
+
tally(replica.visible.binaries, weighed.binaries));
|
|
242
301
|
}
|
|
243
302
|
function listed() {
|
|
244
303
|
return structuredClone([...rooms.values()].map((room) => {
|
|
@@ -265,6 +324,7 @@ export function composeRuntime(options) {
|
|
|
265
324
|
return { listed: roomsListed, rooms: held };
|
|
266
325
|
}
|
|
267
326
|
function emitRooms() {
|
|
327
|
+
readSide.recover();
|
|
268
328
|
const value = listed();
|
|
269
329
|
for (const listener of roomListeners)
|
|
270
330
|
listener(value);
|
|
@@ -319,42 +379,186 @@ export function composeRuntime(options) {
|
|
|
319
379
|
}
|
|
320
380
|
}
|
|
321
381
|
function applyChangeSet(set) {
|
|
322
|
-
const held =
|
|
323
|
-
|
|
324
|
-
|
|
382
|
+
const held = {
|
|
383
|
+
...set,
|
|
384
|
+
documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
|
|
385
|
+
};
|
|
325
386
|
readSide.apply(held);
|
|
326
387
|
searchIndex.changed(held);
|
|
327
388
|
}
|
|
328
|
-
function
|
|
329
|
-
|
|
389
|
+
async function referenced(locator) {
|
|
390
|
+
for (;;) {
|
|
391
|
+
const record = replicas.get(locator)?.confirmed.record;
|
|
392
|
+
if (record === undefined || closed)
|
|
393
|
+
return;
|
|
394
|
+
const held = { text: record.documents, binaries: record.binaries };
|
|
395
|
+
const composition = compositionOf(record);
|
|
396
|
+
if (referencesIndexed(held, composition))
|
|
397
|
+
return;
|
|
398
|
+
await indexReferrers(held, composition);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async function prepareReplica(locator) {
|
|
402
|
+
await referenced(locator);
|
|
403
|
+
const replica = replicas.get(locator);
|
|
404
|
+
if (replica === undefined || closed || prepared.has(locator))
|
|
330
405
|
return;
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
406
|
+
const visible = replica.visible;
|
|
407
|
+
const paths = [
|
|
408
|
+
...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
|
|
409
|
+
];
|
|
410
|
+
const entered = [];
|
|
411
|
+
projecting.add(locator);
|
|
412
|
+
try {
|
|
413
|
+
await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
|
|
414
|
+
const current = replicas.get(locator);
|
|
415
|
+
if (current === undefined || closed)
|
|
416
|
+
return;
|
|
417
|
+
const set = entering(locator, current, chunk);
|
|
418
|
+
readSide.apply(set);
|
|
419
|
+
entered.push(set);
|
|
420
|
+
});
|
|
421
|
+
if (!replicas.has(locator) || closed)
|
|
422
|
+
return;
|
|
423
|
+
prepared.add(locator);
|
|
424
|
+
}
|
|
425
|
+
finally {
|
|
426
|
+
projecting.delete(locator);
|
|
341
427
|
}
|
|
342
|
-
|
|
428
|
+
conversations.changed(mergeChangeSets(entered));
|
|
429
|
+
readSide.recover();
|
|
430
|
+
}
|
|
431
|
+
function unprepared() {
|
|
432
|
+
return [...demanded, ...replicas.keys()].find((locator) => !prepared.has(locator));
|
|
433
|
+
}
|
|
434
|
+
function prepare(locators = []) {
|
|
435
|
+
for (const locator of locators)
|
|
436
|
+
demanded.add(locator);
|
|
437
|
+
if (preparing)
|
|
343
438
|
return;
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
439
|
+
preparing = true;
|
|
440
|
+
void (async () => {
|
|
441
|
+
try {
|
|
442
|
+
for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
|
|
443
|
+
await yielded();
|
|
444
|
+
if (closed)
|
|
445
|
+
return;
|
|
446
|
+
demanded.delete(waiting);
|
|
447
|
+
await prepareReplica(waiting);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
finally {
|
|
451
|
+
preparing = false;
|
|
452
|
+
}
|
|
453
|
+
})();
|
|
347
454
|
}
|
|
348
455
|
function reachedRooms(plan) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
456
|
+
const reach = plan.reach;
|
|
457
|
+
const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
458
|
+
const named = plan.rooms ??
|
|
459
|
+
(reached === null
|
|
460
|
+
? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
|
|
461
|
+
: [...reached]);
|
|
462
|
+
return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
|
|
463
|
+
}
|
|
464
|
+
function awaited(locator) {
|
|
465
|
+
return rooms.has(locator) || !roomsListed;
|
|
466
|
+
}
|
|
467
|
+
function covered(plan) {
|
|
468
|
+
const locators = reachedRooms(plan);
|
|
469
|
+
if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
|
|
470
|
+
return { pending: [] };
|
|
471
|
+
}
|
|
472
|
+
const pending = [];
|
|
473
|
+
const coverage = [];
|
|
474
|
+
const overlay = [];
|
|
475
|
+
for (const locator of locators) {
|
|
476
|
+
const replica = replicas.get(locator);
|
|
477
|
+
if (replica === undefined) {
|
|
478
|
+
if (awaited(locator))
|
|
479
|
+
pending.push({ room: locator, awaits: 'acquisition' });
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
if (!prepared.has(locator)) {
|
|
483
|
+
pending.push({ room: locator, awaits: 'preparation' });
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
coverage.push({
|
|
487
|
+
room: locator,
|
|
488
|
+
generation: replica.confirmed.record.transfer,
|
|
489
|
+
current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
|
|
490
|
+
});
|
|
491
|
+
for (const batch of replica.queue)
|
|
492
|
+
overlay.push(batch.id);
|
|
493
|
+
}
|
|
494
|
+
if (pending.length > 0)
|
|
495
|
+
return { pending };
|
|
496
|
+
return { coverage: { rooms: coverage, indexes: [], overlay } };
|
|
497
|
+
}
|
|
498
|
+
function awaitPreparation(cover) {
|
|
499
|
+
if (!('pending' in cover))
|
|
500
|
+
return;
|
|
501
|
+
const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
|
|
502
|
+
if (waiting.length > 0)
|
|
503
|
+
prepare(waiting.map(({ room }) => room));
|
|
504
|
+
}
|
|
505
|
+
async function rowsQuery(plan) {
|
|
506
|
+
const before = covered(plan);
|
|
507
|
+
awaitPreparation(before);
|
|
508
|
+
if ('pending' in before)
|
|
509
|
+
return before;
|
|
510
|
+
const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
|
|
511
|
+
awaitPreparation(cover);
|
|
512
|
+
if ('pending' in cover)
|
|
513
|
+
return cover;
|
|
514
|
+
return 'groups' in rows
|
|
515
|
+
? { groups: rows.groups, coverage: cover.coverage }
|
|
516
|
+
: { documents: rows.documents, coverage: cover.coverage };
|
|
517
|
+
}
|
|
518
|
+
async function indexConversation(conversation) {
|
|
519
|
+
searchIndex.indexMessages(conversation, [], false);
|
|
520
|
+
let after = null;
|
|
521
|
+
for (;;) {
|
|
522
|
+
const batch = await durably((local) => local.messagesAfter(conversation, after));
|
|
523
|
+
if (closed)
|
|
524
|
+
return;
|
|
525
|
+
searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
|
|
526
|
+
if (batch.length === 0)
|
|
527
|
+
return;
|
|
528
|
+
after = batch[batch.length - 1].identity;
|
|
529
|
+
}
|
|
354
530
|
}
|
|
355
|
-
function
|
|
356
|
-
|
|
357
|
-
|
|
531
|
+
function indexNext() {
|
|
532
|
+
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator));
|
|
533
|
+
if (room !== undefined) {
|
|
534
|
+
roomsToIndex.delete(room);
|
|
535
|
+
return () => (prepared.has(room) ? searchIndex.indexRoom(room) : undefined);
|
|
536
|
+
}
|
|
537
|
+
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
538
|
+
if (conversation === undefined)
|
|
539
|
+
return null;
|
|
540
|
+
conversationsToIndex.delete(conversation);
|
|
541
|
+
return () => indexConversation(conversation);
|
|
542
|
+
}
|
|
543
|
+
function index(rooms, named) {
|
|
544
|
+
for (const room of rooms)
|
|
545
|
+
roomsToIndex.add(room);
|
|
546
|
+
for (const conversation of named)
|
|
547
|
+
conversationsToIndex.add(conversation);
|
|
548
|
+
if (indexing)
|
|
549
|
+
return;
|
|
550
|
+
indexing = true;
|
|
551
|
+
void (async () => {
|
|
552
|
+
try {
|
|
553
|
+
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
554
|
+
await yielded();
|
|
555
|
+
await unit();
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
finally {
|
|
559
|
+
indexing = false;
|
|
560
|
+
}
|
|
561
|
+
})().catch(() => { });
|
|
358
562
|
}
|
|
359
563
|
function publishChangeSet(set) {
|
|
360
564
|
applyChangeSet(set);
|
|
@@ -371,34 +575,7 @@ export function composeRuntime(options) {
|
|
|
371
575
|
emitPresentation(app);
|
|
372
576
|
conversations.changed(set);
|
|
373
577
|
}
|
|
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
578
|
async function refuse(reason, provider = null) {
|
|
401
|
-
stopTimer();
|
|
402
579
|
discoveryPaused = true;
|
|
403
580
|
try {
|
|
404
581
|
if (reason === 'revoked-authorization') {
|
|
@@ -477,14 +654,24 @@ export function composeRuntime(options) {
|
|
|
477
654
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
478
655
|
});
|
|
479
656
|
}
|
|
480
|
-
async function applyDiscovery(
|
|
657
|
+
async function applyDiscovery(found, started) {
|
|
658
|
+
for (const room of found) {
|
|
659
|
+
const fence = fences.get(room.locator);
|
|
660
|
+
if (fence !== undefined && fence.erased && fence.at < started)
|
|
661
|
+
fences.delete(room.locator);
|
|
662
|
+
}
|
|
663
|
+
const listed = found.filter((room) => !fences.has(room.locator));
|
|
481
664
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
482
665
|
for (const locator of listedLocators)
|
|
483
666
|
scopedRefusals.delete(locator);
|
|
484
667
|
discovered.clear();
|
|
485
668
|
for (const room of listed)
|
|
486
669
|
discovered.set(room.locator, room);
|
|
487
|
-
|
|
670
|
+
const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
|
|
671
|
+
if (options.adapter.exhaustiveDiscovery === true)
|
|
672
|
+
await loseRooms(unlisted);
|
|
673
|
+
else
|
|
674
|
+
unlistedKept(unlisted);
|
|
488
675
|
for (const room of listed) {
|
|
489
676
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
490
677
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -495,6 +682,7 @@ export function composeRuntime(options) {
|
|
|
495
682
|
}
|
|
496
683
|
roomsListed = true;
|
|
497
684
|
emitRooms();
|
|
685
|
+
return listed;
|
|
498
686
|
}
|
|
499
687
|
function unlistedHeld(listed) {
|
|
500
688
|
return [
|
|
@@ -506,41 +694,71 @@ export function composeRuntime(options) {
|
|
|
506
694
|
]),
|
|
507
695
|
].filter((locator) => !listed.has(locator));
|
|
508
696
|
}
|
|
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);
|
|
697
|
+
function unlistedKept(unlisted) {
|
|
698
|
+
for (const locator of unlisted) {
|
|
699
|
+
const held = replicas.get(locator)?.confirmed.record;
|
|
700
|
+
if (held !== undefined)
|
|
701
|
+
list(held, false);
|
|
702
|
+
else
|
|
525
703
|
rooms.delete(locator);
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
function fence(locators) {
|
|
707
|
+
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
708
|
+
const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
|
|
709
|
+
room: true,
|
|
710
|
+
queue: true,
|
|
711
|
+
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
712
|
+
}));
|
|
713
|
+
for (const locator of fresh) {
|
|
714
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
715
|
+
lastFenced.set(locator, ++fencings);
|
|
716
|
+
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
717
|
+
pendings.delete(batch.id);
|
|
718
|
+
replicas.delete(locator);
|
|
719
|
+
queuesWithoutReplica.delete(locator);
|
|
720
|
+
rooms.delete(locator);
|
|
721
|
+
roomDrafts.delete(locator);
|
|
722
|
+
transfers.delete(locator);
|
|
723
|
+
catching.delete(locator);
|
|
724
|
+
discovered.delete(locator);
|
|
725
|
+
announced.delete(locator);
|
|
726
|
+
unreadable.delete(locator);
|
|
727
|
+
stale.delete(locator);
|
|
728
|
+
repairable.delete(locator);
|
|
729
|
+
scopedRefusals.delete(locator);
|
|
730
|
+
sourceRefusals.delete(locator);
|
|
731
|
+
declarationRefusals.delete(locator);
|
|
732
|
+
for (const attached of ['collection', 'typed-selection']) {
|
|
733
|
+
const key = modelKey(locator, attached);
|
|
734
|
+
models.delete(key);
|
|
735
|
+
liveModels.delete(key);
|
|
736
|
+
invalidModels.delete(key);
|
|
540
737
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
738
|
+
journal.record(`room lost ${locator}`);
|
|
739
|
+
}
|
|
740
|
+
if (fresh.length === 0)
|
|
741
|
+
return fresh;
|
|
742
|
+
publishChangeSet(mergeChangeSets(removed));
|
|
743
|
+
for (const locator of fresh)
|
|
744
|
+
prepared.delete(locator);
|
|
745
|
+
searchIndex.forget(fresh);
|
|
746
|
+
readSide.recover();
|
|
747
|
+
moveTo({ ...status, ...attention() });
|
|
748
|
+
return fresh;
|
|
749
|
+
}
|
|
750
|
+
async function erased(local, locators) {
|
|
751
|
+
await local.erase(locators);
|
|
752
|
+
for (const locator of locators) {
|
|
753
|
+
const held = fences.get(locator);
|
|
754
|
+
if (held !== undefined)
|
|
755
|
+
held.erased = true;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
async function loseRooms(locators) {
|
|
759
|
+
const fresh = fence(locators);
|
|
760
|
+
if (fresh.length > 0)
|
|
761
|
+
await durably((local) => erased(local, fresh));
|
|
544
762
|
}
|
|
545
763
|
function restored(key, persisted) {
|
|
546
764
|
const prior = models.get(key);
|
|
@@ -635,6 +853,7 @@ export function composeRuntime(options) {
|
|
|
635
853
|
async function readState(opened) {
|
|
636
854
|
const { lastCompleted, locators, read } = await hydrate(opened);
|
|
637
855
|
applyChangeSet(read);
|
|
856
|
+
prepare();
|
|
638
857
|
if (replicas.size > 0)
|
|
639
858
|
emitRooms();
|
|
640
859
|
if (member !== null)
|
|
@@ -645,8 +864,15 @@ export function composeRuntime(options) {
|
|
|
645
864
|
return lastCompleted;
|
|
646
865
|
}
|
|
647
866
|
async function open() {
|
|
648
|
-
|
|
867
|
+
if (closed)
|
|
868
|
+
throw new Error('runtime closed');
|
|
869
|
+
const opened = await openRuntimeStore(isFenced);
|
|
649
870
|
store = opened;
|
|
871
|
+
const interrupted = await opened.fences();
|
|
872
|
+
for (const locator of interrupted) {
|
|
873
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
874
|
+
lastFenced.set(locator, ++fencings);
|
|
875
|
+
}
|
|
650
876
|
const kept = options.shell.generation;
|
|
651
877
|
const storedGeneration = await opened.generation();
|
|
652
878
|
const loss = detectStorageLoss(storedGeneration, kept);
|
|
@@ -660,6 +886,8 @@ export function composeRuntime(options) {
|
|
|
660
886
|
emitStorageLoss();
|
|
661
887
|
}
|
|
662
888
|
const lastCompleted = await readState(opened);
|
|
889
|
+
if (interrupted.length > 0)
|
|
890
|
+
await erased(opened, interrupted);
|
|
663
891
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
664
892
|
return opened;
|
|
665
893
|
}
|
|
@@ -741,17 +969,19 @@ export function composeRuntime(options) {
|
|
|
741
969
|
moveTo({ ...status, ...attention() });
|
|
742
970
|
throw error;
|
|
743
971
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
972
|
+
return () => {
|
|
973
|
+
queuesWithoutReplica.delete(locator);
|
|
974
|
+
for (const { batch, returned } of unqueued) {
|
|
975
|
+
const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
|
|
976
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
977
|
+
}
|
|
978
|
+
replicas.set(locator, replicaOf(confirmed, kept, before));
|
|
979
|
+
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
980
|
+
room: settling.room,
|
|
981
|
+
queue: kept.length !== previousQueue.length,
|
|
982
|
+
drafts: [...written.keys()],
|
|
983
|
+
});
|
|
984
|
+
};
|
|
755
985
|
}
|
|
756
986
|
function adoptModels(locator, record) {
|
|
757
987
|
for (const [attached, model] of [
|
|
@@ -784,11 +1014,16 @@ export function composeRuntime(options) {
|
|
|
784
1014
|
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
785
1015
|
};
|
|
786
1016
|
}
|
|
787
|
-
function publish(local, room, confirmed, landing
|
|
1017
|
+
function publish(local, room, confirmed, landing, live) {
|
|
788
1018
|
const { base, stored } = landing;
|
|
789
1019
|
return exclusive(async () => {
|
|
790
|
-
if (
|
|
1020
|
+
if (stored !== undefined) {
|
|
1021
|
+
await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
|
|
1022
|
+
}
|
|
1023
|
+
if (!live()) {
|
|
1024
|
+
caughtUpTo(room.locator);
|
|
791
1025
|
return true;
|
|
1026
|
+
}
|
|
792
1027
|
const prior = replicas.get(room.locator);
|
|
793
1028
|
if (base !== undefined &&
|
|
794
1029
|
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
@@ -802,9 +1037,11 @@ export function composeRuntime(options) {
|
|
|
802
1037
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
803
1038
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
804
1039
|
});
|
|
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)
|
|
1040
|
+
const set = (await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) }))();
|
|
1041
|
+
if (stored !== undefined) {
|
|
807
1042
|
transfers.delete(room.locator);
|
|
1043
|
+
catching.delete(room.locator);
|
|
1044
|
+
}
|
|
808
1045
|
for (const batch of accepted)
|
|
809
1046
|
pendings.delete(batch.id);
|
|
810
1047
|
stale.delete(room.locator);
|
|
@@ -813,13 +1050,17 @@ export function composeRuntime(options) {
|
|
|
813
1050
|
sourceRefusals.delete(room.locator);
|
|
814
1051
|
declarationRefusals.delete(room.locator);
|
|
815
1052
|
list(room, true);
|
|
1053
|
+
if (prior === undefined)
|
|
1054
|
+
prepared.add(room.locator);
|
|
816
1055
|
publishChangeSet(set);
|
|
817
1056
|
moveTo({ ...status, ...attention() });
|
|
1057
|
+
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
818
1058
|
return true;
|
|
819
1059
|
});
|
|
820
1060
|
}
|
|
821
1061
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
822
1062
|
const local = await openLocal();
|
|
1063
|
+
await referenced(locator);
|
|
823
1064
|
const replica = replicas.get(locator);
|
|
824
1065
|
const room = rooms.get(locator);
|
|
825
1066
|
if (replica === undefined || room === undefined) {
|
|
@@ -867,6 +1108,11 @@ export function composeRuntime(options) {
|
|
|
867
1108
|
changes,
|
|
868
1109
|
bases: parts.flatMap(({ bases }) => bases),
|
|
869
1110
|
writtenBy,
|
|
1111
|
+
clean: {
|
|
1112
|
+
text: replica.confirmed.record.documents,
|
|
1113
|
+
binaries: replica.confirmed.record.binaries,
|
|
1114
|
+
composition: compositionOf(replica.confirmed.record),
|
|
1115
|
+
},
|
|
870
1116
|
};
|
|
871
1117
|
const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
|
|
872
1118
|
if (checked?.kind === 'refused')
|
|
@@ -937,6 +1183,8 @@ export function composeRuntime(options) {
|
|
|
937
1183
|
catch {
|
|
938
1184
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
939
1185
|
}
|
|
1186
|
+
if (fences.has(locator))
|
|
1187
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
940
1188
|
for (const origin of withdrawn.keys())
|
|
941
1189
|
pendings.delete(origin);
|
|
942
1190
|
for (const [id, drafts] of drafted) {
|
|
@@ -966,6 +1214,8 @@ export function composeRuntime(options) {
|
|
|
966
1214
|
return;
|
|
967
1215
|
}
|
|
968
1216
|
await local.submit(locator, identifier, submission, lost);
|
|
1217
|
+
if (fences.has(locator))
|
|
1218
|
+
return;
|
|
969
1219
|
pendings.set(identifier, { ...state, submission, lost });
|
|
970
1220
|
const replica = replicas.get(locator);
|
|
971
1221
|
if (replica !== undefined)
|
|
@@ -976,6 +1226,8 @@ export function composeRuntime(options) {
|
|
|
976
1226
|
if (!continuing())
|
|
977
1227
|
throw new Error('connection refused');
|
|
978
1228
|
await local.keepUpload(locator, identifier, upload);
|
|
1229
|
+
if (fences.has(locator))
|
|
1230
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
979
1231
|
pendings.set(identifier, { ...pendings.get(identifier), upload });
|
|
980
1232
|
if (!continuing())
|
|
981
1233
|
throw new Error('connection refused');
|
|
@@ -991,6 +1243,7 @@ export function composeRuntime(options) {
|
|
|
991
1243
|
}
|
|
992
1244
|
if (closed || authorization === undefined || refusal !== null)
|
|
993
1245
|
return UNCERTAIN;
|
|
1246
|
+
const since = fencings;
|
|
994
1247
|
const transfer = ['transfer', 'history', locator, path];
|
|
995
1248
|
if (query.revision !== undefined)
|
|
996
1249
|
transfer.push(query.revision);
|
|
@@ -1005,6 +1258,8 @@ export function composeRuntime(options) {
|
|
|
1005
1258
|
...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
|
|
1006
1259
|
});
|
|
1007
1260
|
journal.record(`outcome history ${result.kind}`);
|
|
1261
|
+
if (fencedSince(locator, since))
|
|
1262
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
1008
1263
|
if (result.kind === 'scoped-refusal') {
|
|
1009
1264
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
1010
1265
|
if (room !== undefined)
|
|
@@ -1027,6 +1282,12 @@ export function composeRuntime(options) {
|
|
|
1027
1282
|
}
|
|
1028
1283
|
return null;
|
|
1029
1284
|
}
|
|
1285
|
+
function binaryRoom(request) {
|
|
1286
|
+
if (request.kind === 'path')
|
|
1287
|
+
return request.locator;
|
|
1288
|
+
const scope = options.adapter.referrerScope(request.referrer);
|
|
1289
|
+
return scope?.kind === 'room' ? scope.locator : null;
|
|
1290
|
+
}
|
|
1030
1291
|
async function readBinary(request) {
|
|
1031
1292
|
if (closed || refusal !== null)
|
|
1032
1293
|
return UNCERTAIN;
|
|
@@ -1038,12 +1299,16 @@ export function composeRuntime(options) {
|
|
|
1038
1299
|
}
|
|
1039
1300
|
if (closed || authorization === undefined || refusal !== null)
|
|
1040
1301
|
return UNCERTAIN;
|
|
1302
|
+
const since = fencings;
|
|
1041
1303
|
journal.record(request.kind === 'path'
|
|
1042
1304
|
? `transfer binary ${request.locator} ${request.path}`
|
|
1043
1305
|
: `transfer binary ${request.fileRef}`);
|
|
1044
1306
|
try {
|
|
1045
1307
|
const result = await options.adapter.binary(authorization, request);
|
|
1046
1308
|
journal.record(`outcome binary ${result.kind}`);
|
|
1309
|
+
const locator = binaryRoom(request);
|
|
1310
|
+
if (locator !== null && fencedSince(locator, since))
|
|
1311
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
1047
1312
|
if (result.kind === 'scoped-refusal') {
|
|
1048
1313
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
1049
1314
|
if (room !== undefined)
|
|
@@ -1167,11 +1432,11 @@ export function composeRuntime(options) {
|
|
|
1167
1432
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
1168
1433
|
!options.adapter.reached(locator, roomVersion, written));
|
|
1169
1434
|
}
|
|
1170
|
-
async function pullRefresh(authorized, confirmed) {
|
|
1171
|
-
if (!
|
|
1435
|
+
async function pullRefresh(authorized, confirmed, live) {
|
|
1436
|
+
if (!live())
|
|
1172
1437
|
return unconfirmed;
|
|
1173
1438
|
try {
|
|
1174
|
-
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held),
|
|
1439
|
+
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
|
|
1175
1440
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
1176
1441
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
1177
1442
|
return result;
|
|
@@ -1181,47 +1446,69 @@ export function composeRuntime(options) {
|
|
|
1181
1446
|
throw error;
|
|
1182
1447
|
}
|
|
1183
1448
|
}
|
|
1184
|
-
function staging(local) {
|
|
1449
|
+
function staging(local, live) {
|
|
1185
1450
|
return {
|
|
1186
1451
|
held: (locator) => transfers.get(locator) ?? null,
|
|
1187
1452
|
staged: (record) => local.staged(record),
|
|
1188
1453
|
stage: (record, page) => exclusive(async () => {
|
|
1189
|
-
if (!
|
|
1454
|
+
if (!live())
|
|
1190
1455
|
return false;
|
|
1191
1456
|
await local.stage(record, page);
|
|
1192
1457
|
transfers.set(record.locator, record);
|
|
1458
|
+
readSide.recover();
|
|
1193
1459
|
return true;
|
|
1194
1460
|
}),
|
|
1195
1461
|
abandon: (locator) => exclusive(async () => {
|
|
1196
|
-
if (!
|
|
1462
|
+
if (!live())
|
|
1197
1463
|
return;
|
|
1198
1464
|
await local.abandon(locator);
|
|
1199
1465
|
transfers.delete(locator);
|
|
1466
|
+
readSide.recover();
|
|
1200
1467
|
}),
|
|
1201
1468
|
};
|
|
1202
1469
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1470
|
+
function reusable(room) {
|
|
1471
|
+
const record = replicas.get(room.locator)?.confirmed.record;
|
|
1472
|
+
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1473
|
+
return null;
|
|
1474
|
+
return { documents: record.documents, revisions: record.revisions };
|
|
1475
|
+
}
|
|
1476
|
+
async function pullBootstrap(authorized, room, local, live) {
|
|
1477
|
+
if (!live())
|
|
1205
1478
|
return unconfirmed;
|
|
1479
|
+
if (replicas.has(room.locator)) {
|
|
1480
|
+
catching.add(room.locator);
|
|
1481
|
+
readSide.recover();
|
|
1482
|
+
}
|
|
1206
1483
|
try {
|
|
1207
1484
|
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
1208
1485
|
collection: modelOf(room.locator),
|
|
1209
1486
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
1210
|
-
}, (models, held) => remodel(room.locator, models, held),
|
|
1487
|
+
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
1211
1488
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
1212
1489
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1490
|
+
if (result.kind !== 'staged')
|
|
1491
|
+
caughtUpTo(room.locator);
|
|
1213
1492
|
return result;
|
|
1214
1493
|
}
|
|
1215
1494
|
catch (error) {
|
|
1216
1495
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
1496
|
+
caughtUpTo(room.locator);
|
|
1217
1497
|
throw error;
|
|
1218
1498
|
}
|
|
1219
1499
|
}
|
|
1220
|
-
|
|
1500
|
+
function caughtUpTo(locator) {
|
|
1501
|
+
if (catching.delete(locator))
|
|
1502
|
+
readSide.recover();
|
|
1503
|
+
}
|
|
1504
|
+
async function advance(room, local, authorized, live) {
|
|
1505
|
+
await referenced(room.locator);
|
|
1506
|
+
if (!live())
|
|
1507
|
+
return unconfirmed;
|
|
1221
1508
|
const held = replicas.get(room.locator);
|
|
1222
1509
|
if (held !== undefined) {
|
|
1223
|
-
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
1224
|
-
if (!
|
|
1510
|
+
const refreshed = await pullRefresh(authorized, held.confirmed, live);
|
|
1511
|
+
if (!live())
|
|
1225
1512
|
return unconfirmed;
|
|
1226
1513
|
if (refreshed.kind === 'still-current') {
|
|
1227
1514
|
stale.delete(room.locator);
|
|
@@ -1235,31 +1522,45 @@ export function composeRuntime(options) {
|
|
|
1235
1522
|
return null;
|
|
1236
1523
|
}
|
|
1237
1524
|
if (refreshed.kind === 'advanced') {
|
|
1238
|
-
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
|
|
1525
|
+
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
|
|
1239
1526
|
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1240
1527
|
const reached = now !== undefined &&
|
|
1241
1528
|
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
1242
|
-
return reached ? null : advance(room, local, authorized);
|
|
1529
|
+
return reached ? null : advance(room, local, authorized, live);
|
|
1243
1530
|
}
|
|
1244
1531
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1245
1532
|
return null;
|
|
1246
1533
|
}
|
|
1247
1534
|
if (refreshed.kind !== 'beyond-horizon')
|
|
1248
1535
|
return refreshed;
|
|
1536
|
+
if (opened !== null) {
|
|
1537
|
+
owed.add(room.locator);
|
|
1538
|
+
readSide.recover();
|
|
1539
|
+
return unconfirmed;
|
|
1540
|
+
}
|
|
1249
1541
|
}
|
|
1250
|
-
const bootstrapped = await pullBootstrap(authorized, room, local);
|
|
1251
|
-
if (!
|
|
1542
|
+
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
1543
|
+
if (!live())
|
|
1252
1544
|
return unconfirmed;
|
|
1253
1545
|
if (bootstrapped.kind !== 'staged')
|
|
1254
1546
|
return bootstrapped;
|
|
1255
|
-
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
1547
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
1256
1548
|
return null;
|
|
1257
1549
|
}
|
|
1258
1550
|
async function withhold(withheld, room, local, progress) {
|
|
1551
|
+
if (withheld.kind === 'unconfirmed')
|
|
1552
|
+
return;
|
|
1259
1553
|
if (withheld.kind === 'refused')
|
|
1260
1554
|
stale.delete(room.locator);
|
|
1261
1555
|
else
|
|
1262
1556
|
stale.add(room.locator);
|
|
1557
|
+
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
1558
|
+
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
1559
|
+
repairable.set(room.locator, reset);
|
|
1560
|
+
repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
|
|
1561
|
+
}
|
|
1562
|
+
else
|
|
1563
|
+
repairable.delete(room.locator);
|
|
1263
1564
|
if (withheld.kind === 'scoped-refusal') {
|
|
1264
1565
|
await scoped(withheld, room, local);
|
|
1265
1566
|
}
|
|
@@ -1295,6 +1596,7 @@ export function composeRuntime(options) {
|
|
|
1295
1596
|
unreadable.add(room.locator);
|
|
1296
1597
|
list(room, false);
|
|
1297
1598
|
emitRooms();
|
|
1599
|
+
moveTo({ ...status, ...attention() });
|
|
1298
1600
|
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
1299
1601
|
}
|
|
1300
1602
|
function leaving(batch, result) {
|
|
@@ -1316,6 +1618,7 @@ export function composeRuntime(options) {
|
|
|
1316
1618
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1317
1619
|
}
|
|
1318
1620
|
async function adoptAccepted(room, local, submitted, accepted) {
|
|
1621
|
+
await referenced(room.locator);
|
|
1319
1622
|
if (!continuing())
|
|
1320
1623
|
return unconfirmed;
|
|
1321
1624
|
const current = replicas.get(room.locator);
|
|
@@ -1332,16 +1635,17 @@ export function composeRuntime(options) {
|
|
|
1332
1635
|
if (applied.kind !== 'staged') {
|
|
1333
1636
|
const state = pendings.get(submitted.id);
|
|
1334
1637
|
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1335
|
-
if (state !== undefined)
|
|
1638
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
1336
1639
|
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1337
1640
|
emitQueue(room.locator);
|
|
1338
1641
|
return applied;
|
|
1339
1642
|
}
|
|
1340
|
-
const
|
|
1643
|
+
const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
1341
1644
|
pendings.delete(submitted.id);
|
|
1342
1645
|
unreadable.delete(room.locator);
|
|
1343
1646
|
sourceRefusals.delete(room.locator);
|
|
1344
1647
|
declarationRefusals.delete(room.locator);
|
|
1648
|
+
const set = settled();
|
|
1345
1649
|
list(room, true);
|
|
1346
1650
|
publishChangeSet(set);
|
|
1347
1651
|
moveTo({ ...status, ...attention() });
|
|
@@ -1355,14 +1659,14 @@ export function composeRuntime(options) {
|
|
|
1355
1659
|
refusedSource(room, failed);
|
|
1356
1660
|
return false;
|
|
1357
1661
|
}
|
|
1358
|
-
async function submitRoom(room, local, authorized, progress) {
|
|
1359
|
-
while (continuing() && authorization !== undefined) {
|
|
1662
|
+
async function submitRoom(room, local, authorized, progress, signal) {
|
|
1663
|
+
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
1360
1664
|
const replica = replicas.get(room.locator);
|
|
1361
1665
|
const head = replica?.queue[0];
|
|
1362
1666
|
if (replica === undefined || head === undefined)
|
|
1363
|
-
return;
|
|
1667
|
+
return 'done';
|
|
1364
1668
|
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1365
|
-
return;
|
|
1669
|
+
return 'done';
|
|
1366
1670
|
const state = pendings.get(head.id);
|
|
1367
1671
|
const { submission, lost: earlier } = state ?? {
|
|
1368
1672
|
submission: { state: 'waiting' },
|
|
@@ -1370,24 +1674,30 @@ export function composeRuntime(options) {
|
|
|
1370
1674
|
};
|
|
1371
1675
|
if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
|
|
1372
1676
|
progress.complete = false;
|
|
1373
|
-
return;
|
|
1677
|
+
return { at: Date.parse(submission.reset) };
|
|
1374
1678
|
}
|
|
1375
1679
|
if (writesHeld()) {
|
|
1376
1680
|
progress.complete = false;
|
|
1377
1681
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1378
|
-
return;
|
|
1682
|
+
return 'done';
|
|
1379
1683
|
}
|
|
1380
1684
|
const lost = earlier || submission.state === 'submitted';
|
|
1685
|
+
if (lost && pastHorizon(head.id)) {
|
|
1686
|
+
await discardPending(room, local, signal);
|
|
1687
|
+
return 'done';
|
|
1688
|
+
}
|
|
1381
1689
|
let result;
|
|
1382
1690
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
1383
1691
|
await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
|
|
1384
|
-
if (discoveryPaused)
|
|
1385
|
-
return;
|
|
1386
|
-
if (writesHeld()) {
|
|
1692
|
+
if (discoveryPaused || fences.has(room.locator))
|
|
1693
|
+
return 'done';
|
|
1694
|
+
if (signal.aborted || writesHeld()) {
|
|
1387
1695
|
await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
|
|
1696
|
+
if (signal.aborted)
|
|
1697
|
+
return 'done';
|
|
1388
1698
|
progress.complete = false;
|
|
1389
1699
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1390
|
-
return;
|
|
1700
|
+
return 'done';
|
|
1391
1701
|
}
|
|
1392
1702
|
const opensBefore = openSignals;
|
|
1393
1703
|
const answered = awaitingAnswer(room.locator);
|
|
@@ -1403,32 +1713,36 @@ export function composeRuntime(options) {
|
|
|
1403
1713
|
progress.complete = false;
|
|
1404
1714
|
if (continuing())
|
|
1405
1715
|
progress.reachable = false;
|
|
1406
|
-
return;
|
|
1716
|
+
return 'again';
|
|
1717
|
+
}
|
|
1718
|
+
if (signal.aborted) {
|
|
1719
|
+
answered();
|
|
1720
|
+
return 'done';
|
|
1407
1721
|
}
|
|
1408
1722
|
if (result.kind !== 'accepted' || !continuing())
|
|
1409
1723
|
answered();
|
|
1410
1724
|
if (!continuing())
|
|
1411
|
-
return;
|
|
1725
|
+
return 'done';
|
|
1412
1726
|
if (result.kind === 'transport-uncertainty') {
|
|
1413
1727
|
progress.complete = false;
|
|
1414
1728
|
progress.reachable = false;
|
|
1415
|
-
return;
|
|
1729
|
+
return 'again';
|
|
1416
1730
|
}
|
|
1417
1731
|
if (result.kind === 'room-not-readable') {
|
|
1418
1732
|
progress.complete = false;
|
|
1419
1733
|
unreadable.add(room.locator);
|
|
1420
1734
|
list(room, false);
|
|
1421
1735
|
emitRooms();
|
|
1422
|
-
return;
|
|
1736
|
+
return 'done';
|
|
1423
1737
|
}
|
|
1424
1738
|
if (result.kind === 'revoked-authorization') {
|
|
1425
1739
|
await refuse('revoked-authorization');
|
|
1426
|
-
return;
|
|
1740
|
+
return 'done';
|
|
1427
1741
|
}
|
|
1428
1742
|
if (result.kind === 'scoped-refusal') {
|
|
1429
1743
|
await scoped(result, room, local);
|
|
1430
1744
|
progress.complete = false;
|
|
1431
|
-
return;
|
|
1745
|
+
return 'done';
|
|
1432
1746
|
}
|
|
1433
1747
|
if (result.kind === 'write-halted') {
|
|
1434
1748
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
@@ -1437,16 +1751,24 @@ export function composeRuntime(options) {
|
|
|
1437
1751
|
continue;
|
|
1438
1752
|
haltedRefusal = true;
|
|
1439
1753
|
progress.complete = false;
|
|
1440
|
-
return;
|
|
1754
|
+
return 'done';
|
|
1441
1755
|
}
|
|
1442
1756
|
if (result.kind === 'conflicted' ||
|
|
1443
1757
|
result.kind === 'rejected' ||
|
|
1444
1758
|
result.kind === 'expired' ||
|
|
1445
1759
|
result.kind === 'retryable-refusal') {
|
|
1760
|
+
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
1761
|
+
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
1762
|
+
await discardPending(room, local, signal);
|
|
1763
|
+
return 'done';
|
|
1764
|
+
}
|
|
1446
1765
|
if (result.kind === 'expired') {
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1766
|
+
await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
|
|
1767
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
1768
|
+
if (withheld !== null) {
|
|
1769
|
+
await withhold(withheld, room, local, progress);
|
|
1770
|
+
return 'done';
|
|
1771
|
+
}
|
|
1450
1772
|
}
|
|
1451
1773
|
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
|
|
1452
1774
|
(result.kind === 'conflicted' && result.roomVersion !== undefined
|
|
@@ -1459,8 +1781,7 @@ export function composeRuntime(options) {
|
|
|
1459
1781
|
const reset = result.reset;
|
|
1460
1782
|
progress.complete = false;
|
|
1461
1783
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1462
|
-
|
|
1463
|
-
return;
|
|
1784
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
1464
1785
|
}
|
|
1465
1786
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
1466
1787
|
const left = leaving(head, result);
|
|
@@ -1471,37 +1792,82 @@ export function composeRuntime(options) {
|
|
|
1471
1792
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
1472
1793
|
return;
|
|
1473
1794
|
}
|
|
1474
|
-
const
|
|
1795
|
+
const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
1475
1796
|
leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
|
|
1476
1797
|
(result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
|
|
1477
1798
|
? { ...left, returned: null }
|
|
1478
1799
|
: left,
|
|
1479
1800
|
});
|
|
1480
|
-
publishChangeSet(
|
|
1801
|
+
publishChangeSet(settled());
|
|
1481
1802
|
moveTo({ ...status, ...attention() });
|
|
1482
1803
|
});
|
|
1483
1804
|
if (result.kind === 'expired')
|
|
1484
1805
|
continue;
|
|
1485
|
-
const withheld = await advance(room, local, authorized);
|
|
1486
|
-
if (withheld !== null)
|
|
1487
|
-
|
|
1806
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
1807
|
+
if (withheld !== null) {
|
|
1808
|
+
await withhold(withheld, room, local, progress);
|
|
1809
|
+
return 'done';
|
|
1810
|
+
}
|
|
1488
1811
|
continue;
|
|
1489
1812
|
}
|
|
1490
1813
|
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1491
1814
|
if (!kept) {
|
|
1492
1815
|
progress.complete = false;
|
|
1493
|
-
return;
|
|
1816
|
+
return 'done';
|
|
1494
1817
|
}
|
|
1495
1818
|
}
|
|
1819
|
+
return 'done';
|
|
1820
|
+
}
|
|
1821
|
+
function pastHorizon(identity) {
|
|
1822
|
+
return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
|
|
1823
|
+
}
|
|
1824
|
+
async function discardPending(room, local, signal) {
|
|
1825
|
+
const { locator } = room;
|
|
1826
|
+
const discarded = await exclusive(async () => {
|
|
1827
|
+
if (fences.has(locator))
|
|
1828
|
+
return false;
|
|
1829
|
+
const before = replicas.get(locator);
|
|
1830
|
+
const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
1831
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
1832
|
+
lastFenced.set(locator, ++fencings);
|
|
1833
|
+
try {
|
|
1834
|
+
await local.discard(locator);
|
|
1835
|
+
}
|
|
1836
|
+
finally {
|
|
1837
|
+
fences.delete(locator);
|
|
1838
|
+
}
|
|
1839
|
+
for (const batch of queue)
|
|
1840
|
+
pendings.delete(batch.id);
|
|
1841
|
+
queuesWithoutReplica.delete(locator);
|
|
1842
|
+
const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
|
|
1843
|
+
if (after !== null)
|
|
1844
|
+
replicas.set(locator, after);
|
|
1845
|
+
journal.record(`room discarded ${locator}`);
|
|
1846
|
+
publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
|
|
1847
|
+
moveTo({ ...status, ...attention() });
|
|
1848
|
+
return true;
|
|
1849
|
+
});
|
|
1850
|
+
if (!discarded)
|
|
1851
|
+
return;
|
|
1852
|
+
const progress = { complete: true, reachable: true };
|
|
1853
|
+
await roomTask(locator, async (live) => {
|
|
1854
|
+
await Promise.race([
|
|
1855
|
+
reload(room, local, progress, () => live() && !signal.aborted),
|
|
1856
|
+
abandonedOn(signal),
|
|
1857
|
+
]);
|
|
1858
|
+
});
|
|
1859
|
+
if (signal.aborted)
|
|
1860
|
+
return;
|
|
1861
|
+
await rest(progress);
|
|
1496
1862
|
}
|
|
1497
1863
|
function awaitingAnswer(locator) {
|
|
1498
1864
|
let release = () => { };
|
|
1499
1865
|
const awaited = new Promise((resolve) => {
|
|
1500
1866
|
release = resolve;
|
|
1501
1867
|
});
|
|
1502
|
-
answering.set(locator, awaited);
|
|
1868
|
+
answering.set(locator, { generation, answered: awaited });
|
|
1503
1869
|
return () => {
|
|
1504
|
-
if (answering.get(locator) === awaited)
|
|
1870
|
+
if (answering.get(locator)?.answered === awaited)
|
|
1505
1871
|
answering.delete(locator);
|
|
1506
1872
|
release();
|
|
1507
1873
|
};
|
|
@@ -1617,8 +1983,10 @@ export function composeRuntime(options) {
|
|
|
1617
1983
|
];
|
|
1618
1984
|
});
|
|
1619
1985
|
}
|
|
1620
|
-
async function synchronize(listed, local, progress, attempt) {
|
|
1986
|
+
async function synchronize(listed, local, progress, attempt, live) {
|
|
1621
1987
|
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
|
|
1988
|
+
if (!live())
|
|
1989
|
+
return;
|
|
1622
1990
|
const ready = listed.filter((_, index) => {
|
|
1623
1991
|
const outcome = prepared[index];
|
|
1624
1992
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
@@ -1627,9 +1995,10 @@ export function composeRuntime(options) {
|
|
|
1627
1995
|
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
1628
1996
|
handed = true;
|
|
1629
1997
|
}
|
|
1998
|
+
discovering?.release();
|
|
1630
1999
|
const loaded = await Promise.allSettled(ready
|
|
1631
2000
|
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1632
|
-
.map((room) => loadRoom(room, local, progress)));
|
|
2001
|
+
.map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
|
|
1633
2002
|
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1634
2003
|
if (failed !== undefined)
|
|
1635
2004
|
throw failed.reason;
|
|
@@ -1658,14 +2027,14 @@ export function composeRuntime(options) {
|
|
|
1658
2027
|
}
|
|
1659
2028
|
return true;
|
|
1660
2029
|
}
|
|
1661
|
-
async function caughtUp(room, local, progress) {
|
|
2030
|
+
async function caughtUp(room, local, progress, live) {
|
|
1662
2031
|
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1663
|
-
if (held === undefined || authorization === undefined)
|
|
2032
|
+
if (held === undefined || authorization === undefined || !live())
|
|
1664
2033
|
return;
|
|
1665
2034
|
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
1666
2035
|
if (!behind)
|
|
1667
2036
|
return;
|
|
1668
|
-
const withheld = await advance(room, local, authorization);
|
|
2037
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1669
2038
|
if (withheld !== null)
|
|
1670
2039
|
await withhold(withheld, room, local, progress);
|
|
1671
2040
|
}
|
|
@@ -1675,39 +2044,46 @@ export function composeRuntime(options) {
|
|
|
1675
2044
|
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1676
2045
|
});
|
|
1677
2046
|
}
|
|
1678
|
-
async function loadRoom(room, local, progress) {
|
|
1679
|
-
if (
|
|
2047
|
+
async function loadRoom(room, local, progress, live) {
|
|
2048
|
+
if (!live() || authorization === undefined)
|
|
1680
2049
|
return;
|
|
1681
2050
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1682
|
-
await reload(room, local, progress);
|
|
2051
|
+
await reload(room, local, progress, live);
|
|
1683
2052
|
return;
|
|
1684
2053
|
}
|
|
1685
|
-
const withheld = await advance(room, local, authorization);
|
|
2054
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1686
2055
|
if (withheld !== null) {
|
|
1687
2056
|
await withhold(withheld, room, local, progress);
|
|
1688
2057
|
return;
|
|
1689
2058
|
}
|
|
1690
|
-
await caughtUp(room, local, progress);
|
|
2059
|
+
await caughtUp(room, local, progress, live);
|
|
1691
2060
|
}
|
|
1692
|
-
async function reload(room, local, progress) {
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
2061
|
+
async function reload(room, local, progress, live) {
|
|
2062
|
+
try {
|
|
2063
|
+
if (!live() || authorization === undefined)
|
|
2064
|
+
return;
|
|
2065
|
+
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
2066
|
+
if (!live())
|
|
2067
|
+
return;
|
|
2068
|
+
if (bootstrapped.kind !== 'staged') {
|
|
2069
|
+
await withhold(bootstrapped, room, local, progress);
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
2073
|
+
await caughtUp(room, local, progress, live);
|
|
2074
|
+
}
|
|
2075
|
+
finally {
|
|
2076
|
+
caughtUpTo(room.locator);
|
|
1701
2077
|
}
|
|
1702
|
-
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
1703
|
-
await caughtUp(room, local, progress);
|
|
1704
2078
|
}
|
|
1705
|
-
async function discoverRooms(local, progress, attempt) {
|
|
2079
|
+
async function discoverRooms(local, progress, attempt, live) {
|
|
1706
2080
|
if (closed || authorization === undefined) {
|
|
1707
2081
|
notifyReady();
|
|
1708
2082
|
return;
|
|
1709
2083
|
}
|
|
1710
2084
|
moveTo(transferring(status));
|
|
2085
|
+
discoveries += 1;
|
|
2086
|
+
const started = discoveries;
|
|
1711
2087
|
let result;
|
|
1712
2088
|
try {
|
|
1713
2089
|
result = await options.adapter.discover(authorization);
|
|
@@ -1715,7 +2091,8 @@ export function composeRuntime(options) {
|
|
|
1715
2091
|
catch {
|
|
1716
2092
|
result = { kind: 'transport-uncertainty' };
|
|
1717
2093
|
}
|
|
1718
|
-
if (
|
|
2094
|
+
if (!live()) {
|
|
2095
|
+
progress.complete = false;
|
|
1719
2096
|
notifyReady();
|
|
1720
2097
|
return;
|
|
1721
2098
|
}
|
|
@@ -1773,6 +2150,7 @@ export function composeRuntime(options) {
|
|
|
1773
2150
|
}
|
|
1774
2151
|
const previousCapabilities = capabilities;
|
|
1775
2152
|
capabilities = result.capabilities;
|
|
2153
|
+
outcomeRetention = result.retention.outcome;
|
|
1776
2154
|
notifyReady();
|
|
1777
2155
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1778
2156
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
@@ -1785,8 +2163,7 @@ export function composeRuntime(options) {
|
|
|
1785
2163
|
member = signedIn;
|
|
1786
2164
|
emitMember();
|
|
1787
2165
|
}
|
|
1788
|
-
await applyDiscovery(result.rooms, local);
|
|
1789
|
-
await synchronize(result.rooms, local, progress, attempt);
|
|
2166
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
1790
2167
|
}
|
|
1791
2168
|
async function ensureAuthorization() {
|
|
1792
2169
|
if (authorization !== undefined)
|
|
@@ -1800,20 +2177,50 @@ export function composeRuntime(options) {
|
|
|
1800
2177
|
throw error;
|
|
1801
2178
|
}
|
|
1802
2179
|
}
|
|
1803
|
-
function
|
|
2180
|
+
async function admittedRead(work) {
|
|
2181
|
+
if (roomReads.generation !== generation) {
|
|
2182
|
+
roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
2183
|
+
}
|
|
2184
|
+
const reads = roomReads;
|
|
2185
|
+
while (reads.free === 0)
|
|
2186
|
+
await new Promise((admit) => reads.waiting.push(admit));
|
|
2187
|
+
reads.free -= 1;
|
|
2188
|
+
try {
|
|
2189
|
+
await work();
|
|
2190
|
+
}
|
|
2191
|
+
finally {
|
|
2192
|
+
reads.free += 1;
|
|
2193
|
+
reads.waiting.shift()?.();
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
function roomTask(locator, work) {
|
|
2197
|
+
return task(roomLane(locator), (live) => admittedRead(() => work(live)));
|
|
2198
|
+
}
|
|
2199
|
+
function task(lane, work) {
|
|
1804
2200
|
if (stopping)
|
|
1805
|
-
return
|
|
1806
|
-
|
|
1807
|
-
|
|
2201
|
+
return Promise.resolve();
|
|
2202
|
+
const held = lanes.get(lane);
|
|
2203
|
+
const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
|
|
2204
|
+
const turn = current.generation;
|
|
2205
|
+
const live = () => turn === generation && continuing();
|
|
2206
|
+
const run = current.chain.then(() => work(live)).catch(() => undefined);
|
|
2207
|
+
current.chain = run;
|
|
2208
|
+
lanes.set(lane, current);
|
|
2209
|
+
working.add(run);
|
|
2210
|
+
void run.finally(() => working.delete(run));
|
|
2211
|
+
return run;
|
|
1808
2212
|
}
|
|
1809
2213
|
function sync(attempt) {
|
|
1810
|
-
return task(async () => {
|
|
1811
|
-
if (
|
|
2214
|
+
return task(OPENING_LANE, async (live) => {
|
|
2215
|
+
if (!live()) {
|
|
1812
2216
|
notifyReady();
|
|
1813
2217
|
return;
|
|
1814
2218
|
}
|
|
1815
2219
|
const progress = { complete: true, reachable: true };
|
|
1816
|
-
|
|
2220
|
+
const gate = discover();
|
|
2221
|
+
if (attempt === 'opened')
|
|
2222
|
+
opened = generation;
|
|
2223
|
+
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
1817
2224
|
if (attempt === 'opened')
|
|
1818
2225
|
streamed = progress;
|
|
1819
2226
|
complete = false;
|
|
@@ -1822,12 +2229,12 @@ export function composeRuntime(options) {
|
|
|
1822
2229
|
handed = false;
|
|
1823
2230
|
try {
|
|
1824
2231
|
const local = await openLocal();
|
|
1825
|
-
if (
|
|
2232
|
+
if (!live())
|
|
1826
2233
|
return;
|
|
1827
2234
|
await ensureAuthorization();
|
|
1828
|
-
if (
|
|
2235
|
+
if (!live())
|
|
1829
2236
|
return;
|
|
1830
|
-
await discoverRooms(local, progress, attempt);
|
|
2237
|
+
await discoverRooms(local, progress, attempt, live);
|
|
1831
2238
|
}
|
|
1832
2239
|
catch {
|
|
1833
2240
|
progress.complete = false;
|
|
@@ -1835,17 +2242,21 @@ export function composeRuntime(options) {
|
|
|
1835
2242
|
progress.reachable = false;
|
|
1836
2243
|
}
|
|
1837
2244
|
finally {
|
|
2245
|
+
gate.release();
|
|
1838
2246
|
notifyReady();
|
|
1839
2247
|
}
|
|
2248
|
+
if (!live())
|
|
2249
|
+
return;
|
|
1840
2250
|
if (attempt !== 'opened') {
|
|
1841
|
-
await finishOpening(progress);
|
|
2251
|
+
await finishOpening(progress, live);
|
|
1842
2252
|
return;
|
|
1843
2253
|
}
|
|
1844
2254
|
if (handed || closed || progress.reachable)
|
|
1845
2255
|
return;
|
|
1846
2256
|
underway = null;
|
|
2257
|
+
opened = null;
|
|
1847
2258
|
journal.record('connection reopen');
|
|
1848
|
-
await rest(progress);
|
|
2259
|
+
await rest(progress, live);
|
|
1849
2260
|
options.adapter.roomStream?.reopen();
|
|
1850
2261
|
});
|
|
1851
2262
|
}
|
|
@@ -1866,24 +2277,41 @@ export function composeRuntime(options) {
|
|
|
1866
2277
|
});
|
|
1867
2278
|
}
|
|
1868
2279
|
function applyOpening(expired) {
|
|
1869
|
-
|
|
1870
|
-
|
|
2280
|
+
const progress = underway?.generation === generation ? underway.progress : null;
|
|
2281
|
+
const cycle = progress ?? { complete: true, reachable: true };
|
|
2282
|
+
const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
|
|
2283
|
+
if (opened === generation)
|
|
2284
|
+
opened = null;
|
|
2285
|
+
return task(OPENING_LANE, async (live) => {
|
|
1871
2286
|
underway = null;
|
|
1872
|
-
if (
|
|
2287
|
+
if (!live())
|
|
1873
2288
|
return;
|
|
1874
2289
|
if (progress === null) {
|
|
1875
|
-
await
|
|
2290
|
+
await replacing;
|
|
2291
|
+
await rest(cycle, live);
|
|
1876
2292
|
return;
|
|
1877
2293
|
}
|
|
2294
|
+
await roomsCarried(expired);
|
|
2295
|
+
if (!live())
|
|
2296
|
+
return;
|
|
2297
|
+
const replaced = new Set(owed);
|
|
2298
|
+
owed.clear();
|
|
1878
2299
|
try {
|
|
1879
2300
|
const local = await openLocal();
|
|
1880
2301
|
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
1881
|
-
const reloading = held.filter((room) => expired.includes(room.locator)
|
|
1882
|
-
|
|
2302
|
+
const reloading = held.filter((room) => !expired.includes(room.locator) &&
|
|
2303
|
+
(replaced.has(room.locator) || revoked(room.locator)));
|
|
2304
|
+
for (const room of reloading)
|
|
2305
|
+
catching.add(room.locator);
|
|
2306
|
+
readSide.recover();
|
|
2307
|
+
const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
|
|
2308
|
+
const reloaded = [...(await reloads), ...(await replacing)];
|
|
1883
2309
|
if (progress.reachable) {
|
|
1884
|
-
for (const room of held)
|
|
1885
|
-
if (!reloading.includes(room))
|
|
2310
|
+
for (const room of held) {
|
|
2311
|
+
if (!expired.includes(room.locator) && !reloading.includes(room)) {
|
|
1886
2312
|
await confirmModels(room, local);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
1887
2315
|
}
|
|
1888
2316
|
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1889
2317
|
if (failed !== undefined)
|
|
@@ -1893,21 +2321,21 @@ export function composeRuntime(options) {
|
|
|
1893
2321
|
progress.complete = false;
|
|
1894
2322
|
progress.reachable = false;
|
|
1895
2323
|
}
|
|
1896
|
-
|
|
2324
|
+
if (live())
|
|
2325
|
+
await finishOpening(progress, live);
|
|
1897
2326
|
});
|
|
1898
2327
|
}
|
|
1899
|
-
async function
|
|
1900
|
-
const progress = { complete: true, reachable: true };
|
|
2328
|
+
async function reloadNow(replaced, progress) {
|
|
1901
2329
|
try {
|
|
1902
2330
|
const local = await openLocal();
|
|
1903
|
-
await Promise.allSettled([...discovered.values()]
|
|
1904
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
1905
|
-
.map((room) => reload(room, local, progress)));
|
|
2331
|
+
return await Promise.allSettled([...discovered.values()]
|
|
2332
|
+
.filter((room) => replicas.has(room.locator) && replaced(room))
|
|
2333
|
+
.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
1906
2334
|
}
|
|
1907
2335
|
catch {
|
|
1908
2336
|
progress.reachable = false;
|
|
2337
|
+
return [];
|
|
1909
2338
|
}
|
|
1910
|
-
await rest(progress);
|
|
1911
2339
|
}
|
|
1912
2340
|
async function confirmModels(room, local) {
|
|
1913
2341
|
for (const attached of ['collection', 'typed-selection']) {
|
|
@@ -1920,38 +2348,94 @@ export function composeRuntime(options) {
|
|
|
1920
2348
|
function submitLater(locator) {
|
|
1921
2349
|
if (localSaves)
|
|
1922
2350
|
return;
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
2351
|
+
void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
|
|
2352
|
+
}
|
|
2353
|
+
async function submitPass(locator, signal) {
|
|
2354
|
+
const room = discovered.get(locator);
|
|
2355
|
+
if (closed || stopping || discoveryPaused || room === undefined)
|
|
2356
|
+
return 'done';
|
|
2357
|
+
const progress = { complete: true, reachable: true };
|
|
2358
|
+
let next;
|
|
2359
|
+
try {
|
|
2360
|
+
await ensureAuthorization();
|
|
2361
|
+
if (authorization === undefined)
|
|
2362
|
+
return 'done';
|
|
2363
|
+
next = await submitRoom(room, await openLocal(), authorization, progress, signal);
|
|
2364
|
+
}
|
|
2365
|
+
catch {
|
|
2366
|
+
progress.reachable = false;
|
|
2367
|
+
next = 'again';
|
|
2368
|
+
}
|
|
2369
|
+
if (signal.aborted)
|
|
2370
|
+
return 'done';
|
|
2371
|
+
await rest(progress);
|
|
2372
|
+
return next === 'again' && !socketOpen ? 'done' : next;
|
|
2373
|
+
}
|
|
2374
|
+
function repairLater(locator, when) {
|
|
2375
|
+
if (!socketOpen || repairing.has(locator))
|
|
1926
2376
|
return;
|
|
2377
|
+
void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
|
|
2378
|
+
}
|
|
2379
|
+
async function repairPass(locator, signal) {
|
|
2380
|
+
const room = discovered.get(locator);
|
|
2381
|
+
if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
|
|
2382
|
+
return 'done';
|
|
2383
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2384
|
+
return 'done';
|
|
2385
|
+
const progress = { complete: true, reachable: true };
|
|
2386
|
+
repairing.add(locator);
|
|
2387
|
+
try {
|
|
2388
|
+
const local = await openLocal();
|
|
2389
|
+
await roomTask(locator, async (live) => {
|
|
2390
|
+
const current = () => live() && !signal.aborted;
|
|
2391
|
+
if (!current() || authorization === undefined)
|
|
2392
|
+
return;
|
|
2393
|
+
const withheld = await Promise.race([
|
|
2394
|
+
advance(room, local, authorization, current),
|
|
2395
|
+
abandonedOn(signal),
|
|
2396
|
+
]);
|
|
2397
|
+
if (withheld !== null && current())
|
|
2398
|
+
await withhold(withheld, room, local, progress);
|
|
2399
|
+
});
|
|
1927
2400
|
}
|
|
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
|
-
|
|
2401
|
+
catch {
|
|
2402
|
+
stale.add(locator);
|
|
2403
|
+
progress.reachable = false;
|
|
2404
|
+
}
|
|
2405
|
+
finally {
|
|
2406
|
+
repairing.delete(locator);
|
|
2407
|
+
}
|
|
2408
|
+
if (signal.aborted)
|
|
2409
|
+
return 'done';
|
|
2410
|
+
await rest(progress);
|
|
2411
|
+
if (!stale.has(locator)) {
|
|
2412
|
+
if ((replicas.get(locator)?.queue.length ?? 0) > 0)
|
|
2413
|
+
submitLater(locator);
|
|
2414
|
+
return 'done';
|
|
2415
|
+
}
|
|
2416
|
+
if (!repairable.has(locator))
|
|
2417
|
+
return 'done';
|
|
2418
|
+
const reset = repairable.get(locator);
|
|
2419
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
2420
|
+
}
|
|
2421
|
+
function resendLater() {
|
|
2422
|
+
if (!socketOpen)
|
|
2423
|
+
return;
|
|
2424
|
+
void recovery.attempt('resend', resendPass, { again: true });
|
|
2425
|
+
}
|
|
2426
|
+
async function resendPass(signal) {
|
|
2427
|
+
if (!socketOpen || closed || stopping || discoveryPaused)
|
|
2428
|
+
return 'done';
|
|
2429
|
+
const reading = { complete: true, reachable: true };
|
|
2430
|
+
try {
|
|
2431
|
+
await conversations.resend(reading);
|
|
2432
|
+
}
|
|
2433
|
+
catch {
|
|
2434
|
+
reading.reachable = false;
|
|
2435
|
+
}
|
|
2436
|
+
if (signal.aborted)
|
|
2437
|
+
return 'done';
|
|
2438
|
+
return reading.reachable && reading.complete ? 'done' : 'again';
|
|
1955
2439
|
}
|
|
1956
2440
|
function submitAll() {
|
|
1957
2441
|
for (const [locator, replica] of replicas) {
|
|
@@ -1959,31 +2443,40 @@ export function composeRuntime(options) {
|
|
|
1959
2443
|
submitLater(locator);
|
|
1960
2444
|
}
|
|
1961
2445
|
}
|
|
1962
|
-
async function finishOpening(progress) {
|
|
2446
|
+
async function finishOpening(progress, live) {
|
|
1963
2447
|
try {
|
|
1964
|
-
if (
|
|
2448
|
+
if (!live()) {
|
|
1965
2449
|
progress.complete = false;
|
|
1966
2450
|
return;
|
|
1967
2451
|
}
|
|
1968
2452
|
submitAll();
|
|
2453
|
+
for (const locator of stale)
|
|
2454
|
+
if (repairable.has(locator))
|
|
2455
|
+
repairLater(locator, {});
|
|
1969
2456
|
await conversations.reconcile();
|
|
1970
2457
|
if (status.providerUnreachable)
|
|
1971
2458
|
conversations.reconnected();
|
|
1972
2459
|
const reading = { complete: true, reachable: true };
|
|
1973
2460
|
await conversations.catchUp(reading);
|
|
1974
|
-
if (
|
|
2461
|
+
if (progress.reachable && reading.reachable)
|
|
2462
|
+
await conversations.resend(reading);
|
|
2463
|
+
if (!reading.reachable) {
|
|
1975
2464
|
progress.reachable = false;
|
|
2465
|
+
resendLater();
|
|
2466
|
+
}
|
|
1976
2467
|
}
|
|
1977
2468
|
catch {
|
|
1978
2469
|
progress.complete = false;
|
|
1979
2470
|
}
|
|
1980
2471
|
finally {
|
|
1981
|
-
|
|
1982
|
-
|
|
2472
|
+
if (live()) {
|
|
2473
|
+
complete = progress.complete;
|
|
2474
|
+
await rest(progress, live);
|
|
2475
|
+
}
|
|
1983
2476
|
}
|
|
1984
2477
|
}
|
|
1985
|
-
async function rest(progress) {
|
|
1986
|
-
if (
|
|
2478
|
+
async function rest(progress, live = () => !closed) {
|
|
2479
|
+
if (!live())
|
|
1987
2480
|
return;
|
|
1988
2481
|
let lastCompleted = status.lastCompleted;
|
|
1989
2482
|
let synced = complete && stale.size === 0 && !pending();
|
|
@@ -1991,6 +2484,8 @@ export function composeRuntime(options) {
|
|
|
1991
2484
|
const completedAt = new Date().toISOString();
|
|
1992
2485
|
try {
|
|
1993
2486
|
await (await openLocal()).complete(completedAt);
|
|
2487
|
+
if (!live())
|
|
2488
|
+
return;
|
|
1994
2489
|
recorded = true;
|
|
1995
2490
|
lastCompleted = completedAt;
|
|
1996
2491
|
}
|
|
@@ -2005,11 +2500,23 @@ export function composeRuntime(options) {
|
|
|
2005
2500
|
...attention(),
|
|
2006
2501
|
});
|
|
2007
2502
|
}
|
|
2503
|
+
function discover() {
|
|
2504
|
+
let release = () => { };
|
|
2505
|
+
const done = new Promise((resolve) => {
|
|
2506
|
+
release = resolve;
|
|
2507
|
+
});
|
|
2508
|
+
discovering = { generation, done, release };
|
|
2509
|
+
return discovering;
|
|
2510
|
+
}
|
|
2008
2511
|
function applyRoom(locator, roomVersion) {
|
|
2009
2512
|
announced.set(locator, roomVersion);
|
|
2010
|
-
return task(async () => {
|
|
2011
|
-
|
|
2012
|
-
|
|
2513
|
+
return task(roomLane(locator), async (live) => {
|
|
2514
|
+
if (discovering?.generation === generation)
|
|
2515
|
+
await discovering.done;
|
|
2516
|
+
const saving = answering.get(locator);
|
|
2517
|
+
if (saving?.generation === generation)
|
|
2518
|
+
await saving.answered;
|
|
2519
|
+
if (!live() || authorization === undefined)
|
|
2013
2520
|
return;
|
|
2014
2521
|
const room = discovered.get(locator);
|
|
2015
2522
|
const held = replicas.get(locator);
|
|
@@ -2020,25 +2527,41 @@ export function composeRuntime(options) {
|
|
|
2020
2527
|
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
2021
2528
|
return;
|
|
2022
2529
|
}
|
|
2023
|
-
const during = underway;
|
|
2530
|
+
const during = underway?.generation === generation ? underway.progress : null;
|
|
2024
2531
|
const progress = during ?? { complete: true, reachable: true };
|
|
2025
2532
|
if (during === null)
|
|
2026
2533
|
moveTo(transferring(status));
|
|
2027
2534
|
try {
|
|
2028
2535
|
const local = await openLocal();
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2536
|
+
await admittedRead(async () => {
|
|
2537
|
+
if (!live() || authorization === undefined)
|
|
2538
|
+
return;
|
|
2539
|
+
const withheld = await advance(room, local, authorization, live);
|
|
2540
|
+
if (withheld !== null)
|
|
2541
|
+
await withhold(withheld, room, local, progress);
|
|
2542
|
+
});
|
|
2032
2543
|
}
|
|
2033
2544
|
catch {
|
|
2034
2545
|
stale.add(locator);
|
|
2035
2546
|
}
|
|
2036
2547
|
finally {
|
|
2037
2548
|
if (during === null)
|
|
2038
|
-
await rest(progress);
|
|
2549
|
+
await rest(progress, live);
|
|
2039
2550
|
}
|
|
2040
2551
|
});
|
|
2041
2552
|
}
|
|
2553
|
+
function roomsCarried(replacing) {
|
|
2554
|
+
const replaced = new Set(replacing.map(roomLane));
|
|
2555
|
+
return Promise.all([...lanes]
|
|
2556
|
+
.filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
|
|
2557
|
+
.map(([, held]) => held.chain));
|
|
2558
|
+
}
|
|
2559
|
+
const receipts = {
|
|
2560
|
+
heads: async () => (await openLocal()).receivedHeads(),
|
|
2561
|
+
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
2562
|
+
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
2563
|
+
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
2564
|
+
};
|
|
2042
2565
|
function observeStream() {
|
|
2043
2566
|
const stream = options.adapter.roomStream;
|
|
2044
2567
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -2048,6 +2571,9 @@ export function composeRuntime(options) {
|
|
|
2048
2571
|
return;
|
|
2049
2572
|
if (event.kind === 'opened') {
|
|
2050
2573
|
socketOpen = true;
|
|
2574
|
+
generation += 1;
|
|
2575
|
+
conversations.reopened();
|
|
2576
|
+
recovery.resume();
|
|
2051
2577
|
void sync('opened');
|
|
2052
2578
|
}
|
|
2053
2579
|
else if (event.kind === 'closed')
|
|
@@ -2056,7 +2582,7 @@ export function composeRuntime(options) {
|
|
|
2056
2582
|
void applyOpening(event.expired);
|
|
2057
2583
|
else
|
|
2058
2584
|
void applyRoom(event.locator, event.roomVersion);
|
|
2059
|
-
});
|
|
2585
|
+
}, receipts);
|
|
2060
2586
|
}
|
|
2061
2587
|
function writesHeld() {
|
|
2062
2588
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -2094,14 +2620,15 @@ export function composeRuntime(options) {
|
|
|
2094
2620
|
});
|
|
2095
2621
|
}
|
|
2096
2622
|
function scheduleCurrency() {
|
|
2097
|
-
if (currencyQueued || closed || stopping)
|
|
2623
|
+
if (currencyQueued === generation || closed || stopping)
|
|
2098
2624
|
return;
|
|
2099
|
-
currencyQueued =
|
|
2100
|
-
void task(runCurrency);
|
|
2625
|
+
currencyQueued = generation;
|
|
2626
|
+
void task(CONVERSATION_LANE, runCurrency);
|
|
2101
2627
|
}
|
|
2102
|
-
async function runCurrency() {
|
|
2103
|
-
currencyQueued
|
|
2104
|
-
|
|
2628
|
+
async function runCurrency(live) {
|
|
2629
|
+
if (currencyQueued === generation)
|
|
2630
|
+
currencyQueued = null;
|
|
2631
|
+
if (!live())
|
|
2105
2632
|
return;
|
|
2106
2633
|
await conversations.reconcile();
|
|
2107
2634
|
if (conversations.current()) {
|
|
@@ -2116,7 +2643,7 @@ export function composeRuntime(options) {
|
|
|
2116
2643
|
catch {
|
|
2117
2644
|
progress.reachable = false;
|
|
2118
2645
|
}
|
|
2119
|
-
await rest(progress);
|
|
2646
|
+
await rest(progress, live);
|
|
2120
2647
|
}
|
|
2121
2648
|
function close() {
|
|
2122
2649
|
if (closed)
|
|
@@ -2128,7 +2655,7 @@ export function composeRuntime(options) {
|
|
|
2128
2655
|
withdrawConversations = undefined;
|
|
2129
2656
|
withdrawServiceMode?.();
|
|
2130
2657
|
conversations.close();
|
|
2131
|
-
|
|
2658
|
+
void recovery.retire();
|
|
2132
2659
|
store?.close();
|
|
2133
2660
|
void opening?.then((opened) => {
|
|
2134
2661
|
opened.close();
|
|
@@ -2146,9 +2673,8 @@ export function composeRuntime(options) {
|
|
|
2146
2673
|
}
|
|
2147
2674
|
async function stop() {
|
|
2148
2675
|
stopping = true;
|
|
2149
|
-
|
|
2150
|
-
await
|
|
2151
|
-
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
2676
|
+
await recovery.retire();
|
|
2677
|
+
await Promise.all(working);
|
|
2152
2678
|
await mutations;
|
|
2153
2679
|
close();
|
|
2154
2680
|
}
|
|
@@ -2170,17 +2696,54 @@ export function composeRuntime(options) {
|
|
|
2170
2696
|
return planOf(input, roomFacts(reach), planReach(reach));
|
|
2171
2697
|
}
|
|
2172
2698
|
function watchPlan(plan, deliver) {
|
|
2173
|
-
|
|
2174
|
-
|
|
2699
|
+
return readSide.watch(plan, identityOf(plan), () => {
|
|
2700
|
+
const cover = covered(plan);
|
|
2701
|
+
awaitPreparation(cover);
|
|
2702
|
+
return cover;
|
|
2703
|
+
}, deliver);
|
|
2175
2704
|
}
|
|
2176
2705
|
async function searched(input, reach) {
|
|
2177
2706
|
const plan = searchPlanOf(input);
|
|
2178
|
-
const
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2707
|
+
const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
|
|
2708
|
+
for (;;) {
|
|
2709
|
+
const cover = covered(filter);
|
|
2710
|
+
awaitPreparation(cover);
|
|
2711
|
+
const pending = 'pending' in cover ? [...cover.pending] : [];
|
|
2712
|
+
const covering = 'pending' in cover ? [] : cover.coverage.rooms;
|
|
2713
|
+
const unindexed = covering
|
|
2714
|
+
.filter(({ room }) => !searchIndex.rooms().has(room))
|
|
2715
|
+
.map(({ room }) => room);
|
|
2716
|
+
for (const room of unindexed)
|
|
2717
|
+
pending.push({ room, awaits: 'index' });
|
|
2718
|
+
const held = conversations.searchable();
|
|
2719
|
+
const named = reach !== null && !reach.conversations
|
|
2720
|
+
? []
|
|
2721
|
+
: (plan.conversations ?? held).filter((one) => held.includes(one) && conversations.acquired(one));
|
|
2722
|
+
const waiting = named.filter((one) => !searchIndex.conversations().has(one));
|
|
2723
|
+
for (const conversation of waiting)
|
|
2724
|
+
pending.push({ conversation, awaits: 'index' });
|
|
2725
|
+
if (unindexed.length > 0 || waiting.length > 0)
|
|
2726
|
+
index(unindexed, waiting);
|
|
2727
|
+
if (pending.length > 0 || 'pending' in cover)
|
|
2728
|
+
return { pending };
|
|
2729
|
+
const seen = searchIndex.changes();
|
|
2730
|
+
await searchIndex.caughtUp();
|
|
2731
|
+
const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(filter);
|
|
2732
|
+
if (selected !== null && 'pending' in selected)
|
|
2733
|
+
return selected;
|
|
2734
|
+
if (searchIndex.changes() !== seen ||
|
|
2735
|
+
JSON.stringify(covered(filter)) !== JSON.stringify(cover))
|
|
2736
|
+
continue;
|
|
2737
|
+
return {
|
|
2738
|
+
...searchIndex.search(plan, selected === null ? null : selected.documents, named),
|
|
2739
|
+
coverage: {
|
|
2740
|
+
rooms: covering,
|
|
2741
|
+
conversations: named,
|
|
2742
|
+
index: searchIndex.generation(),
|
|
2743
|
+
overlay: cover.coverage.overlay,
|
|
2744
|
+
},
|
|
2745
|
+
};
|
|
2746
|
+
}
|
|
2184
2747
|
}
|
|
2185
2748
|
const watch = (topic, input, deliver) => {
|
|
2186
2749
|
if (topic === 'room-drafts') {
|
|
@@ -2399,14 +2962,16 @@ export function composeRuntime(options) {
|
|
|
2399
2962
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2400
2963
|
return;
|
|
2401
2964
|
journal.record('connection reauthorize');
|
|
2402
|
-
await
|
|
2965
|
+
await Promise.all([...lanes.values()]
|
|
2966
|
+
.filter((held) => held.generation === generation)
|
|
2967
|
+
.map((held) => held.chain));
|
|
2403
2968
|
await mutations;
|
|
2404
2969
|
authorization = undefined;
|
|
2405
2970
|
discoveryPaused = false;
|
|
2406
2971
|
if (options.adapter.roomStream === undefined)
|
|
2407
2972
|
await sync('unstreamed');
|
|
2408
2973
|
else
|
|
2409
|
-
await task(ensureAuthorization);
|
|
2974
|
+
await task(OPENING_LANE, ensureAuthorization);
|
|
2410
2975
|
},
|
|
2411
2976
|
};
|
|
2412
2977
|
const reachable = {
|
|
@@ -2418,5 +2983,5 @@ export function composeRuntime(options) {
|
|
|
2418
2983
|
watchPlan,
|
|
2419
2984
|
search: searched,
|
|
2420
2985
|
};
|
|
2421
|
-
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2986
|
+
return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2422
2987
|
}
|