@agentshouse/mdruntime 0.1.0-c56daf494ecb4 → 0.1.0-c5d09b1c896fb
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 +261 -261
- 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 +37 -9
- package/dist/adapter.d.ts.map +1 -1
- package/dist/bootstrap.d.ts +2 -2
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +26 -4
- package/dist/change-set.d.ts +2 -0
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +36 -10
- package/dist/chat/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 +3 -3
- package/dist/chat/list.d.ts +2 -1
- package/dist/chat/list.d.ts.map +1 -1
- package/dist/chat/list.js +4 -3
- package/dist/chat/message.d.ts.map +1 -1
- package/dist/chat/message.js +2 -2
- package/dist/chat/messages.d.ts +1 -0
- package/dist/chat/messages.d.ts.map +1 -1
- package/dist/chat/messages.js +4 -0
- package/dist/chat/provider.d.ts +1 -1
- package/dist/chat/provider.d.ts.map +1 -1
- package/dist/chat/provider.js +51 -67
- package/dist/chat/ready.d.ts +1 -0
- package/dist/chat/ready.d.ts.map +1 -1
- package/dist/chat/ready.js +2 -2
- package/dist/client.d.ts +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 -309
- 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 +2 -2
- package/dist/entry.d.ts.map +1 -1
- package/dist/entry.js +3 -3
- 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 +13 -2
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +49 -7
- 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 +9 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/memory.d.ts +4 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +23 -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 +2 -0
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +58 -55
- package/dist/query.d.ts +32 -3
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +3 -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 +7 -4
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +6 -4
- package/dist/read-side.d.ts +13 -6
- package/dist/read-side.d.ts.map +1 -1
- package/dist/read-side.js +285 -83
- package/dist/readiness.d.ts +53 -0
- package/dist/readiness.d.ts.map +1 -0
- package/dist/readiness.js +33 -0
- package/dist/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 +17 -10
- package/dist/replica.d.ts +1 -1
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +57 -20
- package/dist/room-draft.d.ts +13 -1
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +2 -2
- package/dist/rows.d.ts +5 -13
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +5 -13
- package/dist/save.d.ts +5 -0
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +36 -13
- package/dist/search.d.ts +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 +42 -11
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +276 -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 +2 -2
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,11 +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
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
];
|
|
240
|
-
return paths.filter((path) => !roots.some((root) => beneath(root, path))).length;
|
|
310
|
+
const weighed = outsideOf(roots);
|
|
311
|
+
return (tally(replica.visible.documents, weighed.documents) +
|
|
312
|
+
tally(replica.visible.binaries, weighed.binaries));
|
|
241
313
|
}
|
|
242
314
|
function listed() {
|
|
243
315
|
return structuredClone([...rooms.values()].map((room) => {
|
|
@@ -264,15 +336,19 @@ export function composeRuntime(options) {
|
|
|
264
336
|
return { listed: roomsListed, rooms: held };
|
|
265
337
|
}
|
|
266
338
|
function emitRooms() {
|
|
339
|
+
readSide.recover();
|
|
340
|
+
queueReadiness();
|
|
267
341
|
const value = listed();
|
|
268
342
|
for (const listener of roomListeners)
|
|
269
343
|
listener(value);
|
|
270
344
|
}
|
|
271
345
|
function emitRefusal() {
|
|
346
|
+
queueReadiness();
|
|
272
347
|
for (const listener of refusalListeners)
|
|
273
348
|
listener(refusal);
|
|
274
349
|
}
|
|
275
350
|
function emitStorageLoss() {
|
|
351
|
+
queueReadiness();
|
|
276
352
|
for (const listener of storageLossListeners)
|
|
277
353
|
listener(storageLoss);
|
|
278
354
|
}
|
|
@@ -289,7 +365,8 @@ export function composeRuntime(options) {
|
|
|
289
365
|
function show() {
|
|
290
366
|
if (closed)
|
|
291
367
|
return;
|
|
292
|
-
|
|
368
|
+
queueReadiness();
|
|
369
|
+
const next = status.state === 'synced' && demand.conversations && !conversations.current()
|
|
293
370
|
? { ...status, state: 'syncing' }
|
|
294
371
|
: status;
|
|
295
372
|
if (same(shown, next))
|
|
@@ -318,42 +395,472 @@ export function composeRuntime(options) {
|
|
|
318
395
|
}
|
|
319
396
|
}
|
|
320
397
|
function applyChangeSet(set) {
|
|
321
|
-
const held =
|
|
322
|
-
|
|
323
|
-
|
|
398
|
+
const held = {
|
|
399
|
+
...set,
|
|
400
|
+
documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
|
|
401
|
+
};
|
|
324
402
|
readSide.apply(held);
|
|
325
403
|
searchIndex.changed(held);
|
|
326
404
|
}
|
|
327
|
-
function
|
|
328
|
-
|
|
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))
|
|
329
421
|
return;
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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);
|
|
340
440
|
}
|
|
341
|
-
|
|
441
|
+
finally {
|
|
442
|
+
projecting.delete(locator);
|
|
443
|
+
}
|
|
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)
|
|
342
460
|
return;
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
+
})();
|
|
346
476
|
}
|
|
347
477
|
function reachedRooms(plan) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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)
|
|
351
540
|
return null;
|
|
352
|
-
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
|
+
};
|
|
353
716
|
}
|
|
354
|
-
function
|
|
355
|
-
|
|
356
|
-
|
|
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
|
+
}
|
|
829
|
+
}
|
|
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(() => { });
|
|
357
864
|
}
|
|
358
865
|
function publishChangeSet(set) {
|
|
359
866
|
applyChangeSet(set);
|
|
@@ -370,34 +877,7 @@ export function composeRuntime(options) {
|
|
|
370
877
|
emitPresentation(app);
|
|
371
878
|
conversations.changed(set);
|
|
372
879
|
}
|
|
373
|
-
function stopTimer() {
|
|
374
|
-
if (resetTimer === undefined)
|
|
375
|
-
return;
|
|
376
|
-
clearTimeout(resetTimer);
|
|
377
|
-
resetTimer = undefined;
|
|
378
|
-
}
|
|
379
|
-
function armTimer() {
|
|
380
|
-
stopTimer();
|
|
381
|
-
if (closed || stopping)
|
|
382
|
-
return;
|
|
383
|
-
let earliest = null;
|
|
384
|
-
for (const { submission } of pendings.values()) {
|
|
385
|
-
if (submission.state !== 'reset')
|
|
386
|
-
continue;
|
|
387
|
-
const at = Date.parse(submission.reset);
|
|
388
|
-
if (earliest === null || at < earliest)
|
|
389
|
-
earliest = at;
|
|
390
|
-
}
|
|
391
|
-
if (earliest === null)
|
|
392
|
-
return;
|
|
393
|
-
// A Save House held back goes again only once the reset its answer named has passed.
|
|
394
|
-
resetTimer = setTimeout(() => {
|
|
395
|
-
resetTimer = undefined;
|
|
396
|
-
submitAll();
|
|
397
|
-
}, Math.max(0, earliest - Date.now()));
|
|
398
|
-
}
|
|
399
880
|
async function refuse(reason, provider = null) {
|
|
400
|
-
stopTimer();
|
|
401
881
|
discoveryPaused = true;
|
|
402
882
|
try {
|
|
403
883
|
if (reason === 'revoked-authorization') {
|
|
@@ -476,14 +956,24 @@ export function composeRuntime(options) {
|
|
|
476
956
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
477
957
|
});
|
|
478
958
|
}
|
|
479
|
-
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));
|
|
480
966
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
481
967
|
for (const locator of listedLocators)
|
|
482
968
|
scopedRefusals.delete(locator);
|
|
483
969
|
discovered.clear();
|
|
484
970
|
for (const room of listed)
|
|
485
971
|
discovered.set(room.locator, room);
|
|
486
|
-
|
|
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);
|
|
487
977
|
for (const room of listed) {
|
|
488
978
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
489
979
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -494,6 +984,7 @@ export function composeRuntime(options) {
|
|
|
494
984
|
}
|
|
495
985
|
roomsListed = true;
|
|
496
986
|
emitRooms();
|
|
987
|
+
return listed;
|
|
497
988
|
}
|
|
498
989
|
function unlistedHeld(listed) {
|
|
499
990
|
return [
|
|
@@ -505,41 +996,73 @@ export function composeRuntime(options) {
|
|
|
505
996
|
]),
|
|
506
997
|
].filter((locator) => !listed.has(locator));
|
|
507
998
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
|
|
515
|
-
room: true,
|
|
516
|
-
queue: true,
|
|
517
|
-
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
518
|
-
}));
|
|
519
|
-
for (const locator of unlisted) {
|
|
520
|
-
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
521
|
-
pendings.delete(batch.id);
|
|
522
|
-
replicas.delete(locator);
|
|
523
|
-
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
|
|
524
1005
|
rooms.delete(locator);
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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);
|
|
539
1041
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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));
|
|
543
1066
|
}
|
|
544
1067
|
function restored(key, persisted) {
|
|
545
1068
|
const prior = models.get(key);
|
|
@@ -560,7 +1083,7 @@ export function composeRuntime(options) {
|
|
|
560
1083
|
return model;
|
|
561
1084
|
}
|
|
562
1085
|
async function hydrate(opened) {
|
|
563
|
-
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([
|
|
564
1087
|
opened.all(),
|
|
565
1088
|
opened.queues(),
|
|
566
1089
|
opened.roomDrafts(),
|
|
@@ -572,7 +1095,9 @@ export function composeRuntime(options) {
|
|
|
572
1095
|
opened.capabilities(),
|
|
573
1096
|
opened.member(),
|
|
574
1097
|
opened.transfers(),
|
|
1098
|
+
opened.targets(),
|
|
575
1099
|
]);
|
|
1100
|
+
targets = heldTargets;
|
|
576
1101
|
transfers.clear();
|
|
577
1102
|
for (const record of staged)
|
|
578
1103
|
transfers.set(record.locator, record);
|
|
@@ -634,6 +1159,7 @@ export function composeRuntime(options) {
|
|
|
634
1159
|
async function readState(opened) {
|
|
635
1160
|
const { lastCompleted, locators, read } = await hydrate(opened);
|
|
636
1161
|
applyChangeSet(read);
|
|
1162
|
+
prepare();
|
|
637
1163
|
if (replicas.size > 0)
|
|
638
1164
|
emitRooms();
|
|
639
1165
|
if (member !== null)
|
|
@@ -644,8 +1170,15 @@ export function composeRuntime(options) {
|
|
|
644
1170
|
return lastCompleted;
|
|
645
1171
|
}
|
|
646
1172
|
async function open() {
|
|
647
|
-
|
|
1173
|
+
if (closed)
|
|
1174
|
+
throw new Error('runtime closed');
|
|
1175
|
+
const opened = await openRuntimeStore(isFenced);
|
|
648
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
|
+
}
|
|
649
1182
|
const kept = options.shell.generation;
|
|
650
1183
|
const storedGeneration = await opened.generation();
|
|
651
1184
|
const loss = detectStorageLoss(storedGeneration, kept);
|
|
@@ -659,6 +1192,8 @@ export function composeRuntime(options) {
|
|
|
659
1192
|
emitStorageLoss();
|
|
660
1193
|
}
|
|
661
1194
|
const lastCompleted = await readState(opened);
|
|
1195
|
+
if (interrupted.length > 0)
|
|
1196
|
+
await erased(opened, interrupted);
|
|
662
1197
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
663
1198
|
return opened;
|
|
664
1199
|
}
|
|
@@ -740,17 +1275,19 @@ export function composeRuntime(options) {
|
|
|
740
1275
|
moveTo({ ...status, ...attention() });
|
|
741
1276
|
throw error;
|
|
742
1277
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
const
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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
|
+
};
|
|
754
1291
|
}
|
|
755
1292
|
function adoptModels(locator, record) {
|
|
756
1293
|
for (const [attached, model] of [
|
|
@@ -783,11 +1320,16 @@ export function composeRuntime(options) {
|
|
|
783
1320
|
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
784
1321
|
};
|
|
785
1322
|
}
|
|
786
|
-
function publish(local, room, confirmed, landing
|
|
1323
|
+
function publish(local, room, confirmed, landing, live) {
|
|
787
1324
|
const { base, stored } = landing;
|
|
788
1325
|
return exclusive(async () => {
|
|
789
|
-
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);
|
|
790
1331
|
return true;
|
|
1332
|
+
}
|
|
791
1333
|
const prior = replicas.get(room.locator);
|
|
792
1334
|
if (base !== undefined &&
|
|
793
1335
|
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
@@ -801,24 +1343,42 @@ export function composeRuntime(options) {
|
|
|
801
1343
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
802
1344
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
803
1345
|
});
|
|
804
|
-
|
|
805
|
-
|
|
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) {
|
|
806
1359
|
transfers.delete(room.locator);
|
|
1360
|
+
catching.delete(room.locator);
|
|
1361
|
+
}
|
|
807
1362
|
for (const batch of accepted)
|
|
808
1363
|
pendings.delete(batch.id);
|
|
809
1364
|
stale.delete(room.locator);
|
|
1365
|
+
unindexable.delete(room.locator);
|
|
810
1366
|
if (unreadable.delete(room.locator))
|
|
811
1367
|
scopedRefusals.delete(room.locator);
|
|
812
1368
|
sourceRefusals.delete(room.locator);
|
|
813
1369
|
declarationRefusals.delete(room.locator);
|
|
814
1370
|
list(room, true);
|
|
1371
|
+
if (prior === undefined)
|
|
1372
|
+
prepared.add(room.locator);
|
|
815
1373
|
publishChangeSet(set);
|
|
816
1374
|
moveTo({ ...status, ...attention() });
|
|
1375
|
+
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
817
1376
|
return true;
|
|
818
1377
|
});
|
|
819
1378
|
}
|
|
820
1379
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
821
1380
|
const local = await openLocal();
|
|
1381
|
+
await referenced(locator);
|
|
822
1382
|
const replica = replicas.get(locator);
|
|
823
1383
|
const room = rooms.get(locator);
|
|
824
1384
|
if (replica === undefined || room === undefined) {
|
|
@@ -866,6 +1426,11 @@ export function composeRuntime(options) {
|
|
|
866
1426
|
changes,
|
|
867
1427
|
bases: parts.flatMap(({ bases }) => bases),
|
|
868
1428
|
writtenBy,
|
|
1429
|
+
clean: {
|
|
1430
|
+
text: replica.confirmed.record.documents,
|
|
1431
|
+
binaries: replica.confirmed.record.binaries,
|
|
1432
|
+
composition: compositionOf(replica.confirmed.record),
|
|
1433
|
+
},
|
|
869
1434
|
};
|
|
870
1435
|
const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
|
|
871
1436
|
if (checked?.kind === 'refused')
|
|
@@ -936,6 +1501,8 @@ export function composeRuntime(options) {
|
|
|
936
1501
|
catch {
|
|
937
1502
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
938
1503
|
}
|
|
1504
|
+
if (fences.has(locator))
|
|
1505
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
939
1506
|
for (const origin of withdrawn.keys())
|
|
940
1507
|
pendings.delete(origin);
|
|
941
1508
|
for (const [id, drafts] of drafted) {
|
|
@@ -965,6 +1532,8 @@ export function composeRuntime(options) {
|
|
|
965
1532
|
return;
|
|
966
1533
|
}
|
|
967
1534
|
await local.submit(locator, identifier, submission, lost);
|
|
1535
|
+
if (fences.has(locator))
|
|
1536
|
+
return;
|
|
968
1537
|
pendings.set(identifier, { ...state, submission, lost });
|
|
969
1538
|
const replica = replicas.get(locator);
|
|
970
1539
|
if (replica !== undefined)
|
|
@@ -975,6 +1544,8 @@ export function composeRuntime(options) {
|
|
|
975
1544
|
if (!continuing())
|
|
976
1545
|
throw new Error('connection refused');
|
|
977
1546
|
await local.keepUpload(locator, identifier, upload);
|
|
1547
|
+
if (fences.has(locator))
|
|
1548
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
978
1549
|
pendings.set(identifier, { ...pendings.get(identifier), upload });
|
|
979
1550
|
if (!continuing())
|
|
980
1551
|
throw new Error('connection refused');
|
|
@@ -990,6 +1561,7 @@ export function composeRuntime(options) {
|
|
|
990
1561
|
}
|
|
991
1562
|
if (closed || authorization === undefined || refusal !== null)
|
|
992
1563
|
return UNCERTAIN;
|
|
1564
|
+
const since = fencings;
|
|
993
1565
|
const transfer = ['transfer', 'history', locator, path];
|
|
994
1566
|
if (query.revision !== undefined)
|
|
995
1567
|
transfer.push(query.revision);
|
|
@@ -1004,6 +1576,8 @@ export function composeRuntime(options) {
|
|
|
1004
1576
|
...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
|
|
1005
1577
|
});
|
|
1006
1578
|
journal.record(`outcome history ${result.kind}`);
|
|
1579
|
+
if (fencedSince(locator, since))
|
|
1580
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
1007
1581
|
if (result.kind === 'scoped-refusal') {
|
|
1008
1582
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
1009
1583
|
if (room !== undefined)
|
|
@@ -1026,6 +1600,12 @@ export function composeRuntime(options) {
|
|
|
1026
1600
|
}
|
|
1027
1601
|
return null;
|
|
1028
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
|
+
}
|
|
1029
1609
|
async function readBinary(request) {
|
|
1030
1610
|
if (closed || refusal !== null)
|
|
1031
1611
|
return UNCERTAIN;
|
|
@@ -1037,12 +1617,16 @@ export function composeRuntime(options) {
|
|
|
1037
1617
|
}
|
|
1038
1618
|
if (closed || authorization === undefined || refusal !== null)
|
|
1039
1619
|
return UNCERTAIN;
|
|
1620
|
+
const since = fencings;
|
|
1040
1621
|
journal.record(request.kind === 'path'
|
|
1041
1622
|
? `transfer binary ${request.locator} ${request.path}`
|
|
1042
1623
|
: `transfer binary ${request.fileRef}`);
|
|
1043
1624
|
try {
|
|
1044
1625
|
const result = await options.adapter.binary(authorization, request);
|
|
1045
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 });
|
|
1046
1630
|
if (result.kind === 'scoped-refusal') {
|
|
1047
1631
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
1048
1632
|
if (room !== undefined)
|
|
@@ -1166,11 +1750,11 @@ export function composeRuntime(options) {
|
|
|
1166
1750
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
1167
1751
|
!options.adapter.reached(locator, roomVersion, written));
|
|
1168
1752
|
}
|
|
1169
|
-
async function pullRefresh(authorized, confirmed) {
|
|
1170
|
-
if (!
|
|
1753
|
+
async function pullRefresh(authorized, confirmed, live) {
|
|
1754
|
+
if (!live())
|
|
1171
1755
|
return unconfirmed;
|
|
1172
1756
|
try {
|
|
1173
|
-
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);
|
|
1174
1758
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
1175
1759
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
1176
1760
|
return result;
|
|
@@ -1180,47 +1764,80 @@ export function composeRuntime(options) {
|
|
|
1180
1764
|
throw error;
|
|
1181
1765
|
}
|
|
1182
1766
|
}
|
|
1183
|
-
function staging(local) {
|
|
1767
|
+
function staging(local, live) {
|
|
1184
1768
|
return {
|
|
1185
1769
|
held: (locator) => transfers.get(locator) ?? null,
|
|
1186
1770
|
staged: (record) => local.staged(record),
|
|
1187
1771
|
stage: (record, page) => exclusive(async () => {
|
|
1188
|
-
if (!
|
|
1772
|
+
if (!live())
|
|
1773
|
+
return false;
|
|
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);
|
|
1189
1784
|
return false;
|
|
1190
|
-
|
|
1785
|
+
}
|
|
1191
1786
|
transfers.set(record.locator, record);
|
|
1787
|
+
readSide.recover();
|
|
1192
1788
|
return true;
|
|
1193
1789
|
}),
|
|
1194
1790
|
abandon: (locator) => exclusive(async () => {
|
|
1195
|
-
if (!
|
|
1791
|
+
if (!live())
|
|
1196
1792
|
return;
|
|
1197
1793
|
await local.abandon(locator);
|
|
1198
1794
|
transfers.delete(locator);
|
|
1795
|
+
readSide.recover();
|
|
1199
1796
|
}),
|
|
1200
1797
|
};
|
|
1201
1798
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
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())
|
|
1204
1807
|
return unconfirmed;
|
|
1808
|
+
if (replicas.has(room.locator)) {
|
|
1809
|
+
catching.add(room.locator);
|
|
1810
|
+
readSide.recover();
|
|
1811
|
+
}
|
|
1205
1812
|
try {
|
|
1206
1813
|
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
1207
1814
|
collection: modelOf(room.locator),
|
|
1208
1815
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
1209
|
-
}, (models, held) => remodel(room.locator, models, held),
|
|
1816
|
+
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
1210
1817
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
1211
1818
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1819
|
+
if (result.kind !== 'staged')
|
|
1820
|
+
caughtUpTo(room.locator);
|
|
1212
1821
|
return result;
|
|
1213
1822
|
}
|
|
1214
1823
|
catch (error) {
|
|
1215
1824
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
1825
|
+
caughtUpTo(room.locator);
|
|
1216
1826
|
throw error;
|
|
1217
1827
|
}
|
|
1218
1828
|
}
|
|
1219
|
-
|
|
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;
|
|
1220
1837
|
const held = replicas.get(room.locator);
|
|
1221
1838
|
if (held !== undefined) {
|
|
1222
|
-
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
1223
|
-
if (!
|
|
1839
|
+
const refreshed = await pullRefresh(authorized, held.confirmed, live);
|
|
1840
|
+
if (!live())
|
|
1224
1841
|
return unconfirmed;
|
|
1225
1842
|
if (refreshed.kind === 'still-current') {
|
|
1226
1843
|
stale.delete(room.locator);
|
|
@@ -1234,31 +1851,45 @@ export function composeRuntime(options) {
|
|
|
1234
1851
|
return null;
|
|
1235
1852
|
}
|
|
1236
1853
|
if (refreshed.kind === 'advanced') {
|
|
1237
|
-
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
|
|
1854
|
+
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
|
|
1238
1855
|
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1239
1856
|
const reached = now !== undefined &&
|
|
1240
1857
|
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
1241
|
-
return reached ? null : advance(room, local, authorized);
|
|
1858
|
+
return reached ? null : advance(room, local, authorized, live);
|
|
1242
1859
|
}
|
|
1243
1860
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1244
1861
|
return null;
|
|
1245
1862
|
}
|
|
1246
1863
|
if (refreshed.kind !== 'beyond-horizon')
|
|
1247
1864
|
return refreshed;
|
|
1865
|
+
if (opened !== null) {
|
|
1866
|
+
owed.add(room.locator);
|
|
1867
|
+
readSide.recover();
|
|
1868
|
+
return unconfirmed;
|
|
1869
|
+
}
|
|
1248
1870
|
}
|
|
1249
|
-
const bootstrapped = await pullBootstrap(authorized, room, local);
|
|
1250
|
-
if (!
|
|
1871
|
+
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
1872
|
+
if (!live())
|
|
1251
1873
|
return unconfirmed;
|
|
1252
1874
|
if (bootstrapped.kind !== 'staged')
|
|
1253
1875
|
return bootstrapped;
|
|
1254
|
-
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
1876
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
1255
1877
|
return null;
|
|
1256
1878
|
}
|
|
1257
1879
|
async function withhold(withheld, room, local, progress) {
|
|
1880
|
+
if (withheld.kind === 'unconfirmed')
|
|
1881
|
+
return;
|
|
1258
1882
|
if (withheld.kind === 'refused')
|
|
1259
1883
|
stale.delete(room.locator);
|
|
1260
1884
|
else
|
|
1261
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);
|
|
1262
1893
|
if (withheld.kind === 'scoped-refusal') {
|
|
1263
1894
|
await scoped(withheld, room, local);
|
|
1264
1895
|
}
|
|
@@ -1294,6 +1925,7 @@ export function composeRuntime(options) {
|
|
|
1294
1925
|
unreadable.add(room.locator);
|
|
1295
1926
|
list(room, false);
|
|
1296
1927
|
emitRooms();
|
|
1928
|
+
moveTo({ ...status, ...attention() });
|
|
1297
1929
|
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
1298
1930
|
}
|
|
1299
1931
|
function leaving(batch, result) {
|
|
@@ -1315,6 +1947,7 @@ export function composeRuntime(options) {
|
|
|
1315
1947
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1316
1948
|
}
|
|
1317
1949
|
async function adoptAccepted(room, local, submitted, accepted) {
|
|
1950
|
+
await referenced(room.locator);
|
|
1318
1951
|
if (!continuing())
|
|
1319
1952
|
return unconfirmed;
|
|
1320
1953
|
const current = replicas.get(room.locator);
|
|
@@ -1331,16 +1964,17 @@ export function composeRuntime(options) {
|
|
|
1331
1964
|
if (applied.kind !== 'staged') {
|
|
1332
1965
|
const state = pendings.get(submitted.id);
|
|
1333
1966
|
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1334
|
-
if (state !== undefined)
|
|
1967
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
1335
1968
|
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1336
1969
|
emitQueue(room.locator);
|
|
1337
1970
|
return applied;
|
|
1338
1971
|
}
|
|
1339
|
-
const
|
|
1972
|
+
const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
1340
1973
|
pendings.delete(submitted.id);
|
|
1341
1974
|
unreadable.delete(room.locator);
|
|
1342
1975
|
sourceRefusals.delete(room.locator);
|
|
1343
1976
|
declarationRefusals.delete(room.locator);
|
|
1977
|
+
const set = settled();
|
|
1344
1978
|
list(room, true);
|
|
1345
1979
|
publishChangeSet(set);
|
|
1346
1980
|
moveTo({ ...status, ...attention() });
|
|
@@ -1354,14 +1988,14 @@ export function composeRuntime(options) {
|
|
|
1354
1988
|
refusedSource(room, failed);
|
|
1355
1989
|
return false;
|
|
1356
1990
|
}
|
|
1357
|
-
async function submitRoom(room, local, authorized, progress) {
|
|
1358
|
-
while (continuing() && authorization !== undefined) {
|
|
1991
|
+
async function submitRoom(room, local, authorized, progress, signal) {
|
|
1992
|
+
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
1359
1993
|
const replica = replicas.get(room.locator);
|
|
1360
1994
|
const head = replica?.queue[0];
|
|
1361
1995
|
if (replica === undefined || head === undefined)
|
|
1362
|
-
return;
|
|
1996
|
+
return 'done';
|
|
1363
1997
|
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1364
|
-
return;
|
|
1998
|
+
return 'done';
|
|
1365
1999
|
const state = pendings.get(head.id);
|
|
1366
2000
|
const { submission, lost: earlier } = state ?? {
|
|
1367
2001
|
submission: { state: 'waiting' },
|
|
@@ -1369,24 +2003,30 @@ export function composeRuntime(options) {
|
|
|
1369
2003
|
};
|
|
1370
2004
|
if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
|
|
1371
2005
|
progress.complete = false;
|
|
1372
|
-
return;
|
|
2006
|
+
return { at: Date.parse(submission.reset) };
|
|
1373
2007
|
}
|
|
1374
2008
|
if (writesHeld()) {
|
|
1375
2009
|
progress.complete = false;
|
|
1376
2010
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1377
|
-
return;
|
|
2011
|
+
return 'done';
|
|
1378
2012
|
}
|
|
1379
2013
|
const lost = earlier || submission.state === 'submitted';
|
|
2014
|
+
if (lost && pastHorizon(head.id)) {
|
|
2015
|
+
await discardPending(room, local, signal);
|
|
2016
|
+
return 'done';
|
|
2017
|
+
}
|
|
1380
2018
|
let result;
|
|
1381
2019
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
1382
2020
|
await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
|
|
1383
|
-
if (discoveryPaused)
|
|
1384
|
-
return;
|
|
1385
|
-
if (writesHeld()) {
|
|
2021
|
+
if (discoveryPaused || fences.has(room.locator))
|
|
2022
|
+
return 'done';
|
|
2023
|
+
if (signal.aborted || writesHeld()) {
|
|
1386
2024
|
await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
|
|
2025
|
+
if (signal.aborted)
|
|
2026
|
+
return 'done';
|
|
1387
2027
|
progress.complete = false;
|
|
1388
2028
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1389
|
-
return;
|
|
2029
|
+
return 'done';
|
|
1390
2030
|
}
|
|
1391
2031
|
const opensBefore = openSignals;
|
|
1392
2032
|
const answered = awaitingAnswer(room.locator);
|
|
@@ -1402,32 +2042,36 @@ export function composeRuntime(options) {
|
|
|
1402
2042
|
progress.complete = false;
|
|
1403
2043
|
if (continuing())
|
|
1404
2044
|
progress.reachable = false;
|
|
1405
|
-
return;
|
|
2045
|
+
return 'again';
|
|
2046
|
+
}
|
|
2047
|
+
if (signal.aborted) {
|
|
2048
|
+
answered();
|
|
2049
|
+
return 'done';
|
|
1406
2050
|
}
|
|
1407
2051
|
if (result.kind !== 'accepted' || !continuing())
|
|
1408
2052
|
answered();
|
|
1409
2053
|
if (!continuing())
|
|
1410
|
-
return;
|
|
2054
|
+
return 'done';
|
|
1411
2055
|
if (result.kind === 'transport-uncertainty') {
|
|
1412
2056
|
progress.complete = false;
|
|
1413
2057
|
progress.reachable = false;
|
|
1414
|
-
return;
|
|
2058
|
+
return 'again';
|
|
1415
2059
|
}
|
|
1416
2060
|
if (result.kind === 'room-not-readable') {
|
|
1417
2061
|
progress.complete = false;
|
|
1418
2062
|
unreadable.add(room.locator);
|
|
1419
2063
|
list(room, false);
|
|
1420
2064
|
emitRooms();
|
|
1421
|
-
return;
|
|
2065
|
+
return 'done';
|
|
1422
2066
|
}
|
|
1423
2067
|
if (result.kind === 'revoked-authorization') {
|
|
1424
2068
|
await refuse('revoked-authorization');
|
|
1425
|
-
return;
|
|
2069
|
+
return 'done';
|
|
1426
2070
|
}
|
|
1427
2071
|
if (result.kind === 'scoped-refusal') {
|
|
1428
2072
|
await scoped(result, room, local);
|
|
1429
2073
|
progress.complete = false;
|
|
1430
|
-
return;
|
|
2074
|
+
return 'done';
|
|
1431
2075
|
}
|
|
1432
2076
|
if (result.kind === 'write-halted') {
|
|
1433
2077
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
@@ -1436,16 +2080,24 @@ export function composeRuntime(options) {
|
|
|
1436
2080
|
continue;
|
|
1437
2081
|
haltedRefusal = true;
|
|
1438
2082
|
progress.complete = false;
|
|
1439
|
-
return;
|
|
2083
|
+
return 'done';
|
|
1440
2084
|
}
|
|
1441
2085
|
if (result.kind === 'conflicted' ||
|
|
1442
2086
|
result.kind === 'rejected' ||
|
|
1443
2087
|
result.kind === 'expired' ||
|
|
1444
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
|
+
}
|
|
1445
2094
|
if (result.kind === 'expired') {
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
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
|
+
}
|
|
1449
2101
|
}
|
|
1450
2102
|
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
|
|
1451
2103
|
(result.kind === 'conflicted' && result.roomVersion !== undefined
|
|
@@ -1458,8 +2110,7 @@ export function composeRuntime(options) {
|
|
|
1458
2110
|
const reset = result.reset;
|
|
1459
2111
|
progress.complete = false;
|
|
1460
2112
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1461
|
-
|
|
1462
|
-
return;
|
|
2113
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
1463
2114
|
}
|
|
1464
2115
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
1465
2116
|
const left = leaving(head, result);
|
|
@@ -1470,37 +2121,82 @@ export function composeRuntime(options) {
|
|
|
1470
2121
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
1471
2122
|
return;
|
|
1472
2123
|
}
|
|
1473
|
-
const
|
|
2124
|
+
const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
1474
2125
|
leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
|
|
1475
2126
|
(result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
|
|
1476
2127
|
? { ...left, returned: null }
|
|
1477
2128
|
: left,
|
|
1478
2129
|
});
|
|
1479
|
-
publishChangeSet(
|
|
2130
|
+
publishChangeSet(settled());
|
|
1480
2131
|
moveTo({ ...status, ...attention() });
|
|
1481
2132
|
});
|
|
1482
2133
|
if (result.kind === 'expired')
|
|
1483
2134
|
continue;
|
|
1484
|
-
const withheld = await advance(room, local, authorized);
|
|
1485
|
-
if (withheld !== null)
|
|
1486
|
-
|
|
2135
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
2136
|
+
if (withheld !== null) {
|
|
2137
|
+
await withhold(withheld, room, local, progress);
|
|
2138
|
+
return 'done';
|
|
2139
|
+
}
|
|
1487
2140
|
continue;
|
|
1488
2141
|
}
|
|
1489
2142
|
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1490
2143
|
if (!kept) {
|
|
1491
2144
|
progress.complete = false;
|
|
1492
|
-
return;
|
|
2145
|
+
return 'done';
|
|
1493
2146
|
}
|
|
1494
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);
|
|
1495
2191
|
}
|
|
1496
2192
|
function awaitingAnswer(locator) {
|
|
1497
2193
|
let release = () => { };
|
|
1498
2194
|
const awaited = new Promise((resolve) => {
|
|
1499
2195
|
release = resolve;
|
|
1500
2196
|
});
|
|
1501
|
-
answering.set(locator, awaited);
|
|
2197
|
+
answering.set(locator, { generation, answered: awaited });
|
|
1502
2198
|
return () => {
|
|
1503
|
-
if (answering.get(locator) === awaited)
|
|
2199
|
+
if (answering.get(locator)?.answered === awaited)
|
|
1504
2200
|
answering.delete(locator);
|
|
1505
2201
|
release();
|
|
1506
2202
|
};
|
|
@@ -1616,19 +2312,23 @@ export function composeRuntime(options) {
|
|
|
1616
2312
|
];
|
|
1617
2313
|
});
|
|
1618
2314
|
}
|
|
1619
|
-
async function synchronize(listed, local, progress, attempt) {
|
|
1620
|
-
const
|
|
1621
|
-
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) => {
|
|
1622
2321
|
const outcome = prepared[index];
|
|
1623
2322
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1624
2323
|
});
|
|
1625
2324
|
if (attempt === 'opened') {
|
|
1626
|
-
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
2325
|
+
options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
|
|
1627
2326
|
handed = true;
|
|
1628
2327
|
}
|
|
2328
|
+
discovering?.release();
|
|
1629
2329
|
const loaded = await Promise.allSettled(ready
|
|
1630
2330
|
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1631
|
-
.map((room) => loadRoom(room, local, progress)));
|
|
2331
|
+
.map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
|
|
1632
2332
|
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1633
2333
|
if (failed !== undefined)
|
|
1634
2334
|
throw failed.reason;
|
|
@@ -1657,14 +2357,14 @@ export function composeRuntime(options) {
|
|
|
1657
2357
|
}
|
|
1658
2358
|
return true;
|
|
1659
2359
|
}
|
|
1660
|
-
async function caughtUp(room, local, progress) {
|
|
2360
|
+
async function caughtUp(room, local, progress, live) {
|
|
1661
2361
|
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1662
|
-
if (held === undefined || authorization === undefined)
|
|
2362
|
+
if (held === undefined || authorization === undefined || !live())
|
|
1663
2363
|
return;
|
|
1664
2364
|
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
1665
2365
|
if (!behind)
|
|
1666
2366
|
return;
|
|
1667
|
-
const withheld = await advance(room, local, authorization);
|
|
2367
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1668
2368
|
if (withheld !== null)
|
|
1669
2369
|
await withhold(withheld, room, local, progress);
|
|
1670
2370
|
}
|
|
@@ -1674,39 +2374,46 @@ export function composeRuntime(options) {
|
|
|
1674
2374
|
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1675
2375
|
});
|
|
1676
2376
|
}
|
|
1677
|
-
async function loadRoom(room, local, progress) {
|
|
1678
|
-
if (
|
|
2377
|
+
async function loadRoom(room, local, progress, live) {
|
|
2378
|
+
if (!live() || authorization === undefined)
|
|
1679
2379
|
return;
|
|
1680
2380
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1681
|
-
await reload(room, local, progress);
|
|
2381
|
+
await reload(room, local, progress, live);
|
|
1682
2382
|
return;
|
|
1683
2383
|
}
|
|
1684
|
-
const withheld = await advance(room, local, authorization);
|
|
2384
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1685
2385
|
if (withheld !== null) {
|
|
1686
2386
|
await withhold(withheld, room, local, progress);
|
|
1687
2387
|
return;
|
|
1688
2388
|
}
|
|
1689
|
-
await caughtUp(room, local, progress);
|
|
2389
|
+
await caughtUp(room, local, progress, live);
|
|
1690
2390
|
}
|
|
1691
|
-
async function reload(room, local, progress) {
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
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);
|
|
1700
2407
|
}
|
|
1701
|
-
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
1702
|
-
await caughtUp(room, local, progress);
|
|
1703
2408
|
}
|
|
1704
|
-
async function discoverRooms(local, progress, attempt) {
|
|
2409
|
+
async function discoverRooms(local, progress, attempt, live) {
|
|
1705
2410
|
if (closed || authorization === undefined) {
|
|
1706
2411
|
notifyReady();
|
|
1707
2412
|
return;
|
|
1708
2413
|
}
|
|
1709
2414
|
moveTo(transferring(status));
|
|
2415
|
+
discoveries += 1;
|
|
2416
|
+
const started = discoveries;
|
|
1710
2417
|
let result;
|
|
1711
2418
|
try {
|
|
1712
2419
|
result = await options.adapter.discover(authorization);
|
|
@@ -1714,7 +2421,8 @@ export function composeRuntime(options) {
|
|
|
1714
2421
|
catch {
|
|
1715
2422
|
result = { kind: 'transport-uncertainty' };
|
|
1716
2423
|
}
|
|
1717
|
-
if (
|
|
2424
|
+
if (!live()) {
|
|
2425
|
+
progress.complete = false;
|
|
1718
2426
|
notifyReady();
|
|
1719
2427
|
return;
|
|
1720
2428
|
}
|
|
@@ -1772,6 +2480,7 @@ export function composeRuntime(options) {
|
|
|
1772
2480
|
}
|
|
1773
2481
|
const previousCapabilities = capabilities;
|
|
1774
2482
|
capabilities = result.capabilities;
|
|
2483
|
+
outcomeRetention = result.retention.outcome;
|
|
1775
2484
|
notifyReady();
|
|
1776
2485
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1777
2486
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
@@ -1784,8 +2493,7 @@ export function composeRuntime(options) {
|
|
|
1784
2493
|
member = signedIn;
|
|
1785
2494
|
emitMember();
|
|
1786
2495
|
}
|
|
1787
|
-
await applyDiscovery(result.rooms, local);
|
|
1788
|
-
await synchronize(result.rooms, local, progress, attempt);
|
|
2496
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
1789
2497
|
}
|
|
1790
2498
|
async function ensureAuthorization() {
|
|
1791
2499
|
if (authorization !== undefined)
|
|
@@ -1799,20 +2507,50 @@ export function composeRuntime(options) {
|
|
|
1799
2507
|
throw error;
|
|
1800
2508
|
}
|
|
1801
2509
|
}
|
|
1802
|
-
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) {
|
|
1803
2530
|
if (stopping)
|
|
1804
|
-
return
|
|
1805
|
-
|
|
1806
|
-
|
|
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;
|
|
1807
2542
|
}
|
|
1808
2543
|
function sync(attempt) {
|
|
1809
|
-
return task(async () => {
|
|
1810
|
-
if (
|
|
2544
|
+
return task(OPENING_LANE, async (live) => {
|
|
2545
|
+
if (!live()) {
|
|
1811
2546
|
notifyReady();
|
|
1812
2547
|
return;
|
|
1813
2548
|
}
|
|
1814
2549
|
const progress = { complete: true, reachable: true };
|
|
1815
|
-
|
|
2550
|
+
const gate = discover();
|
|
2551
|
+
if (attempt === 'opened')
|
|
2552
|
+
opened = generation;
|
|
2553
|
+
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
1816
2554
|
if (attempt === 'opened')
|
|
1817
2555
|
streamed = progress;
|
|
1818
2556
|
complete = false;
|
|
@@ -1821,12 +2559,12 @@ export function composeRuntime(options) {
|
|
|
1821
2559
|
handed = false;
|
|
1822
2560
|
try {
|
|
1823
2561
|
const local = await openLocal();
|
|
1824
|
-
if (
|
|
2562
|
+
if (!live())
|
|
1825
2563
|
return;
|
|
1826
2564
|
await ensureAuthorization();
|
|
1827
|
-
if (
|
|
2565
|
+
if (!live())
|
|
1828
2566
|
return;
|
|
1829
|
-
await discoverRooms(local, progress, attempt);
|
|
2567
|
+
await discoverRooms(local, progress, attempt, live);
|
|
1830
2568
|
}
|
|
1831
2569
|
catch {
|
|
1832
2570
|
progress.complete = false;
|
|
@@ -1834,17 +2572,21 @@ export function composeRuntime(options) {
|
|
|
1834
2572
|
progress.reachable = false;
|
|
1835
2573
|
}
|
|
1836
2574
|
finally {
|
|
2575
|
+
gate.release();
|
|
1837
2576
|
notifyReady();
|
|
1838
2577
|
}
|
|
2578
|
+
if (!live())
|
|
2579
|
+
return;
|
|
1839
2580
|
if (attempt !== 'opened') {
|
|
1840
|
-
await finishOpening(progress);
|
|
2581
|
+
await finishOpening(progress, live);
|
|
1841
2582
|
return;
|
|
1842
2583
|
}
|
|
1843
2584
|
if (handed || closed || progress.reachable)
|
|
1844
2585
|
return;
|
|
1845
2586
|
underway = null;
|
|
2587
|
+
opened = null;
|
|
1846
2588
|
journal.record('connection reopen');
|
|
1847
|
-
await rest(progress);
|
|
2589
|
+
await rest(progress, live);
|
|
1848
2590
|
options.adapter.roomStream?.reopen();
|
|
1849
2591
|
});
|
|
1850
2592
|
}
|
|
@@ -1865,24 +2607,41 @@ export function composeRuntime(options) {
|
|
|
1865
2607
|
});
|
|
1866
2608
|
}
|
|
1867
2609
|
function applyOpening(expired) {
|
|
1868
|
-
|
|
1869
|
-
|
|
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) => {
|
|
1870
2616
|
underway = null;
|
|
1871
|
-
if (
|
|
2617
|
+
if (!live())
|
|
1872
2618
|
return;
|
|
1873
2619
|
if (progress === null) {
|
|
1874
|
-
await
|
|
2620
|
+
await replacing;
|
|
2621
|
+
await rest(cycle, live);
|
|
1875
2622
|
return;
|
|
1876
2623
|
}
|
|
2624
|
+
await roomsCarried(expired);
|
|
2625
|
+
if (!live())
|
|
2626
|
+
return;
|
|
2627
|
+
const replaced = new Set(owed);
|
|
2628
|
+
owed.clear();
|
|
1877
2629
|
try {
|
|
1878
2630
|
const local = await openLocal();
|
|
1879
2631
|
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
1880
|
-
const reloading = held.filter((room) => expired.includes(room.locator)
|
|
1881
|
-
|
|
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)];
|
|
1882
2639
|
if (progress.reachable) {
|
|
1883
|
-
for (const room of held)
|
|
1884
|
-
if (!reloading.includes(room))
|
|
2640
|
+
for (const room of held) {
|
|
2641
|
+
if (!expired.includes(room.locator) && !reloading.includes(room)) {
|
|
1885
2642
|
await confirmModels(room, local);
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
1886
2645
|
}
|
|
1887
2646
|
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1888
2647
|
if (failed !== undefined)
|
|
@@ -1892,21 +2651,21 @@ export function composeRuntime(options) {
|
|
|
1892
2651
|
progress.complete = false;
|
|
1893
2652
|
progress.reachable = false;
|
|
1894
2653
|
}
|
|
1895
|
-
|
|
2654
|
+
if (live())
|
|
2655
|
+
await finishOpening(progress, live);
|
|
1896
2656
|
});
|
|
1897
2657
|
}
|
|
1898
|
-
async function
|
|
1899
|
-
const progress = { complete: true, reachable: true };
|
|
2658
|
+
async function reloadNow(replaced, progress) {
|
|
1900
2659
|
try {
|
|
1901
2660
|
const local = await openLocal();
|
|
1902
|
-
await Promise.allSettled([...discovered.values()]
|
|
1903
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
1904
|
-
.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))));
|
|
1905
2664
|
}
|
|
1906
2665
|
catch {
|
|
1907
2666
|
progress.reachable = false;
|
|
2667
|
+
return [];
|
|
1908
2668
|
}
|
|
1909
|
-
await rest(progress);
|
|
1910
2669
|
}
|
|
1911
2670
|
async function confirmModels(room, local) {
|
|
1912
2671
|
for (const attached of ['collection', 'typed-selection']) {
|
|
@@ -1919,38 +2678,94 @@ export function composeRuntime(options) {
|
|
|
1919
2678
|
function submitLater(locator) {
|
|
1920
2679
|
if (localSaves)
|
|
1921
2680
|
return;
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
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))
|
|
1925
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
|
+
});
|
|
1926
2730
|
}
|
|
1927
|
-
|
|
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
|
-
|
|
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';
|
|
1954
2769
|
}
|
|
1955
2770
|
function submitAll() {
|
|
1956
2771
|
for (const [locator, replica] of replicas) {
|
|
@@ -1958,31 +2773,41 @@ export function composeRuntime(options) {
|
|
|
1958
2773
|
submitLater(locator);
|
|
1959
2774
|
}
|
|
1960
2775
|
}
|
|
1961
|
-
async function finishOpening(progress) {
|
|
2776
|
+
async function finishOpening(progress, live) {
|
|
1962
2777
|
try {
|
|
1963
|
-
if (
|
|
2778
|
+
if (!live()) {
|
|
1964
2779
|
progress.complete = false;
|
|
1965
2780
|
return;
|
|
1966
2781
|
}
|
|
1967
2782
|
submitAll();
|
|
2783
|
+
for (const locator of stale)
|
|
2784
|
+
if (repairable.has(locator))
|
|
2785
|
+
repairLater(locator, {});
|
|
1968
2786
|
await conversations.reconcile();
|
|
1969
2787
|
if (status.providerUnreachable)
|
|
1970
2788
|
conversations.reconnected();
|
|
1971
2789
|
const reading = { complete: true, reachable: true };
|
|
1972
|
-
|
|
1973
|
-
|
|
2790
|
+
if (demand.conversations)
|
|
2791
|
+
await conversations.catchUp(reading);
|
|
2792
|
+
if (progress.reachable && reading.reachable)
|
|
2793
|
+
await conversations.resend(reading);
|
|
2794
|
+
if (!reading.reachable) {
|
|
1974
2795
|
progress.reachable = false;
|
|
2796
|
+
resendLater();
|
|
2797
|
+
}
|
|
1975
2798
|
}
|
|
1976
2799
|
catch {
|
|
1977
2800
|
progress.complete = false;
|
|
1978
2801
|
}
|
|
1979
2802
|
finally {
|
|
1980
|
-
|
|
1981
|
-
|
|
2803
|
+
if (live()) {
|
|
2804
|
+
complete = progress.complete;
|
|
2805
|
+
await rest(progress, live);
|
|
2806
|
+
}
|
|
1982
2807
|
}
|
|
1983
2808
|
}
|
|
1984
|
-
async function rest(progress) {
|
|
1985
|
-
if (
|
|
2809
|
+
async function rest(progress, live = () => !closed) {
|
|
2810
|
+
if (!live())
|
|
1986
2811
|
return;
|
|
1987
2812
|
let lastCompleted = status.lastCompleted;
|
|
1988
2813
|
let synced = complete && stale.size === 0 && !pending();
|
|
@@ -1990,6 +2815,8 @@ export function composeRuntime(options) {
|
|
|
1990
2815
|
const completedAt = new Date().toISOString();
|
|
1991
2816
|
try {
|
|
1992
2817
|
await (await openLocal()).complete(completedAt);
|
|
2818
|
+
if (!live())
|
|
2819
|
+
return;
|
|
1993
2820
|
recorded = true;
|
|
1994
2821
|
lastCompleted = completedAt;
|
|
1995
2822
|
}
|
|
@@ -2004,40 +2831,68 @@ export function composeRuntime(options) {
|
|
|
2004
2831
|
...attention(),
|
|
2005
2832
|
});
|
|
2006
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
|
+
}
|
|
2007
2842
|
function applyRoom(locator, roomVersion) {
|
|
2008
2843
|
announced.set(locator, roomVersion);
|
|
2009
|
-
return task(async () => {
|
|
2010
|
-
|
|
2011
|
-
|
|
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)
|
|
2012
2851
|
return;
|
|
2013
2852
|
const room = discovered.get(locator);
|
|
2014
2853
|
const held = replicas.get(locator);
|
|
2015
|
-
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2854
|
+
if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
|
|
2016
2855
|
return;
|
|
2017
2856
|
if (held !== undefined &&
|
|
2018
2857
|
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
2019
2858
|
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
2020
2859
|
return;
|
|
2021
2860
|
}
|
|
2022
|
-
const during = underway;
|
|
2861
|
+
const during = underway?.generation === generation ? underway.progress : null;
|
|
2023
2862
|
const progress = during ?? { complete: true, reachable: true };
|
|
2024
2863
|
if (during === null)
|
|
2025
2864
|
moveTo(transferring(status));
|
|
2026
2865
|
try {
|
|
2027
2866
|
const local = await openLocal();
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
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
|
+
});
|
|
2031
2874
|
}
|
|
2032
2875
|
catch {
|
|
2033
2876
|
stale.add(locator);
|
|
2034
2877
|
}
|
|
2035
2878
|
finally {
|
|
2036
2879
|
if (during === null)
|
|
2037
|
-
await rest(progress);
|
|
2880
|
+
await rest(progress, live);
|
|
2038
2881
|
}
|
|
2039
2882
|
});
|
|
2040
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
|
+
};
|
|
2041
2896
|
function observeStream() {
|
|
2042
2897
|
const stream = options.adapter.roomStream;
|
|
2043
2898
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -2047,6 +2902,9 @@ export function composeRuntime(options) {
|
|
|
2047
2902
|
return;
|
|
2048
2903
|
if (event.kind === 'opened') {
|
|
2049
2904
|
socketOpen = true;
|
|
2905
|
+
generation += 1;
|
|
2906
|
+
conversations.reopened();
|
|
2907
|
+
recovery.resume();
|
|
2050
2908
|
void sync('opened');
|
|
2051
2909
|
}
|
|
2052
2910
|
else if (event.kind === 'closed')
|
|
@@ -2055,7 +2913,7 @@ export function composeRuntime(options) {
|
|
|
2055
2913
|
void applyOpening(event.expired);
|
|
2056
2914
|
else
|
|
2057
2915
|
void applyRoom(event.locator, event.roomVersion);
|
|
2058
|
-
});
|
|
2916
|
+
}, receipts);
|
|
2059
2917
|
}
|
|
2060
2918
|
function writesHeld() {
|
|
2061
2919
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -2070,6 +2928,7 @@ export function composeRuntime(options) {
|
|
|
2070
2928
|
haltedRefusal = false;
|
|
2071
2929
|
if (mode !== serviceMode) {
|
|
2072
2930
|
serviceMode = mode;
|
|
2931
|
+
queueReadiness();
|
|
2073
2932
|
journal.record(`service mode ${mode}`);
|
|
2074
2933
|
for (const listener of serviceModeListeners)
|
|
2075
2934
|
listener(mode);
|
|
@@ -2082,6 +2941,8 @@ export function composeRuntime(options) {
|
|
|
2082
2941
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
2083
2942
|
return;
|
|
2084
2943
|
withdrawConversations = stream.observe((event) => {
|
|
2944
|
+
if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
|
|
2945
|
+
queueReadiness();
|
|
2085
2946
|
if (conversations.receive(event)) {
|
|
2086
2947
|
void ready.then(scheduleCurrency);
|
|
2087
2948
|
return;
|
|
@@ -2093,17 +2954,18 @@ export function composeRuntime(options) {
|
|
|
2093
2954
|
});
|
|
2094
2955
|
}
|
|
2095
2956
|
function scheduleCurrency() {
|
|
2096
|
-
if (currencyQueued || closed || stopping)
|
|
2957
|
+
if (currencyQueued === generation || closed || stopping)
|
|
2097
2958
|
return;
|
|
2098
|
-
currencyQueued =
|
|
2099
|
-
void task(runCurrency);
|
|
2959
|
+
currencyQueued = generation;
|
|
2960
|
+
void task(CONVERSATION_LANE, runCurrency);
|
|
2100
2961
|
}
|
|
2101
|
-
async function runCurrency() {
|
|
2102
|
-
currencyQueued
|
|
2103
|
-
|
|
2962
|
+
async function runCurrency(live) {
|
|
2963
|
+
if (currencyQueued === generation)
|
|
2964
|
+
currencyQueued = null;
|
|
2965
|
+
if (!live())
|
|
2104
2966
|
return;
|
|
2105
2967
|
await conversations.reconcile();
|
|
2106
|
-
if (conversations.current()) {
|
|
2968
|
+
if (conversations.current() || !demand.conversations) {
|
|
2107
2969
|
show();
|
|
2108
2970
|
return;
|
|
2109
2971
|
}
|
|
@@ -2115,19 +2977,24 @@ export function composeRuntime(options) {
|
|
|
2115
2977
|
catch {
|
|
2116
2978
|
progress.reachable = false;
|
|
2117
2979
|
}
|
|
2118
|
-
await rest(progress);
|
|
2980
|
+
await rest(progress, live);
|
|
2119
2981
|
}
|
|
2120
2982
|
function close() {
|
|
2121
2983
|
if (closed)
|
|
2122
2984
|
return;
|
|
2123
2985
|
closed = true;
|
|
2986
|
+
if (narrowing !== null)
|
|
2987
|
+
clearTimeout(narrowing);
|
|
2988
|
+
if (rechecking !== null)
|
|
2989
|
+
clearInterval(rechecking);
|
|
2990
|
+
readinessWatches.clear();
|
|
2124
2991
|
withdrawStream?.();
|
|
2125
2992
|
withdrawStream = undefined;
|
|
2126
2993
|
withdrawConversations?.();
|
|
2127
2994
|
withdrawConversations = undefined;
|
|
2128
2995
|
withdrawServiceMode?.();
|
|
2129
2996
|
conversations.close();
|
|
2130
|
-
|
|
2997
|
+
void recovery.retire();
|
|
2131
2998
|
store?.close();
|
|
2132
2999
|
void opening?.then((opened) => {
|
|
2133
3000
|
opened.close();
|
|
@@ -2145,9 +3012,8 @@ export function composeRuntime(options) {
|
|
|
2145
3012
|
}
|
|
2146
3013
|
async function stop() {
|
|
2147
3014
|
stopping = true;
|
|
2148
|
-
|
|
2149
|
-
await
|
|
2150
|
-
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
3015
|
+
await recovery.retire();
|
|
3016
|
+
await Promise.all(working);
|
|
2151
3017
|
await mutations;
|
|
2152
3018
|
close();
|
|
2153
3019
|
}
|
|
@@ -2169,17 +3035,69 @@ export function composeRuntime(options) {
|
|
|
2169
3035
|
return planOf(input, roomFacts(reach), planReach(reach));
|
|
2170
3036
|
}
|
|
2171
3037
|
function watchPlan(plan, deliver) {
|
|
2172
|
-
|
|
2173
|
-
|
|
3038
|
+
return readSide.watch(plan, identityOf(plan), () => {
|
|
3039
|
+
const cover = covered(plan);
|
|
3040
|
+
awaitPreparation(cover);
|
|
3041
|
+
return cover;
|
|
3042
|
+
}, deliver);
|
|
2174
3043
|
}
|
|
2175
3044
|
async function searched(input, reach) {
|
|
2176
3045
|
const plan = searchPlanOf(input);
|
|
2177
|
-
const
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
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
|
+
}
|
|
2183
3101
|
}
|
|
2184
3102
|
const watch = (topic, input, deliver) => {
|
|
2185
3103
|
if (topic === 'room-drafts') {
|
|
@@ -2225,6 +3143,9 @@ export function composeRuntime(options) {
|
|
|
2225
3143
|
presentationListeners.delete(listener);
|
|
2226
3144
|
};
|
|
2227
3145
|
}
|
|
3146
|
+
if (topic === 'readiness') {
|
|
3147
|
+
return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
|
|
3148
|
+
}
|
|
2228
3149
|
if (topic === 'rooms') {
|
|
2229
3150
|
roomListeners.add(deliver);
|
|
2230
3151
|
deliver(listed());
|
|
@@ -2391,21 +3312,29 @@ export function composeRuntime(options) {
|
|
|
2391
3312
|
return journal.export();
|
|
2392
3313
|
},
|
|
2393
3314
|
persistence() {
|
|
2394
|
-
return
|
|
3315
|
+
return persistence === 'granted';
|
|
3316
|
+
},
|
|
3317
|
+
async retry() {
|
|
3318
|
+
journal.record('retry');
|
|
3319
|
+
unindexable.clear();
|
|
3320
|
+
queueReadiness();
|
|
3321
|
+
resume();
|
|
2395
3322
|
},
|
|
2396
3323
|
observe: observing(watch),
|
|
2397
3324
|
async reauthorize() {
|
|
2398
3325
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2399
3326
|
return;
|
|
2400
3327
|
journal.record('connection reauthorize');
|
|
2401
|
-
await
|
|
3328
|
+
await Promise.all([...lanes.values()]
|
|
3329
|
+
.filter((held) => held.generation === generation)
|
|
3330
|
+
.map((held) => held.chain));
|
|
2402
3331
|
await mutations;
|
|
2403
3332
|
authorization = undefined;
|
|
2404
3333
|
discoveryPaused = false;
|
|
2405
3334
|
if (options.adapter.roomStream === undefined)
|
|
2406
3335
|
await sync('unstreamed');
|
|
2407
3336
|
else
|
|
2408
|
-
await task(ensureAuthorization);
|
|
3337
|
+
await task(OPENING_LANE, ensureAuthorization);
|
|
2409
3338
|
},
|
|
2410
3339
|
};
|
|
2411
3340
|
const reachable = {
|
|
@@ -2416,6 +3345,21 @@ export function composeRuntime(options) {
|
|
|
2416
3345
|
query: (plan) => rowsQuery(plan),
|
|
2417
3346
|
watchPlan,
|
|
2418
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),
|
|
2419
3364
|
};
|
|
2420
|
-
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2421
3365
|
}
|