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