@agentshouse/mdruntime 0.1.0-c71223552f22c → 0.1.0-c766beb2a980a
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 +257 -272
- package/dist/acquisition.d.ts +14 -0
- package/dist/acquisition.d.ts.map +1 -0
- package/dist/acquisition.js +24 -0
- package/dist/adapter.d.ts +35 -10
- 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/kit.d.ts +2 -1
- package/dist/chat/kit.d.ts.map +1 -1
- package/dist/chat/kit.js +2 -2
- package/dist/chat/list.d.ts +8 -1
- package/dist/chat/list.d.ts.map +1 -1
- package/dist/chat/list.js +10 -6
- 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/messages.d.ts +1 -0
- package/dist/chat/messages.d.ts.map +1 -1
- package/dist/chat/messages.js +4 -0
- package/dist/chat/provider.d.ts +1 -1
- package/dist/chat/provider.d.ts.map +1 -1
- package/dist/chat/provider.js +51 -67
- package/dist/chat/ready.d.ts +1 -0
- package/dist/chat/ready.d.ts.map +1 -1
- package/dist/chat/ready.js +4 -4
- package/dist/chat/style.js +4 -4
- package/dist/client.d.ts +12 -8
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +3 -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 +22 -7
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +1253 -310
- package/dist/conversation.d.ts +14 -7
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +328 -137
- package/dist/durability.d.ts +7 -1
- package/dist/durability.d.ts.map +1 -1
- 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 +14 -3
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +52 -10
- 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 +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/memory.d.ts +3 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +16 -2
- package/dist/model.d.ts +4 -2
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +10 -1
- package/dist/observation.d.ts +5 -2
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +22 -11
- package/dist/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 +29 -3
- package/dist/query.d.ts.map +1 -1
- package/dist/reach.d.ts +6 -3
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +17 -5
- package/dist/react.d.ts +5 -3
- package/dist/react.d.ts.map +1 -1
- package/dist/read-side.d.ts +13 -6
- package/dist/read-side.d.ts.map +1 -1
- package/dist/read-side.js +282 -83
- package/dist/readiness.d.ts +53 -0
- package/dist/readiness.d.ts.map +1 -0
- package/dist/readiness.js +33 -0
- package/dist/recovery.d.ts +25 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +197 -0
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +11 -10
- package/dist/replica.d.ts +1 -1
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +49 -32
- package/dist/room-draft.d.ts +1 -1
- package/dist/room-draft.d.ts.map +1 -1
- 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 +34 -6
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +116 -45
- package/dist/shared.d.ts +11 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +368 -0
- package/dist/status.d.ts +4 -0
- package/dist/status.d.ts.map +1 -1
- package/dist/store.d.ts +43 -11
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +308 -83
- package/dist/turn.d.ts +7 -0
- package/dist/turn.d.ts.map +1 -0
- package/dist/turn.js +45 -0
- package/dist/wire.d.ts +136 -32
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +48 -31
- package/package.json +12 -8
package/dist/connection.js
CHANGED
|
@@ -6,21 +6,27 @@ import { createReadSide } from './read-side.js';
|
|
|
6
6
|
import { markdownPath, roomOfKey } from './rows.js';
|
|
7
7
|
import { createSearchIndex, searchPlanOf, } from './search.js';
|
|
8
8
|
import { bootstrapRoom } from './bootstrap.js';
|
|
9
|
-
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
9
|
+
import { changeSet, entering, mergeChangeSets } from './change-set.js';
|
|
10
10
|
import { createConversationStore, } from './conversation.js';
|
|
11
11
|
import { queuedFile } from './change.js';
|
|
12
|
-
import { validRoot } from './collection.js';
|
|
12
|
+
import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
|
|
13
13
|
import { runtimeError } from './failure.js';
|
|
14
14
|
import { createLog } from './log.js';
|
|
15
15
|
import { binaryRecord } from './binary.js';
|
|
16
|
-
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 { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
|
|
25
|
+
import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
|
|
26
|
+
import { FencedMap } from './fence.js';
|
|
27
|
+
import { tally } from './shared.js';
|
|
28
|
+
import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
|
|
29
|
+
import { identityTime, roomDraftRevision } from './identity.js';
|
|
24
30
|
import { beneath, normalize } from './paths.js';
|
|
25
31
|
import { openRuntimeStore, } from './store.js';
|
|
26
32
|
import { unconfirmed } from './transfer.js';
|
|
@@ -38,23 +44,59 @@ function declaredSelection(room) {
|
|
|
38
44
|
root !== '' &&
|
|
39
45
|
selection.roots.every((other, at) => at === index || !within(other, root)));
|
|
40
46
|
}
|
|
47
|
+
const OPENING_LANE = 'opening';
|
|
48
|
+
const CONVERSATION_LANE = 'conversations';
|
|
49
|
+
const ROOM_READS = 256;
|
|
50
|
+
const ROOM_ATTEMPTS = 16;
|
|
51
|
+
const outside = new Map();
|
|
52
|
+
function outsideOf(roots) {
|
|
53
|
+
const key = JSON.stringify(roots);
|
|
54
|
+
const held = outside.get(key);
|
|
55
|
+
if (held !== undefined)
|
|
56
|
+
return held;
|
|
57
|
+
const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
|
|
58
|
+
const made = {
|
|
59
|
+
documents: (path) => (markdownPath(path) ? counted(path) : 0),
|
|
60
|
+
binaries: counted,
|
|
61
|
+
};
|
|
62
|
+
outside.set(key, made);
|
|
63
|
+
return made;
|
|
64
|
+
}
|
|
65
|
+
function abandonedOn(signal) {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
if (signal.aborted)
|
|
68
|
+
resolve(null);
|
|
69
|
+
else
|
|
70
|
+
signal.addEventListener('abort', () => resolve(null), { once: true });
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function roomLane(locator) {
|
|
74
|
+
return `room ${locator}`;
|
|
75
|
+
}
|
|
41
76
|
export function composeRuntime(options) {
|
|
42
77
|
const localSaves = options.adapter.localSaves === true;
|
|
43
78
|
let authorization = options.authorization;
|
|
44
79
|
let opening;
|
|
45
80
|
let store;
|
|
46
|
-
const
|
|
47
|
-
const
|
|
81
|
+
const fences = new Map();
|
|
82
|
+
const isFenced = (locator) => fences.has(locator);
|
|
83
|
+
let fencings = 0;
|
|
84
|
+
const lastFenced = new Map();
|
|
85
|
+
const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
|
|
86
|
+
let discoveries = 0;
|
|
87
|
+
const replicas = new FencedMap(isFenced);
|
|
88
|
+
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
48
89
|
const pendings = new Map();
|
|
49
|
-
const roomDrafts = new
|
|
90
|
+
const roomDrafts = new FencedMap(isFenced);
|
|
50
91
|
const presentations = new Map();
|
|
51
92
|
let limits = [];
|
|
52
93
|
let capabilities = [];
|
|
53
|
-
const rooms = new
|
|
94
|
+
const rooms = new FencedMap(isFenced);
|
|
54
95
|
const counted = new Map();
|
|
55
96
|
const unreadable = new Set();
|
|
56
|
-
const
|
|
57
|
-
const
|
|
97
|
+
const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
|
|
98
|
+
const models = new FencedMap(isModelFenced);
|
|
99
|
+
const liveModels = new FencedMap(isModelFenced);
|
|
58
100
|
const invalidModels = new Set();
|
|
59
101
|
const discovered = new Map();
|
|
60
102
|
let roomsListed = false;
|
|
@@ -84,7 +126,6 @@ export function composeRuntime(options) {
|
|
|
84
126
|
let status = STARTING;
|
|
85
127
|
let shown = STARTING;
|
|
86
128
|
let discoveryPaused = false;
|
|
87
|
-
let resetTimer;
|
|
88
129
|
let closed = false;
|
|
89
130
|
let stopping = false;
|
|
90
131
|
let withdrawStream;
|
|
@@ -92,12 +133,21 @@ export function composeRuntime(options) {
|
|
|
92
133
|
let underway = null;
|
|
93
134
|
let streamed = null;
|
|
94
135
|
let handed = false;
|
|
95
|
-
const
|
|
136
|
+
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
137
|
+
const repairing = new Set();
|
|
138
|
+
const repairable = new Map();
|
|
139
|
+
let outcomeRetention = null;
|
|
96
140
|
const answering = new Map();
|
|
97
141
|
let complete = false;
|
|
98
142
|
let recorded = false;
|
|
99
143
|
const stale = new Set();
|
|
100
|
-
let
|
|
144
|
+
let generation = 0;
|
|
145
|
+
let discovering = null;
|
|
146
|
+
let opened = null;
|
|
147
|
+
const owed = new Set();
|
|
148
|
+
const lanes = new Map();
|
|
149
|
+
let roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
150
|
+
const working = new Set();
|
|
101
151
|
let mutations = Promise.resolve();
|
|
102
152
|
const roomListeners = new Set();
|
|
103
153
|
const refusalListeners = new Set();
|
|
@@ -108,7 +158,7 @@ export function composeRuntime(options) {
|
|
|
108
158
|
const presentationListeners = new Set();
|
|
109
159
|
const readSide = createReadSide();
|
|
110
160
|
let withdrawConversations;
|
|
111
|
-
let currencyQueued =
|
|
161
|
+
let currencyQueued = null;
|
|
112
162
|
let socketOpen = false;
|
|
113
163
|
let serviceMode = 'open';
|
|
114
164
|
let haltedRefusal = false;
|
|
@@ -116,24 +166,41 @@ export function composeRuntime(options) {
|
|
|
116
166
|
const serviceModeListeners = new Set();
|
|
117
167
|
let member = null;
|
|
118
168
|
const memberListeners = new Set();
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
const
|
|
169
|
+
const prepared = new Set();
|
|
170
|
+
const projecting = new Set();
|
|
171
|
+
const unpreparedDemand = new Set();
|
|
172
|
+
const unindexable = new Set();
|
|
173
|
+
let preparing = false;
|
|
174
|
+
let demand = WHOLE_DEMAND;
|
|
175
|
+
let narrowing = null;
|
|
176
|
+
let targets = {};
|
|
177
|
+
let persistence = options.shell.persistence;
|
|
178
|
+
let capacity = null;
|
|
179
|
+
const paused = new Set();
|
|
180
|
+
let rechecking = null;
|
|
181
|
+
const readinessWatches = new Set();
|
|
182
|
+
let readinessQueued = false;
|
|
183
|
+
const catching = new Set();
|
|
184
|
+
const transfers = new FencedMap(isFenced);
|
|
122
185
|
const conversations = createConversationStore({
|
|
123
186
|
agents: declaredAgents,
|
|
124
|
-
documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
|
|
187
|
+
documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
|
|
125
188
|
call: (name, input) => invokeAction(name, input),
|
|
126
189
|
durably,
|
|
127
190
|
stream: () => options.adapter.conversations,
|
|
128
191
|
publish: publishChangeSet,
|
|
192
|
+
online: () => !status.providerUnreachable && refusal === null,
|
|
129
193
|
closed: () => closed,
|
|
194
|
+
unanswered: resendLater,
|
|
195
|
+
expired: (operation) => pastHorizon(operation),
|
|
130
196
|
});
|
|
131
197
|
const searchIndex = createSearchIndex({
|
|
132
|
-
|
|
198
|
+
room: (room) => readSide.room(room),
|
|
133
199
|
document: (room, path) => readSide.document(room, path),
|
|
134
|
-
messages: () => conversations.messages(),
|
|
135
|
-
message: (conversation, identity) => conversations.message(conversation, identity),
|
|
136
200
|
});
|
|
201
|
+
const roomsToIndex = new Set();
|
|
202
|
+
const conversationsToIndex = new Set();
|
|
203
|
+
let indexing = false;
|
|
137
204
|
const journal = createLog();
|
|
138
205
|
let notifyReady = () => { };
|
|
139
206
|
const ready = new Promise((resolve) => {
|
|
@@ -212,9 +279,16 @@ export function composeRuntime(options) {
|
|
|
212
279
|
returned: structuredClone(record.returned),
|
|
213
280
|
});
|
|
214
281
|
}
|
|
282
|
+
for (const [room, refusal] of sourceRefusals)
|
|
283
|
+
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
215
284
|
attentionRequired.sort((left, right) => {
|
|
216
285
|
if (left.room !== right.room)
|
|
217
286
|
return left.room < right.room ? -1 : 1;
|
|
287
|
+
if ('refusal' in left || 'refusal' in right) {
|
|
288
|
+
if ('refusal' in left && 'refusal' in right)
|
|
289
|
+
return 0;
|
|
290
|
+
return 'refusal' in left ? -1 : 1;
|
|
291
|
+
}
|
|
218
292
|
if (left.path !== right.path)
|
|
219
293
|
return left.path < right.path ? -1 : 1;
|
|
220
294
|
return 0;
|
|
@@ -233,12 +307,9 @@ export function composeRuntime(options) {
|
|
|
233
307
|
if (replica === undefined)
|
|
234
308
|
return 0;
|
|
235
309
|
const roots = replica.confirmed.record.typedSelection?.roots ?? [];
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
count += 1;
|
|
240
|
-
}
|
|
241
|
-
return count;
|
|
310
|
+
const weighed = outsideOf(roots);
|
|
311
|
+
return (tally(replica.visible.documents, weighed.documents) +
|
|
312
|
+
tally(replica.visible.binaries, weighed.binaries));
|
|
242
313
|
}
|
|
243
314
|
function listed() {
|
|
244
315
|
return structuredClone([...rooms.values()].map((room) => {
|
|
@@ -265,15 +336,19 @@ export function composeRuntime(options) {
|
|
|
265
336
|
return { listed: roomsListed, rooms: held };
|
|
266
337
|
}
|
|
267
338
|
function emitRooms() {
|
|
339
|
+
readSide.recover();
|
|
340
|
+
queueReadiness();
|
|
268
341
|
const value = listed();
|
|
269
342
|
for (const listener of roomListeners)
|
|
270
343
|
listener(value);
|
|
271
344
|
}
|
|
272
345
|
function emitRefusal() {
|
|
346
|
+
queueReadiness();
|
|
273
347
|
for (const listener of refusalListeners)
|
|
274
348
|
listener(refusal);
|
|
275
349
|
}
|
|
276
350
|
function emitStorageLoss() {
|
|
351
|
+
queueReadiness();
|
|
277
352
|
for (const listener of storageLossListeners)
|
|
278
353
|
listener(storageLoss);
|
|
279
354
|
}
|
|
@@ -290,7 +365,8 @@ export function composeRuntime(options) {
|
|
|
290
365
|
function show() {
|
|
291
366
|
if (closed)
|
|
292
367
|
return;
|
|
293
|
-
|
|
368
|
+
queueReadiness();
|
|
369
|
+
const next = status.state === 'synced' && demand.conversations && !conversations.current()
|
|
294
370
|
? { ...status, state: 'syncing' }
|
|
295
371
|
: status;
|
|
296
372
|
if (same(shown, next))
|
|
@@ -319,42 +395,472 @@ export function composeRuntime(options) {
|
|
|
319
395
|
}
|
|
320
396
|
}
|
|
321
397
|
function applyChangeSet(set) {
|
|
322
|
-
const held =
|
|
323
|
-
|
|
324
|
-
|
|
398
|
+
const held = {
|
|
399
|
+
...set,
|
|
400
|
+
documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
|
|
401
|
+
};
|
|
325
402
|
readSide.apply(held);
|
|
326
403
|
searchIndex.changed(held);
|
|
327
404
|
}
|
|
328
|
-
function
|
|
329
|
-
|
|
405
|
+
async function referenced(locator) {
|
|
406
|
+
for (;;) {
|
|
407
|
+
const record = replicas.get(locator)?.confirmed.record;
|
|
408
|
+
if (record === undefined || closed)
|
|
409
|
+
return;
|
|
410
|
+
const held = { text: record.documents, binaries: record.binaries };
|
|
411
|
+
const composition = compositionOf(record);
|
|
412
|
+
if (referencesIndexed(held, composition))
|
|
413
|
+
return;
|
|
414
|
+
await indexReferrers(held, composition);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
async function prepareReplica(locator) {
|
|
418
|
+
await referenced(locator);
|
|
419
|
+
const replica = replicas.get(locator);
|
|
420
|
+
if (replica === undefined || closed || prepared.has(locator))
|
|
330
421
|
return;
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
422
|
+
const visible = replica.visible;
|
|
423
|
+
const paths = [
|
|
424
|
+
...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
|
|
425
|
+
];
|
|
426
|
+
const entered = [];
|
|
427
|
+
projecting.add(locator);
|
|
428
|
+
try {
|
|
429
|
+
await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
|
|
430
|
+
const current = replicas.get(locator);
|
|
431
|
+
if (current === undefined || closed)
|
|
432
|
+
return;
|
|
433
|
+
const set = entering(locator, current, chunk);
|
|
434
|
+
readSide.apply(set);
|
|
435
|
+
entered.push(set);
|
|
436
|
+
});
|
|
437
|
+
if (!replicas.has(locator) || closed)
|
|
438
|
+
return;
|
|
439
|
+
prepared.add(locator);
|
|
440
|
+
}
|
|
441
|
+
finally {
|
|
442
|
+
projecting.delete(locator);
|
|
341
443
|
}
|
|
342
|
-
|
|
444
|
+
conversations.changed(mergeChangeSets(entered));
|
|
445
|
+
readSide.recover();
|
|
446
|
+
queueReadiness();
|
|
447
|
+
}
|
|
448
|
+
function unprepared() {
|
|
449
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
|
|
450
|
+
}
|
|
451
|
+
function unbuilt(locator, error) {
|
|
452
|
+
unindexable.add(locator);
|
|
453
|
+
journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
|
|
454
|
+
queueReadiness();
|
|
455
|
+
}
|
|
456
|
+
function prepare(locators = []) {
|
|
457
|
+
for (const locator of locators)
|
|
458
|
+
unpreparedDemand.add(locator);
|
|
459
|
+
if (preparing)
|
|
343
460
|
return;
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
461
|
+
preparing = true;
|
|
462
|
+
void (async () => {
|
|
463
|
+
try {
|
|
464
|
+
for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
|
|
465
|
+
await yielded();
|
|
466
|
+
if (closed)
|
|
467
|
+
return;
|
|
468
|
+
unpreparedDemand.delete(waiting);
|
|
469
|
+
await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
finally {
|
|
473
|
+
preparing = false;
|
|
474
|
+
}
|
|
475
|
+
})();
|
|
347
476
|
}
|
|
348
477
|
function reachedRooms(plan) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
478
|
+
const reach = plan.reach;
|
|
479
|
+
const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
480
|
+
const named = plan.rooms ??
|
|
481
|
+
(reached === null
|
|
482
|
+
? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
|
|
483
|
+
: [...reached]);
|
|
484
|
+
return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
|
|
485
|
+
}
|
|
486
|
+
function awaited(locator) {
|
|
487
|
+
return rooms.has(locator) || !roomsListed;
|
|
488
|
+
}
|
|
489
|
+
function covered(plan) {
|
|
490
|
+
const locators = reachedRooms(plan);
|
|
491
|
+
if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
|
|
492
|
+
return { pending: [] };
|
|
493
|
+
}
|
|
494
|
+
const pending = [];
|
|
495
|
+
const coverage = [];
|
|
496
|
+
const overlay = [];
|
|
497
|
+
for (const locator of locators) {
|
|
498
|
+
const replica = replicas.get(locator);
|
|
499
|
+
if (replica === undefined) {
|
|
500
|
+
if (awaited(locator))
|
|
501
|
+
pending.push({ room: locator, awaits: 'acquisition' });
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (!prepared.has(locator)) {
|
|
505
|
+
pending.push({ room: locator, awaits: 'preparation' });
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
coverage.push({
|
|
509
|
+
room: locator,
|
|
510
|
+
generation: replica.confirmed.record.transfer,
|
|
511
|
+
current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
|
|
512
|
+
});
|
|
513
|
+
for (const batch of replica.queue)
|
|
514
|
+
overlay.push(batch.id);
|
|
515
|
+
}
|
|
516
|
+
if (pending.length > 0)
|
|
517
|
+
return { pending };
|
|
518
|
+
return { coverage: { rooms: coverage, indexes: [], overlay } };
|
|
519
|
+
}
|
|
520
|
+
function awaitPreparation(cover) {
|
|
521
|
+
if (!('pending' in cover))
|
|
522
|
+
return;
|
|
523
|
+
const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
|
|
524
|
+
if (waiting.length > 0)
|
|
525
|
+
prepare(waiting.map(({ room }) => room));
|
|
526
|
+
}
|
|
527
|
+
function acquires(room) {
|
|
528
|
+
return (demanded(demand, room) ||
|
|
529
|
+
(replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
|
|
530
|
+
queuesWithoutReplica.has(room.locator));
|
|
531
|
+
}
|
|
532
|
+
function acquiredLocators() {
|
|
533
|
+
return [...discovered.values()]
|
|
534
|
+
.filter(acquires)
|
|
535
|
+
.map((room) => room.locator)
|
|
536
|
+
.sort();
|
|
537
|
+
}
|
|
538
|
+
function heldDemand(acquired) {
|
|
539
|
+
if (demand.whole)
|
|
352
540
|
return null;
|
|
353
|
-
return
|
|
541
|
+
return { rooms: acquired.map((room) => room.locator), conversations: demand.conversations };
|
|
542
|
+
}
|
|
543
|
+
function applyDemand(next) {
|
|
544
|
+
const before = acquiredLocators();
|
|
545
|
+
const conversed = demand.conversations;
|
|
546
|
+
demand = next;
|
|
547
|
+
const after = acquiredLocators();
|
|
548
|
+
queueReadiness();
|
|
549
|
+
if (!conversed && next.conversations)
|
|
550
|
+
scheduleCurrency();
|
|
551
|
+
if (JSON.stringify(before) === JSON.stringify(after) && conversed === next.conversations)
|
|
552
|
+
return;
|
|
553
|
+
journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
|
|
554
|
+
if (options.adapter.roomStream === undefined)
|
|
555
|
+
void sync('unstreamed');
|
|
556
|
+
else if (socketOpen)
|
|
557
|
+
options.adapter.roomStream.reopen();
|
|
558
|
+
}
|
|
559
|
+
function acquire(next) {
|
|
560
|
+
if (closed)
|
|
561
|
+
return;
|
|
562
|
+
if (narrowing !== null)
|
|
563
|
+
clearTimeout(narrowing);
|
|
564
|
+
narrowing = null;
|
|
565
|
+
const widening = wider(next, demand);
|
|
566
|
+
if (!widening && !narrower(next, demand))
|
|
567
|
+
return;
|
|
568
|
+
if (widening || !roomsListed) {
|
|
569
|
+
applyDemand(next);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
// A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
|
|
573
|
+
narrowing = setTimeout(() => {
|
|
574
|
+
narrowing = null;
|
|
575
|
+
applyDemand(next);
|
|
576
|
+
}, 5_000);
|
|
577
|
+
}
|
|
578
|
+
function requiredRooms(reach) {
|
|
579
|
+
if (reach === null)
|
|
580
|
+
return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
|
|
581
|
+
const named = [...new Set(reach.rooms)].sort();
|
|
582
|
+
if (roomsListed)
|
|
583
|
+
return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
|
|
584
|
+
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
585
|
+
}
|
|
586
|
+
function roomState(locator, search) {
|
|
587
|
+
if (sourceRefusals.has(locator))
|
|
588
|
+
return 'refused';
|
|
589
|
+
const listedRoom = discovered.get(locator);
|
|
590
|
+
if (roomsListed &&
|
|
591
|
+
(listedRoom === undefined ||
|
|
592
|
+
listedRoom.replica.admission !== 'admitted' ||
|
|
593
|
+
unreadable.has(locator)))
|
|
594
|
+
return 'unadmitted';
|
|
595
|
+
if (!replicas.has(locator))
|
|
596
|
+
return 'acquiring';
|
|
597
|
+
if (!prepared.has(locator))
|
|
598
|
+
return 'preparing';
|
|
599
|
+
if (search && !searchIndex.rooms().has(locator))
|
|
600
|
+
return 'indexing';
|
|
601
|
+
return 'complete';
|
|
602
|
+
}
|
|
603
|
+
function conversationCoverage(reach, search) {
|
|
604
|
+
const declared = reach === null || reach.conversations;
|
|
605
|
+
if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
|
|
606
|
+
return 'undeclared';
|
|
607
|
+
if (!conversations.known() || !conversations.current())
|
|
608
|
+
return 'acquiring';
|
|
609
|
+
if (!search)
|
|
610
|
+
return 'complete';
|
|
611
|
+
const unindexed = conversations
|
|
612
|
+
.searchable()
|
|
613
|
+
.filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
|
|
614
|
+
if (unindexed.length === 0)
|
|
615
|
+
return 'complete';
|
|
616
|
+
index([], unindexed);
|
|
617
|
+
return 'indexing';
|
|
618
|
+
}
|
|
619
|
+
function connectionOf() {
|
|
620
|
+
if (socketOpen)
|
|
621
|
+
return 'connected';
|
|
622
|
+
if (status.providerUnreachable)
|
|
623
|
+
return 'unreachable';
|
|
624
|
+
return options.adapter.roomStream === undefined && status.lastCompleted !== null
|
|
625
|
+
? 'connected'
|
|
626
|
+
: 'connecting';
|
|
627
|
+
}
|
|
628
|
+
function refusedIn(locators) {
|
|
629
|
+
return locators.flatMap((room) => {
|
|
630
|
+
const held = sourceRefusals.get(room);
|
|
631
|
+
if (held === undefined)
|
|
632
|
+
return [];
|
|
633
|
+
const { file, location } = held.fields;
|
|
634
|
+
return [
|
|
635
|
+
{
|
|
636
|
+
room,
|
|
637
|
+
file: typeof file === 'string' ? file : '',
|
|
638
|
+
rule: held.code,
|
|
639
|
+
location: typeof location === 'string' ? location : null,
|
|
640
|
+
},
|
|
641
|
+
];
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
function readinessFor(port, reach) {
|
|
645
|
+
const required = requiredRooms(reach);
|
|
646
|
+
const search = port.search === 'required';
|
|
647
|
+
const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
|
|
648
|
+
const building = (state) => coverage
|
|
649
|
+
.filter((room) => room.state === state && !unindexable.has(room.room))
|
|
650
|
+
.map(({ room }) => room);
|
|
651
|
+
const preparing = building('preparing');
|
|
652
|
+
if (preparing.length > 0)
|
|
653
|
+
prepare(preparing);
|
|
654
|
+
const indexing = building('indexing');
|
|
655
|
+
if (indexing.length > 0)
|
|
656
|
+
index(indexing, []);
|
|
657
|
+
return readinessOf({
|
|
658
|
+
code: port.code,
|
|
659
|
+
coverage: {
|
|
660
|
+
listed: required.listed,
|
|
661
|
+
rooms: coverage,
|
|
662
|
+
conversations: conversationCoverage(reach, search),
|
|
663
|
+
search: search ? 'required' : 'unused',
|
|
664
|
+
},
|
|
665
|
+
persistence,
|
|
666
|
+
connection: connectionOf(),
|
|
667
|
+
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
668
|
+
capacity: capacity !== null &&
|
|
669
|
+
coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
|
|
670
|
+
? capacity
|
|
671
|
+
: port.capacity,
|
|
672
|
+
refused: refusedIn(required.rooms),
|
|
673
|
+
lost: storageLoss !== null,
|
|
674
|
+
established: targets[port.target]?.established === true,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
function establish(name) {
|
|
678
|
+
targets = { ...targets, [name]: { established: true } };
|
|
679
|
+
queueReadiness();
|
|
680
|
+
void durably((local) => local.keepTargets(targets)).catch(() => {
|
|
681
|
+
journal.record(`target record failed ${name}`);
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
function deliverReadiness(watch) {
|
|
685
|
+
if (!readinessWatches.has(watch))
|
|
686
|
+
return;
|
|
687
|
+
const readiness = readinessFor(watch.port, watch.reach);
|
|
688
|
+
if (readiness.ready && targets[watch.port.target]?.established !== true) {
|
|
689
|
+
establish(watch.port.target);
|
|
690
|
+
}
|
|
691
|
+
const shown = JSON.stringify(readiness);
|
|
692
|
+
if (shown === watch.last)
|
|
693
|
+
return;
|
|
694
|
+
watch.last = shown;
|
|
695
|
+
watch.deliver(structuredClone(readiness));
|
|
696
|
+
}
|
|
697
|
+
function queueReadiness() {
|
|
698
|
+
if (readinessQueued || closed || readinessWatches.size === 0)
|
|
699
|
+
return;
|
|
700
|
+
readinessQueued = true;
|
|
701
|
+
queueMicrotask(() => {
|
|
702
|
+
readinessQueued = false;
|
|
703
|
+
if (closed)
|
|
704
|
+
return;
|
|
705
|
+
for (const watch of [...readinessWatches])
|
|
706
|
+
deliverReadiness(watch);
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
function watchReadiness(port, reach, deliver) {
|
|
710
|
+
const watch = { port, reach, deliver, last: null };
|
|
711
|
+
readinessWatches.add(watch);
|
|
712
|
+
void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
|
|
713
|
+
return () => {
|
|
714
|
+
readinessWatches.delete(watch);
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
function layerBytes(layer) {
|
|
718
|
+
let bytes = 0;
|
|
719
|
+
for (const [path, text] of Object.entries(layer.documents))
|
|
720
|
+
bytes += (path.length + text.length) * 2;
|
|
721
|
+
return bytes;
|
|
722
|
+
}
|
|
723
|
+
function grownBy(next, previous) {
|
|
724
|
+
if (previous === undefined)
|
|
725
|
+
return layerBytes(next);
|
|
726
|
+
const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
|
|
727
|
+
return layerBytes({ ...next, documents });
|
|
728
|
+
}
|
|
729
|
+
function exceeded(error) {
|
|
730
|
+
return (typeof error === 'object' &&
|
|
731
|
+
error !== null &&
|
|
732
|
+
error.name === 'QuotaExceededError');
|
|
733
|
+
}
|
|
734
|
+
async function estimated() {
|
|
735
|
+
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
736
|
+
return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
|
|
737
|
+
}
|
|
738
|
+
function pause(locator, cause) {
|
|
739
|
+
paused.add(locator);
|
|
740
|
+
capacity = cause;
|
|
741
|
+
journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
|
|
742
|
+
queueReadiness();
|
|
743
|
+
if (rechecking !== null)
|
|
744
|
+
return;
|
|
745
|
+
// A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
|
|
746
|
+
rechecking = setInterval(() => {
|
|
747
|
+
void recheck();
|
|
748
|
+
}, 60_000);
|
|
749
|
+
}
|
|
750
|
+
async function recheck() {
|
|
751
|
+
const held = capacity;
|
|
752
|
+
if (held === null || closed)
|
|
753
|
+
return;
|
|
754
|
+
const available = await estimated().catch(() => null);
|
|
755
|
+
if (available === null || available < (held.needed ?? 0))
|
|
756
|
+
return;
|
|
757
|
+
if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
|
|
758
|
+
return;
|
|
759
|
+
resume();
|
|
760
|
+
}
|
|
761
|
+
function resume() {
|
|
762
|
+
if (rechecking !== null)
|
|
763
|
+
clearInterval(rechecking);
|
|
764
|
+
rechecking = null;
|
|
765
|
+
if (paused.size === 0 && capacity === null)
|
|
766
|
+
return;
|
|
767
|
+
paused.clear();
|
|
768
|
+
capacity = null;
|
|
769
|
+
journal.record('capacity resumed');
|
|
770
|
+
queueReadiness();
|
|
771
|
+
if (!closed && authorization !== undefined)
|
|
772
|
+
void sync('unstreamed');
|
|
773
|
+
}
|
|
774
|
+
async function reclaimed(local) {
|
|
775
|
+
const unreferenced = [...transfers.keys()].filter((locator) => {
|
|
776
|
+
const room = discovered.get(locator);
|
|
777
|
+
return room === undefined || !acquires(room);
|
|
778
|
+
});
|
|
779
|
+
for (const locator of unreferenced) {
|
|
780
|
+
await local.abandon(locator);
|
|
781
|
+
transfers.delete(locator);
|
|
782
|
+
}
|
|
783
|
+
if (unreferenced.length > 0)
|
|
784
|
+
journal.record(`capacity reclaimed ${unreferenced.length}`);
|
|
785
|
+
return unreferenced.length > 0;
|
|
786
|
+
}
|
|
787
|
+
async function admitted(local, locator, needed) {
|
|
788
|
+
if (paused.has(locator))
|
|
789
|
+
return false;
|
|
790
|
+
let available = await estimated().catch(() => null);
|
|
791
|
+
if (available === null || available >= needed)
|
|
792
|
+
return true;
|
|
793
|
+
if (await reclaimed(local))
|
|
794
|
+
available = await estimated().catch(() => null);
|
|
795
|
+
if (available === null || available >= needed)
|
|
796
|
+
return true;
|
|
797
|
+
pause(locator, { cause: 'insufficient-space', needed, available });
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
async function exceededOn(locator, bytes) {
|
|
801
|
+
const available = await estimated().catch(() => null);
|
|
802
|
+
pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
|
|
803
|
+
}
|
|
804
|
+
async function rowsQuery(plan) {
|
|
805
|
+
const before = covered(plan);
|
|
806
|
+
awaitPreparation(before);
|
|
807
|
+
if ('pending' in before)
|
|
808
|
+
return before;
|
|
809
|
+
const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
|
|
810
|
+
awaitPreparation(cover);
|
|
811
|
+
if ('pending' in cover)
|
|
812
|
+
return cover;
|
|
813
|
+
return 'groups' in rows
|
|
814
|
+
? { groups: rows.groups, coverage: cover.coverage }
|
|
815
|
+
: { documents: rows.documents, coverage: cover.coverage };
|
|
816
|
+
}
|
|
817
|
+
async function indexConversation(conversation) {
|
|
818
|
+
searchIndex.indexMessages(conversation, [], false);
|
|
819
|
+
let after = null;
|
|
820
|
+
for (;;) {
|
|
821
|
+
const batch = await durably((local) => local.messagesAfter(conversation, after));
|
|
822
|
+
if (closed)
|
|
823
|
+
return;
|
|
824
|
+
searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
|
|
825
|
+
if (batch.length === 0)
|
|
826
|
+
return;
|
|
827
|
+
after = batch[batch.length - 1].identity;
|
|
828
|
+
}
|
|
354
829
|
}
|
|
355
|
-
function
|
|
356
|
-
|
|
357
|
-
|
|
830
|
+
function indexNext() {
|
|
831
|
+
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
|
|
832
|
+
if (room !== undefined) {
|
|
833
|
+
roomsToIndex.delete(room);
|
|
834
|
+
return () => prepared.has(room)
|
|
835
|
+
? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
|
|
836
|
+
: undefined;
|
|
837
|
+
}
|
|
838
|
+
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
839
|
+
if (conversation === undefined)
|
|
840
|
+
return null;
|
|
841
|
+
conversationsToIndex.delete(conversation);
|
|
842
|
+
return () => indexConversation(conversation);
|
|
843
|
+
}
|
|
844
|
+
function index(rooms, named) {
|
|
845
|
+
for (const room of rooms)
|
|
846
|
+
roomsToIndex.add(room);
|
|
847
|
+
for (const conversation of named)
|
|
848
|
+
conversationsToIndex.add(conversation);
|
|
849
|
+
if (indexing)
|
|
850
|
+
return;
|
|
851
|
+
indexing = true;
|
|
852
|
+
void (async () => {
|
|
853
|
+
try {
|
|
854
|
+
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
855
|
+
await yielded();
|
|
856
|
+
await unit();
|
|
857
|
+
queueReadiness();
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
finally {
|
|
861
|
+
indexing = false;
|
|
862
|
+
}
|
|
863
|
+
})().catch(() => { });
|
|
358
864
|
}
|
|
359
865
|
function publishChangeSet(set) {
|
|
360
866
|
applyChangeSet(set);
|
|
@@ -371,34 +877,7 @@ export function composeRuntime(options) {
|
|
|
371
877
|
emitPresentation(app);
|
|
372
878
|
conversations.changed(set);
|
|
373
879
|
}
|
|
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
880
|
async function refuse(reason, provider = null) {
|
|
401
|
-
stopTimer();
|
|
402
881
|
discoveryPaused = true;
|
|
403
882
|
try {
|
|
404
883
|
if (reason === 'revoked-authorization') {
|
|
@@ -477,14 +956,24 @@ export function composeRuntime(options) {
|
|
|
477
956
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
478
957
|
});
|
|
479
958
|
}
|
|
480
|
-
async function applyDiscovery(
|
|
959
|
+
async function applyDiscovery(found, started) {
|
|
960
|
+
for (const room of found) {
|
|
961
|
+
const fence = fences.get(room.locator);
|
|
962
|
+
if (fence !== undefined && fence.erased && fence.at < started)
|
|
963
|
+
fences.delete(room.locator);
|
|
964
|
+
}
|
|
965
|
+
const listed = found.filter((room) => !fences.has(room.locator));
|
|
481
966
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
482
967
|
for (const locator of listedLocators)
|
|
483
968
|
scopedRefusals.delete(locator);
|
|
484
969
|
discovered.clear();
|
|
485
970
|
for (const room of listed)
|
|
486
971
|
discovered.set(room.locator, room);
|
|
487
|
-
|
|
972
|
+
const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
|
|
973
|
+
if (options.adapter.exhaustiveDiscovery === true)
|
|
974
|
+
await loseRooms(unlisted);
|
|
975
|
+
else
|
|
976
|
+
unlistedKept(unlisted);
|
|
488
977
|
for (const room of listed) {
|
|
489
978
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
490
979
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -495,6 +984,7 @@ export function composeRuntime(options) {
|
|
|
495
984
|
}
|
|
496
985
|
roomsListed = true;
|
|
497
986
|
emitRooms();
|
|
987
|
+
return listed;
|
|
498
988
|
}
|
|
499
989
|
function unlistedHeld(listed) {
|
|
500
990
|
return [
|
|
@@ -506,41 +996,73 @@ export function composeRuntime(options) {
|
|
|
506
996
|
]),
|
|
507
997
|
].filter((locator) => !listed.has(locator));
|
|
508
998
|
}
|
|
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);
|
|
999
|
+
function unlistedKept(unlisted) {
|
|
1000
|
+
for (const locator of unlisted) {
|
|
1001
|
+
const held = replicas.get(locator)?.confirmed.record;
|
|
1002
|
+
if (held !== undefined)
|
|
1003
|
+
list(held, false);
|
|
1004
|
+
else
|
|
525
1005
|
rooms.delete(locator);
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
function fence(locators) {
|
|
1009
|
+
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
1010
|
+
const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
|
|
1011
|
+
room: true,
|
|
1012
|
+
queue: true,
|
|
1013
|
+
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
1014
|
+
}));
|
|
1015
|
+
for (const locator of fresh) {
|
|
1016
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
1017
|
+
lastFenced.set(locator, ++fencings);
|
|
1018
|
+
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
1019
|
+
pendings.delete(batch.id);
|
|
1020
|
+
replicas.delete(locator);
|
|
1021
|
+
queuesWithoutReplica.delete(locator);
|
|
1022
|
+
rooms.delete(locator);
|
|
1023
|
+
roomDrafts.delete(locator);
|
|
1024
|
+
transfers.delete(locator);
|
|
1025
|
+
catching.delete(locator);
|
|
1026
|
+
discovered.delete(locator);
|
|
1027
|
+
announced.delete(locator);
|
|
1028
|
+
unreadable.delete(locator);
|
|
1029
|
+
stale.delete(locator);
|
|
1030
|
+
repairable.delete(locator);
|
|
1031
|
+
unindexable.delete(locator);
|
|
1032
|
+
paused.delete(locator);
|
|
1033
|
+
scopedRefusals.delete(locator);
|
|
1034
|
+
sourceRefusals.delete(locator);
|
|
1035
|
+
declarationRefusals.delete(locator);
|
|
1036
|
+
for (const attached of ['collection', 'typed-selection']) {
|
|
1037
|
+
const key = modelKey(locator, attached);
|
|
1038
|
+
models.delete(key);
|
|
1039
|
+
liveModels.delete(key);
|
|
1040
|
+
invalidModels.delete(key);
|
|
540
1041
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
1042
|
+
journal.record(`room lost ${locator}`);
|
|
1043
|
+
}
|
|
1044
|
+
if (fresh.length === 0)
|
|
1045
|
+
return fresh;
|
|
1046
|
+
publishChangeSet(mergeChangeSets(removed));
|
|
1047
|
+
for (const locator of fresh)
|
|
1048
|
+
prepared.delete(locator);
|
|
1049
|
+
searchIndex.forget(fresh);
|
|
1050
|
+
readSide.recover();
|
|
1051
|
+
moveTo({ ...status, ...attention() });
|
|
1052
|
+
return fresh;
|
|
1053
|
+
}
|
|
1054
|
+
async function erased(local, locators) {
|
|
1055
|
+
await local.erase(locators);
|
|
1056
|
+
for (const locator of locators) {
|
|
1057
|
+
const held = fences.get(locator);
|
|
1058
|
+
if (held !== undefined)
|
|
1059
|
+
held.erased = true;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
async function loseRooms(locators) {
|
|
1063
|
+
const fresh = fence(locators);
|
|
1064
|
+
if (fresh.length > 0)
|
|
1065
|
+
await durably((local) => erased(local, fresh));
|
|
544
1066
|
}
|
|
545
1067
|
function restored(key, persisted) {
|
|
546
1068
|
const prior = models.get(key);
|
|
@@ -561,7 +1083,7 @@ export function composeRuntime(options) {
|
|
|
561
1083
|
return model;
|
|
562
1084
|
}
|
|
563
1085
|
async function hydrate(opened) {
|
|
564
|
-
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
|
|
1086
|
+
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
|
|
565
1087
|
opened.all(),
|
|
566
1088
|
opened.queues(),
|
|
567
1089
|
opened.roomDrafts(),
|
|
@@ -573,7 +1095,9 @@ export function composeRuntime(options) {
|
|
|
573
1095
|
opened.capabilities(),
|
|
574
1096
|
opened.member(),
|
|
575
1097
|
opened.transfers(),
|
|
1098
|
+
opened.targets(),
|
|
576
1099
|
]);
|
|
1100
|
+
targets = heldTargets;
|
|
577
1101
|
transfers.clear();
|
|
578
1102
|
for (const record of staged)
|
|
579
1103
|
transfers.set(record.locator, record);
|
|
@@ -635,6 +1159,7 @@ export function composeRuntime(options) {
|
|
|
635
1159
|
async function readState(opened) {
|
|
636
1160
|
const { lastCompleted, locators, read } = await hydrate(opened);
|
|
637
1161
|
applyChangeSet(read);
|
|
1162
|
+
prepare();
|
|
638
1163
|
if (replicas.size > 0)
|
|
639
1164
|
emitRooms();
|
|
640
1165
|
if (member !== null)
|
|
@@ -645,8 +1170,15 @@ export function composeRuntime(options) {
|
|
|
645
1170
|
return lastCompleted;
|
|
646
1171
|
}
|
|
647
1172
|
async function open() {
|
|
648
|
-
|
|
1173
|
+
if (closed)
|
|
1174
|
+
throw new Error('runtime closed');
|
|
1175
|
+
const opened = await openRuntimeStore(isFenced);
|
|
649
1176
|
store = opened;
|
|
1177
|
+
const interrupted = await opened.fences();
|
|
1178
|
+
for (const locator of interrupted) {
|
|
1179
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
1180
|
+
lastFenced.set(locator, ++fencings);
|
|
1181
|
+
}
|
|
650
1182
|
const kept = options.shell.generation;
|
|
651
1183
|
const storedGeneration = await opened.generation();
|
|
652
1184
|
const loss = detectStorageLoss(storedGeneration, kept);
|
|
@@ -660,6 +1192,8 @@ export function composeRuntime(options) {
|
|
|
660
1192
|
emitStorageLoss();
|
|
661
1193
|
}
|
|
662
1194
|
const lastCompleted = await readState(opened);
|
|
1195
|
+
if (interrupted.length > 0)
|
|
1196
|
+
await erased(opened, interrupted);
|
|
663
1197
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
664
1198
|
return opened;
|
|
665
1199
|
}
|
|
@@ -741,17 +1275,19 @@ export function composeRuntime(options) {
|
|
|
741
1275
|
moveTo({ ...status, ...attention() });
|
|
742
1276
|
throw error;
|
|
743
1277
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
1278
|
+
return () => {
|
|
1279
|
+
queuesWithoutReplica.delete(locator);
|
|
1280
|
+
for (const { batch, returned } of unqueued) {
|
|
1281
|
+
const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
|
|
1282
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
1283
|
+
}
|
|
1284
|
+
replicas.set(locator, replicaOf(confirmed, kept, before));
|
|
1285
|
+
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
1286
|
+
room: settling.room,
|
|
1287
|
+
queue: kept.length !== previousQueue.length,
|
|
1288
|
+
drafts: [...written.keys()],
|
|
1289
|
+
});
|
|
1290
|
+
};
|
|
755
1291
|
}
|
|
756
1292
|
function adoptModels(locator, record) {
|
|
757
1293
|
for (const [attached, model] of [
|
|
@@ -784,11 +1320,16 @@ export function composeRuntime(options) {
|
|
|
784
1320
|
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
785
1321
|
};
|
|
786
1322
|
}
|
|
787
|
-
function publish(local, room, confirmed, landing
|
|
1323
|
+
function publish(local, room, confirmed, landing, live) {
|
|
788
1324
|
const { base, stored } = landing;
|
|
789
1325
|
return exclusive(async () => {
|
|
790
|
-
if (
|
|
1326
|
+
if (stored !== undefined) {
|
|
1327
|
+
await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
|
|
1328
|
+
}
|
|
1329
|
+
if (!live()) {
|
|
1330
|
+
caughtUpTo(room.locator);
|
|
791
1331
|
return true;
|
|
1332
|
+
}
|
|
792
1333
|
const prior = replicas.get(room.locator);
|
|
793
1334
|
if (base !== undefined &&
|
|
794
1335
|
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
@@ -802,24 +1343,42 @@ export function composeRuntime(options) {
|
|
|
802
1343
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
803
1344
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
804
1345
|
});
|
|
805
|
-
|
|
806
|
-
|
|
1346
|
+
let settled;
|
|
1347
|
+
try {
|
|
1348
|
+
settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
|
|
1349
|
+
}
|
|
1350
|
+
catch (error) {
|
|
1351
|
+
if (!exceeded(error))
|
|
1352
|
+
throw error;
|
|
1353
|
+
await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
|
|
1354
|
+
caughtUpTo(room.locator);
|
|
1355
|
+
return true;
|
|
1356
|
+
}
|
|
1357
|
+
const set = settled();
|
|
1358
|
+
if (stored !== undefined) {
|
|
807
1359
|
transfers.delete(room.locator);
|
|
1360
|
+
catching.delete(room.locator);
|
|
1361
|
+
}
|
|
808
1362
|
for (const batch of accepted)
|
|
809
1363
|
pendings.delete(batch.id);
|
|
810
1364
|
stale.delete(room.locator);
|
|
1365
|
+
unindexable.delete(room.locator);
|
|
811
1366
|
if (unreadable.delete(room.locator))
|
|
812
1367
|
scopedRefusals.delete(room.locator);
|
|
813
1368
|
sourceRefusals.delete(room.locator);
|
|
814
1369
|
declarationRefusals.delete(room.locator);
|
|
815
1370
|
list(room, true);
|
|
1371
|
+
if (prior === undefined)
|
|
1372
|
+
prepared.add(room.locator);
|
|
816
1373
|
publishChangeSet(set);
|
|
817
1374
|
moveTo({ ...status, ...attention() });
|
|
1375
|
+
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
818
1376
|
return true;
|
|
819
1377
|
});
|
|
820
1378
|
}
|
|
821
1379
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
822
1380
|
const local = await openLocal();
|
|
1381
|
+
await referenced(locator);
|
|
823
1382
|
const replica = replicas.get(locator);
|
|
824
1383
|
const room = rooms.get(locator);
|
|
825
1384
|
if (replica === undefined || room === undefined) {
|
|
@@ -867,6 +1426,11 @@ export function composeRuntime(options) {
|
|
|
867
1426
|
changes,
|
|
868
1427
|
bases: parts.flatMap(({ bases }) => bases),
|
|
869
1428
|
writtenBy,
|
|
1429
|
+
clean: {
|
|
1430
|
+
text: replica.confirmed.record.documents,
|
|
1431
|
+
binaries: replica.confirmed.record.binaries,
|
|
1432
|
+
composition: compositionOf(replica.confirmed.record),
|
|
1433
|
+
},
|
|
870
1434
|
};
|
|
871
1435
|
const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
|
|
872
1436
|
if (checked?.kind === 'refused')
|
|
@@ -937,6 +1501,8 @@ export function composeRuntime(options) {
|
|
|
937
1501
|
catch {
|
|
938
1502
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
939
1503
|
}
|
|
1504
|
+
if (fences.has(locator))
|
|
1505
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
940
1506
|
for (const origin of withdrawn.keys())
|
|
941
1507
|
pendings.delete(origin);
|
|
942
1508
|
for (const [id, drafts] of drafted) {
|
|
@@ -966,6 +1532,8 @@ export function composeRuntime(options) {
|
|
|
966
1532
|
return;
|
|
967
1533
|
}
|
|
968
1534
|
await local.submit(locator, identifier, submission, lost);
|
|
1535
|
+
if (fences.has(locator))
|
|
1536
|
+
return;
|
|
969
1537
|
pendings.set(identifier, { ...state, submission, lost });
|
|
970
1538
|
const replica = replicas.get(locator);
|
|
971
1539
|
if (replica !== undefined)
|
|
@@ -976,6 +1544,8 @@ export function composeRuntime(options) {
|
|
|
976
1544
|
if (!continuing())
|
|
977
1545
|
throw new Error('connection refused');
|
|
978
1546
|
await local.keepUpload(locator, identifier, upload);
|
|
1547
|
+
if (fences.has(locator))
|
|
1548
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
979
1549
|
pendings.set(identifier, { ...pendings.get(identifier), upload });
|
|
980
1550
|
if (!continuing())
|
|
981
1551
|
throw new Error('connection refused');
|
|
@@ -991,6 +1561,7 @@ export function composeRuntime(options) {
|
|
|
991
1561
|
}
|
|
992
1562
|
if (closed || authorization === undefined || refusal !== null)
|
|
993
1563
|
return UNCERTAIN;
|
|
1564
|
+
const since = fencings;
|
|
994
1565
|
const transfer = ['transfer', 'history', locator, path];
|
|
995
1566
|
if (query.revision !== undefined)
|
|
996
1567
|
transfer.push(query.revision);
|
|
@@ -1005,6 +1576,8 @@ export function composeRuntime(options) {
|
|
|
1005
1576
|
...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
|
|
1006
1577
|
});
|
|
1007
1578
|
journal.record(`outcome history ${result.kind}`);
|
|
1579
|
+
if (fencedSince(locator, since))
|
|
1580
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
1008
1581
|
if (result.kind === 'scoped-refusal') {
|
|
1009
1582
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
1010
1583
|
if (room !== undefined)
|
|
@@ -1027,6 +1600,12 @@ export function composeRuntime(options) {
|
|
|
1027
1600
|
}
|
|
1028
1601
|
return null;
|
|
1029
1602
|
}
|
|
1603
|
+
function binaryRoom(request) {
|
|
1604
|
+
if (request.kind === 'path')
|
|
1605
|
+
return request.locator;
|
|
1606
|
+
const scope = options.adapter.referrerScope(request.referrer);
|
|
1607
|
+
return scope?.kind === 'room' ? scope.locator : null;
|
|
1608
|
+
}
|
|
1030
1609
|
async function readBinary(request) {
|
|
1031
1610
|
if (closed || refusal !== null)
|
|
1032
1611
|
return UNCERTAIN;
|
|
@@ -1038,12 +1617,16 @@ export function composeRuntime(options) {
|
|
|
1038
1617
|
}
|
|
1039
1618
|
if (closed || authorization === undefined || refusal !== null)
|
|
1040
1619
|
return UNCERTAIN;
|
|
1620
|
+
const since = fencings;
|
|
1041
1621
|
journal.record(request.kind === 'path'
|
|
1042
1622
|
? `transfer binary ${request.locator} ${request.path}`
|
|
1043
1623
|
: `transfer binary ${request.fileRef}`);
|
|
1044
1624
|
try {
|
|
1045
1625
|
const result = await options.adapter.binary(authorization, request);
|
|
1046
1626
|
journal.record(`outcome binary ${result.kind}`);
|
|
1627
|
+
const locator = binaryRoom(request);
|
|
1628
|
+
if (locator !== null && fencedSince(locator, since))
|
|
1629
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
1047
1630
|
if (result.kind === 'scoped-refusal') {
|
|
1048
1631
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
1049
1632
|
if (room !== undefined)
|
|
@@ -1167,11 +1750,11 @@ export function composeRuntime(options) {
|
|
|
1167
1750
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
1168
1751
|
!options.adapter.reached(locator, roomVersion, written));
|
|
1169
1752
|
}
|
|
1170
|
-
async function pullRefresh(authorized, confirmed) {
|
|
1171
|
-
if (!
|
|
1753
|
+
async function pullRefresh(authorized, confirmed, live) {
|
|
1754
|
+
if (!live())
|
|
1172
1755
|
return unconfirmed;
|
|
1173
1756
|
try {
|
|
1174
|
-
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held),
|
|
1757
|
+
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
|
|
1175
1758
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
1176
1759
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
1177
1760
|
return result;
|
|
@@ -1181,47 +1764,80 @@ export function composeRuntime(options) {
|
|
|
1181
1764
|
throw error;
|
|
1182
1765
|
}
|
|
1183
1766
|
}
|
|
1184
|
-
function staging(local) {
|
|
1767
|
+
function staging(local, live) {
|
|
1185
1768
|
return {
|
|
1186
1769
|
held: (locator) => transfers.get(locator) ?? null,
|
|
1187
1770
|
staged: (record) => local.staged(record),
|
|
1188
1771
|
stage: (record, page) => exclusive(async () => {
|
|
1189
|
-
if (!
|
|
1772
|
+
if (!live())
|
|
1190
1773
|
return false;
|
|
1191
|
-
|
|
1774
|
+
const bytes = layerBytes(page);
|
|
1775
|
+
if (!(await admitted(local, record.locator, bytes)))
|
|
1776
|
+
return false;
|
|
1777
|
+
try {
|
|
1778
|
+
await local.stage(record, page);
|
|
1779
|
+
}
|
|
1780
|
+
catch (error) {
|
|
1781
|
+
if (!exceeded(error))
|
|
1782
|
+
throw error;
|
|
1783
|
+
await exceededOn(record.locator, bytes);
|
|
1784
|
+
return false;
|
|
1785
|
+
}
|
|
1192
1786
|
transfers.set(record.locator, record);
|
|
1787
|
+
readSide.recover();
|
|
1193
1788
|
return true;
|
|
1194
1789
|
}),
|
|
1195
1790
|
abandon: (locator) => exclusive(async () => {
|
|
1196
|
-
if (!
|
|
1791
|
+
if (!live())
|
|
1197
1792
|
return;
|
|
1198
1793
|
await local.abandon(locator);
|
|
1199
1794
|
transfers.delete(locator);
|
|
1795
|
+
readSide.recover();
|
|
1200
1796
|
}),
|
|
1201
1797
|
};
|
|
1202
1798
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1799
|
+
function reusable(room) {
|
|
1800
|
+
const record = replicas.get(room.locator)?.confirmed.record;
|
|
1801
|
+
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1802
|
+
return null;
|
|
1803
|
+
return { documents: record.documents, revisions: record.revisions };
|
|
1804
|
+
}
|
|
1805
|
+
async function pullBootstrap(authorized, room, local, live) {
|
|
1806
|
+
if (!live())
|
|
1205
1807
|
return unconfirmed;
|
|
1808
|
+
if (replicas.has(room.locator)) {
|
|
1809
|
+
catching.add(room.locator);
|
|
1810
|
+
readSide.recover();
|
|
1811
|
+
}
|
|
1206
1812
|
try {
|
|
1207
1813
|
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
1208
1814
|
collection: modelOf(room.locator),
|
|
1209
1815
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
1210
|
-
}, (models, held) => remodel(room.locator, models, held),
|
|
1816
|
+
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
1211
1817
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
1212
1818
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1819
|
+
if (result.kind !== 'staged')
|
|
1820
|
+
caughtUpTo(room.locator);
|
|
1213
1821
|
return result;
|
|
1214
1822
|
}
|
|
1215
1823
|
catch (error) {
|
|
1216
1824
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
1825
|
+
caughtUpTo(room.locator);
|
|
1217
1826
|
throw error;
|
|
1218
1827
|
}
|
|
1219
1828
|
}
|
|
1220
|
-
|
|
1829
|
+
function caughtUpTo(locator) {
|
|
1830
|
+
if (catching.delete(locator))
|
|
1831
|
+
readSide.recover();
|
|
1832
|
+
}
|
|
1833
|
+
async function advance(room, local, authorized, live) {
|
|
1834
|
+
await referenced(room.locator);
|
|
1835
|
+
if (!live() || paused.has(room.locator))
|
|
1836
|
+
return unconfirmed;
|
|
1221
1837
|
const held = replicas.get(room.locator);
|
|
1222
1838
|
if (held !== undefined) {
|
|
1223
|
-
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
1224
|
-
if (!
|
|
1839
|
+
const refreshed = await pullRefresh(authorized, held.confirmed, live);
|
|
1840
|
+
if (!live())
|
|
1225
1841
|
return unconfirmed;
|
|
1226
1842
|
if (refreshed.kind === 'still-current') {
|
|
1227
1843
|
stale.delete(room.locator);
|
|
@@ -1235,31 +1851,45 @@ export function composeRuntime(options) {
|
|
|
1235
1851
|
return null;
|
|
1236
1852
|
}
|
|
1237
1853
|
if (refreshed.kind === 'advanced') {
|
|
1238
|
-
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
|
|
1854
|
+
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
|
|
1239
1855
|
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1240
1856
|
const reached = now !== undefined &&
|
|
1241
1857
|
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
1242
|
-
return reached ? null : advance(room, local, authorized);
|
|
1858
|
+
return reached ? null : advance(room, local, authorized, live);
|
|
1243
1859
|
}
|
|
1244
1860
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1245
1861
|
return null;
|
|
1246
1862
|
}
|
|
1247
1863
|
if (refreshed.kind !== 'beyond-horizon')
|
|
1248
1864
|
return refreshed;
|
|
1865
|
+
if (opened !== null) {
|
|
1866
|
+
owed.add(room.locator);
|
|
1867
|
+
readSide.recover();
|
|
1868
|
+
return unconfirmed;
|
|
1869
|
+
}
|
|
1249
1870
|
}
|
|
1250
|
-
const bootstrapped = await pullBootstrap(authorized, room, local);
|
|
1251
|
-
if (!
|
|
1871
|
+
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
1872
|
+
if (!live())
|
|
1252
1873
|
return unconfirmed;
|
|
1253
1874
|
if (bootstrapped.kind !== 'staged')
|
|
1254
1875
|
return bootstrapped;
|
|
1255
|
-
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
1876
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
1256
1877
|
return null;
|
|
1257
1878
|
}
|
|
1258
1879
|
async function withhold(withheld, room, local, progress) {
|
|
1880
|
+
if (withheld.kind === 'unconfirmed')
|
|
1881
|
+
return;
|
|
1259
1882
|
if (withheld.kind === 'refused')
|
|
1260
1883
|
stale.delete(room.locator);
|
|
1261
1884
|
else
|
|
1262
1885
|
stale.add(room.locator);
|
|
1886
|
+
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
1887
|
+
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
1888
|
+
repairable.set(room.locator, reset);
|
|
1889
|
+
repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
|
|
1890
|
+
}
|
|
1891
|
+
else
|
|
1892
|
+
repairable.delete(room.locator);
|
|
1263
1893
|
if (withheld.kind === 'scoped-refusal') {
|
|
1264
1894
|
await scoped(withheld, room, local);
|
|
1265
1895
|
}
|
|
@@ -1295,6 +1925,7 @@ export function composeRuntime(options) {
|
|
|
1295
1925
|
unreadable.add(room.locator);
|
|
1296
1926
|
list(room, false);
|
|
1297
1927
|
emitRooms();
|
|
1928
|
+
moveTo({ ...status, ...attention() });
|
|
1298
1929
|
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
1299
1930
|
}
|
|
1300
1931
|
function leaving(batch, result) {
|
|
@@ -1316,6 +1947,7 @@ export function composeRuntime(options) {
|
|
|
1316
1947
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1317
1948
|
}
|
|
1318
1949
|
async function adoptAccepted(room, local, submitted, accepted) {
|
|
1950
|
+
await referenced(room.locator);
|
|
1319
1951
|
if (!continuing())
|
|
1320
1952
|
return unconfirmed;
|
|
1321
1953
|
const current = replicas.get(room.locator);
|
|
@@ -1332,16 +1964,17 @@ export function composeRuntime(options) {
|
|
|
1332
1964
|
if (applied.kind !== 'staged') {
|
|
1333
1965
|
const state = pendings.get(submitted.id);
|
|
1334
1966
|
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1335
|
-
if (state !== undefined)
|
|
1967
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
1336
1968
|
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1337
1969
|
emitQueue(room.locator);
|
|
1338
1970
|
return applied;
|
|
1339
1971
|
}
|
|
1340
|
-
const
|
|
1972
|
+
const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
1341
1973
|
pendings.delete(submitted.id);
|
|
1342
1974
|
unreadable.delete(room.locator);
|
|
1343
1975
|
sourceRefusals.delete(room.locator);
|
|
1344
1976
|
declarationRefusals.delete(room.locator);
|
|
1977
|
+
const set = settled();
|
|
1345
1978
|
list(room, true);
|
|
1346
1979
|
publishChangeSet(set);
|
|
1347
1980
|
moveTo({ ...status, ...attention() });
|
|
@@ -1355,14 +1988,14 @@ export function composeRuntime(options) {
|
|
|
1355
1988
|
refusedSource(room, failed);
|
|
1356
1989
|
return false;
|
|
1357
1990
|
}
|
|
1358
|
-
async function submitRoom(room, local, authorized, progress) {
|
|
1359
|
-
while (continuing() && authorization !== undefined) {
|
|
1991
|
+
async function submitRoom(room, local, authorized, progress, signal) {
|
|
1992
|
+
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
1360
1993
|
const replica = replicas.get(room.locator);
|
|
1361
1994
|
const head = replica?.queue[0];
|
|
1362
1995
|
if (replica === undefined || head === undefined)
|
|
1363
|
-
return;
|
|
1996
|
+
return 'done';
|
|
1364
1997
|
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1365
|
-
return;
|
|
1998
|
+
return 'done';
|
|
1366
1999
|
const state = pendings.get(head.id);
|
|
1367
2000
|
const { submission, lost: earlier } = state ?? {
|
|
1368
2001
|
submission: { state: 'waiting' },
|
|
@@ -1370,24 +2003,30 @@ export function composeRuntime(options) {
|
|
|
1370
2003
|
};
|
|
1371
2004
|
if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
|
|
1372
2005
|
progress.complete = false;
|
|
1373
|
-
return;
|
|
2006
|
+
return { at: Date.parse(submission.reset) };
|
|
1374
2007
|
}
|
|
1375
2008
|
if (writesHeld()) {
|
|
1376
2009
|
progress.complete = false;
|
|
1377
2010
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1378
|
-
return;
|
|
2011
|
+
return 'done';
|
|
1379
2012
|
}
|
|
1380
2013
|
const lost = earlier || submission.state === 'submitted';
|
|
2014
|
+
if (lost && pastHorizon(head.id)) {
|
|
2015
|
+
await discardPending(room, local, signal);
|
|
2016
|
+
return 'done';
|
|
2017
|
+
}
|
|
1381
2018
|
let result;
|
|
1382
2019
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
1383
2020
|
await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
|
|
1384
|
-
if (discoveryPaused)
|
|
1385
|
-
return;
|
|
1386
|
-
if (writesHeld()) {
|
|
2021
|
+
if (discoveryPaused || fences.has(room.locator))
|
|
2022
|
+
return 'done';
|
|
2023
|
+
if (signal.aborted || writesHeld()) {
|
|
1387
2024
|
await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
|
|
2025
|
+
if (signal.aborted)
|
|
2026
|
+
return 'done';
|
|
1388
2027
|
progress.complete = false;
|
|
1389
2028
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1390
|
-
return;
|
|
2029
|
+
return 'done';
|
|
1391
2030
|
}
|
|
1392
2031
|
const opensBefore = openSignals;
|
|
1393
2032
|
const answered = awaitingAnswer(room.locator);
|
|
@@ -1403,32 +2042,36 @@ export function composeRuntime(options) {
|
|
|
1403
2042
|
progress.complete = false;
|
|
1404
2043
|
if (continuing())
|
|
1405
2044
|
progress.reachable = false;
|
|
1406
|
-
return;
|
|
2045
|
+
return 'again';
|
|
2046
|
+
}
|
|
2047
|
+
if (signal.aborted) {
|
|
2048
|
+
answered();
|
|
2049
|
+
return 'done';
|
|
1407
2050
|
}
|
|
1408
2051
|
if (result.kind !== 'accepted' || !continuing())
|
|
1409
2052
|
answered();
|
|
1410
2053
|
if (!continuing())
|
|
1411
|
-
return;
|
|
2054
|
+
return 'done';
|
|
1412
2055
|
if (result.kind === 'transport-uncertainty') {
|
|
1413
2056
|
progress.complete = false;
|
|
1414
2057
|
progress.reachable = false;
|
|
1415
|
-
return;
|
|
2058
|
+
return 'again';
|
|
1416
2059
|
}
|
|
1417
2060
|
if (result.kind === 'room-not-readable') {
|
|
1418
2061
|
progress.complete = false;
|
|
1419
2062
|
unreadable.add(room.locator);
|
|
1420
2063
|
list(room, false);
|
|
1421
2064
|
emitRooms();
|
|
1422
|
-
return;
|
|
2065
|
+
return 'done';
|
|
1423
2066
|
}
|
|
1424
2067
|
if (result.kind === 'revoked-authorization') {
|
|
1425
2068
|
await refuse('revoked-authorization');
|
|
1426
|
-
return;
|
|
2069
|
+
return 'done';
|
|
1427
2070
|
}
|
|
1428
2071
|
if (result.kind === 'scoped-refusal') {
|
|
1429
2072
|
await scoped(result, room, local);
|
|
1430
2073
|
progress.complete = false;
|
|
1431
|
-
return;
|
|
2074
|
+
return 'done';
|
|
1432
2075
|
}
|
|
1433
2076
|
if (result.kind === 'write-halted') {
|
|
1434
2077
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
@@ -1437,16 +2080,24 @@ export function composeRuntime(options) {
|
|
|
1437
2080
|
continue;
|
|
1438
2081
|
haltedRefusal = true;
|
|
1439
2082
|
progress.complete = false;
|
|
1440
|
-
return;
|
|
2083
|
+
return 'done';
|
|
1441
2084
|
}
|
|
1442
2085
|
if (result.kind === 'conflicted' ||
|
|
1443
2086
|
result.kind === 'rejected' ||
|
|
1444
2087
|
result.kind === 'expired' ||
|
|
1445
2088
|
result.kind === 'retryable-refusal') {
|
|
2089
|
+
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
2090
|
+
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
2091
|
+
await discardPending(room, local, signal);
|
|
2092
|
+
return 'done';
|
|
2093
|
+
}
|
|
1446
2094
|
if (result.kind === 'expired') {
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
2095
|
+
await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
|
|
2096
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
2097
|
+
if (withheld !== null) {
|
|
2098
|
+
await withhold(withheld, room, local, progress);
|
|
2099
|
+
return 'done';
|
|
2100
|
+
}
|
|
1450
2101
|
}
|
|
1451
2102
|
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
|
|
1452
2103
|
(result.kind === 'conflicted' && result.roomVersion !== undefined
|
|
@@ -1459,8 +2110,7 @@ export function composeRuntime(options) {
|
|
|
1459
2110
|
const reset = result.reset;
|
|
1460
2111
|
progress.complete = false;
|
|
1461
2112
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1462
|
-
|
|
1463
|
-
return;
|
|
2113
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
1464
2114
|
}
|
|
1465
2115
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
1466
2116
|
const left = leaving(head, result);
|
|
@@ -1471,37 +2121,82 @@ export function composeRuntime(options) {
|
|
|
1471
2121
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
1472
2122
|
return;
|
|
1473
2123
|
}
|
|
1474
|
-
const
|
|
2124
|
+
const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
1475
2125
|
leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
|
|
1476
2126
|
(result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
|
|
1477
2127
|
? { ...left, returned: null }
|
|
1478
2128
|
: left,
|
|
1479
2129
|
});
|
|
1480
|
-
publishChangeSet(
|
|
2130
|
+
publishChangeSet(settled());
|
|
1481
2131
|
moveTo({ ...status, ...attention() });
|
|
1482
2132
|
});
|
|
1483
2133
|
if (result.kind === 'expired')
|
|
1484
2134
|
continue;
|
|
1485
|
-
const withheld = await advance(room, local, authorized);
|
|
1486
|
-
if (withheld !== null)
|
|
1487
|
-
|
|
2135
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
2136
|
+
if (withheld !== null) {
|
|
2137
|
+
await withhold(withheld, room, local, progress);
|
|
2138
|
+
return 'done';
|
|
2139
|
+
}
|
|
1488
2140
|
continue;
|
|
1489
2141
|
}
|
|
1490
2142
|
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1491
2143
|
if (!kept) {
|
|
1492
2144
|
progress.complete = false;
|
|
1493
|
-
return;
|
|
2145
|
+
return 'done';
|
|
1494
2146
|
}
|
|
1495
2147
|
}
|
|
2148
|
+
return 'done';
|
|
2149
|
+
}
|
|
2150
|
+
function pastHorizon(identity) {
|
|
2151
|
+
return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
|
|
2152
|
+
}
|
|
2153
|
+
async function discardPending(room, local, signal) {
|
|
2154
|
+
const { locator } = room;
|
|
2155
|
+
const discarded = await exclusive(async () => {
|
|
2156
|
+
if (fences.has(locator))
|
|
2157
|
+
return false;
|
|
2158
|
+
const before = replicas.get(locator);
|
|
2159
|
+
const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
2160
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
2161
|
+
lastFenced.set(locator, ++fencings);
|
|
2162
|
+
try {
|
|
2163
|
+
await local.discard(locator);
|
|
2164
|
+
}
|
|
2165
|
+
finally {
|
|
2166
|
+
fences.delete(locator);
|
|
2167
|
+
}
|
|
2168
|
+
for (const batch of queue)
|
|
2169
|
+
pendings.delete(batch.id);
|
|
2170
|
+
queuesWithoutReplica.delete(locator);
|
|
2171
|
+
const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
|
|
2172
|
+
if (after !== null)
|
|
2173
|
+
replicas.set(locator, after);
|
|
2174
|
+
journal.record(`room discarded ${locator}`);
|
|
2175
|
+
publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
|
|
2176
|
+
moveTo({ ...status, ...attention() });
|
|
2177
|
+
return true;
|
|
2178
|
+
});
|
|
2179
|
+
if (!discarded)
|
|
2180
|
+
return;
|
|
2181
|
+
const progress = { complete: true, reachable: true };
|
|
2182
|
+
await roomTask(locator, async (live) => {
|
|
2183
|
+
await Promise.race([
|
|
2184
|
+
reload(room, local, progress, () => live() && !signal.aborted),
|
|
2185
|
+
abandonedOn(signal),
|
|
2186
|
+
]);
|
|
2187
|
+
});
|
|
2188
|
+
if (signal.aborted)
|
|
2189
|
+
return;
|
|
2190
|
+
await rest(progress);
|
|
1496
2191
|
}
|
|
1497
2192
|
function awaitingAnswer(locator) {
|
|
1498
2193
|
let release = () => { };
|
|
1499
2194
|
const awaited = new Promise((resolve) => {
|
|
1500
2195
|
release = resolve;
|
|
1501
2196
|
});
|
|
1502
|
-
answering.set(locator, awaited);
|
|
2197
|
+
answering.set(locator, { generation, answered: awaited });
|
|
1503
2198
|
return () => {
|
|
1504
|
-
if (answering.get(locator) === awaited)
|
|
2199
|
+
if (answering.get(locator)?.answered === awaited)
|
|
1505
2200
|
answering.delete(locator);
|
|
1506
2201
|
release();
|
|
1507
2202
|
};
|
|
@@ -1617,19 +2312,23 @@ export function composeRuntime(options) {
|
|
|
1617
2312
|
];
|
|
1618
2313
|
});
|
|
1619
2314
|
}
|
|
1620
|
-
async function synchronize(listed, local, progress, attempt) {
|
|
1621
|
-
const
|
|
1622
|
-
const
|
|
2315
|
+
async function synchronize(listed, local, progress, attempt, live) {
|
|
2316
|
+
const acquired = listed.filter(acquires);
|
|
2317
|
+
const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
|
|
2318
|
+
if (!live())
|
|
2319
|
+
return;
|
|
2320
|
+
const ready = acquired.filter((_, index) => {
|
|
1623
2321
|
const outcome = prepared[index];
|
|
1624
2322
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1625
2323
|
});
|
|
1626
2324
|
if (attempt === 'opened') {
|
|
1627
|
-
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
2325
|
+
options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
|
|
1628
2326
|
handed = true;
|
|
1629
2327
|
}
|
|
2328
|
+
discovering?.release();
|
|
1630
2329
|
const loaded = await Promise.allSettled(ready
|
|
1631
2330
|
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1632
|
-
.map((room) => loadRoom(room, local, progress)));
|
|
2331
|
+
.map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
|
|
1633
2332
|
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1634
2333
|
if (failed !== undefined)
|
|
1635
2334
|
throw failed.reason;
|
|
@@ -1658,14 +2357,14 @@ export function composeRuntime(options) {
|
|
|
1658
2357
|
}
|
|
1659
2358
|
return true;
|
|
1660
2359
|
}
|
|
1661
|
-
async function caughtUp(room, local, progress) {
|
|
2360
|
+
async function caughtUp(room, local, progress, live) {
|
|
1662
2361
|
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1663
|
-
if (held === undefined || authorization === undefined)
|
|
2362
|
+
if (held === undefined || authorization === undefined || !live())
|
|
1664
2363
|
return;
|
|
1665
2364
|
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
1666
2365
|
if (!behind)
|
|
1667
2366
|
return;
|
|
1668
|
-
const withheld = await advance(room, local, authorization);
|
|
2367
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1669
2368
|
if (withheld !== null)
|
|
1670
2369
|
await withhold(withheld, room, local, progress);
|
|
1671
2370
|
}
|
|
@@ -1675,39 +2374,46 @@ export function composeRuntime(options) {
|
|
|
1675
2374
|
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1676
2375
|
});
|
|
1677
2376
|
}
|
|
1678
|
-
async function loadRoom(room, local, progress) {
|
|
1679
|
-
if (
|
|
2377
|
+
async function loadRoom(room, local, progress, live) {
|
|
2378
|
+
if (!live() || authorization === undefined)
|
|
1680
2379
|
return;
|
|
1681
2380
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1682
|
-
await reload(room, local, progress);
|
|
2381
|
+
await reload(room, local, progress, live);
|
|
1683
2382
|
return;
|
|
1684
2383
|
}
|
|
1685
|
-
const withheld = await advance(room, local, authorization);
|
|
2384
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1686
2385
|
if (withheld !== null) {
|
|
1687
2386
|
await withhold(withheld, room, local, progress);
|
|
1688
2387
|
return;
|
|
1689
2388
|
}
|
|
1690
|
-
await caughtUp(room, local, progress);
|
|
2389
|
+
await caughtUp(room, local, progress, live);
|
|
1691
2390
|
}
|
|
1692
|
-
async function reload(room, local, progress) {
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
2391
|
+
async function reload(room, local, progress, live) {
|
|
2392
|
+
try {
|
|
2393
|
+
if (!live() || authorization === undefined)
|
|
2394
|
+
return;
|
|
2395
|
+
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
2396
|
+
if (!live())
|
|
2397
|
+
return;
|
|
2398
|
+
if (bootstrapped.kind !== 'staged') {
|
|
2399
|
+
await withhold(bootstrapped, room, local, progress);
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
2403
|
+
await caughtUp(room, local, progress, live);
|
|
2404
|
+
}
|
|
2405
|
+
finally {
|
|
2406
|
+
caughtUpTo(room.locator);
|
|
1701
2407
|
}
|
|
1702
|
-
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
1703
|
-
await caughtUp(room, local, progress);
|
|
1704
2408
|
}
|
|
1705
|
-
async function discoverRooms(local, progress, attempt) {
|
|
2409
|
+
async function discoverRooms(local, progress, attempt, live) {
|
|
1706
2410
|
if (closed || authorization === undefined) {
|
|
1707
2411
|
notifyReady();
|
|
1708
2412
|
return;
|
|
1709
2413
|
}
|
|
1710
2414
|
moveTo(transferring(status));
|
|
2415
|
+
discoveries += 1;
|
|
2416
|
+
const started = discoveries;
|
|
1711
2417
|
let result;
|
|
1712
2418
|
try {
|
|
1713
2419
|
result = await options.adapter.discover(authorization);
|
|
@@ -1715,7 +2421,8 @@ export function composeRuntime(options) {
|
|
|
1715
2421
|
catch {
|
|
1716
2422
|
result = { kind: 'transport-uncertainty' };
|
|
1717
2423
|
}
|
|
1718
|
-
if (
|
|
2424
|
+
if (!live()) {
|
|
2425
|
+
progress.complete = false;
|
|
1719
2426
|
notifyReady();
|
|
1720
2427
|
return;
|
|
1721
2428
|
}
|
|
@@ -1773,6 +2480,7 @@ export function composeRuntime(options) {
|
|
|
1773
2480
|
}
|
|
1774
2481
|
const previousCapabilities = capabilities;
|
|
1775
2482
|
capabilities = result.capabilities;
|
|
2483
|
+
outcomeRetention = result.retention.outcome;
|
|
1776
2484
|
notifyReady();
|
|
1777
2485
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1778
2486
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
@@ -1785,8 +2493,7 @@ export function composeRuntime(options) {
|
|
|
1785
2493
|
member = signedIn;
|
|
1786
2494
|
emitMember();
|
|
1787
2495
|
}
|
|
1788
|
-
await applyDiscovery(result.rooms, local);
|
|
1789
|
-
await synchronize(result.rooms, local, progress, attempt);
|
|
2496
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
1790
2497
|
}
|
|
1791
2498
|
async function ensureAuthorization() {
|
|
1792
2499
|
if (authorization !== undefined)
|
|
@@ -1800,20 +2507,50 @@ export function composeRuntime(options) {
|
|
|
1800
2507
|
throw error;
|
|
1801
2508
|
}
|
|
1802
2509
|
}
|
|
1803
|
-
function
|
|
2510
|
+
async function admittedRead(work) {
|
|
2511
|
+
if (roomReads.generation !== generation) {
|
|
2512
|
+
roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
2513
|
+
}
|
|
2514
|
+
const reads = roomReads;
|
|
2515
|
+
while (reads.free === 0)
|
|
2516
|
+
await new Promise((admit) => reads.waiting.push(admit));
|
|
2517
|
+
reads.free -= 1;
|
|
2518
|
+
try {
|
|
2519
|
+
await work();
|
|
2520
|
+
}
|
|
2521
|
+
finally {
|
|
2522
|
+
reads.free += 1;
|
|
2523
|
+
reads.waiting.shift()?.();
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
function roomTask(locator, work) {
|
|
2527
|
+
return task(roomLane(locator), (live) => admittedRead(() => work(live)));
|
|
2528
|
+
}
|
|
2529
|
+
function task(lane, work) {
|
|
1804
2530
|
if (stopping)
|
|
1805
|
-
return
|
|
1806
|
-
|
|
1807
|
-
|
|
2531
|
+
return Promise.resolve();
|
|
2532
|
+
const held = lanes.get(lane);
|
|
2533
|
+
const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
|
|
2534
|
+
const turn = current.generation;
|
|
2535
|
+
const live = () => turn === generation && continuing();
|
|
2536
|
+
const run = current.chain.then(() => work(live)).catch(() => undefined);
|
|
2537
|
+
current.chain = run;
|
|
2538
|
+
lanes.set(lane, current);
|
|
2539
|
+
working.add(run);
|
|
2540
|
+
void run.finally(() => working.delete(run));
|
|
2541
|
+
return run;
|
|
1808
2542
|
}
|
|
1809
2543
|
function sync(attempt) {
|
|
1810
|
-
return task(async () => {
|
|
1811
|
-
if (
|
|
2544
|
+
return task(OPENING_LANE, async (live) => {
|
|
2545
|
+
if (!live()) {
|
|
1812
2546
|
notifyReady();
|
|
1813
2547
|
return;
|
|
1814
2548
|
}
|
|
1815
2549
|
const progress = { complete: true, reachable: true };
|
|
1816
|
-
|
|
2550
|
+
const gate = discover();
|
|
2551
|
+
if (attempt === 'opened')
|
|
2552
|
+
opened = generation;
|
|
2553
|
+
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
1817
2554
|
if (attempt === 'opened')
|
|
1818
2555
|
streamed = progress;
|
|
1819
2556
|
complete = false;
|
|
@@ -1822,12 +2559,12 @@ export function composeRuntime(options) {
|
|
|
1822
2559
|
handed = false;
|
|
1823
2560
|
try {
|
|
1824
2561
|
const local = await openLocal();
|
|
1825
|
-
if (
|
|
2562
|
+
if (!live())
|
|
1826
2563
|
return;
|
|
1827
2564
|
await ensureAuthorization();
|
|
1828
|
-
if (
|
|
2565
|
+
if (!live())
|
|
1829
2566
|
return;
|
|
1830
|
-
await discoverRooms(local, progress, attempt);
|
|
2567
|
+
await discoverRooms(local, progress, attempt, live);
|
|
1831
2568
|
}
|
|
1832
2569
|
catch {
|
|
1833
2570
|
progress.complete = false;
|
|
@@ -1835,17 +2572,21 @@ export function composeRuntime(options) {
|
|
|
1835
2572
|
progress.reachable = false;
|
|
1836
2573
|
}
|
|
1837
2574
|
finally {
|
|
2575
|
+
gate.release();
|
|
1838
2576
|
notifyReady();
|
|
1839
2577
|
}
|
|
2578
|
+
if (!live())
|
|
2579
|
+
return;
|
|
1840
2580
|
if (attempt !== 'opened') {
|
|
1841
|
-
await finishOpening(progress);
|
|
2581
|
+
await finishOpening(progress, live);
|
|
1842
2582
|
return;
|
|
1843
2583
|
}
|
|
1844
2584
|
if (handed || closed || progress.reachable)
|
|
1845
2585
|
return;
|
|
1846
2586
|
underway = null;
|
|
2587
|
+
opened = null;
|
|
1847
2588
|
journal.record('connection reopen');
|
|
1848
|
-
await rest(progress);
|
|
2589
|
+
await rest(progress, live);
|
|
1849
2590
|
options.adapter.roomStream?.reopen();
|
|
1850
2591
|
});
|
|
1851
2592
|
}
|
|
@@ -1866,24 +2607,41 @@ export function composeRuntime(options) {
|
|
|
1866
2607
|
});
|
|
1867
2608
|
}
|
|
1868
2609
|
function applyOpening(expired) {
|
|
1869
|
-
|
|
1870
|
-
|
|
2610
|
+
const progress = underway?.generation === generation ? underway.progress : null;
|
|
2611
|
+
const cycle = progress ?? { complete: true, reachable: true };
|
|
2612
|
+
const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
|
|
2613
|
+
if (opened === generation)
|
|
2614
|
+
opened = null;
|
|
2615
|
+
return task(OPENING_LANE, async (live) => {
|
|
1871
2616
|
underway = null;
|
|
1872
|
-
if (
|
|
2617
|
+
if (!live())
|
|
1873
2618
|
return;
|
|
1874
2619
|
if (progress === null) {
|
|
1875
|
-
await
|
|
2620
|
+
await replacing;
|
|
2621
|
+
await rest(cycle, live);
|
|
1876
2622
|
return;
|
|
1877
2623
|
}
|
|
2624
|
+
await roomsCarried(expired);
|
|
2625
|
+
if (!live())
|
|
2626
|
+
return;
|
|
2627
|
+
const replaced = new Set(owed);
|
|
2628
|
+
owed.clear();
|
|
1878
2629
|
try {
|
|
1879
2630
|
const local = await openLocal();
|
|
1880
2631
|
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
1881
|
-
const reloading = held.filter((room) => expired.includes(room.locator)
|
|
1882
|
-
|
|
2632
|
+
const reloading = held.filter((room) => !expired.includes(room.locator) &&
|
|
2633
|
+
(replaced.has(room.locator) || revoked(room.locator)));
|
|
2634
|
+
for (const room of reloading)
|
|
2635
|
+
catching.add(room.locator);
|
|
2636
|
+
readSide.recover();
|
|
2637
|
+
const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
|
|
2638
|
+
const reloaded = [...(await reloads), ...(await replacing)];
|
|
1883
2639
|
if (progress.reachable) {
|
|
1884
|
-
for (const room of held)
|
|
1885
|
-
if (!reloading.includes(room))
|
|
2640
|
+
for (const room of held) {
|
|
2641
|
+
if (!expired.includes(room.locator) && !reloading.includes(room)) {
|
|
1886
2642
|
await confirmModels(room, local);
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
1887
2645
|
}
|
|
1888
2646
|
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1889
2647
|
if (failed !== undefined)
|
|
@@ -1893,21 +2651,21 @@ export function composeRuntime(options) {
|
|
|
1893
2651
|
progress.complete = false;
|
|
1894
2652
|
progress.reachable = false;
|
|
1895
2653
|
}
|
|
1896
|
-
|
|
2654
|
+
if (live())
|
|
2655
|
+
await finishOpening(progress, live);
|
|
1897
2656
|
});
|
|
1898
2657
|
}
|
|
1899
|
-
async function
|
|
1900
|
-
const progress = { complete: true, reachable: true };
|
|
2658
|
+
async function reloadNow(replaced, progress) {
|
|
1901
2659
|
try {
|
|
1902
2660
|
const local = await openLocal();
|
|
1903
|
-
await Promise.allSettled([...discovered.values()]
|
|
1904
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
1905
|
-
.map((room) => reload(room, local, progress)));
|
|
2661
|
+
return await Promise.allSettled([...discovered.values()]
|
|
2662
|
+
.filter((room) => replicas.has(room.locator) && replaced(room))
|
|
2663
|
+
.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
1906
2664
|
}
|
|
1907
2665
|
catch {
|
|
1908
2666
|
progress.reachable = false;
|
|
2667
|
+
return [];
|
|
1909
2668
|
}
|
|
1910
|
-
await rest(progress);
|
|
1911
2669
|
}
|
|
1912
2670
|
async function confirmModels(room, local) {
|
|
1913
2671
|
for (const attached of ['collection', 'typed-selection']) {
|
|
@@ -1920,38 +2678,94 @@ export function composeRuntime(options) {
|
|
|
1920
2678
|
function submitLater(locator) {
|
|
1921
2679
|
if (localSaves)
|
|
1922
2680
|
return;
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
2681
|
+
void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
|
|
2682
|
+
}
|
|
2683
|
+
async function submitPass(locator, signal) {
|
|
2684
|
+
const room = discovered.get(locator);
|
|
2685
|
+
if (closed || stopping || discoveryPaused || room === undefined)
|
|
2686
|
+
return 'done';
|
|
2687
|
+
const progress = { complete: true, reachable: true };
|
|
2688
|
+
let next;
|
|
2689
|
+
try {
|
|
2690
|
+
await ensureAuthorization();
|
|
2691
|
+
if (authorization === undefined)
|
|
2692
|
+
return 'done';
|
|
2693
|
+
next = await submitRoom(room, await openLocal(), authorization, progress, signal);
|
|
2694
|
+
}
|
|
2695
|
+
catch {
|
|
2696
|
+
progress.reachable = false;
|
|
2697
|
+
next = 'again';
|
|
2698
|
+
}
|
|
2699
|
+
if (signal.aborted)
|
|
2700
|
+
return 'done';
|
|
2701
|
+
await rest(progress);
|
|
2702
|
+
return next === 'again' && !socketOpen ? 'done' : next;
|
|
2703
|
+
}
|
|
2704
|
+
function repairLater(locator, when) {
|
|
2705
|
+
if (!socketOpen || repairing.has(locator))
|
|
1926
2706
|
return;
|
|
2707
|
+
void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
|
|
2708
|
+
}
|
|
2709
|
+
async function repairPass(locator, signal) {
|
|
2710
|
+
const room = discovered.get(locator);
|
|
2711
|
+
if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
|
|
2712
|
+
return 'done';
|
|
2713
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2714
|
+
return 'done';
|
|
2715
|
+
const progress = { complete: true, reachable: true };
|
|
2716
|
+
repairing.add(locator);
|
|
2717
|
+
try {
|
|
2718
|
+
const local = await openLocal();
|
|
2719
|
+
await roomTask(locator, async (live) => {
|
|
2720
|
+
const current = () => live() && !signal.aborted;
|
|
2721
|
+
if (!current() || authorization === undefined)
|
|
2722
|
+
return;
|
|
2723
|
+
const withheld = await Promise.race([
|
|
2724
|
+
advance(room, local, authorization, current),
|
|
2725
|
+
abandonedOn(signal),
|
|
2726
|
+
]);
|
|
2727
|
+
if (withheld !== null && current())
|
|
2728
|
+
await withhold(withheld, room, local, progress);
|
|
2729
|
+
});
|
|
1927
2730
|
}
|
|
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
|
-
|
|
2731
|
+
catch {
|
|
2732
|
+
stale.add(locator);
|
|
2733
|
+
progress.reachable = false;
|
|
2734
|
+
}
|
|
2735
|
+
finally {
|
|
2736
|
+
repairing.delete(locator);
|
|
2737
|
+
}
|
|
2738
|
+
if (signal.aborted)
|
|
2739
|
+
return 'done';
|
|
2740
|
+
await rest(progress);
|
|
2741
|
+
if (!stale.has(locator)) {
|
|
2742
|
+
if ((replicas.get(locator)?.queue.length ?? 0) > 0)
|
|
2743
|
+
submitLater(locator);
|
|
2744
|
+
return 'done';
|
|
2745
|
+
}
|
|
2746
|
+
if (!repairable.has(locator))
|
|
2747
|
+
return 'done';
|
|
2748
|
+
const reset = repairable.get(locator);
|
|
2749
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
2750
|
+
}
|
|
2751
|
+
function resendLater() {
|
|
2752
|
+
if (!socketOpen)
|
|
2753
|
+
return;
|
|
2754
|
+
void recovery.attempt('resend', resendPass, { again: true });
|
|
2755
|
+
}
|
|
2756
|
+
async function resendPass(signal) {
|
|
2757
|
+
if (!socketOpen || closed || stopping || discoveryPaused)
|
|
2758
|
+
return 'done';
|
|
2759
|
+
const reading = { complete: true, reachable: true };
|
|
2760
|
+
try {
|
|
2761
|
+
await conversations.resend(reading);
|
|
2762
|
+
}
|
|
2763
|
+
catch {
|
|
2764
|
+
reading.reachable = false;
|
|
2765
|
+
}
|
|
2766
|
+
if (signal.aborted)
|
|
2767
|
+
return 'done';
|
|
2768
|
+
return reading.reachable && reading.complete ? 'done' : 'again';
|
|
1955
2769
|
}
|
|
1956
2770
|
function submitAll() {
|
|
1957
2771
|
for (const [locator, replica] of replicas) {
|
|
@@ -1959,31 +2773,41 @@ export function composeRuntime(options) {
|
|
|
1959
2773
|
submitLater(locator);
|
|
1960
2774
|
}
|
|
1961
2775
|
}
|
|
1962
|
-
async function finishOpening(progress) {
|
|
2776
|
+
async function finishOpening(progress, live) {
|
|
1963
2777
|
try {
|
|
1964
|
-
if (
|
|
2778
|
+
if (!live()) {
|
|
1965
2779
|
progress.complete = false;
|
|
1966
2780
|
return;
|
|
1967
2781
|
}
|
|
1968
2782
|
submitAll();
|
|
2783
|
+
for (const locator of stale)
|
|
2784
|
+
if (repairable.has(locator))
|
|
2785
|
+
repairLater(locator, {});
|
|
1969
2786
|
await conversations.reconcile();
|
|
1970
2787
|
if (status.providerUnreachable)
|
|
1971
2788
|
conversations.reconnected();
|
|
1972
2789
|
const reading = { complete: true, reachable: true };
|
|
1973
|
-
|
|
1974
|
-
|
|
2790
|
+
if (demand.conversations)
|
|
2791
|
+
await conversations.catchUp(reading);
|
|
2792
|
+
if (progress.reachable && reading.reachable)
|
|
2793
|
+
await conversations.resend(reading);
|
|
2794
|
+
if (!reading.reachable) {
|
|
1975
2795
|
progress.reachable = false;
|
|
2796
|
+
resendLater();
|
|
2797
|
+
}
|
|
1976
2798
|
}
|
|
1977
2799
|
catch {
|
|
1978
2800
|
progress.complete = false;
|
|
1979
2801
|
}
|
|
1980
2802
|
finally {
|
|
1981
|
-
|
|
1982
|
-
|
|
2803
|
+
if (live()) {
|
|
2804
|
+
complete = progress.complete;
|
|
2805
|
+
await rest(progress, live);
|
|
2806
|
+
}
|
|
1983
2807
|
}
|
|
1984
2808
|
}
|
|
1985
|
-
async function rest(progress) {
|
|
1986
|
-
if (
|
|
2809
|
+
async function rest(progress, live = () => !closed) {
|
|
2810
|
+
if (!live())
|
|
1987
2811
|
return;
|
|
1988
2812
|
let lastCompleted = status.lastCompleted;
|
|
1989
2813
|
let synced = complete && stale.size === 0 && !pending();
|
|
@@ -1991,6 +2815,8 @@ export function composeRuntime(options) {
|
|
|
1991
2815
|
const completedAt = new Date().toISOString();
|
|
1992
2816
|
try {
|
|
1993
2817
|
await (await openLocal()).complete(completedAt);
|
|
2818
|
+
if (!live())
|
|
2819
|
+
return;
|
|
1994
2820
|
recorded = true;
|
|
1995
2821
|
lastCompleted = completedAt;
|
|
1996
2822
|
}
|
|
@@ -2005,40 +2831,68 @@ export function composeRuntime(options) {
|
|
|
2005
2831
|
...attention(),
|
|
2006
2832
|
});
|
|
2007
2833
|
}
|
|
2834
|
+
function discover() {
|
|
2835
|
+
let release = () => { };
|
|
2836
|
+
const done = new Promise((resolve) => {
|
|
2837
|
+
release = resolve;
|
|
2838
|
+
});
|
|
2839
|
+
discovering = { generation, done, release };
|
|
2840
|
+
return discovering;
|
|
2841
|
+
}
|
|
2008
2842
|
function applyRoom(locator, roomVersion) {
|
|
2009
2843
|
announced.set(locator, roomVersion);
|
|
2010
|
-
return task(async () => {
|
|
2011
|
-
|
|
2012
|
-
|
|
2844
|
+
return task(roomLane(locator), async (live) => {
|
|
2845
|
+
if (discovering?.generation === generation)
|
|
2846
|
+
await discovering.done;
|
|
2847
|
+
const saving = answering.get(locator);
|
|
2848
|
+
if (saving?.generation === generation)
|
|
2849
|
+
await saving.answered;
|
|
2850
|
+
if (!live() || authorization === undefined)
|
|
2013
2851
|
return;
|
|
2014
2852
|
const room = discovered.get(locator);
|
|
2015
2853
|
const held = replicas.get(locator);
|
|
2016
|
-
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2854
|
+
if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
|
|
2017
2855
|
return;
|
|
2018
2856
|
if (held !== undefined &&
|
|
2019
2857
|
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
2020
2858
|
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
2021
2859
|
return;
|
|
2022
2860
|
}
|
|
2023
|
-
const during = underway;
|
|
2861
|
+
const during = underway?.generation === generation ? underway.progress : null;
|
|
2024
2862
|
const progress = during ?? { complete: true, reachable: true };
|
|
2025
2863
|
if (during === null)
|
|
2026
2864
|
moveTo(transferring(status));
|
|
2027
2865
|
try {
|
|
2028
2866
|
const local = await openLocal();
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2867
|
+
await admittedRead(async () => {
|
|
2868
|
+
if (!live() || authorization === undefined)
|
|
2869
|
+
return;
|
|
2870
|
+
const withheld = await advance(room, local, authorization, live);
|
|
2871
|
+
if (withheld !== null)
|
|
2872
|
+
await withhold(withheld, room, local, progress);
|
|
2873
|
+
});
|
|
2032
2874
|
}
|
|
2033
2875
|
catch {
|
|
2034
2876
|
stale.add(locator);
|
|
2035
2877
|
}
|
|
2036
2878
|
finally {
|
|
2037
2879
|
if (during === null)
|
|
2038
|
-
await rest(progress);
|
|
2880
|
+
await rest(progress, live);
|
|
2039
2881
|
}
|
|
2040
2882
|
});
|
|
2041
2883
|
}
|
|
2884
|
+
function roomsCarried(replacing) {
|
|
2885
|
+
const replaced = new Set(replacing.map(roomLane));
|
|
2886
|
+
return Promise.all([...lanes]
|
|
2887
|
+
.filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
|
|
2888
|
+
.map(([, held]) => held.chain));
|
|
2889
|
+
}
|
|
2890
|
+
const receipts = {
|
|
2891
|
+
heads: async () => (await openLocal()).receivedHeads(),
|
|
2892
|
+
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
2893
|
+
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
2894
|
+
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
2895
|
+
};
|
|
2042
2896
|
function observeStream() {
|
|
2043
2897
|
const stream = options.adapter.roomStream;
|
|
2044
2898
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -2048,6 +2902,9 @@ export function composeRuntime(options) {
|
|
|
2048
2902
|
return;
|
|
2049
2903
|
if (event.kind === 'opened') {
|
|
2050
2904
|
socketOpen = true;
|
|
2905
|
+
generation += 1;
|
|
2906
|
+
conversations.reopened();
|
|
2907
|
+
recovery.resume();
|
|
2051
2908
|
void sync('opened');
|
|
2052
2909
|
}
|
|
2053
2910
|
else if (event.kind === 'closed')
|
|
@@ -2056,7 +2913,7 @@ export function composeRuntime(options) {
|
|
|
2056
2913
|
void applyOpening(event.expired);
|
|
2057
2914
|
else
|
|
2058
2915
|
void applyRoom(event.locator, event.roomVersion);
|
|
2059
|
-
});
|
|
2916
|
+
}, receipts);
|
|
2060
2917
|
}
|
|
2061
2918
|
function writesHeld() {
|
|
2062
2919
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -2071,6 +2928,7 @@ export function composeRuntime(options) {
|
|
|
2071
2928
|
haltedRefusal = false;
|
|
2072
2929
|
if (mode !== serviceMode) {
|
|
2073
2930
|
serviceMode = mode;
|
|
2931
|
+
queueReadiness();
|
|
2074
2932
|
journal.record(`service mode ${mode}`);
|
|
2075
2933
|
for (const listener of serviceModeListeners)
|
|
2076
2934
|
listener(mode);
|
|
@@ -2083,6 +2941,8 @@ export function composeRuntime(options) {
|
|
|
2083
2941
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
2084
2942
|
return;
|
|
2085
2943
|
withdrawConversations = stream.observe((event) => {
|
|
2944
|
+
if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
|
|
2945
|
+
queueReadiness();
|
|
2086
2946
|
if (conversations.receive(event)) {
|
|
2087
2947
|
void ready.then(scheduleCurrency);
|
|
2088
2948
|
return;
|
|
@@ -2094,17 +2954,18 @@ export function composeRuntime(options) {
|
|
|
2094
2954
|
});
|
|
2095
2955
|
}
|
|
2096
2956
|
function scheduleCurrency() {
|
|
2097
|
-
if (currencyQueued || closed || stopping)
|
|
2957
|
+
if (currencyQueued === generation || closed || stopping)
|
|
2098
2958
|
return;
|
|
2099
|
-
currencyQueued =
|
|
2100
|
-
void task(runCurrency);
|
|
2959
|
+
currencyQueued = generation;
|
|
2960
|
+
void task(CONVERSATION_LANE, runCurrency);
|
|
2101
2961
|
}
|
|
2102
|
-
async function runCurrency() {
|
|
2103
|
-
currencyQueued
|
|
2104
|
-
|
|
2962
|
+
async function runCurrency(live) {
|
|
2963
|
+
if (currencyQueued === generation)
|
|
2964
|
+
currencyQueued = null;
|
|
2965
|
+
if (!live())
|
|
2105
2966
|
return;
|
|
2106
2967
|
await conversations.reconcile();
|
|
2107
|
-
if (conversations.current()) {
|
|
2968
|
+
if (conversations.current() || !demand.conversations) {
|
|
2108
2969
|
show();
|
|
2109
2970
|
return;
|
|
2110
2971
|
}
|
|
@@ -2116,19 +2977,24 @@ export function composeRuntime(options) {
|
|
|
2116
2977
|
catch {
|
|
2117
2978
|
progress.reachable = false;
|
|
2118
2979
|
}
|
|
2119
|
-
await rest(progress);
|
|
2980
|
+
await rest(progress, live);
|
|
2120
2981
|
}
|
|
2121
2982
|
function close() {
|
|
2122
2983
|
if (closed)
|
|
2123
2984
|
return;
|
|
2124
2985
|
closed = true;
|
|
2986
|
+
if (narrowing !== null)
|
|
2987
|
+
clearTimeout(narrowing);
|
|
2988
|
+
if (rechecking !== null)
|
|
2989
|
+
clearInterval(rechecking);
|
|
2990
|
+
readinessWatches.clear();
|
|
2125
2991
|
withdrawStream?.();
|
|
2126
2992
|
withdrawStream = undefined;
|
|
2127
2993
|
withdrawConversations?.();
|
|
2128
2994
|
withdrawConversations = undefined;
|
|
2129
2995
|
withdrawServiceMode?.();
|
|
2130
2996
|
conversations.close();
|
|
2131
|
-
|
|
2997
|
+
void recovery.retire();
|
|
2132
2998
|
store?.close();
|
|
2133
2999
|
void opening?.then((opened) => {
|
|
2134
3000
|
opened.close();
|
|
@@ -2146,9 +3012,8 @@ export function composeRuntime(options) {
|
|
|
2146
3012
|
}
|
|
2147
3013
|
async function stop() {
|
|
2148
3014
|
stopping = true;
|
|
2149
|
-
|
|
2150
|
-
await
|
|
2151
|
-
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
3015
|
+
await recovery.retire();
|
|
3016
|
+
await Promise.all(working);
|
|
2152
3017
|
await mutations;
|
|
2153
3018
|
close();
|
|
2154
3019
|
}
|
|
@@ -2170,17 +3035,69 @@ export function composeRuntime(options) {
|
|
|
2170
3035
|
return planOf(input, roomFacts(reach), planReach(reach));
|
|
2171
3036
|
}
|
|
2172
3037
|
function watchPlan(plan, deliver) {
|
|
2173
|
-
|
|
2174
|
-
|
|
3038
|
+
return readSide.watch(plan, identityOf(plan), () => {
|
|
3039
|
+
const cover = covered(plan);
|
|
3040
|
+
awaitPreparation(cover);
|
|
3041
|
+
return cover;
|
|
3042
|
+
}, deliver);
|
|
2175
3043
|
}
|
|
2176
3044
|
async function searched(input, reach) {
|
|
2177
3045
|
const plan = searchPlanOf(input);
|
|
2178
|
-
const
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
3046
|
+
const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
|
|
3047
|
+
for (;;) {
|
|
3048
|
+
const cover = covered(filter);
|
|
3049
|
+
awaitPreparation(cover);
|
|
3050
|
+
const pending = 'pending' in cover ? [...cover.pending] : [];
|
|
3051
|
+
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
3052
|
+
const held = covered({
|
|
3053
|
+
...filter,
|
|
3054
|
+
rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
|
|
3055
|
+
});
|
|
3056
|
+
const covering = 'pending' in held ? [] : held.coverage.rooms;
|
|
3057
|
+
const unindexed = covering
|
|
3058
|
+
.filter(({ room }) => !searchIndex.rooms().has(room))
|
|
3059
|
+
.map(({ room }) => room);
|
|
3060
|
+
for (const room of unindexed)
|
|
3061
|
+
pending.push({ room, awaits: 'index' });
|
|
3062
|
+
const listed = conversations.searchable();
|
|
3063
|
+
const named = reach !== null && !reach.conversations
|
|
3064
|
+
? []
|
|
3065
|
+
: (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
|
|
3066
|
+
const waiting = named.filter((one) => !searchIndex.conversations().has(one));
|
|
3067
|
+
for (const conversation of waiting)
|
|
3068
|
+
pending.push({ conversation, awaits: 'index' });
|
|
3069
|
+
if (unindexed.length > 0 || waiting.length > 0)
|
|
3070
|
+
index(unindexed, waiting);
|
|
3071
|
+
const complete = pending.length === 0 && !('pending' in cover);
|
|
3072
|
+
const answering = complete
|
|
3073
|
+
? filter
|
|
3074
|
+
: {
|
|
3075
|
+
...filter,
|
|
3076
|
+
rooms: covering
|
|
3077
|
+
.filter(({ room }) => searchIndex.rooms().has(room))
|
|
3078
|
+
.map(({ room }) => room),
|
|
3079
|
+
};
|
|
3080
|
+
const answered = covered(answering);
|
|
3081
|
+
if ('pending' in answered)
|
|
3082
|
+
continue;
|
|
3083
|
+
const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
|
|
3084
|
+
const seen = searchIndex.changes();
|
|
3085
|
+
await searchIndex.caughtUp();
|
|
3086
|
+
const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(answering);
|
|
3087
|
+
if (selected !== null && 'pending' in selected)
|
|
3088
|
+
continue;
|
|
3089
|
+
if (searchIndex.changes() !== seen ||
|
|
3090
|
+
JSON.stringify(covered(answering)) !== JSON.stringify(answered))
|
|
3091
|
+
continue;
|
|
3092
|
+
const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, complete ? null : new Set(answering.rooms));
|
|
3093
|
+
const coverage = {
|
|
3094
|
+
rooms: answered.coverage.rooms,
|
|
3095
|
+
conversations: searchedConversations,
|
|
3096
|
+
index: searchIndex.generation(),
|
|
3097
|
+
overlay: answered.coverage.overlay,
|
|
3098
|
+
};
|
|
3099
|
+
return complete ? { ...hits, coverage } : { pending, ...hits, coverage };
|
|
3100
|
+
}
|
|
2184
3101
|
}
|
|
2185
3102
|
const watch = (topic, input, deliver) => {
|
|
2186
3103
|
if (topic === 'room-drafts') {
|
|
@@ -2226,6 +3143,9 @@ export function composeRuntime(options) {
|
|
|
2226
3143
|
presentationListeners.delete(listener);
|
|
2227
3144
|
};
|
|
2228
3145
|
}
|
|
3146
|
+
if (topic === 'readiness') {
|
|
3147
|
+
return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
|
|
3148
|
+
}
|
|
2229
3149
|
if (topic === 'rooms') {
|
|
2230
3150
|
roomListeners.add(deliver);
|
|
2231
3151
|
deliver(listed());
|
|
@@ -2392,21 +3312,29 @@ export function composeRuntime(options) {
|
|
|
2392
3312
|
return journal.export();
|
|
2393
3313
|
},
|
|
2394
3314
|
persistence() {
|
|
2395
|
-
return
|
|
3315
|
+
return persistence === 'granted';
|
|
3316
|
+
},
|
|
3317
|
+
async retry() {
|
|
3318
|
+
journal.record('retry');
|
|
3319
|
+
unindexable.clear();
|
|
3320
|
+
queueReadiness();
|
|
3321
|
+
resume();
|
|
2396
3322
|
},
|
|
2397
3323
|
observe: observing(watch),
|
|
2398
3324
|
async reauthorize() {
|
|
2399
3325
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2400
3326
|
return;
|
|
2401
3327
|
journal.record('connection reauthorize');
|
|
2402
|
-
await
|
|
3328
|
+
await Promise.all([...lanes.values()]
|
|
3329
|
+
.filter((held) => held.generation === generation)
|
|
3330
|
+
.map((held) => held.chain));
|
|
2403
3331
|
await mutations;
|
|
2404
3332
|
authorization = undefined;
|
|
2405
3333
|
discoveryPaused = false;
|
|
2406
3334
|
if (options.adapter.roomStream === undefined)
|
|
2407
3335
|
await sync('unstreamed');
|
|
2408
3336
|
else
|
|
2409
|
-
await task(ensureAuthorization);
|
|
3337
|
+
await task(OPENING_LANE, ensureAuthorization);
|
|
2410
3338
|
},
|
|
2411
3339
|
};
|
|
2412
3340
|
const reachable = {
|
|
@@ -2417,6 +3345,21 @@ export function composeRuntime(options) {
|
|
|
2417
3345
|
query: (plan) => rowsQuery(plan),
|
|
2418
3346
|
watchPlan,
|
|
2419
3347
|
search: searched,
|
|
3348
|
+
watchReadiness,
|
|
3349
|
+
};
|
|
3350
|
+
return {
|
|
3351
|
+
md,
|
|
3352
|
+
stop,
|
|
3353
|
+
lose: loseRooms,
|
|
3354
|
+
watch,
|
|
3355
|
+
acquire,
|
|
3356
|
+
persisted(answer) {
|
|
3357
|
+
if (answer === persistence)
|
|
3358
|
+
return;
|
|
3359
|
+
persistence = answer;
|
|
3360
|
+
journal.record(`persistence ${answer}`);
|
|
3361
|
+
queueReadiness();
|
|
3362
|
+
},
|
|
3363
|
+
within: (reach, port) => narrowed(reachable, reach, port),
|
|
2420
3364
|
};
|
|
2421
|
-
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2422
3365
|
}
|