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