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