@agentshouse/mdruntime 0.1.0-c69ca0e865e0b → 0.1.0-c6fd2cbbff187
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 +275 -275
- 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 +50 -9
- package/dist/adapter.d.ts.map +1 -1
- package/dist/bootstrap.d.ts +3 -2
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +26 -4
- package/dist/change-set.d.ts +2 -0
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +36 -10
- package/dist/chat/acts.d.ts +3 -4
- package/dist/chat/acts.d.ts.map +1 -1
- package/dist/chat/acts.js +10 -10
- package/dist/chat/earlier.d.ts +10 -0
- package/dist/chat/earlier.d.ts.map +1 -0
- package/dist/chat/earlier.js +54 -0
- package/dist/chat/kit.d.ts +2 -1
- package/dist/chat/kit.d.ts.map +1 -1
- package/dist/chat/kit.js +3 -3
- package/dist/chat/list.d.ts +2 -1
- package/dist/chat/list.d.ts.map +1 -1
- package/dist/chat/list.js +42 -5
- package/dist/chat/message.d.ts.map +1 -1
- package/dist/chat/message.js +2 -2
- package/dist/chat/messages.d.ts +1 -0
- package/dist/chat/messages.d.ts.map +1 -1
- package/dist/chat/messages.js +4 -0
- package/dist/chat/provider.d.ts +1 -1
- package/dist/chat/provider.d.ts.map +1 -1
- package/dist/chat/provider.js +59 -69
- package/dist/chat/question.d.ts +1 -1
- package/dist/chat/question.d.ts.map +1 -1
- package/dist/chat/question.js +7 -18
- package/dist/chat/ready.d.ts +1 -0
- package/dist/chat/ready.d.ts.map +1 -1
- package/dist/chat/ready.js +2 -2
- package/dist/chat/style.js +1 -1
- package/dist/client.d.ts +14 -8
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +17 -6
- 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 +27 -8
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +1567 -362
- package/dist/conversation.d.ts +19 -7
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +438 -152
- package/dist/durability.d.ts +7 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/entry.d.ts +2 -2
- package/dist/entry.d.ts.map +1 -1
- package/dist/entry.js +3 -3
- package/dist/fence.d.ts +6 -0
- package/dist/fence.d.ts.map +1 -0
- package/dist/fence.js +10 -0
- package/dist/host.d.ts +17 -2
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +90 -9
- package/dist/identity.d.ts +2 -0
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +6 -0
- package/dist/index.d.ts +9 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/memory.d.ts +9 -2
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +36 -7
- 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 +7 -2
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +29 -12
- package/dist/outcome.d.ts +12 -2
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +4 -0
- package/dist/overlay.d.ts +2 -0
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +58 -55
- package/dist/query.d.ts +56 -4
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +36 -7
- package/dist/reach.d.ts +6 -3
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +20 -5
- package/dist/react.d.ts +7 -4
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +6 -4
- package/dist/read-side.d.ts +13 -6
- package/dist/read-side.d.ts.map +1 -1
- package/dist/read-side.js +419 -92
- package/dist/readiness.d.ts +53 -0
- package/dist/readiness.d.ts.map +1 -0
- package/dist/readiness.js +33 -0
- package/dist/reads.d.ts +11 -0
- package/dist/reads.d.ts.map +1 -0
- package/dist/reads.js +98 -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 +4 -4
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +17 -10
- package/dist/replica.d.ts +6 -2
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +76 -23
- package/dist/room-draft.d.ts +13 -1
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +2 -2
- package/dist/rows.d.ts +6 -13
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +10 -13
- package/dist/save.d.ts +6 -0
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +53 -14
- package/dist/search.d.ts +49 -6
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +192 -47
- package/dist/seed.d.ts +16 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +75 -0
- package/dist/shared.d.ts +11 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +368 -0
- package/dist/status.d.ts +4 -0
- package/dist/status.d.ts.map +1 -1
- package/dist/store.d.ts +56 -11
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +311 -84
- package/dist/turn.d.ts +7 -0
- package/dist/turn.d.ts.map +1 -0
- package/dist/turn.js +45 -0
- package/dist/windows.d.ts +19 -0
- package/dist/windows.d.ts.map +1 -0
- package/dist/windows.js +83 -0
- package/dist/wire.d.ts +144 -32
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +50 -31
- package/package.json +2 -2
package/dist/connection.js
CHANGED
|
@@ -1,30 +1,48 @@
|
|
|
1
1
|
import { attachedModel } from './attached-model.js';
|
|
2
2
|
import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
|
|
3
|
-
import { observing } from './observation.js';
|
|
4
|
-
import { filterOf, identityOf, planOf, } from './query.js';
|
|
3
|
+
import { observing, whole } from './observation.js';
|
|
4
|
+
import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
|
|
5
5
|
import { createReadSide } from './read-side.js';
|
|
6
|
+
import { seededResult } from './seed.js';
|
|
6
7
|
import { markdownPath, roomOfKey } from './rows.js';
|
|
7
8
|
import { createSearchIndex, searchPlanOf, } from './search.js';
|
|
9
|
+
import { capped, MAXIMUM_WINDOW } from './windows.js';
|
|
8
10
|
import { bootstrapRoom } from './bootstrap.js';
|
|
9
|
-
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
11
|
+
import { changeSet, entering, mergeChangeSets } from './change-set.js';
|
|
10
12
|
import { createConversationStore, } from './conversation.js';
|
|
11
13
|
import { queuedFile } from './change.js';
|
|
12
|
-
import { validRoot } from './collection.js';
|
|
14
|
+
import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
|
|
13
15
|
import { runtimeError } from './failure.js';
|
|
14
16
|
import { createLog } from './log.js';
|
|
15
17
|
import { binaryRecord } from './binary.js';
|
|
16
|
-
import { absentBinary, binaryAnswer, calledResult, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
|
|
18
|
+
import { absentBinary, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
|
|
19
|
+
import { createRecovery } from './recovery.js';
|
|
20
|
+
import { doorReads } from './reads.js';
|
|
17
21
|
import { refreshRoom } from './refresh.js';
|
|
18
|
-
import { buildBatch, joined, limitedIn, requeued } from './save.js';
|
|
19
|
-
import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership,
|
|
22
|
+
import { buildBatch, joined, limitedIn, queuedDocuments, requeued, } from './save.js';
|
|
23
|
+
import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, standing, } from './replica.js';
|
|
20
24
|
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
21
25
|
import { same, saving, STARTING, transferring, } from './status.js';
|
|
22
26
|
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
23
|
-
import {
|
|
27
|
+
import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
|
|
28
|
+
import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
|
|
29
|
+
import { FencedMap } from './fence.js';
|
|
30
|
+
import { tally } from './shared.js';
|
|
31
|
+
import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
|
|
32
|
+
import { identityTime, roomDraftRevision } from './identity.js';
|
|
24
33
|
import { beneath, normalize } from './paths.js';
|
|
25
34
|
import { openRuntimeStore, } from './store.js';
|
|
26
35
|
import { unconfirmed } from './transfer.js';
|
|
27
36
|
import { governs, holds, narrowed, planReach, } from './reach.js';
|
|
37
|
+
function roomsAlone(plan) {
|
|
38
|
+
return (plan.reach === null &&
|
|
39
|
+
plan.root.types === null &&
|
|
40
|
+
plan.root.where === null &&
|
|
41
|
+
plan.root.pathPrefix === null &&
|
|
42
|
+
plan.paths === null &&
|
|
43
|
+
plan.hop === null &&
|
|
44
|
+
plan.backlinks === null);
|
|
45
|
+
}
|
|
28
46
|
function within(root, path) {
|
|
29
47
|
return root === '' || beneath(root, path);
|
|
30
48
|
}
|
|
@@ -38,25 +56,68 @@ function declaredSelection(room) {
|
|
|
38
56
|
root !== '' &&
|
|
39
57
|
selection.roots.every((other, at) => at === index || !within(other, root)));
|
|
40
58
|
}
|
|
59
|
+
const OPENING_LANE = 'opening';
|
|
60
|
+
const CONVERSATION_LANE = 'conversations';
|
|
61
|
+
const ROOM_READS = 256;
|
|
62
|
+
const ROOM_ATTEMPTS = 16;
|
|
63
|
+
function operationOf(input) {
|
|
64
|
+
const operation = input?.operation_id;
|
|
65
|
+
return typeof operation === 'string' ? operation : null;
|
|
66
|
+
}
|
|
67
|
+
const outside = new Map();
|
|
68
|
+
function outsideOf(roots) {
|
|
69
|
+
const key = JSON.stringify(roots);
|
|
70
|
+
const held = outside.get(key);
|
|
71
|
+
if (held !== undefined)
|
|
72
|
+
return held;
|
|
73
|
+
const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
|
|
74
|
+
const made = {
|
|
75
|
+
documents: (path) => (markdownPath(path) ? counted(path) : 0),
|
|
76
|
+
binaries: counted,
|
|
77
|
+
};
|
|
78
|
+
outside.set(key, made);
|
|
79
|
+
return made;
|
|
80
|
+
}
|
|
81
|
+
function abandonedOn(signal) {
|
|
82
|
+
return new Promise((resolve) => {
|
|
83
|
+
if (signal.aborted)
|
|
84
|
+
resolve(null);
|
|
85
|
+
else
|
|
86
|
+
signal.addEventListener('abort', () => resolve(null), { once: true });
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function roomLane(locator) {
|
|
90
|
+
return `room ${locator}`;
|
|
91
|
+
}
|
|
41
92
|
export function composeRuntime(options) {
|
|
42
93
|
const localSaves = options.adapter.localSaves === true;
|
|
43
94
|
let authorization = options.authorization;
|
|
44
95
|
let opening;
|
|
45
96
|
let store;
|
|
46
|
-
const
|
|
47
|
-
const
|
|
97
|
+
const fences = new Map();
|
|
98
|
+
const isFenced = (locator) => fences.has(locator);
|
|
99
|
+
let fencings = 0;
|
|
100
|
+
const lastFenced = new Map();
|
|
101
|
+
const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
|
|
102
|
+
let discoveries = 0;
|
|
103
|
+
const replicas = new FencedMap(isFenced);
|
|
104
|
+
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
48
105
|
const pendings = new Map();
|
|
49
|
-
const
|
|
106
|
+
const seeds = new Map();
|
|
107
|
+
const seedWatchers = new Map();
|
|
108
|
+
const roomDrafts = new FencedMap(isFenced);
|
|
50
109
|
const presentations = new Map();
|
|
51
110
|
let limits = [];
|
|
52
111
|
let capabilities = [];
|
|
53
|
-
const rooms = new
|
|
112
|
+
const rooms = new FencedMap(isFenced);
|
|
54
113
|
const counted = new Map();
|
|
55
114
|
const unreadable = new Set();
|
|
56
|
-
const
|
|
57
|
-
const
|
|
115
|
+
const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
|
|
116
|
+
const models = new FencedMap(isModelFenced);
|
|
117
|
+
const liveModels = new FencedMap(isModelFenced);
|
|
58
118
|
const invalidModels = new Set();
|
|
59
119
|
const discovered = new Map();
|
|
120
|
+
const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
|
|
60
121
|
let roomsListed = false;
|
|
61
122
|
const scopedRefusals = new Map();
|
|
62
123
|
const sourceRefusals = new Map();
|
|
@@ -84,7 +145,6 @@ export function composeRuntime(options) {
|
|
|
84
145
|
let status = STARTING;
|
|
85
146
|
let shown = STARTING;
|
|
86
147
|
let discoveryPaused = false;
|
|
87
|
-
let resetTimer;
|
|
88
148
|
let closed = false;
|
|
89
149
|
let stopping = false;
|
|
90
150
|
let withdrawStream;
|
|
@@ -92,12 +152,22 @@ export function composeRuntime(options) {
|
|
|
92
152
|
let underway = null;
|
|
93
153
|
let streamed = null;
|
|
94
154
|
let handed = false;
|
|
95
|
-
const
|
|
155
|
+
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
156
|
+
const repairing = new Set();
|
|
157
|
+
const calling = new Set();
|
|
158
|
+
const repairable = new Map();
|
|
159
|
+
let outcomeRetention = null;
|
|
96
160
|
const answering = new Map();
|
|
97
161
|
let complete = false;
|
|
98
162
|
let recorded = false;
|
|
99
163
|
const stale = new Set();
|
|
100
|
-
let
|
|
164
|
+
let generation = 0;
|
|
165
|
+
let discovering = null;
|
|
166
|
+
let opened = null;
|
|
167
|
+
const owed = new Set();
|
|
168
|
+
const lanes = new Map();
|
|
169
|
+
let roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
170
|
+
const working = new Set();
|
|
101
171
|
let mutations = Promise.resolve();
|
|
102
172
|
const roomListeners = new Set();
|
|
103
173
|
const refusalListeners = new Set();
|
|
@@ -108,7 +178,7 @@ export function composeRuntime(options) {
|
|
|
108
178
|
const presentationListeners = new Set();
|
|
109
179
|
const readSide = createReadSide();
|
|
110
180
|
let withdrawConversations;
|
|
111
|
-
let currencyQueued =
|
|
181
|
+
let currencyQueued = null;
|
|
112
182
|
let socketOpen = false;
|
|
113
183
|
let serviceMode = 'open';
|
|
114
184
|
let haltedRefusal = false;
|
|
@@ -116,24 +186,41 @@ export function composeRuntime(options) {
|
|
|
116
186
|
const serviceModeListeners = new Set();
|
|
117
187
|
let member = null;
|
|
118
188
|
const memberListeners = new Set();
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
const
|
|
189
|
+
const prepared = new Set();
|
|
190
|
+
const projecting = new Set();
|
|
191
|
+
const unpreparedDemand = new Set();
|
|
192
|
+
const unindexable = new Set();
|
|
193
|
+
let preparing = false;
|
|
194
|
+
let demand = WHOLE_DEMAND;
|
|
195
|
+
let narrowing = null;
|
|
196
|
+
let targets = {};
|
|
197
|
+
let persistence = options.shell.persistence;
|
|
198
|
+
let capacity = null;
|
|
199
|
+
const paused = new Set();
|
|
200
|
+
let rechecking = null;
|
|
201
|
+
const readinessWatches = new Set();
|
|
202
|
+
let readinessQueued = false;
|
|
203
|
+
const catching = new Set();
|
|
204
|
+
const transfers = new FencedMap(isFenced);
|
|
122
205
|
const conversations = createConversationStore({
|
|
123
206
|
agents: declaredAgents,
|
|
124
|
-
documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
|
|
207
|
+
documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
|
|
125
208
|
call: (name, input) => invokeAction(name, input),
|
|
126
209
|
durably,
|
|
127
210
|
stream: () => options.adapter.conversations,
|
|
128
211
|
publish: publishChangeSet,
|
|
212
|
+
online: () => !status.providerUnreachable && refusal === null,
|
|
129
213
|
closed: () => closed,
|
|
214
|
+
unanswered: resendLater,
|
|
215
|
+
expired: (operation) => pastHorizon(operation),
|
|
130
216
|
});
|
|
131
217
|
const searchIndex = createSearchIndex({
|
|
132
|
-
|
|
218
|
+
room: (room) => readSide.room(room),
|
|
133
219
|
document: (room, path) => readSide.document(room, path),
|
|
134
|
-
messages: () => conversations.messages(),
|
|
135
|
-
message: (conversation, identity) => conversations.message(conversation, identity),
|
|
136
220
|
});
|
|
221
|
+
const roomsToIndex = new Set();
|
|
222
|
+
const conversationsToIndex = new Set();
|
|
223
|
+
let indexing = false;
|
|
137
224
|
const journal = createLog();
|
|
138
225
|
let notifyReady = () => { };
|
|
139
226
|
const ready = new Promise((resolve) => {
|
|
@@ -188,6 +275,15 @@ export function composeRuntime(options) {
|
|
|
188
275
|
const value = presentations.get(app);
|
|
189
276
|
return value === undefined ? null : structuredClone(value);
|
|
190
277
|
}
|
|
278
|
+
function keepPresentation(app, next) {
|
|
279
|
+
return exclusive(async () => {
|
|
280
|
+
const local = await openLocal();
|
|
281
|
+
const stored = next(presentations.get(app));
|
|
282
|
+
await local.present({ app, value: stored });
|
|
283
|
+
presentations.set(app, stored);
|
|
284
|
+
publishChangeSet(changeSet('', null, null, { presentation: app }));
|
|
285
|
+
});
|
|
286
|
+
}
|
|
191
287
|
function emitPresentation(app) {
|
|
192
288
|
if (closed)
|
|
193
289
|
return;
|
|
@@ -212,9 +308,16 @@ export function composeRuntime(options) {
|
|
|
212
308
|
returned: structuredClone(record.returned),
|
|
213
309
|
});
|
|
214
310
|
}
|
|
311
|
+
for (const [room, refusal] of sourceRefusals)
|
|
312
|
+
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
215
313
|
attentionRequired.sort((left, right) => {
|
|
216
314
|
if (left.room !== right.room)
|
|
217
315
|
return left.room < right.room ? -1 : 1;
|
|
316
|
+
if ('refusal' in left || 'refusal' in right) {
|
|
317
|
+
if ('refusal' in left && 'refusal' in right)
|
|
318
|
+
return 0;
|
|
319
|
+
return 'refusal' in left ? -1 : 1;
|
|
320
|
+
}
|
|
218
321
|
if (left.path !== right.path)
|
|
219
322
|
return left.path < right.path ? -1 : 1;
|
|
220
323
|
return 0;
|
|
@@ -233,11 +336,9 @@ export function composeRuntime(options) {
|
|
|
233
336
|
if (replica === undefined)
|
|
234
337
|
return 0;
|
|
235
338
|
const roots = replica.confirmed.record.typedSelection?.roots ?? [];
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
];
|
|
240
|
-
return paths.filter((path) => !roots.some((root) => beneath(root, path))).length;
|
|
339
|
+
const weighed = outsideOf(roots);
|
|
340
|
+
return (tally(replica.visible.documents, weighed.documents) +
|
|
341
|
+
tally(replica.visible.binaries, weighed.binaries));
|
|
241
342
|
}
|
|
242
343
|
function listed() {
|
|
243
344
|
return structuredClone([...rooms.values()].map((room) => {
|
|
@@ -264,15 +365,19 @@ export function composeRuntime(options) {
|
|
|
264
365
|
return { listed: roomsListed, rooms: held };
|
|
265
366
|
}
|
|
266
367
|
function emitRooms() {
|
|
368
|
+
readSide.recover();
|
|
369
|
+
queueReadiness();
|
|
267
370
|
const value = listed();
|
|
268
371
|
for (const listener of roomListeners)
|
|
269
372
|
listener(value);
|
|
270
373
|
}
|
|
271
374
|
function emitRefusal() {
|
|
375
|
+
queueReadiness();
|
|
272
376
|
for (const listener of refusalListeners)
|
|
273
377
|
listener(refusal);
|
|
274
378
|
}
|
|
275
379
|
function emitStorageLoss() {
|
|
380
|
+
queueReadiness();
|
|
276
381
|
for (const listener of storageLossListeners)
|
|
277
382
|
listener(storageLoss);
|
|
278
383
|
}
|
|
@@ -289,7 +394,8 @@ export function composeRuntime(options) {
|
|
|
289
394
|
function show() {
|
|
290
395
|
if (closed)
|
|
291
396
|
return;
|
|
292
|
-
|
|
397
|
+
queueReadiness();
|
|
398
|
+
const next = status.state === 'synced' && demand.conversations && !conversations.current()
|
|
293
399
|
? { ...status, state: 'syncing' }
|
|
294
400
|
: status;
|
|
295
401
|
if (same(shown, next))
|
|
@@ -318,42 +424,517 @@ export function composeRuntime(options) {
|
|
|
318
424
|
}
|
|
319
425
|
}
|
|
320
426
|
function applyChangeSet(set) {
|
|
321
|
-
const held =
|
|
322
|
-
|
|
323
|
-
|
|
427
|
+
const held = {
|
|
428
|
+
...set,
|
|
429
|
+
documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
|
|
430
|
+
};
|
|
324
431
|
readSide.apply(held);
|
|
325
432
|
searchIndex.changed(held);
|
|
326
433
|
}
|
|
327
|
-
function
|
|
328
|
-
|
|
434
|
+
async function referenced(locator) {
|
|
435
|
+
for (;;) {
|
|
436
|
+
const record = replicas.get(locator)?.confirmed.record;
|
|
437
|
+
if (record === undefined || closed)
|
|
438
|
+
return;
|
|
439
|
+
const held = { text: record.documents, binaries: record.binaries };
|
|
440
|
+
const composition = compositionOf(record);
|
|
441
|
+
if (referencesIndexed(held, composition))
|
|
442
|
+
return;
|
|
443
|
+
await indexReferrers(held, composition);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
async function prepareReplica(locator) {
|
|
447
|
+
await referenced(locator);
|
|
448
|
+
const replica = replicas.get(locator);
|
|
449
|
+
if (replica === undefined || closed || prepared.has(locator))
|
|
329
450
|
return;
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
451
|
+
const visible = replica.visible;
|
|
452
|
+
const paths = [
|
|
453
|
+
...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
|
|
454
|
+
];
|
|
455
|
+
const entered = [];
|
|
456
|
+
projecting.add(locator);
|
|
457
|
+
try {
|
|
458
|
+
await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
|
|
459
|
+
const current = replicas.get(locator);
|
|
460
|
+
if (current === undefined || closed)
|
|
461
|
+
return;
|
|
462
|
+
const set = entering(locator, current, chunk);
|
|
463
|
+
readSide.apply(set);
|
|
464
|
+
entered.push(set);
|
|
465
|
+
});
|
|
466
|
+
if (!replicas.has(locator) || closed)
|
|
467
|
+
return;
|
|
468
|
+
prepared.add(locator);
|
|
469
|
+
}
|
|
470
|
+
finally {
|
|
471
|
+
projecting.delete(locator);
|
|
340
472
|
}
|
|
341
|
-
|
|
473
|
+
conversations.changed(mergeChangeSets(entered));
|
|
474
|
+
readSide.recover();
|
|
475
|
+
queueReadiness();
|
|
476
|
+
}
|
|
477
|
+
function unprepared() {
|
|
478
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
|
|
479
|
+
}
|
|
480
|
+
function unbuilt(locator, error) {
|
|
481
|
+
unindexable.add(locator);
|
|
482
|
+
journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
|
|
483
|
+
queueReadiness();
|
|
484
|
+
}
|
|
485
|
+
function prepare(locators = []) {
|
|
486
|
+
for (const locator of locators)
|
|
487
|
+
unpreparedDemand.add(locator);
|
|
488
|
+
if (preparing)
|
|
342
489
|
return;
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
490
|
+
preparing = true;
|
|
491
|
+
void (async () => {
|
|
492
|
+
try {
|
|
493
|
+
for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
|
|
494
|
+
await yielded();
|
|
495
|
+
if (closed)
|
|
496
|
+
return;
|
|
497
|
+
unpreparedDemand.delete(waiting);
|
|
498
|
+
await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
finally {
|
|
502
|
+
preparing = false;
|
|
503
|
+
}
|
|
504
|
+
})();
|
|
346
505
|
}
|
|
347
506
|
function reachedRooms(plan) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
507
|
+
const reach = plan.reach;
|
|
508
|
+
const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
509
|
+
const named = plan.rooms ??
|
|
510
|
+
(reached === null
|
|
511
|
+
? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
|
|
512
|
+
: [...reached]);
|
|
513
|
+
return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
|
|
514
|
+
}
|
|
515
|
+
function awaited(locator) {
|
|
516
|
+
return (!roomsListed ||
|
|
517
|
+
(rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
|
|
518
|
+
}
|
|
519
|
+
function covered(plan) {
|
|
520
|
+
const locators = reachedRooms(plan);
|
|
521
|
+
if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
|
|
522
|
+
return { pending: [] };
|
|
523
|
+
}
|
|
524
|
+
const pending = [];
|
|
525
|
+
const coverage = [];
|
|
526
|
+
const overlay = [];
|
|
527
|
+
for (const locator of locators) {
|
|
528
|
+
const replica = replicas.get(locator);
|
|
529
|
+
if (replica === undefined) {
|
|
530
|
+
if (awaited(locator))
|
|
531
|
+
pending.push({ room: locator, awaits: 'acquisition' });
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (!prepared.has(locator)) {
|
|
535
|
+
pending.push({ room: locator, awaits: 'preparation' });
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
coverage.push({
|
|
539
|
+
room: locator,
|
|
540
|
+
generation: replica.confirmed.record.transfer,
|
|
541
|
+
current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
|
|
542
|
+
});
|
|
543
|
+
for (const batch of replica.queue)
|
|
544
|
+
overlay.push(batch.id);
|
|
545
|
+
}
|
|
546
|
+
if (pending.length > 0)
|
|
547
|
+
return { pending };
|
|
548
|
+
return { coverage: { rooms: coverage, indexes: [], overlay } };
|
|
549
|
+
}
|
|
550
|
+
function awaitPreparation(cover) {
|
|
551
|
+
if (!('pending' in cover))
|
|
552
|
+
return;
|
|
553
|
+
const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
|
|
554
|
+
if (waiting.length > 0)
|
|
555
|
+
prepare(waiting.map(({ room }) => room));
|
|
556
|
+
}
|
|
557
|
+
function acquires(room) {
|
|
558
|
+
return (demanded(demand, room) ||
|
|
559
|
+
(replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
|
|
560
|
+
queuesWithoutReplica.has(room.locator));
|
|
561
|
+
}
|
|
562
|
+
function acquiredLocators() {
|
|
563
|
+
return [...discovered.values()]
|
|
564
|
+
.filter(acquires)
|
|
565
|
+
.map((room) => room.locator)
|
|
566
|
+
.sort();
|
|
567
|
+
}
|
|
568
|
+
function heldLocators(acquired) {
|
|
569
|
+
return demand.whole ? acquired : [...new Set([...acquired, ...demand.rooms])].sort();
|
|
570
|
+
}
|
|
571
|
+
function heldDemand(acquired) {
|
|
572
|
+
if (demand.whole)
|
|
351
573
|
return null;
|
|
352
|
-
return
|
|
574
|
+
return {
|
|
575
|
+
rooms: heldLocators(acquired.map((room) => room.locator)),
|
|
576
|
+
conversations: demand.conversations,
|
|
577
|
+
};
|
|
353
578
|
}
|
|
354
|
-
function
|
|
355
|
-
|
|
356
|
-
|
|
579
|
+
function applyDemand(next) {
|
|
580
|
+
const before = heldLocators(acquiredLocators());
|
|
581
|
+
const { whole, conversations: conversed } = demand;
|
|
582
|
+
demand = next;
|
|
583
|
+
const after = heldLocators(acquiredLocators());
|
|
584
|
+
queueReadiness();
|
|
585
|
+
if (!conversed && next.conversations)
|
|
586
|
+
scheduleCurrency();
|
|
587
|
+
if (JSON.stringify(before) === JSON.stringify(after) &&
|
|
588
|
+
whole === next.whole &&
|
|
589
|
+
conversed === next.conversations)
|
|
590
|
+
return;
|
|
591
|
+
journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
|
|
592
|
+
if (options.adapter.roomStream === undefined)
|
|
593
|
+
void sync('unstreamed');
|
|
594
|
+
else if (socketOpen)
|
|
595
|
+
options.adapter.roomStream.reopen();
|
|
596
|
+
}
|
|
597
|
+
function acquire(next) {
|
|
598
|
+
if (closed)
|
|
599
|
+
return;
|
|
600
|
+
if (narrowing !== null)
|
|
601
|
+
clearTimeout(narrowing);
|
|
602
|
+
narrowing = null;
|
|
603
|
+
const widening = wider(next, demand);
|
|
604
|
+
if (!widening && !narrower(next, demand))
|
|
605
|
+
return;
|
|
606
|
+
if (widening || !roomsListed) {
|
|
607
|
+
applyDemand(next);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
// A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
|
|
611
|
+
narrowing = setTimeout(() => {
|
|
612
|
+
narrowing = null;
|
|
613
|
+
applyDemand(next);
|
|
614
|
+
}, 5_000);
|
|
615
|
+
}
|
|
616
|
+
function requiredRooms(reach) {
|
|
617
|
+
if (reach === null)
|
|
618
|
+
return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
|
|
619
|
+
const named = [...new Set(reach.rooms)].sort();
|
|
620
|
+
if (roomsListed)
|
|
621
|
+
return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
|
|
622
|
+
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
623
|
+
}
|
|
624
|
+
function roomState(locator, search) {
|
|
625
|
+
if (sourceRefusals.has(locator))
|
|
626
|
+
return 'refused';
|
|
627
|
+
const listedRoom = discovered.get(locator);
|
|
628
|
+
if (roomsListed &&
|
|
629
|
+
(listedRoom === undefined ||
|
|
630
|
+
listedRoom.replica.admission !== 'admitted' ||
|
|
631
|
+
unreadable.has(locator)))
|
|
632
|
+
return 'unadmitted';
|
|
633
|
+
if (!replicas.has(locator))
|
|
634
|
+
return 'acquiring';
|
|
635
|
+
if (!prepared.has(locator))
|
|
636
|
+
return 'preparing';
|
|
637
|
+
if (search && !searchIndex.rooms().has(locator))
|
|
638
|
+
return 'indexing';
|
|
639
|
+
return 'complete';
|
|
640
|
+
}
|
|
641
|
+
function conversationCoverage(reach, search) {
|
|
642
|
+
const declared = reach === null || reach.conversations;
|
|
643
|
+
if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
|
|
644
|
+
return 'undeclared';
|
|
645
|
+
if (!conversations.known() || !conversations.current())
|
|
646
|
+
return 'acquiring';
|
|
647
|
+
if (!search)
|
|
648
|
+
return 'complete';
|
|
649
|
+
const unindexed = conversations
|
|
650
|
+
.searchable()
|
|
651
|
+
.filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
|
|
652
|
+
if (unindexed.length === 0)
|
|
653
|
+
return 'complete';
|
|
654
|
+
index([], unindexed);
|
|
655
|
+
return 'indexing';
|
|
656
|
+
}
|
|
657
|
+
function connectionOf() {
|
|
658
|
+
if (socketOpen)
|
|
659
|
+
return 'connected';
|
|
660
|
+
if (status.providerUnreachable)
|
|
661
|
+
return 'unreachable';
|
|
662
|
+
return options.adapter.roomStream === undefined && status.lastCompleted !== null
|
|
663
|
+
? 'connected'
|
|
664
|
+
: 'connecting';
|
|
665
|
+
}
|
|
666
|
+
function refusedIn(locators) {
|
|
667
|
+
return locators.flatMap((room) => {
|
|
668
|
+
const held = sourceRefusals.get(room);
|
|
669
|
+
if (held === undefined)
|
|
670
|
+
return [];
|
|
671
|
+
const { file, location } = held.fields;
|
|
672
|
+
return [
|
|
673
|
+
{
|
|
674
|
+
room,
|
|
675
|
+
file: typeof file === 'string' ? file : '',
|
|
676
|
+
rule: held.code,
|
|
677
|
+
location: typeof location === 'string' ? location : null,
|
|
678
|
+
},
|
|
679
|
+
];
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
function readinessFor(port, reach) {
|
|
683
|
+
const required = requiredRooms(reach);
|
|
684
|
+
const search = port.search === 'required';
|
|
685
|
+
const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
|
|
686
|
+
const building = (state) => coverage
|
|
687
|
+
.filter((room) => room.state === state && !unindexable.has(room.room))
|
|
688
|
+
.map(({ room }) => room);
|
|
689
|
+
const preparing = building('preparing');
|
|
690
|
+
if (preparing.length > 0)
|
|
691
|
+
prepare(preparing);
|
|
692
|
+
const indexing = building('indexing');
|
|
693
|
+
if (indexing.length > 0)
|
|
694
|
+
index(indexing, []);
|
|
695
|
+
return readinessOf({
|
|
696
|
+
code: port.code,
|
|
697
|
+
coverage: {
|
|
698
|
+
listed: required.listed,
|
|
699
|
+
rooms: coverage,
|
|
700
|
+
conversations: conversationCoverage(reach, search),
|
|
701
|
+
search: search ? 'required' : 'unused',
|
|
702
|
+
},
|
|
703
|
+
persistence,
|
|
704
|
+
connection: connectionOf(),
|
|
705
|
+
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
706
|
+
capacity: capacity !== null &&
|
|
707
|
+
coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
|
|
708
|
+
? capacity
|
|
709
|
+
: port.capacity,
|
|
710
|
+
refused: refusedIn(required.rooms),
|
|
711
|
+
lost: storageLoss !== null,
|
|
712
|
+
established: targets[port.target]?.established === true,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
function establish(name) {
|
|
716
|
+
targets = { ...targets, [name]: { established: true } };
|
|
717
|
+
queueReadiness();
|
|
718
|
+
void durably((local) => local.keepTargets(targets)).catch(() => {
|
|
719
|
+
journal.record(`target record failed ${name}`);
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
function deliverReadiness(watch) {
|
|
723
|
+
if (!readinessWatches.has(watch))
|
|
724
|
+
return;
|
|
725
|
+
const readiness = readinessFor(watch.port, watch.reach);
|
|
726
|
+
if (readiness.ready && targets[watch.port.target]?.established !== true) {
|
|
727
|
+
establish(watch.port.target);
|
|
728
|
+
}
|
|
729
|
+
const shown = JSON.stringify(readiness);
|
|
730
|
+
if (shown === watch.last)
|
|
731
|
+
return;
|
|
732
|
+
watch.last = shown;
|
|
733
|
+
watch.deliver(structuredClone(readiness));
|
|
734
|
+
}
|
|
735
|
+
function queueReadiness() {
|
|
736
|
+
if (readinessQueued || closed || readinessWatches.size === 0)
|
|
737
|
+
return;
|
|
738
|
+
readinessQueued = true;
|
|
739
|
+
queueMicrotask(() => {
|
|
740
|
+
readinessQueued = false;
|
|
741
|
+
if (closed)
|
|
742
|
+
return;
|
|
743
|
+
for (const watch of [...readinessWatches])
|
|
744
|
+
deliverReadiness(watch);
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
function watchReadiness(port, reach, deliver) {
|
|
748
|
+
const watch = { port, reach, deliver, last: null };
|
|
749
|
+
readinessWatches.add(watch);
|
|
750
|
+
void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
|
|
751
|
+
return () => {
|
|
752
|
+
readinessWatches.delete(watch);
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
function layerBytes(layer) {
|
|
756
|
+
let bytes = 0;
|
|
757
|
+
for (const [path, text] of Object.entries(layer.documents))
|
|
758
|
+
bytes += (path.length + text.length) * 2;
|
|
759
|
+
return bytes;
|
|
760
|
+
}
|
|
761
|
+
function grownBy(next, previous) {
|
|
762
|
+
if (previous === undefined)
|
|
763
|
+
return layerBytes(next);
|
|
764
|
+
const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
|
|
765
|
+
return layerBytes({ ...next, documents });
|
|
766
|
+
}
|
|
767
|
+
function exceeded(error) {
|
|
768
|
+
return (typeof error === 'object' &&
|
|
769
|
+
error !== null &&
|
|
770
|
+
error.name === 'QuotaExceededError');
|
|
771
|
+
}
|
|
772
|
+
async function estimated() {
|
|
773
|
+
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
774
|
+
return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
|
|
775
|
+
}
|
|
776
|
+
function pause(locator, cause) {
|
|
777
|
+
paused.add(locator);
|
|
778
|
+
capacity = cause;
|
|
779
|
+
journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
|
|
780
|
+
queueReadiness();
|
|
781
|
+
if (rechecking !== null)
|
|
782
|
+
return;
|
|
783
|
+
// A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
|
|
784
|
+
rechecking = setInterval(() => {
|
|
785
|
+
void recheck();
|
|
786
|
+
}, 60_000);
|
|
787
|
+
}
|
|
788
|
+
async function recheck() {
|
|
789
|
+
const held = capacity;
|
|
790
|
+
if (held === null || closed)
|
|
791
|
+
return;
|
|
792
|
+
const available = await estimated().catch(() => null);
|
|
793
|
+
if (available === null || available < (held.needed ?? 0))
|
|
794
|
+
return;
|
|
795
|
+
if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
|
|
796
|
+
return;
|
|
797
|
+
resume();
|
|
798
|
+
}
|
|
799
|
+
function resume() {
|
|
800
|
+
if (rechecking !== null)
|
|
801
|
+
clearInterval(rechecking);
|
|
802
|
+
rechecking = null;
|
|
803
|
+
if (paused.size === 0 && capacity === null)
|
|
804
|
+
return;
|
|
805
|
+
paused.clear();
|
|
806
|
+
capacity = null;
|
|
807
|
+
journal.record('capacity resumed');
|
|
808
|
+
queueReadiness();
|
|
809
|
+
if (!closed && authorization !== undefined)
|
|
810
|
+
void sync('unstreamed');
|
|
811
|
+
}
|
|
812
|
+
async function reclaimed(local) {
|
|
813
|
+
const unreferenced = [...transfers.keys()].filter((locator) => {
|
|
814
|
+
const room = discovered.get(locator);
|
|
815
|
+
return room === undefined || !acquires(room);
|
|
816
|
+
});
|
|
817
|
+
for (const locator of unreferenced) {
|
|
818
|
+
await local.abandon(locator);
|
|
819
|
+
transfers.delete(locator);
|
|
820
|
+
}
|
|
821
|
+
if (unreferenced.length > 0)
|
|
822
|
+
journal.record(`capacity reclaimed ${unreferenced.length}`);
|
|
823
|
+
return unreferenced.length > 0;
|
|
824
|
+
}
|
|
825
|
+
async function admitted(local, locator, needed) {
|
|
826
|
+
if (paused.has(locator))
|
|
827
|
+
return false;
|
|
828
|
+
let available = await estimated().catch(() => null);
|
|
829
|
+
if (available === null || available >= needed)
|
|
830
|
+
return true;
|
|
831
|
+
if (await reclaimed(local))
|
|
832
|
+
available = await estimated().catch(() => null);
|
|
833
|
+
if (available === null || available >= needed)
|
|
834
|
+
return true;
|
|
835
|
+
pause(locator, { cause: 'insufficient-space', needed, available });
|
|
836
|
+
return false;
|
|
837
|
+
}
|
|
838
|
+
async function exceededOn(locator, bytes) {
|
|
839
|
+
const available = await estimated().catch(() => null);
|
|
840
|
+
pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
|
|
841
|
+
}
|
|
842
|
+
function seedRoomHeld(room, version) {
|
|
843
|
+
return prepared.has(room) && reachedBy(room, version);
|
|
844
|
+
}
|
|
845
|
+
function seedHeld(at) {
|
|
846
|
+
return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
|
|
847
|
+
}
|
|
848
|
+
function seedApplies(plan) {
|
|
849
|
+
const seed = seeds.get(plan.key);
|
|
850
|
+
if (seed === undefined)
|
|
851
|
+
return null;
|
|
852
|
+
if (!seedHeld(seed.at))
|
|
853
|
+
return seed;
|
|
854
|
+
seeds.delete(plan.key);
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
function seedBehind(plan, cover) {
|
|
858
|
+
const seed = seedApplies(plan);
|
|
859
|
+
if (seed === null || 'pending' in cover)
|
|
860
|
+
return cover;
|
|
861
|
+
return {
|
|
862
|
+
pending: Object.entries(seed.at)
|
|
863
|
+
.filter(([room, version]) => !seedRoomHeld(room, version))
|
|
864
|
+
.map(([room]) => ({ room, awaits: 'acquisition' })),
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
function seed(entry) {
|
|
868
|
+
if (seedHeld(entry.at))
|
|
869
|
+
return;
|
|
870
|
+
for (const { query, rows } of entry.queries) {
|
|
871
|
+
const key = queryKey(query);
|
|
872
|
+
seeds.set(key, { rows, at: entry.at });
|
|
873
|
+
for (const shown of seedWatchers.get(key) ?? [])
|
|
874
|
+
shown();
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
async function rowsQuery(plan) {
|
|
878
|
+
const before = covered(plan);
|
|
879
|
+
awaitPreparation(before);
|
|
880
|
+
const seeded = seedApplies(plan);
|
|
881
|
+
if (seeded !== null)
|
|
882
|
+
return seededResult(seeded).result;
|
|
883
|
+
if ('pending' in before)
|
|
884
|
+
return before;
|
|
885
|
+
const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
|
|
886
|
+
awaitPreparation(cover);
|
|
887
|
+
if ('pending' in cover)
|
|
888
|
+
return cover;
|
|
889
|
+
return { ...rows, coverage: cover.coverage };
|
|
890
|
+
}
|
|
891
|
+
async function indexConversation(conversation) {
|
|
892
|
+
searchIndex.indexMessages(conversation, [], false);
|
|
893
|
+
let after = null;
|
|
894
|
+
for (;;) {
|
|
895
|
+
const batch = await durably((local) => local.messagesAfter(conversation, after));
|
|
896
|
+
if (closed)
|
|
897
|
+
return;
|
|
898
|
+
searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
|
|
899
|
+
if (batch.length === 0)
|
|
900
|
+
return;
|
|
901
|
+
after = batch[batch.length - 1].identity;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
function indexNext() {
|
|
905
|
+
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
|
|
906
|
+
if (room !== undefined) {
|
|
907
|
+
roomsToIndex.delete(room);
|
|
908
|
+
return () => prepared.has(room)
|
|
909
|
+
? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
|
|
910
|
+
: undefined;
|
|
911
|
+
}
|
|
912
|
+
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
913
|
+
if (conversation === undefined)
|
|
914
|
+
return null;
|
|
915
|
+
conversationsToIndex.delete(conversation);
|
|
916
|
+
return () => indexConversation(conversation);
|
|
917
|
+
}
|
|
918
|
+
function index(rooms, named) {
|
|
919
|
+
for (const room of rooms)
|
|
920
|
+
roomsToIndex.add(room);
|
|
921
|
+
for (const conversation of named)
|
|
922
|
+
conversationsToIndex.add(conversation);
|
|
923
|
+
if (indexing)
|
|
924
|
+
return;
|
|
925
|
+
indexing = true;
|
|
926
|
+
void (async () => {
|
|
927
|
+
try {
|
|
928
|
+
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
929
|
+
await yielded();
|
|
930
|
+
await unit();
|
|
931
|
+
queueReadiness();
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
finally {
|
|
935
|
+
indexing = false;
|
|
936
|
+
}
|
|
937
|
+
})().catch(() => { });
|
|
357
938
|
}
|
|
358
939
|
function publishChangeSet(set) {
|
|
359
940
|
applyChangeSet(set);
|
|
@@ -370,34 +951,7 @@ export function composeRuntime(options) {
|
|
|
370
951
|
emitPresentation(app);
|
|
371
952
|
conversations.changed(set);
|
|
372
953
|
}
|
|
373
|
-
function stopTimer() {
|
|
374
|
-
if (resetTimer === undefined)
|
|
375
|
-
return;
|
|
376
|
-
clearTimeout(resetTimer);
|
|
377
|
-
resetTimer = undefined;
|
|
378
|
-
}
|
|
379
|
-
function armTimer() {
|
|
380
|
-
stopTimer();
|
|
381
|
-
if (closed || stopping)
|
|
382
|
-
return;
|
|
383
|
-
let earliest = null;
|
|
384
|
-
for (const { submission } of pendings.values()) {
|
|
385
|
-
if (submission.state !== 'reset')
|
|
386
|
-
continue;
|
|
387
|
-
const at = Date.parse(submission.reset);
|
|
388
|
-
if (earliest === null || at < earliest)
|
|
389
|
-
earliest = at;
|
|
390
|
-
}
|
|
391
|
-
if (earliest === null)
|
|
392
|
-
return;
|
|
393
|
-
// A Save House held back goes again only once the reset its answer named has passed.
|
|
394
|
-
resetTimer = setTimeout(() => {
|
|
395
|
-
resetTimer = undefined;
|
|
396
|
-
submitAll();
|
|
397
|
-
}, Math.max(0, earliest - Date.now()));
|
|
398
|
-
}
|
|
399
954
|
async function refuse(reason, provider = null) {
|
|
400
|
-
stopTimer();
|
|
401
955
|
discoveryPaused = true;
|
|
402
956
|
try {
|
|
403
957
|
if (reason === 'revoked-authorization') {
|
|
@@ -476,14 +1030,27 @@ export function composeRuntime(options) {
|
|
|
476
1030
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
477
1031
|
});
|
|
478
1032
|
}
|
|
479
|
-
async function applyDiscovery(
|
|
1033
|
+
async function applyDiscovery(found, started) {
|
|
1034
|
+
for (const room of found) {
|
|
1035
|
+
const fence = fences.get(room.locator);
|
|
1036
|
+
if (fence !== undefined && fence.erased && fence.at < started)
|
|
1037
|
+
fences.delete(room.locator);
|
|
1038
|
+
}
|
|
1039
|
+
await loseRooms(found
|
|
1040
|
+
.filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
|
|
1041
|
+
.map((room) => room.locator));
|
|
1042
|
+
const listed = found.filter((room) => !fences.has(room.locator));
|
|
480
1043
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
481
1044
|
for (const locator of listedLocators)
|
|
482
1045
|
scopedRefusals.delete(locator);
|
|
483
1046
|
discovered.clear();
|
|
484
1047
|
for (const room of listed)
|
|
485
1048
|
discovered.set(room.locator, room);
|
|
486
|
-
|
|
1049
|
+
const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
|
|
1050
|
+
if (options.adapter.exhaustiveDiscovery === true)
|
|
1051
|
+
await loseRooms(unlisted);
|
|
1052
|
+
else
|
|
1053
|
+
unlistedKept(unlisted);
|
|
487
1054
|
for (const room of listed) {
|
|
488
1055
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
489
1056
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -494,6 +1061,13 @@ export function composeRuntime(options) {
|
|
|
494
1061
|
}
|
|
495
1062
|
roomsListed = true;
|
|
496
1063
|
emitRooms();
|
|
1064
|
+
return listed;
|
|
1065
|
+
}
|
|
1066
|
+
function heldLocally(locator) {
|
|
1067
|
+
return (replicas.has(locator) ||
|
|
1068
|
+
roomDrafts.has(locator) ||
|
|
1069
|
+
transfers.has(locator) ||
|
|
1070
|
+
queuesWithoutReplica.has(locator));
|
|
497
1071
|
}
|
|
498
1072
|
function unlistedHeld(listed) {
|
|
499
1073
|
return [
|
|
@@ -505,41 +1079,73 @@ export function composeRuntime(options) {
|
|
|
505
1079
|
]),
|
|
506
1080
|
].filter((locator) => !listed.has(locator));
|
|
507
1081
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
|
|
515
|
-
room: true,
|
|
516
|
-
queue: true,
|
|
517
|
-
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
518
|
-
}));
|
|
519
|
-
for (const locator of unlisted) {
|
|
520
|
-
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
521
|
-
pendings.delete(batch.id);
|
|
522
|
-
replicas.delete(locator);
|
|
523
|
-
queuesWithoutReplica.delete(locator);
|
|
1082
|
+
function unlistedKept(unlisted) {
|
|
1083
|
+
for (const locator of unlisted) {
|
|
1084
|
+
const held = replicas.get(locator)?.confirmed.record;
|
|
1085
|
+
if (held !== undefined)
|
|
1086
|
+
list(held, false);
|
|
1087
|
+
else
|
|
524
1088
|
rooms.delete(locator);
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
function fence(locators) {
|
|
1092
|
+
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
1093
|
+
const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
|
|
1094
|
+
room: true,
|
|
1095
|
+
queue: true,
|
|
1096
|
+
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
1097
|
+
}));
|
|
1098
|
+
for (const locator of fresh) {
|
|
1099
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
1100
|
+
lastFenced.set(locator, ++fencings);
|
|
1101
|
+
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
1102
|
+
pendings.delete(batch.id);
|
|
1103
|
+
replicas.delete(locator);
|
|
1104
|
+
queuesWithoutReplica.delete(locator);
|
|
1105
|
+
rooms.delete(locator);
|
|
1106
|
+
roomDrafts.delete(locator);
|
|
1107
|
+
transfers.delete(locator);
|
|
1108
|
+
catching.delete(locator);
|
|
1109
|
+
discovered.delete(locator);
|
|
1110
|
+
announced.delete(locator);
|
|
1111
|
+
unreadable.delete(locator);
|
|
1112
|
+
stale.delete(locator);
|
|
1113
|
+
repairable.delete(locator);
|
|
1114
|
+
unindexable.delete(locator);
|
|
1115
|
+
paused.delete(locator);
|
|
1116
|
+
scopedRefusals.delete(locator);
|
|
1117
|
+
sourceRefusals.delete(locator);
|
|
1118
|
+
declarationRefusals.delete(locator);
|
|
1119
|
+
for (const attached of ['collection', 'typed-selection']) {
|
|
1120
|
+
const key = modelKey(locator, attached);
|
|
1121
|
+
models.delete(key);
|
|
1122
|
+
liveModels.delete(key);
|
|
1123
|
+
invalidModels.delete(key);
|
|
539
1124
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
1125
|
+
journal.record(`room lost ${locator}`);
|
|
1126
|
+
}
|
|
1127
|
+
if (fresh.length === 0)
|
|
1128
|
+
return fresh;
|
|
1129
|
+
publishChangeSet(mergeChangeSets(removed));
|
|
1130
|
+
for (const locator of fresh)
|
|
1131
|
+
prepared.delete(locator);
|
|
1132
|
+
searchIndex.forget(fresh);
|
|
1133
|
+
readSide.recover();
|
|
1134
|
+
moveTo({ ...status, ...attention() });
|
|
1135
|
+
return fresh;
|
|
1136
|
+
}
|
|
1137
|
+
async function erased(local, locators) {
|
|
1138
|
+
await local.erase(locators);
|
|
1139
|
+
for (const locator of locators) {
|
|
1140
|
+
const held = fences.get(locator);
|
|
1141
|
+
if (held !== undefined)
|
|
1142
|
+
held.erased = true;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
async function loseRooms(locators) {
|
|
1146
|
+
const fresh = fence(locators);
|
|
1147
|
+
if (fresh.length > 0)
|
|
1148
|
+
await durably((local) => erased(local, fresh));
|
|
543
1149
|
}
|
|
544
1150
|
function restored(key, persisted) {
|
|
545
1151
|
const prior = models.get(key);
|
|
@@ -560,7 +1166,7 @@ export function composeRuntime(options) {
|
|
|
560
1166
|
return model;
|
|
561
1167
|
}
|
|
562
1168
|
async function hydrate(opened) {
|
|
563
|
-
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
|
|
1169
|
+
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
|
|
564
1170
|
opened.all(),
|
|
565
1171
|
opened.queues(),
|
|
566
1172
|
opened.roomDrafts(),
|
|
@@ -572,7 +1178,9 @@ export function composeRuntime(options) {
|
|
|
572
1178
|
opened.capabilities(),
|
|
573
1179
|
opened.member(),
|
|
574
1180
|
opened.transfers(),
|
|
1181
|
+
opened.targets(),
|
|
575
1182
|
]);
|
|
1183
|
+
targets = heldTargets;
|
|
576
1184
|
transfers.clear();
|
|
577
1185
|
for (const record of staged)
|
|
578
1186
|
transfers.set(record.locator, record);
|
|
@@ -634,6 +1242,7 @@ export function composeRuntime(options) {
|
|
|
634
1242
|
async function readState(opened) {
|
|
635
1243
|
const { lastCompleted, locators, read } = await hydrate(opened);
|
|
636
1244
|
applyChangeSet(read);
|
|
1245
|
+
prepare();
|
|
637
1246
|
if (replicas.size > 0)
|
|
638
1247
|
emitRooms();
|
|
639
1248
|
if (member !== null)
|
|
@@ -644,8 +1253,15 @@ export function composeRuntime(options) {
|
|
|
644
1253
|
return lastCompleted;
|
|
645
1254
|
}
|
|
646
1255
|
async function open() {
|
|
647
|
-
|
|
1256
|
+
if (closed)
|
|
1257
|
+
throw new Error('runtime closed');
|
|
1258
|
+
const opened = await openRuntimeStore(isFenced);
|
|
648
1259
|
store = opened;
|
|
1260
|
+
const interrupted = await opened.fences();
|
|
1261
|
+
for (const locator of interrupted) {
|
|
1262
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
1263
|
+
lastFenced.set(locator, ++fencings);
|
|
1264
|
+
}
|
|
649
1265
|
const kept = options.shell.generation;
|
|
650
1266
|
const storedGeneration = await opened.generation();
|
|
651
1267
|
const loss = detectStorageLoss(storedGeneration, kept);
|
|
@@ -659,6 +1275,8 @@ export function composeRuntime(options) {
|
|
|
659
1275
|
emitStorageLoss();
|
|
660
1276
|
}
|
|
661
1277
|
const lastCompleted = await readState(opened);
|
|
1278
|
+
if (interrupted.length > 0)
|
|
1279
|
+
await erased(opened, interrupted);
|
|
662
1280
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
663
1281
|
return opened;
|
|
664
1282
|
}
|
|
@@ -686,8 +1304,9 @@ export function composeRuntime(options) {
|
|
|
686
1304
|
}
|
|
687
1305
|
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
688
1306
|
const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
689
|
-
const withdrawn =
|
|
690
|
-
const
|
|
1307
|
+
const { withdrawn, kept } = standing(confirmed, queue, withdrawable, localSaves);
|
|
1308
|
+
const founded = kept.filter((batch) => !queue.includes(batch));
|
|
1309
|
+
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
691
1310
|
const unqueued = withdrawn.map((batch) => ({
|
|
692
1311
|
batch,
|
|
693
1312
|
returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
|
|
@@ -711,9 +1330,10 @@ export function composeRuntime(options) {
|
|
|
711
1330
|
});
|
|
712
1331
|
}
|
|
713
1332
|
}
|
|
714
|
-
await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
|
|
1333
|
+
await local.keepAuthored({ locator, queue: kept, founded, roomDrafts: [...written.values()] });
|
|
1334
|
+
const staying = new Set(kept.map((batch) => batch.id));
|
|
715
1335
|
for (const batch of previousQueue) {
|
|
716
|
-
if (!
|
|
1336
|
+
if (!staying.has(batch.id))
|
|
717
1337
|
pendings.delete(batch.id);
|
|
718
1338
|
}
|
|
719
1339
|
for (const record of written.values())
|
|
@@ -734,23 +1354,25 @@ export function composeRuntime(options) {
|
|
|
734
1354
|
else
|
|
735
1355
|
queuesWithoutReplica.delete(locator);
|
|
736
1356
|
publishChangeSet(changeSet(locator, before ?? null, held, {
|
|
737
|
-
queue:
|
|
1357
|
+
queue: queueChanged,
|
|
738
1358
|
drafts: [...written.keys()],
|
|
739
1359
|
}));
|
|
740
1360
|
moveTo({ ...status, ...attention() });
|
|
741
1361
|
throw error;
|
|
742
1362
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
const
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1363
|
+
return () => {
|
|
1364
|
+
queuesWithoutReplica.delete(locator);
|
|
1365
|
+
for (const { batch, returned } of unqueued) {
|
|
1366
|
+
const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
|
|
1367
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
1368
|
+
}
|
|
1369
|
+
replicas.set(locator, replicaOf(confirmed, kept, before));
|
|
1370
|
+
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
1371
|
+
room: settling.room,
|
|
1372
|
+
queue: queueChanged,
|
|
1373
|
+
drafts: [...written.keys()],
|
|
1374
|
+
});
|
|
1375
|
+
};
|
|
754
1376
|
}
|
|
755
1377
|
function adoptModels(locator, record) {
|
|
756
1378
|
for (const [attached, model] of [
|
|
@@ -783,11 +1405,16 @@ export function composeRuntime(options) {
|
|
|
783
1405
|
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
784
1406
|
};
|
|
785
1407
|
}
|
|
786
|
-
function publish(local, room, confirmed, landing
|
|
1408
|
+
function publish(local, room, confirmed, landing, live) {
|
|
787
1409
|
const { base, stored } = landing;
|
|
788
1410
|
return exclusive(async () => {
|
|
789
|
-
if (
|
|
1411
|
+
if (stored !== undefined) {
|
|
1412
|
+
await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
|
|
1413
|
+
}
|
|
1414
|
+
if (!live()) {
|
|
1415
|
+
caughtUpTo(room.locator);
|
|
790
1416
|
return true;
|
|
1417
|
+
}
|
|
791
1418
|
const prior = replicas.get(room.locator);
|
|
792
1419
|
if (base !== undefined &&
|
|
793
1420
|
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
@@ -801,24 +1428,42 @@ export function composeRuntime(options) {
|
|
|
801
1428
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
802
1429
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
803
1430
|
});
|
|
804
|
-
|
|
805
|
-
|
|
1431
|
+
let settled;
|
|
1432
|
+
try {
|
|
1433
|
+
settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
|
|
1434
|
+
}
|
|
1435
|
+
catch (error) {
|
|
1436
|
+
if (!exceeded(error))
|
|
1437
|
+
throw error;
|
|
1438
|
+
await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
|
|
1439
|
+
caughtUpTo(room.locator);
|
|
1440
|
+
return true;
|
|
1441
|
+
}
|
|
1442
|
+
const set = settled();
|
|
1443
|
+
if (stored !== undefined) {
|
|
806
1444
|
transfers.delete(room.locator);
|
|
1445
|
+
catching.delete(room.locator);
|
|
1446
|
+
}
|
|
807
1447
|
for (const batch of accepted)
|
|
808
1448
|
pendings.delete(batch.id);
|
|
809
1449
|
stale.delete(room.locator);
|
|
1450
|
+
unindexable.delete(room.locator);
|
|
810
1451
|
if (unreadable.delete(room.locator))
|
|
811
1452
|
scopedRefusals.delete(room.locator);
|
|
812
1453
|
sourceRefusals.delete(room.locator);
|
|
813
1454
|
declarationRefusals.delete(room.locator);
|
|
814
1455
|
list(room, true);
|
|
1456
|
+
if (prior === undefined)
|
|
1457
|
+
prepared.add(room.locator);
|
|
815
1458
|
publishChangeSet(set);
|
|
816
1459
|
moveTo({ ...status, ...attention() });
|
|
1460
|
+
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
817
1461
|
return true;
|
|
818
1462
|
});
|
|
819
1463
|
}
|
|
820
1464
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
821
1465
|
const local = await openLocal();
|
|
1466
|
+
await referenced(locator);
|
|
822
1467
|
const replica = replicas.get(locator);
|
|
823
1468
|
const room = rooms.get(locator);
|
|
824
1469
|
if (replica === undefined || room === undefined) {
|
|
@@ -865,9 +1510,17 @@ export function composeRuntime(options) {
|
|
|
865
1510
|
visible: replica.visible,
|
|
866
1511
|
changes,
|
|
867
1512
|
bases: parts.flatMap(({ bases }) => bases),
|
|
1513
|
+
queued: queuedDocuments(replica.queue),
|
|
868
1514
|
writtenBy,
|
|
1515
|
+
clean: {
|
|
1516
|
+
text: replica.confirmed.record.documents,
|
|
1517
|
+
binaries: replica.confirmed.record.binaries,
|
|
1518
|
+
composition: compositionOf(replica.confirmed.record),
|
|
1519
|
+
},
|
|
869
1520
|
};
|
|
870
|
-
const checked = withdrawing
|
|
1521
|
+
const checked = withdrawing
|
|
1522
|
+
? buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
|
|
1523
|
+
: null;
|
|
871
1524
|
if (checked?.kind === 'refused')
|
|
872
1525
|
return { outcome: 'refused', ...checked.refusal };
|
|
873
1526
|
const owners = [...requeue.owners].flatMap((origin) => {
|
|
@@ -890,6 +1543,7 @@ export function composeRuntime(options) {
|
|
|
890
1543
|
...input,
|
|
891
1544
|
visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
|
|
892
1545
|
changes: requeue.changes,
|
|
1546
|
+
queued: queuedDocuments(requeue.queue),
|
|
893
1547
|
});
|
|
894
1548
|
if (built?.kind === 'refused')
|
|
895
1549
|
return { outcome: 'refused', ...built.refusal };
|
|
@@ -936,6 +1590,8 @@ export function composeRuntime(options) {
|
|
|
936
1590
|
catch {
|
|
937
1591
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
938
1592
|
}
|
|
1593
|
+
if (fences.has(locator))
|
|
1594
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
939
1595
|
for (const origin of withdrawn.keys())
|
|
940
1596
|
pendings.delete(origin);
|
|
941
1597
|
for (const [id, drafts] of drafted) {
|
|
@@ -965,6 +1621,8 @@ export function composeRuntime(options) {
|
|
|
965
1621
|
return;
|
|
966
1622
|
}
|
|
967
1623
|
await local.submit(locator, identifier, submission, lost);
|
|
1624
|
+
if (fences.has(locator))
|
|
1625
|
+
return;
|
|
968
1626
|
pendings.set(identifier, { ...state, submission, lost });
|
|
969
1627
|
const replica = replicas.get(locator);
|
|
970
1628
|
if (replica !== undefined)
|
|
@@ -975,6 +1633,8 @@ export function composeRuntime(options) {
|
|
|
975
1633
|
if (!continuing())
|
|
976
1634
|
throw new Error('connection refused');
|
|
977
1635
|
await local.keepUpload(locator, identifier, upload);
|
|
1636
|
+
if (fences.has(locator))
|
|
1637
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
978
1638
|
pendings.set(identifier, { ...pendings.get(identifier), upload });
|
|
979
1639
|
if (!continuing())
|
|
980
1640
|
throw new Error('connection refused');
|
|
@@ -990,6 +1650,7 @@ export function composeRuntime(options) {
|
|
|
990
1650
|
}
|
|
991
1651
|
if (closed || authorization === undefined || refusal !== null)
|
|
992
1652
|
return UNCERTAIN;
|
|
1653
|
+
const since = fencings;
|
|
993
1654
|
const transfer = ['transfer', 'history', locator, path];
|
|
994
1655
|
if (query.revision !== undefined)
|
|
995
1656
|
transfer.push(query.revision);
|
|
@@ -1004,6 +1665,8 @@ export function composeRuntime(options) {
|
|
|
1004
1665
|
...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
|
|
1005
1666
|
});
|
|
1006
1667
|
journal.record(`outcome history ${result.kind}`);
|
|
1668
|
+
if (fencedSince(locator, since))
|
|
1669
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
1007
1670
|
if (result.kind === 'scoped-refusal') {
|
|
1008
1671
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
1009
1672
|
if (room !== undefined)
|
|
@@ -1026,7 +1689,13 @@ export function composeRuntime(options) {
|
|
|
1026
1689
|
}
|
|
1027
1690
|
return null;
|
|
1028
1691
|
}
|
|
1029
|
-
|
|
1692
|
+
function binaryRoom(request) {
|
|
1693
|
+
if (request.kind === 'path')
|
|
1694
|
+
return request.locator;
|
|
1695
|
+
const scope = options.adapter.referrerScope(request.referrer);
|
|
1696
|
+
return scope?.kind === 'room' ? scope.locator : null;
|
|
1697
|
+
}
|
|
1698
|
+
async function transferBinary(request, transfer, answer) {
|
|
1030
1699
|
if (closed || refusal !== null)
|
|
1031
1700
|
return UNCERTAIN;
|
|
1032
1701
|
try {
|
|
@@ -1037,41 +1706,46 @@ export function composeRuntime(options) {
|
|
|
1037
1706
|
}
|
|
1038
1707
|
if (closed || authorization === undefined || refusal !== null)
|
|
1039
1708
|
return UNCERTAIN;
|
|
1709
|
+
const since = fencings;
|
|
1040
1710
|
journal.record(request.kind === 'path'
|
|
1041
1711
|
? `transfer binary ${request.locator} ${request.path}`
|
|
1042
1712
|
: `transfer binary ${request.fileRef}`);
|
|
1043
1713
|
try {
|
|
1044
|
-
const result = await
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1714
|
+
const result = await transfer(authorization);
|
|
1715
|
+
const settled = result;
|
|
1716
|
+
journal.record(`outcome binary ${settled.kind}`);
|
|
1717
|
+
const locator = binaryRoom(request);
|
|
1718
|
+
if (locator !== null && fencedSince(locator, since))
|
|
1719
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
1720
|
+
if (settled.kind === 'scoped-refusal') {
|
|
1721
|
+
const room = replicas.get(settled.locator)?.confirmed.record;
|
|
1048
1722
|
if (room !== undefined)
|
|
1049
|
-
await scoped(
|
|
1723
|
+
await scoped(settled, room, await openLocal());
|
|
1050
1724
|
}
|
|
1051
|
-
if (
|
|
1725
|
+
if (settled.kind === 'revoked-authorization')
|
|
1052
1726
|
await refuse('revoked-authorization');
|
|
1053
|
-
return
|
|
1727
|
+
return answer(result);
|
|
1054
1728
|
}
|
|
1055
1729
|
catch {
|
|
1056
1730
|
journal.record('outcome binary transport-uncertainty');
|
|
1057
1731
|
return UNCERTAIN;
|
|
1058
1732
|
}
|
|
1059
1733
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1734
|
+
function readBinary(request) {
|
|
1735
|
+
return transferBinary(request, (held) => options.adapter.binary(held, request), binaryAnswer);
|
|
1736
|
+
}
|
|
1737
|
+
function binaryPath(locator, path, revision) {
|
|
1062
1738
|
if (revision !== undefined)
|
|
1063
|
-
return
|
|
1739
|
+
return { kind: 'path', locator, path, revision };
|
|
1064
1740
|
const visible = replicas.get(locator)?.visible;
|
|
1065
1741
|
const held = visible?.binaries[path];
|
|
1066
1742
|
if (visible === undefined || held === undefined)
|
|
1067
|
-
return
|
|
1068
|
-
if (held.state === 'pending')
|
|
1069
|
-
|
|
1070
|
-
return blob === null ? absentBinary(path) : { kind: 'binary', blob };
|
|
1071
|
-
}
|
|
1743
|
+
return null;
|
|
1744
|
+
if (held.state === 'pending')
|
|
1745
|
+
return { kind: 'pending', localId: held.localId };
|
|
1072
1746
|
const record = binaryRecord(path, held, visible.revisions[path]);
|
|
1073
1747
|
if (record.state !== 'accepted')
|
|
1074
|
-
return
|
|
1748
|
+
return null;
|
|
1075
1749
|
const replica = replicas.get(locator);
|
|
1076
1750
|
const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
|
|
1077
1751
|
const holds = (at) => {
|
|
@@ -1086,13 +1760,31 @@ export function composeRuntime(options) {
|
|
|
1086
1760
|
if (change.kind === 'move' && change.to === confirmed)
|
|
1087
1761
|
confirmed = change.path;
|
|
1088
1762
|
}
|
|
1089
|
-
return
|
|
1763
|
+
return {
|
|
1090
1764
|
kind: 'path',
|
|
1091
1765
|
locator,
|
|
1092
1766
|
path: confirmed,
|
|
1093
1767
|
revision: record.revision,
|
|
1094
1768
|
fileRef: record.fileRef,
|
|
1095
|
-
}
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
async function readBinaryPath(locator, path, revision) {
|
|
1772
|
+
await ready;
|
|
1773
|
+
const asked = binaryPath(locator, path, revision);
|
|
1774
|
+
if (asked === null)
|
|
1775
|
+
return absentBinary(path);
|
|
1776
|
+
if (asked.kind === 'pending') {
|
|
1777
|
+
const blob = queuedBlob(locator, path, asked.localId);
|
|
1778
|
+
return blob === null ? absentBinary(path) : { kind: 'binary', blob };
|
|
1779
|
+
}
|
|
1780
|
+
return readBinary(asked);
|
|
1781
|
+
}
|
|
1782
|
+
async function linkBinaryPath(locator, path, revision) {
|
|
1783
|
+
await ready;
|
|
1784
|
+
const asked = binaryPath(locator, path, revision);
|
|
1785
|
+
if (asked === null || asked.kind === 'pending')
|
|
1786
|
+
return absentBinary(path);
|
|
1787
|
+
return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
|
|
1096
1788
|
}
|
|
1097
1789
|
async function readBinaryReference(reference) {
|
|
1098
1790
|
await ready;
|
|
@@ -1118,7 +1810,7 @@ export function composeRuntime(options) {
|
|
|
1118
1810
|
}
|
|
1119
1811
|
return undefined;
|
|
1120
1812
|
}
|
|
1121
|
-
async function invokeAction(name, input) {
|
|
1813
|
+
async function invokeAction(name, input, dispatching) {
|
|
1122
1814
|
await ready;
|
|
1123
1815
|
if (closed || refusal !== null)
|
|
1124
1816
|
return UNCERTAIN;
|
|
@@ -1132,6 +1824,7 @@ export function composeRuntime(options) {
|
|
|
1132
1824
|
}
|
|
1133
1825
|
if (closed || authorization === undefined || refusal !== null)
|
|
1134
1826
|
return UNCERTAIN;
|
|
1827
|
+
await dispatching?.();
|
|
1135
1828
|
journal.record(`transfer action ${name}`);
|
|
1136
1829
|
try {
|
|
1137
1830
|
const result = await options.adapter.action(authorization, { name, input });
|
|
@@ -1152,6 +1845,60 @@ export function composeRuntime(options) {
|
|
|
1152
1845
|
return UNCERTAIN;
|
|
1153
1846
|
}
|
|
1154
1847
|
}
|
|
1848
|
+
function unsettled(result) {
|
|
1849
|
+
return result.kind !== 'action' && result.retryable;
|
|
1850
|
+
}
|
|
1851
|
+
async function recoveredAction(name, input) {
|
|
1852
|
+
const operation = operationOf(input);
|
|
1853
|
+
if (operation === null)
|
|
1854
|
+
return invokeAction(name, input);
|
|
1855
|
+
const kept = async () => (await openLocal()).keepCall({ operation, name, input });
|
|
1856
|
+
calling.add(operation);
|
|
1857
|
+
try {
|
|
1858
|
+
let result = await invokeAction(name, input, kept);
|
|
1859
|
+
if (unsettled(result)) {
|
|
1860
|
+
await recovery.attempt(`action ${operation}`, async () => {
|
|
1861
|
+
if (closed || refusal !== null)
|
|
1862
|
+
return 'done';
|
|
1863
|
+
if (!socketOpen)
|
|
1864
|
+
return 'again';
|
|
1865
|
+
result = await invokeAction(name, input, kept);
|
|
1866
|
+
return unsettled(result) ? 'again' : 'done';
|
|
1867
|
+
}, { again: true });
|
|
1868
|
+
}
|
|
1869
|
+
if (!unsettled(result))
|
|
1870
|
+
await (await openLocal()).dropCall(operation);
|
|
1871
|
+
return result;
|
|
1872
|
+
}
|
|
1873
|
+
finally {
|
|
1874
|
+
calling.delete(operation);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
async function resendCalls(signal) {
|
|
1878
|
+
if (!socketOpen || closed || stopping)
|
|
1879
|
+
return 'done';
|
|
1880
|
+
const local = await openLocal();
|
|
1881
|
+
for (const held of await local.calls()) {
|
|
1882
|
+
if (calling.has(held.operation))
|
|
1883
|
+
continue;
|
|
1884
|
+
if (closed || signal.aborted)
|
|
1885
|
+
return 'done';
|
|
1886
|
+
if (pastHorizon(held.operation)) {
|
|
1887
|
+
await local.dropCall(held.operation);
|
|
1888
|
+
continue;
|
|
1889
|
+
}
|
|
1890
|
+
calling.add(held.operation);
|
|
1891
|
+
try {
|
|
1892
|
+
if (unsettled(await invokeAction(held.name, held.input)))
|
|
1893
|
+
return 'again';
|
|
1894
|
+
await local.dropCall(held.operation);
|
|
1895
|
+
}
|
|
1896
|
+
finally {
|
|
1897
|
+
calling.delete(held.operation);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
return 'done';
|
|
1901
|
+
}
|
|
1155
1902
|
function continuing() {
|
|
1156
1903
|
return !closed && !discoveryPaused;
|
|
1157
1904
|
}
|
|
@@ -1166,11 +1913,12 @@ export function composeRuntime(options) {
|
|
|
1166
1913
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
1167
1914
|
!options.adapter.reached(locator, roomVersion, written));
|
|
1168
1915
|
}
|
|
1169
|
-
async function pullRefresh(authorized, confirmed) {
|
|
1170
|
-
if (!
|
|
1916
|
+
async function pullRefresh(authorized, confirmed, live) {
|
|
1917
|
+
if (!live())
|
|
1171
1918
|
return unconfirmed;
|
|
1919
|
+
const turn = generation;
|
|
1172
1920
|
try {
|
|
1173
|
-
const result = await refreshRoom(
|
|
1921
|
+
const result = await refreshRoom(reads, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
|
|
1174
1922
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
1175
1923
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
1176
1924
|
return result;
|
|
@@ -1179,48 +1927,101 @@ export function composeRuntime(options) {
|
|
|
1179
1927
|
journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
|
|
1180
1928
|
throw error;
|
|
1181
1929
|
}
|
|
1930
|
+
finally {
|
|
1931
|
+
reads.finished(confirmed.record.locator, turn);
|
|
1932
|
+
}
|
|
1182
1933
|
}
|
|
1183
|
-
function staging(local) {
|
|
1934
|
+
function staging(local, live) {
|
|
1184
1935
|
return {
|
|
1185
1936
|
held: (locator) => transfers.get(locator) ?? null,
|
|
1186
1937
|
staged: (record) => local.staged(record),
|
|
1187
1938
|
stage: (record, page) => exclusive(async () => {
|
|
1188
|
-
if (!
|
|
1939
|
+
if (!live())
|
|
1940
|
+
return false;
|
|
1941
|
+
const bytes = layerBytes(page);
|
|
1942
|
+
if (!(await admitted(local, record.locator, bytes)))
|
|
1943
|
+
return false;
|
|
1944
|
+
try {
|
|
1945
|
+
await local.stage(record, page);
|
|
1946
|
+
}
|
|
1947
|
+
catch (error) {
|
|
1948
|
+
if (!exceeded(error))
|
|
1949
|
+
throw error;
|
|
1950
|
+
await exceededOn(record.locator, bytes);
|
|
1189
1951
|
return false;
|
|
1190
|
-
|
|
1952
|
+
}
|
|
1191
1953
|
transfers.set(record.locator, record);
|
|
1954
|
+
readSide.recover();
|
|
1192
1955
|
return true;
|
|
1193
1956
|
}),
|
|
1194
1957
|
abandon: (locator) => exclusive(async () => {
|
|
1195
|
-
if (!
|
|
1958
|
+
if (!live())
|
|
1196
1959
|
return;
|
|
1197
1960
|
await local.abandon(locator);
|
|
1198
1961
|
transfers.delete(locator);
|
|
1962
|
+
readSide.recover();
|
|
1199
1963
|
}),
|
|
1200
1964
|
};
|
|
1201
1965
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1966
|
+
function reusable(room) {
|
|
1967
|
+
const record = replicas.get(room.locator)?.confirmed.record;
|
|
1968
|
+
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1969
|
+
return null;
|
|
1970
|
+
return { documents: record.documents, revisions: record.revisions };
|
|
1971
|
+
}
|
|
1972
|
+
async function pullBootstrap(authorized, room, local, live) {
|
|
1973
|
+
if (!live())
|
|
1204
1974
|
return unconfirmed;
|
|
1975
|
+
const turn = generation;
|
|
1976
|
+
if (replicas.has(room.locator)) {
|
|
1977
|
+
catching.add(room.locator);
|
|
1978
|
+
readSide.recover();
|
|
1979
|
+
}
|
|
1205
1980
|
try {
|
|
1206
|
-
const result = await bootstrapRoom(
|
|
1981
|
+
const result = await bootstrapRoom(reads, authorized, room, {
|
|
1207
1982
|
collection: modelOf(room.locator),
|
|
1208
1983
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
1209
|
-
}, (models, held) => remodel(room.locator, models, held),
|
|
1984
|
+
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
1210
1985
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
1211
1986
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1987
|
+
if (result.kind !== 'staged')
|
|
1988
|
+
caughtUpTo(room.locator);
|
|
1212
1989
|
return result;
|
|
1213
1990
|
}
|
|
1214
1991
|
catch (error) {
|
|
1215
1992
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
1993
|
+
caughtUpTo(room.locator);
|
|
1216
1994
|
throw error;
|
|
1217
1995
|
}
|
|
1996
|
+
finally {
|
|
1997
|
+
reads.finished(room.locator, turn);
|
|
1998
|
+
}
|
|
1218
1999
|
}
|
|
1219
|
-
|
|
2000
|
+
function caughtUpTo(locator) {
|
|
2001
|
+
if (catching.delete(locator))
|
|
2002
|
+
readSide.recover();
|
|
2003
|
+
}
|
|
2004
|
+
function behind(room) {
|
|
2005
|
+
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
2006
|
+
return (held !== undefined &&
|
|
2007
|
+
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
2008
|
+
}
|
|
2009
|
+
async function advance(room, local, authorized, live) {
|
|
2010
|
+
const carried = reads.carried(room.locator);
|
|
2011
|
+
const withheld = await advanceOnce(room, local, authorized, live);
|
|
2012
|
+
if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
|
|
2013
|
+
return withheld;
|
|
2014
|
+
}
|
|
2015
|
+
return advanceOnce(room, local, authorized, live);
|
|
2016
|
+
}
|
|
2017
|
+
async function advanceOnce(room, local, authorized, live) {
|
|
2018
|
+
await referenced(room.locator);
|
|
2019
|
+
if (!live() || paused.has(room.locator))
|
|
2020
|
+
return unconfirmed;
|
|
1220
2021
|
const held = replicas.get(room.locator);
|
|
1221
2022
|
if (held !== undefined) {
|
|
1222
|
-
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
1223
|
-
if (!
|
|
2023
|
+
const refreshed = await pullRefresh(authorized, held.confirmed, live);
|
|
2024
|
+
if (!live())
|
|
1224
2025
|
return unconfirmed;
|
|
1225
2026
|
if (refreshed.kind === 'still-current') {
|
|
1226
2027
|
stale.delete(room.locator);
|
|
@@ -1234,31 +2035,45 @@ export function composeRuntime(options) {
|
|
|
1234
2035
|
return null;
|
|
1235
2036
|
}
|
|
1236
2037
|
if (refreshed.kind === 'advanced') {
|
|
1237
|
-
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
|
|
2038
|
+
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
|
|
1238
2039
|
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1239
2040
|
const reached = now !== undefined &&
|
|
1240
2041
|
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
1241
|
-
return reached ? null : advance(room, local, authorized);
|
|
2042
|
+
return reached ? null : advance(room, local, authorized, live);
|
|
1242
2043
|
}
|
|
1243
2044
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1244
2045
|
return null;
|
|
1245
2046
|
}
|
|
1246
2047
|
if (refreshed.kind !== 'beyond-horizon')
|
|
1247
2048
|
return refreshed;
|
|
2049
|
+
if (opened !== null) {
|
|
2050
|
+
owed.add(room.locator);
|
|
2051
|
+
readSide.recover();
|
|
2052
|
+
return unconfirmed;
|
|
2053
|
+
}
|
|
1248
2054
|
}
|
|
1249
|
-
const bootstrapped = await pullBootstrap(authorized, room, local);
|
|
1250
|
-
if (!
|
|
2055
|
+
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
2056
|
+
if (!live())
|
|
1251
2057
|
return unconfirmed;
|
|
1252
2058
|
if (bootstrapped.kind !== 'staged')
|
|
1253
2059
|
return bootstrapped;
|
|
1254
|
-
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
2060
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
1255
2061
|
return null;
|
|
1256
2062
|
}
|
|
1257
2063
|
async function withhold(withheld, room, local, progress) {
|
|
2064
|
+
if (withheld.kind === 'unconfirmed')
|
|
2065
|
+
return;
|
|
1258
2066
|
if (withheld.kind === 'refused')
|
|
1259
2067
|
stale.delete(room.locator);
|
|
1260
2068
|
else
|
|
1261
2069
|
stale.add(room.locator);
|
|
2070
|
+
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
2071
|
+
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
2072
|
+
repairable.set(room.locator, reset);
|
|
2073
|
+
repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
|
|
2074
|
+
}
|
|
2075
|
+
else
|
|
2076
|
+
repairable.delete(room.locator);
|
|
1262
2077
|
if (withheld.kind === 'scoped-refusal') {
|
|
1263
2078
|
await scoped(withheld, room, local);
|
|
1264
2079
|
}
|
|
@@ -1294,6 +2109,7 @@ export function composeRuntime(options) {
|
|
|
1294
2109
|
unreadable.add(room.locator);
|
|
1295
2110
|
list(room, false);
|
|
1296
2111
|
emitRooms();
|
|
2112
|
+
moveTo({ ...status, ...attention() });
|
|
1297
2113
|
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
1298
2114
|
}
|
|
1299
2115
|
function leaving(batch, result) {
|
|
@@ -1315,6 +2131,7 @@ export function composeRuntime(options) {
|
|
|
1315
2131
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1316
2132
|
}
|
|
1317
2133
|
async function adoptAccepted(room, local, submitted, accepted) {
|
|
2134
|
+
await referenced(room.locator);
|
|
1318
2135
|
if (!continuing())
|
|
1319
2136
|
return unconfirmed;
|
|
1320
2137
|
const current = replicas.get(room.locator);
|
|
@@ -1331,16 +2148,17 @@ export function composeRuntime(options) {
|
|
|
1331
2148
|
if (applied.kind !== 'staged') {
|
|
1332
2149
|
const state = pendings.get(submitted.id);
|
|
1333
2150
|
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1334
|
-
if (state !== undefined)
|
|
2151
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
1335
2152
|
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1336
2153
|
emitQueue(room.locator);
|
|
1337
2154
|
return applied;
|
|
1338
2155
|
}
|
|
1339
|
-
const
|
|
2156
|
+
const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
1340
2157
|
pendings.delete(submitted.id);
|
|
1341
2158
|
unreadable.delete(room.locator);
|
|
1342
2159
|
sourceRefusals.delete(room.locator);
|
|
1343
2160
|
declarationRefusals.delete(room.locator);
|
|
2161
|
+
const set = settled();
|
|
1344
2162
|
list(room, true);
|
|
1345
2163
|
publishChangeSet(set);
|
|
1346
2164
|
moveTo({ ...status, ...attention() });
|
|
@@ -1354,14 +2172,14 @@ export function composeRuntime(options) {
|
|
|
1354
2172
|
refusedSource(room, failed);
|
|
1355
2173
|
return false;
|
|
1356
2174
|
}
|
|
1357
|
-
async function submitRoom(room, local, authorized, progress) {
|
|
1358
|
-
while (continuing() && authorization !== undefined) {
|
|
2175
|
+
async function submitRoom(room, local, authorized, progress, signal) {
|
|
2176
|
+
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
1359
2177
|
const replica = replicas.get(room.locator);
|
|
1360
2178
|
const head = replica?.queue[0];
|
|
1361
2179
|
if (replica === undefined || head === undefined)
|
|
1362
|
-
return;
|
|
2180
|
+
return 'done';
|
|
1363
2181
|
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1364
|
-
return;
|
|
2182
|
+
return 'done';
|
|
1365
2183
|
const state = pendings.get(head.id);
|
|
1366
2184
|
const { submission, lost: earlier } = state ?? {
|
|
1367
2185
|
submission: { state: 'waiting' },
|
|
@@ -1369,24 +2187,30 @@ export function composeRuntime(options) {
|
|
|
1369
2187
|
};
|
|
1370
2188
|
if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
|
|
1371
2189
|
progress.complete = false;
|
|
1372
|
-
return;
|
|
2190
|
+
return { at: Date.parse(submission.reset) };
|
|
1373
2191
|
}
|
|
1374
2192
|
if (writesHeld()) {
|
|
1375
2193
|
progress.complete = false;
|
|
1376
2194
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1377
|
-
return;
|
|
2195
|
+
return 'done';
|
|
1378
2196
|
}
|
|
1379
2197
|
const lost = earlier || submission.state === 'submitted';
|
|
2198
|
+
if (lost && pastHorizon(head.id)) {
|
|
2199
|
+
await discardPending(room, local, signal);
|
|
2200
|
+
return 'done';
|
|
2201
|
+
}
|
|
1380
2202
|
let result;
|
|
1381
2203
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
1382
2204
|
await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
|
|
1383
|
-
if (discoveryPaused)
|
|
1384
|
-
return;
|
|
1385
|
-
if (writesHeld()) {
|
|
2205
|
+
if (discoveryPaused || fences.has(room.locator))
|
|
2206
|
+
return 'done';
|
|
2207
|
+
if (signal.aborted || writesHeld()) {
|
|
1386
2208
|
await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
|
|
2209
|
+
if (signal.aborted)
|
|
2210
|
+
return 'done';
|
|
1387
2211
|
progress.complete = false;
|
|
1388
2212
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1389
|
-
return;
|
|
2213
|
+
return 'done';
|
|
1390
2214
|
}
|
|
1391
2215
|
const opensBefore = openSignals;
|
|
1392
2216
|
const answered = awaitingAnswer(room.locator);
|
|
@@ -1402,32 +2226,36 @@ export function composeRuntime(options) {
|
|
|
1402
2226
|
progress.complete = false;
|
|
1403
2227
|
if (continuing())
|
|
1404
2228
|
progress.reachable = false;
|
|
1405
|
-
return;
|
|
2229
|
+
return 'again';
|
|
2230
|
+
}
|
|
2231
|
+
if (signal.aborted) {
|
|
2232
|
+
answered();
|
|
2233
|
+
return 'done';
|
|
1406
2234
|
}
|
|
1407
2235
|
if (result.kind !== 'accepted' || !continuing())
|
|
1408
2236
|
answered();
|
|
1409
2237
|
if (!continuing())
|
|
1410
|
-
return;
|
|
2238
|
+
return 'done';
|
|
1411
2239
|
if (result.kind === 'transport-uncertainty') {
|
|
1412
2240
|
progress.complete = false;
|
|
1413
2241
|
progress.reachable = false;
|
|
1414
|
-
return;
|
|
2242
|
+
return 'again';
|
|
1415
2243
|
}
|
|
1416
2244
|
if (result.kind === 'room-not-readable') {
|
|
1417
2245
|
progress.complete = false;
|
|
1418
2246
|
unreadable.add(room.locator);
|
|
1419
2247
|
list(room, false);
|
|
1420
2248
|
emitRooms();
|
|
1421
|
-
return;
|
|
2249
|
+
return 'done';
|
|
1422
2250
|
}
|
|
1423
2251
|
if (result.kind === 'revoked-authorization') {
|
|
1424
2252
|
await refuse('revoked-authorization');
|
|
1425
|
-
return;
|
|
2253
|
+
return 'done';
|
|
1426
2254
|
}
|
|
1427
2255
|
if (result.kind === 'scoped-refusal') {
|
|
1428
2256
|
await scoped(result, room, local);
|
|
1429
2257
|
progress.complete = false;
|
|
1430
|
-
return;
|
|
2258
|
+
return 'done';
|
|
1431
2259
|
}
|
|
1432
2260
|
if (result.kind === 'write-halted') {
|
|
1433
2261
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
@@ -1436,16 +2264,24 @@ export function composeRuntime(options) {
|
|
|
1436
2264
|
continue;
|
|
1437
2265
|
haltedRefusal = true;
|
|
1438
2266
|
progress.complete = false;
|
|
1439
|
-
return;
|
|
2267
|
+
return 'done';
|
|
1440
2268
|
}
|
|
1441
2269
|
if (result.kind === 'conflicted' ||
|
|
1442
2270
|
result.kind === 'rejected' ||
|
|
1443
2271
|
result.kind === 'expired' ||
|
|
1444
2272
|
result.kind === 'retryable-refusal') {
|
|
2273
|
+
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
2274
|
+
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
2275
|
+
await discardPending(room, local, signal);
|
|
2276
|
+
return 'done';
|
|
2277
|
+
}
|
|
1445
2278
|
if (result.kind === 'expired') {
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
2279
|
+
await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
|
|
2280
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
2281
|
+
if (withheld !== null) {
|
|
2282
|
+
await withhold(withheld, room, local, progress);
|
|
2283
|
+
return 'done';
|
|
2284
|
+
}
|
|
1449
2285
|
}
|
|
1450
2286
|
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
|
|
1451
2287
|
(result.kind === 'conflicted' && result.roomVersion !== undefined
|
|
@@ -1458,8 +2294,7 @@ export function composeRuntime(options) {
|
|
|
1458
2294
|
const reset = result.reset;
|
|
1459
2295
|
progress.complete = false;
|
|
1460
2296
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1461
|
-
|
|
1462
|
-
return;
|
|
2297
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
1463
2298
|
}
|
|
1464
2299
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
1465
2300
|
const left = leaving(head, result);
|
|
@@ -1470,37 +2305,82 @@ export function composeRuntime(options) {
|
|
|
1470
2305
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
1471
2306
|
return;
|
|
1472
2307
|
}
|
|
1473
|
-
const
|
|
2308
|
+
const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
1474
2309
|
leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
|
|
1475
2310
|
(result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
|
|
1476
2311
|
? { ...left, returned: null }
|
|
1477
2312
|
: left,
|
|
1478
2313
|
});
|
|
1479
|
-
publishChangeSet(
|
|
2314
|
+
publishChangeSet(settled());
|
|
1480
2315
|
moveTo({ ...status, ...attention() });
|
|
1481
2316
|
});
|
|
1482
2317
|
if (result.kind === 'expired')
|
|
1483
2318
|
continue;
|
|
1484
|
-
const withheld = await advance(room, local, authorized);
|
|
1485
|
-
if (withheld !== null)
|
|
1486
|
-
|
|
2319
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
2320
|
+
if (withheld !== null) {
|
|
2321
|
+
await withhold(withheld, room, local, progress);
|
|
2322
|
+
return 'done';
|
|
2323
|
+
}
|
|
1487
2324
|
continue;
|
|
1488
2325
|
}
|
|
1489
2326
|
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1490
2327
|
if (!kept) {
|
|
1491
2328
|
progress.complete = false;
|
|
1492
|
-
return;
|
|
2329
|
+
return 'done';
|
|
1493
2330
|
}
|
|
1494
2331
|
}
|
|
2332
|
+
return 'done';
|
|
2333
|
+
}
|
|
2334
|
+
function pastHorizon(identity) {
|
|
2335
|
+
return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
|
|
2336
|
+
}
|
|
2337
|
+
async function discardPending(room, local, signal) {
|
|
2338
|
+
const { locator } = room;
|
|
2339
|
+
const discarded = await exclusive(async () => {
|
|
2340
|
+
if (fences.has(locator))
|
|
2341
|
+
return false;
|
|
2342
|
+
const before = replicas.get(locator);
|
|
2343
|
+
const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
2344
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
2345
|
+
lastFenced.set(locator, ++fencings);
|
|
2346
|
+
try {
|
|
2347
|
+
await local.discard(locator);
|
|
2348
|
+
}
|
|
2349
|
+
finally {
|
|
2350
|
+
fences.delete(locator);
|
|
2351
|
+
}
|
|
2352
|
+
for (const batch of queue)
|
|
2353
|
+
pendings.delete(batch.id);
|
|
2354
|
+
queuesWithoutReplica.delete(locator);
|
|
2355
|
+
const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
|
|
2356
|
+
if (after !== null)
|
|
2357
|
+
replicas.set(locator, after);
|
|
2358
|
+
journal.record(`room discarded ${locator}`);
|
|
2359
|
+
publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
|
|
2360
|
+
moveTo({ ...status, ...attention() });
|
|
2361
|
+
return true;
|
|
2362
|
+
});
|
|
2363
|
+
if (!discarded)
|
|
2364
|
+
return;
|
|
2365
|
+
const progress = { complete: true, reachable: true };
|
|
2366
|
+
await roomTask(locator, async (live) => {
|
|
2367
|
+
await Promise.race([
|
|
2368
|
+
reload(room, local, progress, () => live() && !signal.aborted),
|
|
2369
|
+
abandonedOn(signal),
|
|
2370
|
+
]);
|
|
2371
|
+
});
|
|
2372
|
+
if (signal.aborted)
|
|
2373
|
+
return;
|
|
2374
|
+
await rest(progress);
|
|
1495
2375
|
}
|
|
1496
2376
|
function awaitingAnswer(locator) {
|
|
1497
2377
|
let release = () => { };
|
|
1498
2378
|
const awaited = new Promise((resolve) => {
|
|
1499
2379
|
release = resolve;
|
|
1500
2380
|
});
|
|
1501
|
-
answering.set(locator, awaited);
|
|
2381
|
+
answering.set(locator, { generation, answered: awaited });
|
|
1502
2382
|
return () => {
|
|
1503
|
-
if (answering.get(locator) === awaited)
|
|
2383
|
+
if (answering.get(locator)?.answered === awaited)
|
|
1504
2384
|
answering.delete(locator);
|
|
1505
2385
|
release();
|
|
1506
2386
|
};
|
|
@@ -1571,6 +2451,11 @@ export function composeRuntime(options) {
|
|
|
1571
2451
|
});
|
|
1572
2452
|
}
|
|
1573
2453
|
async function scoped(result, room, local) {
|
|
2454
|
+
if ((result.scope === 'content' || result.scope === 'sync') && !result.retryable) {
|
|
2455
|
+
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
2456
|
+
await loseRooms([room.locator]);
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
1574
2459
|
scopedRefusals.set(room.locator, {
|
|
1575
2460
|
code: result.code,
|
|
1576
2461
|
fields: result.fields,
|
|
@@ -1616,19 +2501,23 @@ export function composeRuntime(options) {
|
|
|
1616
2501
|
];
|
|
1617
2502
|
});
|
|
1618
2503
|
}
|
|
1619
|
-
async function synchronize(listed, local, progress, attempt) {
|
|
1620
|
-
const
|
|
1621
|
-
const
|
|
2504
|
+
async function synchronize(listed, local, progress, attempt, live) {
|
|
2505
|
+
const acquired = listed.filter(acquires);
|
|
2506
|
+
const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
|
|
2507
|
+
if (!live())
|
|
2508
|
+
return;
|
|
2509
|
+
const ready = acquired.filter((_, index) => {
|
|
1622
2510
|
const outcome = prepared[index];
|
|
1623
2511
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1624
2512
|
});
|
|
1625
2513
|
if (attempt === 'opened') {
|
|
1626
|
-
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
2514
|
+
options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
|
|
1627
2515
|
handed = true;
|
|
1628
2516
|
}
|
|
2517
|
+
discovering?.release();
|
|
1629
2518
|
const loaded = await Promise.allSettled(ready
|
|
1630
2519
|
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1631
|
-
.map((room) => loadRoom(room, local, progress)));
|
|
2520
|
+
.map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
|
|
1632
2521
|
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1633
2522
|
if (failed !== undefined)
|
|
1634
2523
|
throw failed.reason;
|
|
@@ -1650,21 +2539,12 @@ export function composeRuntime(options) {
|
|
|
1650
2539
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1651
2540
|
if (discoveryPaused || authorization === undefined)
|
|
1652
2541
|
return false;
|
|
1653
|
-
|
|
1654
|
-
if (replicas.has(room.locator))
|
|
1655
|
-
progress.complete = false;
|
|
1656
|
-
return false;
|
|
1657
|
-
}
|
|
1658
|
-
return true;
|
|
2542
|
+
return room.replica.admission === 'admitted';
|
|
1659
2543
|
}
|
|
1660
|
-
async function caughtUp(room, local, progress) {
|
|
1661
|
-
|
|
1662
|
-
if (held === undefined || authorization === undefined)
|
|
2544
|
+
async function caughtUp(room, local, progress, live) {
|
|
2545
|
+
if (authorization === undefined || !live() || !behind(room))
|
|
1663
2546
|
return;
|
|
1664
|
-
const
|
|
1665
|
-
if (!behind)
|
|
1666
|
-
return;
|
|
1667
|
-
const withheld = await advance(room, local, authorization);
|
|
2547
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1668
2548
|
if (withheld !== null)
|
|
1669
2549
|
await withhold(withheld, room, local, progress);
|
|
1670
2550
|
}
|
|
@@ -1674,39 +2554,46 @@ export function composeRuntime(options) {
|
|
|
1674
2554
|
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1675
2555
|
});
|
|
1676
2556
|
}
|
|
1677
|
-
async function loadRoom(room, local, progress) {
|
|
1678
|
-
if (
|
|
2557
|
+
async function loadRoom(room, local, progress, live) {
|
|
2558
|
+
if (!live() || authorization === undefined)
|
|
1679
2559
|
return;
|
|
1680
2560
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1681
|
-
await reload(room, local, progress);
|
|
2561
|
+
await reload(room, local, progress, live);
|
|
1682
2562
|
return;
|
|
1683
2563
|
}
|
|
1684
|
-
const withheld = await advance(room, local, authorization);
|
|
2564
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1685
2565
|
if (withheld !== null) {
|
|
1686
2566
|
await withhold(withheld, room, local, progress);
|
|
1687
2567
|
return;
|
|
1688
2568
|
}
|
|
1689
|
-
await caughtUp(room, local, progress);
|
|
2569
|
+
await caughtUp(room, local, progress, live);
|
|
1690
2570
|
}
|
|
1691
|
-
async function reload(room, local, progress) {
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
2571
|
+
async function reload(room, local, progress, live) {
|
|
2572
|
+
try {
|
|
2573
|
+
if (!live() || authorization === undefined)
|
|
2574
|
+
return;
|
|
2575
|
+
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
2576
|
+
if (!live())
|
|
2577
|
+
return;
|
|
2578
|
+
if (bootstrapped.kind !== 'staged') {
|
|
2579
|
+
await withhold(bootstrapped, room, local, progress);
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2582
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
2583
|
+
await caughtUp(room, local, progress, live);
|
|
2584
|
+
}
|
|
2585
|
+
finally {
|
|
2586
|
+
caughtUpTo(room.locator);
|
|
1700
2587
|
}
|
|
1701
|
-
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
1702
|
-
await caughtUp(room, local, progress);
|
|
1703
2588
|
}
|
|
1704
|
-
async function discoverRooms(local, progress, attempt) {
|
|
2589
|
+
async function discoverRooms(local, progress, attempt, live) {
|
|
1705
2590
|
if (closed || authorization === undefined) {
|
|
1706
2591
|
notifyReady();
|
|
1707
2592
|
return;
|
|
1708
2593
|
}
|
|
1709
2594
|
moveTo(transferring(status));
|
|
2595
|
+
discoveries += 1;
|
|
2596
|
+
const started = discoveries;
|
|
1710
2597
|
let result;
|
|
1711
2598
|
try {
|
|
1712
2599
|
result = await options.adapter.discover(authorization);
|
|
@@ -1714,7 +2601,8 @@ export function composeRuntime(options) {
|
|
|
1714
2601
|
catch {
|
|
1715
2602
|
result = { kind: 'transport-uncertainty' };
|
|
1716
2603
|
}
|
|
1717
|
-
if (
|
|
2604
|
+
if (!live()) {
|
|
2605
|
+
progress.complete = false;
|
|
1718
2606
|
notifyReady();
|
|
1719
2607
|
return;
|
|
1720
2608
|
}
|
|
@@ -1772,6 +2660,7 @@ export function composeRuntime(options) {
|
|
|
1772
2660
|
}
|
|
1773
2661
|
const previousCapabilities = capabilities;
|
|
1774
2662
|
capabilities = result.capabilities;
|
|
2663
|
+
outcomeRetention = result.retention.outcome;
|
|
1775
2664
|
notifyReady();
|
|
1776
2665
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1777
2666
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
@@ -1784,8 +2673,7 @@ export function composeRuntime(options) {
|
|
|
1784
2673
|
member = signedIn;
|
|
1785
2674
|
emitMember();
|
|
1786
2675
|
}
|
|
1787
|
-
await applyDiscovery(result.rooms, local);
|
|
1788
|
-
await synchronize(result.rooms, local, progress, attempt);
|
|
2676
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
1789
2677
|
}
|
|
1790
2678
|
async function ensureAuthorization() {
|
|
1791
2679
|
if (authorization !== undefined)
|
|
@@ -1799,20 +2687,50 @@ export function composeRuntime(options) {
|
|
|
1799
2687
|
throw error;
|
|
1800
2688
|
}
|
|
1801
2689
|
}
|
|
1802
|
-
function
|
|
2690
|
+
async function admittedRead(work) {
|
|
2691
|
+
if (roomReads.generation !== generation) {
|
|
2692
|
+
roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
2693
|
+
}
|
|
2694
|
+
const reads = roomReads;
|
|
2695
|
+
while (reads.free === 0)
|
|
2696
|
+
await new Promise((admit) => reads.waiting.push(admit));
|
|
2697
|
+
reads.free -= 1;
|
|
2698
|
+
try {
|
|
2699
|
+
await work();
|
|
2700
|
+
}
|
|
2701
|
+
finally {
|
|
2702
|
+
reads.free += 1;
|
|
2703
|
+
reads.waiting.shift()?.();
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
function roomTask(locator, work) {
|
|
2707
|
+
return task(roomLane(locator), (live) => admittedRead(() => work(live)));
|
|
2708
|
+
}
|
|
2709
|
+
function task(lane, work) {
|
|
1803
2710
|
if (stopping)
|
|
1804
|
-
return
|
|
1805
|
-
|
|
1806
|
-
|
|
2711
|
+
return Promise.resolve();
|
|
2712
|
+
const held = lanes.get(lane);
|
|
2713
|
+
const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
|
|
2714
|
+
const turn = current.generation;
|
|
2715
|
+
const live = () => turn === generation && continuing();
|
|
2716
|
+
const run = current.chain.then(() => work(live)).catch(() => undefined);
|
|
2717
|
+
current.chain = run;
|
|
2718
|
+
lanes.set(lane, current);
|
|
2719
|
+
working.add(run);
|
|
2720
|
+
void run.finally(() => working.delete(run));
|
|
2721
|
+
return run;
|
|
1807
2722
|
}
|
|
1808
2723
|
function sync(attempt) {
|
|
1809
|
-
return task(async () => {
|
|
1810
|
-
if (
|
|
2724
|
+
return task(OPENING_LANE, async (live) => {
|
|
2725
|
+
if (!live()) {
|
|
1811
2726
|
notifyReady();
|
|
1812
2727
|
return;
|
|
1813
2728
|
}
|
|
1814
2729
|
const progress = { complete: true, reachable: true };
|
|
1815
|
-
|
|
2730
|
+
const gate = discover();
|
|
2731
|
+
if (attempt === 'opened')
|
|
2732
|
+
opened = generation;
|
|
2733
|
+
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
1816
2734
|
if (attempt === 'opened')
|
|
1817
2735
|
streamed = progress;
|
|
1818
2736
|
complete = false;
|
|
@@ -1821,12 +2739,12 @@ export function composeRuntime(options) {
|
|
|
1821
2739
|
handed = false;
|
|
1822
2740
|
try {
|
|
1823
2741
|
const local = await openLocal();
|
|
1824
|
-
if (
|
|
2742
|
+
if (!live())
|
|
1825
2743
|
return;
|
|
1826
2744
|
await ensureAuthorization();
|
|
1827
|
-
if (
|
|
2745
|
+
if (!live())
|
|
1828
2746
|
return;
|
|
1829
|
-
await discoverRooms(local, progress, attempt);
|
|
2747
|
+
await discoverRooms(local, progress, attempt, live);
|
|
1830
2748
|
}
|
|
1831
2749
|
catch {
|
|
1832
2750
|
progress.complete = false;
|
|
@@ -1834,23 +2752,61 @@ export function composeRuntime(options) {
|
|
|
1834
2752
|
progress.reachable = false;
|
|
1835
2753
|
}
|
|
1836
2754
|
finally {
|
|
2755
|
+
gate.release();
|
|
1837
2756
|
notifyReady();
|
|
1838
2757
|
}
|
|
2758
|
+
if (!live())
|
|
2759
|
+
return;
|
|
1839
2760
|
if (attempt !== 'opened') {
|
|
1840
|
-
await finishOpening(progress);
|
|
2761
|
+
await finishOpening(progress, live);
|
|
1841
2762
|
return;
|
|
1842
2763
|
}
|
|
1843
2764
|
if (handed || closed || progress.reachable)
|
|
1844
2765
|
return;
|
|
1845
2766
|
underway = null;
|
|
2767
|
+
opened = null;
|
|
1846
2768
|
journal.record('connection reopen');
|
|
1847
|
-
await rest(progress);
|
|
2769
|
+
await rest(progress, live);
|
|
1848
2770
|
options.adapter.roomStream?.reopen();
|
|
1849
2771
|
});
|
|
1850
2772
|
}
|
|
1851
|
-
function
|
|
2773
|
+
function rediscover() {
|
|
2774
|
+
return task(OPENING_LANE, async (live) => {
|
|
2775
|
+
if (!live() || !socketOpen)
|
|
2776
|
+
return;
|
|
2777
|
+
const progress = { complete: true, reachable: true };
|
|
2778
|
+
const gate = discover();
|
|
2779
|
+
moveTo(transferring(status));
|
|
2780
|
+
try {
|
|
2781
|
+
const local = await openLocal();
|
|
2782
|
+
if (!live())
|
|
2783
|
+
return;
|
|
2784
|
+
await ensureAuthorization();
|
|
2785
|
+
if (!live())
|
|
2786
|
+
return;
|
|
2787
|
+
await discoverRooms(local, progress, 'authorized', live);
|
|
2788
|
+
}
|
|
2789
|
+
catch {
|
|
2790
|
+
progress.complete = false;
|
|
2791
|
+
}
|
|
2792
|
+
finally {
|
|
2793
|
+
gate.release();
|
|
2794
|
+
notifyReady();
|
|
2795
|
+
}
|
|
2796
|
+
if (!live())
|
|
2797
|
+
return;
|
|
2798
|
+
if (!progress.complete)
|
|
2799
|
+
complete = false;
|
|
2800
|
+
await rest(progress, live);
|
|
2801
|
+
});
|
|
2802
|
+
}
|
|
2803
|
+
function closedSocket(revoked, lost) {
|
|
1852
2804
|
const opened = socketOpen;
|
|
1853
2805
|
socketOpen = false;
|
|
2806
|
+
if (lost) {
|
|
2807
|
+
reads.lost();
|
|
2808
|
+
conversations.lost();
|
|
2809
|
+
}
|
|
1854
2810
|
if (revoked) {
|
|
1855
2811
|
void refuse('revoked-authorization');
|
|
1856
2812
|
return;
|
|
@@ -1865,24 +2821,41 @@ export function composeRuntime(options) {
|
|
|
1865
2821
|
});
|
|
1866
2822
|
}
|
|
1867
2823
|
function applyOpening(expired) {
|
|
1868
|
-
|
|
1869
|
-
|
|
2824
|
+
const progress = underway?.generation === generation ? underway.progress : null;
|
|
2825
|
+
const cycle = progress ?? { complete: true, reachable: true };
|
|
2826
|
+
const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
|
|
2827
|
+
if (opened === generation)
|
|
2828
|
+
opened = null;
|
|
2829
|
+
return task(OPENING_LANE, async (live) => {
|
|
1870
2830
|
underway = null;
|
|
1871
|
-
if (
|
|
2831
|
+
if (!live())
|
|
1872
2832
|
return;
|
|
1873
2833
|
if (progress === null) {
|
|
1874
|
-
await
|
|
2834
|
+
await replacing;
|
|
2835
|
+
await rest(cycle, live);
|
|
1875
2836
|
return;
|
|
1876
2837
|
}
|
|
2838
|
+
await roomsCarried(expired);
|
|
2839
|
+
if (!live())
|
|
2840
|
+
return;
|
|
2841
|
+
const replaced = new Set(owed);
|
|
2842
|
+
owed.clear();
|
|
1877
2843
|
try {
|
|
1878
2844
|
const local = await openLocal();
|
|
1879
2845
|
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
1880
|
-
const reloading = held.filter((room) => expired.includes(room.locator)
|
|
1881
|
-
|
|
2846
|
+
const reloading = held.filter((room) => !expired.includes(room.locator) &&
|
|
2847
|
+
(replaced.has(room.locator) || revoked(room.locator)));
|
|
2848
|
+
for (const room of reloading)
|
|
2849
|
+
catching.add(room.locator);
|
|
2850
|
+
readSide.recover();
|
|
2851
|
+
const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
|
|
2852
|
+
const reloaded = [...(await reloads), ...(await replacing)];
|
|
1882
2853
|
if (progress.reachable) {
|
|
1883
|
-
for (const room of held)
|
|
1884
|
-
if (!reloading.includes(room))
|
|
2854
|
+
for (const room of held) {
|
|
2855
|
+
if (!expired.includes(room.locator) && !reloading.includes(room)) {
|
|
1885
2856
|
await confirmModels(room, local);
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
1886
2859
|
}
|
|
1887
2860
|
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1888
2861
|
if (failed !== undefined)
|
|
@@ -1892,21 +2865,21 @@ export function composeRuntime(options) {
|
|
|
1892
2865
|
progress.complete = false;
|
|
1893
2866
|
progress.reachable = false;
|
|
1894
2867
|
}
|
|
1895
|
-
|
|
2868
|
+
if (live())
|
|
2869
|
+
await finishOpening(progress, live);
|
|
1896
2870
|
});
|
|
1897
2871
|
}
|
|
1898
|
-
async function
|
|
1899
|
-
const progress = { complete: true, reachable: true };
|
|
2872
|
+
async function reloadNow(replaced, progress) {
|
|
1900
2873
|
try {
|
|
1901
2874
|
const local = await openLocal();
|
|
1902
|
-
await Promise.allSettled([...discovered.values()]
|
|
1903
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
1904
|
-
.map((room) => reload(room, local, progress)));
|
|
2875
|
+
return await Promise.allSettled([...discovered.values()]
|
|
2876
|
+
.filter((room) => replicas.has(room.locator) && replaced(room))
|
|
2877
|
+
.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
1905
2878
|
}
|
|
1906
2879
|
catch {
|
|
1907
2880
|
progress.reachable = false;
|
|
2881
|
+
return [];
|
|
1908
2882
|
}
|
|
1909
|
-
await rest(progress);
|
|
1910
2883
|
}
|
|
1911
2884
|
async function confirmModels(room, local) {
|
|
1912
2885
|
for (const attached of ['collection', 'typed-selection']) {
|
|
@@ -1919,38 +2892,94 @@ export function composeRuntime(options) {
|
|
|
1919
2892
|
function submitLater(locator) {
|
|
1920
2893
|
if (localSaves)
|
|
1921
2894
|
return;
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
2895
|
+
void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
|
|
2896
|
+
}
|
|
2897
|
+
async function submitPass(locator, signal) {
|
|
2898
|
+
const room = discovered.get(locator);
|
|
2899
|
+
if (closed || stopping || discoveryPaused || room === undefined)
|
|
2900
|
+
return 'done';
|
|
2901
|
+
const progress = { complete: true, reachable: true };
|
|
2902
|
+
let next;
|
|
2903
|
+
try {
|
|
2904
|
+
await ensureAuthorization();
|
|
2905
|
+
if (authorization === undefined)
|
|
2906
|
+
return 'done';
|
|
2907
|
+
next = await submitRoom(room, await openLocal(), authorization, progress, signal);
|
|
2908
|
+
}
|
|
2909
|
+
catch {
|
|
2910
|
+
progress.reachable = false;
|
|
2911
|
+
next = 'again';
|
|
2912
|
+
}
|
|
2913
|
+
if (signal.aborted)
|
|
2914
|
+
return 'done';
|
|
2915
|
+
await rest(progress);
|
|
2916
|
+
return next === 'again' && !socketOpen ? 'done' : next;
|
|
2917
|
+
}
|
|
2918
|
+
function repairLater(locator, when) {
|
|
2919
|
+
if (!socketOpen || repairing.has(locator))
|
|
1925
2920
|
return;
|
|
2921
|
+
void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
|
|
2922
|
+
}
|
|
2923
|
+
async function repairPass(locator, signal) {
|
|
2924
|
+
const room = discovered.get(locator);
|
|
2925
|
+
if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
|
|
2926
|
+
return 'done';
|
|
2927
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2928
|
+
return 'done';
|
|
2929
|
+
const progress = { complete: true, reachable: true };
|
|
2930
|
+
repairing.add(locator);
|
|
2931
|
+
try {
|
|
2932
|
+
const local = await openLocal();
|
|
2933
|
+
await roomTask(locator, async (live) => {
|
|
2934
|
+
const current = () => live() && !signal.aborted;
|
|
2935
|
+
if (!current() || authorization === undefined)
|
|
2936
|
+
return;
|
|
2937
|
+
const withheld = await Promise.race([
|
|
2938
|
+
advance(room, local, authorization, current),
|
|
2939
|
+
abandonedOn(signal),
|
|
2940
|
+
]);
|
|
2941
|
+
if (withheld !== null && current())
|
|
2942
|
+
await withhold(withheld, room, local, progress);
|
|
2943
|
+
});
|
|
1926
2944
|
}
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
2945
|
+
catch {
|
|
2946
|
+
stale.add(locator);
|
|
2947
|
+
progress.reachable = false;
|
|
2948
|
+
}
|
|
2949
|
+
finally {
|
|
2950
|
+
repairing.delete(locator);
|
|
2951
|
+
}
|
|
2952
|
+
if (signal.aborted)
|
|
2953
|
+
return 'done';
|
|
2954
|
+
await rest(progress);
|
|
2955
|
+
if (!stale.has(locator)) {
|
|
2956
|
+
if ((replicas.get(locator)?.queue.length ?? 0) > 0)
|
|
2957
|
+
submitLater(locator);
|
|
2958
|
+
return 'done';
|
|
2959
|
+
}
|
|
2960
|
+
if (!repairable.has(locator))
|
|
2961
|
+
return 'done';
|
|
2962
|
+
const reset = repairable.get(locator);
|
|
2963
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
2964
|
+
}
|
|
2965
|
+
function resendLater() {
|
|
2966
|
+
if (!socketOpen)
|
|
2967
|
+
return;
|
|
2968
|
+
void recovery.attempt('resend', resendPass, { again: true });
|
|
2969
|
+
}
|
|
2970
|
+
async function resendPass(signal) {
|
|
2971
|
+
if (!socketOpen || closed || stopping || discoveryPaused)
|
|
2972
|
+
return 'done';
|
|
2973
|
+
const reading = { complete: true, reachable: true };
|
|
2974
|
+
try {
|
|
2975
|
+
await conversations.resend(reading);
|
|
2976
|
+
}
|
|
2977
|
+
catch {
|
|
2978
|
+
reading.reachable = false;
|
|
2979
|
+
}
|
|
2980
|
+
if (signal.aborted)
|
|
2981
|
+
return 'done';
|
|
2982
|
+
return reading.reachable && reading.complete ? 'done' : 'again';
|
|
1954
2983
|
}
|
|
1955
2984
|
function submitAll() {
|
|
1956
2985
|
for (const [locator, replica] of replicas) {
|
|
@@ -1958,31 +2987,44 @@ export function composeRuntime(options) {
|
|
|
1958
2987
|
submitLater(locator);
|
|
1959
2988
|
}
|
|
1960
2989
|
}
|
|
1961
|
-
async function finishOpening(progress) {
|
|
2990
|
+
async function finishOpening(progress, live) {
|
|
1962
2991
|
try {
|
|
1963
|
-
if (
|
|
2992
|
+
if (!live()) {
|
|
1964
2993
|
progress.complete = false;
|
|
1965
2994
|
return;
|
|
1966
2995
|
}
|
|
2996
|
+
reads.applied();
|
|
1967
2997
|
submitAll();
|
|
2998
|
+
for (const locator of stale)
|
|
2999
|
+
if (repairable.has(locator))
|
|
3000
|
+
repairLater(locator, {});
|
|
1968
3001
|
await conversations.reconcile();
|
|
1969
3002
|
if (status.providerUnreachable)
|
|
1970
3003
|
conversations.reconnected();
|
|
1971
3004
|
const reading = { complete: true, reachable: true };
|
|
1972
|
-
|
|
1973
|
-
|
|
3005
|
+
if (demand.conversations)
|
|
3006
|
+
await conversations.catchUp(reading);
|
|
3007
|
+
if (progress.reachable && reading.reachable)
|
|
3008
|
+
await conversations.resend(reading);
|
|
3009
|
+
if (progress.reachable)
|
|
3010
|
+
void recovery.attempt('calls', resendCalls);
|
|
3011
|
+
if (!reading.reachable) {
|
|
1974
3012
|
progress.reachable = false;
|
|
3013
|
+
resendLater();
|
|
3014
|
+
}
|
|
1975
3015
|
}
|
|
1976
3016
|
catch {
|
|
1977
3017
|
progress.complete = false;
|
|
1978
3018
|
}
|
|
1979
3019
|
finally {
|
|
1980
|
-
|
|
1981
|
-
|
|
3020
|
+
if (live()) {
|
|
3021
|
+
complete = progress.complete;
|
|
3022
|
+
await rest(progress, live);
|
|
3023
|
+
}
|
|
1982
3024
|
}
|
|
1983
3025
|
}
|
|
1984
|
-
async function rest(progress) {
|
|
1985
|
-
if (
|
|
3026
|
+
async function rest(progress, live = () => !closed) {
|
|
3027
|
+
if (!live())
|
|
1986
3028
|
return;
|
|
1987
3029
|
let lastCompleted = status.lastCompleted;
|
|
1988
3030
|
let synced = complete && stale.size === 0 && !pending();
|
|
@@ -1990,6 +3032,8 @@ export function composeRuntime(options) {
|
|
|
1990
3032
|
const completedAt = new Date().toISOString();
|
|
1991
3033
|
try {
|
|
1992
3034
|
await (await openLocal()).complete(completedAt);
|
|
3035
|
+
if (!live())
|
|
3036
|
+
return;
|
|
1993
3037
|
recorded = true;
|
|
1994
3038
|
lastCompleted = completedAt;
|
|
1995
3039
|
}
|
|
@@ -2004,40 +3048,68 @@ export function composeRuntime(options) {
|
|
|
2004
3048
|
...attention(),
|
|
2005
3049
|
});
|
|
2006
3050
|
}
|
|
3051
|
+
function discover() {
|
|
3052
|
+
let release = () => { };
|
|
3053
|
+
const done = new Promise((resolve) => {
|
|
3054
|
+
release = resolve;
|
|
3055
|
+
});
|
|
3056
|
+
discovering = { generation, done, release };
|
|
3057
|
+
return discovering;
|
|
3058
|
+
}
|
|
2007
3059
|
function applyRoom(locator, roomVersion) {
|
|
2008
3060
|
announced.set(locator, roomVersion);
|
|
2009
|
-
return task(async () => {
|
|
2010
|
-
|
|
2011
|
-
|
|
3061
|
+
return task(roomLane(locator), async (live) => {
|
|
3062
|
+
if (discovering?.generation === generation)
|
|
3063
|
+
await discovering.done;
|
|
3064
|
+
const saving = answering.get(locator);
|
|
3065
|
+
if (saving?.generation === generation)
|
|
3066
|
+
await saving.answered;
|
|
3067
|
+
if (!live() || authorization === undefined)
|
|
2012
3068
|
return;
|
|
2013
3069
|
const room = discovered.get(locator);
|
|
2014
3070
|
const held = replicas.get(locator);
|
|
2015
|
-
if (room === undefined || room.replica.admission !== 'admitted')
|
|
3071
|
+
if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
|
|
2016
3072
|
return;
|
|
2017
3073
|
if (held !== undefined &&
|
|
2018
3074
|
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
2019
3075
|
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
2020
3076
|
return;
|
|
2021
3077
|
}
|
|
2022
|
-
const during = underway;
|
|
3078
|
+
const during = underway?.generation === generation ? underway.progress : null;
|
|
2023
3079
|
const progress = during ?? { complete: true, reachable: true };
|
|
2024
3080
|
if (during === null)
|
|
2025
3081
|
moveTo(transferring(status));
|
|
2026
3082
|
try {
|
|
2027
3083
|
const local = await openLocal();
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
3084
|
+
await admittedRead(async () => {
|
|
3085
|
+
if (!live() || authorization === undefined)
|
|
3086
|
+
return;
|
|
3087
|
+
const withheld = await advance(room, local, authorization, live);
|
|
3088
|
+
if (withheld !== null)
|
|
3089
|
+
await withhold(withheld, room, local, progress);
|
|
3090
|
+
});
|
|
2031
3091
|
}
|
|
2032
3092
|
catch {
|
|
2033
3093
|
stale.add(locator);
|
|
2034
3094
|
}
|
|
2035
3095
|
finally {
|
|
2036
3096
|
if (during === null)
|
|
2037
|
-
await rest(progress);
|
|
3097
|
+
await rest(progress, live);
|
|
2038
3098
|
}
|
|
2039
3099
|
});
|
|
2040
3100
|
}
|
|
3101
|
+
function roomsCarried(replacing) {
|
|
3102
|
+
const replaced = new Set(replacing.map(roomLane));
|
|
3103
|
+
return Promise.all([...lanes]
|
|
3104
|
+
.filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
|
|
3105
|
+
.map(([, held]) => held.chain));
|
|
3106
|
+
}
|
|
3107
|
+
const receipts = {
|
|
3108
|
+
heads: async () => (await openLocal()).receivedHeads(),
|
|
3109
|
+
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
3110
|
+
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
3111
|
+
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
3112
|
+
};
|
|
2041
3113
|
function observeStream() {
|
|
2042
3114
|
const stream = options.adapter.roomStream;
|
|
2043
3115
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -2047,15 +3119,21 @@ export function composeRuntime(options) {
|
|
|
2047
3119
|
return;
|
|
2048
3120
|
if (event.kind === 'opened') {
|
|
2049
3121
|
socketOpen = true;
|
|
3122
|
+
generation += 1;
|
|
3123
|
+
reads.opened();
|
|
3124
|
+
conversations.reopened();
|
|
3125
|
+
recovery.resume();
|
|
2050
3126
|
void sync('opened');
|
|
2051
3127
|
}
|
|
2052
3128
|
else if (event.kind === 'closed')
|
|
2053
|
-
closedSocket(event.revoked);
|
|
3129
|
+
closedSocket(event.revoked, event.lost);
|
|
2054
3130
|
else if (event.kind === 'opening')
|
|
2055
3131
|
void applyOpening(event.expired);
|
|
3132
|
+
else if (event.kind === 'authority-changed')
|
|
3133
|
+
void rediscover();
|
|
2056
3134
|
else
|
|
2057
3135
|
void applyRoom(event.locator, event.roomVersion);
|
|
2058
|
-
});
|
|
3136
|
+
}, receipts);
|
|
2059
3137
|
}
|
|
2060
3138
|
function writesHeld() {
|
|
2061
3139
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -2070,6 +3148,7 @@ export function composeRuntime(options) {
|
|
|
2070
3148
|
haltedRefusal = false;
|
|
2071
3149
|
if (mode !== serviceMode) {
|
|
2072
3150
|
serviceMode = mode;
|
|
3151
|
+
queueReadiness();
|
|
2073
3152
|
journal.record(`service mode ${mode}`);
|
|
2074
3153
|
for (const listener of serviceModeListeners)
|
|
2075
3154
|
listener(mode);
|
|
@@ -2082,6 +3161,8 @@ export function composeRuntime(options) {
|
|
|
2082
3161
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
2083
3162
|
return;
|
|
2084
3163
|
withdrawConversations = stream.observe((event) => {
|
|
3164
|
+
if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
|
|
3165
|
+
queueReadiness();
|
|
2085
3166
|
if (conversations.receive(event)) {
|
|
2086
3167
|
void ready.then(scheduleCurrency);
|
|
2087
3168
|
return;
|
|
@@ -2093,17 +3174,18 @@ export function composeRuntime(options) {
|
|
|
2093
3174
|
});
|
|
2094
3175
|
}
|
|
2095
3176
|
function scheduleCurrency() {
|
|
2096
|
-
if (currencyQueued || closed || stopping)
|
|
3177
|
+
if (currencyQueued === generation || closed || stopping)
|
|
2097
3178
|
return;
|
|
2098
|
-
currencyQueued =
|
|
2099
|
-
void task(runCurrency);
|
|
3179
|
+
currencyQueued = generation;
|
|
3180
|
+
void task(CONVERSATION_LANE, runCurrency);
|
|
2100
3181
|
}
|
|
2101
|
-
async function runCurrency() {
|
|
2102
|
-
currencyQueued
|
|
2103
|
-
|
|
3182
|
+
async function runCurrency(live) {
|
|
3183
|
+
if (currencyQueued === generation)
|
|
3184
|
+
currencyQueued = null;
|
|
3185
|
+
if (!live())
|
|
2104
3186
|
return;
|
|
2105
3187
|
await conversations.reconcile();
|
|
2106
|
-
if (conversations.current()) {
|
|
3188
|
+
if (conversations.current() || !demand.conversations) {
|
|
2107
3189
|
show();
|
|
2108
3190
|
return;
|
|
2109
3191
|
}
|
|
@@ -2115,19 +3197,24 @@ export function composeRuntime(options) {
|
|
|
2115
3197
|
catch {
|
|
2116
3198
|
progress.reachable = false;
|
|
2117
3199
|
}
|
|
2118
|
-
await rest(progress);
|
|
3200
|
+
await rest(progress, live);
|
|
2119
3201
|
}
|
|
2120
3202
|
function close() {
|
|
2121
3203
|
if (closed)
|
|
2122
3204
|
return;
|
|
2123
3205
|
closed = true;
|
|
3206
|
+
if (narrowing !== null)
|
|
3207
|
+
clearTimeout(narrowing);
|
|
3208
|
+
if (rechecking !== null)
|
|
3209
|
+
clearInterval(rechecking);
|
|
3210
|
+
readinessWatches.clear();
|
|
2124
3211
|
withdrawStream?.();
|
|
2125
3212
|
withdrawStream = undefined;
|
|
2126
3213
|
withdrawConversations?.();
|
|
2127
3214
|
withdrawConversations = undefined;
|
|
2128
3215
|
withdrawServiceMode?.();
|
|
2129
3216
|
conversations.close();
|
|
2130
|
-
|
|
3217
|
+
void recovery.retire();
|
|
2131
3218
|
store?.close();
|
|
2132
3219
|
void opening?.then((opened) => {
|
|
2133
3220
|
opened.close();
|
|
@@ -2145,9 +3232,8 @@ export function composeRuntime(options) {
|
|
|
2145
3232
|
}
|
|
2146
3233
|
async function stop() {
|
|
2147
3234
|
stopping = true;
|
|
2148
|
-
|
|
2149
|
-
await
|
|
2150
|
-
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
3235
|
+
await recovery.retire();
|
|
3236
|
+
await Promise.all(working);
|
|
2151
3237
|
await mutations;
|
|
2152
3238
|
close();
|
|
2153
3239
|
}
|
|
@@ -2166,20 +3252,111 @@ export function composeRuntime(options) {
|
|
|
2166
3252
|
if (options.adapter.roomStream === undefined)
|
|
2167
3253
|
void sync('unstreamed');
|
|
2168
3254
|
function planned(input, reach) {
|
|
2169
|
-
|
|
3255
|
+
const plan = planOf(input, roomFacts(reach), planReach(reach));
|
|
3256
|
+
const named = plan.paths !== null &&
|
|
3257
|
+
plan.rooms !== null &&
|
|
3258
|
+
plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
|
|
3259
|
+
return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
|
|
2170
3260
|
}
|
|
2171
3261
|
function watchPlan(plan, deliver) {
|
|
2172
|
-
|
|
2173
|
-
|
|
3262
|
+
let seeded = false;
|
|
3263
|
+
let pending = true;
|
|
3264
|
+
const shown = () => {
|
|
3265
|
+
const seed = seeds.get(plan.key);
|
|
3266
|
+
if (seed === undefined || seeded || !pending)
|
|
3267
|
+
return;
|
|
3268
|
+
seeded = true;
|
|
3269
|
+
deliver(seededResult(seed));
|
|
3270
|
+
};
|
|
3271
|
+
const watchers = seedWatchers.get(plan.key) ?? new Set();
|
|
3272
|
+
seedWatchers.set(plan.key, watchers);
|
|
3273
|
+
watchers.add(shown);
|
|
3274
|
+
const stop = readSide.watch(plan, identityOf(plan), () => {
|
|
3275
|
+
const cover = covered(plan);
|
|
3276
|
+
awaitPreparation(cover);
|
|
3277
|
+
return seedBehind(plan, cover);
|
|
3278
|
+
}, (message) => {
|
|
3279
|
+
const answered = message;
|
|
3280
|
+
pending = whole(answered) && 'pending' in answered.result;
|
|
3281
|
+
if (pending && seeds.has(plan.key)) {
|
|
3282
|
+
shown();
|
|
3283
|
+
return;
|
|
3284
|
+
}
|
|
3285
|
+
seeded = false;
|
|
3286
|
+
deliver(message);
|
|
3287
|
+
});
|
|
3288
|
+
return () => {
|
|
3289
|
+
watchers.delete(shown);
|
|
3290
|
+
if (watchers.size === 0)
|
|
3291
|
+
seedWatchers.delete(plan.key);
|
|
3292
|
+
stop();
|
|
3293
|
+
};
|
|
2174
3294
|
}
|
|
2175
3295
|
async function searched(input, reach) {
|
|
2176
|
-
const
|
|
2177
|
-
const
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
3296
|
+
const asked = searchPlanOf(input);
|
|
3297
|
+
const plan = { ...asked, limit: capped(asked.limit) };
|
|
3298
|
+
const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
|
|
3299
|
+
for (;;) {
|
|
3300
|
+
const cover = covered(filter);
|
|
3301
|
+
awaitPreparation(cover);
|
|
3302
|
+
const pending = 'pending' in cover ? [...cover.pending] : [];
|
|
3303
|
+
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
3304
|
+
const held = covered({
|
|
3305
|
+
...filter,
|
|
3306
|
+
rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
|
|
3307
|
+
});
|
|
3308
|
+
const covering = 'pending' in held ? [] : held.coverage.rooms;
|
|
3309
|
+
const unindexed = covering
|
|
3310
|
+
.filter(({ room }) => !searchIndex.rooms().has(room))
|
|
3311
|
+
.map(({ room }) => room);
|
|
3312
|
+
for (const room of unindexed)
|
|
3313
|
+
pending.push({ room, awaits: 'index' });
|
|
3314
|
+
const listed = conversations.searchable();
|
|
3315
|
+
const named = reach !== null && !reach.conversations
|
|
3316
|
+
? []
|
|
3317
|
+
: (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
|
|
3318
|
+
const waiting = named.filter((one) => !searchIndex.conversations().has(one));
|
|
3319
|
+
for (const conversation of waiting)
|
|
3320
|
+
pending.push({ conversation, awaits: 'index' });
|
|
3321
|
+
if (unindexed.length > 0 || waiting.length > 0)
|
|
3322
|
+
index(unindexed, waiting);
|
|
3323
|
+
const complete = pending.length === 0 && !('pending' in cover);
|
|
3324
|
+
const answering = complete
|
|
3325
|
+
? filter
|
|
3326
|
+
: {
|
|
3327
|
+
...filter,
|
|
3328
|
+
rooms: covering
|
|
3329
|
+
.filter(({ room }) => searchIndex.rooms().has(room))
|
|
3330
|
+
.map(({ room }) => room),
|
|
3331
|
+
};
|
|
3332
|
+
const answered = covered(answering);
|
|
3333
|
+
if ('pending' in answered)
|
|
3334
|
+
continue;
|
|
3335
|
+
const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
|
|
3336
|
+
const seen = searchIndex.changes();
|
|
3337
|
+
await searchIndex.caughtUp();
|
|
3338
|
+
const selected = roomsAlone(answering) ? null : await rowsQuery(answering);
|
|
3339
|
+
if (selected !== null && 'pending' in selected)
|
|
3340
|
+
continue;
|
|
3341
|
+
if (searchIndex.changes() !== seen ||
|
|
3342
|
+
JSON.stringify(covered(answering)) !== JSON.stringify(answered))
|
|
3343
|
+
continue;
|
|
3344
|
+
const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms === null ? null : new Set(answering.rooms), plan.collection
|
|
3345
|
+
? new Map(answered.coverage.rooms.map(({ room }) => [
|
|
3346
|
+
room,
|
|
3347
|
+
replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
|
|
3348
|
+
]))
|
|
3349
|
+
: null);
|
|
3350
|
+
const coverage = {
|
|
3351
|
+
rooms: answered.coverage.rooms,
|
|
3352
|
+
conversations: searchedConversations,
|
|
3353
|
+
index: searchIndex.generation(),
|
|
3354
|
+
overlay: answered.coverage.overlay,
|
|
3355
|
+
};
|
|
3356
|
+
return complete
|
|
3357
|
+
? { ...hits, coverage }
|
|
3358
|
+
: { pending, documents: hits.documents, messages: hits.messages, coverage };
|
|
3359
|
+
}
|
|
2183
3360
|
}
|
|
2184
3361
|
const watch = (topic, input, deliver) => {
|
|
2185
3362
|
if (topic === 'room-drafts') {
|
|
@@ -2225,6 +3402,9 @@ export function composeRuntime(options) {
|
|
|
2225
3402
|
presentationListeners.delete(listener);
|
|
2226
3403
|
};
|
|
2227
3404
|
}
|
|
3405
|
+
if (topic === 'readiness') {
|
|
3406
|
+
return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
|
|
3407
|
+
}
|
|
2228
3408
|
if (topic === 'rooms') {
|
|
2229
3409
|
roomListeners.add(deliver);
|
|
2230
3410
|
deliver(listed());
|
|
@@ -2360,13 +3540,13 @@ export function composeRuntime(options) {
|
|
|
2360
3540
|
});
|
|
2361
3541
|
},
|
|
2362
3542
|
write(app, value) {
|
|
2363
|
-
return
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
3543
|
+
return keepPresentation(app, () => structuredClone(value));
|
|
3544
|
+
},
|
|
3545
|
+
edit(app, members) {
|
|
3546
|
+
return keepPresentation(app, (held) => Object.fromEntries([
|
|
3547
|
+
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(members, member)),
|
|
3548
|
+
...Object.entries(structuredClone(members)).filter(([, value]) => value !== null),
|
|
3549
|
+
]));
|
|
2370
3550
|
},
|
|
2371
3551
|
},
|
|
2372
3552
|
save(room, named, writtenBy) {
|
|
@@ -2377,10 +3557,11 @@ export function composeRuntime(options) {
|
|
|
2377
3557
|
},
|
|
2378
3558
|
binaryFile: {
|
|
2379
3559
|
read: readBinaryPath,
|
|
3560
|
+
link: linkBinaryPath,
|
|
2380
3561
|
readReference: readBinaryReference,
|
|
2381
3562
|
},
|
|
2382
3563
|
call(name, input) {
|
|
2383
|
-
return
|
|
3564
|
+
return recoveredAction(name, input);
|
|
2384
3565
|
},
|
|
2385
3566
|
reached(locator, roomVersion, path) {
|
|
2386
3567
|
return path === undefined
|
|
@@ -2391,21 +3572,29 @@ export function composeRuntime(options) {
|
|
|
2391
3572
|
return journal.export();
|
|
2392
3573
|
},
|
|
2393
3574
|
persistence() {
|
|
2394
|
-
return
|
|
3575
|
+
return persistence === 'granted';
|
|
3576
|
+
},
|
|
3577
|
+
async retry() {
|
|
3578
|
+
journal.record('retry');
|
|
3579
|
+
unindexable.clear();
|
|
3580
|
+
queueReadiness();
|
|
3581
|
+
resume();
|
|
2395
3582
|
},
|
|
2396
3583
|
observe: observing(watch),
|
|
2397
3584
|
async reauthorize() {
|
|
2398
3585
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2399
3586
|
return;
|
|
2400
3587
|
journal.record('connection reauthorize');
|
|
2401
|
-
await
|
|
3588
|
+
await Promise.all([...lanes.values()]
|
|
3589
|
+
.filter((held) => held.generation === generation)
|
|
3590
|
+
.map((held) => held.chain));
|
|
2402
3591
|
await mutations;
|
|
2403
3592
|
authorization = undefined;
|
|
2404
3593
|
discoveryPaused = false;
|
|
2405
3594
|
if (options.adapter.roomStream === undefined)
|
|
2406
3595
|
await sync('unstreamed');
|
|
2407
3596
|
else
|
|
2408
|
-
await task(ensureAuthorization);
|
|
3597
|
+
await task(OPENING_LANE, ensureAuthorization);
|
|
2409
3598
|
},
|
|
2410
3599
|
};
|
|
2411
3600
|
const reachable = {
|
|
@@ -2416,6 +3605,22 @@ export function composeRuntime(options) {
|
|
|
2416
3605
|
query: (plan) => rowsQuery(plan),
|
|
2417
3606
|
watchPlan,
|
|
2418
3607
|
search: searched,
|
|
3608
|
+
watchReadiness,
|
|
3609
|
+
};
|
|
3610
|
+
return {
|
|
3611
|
+
md,
|
|
3612
|
+
seed,
|
|
3613
|
+
stop,
|
|
3614
|
+
lose: loseRooms,
|
|
3615
|
+
watch,
|
|
3616
|
+
acquire,
|
|
3617
|
+
persisted(answer) {
|
|
3618
|
+
if (answer === persistence)
|
|
3619
|
+
return;
|
|
3620
|
+
persistence = answer;
|
|
3621
|
+
journal.record(`persistence ${answer}`);
|
|
3622
|
+
queueReadiness();
|
|
3623
|
+
},
|
|
3624
|
+
within: (reach, port) => narrowed(reachable, reach, port),
|
|
2419
3625
|
};
|
|
2420
|
-
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2421
3626
|
}
|