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