@agentshouse/mdruntime 0.1.0-cdf82f77fae68 → 0.1.0-ceb97106cc2fd
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 +259 -272
- package/dist/acquisition.d.ts +14 -0
- package/dist/acquisition.d.ts.map +1 -0
- package/dist/acquisition.js +24 -0
- package/dist/adapter.d.ts +37 -9
- package/dist/adapter.d.ts.map +1 -1
- package/dist/attempt.d.ts.map +1 -1
- package/dist/attempt.js +14 -1
- package/dist/bootstrap.d.ts +2 -2
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +32 -6
- 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 +8 -1
- package/dist/chat/list.d.ts.map +1 -1
- package/dist/chat/list.js +10 -6
- package/dist/chat/markdown.d.ts +2 -1
- package/dist/chat/markdown.d.ts.map +1 -1
- package/dist/chat/markdown.js +7 -6
- package/dist/chat/message.d.ts.map +1 -1
- package/dist/chat/message.js +2 -2
- package/dist/chat/messages.d.ts +1 -0
- package/dist/chat/messages.d.ts.map +1 -1
- package/dist/chat/messages.js +4 -0
- package/dist/chat/provider.d.ts +1 -1
- package/dist/chat/provider.d.ts.map +1 -1
- package/dist/chat/provider.js +51 -67
- package/dist/chat/ready.d.ts +1 -0
- package/dist/chat/ready.d.ts.map +1 -1
- package/dist/chat/ready.js +4 -4
- package/dist/chat/style.js +3 -3
- 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 +24 -7
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +1340 -318
- package/dist/conversation.d.ts +14 -7
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +328 -137
- package/dist/durability.d.ts +7 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/entry.d.ts +17 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +156 -0
- package/dist/fence.d.ts +6 -0
- package/dist/fence.d.ts.map +1 -0
- package/dist/fence.js +10 -0
- package/dist/host.d.ts +18 -3
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +89 -12
- 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 +34 -3
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +7 -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 +6 -2
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +42 -18
- package/dist/search.d.ts +34 -6
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +116 -45
- package/dist/seed.d.ts +16 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +61 -0
- 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 +45 -12
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +315 -85
- package/dist/turn.d.ts +7 -0
- package/dist/turn.d.ts.map +1 -0
- package/dist/turn.js +45 -0
- package/dist/wire.d.ts +136 -32
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +48 -31
- package/package.json +12 -8
package/dist/connection.js
CHANGED
|
@@ -1,26 +1,33 @@
|
|
|
1
1
|
import { attachedModel } from './attached-model.js';
|
|
2
2
|
import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
|
|
3
|
-
import { observing } from './observation.js';
|
|
4
|
-
import { filterOf, identityOf, planOf, } from './query.js';
|
|
3
|
+
import { observing, whole } from './observation.js';
|
|
4
|
+
import { filterOf, identityOf, planOf, queryKey, } from './query.js';
|
|
5
5
|
import { createReadSide } from './read-side.js';
|
|
6
|
+
import { seededResult } from './seed.js';
|
|
6
7
|
import { markdownPath, roomOfKey } from './rows.js';
|
|
7
8
|
import { createSearchIndex, searchPlanOf, } from './search.js';
|
|
8
9
|
import { bootstrapRoom } from './bootstrap.js';
|
|
9
|
-
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
10
|
+
import { changeSet, entering, mergeChangeSets } from './change-set.js';
|
|
10
11
|
import { createConversationStore, } from './conversation.js';
|
|
11
12
|
import { queuedFile } from './change.js';
|
|
12
|
-
import { validRoot } from './collection.js';
|
|
13
|
+
import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
|
|
13
14
|
import { runtimeError } from './failure.js';
|
|
14
15
|
import { createLog } from './log.js';
|
|
15
16
|
import { binaryRecord } from './binary.js';
|
|
16
|
-
import { absentBinary, binaryAnswer, calledResult, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
|
|
17
|
+
import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
|
|
18
|
+
import { createRecovery } from './recovery.js';
|
|
17
19
|
import { refreshRoom } from './refresh.js';
|
|
18
20
|
import { buildBatch, joined, limitedIn, requeued } from './save.js';
|
|
19
21
|
import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
|
|
20
22
|
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
21
23
|
import { same, saving, STARTING, transferring, } from './status.js';
|
|
22
24
|
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
23
|
-
import {
|
|
25
|
+
import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
|
|
26
|
+
import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
|
|
27
|
+
import { FencedMap } from './fence.js';
|
|
28
|
+
import { tally } from './shared.js';
|
|
29
|
+
import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
|
|
30
|
+
import { identityTime, roomDraftRevision } from './identity.js';
|
|
24
31
|
import { beneath, normalize } from './paths.js';
|
|
25
32
|
import { openRuntimeStore, } from './store.js';
|
|
26
33
|
import { unconfirmed } from './transfer.js';
|
|
@@ -38,23 +45,61 @@ function declaredSelection(room) {
|
|
|
38
45
|
root !== '' &&
|
|
39
46
|
selection.roots.every((other, at) => at === index || !within(other, root)));
|
|
40
47
|
}
|
|
48
|
+
const OPENING_LANE = 'opening';
|
|
49
|
+
const CONVERSATION_LANE = 'conversations';
|
|
50
|
+
const ROOM_READS = 256;
|
|
51
|
+
const ROOM_ATTEMPTS = 16;
|
|
52
|
+
const outside = new Map();
|
|
53
|
+
function outsideOf(roots) {
|
|
54
|
+
const key = JSON.stringify(roots);
|
|
55
|
+
const held = outside.get(key);
|
|
56
|
+
if (held !== undefined)
|
|
57
|
+
return held;
|
|
58
|
+
const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
|
|
59
|
+
const made = {
|
|
60
|
+
documents: (path) => (markdownPath(path) ? counted(path) : 0),
|
|
61
|
+
binaries: counted,
|
|
62
|
+
};
|
|
63
|
+
outside.set(key, made);
|
|
64
|
+
return made;
|
|
65
|
+
}
|
|
66
|
+
function abandonedOn(signal) {
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
if (signal.aborted)
|
|
69
|
+
resolve(null);
|
|
70
|
+
else
|
|
71
|
+
signal.addEventListener('abort', () => resolve(null), { once: true });
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function roomLane(locator) {
|
|
75
|
+
return `room ${locator}`;
|
|
76
|
+
}
|
|
41
77
|
export function composeRuntime(options) {
|
|
42
78
|
const localSaves = options.adapter.localSaves === true;
|
|
43
79
|
let authorization = options.authorization;
|
|
44
80
|
let opening;
|
|
45
81
|
let store;
|
|
46
|
-
const
|
|
47
|
-
const
|
|
82
|
+
const fences = new Map();
|
|
83
|
+
const isFenced = (locator) => fences.has(locator);
|
|
84
|
+
let fencings = 0;
|
|
85
|
+
const lastFenced = new Map();
|
|
86
|
+
const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
|
|
87
|
+
let discoveries = 0;
|
|
88
|
+
const replicas = new FencedMap(isFenced);
|
|
89
|
+
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
48
90
|
const pendings = new Map();
|
|
49
|
-
const
|
|
91
|
+
const seeds = new Map();
|
|
92
|
+
const seedWatchers = new Map();
|
|
93
|
+
const roomDrafts = new FencedMap(isFenced);
|
|
50
94
|
const presentations = new Map();
|
|
51
95
|
let limits = [];
|
|
52
96
|
let capabilities = [];
|
|
53
|
-
const rooms = new
|
|
97
|
+
const rooms = new FencedMap(isFenced);
|
|
54
98
|
const counted = new Map();
|
|
55
99
|
const unreadable = new Set();
|
|
56
|
-
const
|
|
57
|
-
const
|
|
100
|
+
const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
|
|
101
|
+
const models = new FencedMap(isModelFenced);
|
|
102
|
+
const liveModels = new FencedMap(isModelFenced);
|
|
58
103
|
const invalidModels = new Set();
|
|
59
104
|
const discovered = new Map();
|
|
60
105
|
let roomsListed = false;
|
|
@@ -84,7 +129,6 @@ export function composeRuntime(options) {
|
|
|
84
129
|
let status = STARTING;
|
|
85
130
|
let shown = STARTING;
|
|
86
131
|
let discoveryPaused = false;
|
|
87
|
-
let resetTimer;
|
|
88
132
|
let closed = false;
|
|
89
133
|
let stopping = false;
|
|
90
134
|
let withdrawStream;
|
|
@@ -92,12 +136,21 @@ export function composeRuntime(options) {
|
|
|
92
136
|
let underway = null;
|
|
93
137
|
let streamed = null;
|
|
94
138
|
let handed = false;
|
|
95
|
-
const
|
|
139
|
+
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
140
|
+
const repairing = new Set();
|
|
141
|
+
const repairable = new Map();
|
|
142
|
+
let outcomeRetention = null;
|
|
96
143
|
const answering = new Map();
|
|
97
144
|
let complete = false;
|
|
98
145
|
let recorded = false;
|
|
99
146
|
const stale = new Set();
|
|
100
|
-
let
|
|
147
|
+
let generation = 0;
|
|
148
|
+
let discovering = null;
|
|
149
|
+
let opened = null;
|
|
150
|
+
const owed = new Set();
|
|
151
|
+
const lanes = new Map();
|
|
152
|
+
let roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
153
|
+
const working = new Set();
|
|
101
154
|
let mutations = Promise.resolve();
|
|
102
155
|
const roomListeners = new Set();
|
|
103
156
|
const refusalListeners = new Set();
|
|
@@ -108,7 +161,7 @@ export function composeRuntime(options) {
|
|
|
108
161
|
const presentationListeners = new Set();
|
|
109
162
|
const readSide = createReadSide();
|
|
110
163
|
let withdrawConversations;
|
|
111
|
-
let currencyQueued =
|
|
164
|
+
let currencyQueued = null;
|
|
112
165
|
let socketOpen = false;
|
|
113
166
|
let serviceMode = 'open';
|
|
114
167
|
let haltedRefusal = false;
|
|
@@ -116,24 +169,41 @@ export function composeRuntime(options) {
|
|
|
116
169
|
const serviceModeListeners = new Set();
|
|
117
170
|
let member = null;
|
|
118
171
|
const memberListeners = new Set();
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
const
|
|
172
|
+
const prepared = new Set();
|
|
173
|
+
const projecting = new Set();
|
|
174
|
+
const unpreparedDemand = new Set();
|
|
175
|
+
const unindexable = new Set();
|
|
176
|
+
let preparing = false;
|
|
177
|
+
let demand = WHOLE_DEMAND;
|
|
178
|
+
let narrowing = null;
|
|
179
|
+
let targets = {};
|
|
180
|
+
let persistence = options.shell.persistence;
|
|
181
|
+
let capacity = null;
|
|
182
|
+
const paused = new Set();
|
|
183
|
+
let rechecking = null;
|
|
184
|
+
const readinessWatches = new Set();
|
|
185
|
+
let readinessQueued = false;
|
|
186
|
+
const catching = new Set();
|
|
187
|
+
const transfers = new FencedMap(isFenced);
|
|
122
188
|
const conversations = createConversationStore({
|
|
123
189
|
agents: declaredAgents,
|
|
124
|
-
documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
|
|
190
|
+
documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
|
|
125
191
|
call: (name, input) => invokeAction(name, input),
|
|
126
192
|
durably,
|
|
127
193
|
stream: () => options.adapter.conversations,
|
|
128
194
|
publish: publishChangeSet,
|
|
195
|
+
online: () => !status.providerUnreachable && refusal === null,
|
|
129
196
|
closed: () => closed,
|
|
197
|
+
unanswered: resendLater,
|
|
198
|
+
expired: (operation) => pastHorizon(operation),
|
|
130
199
|
});
|
|
131
200
|
const searchIndex = createSearchIndex({
|
|
132
|
-
|
|
201
|
+
room: (room) => readSide.room(room),
|
|
133
202
|
document: (room, path) => readSide.document(room, path),
|
|
134
|
-
messages: () => conversations.messages(),
|
|
135
|
-
message: (conversation, identity) => conversations.message(conversation, identity),
|
|
136
203
|
});
|
|
204
|
+
const roomsToIndex = new Set();
|
|
205
|
+
const conversationsToIndex = new Set();
|
|
206
|
+
let indexing = false;
|
|
137
207
|
const journal = createLog();
|
|
138
208
|
let notifyReady = () => { };
|
|
139
209
|
const ready = new Promise((resolve) => {
|
|
@@ -212,9 +282,16 @@ export function composeRuntime(options) {
|
|
|
212
282
|
returned: structuredClone(record.returned),
|
|
213
283
|
});
|
|
214
284
|
}
|
|
285
|
+
for (const [room, refusal] of sourceRefusals)
|
|
286
|
+
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
215
287
|
attentionRequired.sort((left, right) => {
|
|
216
288
|
if (left.room !== right.room)
|
|
217
289
|
return left.room < right.room ? -1 : 1;
|
|
290
|
+
if ('refusal' in left || 'refusal' in right) {
|
|
291
|
+
if ('refusal' in left && 'refusal' in right)
|
|
292
|
+
return 0;
|
|
293
|
+
return 'refusal' in left ? -1 : 1;
|
|
294
|
+
}
|
|
218
295
|
if (left.path !== right.path)
|
|
219
296
|
return left.path < right.path ? -1 : 1;
|
|
220
297
|
return 0;
|
|
@@ -233,12 +310,9 @@ export function composeRuntime(options) {
|
|
|
233
310
|
if (replica === undefined)
|
|
234
311
|
return 0;
|
|
235
312
|
const roots = replica.confirmed.record.typedSelection?.roots ?? [];
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
count += 1;
|
|
240
|
-
}
|
|
241
|
-
return count;
|
|
313
|
+
const weighed = outsideOf(roots);
|
|
314
|
+
return (tally(replica.visible.documents, weighed.documents) +
|
|
315
|
+
tally(replica.visible.binaries, weighed.binaries));
|
|
242
316
|
}
|
|
243
317
|
function listed() {
|
|
244
318
|
return structuredClone([...rooms.values()].map((room) => {
|
|
@@ -265,15 +339,19 @@ export function composeRuntime(options) {
|
|
|
265
339
|
return { listed: roomsListed, rooms: held };
|
|
266
340
|
}
|
|
267
341
|
function emitRooms() {
|
|
342
|
+
readSide.recover();
|
|
343
|
+
queueReadiness();
|
|
268
344
|
const value = listed();
|
|
269
345
|
for (const listener of roomListeners)
|
|
270
346
|
listener(value);
|
|
271
347
|
}
|
|
272
348
|
function emitRefusal() {
|
|
349
|
+
queueReadiness();
|
|
273
350
|
for (const listener of refusalListeners)
|
|
274
351
|
listener(refusal);
|
|
275
352
|
}
|
|
276
353
|
function emitStorageLoss() {
|
|
354
|
+
queueReadiness();
|
|
277
355
|
for (const listener of storageLossListeners)
|
|
278
356
|
listener(storageLoss);
|
|
279
357
|
}
|
|
@@ -290,7 +368,8 @@ export function composeRuntime(options) {
|
|
|
290
368
|
function show() {
|
|
291
369
|
if (closed)
|
|
292
370
|
return;
|
|
293
|
-
|
|
371
|
+
queueReadiness();
|
|
372
|
+
const next = status.state === 'synced' && demand.conversations && !conversations.current()
|
|
294
373
|
? { ...status, state: 'syncing' }
|
|
295
374
|
: status;
|
|
296
375
|
if (same(shown, next))
|
|
@@ -319,42 +398,511 @@ export function composeRuntime(options) {
|
|
|
319
398
|
}
|
|
320
399
|
}
|
|
321
400
|
function applyChangeSet(set) {
|
|
322
|
-
const held =
|
|
323
|
-
|
|
324
|
-
|
|
401
|
+
const held = {
|
|
402
|
+
...set,
|
|
403
|
+
documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
|
|
404
|
+
};
|
|
325
405
|
readSide.apply(held);
|
|
326
406
|
searchIndex.changed(held);
|
|
327
407
|
}
|
|
328
|
-
function
|
|
329
|
-
|
|
408
|
+
async function referenced(locator) {
|
|
409
|
+
for (;;) {
|
|
410
|
+
const record = replicas.get(locator)?.confirmed.record;
|
|
411
|
+
if (record === undefined || closed)
|
|
412
|
+
return;
|
|
413
|
+
const held = { text: record.documents, binaries: record.binaries };
|
|
414
|
+
const composition = compositionOf(record);
|
|
415
|
+
if (referencesIndexed(held, composition))
|
|
416
|
+
return;
|
|
417
|
+
await indexReferrers(held, composition);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async function prepareReplica(locator) {
|
|
421
|
+
await referenced(locator);
|
|
422
|
+
const replica = replicas.get(locator);
|
|
423
|
+
if (replica === undefined || closed || prepared.has(locator))
|
|
330
424
|
return;
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
425
|
+
const visible = replica.visible;
|
|
426
|
+
const paths = [
|
|
427
|
+
...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
|
|
428
|
+
];
|
|
429
|
+
const entered = [];
|
|
430
|
+
projecting.add(locator);
|
|
431
|
+
try {
|
|
432
|
+
await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
|
|
433
|
+
const current = replicas.get(locator);
|
|
434
|
+
if (current === undefined || closed)
|
|
435
|
+
return;
|
|
436
|
+
const set = entering(locator, current, chunk);
|
|
437
|
+
readSide.apply(set);
|
|
438
|
+
entered.push(set);
|
|
439
|
+
});
|
|
440
|
+
if (!replicas.has(locator) || closed)
|
|
441
|
+
return;
|
|
442
|
+
prepared.add(locator);
|
|
341
443
|
}
|
|
342
|
-
|
|
444
|
+
finally {
|
|
445
|
+
projecting.delete(locator);
|
|
446
|
+
}
|
|
447
|
+
conversations.changed(mergeChangeSets(entered));
|
|
448
|
+
readSide.recover();
|
|
449
|
+
queueReadiness();
|
|
450
|
+
}
|
|
451
|
+
function unprepared() {
|
|
452
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
|
|
453
|
+
}
|
|
454
|
+
function unbuilt(locator, error) {
|
|
455
|
+
unindexable.add(locator);
|
|
456
|
+
journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
|
|
457
|
+
queueReadiness();
|
|
458
|
+
}
|
|
459
|
+
function prepare(locators = []) {
|
|
460
|
+
for (const locator of locators)
|
|
461
|
+
unpreparedDemand.add(locator);
|
|
462
|
+
if (preparing)
|
|
343
463
|
return;
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
464
|
+
preparing = true;
|
|
465
|
+
void (async () => {
|
|
466
|
+
try {
|
|
467
|
+
for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
|
|
468
|
+
await yielded();
|
|
469
|
+
if (closed)
|
|
470
|
+
return;
|
|
471
|
+
unpreparedDemand.delete(waiting);
|
|
472
|
+
await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
finally {
|
|
476
|
+
preparing = false;
|
|
477
|
+
}
|
|
478
|
+
})();
|
|
347
479
|
}
|
|
348
480
|
function reachedRooms(plan) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
481
|
+
const reach = plan.reach;
|
|
482
|
+
const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
483
|
+
const named = plan.rooms ??
|
|
484
|
+
(reached === null
|
|
485
|
+
? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
|
|
486
|
+
: [...reached]);
|
|
487
|
+
return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
|
|
488
|
+
}
|
|
489
|
+
function awaited(locator) {
|
|
490
|
+
return (!roomsListed ||
|
|
491
|
+
(rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
|
|
492
|
+
}
|
|
493
|
+
function covered(plan) {
|
|
494
|
+
const locators = reachedRooms(plan);
|
|
495
|
+
if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
|
|
496
|
+
return { pending: [] };
|
|
497
|
+
}
|
|
498
|
+
const pending = [];
|
|
499
|
+
const coverage = [];
|
|
500
|
+
const overlay = [];
|
|
501
|
+
for (const locator of locators) {
|
|
502
|
+
const replica = replicas.get(locator);
|
|
503
|
+
if (replica === undefined) {
|
|
504
|
+
if (awaited(locator))
|
|
505
|
+
pending.push({ room: locator, awaits: 'acquisition' });
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (!prepared.has(locator)) {
|
|
509
|
+
pending.push({ room: locator, awaits: 'preparation' });
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
coverage.push({
|
|
513
|
+
room: locator,
|
|
514
|
+
generation: replica.confirmed.record.transfer,
|
|
515
|
+
current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
|
|
516
|
+
});
|
|
517
|
+
for (const batch of replica.queue)
|
|
518
|
+
overlay.push(batch.id);
|
|
519
|
+
}
|
|
520
|
+
if (pending.length > 0)
|
|
521
|
+
return { pending };
|
|
522
|
+
return { coverage: { rooms: coverage, indexes: [], overlay } };
|
|
523
|
+
}
|
|
524
|
+
function awaitPreparation(cover) {
|
|
525
|
+
if (!('pending' in cover))
|
|
526
|
+
return;
|
|
527
|
+
const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
|
|
528
|
+
if (waiting.length > 0)
|
|
529
|
+
prepare(waiting.map(({ room }) => room));
|
|
530
|
+
}
|
|
531
|
+
function acquires(room) {
|
|
532
|
+
return (demanded(demand, room) ||
|
|
533
|
+
(replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
|
|
534
|
+
queuesWithoutReplica.has(room.locator));
|
|
535
|
+
}
|
|
536
|
+
function acquiredLocators() {
|
|
537
|
+
return [...discovered.values()]
|
|
538
|
+
.filter(acquires)
|
|
539
|
+
.map((room) => room.locator)
|
|
540
|
+
.sort();
|
|
541
|
+
}
|
|
542
|
+
function heldDemand(acquired) {
|
|
543
|
+
if (demand.whole)
|
|
544
|
+
return null;
|
|
545
|
+
return { rooms: acquired.map((room) => room.locator), conversations: demand.conversations };
|
|
546
|
+
}
|
|
547
|
+
function applyDemand(next) {
|
|
548
|
+
const before = acquiredLocators();
|
|
549
|
+
const conversed = demand.conversations;
|
|
550
|
+
demand = next;
|
|
551
|
+
const after = acquiredLocators();
|
|
552
|
+
queueReadiness();
|
|
553
|
+
if (!conversed && next.conversations)
|
|
554
|
+
scheduleCurrency();
|
|
555
|
+
if (JSON.stringify(before) === JSON.stringify(after) && conversed === next.conversations)
|
|
556
|
+
return;
|
|
557
|
+
journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
|
|
558
|
+
if (options.adapter.roomStream === undefined)
|
|
559
|
+
void sync('unstreamed');
|
|
560
|
+
else if (socketOpen)
|
|
561
|
+
options.adapter.roomStream.reopen();
|
|
562
|
+
}
|
|
563
|
+
function acquire(next) {
|
|
564
|
+
if (closed)
|
|
565
|
+
return;
|
|
566
|
+
if (narrowing !== null)
|
|
567
|
+
clearTimeout(narrowing);
|
|
568
|
+
narrowing = null;
|
|
569
|
+
const widening = wider(next, demand);
|
|
570
|
+
if (!widening && !narrower(next, demand))
|
|
571
|
+
return;
|
|
572
|
+
if (widening || !roomsListed) {
|
|
573
|
+
applyDemand(next);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
// A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
|
|
577
|
+
narrowing = setTimeout(() => {
|
|
578
|
+
narrowing = null;
|
|
579
|
+
applyDemand(next);
|
|
580
|
+
}, 5_000);
|
|
581
|
+
}
|
|
582
|
+
function requiredRooms(reach) {
|
|
583
|
+
if (reach === null)
|
|
584
|
+
return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
|
|
585
|
+
const named = [...new Set(reach.rooms)].sort();
|
|
586
|
+
if (roomsListed)
|
|
587
|
+
return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
|
|
588
|
+
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
589
|
+
}
|
|
590
|
+
function roomState(locator, search) {
|
|
591
|
+
if (sourceRefusals.has(locator))
|
|
592
|
+
return 'refused';
|
|
593
|
+
const listedRoom = discovered.get(locator);
|
|
594
|
+
if (roomsListed &&
|
|
595
|
+
(listedRoom === undefined ||
|
|
596
|
+
listedRoom.replica.admission !== 'admitted' ||
|
|
597
|
+
unreadable.has(locator)))
|
|
598
|
+
return 'unadmitted';
|
|
599
|
+
if (!replicas.has(locator))
|
|
600
|
+
return 'acquiring';
|
|
601
|
+
if (!prepared.has(locator))
|
|
602
|
+
return 'preparing';
|
|
603
|
+
if (search && !searchIndex.rooms().has(locator))
|
|
604
|
+
return 'indexing';
|
|
605
|
+
return 'complete';
|
|
606
|
+
}
|
|
607
|
+
function conversationCoverage(reach, search) {
|
|
608
|
+
const declared = reach === null || reach.conversations;
|
|
609
|
+
if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
|
|
610
|
+
return 'undeclared';
|
|
611
|
+
if (!conversations.known() || !conversations.current())
|
|
612
|
+
return 'acquiring';
|
|
613
|
+
if (!search)
|
|
614
|
+
return 'complete';
|
|
615
|
+
const unindexed = conversations
|
|
616
|
+
.searchable()
|
|
617
|
+
.filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
|
|
618
|
+
if (unindexed.length === 0)
|
|
619
|
+
return 'complete';
|
|
620
|
+
index([], unindexed);
|
|
621
|
+
return 'indexing';
|
|
622
|
+
}
|
|
623
|
+
function connectionOf() {
|
|
624
|
+
if (socketOpen)
|
|
625
|
+
return 'connected';
|
|
626
|
+
if (status.providerUnreachable)
|
|
627
|
+
return 'unreachable';
|
|
628
|
+
return options.adapter.roomStream === undefined && status.lastCompleted !== null
|
|
629
|
+
? 'connected'
|
|
630
|
+
: 'connecting';
|
|
631
|
+
}
|
|
632
|
+
function refusedIn(locators) {
|
|
633
|
+
return locators.flatMap((room) => {
|
|
634
|
+
const held = sourceRefusals.get(room);
|
|
635
|
+
if (held === undefined)
|
|
636
|
+
return [];
|
|
637
|
+
const { file, location } = held.fields;
|
|
638
|
+
return [
|
|
639
|
+
{
|
|
640
|
+
room,
|
|
641
|
+
file: typeof file === 'string' ? file : '',
|
|
642
|
+
rule: held.code,
|
|
643
|
+
location: typeof location === 'string' ? location : null,
|
|
644
|
+
},
|
|
645
|
+
];
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
function readinessFor(port, reach) {
|
|
649
|
+
const required = requiredRooms(reach);
|
|
650
|
+
const search = port.search === 'required';
|
|
651
|
+
const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
|
|
652
|
+
const building = (state) => coverage
|
|
653
|
+
.filter((room) => room.state === state && !unindexable.has(room.room))
|
|
654
|
+
.map(({ room }) => room);
|
|
655
|
+
const preparing = building('preparing');
|
|
656
|
+
if (preparing.length > 0)
|
|
657
|
+
prepare(preparing);
|
|
658
|
+
const indexing = building('indexing');
|
|
659
|
+
if (indexing.length > 0)
|
|
660
|
+
index(indexing, []);
|
|
661
|
+
return readinessOf({
|
|
662
|
+
code: port.code,
|
|
663
|
+
coverage: {
|
|
664
|
+
listed: required.listed,
|
|
665
|
+
rooms: coverage,
|
|
666
|
+
conversations: conversationCoverage(reach, search),
|
|
667
|
+
search: search ? 'required' : 'unused',
|
|
668
|
+
},
|
|
669
|
+
persistence,
|
|
670
|
+
connection: connectionOf(),
|
|
671
|
+
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
672
|
+
capacity: capacity !== null &&
|
|
673
|
+
coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
|
|
674
|
+
? capacity
|
|
675
|
+
: port.capacity,
|
|
676
|
+
refused: refusedIn(required.rooms),
|
|
677
|
+
lost: storageLoss !== null,
|
|
678
|
+
established: targets[port.target]?.established === true,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
function establish(name) {
|
|
682
|
+
targets = { ...targets, [name]: { established: true } };
|
|
683
|
+
queueReadiness();
|
|
684
|
+
void durably((local) => local.keepTargets(targets)).catch(() => {
|
|
685
|
+
journal.record(`target record failed ${name}`);
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
function deliverReadiness(watch) {
|
|
689
|
+
if (!readinessWatches.has(watch))
|
|
690
|
+
return;
|
|
691
|
+
const readiness = readinessFor(watch.port, watch.reach);
|
|
692
|
+
if (readiness.ready && targets[watch.port.target]?.established !== true) {
|
|
693
|
+
establish(watch.port.target);
|
|
694
|
+
}
|
|
695
|
+
const shown = JSON.stringify(readiness);
|
|
696
|
+
if (shown === watch.last)
|
|
697
|
+
return;
|
|
698
|
+
watch.last = shown;
|
|
699
|
+
watch.deliver(structuredClone(readiness));
|
|
700
|
+
}
|
|
701
|
+
function queueReadiness() {
|
|
702
|
+
if (readinessQueued || closed || readinessWatches.size === 0)
|
|
703
|
+
return;
|
|
704
|
+
readinessQueued = true;
|
|
705
|
+
queueMicrotask(() => {
|
|
706
|
+
readinessQueued = false;
|
|
707
|
+
if (closed)
|
|
708
|
+
return;
|
|
709
|
+
for (const watch of [...readinessWatches])
|
|
710
|
+
deliverReadiness(watch);
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
function watchReadiness(port, reach, deliver) {
|
|
714
|
+
const watch = { port, reach, deliver, last: null };
|
|
715
|
+
readinessWatches.add(watch);
|
|
716
|
+
void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
|
|
717
|
+
return () => {
|
|
718
|
+
readinessWatches.delete(watch);
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
function layerBytes(layer) {
|
|
722
|
+
let bytes = 0;
|
|
723
|
+
for (const [path, text] of Object.entries(layer.documents))
|
|
724
|
+
bytes += (path.length + text.length) * 2;
|
|
725
|
+
return bytes;
|
|
726
|
+
}
|
|
727
|
+
function grownBy(next, previous) {
|
|
728
|
+
if (previous === undefined)
|
|
729
|
+
return layerBytes(next);
|
|
730
|
+
const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
|
|
731
|
+
return layerBytes({ ...next, documents });
|
|
732
|
+
}
|
|
733
|
+
function exceeded(error) {
|
|
734
|
+
return (typeof error === 'object' &&
|
|
735
|
+
error !== null &&
|
|
736
|
+
error.name === 'QuotaExceededError');
|
|
737
|
+
}
|
|
738
|
+
async function estimated() {
|
|
739
|
+
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
740
|
+
return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
|
|
741
|
+
}
|
|
742
|
+
function pause(locator, cause) {
|
|
743
|
+
paused.add(locator);
|
|
744
|
+
capacity = cause;
|
|
745
|
+
journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
|
|
746
|
+
queueReadiness();
|
|
747
|
+
if (rechecking !== null)
|
|
748
|
+
return;
|
|
749
|
+
// A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
|
|
750
|
+
rechecking = setInterval(() => {
|
|
751
|
+
void recheck();
|
|
752
|
+
}, 60_000);
|
|
753
|
+
}
|
|
754
|
+
async function recheck() {
|
|
755
|
+
const held = capacity;
|
|
756
|
+
if (held === null || closed)
|
|
757
|
+
return;
|
|
758
|
+
const available = await estimated().catch(() => null);
|
|
759
|
+
if (available === null || available < (held.needed ?? 0))
|
|
760
|
+
return;
|
|
761
|
+
if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
|
|
762
|
+
return;
|
|
763
|
+
resume();
|
|
764
|
+
}
|
|
765
|
+
function resume() {
|
|
766
|
+
if (rechecking !== null)
|
|
767
|
+
clearInterval(rechecking);
|
|
768
|
+
rechecking = null;
|
|
769
|
+
if (paused.size === 0 && capacity === null)
|
|
770
|
+
return;
|
|
771
|
+
paused.clear();
|
|
772
|
+
capacity = null;
|
|
773
|
+
journal.record('capacity resumed');
|
|
774
|
+
queueReadiness();
|
|
775
|
+
if (!closed && authorization !== undefined)
|
|
776
|
+
void sync('unstreamed');
|
|
777
|
+
}
|
|
778
|
+
async function reclaimed(local) {
|
|
779
|
+
const unreferenced = [...transfers.keys()].filter((locator) => {
|
|
780
|
+
const room = discovered.get(locator);
|
|
781
|
+
return room === undefined || !acquires(room);
|
|
782
|
+
});
|
|
783
|
+
for (const locator of unreferenced) {
|
|
784
|
+
await local.abandon(locator);
|
|
785
|
+
transfers.delete(locator);
|
|
786
|
+
}
|
|
787
|
+
if (unreferenced.length > 0)
|
|
788
|
+
journal.record(`capacity reclaimed ${unreferenced.length}`);
|
|
789
|
+
return unreferenced.length > 0;
|
|
790
|
+
}
|
|
791
|
+
async function admitted(local, locator, needed) {
|
|
792
|
+
if (paused.has(locator))
|
|
793
|
+
return false;
|
|
794
|
+
let available = await estimated().catch(() => null);
|
|
795
|
+
if (available === null || available >= needed)
|
|
796
|
+
return true;
|
|
797
|
+
if (await reclaimed(local))
|
|
798
|
+
available = await estimated().catch(() => null);
|
|
799
|
+
if (available === null || available >= needed)
|
|
800
|
+
return true;
|
|
801
|
+
pause(locator, { cause: 'insufficient-space', needed, available });
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
async function exceededOn(locator, bytes) {
|
|
805
|
+
const available = await estimated().catch(() => null);
|
|
806
|
+
pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
|
|
807
|
+
}
|
|
808
|
+
function seedRoomHeld(room, version) {
|
|
809
|
+
return prepared.has(room) && reachedBy(room, version);
|
|
810
|
+
}
|
|
811
|
+
function seedHeld(at) {
|
|
812
|
+
return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
|
|
813
|
+
}
|
|
814
|
+
function seedApplies(plan) {
|
|
815
|
+
const seed = seeds.get(plan.key);
|
|
816
|
+
if (seed === undefined)
|
|
352
817
|
return null;
|
|
353
|
-
|
|
818
|
+
if (!seedHeld(seed.at))
|
|
819
|
+
return seed;
|
|
820
|
+
seeds.delete(plan.key);
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
function seedBehind(plan, cover) {
|
|
824
|
+
const seed = seedApplies(plan);
|
|
825
|
+
if (seed === null || 'pending' in cover)
|
|
826
|
+
return cover;
|
|
827
|
+
return {
|
|
828
|
+
pending: Object.entries(seed.at)
|
|
829
|
+
.filter(([room, version]) => !seedRoomHeld(room, version))
|
|
830
|
+
.map(([room]) => ({ room, awaits: 'acquisition' })),
|
|
831
|
+
};
|
|
354
832
|
}
|
|
355
|
-
function
|
|
356
|
-
|
|
357
|
-
|
|
833
|
+
function seed(entry) {
|
|
834
|
+
if (seedHeld(entry.at))
|
|
835
|
+
return;
|
|
836
|
+
for (const { query, rows } of entry.queries) {
|
|
837
|
+
const key = queryKey(query);
|
|
838
|
+
seeds.set(key, { rows, at: entry.at });
|
|
839
|
+
for (const shown of seedWatchers.get(key) ?? [])
|
|
840
|
+
shown();
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
async function rowsQuery(plan) {
|
|
844
|
+
const before = covered(plan);
|
|
845
|
+
awaitPreparation(before);
|
|
846
|
+
const seeded = seedApplies(plan);
|
|
847
|
+
if (seeded !== null)
|
|
848
|
+
return seededResult(seeded).result;
|
|
849
|
+
if ('pending' in before)
|
|
850
|
+
return before;
|
|
851
|
+
const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
|
|
852
|
+
awaitPreparation(cover);
|
|
853
|
+
if ('pending' in cover)
|
|
854
|
+
return cover;
|
|
855
|
+
return 'groups' in rows
|
|
856
|
+
? { groups: rows.groups, coverage: cover.coverage }
|
|
857
|
+
: { documents: rows.documents, coverage: cover.coverage };
|
|
858
|
+
}
|
|
859
|
+
async function indexConversation(conversation) {
|
|
860
|
+
searchIndex.indexMessages(conversation, [], false);
|
|
861
|
+
let after = null;
|
|
862
|
+
for (;;) {
|
|
863
|
+
const batch = await durably((local) => local.messagesAfter(conversation, after));
|
|
864
|
+
if (closed)
|
|
865
|
+
return;
|
|
866
|
+
searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
|
|
867
|
+
if (batch.length === 0)
|
|
868
|
+
return;
|
|
869
|
+
after = batch[batch.length - 1].identity;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
function indexNext() {
|
|
873
|
+
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
|
|
874
|
+
if (room !== undefined) {
|
|
875
|
+
roomsToIndex.delete(room);
|
|
876
|
+
return () => prepared.has(room)
|
|
877
|
+
? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
|
|
878
|
+
: undefined;
|
|
879
|
+
}
|
|
880
|
+
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
881
|
+
if (conversation === undefined)
|
|
882
|
+
return null;
|
|
883
|
+
conversationsToIndex.delete(conversation);
|
|
884
|
+
return () => indexConversation(conversation);
|
|
885
|
+
}
|
|
886
|
+
function index(rooms, named) {
|
|
887
|
+
for (const room of rooms)
|
|
888
|
+
roomsToIndex.add(room);
|
|
889
|
+
for (const conversation of named)
|
|
890
|
+
conversationsToIndex.add(conversation);
|
|
891
|
+
if (indexing)
|
|
892
|
+
return;
|
|
893
|
+
indexing = true;
|
|
894
|
+
void (async () => {
|
|
895
|
+
try {
|
|
896
|
+
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
897
|
+
await yielded();
|
|
898
|
+
await unit();
|
|
899
|
+
queueReadiness();
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
finally {
|
|
903
|
+
indexing = false;
|
|
904
|
+
}
|
|
905
|
+
})().catch(() => { });
|
|
358
906
|
}
|
|
359
907
|
function publishChangeSet(set) {
|
|
360
908
|
applyChangeSet(set);
|
|
@@ -371,34 +919,7 @@ export function composeRuntime(options) {
|
|
|
371
919
|
emitPresentation(app);
|
|
372
920
|
conversations.changed(set);
|
|
373
921
|
}
|
|
374
|
-
function stopTimer() {
|
|
375
|
-
if (resetTimer === undefined)
|
|
376
|
-
return;
|
|
377
|
-
clearTimeout(resetTimer);
|
|
378
|
-
resetTimer = undefined;
|
|
379
|
-
}
|
|
380
|
-
function armTimer() {
|
|
381
|
-
stopTimer();
|
|
382
|
-
if (closed || stopping)
|
|
383
|
-
return;
|
|
384
|
-
let earliest = null;
|
|
385
|
-
for (const { submission } of pendings.values()) {
|
|
386
|
-
if (submission.state !== 'reset')
|
|
387
|
-
continue;
|
|
388
|
-
const at = Date.parse(submission.reset);
|
|
389
|
-
if (earliest === null || at < earliest)
|
|
390
|
-
earliest = at;
|
|
391
|
-
}
|
|
392
|
-
if (earliest === null)
|
|
393
|
-
return;
|
|
394
|
-
// A Save House held back goes again only once the reset its answer named has passed.
|
|
395
|
-
resetTimer = setTimeout(() => {
|
|
396
|
-
resetTimer = undefined;
|
|
397
|
-
submitAll();
|
|
398
|
-
}, Math.max(0, earliest - Date.now()));
|
|
399
|
-
}
|
|
400
922
|
async function refuse(reason, provider = null) {
|
|
401
|
-
stopTimer();
|
|
402
923
|
discoveryPaused = true;
|
|
403
924
|
try {
|
|
404
925
|
if (reason === 'revoked-authorization') {
|
|
@@ -477,14 +998,27 @@ export function composeRuntime(options) {
|
|
|
477
998
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
478
999
|
});
|
|
479
1000
|
}
|
|
480
|
-
async function applyDiscovery(
|
|
1001
|
+
async function applyDiscovery(found, started) {
|
|
1002
|
+
for (const room of found) {
|
|
1003
|
+
const fence = fences.get(room.locator);
|
|
1004
|
+
if (fence !== undefined && fence.erased && fence.at < started)
|
|
1005
|
+
fences.delete(room.locator);
|
|
1006
|
+
}
|
|
1007
|
+
await loseRooms(found
|
|
1008
|
+
.filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
|
|
1009
|
+
.map((room) => room.locator));
|
|
1010
|
+
const listed = found.filter((room) => !fences.has(room.locator));
|
|
481
1011
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
482
1012
|
for (const locator of listedLocators)
|
|
483
1013
|
scopedRefusals.delete(locator);
|
|
484
1014
|
discovered.clear();
|
|
485
1015
|
for (const room of listed)
|
|
486
1016
|
discovered.set(room.locator, room);
|
|
487
|
-
|
|
1017
|
+
const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
|
|
1018
|
+
if (options.adapter.exhaustiveDiscovery === true)
|
|
1019
|
+
await loseRooms(unlisted);
|
|
1020
|
+
else
|
|
1021
|
+
unlistedKept(unlisted);
|
|
488
1022
|
for (const room of listed) {
|
|
489
1023
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
490
1024
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -495,6 +1029,13 @@ export function composeRuntime(options) {
|
|
|
495
1029
|
}
|
|
496
1030
|
roomsListed = true;
|
|
497
1031
|
emitRooms();
|
|
1032
|
+
return listed;
|
|
1033
|
+
}
|
|
1034
|
+
function heldLocally(locator) {
|
|
1035
|
+
return (replicas.has(locator) ||
|
|
1036
|
+
roomDrafts.has(locator) ||
|
|
1037
|
+
transfers.has(locator) ||
|
|
1038
|
+
queuesWithoutReplica.has(locator));
|
|
498
1039
|
}
|
|
499
1040
|
function unlistedHeld(listed) {
|
|
500
1041
|
return [
|
|
@@ -506,41 +1047,73 @@ export function composeRuntime(options) {
|
|
|
506
1047
|
]),
|
|
507
1048
|
].filter((locator) => !listed.has(locator));
|
|
508
1049
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
|
|
516
|
-
room: true,
|
|
517
|
-
queue: true,
|
|
518
|
-
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
519
|
-
}));
|
|
520
|
-
for (const locator of unlisted) {
|
|
521
|
-
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
522
|
-
pendings.delete(batch.id);
|
|
523
|
-
replicas.delete(locator);
|
|
524
|
-
queuesWithoutReplica.delete(locator);
|
|
1050
|
+
function unlistedKept(unlisted) {
|
|
1051
|
+
for (const locator of unlisted) {
|
|
1052
|
+
const held = replicas.get(locator)?.confirmed.record;
|
|
1053
|
+
if (held !== undefined)
|
|
1054
|
+
list(held, false);
|
|
1055
|
+
else
|
|
525
1056
|
rooms.delete(locator);
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
function fence(locators) {
|
|
1060
|
+
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
1061
|
+
const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
|
|
1062
|
+
room: true,
|
|
1063
|
+
queue: true,
|
|
1064
|
+
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
1065
|
+
}));
|
|
1066
|
+
for (const locator of fresh) {
|
|
1067
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
1068
|
+
lastFenced.set(locator, ++fencings);
|
|
1069
|
+
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
1070
|
+
pendings.delete(batch.id);
|
|
1071
|
+
replicas.delete(locator);
|
|
1072
|
+
queuesWithoutReplica.delete(locator);
|
|
1073
|
+
rooms.delete(locator);
|
|
1074
|
+
roomDrafts.delete(locator);
|
|
1075
|
+
transfers.delete(locator);
|
|
1076
|
+
catching.delete(locator);
|
|
1077
|
+
discovered.delete(locator);
|
|
1078
|
+
announced.delete(locator);
|
|
1079
|
+
unreadable.delete(locator);
|
|
1080
|
+
stale.delete(locator);
|
|
1081
|
+
repairable.delete(locator);
|
|
1082
|
+
unindexable.delete(locator);
|
|
1083
|
+
paused.delete(locator);
|
|
1084
|
+
scopedRefusals.delete(locator);
|
|
1085
|
+
sourceRefusals.delete(locator);
|
|
1086
|
+
declarationRefusals.delete(locator);
|
|
1087
|
+
for (const attached of ['collection', 'typed-selection']) {
|
|
1088
|
+
const key = modelKey(locator, attached);
|
|
1089
|
+
models.delete(key);
|
|
1090
|
+
liveModels.delete(key);
|
|
1091
|
+
invalidModels.delete(key);
|
|
540
1092
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
1093
|
+
journal.record(`room lost ${locator}`);
|
|
1094
|
+
}
|
|
1095
|
+
if (fresh.length === 0)
|
|
1096
|
+
return fresh;
|
|
1097
|
+
publishChangeSet(mergeChangeSets(removed));
|
|
1098
|
+
for (const locator of fresh)
|
|
1099
|
+
prepared.delete(locator);
|
|
1100
|
+
searchIndex.forget(fresh);
|
|
1101
|
+
readSide.recover();
|
|
1102
|
+
moveTo({ ...status, ...attention() });
|
|
1103
|
+
return fresh;
|
|
1104
|
+
}
|
|
1105
|
+
async function erased(local, locators) {
|
|
1106
|
+
await local.erase(locators);
|
|
1107
|
+
for (const locator of locators) {
|
|
1108
|
+
const held = fences.get(locator);
|
|
1109
|
+
if (held !== undefined)
|
|
1110
|
+
held.erased = true;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
async function loseRooms(locators) {
|
|
1114
|
+
const fresh = fence(locators);
|
|
1115
|
+
if (fresh.length > 0)
|
|
1116
|
+
await durably((local) => erased(local, fresh));
|
|
544
1117
|
}
|
|
545
1118
|
function restored(key, persisted) {
|
|
546
1119
|
const prior = models.get(key);
|
|
@@ -561,7 +1134,7 @@ export function composeRuntime(options) {
|
|
|
561
1134
|
return model;
|
|
562
1135
|
}
|
|
563
1136
|
async function hydrate(opened) {
|
|
564
|
-
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
|
|
1137
|
+
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
|
|
565
1138
|
opened.all(),
|
|
566
1139
|
opened.queues(),
|
|
567
1140
|
opened.roomDrafts(),
|
|
@@ -573,7 +1146,9 @@ export function composeRuntime(options) {
|
|
|
573
1146
|
opened.capabilities(),
|
|
574
1147
|
opened.member(),
|
|
575
1148
|
opened.transfers(),
|
|
1149
|
+
opened.targets(),
|
|
576
1150
|
]);
|
|
1151
|
+
targets = heldTargets;
|
|
577
1152
|
transfers.clear();
|
|
578
1153
|
for (const record of staged)
|
|
579
1154
|
transfers.set(record.locator, record);
|
|
@@ -635,6 +1210,7 @@ export function composeRuntime(options) {
|
|
|
635
1210
|
async function readState(opened) {
|
|
636
1211
|
const { lastCompleted, locators, read } = await hydrate(opened);
|
|
637
1212
|
applyChangeSet(read);
|
|
1213
|
+
prepare();
|
|
638
1214
|
if (replicas.size > 0)
|
|
639
1215
|
emitRooms();
|
|
640
1216
|
if (member !== null)
|
|
@@ -645,8 +1221,15 @@ export function composeRuntime(options) {
|
|
|
645
1221
|
return lastCompleted;
|
|
646
1222
|
}
|
|
647
1223
|
async function open() {
|
|
648
|
-
|
|
1224
|
+
if (closed)
|
|
1225
|
+
throw new Error('runtime closed');
|
|
1226
|
+
const opened = await openRuntimeStore(isFenced);
|
|
649
1227
|
store = opened;
|
|
1228
|
+
const interrupted = await opened.fences();
|
|
1229
|
+
for (const locator of interrupted) {
|
|
1230
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
1231
|
+
lastFenced.set(locator, ++fencings);
|
|
1232
|
+
}
|
|
650
1233
|
const kept = options.shell.generation;
|
|
651
1234
|
const storedGeneration = await opened.generation();
|
|
652
1235
|
const loss = detectStorageLoss(storedGeneration, kept);
|
|
@@ -660,6 +1243,8 @@ export function composeRuntime(options) {
|
|
|
660
1243
|
emitStorageLoss();
|
|
661
1244
|
}
|
|
662
1245
|
const lastCompleted = await readState(opened);
|
|
1246
|
+
if (interrupted.length > 0)
|
|
1247
|
+
await erased(opened, interrupted);
|
|
663
1248
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
664
1249
|
return opened;
|
|
665
1250
|
}
|
|
@@ -741,17 +1326,19 @@ export function composeRuntime(options) {
|
|
|
741
1326
|
moveTo({ ...status, ...attention() });
|
|
742
1327
|
throw error;
|
|
743
1328
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
1329
|
+
return () => {
|
|
1330
|
+
queuesWithoutReplica.delete(locator);
|
|
1331
|
+
for (const { batch, returned } of unqueued) {
|
|
1332
|
+
const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
|
|
1333
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
1334
|
+
}
|
|
1335
|
+
replicas.set(locator, replicaOf(confirmed, kept, before));
|
|
1336
|
+
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
1337
|
+
room: settling.room,
|
|
1338
|
+
queue: kept.length !== previousQueue.length,
|
|
1339
|
+
drafts: [...written.keys()],
|
|
1340
|
+
});
|
|
1341
|
+
};
|
|
755
1342
|
}
|
|
756
1343
|
function adoptModels(locator, record) {
|
|
757
1344
|
for (const [attached, model] of [
|
|
@@ -784,11 +1371,16 @@ export function composeRuntime(options) {
|
|
|
784
1371
|
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
785
1372
|
};
|
|
786
1373
|
}
|
|
787
|
-
function publish(local, room, confirmed, landing
|
|
1374
|
+
function publish(local, room, confirmed, landing, live) {
|
|
788
1375
|
const { base, stored } = landing;
|
|
789
1376
|
return exclusive(async () => {
|
|
790
|
-
if (
|
|
1377
|
+
if (stored !== undefined) {
|
|
1378
|
+
await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
|
|
1379
|
+
}
|
|
1380
|
+
if (!live()) {
|
|
1381
|
+
caughtUpTo(room.locator);
|
|
791
1382
|
return true;
|
|
1383
|
+
}
|
|
792
1384
|
const prior = replicas.get(room.locator);
|
|
793
1385
|
if (base !== undefined &&
|
|
794
1386
|
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
@@ -802,24 +1394,42 @@ export function composeRuntime(options) {
|
|
|
802
1394
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
803
1395
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
804
1396
|
});
|
|
805
|
-
|
|
806
|
-
|
|
1397
|
+
let settled;
|
|
1398
|
+
try {
|
|
1399
|
+
settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
|
|
1400
|
+
}
|
|
1401
|
+
catch (error) {
|
|
1402
|
+
if (!exceeded(error))
|
|
1403
|
+
throw error;
|
|
1404
|
+
await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
|
|
1405
|
+
caughtUpTo(room.locator);
|
|
1406
|
+
return true;
|
|
1407
|
+
}
|
|
1408
|
+
const set = settled();
|
|
1409
|
+
if (stored !== undefined) {
|
|
807
1410
|
transfers.delete(room.locator);
|
|
1411
|
+
catching.delete(room.locator);
|
|
1412
|
+
}
|
|
808
1413
|
for (const batch of accepted)
|
|
809
1414
|
pendings.delete(batch.id);
|
|
810
1415
|
stale.delete(room.locator);
|
|
1416
|
+
unindexable.delete(room.locator);
|
|
811
1417
|
if (unreadable.delete(room.locator))
|
|
812
1418
|
scopedRefusals.delete(room.locator);
|
|
813
1419
|
sourceRefusals.delete(room.locator);
|
|
814
1420
|
declarationRefusals.delete(room.locator);
|
|
815
1421
|
list(room, true);
|
|
1422
|
+
if (prior === undefined)
|
|
1423
|
+
prepared.add(room.locator);
|
|
816
1424
|
publishChangeSet(set);
|
|
817
1425
|
moveTo({ ...status, ...attention() });
|
|
1426
|
+
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
818
1427
|
return true;
|
|
819
1428
|
});
|
|
820
1429
|
}
|
|
821
1430
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
822
1431
|
const local = await openLocal();
|
|
1432
|
+
await referenced(locator);
|
|
823
1433
|
const replica = replicas.get(locator);
|
|
824
1434
|
const room = rooms.get(locator);
|
|
825
1435
|
if (replica === undefined || room === undefined) {
|
|
@@ -867,6 +1477,11 @@ export function composeRuntime(options) {
|
|
|
867
1477
|
changes,
|
|
868
1478
|
bases: parts.flatMap(({ bases }) => bases),
|
|
869
1479
|
writtenBy,
|
|
1480
|
+
clean: {
|
|
1481
|
+
text: replica.confirmed.record.documents,
|
|
1482
|
+
binaries: replica.confirmed.record.binaries,
|
|
1483
|
+
composition: compositionOf(replica.confirmed.record),
|
|
1484
|
+
},
|
|
870
1485
|
};
|
|
871
1486
|
const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
|
|
872
1487
|
if (checked?.kind === 'refused')
|
|
@@ -937,6 +1552,8 @@ export function composeRuntime(options) {
|
|
|
937
1552
|
catch {
|
|
938
1553
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
939
1554
|
}
|
|
1555
|
+
if (fences.has(locator))
|
|
1556
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
940
1557
|
for (const origin of withdrawn.keys())
|
|
941
1558
|
pendings.delete(origin);
|
|
942
1559
|
for (const [id, drafts] of drafted) {
|
|
@@ -966,6 +1583,8 @@ export function composeRuntime(options) {
|
|
|
966
1583
|
return;
|
|
967
1584
|
}
|
|
968
1585
|
await local.submit(locator, identifier, submission, lost);
|
|
1586
|
+
if (fences.has(locator))
|
|
1587
|
+
return;
|
|
969
1588
|
pendings.set(identifier, { ...state, submission, lost });
|
|
970
1589
|
const replica = replicas.get(locator);
|
|
971
1590
|
if (replica !== undefined)
|
|
@@ -976,6 +1595,8 @@ export function composeRuntime(options) {
|
|
|
976
1595
|
if (!continuing())
|
|
977
1596
|
throw new Error('connection refused');
|
|
978
1597
|
await local.keepUpload(locator, identifier, upload);
|
|
1598
|
+
if (fences.has(locator))
|
|
1599
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
979
1600
|
pendings.set(identifier, { ...pendings.get(identifier), upload });
|
|
980
1601
|
if (!continuing())
|
|
981
1602
|
throw new Error('connection refused');
|
|
@@ -991,6 +1612,7 @@ export function composeRuntime(options) {
|
|
|
991
1612
|
}
|
|
992
1613
|
if (closed || authorization === undefined || refusal !== null)
|
|
993
1614
|
return UNCERTAIN;
|
|
1615
|
+
const since = fencings;
|
|
994
1616
|
const transfer = ['transfer', 'history', locator, path];
|
|
995
1617
|
if (query.revision !== undefined)
|
|
996
1618
|
transfer.push(query.revision);
|
|
@@ -1005,6 +1627,8 @@ export function composeRuntime(options) {
|
|
|
1005
1627
|
...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
|
|
1006
1628
|
});
|
|
1007
1629
|
journal.record(`outcome history ${result.kind}`);
|
|
1630
|
+
if (fencedSince(locator, since))
|
|
1631
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
1008
1632
|
if (result.kind === 'scoped-refusal') {
|
|
1009
1633
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
1010
1634
|
if (room !== undefined)
|
|
@@ -1027,6 +1651,12 @@ export function composeRuntime(options) {
|
|
|
1027
1651
|
}
|
|
1028
1652
|
return null;
|
|
1029
1653
|
}
|
|
1654
|
+
function binaryRoom(request) {
|
|
1655
|
+
if (request.kind === 'path')
|
|
1656
|
+
return request.locator;
|
|
1657
|
+
const scope = options.adapter.referrerScope(request.referrer);
|
|
1658
|
+
return scope?.kind === 'room' ? scope.locator : null;
|
|
1659
|
+
}
|
|
1030
1660
|
async function readBinary(request) {
|
|
1031
1661
|
if (closed || refusal !== null)
|
|
1032
1662
|
return UNCERTAIN;
|
|
@@ -1038,12 +1668,16 @@ export function composeRuntime(options) {
|
|
|
1038
1668
|
}
|
|
1039
1669
|
if (closed || authorization === undefined || refusal !== null)
|
|
1040
1670
|
return UNCERTAIN;
|
|
1671
|
+
const since = fencings;
|
|
1041
1672
|
journal.record(request.kind === 'path'
|
|
1042
1673
|
? `transfer binary ${request.locator} ${request.path}`
|
|
1043
1674
|
: `transfer binary ${request.fileRef}`);
|
|
1044
1675
|
try {
|
|
1045
1676
|
const result = await options.adapter.binary(authorization, request);
|
|
1046
1677
|
journal.record(`outcome binary ${result.kind}`);
|
|
1678
|
+
const locator = binaryRoom(request);
|
|
1679
|
+
if (locator !== null && fencedSince(locator, since))
|
|
1680
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
1047
1681
|
if (result.kind === 'scoped-refusal') {
|
|
1048
1682
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
1049
1683
|
if (room !== undefined)
|
|
@@ -1167,11 +1801,11 @@ export function composeRuntime(options) {
|
|
|
1167
1801
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
1168
1802
|
!options.adapter.reached(locator, roomVersion, written));
|
|
1169
1803
|
}
|
|
1170
|
-
async function pullRefresh(authorized, confirmed) {
|
|
1171
|
-
if (!
|
|
1804
|
+
async function pullRefresh(authorized, confirmed, live) {
|
|
1805
|
+
if (!live())
|
|
1172
1806
|
return unconfirmed;
|
|
1173
1807
|
try {
|
|
1174
|
-
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held),
|
|
1808
|
+
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
|
|
1175
1809
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
1176
1810
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
1177
1811
|
return result;
|
|
@@ -1181,47 +1815,80 @@ export function composeRuntime(options) {
|
|
|
1181
1815
|
throw error;
|
|
1182
1816
|
}
|
|
1183
1817
|
}
|
|
1184
|
-
function staging(local) {
|
|
1818
|
+
function staging(local, live) {
|
|
1185
1819
|
return {
|
|
1186
1820
|
held: (locator) => transfers.get(locator) ?? null,
|
|
1187
1821
|
staged: (record) => local.staged(record),
|
|
1188
1822
|
stage: (record, page) => exclusive(async () => {
|
|
1189
|
-
if (!
|
|
1823
|
+
if (!live())
|
|
1824
|
+
return false;
|
|
1825
|
+
const bytes = layerBytes(page);
|
|
1826
|
+
if (!(await admitted(local, record.locator, bytes)))
|
|
1190
1827
|
return false;
|
|
1191
|
-
|
|
1828
|
+
try {
|
|
1829
|
+
await local.stage(record, page);
|
|
1830
|
+
}
|
|
1831
|
+
catch (error) {
|
|
1832
|
+
if (!exceeded(error))
|
|
1833
|
+
throw error;
|
|
1834
|
+
await exceededOn(record.locator, bytes);
|
|
1835
|
+
return false;
|
|
1836
|
+
}
|
|
1192
1837
|
transfers.set(record.locator, record);
|
|
1838
|
+
readSide.recover();
|
|
1193
1839
|
return true;
|
|
1194
1840
|
}),
|
|
1195
1841
|
abandon: (locator) => exclusive(async () => {
|
|
1196
|
-
if (!
|
|
1842
|
+
if (!live())
|
|
1197
1843
|
return;
|
|
1198
1844
|
await local.abandon(locator);
|
|
1199
1845
|
transfers.delete(locator);
|
|
1846
|
+
readSide.recover();
|
|
1200
1847
|
}),
|
|
1201
1848
|
};
|
|
1202
1849
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1850
|
+
function reusable(room) {
|
|
1851
|
+
const record = replicas.get(room.locator)?.confirmed.record;
|
|
1852
|
+
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1853
|
+
return null;
|
|
1854
|
+
return { documents: record.documents, revisions: record.revisions };
|
|
1855
|
+
}
|
|
1856
|
+
async function pullBootstrap(authorized, room, local, live) {
|
|
1857
|
+
if (!live())
|
|
1205
1858
|
return unconfirmed;
|
|
1859
|
+
if (replicas.has(room.locator)) {
|
|
1860
|
+
catching.add(room.locator);
|
|
1861
|
+
readSide.recover();
|
|
1862
|
+
}
|
|
1206
1863
|
try {
|
|
1207
1864
|
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
1208
1865
|
collection: modelOf(room.locator),
|
|
1209
1866
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
1210
|
-
}, (models, held) => remodel(room.locator, models, held),
|
|
1867
|
+
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
1211
1868
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
1212
1869
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1870
|
+
if (result.kind !== 'staged')
|
|
1871
|
+
caughtUpTo(room.locator);
|
|
1213
1872
|
return result;
|
|
1214
1873
|
}
|
|
1215
1874
|
catch (error) {
|
|
1216
1875
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
1876
|
+
caughtUpTo(room.locator);
|
|
1217
1877
|
throw error;
|
|
1218
1878
|
}
|
|
1219
1879
|
}
|
|
1220
|
-
|
|
1880
|
+
function caughtUpTo(locator) {
|
|
1881
|
+
if (catching.delete(locator))
|
|
1882
|
+
readSide.recover();
|
|
1883
|
+
}
|
|
1884
|
+
async function advance(room, local, authorized, live) {
|
|
1885
|
+
await referenced(room.locator);
|
|
1886
|
+
if (!live() || paused.has(room.locator))
|
|
1887
|
+
return unconfirmed;
|
|
1221
1888
|
const held = replicas.get(room.locator);
|
|
1222
1889
|
if (held !== undefined) {
|
|
1223
|
-
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
1224
|
-
if (!
|
|
1890
|
+
const refreshed = await pullRefresh(authorized, held.confirmed, live);
|
|
1891
|
+
if (!live())
|
|
1225
1892
|
return unconfirmed;
|
|
1226
1893
|
if (refreshed.kind === 'still-current') {
|
|
1227
1894
|
stale.delete(room.locator);
|
|
@@ -1235,31 +1902,45 @@ export function composeRuntime(options) {
|
|
|
1235
1902
|
return null;
|
|
1236
1903
|
}
|
|
1237
1904
|
if (refreshed.kind === 'advanced') {
|
|
1238
|
-
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
|
|
1905
|
+
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
|
|
1239
1906
|
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1240
1907
|
const reached = now !== undefined &&
|
|
1241
1908
|
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
1242
|
-
return reached ? null : advance(room, local, authorized);
|
|
1909
|
+
return reached ? null : advance(room, local, authorized, live);
|
|
1243
1910
|
}
|
|
1244
1911
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1245
1912
|
return null;
|
|
1246
1913
|
}
|
|
1247
1914
|
if (refreshed.kind !== 'beyond-horizon')
|
|
1248
1915
|
return refreshed;
|
|
1916
|
+
if (opened !== null) {
|
|
1917
|
+
owed.add(room.locator);
|
|
1918
|
+
readSide.recover();
|
|
1919
|
+
return unconfirmed;
|
|
1920
|
+
}
|
|
1249
1921
|
}
|
|
1250
|
-
const bootstrapped = await pullBootstrap(authorized, room, local);
|
|
1251
|
-
if (!
|
|
1922
|
+
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
1923
|
+
if (!live())
|
|
1252
1924
|
return unconfirmed;
|
|
1253
1925
|
if (bootstrapped.kind !== 'staged')
|
|
1254
1926
|
return bootstrapped;
|
|
1255
|
-
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
1927
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
1256
1928
|
return null;
|
|
1257
1929
|
}
|
|
1258
1930
|
async function withhold(withheld, room, local, progress) {
|
|
1931
|
+
if (withheld.kind === 'unconfirmed')
|
|
1932
|
+
return;
|
|
1259
1933
|
if (withheld.kind === 'refused')
|
|
1260
1934
|
stale.delete(room.locator);
|
|
1261
1935
|
else
|
|
1262
1936
|
stale.add(room.locator);
|
|
1937
|
+
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
1938
|
+
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
1939
|
+
repairable.set(room.locator, reset);
|
|
1940
|
+
repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
|
|
1941
|
+
}
|
|
1942
|
+
else
|
|
1943
|
+
repairable.delete(room.locator);
|
|
1263
1944
|
if (withheld.kind === 'scoped-refusal') {
|
|
1264
1945
|
await scoped(withheld, room, local);
|
|
1265
1946
|
}
|
|
@@ -1295,6 +1976,7 @@ export function composeRuntime(options) {
|
|
|
1295
1976
|
unreadable.add(room.locator);
|
|
1296
1977
|
list(room, false);
|
|
1297
1978
|
emitRooms();
|
|
1979
|
+
moveTo({ ...status, ...attention() });
|
|
1298
1980
|
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
1299
1981
|
}
|
|
1300
1982
|
function leaving(batch, result) {
|
|
@@ -1316,6 +1998,7 @@ export function composeRuntime(options) {
|
|
|
1316
1998
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1317
1999
|
}
|
|
1318
2000
|
async function adoptAccepted(room, local, submitted, accepted) {
|
|
2001
|
+
await referenced(room.locator);
|
|
1319
2002
|
if (!continuing())
|
|
1320
2003
|
return unconfirmed;
|
|
1321
2004
|
const current = replicas.get(room.locator);
|
|
@@ -1332,16 +2015,17 @@ export function composeRuntime(options) {
|
|
|
1332
2015
|
if (applied.kind !== 'staged') {
|
|
1333
2016
|
const state = pendings.get(submitted.id);
|
|
1334
2017
|
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1335
|
-
if (state !== undefined)
|
|
2018
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
1336
2019
|
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1337
2020
|
emitQueue(room.locator);
|
|
1338
2021
|
return applied;
|
|
1339
2022
|
}
|
|
1340
|
-
const
|
|
2023
|
+
const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
1341
2024
|
pendings.delete(submitted.id);
|
|
1342
2025
|
unreadable.delete(room.locator);
|
|
1343
2026
|
sourceRefusals.delete(room.locator);
|
|
1344
2027
|
declarationRefusals.delete(room.locator);
|
|
2028
|
+
const set = settled();
|
|
1345
2029
|
list(room, true);
|
|
1346
2030
|
publishChangeSet(set);
|
|
1347
2031
|
moveTo({ ...status, ...attention() });
|
|
@@ -1355,14 +2039,14 @@ export function composeRuntime(options) {
|
|
|
1355
2039
|
refusedSource(room, failed);
|
|
1356
2040
|
return false;
|
|
1357
2041
|
}
|
|
1358
|
-
async function submitRoom(room, local, authorized, progress) {
|
|
1359
|
-
while (continuing() && authorization !== undefined) {
|
|
2042
|
+
async function submitRoom(room, local, authorized, progress, signal) {
|
|
2043
|
+
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
1360
2044
|
const replica = replicas.get(room.locator);
|
|
1361
2045
|
const head = replica?.queue[0];
|
|
1362
2046
|
if (replica === undefined || head === undefined)
|
|
1363
|
-
return;
|
|
2047
|
+
return 'done';
|
|
1364
2048
|
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1365
|
-
return;
|
|
2049
|
+
return 'done';
|
|
1366
2050
|
const state = pendings.get(head.id);
|
|
1367
2051
|
const { submission, lost: earlier } = state ?? {
|
|
1368
2052
|
submission: { state: 'waiting' },
|
|
@@ -1370,24 +2054,30 @@ export function composeRuntime(options) {
|
|
|
1370
2054
|
};
|
|
1371
2055
|
if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
|
|
1372
2056
|
progress.complete = false;
|
|
1373
|
-
return;
|
|
2057
|
+
return { at: Date.parse(submission.reset) };
|
|
1374
2058
|
}
|
|
1375
2059
|
if (writesHeld()) {
|
|
1376
2060
|
progress.complete = false;
|
|
1377
2061
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1378
|
-
return;
|
|
2062
|
+
return 'done';
|
|
1379
2063
|
}
|
|
1380
2064
|
const lost = earlier || submission.state === 'submitted';
|
|
2065
|
+
if (lost && pastHorizon(head.id)) {
|
|
2066
|
+
await discardPending(room, local, signal);
|
|
2067
|
+
return 'done';
|
|
2068
|
+
}
|
|
1381
2069
|
let result;
|
|
1382
2070
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
1383
2071
|
await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
|
|
1384
|
-
if (discoveryPaused)
|
|
1385
|
-
return;
|
|
1386
|
-
if (writesHeld()) {
|
|
2072
|
+
if (discoveryPaused || fences.has(room.locator))
|
|
2073
|
+
return 'done';
|
|
2074
|
+
if (signal.aborted || writesHeld()) {
|
|
1387
2075
|
await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
|
|
2076
|
+
if (signal.aborted)
|
|
2077
|
+
return 'done';
|
|
1388
2078
|
progress.complete = false;
|
|
1389
2079
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1390
|
-
return;
|
|
2080
|
+
return 'done';
|
|
1391
2081
|
}
|
|
1392
2082
|
const opensBefore = openSignals;
|
|
1393
2083
|
const answered = awaitingAnswer(room.locator);
|
|
@@ -1403,32 +2093,36 @@ export function composeRuntime(options) {
|
|
|
1403
2093
|
progress.complete = false;
|
|
1404
2094
|
if (continuing())
|
|
1405
2095
|
progress.reachable = false;
|
|
1406
|
-
return;
|
|
2096
|
+
return 'again';
|
|
2097
|
+
}
|
|
2098
|
+
if (signal.aborted) {
|
|
2099
|
+
answered();
|
|
2100
|
+
return 'done';
|
|
1407
2101
|
}
|
|
1408
2102
|
if (result.kind !== 'accepted' || !continuing())
|
|
1409
2103
|
answered();
|
|
1410
2104
|
if (!continuing())
|
|
1411
|
-
return;
|
|
2105
|
+
return 'done';
|
|
1412
2106
|
if (result.kind === 'transport-uncertainty') {
|
|
1413
2107
|
progress.complete = false;
|
|
1414
2108
|
progress.reachable = false;
|
|
1415
|
-
return;
|
|
2109
|
+
return 'again';
|
|
1416
2110
|
}
|
|
1417
2111
|
if (result.kind === 'room-not-readable') {
|
|
1418
2112
|
progress.complete = false;
|
|
1419
2113
|
unreadable.add(room.locator);
|
|
1420
2114
|
list(room, false);
|
|
1421
2115
|
emitRooms();
|
|
1422
|
-
return;
|
|
2116
|
+
return 'done';
|
|
1423
2117
|
}
|
|
1424
2118
|
if (result.kind === 'revoked-authorization') {
|
|
1425
2119
|
await refuse('revoked-authorization');
|
|
1426
|
-
return;
|
|
2120
|
+
return 'done';
|
|
1427
2121
|
}
|
|
1428
2122
|
if (result.kind === 'scoped-refusal') {
|
|
1429
2123
|
await scoped(result, room, local);
|
|
1430
2124
|
progress.complete = false;
|
|
1431
|
-
return;
|
|
2125
|
+
return 'done';
|
|
1432
2126
|
}
|
|
1433
2127
|
if (result.kind === 'write-halted') {
|
|
1434
2128
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
@@ -1437,16 +2131,24 @@ export function composeRuntime(options) {
|
|
|
1437
2131
|
continue;
|
|
1438
2132
|
haltedRefusal = true;
|
|
1439
2133
|
progress.complete = false;
|
|
1440
|
-
return;
|
|
2134
|
+
return 'done';
|
|
1441
2135
|
}
|
|
1442
2136
|
if (result.kind === 'conflicted' ||
|
|
1443
2137
|
result.kind === 'rejected' ||
|
|
1444
2138
|
result.kind === 'expired' ||
|
|
1445
2139
|
result.kind === 'retryable-refusal') {
|
|
2140
|
+
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
2141
|
+
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
2142
|
+
await discardPending(room, local, signal);
|
|
2143
|
+
return 'done';
|
|
2144
|
+
}
|
|
1446
2145
|
if (result.kind === 'expired') {
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
2146
|
+
await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
|
|
2147
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
2148
|
+
if (withheld !== null) {
|
|
2149
|
+
await withhold(withheld, room, local, progress);
|
|
2150
|
+
return 'done';
|
|
2151
|
+
}
|
|
1450
2152
|
}
|
|
1451
2153
|
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
|
|
1452
2154
|
(result.kind === 'conflicted' && result.roomVersion !== undefined
|
|
@@ -1459,8 +2161,7 @@ export function composeRuntime(options) {
|
|
|
1459
2161
|
const reset = result.reset;
|
|
1460
2162
|
progress.complete = false;
|
|
1461
2163
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1462
|
-
|
|
1463
|
-
return;
|
|
2164
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
1464
2165
|
}
|
|
1465
2166
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
1466
2167
|
const left = leaving(head, result);
|
|
@@ -1471,37 +2172,82 @@ export function composeRuntime(options) {
|
|
|
1471
2172
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
1472
2173
|
return;
|
|
1473
2174
|
}
|
|
1474
|
-
const
|
|
2175
|
+
const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
1475
2176
|
leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
|
|
1476
2177
|
(result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
|
|
1477
2178
|
? { ...left, returned: null }
|
|
1478
2179
|
: left,
|
|
1479
2180
|
});
|
|
1480
|
-
publishChangeSet(
|
|
2181
|
+
publishChangeSet(settled());
|
|
1481
2182
|
moveTo({ ...status, ...attention() });
|
|
1482
2183
|
});
|
|
1483
2184
|
if (result.kind === 'expired')
|
|
1484
2185
|
continue;
|
|
1485
|
-
const withheld = await advance(room, local, authorized);
|
|
1486
|
-
if (withheld !== null)
|
|
1487
|
-
|
|
2186
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
2187
|
+
if (withheld !== null) {
|
|
2188
|
+
await withhold(withheld, room, local, progress);
|
|
2189
|
+
return 'done';
|
|
2190
|
+
}
|
|
1488
2191
|
continue;
|
|
1489
2192
|
}
|
|
1490
2193
|
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1491
2194
|
if (!kept) {
|
|
1492
2195
|
progress.complete = false;
|
|
1493
|
-
return;
|
|
2196
|
+
return 'done';
|
|
1494
2197
|
}
|
|
1495
2198
|
}
|
|
2199
|
+
return 'done';
|
|
2200
|
+
}
|
|
2201
|
+
function pastHorizon(identity) {
|
|
2202
|
+
return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
|
|
2203
|
+
}
|
|
2204
|
+
async function discardPending(room, local, signal) {
|
|
2205
|
+
const { locator } = room;
|
|
2206
|
+
const discarded = await exclusive(async () => {
|
|
2207
|
+
if (fences.has(locator))
|
|
2208
|
+
return false;
|
|
2209
|
+
const before = replicas.get(locator);
|
|
2210
|
+
const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
2211
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
2212
|
+
lastFenced.set(locator, ++fencings);
|
|
2213
|
+
try {
|
|
2214
|
+
await local.discard(locator);
|
|
2215
|
+
}
|
|
2216
|
+
finally {
|
|
2217
|
+
fences.delete(locator);
|
|
2218
|
+
}
|
|
2219
|
+
for (const batch of queue)
|
|
2220
|
+
pendings.delete(batch.id);
|
|
2221
|
+
queuesWithoutReplica.delete(locator);
|
|
2222
|
+
const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
|
|
2223
|
+
if (after !== null)
|
|
2224
|
+
replicas.set(locator, after);
|
|
2225
|
+
journal.record(`room discarded ${locator}`);
|
|
2226
|
+
publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
|
|
2227
|
+
moveTo({ ...status, ...attention() });
|
|
2228
|
+
return true;
|
|
2229
|
+
});
|
|
2230
|
+
if (!discarded)
|
|
2231
|
+
return;
|
|
2232
|
+
const progress = { complete: true, reachable: true };
|
|
2233
|
+
await roomTask(locator, async (live) => {
|
|
2234
|
+
await Promise.race([
|
|
2235
|
+
reload(room, local, progress, () => live() && !signal.aborted),
|
|
2236
|
+
abandonedOn(signal),
|
|
2237
|
+
]);
|
|
2238
|
+
});
|
|
2239
|
+
if (signal.aborted)
|
|
2240
|
+
return;
|
|
2241
|
+
await rest(progress);
|
|
1496
2242
|
}
|
|
1497
2243
|
function awaitingAnswer(locator) {
|
|
1498
2244
|
let release = () => { };
|
|
1499
2245
|
const awaited = new Promise((resolve) => {
|
|
1500
2246
|
release = resolve;
|
|
1501
2247
|
});
|
|
1502
|
-
answering.set(locator, awaited);
|
|
2248
|
+
answering.set(locator, { generation, answered: awaited });
|
|
1503
2249
|
return () => {
|
|
1504
|
-
if (answering.get(locator) === awaited)
|
|
2250
|
+
if (answering.get(locator)?.answered === awaited)
|
|
1505
2251
|
answering.delete(locator);
|
|
1506
2252
|
release();
|
|
1507
2253
|
};
|
|
@@ -1572,6 +2318,11 @@ export function composeRuntime(options) {
|
|
|
1572
2318
|
});
|
|
1573
2319
|
}
|
|
1574
2320
|
async function scoped(result, room, local) {
|
|
2321
|
+
if ((result.scope === 'content' || result.scope === 'sync') && !result.retryable) {
|
|
2322
|
+
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
2323
|
+
await loseRooms([room.locator]);
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
1575
2326
|
scopedRefusals.set(room.locator, {
|
|
1576
2327
|
code: result.code,
|
|
1577
2328
|
fields: result.fields,
|
|
@@ -1617,19 +2368,23 @@ export function composeRuntime(options) {
|
|
|
1617
2368
|
];
|
|
1618
2369
|
});
|
|
1619
2370
|
}
|
|
1620
|
-
async function synchronize(listed, local, progress, attempt) {
|
|
1621
|
-
const
|
|
1622
|
-
const
|
|
2371
|
+
async function synchronize(listed, local, progress, attempt, live) {
|
|
2372
|
+
const acquired = listed.filter(acquires);
|
|
2373
|
+
const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
|
|
2374
|
+
if (!live())
|
|
2375
|
+
return;
|
|
2376
|
+
const ready = acquired.filter((_, index) => {
|
|
1623
2377
|
const outcome = prepared[index];
|
|
1624
2378
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1625
2379
|
});
|
|
1626
2380
|
if (attempt === 'opened') {
|
|
1627
|
-
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
2381
|
+
options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
|
|
1628
2382
|
handed = true;
|
|
1629
2383
|
}
|
|
2384
|
+
discovering?.release();
|
|
1630
2385
|
const loaded = await Promise.allSettled(ready
|
|
1631
2386
|
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1632
|
-
.map((room) => loadRoom(room, local, progress)));
|
|
2387
|
+
.map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
|
|
1633
2388
|
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1634
2389
|
if (failed !== undefined)
|
|
1635
2390
|
throw failed.reason;
|
|
@@ -1651,21 +2406,16 @@ export function composeRuntime(options) {
|
|
|
1651
2406
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1652
2407
|
if (discoveryPaused || authorization === undefined)
|
|
1653
2408
|
return false;
|
|
1654
|
-
|
|
1655
|
-
if (replicas.has(room.locator))
|
|
1656
|
-
progress.complete = false;
|
|
1657
|
-
return false;
|
|
1658
|
-
}
|
|
1659
|
-
return true;
|
|
2409
|
+
return room.replica.admission === 'admitted';
|
|
1660
2410
|
}
|
|
1661
|
-
async function caughtUp(room, local, progress) {
|
|
2411
|
+
async function caughtUp(room, local, progress, live) {
|
|
1662
2412
|
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1663
|
-
if (held === undefined || authorization === undefined)
|
|
2413
|
+
if (held === undefined || authorization === undefined || !live())
|
|
1664
2414
|
return;
|
|
1665
2415
|
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
1666
2416
|
if (!behind)
|
|
1667
2417
|
return;
|
|
1668
|
-
const withheld = await advance(room, local, authorization);
|
|
2418
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1669
2419
|
if (withheld !== null)
|
|
1670
2420
|
await withhold(withheld, room, local, progress);
|
|
1671
2421
|
}
|
|
@@ -1675,39 +2425,46 @@ export function composeRuntime(options) {
|
|
|
1675
2425
|
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1676
2426
|
});
|
|
1677
2427
|
}
|
|
1678
|
-
async function loadRoom(room, local, progress) {
|
|
1679
|
-
if (
|
|
2428
|
+
async function loadRoom(room, local, progress, live) {
|
|
2429
|
+
if (!live() || authorization === undefined)
|
|
1680
2430
|
return;
|
|
1681
2431
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1682
|
-
await reload(room, local, progress);
|
|
2432
|
+
await reload(room, local, progress, live);
|
|
1683
2433
|
return;
|
|
1684
2434
|
}
|
|
1685
|
-
const withheld = await advance(room, local, authorization);
|
|
2435
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1686
2436
|
if (withheld !== null) {
|
|
1687
2437
|
await withhold(withheld, room, local, progress);
|
|
1688
2438
|
return;
|
|
1689
2439
|
}
|
|
1690
|
-
await caughtUp(room, local, progress);
|
|
2440
|
+
await caughtUp(room, local, progress, live);
|
|
1691
2441
|
}
|
|
1692
|
-
async function reload(room, local, progress) {
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
2442
|
+
async function reload(room, local, progress, live) {
|
|
2443
|
+
try {
|
|
2444
|
+
if (!live() || authorization === undefined)
|
|
2445
|
+
return;
|
|
2446
|
+
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
2447
|
+
if (!live())
|
|
2448
|
+
return;
|
|
2449
|
+
if (bootstrapped.kind !== 'staged') {
|
|
2450
|
+
await withhold(bootstrapped, room, local, progress);
|
|
2451
|
+
return;
|
|
2452
|
+
}
|
|
2453
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
2454
|
+
await caughtUp(room, local, progress, live);
|
|
2455
|
+
}
|
|
2456
|
+
finally {
|
|
2457
|
+
caughtUpTo(room.locator);
|
|
1701
2458
|
}
|
|
1702
|
-
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
1703
|
-
await caughtUp(room, local, progress);
|
|
1704
2459
|
}
|
|
1705
|
-
async function discoverRooms(local, progress, attempt) {
|
|
2460
|
+
async function discoverRooms(local, progress, attempt, live) {
|
|
1706
2461
|
if (closed || authorization === undefined) {
|
|
1707
2462
|
notifyReady();
|
|
1708
2463
|
return;
|
|
1709
2464
|
}
|
|
1710
2465
|
moveTo(transferring(status));
|
|
2466
|
+
discoveries += 1;
|
|
2467
|
+
const started = discoveries;
|
|
1711
2468
|
let result;
|
|
1712
2469
|
try {
|
|
1713
2470
|
result = await options.adapter.discover(authorization);
|
|
@@ -1715,7 +2472,8 @@ export function composeRuntime(options) {
|
|
|
1715
2472
|
catch {
|
|
1716
2473
|
result = { kind: 'transport-uncertainty' };
|
|
1717
2474
|
}
|
|
1718
|
-
if (
|
|
2475
|
+
if (!live()) {
|
|
2476
|
+
progress.complete = false;
|
|
1719
2477
|
notifyReady();
|
|
1720
2478
|
return;
|
|
1721
2479
|
}
|
|
@@ -1773,6 +2531,7 @@ export function composeRuntime(options) {
|
|
|
1773
2531
|
}
|
|
1774
2532
|
const previousCapabilities = capabilities;
|
|
1775
2533
|
capabilities = result.capabilities;
|
|
2534
|
+
outcomeRetention = result.retention.outcome;
|
|
1776
2535
|
notifyReady();
|
|
1777
2536
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1778
2537
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
@@ -1785,8 +2544,7 @@ export function composeRuntime(options) {
|
|
|
1785
2544
|
member = signedIn;
|
|
1786
2545
|
emitMember();
|
|
1787
2546
|
}
|
|
1788
|
-
await applyDiscovery(result.rooms, local);
|
|
1789
|
-
await synchronize(result.rooms, local, progress, attempt);
|
|
2547
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
1790
2548
|
}
|
|
1791
2549
|
async function ensureAuthorization() {
|
|
1792
2550
|
if (authorization !== undefined)
|
|
@@ -1800,20 +2558,50 @@ export function composeRuntime(options) {
|
|
|
1800
2558
|
throw error;
|
|
1801
2559
|
}
|
|
1802
2560
|
}
|
|
1803
|
-
function
|
|
2561
|
+
async function admittedRead(work) {
|
|
2562
|
+
if (roomReads.generation !== generation) {
|
|
2563
|
+
roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
2564
|
+
}
|
|
2565
|
+
const reads = roomReads;
|
|
2566
|
+
while (reads.free === 0)
|
|
2567
|
+
await new Promise((admit) => reads.waiting.push(admit));
|
|
2568
|
+
reads.free -= 1;
|
|
2569
|
+
try {
|
|
2570
|
+
await work();
|
|
2571
|
+
}
|
|
2572
|
+
finally {
|
|
2573
|
+
reads.free += 1;
|
|
2574
|
+
reads.waiting.shift()?.();
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
function roomTask(locator, work) {
|
|
2578
|
+
return task(roomLane(locator), (live) => admittedRead(() => work(live)));
|
|
2579
|
+
}
|
|
2580
|
+
function task(lane, work) {
|
|
1804
2581
|
if (stopping)
|
|
1805
|
-
return
|
|
1806
|
-
|
|
1807
|
-
|
|
2582
|
+
return Promise.resolve();
|
|
2583
|
+
const held = lanes.get(lane);
|
|
2584
|
+
const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
|
|
2585
|
+
const turn = current.generation;
|
|
2586
|
+
const live = () => turn === generation && continuing();
|
|
2587
|
+
const run = current.chain.then(() => work(live)).catch(() => undefined);
|
|
2588
|
+
current.chain = run;
|
|
2589
|
+
lanes.set(lane, current);
|
|
2590
|
+
working.add(run);
|
|
2591
|
+
void run.finally(() => working.delete(run));
|
|
2592
|
+
return run;
|
|
1808
2593
|
}
|
|
1809
2594
|
function sync(attempt) {
|
|
1810
|
-
return task(async () => {
|
|
1811
|
-
if (
|
|
2595
|
+
return task(OPENING_LANE, async (live) => {
|
|
2596
|
+
if (!live()) {
|
|
1812
2597
|
notifyReady();
|
|
1813
2598
|
return;
|
|
1814
2599
|
}
|
|
1815
2600
|
const progress = { complete: true, reachable: true };
|
|
1816
|
-
|
|
2601
|
+
const gate = discover();
|
|
2602
|
+
if (attempt === 'opened')
|
|
2603
|
+
opened = generation;
|
|
2604
|
+
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
1817
2605
|
if (attempt === 'opened')
|
|
1818
2606
|
streamed = progress;
|
|
1819
2607
|
complete = false;
|
|
@@ -1822,12 +2610,12 @@ export function composeRuntime(options) {
|
|
|
1822
2610
|
handed = false;
|
|
1823
2611
|
try {
|
|
1824
2612
|
const local = await openLocal();
|
|
1825
|
-
if (
|
|
2613
|
+
if (!live())
|
|
1826
2614
|
return;
|
|
1827
2615
|
await ensureAuthorization();
|
|
1828
|
-
if (
|
|
2616
|
+
if (!live())
|
|
1829
2617
|
return;
|
|
1830
|
-
await discoverRooms(local, progress, attempt);
|
|
2618
|
+
await discoverRooms(local, progress, attempt, live);
|
|
1831
2619
|
}
|
|
1832
2620
|
catch {
|
|
1833
2621
|
progress.complete = false;
|
|
@@ -1835,17 +2623,21 @@ export function composeRuntime(options) {
|
|
|
1835
2623
|
progress.reachable = false;
|
|
1836
2624
|
}
|
|
1837
2625
|
finally {
|
|
2626
|
+
gate.release();
|
|
1838
2627
|
notifyReady();
|
|
1839
2628
|
}
|
|
2629
|
+
if (!live())
|
|
2630
|
+
return;
|
|
1840
2631
|
if (attempt !== 'opened') {
|
|
1841
|
-
await finishOpening(progress);
|
|
2632
|
+
await finishOpening(progress, live);
|
|
1842
2633
|
return;
|
|
1843
2634
|
}
|
|
1844
2635
|
if (handed || closed || progress.reachable)
|
|
1845
2636
|
return;
|
|
1846
2637
|
underway = null;
|
|
2638
|
+
opened = null;
|
|
1847
2639
|
journal.record('connection reopen');
|
|
1848
|
-
await rest(progress);
|
|
2640
|
+
await rest(progress, live);
|
|
1849
2641
|
options.adapter.roomStream?.reopen();
|
|
1850
2642
|
});
|
|
1851
2643
|
}
|
|
@@ -1866,24 +2658,41 @@ export function composeRuntime(options) {
|
|
|
1866
2658
|
});
|
|
1867
2659
|
}
|
|
1868
2660
|
function applyOpening(expired) {
|
|
1869
|
-
|
|
1870
|
-
|
|
2661
|
+
const progress = underway?.generation === generation ? underway.progress : null;
|
|
2662
|
+
const cycle = progress ?? { complete: true, reachable: true };
|
|
2663
|
+
const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
|
|
2664
|
+
if (opened === generation)
|
|
2665
|
+
opened = null;
|
|
2666
|
+
return task(OPENING_LANE, async (live) => {
|
|
1871
2667
|
underway = null;
|
|
1872
|
-
if (
|
|
2668
|
+
if (!live())
|
|
1873
2669
|
return;
|
|
1874
2670
|
if (progress === null) {
|
|
1875
|
-
await
|
|
2671
|
+
await replacing;
|
|
2672
|
+
await rest(cycle, live);
|
|
1876
2673
|
return;
|
|
1877
2674
|
}
|
|
2675
|
+
await roomsCarried(expired);
|
|
2676
|
+
if (!live())
|
|
2677
|
+
return;
|
|
2678
|
+
const replaced = new Set(owed);
|
|
2679
|
+
owed.clear();
|
|
1878
2680
|
try {
|
|
1879
2681
|
const local = await openLocal();
|
|
1880
2682
|
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
1881
|
-
const reloading = held.filter((room) => expired.includes(room.locator)
|
|
1882
|
-
|
|
2683
|
+
const reloading = held.filter((room) => !expired.includes(room.locator) &&
|
|
2684
|
+
(replaced.has(room.locator) || revoked(room.locator)));
|
|
2685
|
+
for (const room of reloading)
|
|
2686
|
+
catching.add(room.locator);
|
|
2687
|
+
readSide.recover();
|
|
2688
|
+
const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
|
|
2689
|
+
const reloaded = [...(await reloads), ...(await replacing)];
|
|
1883
2690
|
if (progress.reachable) {
|
|
1884
|
-
for (const room of held)
|
|
1885
|
-
if (!reloading.includes(room))
|
|
2691
|
+
for (const room of held) {
|
|
2692
|
+
if (!expired.includes(room.locator) && !reloading.includes(room)) {
|
|
1886
2693
|
await confirmModels(room, local);
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
1887
2696
|
}
|
|
1888
2697
|
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1889
2698
|
if (failed !== undefined)
|
|
@@ -1893,21 +2702,21 @@ export function composeRuntime(options) {
|
|
|
1893
2702
|
progress.complete = false;
|
|
1894
2703
|
progress.reachable = false;
|
|
1895
2704
|
}
|
|
1896
|
-
|
|
2705
|
+
if (live())
|
|
2706
|
+
await finishOpening(progress, live);
|
|
1897
2707
|
});
|
|
1898
2708
|
}
|
|
1899
|
-
async function
|
|
1900
|
-
const progress = { complete: true, reachable: true };
|
|
2709
|
+
async function reloadNow(replaced, progress) {
|
|
1901
2710
|
try {
|
|
1902
2711
|
const local = await openLocal();
|
|
1903
|
-
await Promise.allSettled([...discovered.values()]
|
|
1904
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
1905
|
-
.map((room) => reload(room, local, progress)));
|
|
2712
|
+
return await Promise.allSettled([...discovered.values()]
|
|
2713
|
+
.filter((room) => replicas.has(room.locator) && replaced(room))
|
|
2714
|
+
.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
1906
2715
|
}
|
|
1907
2716
|
catch {
|
|
1908
2717
|
progress.reachable = false;
|
|
2718
|
+
return [];
|
|
1909
2719
|
}
|
|
1910
|
-
await rest(progress);
|
|
1911
2720
|
}
|
|
1912
2721
|
async function confirmModels(room, local) {
|
|
1913
2722
|
for (const attached of ['collection', 'typed-selection']) {
|
|
@@ -1920,38 +2729,94 @@ export function composeRuntime(options) {
|
|
|
1920
2729
|
function submitLater(locator) {
|
|
1921
2730
|
if (localSaves)
|
|
1922
2731
|
return;
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
2732
|
+
void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
|
|
2733
|
+
}
|
|
2734
|
+
async function submitPass(locator, signal) {
|
|
2735
|
+
const room = discovered.get(locator);
|
|
2736
|
+
if (closed || stopping || discoveryPaused || room === undefined)
|
|
2737
|
+
return 'done';
|
|
2738
|
+
const progress = { complete: true, reachable: true };
|
|
2739
|
+
let next;
|
|
2740
|
+
try {
|
|
2741
|
+
await ensureAuthorization();
|
|
2742
|
+
if (authorization === undefined)
|
|
2743
|
+
return 'done';
|
|
2744
|
+
next = await submitRoom(room, await openLocal(), authorization, progress, signal);
|
|
2745
|
+
}
|
|
2746
|
+
catch {
|
|
2747
|
+
progress.reachable = false;
|
|
2748
|
+
next = 'again';
|
|
2749
|
+
}
|
|
2750
|
+
if (signal.aborted)
|
|
2751
|
+
return 'done';
|
|
2752
|
+
await rest(progress);
|
|
2753
|
+
return next === 'again' && !socketOpen ? 'done' : next;
|
|
2754
|
+
}
|
|
2755
|
+
function repairLater(locator, when) {
|
|
2756
|
+
if (!socketOpen || repairing.has(locator))
|
|
1926
2757
|
return;
|
|
2758
|
+
void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
|
|
2759
|
+
}
|
|
2760
|
+
async function repairPass(locator, signal) {
|
|
2761
|
+
const room = discovered.get(locator);
|
|
2762
|
+
if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
|
|
2763
|
+
return 'done';
|
|
2764
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2765
|
+
return 'done';
|
|
2766
|
+
const progress = { complete: true, reachable: true };
|
|
2767
|
+
repairing.add(locator);
|
|
2768
|
+
try {
|
|
2769
|
+
const local = await openLocal();
|
|
2770
|
+
await roomTask(locator, async (live) => {
|
|
2771
|
+
const current = () => live() && !signal.aborted;
|
|
2772
|
+
if (!current() || authorization === undefined)
|
|
2773
|
+
return;
|
|
2774
|
+
const withheld = await Promise.race([
|
|
2775
|
+
advance(room, local, authorization, current),
|
|
2776
|
+
abandonedOn(signal),
|
|
2777
|
+
]);
|
|
2778
|
+
if (withheld !== null && current())
|
|
2779
|
+
await withhold(withheld, room, local, progress);
|
|
2780
|
+
});
|
|
1927
2781
|
}
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
2782
|
+
catch {
|
|
2783
|
+
stale.add(locator);
|
|
2784
|
+
progress.reachable = false;
|
|
2785
|
+
}
|
|
2786
|
+
finally {
|
|
2787
|
+
repairing.delete(locator);
|
|
2788
|
+
}
|
|
2789
|
+
if (signal.aborted)
|
|
2790
|
+
return 'done';
|
|
2791
|
+
await rest(progress);
|
|
2792
|
+
if (!stale.has(locator)) {
|
|
2793
|
+
if ((replicas.get(locator)?.queue.length ?? 0) > 0)
|
|
2794
|
+
submitLater(locator);
|
|
2795
|
+
return 'done';
|
|
2796
|
+
}
|
|
2797
|
+
if (!repairable.has(locator))
|
|
2798
|
+
return 'done';
|
|
2799
|
+
const reset = repairable.get(locator);
|
|
2800
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
2801
|
+
}
|
|
2802
|
+
function resendLater() {
|
|
2803
|
+
if (!socketOpen)
|
|
2804
|
+
return;
|
|
2805
|
+
void recovery.attempt('resend', resendPass, { again: true });
|
|
2806
|
+
}
|
|
2807
|
+
async function resendPass(signal) {
|
|
2808
|
+
if (!socketOpen || closed || stopping || discoveryPaused)
|
|
2809
|
+
return 'done';
|
|
2810
|
+
const reading = { complete: true, reachable: true };
|
|
2811
|
+
try {
|
|
2812
|
+
await conversations.resend(reading);
|
|
2813
|
+
}
|
|
2814
|
+
catch {
|
|
2815
|
+
reading.reachable = false;
|
|
2816
|
+
}
|
|
2817
|
+
if (signal.aborted)
|
|
2818
|
+
return 'done';
|
|
2819
|
+
return reading.reachable && reading.complete ? 'done' : 'again';
|
|
1955
2820
|
}
|
|
1956
2821
|
function submitAll() {
|
|
1957
2822
|
for (const [locator, replica] of replicas) {
|
|
@@ -1959,31 +2824,41 @@ export function composeRuntime(options) {
|
|
|
1959
2824
|
submitLater(locator);
|
|
1960
2825
|
}
|
|
1961
2826
|
}
|
|
1962
|
-
async function finishOpening(progress) {
|
|
2827
|
+
async function finishOpening(progress, live) {
|
|
1963
2828
|
try {
|
|
1964
|
-
if (
|
|
2829
|
+
if (!live()) {
|
|
1965
2830
|
progress.complete = false;
|
|
1966
2831
|
return;
|
|
1967
2832
|
}
|
|
1968
2833
|
submitAll();
|
|
2834
|
+
for (const locator of stale)
|
|
2835
|
+
if (repairable.has(locator))
|
|
2836
|
+
repairLater(locator, {});
|
|
1969
2837
|
await conversations.reconcile();
|
|
1970
2838
|
if (status.providerUnreachable)
|
|
1971
2839
|
conversations.reconnected();
|
|
1972
2840
|
const reading = { complete: true, reachable: true };
|
|
1973
|
-
|
|
1974
|
-
|
|
2841
|
+
if (demand.conversations)
|
|
2842
|
+
await conversations.catchUp(reading);
|
|
2843
|
+
if (progress.reachable && reading.reachable)
|
|
2844
|
+
await conversations.resend(reading);
|
|
2845
|
+
if (!reading.reachable) {
|
|
1975
2846
|
progress.reachable = false;
|
|
2847
|
+
resendLater();
|
|
2848
|
+
}
|
|
1976
2849
|
}
|
|
1977
2850
|
catch {
|
|
1978
2851
|
progress.complete = false;
|
|
1979
2852
|
}
|
|
1980
2853
|
finally {
|
|
1981
|
-
|
|
1982
|
-
|
|
2854
|
+
if (live()) {
|
|
2855
|
+
complete = progress.complete;
|
|
2856
|
+
await rest(progress, live);
|
|
2857
|
+
}
|
|
1983
2858
|
}
|
|
1984
2859
|
}
|
|
1985
|
-
async function rest(progress) {
|
|
1986
|
-
if (
|
|
2860
|
+
async function rest(progress, live = () => !closed) {
|
|
2861
|
+
if (!live())
|
|
1987
2862
|
return;
|
|
1988
2863
|
let lastCompleted = status.lastCompleted;
|
|
1989
2864
|
let synced = complete && stale.size === 0 && !pending();
|
|
@@ -1991,6 +2866,8 @@ export function composeRuntime(options) {
|
|
|
1991
2866
|
const completedAt = new Date().toISOString();
|
|
1992
2867
|
try {
|
|
1993
2868
|
await (await openLocal()).complete(completedAt);
|
|
2869
|
+
if (!live())
|
|
2870
|
+
return;
|
|
1994
2871
|
recorded = true;
|
|
1995
2872
|
lastCompleted = completedAt;
|
|
1996
2873
|
}
|
|
@@ -2005,40 +2882,68 @@ export function composeRuntime(options) {
|
|
|
2005
2882
|
...attention(),
|
|
2006
2883
|
});
|
|
2007
2884
|
}
|
|
2885
|
+
function discover() {
|
|
2886
|
+
let release = () => { };
|
|
2887
|
+
const done = new Promise((resolve) => {
|
|
2888
|
+
release = resolve;
|
|
2889
|
+
});
|
|
2890
|
+
discovering = { generation, done, release };
|
|
2891
|
+
return discovering;
|
|
2892
|
+
}
|
|
2008
2893
|
function applyRoom(locator, roomVersion) {
|
|
2009
2894
|
announced.set(locator, roomVersion);
|
|
2010
|
-
return task(async () => {
|
|
2011
|
-
|
|
2012
|
-
|
|
2895
|
+
return task(roomLane(locator), async (live) => {
|
|
2896
|
+
if (discovering?.generation === generation)
|
|
2897
|
+
await discovering.done;
|
|
2898
|
+
const saving = answering.get(locator);
|
|
2899
|
+
if (saving?.generation === generation)
|
|
2900
|
+
await saving.answered;
|
|
2901
|
+
if (!live() || authorization === undefined)
|
|
2013
2902
|
return;
|
|
2014
2903
|
const room = discovered.get(locator);
|
|
2015
2904
|
const held = replicas.get(locator);
|
|
2016
|
-
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2905
|
+
if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
|
|
2017
2906
|
return;
|
|
2018
2907
|
if (held !== undefined &&
|
|
2019
2908
|
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
2020
2909
|
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
2021
2910
|
return;
|
|
2022
2911
|
}
|
|
2023
|
-
const during = underway;
|
|
2912
|
+
const during = underway?.generation === generation ? underway.progress : null;
|
|
2024
2913
|
const progress = during ?? { complete: true, reachable: true };
|
|
2025
2914
|
if (during === null)
|
|
2026
2915
|
moveTo(transferring(status));
|
|
2027
2916
|
try {
|
|
2028
2917
|
const local = await openLocal();
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2918
|
+
await admittedRead(async () => {
|
|
2919
|
+
if (!live() || authorization === undefined)
|
|
2920
|
+
return;
|
|
2921
|
+
const withheld = await advance(room, local, authorization, live);
|
|
2922
|
+
if (withheld !== null)
|
|
2923
|
+
await withhold(withheld, room, local, progress);
|
|
2924
|
+
});
|
|
2032
2925
|
}
|
|
2033
2926
|
catch {
|
|
2034
2927
|
stale.add(locator);
|
|
2035
2928
|
}
|
|
2036
2929
|
finally {
|
|
2037
2930
|
if (during === null)
|
|
2038
|
-
await rest(progress);
|
|
2931
|
+
await rest(progress, live);
|
|
2039
2932
|
}
|
|
2040
2933
|
});
|
|
2041
2934
|
}
|
|
2935
|
+
function roomsCarried(replacing) {
|
|
2936
|
+
const replaced = new Set(replacing.map(roomLane));
|
|
2937
|
+
return Promise.all([...lanes]
|
|
2938
|
+
.filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
|
|
2939
|
+
.map(([, held]) => held.chain));
|
|
2940
|
+
}
|
|
2941
|
+
const receipts = {
|
|
2942
|
+
heads: async () => (await openLocal()).receivedHeads(),
|
|
2943
|
+
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
2944
|
+
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
2945
|
+
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
2946
|
+
};
|
|
2042
2947
|
function observeStream() {
|
|
2043
2948
|
const stream = options.adapter.roomStream;
|
|
2044
2949
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -2048,6 +2953,9 @@ export function composeRuntime(options) {
|
|
|
2048
2953
|
return;
|
|
2049
2954
|
if (event.kind === 'opened') {
|
|
2050
2955
|
socketOpen = true;
|
|
2956
|
+
generation += 1;
|
|
2957
|
+
conversations.reopened();
|
|
2958
|
+
recovery.resume();
|
|
2051
2959
|
void sync('opened');
|
|
2052
2960
|
}
|
|
2053
2961
|
else if (event.kind === 'closed')
|
|
@@ -2056,7 +2964,7 @@ export function composeRuntime(options) {
|
|
|
2056
2964
|
void applyOpening(event.expired);
|
|
2057
2965
|
else
|
|
2058
2966
|
void applyRoom(event.locator, event.roomVersion);
|
|
2059
|
-
});
|
|
2967
|
+
}, receipts);
|
|
2060
2968
|
}
|
|
2061
2969
|
function writesHeld() {
|
|
2062
2970
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -2071,6 +2979,7 @@ export function composeRuntime(options) {
|
|
|
2071
2979
|
haltedRefusal = false;
|
|
2072
2980
|
if (mode !== serviceMode) {
|
|
2073
2981
|
serviceMode = mode;
|
|
2982
|
+
queueReadiness();
|
|
2074
2983
|
journal.record(`service mode ${mode}`);
|
|
2075
2984
|
for (const listener of serviceModeListeners)
|
|
2076
2985
|
listener(mode);
|
|
@@ -2083,6 +2992,8 @@ export function composeRuntime(options) {
|
|
|
2083
2992
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
2084
2993
|
return;
|
|
2085
2994
|
withdrawConversations = stream.observe((event) => {
|
|
2995
|
+
if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
|
|
2996
|
+
queueReadiness();
|
|
2086
2997
|
if (conversations.receive(event)) {
|
|
2087
2998
|
void ready.then(scheduleCurrency);
|
|
2088
2999
|
return;
|
|
@@ -2094,17 +3005,18 @@ export function composeRuntime(options) {
|
|
|
2094
3005
|
});
|
|
2095
3006
|
}
|
|
2096
3007
|
function scheduleCurrency() {
|
|
2097
|
-
if (currencyQueued || closed || stopping)
|
|
3008
|
+
if (currencyQueued === generation || closed || stopping)
|
|
2098
3009
|
return;
|
|
2099
|
-
currencyQueued =
|
|
2100
|
-
void task(runCurrency);
|
|
3010
|
+
currencyQueued = generation;
|
|
3011
|
+
void task(CONVERSATION_LANE, runCurrency);
|
|
2101
3012
|
}
|
|
2102
|
-
async function runCurrency() {
|
|
2103
|
-
currencyQueued
|
|
2104
|
-
|
|
3013
|
+
async function runCurrency(live) {
|
|
3014
|
+
if (currencyQueued === generation)
|
|
3015
|
+
currencyQueued = null;
|
|
3016
|
+
if (!live())
|
|
2105
3017
|
return;
|
|
2106
3018
|
await conversations.reconcile();
|
|
2107
|
-
if (conversations.current()) {
|
|
3019
|
+
if (conversations.current() || !demand.conversations) {
|
|
2108
3020
|
show();
|
|
2109
3021
|
return;
|
|
2110
3022
|
}
|
|
@@ -2116,19 +3028,24 @@ export function composeRuntime(options) {
|
|
|
2116
3028
|
catch {
|
|
2117
3029
|
progress.reachable = false;
|
|
2118
3030
|
}
|
|
2119
|
-
await rest(progress);
|
|
3031
|
+
await rest(progress, live);
|
|
2120
3032
|
}
|
|
2121
3033
|
function close() {
|
|
2122
3034
|
if (closed)
|
|
2123
3035
|
return;
|
|
2124
3036
|
closed = true;
|
|
3037
|
+
if (narrowing !== null)
|
|
3038
|
+
clearTimeout(narrowing);
|
|
3039
|
+
if (rechecking !== null)
|
|
3040
|
+
clearInterval(rechecking);
|
|
3041
|
+
readinessWatches.clear();
|
|
2125
3042
|
withdrawStream?.();
|
|
2126
3043
|
withdrawStream = undefined;
|
|
2127
3044
|
withdrawConversations?.();
|
|
2128
3045
|
withdrawConversations = undefined;
|
|
2129
3046
|
withdrawServiceMode?.();
|
|
2130
3047
|
conversations.close();
|
|
2131
|
-
|
|
3048
|
+
void recovery.retire();
|
|
2132
3049
|
store?.close();
|
|
2133
3050
|
void opening?.then((opened) => {
|
|
2134
3051
|
opened.close();
|
|
@@ -2146,9 +3063,8 @@ export function composeRuntime(options) {
|
|
|
2146
3063
|
}
|
|
2147
3064
|
async function stop() {
|
|
2148
3065
|
stopping = true;
|
|
2149
|
-
|
|
2150
|
-
await
|
|
2151
|
-
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
3066
|
+
await recovery.retire();
|
|
3067
|
+
await Promise.all(working);
|
|
2152
3068
|
await mutations;
|
|
2153
3069
|
close();
|
|
2154
3070
|
}
|
|
@@ -2170,17 +3086,96 @@ export function composeRuntime(options) {
|
|
|
2170
3086
|
return planOf(input, roomFacts(reach), planReach(reach));
|
|
2171
3087
|
}
|
|
2172
3088
|
function watchPlan(plan, deliver) {
|
|
2173
|
-
|
|
2174
|
-
|
|
3089
|
+
let seeded = false;
|
|
3090
|
+
let pending = true;
|
|
3091
|
+
const shown = () => {
|
|
3092
|
+
const seed = seeds.get(plan.key);
|
|
3093
|
+
if (seed === undefined || seeded || !pending)
|
|
3094
|
+
return;
|
|
3095
|
+
seeded = true;
|
|
3096
|
+
deliver(seededResult(seed));
|
|
3097
|
+
};
|
|
3098
|
+
const watchers = seedWatchers.get(plan.key) ?? new Set();
|
|
3099
|
+
seedWatchers.set(plan.key, watchers);
|
|
3100
|
+
watchers.add(shown);
|
|
3101
|
+
const stop = readSide.watch(plan, identityOf(plan), () => {
|
|
3102
|
+
const cover = covered(plan);
|
|
3103
|
+
awaitPreparation(cover);
|
|
3104
|
+
return seedBehind(plan, cover);
|
|
3105
|
+
}, (message) => {
|
|
3106
|
+
const answered = message;
|
|
3107
|
+
pending = whole(answered) && 'pending' in answered.result;
|
|
3108
|
+
if (pending && seeds.has(plan.key)) {
|
|
3109
|
+
shown();
|
|
3110
|
+
return;
|
|
3111
|
+
}
|
|
3112
|
+
seeded = false;
|
|
3113
|
+
deliver(message);
|
|
3114
|
+
});
|
|
3115
|
+
return () => {
|
|
3116
|
+
watchers.delete(shown);
|
|
3117
|
+
if (watchers.size === 0)
|
|
3118
|
+
seedWatchers.delete(plan.key);
|
|
3119
|
+
stop();
|
|
3120
|
+
};
|
|
2175
3121
|
}
|
|
2176
3122
|
async function searched(input, reach) {
|
|
2177
3123
|
const plan = searchPlanOf(input);
|
|
2178
|
-
const
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
3124
|
+
const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
|
|
3125
|
+
for (;;) {
|
|
3126
|
+
const cover = covered(filter);
|
|
3127
|
+
awaitPreparation(cover);
|
|
3128
|
+
const pending = 'pending' in cover ? [...cover.pending] : [];
|
|
3129
|
+
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
3130
|
+
const held = covered({
|
|
3131
|
+
...filter,
|
|
3132
|
+
rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
|
|
3133
|
+
});
|
|
3134
|
+
const covering = 'pending' in held ? [] : held.coverage.rooms;
|
|
3135
|
+
const unindexed = covering
|
|
3136
|
+
.filter(({ room }) => !searchIndex.rooms().has(room))
|
|
3137
|
+
.map(({ room }) => room);
|
|
3138
|
+
for (const room of unindexed)
|
|
3139
|
+
pending.push({ room, awaits: 'index' });
|
|
3140
|
+
const listed = conversations.searchable();
|
|
3141
|
+
const named = reach !== null && !reach.conversations
|
|
3142
|
+
? []
|
|
3143
|
+
: (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
|
|
3144
|
+
const waiting = named.filter((one) => !searchIndex.conversations().has(one));
|
|
3145
|
+
for (const conversation of waiting)
|
|
3146
|
+
pending.push({ conversation, awaits: 'index' });
|
|
3147
|
+
if (unindexed.length > 0 || waiting.length > 0)
|
|
3148
|
+
index(unindexed, waiting);
|
|
3149
|
+
const complete = pending.length === 0 && !('pending' in cover);
|
|
3150
|
+
const answering = complete
|
|
3151
|
+
? filter
|
|
3152
|
+
: {
|
|
3153
|
+
...filter,
|
|
3154
|
+
rooms: covering
|
|
3155
|
+
.filter(({ room }) => searchIndex.rooms().has(room))
|
|
3156
|
+
.map(({ room }) => room),
|
|
3157
|
+
};
|
|
3158
|
+
const answered = covered(answering);
|
|
3159
|
+
if ('pending' in answered)
|
|
3160
|
+
continue;
|
|
3161
|
+
const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
|
|
3162
|
+
const seen = searchIndex.changes();
|
|
3163
|
+
await searchIndex.caughtUp();
|
|
3164
|
+
const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(answering);
|
|
3165
|
+
if (selected !== null && 'pending' in selected)
|
|
3166
|
+
continue;
|
|
3167
|
+
if (searchIndex.changes() !== seen ||
|
|
3168
|
+
JSON.stringify(covered(answering)) !== JSON.stringify(answered))
|
|
3169
|
+
continue;
|
|
3170
|
+
const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, complete ? null : new Set(answering.rooms));
|
|
3171
|
+
const coverage = {
|
|
3172
|
+
rooms: answered.coverage.rooms,
|
|
3173
|
+
conversations: searchedConversations,
|
|
3174
|
+
index: searchIndex.generation(),
|
|
3175
|
+
overlay: answered.coverage.overlay,
|
|
3176
|
+
};
|
|
3177
|
+
return complete ? { ...hits, coverage } : { pending, ...hits, coverage };
|
|
3178
|
+
}
|
|
2184
3179
|
}
|
|
2185
3180
|
const watch = (topic, input, deliver) => {
|
|
2186
3181
|
if (topic === 'room-drafts') {
|
|
@@ -2226,6 +3221,9 @@ export function composeRuntime(options) {
|
|
|
2226
3221
|
presentationListeners.delete(listener);
|
|
2227
3222
|
};
|
|
2228
3223
|
}
|
|
3224
|
+
if (topic === 'readiness') {
|
|
3225
|
+
return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
|
|
3226
|
+
}
|
|
2229
3227
|
if (topic === 'rooms') {
|
|
2230
3228
|
roomListeners.add(deliver);
|
|
2231
3229
|
deliver(listed());
|
|
@@ -2392,21 +3390,29 @@ export function composeRuntime(options) {
|
|
|
2392
3390
|
return journal.export();
|
|
2393
3391
|
},
|
|
2394
3392
|
persistence() {
|
|
2395
|
-
return
|
|
3393
|
+
return persistence === 'granted';
|
|
3394
|
+
},
|
|
3395
|
+
async retry() {
|
|
3396
|
+
journal.record('retry');
|
|
3397
|
+
unindexable.clear();
|
|
3398
|
+
queueReadiness();
|
|
3399
|
+
resume();
|
|
2396
3400
|
},
|
|
2397
3401
|
observe: observing(watch),
|
|
2398
3402
|
async reauthorize() {
|
|
2399
3403
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2400
3404
|
return;
|
|
2401
3405
|
journal.record('connection reauthorize');
|
|
2402
|
-
await
|
|
3406
|
+
await Promise.all([...lanes.values()]
|
|
3407
|
+
.filter((held) => held.generation === generation)
|
|
3408
|
+
.map((held) => held.chain));
|
|
2403
3409
|
await mutations;
|
|
2404
3410
|
authorization = undefined;
|
|
2405
3411
|
discoveryPaused = false;
|
|
2406
3412
|
if (options.adapter.roomStream === undefined)
|
|
2407
3413
|
await sync('unstreamed');
|
|
2408
3414
|
else
|
|
2409
|
-
await task(ensureAuthorization);
|
|
3415
|
+
await task(OPENING_LANE, ensureAuthorization);
|
|
2410
3416
|
},
|
|
2411
3417
|
};
|
|
2412
3418
|
const reachable = {
|
|
@@ -2417,6 +3423,22 @@ export function composeRuntime(options) {
|
|
|
2417
3423
|
query: (plan) => rowsQuery(plan),
|
|
2418
3424
|
watchPlan,
|
|
2419
3425
|
search: searched,
|
|
3426
|
+
watchReadiness,
|
|
3427
|
+
};
|
|
3428
|
+
return {
|
|
3429
|
+
md,
|
|
3430
|
+
seed,
|
|
3431
|
+
stop,
|
|
3432
|
+
lose: loseRooms,
|
|
3433
|
+
watch,
|
|
3434
|
+
acquire,
|
|
3435
|
+
persisted(answer) {
|
|
3436
|
+
if (answer === persistence)
|
|
3437
|
+
return;
|
|
3438
|
+
persistence = answer;
|
|
3439
|
+
journal.record(`persistence ${answer}`);
|
|
3440
|
+
queueReadiness();
|
|
3441
|
+
},
|
|
3442
|
+
within: (reach, port) => narrowed(reachable, reach, port),
|
|
2420
3443
|
};
|
|
2421
|
-
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2422
3444
|
}
|