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