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