@agentshouse/mdruntime 0.1.0-c967d1ca66fee → 0.1.0-c97456d63baba
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +257 -227
- package/README.md +11 -1
- 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 +69 -20
- package/dist/adapter.d.ts.map +1 -1
- package/dist/attempt.d.ts +16 -0
- package/dist/attempt.d.ts.map +1 -0
- package/dist/attempt.js +70 -0
- package/dist/binary.d.ts +23 -0
- package/dist/binary.d.ts.map +1 -0
- package/dist/binary.js +8 -0
- package/dist/bootstrap.d.ts +2 -2
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +33 -14
- package/dist/change-set.d.ts +2 -0
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +39 -13
- package/dist/change.d.ts +11 -0
- package/dist/change.d.ts.map +1 -1
- package/dist/change.js +26 -1
- package/dist/chat/acts.d.ts +33 -0
- package/dist/chat/acts.d.ts.map +1 -0
- package/dist/chat/acts.js +36 -0
- package/dist/chat/context.d.ts +3 -0
- package/dist/chat/context.d.ts.map +1 -0
- package/dist/chat/context.js +10 -0
- package/dist/chat/input.d.ts +9 -0
- package/dist/chat/input.d.ts.map +1 -0
- package/dist/chat/input.js +99 -0
- package/dist/chat/kit.d.ts +61 -0
- package/dist/chat/kit.d.ts.map +1 -0
- package/dist/chat/kit.js +123 -0
- package/dist/chat/lasting.d.ts +6 -0
- package/dist/chat/lasting.d.ts.map +1 -0
- package/dist/chat/lasting.js +19 -0
- package/dist/chat/list.d.ts +27 -0
- package/dist/chat/list.d.ts.map +1 -0
- package/dist/chat/list.js +63 -0
- package/dist/chat/markdown.d.ts +20 -0
- package/dist/chat/markdown.d.ts.map +1 -0
- package/dist/chat/markdown.js +188 -0
- package/dist/chat/message.d.ts +6 -0
- package/dist/chat/message.d.ts.map +1 -0
- package/dist/chat/message.js +192 -0
- package/dist/chat/messages.d.ts +22 -0
- package/dist/chat/messages.d.ts.map +1 -0
- package/dist/chat/messages.js +155 -0
- package/dist/chat/name.d.ts +6 -0
- package/dist/chat/name.d.ts.map +1 -0
- package/dist/chat/name.js +37 -0
- package/dist/chat/opened.d.ts +11 -0
- package/dist/chat/opened.d.ts.map +1 -0
- package/dist/chat/opened.js +17 -0
- package/dist/chat/overlay.d.ts +31 -0
- package/dist/chat/overlay.d.ts.map +1 -0
- package/dist/chat/overlay.js +97 -0
- package/dist/chat/parts.d.ts +57 -0
- package/dist/chat/parts.d.ts.map +1 -0
- package/dist/chat/parts.js +119 -0
- package/dist/chat/process.d.ts +20 -0
- package/dist/chat/process.d.ts.map +1 -0
- package/dist/chat/process.js +32 -0
- package/dist/chat/provider.d.ts +63 -0
- package/dist/chat/provider.d.ts.map +1 -0
- package/dist/chat/provider.js +629 -0
- package/dist/chat/question.d.ts +9 -0
- package/dist/chat/question.d.ts.map +1 -0
- package/dist/chat/question.js +80 -0
- package/dist/chat/questions.d.ts +31 -0
- package/dist/chat/questions.d.ts.map +1 -0
- package/dist/chat/questions.js +111 -0
- package/dist/chat/read.d.ts +5 -0
- package/dist/chat/read.d.ts.map +1 -0
- package/dist/chat/read.js +13 -0
- package/dist/chat/ready.d.ts +34 -0
- package/dist/chat/ready.d.ts.map +1 -0
- package/dist/chat/ready.js +91 -0
- package/dist/chat/style.d.ts +4 -0
- package/dist/chat/style.d.ts.map +1 -0
- package/dist/chat/style.js +302 -0
- package/dist/chat.d.ts +14 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +8 -0
- package/dist/client.d.ts +21 -10
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +14 -2
- 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 +32 -12
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +1482 -351
- package/dist/conversation.d.ts +23 -9
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +372 -142
- 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/expectation.d.ts +21 -0
- package/dist/expectation.d.ts.map +1 -0
- package/dist/expectation.js +70 -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 +13 -3
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +48 -11
- package/dist/identity.d.ts +4 -0
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +12 -0
- package/dist/index.d.ts +13 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/memory.d.ts +5 -3
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +47 -13
- 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 +11 -1
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +7 -0
- package/dist/overlay.d.ts +14 -2
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +75 -62
- package/dist/query.d.ts +31 -3
- package/dist/query.d.ts.map +1 -1
- package/dist/reach.d.ts +8 -2
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +42 -8
- package/dist/react.d.ts +7 -4
- package/dist/react.d.ts.map +1 -1
- package/dist/read-side.d.ts +13 -6
- package/dist/read-side.d.ts.map +1 -1
- package/dist/read-side.js +291 -85
- 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 +21 -25
- package/dist/replica.d.ts +6 -3
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +78 -45
- package/dist/room-draft.d.ts +20 -3
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +119 -1
- package/dist/rows.d.ts +6 -13
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +5 -13
- package/dist/save.d.ts +28 -0
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +343 -29
- package/dist/search.d.ts +28 -6
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +108 -45
- package/dist/shared.d.ts +11 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +368 -0
- package/dist/status.d.ts +4 -0
- package/dist/status.d.ts.map +1 -1
- package/dist/store.d.ts +55 -21
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +424 -206
- 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 +137 -30
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +49 -29
- package/package.json +26 -14
- package/dist/attachment.d.ts +0 -3
- package/dist/attachment.d.ts.map +0 -1
- package/dist/attachment.js +0 -8
package/dist/connection.js
CHANGED
|
@@ -6,19 +6,27 @@ import { createReadSide } from './read-side.js';
|
|
|
6
6
|
import { markdownPath, roomOfKey } from './rows.js';
|
|
7
7
|
import { createSearchIndex, searchPlanOf, } from './search.js';
|
|
8
8
|
import { bootstrapRoom } from './bootstrap.js';
|
|
9
|
-
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
9
|
+
import { changeSet, entering, mergeChangeSets } from './change-set.js';
|
|
10
10
|
import { createConversationStore, } from './conversation.js';
|
|
11
|
-
import {
|
|
11
|
+
import { queuedFile } from './change.js';
|
|
12
|
+
import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
|
|
12
13
|
import { runtimeError } from './failure.js';
|
|
13
14
|
import { createLog } from './log.js';
|
|
14
|
-
import {
|
|
15
|
+
import { binaryRecord } from './binary.js';
|
|
16
|
+
import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
|
|
17
|
+
import { createRecovery } from './recovery.js';
|
|
15
18
|
import { refreshRoom } from './refresh.js';
|
|
16
|
-
import { buildBatch } from './save.js';
|
|
19
|
+
import { buildBatch, joined, limitedIn, requeued } from './save.js';
|
|
17
20
|
import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
|
|
18
|
-
import { changedParts, changesOf, held, readParts, returnedBy, returnedInto, staleWrite, underneath, } from './room-draft.js';
|
|
21
|
+
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
19
22
|
import { same, saving, STARTING, transferring, } from './status.js';
|
|
20
23
|
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
21
|
-
import {
|
|
24
|
+
import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
|
|
25
|
+
import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
|
|
26
|
+
import { FencedMap } from './fence.js';
|
|
27
|
+
import { tally } from './shared.js';
|
|
28
|
+
import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
|
|
29
|
+
import { identityTime, roomDraftRevision } from './identity.js';
|
|
22
30
|
import { beneath, normalize } from './paths.js';
|
|
23
31
|
import { openRuntimeStore, } from './store.js';
|
|
24
32
|
import { unconfirmed } from './transfer.js';
|
|
@@ -36,22 +44,59 @@ function declaredSelection(room) {
|
|
|
36
44
|
root !== '' &&
|
|
37
45
|
selection.roots.every((other, at) => at === index || !within(other, root)));
|
|
38
46
|
}
|
|
47
|
+
const OPENING_LANE = 'opening';
|
|
48
|
+
const CONVERSATION_LANE = 'conversations';
|
|
49
|
+
const ROOM_READS = 256;
|
|
50
|
+
const ROOM_ATTEMPTS = 16;
|
|
51
|
+
const outside = new Map();
|
|
52
|
+
function outsideOf(roots) {
|
|
53
|
+
const key = JSON.stringify(roots);
|
|
54
|
+
const held = outside.get(key);
|
|
55
|
+
if (held !== undefined)
|
|
56
|
+
return held;
|
|
57
|
+
const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
|
|
58
|
+
const made = {
|
|
59
|
+
documents: (path) => (markdownPath(path) ? counted(path) : 0),
|
|
60
|
+
binaries: counted,
|
|
61
|
+
};
|
|
62
|
+
outside.set(key, made);
|
|
63
|
+
return made;
|
|
64
|
+
}
|
|
65
|
+
function abandonedOn(signal) {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
if (signal.aborted)
|
|
68
|
+
resolve(null);
|
|
69
|
+
else
|
|
70
|
+
signal.addEventListener('abort', () => resolve(null), { once: true });
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function roomLane(locator) {
|
|
74
|
+
return `room ${locator}`;
|
|
75
|
+
}
|
|
39
76
|
export function composeRuntime(options) {
|
|
40
77
|
const localSaves = options.adapter.localSaves === true;
|
|
41
78
|
let authorization = options.authorization;
|
|
42
79
|
let opening;
|
|
43
80
|
let store;
|
|
44
|
-
const
|
|
81
|
+
const fences = new Map();
|
|
82
|
+
const isFenced = (locator) => fences.has(locator);
|
|
83
|
+
let fencings = 0;
|
|
84
|
+
const lastFenced = new Map();
|
|
85
|
+
const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
|
|
86
|
+
let discoveries = 0;
|
|
87
|
+
const replicas = new FencedMap(isFenced);
|
|
88
|
+
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
45
89
|
const pendings = new Map();
|
|
46
|
-
const roomDrafts = new
|
|
90
|
+
const roomDrafts = new FencedMap(isFenced);
|
|
47
91
|
const presentations = new Map();
|
|
48
92
|
let limits = [];
|
|
49
93
|
let capabilities = [];
|
|
50
|
-
const rooms = new
|
|
94
|
+
const rooms = new FencedMap(isFenced);
|
|
51
95
|
const counted = new Map();
|
|
52
96
|
const unreadable = new Set();
|
|
53
|
-
const
|
|
54
|
-
const
|
|
97
|
+
const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
|
|
98
|
+
const models = new FencedMap(isModelFenced);
|
|
99
|
+
const liveModels = new FencedMap(isModelFenced);
|
|
55
100
|
const invalidModels = new Set();
|
|
56
101
|
const discovered = new Map();
|
|
57
102
|
let roomsListed = false;
|
|
@@ -81,7 +126,6 @@ export function composeRuntime(options) {
|
|
|
81
126
|
let status = STARTING;
|
|
82
127
|
let shown = STARTING;
|
|
83
128
|
let discoveryPaused = false;
|
|
84
|
-
let resetTimer;
|
|
85
129
|
let closed = false;
|
|
86
130
|
let stopping = false;
|
|
87
131
|
let withdrawStream;
|
|
@@ -89,12 +133,21 @@ export function composeRuntime(options) {
|
|
|
89
133
|
let underway = null;
|
|
90
134
|
let streamed = null;
|
|
91
135
|
let handed = false;
|
|
92
|
-
const
|
|
136
|
+
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
137
|
+
const repairing = new Set();
|
|
138
|
+
const repairable = new Map();
|
|
139
|
+
let outcomeRetention = null;
|
|
93
140
|
const answering = new Map();
|
|
94
141
|
let complete = false;
|
|
95
142
|
let recorded = false;
|
|
96
143
|
const stale = new Set();
|
|
97
|
-
let
|
|
144
|
+
let generation = 0;
|
|
145
|
+
let discovering = null;
|
|
146
|
+
let opened = null;
|
|
147
|
+
const owed = new Set();
|
|
148
|
+
const lanes = new Map();
|
|
149
|
+
let roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
150
|
+
const working = new Set();
|
|
98
151
|
let mutations = Promise.resolve();
|
|
99
152
|
const roomListeners = new Set();
|
|
100
153
|
const refusalListeners = new Set();
|
|
@@ -105,7 +158,7 @@ export function composeRuntime(options) {
|
|
|
105
158
|
const presentationListeners = new Set();
|
|
106
159
|
const readSide = createReadSide();
|
|
107
160
|
let withdrawConversations;
|
|
108
|
-
let currencyQueued =
|
|
161
|
+
let currencyQueued = null;
|
|
109
162
|
let socketOpen = false;
|
|
110
163
|
let serviceMode = 'open';
|
|
111
164
|
let haltedRefusal = false;
|
|
@@ -113,24 +166,41 @@ export function composeRuntime(options) {
|
|
|
113
166
|
const serviceModeListeners = new Set();
|
|
114
167
|
let member = null;
|
|
115
168
|
const memberListeners = new Set();
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
const
|
|
169
|
+
const prepared = new Set();
|
|
170
|
+
const projecting = new Set();
|
|
171
|
+
const unpreparedDemand = new Set();
|
|
172
|
+
const unindexable = new Set();
|
|
173
|
+
let preparing = false;
|
|
174
|
+
let demand = WHOLE_DEMAND;
|
|
175
|
+
let narrowing = null;
|
|
176
|
+
let targets = {};
|
|
177
|
+
let persistence = options.shell.persistence;
|
|
178
|
+
let capacity = null;
|
|
179
|
+
const paused = new Set();
|
|
180
|
+
let rechecking = null;
|
|
181
|
+
const readinessWatches = new Set();
|
|
182
|
+
let readinessQueued = false;
|
|
183
|
+
const catching = new Set();
|
|
184
|
+
const transfers = new FencedMap(isFenced);
|
|
119
185
|
const conversations = createConversationStore({
|
|
120
186
|
agents: declaredAgents,
|
|
121
|
-
documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
|
|
187
|
+
documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
|
|
122
188
|
call: (name, input) => invokeAction(name, input),
|
|
123
189
|
durably,
|
|
124
190
|
stream: () => options.adapter.conversations,
|
|
125
191
|
publish: publishChangeSet,
|
|
192
|
+
online: () => !status.providerUnreachable && refusal === null,
|
|
126
193
|
closed: () => closed,
|
|
194
|
+
unanswered: resendLater,
|
|
195
|
+
expired: (operation) => pastHorizon(operation),
|
|
127
196
|
});
|
|
128
197
|
const searchIndex = createSearchIndex({
|
|
129
|
-
|
|
198
|
+
room: (room) => readSide.room(room),
|
|
130
199
|
document: (room, path) => readSide.document(room, path),
|
|
131
|
-
messages: () => conversations.messages(),
|
|
132
|
-
message: (conversation, identity) => conversations.message(conversation, identity),
|
|
133
200
|
});
|
|
201
|
+
const roomsToIndex = new Set();
|
|
202
|
+
const conversationsToIndex = new Set();
|
|
203
|
+
let indexing = false;
|
|
134
204
|
const journal = createLog();
|
|
135
205
|
let notifyReady = () => { };
|
|
136
206
|
const ready = new Promise((resolve) => {
|
|
@@ -194,7 +264,9 @@ export function composeRuntime(options) {
|
|
|
194
264
|
}
|
|
195
265
|
}
|
|
196
266
|
function pending() {
|
|
197
|
-
return !localSaves &&
|
|
267
|
+
return (!localSaves &&
|
|
268
|
+
(queuesWithoutReplica.size > 0 ||
|
|
269
|
+
[...replicas.values()].some((replica) => replica.queue.length > 0)));
|
|
198
270
|
}
|
|
199
271
|
function attention() {
|
|
200
272
|
const attentionRequired = [];
|
|
@@ -207,9 +279,16 @@ export function composeRuntime(options) {
|
|
|
207
279
|
returned: structuredClone(record.returned),
|
|
208
280
|
});
|
|
209
281
|
}
|
|
282
|
+
for (const [room, refusal] of sourceRefusals)
|
|
283
|
+
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
210
284
|
attentionRequired.sort((left, right) => {
|
|
211
285
|
if (left.room !== right.room)
|
|
212
286
|
return left.room < right.room ? -1 : 1;
|
|
287
|
+
if ('refusal' in left || 'refusal' in right) {
|
|
288
|
+
if ('refusal' in left && 'refusal' in right)
|
|
289
|
+
return 0;
|
|
290
|
+
return 'refusal' in left ? -1 : 1;
|
|
291
|
+
}
|
|
213
292
|
if (left.path !== right.path)
|
|
214
293
|
return left.path < right.path ? -1 : 1;
|
|
215
294
|
return 0;
|
|
@@ -228,12 +307,9 @@ export function composeRuntime(options) {
|
|
|
228
307
|
if (replica === undefined)
|
|
229
308
|
return 0;
|
|
230
309
|
const roots = replica.confirmed.record.typedSelection?.roots ?? [];
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
count += 1;
|
|
235
|
-
}
|
|
236
|
-
return count;
|
|
310
|
+
const weighed = outsideOf(roots);
|
|
311
|
+
return (tally(replica.visible.documents, weighed.documents) +
|
|
312
|
+
tally(replica.visible.binaries, weighed.binaries));
|
|
237
313
|
}
|
|
238
314
|
function listed() {
|
|
239
315
|
return structuredClone([...rooms.values()].map((room) => {
|
|
@@ -260,15 +336,19 @@ export function composeRuntime(options) {
|
|
|
260
336
|
return { listed: roomsListed, rooms: held };
|
|
261
337
|
}
|
|
262
338
|
function emitRooms() {
|
|
339
|
+
readSide.recover();
|
|
340
|
+
queueReadiness();
|
|
263
341
|
const value = listed();
|
|
264
342
|
for (const listener of roomListeners)
|
|
265
343
|
listener(value);
|
|
266
344
|
}
|
|
267
345
|
function emitRefusal() {
|
|
346
|
+
queueReadiness();
|
|
268
347
|
for (const listener of refusalListeners)
|
|
269
348
|
listener(refusal);
|
|
270
349
|
}
|
|
271
350
|
function emitStorageLoss() {
|
|
351
|
+
queueReadiness();
|
|
272
352
|
for (const listener of storageLossListeners)
|
|
273
353
|
listener(storageLoss);
|
|
274
354
|
}
|
|
@@ -285,7 +365,8 @@ export function composeRuntime(options) {
|
|
|
285
365
|
function show() {
|
|
286
366
|
if (closed)
|
|
287
367
|
return;
|
|
288
|
-
|
|
368
|
+
queueReadiness();
|
|
369
|
+
const next = status.state === 'synced' && demand.conversations && !conversations.current()
|
|
289
370
|
? { ...status, state: 'syncing' }
|
|
290
371
|
: status;
|
|
291
372
|
if (same(shown, next))
|
|
@@ -304,10 +385,7 @@ export function composeRuntime(options) {
|
|
|
304
385
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
305
386
|
}
|
|
306
387
|
function entriesOf(locator) {
|
|
307
|
-
|
|
308
|
-
if (replica === undefined)
|
|
309
|
-
return [];
|
|
310
|
-
return replica.queue.map(entryOf);
|
|
388
|
+
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
|
|
311
389
|
}
|
|
312
390
|
function emitQueue(locator) {
|
|
313
391
|
const entries = entriesOf(locator);
|
|
@@ -317,42 +395,481 @@ export function composeRuntime(options) {
|
|
|
317
395
|
}
|
|
318
396
|
}
|
|
319
397
|
function applyChangeSet(set) {
|
|
320
|
-
const held =
|
|
321
|
-
|
|
322
|
-
|
|
398
|
+
const held = {
|
|
399
|
+
...set,
|
|
400
|
+
documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
|
|
401
|
+
};
|
|
323
402
|
readSide.apply(held);
|
|
324
403
|
searchIndex.changed(held);
|
|
325
404
|
}
|
|
326
|
-
function
|
|
327
|
-
|
|
405
|
+
async function referenced(locator) {
|
|
406
|
+
for (;;) {
|
|
407
|
+
const record = replicas.get(locator)?.confirmed.record;
|
|
408
|
+
if (record === undefined || closed)
|
|
409
|
+
return;
|
|
410
|
+
const held = { text: record.documents, binaries: record.binaries };
|
|
411
|
+
const composition = compositionOf(record);
|
|
412
|
+
if (referencesIndexed(held, composition))
|
|
413
|
+
return;
|
|
414
|
+
await indexReferrers(held, composition);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
async function prepareReplica(locator) {
|
|
418
|
+
await referenced(locator);
|
|
419
|
+
const replica = replicas.get(locator);
|
|
420
|
+
if (replica === undefined || closed || prepared.has(locator))
|
|
328
421
|
return;
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
422
|
+
const visible = replica.visible;
|
|
423
|
+
const paths = [
|
|
424
|
+
...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
|
|
425
|
+
];
|
|
426
|
+
const entered = [];
|
|
427
|
+
projecting.add(locator);
|
|
428
|
+
try {
|
|
429
|
+
await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
|
|
430
|
+
const current = replicas.get(locator);
|
|
431
|
+
if (current === undefined || closed)
|
|
432
|
+
return;
|
|
433
|
+
const set = entering(locator, current, chunk);
|
|
434
|
+
readSide.apply(set);
|
|
435
|
+
entered.push(set);
|
|
436
|
+
});
|
|
437
|
+
if (!replicas.has(locator) || closed)
|
|
438
|
+
return;
|
|
439
|
+
prepared.add(locator);
|
|
339
440
|
}
|
|
340
|
-
|
|
441
|
+
finally {
|
|
442
|
+
projecting.delete(locator);
|
|
443
|
+
}
|
|
444
|
+
conversations.changed(mergeChangeSets(entered));
|
|
445
|
+
readSide.recover();
|
|
446
|
+
queueReadiness();
|
|
447
|
+
}
|
|
448
|
+
function unprepared() {
|
|
449
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
|
|
450
|
+
}
|
|
451
|
+
function unbuilt(locator, error) {
|
|
452
|
+
unindexable.add(locator);
|
|
453
|
+
journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
|
|
454
|
+
queueReadiness();
|
|
455
|
+
}
|
|
456
|
+
function prepare(locators = []) {
|
|
457
|
+
for (const locator of locators)
|
|
458
|
+
unpreparedDemand.add(locator);
|
|
459
|
+
if (preparing)
|
|
341
460
|
return;
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
461
|
+
preparing = true;
|
|
462
|
+
void (async () => {
|
|
463
|
+
try {
|
|
464
|
+
for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
|
|
465
|
+
await yielded();
|
|
466
|
+
if (closed)
|
|
467
|
+
return;
|
|
468
|
+
unpreparedDemand.delete(waiting);
|
|
469
|
+
await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
finally {
|
|
473
|
+
preparing = false;
|
|
474
|
+
}
|
|
475
|
+
})();
|
|
345
476
|
}
|
|
346
477
|
function reachedRooms(plan) {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
478
|
+
const reach = plan.reach;
|
|
479
|
+
const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
480
|
+
const named = plan.rooms ??
|
|
481
|
+
(reached === null
|
|
482
|
+
? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
|
|
483
|
+
: [...reached]);
|
|
484
|
+
return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
|
|
485
|
+
}
|
|
486
|
+
function awaited(locator) {
|
|
487
|
+
return rooms.has(locator) || !roomsListed;
|
|
488
|
+
}
|
|
489
|
+
function covered(plan) {
|
|
490
|
+
const locators = reachedRooms(plan);
|
|
491
|
+
if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
|
|
492
|
+
return { pending: [] };
|
|
493
|
+
}
|
|
494
|
+
const pending = [];
|
|
495
|
+
const coverage = [];
|
|
496
|
+
const overlay = [];
|
|
497
|
+
for (const locator of locators) {
|
|
498
|
+
const replica = replicas.get(locator);
|
|
499
|
+
if (replica === undefined) {
|
|
500
|
+
if (awaited(locator))
|
|
501
|
+
pending.push({ room: locator, awaits: 'acquisition' });
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (!prepared.has(locator)) {
|
|
505
|
+
pending.push({ room: locator, awaits: 'preparation' });
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
coverage.push({
|
|
509
|
+
room: locator,
|
|
510
|
+
generation: replica.confirmed.record.transfer,
|
|
511
|
+
current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
|
|
512
|
+
});
|
|
513
|
+
for (const batch of replica.queue)
|
|
514
|
+
overlay.push(batch.id);
|
|
515
|
+
}
|
|
516
|
+
if (pending.length > 0)
|
|
517
|
+
return { pending };
|
|
518
|
+
return { coverage: { rooms: coverage, indexes: [], overlay } };
|
|
519
|
+
}
|
|
520
|
+
function awaitPreparation(cover) {
|
|
521
|
+
if (!('pending' in cover))
|
|
522
|
+
return;
|
|
523
|
+
const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
|
|
524
|
+
if (waiting.length > 0)
|
|
525
|
+
prepare(waiting.map(({ room }) => room));
|
|
526
|
+
}
|
|
527
|
+
function acquires(room) {
|
|
528
|
+
return (demanded(demand, room) ||
|
|
529
|
+
(replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
|
|
530
|
+
queuesWithoutReplica.has(room.locator));
|
|
531
|
+
}
|
|
532
|
+
function acquiredLocators() {
|
|
533
|
+
return [...discovered.values()]
|
|
534
|
+
.filter(acquires)
|
|
535
|
+
.map((room) => room.locator)
|
|
536
|
+
.sort();
|
|
537
|
+
}
|
|
538
|
+
function heldDemand(acquired) {
|
|
539
|
+
if (demand.whole)
|
|
350
540
|
return null;
|
|
351
|
-
return
|
|
541
|
+
return { rooms: acquired.map((room) => room.locator), conversations: demand.conversations };
|
|
542
|
+
}
|
|
543
|
+
function applyDemand(next) {
|
|
544
|
+
const before = acquiredLocators();
|
|
545
|
+
const conversed = demand.conversations;
|
|
546
|
+
demand = next;
|
|
547
|
+
const after = acquiredLocators();
|
|
548
|
+
queueReadiness();
|
|
549
|
+
if (!conversed && next.conversations)
|
|
550
|
+
scheduleCurrency();
|
|
551
|
+
if (JSON.stringify(before) === JSON.stringify(after) && conversed === next.conversations)
|
|
552
|
+
return;
|
|
553
|
+
journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
|
|
554
|
+
if (options.adapter.roomStream === undefined)
|
|
555
|
+
void sync('unstreamed');
|
|
556
|
+
else if (socketOpen)
|
|
557
|
+
options.adapter.roomStream.reopen();
|
|
558
|
+
}
|
|
559
|
+
function acquire(next) {
|
|
560
|
+
if (closed)
|
|
561
|
+
return;
|
|
562
|
+
if (narrowing !== null)
|
|
563
|
+
clearTimeout(narrowing);
|
|
564
|
+
narrowing = null;
|
|
565
|
+
const widening = wider(next, demand);
|
|
566
|
+
if (!widening && !narrower(next, demand))
|
|
567
|
+
return;
|
|
568
|
+
if (widening || !roomsListed) {
|
|
569
|
+
applyDemand(next);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
// A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
|
|
573
|
+
narrowing = setTimeout(() => {
|
|
574
|
+
narrowing = null;
|
|
575
|
+
applyDemand(next);
|
|
576
|
+
}, 5_000);
|
|
577
|
+
}
|
|
578
|
+
function requiredRooms(reach) {
|
|
579
|
+
if (reach === null)
|
|
580
|
+
return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
|
|
581
|
+
const named = [...new Set(reach.rooms)].sort();
|
|
582
|
+
if (roomsListed)
|
|
583
|
+
return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
|
|
584
|
+
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
585
|
+
}
|
|
586
|
+
function roomState(locator, search) {
|
|
587
|
+
if (sourceRefusals.has(locator))
|
|
588
|
+
return 'refused';
|
|
589
|
+
const listedRoom = discovered.get(locator);
|
|
590
|
+
if (roomsListed &&
|
|
591
|
+
(listedRoom === undefined ||
|
|
592
|
+
listedRoom.replica.admission !== 'admitted' ||
|
|
593
|
+
unreadable.has(locator)))
|
|
594
|
+
return 'unadmitted';
|
|
595
|
+
if (!replicas.has(locator))
|
|
596
|
+
return 'acquiring';
|
|
597
|
+
if (!prepared.has(locator))
|
|
598
|
+
return 'preparing';
|
|
599
|
+
if (search && !searchIndex.rooms().has(locator))
|
|
600
|
+
return 'indexing';
|
|
601
|
+
return 'complete';
|
|
602
|
+
}
|
|
603
|
+
function conversationCoverage(reach, search) {
|
|
604
|
+
const declared = reach === null || reach.conversations;
|
|
605
|
+
if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
|
|
606
|
+
return 'undeclared';
|
|
607
|
+
if (!conversations.known() || !conversations.current())
|
|
608
|
+
return 'acquiring';
|
|
609
|
+
if (!search)
|
|
610
|
+
return 'complete';
|
|
611
|
+
const unindexed = conversations
|
|
612
|
+
.searchable()
|
|
613
|
+
.filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
|
|
614
|
+
if (unindexed.length === 0)
|
|
615
|
+
return 'complete';
|
|
616
|
+
index([], unindexed);
|
|
617
|
+
return 'indexing';
|
|
618
|
+
}
|
|
619
|
+
function connectionOf() {
|
|
620
|
+
if (socketOpen)
|
|
621
|
+
return 'connected';
|
|
622
|
+
if (status.providerUnreachable)
|
|
623
|
+
return 'unreachable';
|
|
624
|
+
return options.adapter.roomStream === undefined && status.lastCompleted !== null
|
|
625
|
+
? 'connected'
|
|
626
|
+
: 'connecting';
|
|
627
|
+
}
|
|
628
|
+
function refusedIn(locators) {
|
|
629
|
+
return locators.flatMap((room) => {
|
|
630
|
+
const held = sourceRefusals.get(room);
|
|
631
|
+
if (held === undefined)
|
|
632
|
+
return [];
|
|
633
|
+
const { file, location } = held.fields;
|
|
634
|
+
return [
|
|
635
|
+
{
|
|
636
|
+
room,
|
|
637
|
+
file: typeof file === 'string' ? file : '',
|
|
638
|
+
rule: held.code,
|
|
639
|
+
location: typeof location === 'string' ? location : null,
|
|
640
|
+
},
|
|
641
|
+
];
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
function readinessFor(port, reach) {
|
|
645
|
+
const required = requiredRooms(reach);
|
|
646
|
+
const target = targets[port.target];
|
|
647
|
+
const search = target?.search === true;
|
|
648
|
+
const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
|
|
649
|
+
const building = (state) => coverage
|
|
650
|
+
.filter((room) => room.state === state && !unindexable.has(room.room))
|
|
651
|
+
.map(({ room }) => room);
|
|
652
|
+
const preparing = building('preparing');
|
|
653
|
+
if (preparing.length > 0)
|
|
654
|
+
prepare(preparing);
|
|
655
|
+
const indexing = building('indexing');
|
|
656
|
+
if (indexing.length > 0)
|
|
657
|
+
index(indexing, []);
|
|
658
|
+
return readinessOf({
|
|
659
|
+
code: port.code,
|
|
660
|
+
coverage: {
|
|
661
|
+
listed: required.listed,
|
|
662
|
+
rooms: coverage,
|
|
663
|
+
conversations: conversationCoverage(reach, search),
|
|
664
|
+
search: search ? 'required' : 'unused',
|
|
665
|
+
},
|
|
666
|
+
persistence,
|
|
667
|
+
connection: connectionOf(),
|
|
668
|
+
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
669
|
+
capacity: capacity !== null &&
|
|
670
|
+
coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
|
|
671
|
+
? capacity
|
|
672
|
+
: port.capacity,
|
|
673
|
+
refused: refusedIn(required.rooms),
|
|
674
|
+
lost: storageLoss !== null,
|
|
675
|
+
established: target?.established === true,
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
function keepTarget(name, change) {
|
|
679
|
+
const held = targets[name] ?? { established: false, search: false };
|
|
680
|
+
const next = { ...held, ...change };
|
|
681
|
+
if (next.established === held.established && next.search === held.search)
|
|
682
|
+
return;
|
|
683
|
+
targets = { ...targets, [name]: next };
|
|
684
|
+
queueReadiness();
|
|
685
|
+
void durably((local) => local.keepTargets(targets)).catch(() => {
|
|
686
|
+
journal.record(`target record failed ${name}`);
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
function deliverReadiness(watch) {
|
|
690
|
+
if (!readinessWatches.has(watch))
|
|
691
|
+
return;
|
|
692
|
+
const readiness = readinessFor(watch.port, watch.reach);
|
|
693
|
+
if (readiness.ready && targets[watch.port.target]?.established !== true) {
|
|
694
|
+
keepTarget(watch.port.target, { established: true });
|
|
695
|
+
}
|
|
696
|
+
const shown = JSON.stringify(readiness);
|
|
697
|
+
if (shown === watch.last)
|
|
698
|
+
return;
|
|
699
|
+
watch.last = shown;
|
|
700
|
+
watch.deliver(structuredClone(readiness));
|
|
701
|
+
}
|
|
702
|
+
function queueReadiness() {
|
|
703
|
+
if (readinessQueued || closed || readinessWatches.size === 0)
|
|
704
|
+
return;
|
|
705
|
+
readinessQueued = true;
|
|
706
|
+
queueMicrotask(() => {
|
|
707
|
+
readinessQueued = false;
|
|
708
|
+
if (closed)
|
|
709
|
+
return;
|
|
710
|
+
for (const watch of [...readinessWatches])
|
|
711
|
+
deliverReadiness(watch);
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
function watchReadiness(port, reach, deliver) {
|
|
715
|
+
const watch = { port, reach, deliver, last: null };
|
|
716
|
+
readinessWatches.add(watch);
|
|
717
|
+
void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
|
|
718
|
+
return () => {
|
|
719
|
+
readinessWatches.delete(watch);
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
function searching(target) {
|
|
723
|
+
if (targets[target]?.search !== true)
|
|
724
|
+
keepTarget(target, { search: true });
|
|
725
|
+
}
|
|
726
|
+
function layerBytes(layer) {
|
|
727
|
+
let bytes = 0;
|
|
728
|
+
for (const [path, text] of Object.entries(layer.documents))
|
|
729
|
+
bytes += (path.length + text.length) * 2;
|
|
730
|
+
return bytes;
|
|
731
|
+
}
|
|
732
|
+
function grownBy(next, previous) {
|
|
733
|
+
if (previous === undefined)
|
|
734
|
+
return layerBytes(next);
|
|
735
|
+
const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
|
|
736
|
+
return layerBytes({ ...next, documents });
|
|
737
|
+
}
|
|
738
|
+
function exceeded(error) {
|
|
739
|
+
return (typeof error === 'object' &&
|
|
740
|
+
error !== null &&
|
|
741
|
+
error.name === 'QuotaExceededError');
|
|
742
|
+
}
|
|
743
|
+
async function estimated() {
|
|
744
|
+
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
745
|
+
return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
|
|
746
|
+
}
|
|
747
|
+
function pause(locator, cause) {
|
|
748
|
+
paused.add(locator);
|
|
749
|
+
capacity = cause;
|
|
750
|
+
journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
|
|
751
|
+
queueReadiness();
|
|
752
|
+
if (rechecking !== null)
|
|
753
|
+
return;
|
|
754
|
+
// A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
|
|
755
|
+
rechecking = setInterval(() => {
|
|
756
|
+
void recheck();
|
|
757
|
+
}, 60_000);
|
|
758
|
+
}
|
|
759
|
+
async function recheck() {
|
|
760
|
+
const held = capacity;
|
|
761
|
+
if (held === null || closed)
|
|
762
|
+
return;
|
|
763
|
+
const available = await estimated().catch(() => null);
|
|
764
|
+
if (available === null || available < (held.needed ?? 0))
|
|
765
|
+
return;
|
|
766
|
+
if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
|
|
767
|
+
return;
|
|
768
|
+
resume();
|
|
769
|
+
}
|
|
770
|
+
function resume() {
|
|
771
|
+
if (rechecking !== null)
|
|
772
|
+
clearInterval(rechecking);
|
|
773
|
+
rechecking = null;
|
|
774
|
+
if (paused.size === 0 && capacity === null)
|
|
775
|
+
return;
|
|
776
|
+
paused.clear();
|
|
777
|
+
capacity = null;
|
|
778
|
+
journal.record('capacity resumed');
|
|
779
|
+
queueReadiness();
|
|
780
|
+
if (!closed && authorization !== undefined)
|
|
781
|
+
void sync('unstreamed');
|
|
782
|
+
}
|
|
783
|
+
async function reclaimed(local) {
|
|
784
|
+
const unreferenced = [...transfers.keys()].filter((locator) => {
|
|
785
|
+
const room = discovered.get(locator);
|
|
786
|
+
return room === undefined || !acquires(room);
|
|
787
|
+
});
|
|
788
|
+
for (const locator of unreferenced) {
|
|
789
|
+
await local.abandon(locator);
|
|
790
|
+
transfers.delete(locator);
|
|
791
|
+
}
|
|
792
|
+
if (unreferenced.length > 0)
|
|
793
|
+
journal.record(`capacity reclaimed ${unreferenced.length}`);
|
|
794
|
+
return unreferenced.length > 0;
|
|
795
|
+
}
|
|
796
|
+
async function admitted(local, locator, needed) {
|
|
797
|
+
if (paused.has(locator))
|
|
798
|
+
return false;
|
|
799
|
+
let available = await estimated().catch(() => null);
|
|
800
|
+
if (available === null || available >= needed)
|
|
801
|
+
return true;
|
|
802
|
+
if (await reclaimed(local))
|
|
803
|
+
available = await estimated().catch(() => null);
|
|
804
|
+
if (available === null || available >= needed)
|
|
805
|
+
return true;
|
|
806
|
+
pause(locator, { cause: 'insufficient-space', needed, available });
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
async function exceededOn(locator, bytes) {
|
|
810
|
+
const available = await estimated().catch(() => null);
|
|
811
|
+
pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
|
|
812
|
+
}
|
|
813
|
+
async function rowsQuery(plan) {
|
|
814
|
+
const before = covered(plan);
|
|
815
|
+
awaitPreparation(before);
|
|
816
|
+
if ('pending' in before)
|
|
817
|
+
return before;
|
|
818
|
+
const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
|
|
819
|
+
awaitPreparation(cover);
|
|
820
|
+
if ('pending' in cover)
|
|
821
|
+
return cover;
|
|
822
|
+
return 'groups' in rows
|
|
823
|
+
? { groups: rows.groups, coverage: cover.coverage }
|
|
824
|
+
: { documents: rows.documents, coverage: cover.coverage };
|
|
825
|
+
}
|
|
826
|
+
async function indexConversation(conversation) {
|
|
827
|
+
searchIndex.indexMessages(conversation, [], false);
|
|
828
|
+
let after = null;
|
|
829
|
+
for (;;) {
|
|
830
|
+
const batch = await durably((local) => local.messagesAfter(conversation, after));
|
|
831
|
+
if (closed)
|
|
832
|
+
return;
|
|
833
|
+
searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
|
|
834
|
+
if (batch.length === 0)
|
|
835
|
+
return;
|
|
836
|
+
after = batch[batch.length - 1].identity;
|
|
837
|
+
}
|
|
352
838
|
}
|
|
353
|
-
function
|
|
354
|
-
|
|
355
|
-
|
|
839
|
+
function indexNext() {
|
|
840
|
+
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
|
|
841
|
+
if (room !== undefined) {
|
|
842
|
+
roomsToIndex.delete(room);
|
|
843
|
+
return () => prepared.has(room)
|
|
844
|
+
? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
|
|
845
|
+
: undefined;
|
|
846
|
+
}
|
|
847
|
+
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
848
|
+
if (conversation === undefined)
|
|
849
|
+
return null;
|
|
850
|
+
conversationsToIndex.delete(conversation);
|
|
851
|
+
return () => indexConversation(conversation);
|
|
852
|
+
}
|
|
853
|
+
function index(rooms, named) {
|
|
854
|
+
for (const room of rooms)
|
|
855
|
+
roomsToIndex.add(room);
|
|
856
|
+
for (const conversation of named)
|
|
857
|
+
conversationsToIndex.add(conversation);
|
|
858
|
+
if (indexing)
|
|
859
|
+
return;
|
|
860
|
+
indexing = true;
|
|
861
|
+
void (async () => {
|
|
862
|
+
try {
|
|
863
|
+
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
864
|
+
await yielded();
|
|
865
|
+
await unit();
|
|
866
|
+
queueReadiness();
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
finally {
|
|
870
|
+
indexing = false;
|
|
871
|
+
}
|
|
872
|
+
})().catch(() => { });
|
|
356
873
|
}
|
|
357
874
|
function publishChangeSet(set) {
|
|
358
875
|
applyChangeSet(set);
|
|
@@ -369,34 +886,7 @@ export function composeRuntime(options) {
|
|
|
369
886
|
emitPresentation(app);
|
|
370
887
|
conversations.changed(set);
|
|
371
888
|
}
|
|
372
|
-
function stopTimer() {
|
|
373
|
-
if (resetTimer === undefined)
|
|
374
|
-
return;
|
|
375
|
-
clearTimeout(resetTimer);
|
|
376
|
-
resetTimer = undefined;
|
|
377
|
-
}
|
|
378
|
-
function armTimer() {
|
|
379
|
-
stopTimer();
|
|
380
|
-
if (closed || stopping)
|
|
381
|
-
return;
|
|
382
|
-
let earliest = null;
|
|
383
|
-
for (const { submission } of pendings.values()) {
|
|
384
|
-
if (submission.state !== 'reset')
|
|
385
|
-
continue;
|
|
386
|
-
const at = Date.parse(submission.reset);
|
|
387
|
-
if (earliest === null || at < earliest)
|
|
388
|
-
earliest = at;
|
|
389
|
-
}
|
|
390
|
-
if (earliest === null)
|
|
391
|
-
return;
|
|
392
|
-
// A Save House held back goes again only once the reset its answer named has passed.
|
|
393
|
-
resetTimer = setTimeout(() => {
|
|
394
|
-
resetTimer = undefined;
|
|
395
|
-
submitAll();
|
|
396
|
-
}, Math.max(0, earliest - Date.now()));
|
|
397
|
-
}
|
|
398
889
|
async function refuse(reason, provider = null) {
|
|
399
|
-
stopTimer();
|
|
400
890
|
discoveryPaused = true;
|
|
401
891
|
try {
|
|
402
892
|
if (reason === 'revoked-authorization') {
|
|
@@ -404,10 +894,7 @@ export function composeRuntime(options) {
|
|
|
404
894
|
const revoked = { status: 'unavailable', reason: 'revoked' };
|
|
405
895
|
try {
|
|
406
896
|
const local = await openLocal();
|
|
407
|
-
await local.revokeModels([...replicas.values()].map((replica) =>
|
|
408
|
-
record: replica.confirmed.record,
|
|
409
|
-
queue: replica.queue,
|
|
410
|
-
})));
|
|
897
|
+
await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
|
|
411
898
|
}
|
|
412
899
|
catch {
|
|
413
900
|
journal.record('model revocation persistence failed');
|
|
@@ -474,19 +961,28 @@ export function composeRuntime(options) {
|
|
|
474
961
|
},
|
|
475
962
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
476
963
|
roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
|
|
477
|
-
attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
|
|
478
964
|
synchronizationAvailable,
|
|
479
965
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
480
966
|
});
|
|
481
967
|
}
|
|
482
|
-
async function applyDiscovery(
|
|
968
|
+
async function applyDiscovery(found, started) {
|
|
969
|
+
for (const room of found) {
|
|
970
|
+
const fence = fences.get(room.locator);
|
|
971
|
+
if (fence !== undefined && fence.erased && fence.at < started)
|
|
972
|
+
fences.delete(room.locator);
|
|
973
|
+
}
|
|
974
|
+
const listed = found.filter((room) => !fences.has(room.locator));
|
|
483
975
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
484
976
|
for (const locator of listedLocators)
|
|
485
977
|
scopedRefusals.delete(locator);
|
|
486
978
|
discovered.clear();
|
|
487
979
|
for (const room of listed)
|
|
488
980
|
discovered.set(room.locator, room);
|
|
489
|
-
|
|
981
|
+
const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
|
|
982
|
+
if (options.adapter.exhaustiveDiscovery === true)
|
|
983
|
+
await loseRooms(unlisted);
|
|
984
|
+
else
|
|
985
|
+
unlistedKept(unlisted);
|
|
490
986
|
for (const room of listed) {
|
|
491
987
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
492
988
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -497,44 +993,85 @@ export function composeRuntime(options) {
|
|
|
497
993
|
}
|
|
498
994
|
roomsListed = true;
|
|
499
995
|
emitRooms();
|
|
996
|
+
return listed;
|
|
500
997
|
}
|
|
501
998
|
function unlistedHeld(listed) {
|
|
502
|
-
return [
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
pendings.delete(batch.id);
|
|
518
|
-
replicas.delete(locator);
|
|
999
|
+
return [
|
|
1000
|
+
...new Set([
|
|
1001
|
+
...rooms.keys(),
|
|
1002
|
+
...roomDrafts.keys(),
|
|
1003
|
+
...transfers.keys(),
|
|
1004
|
+
...queuesWithoutReplica.keys(),
|
|
1005
|
+
]),
|
|
1006
|
+
].filter((locator) => !listed.has(locator));
|
|
1007
|
+
}
|
|
1008
|
+
function unlistedKept(unlisted) {
|
|
1009
|
+
for (const locator of unlisted) {
|
|
1010
|
+
const held = replicas.get(locator)?.confirmed.record;
|
|
1011
|
+
if (held !== undefined)
|
|
1012
|
+
list(held, false);
|
|
1013
|
+
else
|
|
519
1014
|
rooms.delete(locator);
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
function fence(locators) {
|
|
1018
|
+
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
1019
|
+
const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
|
|
1020
|
+
room: true,
|
|
1021
|
+
queue: true,
|
|
1022
|
+
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
1023
|
+
}));
|
|
1024
|
+
for (const locator of fresh) {
|
|
1025
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
1026
|
+
lastFenced.set(locator, ++fencings);
|
|
1027
|
+
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
1028
|
+
pendings.delete(batch.id);
|
|
1029
|
+
replicas.delete(locator);
|
|
1030
|
+
queuesWithoutReplica.delete(locator);
|
|
1031
|
+
rooms.delete(locator);
|
|
1032
|
+
roomDrafts.delete(locator);
|
|
1033
|
+
transfers.delete(locator);
|
|
1034
|
+
catching.delete(locator);
|
|
1035
|
+
discovered.delete(locator);
|
|
1036
|
+
announced.delete(locator);
|
|
1037
|
+
unreadable.delete(locator);
|
|
1038
|
+
stale.delete(locator);
|
|
1039
|
+
repairable.delete(locator);
|
|
1040
|
+
unindexable.delete(locator);
|
|
1041
|
+
paused.delete(locator);
|
|
1042
|
+
scopedRefusals.delete(locator);
|
|
1043
|
+
sourceRefusals.delete(locator);
|
|
1044
|
+
declarationRefusals.delete(locator);
|
|
1045
|
+
for (const attached of ['collection', 'typed-selection']) {
|
|
1046
|
+
const key = modelKey(locator, attached);
|
|
1047
|
+
models.delete(key);
|
|
1048
|
+
liveModels.delete(key);
|
|
1049
|
+
invalidModels.delete(key);
|
|
534
1050
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
1051
|
+
journal.record(`room lost ${locator}`);
|
|
1052
|
+
}
|
|
1053
|
+
if (fresh.length === 0)
|
|
1054
|
+
return fresh;
|
|
1055
|
+
publishChangeSet(mergeChangeSets(removed));
|
|
1056
|
+
for (const locator of fresh)
|
|
1057
|
+
prepared.delete(locator);
|
|
1058
|
+
searchIndex.forget(fresh);
|
|
1059
|
+
readSide.recover();
|
|
1060
|
+
moveTo({ ...status, ...attention() });
|
|
1061
|
+
return fresh;
|
|
1062
|
+
}
|
|
1063
|
+
async function erased(local, locators) {
|
|
1064
|
+
await local.erase(locators);
|
|
1065
|
+
for (const locator of locators) {
|
|
1066
|
+
const held = fences.get(locator);
|
|
1067
|
+
if (held !== undefined)
|
|
1068
|
+
held.erased = true;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
async function loseRooms(locators) {
|
|
1072
|
+
const fresh = fence(locators);
|
|
1073
|
+
if (fresh.length > 0)
|
|
1074
|
+
await durably((local) => erased(local, fresh));
|
|
538
1075
|
}
|
|
539
1076
|
function restored(key, persisted) {
|
|
540
1077
|
const prior = models.get(key);
|
|
@@ -555,7 +1092,7 @@ export function composeRuntime(options) {
|
|
|
555
1092
|
return model;
|
|
556
1093
|
}
|
|
557
1094
|
async function hydrate(opened) {
|
|
558
|
-
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
|
|
1095
|
+
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
|
|
559
1096
|
opened.all(),
|
|
560
1097
|
opened.queues(),
|
|
561
1098
|
opened.roomDrafts(),
|
|
@@ -567,7 +1104,9 @@ export function composeRuntime(options) {
|
|
|
567
1104
|
opened.capabilities(),
|
|
568
1105
|
opened.member(),
|
|
569
1106
|
opened.transfers(),
|
|
1107
|
+
opened.targets(),
|
|
570
1108
|
]);
|
|
1109
|
+
targets = heldTargets;
|
|
571
1110
|
transfers.clear();
|
|
572
1111
|
for (const record of staged)
|
|
573
1112
|
transfers.set(record.locator, record);
|
|
@@ -589,6 +1128,12 @@ export function composeRuntime(options) {
|
|
|
589
1128
|
}
|
|
590
1129
|
const previousReplicas = new Map(replicas);
|
|
591
1130
|
replicas.clear();
|
|
1131
|
+
queuesWithoutReplica.clear();
|
|
1132
|
+
const replicated = new Set(records.map((record) => record.locator));
|
|
1133
|
+
for (const [locator, queue] of queuedBatches) {
|
|
1134
|
+
if (!replicated.has(locator))
|
|
1135
|
+
queuesWithoutReplica.set(locator, queue);
|
|
1136
|
+
}
|
|
592
1137
|
for (const record of records) {
|
|
593
1138
|
const model = restored(modelKey(record.locator, 'collection'), record.model);
|
|
594
1139
|
const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
|
|
@@ -602,7 +1147,6 @@ export function composeRuntime(options) {
|
|
|
602
1147
|
rooms.set(record.locator, {
|
|
603
1148
|
...existing,
|
|
604
1149
|
roomVersion: record.roomVersion,
|
|
605
|
-
attachments: record.attachments,
|
|
606
1150
|
operations: record.operations,
|
|
607
1151
|
replica: record.replica,
|
|
608
1152
|
model: modelTypes(model),
|
|
@@ -624,6 +1168,7 @@ export function composeRuntime(options) {
|
|
|
624
1168
|
async function readState(opened) {
|
|
625
1169
|
const { lastCompleted, locators, read } = await hydrate(opened);
|
|
626
1170
|
applyChangeSet(read);
|
|
1171
|
+
prepare();
|
|
627
1172
|
if (replicas.size > 0)
|
|
628
1173
|
emitRooms();
|
|
629
1174
|
if (member !== null)
|
|
@@ -634,8 +1179,15 @@ export function composeRuntime(options) {
|
|
|
634
1179
|
return lastCompleted;
|
|
635
1180
|
}
|
|
636
1181
|
async function open() {
|
|
637
|
-
|
|
1182
|
+
if (closed)
|
|
1183
|
+
throw new Error('runtime closed');
|
|
1184
|
+
const opened = await openRuntimeStore(isFenced);
|
|
638
1185
|
store = opened;
|
|
1186
|
+
const interrupted = await opened.fences();
|
|
1187
|
+
for (const locator of interrupted) {
|
|
1188
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
1189
|
+
lastFenced.set(locator, ++fencings);
|
|
1190
|
+
}
|
|
639
1191
|
const kept = options.shell.generation;
|
|
640
1192
|
const storedGeneration = await opened.generation();
|
|
641
1193
|
const loss = detectStorageLoss(storedGeneration, kept);
|
|
@@ -649,6 +1201,8 @@ export function composeRuntime(options) {
|
|
|
649
1201
|
emitStorageLoss();
|
|
650
1202
|
}
|
|
651
1203
|
const lastCompleted = await readState(opened);
|
|
1204
|
+
if (interrupted.length > 0)
|
|
1205
|
+
await erased(opened, interrupted);
|
|
652
1206
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
653
1207
|
return opened;
|
|
654
1208
|
}
|
|
@@ -675,6 +1229,7 @@ export function composeRuntime(options) {
|
|
|
675
1229
|
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
676
1230
|
}
|
|
677
1231
|
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
1232
|
+
const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
678
1233
|
const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
|
|
679
1234
|
const kept = queue.filter((batch) => !withdrawn.includes(batch));
|
|
680
1235
|
const unqueued = withdrawn.map((batch) => ({
|
|
@@ -700,30 +1255,48 @@ export function composeRuntime(options) {
|
|
|
700
1255
|
});
|
|
701
1256
|
}
|
|
702
1257
|
}
|
|
703
|
-
await local.
|
|
704
|
-
|
|
705
|
-
previous: before?.confirmed.record ?? null,
|
|
706
|
-
...(settling.stored === undefined ? {} : { stored: settling.stored }),
|
|
707
|
-
queue: kept,
|
|
708
|
-
previousQueue: before?.queue ?? [],
|
|
709
|
-
roomDrafts: [...written.values()],
|
|
710
|
-
});
|
|
711
|
-
for (const batch of before?.queue ?? []) {
|
|
1258
|
+
await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
|
|
1259
|
+
for (const batch of previousQueue) {
|
|
712
1260
|
if (!kept.includes(batch))
|
|
713
1261
|
pendings.delete(batch.id);
|
|
714
1262
|
}
|
|
715
1263
|
for (const record of written.values())
|
|
716
1264
|
rememberRoomDraft(record);
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
1265
|
+
try {
|
|
1266
|
+
await local.write({
|
|
1267
|
+
next: confirmed.record,
|
|
1268
|
+
previous: before?.confirmed.record ?? null,
|
|
1269
|
+
...(settling.stored === undefined ? {} : { stored: settling.stored }),
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
catch (error) {
|
|
1273
|
+
const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
|
|
1274
|
+
if (held !== null)
|
|
1275
|
+
replicas.set(locator, held);
|
|
1276
|
+
else if (kept.length > 0)
|
|
1277
|
+
queuesWithoutReplica.set(locator, kept);
|
|
1278
|
+
else
|
|
1279
|
+
queuesWithoutReplica.delete(locator);
|
|
1280
|
+
publishChangeSet(changeSet(locator, before ?? null, held, {
|
|
1281
|
+
queue: kept.length !== previousQueue.length,
|
|
1282
|
+
drafts: [...written.keys()],
|
|
1283
|
+
}));
|
|
1284
|
+
moveTo({ ...status, ...attention() });
|
|
1285
|
+
throw error;
|
|
1286
|
+
}
|
|
1287
|
+
return () => {
|
|
1288
|
+
queuesWithoutReplica.delete(locator);
|
|
1289
|
+
for (const { batch, returned } of unqueued) {
|
|
1290
|
+
const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
|
|
1291
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
1292
|
+
}
|
|
1293
|
+
replicas.set(locator, replicaOf(confirmed, kept, before));
|
|
1294
|
+
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
1295
|
+
room: settling.room,
|
|
1296
|
+
queue: kept.length !== previousQueue.length,
|
|
1297
|
+
drafts: [...written.keys()],
|
|
1298
|
+
});
|
|
1299
|
+
};
|
|
727
1300
|
}
|
|
728
1301
|
function adoptModels(locator, record) {
|
|
729
1302
|
for (const [attached, model] of [
|
|
@@ -756,11 +1329,16 @@ export function composeRuntime(options) {
|
|
|
756
1329
|
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
757
1330
|
};
|
|
758
1331
|
}
|
|
759
|
-
function publish(local, room, confirmed, landing
|
|
1332
|
+
function publish(local, room, confirmed, landing, live) {
|
|
760
1333
|
const { base, stored } = landing;
|
|
761
1334
|
return exclusive(async () => {
|
|
762
|
-
if (
|
|
1335
|
+
if (stored !== undefined) {
|
|
1336
|
+
await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
|
|
1337
|
+
}
|
|
1338
|
+
if (!live()) {
|
|
1339
|
+
caughtUpTo(room.locator);
|
|
763
1340
|
return true;
|
|
1341
|
+
}
|
|
764
1342
|
const prior = replicas.get(room.locator);
|
|
765
1343
|
if (base !== undefined &&
|
|
766
1344
|
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
@@ -768,30 +1346,48 @@ export function composeRuntime(options) {
|
|
|
768
1346
|
return false;
|
|
769
1347
|
}
|
|
770
1348
|
adoptModels(room.locator, confirmed.record);
|
|
771
|
-
const queue = prior?.queue ?? [];
|
|
1349
|
+
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
772
1350
|
const roomVersion = confirmed.record.roomVersion;
|
|
773
1351
|
const accepted = queue.filter((batch) => {
|
|
774
1352
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
775
1353
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
776
1354
|
});
|
|
777
|
-
|
|
778
|
-
|
|
1355
|
+
let settled;
|
|
1356
|
+
try {
|
|
1357
|
+
settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
|
|
1358
|
+
}
|
|
1359
|
+
catch (error) {
|
|
1360
|
+
if (!exceeded(error))
|
|
1361
|
+
throw error;
|
|
1362
|
+
await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
|
|
1363
|
+
caughtUpTo(room.locator);
|
|
1364
|
+
return true;
|
|
1365
|
+
}
|
|
1366
|
+
const set = settled();
|
|
1367
|
+
if (stored !== undefined) {
|
|
779
1368
|
transfers.delete(room.locator);
|
|
1369
|
+
catching.delete(room.locator);
|
|
1370
|
+
}
|
|
780
1371
|
for (const batch of accepted)
|
|
781
1372
|
pendings.delete(batch.id);
|
|
782
1373
|
stale.delete(room.locator);
|
|
1374
|
+
unindexable.delete(room.locator);
|
|
783
1375
|
if (unreadable.delete(room.locator))
|
|
784
1376
|
scopedRefusals.delete(room.locator);
|
|
785
1377
|
sourceRefusals.delete(room.locator);
|
|
786
1378
|
declarationRefusals.delete(room.locator);
|
|
787
1379
|
list(room, true);
|
|
1380
|
+
if (prior === undefined)
|
|
1381
|
+
prepared.add(room.locator);
|
|
788
1382
|
publishChangeSet(set);
|
|
789
1383
|
moveTo({ ...status, ...attention() });
|
|
1384
|
+
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
790
1385
|
return true;
|
|
791
1386
|
});
|
|
792
1387
|
}
|
|
793
1388
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
794
1389
|
const local = await openLocal();
|
|
1390
|
+
await referenced(locator);
|
|
795
1391
|
const replica = replicas.get(locator);
|
|
796
1392
|
const room = rooms.get(locator);
|
|
797
1393
|
if (replica === undefined || room === undefined) {
|
|
@@ -810,7 +1406,24 @@ export function composeRuntime(options) {
|
|
|
810
1406
|
if (changed.length > 0)
|
|
811
1407
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
812
1408
|
const parts = saved.map(({ path, parts }) => changesOf(path, parts));
|
|
813
|
-
const
|
|
1409
|
+
const changes = parts.flatMap(({ changes }) => changes);
|
|
1410
|
+
const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
|
|
1411
|
+
if (requeue.kind === 'refused')
|
|
1412
|
+
return { outcome: 'refused', ...requeue.refusal };
|
|
1413
|
+
const foreign = [...requeue.withdrawn.keys()].some((origin) => {
|
|
1414
|
+
const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
|
|
1415
|
+
return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
|
|
1416
|
+
});
|
|
1417
|
+
if (foreign) {
|
|
1418
|
+
return {
|
|
1419
|
+
outcome: 'refused',
|
|
1420
|
+
code: 'unknown-revision',
|
|
1421
|
+
retryable: false,
|
|
1422
|
+
file: [...requeue.queued].at(-1),
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
const withdrawing = requeue.withdrawn.size > 0;
|
|
1426
|
+
const input = {
|
|
814
1427
|
local: localSaves,
|
|
815
1428
|
baseRoomVersion: replica.confirmed.record.roomVersion,
|
|
816
1429
|
composition: compositionFor(replica.confirmed.record),
|
|
@@ -819,28 +1432,91 @@ export function composeRuntime(options) {
|
|
|
819
1432
|
providerLimits: limits,
|
|
820
1433
|
handle: room.handle,
|
|
821
1434
|
visible: replica.visible,
|
|
822
|
-
changes
|
|
1435
|
+
changes,
|
|
823
1436
|
bases: parts.flatMap(({ bases }) => bases),
|
|
824
1437
|
writtenBy,
|
|
1438
|
+
clean: {
|
|
1439
|
+
text: replica.confirmed.record.documents,
|
|
1440
|
+
binaries: replica.confirmed.record.binaries,
|
|
1441
|
+
composition: compositionOf(replica.confirmed.record),
|
|
1442
|
+
},
|
|
1443
|
+
};
|
|
1444
|
+
const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
|
|
1445
|
+
if (checked?.kind === 'refused')
|
|
1446
|
+
return { outcome: 'refused', ...checked.refusal };
|
|
1447
|
+
const owners = [...requeue.owners].flatMap((origin) => {
|
|
1448
|
+
const batch = requeue.withdrawn.get(origin);
|
|
1449
|
+
return batch === undefined || batch === null ? [] : [{ origin, batch }];
|
|
825
1450
|
});
|
|
826
|
-
if (
|
|
1451
|
+
if (owners.length > 1) {
|
|
1452
|
+
return {
|
|
1453
|
+
outcome: 'refused',
|
|
1454
|
+
code: 'unknown-revision',
|
|
1455
|
+
retryable: false,
|
|
1456
|
+
file: [...requeue.queued].at(-1),
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
const built = !withdrawing
|
|
1460
|
+
? buildBatch(input)
|
|
1461
|
+
: requeue.changes.length === 0
|
|
1462
|
+
? null
|
|
1463
|
+
: buildBatch({
|
|
1464
|
+
...input,
|
|
1465
|
+
visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
|
|
1466
|
+
changes: requeue.changes,
|
|
1467
|
+
});
|
|
1468
|
+
if (built?.kind === 'refused')
|
|
827
1469
|
return { outcome: 'refused', ...built.refusal };
|
|
828
|
-
const
|
|
1470
|
+
const [owner] = owners;
|
|
1471
|
+
const join = built === null || owner === undefined
|
|
1472
|
+
? null
|
|
1473
|
+
: joined(requeue.queue, owner.batch, built.batch);
|
|
1474
|
+
if (join?.kind === 'refused')
|
|
1475
|
+
return { outcome: 'refused', ...join.refusal };
|
|
1476
|
+
const withdrawn = new Map(requeue.withdrawn);
|
|
1477
|
+
if (join !== null && owner !== undefined)
|
|
1478
|
+
withdrawn.set(owner.origin, join.batch);
|
|
1479
|
+
const drafted = new Map();
|
|
1480
|
+
for (const [origin, replaced] of withdrawn) {
|
|
1481
|
+
if (replaced === null)
|
|
1482
|
+
continue;
|
|
1483
|
+
const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
|
|
1484
|
+
drafted.set(replaced.id, replaced === join?.batch
|
|
1485
|
+
? [
|
|
1486
|
+
...carried.map((held) => joinedDraft(held, saved)),
|
|
1487
|
+
...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
|
|
1488
|
+
]
|
|
1489
|
+
: carried);
|
|
1490
|
+
}
|
|
1491
|
+
if (built !== null && join === null)
|
|
1492
|
+
drafted.set(built.batch.id, saved);
|
|
1493
|
+
const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
|
|
1494
|
+
for (const replaced of withdrawn.values()) {
|
|
1495
|
+
if (replaced === null || !batches.includes(replaced))
|
|
1496
|
+
continue;
|
|
1497
|
+
const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
|
|
1498
|
+
if (limited !== null)
|
|
1499
|
+
return { outcome: 'refused', ...limited };
|
|
1500
|
+
}
|
|
829
1501
|
const consumed = saved.map(({ path }) => path);
|
|
830
1502
|
try {
|
|
831
|
-
await local.
|
|
832
|
-
|
|
833
|
-
previous: replica.confirmed.record,
|
|
1503
|
+
await local.keepAuthored({
|
|
1504
|
+
locator,
|
|
834
1505
|
queue: batches,
|
|
835
|
-
|
|
836
|
-
saved,
|
|
1506
|
+
saved: drafted,
|
|
837
1507
|
consumedRoomDrafts: consumed,
|
|
838
1508
|
});
|
|
839
1509
|
}
|
|
840
1510
|
catch {
|
|
841
1511
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
842
1512
|
}
|
|
843
|
-
|
|
1513
|
+
if (fences.has(locator))
|
|
1514
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1515
|
+
for (const origin of withdrawn.keys())
|
|
1516
|
+
pendings.delete(origin);
|
|
1517
|
+
for (const [id, drafts] of drafted) {
|
|
1518
|
+
pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
|
|
1519
|
+
}
|
|
844
1520
|
if (!localSaves)
|
|
845
1521
|
recorded = false;
|
|
846
1522
|
for (const path of consumed)
|
|
@@ -852,7 +1528,10 @@ export function composeRuntime(options) {
|
|
|
852
1528
|
}));
|
|
853
1529
|
moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
|
|
854
1530
|
submitLater(locator);
|
|
855
|
-
|
|
1531
|
+
const identifier = join?.batch.id ??
|
|
1532
|
+
built?.batch.id ??
|
|
1533
|
+
[...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
|
|
1534
|
+
return { outcome: 'saved', identifier };
|
|
856
1535
|
}
|
|
857
1536
|
async function mark(local, locator, identifier, submission, lost) {
|
|
858
1537
|
const state = pendings.get(identifier);
|
|
@@ -862,6 +1541,8 @@ export function composeRuntime(options) {
|
|
|
862
1541
|
return;
|
|
863
1542
|
}
|
|
864
1543
|
await local.submit(locator, identifier, submission, lost);
|
|
1544
|
+
if (fences.has(locator))
|
|
1545
|
+
return;
|
|
865
1546
|
pendings.set(identifier, { ...state, submission, lost });
|
|
866
1547
|
const replica = replicas.get(locator);
|
|
867
1548
|
if (replica !== undefined)
|
|
@@ -872,6 +1553,8 @@ export function composeRuntime(options) {
|
|
|
872
1553
|
if (!continuing())
|
|
873
1554
|
throw new Error('connection refused');
|
|
874
1555
|
await local.keepUpload(locator, identifier, upload);
|
|
1556
|
+
if (fences.has(locator))
|
|
1557
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
875
1558
|
pendings.set(identifier, { ...pendings.get(identifier), upload });
|
|
876
1559
|
if (!continuing())
|
|
877
1560
|
throw new Error('connection refused');
|
|
@@ -887,6 +1570,7 @@ export function composeRuntime(options) {
|
|
|
887
1570
|
}
|
|
888
1571
|
if (closed || authorization === undefined || refusal !== null)
|
|
889
1572
|
return UNCERTAIN;
|
|
1573
|
+
const since = fencings;
|
|
890
1574
|
const transfer = ['transfer', 'history', locator, path];
|
|
891
1575
|
if (query.revision !== undefined)
|
|
892
1576
|
transfer.push(query.revision);
|
|
@@ -901,6 +1585,8 @@ export function composeRuntime(options) {
|
|
|
901
1585
|
...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
|
|
902
1586
|
});
|
|
903
1587
|
journal.record(`outcome history ${result.kind}`);
|
|
1588
|
+
if (fencedSince(locator, since))
|
|
1589
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
904
1590
|
if (result.kind === 'scoped-refusal') {
|
|
905
1591
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
906
1592
|
if (room !== undefined)
|
|
@@ -915,6 +1601,100 @@ export function composeRuntime(options) {
|
|
|
915
1601
|
return UNCERTAIN;
|
|
916
1602
|
}
|
|
917
1603
|
}
|
|
1604
|
+
function queuedBlob(locator, path, localId) {
|
|
1605
|
+
for (const batch of replicas.get(locator)?.queue ?? []) {
|
|
1606
|
+
const change = batch.changes[queuedFile(batch, path, localId)];
|
|
1607
|
+
if (change?.kind === 'binary')
|
|
1608
|
+
return change.blob;
|
|
1609
|
+
}
|
|
1610
|
+
return null;
|
|
1611
|
+
}
|
|
1612
|
+
function binaryRoom(request) {
|
|
1613
|
+
if (request.kind === 'path')
|
|
1614
|
+
return request.locator;
|
|
1615
|
+
const scope = options.adapter.referrerScope(request.referrer);
|
|
1616
|
+
return scope?.kind === 'room' ? scope.locator : null;
|
|
1617
|
+
}
|
|
1618
|
+
async function readBinary(request) {
|
|
1619
|
+
if (closed || refusal !== null)
|
|
1620
|
+
return UNCERTAIN;
|
|
1621
|
+
try {
|
|
1622
|
+
await ensureAuthorization();
|
|
1623
|
+
}
|
|
1624
|
+
catch {
|
|
1625
|
+
return UNCERTAIN;
|
|
1626
|
+
}
|
|
1627
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1628
|
+
return UNCERTAIN;
|
|
1629
|
+
const since = fencings;
|
|
1630
|
+
journal.record(request.kind === 'path'
|
|
1631
|
+
? `transfer binary ${request.locator} ${request.path}`
|
|
1632
|
+
: `transfer binary ${request.fileRef}`);
|
|
1633
|
+
try {
|
|
1634
|
+
const result = await options.adapter.binary(authorization, request);
|
|
1635
|
+
journal.record(`outcome binary ${result.kind}`);
|
|
1636
|
+
const locator = binaryRoom(request);
|
|
1637
|
+
if (locator !== null && fencedSince(locator, since))
|
|
1638
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
1639
|
+
if (result.kind === 'scoped-refusal') {
|
|
1640
|
+
const room = replicas.get(result.locator)?.confirmed.record;
|
|
1641
|
+
if (room !== undefined)
|
|
1642
|
+
await scoped(result, room, await openLocal());
|
|
1643
|
+
}
|
|
1644
|
+
if (result.kind === 'revoked-authorization')
|
|
1645
|
+
await refuse('revoked-authorization');
|
|
1646
|
+
return binaryAnswer(result);
|
|
1647
|
+
}
|
|
1648
|
+
catch {
|
|
1649
|
+
journal.record('outcome binary transport-uncertainty');
|
|
1650
|
+
return UNCERTAIN;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
async function readBinaryPath(locator, path, revision) {
|
|
1654
|
+
await ready;
|
|
1655
|
+
if (revision !== undefined)
|
|
1656
|
+
return readBinary({ kind: 'path', locator, path, revision });
|
|
1657
|
+
const visible = replicas.get(locator)?.visible;
|
|
1658
|
+
const held = visible?.binaries[path];
|
|
1659
|
+
if (visible === undefined || held === undefined)
|
|
1660
|
+
return absentBinary(path);
|
|
1661
|
+
if (held.state === 'pending') {
|
|
1662
|
+
const blob = queuedBlob(locator, path, held.localId);
|
|
1663
|
+
return blob === null ? absentBinary(path) : { kind: 'binary', blob };
|
|
1664
|
+
}
|
|
1665
|
+
const record = binaryRecord(path, held, visible.revisions[path]);
|
|
1666
|
+
if (record.state !== 'accepted')
|
|
1667
|
+
return absentBinary(path);
|
|
1668
|
+
const replica = replicas.get(locator);
|
|
1669
|
+
const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
|
|
1670
|
+
const holds = (at) => {
|
|
1671
|
+
const kept = replica?.confirmed.record.binaries[at];
|
|
1672
|
+
return (kept?.state === 'accepted' &&
|
|
1673
|
+
kept.fileRef === record.fileRef &&
|
|
1674
|
+
replica?.confirmed.record.revisions[at] === record.revision);
|
|
1675
|
+
};
|
|
1676
|
+
let confirmed = path;
|
|
1677
|
+
for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
|
|
1678
|
+
const change = queued[index];
|
|
1679
|
+
if (change.kind === 'move' && change.to === confirmed)
|
|
1680
|
+
confirmed = change.path;
|
|
1681
|
+
}
|
|
1682
|
+
return readBinary({
|
|
1683
|
+
kind: 'path',
|
|
1684
|
+
locator,
|
|
1685
|
+
path: confirmed,
|
|
1686
|
+
revision: record.revision,
|
|
1687
|
+
fileRef: record.fileRef,
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
async function readBinaryReference(reference) {
|
|
1691
|
+
await ready;
|
|
1692
|
+
return readBinary({
|
|
1693
|
+
kind: 'reference',
|
|
1694
|
+
fileRef: reference.fileRef,
|
|
1695
|
+
referrer: reference.referrer,
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
918
1698
|
function declaredAgents() {
|
|
919
1699
|
for (const capability of capabilities) {
|
|
920
1700
|
if (capability.conversationAgents !== undefined)
|
|
@@ -979,11 +1759,11 @@ export function composeRuntime(options) {
|
|
|
979
1759
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
980
1760
|
!options.adapter.reached(locator, roomVersion, written));
|
|
981
1761
|
}
|
|
982
|
-
async function pullRefresh(authorized, confirmed) {
|
|
983
|
-
if (!
|
|
1762
|
+
async function pullRefresh(authorized, confirmed, live) {
|
|
1763
|
+
if (!live())
|
|
984
1764
|
return unconfirmed;
|
|
985
1765
|
try {
|
|
986
|
-
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held),
|
|
1766
|
+
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
|
|
987
1767
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
988
1768
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
989
1769
|
return result;
|
|
@@ -993,47 +1773,80 @@ export function composeRuntime(options) {
|
|
|
993
1773
|
throw error;
|
|
994
1774
|
}
|
|
995
1775
|
}
|
|
996
|
-
function staging(local) {
|
|
1776
|
+
function staging(local, live) {
|
|
997
1777
|
return {
|
|
998
1778
|
held: (locator) => transfers.get(locator) ?? null,
|
|
999
1779
|
staged: (record) => local.staged(record),
|
|
1000
1780
|
stage: (record, page) => exclusive(async () => {
|
|
1001
|
-
if (!
|
|
1781
|
+
if (!live())
|
|
1782
|
+
return false;
|
|
1783
|
+
const bytes = layerBytes(page);
|
|
1784
|
+
if (!(await admitted(local, record.locator, bytes)))
|
|
1002
1785
|
return false;
|
|
1003
|
-
|
|
1786
|
+
try {
|
|
1787
|
+
await local.stage(record, page);
|
|
1788
|
+
}
|
|
1789
|
+
catch (error) {
|
|
1790
|
+
if (!exceeded(error))
|
|
1791
|
+
throw error;
|
|
1792
|
+
await exceededOn(record.locator, bytes);
|
|
1793
|
+
return false;
|
|
1794
|
+
}
|
|
1004
1795
|
transfers.set(record.locator, record);
|
|
1796
|
+
readSide.recover();
|
|
1005
1797
|
return true;
|
|
1006
1798
|
}),
|
|
1007
1799
|
abandon: (locator) => exclusive(async () => {
|
|
1008
|
-
if (!
|
|
1800
|
+
if (!live())
|
|
1009
1801
|
return;
|
|
1010
1802
|
await local.abandon(locator);
|
|
1011
1803
|
transfers.delete(locator);
|
|
1804
|
+
readSide.recover();
|
|
1012
1805
|
}),
|
|
1013
1806
|
};
|
|
1014
1807
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1808
|
+
function reusable(room) {
|
|
1809
|
+
const record = replicas.get(room.locator)?.confirmed.record;
|
|
1810
|
+
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1811
|
+
return null;
|
|
1812
|
+
return { documents: record.documents, revisions: record.revisions };
|
|
1813
|
+
}
|
|
1814
|
+
async function pullBootstrap(authorized, room, local, live) {
|
|
1815
|
+
if (!live())
|
|
1017
1816
|
return unconfirmed;
|
|
1817
|
+
if (replicas.has(room.locator)) {
|
|
1818
|
+
catching.add(room.locator);
|
|
1819
|
+
readSide.recover();
|
|
1820
|
+
}
|
|
1018
1821
|
try {
|
|
1019
1822
|
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
1020
1823
|
collection: modelOf(room.locator),
|
|
1021
1824
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
1022
|
-
}, (models, held) => remodel(room.locator, models, held),
|
|
1825
|
+
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
1023
1826
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
1024
1827
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1828
|
+
if (result.kind !== 'staged')
|
|
1829
|
+
caughtUpTo(room.locator);
|
|
1025
1830
|
return result;
|
|
1026
1831
|
}
|
|
1027
1832
|
catch (error) {
|
|
1028
1833
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
1834
|
+
caughtUpTo(room.locator);
|
|
1029
1835
|
throw error;
|
|
1030
1836
|
}
|
|
1031
1837
|
}
|
|
1032
|
-
|
|
1838
|
+
function caughtUpTo(locator) {
|
|
1839
|
+
if (catching.delete(locator))
|
|
1840
|
+
readSide.recover();
|
|
1841
|
+
}
|
|
1842
|
+
async function advance(room, local, authorized, live) {
|
|
1843
|
+
await referenced(room.locator);
|
|
1844
|
+
if (!live() || paused.has(room.locator))
|
|
1845
|
+
return unconfirmed;
|
|
1033
1846
|
const held = replicas.get(room.locator);
|
|
1034
1847
|
if (held !== undefined) {
|
|
1035
|
-
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
1036
|
-
if (!
|
|
1848
|
+
const refreshed = await pullRefresh(authorized, held.confirmed, live);
|
|
1849
|
+
if (!live())
|
|
1037
1850
|
return unconfirmed;
|
|
1038
1851
|
if (refreshed.kind === 'still-current') {
|
|
1039
1852
|
stale.delete(room.locator);
|
|
@@ -1047,31 +1860,45 @@ export function composeRuntime(options) {
|
|
|
1047
1860
|
return null;
|
|
1048
1861
|
}
|
|
1049
1862
|
if (refreshed.kind === 'advanced') {
|
|
1050
|
-
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
|
|
1863
|
+
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
|
|
1051
1864
|
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1052
1865
|
const reached = now !== undefined &&
|
|
1053
1866
|
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
1054
|
-
return reached ? null : advance(room, local, authorized);
|
|
1867
|
+
return reached ? null : advance(room, local, authorized, live);
|
|
1055
1868
|
}
|
|
1056
1869
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1057
1870
|
return null;
|
|
1058
1871
|
}
|
|
1059
1872
|
if (refreshed.kind !== 'beyond-horizon')
|
|
1060
1873
|
return refreshed;
|
|
1874
|
+
if (opened !== null) {
|
|
1875
|
+
owed.add(room.locator);
|
|
1876
|
+
readSide.recover();
|
|
1877
|
+
return unconfirmed;
|
|
1878
|
+
}
|
|
1061
1879
|
}
|
|
1062
|
-
const bootstrapped = await pullBootstrap(authorized, room, local);
|
|
1063
|
-
if (!
|
|
1880
|
+
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
1881
|
+
if (!live())
|
|
1064
1882
|
return unconfirmed;
|
|
1065
1883
|
if (bootstrapped.kind !== 'staged')
|
|
1066
1884
|
return bootstrapped;
|
|
1067
|
-
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
1885
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
1068
1886
|
return null;
|
|
1069
1887
|
}
|
|
1070
1888
|
async function withhold(withheld, room, local, progress) {
|
|
1889
|
+
if (withheld.kind === 'unconfirmed')
|
|
1890
|
+
return;
|
|
1071
1891
|
if (withheld.kind === 'refused')
|
|
1072
1892
|
stale.delete(room.locator);
|
|
1073
1893
|
else
|
|
1074
1894
|
stale.add(room.locator);
|
|
1895
|
+
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
1896
|
+
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
1897
|
+
repairable.set(room.locator, reset);
|
|
1898
|
+
repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
|
|
1899
|
+
}
|
|
1900
|
+
else
|
|
1901
|
+
repairable.delete(room.locator);
|
|
1075
1902
|
if (withheld.kind === 'scoped-refusal') {
|
|
1076
1903
|
await scoped(withheld, room, local);
|
|
1077
1904
|
}
|
|
@@ -1107,6 +1934,7 @@ export function composeRuntime(options) {
|
|
|
1107
1934
|
unreadable.add(room.locator);
|
|
1108
1935
|
list(room, false);
|
|
1109
1936
|
emitRooms();
|
|
1937
|
+
moveTo({ ...status, ...attention() });
|
|
1110
1938
|
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
1111
1939
|
}
|
|
1112
1940
|
function leaving(batch, result) {
|
|
@@ -1128,6 +1956,7 @@ export function composeRuntime(options) {
|
|
|
1128
1956
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1129
1957
|
}
|
|
1130
1958
|
async function adoptAccepted(room, local, submitted, accepted) {
|
|
1959
|
+
await referenced(room.locator);
|
|
1131
1960
|
if (!continuing())
|
|
1132
1961
|
return unconfirmed;
|
|
1133
1962
|
const current = replicas.get(room.locator);
|
|
@@ -1138,22 +1967,23 @@ export function composeRuntime(options) {
|
|
|
1138
1967
|
logAccepted(room.locator, submitted, accepted);
|
|
1139
1968
|
const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
1140
1969
|
? { kind: 'staged', confirmed: current.confirmed }
|
|
1141
|
-
: applyReturned(current.confirmed,
|
|
1970
|
+
: applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
1142
1971
|
? accepted.roomVersion
|
|
1143
1972
|
: held, accepted.roomVersion);
|
|
1144
1973
|
if (applied.kind !== 'staged') {
|
|
1145
1974
|
const state = pendings.get(submitted.id);
|
|
1146
1975
|
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1147
|
-
if (state !== undefined)
|
|
1976
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
1148
1977
|
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1149
1978
|
emitQueue(room.locator);
|
|
1150
1979
|
return applied;
|
|
1151
1980
|
}
|
|
1152
|
-
const
|
|
1981
|
+
const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
1153
1982
|
pendings.delete(submitted.id);
|
|
1154
1983
|
unreadable.delete(room.locator);
|
|
1155
1984
|
sourceRefusals.delete(room.locator);
|
|
1156
1985
|
declarationRefusals.delete(room.locator);
|
|
1986
|
+
const set = settled();
|
|
1157
1987
|
list(room, true);
|
|
1158
1988
|
publishChangeSet(set);
|
|
1159
1989
|
moveTo({ ...status, ...attention() });
|
|
@@ -1167,14 +1997,14 @@ export function composeRuntime(options) {
|
|
|
1167
1997
|
refusedSource(room, failed);
|
|
1168
1998
|
return false;
|
|
1169
1999
|
}
|
|
1170
|
-
async function submitRoom(room, local, authorized, progress) {
|
|
1171
|
-
while (continuing() && authorization !== undefined) {
|
|
2000
|
+
async function submitRoom(room, local, authorized, progress, signal) {
|
|
2001
|
+
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
1172
2002
|
const replica = replicas.get(room.locator);
|
|
1173
2003
|
const head = replica?.queue[0];
|
|
1174
2004
|
if (replica === undefined || head === undefined)
|
|
1175
|
-
return;
|
|
2005
|
+
return 'done';
|
|
1176
2006
|
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1177
|
-
return;
|
|
2007
|
+
return 'done';
|
|
1178
2008
|
const state = pendings.get(head.id);
|
|
1179
2009
|
const { submission, lost: earlier } = state ?? {
|
|
1180
2010
|
submission: { state: 'waiting' },
|
|
@@ -1182,24 +2012,30 @@ export function composeRuntime(options) {
|
|
|
1182
2012
|
};
|
|
1183
2013
|
if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
|
|
1184
2014
|
progress.complete = false;
|
|
1185
|
-
return;
|
|
2015
|
+
return { at: Date.parse(submission.reset) };
|
|
1186
2016
|
}
|
|
1187
2017
|
if (writesHeld()) {
|
|
1188
2018
|
progress.complete = false;
|
|
1189
2019
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1190
|
-
return;
|
|
2020
|
+
return 'done';
|
|
1191
2021
|
}
|
|
1192
2022
|
const lost = earlier || submission.state === 'submitted';
|
|
2023
|
+
if (lost && pastHorizon(head.id)) {
|
|
2024
|
+
await discardPending(room, local, signal);
|
|
2025
|
+
return 'done';
|
|
2026
|
+
}
|
|
1193
2027
|
let result;
|
|
1194
2028
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
1195
2029
|
await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
|
|
1196
|
-
if (discoveryPaused)
|
|
1197
|
-
return;
|
|
1198
|
-
if (writesHeld()) {
|
|
2030
|
+
if (discoveryPaused || fences.has(room.locator))
|
|
2031
|
+
return 'done';
|
|
2032
|
+
if (signal.aborted || writesHeld()) {
|
|
1199
2033
|
await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
|
|
2034
|
+
if (signal.aborted)
|
|
2035
|
+
return 'done';
|
|
1200
2036
|
progress.complete = false;
|
|
1201
2037
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1202
|
-
return;
|
|
2038
|
+
return 'done';
|
|
1203
2039
|
}
|
|
1204
2040
|
const opensBefore = openSignals;
|
|
1205
2041
|
const answered = awaitingAnswer(room.locator);
|
|
@@ -1215,32 +2051,36 @@ export function composeRuntime(options) {
|
|
|
1215
2051
|
progress.complete = false;
|
|
1216
2052
|
if (continuing())
|
|
1217
2053
|
progress.reachable = false;
|
|
1218
|
-
return;
|
|
2054
|
+
return 'again';
|
|
2055
|
+
}
|
|
2056
|
+
if (signal.aborted) {
|
|
2057
|
+
answered();
|
|
2058
|
+
return 'done';
|
|
1219
2059
|
}
|
|
1220
2060
|
if (result.kind !== 'accepted' || !continuing())
|
|
1221
2061
|
answered();
|
|
1222
2062
|
if (!continuing())
|
|
1223
|
-
return;
|
|
2063
|
+
return 'done';
|
|
1224
2064
|
if (result.kind === 'transport-uncertainty') {
|
|
1225
2065
|
progress.complete = false;
|
|
1226
2066
|
progress.reachable = false;
|
|
1227
|
-
return;
|
|
2067
|
+
return 'again';
|
|
1228
2068
|
}
|
|
1229
2069
|
if (result.kind === 'room-not-readable') {
|
|
1230
2070
|
progress.complete = false;
|
|
1231
2071
|
unreadable.add(room.locator);
|
|
1232
2072
|
list(room, false);
|
|
1233
2073
|
emitRooms();
|
|
1234
|
-
return;
|
|
2074
|
+
return 'done';
|
|
1235
2075
|
}
|
|
1236
2076
|
if (result.kind === 'revoked-authorization') {
|
|
1237
2077
|
await refuse('revoked-authorization');
|
|
1238
|
-
return;
|
|
2078
|
+
return 'done';
|
|
1239
2079
|
}
|
|
1240
2080
|
if (result.kind === 'scoped-refusal') {
|
|
1241
2081
|
await scoped(result, room, local);
|
|
1242
2082
|
progress.complete = false;
|
|
1243
|
-
return;
|
|
2083
|
+
return 'done';
|
|
1244
2084
|
}
|
|
1245
2085
|
if (result.kind === 'write-halted') {
|
|
1246
2086
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
@@ -1249,16 +2089,24 @@ export function composeRuntime(options) {
|
|
|
1249
2089
|
continue;
|
|
1250
2090
|
haltedRefusal = true;
|
|
1251
2091
|
progress.complete = false;
|
|
1252
|
-
return;
|
|
2092
|
+
return 'done';
|
|
1253
2093
|
}
|
|
1254
2094
|
if (result.kind === 'conflicted' ||
|
|
1255
2095
|
result.kind === 'rejected' ||
|
|
1256
2096
|
result.kind === 'expired' ||
|
|
1257
2097
|
result.kind === 'retryable-refusal') {
|
|
2098
|
+
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
2099
|
+
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
2100
|
+
await discardPending(room, local, signal);
|
|
2101
|
+
return 'done';
|
|
2102
|
+
}
|
|
1258
2103
|
if (result.kind === 'expired') {
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
2104
|
+
await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
|
|
2105
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
2106
|
+
if (withheld !== null) {
|
|
2107
|
+
await withhold(withheld, room, local, progress);
|
|
2108
|
+
return 'done';
|
|
2109
|
+
}
|
|
1262
2110
|
}
|
|
1263
2111
|
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
|
|
1264
2112
|
(result.kind === 'conflicted' && result.roomVersion !== undefined
|
|
@@ -1271,8 +2119,7 @@ export function composeRuntime(options) {
|
|
|
1271
2119
|
const reset = result.reset;
|
|
1272
2120
|
progress.complete = false;
|
|
1273
2121
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1274
|
-
|
|
1275
|
-
return;
|
|
2122
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
1276
2123
|
}
|
|
1277
2124
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
1278
2125
|
const left = leaving(head, result);
|
|
@@ -1283,37 +2130,82 @@ export function composeRuntime(options) {
|
|
|
1283
2130
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
1284
2131
|
return;
|
|
1285
2132
|
}
|
|
1286
|
-
const
|
|
2133
|
+
const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
1287
2134
|
leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
|
|
1288
2135
|
(result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
|
|
1289
2136
|
? { ...left, returned: null }
|
|
1290
2137
|
: left,
|
|
1291
2138
|
});
|
|
1292
|
-
publishChangeSet(
|
|
2139
|
+
publishChangeSet(settled());
|
|
1293
2140
|
moveTo({ ...status, ...attention() });
|
|
1294
2141
|
});
|
|
1295
2142
|
if (result.kind === 'expired')
|
|
1296
2143
|
continue;
|
|
1297
|
-
const withheld = await advance(room, local, authorized);
|
|
1298
|
-
if (withheld !== null)
|
|
1299
|
-
|
|
2144
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
2145
|
+
if (withheld !== null) {
|
|
2146
|
+
await withhold(withheld, room, local, progress);
|
|
2147
|
+
return 'done';
|
|
2148
|
+
}
|
|
1300
2149
|
continue;
|
|
1301
2150
|
}
|
|
1302
2151
|
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1303
2152
|
if (!kept) {
|
|
1304
2153
|
progress.complete = false;
|
|
1305
|
-
return;
|
|
2154
|
+
return 'done';
|
|
1306
2155
|
}
|
|
1307
2156
|
}
|
|
2157
|
+
return 'done';
|
|
2158
|
+
}
|
|
2159
|
+
function pastHorizon(identity) {
|
|
2160
|
+
return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
|
|
2161
|
+
}
|
|
2162
|
+
async function discardPending(room, local, signal) {
|
|
2163
|
+
const { locator } = room;
|
|
2164
|
+
const discarded = await exclusive(async () => {
|
|
2165
|
+
if (fences.has(locator))
|
|
2166
|
+
return false;
|
|
2167
|
+
const before = replicas.get(locator);
|
|
2168
|
+
const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
2169
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
2170
|
+
lastFenced.set(locator, ++fencings);
|
|
2171
|
+
try {
|
|
2172
|
+
await local.discard(locator);
|
|
2173
|
+
}
|
|
2174
|
+
finally {
|
|
2175
|
+
fences.delete(locator);
|
|
2176
|
+
}
|
|
2177
|
+
for (const batch of queue)
|
|
2178
|
+
pendings.delete(batch.id);
|
|
2179
|
+
queuesWithoutReplica.delete(locator);
|
|
2180
|
+
const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
|
|
2181
|
+
if (after !== null)
|
|
2182
|
+
replicas.set(locator, after);
|
|
2183
|
+
journal.record(`room discarded ${locator}`);
|
|
2184
|
+
publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
|
|
2185
|
+
moveTo({ ...status, ...attention() });
|
|
2186
|
+
return true;
|
|
2187
|
+
});
|
|
2188
|
+
if (!discarded)
|
|
2189
|
+
return;
|
|
2190
|
+
const progress = { complete: true, reachable: true };
|
|
2191
|
+
await roomTask(locator, async (live) => {
|
|
2192
|
+
await Promise.race([
|
|
2193
|
+
reload(room, local, progress, () => live() && !signal.aborted),
|
|
2194
|
+
abandonedOn(signal),
|
|
2195
|
+
]);
|
|
2196
|
+
});
|
|
2197
|
+
if (signal.aborted)
|
|
2198
|
+
return;
|
|
2199
|
+
await rest(progress);
|
|
1308
2200
|
}
|
|
1309
2201
|
function awaitingAnswer(locator) {
|
|
1310
2202
|
let release = () => { };
|
|
1311
2203
|
const awaited = new Promise((resolve) => {
|
|
1312
2204
|
release = resolve;
|
|
1313
2205
|
});
|
|
1314
|
-
answering.set(locator, awaited);
|
|
2206
|
+
answering.set(locator, { generation, answered: awaited });
|
|
1315
2207
|
return () => {
|
|
1316
|
-
if (answering.get(locator) === awaited)
|
|
2208
|
+
if (answering.get(locator)?.answered === awaited)
|
|
1317
2209
|
answering.delete(locator);
|
|
1318
2210
|
release();
|
|
1319
2211
|
};
|
|
@@ -1357,11 +2249,7 @@ export function composeRuntime(options) {
|
|
|
1357
2249
|
const read = reclassified.kind === 'deferred'
|
|
1358
2250
|
? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
|
|
1359
2251
|
: reclassified;
|
|
1360
|
-
await local.write({
|
|
1361
|
-
next: record,
|
|
1362
|
-
previous: held.confirmed.record,
|
|
1363
|
-
queue: held.queue,
|
|
1364
|
-
});
|
|
2252
|
+
await local.write({ next: record, previous: held.confirmed.record });
|
|
1365
2253
|
if (read.kind === 'staged') {
|
|
1366
2254
|
replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
|
|
1367
2255
|
if (declarationRefusals.delete(room.locator)) {
|
|
@@ -1429,24 +2317,27 @@ export function composeRuntime(options) {
|
|
|
1429
2317
|
roomVersion: record.roomVersion,
|
|
1430
2318
|
documents: record.documents,
|
|
1431
2319
|
revisions: record.revisions,
|
|
1432
|
-
attachments: record.attachments,
|
|
1433
2320
|
},
|
|
1434
2321
|
];
|
|
1435
2322
|
});
|
|
1436
2323
|
}
|
|
1437
|
-
async function synchronize(listed, local, progress, attempt) {
|
|
1438
|
-
const
|
|
1439
|
-
const
|
|
2324
|
+
async function synchronize(listed, local, progress, attempt, live) {
|
|
2325
|
+
const acquired = listed.filter(acquires);
|
|
2326
|
+
const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
|
|
2327
|
+
if (!live())
|
|
2328
|
+
return;
|
|
2329
|
+
const ready = acquired.filter((_, index) => {
|
|
1440
2330
|
const outcome = prepared[index];
|
|
1441
2331
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1442
2332
|
});
|
|
1443
2333
|
if (attempt === 'opened') {
|
|
1444
|
-
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
2334
|
+
options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
|
|
1445
2335
|
handed = true;
|
|
1446
2336
|
}
|
|
2337
|
+
discovering?.release();
|
|
1447
2338
|
const loaded = await Promise.allSettled(ready
|
|
1448
2339
|
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1449
|
-
.map((room) => loadRoom(room, local, progress)));
|
|
2340
|
+
.map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
|
|
1450
2341
|
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1451
2342
|
if (failed !== undefined)
|
|
1452
2343
|
throw failed.reason;
|
|
@@ -1475,14 +2366,14 @@ export function composeRuntime(options) {
|
|
|
1475
2366
|
}
|
|
1476
2367
|
return true;
|
|
1477
2368
|
}
|
|
1478
|
-
async function caughtUp(room, local, progress) {
|
|
2369
|
+
async function caughtUp(room, local, progress, live) {
|
|
1479
2370
|
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1480
|
-
if (held === undefined || authorization === undefined)
|
|
2371
|
+
if (held === undefined || authorization === undefined || !live())
|
|
1481
2372
|
return;
|
|
1482
2373
|
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
1483
2374
|
if (!behind)
|
|
1484
2375
|
return;
|
|
1485
|
-
const withheld = await advance(room, local, authorization);
|
|
2376
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1486
2377
|
if (withheld !== null)
|
|
1487
2378
|
await withhold(withheld, room, local, progress);
|
|
1488
2379
|
}
|
|
@@ -1492,39 +2383,46 @@ export function composeRuntime(options) {
|
|
|
1492
2383
|
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1493
2384
|
});
|
|
1494
2385
|
}
|
|
1495
|
-
async function loadRoom(room, local, progress) {
|
|
1496
|
-
if (
|
|
2386
|
+
async function loadRoom(room, local, progress, live) {
|
|
2387
|
+
if (!live() || authorization === undefined)
|
|
1497
2388
|
return;
|
|
1498
2389
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1499
|
-
await reload(room, local, progress);
|
|
2390
|
+
await reload(room, local, progress, live);
|
|
1500
2391
|
return;
|
|
1501
2392
|
}
|
|
1502
|
-
const withheld = await advance(room, local, authorization);
|
|
2393
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1503
2394
|
if (withheld !== null) {
|
|
1504
2395
|
await withhold(withheld, room, local, progress);
|
|
1505
2396
|
return;
|
|
1506
2397
|
}
|
|
1507
|
-
await caughtUp(room, local, progress);
|
|
2398
|
+
await caughtUp(room, local, progress, live);
|
|
1508
2399
|
}
|
|
1509
|
-
async function reload(room, local, progress) {
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
2400
|
+
async function reload(room, local, progress, live) {
|
|
2401
|
+
try {
|
|
2402
|
+
if (!live() || authorization === undefined)
|
|
2403
|
+
return;
|
|
2404
|
+
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
2405
|
+
if (!live())
|
|
2406
|
+
return;
|
|
2407
|
+
if (bootstrapped.kind !== 'staged') {
|
|
2408
|
+
await withhold(bootstrapped, room, local, progress);
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
2412
|
+
await caughtUp(room, local, progress, live);
|
|
2413
|
+
}
|
|
2414
|
+
finally {
|
|
2415
|
+
caughtUpTo(room.locator);
|
|
1518
2416
|
}
|
|
1519
|
-
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
1520
|
-
await caughtUp(room, local, progress);
|
|
1521
2417
|
}
|
|
1522
|
-
async function discoverRooms(local, progress, attempt) {
|
|
2418
|
+
async function discoverRooms(local, progress, attempt, live) {
|
|
1523
2419
|
if (closed || authorization === undefined) {
|
|
1524
2420
|
notifyReady();
|
|
1525
2421
|
return;
|
|
1526
2422
|
}
|
|
1527
2423
|
moveTo(transferring(status));
|
|
2424
|
+
discoveries += 1;
|
|
2425
|
+
const started = discoveries;
|
|
1528
2426
|
let result;
|
|
1529
2427
|
try {
|
|
1530
2428
|
result = await options.adapter.discover(authorization);
|
|
@@ -1532,7 +2430,8 @@ export function composeRuntime(options) {
|
|
|
1532
2430
|
catch {
|
|
1533
2431
|
result = { kind: 'transport-uncertainty' };
|
|
1534
2432
|
}
|
|
1535
|
-
if (
|
|
2433
|
+
if (!live()) {
|
|
2434
|
+
progress.complete = false;
|
|
1536
2435
|
notifyReady();
|
|
1537
2436
|
return;
|
|
1538
2437
|
}
|
|
@@ -1590,6 +2489,7 @@ export function composeRuntime(options) {
|
|
|
1590
2489
|
}
|
|
1591
2490
|
const previousCapabilities = capabilities;
|
|
1592
2491
|
capabilities = result.capabilities;
|
|
2492
|
+
outcomeRetention = result.retention.outcome;
|
|
1593
2493
|
notifyReady();
|
|
1594
2494
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1595
2495
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
@@ -1602,8 +2502,7 @@ export function composeRuntime(options) {
|
|
|
1602
2502
|
member = signedIn;
|
|
1603
2503
|
emitMember();
|
|
1604
2504
|
}
|
|
1605
|
-
await applyDiscovery(result.rooms, local);
|
|
1606
|
-
await synchronize(result.rooms, local, progress, attempt);
|
|
2505
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
1607
2506
|
}
|
|
1608
2507
|
async function ensureAuthorization() {
|
|
1609
2508
|
if (authorization !== undefined)
|
|
@@ -1617,20 +2516,50 @@ export function composeRuntime(options) {
|
|
|
1617
2516
|
throw error;
|
|
1618
2517
|
}
|
|
1619
2518
|
}
|
|
1620
|
-
function
|
|
2519
|
+
async function admittedRead(work) {
|
|
2520
|
+
if (roomReads.generation !== generation) {
|
|
2521
|
+
roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
2522
|
+
}
|
|
2523
|
+
const reads = roomReads;
|
|
2524
|
+
while (reads.free === 0)
|
|
2525
|
+
await new Promise((admit) => reads.waiting.push(admit));
|
|
2526
|
+
reads.free -= 1;
|
|
2527
|
+
try {
|
|
2528
|
+
await work();
|
|
2529
|
+
}
|
|
2530
|
+
finally {
|
|
2531
|
+
reads.free += 1;
|
|
2532
|
+
reads.waiting.shift()?.();
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
function roomTask(locator, work) {
|
|
2536
|
+
return task(roomLane(locator), (live) => admittedRead(() => work(live)));
|
|
2537
|
+
}
|
|
2538
|
+
function task(lane, work) {
|
|
1621
2539
|
if (stopping)
|
|
1622
|
-
return
|
|
1623
|
-
|
|
1624
|
-
|
|
2540
|
+
return Promise.resolve();
|
|
2541
|
+
const held = lanes.get(lane);
|
|
2542
|
+
const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
|
|
2543
|
+
const turn = current.generation;
|
|
2544
|
+
const live = () => turn === generation && continuing();
|
|
2545
|
+
const run = current.chain.then(() => work(live)).catch(() => undefined);
|
|
2546
|
+
current.chain = run;
|
|
2547
|
+
lanes.set(lane, current);
|
|
2548
|
+
working.add(run);
|
|
2549
|
+
void run.finally(() => working.delete(run));
|
|
2550
|
+
return run;
|
|
1625
2551
|
}
|
|
1626
2552
|
function sync(attempt) {
|
|
1627
|
-
return task(async () => {
|
|
1628
|
-
if (
|
|
2553
|
+
return task(OPENING_LANE, async (live) => {
|
|
2554
|
+
if (!live()) {
|
|
1629
2555
|
notifyReady();
|
|
1630
2556
|
return;
|
|
1631
2557
|
}
|
|
1632
2558
|
const progress = { complete: true, reachable: true };
|
|
1633
|
-
|
|
2559
|
+
const gate = discover();
|
|
2560
|
+
if (attempt === 'opened')
|
|
2561
|
+
opened = generation;
|
|
2562
|
+
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
1634
2563
|
if (attempt === 'opened')
|
|
1635
2564
|
streamed = progress;
|
|
1636
2565
|
complete = false;
|
|
@@ -1639,12 +2568,12 @@ export function composeRuntime(options) {
|
|
|
1639
2568
|
handed = false;
|
|
1640
2569
|
try {
|
|
1641
2570
|
const local = await openLocal();
|
|
1642
|
-
if (
|
|
2571
|
+
if (!live())
|
|
1643
2572
|
return;
|
|
1644
2573
|
await ensureAuthorization();
|
|
1645
|
-
if (
|
|
2574
|
+
if (!live())
|
|
1646
2575
|
return;
|
|
1647
|
-
await discoverRooms(local, progress, attempt);
|
|
2576
|
+
await discoverRooms(local, progress, attempt, live);
|
|
1648
2577
|
}
|
|
1649
2578
|
catch {
|
|
1650
2579
|
progress.complete = false;
|
|
@@ -1652,17 +2581,21 @@ export function composeRuntime(options) {
|
|
|
1652
2581
|
progress.reachable = false;
|
|
1653
2582
|
}
|
|
1654
2583
|
finally {
|
|
2584
|
+
gate.release();
|
|
1655
2585
|
notifyReady();
|
|
1656
2586
|
}
|
|
2587
|
+
if (!live())
|
|
2588
|
+
return;
|
|
1657
2589
|
if (attempt !== 'opened') {
|
|
1658
|
-
await finishOpening(progress);
|
|
2590
|
+
await finishOpening(progress, live);
|
|
1659
2591
|
return;
|
|
1660
2592
|
}
|
|
1661
2593
|
if (handed || closed || progress.reachable)
|
|
1662
2594
|
return;
|
|
1663
2595
|
underway = null;
|
|
2596
|
+
opened = null;
|
|
1664
2597
|
journal.record('connection reopen');
|
|
1665
|
-
await rest(progress);
|
|
2598
|
+
await rest(progress, live);
|
|
1666
2599
|
options.adapter.roomStream?.reopen();
|
|
1667
2600
|
});
|
|
1668
2601
|
}
|
|
@@ -1683,24 +2616,41 @@ export function composeRuntime(options) {
|
|
|
1683
2616
|
});
|
|
1684
2617
|
}
|
|
1685
2618
|
function applyOpening(expired) {
|
|
1686
|
-
|
|
1687
|
-
|
|
2619
|
+
const progress = underway?.generation === generation ? underway.progress : null;
|
|
2620
|
+
const cycle = progress ?? { complete: true, reachable: true };
|
|
2621
|
+
const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
|
|
2622
|
+
if (opened === generation)
|
|
2623
|
+
opened = null;
|
|
2624
|
+
return task(OPENING_LANE, async (live) => {
|
|
1688
2625
|
underway = null;
|
|
1689
|
-
if (
|
|
2626
|
+
if (!live())
|
|
1690
2627
|
return;
|
|
1691
2628
|
if (progress === null) {
|
|
1692
|
-
await
|
|
2629
|
+
await replacing;
|
|
2630
|
+
await rest(cycle, live);
|
|
1693
2631
|
return;
|
|
1694
2632
|
}
|
|
2633
|
+
await roomsCarried(expired);
|
|
2634
|
+
if (!live())
|
|
2635
|
+
return;
|
|
2636
|
+
const replaced = new Set(owed);
|
|
2637
|
+
owed.clear();
|
|
1695
2638
|
try {
|
|
1696
2639
|
const local = await openLocal();
|
|
1697
2640
|
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
1698
|
-
const reloading = held.filter((room) => expired.includes(room.locator)
|
|
1699
|
-
|
|
2641
|
+
const reloading = held.filter((room) => !expired.includes(room.locator) &&
|
|
2642
|
+
(replaced.has(room.locator) || revoked(room.locator)));
|
|
2643
|
+
for (const room of reloading)
|
|
2644
|
+
catching.add(room.locator);
|
|
2645
|
+
readSide.recover();
|
|
2646
|
+
const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
|
|
2647
|
+
const reloaded = [...(await reloads), ...(await replacing)];
|
|
1700
2648
|
if (progress.reachable) {
|
|
1701
|
-
for (const room of held)
|
|
1702
|
-
if (!reloading.includes(room))
|
|
2649
|
+
for (const room of held) {
|
|
2650
|
+
if (!expired.includes(room.locator) && !reloading.includes(room)) {
|
|
1703
2651
|
await confirmModels(room, local);
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
1704
2654
|
}
|
|
1705
2655
|
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1706
2656
|
if (failed !== undefined)
|
|
@@ -1710,21 +2660,21 @@ export function composeRuntime(options) {
|
|
|
1710
2660
|
progress.complete = false;
|
|
1711
2661
|
progress.reachable = false;
|
|
1712
2662
|
}
|
|
1713
|
-
|
|
2663
|
+
if (live())
|
|
2664
|
+
await finishOpening(progress, live);
|
|
1714
2665
|
});
|
|
1715
2666
|
}
|
|
1716
|
-
async function
|
|
1717
|
-
const progress = { complete: true, reachable: true };
|
|
2667
|
+
async function reloadNow(replaced, progress) {
|
|
1718
2668
|
try {
|
|
1719
2669
|
const local = await openLocal();
|
|
1720
|
-
await Promise.allSettled([...discovered.values()]
|
|
1721
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
1722
|
-
.map((room) => reload(room, local, progress)));
|
|
2670
|
+
return await Promise.allSettled([...discovered.values()]
|
|
2671
|
+
.filter((room) => replicas.has(room.locator) && replaced(room))
|
|
2672
|
+
.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
1723
2673
|
}
|
|
1724
2674
|
catch {
|
|
1725
2675
|
progress.reachable = false;
|
|
2676
|
+
return [];
|
|
1726
2677
|
}
|
|
1727
|
-
await rest(progress);
|
|
1728
2678
|
}
|
|
1729
2679
|
async function confirmModels(room, local) {
|
|
1730
2680
|
for (const attached of ['collection', 'typed-selection']) {
|
|
@@ -1737,38 +2687,94 @@ export function composeRuntime(options) {
|
|
|
1737
2687
|
function submitLater(locator) {
|
|
1738
2688
|
if (localSaves)
|
|
1739
2689
|
return;
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
2690
|
+
void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
|
|
2691
|
+
}
|
|
2692
|
+
async function submitPass(locator, signal) {
|
|
2693
|
+
const room = discovered.get(locator);
|
|
2694
|
+
if (closed || stopping || discoveryPaused || room === undefined)
|
|
2695
|
+
return 'done';
|
|
2696
|
+
const progress = { complete: true, reachable: true };
|
|
2697
|
+
let next;
|
|
2698
|
+
try {
|
|
2699
|
+
await ensureAuthorization();
|
|
2700
|
+
if (authorization === undefined)
|
|
2701
|
+
return 'done';
|
|
2702
|
+
next = await submitRoom(room, await openLocal(), authorization, progress, signal);
|
|
2703
|
+
}
|
|
2704
|
+
catch {
|
|
2705
|
+
progress.reachable = false;
|
|
2706
|
+
next = 'again';
|
|
2707
|
+
}
|
|
2708
|
+
if (signal.aborted)
|
|
2709
|
+
return 'done';
|
|
2710
|
+
await rest(progress);
|
|
2711
|
+
return next === 'again' && !socketOpen ? 'done' : next;
|
|
2712
|
+
}
|
|
2713
|
+
function repairLater(locator, when) {
|
|
2714
|
+
if (!socketOpen || repairing.has(locator))
|
|
1743
2715
|
return;
|
|
2716
|
+
void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
|
|
2717
|
+
}
|
|
2718
|
+
async function repairPass(locator, signal) {
|
|
2719
|
+
const room = discovered.get(locator);
|
|
2720
|
+
if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
|
|
2721
|
+
return 'done';
|
|
2722
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2723
|
+
return 'done';
|
|
2724
|
+
const progress = { complete: true, reachable: true };
|
|
2725
|
+
repairing.add(locator);
|
|
2726
|
+
try {
|
|
2727
|
+
const local = await openLocal();
|
|
2728
|
+
await roomTask(locator, async (live) => {
|
|
2729
|
+
const current = () => live() && !signal.aborted;
|
|
2730
|
+
if (!current() || authorization === undefined)
|
|
2731
|
+
return;
|
|
2732
|
+
const withheld = await Promise.race([
|
|
2733
|
+
advance(room, local, authorization, current),
|
|
2734
|
+
abandonedOn(signal),
|
|
2735
|
+
]);
|
|
2736
|
+
if (withheld !== null && current())
|
|
2737
|
+
await withhold(withheld, room, local, progress);
|
|
2738
|
+
});
|
|
1744
2739
|
}
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
2740
|
+
catch {
|
|
2741
|
+
stale.add(locator);
|
|
2742
|
+
progress.reachable = false;
|
|
2743
|
+
}
|
|
2744
|
+
finally {
|
|
2745
|
+
repairing.delete(locator);
|
|
2746
|
+
}
|
|
2747
|
+
if (signal.aborted)
|
|
2748
|
+
return 'done';
|
|
2749
|
+
await rest(progress);
|
|
2750
|
+
if (!stale.has(locator)) {
|
|
2751
|
+
if ((replicas.get(locator)?.queue.length ?? 0) > 0)
|
|
2752
|
+
submitLater(locator);
|
|
2753
|
+
return 'done';
|
|
2754
|
+
}
|
|
2755
|
+
if (!repairable.has(locator))
|
|
2756
|
+
return 'done';
|
|
2757
|
+
const reset = repairable.get(locator);
|
|
2758
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
2759
|
+
}
|
|
2760
|
+
function resendLater() {
|
|
2761
|
+
if (!socketOpen)
|
|
2762
|
+
return;
|
|
2763
|
+
void recovery.attempt('resend', resendPass, { again: true });
|
|
2764
|
+
}
|
|
2765
|
+
async function resendPass(signal) {
|
|
2766
|
+
if (!socketOpen || closed || stopping || discoveryPaused)
|
|
2767
|
+
return 'done';
|
|
2768
|
+
const reading = { complete: true, reachable: true };
|
|
2769
|
+
try {
|
|
2770
|
+
await conversations.resend(reading);
|
|
2771
|
+
}
|
|
2772
|
+
catch {
|
|
2773
|
+
reading.reachable = false;
|
|
2774
|
+
}
|
|
2775
|
+
if (signal.aborted)
|
|
2776
|
+
return 'done';
|
|
2777
|
+
return reading.reachable && reading.complete ? 'done' : 'again';
|
|
1772
2778
|
}
|
|
1773
2779
|
function submitAll() {
|
|
1774
2780
|
for (const [locator, replica] of replicas) {
|
|
@@ -1776,31 +2782,41 @@ export function composeRuntime(options) {
|
|
|
1776
2782
|
submitLater(locator);
|
|
1777
2783
|
}
|
|
1778
2784
|
}
|
|
1779
|
-
async function finishOpening(progress) {
|
|
2785
|
+
async function finishOpening(progress, live) {
|
|
1780
2786
|
try {
|
|
1781
|
-
if (
|
|
2787
|
+
if (!live()) {
|
|
1782
2788
|
progress.complete = false;
|
|
1783
2789
|
return;
|
|
1784
2790
|
}
|
|
1785
2791
|
submitAll();
|
|
2792
|
+
for (const locator of stale)
|
|
2793
|
+
if (repairable.has(locator))
|
|
2794
|
+
repairLater(locator, {});
|
|
1786
2795
|
await conversations.reconcile();
|
|
1787
2796
|
if (status.providerUnreachable)
|
|
1788
2797
|
conversations.reconnected();
|
|
1789
2798
|
const reading = { complete: true, reachable: true };
|
|
1790
|
-
|
|
1791
|
-
|
|
2799
|
+
if (demand.conversations)
|
|
2800
|
+
await conversations.catchUp(reading);
|
|
2801
|
+
if (progress.reachable && reading.reachable)
|
|
2802
|
+
await conversations.resend(reading);
|
|
2803
|
+
if (!reading.reachable) {
|
|
1792
2804
|
progress.reachable = false;
|
|
2805
|
+
resendLater();
|
|
2806
|
+
}
|
|
1793
2807
|
}
|
|
1794
2808
|
catch {
|
|
1795
2809
|
progress.complete = false;
|
|
1796
2810
|
}
|
|
1797
2811
|
finally {
|
|
1798
|
-
|
|
1799
|
-
|
|
2812
|
+
if (live()) {
|
|
2813
|
+
complete = progress.complete;
|
|
2814
|
+
await rest(progress, live);
|
|
2815
|
+
}
|
|
1800
2816
|
}
|
|
1801
2817
|
}
|
|
1802
|
-
async function rest(progress) {
|
|
1803
|
-
if (
|
|
2818
|
+
async function rest(progress, live = () => !closed) {
|
|
2819
|
+
if (!live())
|
|
1804
2820
|
return;
|
|
1805
2821
|
let lastCompleted = status.lastCompleted;
|
|
1806
2822
|
let synced = complete && stale.size === 0 && !pending();
|
|
@@ -1808,6 +2824,8 @@ export function composeRuntime(options) {
|
|
|
1808
2824
|
const completedAt = new Date().toISOString();
|
|
1809
2825
|
try {
|
|
1810
2826
|
await (await openLocal()).complete(completedAt);
|
|
2827
|
+
if (!live())
|
|
2828
|
+
return;
|
|
1811
2829
|
recorded = true;
|
|
1812
2830
|
lastCompleted = completedAt;
|
|
1813
2831
|
}
|
|
@@ -1822,40 +2840,68 @@ export function composeRuntime(options) {
|
|
|
1822
2840
|
...attention(),
|
|
1823
2841
|
});
|
|
1824
2842
|
}
|
|
2843
|
+
function discover() {
|
|
2844
|
+
let release = () => { };
|
|
2845
|
+
const done = new Promise((resolve) => {
|
|
2846
|
+
release = resolve;
|
|
2847
|
+
});
|
|
2848
|
+
discovering = { generation, done, release };
|
|
2849
|
+
return discovering;
|
|
2850
|
+
}
|
|
1825
2851
|
function applyRoom(locator, roomVersion) {
|
|
1826
2852
|
announced.set(locator, roomVersion);
|
|
1827
|
-
return task(async () => {
|
|
1828
|
-
|
|
1829
|
-
|
|
2853
|
+
return task(roomLane(locator), async (live) => {
|
|
2854
|
+
if (discovering?.generation === generation)
|
|
2855
|
+
await discovering.done;
|
|
2856
|
+
const saving = answering.get(locator);
|
|
2857
|
+
if (saving?.generation === generation)
|
|
2858
|
+
await saving.answered;
|
|
2859
|
+
if (!live() || authorization === undefined)
|
|
1830
2860
|
return;
|
|
1831
2861
|
const room = discovered.get(locator);
|
|
1832
2862
|
const held = replicas.get(locator);
|
|
1833
|
-
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2863
|
+
if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
|
|
1834
2864
|
return;
|
|
1835
2865
|
if (held !== undefined &&
|
|
1836
2866
|
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
1837
2867
|
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
1838
2868
|
return;
|
|
1839
2869
|
}
|
|
1840
|
-
const during = underway;
|
|
2870
|
+
const during = underway?.generation === generation ? underway.progress : null;
|
|
1841
2871
|
const progress = during ?? { complete: true, reachable: true };
|
|
1842
2872
|
if (during === null)
|
|
1843
2873
|
moveTo(transferring(status));
|
|
1844
2874
|
try {
|
|
1845
2875
|
const local = await openLocal();
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
2876
|
+
await admittedRead(async () => {
|
|
2877
|
+
if (!live() || authorization === undefined)
|
|
2878
|
+
return;
|
|
2879
|
+
const withheld = await advance(room, local, authorization, live);
|
|
2880
|
+
if (withheld !== null)
|
|
2881
|
+
await withhold(withheld, room, local, progress);
|
|
2882
|
+
});
|
|
1849
2883
|
}
|
|
1850
2884
|
catch {
|
|
1851
2885
|
stale.add(locator);
|
|
1852
2886
|
}
|
|
1853
2887
|
finally {
|
|
1854
2888
|
if (during === null)
|
|
1855
|
-
await rest(progress);
|
|
2889
|
+
await rest(progress, live);
|
|
1856
2890
|
}
|
|
1857
2891
|
});
|
|
1858
2892
|
}
|
|
2893
|
+
function roomsCarried(replacing) {
|
|
2894
|
+
const replaced = new Set(replacing.map(roomLane));
|
|
2895
|
+
return Promise.all([...lanes]
|
|
2896
|
+
.filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
|
|
2897
|
+
.map(([, held]) => held.chain));
|
|
2898
|
+
}
|
|
2899
|
+
const receipts = {
|
|
2900
|
+
heads: async () => (await openLocal()).receivedHeads(),
|
|
2901
|
+
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
2902
|
+
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
2903
|
+
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
2904
|
+
};
|
|
1859
2905
|
function observeStream() {
|
|
1860
2906
|
const stream = options.adapter.roomStream;
|
|
1861
2907
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -1865,6 +2911,9 @@ export function composeRuntime(options) {
|
|
|
1865
2911
|
return;
|
|
1866
2912
|
if (event.kind === 'opened') {
|
|
1867
2913
|
socketOpen = true;
|
|
2914
|
+
generation += 1;
|
|
2915
|
+
conversations.reopened();
|
|
2916
|
+
recovery.resume();
|
|
1868
2917
|
void sync('opened');
|
|
1869
2918
|
}
|
|
1870
2919
|
else if (event.kind === 'closed')
|
|
@@ -1873,7 +2922,7 @@ export function composeRuntime(options) {
|
|
|
1873
2922
|
void applyOpening(event.expired);
|
|
1874
2923
|
else
|
|
1875
2924
|
void applyRoom(event.locator, event.roomVersion);
|
|
1876
|
-
});
|
|
2925
|
+
}, receipts);
|
|
1877
2926
|
}
|
|
1878
2927
|
function writesHeld() {
|
|
1879
2928
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -1888,6 +2937,7 @@ export function composeRuntime(options) {
|
|
|
1888
2937
|
haltedRefusal = false;
|
|
1889
2938
|
if (mode !== serviceMode) {
|
|
1890
2939
|
serviceMode = mode;
|
|
2940
|
+
queueReadiness();
|
|
1891
2941
|
journal.record(`service mode ${mode}`);
|
|
1892
2942
|
for (const listener of serviceModeListeners)
|
|
1893
2943
|
listener(mode);
|
|
@@ -1900,6 +2950,8 @@ export function composeRuntime(options) {
|
|
|
1900
2950
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
1901
2951
|
return;
|
|
1902
2952
|
withdrawConversations = stream.observe((event) => {
|
|
2953
|
+
if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
|
|
2954
|
+
queueReadiness();
|
|
1903
2955
|
if (conversations.receive(event)) {
|
|
1904
2956
|
void ready.then(scheduleCurrency);
|
|
1905
2957
|
return;
|
|
@@ -1911,17 +2963,18 @@ export function composeRuntime(options) {
|
|
|
1911
2963
|
});
|
|
1912
2964
|
}
|
|
1913
2965
|
function scheduleCurrency() {
|
|
1914
|
-
if (currencyQueued || closed || stopping)
|
|
2966
|
+
if (currencyQueued === generation || closed || stopping)
|
|
1915
2967
|
return;
|
|
1916
|
-
currencyQueued =
|
|
1917
|
-
void task(runCurrency);
|
|
2968
|
+
currencyQueued = generation;
|
|
2969
|
+
void task(CONVERSATION_LANE, runCurrency);
|
|
1918
2970
|
}
|
|
1919
|
-
async function runCurrency() {
|
|
1920
|
-
currencyQueued
|
|
1921
|
-
|
|
2971
|
+
async function runCurrency(live) {
|
|
2972
|
+
if (currencyQueued === generation)
|
|
2973
|
+
currencyQueued = null;
|
|
2974
|
+
if (!live())
|
|
1922
2975
|
return;
|
|
1923
2976
|
await conversations.reconcile();
|
|
1924
|
-
if (conversations.current()) {
|
|
2977
|
+
if (conversations.current() || !demand.conversations) {
|
|
1925
2978
|
show();
|
|
1926
2979
|
return;
|
|
1927
2980
|
}
|
|
@@ -1933,19 +2986,24 @@ export function composeRuntime(options) {
|
|
|
1933
2986
|
catch {
|
|
1934
2987
|
progress.reachable = false;
|
|
1935
2988
|
}
|
|
1936
|
-
await rest(progress);
|
|
2989
|
+
await rest(progress, live);
|
|
1937
2990
|
}
|
|
1938
2991
|
function close() {
|
|
1939
2992
|
if (closed)
|
|
1940
2993
|
return;
|
|
1941
2994
|
closed = true;
|
|
2995
|
+
if (narrowing !== null)
|
|
2996
|
+
clearTimeout(narrowing);
|
|
2997
|
+
if (rechecking !== null)
|
|
2998
|
+
clearInterval(rechecking);
|
|
2999
|
+
readinessWatches.clear();
|
|
1942
3000
|
withdrawStream?.();
|
|
1943
3001
|
withdrawStream = undefined;
|
|
1944
3002
|
withdrawConversations?.();
|
|
1945
3003
|
withdrawConversations = undefined;
|
|
1946
3004
|
withdrawServiceMode?.();
|
|
1947
3005
|
conversations.close();
|
|
1948
|
-
|
|
3006
|
+
void recovery.retire();
|
|
1949
3007
|
store?.close();
|
|
1950
3008
|
void opening?.then((opened) => {
|
|
1951
3009
|
opened.close();
|
|
@@ -1963,9 +3021,8 @@ export function composeRuntime(options) {
|
|
|
1963
3021
|
}
|
|
1964
3022
|
async function stop() {
|
|
1965
3023
|
stopping = true;
|
|
1966
|
-
|
|
1967
|
-
await
|
|
1968
|
-
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
3024
|
+
await recovery.retire();
|
|
3025
|
+
await Promise.all(working);
|
|
1969
3026
|
await mutations;
|
|
1970
3027
|
close();
|
|
1971
3028
|
}
|
|
@@ -1987,17 +3044,54 @@ export function composeRuntime(options) {
|
|
|
1987
3044
|
return planOf(input, roomFacts(reach), planReach(reach));
|
|
1988
3045
|
}
|
|
1989
3046
|
function watchPlan(plan, deliver) {
|
|
1990
|
-
|
|
1991
|
-
|
|
3047
|
+
return readSide.watch(plan, identityOf(plan), () => {
|
|
3048
|
+
const cover = covered(plan);
|
|
3049
|
+
awaitPreparation(cover);
|
|
3050
|
+
return cover;
|
|
3051
|
+
}, deliver);
|
|
1992
3052
|
}
|
|
1993
3053
|
async function searched(input, reach) {
|
|
1994
3054
|
const plan = searchPlanOf(input);
|
|
1995
|
-
const
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
3055
|
+
const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
|
|
3056
|
+
for (;;) {
|
|
3057
|
+
const cover = covered(filter);
|
|
3058
|
+
awaitPreparation(cover);
|
|
3059
|
+
const pending = 'pending' in cover ? [...cover.pending] : [];
|
|
3060
|
+
const covering = 'pending' in cover ? [] : cover.coverage.rooms;
|
|
3061
|
+
const unindexed = covering
|
|
3062
|
+
.filter(({ room }) => !searchIndex.rooms().has(room))
|
|
3063
|
+
.map(({ room }) => room);
|
|
3064
|
+
for (const room of unindexed)
|
|
3065
|
+
pending.push({ room, awaits: 'index' });
|
|
3066
|
+
const held = conversations.searchable();
|
|
3067
|
+
const named = reach !== null && !reach.conversations
|
|
3068
|
+
? []
|
|
3069
|
+
: (plan.conversations ?? held).filter((one) => held.includes(one) && conversations.acquired(one));
|
|
3070
|
+
const waiting = named.filter((one) => !searchIndex.conversations().has(one));
|
|
3071
|
+
for (const conversation of waiting)
|
|
3072
|
+
pending.push({ conversation, awaits: 'index' });
|
|
3073
|
+
if (unindexed.length > 0 || waiting.length > 0)
|
|
3074
|
+
index(unindexed, waiting);
|
|
3075
|
+
if (pending.length > 0 || 'pending' in cover)
|
|
3076
|
+
return { pending };
|
|
3077
|
+
const seen = searchIndex.changes();
|
|
3078
|
+
await searchIndex.caughtUp();
|
|
3079
|
+
const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(filter);
|
|
3080
|
+
if (selected !== null && 'pending' in selected)
|
|
3081
|
+
return selected;
|
|
3082
|
+
if (searchIndex.changes() !== seen ||
|
|
3083
|
+
JSON.stringify(covered(filter)) !== JSON.stringify(cover))
|
|
3084
|
+
continue;
|
|
3085
|
+
return {
|
|
3086
|
+
...searchIndex.search(plan, selected === null ? null : selected.documents, named),
|
|
3087
|
+
coverage: {
|
|
3088
|
+
rooms: covering,
|
|
3089
|
+
conversations: named,
|
|
3090
|
+
index: searchIndex.generation(),
|
|
3091
|
+
overlay: cover.coverage.overlay,
|
|
3092
|
+
},
|
|
3093
|
+
};
|
|
3094
|
+
}
|
|
2001
3095
|
}
|
|
2002
3096
|
const watch = (topic, input, deliver) => {
|
|
2003
3097
|
if (topic === 'room-drafts') {
|
|
@@ -2022,6 +3116,8 @@ export function composeRuntime(options) {
|
|
|
2022
3116
|
}
|
|
2023
3117
|
if (topic === 'conversations')
|
|
2024
3118
|
return conversations.watchList(deliver);
|
|
3119
|
+
if (topic === 'agents')
|
|
3120
|
+
return conversations.watchAgents(deliver);
|
|
2025
3121
|
if (topic === 'presentation') {
|
|
2026
3122
|
const listener = {
|
|
2027
3123
|
app: input[0],
|
|
@@ -2041,6 +3137,9 @@ export function composeRuntime(options) {
|
|
|
2041
3137
|
presentationListeners.delete(listener);
|
|
2042
3138
|
};
|
|
2043
3139
|
}
|
|
3140
|
+
if (topic === 'readiness') {
|
|
3141
|
+
return watchReadiness({ target: '', code: 'stored', capacity: null }, null, deliver);
|
|
3142
|
+
}
|
|
2044
3143
|
if (topic === 'rooms') {
|
|
2045
3144
|
roomListeners.add(deliver);
|
|
2046
3145
|
deliver(listed());
|
|
@@ -2135,11 +3234,14 @@ export function composeRuntime(options) {
|
|
|
2135
3234
|
if ((current?.revision ?? null) !== revision) {
|
|
2136
3235
|
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2137
3236
|
}
|
|
3237
|
+
const file = parts.file === undefined
|
|
3238
|
+
? undefined
|
|
3239
|
+
: await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
|
|
2138
3240
|
const record = {
|
|
2139
3241
|
locator: room,
|
|
2140
3242
|
path,
|
|
2141
3243
|
revision: roomDraftRevision(),
|
|
2142
|
-
parts: held(current?.parts, parts),
|
|
3244
|
+
parts: held(current?.parts, parts, file),
|
|
2143
3245
|
returned: current?.returned ?? null,
|
|
2144
3246
|
};
|
|
2145
3247
|
await local.keepRoomDraft(record);
|
|
@@ -2188,6 +3290,10 @@ export function composeRuntime(options) {
|
|
|
2188
3290
|
history(locator, path, query) {
|
|
2189
3291
|
return readHistory(locator, path, query);
|
|
2190
3292
|
},
|
|
3293
|
+
binaryFile: {
|
|
3294
|
+
read: readBinaryPath,
|
|
3295
|
+
readReference: readBinaryReference,
|
|
3296
|
+
},
|
|
2191
3297
|
call(name, input) {
|
|
2192
3298
|
return invokeAction(name, input);
|
|
2193
3299
|
},
|
|
@@ -2200,30 +3306,55 @@ export function composeRuntime(options) {
|
|
|
2200
3306
|
return journal.export();
|
|
2201
3307
|
},
|
|
2202
3308
|
persistence() {
|
|
2203
|
-
return
|
|
3309
|
+
return persistence === 'granted';
|
|
3310
|
+
},
|
|
3311
|
+
async retry() {
|
|
3312
|
+
journal.record('retry');
|
|
3313
|
+
unindexable.clear();
|
|
3314
|
+
queueReadiness();
|
|
3315
|
+
resume();
|
|
2204
3316
|
},
|
|
2205
3317
|
observe: observing(watch),
|
|
2206
3318
|
async reauthorize() {
|
|
2207
3319
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2208
3320
|
return;
|
|
2209
3321
|
journal.record('connection reauthorize');
|
|
2210
|
-
await
|
|
3322
|
+
await Promise.all([...lanes.values()]
|
|
3323
|
+
.filter((held) => held.generation === generation)
|
|
3324
|
+
.map((held) => held.chain));
|
|
2211
3325
|
await mutations;
|
|
2212
3326
|
authorization = undefined;
|
|
2213
3327
|
discoveryPaused = false;
|
|
2214
3328
|
if (options.adapter.roomStream === undefined)
|
|
2215
3329
|
await sync('unstreamed');
|
|
2216
3330
|
else
|
|
2217
|
-
await task(ensureAuthorization);
|
|
3331
|
+
await task(OPENING_LANE, ensureAuthorization);
|
|
2218
3332
|
},
|
|
2219
3333
|
};
|
|
2220
3334
|
const reachable = {
|
|
2221
3335
|
md,
|
|
2222
3336
|
watch,
|
|
3337
|
+
referrerScope: (referrer) => options.adapter.referrerScope(referrer),
|
|
2223
3338
|
plan: planned,
|
|
2224
3339
|
query: (plan) => rowsQuery(plan),
|
|
2225
3340
|
watchPlan,
|
|
2226
3341
|
search: searched,
|
|
3342
|
+
searching,
|
|
3343
|
+
watchReadiness,
|
|
3344
|
+
};
|
|
3345
|
+
return {
|
|
3346
|
+
md,
|
|
3347
|
+
stop,
|
|
3348
|
+
lose: loseRooms,
|
|
3349
|
+
watch,
|
|
3350
|
+
acquire,
|
|
3351
|
+
persisted(answer) {
|
|
3352
|
+
if (answer === persistence)
|
|
3353
|
+
return;
|
|
3354
|
+
persistence = answer;
|
|
3355
|
+
journal.record(`persistence ${answer}`);
|
|
3356
|
+
queueReadiness();
|
|
3357
|
+
},
|
|
3358
|
+
within: (reach, port) => narrowed(reachable, reach, port),
|
|
2227
3359
|
};
|
|
2228
|
-
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2229
3360
|
}
|