@agentshouse/mdruntime 0.1.0-c6e4c19c13638 → 0.1.0-c6fd2cbbff187
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +277 -219
- package/README.md +20 -10
- 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 +94 -20
- package/dist/adapter.d.ts.map +1 -1
- package/dist/attached-model.d.ts.map +1 -1
- package/dist/attached-model.js +15 -2
- 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 +18 -5
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +113 -37
- package/dist/change-set.d.ts +2 -0
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +49 -16
- package/dist/change.d.ts +15 -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 +635 -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 +27 -12
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +77 -16
- package/dist/collection.d.ts +21 -12
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +271 -23
- package/dist/connection.d.ts +46 -15
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +1946 -414
- package/dist/conversation.d.ts +30 -10
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +485 -159
- 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/failure.d.ts +13 -0
- package/dist/failure.d.ts.map +1 -0
- package/dist/failure.js +19 -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 -2
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +101 -19
- package/dist/identity.d.ts +5 -0
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +19 -1
- package/dist/index.d.ts +16 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/memory.d.ts +12 -4
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +94 -25
- package/dist/model.d.ts +14 -4
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +12 -1
- package/dist/observation.d.ts +7 -2
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +29 -12
- package/dist/open.d.ts +2 -0
- package/dist/open.d.ts.map +1 -0
- package/dist/open.js +1 -0
- package/dist/outcome.d.ts +38 -3
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +45 -1
- package/dist/overlay.d.ts +16 -3
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +118 -77
- package/dist/query.d.ts +60 -4
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +41 -38
- package/dist/reach.d.ts +8 -3
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +63 -24
- package/dist/react.d.ts +31 -18
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +15 -11
- 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 +13 -3
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +49 -38
- package/dist/replica.d.ts +13 -6
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +143 -70
- package/dist/room-draft.d.ts +38 -9
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +134 -25
- package/dist/rows.d.ts +10 -13
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +19 -14
- package/dist/save.d.ts +32 -1
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +392 -45
- package/dist/search.d.ts +52 -8
- 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 +6 -2
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +0 -8
- package/dist/store.d.ts +89 -18
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +573 -200
- package/dist/turn.d.ts +7 -0
- package/dist/turn.d.ts.map +1 -0
- package/dist/turn.js +45 -0
- package/dist/windows.d.ts +19 -0
- package/dist/windows.d.ts.map +1 -0
- package/dist/windows.js +83 -0
- package/dist/wire.d.ts +149 -33
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +63 -31
- 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,25 +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';
|
|
7
|
+
import { markdownPath, roomOfKey } from './rows.js';
|
|
6
8
|
import { createSearchIndex, searchPlanOf, } from './search.js';
|
|
9
|
+
import { capped, MAXIMUM_WINDOW } from './windows.js';
|
|
7
10
|
import { bootstrapRoom } from './bootstrap.js';
|
|
8
|
-
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
11
|
+
import { changeSet, entering, mergeChangeSets } from './change-set.js';
|
|
9
12
|
import { createConversationStore, } from './conversation.js';
|
|
10
|
-
import {
|
|
13
|
+
import { queuedFile } from './change.js';
|
|
14
|
+
import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
|
|
15
|
+
import { runtimeError } from './failure.js';
|
|
11
16
|
import { createLog } from './log.js';
|
|
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';
|
|
12
21
|
import { refreshRoom } from './refresh.js';
|
|
13
|
-
import { buildBatch } from './save.js';
|
|
14
|
-
import { applyReturned, compositionOf,
|
|
15
|
-
import { changedParts, changesOf, held, readParts, returnedInto, underneath, } from './room-draft.js';
|
|
16
|
-
import {
|
|
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, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
25
|
+
import { same, saving, STARTING, transferring, } from './status.js';
|
|
17
26
|
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
18
|
-
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';
|
|
19
33
|
import { beneath, normalize } from './paths.js';
|
|
20
34
|
import { openRuntimeStore, } from './store.js';
|
|
21
35
|
import { unconfirmed } from './transfer.js';
|
|
22
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
|
+
}
|
|
23
46
|
function within(root, path) {
|
|
24
47
|
return root === '' || beneath(root, path);
|
|
25
48
|
}
|
|
@@ -33,22 +56,68 @@ function declaredSelection(room) {
|
|
|
33
56
|
root !== '' &&
|
|
34
57
|
selection.roots.every((other, at) => at === index || !within(other, root)));
|
|
35
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
|
+
}
|
|
36
92
|
export function composeRuntime(options) {
|
|
93
|
+
const localSaves = options.adapter.localSaves === true;
|
|
37
94
|
let authorization = options.authorization;
|
|
38
95
|
let opening;
|
|
39
96
|
let store;
|
|
40
|
-
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);
|
|
41
105
|
const pendings = new Map();
|
|
42
|
-
const
|
|
106
|
+
const seeds = new Map();
|
|
107
|
+
const seedWatchers = new Map();
|
|
108
|
+
const roomDrafts = new FencedMap(isFenced);
|
|
43
109
|
const presentations = new Map();
|
|
44
110
|
let limits = [];
|
|
45
111
|
let capabilities = [];
|
|
46
|
-
const rooms = new
|
|
112
|
+
const rooms = new FencedMap(isFenced);
|
|
113
|
+
const counted = new Map();
|
|
47
114
|
const unreadable = new Set();
|
|
48
|
-
const
|
|
49
|
-
const
|
|
115
|
+
const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
|
|
116
|
+
const models = new FencedMap(isModelFenced);
|
|
117
|
+
const liveModels = new FencedMap(isModelFenced);
|
|
50
118
|
const invalidModels = new Set();
|
|
51
119
|
const discovered = new Map();
|
|
120
|
+
const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
|
|
52
121
|
let roomsListed = false;
|
|
53
122
|
const scopedRefusals = new Map();
|
|
54
123
|
const sourceRefusals = new Map();
|
|
@@ -76,18 +145,29 @@ export function composeRuntime(options) {
|
|
|
76
145
|
let status = STARTING;
|
|
77
146
|
let shown = STARTING;
|
|
78
147
|
let discoveryPaused = false;
|
|
79
|
-
let resetTimer;
|
|
80
148
|
let closed = false;
|
|
81
149
|
let stopping = false;
|
|
82
150
|
let withdrawStream;
|
|
83
151
|
const announced = new Map();
|
|
84
152
|
let underway = null;
|
|
85
|
-
|
|
153
|
+
let streamed = null;
|
|
154
|
+
let handed = false;
|
|
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;
|
|
86
160
|
const answering = new Map();
|
|
87
161
|
let complete = false;
|
|
88
162
|
let recorded = false;
|
|
89
163
|
const stale = new Set();
|
|
90
|
-
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();
|
|
91
171
|
let mutations = Promise.resolve();
|
|
92
172
|
const roomListeners = new Set();
|
|
93
173
|
const refusalListeners = new Set();
|
|
@@ -98,27 +178,49 @@ export function composeRuntime(options) {
|
|
|
98
178
|
const presentationListeners = new Set();
|
|
99
179
|
const readSide = createReadSide();
|
|
100
180
|
let withdrawConversations;
|
|
101
|
-
let currencyQueued =
|
|
181
|
+
let currencyQueued = null;
|
|
102
182
|
let socketOpen = false;
|
|
103
183
|
let serviceMode = 'open';
|
|
104
184
|
let haltedRefusal = false;
|
|
105
185
|
let openSignals = 0;
|
|
106
186
|
const serviceModeListeners = new Set();
|
|
187
|
+
let member = null;
|
|
188
|
+
const memberListeners = new Set();
|
|
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);
|
|
107
205
|
const conversations = createConversationStore({
|
|
108
206
|
agents: declaredAgents,
|
|
109
|
-
documents: (locator) =>
|
|
207
|
+
documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
|
|
110
208
|
call: (name, input) => invokeAction(name, input),
|
|
111
209
|
durably,
|
|
112
210
|
stream: () => options.adapter.conversations,
|
|
113
211
|
publish: publishChangeSet,
|
|
212
|
+
online: () => !status.providerUnreachable && refusal === null,
|
|
114
213
|
closed: () => closed,
|
|
214
|
+
unanswered: resendLater,
|
|
215
|
+
expired: (operation) => pastHorizon(operation),
|
|
115
216
|
});
|
|
116
217
|
const searchIndex = createSearchIndex({
|
|
117
|
-
|
|
218
|
+
room: (room) => readSide.room(room),
|
|
118
219
|
document: (room, path) => readSide.document(room, path),
|
|
119
|
-
messages: () => conversations.messages(),
|
|
120
|
-
message: (conversation, identity) => conversations.message(conversation, identity),
|
|
121
220
|
});
|
|
221
|
+
const roomsToIndex = new Set();
|
|
222
|
+
const conversationsToIndex = new Set();
|
|
223
|
+
let indexing = false;
|
|
122
224
|
const journal = createLog();
|
|
123
225
|
let notifyReady = () => { };
|
|
124
226
|
const ready = new Promise((resolve) => {
|
|
@@ -173,6 +275,15 @@ export function composeRuntime(options) {
|
|
|
173
275
|
const value = presentations.get(app);
|
|
174
276
|
return value === undefined ? null : structuredClone(value);
|
|
175
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
|
+
}
|
|
176
287
|
function emitPresentation(app) {
|
|
177
288
|
if (closed)
|
|
178
289
|
return;
|
|
@@ -182,7 +293,9 @@ export function composeRuntime(options) {
|
|
|
182
293
|
}
|
|
183
294
|
}
|
|
184
295
|
function pending() {
|
|
185
|
-
return
|
|
296
|
+
return (!localSaves &&
|
|
297
|
+
(queuesWithoutReplica.size > 0 ||
|
|
298
|
+
[...replicas.values()].some((replica) => replica.queue.length > 0)));
|
|
186
299
|
}
|
|
187
300
|
function attention() {
|
|
188
301
|
const attentionRequired = [];
|
|
@@ -195,9 +308,16 @@ export function composeRuntime(options) {
|
|
|
195
308
|
returned: structuredClone(record.returned),
|
|
196
309
|
});
|
|
197
310
|
}
|
|
311
|
+
for (const [room, refusal] of sourceRefusals)
|
|
312
|
+
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
198
313
|
attentionRequired.sort((left, right) => {
|
|
199
314
|
if (left.room !== right.room)
|
|
200
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
|
+
}
|
|
201
321
|
if (left.path !== right.path)
|
|
202
322
|
return left.path < right.path ? -1 : 1;
|
|
203
323
|
return 0;
|
|
@@ -211,8 +331,21 @@ export function composeRuntime(options) {
|
|
|
211
331
|
}
|
|
212
332
|
return { attentionRequired, reset };
|
|
213
333
|
}
|
|
334
|
+
function documentsIn(locator) {
|
|
335
|
+
const replica = replicas.get(locator);
|
|
336
|
+
if (replica === undefined)
|
|
337
|
+
return 0;
|
|
338
|
+
const roots = replica.confirmed.record.typedSelection?.roots ?? [];
|
|
339
|
+
const weighed = outsideOf(roots);
|
|
340
|
+
return (tally(replica.visible.documents, weighed.documents) +
|
|
341
|
+
tally(replica.visible.binaries, weighed.binaries));
|
|
342
|
+
}
|
|
214
343
|
function listed() {
|
|
215
|
-
return structuredClone([...rooms.values()])
|
|
344
|
+
return structuredClone([...rooms.values()].map((room) => {
|
|
345
|
+
const documents = documentsIn(room.locator);
|
|
346
|
+
counted.set(room.locator, documents);
|
|
347
|
+
return { ...room, documents };
|
|
348
|
+
}));
|
|
216
349
|
}
|
|
217
350
|
function roomFacts(reach = null) {
|
|
218
351
|
const held = [...rooms.values()]
|
|
@@ -232,18 +365,26 @@ export function composeRuntime(options) {
|
|
|
232
365
|
return { listed: roomsListed, rooms: held };
|
|
233
366
|
}
|
|
234
367
|
function emitRooms() {
|
|
368
|
+
readSide.recover();
|
|
369
|
+
queueReadiness();
|
|
235
370
|
const value = listed();
|
|
236
371
|
for (const listener of roomListeners)
|
|
237
372
|
listener(value);
|
|
238
373
|
}
|
|
239
374
|
function emitRefusal() {
|
|
375
|
+
queueReadiness();
|
|
240
376
|
for (const listener of refusalListeners)
|
|
241
377
|
listener(refusal);
|
|
242
378
|
}
|
|
243
379
|
function emitStorageLoss() {
|
|
380
|
+
queueReadiness();
|
|
244
381
|
for (const listener of storageLossListeners)
|
|
245
382
|
listener(storageLoss);
|
|
246
383
|
}
|
|
384
|
+
function emitMember() {
|
|
385
|
+
for (const listener of memberListeners)
|
|
386
|
+
listener(member);
|
|
387
|
+
}
|
|
247
388
|
function moveTo(next) {
|
|
248
389
|
if (closed)
|
|
249
390
|
return;
|
|
@@ -253,7 +394,8 @@ export function composeRuntime(options) {
|
|
|
253
394
|
function show() {
|
|
254
395
|
if (closed)
|
|
255
396
|
return;
|
|
256
|
-
|
|
397
|
+
queueReadiness();
|
|
398
|
+
const next = status.state === 'synced' && demand.conversations && !conversations.current()
|
|
257
399
|
? { ...status, state: 'syncing' }
|
|
258
400
|
: status;
|
|
259
401
|
if (same(shown, next))
|
|
@@ -263,6 +405,8 @@ export function composeRuntime(options) {
|
|
|
263
405
|
listener(shown);
|
|
264
406
|
}
|
|
265
407
|
function entryOf(batch) {
|
|
408
|
+
if (localSaves)
|
|
409
|
+
return Object.freeze({ batch, state: 'local' });
|
|
266
410
|
const state = pendings.get(batch.id);
|
|
267
411
|
if (state?.accepted !== undefined) {
|
|
268
412
|
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
@@ -270,10 +414,7 @@ export function composeRuntime(options) {
|
|
|
270
414
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
271
415
|
}
|
|
272
416
|
function entriesOf(locator) {
|
|
273
|
-
|
|
274
|
-
if (replica === undefined)
|
|
275
|
-
return [];
|
|
276
|
-
return replica.queue.map(entryOf);
|
|
417
|
+
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
|
|
277
418
|
}
|
|
278
419
|
function emitQueue(locator) {
|
|
279
420
|
const entries = entriesOf(locator);
|
|
@@ -283,12 +424,522 @@ export function composeRuntime(options) {
|
|
|
283
424
|
}
|
|
284
425
|
}
|
|
285
426
|
function applyChangeSet(set) {
|
|
286
|
-
|
|
287
|
-
|
|
427
|
+
const held = {
|
|
428
|
+
...set,
|
|
429
|
+
documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
|
|
430
|
+
};
|
|
431
|
+
readSide.apply(held);
|
|
432
|
+
searchIndex.changed(held);
|
|
433
|
+
}
|
|
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))
|
|
450
|
+
return;
|
|
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);
|
|
472
|
+
}
|
|
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)
|
|
489
|
+
return;
|
|
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
|
+
})();
|
|
505
|
+
}
|
|
506
|
+
function reachedRooms(plan) {
|
|
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)
|
|
851
|
+
return null;
|
|
852
|
+
if (!seedHeld(seed.at))
|
|
853
|
+
return seed;
|
|
854
|
+
seeds.delete(plan.key);
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
function seedBehind(plan, cover) {
|
|
858
|
+
const seed = seedApplies(plan);
|
|
859
|
+
if (seed === null || 'pending' in cover)
|
|
860
|
+
return cover;
|
|
861
|
+
return {
|
|
862
|
+
pending: Object.entries(seed.at)
|
|
863
|
+
.filter(([room, version]) => !seedRoomHeld(room, version))
|
|
864
|
+
.map(([room]) => ({ room, awaits: 'acquisition' })),
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
function seed(entry) {
|
|
868
|
+
if (seedHeld(entry.at))
|
|
869
|
+
return;
|
|
870
|
+
for (const { query, rows } of entry.queries) {
|
|
871
|
+
const key = queryKey(query);
|
|
872
|
+
seeds.set(key, { rows, at: entry.at });
|
|
873
|
+
for (const shown of seedWatchers.get(key) ?? [])
|
|
874
|
+
shown();
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
async function rowsQuery(plan) {
|
|
878
|
+
const before = covered(plan);
|
|
879
|
+
awaitPreparation(before);
|
|
880
|
+
const seeded = seedApplies(plan);
|
|
881
|
+
if (seeded !== null)
|
|
882
|
+
return seededResult(seeded).result;
|
|
883
|
+
if ('pending' in before)
|
|
884
|
+
return before;
|
|
885
|
+
const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
|
|
886
|
+
awaitPreparation(cover);
|
|
887
|
+
if ('pending' in cover)
|
|
888
|
+
return cover;
|
|
889
|
+
return { ...rows, coverage: cover.coverage };
|
|
890
|
+
}
|
|
891
|
+
async function indexConversation(conversation) {
|
|
892
|
+
searchIndex.indexMessages(conversation, [], false);
|
|
893
|
+
let after = null;
|
|
894
|
+
for (;;) {
|
|
895
|
+
const batch = await durably((local) => local.messagesAfter(conversation, after));
|
|
896
|
+
if (closed)
|
|
897
|
+
return;
|
|
898
|
+
searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
|
|
899
|
+
if (batch.length === 0)
|
|
900
|
+
return;
|
|
901
|
+
after = batch[batch.length - 1].identity;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
function indexNext() {
|
|
905
|
+
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
|
|
906
|
+
if (room !== undefined) {
|
|
907
|
+
roomsToIndex.delete(room);
|
|
908
|
+
return () => prepared.has(room)
|
|
909
|
+
? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
|
|
910
|
+
: undefined;
|
|
911
|
+
}
|
|
912
|
+
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
913
|
+
if (conversation === undefined)
|
|
914
|
+
return null;
|
|
915
|
+
conversationsToIndex.delete(conversation);
|
|
916
|
+
return () => indexConversation(conversation);
|
|
917
|
+
}
|
|
918
|
+
function index(rooms, named) {
|
|
919
|
+
for (const room of rooms)
|
|
920
|
+
roomsToIndex.add(room);
|
|
921
|
+
for (const conversation of named)
|
|
922
|
+
conversationsToIndex.add(conversation);
|
|
923
|
+
if (indexing)
|
|
924
|
+
return;
|
|
925
|
+
indexing = true;
|
|
926
|
+
void (async () => {
|
|
927
|
+
try {
|
|
928
|
+
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
929
|
+
await yielded();
|
|
930
|
+
await unit();
|
|
931
|
+
queueReadiness();
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
finally {
|
|
935
|
+
indexing = false;
|
|
936
|
+
}
|
|
937
|
+
})().catch(() => { });
|
|
288
938
|
}
|
|
289
939
|
function publishChangeSet(set) {
|
|
290
940
|
applyChangeSet(set);
|
|
291
|
-
if (set.rooms.length > 0
|
|
941
|
+
if (set.rooms.length > 0 ||
|
|
942
|
+
[...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
|
|
292
943
|
emitRooms();
|
|
293
944
|
for (const locator of set.queues)
|
|
294
945
|
emitQueue(locator);
|
|
@@ -300,34 +951,7 @@ export function composeRuntime(options) {
|
|
|
300
951
|
emitPresentation(app);
|
|
301
952
|
conversations.changed(set);
|
|
302
953
|
}
|
|
303
|
-
function stopTimer() {
|
|
304
|
-
if (resetTimer === undefined)
|
|
305
|
-
return;
|
|
306
|
-
clearTimeout(resetTimer);
|
|
307
|
-
resetTimer = undefined;
|
|
308
|
-
}
|
|
309
|
-
function armTimer() {
|
|
310
|
-
stopTimer();
|
|
311
|
-
if (closed || stopping)
|
|
312
|
-
return;
|
|
313
|
-
let earliest = null;
|
|
314
|
-
for (const { submission } of pendings.values()) {
|
|
315
|
-
if (submission.state !== 'reset')
|
|
316
|
-
continue;
|
|
317
|
-
const at = Date.parse(submission.reset);
|
|
318
|
-
if (earliest === null || at < earliest)
|
|
319
|
-
earliest = at;
|
|
320
|
-
}
|
|
321
|
-
if (earliest === null)
|
|
322
|
-
return;
|
|
323
|
-
// A Save House held back goes again only once the reset its answer named has passed.
|
|
324
|
-
resetTimer = setTimeout(() => {
|
|
325
|
-
resetTimer = undefined;
|
|
326
|
-
submitAll();
|
|
327
|
-
}, Math.max(0, earliest - Date.now()));
|
|
328
|
-
}
|
|
329
954
|
async function refuse(reason, provider = null) {
|
|
330
|
-
stopTimer();
|
|
331
955
|
discoveryPaused = true;
|
|
332
956
|
try {
|
|
333
957
|
if (reason === 'revoked-authorization') {
|
|
@@ -335,10 +959,7 @@ export function composeRuntime(options) {
|
|
|
335
959
|
const revoked = { status: 'unavailable', reason: 'revoked' };
|
|
336
960
|
try {
|
|
337
961
|
const local = await openLocal();
|
|
338
|
-
await local.revokeModels([...replicas.values()].map((replica) =>
|
|
339
|
-
record: replica.confirmed.record,
|
|
340
|
-
queue: replica.queue,
|
|
341
|
-
})));
|
|
962
|
+
await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
|
|
342
963
|
}
|
|
343
964
|
catch {
|
|
344
965
|
journal.record('model revocation persistence failed');
|
|
@@ -362,10 +983,7 @@ export function composeRuntime(options) {
|
|
|
362
983
|
model: revoked,
|
|
363
984
|
selectionModel: revoked,
|
|
364
985
|
};
|
|
365
|
-
|
|
366
|
-
replicas.set(locator, read.kind === 'staged'
|
|
367
|
-
? replicaOf(read.confirmed, before.queue, before)
|
|
368
|
-
: { ...before, confirmed: { ...before.confirmed, record } });
|
|
986
|
+
replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
|
|
369
987
|
}
|
|
370
988
|
rooms.set(locator, {
|
|
371
989
|
...room,
|
|
@@ -382,7 +1000,8 @@ export function composeRuntime(options) {
|
|
|
382
1000
|
}
|
|
383
1001
|
finally {
|
|
384
1002
|
refusal = {
|
|
385
|
-
reason,
|
|
1003
|
+
code: reason,
|
|
1004
|
+
retryable: false,
|
|
386
1005
|
reauthorizationOffered: reason === 'revoked-authorization',
|
|
387
1006
|
refusal: provider,
|
|
388
1007
|
};
|
|
@@ -407,19 +1026,31 @@ export function composeRuntime(options) {
|
|
|
407
1026
|
},
|
|
408
1027
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
409
1028
|
roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
|
|
410
|
-
attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
|
|
411
1029
|
synchronizationAvailable,
|
|
412
1030
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
413
1031
|
});
|
|
414
1032
|
}
|
|
415
|
-
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));
|
|
416
1043
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
417
1044
|
for (const locator of listedLocators)
|
|
418
1045
|
scopedRefusals.delete(locator);
|
|
419
1046
|
discovered.clear();
|
|
420
1047
|
for (const room of listed)
|
|
421
1048
|
discovered.set(room.locator, room);
|
|
422
|
-
|
|
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);
|
|
423
1054
|
for (const room of listed) {
|
|
424
1055
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
425
1056
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -430,43 +1061,91 @@ export function composeRuntime(options) {
|
|
|
430
1061
|
}
|
|
431
1062
|
roomsListed = true;
|
|
432
1063
|
emitRooms();
|
|
1064
|
+
return listed;
|
|
433
1065
|
}
|
|
434
|
-
function
|
|
435
|
-
return
|
|
1066
|
+
function heldLocally(locator) {
|
|
1067
|
+
return (replicas.has(locator) ||
|
|
1068
|
+
roomDrafts.has(locator) ||
|
|
1069
|
+
transfers.has(locator) ||
|
|
1070
|
+
queuesWithoutReplica.has(locator));
|
|
436
1071
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
|
452
1088
|
rooms.delete(locator);
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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);
|
|
466
1124
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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));
|
|
470
1149
|
}
|
|
471
1150
|
function restored(key, persisted) {
|
|
472
1151
|
const prior = models.get(key);
|
|
@@ -487,7 +1166,7 @@ export function composeRuntime(options) {
|
|
|
487
1166
|
return model;
|
|
488
1167
|
}
|
|
489
1168
|
async function hydrate(opened) {
|
|
490
|
-
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
|
|
1169
|
+
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
|
|
491
1170
|
opened.all(),
|
|
492
1171
|
opened.queues(),
|
|
493
1172
|
opened.roomDrafts(),
|
|
@@ -497,9 +1176,17 @@ export function composeRuntime(options) {
|
|
|
497
1176
|
opened.lastCompleted(),
|
|
498
1177
|
opened.providerLimits(),
|
|
499
1178
|
opened.capabilities(),
|
|
1179
|
+
opened.member(),
|
|
1180
|
+
opened.transfers(),
|
|
1181
|
+
opened.targets(),
|
|
500
1182
|
]);
|
|
1183
|
+
targets = heldTargets;
|
|
1184
|
+
transfers.clear();
|
|
1185
|
+
for (const record of staged)
|
|
1186
|
+
transfers.set(record.locator, record);
|
|
501
1187
|
limits = declared;
|
|
502
1188
|
capabilities = named;
|
|
1189
|
+
member = known;
|
|
503
1190
|
roomDrafts.clear();
|
|
504
1191
|
for (const record of kept)
|
|
505
1192
|
rememberRoomDraft(record);
|
|
@@ -515,18 +1202,17 @@ export function composeRuntime(options) {
|
|
|
515
1202
|
}
|
|
516
1203
|
const previousReplicas = new Map(replicas);
|
|
517
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
|
+
}
|
|
518
1211
|
for (const record of records) {
|
|
519
1212
|
const model = restored(modelKey(record.locator, 'collection'), record.model);
|
|
520
1213
|
const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
|
|
521
1214
|
const prior = previousReplicas.get(record.locator);
|
|
522
|
-
const
|
|
523
|
-
const read = readConfirmed(retyped, prior?.confirmed);
|
|
524
|
-
const confirmed = read.kind === 'staged'
|
|
525
|
-
? read.confirmed
|
|
526
|
-
: {
|
|
527
|
-
record: retyped,
|
|
528
|
-
collection: parseCollection(retyped.documents, compositionOf(retyped)),
|
|
529
|
-
};
|
|
1215
|
+
const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
|
|
530
1216
|
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
|
|
531
1217
|
const existing = rooms.get(record.locator);
|
|
532
1218
|
if (existing === undefined)
|
|
@@ -535,7 +1221,6 @@ export function composeRuntime(options) {
|
|
|
535
1221
|
rooms.set(record.locator, {
|
|
536
1222
|
...existing,
|
|
537
1223
|
roomVersion: record.roomVersion,
|
|
538
|
-
attachments: record.attachments,
|
|
539
1224
|
operations: record.operations,
|
|
540
1225
|
replica: record.replica,
|
|
541
1226
|
model: modelTypes(model),
|
|
@@ -547,10 +1232,6 @@ export function composeRuntime(options) {
|
|
|
547
1232
|
},
|
|
548
1233
|
});
|
|
549
1234
|
}
|
|
550
|
-
if (read.kind === 'refused') {
|
|
551
|
-
refusedSource(record, read);
|
|
552
|
-
declarationRefusals.add(record.locator);
|
|
553
|
-
}
|
|
554
1235
|
}
|
|
555
1236
|
return {
|
|
556
1237
|
lastCompleted,
|
|
@@ -561,16 +1242,26 @@ export function composeRuntime(options) {
|
|
|
561
1242
|
async function readState(opened) {
|
|
562
1243
|
const { lastCompleted, locators, read } = await hydrate(opened);
|
|
563
1244
|
applyChangeSet(read);
|
|
1245
|
+
prepare();
|
|
564
1246
|
if (replicas.size > 0)
|
|
565
1247
|
emitRooms();
|
|
1248
|
+
if (member !== null)
|
|
1249
|
+
emitMember();
|
|
566
1250
|
for (const locator of locators)
|
|
567
1251
|
emitQueue(locator);
|
|
568
1252
|
publishRoomDrafts(false);
|
|
569
1253
|
return lastCompleted;
|
|
570
1254
|
}
|
|
571
1255
|
async function open() {
|
|
572
|
-
|
|
1256
|
+
if (closed)
|
|
1257
|
+
throw new Error('runtime closed');
|
|
1258
|
+
const opened = await openRuntimeStore(isFenced);
|
|
573
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
|
+
}
|
|
574
1265
|
const kept = options.shell.generation;
|
|
575
1266
|
const storedGeneration = await opened.generation();
|
|
576
1267
|
const loss = detectStorageLoss(storedGeneration, kept);
|
|
@@ -584,6 +1275,8 @@ export function composeRuntime(options) {
|
|
|
584
1275
|
emitStorageLoss();
|
|
585
1276
|
}
|
|
586
1277
|
const lastCompleted = await readState(opened);
|
|
1278
|
+
if (interrupted.length > 0)
|
|
1279
|
+
await erased(opened, interrupted);
|
|
587
1280
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
588
1281
|
return opened;
|
|
589
1282
|
}
|
|
@@ -610,10 +1303,16 @@ export function composeRuntime(options) {
|
|
|
610
1303
|
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
611
1304
|
}
|
|
612
1305
|
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
613
|
-
const
|
|
614
|
-
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;
|
|
1310
|
+
const unqueued = withdrawn.map((batch) => ({
|
|
1311
|
+
batch,
|
|
1312
|
+
returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
|
|
1313
|
+
}));
|
|
615
1314
|
const returning = [
|
|
616
|
-
...
|
|
1315
|
+
...[...unqueued].reverse(),
|
|
617
1316
|
...(settling.leaving === undefined ? [] : [settling.leaving]),
|
|
618
1317
|
];
|
|
619
1318
|
const written = new Map();
|
|
@@ -631,27 +1330,49 @@ export function composeRuntime(options) {
|
|
|
631
1330
|
});
|
|
632
1331
|
}
|
|
633
1332
|
}
|
|
634
|
-
await local.
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
previousQueue: before?.queue ?? [],
|
|
639
|
-
roomDrafts: [...written.values()],
|
|
640
|
-
});
|
|
641
|
-
for (const batch of before?.queue ?? []) {
|
|
642
|
-
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))
|
|
643
1337
|
pendings.delete(batch.id);
|
|
644
1338
|
}
|
|
645
1339
|
for (const record of written.values())
|
|
646
1340
|
rememberRoomDraft(record);
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
+
};
|
|
655
1376
|
}
|
|
656
1377
|
function adoptModels(locator, record) {
|
|
657
1378
|
for (const [attached, model] of [
|
|
@@ -684,10 +1405,16 @@ export function composeRuntime(options) {
|
|
|
684
1405
|
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
685
1406
|
};
|
|
686
1407
|
}
|
|
687
|
-
function publish(local, room, confirmed,
|
|
1408
|
+
function publish(local, room, confirmed, landing, live) {
|
|
1409
|
+
const { base, stored } = landing;
|
|
688
1410
|
return exclusive(async () => {
|
|
689
|
-
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);
|
|
690
1416
|
return true;
|
|
1417
|
+
}
|
|
691
1418
|
const prior = replicas.get(room.locator);
|
|
692
1419
|
if (base !== undefined &&
|
|
693
1420
|
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
@@ -695,47 +1422,85 @@ export function composeRuntime(options) {
|
|
|
695
1422
|
return false;
|
|
696
1423
|
}
|
|
697
1424
|
adoptModels(room.locator, confirmed.record);
|
|
698
|
-
const queue = prior?.queue ?? [];
|
|
1425
|
+
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
699
1426
|
const roomVersion = confirmed.record.roomVersion;
|
|
700
1427
|
const accepted = queue.filter((batch) => {
|
|
701
1428
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
702
1429
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
703
1430
|
});
|
|
704
|
-
|
|
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) {
|
|
1444
|
+
transfers.delete(room.locator);
|
|
1445
|
+
catching.delete(room.locator);
|
|
1446
|
+
}
|
|
705
1447
|
for (const batch of accepted)
|
|
706
1448
|
pendings.delete(batch.id);
|
|
707
1449
|
stale.delete(room.locator);
|
|
1450
|
+
unindexable.delete(room.locator);
|
|
708
1451
|
if (unreadable.delete(room.locator))
|
|
709
1452
|
scopedRefusals.delete(room.locator);
|
|
710
1453
|
sourceRefusals.delete(room.locator);
|
|
711
1454
|
declarationRefusals.delete(room.locator);
|
|
712
1455
|
list(room, true);
|
|
1456
|
+
if (prior === undefined)
|
|
1457
|
+
prepared.add(room.locator);
|
|
713
1458
|
publishChangeSet(set);
|
|
714
1459
|
moveTo({ ...status, ...attention() });
|
|
1460
|
+
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
715
1461
|
return true;
|
|
716
1462
|
});
|
|
717
1463
|
}
|
|
718
1464
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
719
1465
|
const local = await openLocal();
|
|
1466
|
+
await referenced(locator);
|
|
720
1467
|
const replica = replicas.get(locator);
|
|
721
1468
|
const room = rooms.get(locator);
|
|
722
1469
|
if (replica === undefined || room === undefined) {
|
|
723
|
-
throw
|
|
1470
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
724
1471
|
}
|
|
725
1472
|
const saved = [];
|
|
726
1473
|
for (const { path, revision } of named) {
|
|
727
1474
|
const record = roomDrafts.get(locator)?.get(path);
|
|
728
1475
|
if (record === undefined || record.revision !== revision) {
|
|
729
|
-
return { outcome: 'refused',
|
|
1476
|
+
return { outcome: 'refused', code: 'stale', retryable: false, file: path };
|
|
730
1477
|
}
|
|
731
1478
|
if (!saved.some((draft) => draft.path === path))
|
|
732
1479
|
saved.push({ path, parts: record.parts });
|
|
733
1480
|
}
|
|
734
1481
|
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
|
|
735
1482
|
if (changed.length > 0)
|
|
736
|
-
return { outcome: 'refused',
|
|
1483
|
+
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
737
1484
|
const parts = saved.map(({ path, parts }) => changesOf(path, parts));
|
|
738
|
-
const
|
|
1485
|
+
const changes = parts.flatMap(({ changes }) => changes);
|
|
1486
|
+
const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
|
|
1487
|
+
if (requeue.kind === 'refused')
|
|
1488
|
+
return { outcome: 'refused', ...requeue.refusal };
|
|
1489
|
+
const foreign = [...requeue.withdrawn.keys()].some((origin) => {
|
|
1490
|
+
const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
|
|
1491
|
+
return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
|
|
1492
|
+
});
|
|
1493
|
+
if (foreign) {
|
|
1494
|
+
return {
|
|
1495
|
+
outcome: 'refused',
|
|
1496
|
+
code: 'unknown-revision',
|
|
1497
|
+
retryable: false,
|
|
1498
|
+
file: [...requeue.queued].at(-1),
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
const withdrawing = requeue.withdrawn.size > 0;
|
|
1502
|
+
const input = {
|
|
1503
|
+
local: localSaves,
|
|
739
1504
|
baseRoomVersion: replica.confirmed.record.roomVersion,
|
|
740
1505
|
composition: compositionFor(replica.confirmed.record),
|
|
741
1506
|
operations: room.operations,
|
|
@@ -743,29 +1508,97 @@ export function composeRuntime(options) {
|
|
|
743
1508
|
providerLimits: limits,
|
|
744
1509
|
handle: room.handle,
|
|
745
1510
|
visible: replica.visible,
|
|
746
|
-
changes
|
|
1511
|
+
changes,
|
|
747
1512
|
bases: parts.flatMap(({ bases }) => bases),
|
|
1513
|
+
queued: queuedDocuments(replica.queue),
|
|
748
1514
|
writtenBy,
|
|
1515
|
+
clean: {
|
|
1516
|
+
text: replica.confirmed.record.documents,
|
|
1517
|
+
binaries: replica.confirmed.record.binaries,
|
|
1518
|
+
composition: compositionOf(replica.confirmed.record),
|
|
1519
|
+
},
|
|
1520
|
+
};
|
|
1521
|
+
const checked = withdrawing
|
|
1522
|
+
? buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
|
|
1523
|
+
: null;
|
|
1524
|
+
if (checked?.kind === 'refused')
|
|
1525
|
+
return { outcome: 'refused', ...checked.refusal };
|
|
1526
|
+
const owners = [...requeue.owners].flatMap((origin) => {
|
|
1527
|
+
const batch = requeue.withdrawn.get(origin);
|
|
1528
|
+
return batch === undefined || batch === null ? [] : [{ origin, batch }];
|
|
749
1529
|
});
|
|
750
|
-
if (
|
|
1530
|
+
if (owners.length > 1) {
|
|
1531
|
+
return {
|
|
1532
|
+
outcome: 'refused',
|
|
1533
|
+
code: 'unknown-revision',
|
|
1534
|
+
retryable: false,
|
|
1535
|
+
file: [...requeue.queued].at(-1),
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
const built = !withdrawing
|
|
1539
|
+
? buildBatch(input)
|
|
1540
|
+
: requeue.changes.length === 0
|
|
1541
|
+
? null
|
|
1542
|
+
: buildBatch({
|
|
1543
|
+
...input,
|
|
1544
|
+
visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
|
|
1545
|
+
changes: requeue.changes,
|
|
1546
|
+
queued: queuedDocuments(requeue.queue),
|
|
1547
|
+
});
|
|
1548
|
+
if (built?.kind === 'refused')
|
|
751
1549
|
return { outcome: 'refused', ...built.refusal };
|
|
752
|
-
const
|
|
1550
|
+
const [owner] = owners;
|
|
1551
|
+
const join = built === null || owner === undefined
|
|
1552
|
+
? null
|
|
1553
|
+
: joined(requeue.queue, owner.batch, built.batch);
|
|
1554
|
+
if (join?.kind === 'refused')
|
|
1555
|
+
return { outcome: 'refused', ...join.refusal };
|
|
1556
|
+
const withdrawn = new Map(requeue.withdrawn);
|
|
1557
|
+
if (join !== null && owner !== undefined)
|
|
1558
|
+
withdrawn.set(owner.origin, join.batch);
|
|
1559
|
+
const drafted = new Map();
|
|
1560
|
+
for (const [origin, replaced] of withdrawn) {
|
|
1561
|
+
if (replaced === null)
|
|
1562
|
+
continue;
|
|
1563
|
+
const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
|
|
1564
|
+
drafted.set(replaced.id, replaced === join?.batch
|
|
1565
|
+
? [
|
|
1566
|
+
...carried.map((held) => joinedDraft(held, saved)),
|
|
1567
|
+
...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
|
|
1568
|
+
]
|
|
1569
|
+
: carried);
|
|
1570
|
+
}
|
|
1571
|
+
if (built !== null && join === null)
|
|
1572
|
+
drafted.set(built.batch.id, saved);
|
|
1573
|
+
const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
|
|
1574
|
+
for (const replaced of withdrawn.values()) {
|
|
1575
|
+
if (replaced === null || !batches.includes(replaced))
|
|
1576
|
+
continue;
|
|
1577
|
+
const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
|
|
1578
|
+
if (limited !== null)
|
|
1579
|
+
return { outcome: 'refused', ...limited };
|
|
1580
|
+
}
|
|
753
1581
|
const consumed = saved.map(({ path }) => path);
|
|
754
1582
|
try {
|
|
755
|
-
await local.
|
|
756
|
-
|
|
757
|
-
previous: replica.confirmed.record,
|
|
1583
|
+
await local.keepAuthored({
|
|
1584
|
+
locator,
|
|
758
1585
|
queue: batches,
|
|
759
|
-
|
|
760
|
-
saved,
|
|
1586
|
+
saved: drafted,
|
|
761
1587
|
consumedRoomDrafts: consumed,
|
|
762
1588
|
});
|
|
763
1589
|
}
|
|
764
1590
|
catch {
|
|
765
|
-
return { outcome: 'refused',
|
|
1591
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
1592
|
+
}
|
|
1593
|
+
if (fences.has(locator))
|
|
1594
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1595
|
+
for (const origin of withdrawn.keys())
|
|
1596
|
+
pendings.delete(origin);
|
|
1597
|
+
for (const [id, drafts] of drafted) {
|
|
1598
|
+
pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
|
|
766
1599
|
}
|
|
767
|
-
|
|
768
|
-
|
|
1600
|
+
if (!localSaves)
|
|
1601
|
+
recorded = false;
|
|
769
1602
|
for (const path of consumed)
|
|
770
1603
|
forgetRoomDraft(locator, path);
|
|
771
1604
|
replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
|
|
@@ -773,9 +1606,12 @@ export function composeRuntime(options) {
|
|
|
773
1606
|
queue: true,
|
|
774
1607
|
drafts: consumed,
|
|
775
1608
|
}));
|
|
776
|
-
moveTo({ ...saving(status), ...attention() });
|
|
1609
|
+
moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
|
|
777
1610
|
submitLater(locator);
|
|
778
|
-
|
|
1611
|
+
const identifier = join?.batch.id ??
|
|
1612
|
+
built?.batch.id ??
|
|
1613
|
+
[...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
|
|
1614
|
+
return { outcome: 'saved', identifier };
|
|
779
1615
|
}
|
|
780
1616
|
async function mark(local, locator, identifier, submission, lost) {
|
|
781
1617
|
const state = pendings.get(identifier);
|
|
@@ -785,6 +1621,8 @@ export function composeRuntime(options) {
|
|
|
785
1621
|
return;
|
|
786
1622
|
}
|
|
787
1623
|
await local.submit(locator, identifier, submission, lost);
|
|
1624
|
+
if (fences.has(locator))
|
|
1625
|
+
return;
|
|
788
1626
|
pendings.set(identifier, { ...state, submission, lost });
|
|
789
1627
|
const replica = replicas.get(locator);
|
|
790
1628
|
if (replica !== undefined)
|
|
@@ -795,22 +1633,24 @@ export function composeRuntime(options) {
|
|
|
795
1633
|
if (!continuing())
|
|
796
1634
|
throw new Error('connection refused');
|
|
797
1635
|
await local.keepUpload(locator, identifier, upload);
|
|
1636
|
+
if (fences.has(locator))
|
|
1637
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
798
1638
|
pendings.set(identifier, { ...pendings.get(identifier), upload });
|
|
799
1639
|
if (!continuing())
|
|
800
1640
|
throw new Error('connection refused');
|
|
801
1641
|
}
|
|
802
1642
|
async function readHistory(locator, path, query = {}) {
|
|
803
1643
|
if (closed || refusal !== null)
|
|
804
|
-
return
|
|
1644
|
+
return UNCERTAIN;
|
|
805
1645
|
try {
|
|
806
1646
|
await ensureAuthorization();
|
|
807
1647
|
}
|
|
808
1648
|
catch {
|
|
809
|
-
return
|
|
810
|
-
}
|
|
811
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
812
|
-
return { kind: 'transport-uncertainty' };
|
|
1649
|
+
return UNCERTAIN;
|
|
813
1650
|
}
|
|
1651
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1652
|
+
return UNCERTAIN;
|
|
1653
|
+
const since = fencings;
|
|
814
1654
|
const transfer = ['transfer', 'history', locator, path];
|
|
815
1655
|
if (query.revision !== undefined)
|
|
816
1656
|
transfer.push(query.revision);
|
|
@@ -825,6 +1665,8 @@ export function composeRuntime(options) {
|
|
|
825
1665
|
...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
|
|
826
1666
|
});
|
|
827
1667
|
journal.record(`outcome history ${result.kind}`);
|
|
1668
|
+
if (fencedSince(locator, since))
|
|
1669
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
828
1670
|
if (result.kind === 'scoped-refusal') {
|
|
829
1671
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
830
1672
|
if (room !== undefined)
|
|
@@ -832,12 +1674,125 @@ export function composeRuntime(options) {
|
|
|
832
1674
|
}
|
|
833
1675
|
if (result.kind === 'revoked-authorization')
|
|
834
1676
|
await refuse('revoked-authorization');
|
|
835
|
-
return result;
|
|
1677
|
+
return historyAnswer(result);
|
|
836
1678
|
}
|
|
837
1679
|
catch {
|
|
838
1680
|
journal.record('outcome history transport-uncertainty');
|
|
839
|
-
return
|
|
1681
|
+
return UNCERTAIN;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
function queuedBlob(locator, path, localId) {
|
|
1685
|
+
for (const batch of replicas.get(locator)?.queue ?? []) {
|
|
1686
|
+
const change = batch.changes[queuedFile(batch, path, localId)];
|
|
1687
|
+
if (change?.kind === 'binary')
|
|
1688
|
+
return change.blob;
|
|
840
1689
|
}
|
|
1690
|
+
return null;
|
|
1691
|
+
}
|
|
1692
|
+
function binaryRoom(request) {
|
|
1693
|
+
if (request.kind === 'path')
|
|
1694
|
+
return request.locator;
|
|
1695
|
+
const scope = options.adapter.referrerScope(request.referrer);
|
|
1696
|
+
return scope?.kind === 'room' ? scope.locator : null;
|
|
1697
|
+
}
|
|
1698
|
+
async function transferBinary(request, transfer, answer) {
|
|
1699
|
+
if (closed || refusal !== null)
|
|
1700
|
+
return UNCERTAIN;
|
|
1701
|
+
try {
|
|
1702
|
+
await ensureAuthorization();
|
|
1703
|
+
}
|
|
1704
|
+
catch {
|
|
1705
|
+
return UNCERTAIN;
|
|
1706
|
+
}
|
|
1707
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1708
|
+
return UNCERTAIN;
|
|
1709
|
+
const since = fencings;
|
|
1710
|
+
journal.record(request.kind === 'path'
|
|
1711
|
+
? `transfer binary ${request.locator} ${request.path}`
|
|
1712
|
+
: `transfer binary ${request.fileRef}`);
|
|
1713
|
+
try {
|
|
1714
|
+
const result = await transfer(authorization);
|
|
1715
|
+
const settled = result;
|
|
1716
|
+
journal.record(`outcome binary ${settled.kind}`);
|
|
1717
|
+
const locator = binaryRoom(request);
|
|
1718
|
+
if (locator !== null && fencedSince(locator, since))
|
|
1719
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
1720
|
+
if (settled.kind === 'scoped-refusal') {
|
|
1721
|
+
const room = replicas.get(settled.locator)?.confirmed.record;
|
|
1722
|
+
if (room !== undefined)
|
|
1723
|
+
await scoped(settled, room, await openLocal());
|
|
1724
|
+
}
|
|
1725
|
+
if (settled.kind === 'revoked-authorization')
|
|
1726
|
+
await refuse('revoked-authorization');
|
|
1727
|
+
return answer(result);
|
|
1728
|
+
}
|
|
1729
|
+
catch {
|
|
1730
|
+
journal.record('outcome binary transport-uncertainty');
|
|
1731
|
+
return UNCERTAIN;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
function readBinary(request) {
|
|
1735
|
+
return transferBinary(request, (held) => options.adapter.binary(held, request), binaryAnswer);
|
|
1736
|
+
}
|
|
1737
|
+
function binaryPath(locator, path, revision) {
|
|
1738
|
+
if (revision !== undefined)
|
|
1739
|
+
return { kind: 'path', locator, path, revision };
|
|
1740
|
+
const visible = replicas.get(locator)?.visible;
|
|
1741
|
+
const held = visible?.binaries[path];
|
|
1742
|
+
if (visible === undefined || held === undefined)
|
|
1743
|
+
return null;
|
|
1744
|
+
if (held.state === 'pending')
|
|
1745
|
+
return { kind: 'pending', localId: held.localId };
|
|
1746
|
+
const record = binaryRecord(path, held, visible.revisions[path]);
|
|
1747
|
+
if (record.state !== 'accepted')
|
|
1748
|
+
return null;
|
|
1749
|
+
const replica = replicas.get(locator);
|
|
1750
|
+
const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
|
|
1751
|
+
const holds = (at) => {
|
|
1752
|
+
const kept = replica?.confirmed.record.binaries[at];
|
|
1753
|
+
return (kept?.state === 'accepted' &&
|
|
1754
|
+
kept.fileRef === record.fileRef &&
|
|
1755
|
+
replica?.confirmed.record.revisions[at] === record.revision);
|
|
1756
|
+
};
|
|
1757
|
+
let confirmed = path;
|
|
1758
|
+
for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
|
|
1759
|
+
const change = queued[index];
|
|
1760
|
+
if (change.kind === 'move' && change.to === confirmed)
|
|
1761
|
+
confirmed = change.path;
|
|
1762
|
+
}
|
|
1763
|
+
return {
|
|
1764
|
+
kind: 'path',
|
|
1765
|
+
locator,
|
|
1766
|
+
path: confirmed,
|
|
1767
|
+
revision: record.revision,
|
|
1768
|
+
fileRef: record.fileRef,
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
async function readBinaryPath(locator, path, revision) {
|
|
1772
|
+
await ready;
|
|
1773
|
+
const asked = binaryPath(locator, path, revision);
|
|
1774
|
+
if (asked === null)
|
|
1775
|
+
return absentBinary(path);
|
|
1776
|
+
if (asked.kind === 'pending') {
|
|
1777
|
+
const blob = queuedBlob(locator, path, asked.localId);
|
|
1778
|
+
return blob === null ? absentBinary(path) : { kind: 'binary', blob };
|
|
1779
|
+
}
|
|
1780
|
+
return readBinary(asked);
|
|
1781
|
+
}
|
|
1782
|
+
async function linkBinaryPath(locator, path, revision) {
|
|
1783
|
+
await ready;
|
|
1784
|
+
const asked = binaryPath(locator, path, revision);
|
|
1785
|
+
if (asked === null || asked.kind === 'pending')
|
|
1786
|
+
return absentBinary(path);
|
|
1787
|
+
return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
|
|
1788
|
+
}
|
|
1789
|
+
async function readBinaryReference(reference) {
|
|
1790
|
+
await ready;
|
|
1791
|
+
return readBinary({
|
|
1792
|
+
kind: 'reference',
|
|
1793
|
+
fileRef: reference.fileRef,
|
|
1794
|
+
referrer: reference.referrer,
|
|
1795
|
+
});
|
|
841
1796
|
}
|
|
842
1797
|
function declaredAgents() {
|
|
843
1798
|
for (const capability of capabilities) {
|
|
@@ -855,21 +1810,21 @@ export function composeRuntime(options) {
|
|
|
855
1810
|
}
|
|
856
1811
|
return undefined;
|
|
857
1812
|
}
|
|
858
|
-
async function invokeAction(name, input) {
|
|
1813
|
+
async function invokeAction(name, input, dispatching) {
|
|
859
1814
|
await ready;
|
|
860
1815
|
if (closed || refusal !== null)
|
|
861
|
-
return
|
|
1816
|
+
return UNCERTAIN;
|
|
862
1817
|
if (declaredAction(name) === undefined)
|
|
863
|
-
return
|
|
1818
|
+
return undeclared(name);
|
|
864
1819
|
try {
|
|
865
1820
|
await ensureAuthorization();
|
|
866
1821
|
}
|
|
867
1822
|
catch {
|
|
868
|
-
return
|
|
869
|
-
}
|
|
870
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
871
|
-
return { kind: 'transport-uncertainty' };
|
|
1823
|
+
return UNCERTAIN;
|
|
872
1824
|
}
|
|
1825
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1826
|
+
return UNCERTAIN;
|
|
1827
|
+
await dispatching?.();
|
|
873
1828
|
journal.record(`transfer action ${name}`);
|
|
874
1829
|
try {
|
|
875
1830
|
const result = await options.adapter.action(authorization, { name, input });
|
|
@@ -883,12 +1838,66 @@ export function composeRuntime(options) {
|
|
|
883
1838
|
journal.record(`outcome action ${result.kind}`);
|
|
884
1839
|
if (result.kind === 'rejected')
|
|
885
1840
|
journal.record(`refusal rejected ${result.code}`);
|
|
1841
|
+
return calledResult(result);
|
|
1842
|
+
}
|
|
1843
|
+
catch {
|
|
1844
|
+
journal.record('outcome action transport-uncertainty');
|
|
1845
|
+
return UNCERTAIN;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
function unsettled(result) {
|
|
1849
|
+
return result.kind !== 'action' && result.retryable;
|
|
1850
|
+
}
|
|
1851
|
+
async function recoveredAction(name, input) {
|
|
1852
|
+
const operation = operationOf(input);
|
|
1853
|
+
if (operation === null)
|
|
1854
|
+
return invokeAction(name, input);
|
|
1855
|
+
const kept = async () => (await openLocal()).keepCall({ operation, name, input });
|
|
1856
|
+
calling.add(operation);
|
|
1857
|
+
try {
|
|
1858
|
+
let result = await invokeAction(name, input, kept);
|
|
1859
|
+
if (unsettled(result)) {
|
|
1860
|
+
await recovery.attempt(`action ${operation}`, async () => {
|
|
1861
|
+
if (closed || refusal !== null)
|
|
1862
|
+
return 'done';
|
|
1863
|
+
if (!socketOpen)
|
|
1864
|
+
return 'again';
|
|
1865
|
+
result = await invokeAction(name, input, kept);
|
|
1866
|
+
return unsettled(result) ? 'again' : 'done';
|
|
1867
|
+
}, { again: true });
|
|
1868
|
+
}
|
|
1869
|
+
if (!unsettled(result))
|
|
1870
|
+
await (await openLocal()).dropCall(operation);
|
|
886
1871
|
return result;
|
|
887
1872
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1873
|
+
finally {
|
|
1874
|
+
calling.delete(operation);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
async function resendCalls(signal) {
|
|
1878
|
+
if (!socketOpen || closed || stopping)
|
|
1879
|
+
return 'done';
|
|
1880
|
+
const local = await openLocal();
|
|
1881
|
+
for (const held of await local.calls()) {
|
|
1882
|
+
if (calling.has(held.operation))
|
|
1883
|
+
continue;
|
|
1884
|
+
if (closed || signal.aborted)
|
|
1885
|
+
return 'done';
|
|
1886
|
+
if (pastHorizon(held.operation)) {
|
|
1887
|
+
await local.dropCall(held.operation);
|
|
1888
|
+
continue;
|
|
1889
|
+
}
|
|
1890
|
+
calling.add(held.operation);
|
|
1891
|
+
try {
|
|
1892
|
+
if (unsettled(await invokeAction(held.name, held.input)))
|
|
1893
|
+
return 'again';
|
|
1894
|
+
await local.dropCall(held.operation);
|
|
1895
|
+
}
|
|
1896
|
+
finally {
|
|
1897
|
+
calling.delete(held.operation);
|
|
1898
|
+
}
|
|
891
1899
|
}
|
|
1900
|
+
return 'done';
|
|
892
1901
|
}
|
|
893
1902
|
function continuing() {
|
|
894
1903
|
return !closed && !discoveryPaused;
|
|
@@ -904,11 +1913,12 @@ export function composeRuntime(options) {
|
|
|
904
1913
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
905
1914
|
!options.adapter.reached(locator, roomVersion, written));
|
|
906
1915
|
}
|
|
907
|
-
async function pullRefresh(authorized, confirmed) {
|
|
908
|
-
if (!
|
|
1916
|
+
async function pullRefresh(authorized, confirmed, live) {
|
|
1917
|
+
if (!live())
|
|
909
1918
|
return unconfirmed;
|
|
1919
|
+
const turn = generation;
|
|
910
1920
|
try {
|
|
911
|
-
const result = await refreshRoom(
|
|
1921
|
+
const result = await refreshRoom(reads, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
|
|
912
1922
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
913
1923
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
914
1924
|
return result;
|
|
@@ -917,29 +1927,101 @@ export function composeRuntime(options) {
|
|
|
917
1927
|
journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
|
|
918
1928
|
throw error;
|
|
919
1929
|
}
|
|
1930
|
+
finally {
|
|
1931
|
+
reads.finished(confirmed.record.locator, turn);
|
|
1932
|
+
}
|
|
920
1933
|
}
|
|
921
|
-
|
|
922
|
-
|
|
1934
|
+
function staging(local, live) {
|
|
1935
|
+
return {
|
|
1936
|
+
held: (locator) => transfers.get(locator) ?? null,
|
|
1937
|
+
staged: (record) => local.staged(record),
|
|
1938
|
+
stage: (record, page) => exclusive(async () => {
|
|
1939
|
+
if (!live())
|
|
1940
|
+
return false;
|
|
1941
|
+
const bytes = layerBytes(page);
|
|
1942
|
+
if (!(await admitted(local, record.locator, bytes)))
|
|
1943
|
+
return false;
|
|
1944
|
+
try {
|
|
1945
|
+
await local.stage(record, page);
|
|
1946
|
+
}
|
|
1947
|
+
catch (error) {
|
|
1948
|
+
if (!exceeded(error))
|
|
1949
|
+
throw error;
|
|
1950
|
+
await exceededOn(record.locator, bytes);
|
|
1951
|
+
return false;
|
|
1952
|
+
}
|
|
1953
|
+
transfers.set(record.locator, record);
|
|
1954
|
+
readSide.recover();
|
|
1955
|
+
return true;
|
|
1956
|
+
}),
|
|
1957
|
+
abandon: (locator) => exclusive(async () => {
|
|
1958
|
+
if (!live())
|
|
1959
|
+
return;
|
|
1960
|
+
await local.abandon(locator);
|
|
1961
|
+
transfers.delete(locator);
|
|
1962
|
+
readSide.recover();
|
|
1963
|
+
}),
|
|
1964
|
+
};
|
|
1965
|
+
}
|
|
1966
|
+
function reusable(room) {
|
|
1967
|
+
const record = replicas.get(room.locator)?.confirmed.record;
|
|
1968
|
+
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1969
|
+
return null;
|
|
1970
|
+
return { documents: record.documents, revisions: record.revisions };
|
|
1971
|
+
}
|
|
1972
|
+
async function pullBootstrap(authorized, room, local, live) {
|
|
1973
|
+
if (!live())
|
|
923
1974
|
return unconfirmed;
|
|
1975
|
+
const turn = generation;
|
|
1976
|
+
if (replicas.has(room.locator)) {
|
|
1977
|
+
catching.add(room.locator);
|
|
1978
|
+
readSide.recover();
|
|
1979
|
+
}
|
|
924
1980
|
try {
|
|
925
|
-
const result = await bootstrapRoom(
|
|
1981
|
+
const result = await bootstrapRoom(reads, authorized, room, {
|
|
926
1982
|
collection: modelOf(room.locator),
|
|
927
1983
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
928
|
-
}, (models, held) => remodel(room.locator, models, held),
|
|
1984
|
+
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
929
1985
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
930
1986
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1987
|
+
if (result.kind !== 'staged')
|
|
1988
|
+
caughtUpTo(room.locator);
|
|
931
1989
|
return result;
|
|
932
1990
|
}
|
|
933
1991
|
catch (error) {
|
|
934
1992
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
1993
|
+
caughtUpTo(room.locator);
|
|
935
1994
|
throw error;
|
|
936
1995
|
}
|
|
1996
|
+
finally {
|
|
1997
|
+
reads.finished(room.locator, turn);
|
|
1998
|
+
}
|
|
937
1999
|
}
|
|
938
|
-
|
|
2000
|
+
function caughtUpTo(locator) {
|
|
2001
|
+
if (catching.delete(locator))
|
|
2002
|
+
readSide.recover();
|
|
2003
|
+
}
|
|
2004
|
+
function behind(room) {
|
|
2005
|
+
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
2006
|
+
return (held !== undefined &&
|
|
2007
|
+
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
2008
|
+
}
|
|
2009
|
+
async function advance(room, local, authorized, live) {
|
|
2010
|
+
const carried = reads.carried(room.locator);
|
|
2011
|
+
const withheld = await advanceOnce(room, local, authorized, live);
|
|
2012
|
+
if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
|
|
2013
|
+
return withheld;
|
|
2014
|
+
}
|
|
2015
|
+
return advanceOnce(room, local, authorized, live);
|
|
2016
|
+
}
|
|
2017
|
+
async function advanceOnce(room, local, authorized, live) {
|
|
2018
|
+
await referenced(room.locator);
|
|
2019
|
+
if (!live() || paused.has(room.locator))
|
|
2020
|
+
return unconfirmed;
|
|
939
2021
|
const held = replicas.get(room.locator);
|
|
940
2022
|
if (held !== undefined) {
|
|
941
|
-
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
942
|
-
if (!
|
|
2023
|
+
const refreshed = await pullRefresh(authorized, held.confirmed, live);
|
|
2024
|
+
if (!live())
|
|
943
2025
|
return unconfirmed;
|
|
944
2026
|
if (refreshed.kind === 'still-current') {
|
|
945
2027
|
stale.delete(room.locator);
|
|
@@ -953,31 +2035,45 @@ export function composeRuntime(options) {
|
|
|
953
2035
|
return null;
|
|
954
2036
|
}
|
|
955
2037
|
if (refreshed.kind === 'advanced') {
|
|
956
|
-
if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
|
|
2038
|
+
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
|
|
957
2039
|
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
958
2040
|
const reached = now !== undefined &&
|
|
959
2041
|
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
960
|
-
return reached ? null : advance(room, local, authorized);
|
|
2042
|
+
return reached ? null : advance(room, local, authorized, live);
|
|
961
2043
|
}
|
|
962
2044
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
963
2045
|
return null;
|
|
964
2046
|
}
|
|
965
2047
|
if (refreshed.kind !== 'beyond-horizon')
|
|
966
2048
|
return refreshed;
|
|
2049
|
+
if (opened !== null) {
|
|
2050
|
+
owed.add(room.locator);
|
|
2051
|
+
readSide.recover();
|
|
2052
|
+
return unconfirmed;
|
|
2053
|
+
}
|
|
967
2054
|
}
|
|
968
|
-
const bootstrapped = await pullBootstrap(authorized, room);
|
|
969
|
-
if (!
|
|
2055
|
+
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
2056
|
+
if (!live())
|
|
970
2057
|
return unconfirmed;
|
|
971
2058
|
if (bootstrapped.kind !== 'staged')
|
|
972
2059
|
return bootstrapped;
|
|
973
|
-
await publish(local, room, bootstrapped.confirmed);
|
|
2060
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
974
2061
|
return null;
|
|
975
2062
|
}
|
|
976
2063
|
async function withhold(withheld, room, local, progress) {
|
|
2064
|
+
if (withheld.kind === 'unconfirmed')
|
|
2065
|
+
return;
|
|
977
2066
|
if (withheld.kind === 'refused')
|
|
978
2067
|
stale.delete(room.locator);
|
|
979
2068
|
else
|
|
980
2069
|
stale.add(room.locator);
|
|
2070
|
+
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
2071
|
+
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
2072
|
+
repairable.set(room.locator, reset);
|
|
2073
|
+
repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
|
|
2074
|
+
}
|
|
2075
|
+
else
|
|
2076
|
+
repairable.delete(room.locator);
|
|
981
2077
|
if (withheld.kind === 'scoped-refusal') {
|
|
982
2078
|
await scoped(withheld, room, local);
|
|
983
2079
|
}
|
|
@@ -1002,6 +2098,7 @@ export function composeRuntime(options) {
|
|
|
1002
2098
|
function refusedSource(room, { refusal }) {
|
|
1003
2099
|
sourceRefusals.set(room.locator, {
|
|
1004
2100
|
code: refusal.rule,
|
|
2101
|
+
retryable: false,
|
|
1005
2102
|
fields: {
|
|
1006
2103
|
file: refusal.file,
|
|
1007
2104
|
...(refusal.location === undefined ? {} : { location: refusal.location }),
|
|
@@ -1012,12 +2109,21 @@ export function composeRuntime(options) {
|
|
|
1012
2109
|
unreadable.add(room.locator);
|
|
1013
2110
|
list(room, false);
|
|
1014
2111
|
emitRooms();
|
|
2112
|
+
moveTo({ ...status, ...attention() });
|
|
1015
2113
|
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
1016
2114
|
}
|
|
1017
2115
|
function leaving(batch, result) {
|
|
1018
2116
|
if (result.kind === 'expired')
|
|
1019
|
-
return { batch, returned:
|
|
1020
|
-
return {
|
|
2117
|
+
return { batch, returned: returnedBy('expired') };
|
|
2118
|
+
return {
|
|
2119
|
+
batch,
|
|
2120
|
+
returned: {
|
|
2121
|
+
kind: result.kind,
|
|
2122
|
+
code: result.code,
|
|
2123
|
+
fields: result.fields,
|
|
2124
|
+
retryable: result.retryable,
|
|
2125
|
+
},
|
|
2126
|
+
};
|
|
1021
2127
|
}
|
|
1022
2128
|
function logAccepted(locator, submitted, accepted) {
|
|
1023
2129
|
journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
|
|
@@ -1025,6 +2131,7 @@ export function composeRuntime(options) {
|
|
|
1025
2131
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1026
2132
|
}
|
|
1027
2133
|
async function adoptAccepted(room, local, submitted, accepted) {
|
|
2134
|
+
await referenced(room.locator);
|
|
1028
2135
|
if (!continuing())
|
|
1029
2136
|
return unconfirmed;
|
|
1030
2137
|
const current = replicas.get(room.locator);
|
|
@@ -1035,22 +2142,23 @@ export function composeRuntime(options) {
|
|
|
1035
2142
|
logAccepted(room.locator, submitted, accepted);
|
|
1036
2143
|
const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
1037
2144
|
? { kind: 'staged', confirmed: current.confirmed }
|
|
1038
|
-
: applyReturned(current.confirmed,
|
|
2145
|
+
: applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
1039
2146
|
? accepted.roomVersion
|
|
1040
2147
|
: held, accepted.roomVersion);
|
|
1041
2148
|
if (applied.kind !== 'staged') {
|
|
1042
2149
|
const state = pendings.get(submitted.id);
|
|
1043
2150
|
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1044
|
-
if (state !== undefined)
|
|
2151
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
1045
2152
|
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1046
2153
|
emitQueue(room.locator);
|
|
1047
2154
|
return applied;
|
|
1048
2155
|
}
|
|
1049
|
-
const
|
|
2156
|
+
const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
1050
2157
|
pendings.delete(submitted.id);
|
|
1051
2158
|
unreadable.delete(room.locator);
|
|
1052
2159
|
sourceRefusals.delete(room.locator);
|
|
1053
2160
|
declarationRefusals.delete(room.locator);
|
|
2161
|
+
const set = settled();
|
|
1054
2162
|
list(room, true);
|
|
1055
2163
|
publishChangeSet(set);
|
|
1056
2164
|
moveTo({ ...status, ...attention() });
|
|
@@ -1064,14 +2172,14 @@ export function composeRuntime(options) {
|
|
|
1064
2172
|
refusedSource(room, failed);
|
|
1065
2173
|
return false;
|
|
1066
2174
|
}
|
|
1067
|
-
async function submitRoom(room, local, authorized, progress) {
|
|
1068
|
-
while (continuing() && authorization !== undefined) {
|
|
2175
|
+
async function submitRoom(room, local, authorized, progress, signal) {
|
|
2176
|
+
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
1069
2177
|
const replica = replicas.get(room.locator);
|
|
1070
2178
|
const head = replica?.queue[0];
|
|
1071
2179
|
if (replica === undefined || head === undefined)
|
|
1072
|
-
return;
|
|
2180
|
+
return 'done';
|
|
1073
2181
|
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1074
|
-
return;
|
|
2182
|
+
return 'done';
|
|
1075
2183
|
const state = pendings.get(head.id);
|
|
1076
2184
|
const { submission, lost: earlier } = state ?? {
|
|
1077
2185
|
submission: { state: 'waiting' },
|
|
@@ -1079,24 +2187,30 @@ export function composeRuntime(options) {
|
|
|
1079
2187
|
};
|
|
1080
2188
|
if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
|
|
1081
2189
|
progress.complete = false;
|
|
1082
|
-
return;
|
|
2190
|
+
return { at: Date.parse(submission.reset) };
|
|
1083
2191
|
}
|
|
1084
2192
|
if (writesHeld()) {
|
|
1085
2193
|
progress.complete = false;
|
|
1086
2194
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1087
|
-
return;
|
|
2195
|
+
return 'done';
|
|
1088
2196
|
}
|
|
1089
2197
|
const lost = earlier || submission.state === 'submitted';
|
|
2198
|
+
if (lost && pastHorizon(head.id)) {
|
|
2199
|
+
await discardPending(room, local, signal);
|
|
2200
|
+
return 'done';
|
|
2201
|
+
}
|
|
1090
2202
|
let result;
|
|
1091
2203
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
1092
2204
|
await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
|
|
1093
|
-
if (discoveryPaused)
|
|
1094
|
-
return;
|
|
1095
|
-
if (writesHeld()) {
|
|
2205
|
+
if (discoveryPaused || fences.has(room.locator))
|
|
2206
|
+
return 'done';
|
|
2207
|
+
if (signal.aborted || writesHeld()) {
|
|
1096
2208
|
await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
|
|
2209
|
+
if (signal.aborted)
|
|
2210
|
+
return 'done';
|
|
1097
2211
|
progress.complete = false;
|
|
1098
2212
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1099
|
-
return;
|
|
2213
|
+
return 'done';
|
|
1100
2214
|
}
|
|
1101
2215
|
const opensBefore = openSignals;
|
|
1102
2216
|
const answered = awaitingAnswer(room.locator);
|
|
@@ -1112,32 +2226,36 @@ export function composeRuntime(options) {
|
|
|
1112
2226
|
progress.complete = false;
|
|
1113
2227
|
if (continuing())
|
|
1114
2228
|
progress.reachable = false;
|
|
1115
|
-
return;
|
|
2229
|
+
return 'again';
|
|
2230
|
+
}
|
|
2231
|
+
if (signal.aborted) {
|
|
2232
|
+
answered();
|
|
2233
|
+
return 'done';
|
|
1116
2234
|
}
|
|
1117
2235
|
if (result.kind !== 'accepted' || !continuing())
|
|
1118
2236
|
answered();
|
|
1119
2237
|
if (!continuing())
|
|
1120
|
-
return;
|
|
2238
|
+
return 'done';
|
|
1121
2239
|
if (result.kind === 'transport-uncertainty') {
|
|
1122
2240
|
progress.complete = false;
|
|
1123
2241
|
progress.reachable = false;
|
|
1124
|
-
return;
|
|
2242
|
+
return 'again';
|
|
1125
2243
|
}
|
|
1126
2244
|
if (result.kind === 'room-not-readable') {
|
|
1127
2245
|
progress.complete = false;
|
|
1128
2246
|
unreadable.add(room.locator);
|
|
1129
2247
|
list(room, false);
|
|
1130
2248
|
emitRooms();
|
|
1131
|
-
return;
|
|
2249
|
+
return 'done';
|
|
1132
2250
|
}
|
|
1133
2251
|
if (result.kind === 'revoked-authorization') {
|
|
1134
2252
|
await refuse('revoked-authorization');
|
|
1135
|
-
return;
|
|
2253
|
+
return 'done';
|
|
1136
2254
|
}
|
|
1137
2255
|
if (result.kind === 'scoped-refusal') {
|
|
1138
2256
|
await scoped(result, room, local);
|
|
1139
2257
|
progress.complete = false;
|
|
1140
|
-
return;
|
|
2258
|
+
return 'done';
|
|
1141
2259
|
}
|
|
1142
2260
|
if (result.kind === 'write-halted') {
|
|
1143
2261
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
@@ -1146,16 +2264,24 @@ export function composeRuntime(options) {
|
|
|
1146
2264
|
continue;
|
|
1147
2265
|
haltedRefusal = true;
|
|
1148
2266
|
progress.complete = false;
|
|
1149
|
-
return;
|
|
2267
|
+
return 'done';
|
|
1150
2268
|
}
|
|
1151
2269
|
if (result.kind === 'conflicted' ||
|
|
1152
2270
|
result.kind === 'rejected' ||
|
|
1153
2271
|
result.kind === 'expired' ||
|
|
1154
2272
|
result.kind === 'retryable-refusal') {
|
|
2273
|
+
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
2274
|
+
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
2275
|
+
await discardPending(room, local, signal);
|
|
2276
|
+
return 'done';
|
|
2277
|
+
}
|
|
1155
2278
|
if (result.kind === 'expired') {
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
2279
|
+
await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
|
|
2280
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
2281
|
+
if (withheld !== null) {
|
|
2282
|
+
await withhold(withheld, room, local, progress);
|
|
2283
|
+
return 'done';
|
|
2284
|
+
}
|
|
1159
2285
|
}
|
|
1160
2286
|
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
|
|
1161
2287
|
(result.kind === 'conflicted' && result.roomVersion !== undefined
|
|
@@ -1168,8 +2294,7 @@ export function composeRuntime(options) {
|
|
|
1168
2294
|
const reset = result.reset;
|
|
1169
2295
|
progress.complete = false;
|
|
1170
2296
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1171
|
-
|
|
1172
|
-
return;
|
|
2297
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
1173
2298
|
}
|
|
1174
2299
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
1175
2300
|
const left = leaving(head, result);
|
|
@@ -1180,36 +2305,82 @@ export function composeRuntime(options) {
|
|
|
1180
2305
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
1181
2306
|
return;
|
|
1182
2307
|
}
|
|
1183
|
-
const
|
|
1184
|
-
leaving: result.kind === 'expired' && landed(head, current.confirmed)
|
|
2308
|
+
const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
2309
|
+
leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
|
|
2310
|
+
(result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
|
|
1185
2311
|
? { ...left, returned: null }
|
|
1186
2312
|
: left,
|
|
1187
2313
|
});
|
|
1188
|
-
publishChangeSet(
|
|
2314
|
+
publishChangeSet(settled());
|
|
1189
2315
|
moveTo({ ...status, ...attention() });
|
|
1190
2316
|
});
|
|
1191
2317
|
if (result.kind === 'expired')
|
|
1192
2318
|
continue;
|
|
1193
|
-
const withheld = await advance(room, local, authorized);
|
|
1194
|
-
if (withheld !== null)
|
|
1195
|
-
|
|
2319
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
2320
|
+
if (withheld !== null) {
|
|
2321
|
+
await withhold(withheld, room, local, progress);
|
|
2322
|
+
return 'done';
|
|
2323
|
+
}
|
|
1196
2324
|
continue;
|
|
1197
2325
|
}
|
|
1198
2326
|
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1199
2327
|
if (!kept) {
|
|
1200
2328
|
progress.complete = false;
|
|
1201
|
-
return;
|
|
2329
|
+
return 'done';
|
|
1202
2330
|
}
|
|
1203
2331
|
}
|
|
2332
|
+
return 'done';
|
|
2333
|
+
}
|
|
2334
|
+
function pastHorizon(identity) {
|
|
2335
|
+
return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
|
|
2336
|
+
}
|
|
2337
|
+
async function discardPending(room, local, signal) {
|
|
2338
|
+
const { locator } = room;
|
|
2339
|
+
const discarded = await exclusive(async () => {
|
|
2340
|
+
if (fences.has(locator))
|
|
2341
|
+
return false;
|
|
2342
|
+
const before = replicas.get(locator);
|
|
2343
|
+
const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
2344
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
2345
|
+
lastFenced.set(locator, ++fencings);
|
|
2346
|
+
try {
|
|
2347
|
+
await local.discard(locator);
|
|
2348
|
+
}
|
|
2349
|
+
finally {
|
|
2350
|
+
fences.delete(locator);
|
|
2351
|
+
}
|
|
2352
|
+
for (const batch of queue)
|
|
2353
|
+
pendings.delete(batch.id);
|
|
2354
|
+
queuesWithoutReplica.delete(locator);
|
|
2355
|
+
const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
|
|
2356
|
+
if (after !== null)
|
|
2357
|
+
replicas.set(locator, after);
|
|
2358
|
+
journal.record(`room discarded ${locator}`);
|
|
2359
|
+
publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
|
|
2360
|
+
moveTo({ ...status, ...attention() });
|
|
2361
|
+
return true;
|
|
2362
|
+
});
|
|
2363
|
+
if (!discarded)
|
|
2364
|
+
return;
|
|
2365
|
+
const progress = { complete: true, reachable: true };
|
|
2366
|
+
await roomTask(locator, async (live) => {
|
|
2367
|
+
await Promise.race([
|
|
2368
|
+
reload(room, local, progress, () => live() && !signal.aborted),
|
|
2369
|
+
abandonedOn(signal),
|
|
2370
|
+
]);
|
|
2371
|
+
});
|
|
2372
|
+
if (signal.aborted)
|
|
2373
|
+
return;
|
|
2374
|
+
await rest(progress);
|
|
1204
2375
|
}
|
|
1205
2376
|
function awaitingAnswer(locator) {
|
|
1206
2377
|
let release = () => { };
|
|
1207
2378
|
const awaited = new Promise((resolve) => {
|
|
1208
2379
|
release = resolve;
|
|
1209
2380
|
});
|
|
1210
|
-
answering.set(locator, awaited);
|
|
2381
|
+
answering.set(locator, { generation, answered: awaited });
|
|
1211
2382
|
return () => {
|
|
1212
|
-
if (answering.get(locator) === awaited)
|
|
2383
|
+
if (answering.get(locator)?.answered === awaited)
|
|
1213
2384
|
answering.delete(locator);
|
|
1214
2385
|
release();
|
|
1215
2386
|
};
|
|
@@ -1242,7 +2413,7 @@ export function composeRuntime(options) {
|
|
|
1242
2413
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
1243
2414
|
};
|
|
1244
2415
|
const reclassified = sameMembership(declared, held.confirmed.record)
|
|
1245
|
-
?
|
|
2416
|
+
? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
|
|
1246
2417
|
: readReclassified(declared, held.confirmed);
|
|
1247
2418
|
const record = reclassified.kind === 'staged'
|
|
1248
2419
|
? declared
|
|
@@ -1250,12 +2421,10 @@ export function composeRuntime(options) {
|
|
|
1250
2421
|
...declared,
|
|
1251
2422
|
typedSelection: held.confirmed.record.typedSelection,
|
|
1252
2423
|
};
|
|
1253
|
-
const read = reclassified.kind === 'deferred'
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
queue: held.queue,
|
|
1258
|
-
});
|
|
2424
|
+
const read = reclassified.kind === 'deferred'
|
|
2425
|
+
? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
|
|
2426
|
+
: reclassified;
|
|
2427
|
+
await local.write({ next: record, previous: held.confirmed.record });
|
|
1259
2428
|
if (read.kind === 'staged') {
|
|
1260
2429
|
replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
|
|
1261
2430
|
if (declarationRefusals.delete(room.locator)) {
|
|
@@ -1282,7 +2451,16 @@ export function composeRuntime(options) {
|
|
|
1282
2451
|
});
|
|
1283
2452
|
}
|
|
1284
2453
|
async function scoped(result, room, local) {
|
|
1285
|
-
|
|
2454
|
+
if ((result.scope === 'content' || result.scope === 'sync') && !result.retryable) {
|
|
2455
|
+
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
2456
|
+
await loseRooms([room.locator]);
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
2459
|
+
scopedRefusals.set(room.locator, {
|
|
2460
|
+
code: result.code,
|
|
2461
|
+
fields: result.fields,
|
|
2462
|
+
retryable: result.retryable,
|
|
2463
|
+
});
|
|
1286
2464
|
let scopedAway = room;
|
|
1287
2465
|
if (result.scope === 'model')
|
|
1288
2466
|
await retainModel(local, room, 'collection', {
|
|
@@ -1319,22 +2497,27 @@ export function composeRuntime(options) {
|
|
|
1319
2497
|
roomVersion: record.roomVersion,
|
|
1320
2498
|
documents: record.documents,
|
|
1321
2499
|
revisions: record.revisions,
|
|
1322
|
-
attachments: record.attachments,
|
|
1323
2500
|
},
|
|
1324
2501
|
];
|
|
1325
2502
|
});
|
|
1326
2503
|
}
|
|
1327
|
-
async function synchronize(listed, local, progress, attempt) {
|
|
1328
|
-
const
|
|
1329
|
-
const
|
|
2504
|
+
async function synchronize(listed, local, progress, attempt, live) {
|
|
2505
|
+
const acquired = listed.filter(acquires);
|
|
2506
|
+
const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
|
|
2507
|
+
if (!live())
|
|
2508
|
+
return;
|
|
2509
|
+
const ready = acquired.filter((_, index) => {
|
|
1330
2510
|
const outcome = prepared[index];
|
|
1331
2511
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1332
2512
|
});
|
|
1333
|
-
if (attempt === 'opened')
|
|
1334
|
-
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
2513
|
+
if (attempt === 'opened') {
|
|
2514
|
+
options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
|
|
2515
|
+
handed = true;
|
|
2516
|
+
}
|
|
2517
|
+
discovering?.release();
|
|
1335
2518
|
const loaded = await Promise.allSettled(ready
|
|
1336
2519
|
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1337
|
-
.map((room) => loadRoom(room, local, progress)));
|
|
2520
|
+
.map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
|
|
1338
2521
|
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1339
2522
|
if (failed !== undefined)
|
|
1340
2523
|
throw failed.reason;
|
|
@@ -1356,21 +2539,12 @@ export function composeRuntime(options) {
|
|
|
1356
2539
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1357
2540
|
if (discoveryPaused || authorization === undefined)
|
|
1358
2541
|
return false;
|
|
1359
|
-
|
|
1360
|
-
if (replicas.has(room.locator))
|
|
1361
|
-
progress.complete = false;
|
|
1362
|
-
return false;
|
|
1363
|
-
}
|
|
1364
|
-
return true;
|
|
2542
|
+
return room.replica.admission === 'admitted';
|
|
1365
2543
|
}
|
|
1366
|
-
async function caughtUp(room, local, progress) {
|
|
1367
|
-
|
|
1368
|
-
if (held === undefined || authorization === undefined)
|
|
1369
|
-
return;
|
|
1370
|
-
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
1371
|
-
if (!behind)
|
|
2544
|
+
async function caughtUp(room, local, progress, live) {
|
|
2545
|
+
if (authorization === undefined || !live() || !behind(room))
|
|
1372
2546
|
return;
|
|
1373
|
-
const withheld = await advance(room, local, authorization);
|
|
2547
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1374
2548
|
if (withheld !== null)
|
|
1375
2549
|
await withhold(withheld, room, local, progress);
|
|
1376
2550
|
}
|
|
@@ -1380,39 +2554,46 @@ export function composeRuntime(options) {
|
|
|
1380
2554
|
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1381
2555
|
});
|
|
1382
2556
|
}
|
|
1383
|
-
async function loadRoom(room, local, progress) {
|
|
1384
|
-
if (
|
|
2557
|
+
async function loadRoom(room, local, progress, live) {
|
|
2558
|
+
if (!live() || authorization === undefined)
|
|
1385
2559
|
return;
|
|
1386
2560
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1387
|
-
await reload(room, local, progress);
|
|
2561
|
+
await reload(room, local, progress, live);
|
|
1388
2562
|
return;
|
|
1389
2563
|
}
|
|
1390
|
-
const withheld = await advance(room, local, authorization);
|
|
2564
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1391
2565
|
if (withheld !== null) {
|
|
1392
2566
|
await withhold(withheld, room, local, progress);
|
|
1393
2567
|
return;
|
|
1394
2568
|
}
|
|
1395
|
-
await caughtUp(room, local, progress);
|
|
2569
|
+
await caughtUp(room, local, progress, live);
|
|
1396
2570
|
}
|
|
1397
|
-
async function reload(room, local, progress) {
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
2571
|
+
async function reload(room, local, progress, live) {
|
|
2572
|
+
try {
|
|
2573
|
+
if (!live() || authorization === undefined)
|
|
2574
|
+
return;
|
|
2575
|
+
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
2576
|
+
if (!live())
|
|
2577
|
+
return;
|
|
2578
|
+
if (bootstrapped.kind !== 'staged') {
|
|
2579
|
+
await withhold(bootstrapped, room, local, progress);
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2582
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
2583
|
+
await caughtUp(room, local, progress, live);
|
|
2584
|
+
}
|
|
2585
|
+
finally {
|
|
2586
|
+
caughtUpTo(room.locator);
|
|
1406
2587
|
}
|
|
1407
|
-
await publish(local, room, bootstrapped.confirmed);
|
|
1408
|
-
await caughtUp(room, local, progress);
|
|
1409
2588
|
}
|
|
1410
|
-
async function discoverRooms(local, progress, attempt) {
|
|
2589
|
+
async function discoverRooms(local, progress, attempt, live) {
|
|
1411
2590
|
if (closed || authorization === undefined) {
|
|
1412
2591
|
notifyReady();
|
|
1413
2592
|
return;
|
|
1414
2593
|
}
|
|
1415
2594
|
moveTo(transferring(status));
|
|
2595
|
+
discoveries += 1;
|
|
2596
|
+
const started = discoveries;
|
|
1416
2597
|
let result;
|
|
1417
2598
|
try {
|
|
1418
2599
|
result = await options.adapter.discover(authorization);
|
|
@@ -1420,7 +2601,8 @@ export function composeRuntime(options) {
|
|
|
1420
2601
|
catch {
|
|
1421
2602
|
result = { kind: 'transport-uncertainty' };
|
|
1422
2603
|
}
|
|
1423
|
-
if (
|
|
2604
|
+
if (!live()) {
|
|
2605
|
+
progress.complete = false;
|
|
1424
2606
|
notifyReady();
|
|
1425
2607
|
return;
|
|
1426
2608
|
}
|
|
@@ -1434,15 +2616,16 @@ export function composeRuntime(options) {
|
|
|
1434
2616
|
}
|
|
1435
2617
|
progress.reachable = false;
|
|
1436
2618
|
progress.complete = false;
|
|
1437
|
-
if (attempt === 'opened') {
|
|
1438
|
-
options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
|
|
1439
|
-
}
|
|
1440
2619
|
notifyReady();
|
|
1441
2620
|
return;
|
|
1442
2621
|
}
|
|
1443
2622
|
if (result.kind === 'contract-unavailable') {
|
|
1444
2623
|
journal.record(`connection contract unavailable ${result.code}`);
|
|
1445
|
-
await refuse('missing-required-guarantee', {
|
|
2624
|
+
await refuse('missing-required-guarantee', {
|
|
2625
|
+
code: result.code,
|
|
2626
|
+
fields: result.fields,
|
|
2627
|
+
retryable: result.retryable,
|
|
2628
|
+
});
|
|
1446
2629
|
notifyReady();
|
|
1447
2630
|
return;
|
|
1448
2631
|
}
|
|
@@ -1477,14 +2660,20 @@ export function composeRuntime(options) {
|
|
|
1477
2660
|
}
|
|
1478
2661
|
const previousCapabilities = capabilities;
|
|
1479
2662
|
capabilities = result.capabilities;
|
|
2663
|
+
outcomeRetention = result.retention.outcome;
|
|
1480
2664
|
notifyReady();
|
|
1481
2665
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1482
2666
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
1483
2667
|
limits = result.providerLimits;
|
|
1484
2668
|
await local.declare(limits, capabilities);
|
|
1485
2669
|
}
|
|
1486
|
-
|
|
1487
|
-
|
|
2670
|
+
const signedIn = result.member ?? null;
|
|
2671
|
+
if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
|
|
2672
|
+
await local.keepMember(signedIn);
|
|
2673
|
+
member = signedIn;
|
|
2674
|
+
emitMember();
|
|
2675
|
+
}
|
|
2676
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
1488
2677
|
}
|
|
1489
2678
|
async function ensureAuthorization() {
|
|
1490
2679
|
if (authorization !== undefined)
|
|
@@ -1498,76 +2687,175 @@ export function composeRuntime(options) {
|
|
|
1498
2687
|
throw error;
|
|
1499
2688
|
}
|
|
1500
2689
|
}
|
|
1501
|
-
function
|
|
2690
|
+
async function admittedRead(work) {
|
|
2691
|
+
if (roomReads.generation !== generation) {
|
|
2692
|
+
roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
2693
|
+
}
|
|
2694
|
+
const reads = roomReads;
|
|
2695
|
+
while (reads.free === 0)
|
|
2696
|
+
await new Promise((admit) => reads.waiting.push(admit));
|
|
2697
|
+
reads.free -= 1;
|
|
2698
|
+
try {
|
|
2699
|
+
await work();
|
|
2700
|
+
}
|
|
2701
|
+
finally {
|
|
2702
|
+
reads.free += 1;
|
|
2703
|
+
reads.waiting.shift()?.();
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
function roomTask(locator, work) {
|
|
2707
|
+
return task(roomLane(locator), (live) => admittedRead(() => work(live)));
|
|
2708
|
+
}
|
|
2709
|
+
function task(lane, work) {
|
|
1502
2710
|
if (stopping)
|
|
1503
|
-
return
|
|
1504
|
-
|
|
1505
|
-
|
|
2711
|
+
return Promise.resolve();
|
|
2712
|
+
const held = lanes.get(lane);
|
|
2713
|
+
const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
|
|
2714
|
+
const turn = current.generation;
|
|
2715
|
+
const live = () => turn === generation && continuing();
|
|
2716
|
+
const run = current.chain.then(() => work(live)).catch(() => undefined);
|
|
2717
|
+
current.chain = run;
|
|
2718
|
+
lanes.set(lane, current);
|
|
2719
|
+
working.add(run);
|
|
2720
|
+
void run.finally(() => working.delete(run));
|
|
2721
|
+
return run;
|
|
1506
2722
|
}
|
|
1507
2723
|
function sync(attempt) {
|
|
1508
|
-
return task(async () => {
|
|
1509
|
-
if (
|
|
2724
|
+
return task(OPENING_LANE, async (live) => {
|
|
2725
|
+
if (!live()) {
|
|
1510
2726
|
notifyReady();
|
|
1511
2727
|
return;
|
|
1512
2728
|
}
|
|
1513
2729
|
const progress = { complete: true, reachable: true };
|
|
1514
|
-
|
|
2730
|
+
const gate = discover();
|
|
2731
|
+
if (attempt === 'opened')
|
|
2732
|
+
opened = generation;
|
|
2733
|
+
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
2734
|
+
if (attempt === 'opened')
|
|
2735
|
+
streamed = progress;
|
|
1515
2736
|
complete = false;
|
|
1516
2737
|
recorded = false;
|
|
1517
2738
|
moveTo(transferring(status));
|
|
2739
|
+
handed = false;
|
|
1518
2740
|
try {
|
|
1519
2741
|
const local = await openLocal();
|
|
1520
|
-
if (
|
|
2742
|
+
if (!live())
|
|
1521
2743
|
return;
|
|
1522
2744
|
await ensureAuthorization();
|
|
1523
|
-
if (
|
|
2745
|
+
if (!live())
|
|
1524
2746
|
return;
|
|
1525
|
-
await discoverRooms(local, progress, attempt);
|
|
2747
|
+
await discoverRooms(local, progress, attempt, live);
|
|
1526
2748
|
}
|
|
1527
2749
|
catch {
|
|
1528
2750
|
progress.complete = false;
|
|
1529
|
-
if (attempt === 'opened')
|
|
1530
|
-
|
|
1531
|
-
}
|
|
2751
|
+
if (attempt === 'opened' && !handed)
|
|
2752
|
+
progress.reachable = false;
|
|
1532
2753
|
}
|
|
1533
2754
|
finally {
|
|
2755
|
+
gate.release();
|
|
1534
2756
|
notifyReady();
|
|
1535
2757
|
}
|
|
1536
|
-
if (
|
|
2758
|
+
if (!live())
|
|
2759
|
+
return;
|
|
2760
|
+
if (attempt !== 'opened') {
|
|
2761
|
+
await finishOpening(progress, live);
|
|
2762
|
+
return;
|
|
2763
|
+
}
|
|
2764
|
+
if (handed || closed || progress.reachable)
|
|
2765
|
+
return;
|
|
2766
|
+
underway = null;
|
|
2767
|
+
opened = null;
|
|
2768
|
+
journal.record('connection reopen');
|
|
2769
|
+
await rest(progress, live);
|
|
2770
|
+
options.adapter.roomStream?.reopen();
|
|
2771
|
+
});
|
|
2772
|
+
}
|
|
2773
|
+
function rediscover() {
|
|
2774
|
+
return task(OPENING_LANE, async (live) => {
|
|
2775
|
+
if (!live() || !socketOpen)
|
|
2776
|
+
return;
|
|
2777
|
+
const progress = { complete: true, reachable: true };
|
|
2778
|
+
const gate = discover();
|
|
2779
|
+
moveTo(transferring(status));
|
|
2780
|
+
try {
|
|
2781
|
+
const local = await openLocal();
|
|
2782
|
+
if (!live())
|
|
2783
|
+
return;
|
|
2784
|
+
await ensureAuthorization();
|
|
2785
|
+
if (!live())
|
|
2786
|
+
return;
|
|
2787
|
+
await discoverRooms(local, progress, 'authorized', live);
|
|
2788
|
+
}
|
|
2789
|
+
catch {
|
|
2790
|
+
progress.complete = false;
|
|
2791
|
+
}
|
|
2792
|
+
finally {
|
|
2793
|
+
gate.release();
|
|
2794
|
+
notifyReady();
|
|
2795
|
+
}
|
|
2796
|
+
if (!live())
|
|
1537
2797
|
return;
|
|
1538
|
-
|
|
2798
|
+
if (!progress.complete)
|
|
2799
|
+
complete = false;
|
|
2800
|
+
await rest(progress, live);
|
|
1539
2801
|
});
|
|
1540
2802
|
}
|
|
1541
|
-
function closedSocket(revoked) {
|
|
2803
|
+
function closedSocket(revoked, lost) {
|
|
1542
2804
|
const opened = socketOpen;
|
|
1543
2805
|
socketOpen = false;
|
|
2806
|
+
if (lost) {
|
|
2807
|
+
reads.lost();
|
|
2808
|
+
conversations.lost();
|
|
2809
|
+
}
|
|
1544
2810
|
if (revoked) {
|
|
1545
2811
|
void refuse('revoked-authorization');
|
|
1546
2812
|
return;
|
|
1547
2813
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
2814
|
+
complete = false;
|
|
2815
|
+
if (streamed !== null)
|
|
2816
|
+
streamed.complete = false;
|
|
2817
|
+
moveTo({
|
|
2818
|
+
...status,
|
|
2819
|
+
state: status.state === 'synced' ? 'syncing' : status.state,
|
|
2820
|
+
providerUnreachable: status.providerUnreachable || !opened,
|
|
2821
|
+
});
|
|
1551
2822
|
}
|
|
1552
2823
|
function applyOpening(expired) {
|
|
1553
|
-
|
|
1554
|
-
|
|
2824
|
+
const progress = underway?.generation === generation ? underway.progress : null;
|
|
2825
|
+
const cycle = progress ?? { complete: true, reachable: true };
|
|
2826
|
+
const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
|
|
2827
|
+
if (opened === generation)
|
|
2828
|
+
opened = null;
|
|
2829
|
+
return task(OPENING_LANE, async (live) => {
|
|
1555
2830
|
underway = null;
|
|
1556
|
-
if (
|
|
2831
|
+
if (!live())
|
|
1557
2832
|
return;
|
|
1558
2833
|
if (progress === null) {
|
|
1559
|
-
await
|
|
2834
|
+
await replacing;
|
|
2835
|
+
await rest(cycle, live);
|
|
1560
2836
|
return;
|
|
1561
2837
|
}
|
|
2838
|
+
await roomsCarried(expired);
|
|
2839
|
+
if (!live())
|
|
2840
|
+
return;
|
|
2841
|
+
const replaced = new Set(owed);
|
|
2842
|
+
owed.clear();
|
|
1562
2843
|
try {
|
|
1563
2844
|
const local = await openLocal();
|
|
1564
2845
|
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
1565
|
-
const reloading = held.filter((room) => expired.includes(room.locator)
|
|
1566
|
-
|
|
2846
|
+
const reloading = held.filter((room) => !expired.includes(room.locator) &&
|
|
2847
|
+
(replaced.has(room.locator) || revoked(room.locator)));
|
|
2848
|
+
for (const room of reloading)
|
|
2849
|
+
catching.add(room.locator);
|
|
2850
|
+
readSide.recover();
|
|
2851
|
+
const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
|
|
2852
|
+
const reloaded = [...(await reloads), ...(await replacing)];
|
|
1567
2853
|
if (progress.reachable) {
|
|
1568
|
-
for (const room of held)
|
|
1569
|
-
if (!reloading.includes(room))
|
|
2854
|
+
for (const room of held) {
|
|
2855
|
+
if (!expired.includes(room.locator) && !reloading.includes(room)) {
|
|
1570
2856
|
await confirmModels(room, local);
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
1571
2859
|
}
|
|
1572
2860
|
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1573
2861
|
if (failed !== undefined)
|
|
@@ -1577,21 +2865,21 @@ export function composeRuntime(options) {
|
|
|
1577
2865
|
progress.complete = false;
|
|
1578
2866
|
progress.reachable = false;
|
|
1579
2867
|
}
|
|
1580
|
-
|
|
2868
|
+
if (live())
|
|
2869
|
+
await finishOpening(progress, live);
|
|
1581
2870
|
});
|
|
1582
2871
|
}
|
|
1583
|
-
async function
|
|
1584
|
-
const progress = { complete: true, reachable: true };
|
|
2872
|
+
async function reloadNow(replaced, progress) {
|
|
1585
2873
|
try {
|
|
1586
2874
|
const local = await openLocal();
|
|
1587
|
-
await Promise.allSettled([...discovered.values()]
|
|
1588
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
1589
|
-
.map((room) => reload(room, local, progress)));
|
|
2875
|
+
return await Promise.allSettled([...discovered.values()]
|
|
2876
|
+
.filter((room) => replicas.has(room.locator) && replaced(room))
|
|
2877
|
+
.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
1590
2878
|
}
|
|
1591
2879
|
catch {
|
|
1592
2880
|
progress.reachable = false;
|
|
2881
|
+
return [];
|
|
1593
2882
|
}
|
|
1594
|
-
await rest(progress);
|
|
1595
2883
|
}
|
|
1596
2884
|
async function confirmModels(room, local) {
|
|
1597
2885
|
for (const attached of ['collection', 'typed-selection']) {
|
|
@@ -1602,38 +2890,96 @@ export function composeRuntime(options) {
|
|
|
1602
2890
|
}
|
|
1603
2891
|
}
|
|
1604
2892
|
function submitLater(locator) {
|
|
1605
|
-
|
|
1606
|
-
if (running !== undefined) {
|
|
1607
|
-
running.again = true;
|
|
2893
|
+
if (localSaves)
|
|
1608
2894
|
return;
|
|
2895
|
+
void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
|
|
2896
|
+
}
|
|
2897
|
+
async function submitPass(locator, signal) {
|
|
2898
|
+
const room = discovered.get(locator);
|
|
2899
|
+
if (closed || stopping || discoveryPaused || room === undefined)
|
|
2900
|
+
return 'done';
|
|
2901
|
+
const progress = { complete: true, reachable: true };
|
|
2902
|
+
let next;
|
|
2903
|
+
try {
|
|
2904
|
+
await ensureAuthorization();
|
|
2905
|
+
if (authorization === undefined)
|
|
2906
|
+
return 'done';
|
|
2907
|
+
next = await submitRoom(room, await openLocal(), authorization, progress, signal);
|
|
1609
2908
|
}
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
2909
|
+
catch {
|
|
2910
|
+
progress.reachable = false;
|
|
2911
|
+
next = 'again';
|
|
2912
|
+
}
|
|
2913
|
+
if (signal.aborted)
|
|
2914
|
+
return 'done';
|
|
2915
|
+
await rest(progress);
|
|
2916
|
+
return next === 'again' && !socketOpen ? 'done' : next;
|
|
2917
|
+
}
|
|
2918
|
+
function repairLater(locator, when) {
|
|
2919
|
+
if (!socketOpen || repairing.has(locator))
|
|
2920
|
+
return;
|
|
2921
|
+
void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
|
|
2922
|
+
}
|
|
2923
|
+
async function repairPass(locator, signal) {
|
|
2924
|
+
const room = discovered.get(locator);
|
|
2925
|
+
if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
|
|
2926
|
+
return 'done';
|
|
2927
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2928
|
+
return 'done';
|
|
2929
|
+
const progress = { complete: true, reachable: true };
|
|
2930
|
+
repairing.add(locator);
|
|
2931
|
+
try {
|
|
2932
|
+
const local = await openLocal();
|
|
2933
|
+
await roomTask(locator, async (live) => {
|
|
2934
|
+
const current = () => live() && !signal.aborted;
|
|
2935
|
+
if (!current() || authorization === undefined)
|
|
2936
|
+
return;
|
|
2937
|
+
const withheld = await Promise.race([
|
|
2938
|
+
advance(room, local, authorization, current),
|
|
2939
|
+
abandonedOn(signal),
|
|
2940
|
+
]);
|
|
2941
|
+
if (withheld !== null && current())
|
|
2942
|
+
await withhold(withheld, room, local, progress);
|
|
2943
|
+
});
|
|
2944
|
+
}
|
|
2945
|
+
catch {
|
|
2946
|
+
stale.add(locator);
|
|
2947
|
+
progress.reachable = false;
|
|
2948
|
+
}
|
|
2949
|
+
finally {
|
|
2950
|
+
repairing.delete(locator);
|
|
2951
|
+
}
|
|
2952
|
+
if (signal.aborted)
|
|
2953
|
+
return 'done';
|
|
2954
|
+
await rest(progress);
|
|
2955
|
+
if (!stale.has(locator)) {
|
|
2956
|
+
if ((replicas.get(locator)?.queue.length ?? 0) > 0)
|
|
2957
|
+
submitLater(locator);
|
|
2958
|
+
return 'done';
|
|
2959
|
+
}
|
|
2960
|
+
if (!repairable.has(locator))
|
|
2961
|
+
return 'done';
|
|
2962
|
+
const reset = repairable.get(locator);
|
|
2963
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
2964
|
+
}
|
|
2965
|
+
function resendLater() {
|
|
2966
|
+
if (!socketOpen)
|
|
2967
|
+
return;
|
|
2968
|
+
void recovery.attempt('resend', resendPass, { again: true });
|
|
2969
|
+
}
|
|
2970
|
+
async function resendPass(signal) {
|
|
2971
|
+
if (!socketOpen || closed || stopping || discoveryPaused)
|
|
2972
|
+
return 'done';
|
|
2973
|
+
const reading = { complete: true, reachable: true };
|
|
2974
|
+
try {
|
|
2975
|
+
await conversations.resend(reading);
|
|
2976
|
+
}
|
|
2977
|
+
catch {
|
|
2978
|
+
reading.reachable = false;
|
|
2979
|
+
}
|
|
2980
|
+
if (signal.aborted)
|
|
2981
|
+
return 'done';
|
|
2982
|
+
return reading.reachable && reading.complete ? 'done' : 'again';
|
|
1637
2983
|
}
|
|
1638
2984
|
function submitAll() {
|
|
1639
2985
|
for (const [locator, replica] of replicas) {
|
|
@@ -1641,31 +2987,44 @@ export function composeRuntime(options) {
|
|
|
1641
2987
|
submitLater(locator);
|
|
1642
2988
|
}
|
|
1643
2989
|
}
|
|
1644
|
-
async function finishOpening(progress) {
|
|
2990
|
+
async function finishOpening(progress, live) {
|
|
1645
2991
|
try {
|
|
1646
|
-
if (
|
|
2992
|
+
if (!live()) {
|
|
1647
2993
|
progress.complete = false;
|
|
1648
2994
|
return;
|
|
1649
2995
|
}
|
|
2996
|
+
reads.applied();
|
|
1650
2997
|
submitAll();
|
|
2998
|
+
for (const locator of stale)
|
|
2999
|
+
if (repairable.has(locator))
|
|
3000
|
+
repairLater(locator, {});
|
|
1651
3001
|
await conversations.reconcile();
|
|
1652
3002
|
if (status.providerUnreachable)
|
|
1653
3003
|
conversations.reconnected();
|
|
1654
3004
|
const reading = { complete: true, reachable: true };
|
|
1655
|
-
|
|
1656
|
-
|
|
3005
|
+
if (demand.conversations)
|
|
3006
|
+
await conversations.catchUp(reading);
|
|
3007
|
+
if (progress.reachable && reading.reachable)
|
|
3008
|
+
await conversations.resend(reading);
|
|
3009
|
+
if (progress.reachable)
|
|
3010
|
+
void recovery.attempt('calls', resendCalls);
|
|
3011
|
+
if (!reading.reachable) {
|
|
1657
3012
|
progress.reachable = false;
|
|
3013
|
+
resendLater();
|
|
3014
|
+
}
|
|
1658
3015
|
}
|
|
1659
3016
|
catch {
|
|
1660
3017
|
progress.complete = false;
|
|
1661
3018
|
}
|
|
1662
3019
|
finally {
|
|
1663
|
-
|
|
1664
|
-
|
|
3020
|
+
if (live()) {
|
|
3021
|
+
complete = progress.complete;
|
|
3022
|
+
await rest(progress, live);
|
|
3023
|
+
}
|
|
1665
3024
|
}
|
|
1666
3025
|
}
|
|
1667
|
-
async function rest(progress) {
|
|
1668
|
-
if (
|
|
3026
|
+
async function rest(progress, live = () => !closed) {
|
|
3027
|
+
if (!live())
|
|
1669
3028
|
return;
|
|
1670
3029
|
let lastCompleted = status.lastCompleted;
|
|
1671
3030
|
let synced = complete && stale.size === 0 && !pending();
|
|
@@ -1673,6 +3032,8 @@ export function composeRuntime(options) {
|
|
|
1673
3032
|
const completedAt = new Date().toISOString();
|
|
1674
3033
|
try {
|
|
1675
3034
|
await (await openLocal()).complete(completedAt);
|
|
3035
|
+
if (!live())
|
|
3036
|
+
return;
|
|
1676
3037
|
recorded = true;
|
|
1677
3038
|
lastCompleted = completedAt;
|
|
1678
3039
|
}
|
|
@@ -1681,46 +3042,74 @@ export function composeRuntime(options) {
|
|
|
1681
3042
|
}
|
|
1682
3043
|
}
|
|
1683
3044
|
moveTo({
|
|
1684
|
-
state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
|
|
3045
|
+
state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
|
|
1685
3046
|
lastCompleted,
|
|
1686
3047
|
providerUnreachable: !progress.reachable,
|
|
1687
3048
|
...attention(),
|
|
1688
3049
|
});
|
|
1689
3050
|
}
|
|
3051
|
+
function discover() {
|
|
3052
|
+
let release = () => { };
|
|
3053
|
+
const done = new Promise((resolve) => {
|
|
3054
|
+
release = resolve;
|
|
3055
|
+
});
|
|
3056
|
+
discovering = { generation, done, release };
|
|
3057
|
+
return discovering;
|
|
3058
|
+
}
|
|
1690
3059
|
function applyRoom(locator, roomVersion) {
|
|
1691
3060
|
announced.set(locator, roomVersion);
|
|
1692
|
-
return task(async () => {
|
|
1693
|
-
|
|
1694
|
-
|
|
3061
|
+
return task(roomLane(locator), async (live) => {
|
|
3062
|
+
if (discovering?.generation === generation)
|
|
3063
|
+
await discovering.done;
|
|
3064
|
+
const saving = answering.get(locator);
|
|
3065
|
+
if (saving?.generation === generation)
|
|
3066
|
+
await saving.answered;
|
|
3067
|
+
if (!live() || authorization === undefined)
|
|
1695
3068
|
return;
|
|
1696
3069
|
const room = discovered.get(locator);
|
|
1697
3070
|
const held = replicas.get(locator);
|
|
1698
|
-
if (room === undefined || room.replica.admission !== 'admitted')
|
|
3071
|
+
if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
|
|
1699
3072
|
return;
|
|
1700
3073
|
if (held !== undefined &&
|
|
1701
3074
|
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
1702
3075
|
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
1703
3076
|
return;
|
|
1704
3077
|
}
|
|
1705
|
-
const during = underway;
|
|
3078
|
+
const during = underway?.generation === generation ? underway.progress : null;
|
|
1706
3079
|
const progress = during ?? { complete: true, reachable: true };
|
|
1707
3080
|
if (during === null)
|
|
1708
3081
|
moveTo(transferring(status));
|
|
1709
3082
|
try {
|
|
1710
3083
|
const local = await openLocal();
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
3084
|
+
await admittedRead(async () => {
|
|
3085
|
+
if (!live() || authorization === undefined)
|
|
3086
|
+
return;
|
|
3087
|
+
const withheld = await advance(room, local, authorization, live);
|
|
3088
|
+
if (withheld !== null)
|
|
3089
|
+
await withhold(withheld, room, local, progress);
|
|
3090
|
+
});
|
|
1714
3091
|
}
|
|
1715
3092
|
catch {
|
|
1716
3093
|
stale.add(locator);
|
|
1717
3094
|
}
|
|
1718
3095
|
finally {
|
|
1719
3096
|
if (during === null)
|
|
1720
|
-
await rest(progress);
|
|
3097
|
+
await rest(progress, live);
|
|
1721
3098
|
}
|
|
1722
3099
|
});
|
|
1723
3100
|
}
|
|
3101
|
+
function roomsCarried(replacing) {
|
|
3102
|
+
const replaced = new Set(replacing.map(roomLane));
|
|
3103
|
+
return Promise.all([...lanes]
|
|
3104
|
+
.filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
|
|
3105
|
+
.map(([, held]) => held.chain));
|
|
3106
|
+
}
|
|
3107
|
+
const receipts = {
|
|
3108
|
+
heads: async () => (await openLocal()).receivedHeads(),
|
|
3109
|
+
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
3110
|
+
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
3111
|
+
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
3112
|
+
};
|
|
1724
3113
|
function observeStream() {
|
|
1725
3114
|
const stream = options.adapter.roomStream;
|
|
1726
3115
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -1730,15 +3119,21 @@ export function composeRuntime(options) {
|
|
|
1730
3119
|
return;
|
|
1731
3120
|
if (event.kind === 'opened') {
|
|
1732
3121
|
socketOpen = true;
|
|
3122
|
+
generation += 1;
|
|
3123
|
+
reads.opened();
|
|
3124
|
+
conversations.reopened();
|
|
3125
|
+
recovery.resume();
|
|
1733
3126
|
void sync('opened');
|
|
1734
3127
|
}
|
|
1735
3128
|
else if (event.kind === 'closed')
|
|
1736
|
-
closedSocket(event.revoked);
|
|
3129
|
+
closedSocket(event.revoked, event.lost);
|
|
1737
3130
|
else if (event.kind === 'opening')
|
|
1738
3131
|
void applyOpening(event.expired);
|
|
3132
|
+
else if (event.kind === 'authority-changed')
|
|
3133
|
+
void rediscover();
|
|
1739
3134
|
else
|
|
1740
3135
|
void applyRoom(event.locator, event.roomVersion);
|
|
1741
|
-
});
|
|
3136
|
+
}, receipts);
|
|
1742
3137
|
}
|
|
1743
3138
|
function writesHeld() {
|
|
1744
3139
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -1753,6 +3148,7 @@ export function composeRuntime(options) {
|
|
|
1753
3148
|
haltedRefusal = false;
|
|
1754
3149
|
if (mode !== serviceMode) {
|
|
1755
3150
|
serviceMode = mode;
|
|
3151
|
+
queueReadiness();
|
|
1756
3152
|
journal.record(`service mode ${mode}`);
|
|
1757
3153
|
for (const listener of serviceModeListeners)
|
|
1758
3154
|
listener(mode);
|
|
@@ -1765,6 +3161,8 @@ export function composeRuntime(options) {
|
|
|
1765
3161
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
1766
3162
|
return;
|
|
1767
3163
|
withdrawConversations = stream.observe((event) => {
|
|
3164
|
+
if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
|
|
3165
|
+
queueReadiness();
|
|
1768
3166
|
if (conversations.receive(event)) {
|
|
1769
3167
|
void ready.then(scheduleCurrency);
|
|
1770
3168
|
return;
|
|
@@ -1776,46 +3174,47 @@ export function composeRuntime(options) {
|
|
|
1776
3174
|
});
|
|
1777
3175
|
}
|
|
1778
3176
|
function scheduleCurrency() {
|
|
1779
|
-
if (currencyQueued || closed || stopping)
|
|
3177
|
+
if (currencyQueued === generation || closed || stopping)
|
|
1780
3178
|
return;
|
|
1781
|
-
currencyQueued =
|
|
1782
|
-
void task(runCurrency);
|
|
3179
|
+
currencyQueued = generation;
|
|
3180
|
+
void task(CONVERSATION_LANE, runCurrency);
|
|
1783
3181
|
}
|
|
1784
|
-
async function runCurrency() {
|
|
1785
|
-
currencyQueued
|
|
1786
|
-
|
|
3182
|
+
async function runCurrency(live) {
|
|
3183
|
+
if (currencyQueued === generation)
|
|
3184
|
+
currencyQueued = null;
|
|
3185
|
+
if (!live())
|
|
1787
3186
|
return;
|
|
1788
3187
|
await conversations.reconcile();
|
|
1789
|
-
if (conversations.current()) {
|
|
3188
|
+
if (conversations.current() || !demand.conversations) {
|
|
1790
3189
|
show();
|
|
1791
3190
|
return;
|
|
1792
3191
|
}
|
|
1793
|
-
const
|
|
1794
|
-
|
|
1795
|
-
moveTo(transferring(resting));
|
|
3192
|
+
const progress = { complete: false, reachable: !status.providerUnreachable };
|
|
3193
|
+
moveTo(transferring(status));
|
|
1796
3194
|
try {
|
|
1797
3195
|
await conversations.catchUp(progress);
|
|
1798
3196
|
}
|
|
1799
3197
|
catch {
|
|
1800
3198
|
progress.reachable = false;
|
|
1801
3199
|
}
|
|
1802
|
-
|
|
1803
|
-
if (!closed) {
|
|
1804
|
-
moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
3200
|
+
await rest(progress, live);
|
|
1807
3201
|
}
|
|
1808
3202
|
function close() {
|
|
1809
3203
|
if (closed)
|
|
1810
3204
|
return;
|
|
1811
3205
|
closed = true;
|
|
3206
|
+
if (narrowing !== null)
|
|
3207
|
+
clearTimeout(narrowing);
|
|
3208
|
+
if (rechecking !== null)
|
|
3209
|
+
clearInterval(rechecking);
|
|
3210
|
+
readinessWatches.clear();
|
|
1812
3211
|
withdrawStream?.();
|
|
1813
3212
|
withdrawStream = undefined;
|
|
1814
3213
|
withdrawConversations?.();
|
|
1815
3214
|
withdrawConversations = undefined;
|
|
1816
3215
|
withdrawServiceMode?.();
|
|
1817
3216
|
conversations.close();
|
|
1818
|
-
|
|
3217
|
+
void recovery.retire();
|
|
1819
3218
|
store?.close();
|
|
1820
3219
|
void opening?.then((opened) => {
|
|
1821
3220
|
opened.close();
|
|
@@ -1825,6 +3224,7 @@ export function composeRuntime(options) {
|
|
|
1825
3224
|
storageLossListeners.clear();
|
|
1826
3225
|
statusListeners.clear();
|
|
1827
3226
|
serviceModeListeners.clear();
|
|
3227
|
+
memberListeners.clear();
|
|
1828
3228
|
readSide.close();
|
|
1829
3229
|
queueSubscriptions.clear();
|
|
1830
3230
|
roomDraftListeners.clear();
|
|
@@ -1832,9 +3232,8 @@ export function composeRuntime(options) {
|
|
|
1832
3232
|
}
|
|
1833
3233
|
async function stop() {
|
|
1834
3234
|
stopping = true;
|
|
1835
|
-
|
|
1836
|
-
await
|
|
1837
|
-
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
3235
|
+
await recovery.retire();
|
|
3236
|
+
await Promise.all(working);
|
|
1838
3237
|
await mutations;
|
|
1839
3238
|
close();
|
|
1840
3239
|
}
|
|
@@ -1853,17 +3252,111 @@ export function composeRuntime(options) {
|
|
|
1853
3252
|
if (options.adapter.roomStream === undefined)
|
|
1854
3253
|
void sync('unstreamed');
|
|
1855
3254
|
function planned(input, reach) {
|
|
1856
|
-
|
|
3255
|
+
const plan = planOf(input, roomFacts(reach), planReach(reach));
|
|
3256
|
+
const named = plan.paths !== null &&
|
|
3257
|
+
plan.rooms !== null &&
|
|
3258
|
+
plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
|
|
3259
|
+
return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
|
|
1857
3260
|
}
|
|
1858
3261
|
function watchPlan(plan, deliver) {
|
|
1859
|
-
|
|
3262
|
+
let seeded = false;
|
|
3263
|
+
let pending = true;
|
|
3264
|
+
const shown = () => {
|
|
3265
|
+
const seed = seeds.get(plan.key);
|
|
3266
|
+
if (seed === undefined || seeded || !pending)
|
|
3267
|
+
return;
|
|
3268
|
+
seeded = true;
|
|
3269
|
+
deliver(seededResult(seed));
|
|
3270
|
+
};
|
|
3271
|
+
const watchers = seedWatchers.get(plan.key) ?? new Set();
|
|
3272
|
+
seedWatchers.set(plan.key, watchers);
|
|
3273
|
+
watchers.add(shown);
|
|
3274
|
+
const stop = readSide.watch(plan, identityOf(plan), () => {
|
|
3275
|
+
const cover = covered(plan);
|
|
3276
|
+
awaitPreparation(cover);
|
|
3277
|
+
return seedBehind(plan, cover);
|
|
3278
|
+
}, (message) => {
|
|
3279
|
+
const answered = message;
|
|
3280
|
+
pending = whole(answered) && 'pending' in answered.result;
|
|
3281
|
+
if (pending && seeds.has(plan.key)) {
|
|
3282
|
+
shown();
|
|
3283
|
+
return;
|
|
3284
|
+
}
|
|
3285
|
+
seeded = false;
|
|
3286
|
+
deliver(message);
|
|
3287
|
+
});
|
|
3288
|
+
return () => {
|
|
3289
|
+
watchers.delete(shown);
|
|
3290
|
+
if (watchers.size === 0)
|
|
3291
|
+
seedWatchers.delete(plan.key);
|
|
3292
|
+
stop();
|
|
3293
|
+
};
|
|
1860
3294
|
}
|
|
1861
3295
|
async function searched(input, reach) {
|
|
1862
|
-
const
|
|
1863
|
-
const
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
3296
|
+
const asked = searchPlanOf(input);
|
|
3297
|
+
const plan = { ...asked, limit: capped(asked.limit) };
|
|
3298
|
+
const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
|
|
3299
|
+
for (;;) {
|
|
3300
|
+
const cover = covered(filter);
|
|
3301
|
+
awaitPreparation(cover);
|
|
3302
|
+
const pending = 'pending' in cover ? [...cover.pending] : [];
|
|
3303
|
+
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
3304
|
+
const held = covered({
|
|
3305
|
+
...filter,
|
|
3306
|
+
rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
|
|
3307
|
+
});
|
|
3308
|
+
const covering = 'pending' in held ? [] : held.coverage.rooms;
|
|
3309
|
+
const unindexed = covering
|
|
3310
|
+
.filter(({ room }) => !searchIndex.rooms().has(room))
|
|
3311
|
+
.map(({ room }) => room);
|
|
3312
|
+
for (const room of unindexed)
|
|
3313
|
+
pending.push({ room, awaits: 'index' });
|
|
3314
|
+
const listed = conversations.searchable();
|
|
3315
|
+
const named = reach !== null && !reach.conversations
|
|
3316
|
+
? []
|
|
3317
|
+
: (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
|
|
3318
|
+
const waiting = named.filter((one) => !searchIndex.conversations().has(one));
|
|
3319
|
+
for (const conversation of waiting)
|
|
3320
|
+
pending.push({ conversation, awaits: 'index' });
|
|
3321
|
+
if (unindexed.length > 0 || waiting.length > 0)
|
|
3322
|
+
index(unindexed, waiting);
|
|
3323
|
+
const complete = pending.length === 0 && !('pending' in cover);
|
|
3324
|
+
const answering = complete
|
|
3325
|
+
? filter
|
|
3326
|
+
: {
|
|
3327
|
+
...filter,
|
|
3328
|
+
rooms: covering
|
|
3329
|
+
.filter(({ room }) => searchIndex.rooms().has(room))
|
|
3330
|
+
.map(({ room }) => room),
|
|
3331
|
+
};
|
|
3332
|
+
const answered = covered(answering);
|
|
3333
|
+
if ('pending' in answered)
|
|
3334
|
+
continue;
|
|
3335
|
+
const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
|
|
3336
|
+
const seen = searchIndex.changes();
|
|
3337
|
+
await searchIndex.caughtUp();
|
|
3338
|
+
const selected = roomsAlone(answering) ? null : await rowsQuery(answering);
|
|
3339
|
+
if (selected !== null && 'pending' in selected)
|
|
3340
|
+
continue;
|
|
3341
|
+
if (searchIndex.changes() !== seen ||
|
|
3342
|
+
JSON.stringify(covered(answering)) !== JSON.stringify(answered))
|
|
3343
|
+
continue;
|
|
3344
|
+
const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms === null ? null : new Set(answering.rooms), plan.collection
|
|
3345
|
+
? new Map(answered.coverage.rooms.map(({ room }) => [
|
|
3346
|
+
room,
|
|
3347
|
+
replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
|
|
3348
|
+
]))
|
|
3349
|
+
: null);
|
|
3350
|
+
const coverage = {
|
|
3351
|
+
rooms: answered.coverage.rooms,
|
|
3352
|
+
conversations: searchedConversations,
|
|
3353
|
+
index: searchIndex.generation(),
|
|
3354
|
+
overlay: answered.coverage.overlay,
|
|
3355
|
+
};
|
|
3356
|
+
return complete
|
|
3357
|
+
? { ...hits, coverage }
|
|
3358
|
+
: { pending, documents: hits.documents, messages: hits.messages, coverage };
|
|
3359
|
+
}
|
|
1867
3360
|
}
|
|
1868
3361
|
const watch = (topic, input, deliver) => {
|
|
1869
3362
|
if (topic === 'room-drafts') {
|
|
@@ -1888,6 +3381,8 @@ export function composeRuntime(options) {
|
|
|
1888
3381
|
}
|
|
1889
3382
|
if (topic === 'conversations')
|
|
1890
3383
|
return conversations.watchList(deliver);
|
|
3384
|
+
if (topic === 'agents')
|
|
3385
|
+
return conversations.watchAgents(deliver);
|
|
1891
3386
|
if (topic === 'presentation') {
|
|
1892
3387
|
const listener = {
|
|
1893
3388
|
app: input[0],
|
|
@@ -1907,6 +3402,9 @@ export function composeRuntime(options) {
|
|
|
1907
3402
|
presentationListeners.delete(listener);
|
|
1908
3403
|
};
|
|
1909
3404
|
}
|
|
3405
|
+
if (topic === 'readiness') {
|
|
3406
|
+
return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
|
|
3407
|
+
}
|
|
1910
3408
|
if (topic === 'rooms') {
|
|
1911
3409
|
roomListeners.add(deliver);
|
|
1912
3410
|
deliver(listed());
|
|
@@ -1942,6 +3440,13 @@ export function composeRuntime(options) {
|
|
|
1942
3440
|
serviceModeListeners.delete(deliver);
|
|
1943
3441
|
};
|
|
1944
3442
|
}
|
|
3443
|
+
if (topic === 'member') {
|
|
3444
|
+
memberListeners.add(deliver);
|
|
3445
|
+
deliver(member);
|
|
3446
|
+
return () => {
|
|
3447
|
+
memberListeners.delete(deliver);
|
|
3448
|
+
};
|
|
3449
|
+
}
|
|
1945
3450
|
if (topic === 'queue') {
|
|
1946
3451
|
const queue = { locator: input[0], notify: deliver };
|
|
1947
3452
|
queueSubscriptions.add(queue);
|
|
@@ -1959,7 +3464,7 @@ export function composeRuntime(options) {
|
|
|
1959
3464
|
rooms() {
|
|
1960
3465
|
return listed();
|
|
1961
3466
|
},
|
|
1962
|
-
query: ((input) =>
|
|
3467
|
+
query: ((input) => rowsQuery(planned(input, null))),
|
|
1963
3468
|
search: (input) => searched(input, null),
|
|
1964
3469
|
types(locator) {
|
|
1965
3470
|
return {
|
|
@@ -1988,20 +3493,20 @@ export function composeRuntime(options) {
|
|
|
1988
3493
|
return exclusive(async () => {
|
|
1989
3494
|
const parts = readParts(input);
|
|
1990
3495
|
if (path === '' || normalize(path) !== path)
|
|
1991
|
-
throw
|
|
3496
|
+
throw runtimeError('room-draft-malformed');
|
|
1992
3497
|
const local = await openLocal();
|
|
1993
3498
|
const current = roomDrafts.get(room)?.get(path);
|
|
1994
3499
|
if ((current?.revision ?? null) !== revision) {
|
|
1995
|
-
return
|
|
1996
|
-
outcome: 'stale',
|
|
1997
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
1998
|
-
};
|
|
3500
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
1999
3501
|
}
|
|
3502
|
+
const file = parts.file === undefined
|
|
3503
|
+
? undefined
|
|
3504
|
+
: await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
|
|
2000
3505
|
const record = {
|
|
2001
3506
|
locator: room,
|
|
2002
3507
|
path,
|
|
2003
3508
|
revision: roomDraftRevision(),
|
|
2004
|
-
parts: held(current?.parts, parts),
|
|
3509
|
+
parts: held(current?.parts, parts, file),
|
|
2005
3510
|
returned: current?.returned ?? null,
|
|
2006
3511
|
};
|
|
2007
3512
|
await local.keepRoomDraft(record);
|
|
@@ -2015,10 +3520,7 @@ export function composeRuntime(options) {
|
|
|
2015
3520
|
const local = await openLocal();
|
|
2016
3521
|
const current = roomDrafts.get(room)?.get(path);
|
|
2017
3522
|
if (current === undefined || current.revision !== revision) {
|
|
2018
|
-
return
|
|
2019
|
-
outcome: 'stale',
|
|
2020
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
2021
|
-
};
|
|
3523
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2022
3524
|
}
|
|
2023
3525
|
await local.discardRoomDraft(room, path);
|
|
2024
3526
|
forgetRoomDraft(room, path);
|
|
@@ -2038,13 +3540,13 @@ export function composeRuntime(options) {
|
|
|
2038
3540
|
});
|
|
2039
3541
|
},
|
|
2040
3542
|
write(app, value) {
|
|
2041
|
-
return
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
3543
|
+
return keepPresentation(app, () => structuredClone(value));
|
|
3544
|
+
},
|
|
3545
|
+
edit(app, members) {
|
|
3546
|
+
return keepPresentation(app, (held) => Object.fromEntries([
|
|
3547
|
+
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(members, member)),
|
|
3548
|
+
...Object.entries(structuredClone(members)).filter(([, value]) => value !== null),
|
|
3549
|
+
]));
|
|
2048
3550
|
},
|
|
2049
3551
|
},
|
|
2050
3552
|
save(room, named, writtenBy) {
|
|
@@ -2053,8 +3555,13 @@ export function composeRuntime(options) {
|
|
|
2053
3555
|
history(locator, path, query) {
|
|
2054
3556
|
return readHistory(locator, path, query);
|
|
2055
3557
|
},
|
|
3558
|
+
binaryFile: {
|
|
3559
|
+
read: readBinaryPath,
|
|
3560
|
+
link: linkBinaryPath,
|
|
3561
|
+
readReference: readBinaryReference,
|
|
3562
|
+
},
|
|
2056
3563
|
call(name, input) {
|
|
2057
|
-
return
|
|
3564
|
+
return recoveredAction(name, input);
|
|
2058
3565
|
},
|
|
2059
3566
|
reached(locator, roomVersion, path) {
|
|
2060
3567
|
return path === undefined
|
|
@@ -2065,30 +3572,55 @@ export function composeRuntime(options) {
|
|
|
2065
3572
|
return journal.export();
|
|
2066
3573
|
},
|
|
2067
3574
|
persistence() {
|
|
2068
|
-
return
|
|
3575
|
+
return persistence === 'granted';
|
|
3576
|
+
},
|
|
3577
|
+
async retry() {
|
|
3578
|
+
journal.record('retry');
|
|
3579
|
+
unindexable.clear();
|
|
3580
|
+
queueReadiness();
|
|
3581
|
+
resume();
|
|
2069
3582
|
},
|
|
2070
3583
|
observe: observing(watch),
|
|
2071
3584
|
async reauthorize() {
|
|
2072
3585
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2073
3586
|
return;
|
|
2074
3587
|
journal.record('connection reauthorize');
|
|
2075
|
-
await
|
|
3588
|
+
await Promise.all([...lanes.values()]
|
|
3589
|
+
.filter((held) => held.generation === generation)
|
|
3590
|
+
.map((held) => held.chain));
|
|
2076
3591
|
await mutations;
|
|
2077
3592
|
authorization = undefined;
|
|
2078
3593
|
discoveryPaused = false;
|
|
2079
3594
|
if (options.adapter.roomStream === undefined)
|
|
2080
3595
|
await sync('unstreamed');
|
|
2081
3596
|
else
|
|
2082
|
-
await task(ensureAuthorization);
|
|
3597
|
+
await task(OPENING_LANE, ensureAuthorization);
|
|
2083
3598
|
},
|
|
2084
3599
|
};
|
|
2085
3600
|
const reachable = {
|
|
2086
3601
|
md,
|
|
2087
3602
|
watch,
|
|
3603
|
+
referrerScope: (referrer) => options.adapter.referrerScope(referrer),
|
|
2088
3604
|
plan: planned,
|
|
2089
|
-
query: (plan) =>
|
|
3605
|
+
query: (plan) => rowsQuery(plan),
|
|
2090
3606
|
watchPlan,
|
|
2091
3607
|
search: searched,
|
|
3608
|
+
watchReadiness,
|
|
3609
|
+
};
|
|
3610
|
+
return {
|
|
3611
|
+
md,
|
|
3612
|
+
seed,
|
|
3613
|
+
stop,
|
|
3614
|
+
lose: loseRooms,
|
|
3615
|
+
watch,
|
|
3616
|
+
acquire,
|
|
3617
|
+
persisted(answer) {
|
|
3618
|
+
if (answer === persistence)
|
|
3619
|
+
return;
|
|
3620
|
+
persistence = answer;
|
|
3621
|
+
journal.record(`persistence ${answer}`);
|
|
3622
|
+
queueReadiness();
|
|
3623
|
+
},
|
|
3624
|
+
within: (reach, port) => narrowed(reachable, reach, port),
|
|
2092
3625
|
};
|
|
2093
|
-
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2094
3626
|
}
|