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