@agentshouse/mdruntime 0.1.0-c09bf842921ff → 0.1.0-c0b4d0492dec3
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/README.md +1 -1
- package/dist/adapter.d.ts +15 -16
- package/dist/adapter.d.ts.map +1 -1
- package/dist/bootstrap.d.ts +8 -4
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +206 -76
- package/dist/change.d.ts +1 -0
- package/dist/change.d.ts.map +1 -1
- package/dist/chat-types.d.ts +3 -0
- package/dist/chat-types.d.ts.map +1 -0
- package/dist/chat-types.js +2 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1 -0
- package/dist/compile.d.ts +5 -0
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +186 -55
- package/dist/connection.d.ts +8 -2
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +1386 -1040
- package/dist/conversation-row.d.ts +33 -0
- package/dist/conversation-row.d.ts.map +1 -0
- package/dist/conversation-row.js +47 -0
- package/dist/conversation.d.ts +13 -5
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +354 -192
- package/dist/entry.d.ts.map +1 -1
- package/dist/entry.js +1 -0
- package/dist/figures.d.ts +48 -0
- package/dist/figures.d.ts.map +1 -0
- package/dist/figures.js +197 -0
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +3 -3
- package/dist/index.d.ts +9 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/job.d.ts +20 -1
- package/dist/job.d.ts.map +1 -1
- package/dist/job.js +26 -0
- package/dist/live.d.ts +4 -4
- package/dist/live.d.ts.map +1 -1
- package/dist/live.js +53 -15
- package/dist/memory.d.ts +1 -0
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +11 -3
- package/dist/messages.d.ts +25 -1
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +82 -3
- package/dist/model.d.ts +0 -2
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +0 -6
- package/dist/outcome.d.ts +1 -0
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +3 -0
- package/dist/overlay.d.ts +1 -0
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +11 -0
- package/dist/parts.d.ts +59 -0
- package/dist/parts.d.ts.map +1 -0
- package/dist/parts.js +118 -0
- package/dist/query.d.ts +10 -0
- package/dist/query.d.ts.map +1 -1
- package/dist/questions.d.ts +32 -0
- package/dist/questions.d.ts.map +1 -0
- package/dist/questions.js +118 -0
- package/dist/reach.d.ts +5 -5
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +156 -99
- package/dist/read.d.ts +7 -0
- package/dist/read.d.ts.map +1 -0
- package/dist/read.js +17 -0
- package/dist/reads.d.ts +35 -0
- package/dist/reads.d.ts.map +1 -0
- package/dist/reads.js +128 -0
- package/dist/recovery.d.ts.map +1 -1
- package/dist/recovery.js +9 -1
- package/dist/refresh.d.ts +1 -1
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +4 -0
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +23 -6
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +10 -7
- package/dist/source.d.ts +61 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +81 -0
- package/dist/status.d.ts +0 -2
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +0 -6
- package/dist/store.d.ts +85 -25
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +136 -98
- package/dist/stored.d.ts +55 -28
- package/dist/stored.d.ts.map +1 -1
- package/dist/stored.js +187 -74
- package/dist/tables.d.ts +99 -19
- package/dist/tables.d.ts.map +1 -1
- package/dist/tables.js +171 -34
- package/dist/tallies.d.ts +18 -0
- package/dist/tallies.d.ts.map +1 -0
- package/dist/tallies.js +129 -0
- package/dist/wire.d.ts +6 -0
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +3 -0
- package/package.json +2 -2
- package/dist/fence.d.ts +0 -6
- package/dist/fence.d.ts.map +0 -1
- package/dist/fence.js +0 -10
package/dist/connection.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { identityOf, parseFrontmatter, plain, queryKey, roomTarget, target, } from '@agentshouse/mdmodel';
|
|
2
2
|
import { attachedModel, builtInModel, roomTypesOf, schemaTypes } from './attached-model.js';
|
|
3
|
-
import {
|
|
3
|
+
import { modelTypes, NO_MODEL, } from './model.js';
|
|
4
4
|
import { observing, whole } from './observation.js';
|
|
5
5
|
import { planWithin, } from './query.js';
|
|
6
|
+
import { createFigures, ROOM_TOTALS } from './figures.js';
|
|
6
7
|
import { createLiveQueries } from './live.js';
|
|
7
8
|
import { seededResult } from './seed.js';
|
|
8
9
|
import { documentKey, roomOfKey } from './rows.js';
|
|
@@ -19,15 +20,15 @@ import { binaryRecord } from './binary.js';
|
|
|
19
20
|
import { attachedFile } from './file.js';
|
|
20
21
|
import { openedFiles } from './opened.js';
|
|
21
22
|
import { ABSENT_TARGET, absentBinary, attachAnswer, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, OFFLINE, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
|
|
23
|
+
import { readAnswers } from './reads.js';
|
|
22
24
|
import { createRecovery } from './recovery.js';
|
|
23
25
|
import { advancement } from './refresh.js';
|
|
24
26
|
import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
|
|
25
27
|
import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
|
|
26
28
|
import { binaryBase, changedParts, changesOf, gatheredParts, held, heldFile, joinedDraft, moveTarget, readParts, readSavedParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
27
|
-
import { same,
|
|
29
|
+
import { same, STARTING, } from './status.js';
|
|
28
30
|
import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
|
|
29
31
|
import { readinessOf, } from './readiness.js';
|
|
30
|
-
import { FencedMap } from './fence.js';
|
|
31
32
|
import { sameBinary } from './overlay.js';
|
|
32
33
|
import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
|
|
33
34
|
import { roomDraftRevision } from './identity.js';
|
|
@@ -68,6 +69,8 @@ const STAGE_AIM_MS = 50;
|
|
|
68
69
|
const STREAMS_HELD = 4;
|
|
69
70
|
const NOTHING_HELD = { bytes: 0 };
|
|
70
71
|
const QUEUED_TEXT = 1 << 23;
|
|
72
|
+
// A Room the Provider answers not ready is asked again after these waits, the last repeating, so a chunk build that catches up within seconds installs at once and a long one is asked every 15 seconds.
|
|
73
|
+
const NOT_READY_MS = [1_000, 2_000, 3_000, 5_000, 8_000, 11_000, 15_000];
|
|
71
74
|
// Bytes still arriving are reported at most this often, so readiness is not rebuilt for every chunk the network hands over while a slow link still moves the figure.
|
|
72
75
|
const ARRIVAL_REPORT_MS = 250;
|
|
73
76
|
// A Room that locks offline stays readable this long after the connection drops, so a laptop waking or changing network keeps working while a disconnected device locks soon after.
|
|
@@ -93,33 +96,25 @@ export function composeRuntime(options) {
|
|
|
93
96
|
const roomTypes = roomTypesOf(options.adapter.roomTypes, options.adapter.nameableRoomTypes);
|
|
94
97
|
let authorization = options.authorization;
|
|
95
98
|
let opening;
|
|
96
|
-
const
|
|
97
|
-
const isFenced = (locator) =>
|
|
99
|
+
const fencedAt = new Map();
|
|
100
|
+
const isFenced = (locator) => fencedAt.has(locator);
|
|
98
101
|
let fencings = 0;
|
|
99
102
|
const lastFenced = new Map();
|
|
100
|
-
const fencedSince = (locator, since) =>
|
|
103
|
+
const fencedSince = (locator, since) => fencedAt.has(locator) || (lastFenced.get(locator) ?? 0) > since;
|
|
101
104
|
let discoveries = 0;
|
|
102
|
-
const
|
|
103
|
-
const reopened = new Set();
|
|
105
|
+
const confirmations = new Map();
|
|
104
106
|
const usableWhenHeld = options.usableWhenHeld ?? [];
|
|
105
|
-
const
|
|
106
|
-
const pendings = new Map();
|
|
107
|
+
const build = options.build ?? '';
|
|
107
108
|
const landedAhead = new Map();
|
|
108
109
|
const seeds = new Map();
|
|
109
110
|
const seedWatchers = new Map();
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
let capabilities = [];
|
|
115
|
-
const rooms = new FencedMap(isFenced);
|
|
116
|
-
const counted = new Map();
|
|
111
|
+
const attachedFiles = new Map();
|
|
112
|
+
const declared = new Map();
|
|
113
|
+
const unheldModels = new Map();
|
|
114
|
+
const typesHeld = new Map();
|
|
117
115
|
const unreadable = new Set();
|
|
118
116
|
const builtIn = builtInModel(roomTypes);
|
|
119
117
|
const typedPaths = options.adapter.typedPaths ?? [];
|
|
120
|
-
const models = new FencedMap(isFenced);
|
|
121
|
-
const liveModels = new FencedMap(isFenced);
|
|
122
|
-
const invalidModels = new Set();
|
|
123
118
|
const discovered = new Map();
|
|
124
119
|
let inFlight = new AbortController();
|
|
125
120
|
const refreshed = options.adapter.refresh?.bind(options.adapter);
|
|
@@ -134,23 +129,74 @@ export function composeRuntime(options) {
|
|
|
134
129
|
};
|
|
135
130
|
let roomsListed = false;
|
|
136
131
|
const scopedRefusals = new Map();
|
|
137
|
-
function
|
|
138
|
-
|
|
132
|
+
function typed(locator, persisted) {
|
|
133
|
+
if (persisted.status === 'unavailable')
|
|
134
|
+
return persisted;
|
|
135
|
+
const schema = persisted.snapshot.schema;
|
|
136
|
+
const held = typesHeld.get(locator);
|
|
137
|
+
if (held !== undefined && held.schema === schema)
|
|
138
|
+
return { ...persisted, types: held.types };
|
|
139
|
+
const types = schemaTypes(schema, roomTypes);
|
|
140
|
+
typesHeld.set(locator, { schema, types });
|
|
141
|
+
return { ...persisted, types };
|
|
139
142
|
}
|
|
140
|
-
function
|
|
141
|
-
return
|
|
142
|
-
? { status: 'unavailable', reason: 'revoked' }
|
|
143
|
-
: modelOf(locator);
|
|
143
|
+
function recordOf(stored) {
|
|
144
|
+
return { ...stored, model: typed(stored.locator, stored.model) };
|
|
144
145
|
}
|
|
145
146
|
function composed(record) {
|
|
146
147
|
return compositionOf(record, builtIn, typedPaths);
|
|
147
148
|
}
|
|
149
|
+
async function storedRooms(locators) {
|
|
150
|
+
const held = await (await openLocal()).rooms(locators);
|
|
151
|
+
return new Map(held
|
|
152
|
+
.filter(({ record }) => !isFenced(record.locator))
|
|
153
|
+
.map(({ record, indexed, documents }) => [
|
|
154
|
+
record.locator,
|
|
155
|
+
{ record: recordOf(record), indexed, documents },
|
|
156
|
+
]));
|
|
157
|
+
}
|
|
158
|
+
async function recordAt(locator) {
|
|
159
|
+
return (await storedRooms([locator])).get(locator)?.record;
|
|
160
|
+
}
|
|
161
|
+
async function workOf(locator) {
|
|
162
|
+
if (isFenced(locator))
|
|
163
|
+
return { replica: undefined, queue: [], states: new Map() };
|
|
164
|
+
const { room, queue } = await (await openLocal()).room(locator);
|
|
165
|
+
const batches = queue.map(({ batch }) => batch);
|
|
166
|
+
const states = new Map(queue.map(({ batch, state }) => [batch.id, state]));
|
|
167
|
+
return {
|
|
168
|
+
replica: room === null
|
|
169
|
+
? undefined
|
|
170
|
+
: {
|
|
171
|
+
record: recordOf(room.record),
|
|
172
|
+
queue: batches,
|
|
173
|
+
states,
|
|
174
|
+
overlaid: room.overlaid,
|
|
175
|
+
count: room.documents,
|
|
176
|
+
confirmed: confirmations.get(locator) ?? 0,
|
|
177
|
+
indexed: room.indexed,
|
|
178
|
+
},
|
|
179
|
+
queue: batches,
|
|
180
|
+
states,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
async function replicaOf(locator) {
|
|
184
|
+
return (await workOf(locator)).replica;
|
|
185
|
+
}
|
|
186
|
+
function modelIn(locator, stored) {
|
|
187
|
+
if (refusal?.code === 'revoked-authorization')
|
|
188
|
+
return { status: 'unavailable', reason: 'revoked' };
|
|
189
|
+
return stored?.record.model ?? unheldModels.get(locator) ?? NO_MODEL;
|
|
190
|
+
}
|
|
148
191
|
function compositionFor(record) {
|
|
149
|
-
return composed({
|
|
192
|
+
return composed({
|
|
193
|
+
...record,
|
|
194
|
+
model: modelIn(record.locator, { record, indexed: true, documents: 0 }),
|
|
195
|
+
});
|
|
150
196
|
}
|
|
151
197
|
let refusal = null;
|
|
152
198
|
let storageLoss = null;
|
|
153
|
-
let status =
|
|
199
|
+
let status = { state: 'syncing', providerUnreachable: false, resting: false };
|
|
154
200
|
let shown = STARTING;
|
|
155
201
|
let discoveryPaused = false;
|
|
156
202
|
let closed = false;
|
|
@@ -164,6 +210,7 @@ export function composeRuntime(options) {
|
|
|
164
210
|
const repairing = new Set();
|
|
165
211
|
const calling = new Set();
|
|
166
212
|
const repairable = new Map();
|
|
213
|
+
const unready = new Map();
|
|
167
214
|
const answering = new Map();
|
|
168
215
|
let complete = false;
|
|
169
216
|
let recorded = false;
|
|
@@ -187,7 +234,17 @@ export function composeRuntime(options) {
|
|
|
187
234
|
const queueSubscriptions = new Set();
|
|
188
235
|
const roomDraftListeners = new Set();
|
|
189
236
|
const presentationListeners = new Set();
|
|
190
|
-
const liveQueries = createLiveQueries((plan, cover) => answeredPlan(plan, cover));
|
|
237
|
+
const liveQueries = createLiveQueries((plan, cover) => answeredPlan(plan, cover), () => covering());
|
|
238
|
+
const figures = createFigures({
|
|
239
|
+
cover: figuresCover,
|
|
240
|
+
rooms: async (rooms, since) => database.ask({
|
|
241
|
+
kind: 'replica.tallies',
|
|
242
|
+
totals: { ...(await planned(ROOM_TOTALS, null)), limit: null },
|
|
243
|
+
rooms,
|
|
244
|
+
since,
|
|
245
|
+
}),
|
|
246
|
+
conversations: (conversations, since) => database.ask({ kind: 'conversation.tallies', conversations, since }),
|
|
247
|
+
});
|
|
191
248
|
let withdrawConversations;
|
|
192
249
|
let currencyQueued = null;
|
|
193
250
|
let socketOpen = false;
|
|
@@ -200,15 +257,14 @@ export function composeRuntime(options) {
|
|
|
200
257
|
let haltedRefusal = false;
|
|
201
258
|
let openSignals = 0;
|
|
202
259
|
const serviceModeListeners = new Set();
|
|
203
|
-
let member = null;
|
|
204
260
|
const memberListeners = new Set();
|
|
205
261
|
let authorityChanges = 0;
|
|
206
262
|
const authorityChangeListeners = new Set();
|
|
207
263
|
const unpreparedDemand = new Set();
|
|
208
264
|
const unpreparable = new Set();
|
|
209
265
|
let preparing = false;
|
|
210
|
-
let targets = {};
|
|
211
266
|
let persistence = options.shell.persistence;
|
|
267
|
+
let heldWhenOpened = false;
|
|
212
268
|
let full = false;
|
|
213
269
|
let probing = null;
|
|
214
270
|
let testing = null;
|
|
@@ -227,7 +283,7 @@ export function composeRuntime(options) {
|
|
|
227
283
|
const readinessWatches = new Set();
|
|
228
284
|
let readinessQueued = false;
|
|
229
285
|
const catching = new Set();
|
|
230
|
-
const transfers = new
|
|
286
|
+
const transfers = new Map();
|
|
231
287
|
const installs = new Set();
|
|
232
288
|
const arrivals = new Map();
|
|
233
289
|
const streaming = new Set();
|
|
@@ -255,13 +311,31 @@ export function composeRuntime(options) {
|
|
|
255
311
|
call: (name, input) => invokeAction(name, input),
|
|
256
312
|
history: (request) => readConversationHistory(request),
|
|
257
313
|
durably,
|
|
314
|
+
local: () => openLocal(),
|
|
315
|
+
again: () => {
|
|
316
|
+
void ready.then(scheduleCurrency);
|
|
317
|
+
},
|
|
318
|
+
settled: () => {
|
|
319
|
+
show();
|
|
320
|
+
},
|
|
258
321
|
stream: () => options.adapter.conversations,
|
|
259
322
|
publish: publishChangeSet,
|
|
260
|
-
online:
|
|
323
|
+
online: providerReached,
|
|
261
324
|
full: () => full,
|
|
262
325
|
closed: () => closed,
|
|
263
326
|
unanswered: resendLater,
|
|
264
327
|
});
|
|
328
|
+
const lastAnswers = readAnswers({
|
|
329
|
+
database,
|
|
330
|
+
build,
|
|
331
|
+
unkept: options.adapter.unkeptReads ?? [],
|
|
332
|
+
recovery: recovery.scope(),
|
|
333
|
+
call: (name, input) => md.call(name, input),
|
|
334
|
+
online: providerReached,
|
|
335
|
+
closed: () => closed,
|
|
336
|
+
full: () => full,
|
|
337
|
+
filled,
|
|
338
|
+
});
|
|
265
339
|
const journal = createLog();
|
|
266
340
|
const keptFiles = openedFiles(database, isFenced);
|
|
267
341
|
let readyFromStore = () => { };
|
|
@@ -277,26 +351,13 @@ export function composeRuntime(options) {
|
|
|
277
351
|
notifyDiscovered();
|
|
278
352
|
readyFromStore();
|
|
279
353
|
}
|
|
280
|
-
function rememberRoomDraft(record) {
|
|
281
|
-
const room = roomDrafts.get(record.locator) ?? new Map();
|
|
282
|
-
room.set(record.path, record);
|
|
283
|
-
roomDrafts.set(record.locator, room);
|
|
284
|
-
}
|
|
285
|
-
function forgetRoomDraft(locator, path) {
|
|
286
|
-
const room = roomDrafts.get(locator);
|
|
287
|
-
if (room === undefined)
|
|
288
|
-
return;
|
|
289
|
-
room.delete(path);
|
|
290
|
-
if (room.size === 0)
|
|
291
|
-
roomDrafts.delete(locator);
|
|
292
|
-
}
|
|
293
354
|
async function visibleOf(locator, paths) {
|
|
294
|
-
const held =
|
|
355
|
+
const held = await recordAt(locator);
|
|
295
356
|
if (held === undefined)
|
|
296
357
|
return undefined;
|
|
297
358
|
const local = await openLocal();
|
|
298
|
-
const slice = await stepped(slicing(await local.entries(held.
|
|
299
|
-
return { ...slice, collection: collectionOf(slice.documents, composed(held
|
|
359
|
+
const slice = await stepped(slicing(await local.entries(held.key, 'visible', paths), paths));
|
|
360
|
+
return { ...slice, collection: collectionOf(slice.documents, composed(held)) };
|
|
300
361
|
}
|
|
301
362
|
function roomDraftOf(record, visible) {
|
|
302
363
|
return structuredClone({
|
|
@@ -320,25 +381,45 @@ export function composeRuntime(options) {
|
|
|
320
381
|
layers.set(locator, await visibleOf(locator, paths));
|
|
321
382
|
return records.map((record) => roomDraftOf(record, layers.get(record.locator)));
|
|
322
383
|
}
|
|
323
|
-
function
|
|
324
|
-
const
|
|
325
|
-
|
|
384
|
+
async function draftsOf(locator, paths) {
|
|
385
|
+
const held = await (await openLocal()).roomDrafts(locator, paths);
|
|
386
|
+
return held.filter((record) => !isFenced(record.locator));
|
|
387
|
+
}
|
|
388
|
+
async function draftOf(locator, path) {
|
|
389
|
+
return (await draftsOf(locator, [path]))[0];
|
|
390
|
+
}
|
|
391
|
+
function inOrder(records) {
|
|
392
|
+
return [...records].sort((left, right) => {
|
|
326
393
|
if (left.locator !== right.locator)
|
|
327
394
|
return left.locator < right.locator ? -1 : 1;
|
|
328
395
|
if (left.path !== right.path)
|
|
329
396
|
return left.path < right.path ? -1 : 1;
|
|
330
397
|
return 0;
|
|
331
398
|
});
|
|
332
|
-
return roomDraftsOf(records);
|
|
333
399
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
400
|
+
async function snapshotRoomDrafts() {
|
|
401
|
+
return roomDraftsOf(inOrder(await draftsOf()));
|
|
402
|
+
}
|
|
403
|
+
let published = Promise.resolve();
|
|
404
|
+
function publication(work) {
|
|
405
|
+
published = published.then(work).catch(unpublished);
|
|
406
|
+
return published;
|
|
407
|
+
}
|
|
408
|
+
function publishRoomDrafts(waitingOnly, touched = null) {
|
|
409
|
+
if (closed || roomDraftListeners.size === 0)
|
|
410
|
+
return Promise.resolve();
|
|
411
|
+
return publication(async () => {
|
|
412
|
+
if (closed || roomDraftListeners.size === 0)
|
|
413
|
+
return;
|
|
414
|
+
const records = await draftsOf();
|
|
415
|
+
if (closed)
|
|
416
|
+
return;
|
|
417
|
+
if (touched !== null &&
|
|
418
|
+
!records.some((record) => touched.some(({ locator, path }) => locator === record.locator && (path === null || path === record.path)))) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const value = await roomDraftsOf(inOrder(records));
|
|
422
|
+
if (closed)
|
|
342
423
|
return;
|
|
343
424
|
for (const listener of roomDraftListeners) {
|
|
344
425
|
if (waitingOnly && listener.ready)
|
|
@@ -348,8 +429,8 @@ export function composeRuntime(options) {
|
|
|
348
429
|
}
|
|
349
430
|
});
|
|
350
431
|
}
|
|
351
|
-
function presentationOf(app) {
|
|
352
|
-
const value =
|
|
432
|
+
async function presentationOf(app) {
|
|
433
|
+
const value = await (await openLocal()).presentation(app);
|
|
353
434
|
return value === undefined ? null : structuredClone(value);
|
|
354
435
|
}
|
|
355
436
|
async function writtenRoomDraft(room, path, revision, input, next) {
|
|
@@ -357,7 +438,7 @@ export function composeRuntime(options) {
|
|
|
357
438
|
if (path === '' || normalize(path) !== path)
|
|
358
439
|
throw runtimeError('room-draft-malformed', `room-draft-malformed at ${path}: a path inside the Room, such as notes/a.md`);
|
|
359
440
|
const local = await openLocal();
|
|
360
|
-
const current =
|
|
441
|
+
const current = await draftOf(room, path);
|
|
361
442
|
if ((current?.revision ?? null) !== revision) {
|
|
362
443
|
return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
|
|
363
444
|
}
|
|
@@ -376,55 +457,34 @@ export function composeRuntime(options) {
|
|
|
376
457
|
await local.keepRoomDraft(record, parts.file === undefined || file === undefined
|
|
377
458
|
? []
|
|
378
459
|
: [{ key: file.localId, blob: parts.file.blob }]);
|
|
379
|
-
|
|
380
|
-
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
460
|
+
await publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
381
461
|
return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
|
|
382
462
|
}
|
|
383
463
|
async function keptPresentation(app, next) {
|
|
384
464
|
const local = await openLocal();
|
|
385
|
-
const stored = next(
|
|
465
|
+
const stored = next(await local.presentation(app));
|
|
386
466
|
await local.present({ app, value: stored });
|
|
387
|
-
|
|
388
|
-
publishChangeSet(changeSet('', { presentation: app }));
|
|
467
|
+
await publishChangeSet(changeSet('', { presentation: app }));
|
|
389
468
|
}
|
|
390
469
|
function emitPresentation(app) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
(
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
function unsent() {
|
|
404
|
-
const changed = new Map();
|
|
405
|
-
for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
|
|
406
|
-
for (const { batch, state } of entriesOf(room)) {
|
|
407
|
-
if (state === 'local')
|
|
408
|
-
continue;
|
|
409
|
-
for (const { path } of batch.changes)
|
|
410
|
-
changed.set(`${room}\u0000${path}`, { room, path });
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
return [...changed]
|
|
414
|
-
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
415
|
-
.map(([, change]) => change);
|
|
470
|
+
const listening = () => [...presentationListeners].filter((listener) => listener.app === app && listener.ready);
|
|
471
|
+
if (closed || listening().length === 0)
|
|
472
|
+
return Promise.resolve();
|
|
473
|
+
return publication(async () => {
|
|
474
|
+
if (closed || listening().length === 0)
|
|
475
|
+
return;
|
|
476
|
+
const value = await presentationOf(app);
|
|
477
|
+
if (closed)
|
|
478
|
+
return;
|
|
479
|
+
for (const listener of listening())
|
|
480
|
+
listener.notify(value);
|
|
481
|
+
});
|
|
416
482
|
}
|
|
417
|
-
function
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
attentionRequired.push({
|
|
423
|
-
room: record.locator,
|
|
424
|
-
path: record.path,
|
|
425
|
-
returned: structuredClone(record.returned),
|
|
426
|
-
});
|
|
427
|
-
}
|
|
483
|
+
async function attended() {
|
|
484
|
+
const held = await (await openLocal()).status();
|
|
485
|
+
const attentionRequired = held.returned.flatMap((record) => record.returned === null || isFenced(record.locator)
|
|
486
|
+
? []
|
|
487
|
+
: [{ room: record.locator, path: record.path, returned: structuredClone(record.returned) }]);
|
|
428
488
|
attentionRequired.sort((left, right) => {
|
|
429
489
|
if (left.room !== right.room)
|
|
430
490
|
return left.room < right.room ? -1 : 1;
|
|
@@ -432,41 +492,130 @@ export function composeRuntime(options) {
|
|
|
432
492
|
return left.path < right.path ? -1 : 1;
|
|
433
493
|
return 0;
|
|
434
494
|
});
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
495
|
+
const changed = new Map();
|
|
496
|
+
if (!localSaves) {
|
|
497
|
+
for (const change of held.unsent) {
|
|
498
|
+
if (!isFenced(change.room))
|
|
499
|
+
changed.set(`${change.room}\u0000${change.path}`, change);
|
|
500
|
+
}
|
|
441
501
|
}
|
|
442
|
-
|
|
502
|
+
const unsent = [...changed]
|
|
503
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
504
|
+
.map(([, change]) => change);
|
|
505
|
+
return {
|
|
506
|
+
queued: !localSaves && held.queued.some((locator) => !isFenced(locator)),
|
|
507
|
+
lastCompleted: held.lastCompleted,
|
|
508
|
+
attentionRequired,
|
|
509
|
+
unsent,
|
|
510
|
+
reset: held.resets.reduce((earliest, { locator, reset }) => isFenced(locator) || (earliest !== null && earliest <= reset) ? earliest : reset, null),
|
|
511
|
+
};
|
|
443
512
|
}
|
|
444
|
-
function
|
|
445
|
-
|
|
513
|
+
async function queuedWork() {
|
|
514
|
+
const held = await (await openLocal()).queued();
|
|
515
|
+
return !localSaves && held.some((locator) => !isFenced(locator));
|
|
446
516
|
}
|
|
447
|
-
function
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
517
|
+
function declarationOf(locator, stored) {
|
|
518
|
+
const held = declared.get(locator);
|
|
519
|
+
const room = stored?.record ?? held?.room ?? discovered.get(locator);
|
|
520
|
+
return room === undefined ? undefined : { room, available: held?.available ?? true };
|
|
521
|
+
}
|
|
522
|
+
function listedLocators(held) {
|
|
523
|
+
return [
|
|
524
|
+
...declared.keys(),
|
|
525
|
+
...[...held.keys()].filter((locator) => !declared.has(locator)),
|
|
526
|
+
].filter((locator) => !isFenced(locator));
|
|
527
|
+
}
|
|
528
|
+
function selectionModel() {
|
|
529
|
+
return refusal?.code === 'revoked-authorization'
|
|
530
|
+
? { status: 'unavailable', reason: 'revoked' }
|
|
531
|
+
: builtIn;
|
|
532
|
+
}
|
|
533
|
+
function listedRoom(locator, stored) {
|
|
534
|
+
const declaration = declarationOf(locator, stored);
|
|
535
|
+
if (declaration === undefined)
|
|
536
|
+
return undefined;
|
|
537
|
+
const { room, available } = declaration;
|
|
538
|
+
const selection = room.typedSelection ?? null;
|
|
539
|
+
return {
|
|
540
|
+
name: room.name,
|
|
541
|
+
locator,
|
|
542
|
+
handle: room.handle,
|
|
543
|
+
operations: room.operations,
|
|
544
|
+
replica: room.replica,
|
|
545
|
+
model: modelTypes(modelIn(locator, stored)),
|
|
546
|
+
typedSelection: selection === null ? null : { roots: selection.roots, model: modelTypes(selectionModel()) },
|
|
547
|
+
providerOwnedPaths: room.providerOwnedPaths,
|
|
548
|
+
roomVersion: stored?.record.roomVersion ?? null,
|
|
549
|
+
synchronizationAvailable: available,
|
|
550
|
+
refusal: scopedRefusals.get(locator) ?? null,
|
|
551
|
+
documents: stored?.documents ?? 0,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
function listingOf(held) {
|
|
555
|
+
const listing = new Map();
|
|
556
|
+
for (const locator of listedLocators(held)) {
|
|
557
|
+
const room = listedRoom(locator, held.get(locator));
|
|
558
|
+
if (room !== undefined)
|
|
559
|
+
listing.set(locator, room);
|
|
560
|
+
}
|
|
561
|
+
return listing;
|
|
562
|
+
}
|
|
563
|
+
async function patchedListing(changed) {
|
|
564
|
+
if (roomsShown === null)
|
|
565
|
+
return null;
|
|
566
|
+
const held = await storedRooms([...changed]);
|
|
567
|
+
const listing = new Map(roomsShown);
|
|
568
|
+
for (const locator of changed) {
|
|
569
|
+
const room = isFenced(locator) ? undefined : listedRoom(locator, held.get(locator));
|
|
570
|
+
if (room === undefined || !listing.has(locator))
|
|
571
|
+
return null;
|
|
572
|
+
listing.set(locator, room);
|
|
573
|
+
}
|
|
574
|
+
return listing;
|
|
575
|
+
}
|
|
576
|
+
async function listingNow() {
|
|
577
|
+
return listingOf(await storedRooms());
|
|
461
578
|
}
|
|
462
|
-
function
|
|
463
|
-
return
|
|
579
|
+
function lockedIn(listing, locator) {
|
|
580
|
+
return listing.get(locator)?.replica.offline === 'locked' && !confirmed.has(locator);
|
|
464
581
|
}
|
|
465
|
-
function
|
|
466
|
-
return [...
|
|
582
|
+
function shownRooms(listing) {
|
|
583
|
+
return structuredClone([...listing.values()].map(({ documents, ...room }) => lockedIn(listing, room.locator)
|
|
584
|
+
? {
|
|
585
|
+
...room,
|
|
586
|
+
model: modelTypes(NO_MODEL),
|
|
587
|
+
typedSelection: null,
|
|
588
|
+
documents: 0,
|
|
589
|
+
locked: true,
|
|
590
|
+
}
|
|
591
|
+
: { ...room, documents, locked: false }));
|
|
467
592
|
}
|
|
468
|
-
function
|
|
469
|
-
const
|
|
593
|
+
async function offlineModes() {
|
|
594
|
+
const modes = new Map();
|
|
595
|
+
for (const [locator, { room }] of declared) {
|
|
596
|
+
const declaration = room ?? discovered.get(locator);
|
|
597
|
+
if (declaration !== undefined)
|
|
598
|
+
modes.set(locator, declaration.replica.offline);
|
|
599
|
+
}
|
|
600
|
+
for (const [locator, { offline }] of (await covering()).rooms)
|
|
601
|
+
modes.set(locator, offline);
|
|
602
|
+
return modes;
|
|
603
|
+
}
|
|
604
|
+
async function locked(locator) {
|
|
605
|
+
if (isFenced(locator) || confirmed.has(locator))
|
|
606
|
+
return false;
|
|
607
|
+
const held = declared.get(locator)?.room ?? discovered.get(locator);
|
|
608
|
+
const mode = (await (await openLocal()).offline(locator)) ?? held?.replica.offline;
|
|
609
|
+
return mode === 'locked';
|
|
610
|
+
}
|
|
611
|
+
async function lockedRooms() {
|
|
612
|
+
return [...(await offlineModes())]
|
|
613
|
+
.filter(([locator, mode]) => mode === 'locked' && !confirmed.has(locator) && !isFenced(locator))
|
|
614
|
+
.map(([locator]) => locator)
|
|
615
|
+
.sort();
|
|
616
|
+
}
|
|
617
|
+
async function relocked(before) {
|
|
618
|
+
const after = await lockedRooms();
|
|
470
619
|
if (JSON.stringify(after) === JSON.stringify(before))
|
|
471
620
|
return;
|
|
472
621
|
journal.record(`locked ${after.length}`);
|
|
@@ -489,9 +638,10 @@ export function composeRuntime(options) {
|
|
|
489
638
|
lockGrace = null;
|
|
490
639
|
if (socketOpen || closed)
|
|
491
640
|
return;
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
641
|
+
void lockedRooms().then((before) => {
|
|
642
|
+
confirmed.clear();
|
|
643
|
+
return relocked(before);
|
|
644
|
+
}, () => { });
|
|
495
645
|
}, LOCK_GRACE_MS);
|
|
496
646
|
}
|
|
497
647
|
function connectionLive() {
|
|
@@ -505,13 +655,14 @@ export function composeRuntime(options) {
|
|
|
505
655
|
lockListeners.delete(notify);
|
|
506
656
|
};
|
|
507
657
|
}
|
|
508
|
-
function roomFacts(reach = null) {
|
|
509
|
-
const held =
|
|
658
|
+
async function roomFacts(reach = null) {
|
|
659
|
+
const held = await storedRooms();
|
|
660
|
+
const rooms = [...listingOf(held).values()]
|
|
510
661
|
.filter((room) => holds(reach, room.locator))
|
|
511
662
|
.map((room) => {
|
|
512
663
|
const governed = governs(reach, room);
|
|
513
664
|
const attached = [
|
|
514
|
-
...(governed.collection ? [
|
|
665
|
+
...(governed.collection ? [modelIn(room.locator, held.get(room.locator))] : []),
|
|
515
666
|
...(governed.selection ? [builtIn] : []),
|
|
516
667
|
];
|
|
517
668
|
return {
|
|
@@ -520,28 +671,86 @@ export function composeRuntime(options) {
|
|
|
520
671
|
types: attached.flatMap((model) => model.status === 'unavailable' ? [] : Object.values(model.types)),
|
|
521
672
|
};
|
|
522
673
|
});
|
|
523
|
-
return { listed: roomsListed, rooms
|
|
674
|
+
return { listed: roomsListed, rooms };
|
|
524
675
|
}
|
|
525
|
-
function emitRooms() {
|
|
526
|
-
|
|
676
|
+
function emitRooms(changed) {
|
|
677
|
+
recoverReads();
|
|
527
678
|
queueReadiness();
|
|
528
|
-
|
|
529
|
-
for (const listener of roomListeners)
|
|
530
|
-
listener(value);
|
|
531
|
-
emitKnownRooms();
|
|
679
|
+
return changed === undefined ? publishRooms('all') : publishRooms('changed', changed);
|
|
532
680
|
}
|
|
533
681
|
function emitKnownRooms() {
|
|
534
682
|
conversations.learned();
|
|
535
683
|
if (unlistedRooms.size === 0)
|
|
536
684
|
return;
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
685
|
+
void publishRooms('known');
|
|
686
|
+
}
|
|
687
|
+
let roomsOwed = null;
|
|
688
|
+
let roomsShown = null;
|
|
689
|
+
function publishRooms(mode, touched = []) {
|
|
690
|
+
if (closed)
|
|
691
|
+
return Promise.resolve();
|
|
692
|
+
if (roomsOwed !== null) {
|
|
693
|
+
roomsOwed.all ||= mode === 'all';
|
|
694
|
+
for (const locator of touched) {
|
|
695
|
+
(mode === 'changed' ? roomsOwed.changed : roomsOwed.counted).add(locator);
|
|
696
|
+
}
|
|
697
|
+
return roomsOwed.shown;
|
|
544
698
|
}
|
|
699
|
+
const owing = {
|
|
700
|
+
all: mode === 'all',
|
|
701
|
+
changed: new Set(mode === 'changed' ? touched : []),
|
|
702
|
+
counted: new Set(mode === 'changed' ? [] : touched),
|
|
703
|
+
shown: Promise.resolve(),
|
|
704
|
+
};
|
|
705
|
+
roomsOwed = owing;
|
|
706
|
+
owing.shown = publication(async () => {
|
|
707
|
+
const owed = owing;
|
|
708
|
+
if (roomsOwed === owing)
|
|
709
|
+
roomsOwed = null;
|
|
710
|
+
if (closed || (roomListeners.size === 0 && unlistedRooms.size === 0))
|
|
711
|
+
return;
|
|
712
|
+
const patched = owed.all || owed.changed.size === 0 || unlistedRooms.size > 0
|
|
713
|
+
? null
|
|
714
|
+
: await patchedListing(new Set([...owed.changed, ...owed.counted]));
|
|
715
|
+
if (patched === null && owed.changed.size > 0)
|
|
716
|
+
owed.all = true;
|
|
717
|
+
if (patched === null && !owed.all && owed.counted.size > 0) {
|
|
718
|
+
const held = await covering().catch(() => null);
|
|
719
|
+
if (held === null)
|
|
720
|
+
return;
|
|
721
|
+
owed.all = [...owed.counted].some((locator) => held.rooms.has(locator) &&
|
|
722
|
+
roomsShown?.get(locator)?.documents !== held.rooms.get(locator).documents);
|
|
723
|
+
if (owed.all) {
|
|
724
|
+
recoverReads();
|
|
725
|
+
queueReadiness();
|
|
726
|
+
}
|
|
727
|
+
else if (unlistedRooms.size === 0)
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
const listing = patched ?? (await listingNow().catch(() => null));
|
|
731
|
+
if (listing === null || closed)
|
|
732
|
+
return;
|
|
733
|
+
const value = shownRooms(listing);
|
|
734
|
+
const joining = unlistedRooms.size > 0 &&
|
|
735
|
+
(roomsListed || listing.size > 0 || refusal !== null || status.providerUnreachable);
|
|
736
|
+
if (!owed.all && patched === null && !joining)
|
|
737
|
+
return;
|
|
738
|
+
const moved = owed.all ||
|
|
739
|
+
patched !== null ||
|
|
740
|
+
(roomsShown !== null && JSON.stringify(shownRooms(roomsShown)) !== JSON.stringify(value));
|
|
741
|
+
if (moved)
|
|
742
|
+
for (const listener of roomListeners)
|
|
743
|
+
listener(value);
|
|
744
|
+
if (joining) {
|
|
745
|
+
for (const listener of [...unlistedRooms]) {
|
|
746
|
+
unlistedRooms.delete(listener);
|
|
747
|
+
roomListeners.add(listener);
|
|
748
|
+
listener(value);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
roomsShown = roomListeners.size === 0 ? null : listing;
|
|
752
|
+
});
|
|
753
|
+
return owing.shown;
|
|
545
754
|
}
|
|
546
755
|
function emitRefusal() {
|
|
547
756
|
queueReadiness();
|
|
@@ -554,58 +763,98 @@ export function composeRuntime(options) {
|
|
|
554
763
|
for (const listener of storageLossListeners)
|
|
555
764
|
listener(storageLoss);
|
|
556
765
|
}
|
|
557
|
-
function emitMember() {
|
|
766
|
+
function emitMember(member) {
|
|
558
767
|
for (const listener of memberListeners)
|
|
559
768
|
listener(member);
|
|
560
769
|
}
|
|
561
770
|
function moveTo(next) {
|
|
562
771
|
if (closed)
|
|
563
|
-
return;
|
|
772
|
+
return Promise.resolve();
|
|
564
773
|
status = next;
|
|
565
|
-
show();
|
|
774
|
+
return show();
|
|
566
775
|
}
|
|
776
|
+
function transferring() {
|
|
777
|
+
return { ...status, state: 'syncing', resting: false };
|
|
778
|
+
}
|
|
779
|
+
let statusOwed = null;
|
|
780
|
+
let statusShown = Promise.resolve();
|
|
567
781
|
function show() {
|
|
568
782
|
if (closed)
|
|
569
|
-
return;
|
|
783
|
+
return Promise.resolve();
|
|
570
784
|
queueReadiness();
|
|
571
785
|
emitKnownRooms();
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
786
|
+
const moved = { moving: status, current: conversations.current() };
|
|
787
|
+
if (statusOwed !== null) {
|
|
788
|
+
statusOwed.push(moved);
|
|
789
|
+
return statusShown;
|
|
790
|
+
}
|
|
791
|
+
const owed = [moved];
|
|
792
|
+
statusOwed = owed;
|
|
793
|
+
statusShown = publication(async () => {
|
|
794
|
+
if (statusOwed === owed)
|
|
795
|
+
statusOwed = null;
|
|
796
|
+
const held = await attended();
|
|
797
|
+
if (closed)
|
|
798
|
+
return;
|
|
799
|
+
for (const { moving, current } of owed) {
|
|
800
|
+
const state = moving.resting && held.queued ? 'pending' : moving.state;
|
|
801
|
+
const next = {
|
|
802
|
+
state: state === 'synced' && !current ? 'syncing' : state,
|
|
803
|
+
lastCompleted: held.lastCompleted,
|
|
804
|
+
providerUnreachable: moving.providerUnreachable,
|
|
805
|
+
attentionRequired: held.attentionRequired,
|
|
806
|
+
unsent: held.unsent,
|
|
807
|
+
reset: held.reset,
|
|
808
|
+
};
|
|
809
|
+
if (same(shown, next))
|
|
810
|
+
continue;
|
|
811
|
+
shown = next;
|
|
812
|
+
for (const listener of statusListeners)
|
|
813
|
+
listener(shown);
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
return statusShown;
|
|
580
817
|
}
|
|
581
|
-
function entryOf(
|
|
818
|
+
function entryOf(states, batch) {
|
|
582
819
|
if (localSaves)
|
|
583
820
|
return Object.freeze({ batch, state: 'local' });
|
|
584
|
-
const state =
|
|
821
|
+
const state = states.get(batch.id);
|
|
585
822
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
586
823
|
}
|
|
587
|
-
function entriesOf(locator) {
|
|
588
|
-
|
|
824
|
+
async function entriesOf(locator) {
|
|
825
|
+
if (isFenced(locator))
|
|
826
|
+
return [];
|
|
827
|
+
const { queue, states } = await workOf(locator);
|
|
828
|
+
return queue.map((batch) => entryOf(states, batch));
|
|
589
829
|
}
|
|
590
830
|
function emitQueue(locator) {
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
831
|
+
const listening = () => [...queueSubscriptions].filter((subscription) => subscription.locator === locator);
|
|
832
|
+
if (closed || listening().length === 0)
|
|
833
|
+
return Promise.resolve();
|
|
834
|
+
return publication(async () => {
|
|
835
|
+
if (closed || listening().length === 0)
|
|
836
|
+
return;
|
|
837
|
+
const entries = await entriesOf(locator);
|
|
838
|
+
if (closed)
|
|
839
|
+
return;
|
|
840
|
+
for (const subscription of listening())
|
|
594
841
|
subscription.notify(entries);
|
|
595
|
-
}
|
|
842
|
+
});
|
|
596
843
|
}
|
|
597
844
|
function applyChangeSet(set) {
|
|
598
845
|
liveQueries.touched(set);
|
|
846
|
+
figures.touched(set);
|
|
847
|
+
}
|
|
848
|
+
function recoverReads() {
|
|
849
|
+
liveQueries.recover();
|
|
850
|
+
figures.recover();
|
|
599
851
|
}
|
|
600
852
|
function unpublished(error) {
|
|
601
853
|
journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
|
|
602
854
|
}
|
|
603
|
-
function indexed(locator) {
|
|
604
|
-
return replicas.get(locator)?.indexed === true;
|
|
605
|
-
}
|
|
606
855
|
async function prepareReplica(locator) {
|
|
607
856
|
for (;;) {
|
|
608
|
-
const held =
|
|
857
|
+
const held = (await storedRooms([locator])).get(locator);
|
|
609
858
|
if (held === undefined || closed || held.indexed)
|
|
610
859
|
return;
|
|
611
860
|
const key = held.record.key;
|
|
@@ -613,20 +862,19 @@ export function composeRuntime(options) {
|
|
|
613
862
|
const local = await openLocal();
|
|
614
863
|
return timed(deriveBudget, () => local.rederive(key, composed(held.record), deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR));
|
|
615
864
|
});
|
|
616
|
-
const now = replicas.get(locator);
|
|
617
|
-
if (now?.record.key !== key)
|
|
618
|
-
continue;
|
|
619
865
|
if (!done.done)
|
|
620
866
|
continue;
|
|
621
|
-
|
|
867
|
+
if ((await storedRooms([locator])).get(locator)?.record.key !== key)
|
|
868
|
+
continue;
|
|
622
869
|
publishChangeSet(changeSet(locator, { documents: null }));
|
|
623
|
-
|
|
870
|
+
recoverReads();
|
|
624
871
|
queueReadiness();
|
|
625
872
|
return;
|
|
626
873
|
}
|
|
627
874
|
}
|
|
628
|
-
function unprepared() {
|
|
629
|
-
|
|
875
|
+
async function unprepared() {
|
|
876
|
+
const held = (await covering()).rooms;
|
|
877
|
+
return [...unpreparedDemand, ...held.keys()].find((locator) => !(held.get(locator)?.indexed ?? false) && !unpreparable.has(locator));
|
|
630
878
|
}
|
|
631
879
|
function unpreparedBy(locator, error) {
|
|
632
880
|
unpreparable.add(locator);
|
|
@@ -641,7 +889,7 @@ export function composeRuntime(options) {
|
|
|
641
889
|
preparing = true;
|
|
642
890
|
void (async () => {
|
|
643
891
|
try {
|
|
644
|
-
for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
|
|
892
|
+
for (let waiting = await unprepared(); waiting !== undefined; waiting = await unprepared()) {
|
|
645
893
|
await yielded();
|
|
646
894
|
if (closed)
|
|
647
895
|
return;
|
|
@@ -649,6 +897,9 @@ export function composeRuntime(options) {
|
|
|
649
897
|
await prepareReplica(waiting).catch((error) => unpreparedBy(waiting, error));
|
|
650
898
|
}
|
|
651
899
|
}
|
|
900
|
+
catch {
|
|
901
|
+
journal.record('preparation failed');
|
|
902
|
+
}
|
|
652
903
|
finally {
|
|
653
904
|
preparing = false;
|
|
654
905
|
}
|
|
@@ -657,31 +908,48 @@ export function composeRuntime(options) {
|
|
|
657
908
|
function roomsOfReach(reach) {
|
|
658
909
|
return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
659
910
|
}
|
|
660
|
-
function
|
|
911
|
+
async function covering() {
|
|
912
|
+
const held = await (await openLocal()).covering();
|
|
913
|
+
return {
|
|
914
|
+
rooms: new Map([...held.rooms].filter(([locator]) => !isFenced(locator))),
|
|
915
|
+
queues: new Map([...held.queues].filter(([locator]) => !isFenced(locator))),
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
function everyRoom(held) {
|
|
919
|
+
return [...new Set([...declared.keys(), ...held.rooms.keys(), ...held.queues.keys()])];
|
|
920
|
+
}
|
|
921
|
+
function reachedRooms(plan, held) {
|
|
661
922
|
const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
|
|
662
|
-
const named = plan.rooms ??
|
|
663
|
-
(reached === null
|
|
664
|
-
? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
|
|
665
|
-
: [...reached]);
|
|
923
|
+
const named = plan.rooms ?? (reached === null ? everyRoom(held) : [...reached]);
|
|
666
924
|
return [...new Set(named)]
|
|
667
925
|
.filter((locator) => (reached === null || reached.has(locator)) && !plan.locked.includes(locator))
|
|
668
926
|
.sort();
|
|
669
927
|
}
|
|
670
928
|
function awaited(locator) {
|
|
671
929
|
return (!roomsListed ||
|
|
672
|
-
(
|
|
673
|
-
}
|
|
674
|
-
function covered(plan) {
|
|
675
|
-
|
|
676
|
-
|
|
930
|
+
(declared.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
|
|
931
|
+
}
|
|
932
|
+
function covered(plan, held) {
|
|
933
|
+
return coveredRooms(reachedRooms(plan, held), plan.rooms === null && plan.reach === null, held);
|
|
934
|
+
}
|
|
935
|
+
async function figuresCover() {
|
|
936
|
+
const held = await covering();
|
|
937
|
+
const locked = await lockedRooms();
|
|
938
|
+
const cover = coveredRooms(everyRoom(held)
|
|
939
|
+
.filter((locator) => !locked.includes(locator))
|
|
940
|
+
.sort(), true, held);
|
|
941
|
+
awaitPreparation(cover);
|
|
942
|
+
return 'pending' in cover ? cover : { rooms: cover.rooms };
|
|
943
|
+
}
|
|
944
|
+
function coveredRooms(locators, whole, held) {
|
|
945
|
+
if (locators.length === 0 && whole && !roomsListed)
|
|
677
946
|
return { pending: [] };
|
|
678
|
-
}
|
|
679
947
|
const pending = [];
|
|
680
948
|
const coverage = [];
|
|
681
949
|
const overlay = [];
|
|
682
950
|
const read = [];
|
|
683
951
|
for (const locator of locators) {
|
|
684
|
-
const replica =
|
|
952
|
+
const replica = held.rooms.get(locator);
|
|
685
953
|
if (replica === undefined) {
|
|
686
954
|
if (awaited(locator))
|
|
687
955
|
pending.push({ room: locator, awaits: 'acquisition' });
|
|
@@ -693,12 +961,12 @@ export function composeRuntime(options) {
|
|
|
693
961
|
}
|
|
694
962
|
coverage.push({
|
|
695
963
|
room: locator,
|
|
696
|
-
generation: replica.
|
|
964
|
+
generation: replica.transfer,
|
|
697
965
|
current: !(catching.has(locator) || transfers.has(locator)),
|
|
698
966
|
});
|
|
699
|
-
read.push({ locator, key: replica.
|
|
700
|
-
for (const
|
|
701
|
-
overlay.push(
|
|
967
|
+
read.push({ locator, key: replica.key });
|
|
968
|
+
for (const id of held.queues.get(locator) ?? [])
|
|
969
|
+
overlay.push(id);
|
|
702
970
|
}
|
|
703
971
|
if (pending.length > 0)
|
|
704
972
|
return { pending };
|
|
@@ -711,46 +979,54 @@ export function composeRuntime(options) {
|
|
|
711
979
|
if (waiting.length > 0)
|
|
712
980
|
prepare(waiting.map(({ room }) => room));
|
|
713
981
|
}
|
|
714
|
-
function requiredRooms(given) {
|
|
982
|
+
function requiredRooms(given, held) {
|
|
715
983
|
const reach = planReach(given);
|
|
984
|
+
const listed = new Set(listedLocators(held));
|
|
716
985
|
if (reach === null)
|
|
717
|
-
return { listed: roomsListed, rooms: [...
|
|
986
|
+
return { listed: roomsListed, rooms: [...listed].sort() };
|
|
718
987
|
const named = [...roomsOfReach(reach)].sort();
|
|
719
988
|
if (roomsListed)
|
|
720
|
-
return { listed: true, rooms: named.filter((room) =>
|
|
721
|
-
return { listed: named.every((room) =>
|
|
989
|
+
return { listed: true, rooms: named.filter((room) => listed.has(room)) };
|
|
990
|
+
return { listed: named.every((room) => held.has(room)), rooms: named };
|
|
722
991
|
}
|
|
723
|
-
function roomState(locator) {
|
|
992
|
+
function roomState(locator, held) {
|
|
724
993
|
const listedRoom = discovered.get(locator);
|
|
725
994
|
if (roomsListed &&
|
|
726
995
|
(listedRoom === undefined ||
|
|
727
996
|
listedRoom.replica.admission !== 'admitted' ||
|
|
728
997
|
unreadable.has(locator)))
|
|
729
998
|
return 'unadmitted';
|
|
730
|
-
|
|
999
|
+
const stored = held.get(locator);
|
|
1000
|
+
if (stored === undefined)
|
|
731
1001
|
return 'acquiring';
|
|
732
|
-
if (!indexed
|
|
1002
|
+
if (!stored.indexed)
|
|
733
1003
|
return 'preparing';
|
|
734
1004
|
return 'complete';
|
|
735
1005
|
}
|
|
736
1006
|
function conversationCoverage(reach) {
|
|
737
|
-
const
|
|
738
|
-
if (!
|
|
1007
|
+
const declaredConversations = reach === null || reach.conversations;
|
|
1008
|
+
if (!declaredConversations || options.adapter.conversations === undefined)
|
|
739
1009
|
return 'undeclared';
|
|
740
1010
|
if (!conversations.known() || !conversations.current())
|
|
741
1011
|
return 'acquiring';
|
|
742
1012
|
return 'complete';
|
|
743
1013
|
}
|
|
744
|
-
function connectionOf() {
|
|
1014
|
+
function connectionOf(lastCompleted) {
|
|
745
1015
|
if (socketOpen)
|
|
746
1016
|
return 'connected';
|
|
747
1017
|
if (status.providerUnreachable)
|
|
748
1018
|
return 'unreachable';
|
|
749
|
-
return options.adapter.roomStream === undefined &&
|
|
1019
|
+
return options.adapter.roomStream === undefined && lastCompleted !== null
|
|
750
1020
|
? 'connected'
|
|
751
1021
|
: 'connecting';
|
|
752
1022
|
}
|
|
753
|
-
function
|
|
1023
|
+
async function connectionNow() {
|
|
1024
|
+
if (socketOpen || status.providerUnreachable || options.adapter.roomStream !== undefined) {
|
|
1025
|
+
return connectionOf(null);
|
|
1026
|
+
}
|
|
1027
|
+
return connectionOf((await (await openLocal()).facts()).lastCompleted);
|
|
1028
|
+
}
|
|
1029
|
+
function bytesOf(room, underway, held) {
|
|
754
1030
|
const transfer = transfers.get(room);
|
|
755
1031
|
if (transfer !== undefined) {
|
|
756
1032
|
const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
|
|
@@ -759,10 +1035,10 @@ export function composeRuntime(options) {
|
|
|
759
1035
|
total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
|
|
760
1036
|
};
|
|
761
1037
|
}
|
|
762
|
-
const
|
|
763
|
-
return { done:
|
|
1038
|
+
const kept = underway ? NOTHING_HELD.bytes : (held.get(room)?.bytes ?? NOTHING_HELD.bytes);
|
|
1039
|
+
return { done: kept, total: arrivals.get(room) ?? kept };
|
|
764
1040
|
}
|
|
765
|
-
function installationOf(coverage) {
|
|
1041
|
+
function installationOf(coverage, held) {
|
|
766
1042
|
const rooms = { done: 0, total: 0 };
|
|
767
1043
|
const bytes = { done: 0, total: 0 };
|
|
768
1044
|
let installing = 0;
|
|
@@ -778,7 +1054,7 @@ export function composeRuntime(options) {
|
|
|
778
1054
|
installing += 1;
|
|
779
1055
|
if (underway && (streaming.has(room) || !arrivals.has(room)))
|
|
780
1056
|
downloading = true;
|
|
781
|
-
const now = bytesOf(room, underway);
|
|
1057
|
+
const now = bytesOf(room, underway, held);
|
|
782
1058
|
const earlier = underway ? startedOver.get(room) : undefined;
|
|
783
1059
|
bytes.done += Math.max(now.done, earlier?.done ?? 0);
|
|
784
1060
|
bytes.total += Math.max(now.total, earlier?.total ?? 0);
|
|
@@ -792,9 +1068,14 @@ export function composeRuntime(options) {
|
|
|
792
1068
|
: 'replaying';
|
|
793
1069
|
return { step, rooms, bytes };
|
|
794
1070
|
}
|
|
795
|
-
function
|
|
796
|
-
const
|
|
797
|
-
const
|
|
1071
|
+
async function readinessHeld() {
|
|
1072
|
+
const local = await openLocal();
|
|
1073
|
+
const [held, facts] = await Promise.all([covering(), local.facts()]);
|
|
1074
|
+
return { held: held.rooms, facts };
|
|
1075
|
+
}
|
|
1076
|
+
function readinessFor(port, reach, { held, facts }) {
|
|
1077
|
+
const required = requiredRooms(reach, held);
|
|
1078
|
+
const coverage = required.rooms.map((room) => ({ room, state: roomState(room, held) }));
|
|
798
1079
|
const preparing = coverage
|
|
799
1080
|
.filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
|
|
800
1081
|
.map(({ room }) => room);
|
|
@@ -808,36 +1089,65 @@ export function composeRuntime(options) {
|
|
|
808
1089
|
conversations: conversationCoverage(reach),
|
|
809
1090
|
},
|
|
810
1091
|
persistence,
|
|
811
|
-
connection: connectionOf(),
|
|
1092
|
+
connection: connectionOf(facts.lastCompleted),
|
|
812
1093
|
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
813
1094
|
space: full ? 'full' : null,
|
|
814
|
-
installation: installationOf(coverage),
|
|
1095
|
+
installation: installationOf(coverage, held),
|
|
815
1096
|
lost: storageLoss !== null,
|
|
816
|
-
established: targets[port.target]?.established === true ||
|
|
817
|
-
(usableWhenHeld.includes(port.target)
|
|
1097
|
+
established: facts.targets[port.target]?.established === true ||
|
|
1098
|
+
(heldWhenOpened && usableWhenHeld.includes(port.target)),
|
|
818
1099
|
});
|
|
819
1100
|
}
|
|
1101
|
+
const establishing = new Set();
|
|
820
1102
|
function establish(name) {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
void durably((local) =>
|
|
1103
|
+
if (establishing.has(name))
|
|
1104
|
+
return;
|
|
1105
|
+
establishing.add(name);
|
|
1106
|
+
void durably(async (local) => {
|
|
1107
|
+
const { targets } = await local.facts();
|
|
1108
|
+
await local.fact('targets', { ...targets, [name]: { established: true } });
|
|
1109
|
+
})
|
|
1110
|
+
.then(() => {
|
|
1111
|
+
queueReadiness();
|
|
1112
|
+
void probeEachMinute();
|
|
1113
|
+
}, () => {
|
|
825
1114
|
journal.record(`target record failed ${name}`);
|
|
826
|
-
})
|
|
1115
|
+
})
|
|
1116
|
+
.finally(() => establishing.delete(name));
|
|
827
1117
|
}
|
|
828
|
-
function deliverReadiness(watch) {
|
|
1118
|
+
function deliverReadiness(watch, held) {
|
|
829
1119
|
if (!readinessWatches.has(watch))
|
|
830
1120
|
return;
|
|
831
|
-
const readiness = readinessFor(watch.port, watch.reach);
|
|
832
|
-
if (readiness.ready && targets[watch.port.target]?.established !== true) {
|
|
1121
|
+
const readiness = readinessFor(watch.port, watch.reach, held);
|
|
1122
|
+
if (readiness.ready && held.facts.targets[watch.port.target]?.established !== true) {
|
|
833
1123
|
establish(watch.port.target);
|
|
834
1124
|
}
|
|
835
|
-
const
|
|
836
|
-
if (
|
|
1125
|
+
const shownReadiness = JSON.stringify(readiness);
|
|
1126
|
+
if (shownReadiness === watch.last)
|
|
837
1127
|
return;
|
|
838
|
-
watch.last =
|
|
1128
|
+
watch.last = shownReadiness;
|
|
839
1129
|
watch.deliver(structuredClone(readiness));
|
|
840
1130
|
}
|
|
1131
|
+
let readinessShown = Promise.resolve();
|
|
1132
|
+
const readinessOwed = new Set();
|
|
1133
|
+
function deliverAll(watches) {
|
|
1134
|
+
const queued = readinessOwed.size > 0;
|
|
1135
|
+
for (const watch of watches)
|
|
1136
|
+
readinessOwed.add(watch);
|
|
1137
|
+
if (queued)
|
|
1138
|
+
return;
|
|
1139
|
+
readinessShown = readinessShown
|
|
1140
|
+
.then(async () => {
|
|
1141
|
+
const owed = [...readinessOwed];
|
|
1142
|
+
readinessOwed.clear();
|
|
1143
|
+
const held = await readinessHeld();
|
|
1144
|
+
if (closed)
|
|
1145
|
+
return;
|
|
1146
|
+
for (const watch of owed)
|
|
1147
|
+
deliverReadiness(watch, held);
|
|
1148
|
+
})
|
|
1149
|
+
.catch(unpublished);
|
|
1150
|
+
}
|
|
841
1151
|
function queueReadiness() {
|
|
842
1152
|
if (readinessQueued || closed || readinessWatches.size === 0)
|
|
843
1153
|
return;
|
|
@@ -846,32 +1156,40 @@ export function composeRuntime(options) {
|
|
|
846
1156
|
readinessQueued = false;
|
|
847
1157
|
if (closed)
|
|
848
1158
|
return;
|
|
849
|
-
|
|
850
|
-
deliverReadiness(watch);
|
|
1159
|
+
deliverAll([...readinessWatches]);
|
|
851
1160
|
});
|
|
852
1161
|
}
|
|
853
1162
|
function watchReadiness(port, reach, deliver) {
|
|
854
1163
|
const watch = { port, reach, deliver, last: null };
|
|
855
1164
|
readinessWatches.add(watch);
|
|
856
|
-
void openLocal().then(() =>
|
|
1165
|
+
void openLocal().then(() => deliverAll([watch]), () => deliverAll([watch]));
|
|
857
1166
|
return () => {
|
|
858
1167
|
readinessWatches.delete(watch);
|
|
859
1168
|
};
|
|
860
1169
|
}
|
|
1170
|
+
function providerReached() {
|
|
1171
|
+
return (socketOpen || !status.providerUnreachable) && refusal === null;
|
|
1172
|
+
}
|
|
861
1173
|
function filled() {
|
|
862
1174
|
if (full || closed)
|
|
863
1175
|
return;
|
|
864
1176
|
full = true;
|
|
865
1177
|
journal.record('space full');
|
|
866
1178
|
queueReadiness();
|
|
867
|
-
probeEachMinute();
|
|
1179
|
+
void probeEachMinute();
|
|
868
1180
|
}
|
|
869
|
-
function installedTarget() {
|
|
870
|
-
|
|
871
|
-
|
|
1181
|
+
async function installedTarget() {
|
|
1182
|
+
if (heldWhenOpened && usableWhenHeld.length > 0)
|
|
1183
|
+
return true;
|
|
1184
|
+
const { targets } = await (await openLocal()).facts();
|
|
1185
|
+
return Object.values(targets).some((target) => target.established);
|
|
872
1186
|
}
|
|
873
|
-
function probeEachMinute() {
|
|
874
|
-
if (!full || probing !== null || closed
|
|
1187
|
+
async function probeEachMinute() {
|
|
1188
|
+
if (!full || probing !== null || closed)
|
|
1189
|
+
return;
|
|
1190
|
+
if (!(await installedTarget().catch(() => false)))
|
|
1191
|
+
return;
|
|
1192
|
+
if (!full || probing !== null || closed)
|
|
875
1193
|
return;
|
|
876
1194
|
// A device whose storage filled after its install tries a write each minute, so space freed elsewhere ends its read-only mode without a Retry.
|
|
877
1195
|
probing = setInterval(() => {
|
|
@@ -906,39 +1224,41 @@ export function composeRuntime(options) {
|
|
|
906
1224
|
else
|
|
907
1225
|
void resynchronize();
|
|
908
1226
|
}
|
|
909
|
-
function seedRoomHeld(room, version) {
|
|
910
|
-
|
|
1227
|
+
function seedRoomHeld(room, version, held) {
|
|
1228
|
+
const stored = held.rooms.get(room);
|
|
1229
|
+
return (stored !== undefined &&
|
|
1230
|
+
stored.indexed &&
|
|
1231
|
+
options.adapter.reached(room, stored.roomVersion, version));
|
|
911
1232
|
}
|
|
912
|
-
function seedHeld(at) {
|
|
913
|
-
return Object.entries(at).every(([room, version]) => (roomsListed && !
|
|
1233
|
+
function seedHeld(at, held) {
|
|
1234
|
+
return Object.entries(at).every(([room, version]) => (roomsListed && !declared.has(room) && !held.rooms.has(room)) ||
|
|
1235
|
+
seedRoomHeld(room, version, held));
|
|
914
1236
|
}
|
|
915
|
-
function seedApplies(plan) {
|
|
1237
|
+
function seedApplies(plan, held) {
|
|
916
1238
|
const seed = seeds.get(plan.key);
|
|
917
1239
|
if (seed === undefined)
|
|
918
1240
|
return null;
|
|
919
|
-
if (!seedHeld(seed.at))
|
|
1241
|
+
if (!seedHeld(seed.at, held))
|
|
920
1242
|
return seed;
|
|
921
1243
|
seeds.delete(plan.key);
|
|
922
1244
|
return null;
|
|
923
1245
|
}
|
|
924
|
-
function seedBehind(plan, cover) {
|
|
925
|
-
const seed = seedApplies(plan);
|
|
1246
|
+
function seedBehind(plan, cover, held) {
|
|
1247
|
+
const seed = seedApplies(plan, held);
|
|
926
1248
|
if (seed === null || 'pending' in cover)
|
|
927
1249
|
return cover;
|
|
928
1250
|
return {
|
|
929
1251
|
pending: Object.entries(seed.at)
|
|
930
|
-
.filter(([room, version]) => !seedRoomHeld(room, version))
|
|
1252
|
+
.filter(([room, version]) => !seedRoomHeld(room, version, held))
|
|
931
1253
|
.map(([room]) => ({ room, awaits: 'acquisition' })),
|
|
932
1254
|
};
|
|
933
1255
|
}
|
|
934
1256
|
function seed(entry) {
|
|
935
|
-
if (seedHeld(entry.at))
|
|
936
|
-
return;
|
|
937
1257
|
for (const { query, rows } of entry.queries) {
|
|
938
1258
|
const key = queryKey(query);
|
|
939
1259
|
seeds.set(key, { rows, at: entry.at });
|
|
940
|
-
for (const
|
|
941
|
-
|
|
1260
|
+
for (const shownSeed of seedWatchers.get(key) ?? [])
|
|
1261
|
+
shownSeed();
|
|
942
1262
|
}
|
|
943
1263
|
}
|
|
944
1264
|
function sameCover(left, right) {
|
|
@@ -946,7 +1266,7 @@ export function composeRuntime(options) {
|
|
|
946
1266
|
}
|
|
947
1267
|
async function coveredRows(plan, cover) {
|
|
948
1268
|
for (;;) {
|
|
949
|
-
const before = cover();
|
|
1269
|
+
const before = await cover();
|
|
950
1270
|
if ('pending' in before)
|
|
951
1271
|
return { pending: before };
|
|
952
1272
|
const local = await openLocal();
|
|
@@ -954,7 +1274,7 @@ export function composeRuntime(options) {
|
|
|
954
1274
|
const changes = local.changes(keys);
|
|
955
1275
|
const current = () => local.changes(keys) === changes;
|
|
956
1276
|
const rows = await windowRows(local, plan, before.rooms, current);
|
|
957
|
-
const after = cover();
|
|
1277
|
+
const after = await cover();
|
|
958
1278
|
if (rows !== null && sameCover(before, after))
|
|
959
1279
|
return { rows, cover: before, current };
|
|
960
1280
|
}
|
|
@@ -1010,23 +1330,25 @@ export function composeRuntime(options) {
|
|
|
1010
1330
|
const held = answered.cover;
|
|
1011
1331
|
return { ...keyedOf(answered.rows, held.coverage), current: answered.current };
|
|
1012
1332
|
}
|
|
1333
|
+
async function coverNow(plan) {
|
|
1334
|
+
const cover = covered(plan, await covering());
|
|
1335
|
+
awaitPreparation(cover);
|
|
1336
|
+
return cover;
|
|
1337
|
+
}
|
|
1013
1338
|
async function rowsQuery(plan) {
|
|
1014
|
-
const
|
|
1339
|
+
const held = await covering();
|
|
1340
|
+
const before = covered(plan, held);
|
|
1015
1341
|
awaitPreparation(before);
|
|
1016
|
-
const seeded = seedApplies(plan);
|
|
1342
|
+
const seeded = seedApplies(plan, held);
|
|
1017
1343
|
if (seeded !== null)
|
|
1018
1344
|
return seededResult(seeded).result;
|
|
1019
1345
|
if ('pending' in before)
|
|
1020
1346
|
return before;
|
|
1021
|
-
const answered = await coveredRows(plan, () =>
|
|
1022
|
-
const cover = covered(plan);
|
|
1023
|
-
awaitPreparation(cover);
|
|
1024
|
-
return cover;
|
|
1025
|
-
});
|
|
1347
|
+
const answered = await coveredRows(plan, () => coverNow(plan));
|
|
1026
1348
|
if ('pending' in answered)
|
|
1027
1349
|
return answered.pending;
|
|
1028
|
-
const
|
|
1029
|
-
return { ...answered.rows, coverage:
|
|
1350
|
+
const shownCover = answered.cover;
|
|
1351
|
+
return { ...answered.rows, coverage: shownCover.coverage };
|
|
1030
1352
|
}
|
|
1031
1353
|
function publishChangeSet(set) {
|
|
1032
1354
|
applyChangeSet(set);
|
|
@@ -1034,21 +1356,24 @@ export function composeRuntime(options) {
|
|
|
1034
1356
|
if (path !== null)
|
|
1035
1357
|
attachedFiles.get(locator)?.delete(path);
|
|
1036
1358
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
if (set.
|
|
1043
|
-
set.documents.
|
|
1044
|
-
const held = roomDrafts.get(change.locator);
|
|
1045
|
-
return held !== undefined && (change.path === null || held.has(change.path));
|
|
1046
|
-
})) {
|
|
1047
|
-
publishRoomDrafts(false);
|
|
1359
|
+
const shownNow = [];
|
|
1360
|
+
if (set.queues.length > 0 || set.drafts.length > 0)
|
|
1361
|
+
shownNow.push(show());
|
|
1362
|
+
if (set.rooms.length > 0)
|
|
1363
|
+
shownNow.push(emitRooms(set.rooms));
|
|
1364
|
+
else if (set.documents.length > 0) {
|
|
1365
|
+
shownNow.push(publishRooms('counted', [...new Set(set.documents.map(({ locator }) => locator))]));
|
|
1048
1366
|
}
|
|
1367
|
+
for (const locator of set.queues)
|
|
1368
|
+
shownNow.push(emitQueue(locator));
|
|
1369
|
+
if (set.drafts.length > 0)
|
|
1370
|
+
shownNow.push(publishRoomDrafts(false));
|
|
1371
|
+
else if (set.documents.length > 0)
|
|
1372
|
+
shownNow.push(publishRoomDrafts(false, set.documents));
|
|
1049
1373
|
for (const app of set.presentations)
|
|
1050
|
-
emitPresentation(app);
|
|
1374
|
+
shownNow.push(emitPresentation(app));
|
|
1051
1375
|
conversations.changed(set);
|
|
1376
|
+
return Promise.all(shownNow).then(() => undefined);
|
|
1052
1377
|
}
|
|
1053
1378
|
async function refuse(reason, provider = null) {
|
|
1054
1379
|
discoveryPaused = true;
|
|
@@ -1058,53 +1383,31 @@ export function composeRuntime(options) {
|
|
|
1058
1383
|
if (reason === 'revoked-authorization') {
|
|
1059
1384
|
await exclusive(async () => {
|
|
1060
1385
|
const revoked = { status: 'unavailable', reason: 'revoked' };
|
|
1061
|
-
|
|
1062
|
-
locator,
|
|
1063
|
-
held,
|
|
1064
|
-
record: { ...held.record, model: revoked },
|
|
1065
|
-
}));
|
|
1066
|
-
let answer = null;
|
|
1386
|
+
let held = new Map();
|
|
1067
1387
|
try {
|
|
1068
1388
|
const local = await openLocal();
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1389
|
+
held = await storedRooms();
|
|
1390
|
+
await local.commit({
|
|
1391
|
+
rooms: [...held.values()].map(({ record: kept }) => {
|
|
1392
|
+
const record = { ...kept, model: revoked };
|
|
1393
|
+
return {
|
|
1394
|
+
locator: record.locator,
|
|
1395
|
+
key: record.key,
|
|
1396
|
+
record: storedRecord(record),
|
|
1397
|
+
composed: composedOf(composed(record)),
|
|
1398
|
+
};
|
|
1399
|
+
}),
|
|
1076
1400
|
});
|
|
1077
1401
|
}
|
|
1078
1402
|
catch {
|
|
1079
1403
|
journal.record('model revocation persistence failed');
|
|
1080
1404
|
}
|
|
1081
|
-
const
|
|
1082
|
-
for (const
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
liveModels.delete(key);
|
|
1086
|
-
}
|
|
1087
|
-
const changed = [];
|
|
1088
|
-
for (const { locator, held, record } of records) {
|
|
1089
|
-
if (replicas.get(locator) !== held)
|
|
1090
|
-
continue;
|
|
1091
|
-
const indexed = answer === null ? held.indexed : committedRoom(locator, answer).indexed;
|
|
1092
|
-
replicas.set(locator, { ...held, record, indexed });
|
|
1093
|
-
}
|
|
1094
|
-
for (const locator of [...rooms.keys()]) {
|
|
1095
|
-
const room = rooms.get(locator);
|
|
1096
|
-
if (room === undefined)
|
|
1097
|
-
continue;
|
|
1098
|
-
rooms.set(locator, {
|
|
1099
|
-
...room,
|
|
1100
|
-
model: modelTypes(revoked),
|
|
1101
|
-
typedSelection: room.typedSelection === null
|
|
1102
|
-
? null
|
|
1103
|
-
: { ...room.typedSelection, model: modelTypes(revoked) },
|
|
1104
|
-
});
|
|
1105
|
-
changed.push(changeSet(locator, { documents: null, room: true }));
|
|
1405
|
+
const listedNow = listedLocators(held);
|
|
1406
|
+
for (const locator of listedNow) {
|
|
1407
|
+
if (!held.has(locator))
|
|
1408
|
+
unheldModels.set(locator, revoked);
|
|
1106
1409
|
}
|
|
1107
|
-
publishChangeSet(mergeChangeSets(
|
|
1410
|
+
publishChangeSet(mergeChangeSets(listedNow.map((locator) => changeSet(locator, { documents: null, room: true }))));
|
|
1108
1411
|
prepare();
|
|
1109
1412
|
});
|
|
1110
1413
|
}
|
|
@@ -1121,49 +1424,66 @@ export function composeRuntime(options) {
|
|
|
1121
1424
|
}
|
|
1122
1425
|
}
|
|
1123
1426
|
function list(room, synchronizationAvailable) {
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
:
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
},
|
|
1138
|
-
providerOwnedPaths: room.providerOwnedPaths,
|
|
1139
|
-
roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
|
|
1140
|
-
synchronizationAvailable,
|
|
1141
|
-
refusal: scopedRefusals.get(room.locator) ?? null,
|
|
1427
|
+
if (isFenced(room.locator))
|
|
1428
|
+
return;
|
|
1429
|
+
declared.set(room.locator, {
|
|
1430
|
+
room: {
|
|
1431
|
+
name: room.name,
|
|
1432
|
+
locator: room.locator,
|
|
1433
|
+
handle: room.handle,
|
|
1434
|
+
operations: room.operations,
|
|
1435
|
+
replica: room.replica,
|
|
1436
|
+
typedSelection: room.typedSelection ?? null,
|
|
1437
|
+
providerOwnedPaths: room.providerOwnedPaths,
|
|
1438
|
+
},
|
|
1439
|
+
available: synchronizationAvailable,
|
|
1142
1440
|
});
|
|
1143
1441
|
}
|
|
1442
|
+
function listHeld(locator) {
|
|
1443
|
+
if (isFenced(locator))
|
|
1444
|
+
return;
|
|
1445
|
+
declared.set(locator, { room: null, available: false });
|
|
1446
|
+
}
|
|
1144
1447
|
async function applyDiscovery(found, started, left) {
|
|
1145
|
-
const lockedBefore = lockedRooms();
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1448
|
+
const lockedBefore = await lockedRooms();
|
|
1449
|
+
const local = await openLocal();
|
|
1450
|
+
const stillFenced = new Set(await local.fences());
|
|
1451
|
+
const relisting = found
|
|
1452
|
+
.map(({ locator }) => locator)
|
|
1453
|
+
.filter((locator) => {
|
|
1454
|
+
const fence = fencedAt.get(locator);
|
|
1455
|
+
return stillFenced.has(locator) && (fence === undefined || fence.at < started);
|
|
1456
|
+
});
|
|
1457
|
+
const relisted = relisting.length === 0 ? new Set() : await durably((held) => held.unfence(relisting));
|
|
1458
|
+
for (const { locator } of found) {
|
|
1459
|
+
const fence = fencedAt.get(locator);
|
|
1460
|
+
if (fence === undefined || fence.at >= started)
|
|
1461
|
+
continue;
|
|
1462
|
+
if (relisted.has(locator) || (!stillFenced.has(locator) && !fence.written)) {
|
|
1463
|
+
fencedAt.delete(locator);
|
|
1464
|
+
}
|
|
1150
1465
|
}
|
|
1466
|
+
const kept = await local.held();
|
|
1151
1467
|
await loseRooms(found
|
|
1152
|
-
.filter((room) => room.replica.admission !== 'admitted' &&
|
|
1468
|
+
.filter((room) => room.replica.admission !== 'admitted' &&
|
|
1469
|
+
(kept.held.has(room.locator) || transfers.has(room.locator)))
|
|
1153
1470
|
.map((room) => room.locator));
|
|
1154
|
-
const listed = found.filter((
|
|
1155
|
-
const
|
|
1156
|
-
for (const locator of
|
|
1471
|
+
const listed = found.filter(({ locator }) => !fencedAt.has(locator) && (!stillFenced.has(locator) || relisted.has(locator)));
|
|
1472
|
+
const listedLocatorsNow = new Set(listed.map((room) => room.locator));
|
|
1473
|
+
for (const locator of listedLocatorsNow)
|
|
1157
1474
|
scopedRefusals.delete(locator);
|
|
1158
1475
|
if (left === undefined)
|
|
1159
1476
|
discovered.clear();
|
|
1160
1477
|
for (const room of listed)
|
|
1161
1478
|
discovered.set(room.locator, room);
|
|
1162
|
-
|
|
1479
|
+
const named = new Set(found.map((room) => room.locator));
|
|
1480
|
+
await loseRooms(left ??
|
|
1481
|
+
[...new Set([...declared.keys(), ...kept.held, ...transfers.keys()])].filter((locator) => !named.has(locator)));
|
|
1482
|
+
const held = await storedRooms(listed.map((room) => room.locator));
|
|
1163
1483
|
for (const room of listed) {
|
|
1164
|
-
const
|
|
1165
|
-
if (
|
|
1166
|
-
|
|
1484
|
+
const record = held.get(room.locator)?.record;
|
|
1485
|
+
if (record !== undefined && record.replica.selection !== room.replica.selection) {
|
|
1486
|
+
listHeld(room.locator);
|
|
1167
1487
|
continue;
|
|
1168
1488
|
}
|
|
1169
1489
|
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
@@ -1171,74 +1491,36 @@ export function composeRuntime(options) {
|
|
|
1171
1491
|
confirmLocks(listed);
|
|
1172
1492
|
roomsListed = true;
|
|
1173
1493
|
emitRooms();
|
|
1174
|
-
relocked(lockedBefore);
|
|
1494
|
+
await relocked(lockedBefore);
|
|
1175
1495
|
return listed;
|
|
1176
1496
|
}
|
|
1177
|
-
function
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
for (const locator of fresh) {
|
|
1200
|
-
fences.set(locator, { at: discoveries, erased: false });
|
|
1201
|
-
lastFenced.set(locator, ++fencings);
|
|
1202
|
-
confirmed.delete(locator);
|
|
1203
|
-
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []) {
|
|
1204
|
-
pendings.delete(pendingKey(locator, batch.id));
|
|
1205
|
-
landedAhead.delete(pendingKey(locator, batch.id));
|
|
1206
|
-
}
|
|
1207
|
-
replicas.delete(locator);
|
|
1208
|
-
highest.delete(locator);
|
|
1209
|
-
beyondHorizon.delete(locator);
|
|
1210
|
-
queuesWithoutReplica.delete(locator);
|
|
1211
|
-
rooms.delete(locator);
|
|
1212
|
-
roomDrafts.delete(locator);
|
|
1213
|
-
attachedFiles.delete(locator);
|
|
1214
|
-
untransferred(locator);
|
|
1215
|
-
startedOver.delete(locator);
|
|
1216
|
-
catching.delete(locator);
|
|
1217
|
-
discovered.delete(locator);
|
|
1218
|
-
unreadable.delete(locator);
|
|
1219
|
-
stale.delete(locator);
|
|
1220
|
-
repairable.delete(locator);
|
|
1221
|
-
unpreparable.delete(locator);
|
|
1222
|
-
scopedRefusals.delete(locator);
|
|
1223
|
-
models.delete(locator);
|
|
1224
|
-
liveModels.delete(locator);
|
|
1225
|
-
invalidModels.delete(locator);
|
|
1226
|
-
journal.record(`room lost ${locator}`);
|
|
1227
|
-
}
|
|
1228
|
-
if (fresh.length === 0)
|
|
1229
|
-
return fresh;
|
|
1230
|
-
publishChangeSet(mergeChangeSets(removed.map(({ locator, drafts }) => changeSet(locator, { documents: null, room: true, queue: true, drafts }))));
|
|
1231
|
-
liveQueries.recover();
|
|
1232
|
-
moveTo({ ...status, ...attention() });
|
|
1233
|
-
return fresh;
|
|
1497
|
+
function forget(locator) {
|
|
1498
|
+
confirmed.delete(locator);
|
|
1499
|
+
for (const key of [...landedAhead.keys()]) {
|
|
1500
|
+
if (key.startsWith(`${locator}\u0000`))
|
|
1501
|
+
landedAhead.delete(key);
|
|
1502
|
+
}
|
|
1503
|
+
confirmations.delete(locator);
|
|
1504
|
+
highest.delete(locator);
|
|
1505
|
+
beyondHorizon.delete(locator);
|
|
1506
|
+
declared.delete(locator);
|
|
1507
|
+
attachedFiles.delete(locator);
|
|
1508
|
+
untransferred(locator);
|
|
1509
|
+
startedOver.delete(locator);
|
|
1510
|
+
catching.delete(locator);
|
|
1511
|
+
discovered.delete(locator);
|
|
1512
|
+
unreadable.delete(locator);
|
|
1513
|
+
stale.delete(locator);
|
|
1514
|
+
repairable.delete(locator);
|
|
1515
|
+
unpreparable.delete(locator);
|
|
1516
|
+
scopedRefusals.delete(locator);
|
|
1517
|
+
unheldModels.delete(locator);
|
|
1518
|
+
typesHeld.delete(locator);
|
|
1234
1519
|
}
|
|
1235
1520
|
async function eraseFenced(locators) {
|
|
1236
1521
|
for (const locator of locators) {
|
|
1237
1522
|
while (await durably((local) => local.erase(locator, ERASE_BATCH)))
|
|
1238
1523
|
await yielded();
|
|
1239
|
-
const held = fences.get(locator);
|
|
1240
|
-
if (held !== undefined)
|
|
1241
|
-
held.erased = true;
|
|
1242
1524
|
}
|
|
1243
1525
|
}
|
|
1244
1526
|
let sweeping = false;
|
|
@@ -1260,147 +1542,60 @@ export function composeRuntime(options) {
|
|
|
1260
1542
|
}
|
|
1261
1543
|
})();
|
|
1262
1544
|
}
|
|
1545
|
+
function fenceLive(locators) {
|
|
1546
|
+
for (const locator of new Set(locators)) {
|
|
1547
|
+
if (fencedAt.has(locator))
|
|
1548
|
+
continue;
|
|
1549
|
+
fencedAt.set(locator, { at: discoveries, written: false });
|
|
1550
|
+
lastFenced.set(locator, ++fencings);
|
|
1551
|
+
forget(locator);
|
|
1552
|
+
journal.record(`room lost ${locator}`);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1263
1555
|
async function loseRooms(locators) {
|
|
1264
|
-
|
|
1556
|
+
fenceLive(locators);
|
|
1557
|
+
const fresh = [...new Set(locators)].filter((locator) => {
|
|
1558
|
+
const fence = fencedAt.get(locator);
|
|
1559
|
+
if (fence === undefined || fence.written)
|
|
1560
|
+
return false;
|
|
1561
|
+
fence.written = true;
|
|
1562
|
+
return true;
|
|
1563
|
+
});
|
|
1265
1564
|
if (fresh.length === 0)
|
|
1266
1565
|
return;
|
|
1267
|
-
|
|
1268
|
-
await local.fence(fresh);
|
|
1269
|
-
await unpair(local);
|
|
1270
|
-
});
|
|
1271
|
-
await eraseFenced(fresh);
|
|
1272
|
-
}
|
|
1273
|
-
function retyped(persisted) {
|
|
1274
|
-
if (persisted.status === 'unavailable')
|
|
1275
|
-
return persisted;
|
|
1276
|
-
return { ...persisted, types: schemaTypes(persisted.snapshot.schema, roomTypes) };
|
|
1277
|
-
}
|
|
1278
|
-
function restored(key, held) {
|
|
1279
|
-
const persisted = retyped(held);
|
|
1280
|
-
const prior = models.get(key);
|
|
1281
|
-
if (prior !== undefined &&
|
|
1282
|
-
prior.status !== 'unavailable' &&
|
|
1283
|
-
(persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
|
|
1284
|
-
invalidModels.add(key);
|
|
1285
|
-
}
|
|
1286
|
-
const model = persisted.status === 'unavailable' ||
|
|
1287
|
-
liveModels.get(key) === JSON.stringify(persisted.snapshot)
|
|
1288
|
-
? persisted
|
|
1289
|
-
: cached(persisted);
|
|
1290
|
-
models.set(key, model);
|
|
1291
|
-
if (model.status === 'unavailable') {
|
|
1292
|
-
liveModels.delete(key);
|
|
1293
|
-
invalidModels.delete(key);
|
|
1294
|
-
}
|
|
1295
|
-
return model;
|
|
1296
|
-
}
|
|
1297
|
-
async function hydrate(opened) {
|
|
1298
|
-
const [held, queues, kept, heldConversations, chatDrafts] = await Promise.all([
|
|
1299
|
-
opened.opened(),
|
|
1300
|
-
opened.queues(),
|
|
1301
|
-
opened.roomDrafts(),
|
|
1302
|
-
opened.conversations(),
|
|
1303
|
-
opened.chatDrafts(),
|
|
1304
|
-
]);
|
|
1305
|
-
const records = [...held.rooms.values()];
|
|
1306
|
-
const counts = await opened.counts(records.map(({ key, typedSelection }) => ({ key, roots: typedSelection?.roots ?? [] })));
|
|
1307
|
-
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1308
|
-
targets = held.targets;
|
|
1309
|
-
transfers.clear();
|
|
1310
|
-
arrivals.clear();
|
|
1311
|
-
streaming.clear();
|
|
1312
|
-
startedOver.clear();
|
|
1313
|
-
limits = held.providerLimits;
|
|
1314
|
-
capabilities = held.capabilities;
|
|
1315
|
-
member = held.member;
|
|
1316
|
-
roomDrafts.clear();
|
|
1317
|
-
for (const record of kept)
|
|
1318
|
-
rememberRoomDraft(record);
|
|
1319
|
-
presentations.clear();
|
|
1320
|
-
for (const record of held.presentation)
|
|
1321
|
-
presentations.set(record.app, record.value);
|
|
1322
|
-
conversations.hydrate(heldConversations, chatDrafts, held.conversationList, held.conversationCursor);
|
|
1323
|
-
pendings.clear();
|
|
1324
|
-
landedAhead.clear();
|
|
1325
|
-
for (const queue of queues) {
|
|
1326
|
-
for (const { batch, state } of queue.pending) {
|
|
1327
|
-
pendings.set(pendingKey(queue.locator, batch.id), state);
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
replicas.clear();
|
|
1331
|
-
reopened.clear();
|
|
1332
|
-
queuesWithoutReplica.clear();
|
|
1333
|
-
for (const [locator, queue] of queuedBatches) {
|
|
1334
|
-
if (!held.rooms.has(locator))
|
|
1335
|
-
queuesWithoutReplica.set(locator, queue);
|
|
1336
|
-
}
|
|
1337
|
-
records.forEach((stored, index) => {
|
|
1338
|
-
if (fences.has(stored.locator))
|
|
1339
|
-
return;
|
|
1340
|
-
const record = { ...stored, model: restored(stored.locator, stored.model) };
|
|
1341
|
-
reopened.add(record.locator);
|
|
1342
|
-
replicas.set(record.locator, {
|
|
1343
|
-
record,
|
|
1344
|
-
queue: queuedBatches.get(record.locator) ?? [],
|
|
1345
|
-
overlaid: held.overlaid.get(record.key) ?? new Set(),
|
|
1346
|
-
count: counts[index] ?? 0,
|
|
1347
|
-
confirmed: 0,
|
|
1348
|
-
indexed: !held.unindexed.has(record.key),
|
|
1349
|
-
});
|
|
1350
|
-
const existing = rooms.get(record.locator);
|
|
1351
|
-
if (existing === undefined)
|
|
1352
|
-
list(record, record.roomVersion !== null);
|
|
1353
|
-
else {
|
|
1354
|
-
rooms.set(record.locator, {
|
|
1355
|
-
...existing,
|
|
1356
|
-
roomVersion: record.roomVersion,
|
|
1357
|
-
operations: record.operations,
|
|
1358
|
-
replica: record.replica,
|
|
1359
|
-
model: modelTypes(record.model),
|
|
1360
|
-
typedSelection: record.typedSelection === null
|
|
1361
|
-
? null
|
|
1362
|
-
: { roots: record.typedSelection.roots, model: modelTypes(builtIn) },
|
|
1363
|
-
});
|
|
1364
|
-
}
|
|
1365
|
-
});
|
|
1566
|
+
let held = [];
|
|
1366
1567
|
try {
|
|
1367
|
-
|
|
1368
|
-
await
|
|
1568
|
+
await durably(async (local) => {
|
|
1569
|
+
held = await local.roomDrafts();
|
|
1570
|
+
await local.fence(fresh);
|
|
1571
|
+
});
|
|
1369
1572
|
}
|
|
1370
1573
|
catch (error) {
|
|
1371
|
-
|
|
1372
|
-
|
|
1574
|
+
for (const locator of fresh) {
|
|
1575
|
+
const fence = fencedAt.get(locator);
|
|
1576
|
+
if (fence !== undefined)
|
|
1577
|
+
fence.written = false;
|
|
1578
|
+
}
|
|
1579
|
+
throw error;
|
|
1373
1580
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
if (replicas.size > 0)
|
|
1387
|
-
emitRooms();
|
|
1388
|
-
if (member !== null)
|
|
1389
|
-
emitMember();
|
|
1390
|
-
for (const locator of locators)
|
|
1391
|
-
emitQueue(locator);
|
|
1392
|
-
publishRoomDrafts(false);
|
|
1393
|
-
return lastCompleted;
|
|
1581
|
+
finally {
|
|
1582
|
+
publishChangeSet(mergeChangeSets(fresh.map((locator) => changeSet(locator, {
|
|
1583
|
+
documents: null,
|
|
1584
|
+
room: true,
|
|
1585
|
+
queue: true,
|
|
1586
|
+
drafts: held.filter((draft) => draft.locator === locator).map(({ path }) => path),
|
|
1587
|
+
}))));
|
|
1588
|
+
recoverReads();
|
|
1589
|
+
moveTo(status);
|
|
1590
|
+
}
|
|
1591
|
+
await durably(unpair);
|
|
1592
|
+
await eraseFenced(fresh);
|
|
1394
1593
|
}
|
|
1395
1594
|
async function open() {
|
|
1396
1595
|
if (closed)
|
|
1397
1596
|
throw new Error('runtime closed');
|
|
1398
|
-
const opened = openRuntimeStore(
|
|
1597
|
+
const opened = openRuntimeStore(database);
|
|
1399
1598
|
const interrupted = await opened.fences();
|
|
1400
|
-
for (const locator of interrupted) {
|
|
1401
|
-
fences.set(locator, { at: discoveries, erased: false });
|
|
1402
|
-
lastFenced.set(locator, ++fencings);
|
|
1403
|
-
}
|
|
1404
1599
|
const kept = options.shell.generation;
|
|
1405
1600
|
const storedGeneration = await opened.generation();
|
|
1406
1601
|
const loss = detectStorageLoss(storedGeneration, kept);
|
|
@@ -1413,7 +1608,21 @@ export function composeRuntime(options) {
|
|
|
1413
1608
|
storageLoss = loss;
|
|
1414
1609
|
emitStorageLoss();
|
|
1415
1610
|
}
|
|
1416
|
-
const
|
|
1611
|
+
const held = await opened.opened();
|
|
1612
|
+
heldWhenOpened = usableWhenHeld.length > 0 && (await opened.covering()).rooms.size > 0;
|
|
1613
|
+
try {
|
|
1614
|
+
await opened.demote();
|
|
1615
|
+
await database.ask({ kind: 'read.builds', build });
|
|
1616
|
+
for (const key of held.orphans)
|
|
1617
|
+
await opened.release(key);
|
|
1618
|
+
}
|
|
1619
|
+
catch (error) {
|
|
1620
|
+
if (!refusedForSpace(error))
|
|
1621
|
+
throw error;
|
|
1622
|
+
}
|
|
1623
|
+
if (held.erasing || held.orphans.length > 0)
|
|
1624
|
+
queueMicrotask(sweep);
|
|
1625
|
+
await conversations.opened(opened);
|
|
1417
1626
|
if (interrupted.length > 0) {
|
|
1418
1627
|
queueMicrotask(() => {
|
|
1419
1628
|
void eraseFenced(interrupted).catch(() => {
|
|
@@ -1421,7 +1630,9 @@ export function composeRuntime(options) {
|
|
|
1421
1630
|
});
|
|
1422
1631
|
});
|
|
1423
1632
|
}
|
|
1424
|
-
moveTo(
|
|
1633
|
+
moveTo(status);
|
|
1634
|
+
prepare();
|
|
1635
|
+
emitRooms();
|
|
1425
1636
|
void exclusive(() => unpair(opened)).catch(() => {
|
|
1426
1637
|
journal.record('unpair failed');
|
|
1427
1638
|
});
|
|
@@ -1464,9 +1675,9 @@ export function composeRuntime(options) {
|
|
|
1464
1675
|
gathering = entry;
|
|
1465
1676
|
return entry.done;
|
|
1466
1677
|
}
|
|
1467
|
-
function withdrawable(
|
|
1678
|
+
function withdrawable(states) {
|
|
1468
1679
|
return (batch) => {
|
|
1469
|
-
const state =
|
|
1680
|
+
const state = states.get(batch.id);
|
|
1470
1681
|
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
1471
1682
|
};
|
|
1472
1683
|
}
|
|
@@ -1488,13 +1699,13 @@ export function composeRuntime(options) {
|
|
|
1488
1699
|
const partner = partnerOf(batch, locator);
|
|
1489
1700
|
if (partner === null)
|
|
1490
1701
|
return true;
|
|
1491
|
-
const other =
|
|
1702
|
+
const other = await replicaOf(partner);
|
|
1492
1703
|
const half = other?.queue.find((one) => one.id === batch.id);
|
|
1493
1704
|
if (other === undefined || half === undefined)
|
|
1494
1705
|
return false;
|
|
1495
1706
|
return landed(half, await sliceAt(other.record, 'confirmed', [half]));
|
|
1496
1707
|
}
|
|
1497
|
-
function carriedBack(locator, unlanded, written) {
|
|
1708
|
+
async function carriedBack(locator, unlanded, written) {
|
|
1498
1709
|
const back = new Map();
|
|
1499
1710
|
const taken = [];
|
|
1500
1711
|
for (const batch of unlanded) {
|
|
@@ -1503,13 +1714,15 @@ export function composeRuntime(options) {
|
|
|
1503
1714
|
continue;
|
|
1504
1715
|
const source = between.source.room;
|
|
1505
1716
|
const destination = between.destination.room;
|
|
1506
|
-
|
|
1507
|
-
|
|
1717
|
+
const local = await openLocal();
|
|
1718
|
+
const arrived = (await local.pending(destination, batch.id)).state?.drafts ?? [];
|
|
1719
|
+
for (const { path: to } of arrived) {
|
|
1720
|
+
const record = await draftOf(destination, to);
|
|
1508
1721
|
const path = between.moves.find((move) => move.to === to)?.path;
|
|
1509
1722
|
if (record === undefined || path === undefined)
|
|
1510
1723
|
continue;
|
|
1511
1724
|
const [into, key] = source === locator ? [written, path] : [back, documentKey(source, path)];
|
|
1512
|
-
const existing = into.get(key) ??
|
|
1725
|
+
const existing = into.get(key) ?? (await draftOf(source, path));
|
|
1513
1726
|
taken.push({ locator: destination, path: to });
|
|
1514
1727
|
into.set(key, {
|
|
1515
1728
|
locator: source,
|
|
@@ -1535,19 +1748,14 @@ export function composeRuntime(options) {
|
|
|
1535
1748
|
.filter((one) => one.locator === room)
|
|
1536
1749
|
.map(({ path }) => path),
|
|
1537
1750
|
})),
|
|
1538
|
-
remember() {
|
|
1539
|
-
for (const one of taken)
|
|
1540
|
-
forgetRoomDraft(one.locator, one.path);
|
|
1541
|
-
for (const record of records)
|
|
1542
|
-
rememberRoomDraft(record);
|
|
1543
|
-
},
|
|
1544
1751
|
};
|
|
1545
1752
|
}
|
|
1546
|
-
function
|
|
1547
|
-
|
|
1548
|
-
if (!indexed)
|
|
1753
|
+
function preparedAfter(locator, answer) {
|
|
1754
|
+
if (answer.indexed[locator] === false)
|
|
1549
1755
|
prepare([locator]);
|
|
1550
|
-
|
|
1756
|
+
}
|
|
1757
|
+
function confirmedAgain(locator) {
|
|
1758
|
+
confirmations.set(locator, (confirmations.get(locator) ?? 0) + 1);
|
|
1551
1759
|
}
|
|
1552
1760
|
function replayed(record, confirmedLayer, queue) {
|
|
1553
1761
|
return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
|
|
@@ -1564,13 +1772,14 @@ export function composeRuntime(options) {
|
|
|
1564
1772
|
touched,
|
|
1565
1773
|
};
|
|
1566
1774
|
}
|
|
1567
|
-
async function settle(local, locator,
|
|
1568
|
-
return together(local, [await settlement(local, locator,
|
|
1775
|
+
async function settle(local, locator, work, record, queue, settling = {}) {
|
|
1776
|
+
return together(local, [await settlement(local, locator, work, record, queue, settling)]);
|
|
1569
1777
|
}
|
|
1570
|
-
function unpairedIn(locator, batch) {
|
|
1571
|
-
|
|
1778
|
+
async function unpairedIn(locator, batch) {
|
|
1779
|
+
const { state } = await (await openLocal()).pending(locator, batch.id);
|
|
1780
|
+
if (state === undefined)
|
|
1572
1781
|
return false;
|
|
1573
|
-
return !leavingIn(batch, locator) || withdrawable(
|
|
1782
|
+
return !leavingIn(batch, locator) || withdrawable(new Map([[batch.id, state]]))(batch);
|
|
1574
1783
|
}
|
|
1575
1784
|
async function together(local, settlements) {
|
|
1576
1785
|
const settled = new Set(settlements.map(({ locator }) => locator));
|
|
@@ -1580,8 +1789,9 @@ export function composeRuntime(options) {
|
|
|
1580
1789
|
for (const { locator, left } of wave) {
|
|
1581
1790
|
for (const batch of left) {
|
|
1582
1791
|
const partner = partnerOf(batch, locator);
|
|
1583
|
-
if (partner === null || settled.has(partner) || !unpairedIn(partner, batch))
|
|
1792
|
+
if (partner === null || settled.has(partner) || !(await unpairedIn(partner, batch))) {
|
|
1584
1793
|
continue;
|
|
1794
|
+
}
|
|
1585
1795
|
partnered.set(partner, [...(partnered.get(partner) ?? []), batch]);
|
|
1586
1796
|
}
|
|
1587
1797
|
}
|
|
@@ -1599,9 +1809,10 @@ export function composeRuntime(options) {
|
|
|
1599
1809
|
});
|
|
1600
1810
|
return mergeChangeSets(all.map((one) => one.apply(answer)));
|
|
1601
1811
|
}
|
|
1602
|
-
async function settlement(local, locator,
|
|
1812
|
+
async function settlement(local, locator, work, record, queue, settling = {}) {
|
|
1813
|
+
const before = work.replica;
|
|
1603
1814
|
const view = settling.view ?? 'confirmed';
|
|
1604
|
-
const previousQueue =
|
|
1815
|
+
const previousQueue = work.queue;
|
|
1605
1816
|
const overrides = settling.changes;
|
|
1606
1817
|
const read = await sliceAt(record, view, queue, [
|
|
1607
1818
|
...(before !== undefined && before.record.key === record.key ? before.overlaid : []),
|
|
@@ -1618,7 +1829,7 @@ export function composeRuntime(options) {
|
|
|
1618
1829
|
Object.entries(held).some(([path, revision]) => confirmedLayer.revisions[path] !== revision));
|
|
1619
1830
|
});
|
|
1620
1831
|
const unshown = queue.filter((batch) => !shown.includes(batch));
|
|
1621
|
-
const { withdrawn, kept } = await stepped(standing(confirmedLayer, unshown, withdrawable(
|
|
1832
|
+
const { withdrawn, kept } = await stepped(standing(confirmedLayer, unshown, withdrawable(work.states), localSaves, locator, composition));
|
|
1622
1833
|
const founded = kept.filter((batch) => !unshown.includes(batch));
|
|
1623
1834
|
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
1624
1835
|
const unqueued = shown.map((batch) => ({ batch, landed: true }));
|
|
@@ -1636,8 +1847,8 @@ export function composeRuntime(options) {
|
|
|
1636
1847
|
for (const { batch, returned: reason } of returning) {
|
|
1637
1848
|
if (reason === null || batch.between?.destination.room === locator)
|
|
1638
1849
|
continue;
|
|
1639
|
-
for (const saved of
|
|
1640
|
-
const existing = written.get(saved.path) ??
|
|
1850
|
+
for (const saved of work.states.get(batch.id)?.drafts ?? []) {
|
|
1851
|
+
const existing = written.get(saved.path) ?? (await draftOf(locator, saved.path));
|
|
1641
1852
|
written.set(saved.path, {
|
|
1642
1853
|
locator,
|
|
1643
1854
|
path: saved.path,
|
|
@@ -1647,12 +1858,12 @@ export function composeRuntime(options) {
|
|
|
1647
1858
|
});
|
|
1648
1859
|
}
|
|
1649
1860
|
}
|
|
1650
|
-
const carried = carriedBack(locator, [
|
|
1861
|
+
const carried = await carriedBack(locator, [
|
|
1651
1862
|
...[...unqueued].reverse().flatMap(({ batch, landed: held }) => (held ? [] : [batch])),
|
|
1652
1863
|
...(settling.leaving ?? []).flatMap(({ batch, returned }) => returned === null ? [] : [batch]),
|
|
1653
1864
|
], written);
|
|
1654
1865
|
const replayedLayer = await replayed(record, confirmedLayer, kept);
|
|
1655
|
-
const { overlay
|
|
1866
|
+
const { overlay } = await overlayDiff(record, before, replayedLayer.visible, replayedLayer.touched);
|
|
1656
1867
|
const commit = {
|
|
1657
1868
|
rooms: [
|
|
1658
1869
|
{
|
|
@@ -1672,6 +1883,7 @@ export function composeRuntime(options) {
|
|
|
1672
1883
|
{
|
|
1673
1884
|
locator,
|
|
1674
1885
|
queue: kept,
|
|
1886
|
+
stored: previousQueue,
|
|
1675
1887
|
founded,
|
|
1676
1888
|
roomDrafts: [...written.values()],
|
|
1677
1889
|
consumedRoomDrafts: carried.taken,
|
|
@@ -1686,33 +1898,21 @@ export function composeRuntime(options) {
|
|
|
1686
1898
|
commit,
|
|
1687
1899
|
apply: (answer) => {
|
|
1688
1900
|
for (const batch of previousQueue) {
|
|
1689
|
-
if (staying.has(batch.id))
|
|
1690
|
-
|
|
1691
|
-
pendings.delete(pendingKey(locator, batch.id));
|
|
1692
|
-
landedAhead.delete(pendingKey(locator, batch.id));
|
|
1901
|
+
if (!staying.has(batch.id))
|
|
1902
|
+
landedAhead.delete(pendingKey(locator, batch.id));
|
|
1693
1903
|
}
|
|
1694
|
-
for (const draft of written.values())
|
|
1695
|
-
rememberRoomDraft(draft);
|
|
1696
|
-
carried.remember();
|
|
1697
1904
|
const drafts = [...written.keys(), ...carried.taken];
|
|
1698
|
-
queuesWithoutReplica.delete(locator);
|
|
1699
1905
|
for (const { batch, landed: held } of unqueued) {
|
|
1700
1906
|
const outcome = localSaves ? 'dropped' : held ? 'landed' : 'withdrawn';
|
|
1701
1907
|
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
1702
1908
|
}
|
|
1703
1909
|
const changed = answer.changed[locator];
|
|
1704
|
-
|
|
1910
|
+
if (settling.commit?.published !== undefined ||
|
|
1705
1911
|
settling.commit?.refreshed === true ||
|
|
1706
|
-
overrides !== undefined
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
queue: kept,
|
|
1711
|
-
overlaid: touched,
|
|
1712
|
-
count,
|
|
1713
|
-
confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
|
|
1714
|
-
indexed,
|
|
1715
|
-
});
|
|
1912
|
+
overrides !== undefined) {
|
|
1913
|
+
confirmedAgain(locator);
|
|
1914
|
+
}
|
|
1915
|
+
preparedAfter(locator, answer);
|
|
1716
1916
|
return mergeChangeSets([
|
|
1717
1917
|
changeSet(locator, {
|
|
1718
1918
|
documents: changed === undefined ? [] : changed,
|
|
@@ -1725,22 +1925,11 @@ export function composeRuntime(options) {
|
|
|
1725
1925
|
},
|
|
1726
1926
|
};
|
|
1727
1927
|
}
|
|
1728
|
-
function
|
|
1729
|
-
const model = record.model;
|
|
1730
|
-
models.set(locator, model);
|
|
1731
|
-
invalidModels.delete(locator);
|
|
1732
|
-
if (model.status === 'current')
|
|
1733
|
-
liveModels.set(locator, JSON.stringify(model.snapshot));
|
|
1734
|
-
else if (model.status === 'unavailable')
|
|
1735
|
-
liveModels.delete(locator);
|
|
1736
|
-
}
|
|
1737
|
-
function remodel(locator, answers, held) {
|
|
1928
|
+
function remodel(answers, held) {
|
|
1738
1929
|
const answer = answers?.collection;
|
|
1739
1930
|
if (answer === undefined)
|
|
1740
1931
|
return held;
|
|
1741
|
-
|
|
1742
|
-
models.set(locator, state);
|
|
1743
|
-
return { collection: state };
|
|
1932
|
+
return { collection: attachedModel(answer, roomTypes) };
|
|
1744
1933
|
}
|
|
1745
1934
|
function publish(local, room, reached, landing, live) {
|
|
1746
1935
|
const landed = exclusive(async () => {
|
|
@@ -1748,8 +1937,9 @@ export function composeRuntime(options) {
|
|
|
1748
1937
|
caughtUpTo(room.locator);
|
|
1749
1938
|
return true;
|
|
1750
1939
|
}
|
|
1751
|
-
const
|
|
1752
|
-
|
|
1940
|
+
const work = await workOf(room.locator);
|
|
1941
|
+
const prior = work.replica;
|
|
1942
|
+
if (!standsAt(room.locator, prior, landing.from)) {
|
|
1753
1943
|
if (landing.kind === 'refreshed') {
|
|
1754
1944
|
await local.unrefresh(reached.key);
|
|
1755
1945
|
return false;
|
|
@@ -1765,11 +1955,9 @@ export function composeRuntime(options) {
|
|
|
1765
1955
|
...(landing.remodelled ? { model: reached.model } : {}),
|
|
1766
1956
|
}
|
|
1767
1957
|
: reached;
|
|
1768
|
-
adoptModels(room.locator, record);
|
|
1769
|
-
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
1770
1958
|
let set;
|
|
1771
1959
|
try {
|
|
1772
|
-
set = await settle(local, room.locator,
|
|
1960
|
+
set = await settle(local, room.locator, work, record, work.queue, landing.kind === 'refreshed'
|
|
1773
1961
|
? {
|
|
1774
1962
|
room: true,
|
|
1775
1963
|
view: 'refreshed',
|
|
@@ -1791,14 +1979,16 @@ export function composeRuntime(options) {
|
|
|
1791
1979
|
return true;
|
|
1792
1980
|
}
|
|
1793
1981
|
if (landing.kind === 'bootstrapped') {
|
|
1982
|
+
readied(room.locator);
|
|
1794
1983
|
highest.delete(room.locator);
|
|
1795
1984
|
beyondHorizon.delete(room.locator);
|
|
1796
1985
|
untransferred(room.locator);
|
|
1797
1986
|
startedOver.delete(room.locator);
|
|
1798
1987
|
catching.delete(room.locator);
|
|
1988
|
+
unheldModels.delete(room.locator);
|
|
1799
1989
|
if (prior !== undefined && prior.record.key !== record.key)
|
|
1800
1990
|
sweep();
|
|
1801
|
-
name(room.locator);
|
|
1991
|
+
void name(room.locator);
|
|
1802
1992
|
}
|
|
1803
1993
|
stale.delete(room.locator);
|
|
1804
1994
|
unpreparable.delete(room.locator);
|
|
@@ -1807,28 +1997,27 @@ export function composeRuntime(options) {
|
|
|
1807
1997
|
list(room, true);
|
|
1808
1998
|
publishChangeSet(set);
|
|
1809
1999
|
await unpair(local);
|
|
1810
|
-
moveTo(
|
|
1811
|
-
if (landing.kind === 'bootstrapped' &&
|
|
2000
|
+
moveTo(status);
|
|
2001
|
+
if (landing.kind === 'bootstrapped' && work.queue.length > 0)
|
|
1812
2002
|
submitLater(room.locator);
|
|
1813
|
-
}
|
|
1814
2003
|
return true;
|
|
1815
2004
|
});
|
|
1816
2005
|
return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
|
|
1817
2006
|
}
|
|
1818
2007
|
async function savedRoom(locator) {
|
|
1819
2008
|
const local = await openLocal();
|
|
1820
|
-
const replica =
|
|
1821
|
-
|
|
1822
|
-
if (replica === undefined || room === undefined) {
|
|
2009
|
+
const replica = await replicaOf(locator);
|
|
2010
|
+
if (replica === undefined || isFenced(locator)) {
|
|
1823
2011
|
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
1824
2012
|
}
|
|
1825
|
-
return { local, replica
|
|
2013
|
+
return { local, replica };
|
|
1826
2014
|
}
|
|
1827
2015
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
1828
2016
|
const target = await savedRoom(locator);
|
|
2017
|
+
const drafts = new Map((await draftsOf(locator, named.map(({ path }) => path))).map((record) => [record.path, record]));
|
|
1829
2018
|
const saved = [];
|
|
1830
2019
|
for (const { path, revision } of named) {
|
|
1831
|
-
const record =
|
|
2020
|
+
const record = drafts.get(path);
|
|
1832
2021
|
if (record === undefined || record.revision !== revision) {
|
|
1833
2022
|
return { outcome: 'refused', code: 'stale', retryable: false, file: path };
|
|
1834
2023
|
}
|
|
@@ -1855,14 +2044,16 @@ export function composeRuntime(options) {
|
|
|
1855
2044
|
}
|
|
1856
2045
|
return committed(locator, target, saved, new Set(), writtenBy, files);
|
|
1857
2046
|
}
|
|
1858
|
-
function carriedDrafts(locator, saved, consumed) {
|
|
1859
|
-
|
|
2047
|
+
async function carriedDrafts(locator, saved, consumed) {
|
|
2048
|
+
if (!saved.some(({ parts }) => parts.move !== undefined))
|
|
2049
|
+
return new Map();
|
|
2050
|
+
const holding = new Map((await draftsOf(locator)).map((record) => [record.path, record]));
|
|
1860
2051
|
return new Map(saved.flatMap(({ path, parts }) => {
|
|
1861
|
-
const record = holding
|
|
2052
|
+
const record = holding.get(path);
|
|
1862
2053
|
if (parts.move === undefined || record === undefined || consumed.has(path))
|
|
1863
2054
|
return [];
|
|
1864
2055
|
const to = moveTarget(parts.move);
|
|
1865
|
-
if (to === path || holding
|
|
2056
|
+
if (to === path || holding.has(to))
|
|
1866
2057
|
return [];
|
|
1867
2058
|
return [[path, { ...record, path: to, revision: roomDraftRevision() }]];
|
|
1868
2059
|
}));
|
|
@@ -1977,15 +2168,15 @@ export function composeRuntime(options) {
|
|
|
1977
2168
|
...new Set((await local.referrers(key, view, targets)).map(({ path }) => path)),
|
|
1978
2169
|
];
|
|
1979
2170
|
}
|
|
1980
|
-
async function sideOf(locator, replica,
|
|
2171
|
+
async function sideOf(locator, replica, saving) {
|
|
1981
2172
|
const record = replica.record;
|
|
1982
2173
|
return {
|
|
1983
2174
|
room: locator,
|
|
1984
|
-
handle:
|
|
2175
|
+
handle: record.handle,
|
|
1985
2176
|
baseRoomVersion: record.roomVersion,
|
|
1986
2177
|
composition: compositionFor(record),
|
|
1987
|
-
operations:
|
|
1988
|
-
providerOwnedPaths:
|
|
2178
|
+
operations: record.operations,
|
|
2179
|
+
providerOwnedPaths: record.providerOwnedPaths,
|
|
1989
2180
|
visible: saving.visible,
|
|
1990
2181
|
folders: saving.folders,
|
|
1991
2182
|
queued: queuedDocuments(replica.queue),
|
|
@@ -1998,12 +2189,10 @@ export function composeRuntime(options) {
|
|
|
1998
2189
|
return into?.parts.move?.room ?? null;
|
|
1999
2190
|
}
|
|
2000
2191
|
async function saveCommit(local, files, rooms) {
|
|
2001
|
-
const touched = new Map();
|
|
2002
2192
|
const commits = [];
|
|
2003
2193
|
for (const { locator, saving, before, queue } of rooms) {
|
|
2004
2194
|
const replayedLayer = await replayed(saving.record, saving.confirmed, queue);
|
|
2005
2195
|
const diff = await overlayDiff(saving.record, before, replayedLayer.visible, replayedLayer.touched);
|
|
2006
|
-
touched.set(locator, diff.touched);
|
|
2007
2196
|
commits.push({
|
|
2008
2197
|
locator,
|
|
2009
2198
|
key: saving.record.key,
|
|
@@ -2013,27 +2202,18 @@ export function composeRuntime(options) {
|
|
|
2013
2202
|
});
|
|
2014
2203
|
}
|
|
2015
2204
|
try {
|
|
2016
|
-
|
|
2205
|
+
return await local.commit({
|
|
2017
2206
|
rooms: commits,
|
|
2018
2207
|
writes: rooms.map(({ write }) => write),
|
|
2019
2208
|
files,
|
|
2020
2209
|
whole: true,
|
|
2021
2210
|
});
|
|
2022
|
-
return { answer, touched };
|
|
2023
2211
|
}
|
|
2024
2212
|
catch {
|
|
2025
2213
|
return null;
|
|
2026
2214
|
}
|
|
2027
2215
|
}
|
|
2028
|
-
function
|
|
2029
|
-
replicas.set(locator, {
|
|
2030
|
-
...before,
|
|
2031
|
-
queue,
|
|
2032
|
-
overlaid: touched,
|
|
2033
|
-
count: answer.counts[locator] ?? before.count,
|
|
2034
|
-
});
|
|
2035
|
-
}
|
|
2036
|
-
async function committedMove(locator, destination, { local, replica, room }, saved, consumed, writtenBy, files) {
|
|
2216
|
+
async function committedMove(locator, destination, { local, replica }, saved, consumed, writtenBy, files) {
|
|
2037
2217
|
const mixed = saved.find(({ parts }) => Object.keys(parts).length !== 1 || parts.move?.room !== destination);
|
|
2038
2218
|
if (mixed !== undefined) {
|
|
2039
2219
|
return { outcome: 'refused', code: 'mixed-room-move', retryable: false, file: mixed.path };
|
|
@@ -2043,27 +2223,28 @@ export function composeRuntime(options) {
|
|
|
2043
2223
|
const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
|
|
2044
2224
|
if (changed.length > 0)
|
|
2045
2225
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2046
|
-
const target =
|
|
2047
|
-
|
|
2048
|
-
if (target === undefined || listedRoom === undefined) {
|
|
2226
|
+
const target = await replicaOf(destination);
|
|
2227
|
+
if (target === undefined || isFenced(destination)) {
|
|
2049
2228
|
throw runtimeError('unknown-room', `Unknown Room ${destination}`);
|
|
2050
2229
|
}
|
|
2051
2230
|
const arriving = await savingIn(target, [], moves.map(({ to }) => to));
|
|
2052
2231
|
const built = await buildMove({
|
|
2053
|
-
source: await sideOf(locator, replica,
|
|
2054
|
-
destination: await sideOf(destination, target,
|
|
2232
|
+
source: await sideOf(locator, replica, sourceSaving),
|
|
2233
|
+
destination: await sideOf(destination, target, arriving),
|
|
2055
2234
|
moves,
|
|
2056
|
-
providerLimits:
|
|
2235
|
+
providerLimits: (await local.facts()).providerLimits,
|
|
2057
2236
|
local: localSaves,
|
|
2058
2237
|
...(writtenBy === undefined ? {} : { writtenBy }),
|
|
2059
2238
|
});
|
|
2060
2239
|
if (built.kind === 'refused')
|
|
2061
2240
|
return { outcome: 'refused', ...built.refusal };
|
|
2062
2241
|
const id = built.leaving.id;
|
|
2242
|
+
const leavingDrafts = new Map((await draftsOf(locator, saved.map(({ path }) => path))).map((record) => [record.path, record]));
|
|
2243
|
+
const arrivingDrafts = new Set((await draftsOf(destination, moves.map(({ to }) => to))).map(({ path }) => path));
|
|
2063
2244
|
const carrying = saved.flatMap(({ path, parts }) => {
|
|
2064
|
-
const record =
|
|
2245
|
+
const record = leavingDrafts.get(path);
|
|
2065
2246
|
const to = moveTarget(parts.move);
|
|
2066
|
-
if (record === undefined || consumed.has(path) ||
|
|
2247
|
+
if (record === undefined || consumed.has(path) || arrivingDrafts.has(to)) {
|
|
2067
2248
|
return [];
|
|
2068
2249
|
}
|
|
2069
2250
|
const moved = {
|
|
@@ -2079,7 +2260,7 @@ export function composeRuntime(options) {
|
|
|
2079
2260
|
const left = [...consumed, ...carrying.map(({ from }) => from)];
|
|
2080
2261
|
const leavingQueue = [...replica.queue, built.leaving];
|
|
2081
2262
|
const arrivingQueue = [...target.queue, built.arriving];
|
|
2082
|
-
const
|
|
2263
|
+
const answer = await saveCommit(local, files, [
|
|
2083
2264
|
{
|
|
2084
2265
|
locator,
|
|
2085
2266
|
saving: sourceSaving,
|
|
@@ -2088,6 +2269,7 @@ export function composeRuntime(options) {
|
|
|
2088
2269
|
write: {
|
|
2089
2270
|
locator,
|
|
2090
2271
|
queue: leavingQueue,
|
|
2272
|
+
stored: replica.queue,
|
|
2091
2273
|
saved: new Map([[id, saved]]),
|
|
2092
2274
|
consumedRoomDrafts: left,
|
|
2093
2275
|
},
|
|
@@ -2100,37 +2282,23 @@ export function composeRuntime(options) {
|
|
|
2100
2282
|
write: {
|
|
2101
2283
|
locator: destination,
|
|
2102
2284
|
queue: arrivingQueue,
|
|
2285
|
+
stored: target.queue,
|
|
2103
2286
|
saved: new Map([[id, marked]]),
|
|
2104
2287
|
roomDrafts: carried,
|
|
2105
2288
|
},
|
|
2106
2289
|
},
|
|
2107
2290
|
]);
|
|
2108
|
-
if (
|
|
2291
|
+
if (answer === null)
|
|
2109
2292
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2110
|
-
const { answer, touched } = committedBoth;
|
|
2111
2293
|
if (answer.fenced !== null) {
|
|
2112
2294
|
throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
|
|
2113
2295
|
}
|
|
2114
|
-
if (
|
|
2296
|
+
if (isFenced(locator) || isFenced(destination)) {
|
|
2115
2297
|
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
2116
2298
|
}
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
lost: false,
|
|
2121
|
-
});
|
|
2122
|
-
pendings.set(pendingKey(destination, id), {
|
|
2123
|
-
drafts: marked,
|
|
2124
|
-
submission: { state: 'waiting' },
|
|
2125
|
-
lost: false,
|
|
2126
|
-
});
|
|
2127
|
-
for (const path of left)
|
|
2128
|
-
forgetRoomDraft(locator, path);
|
|
2129
|
-
for (const record of carried)
|
|
2130
|
-
rememberRoomDraft(record);
|
|
2131
|
-
savedReplica(locator, replica, leavingQueue, touched.get(locator), answer);
|
|
2132
|
-
savedReplica(destination, target, arrivingQueue, touched.get(destination), answer);
|
|
2133
|
-
publishChangeSet(mergeChangeSets([
|
|
2299
|
+
preparedAfter(locator, answer);
|
|
2300
|
+
preparedAfter(destination, answer);
|
|
2301
|
+
const shownNow = publishChangeSet(mergeChangeSets([
|
|
2134
2302
|
changeSet(locator, {
|
|
2135
2303
|
documents: answer.changed[locator] ?? [],
|
|
2136
2304
|
queue: true,
|
|
@@ -2142,14 +2310,15 @@ export function composeRuntime(options) {
|
|
|
2142
2310
|
drafts: carried.map(({ path }) => path),
|
|
2143
2311
|
}),
|
|
2144
2312
|
]));
|
|
2145
|
-
queuedSave(locator);
|
|
2313
|
+
await Promise.all([shownNow, queuedSave(locator)]);
|
|
2146
2314
|
return { outcome: 'saved', identifier: id };
|
|
2147
2315
|
}
|
|
2148
2316
|
function queuedSave(locator) {
|
|
2149
2317
|
if (!localSaves)
|
|
2150
2318
|
recorded = false;
|
|
2151
|
-
moveTo(
|
|
2319
|
+
const shownNow = moveTo(localSaves ? status : { ...status, state: 'pending', resting: false });
|
|
2152
2320
|
submitLater(locator);
|
|
2321
|
+
return shownNow;
|
|
2153
2322
|
}
|
|
2154
2323
|
async function committed(locator, target, saved, consumed, writtenBy, files = []) {
|
|
2155
2324
|
if (full)
|
|
@@ -2158,7 +2327,8 @@ export function composeRuntime(options) {
|
|
|
2158
2327
|
if (into !== null) {
|
|
2159
2328
|
return committedMove(locator, into, target, saved, consumed, writtenBy, files);
|
|
2160
2329
|
}
|
|
2161
|
-
const { local, replica
|
|
2330
|
+
const { local, replica } = target;
|
|
2331
|
+
const { providerLimits } = await local.facts();
|
|
2162
2332
|
const parts = saved.map(({ path, parts: held }) => changesOf(path, held));
|
|
2163
2333
|
const changes = parts.flatMap(({ changes: made }) => made);
|
|
2164
2334
|
const saving = await savingIn(replica, changes, [
|
|
@@ -2168,7 +2338,7 @@ export function composeRuntime(options) {
|
|
|
2168
2338
|
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
|
|
2169
2339
|
if (changed.length > 0)
|
|
2170
2340
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2171
|
-
const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(
|
|
2341
|
+
const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(replica.states), saving.visibleOver));
|
|
2172
2342
|
if (requeue.kind === 'refused')
|
|
2173
2343
|
return { outcome: 'refused', ...requeue.refusal };
|
|
2174
2344
|
const foreign = [...requeue.withdrawn.keys()].some((origin) => {
|
|
@@ -2188,10 +2358,10 @@ export function composeRuntime(options) {
|
|
|
2188
2358
|
local: localSaves,
|
|
2189
2359
|
baseRoomVersion: replica.record.roomVersion,
|
|
2190
2360
|
composition: compositionFor(replica.record),
|
|
2191
|
-
operations:
|
|
2192
|
-
providerOwnedPaths:
|
|
2193
|
-
providerLimits
|
|
2194
|
-
handle:
|
|
2361
|
+
operations: replica.record.operations,
|
|
2362
|
+
providerOwnedPaths: replica.record.providerOwnedPaths,
|
|
2363
|
+
providerLimits,
|
|
2364
|
+
handle: replica.record.handle,
|
|
2195
2365
|
visible: saving.visible,
|
|
2196
2366
|
folders: saving.folders,
|
|
2197
2367
|
changes,
|
|
@@ -2243,7 +2413,7 @@ export function composeRuntime(options) {
|
|
|
2243
2413
|
for (const [origin, replaced] of withdrawn) {
|
|
2244
2414
|
if (replaced === null)
|
|
2245
2415
|
continue;
|
|
2246
|
-
const carried = withdrawnDrafts(
|
|
2416
|
+
const carried = withdrawnDrafts(replica.states.get(origin)?.drafts ?? [], changes);
|
|
2247
2417
|
drafted.set(replaced.id, replaced === join?.batch
|
|
2248
2418
|
? [
|
|
2249
2419
|
...carried.map((held) => joinedDraft(held, saved)),
|
|
@@ -2257,14 +2427,14 @@ export function composeRuntime(options) {
|
|
|
2257
2427
|
for (const replaced of withdrawn.values()) {
|
|
2258
2428
|
if (replaced === null || !batches.includes(replaced))
|
|
2259
2429
|
continue;
|
|
2260
|
-
const limited = await stepped(limiting(batches, replaced,
|
|
2430
|
+
const limited = await stepped(limiting(batches, replaced, providerLimits, saving.visibleOver));
|
|
2261
2431
|
if (limited !== null)
|
|
2262
2432
|
return { outcome: 'refused', ...limited };
|
|
2263
2433
|
}
|
|
2264
|
-
const carried = carriedDrafts(locator, saved, consumed);
|
|
2434
|
+
const carried = await carriedDrafts(locator, saved, consumed);
|
|
2265
2435
|
const left = [...consumed, ...carried.keys()];
|
|
2266
2436
|
const arrived = [...carried.values()];
|
|
2267
|
-
const
|
|
2437
|
+
const answer = await saveCommit(local, files, [
|
|
2268
2438
|
{
|
|
2269
2439
|
locator,
|
|
2270
2440
|
saving,
|
|
@@ -2273,40 +2443,27 @@ export function composeRuntime(options) {
|
|
|
2273
2443
|
write: {
|
|
2274
2444
|
locator,
|
|
2275
2445
|
queue: batches,
|
|
2446
|
+
stored: replica.queue,
|
|
2276
2447
|
saved: drafted,
|
|
2277
2448
|
consumedRoomDrafts: left,
|
|
2278
2449
|
roomDrafts: arrived,
|
|
2279
2450
|
},
|
|
2280
2451
|
},
|
|
2281
2452
|
]);
|
|
2282
|
-
if (
|
|
2453
|
+
if (answer === null)
|
|
2283
2454
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2284
|
-
const { answer, touched } = done;
|
|
2285
2455
|
if (answer.fenced !== null) {
|
|
2286
2456
|
throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
|
|
2287
2457
|
}
|
|
2288
|
-
if (
|
|
2458
|
+
if (isFenced(locator))
|
|
2289
2459
|
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
for (const [id, drafts] of drafted) {
|
|
2293
|
-
pendings.set(pendingKey(locator, id), {
|
|
2294
|
-
drafts,
|
|
2295
|
-
submission: { state: 'waiting' },
|
|
2296
|
-
lost: false,
|
|
2297
|
-
});
|
|
2298
|
-
}
|
|
2299
|
-
for (const path of left)
|
|
2300
|
-
forgetRoomDraft(locator, path);
|
|
2301
|
-
for (const record of arrived)
|
|
2302
|
-
rememberRoomDraft(record);
|
|
2303
|
-
savedReplica(locator, replica, batches, touched.get(locator), answer);
|
|
2304
|
-
publishChangeSet(changeSet(locator, {
|
|
2460
|
+
preparedAfter(locator, answer);
|
|
2461
|
+
const shownNow = publishChangeSet(changeSet(locator, {
|
|
2305
2462
|
documents: answer.changed[locator] ?? [],
|
|
2306
2463
|
queue: true,
|
|
2307
2464
|
drafts: [...left, ...arrived.map(({ path }) => path)],
|
|
2308
2465
|
}));
|
|
2309
|
-
queuedSave(locator);
|
|
2466
|
+
await Promise.all([shownNow, queuedSave(locator)]);
|
|
2310
2467
|
const identifier = join?.batch.id ??
|
|
2311
2468
|
built?.batch.id ??
|
|
2312
2469
|
[...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
|
|
@@ -2315,29 +2472,26 @@ export function composeRuntime(options) {
|
|
|
2315
2472
|
async function mark(local, locator, batch, submission, lost) {
|
|
2316
2473
|
const partner = partnerOf(batch, locator);
|
|
2317
2474
|
for (const held of partner === null ? [locator] : [locator, partner]) {
|
|
2318
|
-
const state =
|
|
2475
|
+
const { state, room } = await local.pending(held, batch.id);
|
|
2319
2476
|
if (!continuing() ||
|
|
2320
2477
|
state === undefined ||
|
|
2321
2478
|
(JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
|
|
2322
2479
|
continue;
|
|
2323
2480
|
}
|
|
2324
2481
|
await local.submit(held, batch.id, submission, lost);
|
|
2325
|
-
if (
|
|
2482
|
+
if (isFenced(held))
|
|
2326
2483
|
continue;
|
|
2327
|
-
|
|
2328
|
-
if (replicas.has(held))
|
|
2484
|
+
if (room)
|
|
2329
2485
|
publishChangeSet(changeSet(held, { queue: true }));
|
|
2330
2486
|
}
|
|
2331
|
-
moveTo(
|
|
2487
|
+
moveTo(status);
|
|
2332
2488
|
}
|
|
2333
2489
|
async function keepUpload(local, locator, identifier, upload) {
|
|
2334
2490
|
if (!continuing())
|
|
2335
2491
|
throw new Error('connection refused');
|
|
2336
2492
|
await local.keepUpload(locator, identifier, upload);
|
|
2337
|
-
if (
|
|
2493
|
+
if (isFenced(locator))
|
|
2338
2494
|
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
2339
|
-
const key = pendingKey(locator, identifier);
|
|
2340
|
-
pendings.set(key, { ...pendings.get(key), upload });
|
|
2341
2495
|
if (!continuing())
|
|
2342
2496
|
throw new Error('connection refused');
|
|
2343
2497
|
}
|
|
@@ -2373,7 +2527,7 @@ export function composeRuntime(options) {
|
|
|
2373
2527
|
if (fencedSince(locator, since))
|
|
2374
2528
|
return failure({ kind: 'room-not-readable', locator });
|
|
2375
2529
|
if (result.kind === 'scoped-refusal') {
|
|
2376
|
-
const room =
|
|
2530
|
+
const room = await recordAt(result.locator);
|
|
2377
2531
|
if (room !== undefined)
|
|
2378
2532
|
await scoped(result, room, await openLocal());
|
|
2379
2533
|
}
|
|
@@ -2386,27 +2540,28 @@ export function composeRuntime(options) {
|
|
|
2386
2540
|
return UNCERTAIN;
|
|
2387
2541
|
}
|
|
2388
2542
|
}
|
|
2389
|
-
function located(room, path, destination) {
|
|
2543
|
+
async function located(room, path, destination) {
|
|
2390
2544
|
const crossing = roomTarget(destination);
|
|
2545
|
+
const listing = await listingNow();
|
|
2391
2546
|
if (crossing === null) {
|
|
2392
2547
|
const named = target(path, destination);
|
|
2393
|
-
return named === null || !
|
|
2548
|
+
return named === null || !listing.has(room) ? null : { room, path: named };
|
|
2394
2549
|
}
|
|
2395
|
-
const held = [...
|
|
2550
|
+
const held = [...listing.values()].find((listed) => listed.handle === crossing.handle);
|
|
2396
2551
|
return held === undefined ? null : { room: held.locator, path: crossing.path };
|
|
2397
2552
|
}
|
|
2398
2553
|
async function resolveReference(room, path, destination) {
|
|
2399
2554
|
if (roomTarget(destination) === null && target(path, destination) === null) {
|
|
2400
2555
|
return ABSENT_TARGET;
|
|
2401
2556
|
}
|
|
2402
|
-
const at = located(room, path, destination);
|
|
2557
|
+
const at = await located(room, path, destination);
|
|
2403
2558
|
if (at === null)
|
|
2404
2559
|
return UNREADABLE;
|
|
2405
|
-
if (locked(at.room))
|
|
2560
|
+
if (await locked(at.room))
|
|
2406
2561
|
return LOCKED_UNREADABLE;
|
|
2407
|
-
const held =
|
|
2562
|
+
const held = await recordAt(at.room);
|
|
2408
2563
|
if (held !== undefined &&
|
|
2409
|
-
(await (await openLocal()).entries(held.
|
|
2564
|
+
(await (await openLocal()).entries(held.key, 'visible', [at.path])).length > 0) {
|
|
2410
2565
|
return { kind: 'document', ...at };
|
|
2411
2566
|
}
|
|
2412
2567
|
const history = await readHistory(at.room, at.path);
|
|
@@ -2421,13 +2576,13 @@ export function composeRuntime(options) {
|
|
|
2421
2576
|
? { kind: 'deleted', ...at, revision: last }
|
|
2422
2577
|
: { kind: 'document', ...at };
|
|
2423
2578
|
}
|
|
2424
|
-
function queuedFacts(locator, path, localId) {
|
|
2425
|
-
for (const batch of
|
|
2579
|
+
async function queuedFacts(locator, path, localId) {
|
|
2580
|
+
for (const batch of (await replicaOf(locator))?.queue ?? []) {
|
|
2426
2581
|
const change = batch.changes[queuedFile(batch, path, localId)];
|
|
2427
2582
|
if (change?.kind === 'binary')
|
|
2428
2583
|
return change;
|
|
2429
2584
|
}
|
|
2430
|
-
const drafted =
|
|
2585
|
+
const drafted = (await draftOf(locator, path))?.parts.file;
|
|
2431
2586
|
return drafted?.localId === localId ? drafted : null;
|
|
2432
2587
|
}
|
|
2433
2588
|
function batchFile(batch) {
|
|
@@ -2459,7 +2614,7 @@ export function composeRuntime(options) {
|
|
|
2459
2614
|
if (fencedSince(request.locator, since))
|
|
2460
2615
|
return failure({ kind: 'room-not-readable', locator: request.locator });
|
|
2461
2616
|
if (settled.kind === 'scoped-refusal') {
|
|
2462
|
-
const room =
|
|
2617
|
+
const room = await recordAt(settled.locator);
|
|
2463
2618
|
if (room !== undefined)
|
|
2464
2619
|
await scoped(settled, room, await openLocal());
|
|
2465
2620
|
}
|
|
@@ -2486,7 +2641,7 @@ export function composeRuntime(options) {
|
|
|
2486
2641
|
async function binaryPath(locator, path, revision) {
|
|
2487
2642
|
if (revision !== undefined)
|
|
2488
2643
|
return { kind: 'path', locator, path, revision };
|
|
2489
|
-
const replica =
|
|
2644
|
+
const replica = await replicaOf(locator);
|
|
2490
2645
|
const local = await openLocal();
|
|
2491
2646
|
const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
|
|
2492
2647
|
const attached = held === undefined ? attachedFiles.get(locator)?.get(path) : undefined;
|
|
@@ -2526,13 +2681,13 @@ export function composeRuntime(options) {
|
|
|
2526
2681
|
}
|
|
2527
2682
|
async function askedBinary(locator, path, revision) {
|
|
2528
2683
|
await ready;
|
|
2529
|
-
const drafted =
|
|
2684
|
+
const drafted = (await draftOf(locator, path))?.parts.file;
|
|
2530
2685
|
return drafted !== undefined && drafted.localId === revision
|
|
2531
2686
|
? { kind: 'pending', localId: revision }
|
|
2532
2687
|
: binaryPath(locator, path, revision);
|
|
2533
2688
|
}
|
|
2534
|
-
function pendingBinary(locator, path, localId) {
|
|
2535
|
-
const facts = queuedFacts(locator, path, localId);
|
|
2689
|
+
async function pendingBinary(locator, path, localId) {
|
|
2690
|
+
const facts = await queuedFacts(locator, path, localId);
|
|
2536
2691
|
return facts === null
|
|
2537
2692
|
? absentBinary(path)
|
|
2538
2693
|
: {
|
|
@@ -2551,10 +2706,10 @@ export function composeRuntime(options) {
|
|
|
2551
2706
|
return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
|
|
2552
2707
|
}
|
|
2553
2708
|
async function heldMediaType(locator, path) {
|
|
2554
|
-
const replica =
|
|
2709
|
+
const replica = await recordAt(locator);
|
|
2555
2710
|
const [held] = replica === undefined
|
|
2556
2711
|
? []
|
|
2557
|
-
: await (await openLocal()).entries(replica.
|
|
2712
|
+
: await (await openLocal()).entries(replica.key, 'visible', [path]);
|
|
2558
2713
|
return held === undefined
|
|
2559
2714
|
? attachedFiles.get(locator)?.get(path)?.mediaType
|
|
2560
2715
|
: held.binary?.mediaType;
|
|
@@ -2589,7 +2744,7 @@ export function composeRuntime(options) {
|
|
|
2589
2744
|
async function attachBinary(locator, file) {
|
|
2590
2745
|
await ready;
|
|
2591
2746
|
await firstDiscovery;
|
|
2592
|
-
if (closed || !roomsNamed ||
|
|
2747
|
+
if (closed || !roomsNamed || (await connectionNow()) === 'unreachable' || refusal !== null)
|
|
2593
2748
|
return OFFLINE;
|
|
2594
2749
|
const request = await attachedFile(locator, file);
|
|
2595
2750
|
return transferBinary({ locator, path: file.name }, async (held) => {
|
|
@@ -2609,8 +2764,8 @@ export function composeRuntime(options) {
|
|
|
2609
2764
|
const freed = await database.ask({ kind: 'opened.clean' });
|
|
2610
2765
|
return freed + ((await options.shell.cleanUp?.()) ?? 0);
|
|
2611
2766
|
}
|
|
2612
|
-
function declaredAction(name) {
|
|
2613
|
-
for (const capability of capabilities) {
|
|
2767
|
+
async function declaredAction(name) {
|
|
2768
|
+
for (const capability of (await (await openLocal()).facts()).capabilities) {
|
|
2614
2769
|
for (const action of capability.actions ?? []) {
|
|
2615
2770
|
if (action.name === name)
|
|
2616
2771
|
return action;
|
|
@@ -2618,8 +2773,8 @@ export function composeRuntime(options) {
|
|
|
2618
2773
|
}
|
|
2619
2774
|
return undefined;
|
|
2620
2775
|
}
|
|
2621
|
-
function calledRooms(name, input) {
|
|
2622
|
-
const member = declaredAction(name)?.room;
|
|
2776
|
+
async function calledRooms(name, input) {
|
|
2777
|
+
const member = (await declaredAction(name))?.room;
|
|
2623
2778
|
if (member === undefined || input === null || typeof input !== 'object')
|
|
2624
2779
|
return [];
|
|
2625
2780
|
const named = input[member];
|
|
@@ -2629,7 +2784,7 @@ export function composeRuntime(options) {
|
|
|
2629
2784
|
await ready;
|
|
2630
2785
|
if (closed || refusal !== null)
|
|
2631
2786
|
return UNCERTAIN;
|
|
2632
|
-
if (declaredAction(name) === undefined)
|
|
2787
|
+
if ((await declaredAction(name)) === undefined)
|
|
2633
2788
|
return undeclared(name);
|
|
2634
2789
|
try {
|
|
2635
2790
|
await ensureAuthorization();
|
|
@@ -2644,7 +2799,7 @@ export function composeRuntime(options) {
|
|
|
2644
2799
|
try {
|
|
2645
2800
|
const result = await options.adapter.action(authorization, { name, input });
|
|
2646
2801
|
if (result.kind === 'scoped-refusal') {
|
|
2647
|
-
const room =
|
|
2802
|
+
const room = await recordAt(result.locator);
|
|
2648
2803
|
if (room !== undefined)
|
|
2649
2804
|
await scoped(result, room, await openLocal());
|
|
2650
2805
|
}
|
|
@@ -2731,31 +2886,35 @@ export function composeRuntime(options) {
|
|
|
2731
2886
|
function continuing() {
|
|
2732
2887
|
return !closed && !discoveryPaused;
|
|
2733
2888
|
}
|
|
2734
|
-
function reachedBy(locator, roomVersion) {
|
|
2735
|
-
const held =
|
|
2736
|
-
return
|
|
2889
|
+
async function reachedBy(locator, roomVersion) {
|
|
2890
|
+
const held = await recordAt(locator);
|
|
2891
|
+
return held !== undefined && options.adapter.reached(locator, held.roomVersion, roomVersion);
|
|
2737
2892
|
}
|
|
2738
|
-
function shownAt(locator) {
|
|
2739
|
-
const held = replicas.get(locator)?.record.roomVersion;
|
|
2893
|
+
function shownAt(locator, held) {
|
|
2740
2894
|
const raised = highest.get(locator);
|
|
2741
2895
|
if (held === undefined || raised === undefined)
|
|
2742
2896
|
return held ?? raised;
|
|
2743
2897
|
return options.adapter.reached(locator, held, raised) ? held : raised;
|
|
2744
2898
|
}
|
|
2745
|
-
function showing(locator, roomVersion) {
|
|
2746
|
-
const at = shownAt(locator);
|
|
2899
|
+
function showing(locator, roomVersion, held) {
|
|
2900
|
+
const at = shownAt(locator, held);
|
|
2747
2901
|
if (at === undefined || !options.adapter.reached(locator, at, roomVersion)) {
|
|
2748
2902
|
highest.set(locator, roomVersion);
|
|
2749
2903
|
}
|
|
2750
2904
|
}
|
|
2751
|
-
function
|
|
2905
|
+
function olderThan(locator, roomVersion, held) {
|
|
2752
2906
|
if (beyondHorizon.has(locator))
|
|
2753
2907
|
return false;
|
|
2754
|
-
const at = shownAt(locator);
|
|
2908
|
+
const at = shownAt(locator, held);
|
|
2755
2909
|
return (at !== undefined &&
|
|
2756
2910
|
options.adapter.reached(locator, at, roomVersion) &&
|
|
2757
2911
|
!options.adapter.reached(locator, roomVersion, at));
|
|
2758
2912
|
}
|
|
2913
|
+
async function older(locator, roomVersion) {
|
|
2914
|
+
if (beyondHorizon.has(locator))
|
|
2915
|
+
return false;
|
|
2916
|
+
return olderThan(locator, roomVersion, (await recordAt(locator))?.roomVersion);
|
|
2917
|
+
}
|
|
2759
2918
|
function replaced(locators) {
|
|
2760
2919
|
for (const locator of locators) {
|
|
2761
2920
|
highest.delete(locator);
|
|
@@ -2763,17 +2922,22 @@ export function composeRuntime(options) {
|
|
|
2763
2922
|
}
|
|
2764
2923
|
}
|
|
2765
2924
|
async function writtenAfter(locator, roomVersion, path) {
|
|
2766
|
-
const held =
|
|
2925
|
+
const held = await recordAt(locator);
|
|
2767
2926
|
if (held === undefined)
|
|
2768
2927
|
return false;
|
|
2769
|
-
const [entry] = await (await openLocal()).entries(held.
|
|
2928
|
+
const [entry] = await (await openLocal()).entries(held.key, 'confirmed', [path]);
|
|
2770
2929
|
const written = entry?.written ?? undefined;
|
|
2771
2930
|
return (written !== undefined &&
|
|
2772
2931
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
2773
2932
|
!options.adapter.reached(locator, roomVersion, written));
|
|
2774
2933
|
}
|
|
2775
|
-
async function writtenLater(local, locator, key, paths, roomVersion) {
|
|
2776
|
-
if (paths.length === 0
|
|
2934
|
+
async function writtenLater(local, locator, key, paths, roomVersion, heldAt) {
|
|
2935
|
+
if (paths.length === 0)
|
|
2936
|
+
return new Set();
|
|
2937
|
+
const behindShown = heldAt === undefined
|
|
2938
|
+
? await older(locator, roomVersion)
|
|
2939
|
+
: olderThan(locator, roomVersion, heldAt);
|
|
2940
|
+
if (!behindShown)
|
|
2777
2941
|
return new Set();
|
|
2778
2942
|
const entries = await local.entries(key, 'confirmed', paths);
|
|
2779
2943
|
return new Set(entries.flatMap(({ path, written }) => written !== null &&
|
|
@@ -2782,7 +2946,7 @@ export function composeRuntime(options) {
|
|
|
2782
2946
|
? [path]
|
|
2783
2947
|
: []));
|
|
2784
2948
|
}
|
|
2785
|
-
function refreshingOf(local, locator, key, composition, liveness, replaying = false) {
|
|
2949
|
+
function refreshingOf(local, locator, key, composition, liveness, replaying = false, heldAt) {
|
|
2786
2950
|
const write = replaying ? finishingWrite : exclusive;
|
|
2787
2951
|
return {
|
|
2788
2952
|
turn() {
|
|
@@ -2790,7 +2954,7 @@ export function composeRuntime(options) {
|
|
|
2790
2954
|
installing(locator);
|
|
2791
2955
|
},
|
|
2792
2956
|
prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
|
|
2793
|
-
later: (paths, roomVersion) => writtenLater(local, locator, key, paths, roomVersion),
|
|
2957
|
+
later: (paths, roomVersion) => writtenLater(local, locator, key, paths, roomVersion, heldAt),
|
|
2794
2958
|
async stage(page) {
|
|
2795
2959
|
for (let written = 0;;) {
|
|
2796
2960
|
const end = batchEnd(page.entries, written);
|
|
@@ -2928,7 +3092,7 @@ export function composeRuntime(options) {
|
|
|
2928
3092
|
unindexed.delete(locator);
|
|
2929
3093
|
}
|
|
2930
3094
|
function setBack(locator) {
|
|
2931
|
-
const now = bytesOf(locator, true);
|
|
3095
|
+
const now = bytesOf(locator, true, new Map());
|
|
2932
3096
|
const earlier = startedOver.get(locator);
|
|
2933
3097
|
startedOver.set(locator, {
|
|
2934
3098
|
done: Math.max(now.done, earlier?.done ?? 0),
|
|
@@ -2964,11 +3128,13 @@ export function composeRuntime(options) {
|
|
|
2964
3128
|
if (installs.delete(locator))
|
|
2965
3129
|
queueReadiness();
|
|
2966
3130
|
}
|
|
2967
|
-
async function pullRefresh(authorized,
|
|
3131
|
+
async function pullRefresh(authorized, record, liveness, hold, refresh) {
|
|
2968
3132
|
if (!liveness())
|
|
2969
3133
|
return unconfirmed;
|
|
2970
|
-
const
|
|
2971
|
-
|
|
3134
|
+
const from = {
|
|
3135
|
+
key: record.key,
|
|
3136
|
+
confirmed: confirmations.get(record.locator) ?? 0,
|
|
3137
|
+
};
|
|
2972
3138
|
const local = await openLocal();
|
|
2973
3139
|
try {
|
|
2974
3140
|
await exclusive(() => local.unrefresh(record.key));
|
|
@@ -2980,7 +3146,7 @@ export function composeRuntime(options) {
|
|
|
2980
3146
|
finally {
|
|
2981
3147
|
hold.take();
|
|
2982
3148
|
}
|
|
2983
|
-
}, authorized, record, (models, kept) => (liveness() ? remodel(
|
|
3149
|
+
}, authorized, record, (models, kept) => (liveness() ? remodel(models, kept) : kept), liveness, refreshingOf(local, record.locator, record.key, composed(record), liveness, false, record.roomVersion));
|
|
2984
3150
|
if (result.kind !== 'advanced') {
|
|
2985
3151
|
journal.record(`transfer refresh ${record.locator} ${result.kind}`);
|
|
2986
3152
|
return result;
|
|
@@ -3000,30 +3166,14 @@ export function composeRuntime(options) {
|
|
|
3000
3166
|
throw error;
|
|
3001
3167
|
}
|
|
3002
3168
|
}
|
|
3003
|
-
const heldContents = new Map();
|
|
3004
|
-
function heldContentOf(local, key) {
|
|
3005
|
-
const known = heldContents.get(key);
|
|
3006
|
-
if (known !== undefined)
|
|
3007
|
-
return known;
|
|
3008
|
-
if (heldContents.size > ROOM_READS)
|
|
3009
|
-
heldContents.clear();
|
|
3010
|
-
const made = {
|
|
3011
|
-
files: (after, limit) => local.files(key, 'confirmed', after, limit),
|
|
3012
|
-
async contents(paths) {
|
|
3013
|
-
return new Map((await local.entries(key, 'confirmed', paths)).flatMap(({ path, content, revision }) => content === null || revision === null ? [] : [[path, { content, revision }]]));
|
|
3014
|
-
},
|
|
3015
|
-
};
|
|
3016
|
-
heldContents.set(key, made);
|
|
3017
|
-
return made;
|
|
3018
|
-
}
|
|
3019
3169
|
async function abandoned(local, locator) {
|
|
3020
3170
|
await local.abandon(locator);
|
|
3021
3171
|
setBack(locator);
|
|
3022
3172
|
untransferred(locator);
|
|
3023
|
-
|
|
3173
|
+
recoverReads();
|
|
3024
3174
|
sweep();
|
|
3025
3175
|
}
|
|
3026
|
-
function staging(local, liveness) {
|
|
3176
|
+
function staging(local, liveness, unheld) {
|
|
3027
3177
|
const derived = async (locator, key, composition) => {
|
|
3028
3178
|
underived.delete(locator);
|
|
3029
3179
|
for (;;) {
|
|
@@ -3093,9 +3243,9 @@ export function composeRuntime(options) {
|
|
|
3093
3243
|
const held = { ...record, key };
|
|
3094
3244
|
transfers.set(locator, held);
|
|
3095
3245
|
if (arrived)
|
|
3096
|
-
|
|
3246
|
+
recoverReads();
|
|
3097
3247
|
installing(locator);
|
|
3098
|
-
|
|
3248
|
+
recoverReads();
|
|
3099
3249
|
const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
|
|
3100
3250
|
owedIndexes(locator, record.done.bytes - prior, text);
|
|
3101
3251
|
const owed = text + (underived.get(locator) ?? 0);
|
|
@@ -3104,6 +3254,37 @@ export function composeRuntime(options) {
|
|
|
3104
3254
|
return held;
|
|
3105
3255
|
return (await derived(locator, key, composition)) ? held : null;
|
|
3106
3256
|
},
|
|
3257
|
+
async resume(record, parts, composition) {
|
|
3258
|
+
const { locator } = record;
|
|
3259
|
+
installing(locator);
|
|
3260
|
+
if (!liveness())
|
|
3261
|
+
return null;
|
|
3262
|
+
const prior = unheld === undefined ? await recordAt(locator) : undefined;
|
|
3263
|
+
const published = prior !== undefined && prior.replica.selection === record.selection ? prior.key : null;
|
|
3264
|
+
const kept = unheld?.staged ?? (await local.held()).staged.has(locator);
|
|
3265
|
+
if (published === null && !kept)
|
|
3266
|
+
return { record: null, staged: new Set() };
|
|
3267
|
+
const resumed = await installWrite(async () => {
|
|
3268
|
+
if (!liveness())
|
|
3269
|
+
return null;
|
|
3270
|
+
return local.resume(record, composition, parts.map((part) => part.id), published);
|
|
3271
|
+
});
|
|
3272
|
+
if (resumed === null || !liveness())
|
|
3273
|
+
return null;
|
|
3274
|
+
const parted = new Set(resumed.staged);
|
|
3275
|
+
if (resumed.key === null)
|
|
3276
|
+
return { record: null, staged: parted };
|
|
3277
|
+
const bytes = parts.reduce((sum, part) => sum + (parted.has(part.id) ? part.bytes : 0), 0);
|
|
3278
|
+
const held = {
|
|
3279
|
+
...record,
|
|
3280
|
+
transfer: resumed.transfer,
|
|
3281
|
+
key: resumed.key,
|
|
3282
|
+
done: { bytes },
|
|
3283
|
+
};
|
|
3284
|
+
transfers.set(locator, held);
|
|
3285
|
+
recoverReads();
|
|
3286
|
+
return { record: held, staged: parted };
|
|
3287
|
+
},
|
|
3107
3288
|
derive: derived,
|
|
3108
3289
|
async refreshing(locator, key, composition) {
|
|
3109
3290
|
await exclusive(() => local.unrefresh(key));
|
|
@@ -3118,33 +3299,26 @@ export function composeRuntime(options) {
|
|
|
3118
3299
|
return;
|
|
3119
3300
|
setBack(locator);
|
|
3120
3301
|
untransferred(locator);
|
|
3121
|
-
|
|
3302
|
+
recoverReads();
|
|
3122
3303
|
},
|
|
3123
3304
|
release: (key) => exclusive(async () => {
|
|
3124
3305
|
await local.release(key);
|
|
3125
3306
|
sweep();
|
|
3126
3307
|
}),
|
|
3127
|
-
content: (key) => heldContentOf(local, key),
|
|
3128
3308
|
};
|
|
3129
3309
|
}
|
|
3130
|
-
function
|
|
3131
|
-
const record = replicas.get(room.locator)?.record;
|
|
3132
|
-
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
3133
|
-
return null;
|
|
3134
|
-
return heldContentOf(local, record.key);
|
|
3135
|
-
}
|
|
3136
|
-
async function pullBootstrap(authorized, room, local, liveness) {
|
|
3310
|
+
async function pullBootstrap(authorized, room, local, liveness, prior, from = reads, unheld) {
|
|
3137
3311
|
if (!liveness())
|
|
3138
3312
|
return unconfirmed;
|
|
3139
|
-
if (
|
|
3313
|
+
if (prior !== undefined) {
|
|
3140
3314
|
catching.add(room.locator);
|
|
3141
|
-
|
|
3315
|
+
recoverReads();
|
|
3142
3316
|
}
|
|
3143
3317
|
try {
|
|
3144
|
-
const result = await bootstrapRoom(
|
|
3318
|
+
const result = await bootstrapRoom(from, authorized, room, { collection: prior?.model ?? unheldModels.get(room.locator) ?? NO_MODEL }, builtIn, typedPaths, (models, held) => (liveness() ? remodel(models, held) : held), liveness, staging(local, liveness, unheld));
|
|
3145
3319
|
const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
|
|
3146
3320
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
3147
|
-
if (result.kind === 'staged' && older(room.locator, result.record.roomVersion)) {
|
|
3321
|
+
if (result.kind === 'staged' && (await older(room.locator, result.record.roomVersion))) {
|
|
3148
3322
|
caughtUpTo(room.locator);
|
|
3149
3323
|
installed(room.locator);
|
|
3150
3324
|
await abandoned(local, room.locator);
|
|
@@ -3168,28 +3342,31 @@ export function composeRuntime(options) {
|
|
|
3168
3342
|
}
|
|
3169
3343
|
function caughtUpTo(locator) {
|
|
3170
3344
|
if (catching.delete(locator))
|
|
3171
|
-
|
|
3345
|
+
recoverReads();
|
|
3172
3346
|
}
|
|
3173
|
-
function behind(room) {
|
|
3174
|
-
const held =
|
|
3347
|
+
async function behind(room) {
|
|
3348
|
+
const held = (await recordAt(room.locator))?.roomVersion;
|
|
3175
3349
|
return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
|
|
3176
3350
|
}
|
|
3177
|
-
function
|
|
3178
|
-
|
|
3179
|
-
|
|
3351
|
+
function standingFrom(locator, held) {
|
|
3352
|
+
return { key: held?.record.key ?? null, confirmed: confirmations.get(locator) ?? 0 };
|
|
3353
|
+
}
|
|
3354
|
+
function standingAt(locator, held) {
|
|
3355
|
+
return { key: held?.key ?? null, confirmed: confirmations.get(locator) ?? 0 };
|
|
3180
3356
|
}
|
|
3181
|
-
function
|
|
3182
|
-
const now =
|
|
3357
|
+
function standsAt(locator, held, from) {
|
|
3358
|
+
const now = standingFrom(locator, held);
|
|
3183
3359
|
return now.key === from.key && now.confirmed === from.confirmed;
|
|
3184
3360
|
}
|
|
3185
|
-
async function advance(room, local, authorized, running) {
|
|
3361
|
+
async function advance(room, local, authorized, running, unheld) {
|
|
3186
3362
|
const live = () => running() && !full;
|
|
3187
3363
|
if (!live())
|
|
3188
3364
|
return unconfirmed;
|
|
3189
|
-
const held =
|
|
3190
|
-
if (
|
|
3365
|
+
const held = unheld === undefined ? await recordAt(room.locator) : undefined;
|
|
3366
|
+
if (options.adapter.roomStream !== undefined &&
|
|
3367
|
+
(held !== undefined || !beyondHorizon.has(room.locator))) {
|
|
3191
3368
|
stale.delete(room.locator);
|
|
3192
|
-
name(room.locator);
|
|
3369
|
+
await name(room.locator);
|
|
3193
3370
|
return null;
|
|
3194
3371
|
}
|
|
3195
3372
|
const refresh = reads.refresh;
|
|
@@ -3203,15 +3380,16 @@ export function composeRuntime(options) {
|
|
|
3203
3380
|
return unconfirmed;
|
|
3204
3381
|
replaced([room.locator]);
|
|
3205
3382
|
}
|
|
3206
|
-
const
|
|
3207
|
-
const
|
|
3383
|
+
const prior = held === undefined ? undefined : await recordAt(room.locator);
|
|
3384
|
+
const from = standingAt(room.locator, prior);
|
|
3385
|
+
const bootstrapped = await pullBootstrap(authorized, room, local, live, prior, reads, unheld);
|
|
3208
3386
|
if (bootstrapped.kind !== 'staged')
|
|
3209
3387
|
return live() ? bootstrapped : unconfirmed;
|
|
3210
3388
|
const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
|
|
3211
3389
|
if (await publish(local, room, bootstrapped.record, landing, live)) {
|
|
3212
3390
|
return live() ? null : unconfirmed;
|
|
3213
3391
|
}
|
|
3214
|
-
return reachedBy(room.locator, bootstrapped.record.roomVersion)
|
|
3392
|
+
return (await reachedBy(room.locator, bootstrapped.record.roomVersion))
|
|
3215
3393
|
? null
|
|
3216
3394
|
: advance(room, local, authorized, live);
|
|
3217
3395
|
}
|
|
@@ -3235,12 +3413,12 @@ export function composeRuntime(options) {
|
|
|
3235
3413
|
remodelled: refreshed.remodelled,
|
|
3236
3414
|
};
|
|
3237
3415
|
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3238
|
-
const now =
|
|
3416
|
+
const now = (await recordAt(room.locator))?.roomVersion;
|
|
3239
3417
|
const reached = now !== undefined &&
|
|
3240
3418
|
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
3241
3419
|
return reached ? null : 'behind';
|
|
3242
3420
|
}
|
|
3243
|
-
await retainModel(local, room,
|
|
3421
|
+
await retainModel(local, room, null);
|
|
3244
3422
|
return null;
|
|
3245
3423
|
}
|
|
3246
3424
|
return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
|
|
@@ -3248,6 +3426,11 @@ export function composeRuntime(options) {
|
|
|
3248
3426
|
async function withhold(withheld, room, local, progress) {
|
|
3249
3427
|
if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
|
|
3250
3428
|
return;
|
|
3429
|
+
if (withheld.kind === 'not-ready') {
|
|
3430
|
+
progress.complete = false;
|
|
3431
|
+
askAgain(room.locator);
|
|
3432
|
+
return;
|
|
3433
|
+
}
|
|
3251
3434
|
stale.add(room.locator);
|
|
3252
3435
|
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
3253
3436
|
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
@@ -3274,6 +3457,37 @@ export function composeRuntime(options) {
|
|
|
3274
3457
|
await refuse('revoked-authorization');
|
|
3275
3458
|
}
|
|
3276
3459
|
}
|
|
3460
|
+
function askAgain(locator) {
|
|
3461
|
+
const held = unready.get(locator) ?? { tries: 0, timer: null };
|
|
3462
|
+
unready.set(locator, held);
|
|
3463
|
+
if (held.timer !== null)
|
|
3464
|
+
return;
|
|
3465
|
+
const wait = NOT_READY_MS[Math.min(held.tries, NOT_READY_MS.length - 1)];
|
|
3466
|
+
held.tries += 1;
|
|
3467
|
+
held.timer = setTimeout(() => {
|
|
3468
|
+
held.timer = null;
|
|
3469
|
+
const room = discovered.get(locator);
|
|
3470
|
+
const unstreamed = options.adapter.roomStream === undefined;
|
|
3471
|
+
if (closed || stopping || (!unstreamed && !socketOpen) || room === undefined)
|
|
3472
|
+
return;
|
|
3473
|
+
const progress = { complete: true, reachable: true };
|
|
3474
|
+
void openLocal()
|
|
3475
|
+
.then((local) => roomTask(locator, (live) => loadRoom(room, local, progress, live)))
|
|
3476
|
+
.then(() => rest(progress), () => undefined);
|
|
3477
|
+
}, wait);
|
|
3478
|
+
}
|
|
3479
|
+
function readied(locator) {
|
|
3480
|
+
const held = unready.get(locator);
|
|
3481
|
+
if (held === undefined)
|
|
3482
|
+
return;
|
|
3483
|
+
if (held.timer !== null)
|
|
3484
|
+
clearTimeout(held.timer);
|
|
3485
|
+
unready.delete(locator);
|
|
3486
|
+
}
|
|
3487
|
+
function unreadied() {
|
|
3488
|
+
for (const locator of [...unready.keys()])
|
|
3489
|
+
readied(locator);
|
|
3490
|
+
}
|
|
3277
3491
|
function leaving(batch, result) {
|
|
3278
3492
|
return {
|
|
3279
3493
|
batch,
|
|
@@ -3299,7 +3513,8 @@ export function composeRuntime(options) {
|
|
|
3299
3513
|
async function acceptance(room, local, submitted, accepted) {
|
|
3300
3514
|
if (!continuing())
|
|
3301
3515
|
return unconfirmed;
|
|
3302
|
-
const
|
|
3516
|
+
const work = await workOf(room.locator);
|
|
3517
|
+
const current = work.replica;
|
|
3303
3518
|
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
3304
3519
|
return null;
|
|
3305
3520
|
}
|
|
@@ -3308,7 +3523,7 @@ export function composeRuntime(options) {
|
|
|
3308
3523
|
let record = current.record;
|
|
3309
3524
|
let changes;
|
|
3310
3525
|
if (!options.adapter.reached(room.locator, held, accepted.roomVersion) &&
|
|
3311
|
-
!
|
|
3526
|
+
!olderThan(room.locator, accepted.roomVersion, held)) {
|
|
3312
3527
|
const paths = [...returnedPaths(submitted, accepted)];
|
|
3313
3528
|
const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
|
|
3314
3529
|
const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion, composed(record)));
|
|
@@ -3320,13 +3535,13 @@ export function composeRuntime(options) {
|
|
|
3320
3535
|
};
|
|
3321
3536
|
changes = back;
|
|
3322
3537
|
}
|
|
3323
|
-
const settled = await settlement(local, room.locator,
|
|
3538
|
+
const settled = await settlement(local, room.locator, work, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
|
|
3539
|
+
const reached = record.roomVersion;
|
|
3324
3540
|
return {
|
|
3325
3541
|
...settled,
|
|
3326
3542
|
apply(answer) {
|
|
3327
3543
|
const set = settled.apply(answer);
|
|
3328
|
-
showing(room.locator, accepted.roomVersion);
|
|
3329
|
-
pendings.delete(pendingKey(room.locator, submitted.id));
|
|
3544
|
+
showing(room.locator, accepted.roomVersion, reached);
|
|
3330
3545
|
unreadable.delete(room.locator);
|
|
3331
3546
|
list(room, true);
|
|
3332
3547
|
return set;
|
|
@@ -3336,8 +3551,9 @@ export function composeRuntime(options) {
|
|
|
3336
3551
|
async function adoptedTogether(local, acceptances) {
|
|
3337
3552
|
const settlements = acceptances.filter(settling);
|
|
3338
3553
|
if (settlements.length > 0) {
|
|
3339
|
-
|
|
3340
|
-
moveTo(
|
|
3554
|
+
const set = await together(local, settlements);
|
|
3555
|
+
void moveTo(status);
|
|
3556
|
+
void publishChangeSet(set);
|
|
3341
3557
|
}
|
|
3342
3558
|
await unpair(local);
|
|
3343
3559
|
}
|
|
@@ -3353,7 +3569,7 @@ export function composeRuntime(options) {
|
|
|
3353
3569
|
const other = discovered.get(partner);
|
|
3354
3570
|
const failed = await exclusive(async () => {
|
|
3355
3571
|
const left = await acceptance(room, local, submitted, accepted);
|
|
3356
|
-
const arriving =
|
|
3572
|
+
const arriving = (await replicaOf(partner))?.queue.find((batch) => batch.id === submitted.id);
|
|
3357
3573
|
const arrived = other === undefined || arriving === undefined || accepted.destination === undefined
|
|
3358
3574
|
? null
|
|
3359
3575
|
: await acceptance(other, local, arriving, {
|
|
@@ -3366,25 +3582,24 @@ export function composeRuntime(options) {
|
|
|
3366
3582
|
submitLater(partner);
|
|
3367
3583
|
return failed === null;
|
|
3368
3584
|
}
|
|
3369
|
-
function
|
|
3370
|
-
|
|
3585
|
+
async function behindIn(locator, batch) {
|
|
3586
|
+
const local = await openLocal();
|
|
3587
|
+
const [order, { room }] = await Promise.all([
|
|
3588
|
+
local.order(locator),
|
|
3589
|
+
local.pending(locator, batch.id),
|
|
3590
|
+
]);
|
|
3591
|
+
return room ? order.indexOf(batch.id) > 0 : order.length > 0;
|
|
3371
3592
|
}
|
|
3372
|
-
function
|
|
3373
|
-
const
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
if (partner === null || skipped.has(pendingKey(locator, batch.id)))
|
|
3383
|
-
continue;
|
|
3384
|
-
if (queueIn(partner).some((held) => held.id === batch.id))
|
|
3385
|
-
continue;
|
|
3386
|
-
if (unpairedIn(locator, batch))
|
|
3387
|
-
return { locator, batch };
|
|
3593
|
+
async function unpaired(local, skipped) {
|
|
3594
|
+
const { queued, moving } = await local.moving();
|
|
3595
|
+
for (const { locator, batch, state } of moving) {
|
|
3596
|
+
const partner = partnerOf(batch, locator);
|
|
3597
|
+
if (partner === null || skipped.has(pendingKey(locator, batch.id)))
|
|
3598
|
+
continue;
|
|
3599
|
+
if (queued.get(partner)?.includes(batch.id) === true)
|
|
3600
|
+
continue;
|
|
3601
|
+
if (!leavingIn(batch, locator) || withdrawable(new Map([[batch.id, state]]))(batch)) {
|
|
3602
|
+
return { locator, batch };
|
|
3388
3603
|
}
|
|
3389
3604
|
}
|
|
3390
3605
|
return null;
|
|
@@ -3396,9 +3611,10 @@ export function composeRuntime(options) {
|
|
|
3396
3611
|
returned: !localSaves && leavingIn(batch, locator) ? returnedBy('withdrawn') : null,
|
|
3397
3612
|
}));
|
|
3398
3613
|
const outcome = localSaves ? 'dropped' : 'unpaired';
|
|
3399
|
-
const
|
|
3614
|
+
const work = await workOf(locator);
|
|
3615
|
+
const current = work.replica;
|
|
3400
3616
|
if (current !== undefined) {
|
|
3401
|
-
const settled = await settlement(local, locator,
|
|
3617
|
+
const settled = await settlement(local, locator, work, current.record, current.queue.filter((one) => !ids.has(one.id)), { leaving });
|
|
3402
3618
|
return {
|
|
3403
3619
|
...settled,
|
|
3404
3620
|
apply(answer) {
|
|
@@ -3410,22 +3626,22 @@ export function composeRuntime(options) {
|
|
|
3410
3626
|
},
|
|
3411
3627
|
};
|
|
3412
3628
|
}
|
|
3413
|
-
const queue =
|
|
3629
|
+
const queue = work.queue.filter((one) => !ids.has(one.id));
|
|
3414
3630
|
const written = new Map();
|
|
3415
3631
|
for (const { batch, returned } of leaving) {
|
|
3416
3632
|
if (returned === null)
|
|
3417
3633
|
continue;
|
|
3418
|
-
for (const saved of
|
|
3634
|
+
for (const saved of work.states.get(batch.id)?.drafts ?? []) {
|
|
3419
3635
|
written.set(saved.path, {
|
|
3420
3636
|
locator,
|
|
3421
3637
|
path: saved.path,
|
|
3422
3638
|
revision: roomDraftRevision(),
|
|
3423
|
-
parts: returnedInto((written.get(saved.path) ??
|
|
3639
|
+
parts: returnedInto((written.get(saved.path) ?? (await draftOf(locator, saved.path)))?.parts, saved.parts),
|
|
3424
3640
|
returned,
|
|
3425
3641
|
});
|
|
3426
3642
|
}
|
|
3427
3643
|
}
|
|
3428
|
-
const carried = carriedBack(locator, leaving.flatMap(({ batch, returned }) => (returned === null ? [] : [batch])), written);
|
|
3644
|
+
const carried = await carriedBack(locator, leaving.flatMap(({ batch, returned }) => (returned === null ? [] : [batch])), written);
|
|
3429
3645
|
return {
|
|
3430
3646
|
locator,
|
|
3431
3647
|
left: batches,
|
|
@@ -3435,6 +3651,7 @@ export function composeRuntime(options) {
|
|
|
3435
3651
|
{
|
|
3436
3652
|
locator,
|
|
3437
3653
|
queue,
|
|
3654
|
+
stored: work.queue,
|
|
3438
3655
|
roomDrafts: [...written.values()],
|
|
3439
3656
|
consumedRoomDrafts: carried.taken,
|
|
3440
3657
|
},
|
|
@@ -3442,17 +3659,8 @@ export function composeRuntime(options) {
|
|
|
3442
3659
|
],
|
|
3443
3660
|
},
|
|
3444
3661
|
apply() {
|
|
3445
|
-
for (const batch of batches)
|
|
3662
|
+
for (const batch of batches)
|
|
3446
3663
|
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
3447
|
-
pendings.delete(pendingKey(locator, batch.id));
|
|
3448
|
-
}
|
|
3449
|
-
if (queue.length > 0)
|
|
3450
|
-
queuesWithoutReplica.set(locator, queue);
|
|
3451
|
-
else
|
|
3452
|
-
queuesWithoutReplica.delete(locator);
|
|
3453
|
-
for (const record of written.values())
|
|
3454
|
-
rememberRoomDraft(record);
|
|
3455
|
-
carried.remember();
|
|
3456
3664
|
submitLater(locator);
|
|
3457
3665
|
return mergeChangeSets([
|
|
3458
3666
|
changeSet(locator, {
|
|
@@ -3466,28 +3674,33 @@ export function composeRuntime(options) {
|
|
|
3466
3674
|
}
|
|
3467
3675
|
async function unpair(local) {
|
|
3468
3676
|
const handled = new Set();
|
|
3469
|
-
for (let held = unpaired(handled); held !== null && continuing(); held = unpaired(handled)) {
|
|
3677
|
+
for (let held = await unpaired(local, handled); held !== null && continuing(); held = await unpaired(local, handled)) {
|
|
3470
3678
|
const { locator, batch } = held;
|
|
3471
3679
|
handled.add(pendingKey(locator, batch.id));
|
|
3472
3680
|
publishChangeSet(await together(local, [await unpairing(local, locator, [batch])]));
|
|
3473
3681
|
}
|
|
3474
3682
|
if (handled.size === 0)
|
|
3475
3683
|
return;
|
|
3476
|
-
moveTo(
|
|
3684
|
+
moveTo(status);
|
|
3477
3685
|
}
|
|
3478
3686
|
async function submitRoom(room, local, authorized, progress, signal) {
|
|
3479
3687
|
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
3480
|
-
const
|
|
3481
|
-
|
|
3482
|
-
|
|
3688
|
+
const headId = (await local.order(room.locator)).find((id) => !landedAhead.has(pendingKey(room.locator, id)));
|
|
3689
|
+
if (headId === undefined)
|
|
3690
|
+
return 'done';
|
|
3691
|
+
const [head, pending] = await Promise.all([
|
|
3692
|
+
local.batch(room.locator, headId),
|
|
3693
|
+
local.pending(room.locator, headId),
|
|
3694
|
+
]);
|
|
3695
|
+
if (head === null || !pending.room)
|
|
3483
3696
|
return 'done';
|
|
3484
|
-
const state =
|
|
3697
|
+
const { state } = pending;
|
|
3485
3698
|
const partner = partnerOf(head, room.locator);
|
|
3486
3699
|
if (partner !== null && !leavingIn(head, room.locator)) {
|
|
3487
3700
|
submitLater(partner);
|
|
3488
3701
|
return 'done';
|
|
3489
3702
|
}
|
|
3490
|
-
if (partner !== null && behindIn(partner, head)) {
|
|
3703
|
+
if (partner !== null && (await behindIn(partner, head))) {
|
|
3491
3704
|
progress.complete = false;
|
|
3492
3705
|
return 'done';
|
|
3493
3706
|
}
|
|
@@ -3508,7 +3721,7 @@ export function composeRuntime(options) {
|
|
|
3508
3721
|
let result;
|
|
3509
3722
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
3510
3723
|
await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
|
|
3511
|
-
if (discoveryPaused ||
|
|
3724
|
+
if (discoveryPaused || isFenced(room.locator))
|
|
3512
3725
|
return 'done';
|
|
3513
3726
|
if (signal.aborted || writesHeld()) {
|
|
3514
3727
|
await exclusive(() => mark(local, room.locator, head, submission, earlier));
|
|
@@ -3614,7 +3827,8 @@ export function composeRuntime(options) {
|
|
|
3614
3827
|
await exclusive(async () => {
|
|
3615
3828
|
if (!continuing())
|
|
3616
3829
|
return;
|
|
3617
|
-
const
|
|
3830
|
+
const work = await workOf(room.locator);
|
|
3831
|
+
const current = work.replica;
|
|
3618
3832
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
3619
3833
|
return;
|
|
3620
3834
|
}
|
|
@@ -3628,9 +3842,9 @@ export function composeRuntime(options) {
|
|
|
3628
3842
|
landedAhead.set(pendingKey(room.locator, head.id), Object.fromEntries([...touchedBy([head])].map((path) => [path, confirmedLayer.revisions[path]])));
|
|
3629
3843
|
return;
|
|
3630
3844
|
}
|
|
3631
|
-
publishChangeSet(await settle(local, room.locator,
|
|
3845
|
+
publishChangeSet(await settle(local, room.locator, work, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
|
|
3632
3846
|
await unpair(local);
|
|
3633
|
-
moveTo(
|
|
3847
|
+
moveTo(status);
|
|
3634
3848
|
});
|
|
3635
3849
|
const { there, here } = judged ?? (await refreshed());
|
|
3636
3850
|
if (other !== undefined && there !== null)
|
|
@@ -3665,24 +3879,24 @@ export function composeRuntime(options) {
|
|
|
3665
3879
|
release();
|
|
3666
3880
|
};
|
|
3667
3881
|
}
|
|
3668
|
-
function declaredAsHeld(room) {
|
|
3669
|
-
const held = replicas.get(room.locator)?.record;
|
|
3882
|
+
function declaredAsHeld(held, room) {
|
|
3670
3883
|
return (held === undefined ||
|
|
3671
3884
|
(JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
|
|
3672
3885
|
JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
|
|
3886
|
+
JSON.stringify(held.providerOwnedPaths) === JSON.stringify(room.providerOwnedPaths) &&
|
|
3673
3887
|
JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
|
|
3674
3888
|
}
|
|
3675
3889
|
async function keepNaming(local, room) {
|
|
3676
|
-
const held =
|
|
3890
|
+
const held = await recordAt(room.locator);
|
|
3677
3891
|
if (held === undefined || (held.name === room.name && held.handle === room.handle))
|
|
3678
3892
|
return;
|
|
3679
3893
|
await exclusive(async () => {
|
|
3680
3894
|
if (!continuing())
|
|
3681
3895
|
return;
|
|
3682
|
-
const
|
|
3683
|
-
if (
|
|
3896
|
+
const current = await recordAt(room.locator);
|
|
3897
|
+
if (current === undefined)
|
|
3684
3898
|
return;
|
|
3685
|
-
const record = { ...
|
|
3899
|
+
const record = { ...current, name: room.name, handle: room.handle };
|
|
3686
3900
|
await local.commit({
|
|
3687
3901
|
rooms: [
|
|
3688
3902
|
{
|
|
@@ -3693,36 +3907,38 @@ export function composeRuntime(options) {
|
|
|
3693
3907
|
},
|
|
3694
3908
|
],
|
|
3695
3909
|
});
|
|
3696
|
-
|
|
3910
|
+
emitRooms();
|
|
3697
3911
|
});
|
|
3698
3912
|
}
|
|
3699
|
-
async function
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3913
|
+
async function modelNow(locator) {
|
|
3914
|
+
return (await recordAt(locator))?.model ?? unheldModels.get(locator) ?? NO_MODEL;
|
|
3915
|
+
}
|
|
3916
|
+
async function retainModel(local, room, given) {
|
|
3917
|
+
const held = await recordAt(room.locator);
|
|
3918
|
+
const previous = held?.model ?? unheldModels.get(room.locator) ?? NO_MODEL;
|
|
3919
|
+
const next = given ?? previous;
|
|
3920
|
+
if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(held, room))
|
|
3703
3921
|
return;
|
|
3704
|
-
if (next.status === 'unavailable') {
|
|
3705
|
-
invalidModels.add(key);
|
|
3706
|
-
liveModels.delete(key);
|
|
3707
|
-
}
|
|
3708
3922
|
await exclusive(async () => {
|
|
3709
3923
|
if (!continuing())
|
|
3710
3924
|
return;
|
|
3711
|
-
const
|
|
3925
|
+
const work = await workOf(room.locator);
|
|
3926
|
+
const current = work.replica;
|
|
3712
3927
|
let changed = null;
|
|
3713
|
-
if (
|
|
3928
|
+
if (current !== undefined) {
|
|
3714
3929
|
const record = {
|
|
3715
|
-
...
|
|
3930
|
+
...current.record,
|
|
3716
3931
|
name: room.name,
|
|
3717
3932
|
handle: room.handle,
|
|
3718
3933
|
operations: room.operations,
|
|
3719
3934
|
replica: room.replica,
|
|
3935
|
+
providerOwnedPaths: room.providerOwnedPaths,
|
|
3720
3936
|
typedSelection: room.typedSelection ?? null,
|
|
3721
3937
|
model: next,
|
|
3722
3938
|
};
|
|
3723
|
-
const confirmedLayer = await sliceAt(record, 'confirmed',
|
|
3724
|
-
const replayedLayer = await replayed(record, confirmedLayer,
|
|
3725
|
-
const diff = await overlayDiff(record,
|
|
3939
|
+
const confirmedLayer = await sliceAt(record, 'confirmed', current.queue, current.overlaid);
|
|
3940
|
+
const replayedLayer = await replayed(record, confirmedLayer, current.queue);
|
|
3941
|
+
const diff = await overlayDiff(record, current, replayedLayer.visible, replayedLayer.touched);
|
|
3726
3942
|
const answer = await local.commit({
|
|
3727
3943
|
rooms: [
|
|
3728
3944
|
{
|
|
@@ -3735,28 +3951,21 @@ export function composeRuntime(options) {
|
|
|
3735
3951
|
},
|
|
3736
3952
|
],
|
|
3737
3953
|
});
|
|
3738
|
-
|
|
3739
|
-
replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
|
|
3954
|
+
preparedAfter(room.locator, answer);
|
|
3740
3955
|
changed = changeSet(room.locator, {
|
|
3741
3956
|
documents: answer.changed[room.locator] ?? [],
|
|
3742
3957
|
room: true,
|
|
3743
3958
|
});
|
|
3744
3959
|
}
|
|
3745
|
-
|
|
3960
|
+
else
|
|
3961
|
+
unheldModels.set(room.locator, next);
|
|
3962
|
+
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
3746
3963
|
if (changed === null)
|
|
3747
3964
|
emitRooms();
|
|
3748
3965
|
else
|
|
3749
3966
|
publishChangeSet(changed);
|
|
3750
3967
|
});
|
|
3751
3968
|
}
|
|
3752
|
-
function heldModel(room, next) {
|
|
3753
|
-
models.set(room.locator, next);
|
|
3754
|
-
if (next.status === 'current') {
|
|
3755
|
-
liveModels.set(room.locator, JSON.stringify(next.snapshot));
|
|
3756
|
-
invalidModels.delete(room.locator);
|
|
3757
|
-
}
|
|
3758
|
-
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
3759
|
-
}
|
|
3760
3969
|
async function scoped(result, room, local) {
|
|
3761
3970
|
if (result.scope === 'content' && !result.retryable) {
|
|
3762
3971
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
@@ -3777,7 +3986,7 @@ export function composeRuntime(options) {
|
|
|
3777
3986
|
else if (result.scope !== 'operation') {
|
|
3778
3987
|
unreadable.add(room.locator);
|
|
3779
3988
|
scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
|
|
3780
|
-
await retainModel(local, scopedAway,
|
|
3989
|
+
await retainModel(local, scopedAway, null);
|
|
3781
3990
|
}
|
|
3782
3991
|
if (!continuing())
|
|
3783
3992
|
return;
|
|
@@ -3785,42 +3994,55 @@ export function composeRuntime(options) {
|
|
|
3785
3994
|
emitRooms();
|
|
3786
3995
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
3787
3996
|
}
|
|
3788
|
-
function name(locator) {
|
|
3997
|
+
async function name(locator) {
|
|
3789
3998
|
const stream = options.adapter.roomStream;
|
|
3790
|
-
const record = replicas.get(locator)?.record;
|
|
3791
3999
|
const room = discovered.get(locator);
|
|
3792
|
-
if (stream === undefined ||
|
|
4000
|
+
if (stream === undefined || room === undefined || !socketOpen || !handed)
|
|
3793
4001
|
return;
|
|
3794
|
-
|
|
4002
|
+
const [held] = await heldRooms([room]);
|
|
4003
|
+
if (held === undefined || !socketOpen || !handed)
|
|
3795
4004
|
return;
|
|
3796
4005
|
naming += 1;
|
|
3797
|
-
stream.hold([
|
|
4006
|
+
stream.hold([held]);
|
|
3798
4007
|
}
|
|
3799
|
-
function heldRooms(listed) {
|
|
4008
|
+
async function heldRooms(listed) {
|
|
4009
|
+
const stored = await storedRooms(listed.map((room) => room.locator));
|
|
3800
4010
|
return listed.flatMap((room) => {
|
|
3801
|
-
const record =
|
|
3802
|
-
if (record === undefined
|
|
4011
|
+
const record = stored.get(room.locator)?.record;
|
|
4012
|
+
if (record === undefined) {
|
|
4013
|
+
return room.replica.admission === 'admitted'
|
|
4014
|
+
? [{ locator: room.locator, roomVersion: null }]
|
|
4015
|
+
: [];
|
|
4016
|
+
}
|
|
4017
|
+
if (record.replica.selection !== room.replica.selection)
|
|
3803
4018
|
return [];
|
|
3804
4019
|
return [{ locator: room.locator, roomVersion: record.roomVersion }];
|
|
3805
4020
|
});
|
|
3806
4021
|
}
|
|
3807
4022
|
async function synchronize(listed, local, progress, attempt, live) {
|
|
3808
|
-
const
|
|
4023
|
+
const heldNow = await storedRooms(listed.map(({ locator }) => locator));
|
|
4024
|
+
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress, heldNow.get(room.locator)?.record)));
|
|
3809
4025
|
if (!live())
|
|
3810
4026
|
return;
|
|
3811
4027
|
const ready = listed.filter((_, index) => {
|
|
3812
4028
|
const outcome = prepared[index];
|
|
3813
4029
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
3814
4030
|
});
|
|
4031
|
+
const held = await heldRooms(ready);
|
|
4032
|
+
if (!live())
|
|
4033
|
+
return;
|
|
3815
4034
|
if (attempt === 'opened') {
|
|
3816
4035
|
naming += 1;
|
|
3817
|
-
options.adapter.roomStream?.hold(
|
|
4036
|
+
options.adapter.roomStream?.hold(held);
|
|
3818
4037
|
handed = true;
|
|
3819
4038
|
}
|
|
4039
|
+
const kept = attempt === 'unstreamed'
|
|
4040
|
+
? new Set()
|
|
4041
|
+
: new Set((await storedRooms(ready.map(({ locator }) => locator))).keys());
|
|
3820
4042
|
for (const room of ready) {
|
|
3821
|
-
if (attempt
|
|
4043
|
+
if (attempt === 'opened' || kept.has(room.locator)) {
|
|
3822
4044
|
if (attempt === 'authorized')
|
|
3823
|
-
name(room.locator);
|
|
4045
|
+
await name(room.locator);
|
|
3824
4046
|
continue;
|
|
3825
4047
|
}
|
|
3826
4048
|
void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
|
|
@@ -3829,23 +4051,23 @@ export function composeRuntime(options) {
|
|
|
3829
4051
|
if (failed !== undefined)
|
|
3830
4052
|
throw failed.reason;
|
|
3831
4053
|
}
|
|
3832
|
-
async function prepareRoom(room, local, progress) {
|
|
4054
|
+
async function prepareRoom(room, local, progress, prior) {
|
|
3833
4055
|
if (closed || authorization === undefined) {
|
|
3834
4056
|
progress.complete = false;
|
|
3835
4057
|
return false;
|
|
3836
4058
|
}
|
|
3837
|
-
const prior = replicas.get(room.locator)?.record;
|
|
3838
4059
|
if (prior !== undefined && prior.replica.selection !== room.replica.selection) {
|
|
3839
|
-
|
|
4060
|
+
listHeld(room.locator);
|
|
3840
4061
|
emitRooms();
|
|
3841
4062
|
journal.record(`selection changed ${room.locator}`);
|
|
3842
4063
|
progress.complete = false;
|
|
3843
4064
|
return false;
|
|
3844
4065
|
}
|
|
3845
|
-
if (prior !== undefined)
|
|
3846
|
-
await retainModel(local, room,
|
|
3847
|
-
if (prior !== undefined)
|
|
4066
|
+
if (prior !== undefined && !declaredAsHeld(prior, room))
|
|
4067
|
+
await retainModel(local, room, null);
|
|
4068
|
+
if (prior !== undefined && (prior.name !== room.name || prior.handle !== room.handle)) {
|
|
3848
4069
|
await keepNaming(local, room);
|
|
4070
|
+
}
|
|
3849
4071
|
if (discoveryPaused || authorization === undefined)
|
|
3850
4072
|
return false;
|
|
3851
4073
|
return room.replica.admission === 'admitted';
|
|
@@ -3853,26 +4075,28 @@ export function composeRuntime(options) {
|
|
|
3853
4075
|
async function caughtUp(room, local, progress, live) {
|
|
3854
4076
|
if (options.adapter.roomStream !== undefined || authorization === undefined)
|
|
3855
4077
|
return;
|
|
3856
|
-
if (!live() || !behind(room))
|
|
4078
|
+
if (!live() || !(await behind(room)))
|
|
3857
4079
|
return;
|
|
3858
4080
|
const withheld = await advance(room, local, authorization, live);
|
|
3859
4081
|
if (withheld !== null)
|
|
3860
4082
|
await withhold(withheld, room, local, progress);
|
|
3861
4083
|
}
|
|
3862
|
-
function revoked(
|
|
3863
|
-
|
|
3864
|
-
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
4084
|
+
function revoked(model) {
|
|
4085
|
+
return model?.status === 'unavailable' && model.reason === 'revoked';
|
|
3865
4086
|
}
|
|
3866
|
-
async function loadRoom(room, local, progress, live) {
|
|
4087
|
+
async function loadRoom(room, local, progress, live, unheld) {
|
|
3867
4088
|
if (!live() || authorization === undefined)
|
|
3868
4089
|
return;
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
4090
|
+
const prior = unheld === undefined ? await recordAt(room.locator) : undefined;
|
|
4091
|
+
if (prior !== undefined) {
|
|
4092
|
+
if (!(await prepareRoom(room, local, progress, prior)))
|
|
4093
|
+
return;
|
|
4094
|
+
if (revoked(prior.model)) {
|
|
4095
|
+
await reload(room, local, progress, live);
|
|
4096
|
+
return;
|
|
4097
|
+
}
|
|
3874
4098
|
}
|
|
3875
|
-
const withheld = await advance(room, local, authorization, live);
|
|
4099
|
+
const withheld = await advance(room, local, authorization, live, unheld);
|
|
3876
4100
|
if (withheld !== null) {
|
|
3877
4101
|
await withhold(withheld, room, local, progress);
|
|
3878
4102
|
return;
|
|
@@ -3885,8 +4109,9 @@ export function composeRuntime(options) {
|
|
|
3885
4109
|
reloadsDue.delete(room.locator);
|
|
3886
4110
|
if (!live() || authorization === undefined)
|
|
3887
4111
|
return;
|
|
3888
|
-
const
|
|
3889
|
-
const
|
|
4112
|
+
const prior = await recordAt(room.locator);
|
|
4113
|
+
const from = standingAt(room.locator, prior);
|
|
4114
|
+
const bootstrapped = await pullBootstrap(authorization, room, local, live, prior);
|
|
3890
4115
|
if (bootstrapped.kind !== 'staged') {
|
|
3891
4116
|
if (live())
|
|
3892
4117
|
await withhold(bootstrapped, room, local, progress);
|
|
@@ -3910,7 +4135,7 @@ export function composeRuntime(options) {
|
|
|
3910
4135
|
function malformed(listed) {
|
|
3911
4136
|
for (const room of listed) {
|
|
3912
4137
|
if (room.operations === undefined ||
|
|
3913
|
-
!['write', '
|
|
4138
|
+
!['write', 'erase'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
3914
4139
|
room.operations[operation].every((scope) => typeof scope === 'string')) ||
|
|
3915
4140
|
room.replica === undefined ||
|
|
3916
4141
|
typeof room.replica.selection !== 'string' ||
|
|
@@ -3925,18 +4150,17 @@ export function composeRuntime(options) {
|
|
|
3925
4150
|
return null;
|
|
3926
4151
|
}
|
|
3927
4152
|
async function signIn(local, signedIn) {
|
|
3928
|
-
if (JSON.stringify(signedIn) === JSON.stringify(member))
|
|
4153
|
+
if (JSON.stringify(signedIn) === JSON.stringify((await local.facts()).member))
|
|
3929
4154
|
return;
|
|
3930
4155
|
await local.fact('member', signedIn);
|
|
3931
|
-
|
|
3932
|
-
emitMember();
|
|
4156
|
+
emitMember(signedIn);
|
|
3933
4157
|
}
|
|
3934
4158
|
async function discoverRooms(local, progress, attempt, live, found) {
|
|
3935
4159
|
if (closed || authorization === undefined) {
|
|
3936
4160
|
notifyReady();
|
|
3937
4161
|
return;
|
|
3938
4162
|
}
|
|
3939
|
-
moveTo(transferring(
|
|
4163
|
+
moveTo(transferring());
|
|
3940
4164
|
discoveries += 1;
|
|
3941
4165
|
const started = discoveries;
|
|
3942
4166
|
const result = found ?? (await asked(authorization));
|
|
@@ -3981,15 +4205,13 @@ export function composeRuntime(options) {
|
|
|
3981
4205
|
refusal = null;
|
|
3982
4206
|
emitRefusal();
|
|
3983
4207
|
}
|
|
3984
|
-
const
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
limits = result.providerLimits;
|
|
3990
|
-
await local.fact('providerLimits', limits);
|
|
3991
|
-
await local.fact('capabilities', capabilities);
|
|
4208
|
+
const held = await local.facts();
|
|
4209
|
+
if (JSON.stringify(result.providerLimits) !== JSON.stringify(held.providerLimits) ||
|
|
4210
|
+
JSON.stringify(result.capabilities) !== JSON.stringify(held.capabilities)) {
|
|
4211
|
+
await local.fact('providerLimits', result.providerLimits);
|
|
4212
|
+
await local.fact('capabilities', result.capabilities);
|
|
3992
4213
|
}
|
|
4214
|
+
notifyReady();
|
|
3993
4215
|
await signIn(local, result.member ?? null);
|
|
3994
4216
|
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
3995
4217
|
}
|
|
@@ -4072,7 +4294,7 @@ export function composeRuntime(options) {
|
|
|
4072
4294
|
streamed = progress;
|
|
4073
4295
|
complete = false;
|
|
4074
4296
|
recorded = false;
|
|
4075
|
-
moveTo(transferring(
|
|
4297
|
+
moveTo(transferring());
|
|
4076
4298
|
try {
|
|
4077
4299
|
const local = await openLocal();
|
|
4078
4300
|
if (!live())
|
|
@@ -4107,7 +4329,7 @@ export function composeRuntime(options) {
|
|
|
4107
4329
|
if (!live() || !announced)
|
|
4108
4330
|
return;
|
|
4109
4331
|
const progress = { complete: true, reachable: true };
|
|
4110
|
-
moveTo(transferring(
|
|
4332
|
+
moveTo(transferring());
|
|
4111
4333
|
try {
|
|
4112
4334
|
const local = await openLocal();
|
|
4113
4335
|
if (!live())
|
|
@@ -4125,6 +4347,7 @@ export function composeRuntime(options) {
|
|
|
4125
4347
|
authorityChanges += 1;
|
|
4126
4348
|
for (const listener of authorityChangeListeners)
|
|
4127
4349
|
listener(authorityChanges);
|
|
4350
|
+
lastAnswers.again();
|
|
4128
4351
|
}
|
|
4129
4352
|
function changeAuthority(change) {
|
|
4130
4353
|
return task(OPENING_LANE, async (live) => {
|
|
@@ -4132,7 +4355,7 @@ export function composeRuntime(options) {
|
|
|
4132
4355
|
return;
|
|
4133
4356
|
countAuthorityChange();
|
|
4134
4357
|
const progress = { complete: true, reachable: true };
|
|
4135
|
-
moveTo(transferring(
|
|
4358
|
+
moveTo(transferring());
|
|
4136
4359
|
try {
|
|
4137
4360
|
const local = await openLocal();
|
|
4138
4361
|
if (!live())
|
|
@@ -4167,6 +4390,7 @@ export function composeRuntime(options) {
|
|
|
4167
4390
|
missedChange ||= authorityChanged;
|
|
4168
4391
|
handed = false;
|
|
4169
4392
|
naming = 0;
|
|
4393
|
+
unreadied();
|
|
4170
4394
|
connectionLost();
|
|
4171
4395
|
if (lost)
|
|
4172
4396
|
conversations.lost();
|
|
@@ -4194,8 +4418,12 @@ export function composeRuntime(options) {
|
|
|
4194
4418
|
await rest(null, live);
|
|
4195
4419
|
});
|
|
4196
4420
|
}
|
|
4197
|
-
function cachedModels(local) {
|
|
4198
|
-
|
|
4421
|
+
async function cachedModels(local) {
|
|
4422
|
+
const held = await storedRooms();
|
|
4423
|
+
await settleModels(listedLocators(held).flatMap((locator) => {
|
|
4424
|
+
const room = discovered.get(locator) ?? declarationOf(locator, held.get(locator))?.room;
|
|
4425
|
+
return room === undefined ? [] : [room];
|
|
4426
|
+
}), 'cached', local);
|
|
4199
4427
|
}
|
|
4200
4428
|
function caughtUpRooms(expired) {
|
|
4201
4429
|
const progress = underway;
|
|
@@ -4203,8 +4431,12 @@ export function composeRuntime(options) {
|
|
|
4203
4431
|
naming = Math.max(0, naming - 1);
|
|
4204
4432
|
const cycle = progress ?? { complete: true, reachable: true };
|
|
4205
4433
|
replaced(expired);
|
|
4206
|
-
const
|
|
4207
|
-
const
|
|
4434
|
+
const installing = new Set(installs);
|
|
4435
|
+
const held = progress === null ? storedRooms(expired) : storedRooms();
|
|
4436
|
+
const reloading = progress === null
|
|
4437
|
+
? Promise.resolve([])
|
|
4438
|
+
: held.then((stored) => owedReloads(expired, stored));
|
|
4439
|
+
const replacing = Promise.all([held, reloading]).then(([stored, owed]) => reloadNow((room) => expired.includes(room.locator) || owed.includes(room.locator), cycle, stored, installing));
|
|
4208
4440
|
return task(OPENING_LANE, async (live) => {
|
|
4209
4441
|
if (!live())
|
|
4210
4442
|
return;
|
|
@@ -4217,9 +4449,11 @@ export function composeRuntime(options) {
|
|
|
4217
4449
|
const local = await openLocal();
|
|
4218
4450
|
await replacing;
|
|
4219
4451
|
if (progress.reachable) {
|
|
4220
|
-
|
|
4452
|
+
const stored = await held;
|
|
4453
|
+
const owed = await reloading;
|
|
4454
|
+
await settleModels([...discovered.values()].filter((room) => stored.has(room.locator) &&
|
|
4221
4455
|
!expired.includes(room.locator) &&
|
|
4222
|
-
!
|
|
4456
|
+
!owed.includes(room.locator)), 'current', local);
|
|
4223
4457
|
}
|
|
4224
4458
|
}
|
|
4225
4459
|
catch {
|
|
@@ -4230,24 +4464,34 @@ export function composeRuntime(options) {
|
|
|
4230
4464
|
await finishOpening(progress, live);
|
|
4231
4465
|
});
|
|
4232
4466
|
}
|
|
4233
|
-
function owedReloads(expired) {
|
|
4467
|
+
function owedReloads(expired, held) {
|
|
4234
4468
|
const reloading = [...discovered.values()]
|
|
4235
|
-
.filter((room) =>
|
|
4469
|
+
.filter((room) => !expired.includes(room.locator) && revoked(held.get(room.locator)?.record.model))
|
|
4236
4470
|
.map((room) => room.locator);
|
|
4237
4471
|
for (const locator of reloading)
|
|
4238
4472
|
catching.add(locator);
|
|
4239
|
-
|
|
4473
|
+
recoverReads();
|
|
4240
4474
|
return reloading;
|
|
4241
4475
|
}
|
|
4242
|
-
async function reloadNow(replaced, progress) {
|
|
4243
|
-
const reloaded = [...discovered.values()].filter((room) =>
|
|
4476
|
+
async function reloadNow(replaced, progress, held, installing) {
|
|
4477
|
+
const reloaded = [...discovered.values()].filter((room) => held.has(room.locator) && !installing.has(room.locator) && replaced(room));
|
|
4478
|
+
const acquired = [...discovered.values()].filter((room) => (!held.has(room.locator) || installing.has(room.locator)) &&
|
|
4479
|
+
room.replica.admission === 'admitted' &&
|
|
4480
|
+
replaced(room));
|
|
4244
4481
|
for (const room of reloaded)
|
|
4245
4482
|
reloadsDue.add(room.locator);
|
|
4246
4483
|
try {
|
|
4247
4484
|
const local = await openLocal();
|
|
4485
|
+
const kept = acquired.length === 0 ? new Set() : (await local.held()).staged;
|
|
4248
4486
|
for (const room of reloaded) {
|
|
4249
4487
|
void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
|
|
4250
4488
|
}
|
|
4489
|
+
for (const room of acquired) {
|
|
4490
|
+
const unheld = installing.has(room.locator)
|
|
4491
|
+
? undefined
|
|
4492
|
+
: { staged: kept.has(room.locator) };
|
|
4493
|
+
void roomTask(room.locator, (live) => loadRoom(room, local, progress, live, unheld)).then(() => rest(progress));
|
|
4494
|
+
}
|
|
4251
4495
|
}
|
|
4252
4496
|
catch {
|
|
4253
4497
|
for (const room of reloaded)
|
|
@@ -4256,29 +4500,56 @@ export function composeRuntime(options) {
|
|
|
4256
4500
|
}
|
|
4257
4501
|
}
|
|
4258
4502
|
async function settleModels(held, status, local) {
|
|
4259
|
-
const owed = (
|
|
4260
|
-
|
|
4261
|
-
return model.status !== 'unavailable' && model.status !== status;
|
|
4262
|
-
};
|
|
4263
|
-
const settled = await exclusive(async () => {
|
|
4503
|
+
const owed = (model) => model.status !== 'unavailable' && model.status !== status;
|
|
4504
|
+
const declaredAnew = await exclusive(async () => {
|
|
4264
4505
|
if (!continuing())
|
|
4265
4506
|
return [];
|
|
4266
|
-
const
|
|
4507
|
+
const stored = await storedRooms(held.map(({ locator }) => locator));
|
|
4508
|
+
const unchanged = held.filter((room) => {
|
|
4509
|
+
const record = stored.get(room.locator)?.record;
|
|
4510
|
+
return (owed(record?.model ?? unheldModels.get(room.locator) ?? NO_MODEL) &&
|
|
4511
|
+
declaredAsHeld(record, room));
|
|
4512
|
+
});
|
|
4513
|
+
const records = unchanged.flatMap((room) => {
|
|
4514
|
+
const record = stored.get(room.locator)?.record;
|
|
4515
|
+
return record === undefined
|
|
4516
|
+
? []
|
|
4517
|
+
: [{ ...record, model: { ...record.model, status } }];
|
|
4518
|
+
});
|
|
4519
|
+
if (records.length > 0) {
|
|
4520
|
+
try {
|
|
4521
|
+
await local.commit({
|
|
4522
|
+
rooms: records.map((record) => ({
|
|
4523
|
+
locator: record.locator,
|
|
4524
|
+
key: record.key,
|
|
4525
|
+
record: storedRecord(record),
|
|
4526
|
+
composed: composedOf(composed(record)),
|
|
4527
|
+
})),
|
|
4528
|
+
});
|
|
4529
|
+
}
|
|
4530
|
+
catch {
|
|
4531
|
+
journal.record(`model ${status} persistence failed`);
|
|
4532
|
+
}
|
|
4533
|
+
}
|
|
4267
4534
|
for (const room of unchanged) {
|
|
4268
|
-
const
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4535
|
+
const unheld = unheldModels.get(room.locator);
|
|
4536
|
+
if (!stored.has(room.locator) && unheld !== undefined) {
|
|
4537
|
+
unheldModels.set(room.locator, { ...unheld, status });
|
|
4538
|
+
}
|
|
4539
|
+
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
4273
4540
|
}
|
|
4274
4541
|
if (unchanged.length > 0)
|
|
4275
4542
|
emitRooms();
|
|
4276
|
-
return
|
|
4543
|
+
return held.filter((room) => {
|
|
4544
|
+
const record = stored.get(room.locator)?.record;
|
|
4545
|
+
return (!unchanged.includes(room) &&
|
|
4546
|
+
owed(record?.model ?? unheldModels.get(room.locator) ?? NO_MODEL));
|
|
4547
|
+
});
|
|
4277
4548
|
});
|
|
4278
|
-
for (const room of
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4549
|
+
for (const room of declaredAnew) {
|
|
4550
|
+
const model = await modelNow(room.locator);
|
|
4551
|
+
if (owed(model))
|
|
4552
|
+
await retainModel(local, room, { ...model, status });
|
|
4282
4553
|
}
|
|
4283
4554
|
}
|
|
4284
4555
|
function submitLater(locator) {
|
|
@@ -4341,7 +4612,7 @@ export function composeRuntime(options) {
|
|
|
4341
4612
|
return 'done';
|
|
4342
4613
|
await rest(progress);
|
|
4343
4614
|
if (!stale.has(locator)) {
|
|
4344
|
-
if ((
|
|
4615
|
+
if ((await (await openLocal()).order(locator)).length > 0)
|
|
4345
4616
|
submitLater(locator);
|
|
4346
4617
|
return 'done';
|
|
4347
4618
|
}
|
|
@@ -4369,9 +4640,11 @@ export function composeRuntime(options) {
|
|
|
4369
4640
|
return 'done';
|
|
4370
4641
|
return reading.reachable && reading.complete ? 'done' : 'again';
|
|
4371
4642
|
}
|
|
4372
|
-
function submitAll() {
|
|
4373
|
-
|
|
4374
|
-
|
|
4643
|
+
async function submitAll() {
|
|
4644
|
+
if (localSaves)
|
|
4645
|
+
return;
|
|
4646
|
+
for (const locator of await (await openLocal()).queued()) {
|
|
4647
|
+
if (!isFenced(locator))
|
|
4375
4648
|
submitLater(locator);
|
|
4376
4649
|
}
|
|
4377
4650
|
}
|
|
@@ -4381,7 +4654,7 @@ export function composeRuntime(options) {
|
|
|
4381
4654
|
progress.complete = false;
|
|
4382
4655
|
return;
|
|
4383
4656
|
}
|
|
4384
|
-
submitAll();
|
|
4657
|
+
await submitAll();
|
|
4385
4658
|
for (const locator of stale)
|
|
4386
4659
|
if (repairable.has(locator))
|
|
4387
4660
|
repairLater(locator, {});
|
|
@@ -4410,8 +4683,14 @@ export function composeRuntime(options) {
|
|
|
4410
4683
|
async function rest(progress, live = () => !closed) {
|
|
4411
4684
|
if (!live())
|
|
4412
4685
|
return;
|
|
4413
|
-
|
|
4414
|
-
let synced =
|
|
4686
|
+
const settled = () => complete && roomWork === 0 && naming === 0 && stale.size === 0;
|
|
4687
|
+
let synced = settled();
|
|
4688
|
+
if (synced && !recorded) {
|
|
4689
|
+
const queued = await queuedWork().catch(() => true);
|
|
4690
|
+
if (!live())
|
|
4691
|
+
return;
|
|
4692
|
+
synced = !queued && settled();
|
|
4693
|
+
}
|
|
4415
4694
|
if (synced && !recorded) {
|
|
4416
4695
|
const completedAt = new Date().toISOString();
|
|
4417
4696
|
try {
|
|
@@ -4419,21 +4698,15 @@ export function composeRuntime(options) {
|
|
|
4419
4698
|
if (!live())
|
|
4420
4699
|
return;
|
|
4421
4700
|
recorded = true;
|
|
4422
|
-
lastCompleted = completedAt;
|
|
4423
4701
|
}
|
|
4424
4702
|
catch {
|
|
4425
4703
|
synced = false;
|
|
4426
4704
|
}
|
|
4427
4705
|
}
|
|
4428
4706
|
moveTo({
|
|
4429
|
-
state:
|
|
4430
|
-
? 'pending'
|
|
4431
|
-
: synced && complete && roomWork === 0 && naming === 0
|
|
4432
|
-
? 'synced'
|
|
4433
|
-
: 'syncing',
|
|
4434
|
-
lastCompleted,
|
|
4707
|
+
state: synced && complete && roomWork === 0 && naming === 0 ? 'synced' : 'syncing',
|
|
4435
4708
|
providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
|
|
4436
|
-
|
|
4709
|
+
resting: true,
|
|
4437
4710
|
});
|
|
4438
4711
|
}
|
|
4439
4712
|
function applyRoom(change) {
|
|
@@ -4449,22 +4722,26 @@ export function composeRuntime(options) {
|
|
|
4449
4722
|
if (!live())
|
|
4450
4723
|
return null;
|
|
4451
4724
|
const room = discovered.get(locator);
|
|
4452
|
-
|
|
4453
|
-
if (room === undefined ||
|
|
4454
|
-
held === undefined ||
|
|
4455
|
-
room.replica.admission !== 'admitted' ||
|
|
4456
|
-
held.record.replica.selection !== room.replica.selection) {
|
|
4725
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
4457
4726
|
return null;
|
|
4727
|
+
const held = await recordAt(locator);
|
|
4728
|
+
if (held === undefined) {
|
|
4729
|
+
if (change.from !== null)
|
|
4730
|
+
return null;
|
|
4731
|
+
await installCarried(room, local, change, live, progress);
|
|
4732
|
+
return progress;
|
|
4458
4733
|
}
|
|
4459
|
-
|
|
4734
|
+
if (held.replica.selection !== room.replica.selection)
|
|
4735
|
+
return null;
|
|
4736
|
+
const at = held.roomVersion;
|
|
4460
4737
|
if (options.adapter.reached(locator, at, change.roomVersion))
|
|
4461
4738
|
return progress;
|
|
4462
|
-
if (!options.adapter.reached(locator, at, change.from)) {
|
|
4463
|
-
name(locator);
|
|
4739
|
+
if (change.from === null || !options.adapter.reached(locator, at, change.from)) {
|
|
4740
|
+
await name(locator);
|
|
4464
4741
|
return progress;
|
|
4465
4742
|
}
|
|
4466
4743
|
if (underway === null)
|
|
4467
|
-
moveTo(transferring(
|
|
4744
|
+
moveTo(transferring());
|
|
4468
4745
|
const writing = () => live() && !full;
|
|
4469
4746
|
const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, () => change.read()));
|
|
4470
4747
|
if (refreshed.kind !== 'advanced')
|
|
@@ -4476,19 +4753,52 @@ export function composeRuntime(options) {
|
|
|
4476
4753
|
};
|
|
4477
4754
|
if (await publish(local, room, refreshed.record, landing, writing)) {
|
|
4478
4755
|
stale.delete(locator);
|
|
4479
|
-
|
|
4756
|
+
if (!declaredAsHeld(held, room))
|
|
4757
|
+
await retainModel(local, room, null);
|
|
4480
4758
|
return progress;
|
|
4481
4759
|
}
|
|
4482
4760
|
}
|
|
4483
4761
|
}
|
|
4484
4762
|
catch {
|
|
4485
4763
|
if (!stale.has(locator))
|
|
4486
|
-
name(locator);
|
|
4764
|
+
void name(locator);
|
|
4487
4765
|
stale.add(locator);
|
|
4488
4766
|
return progress;
|
|
4489
4767
|
}
|
|
4490
4768
|
}).then((progress) => rest(progress), () => undefined);
|
|
4491
4769
|
}
|
|
4770
|
+
async function installCarried(room, local, change, live, progress) {
|
|
4771
|
+
if (authorization === undefined)
|
|
4772
|
+
return;
|
|
4773
|
+
if (underway === null)
|
|
4774
|
+
moveTo(transferring());
|
|
4775
|
+
const prior = await recordAt(room.locator);
|
|
4776
|
+
const from = standingAt(room.locator, prior);
|
|
4777
|
+
const page = await change.read();
|
|
4778
|
+
const carried = {
|
|
4779
|
+
bootstrap: () => Promise.resolve({
|
|
4780
|
+
kind: 'bootstrap',
|
|
4781
|
+
roomVersion: page.roomVersion,
|
|
4782
|
+
...(page.models === undefined ? {} : { models: page.models }),
|
|
4783
|
+
documents: page.documents,
|
|
4784
|
+
facts: page.facts,
|
|
4785
|
+
revisions: page.revisions,
|
|
4786
|
+
...(page.fileIds === undefined ? {} : { fileIds: page.fileIds }),
|
|
4787
|
+
...(page.created === undefined ? {} : { created: page.created }),
|
|
4788
|
+
changes: page.changes,
|
|
4789
|
+
binaryFiles: page.binaryFiles,
|
|
4790
|
+
...(page.folders === undefined ? {} : { folders: page.folders }),
|
|
4791
|
+
}),
|
|
4792
|
+
reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
|
|
4793
|
+
};
|
|
4794
|
+
const bootstrapped = await pullBootstrap(authorization, room, local, live, prior, carried);
|
|
4795
|
+
if (bootstrapped.kind !== 'staged') {
|
|
4796
|
+
if (live())
|
|
4797
|
+
await withhold(bootstrapped, room, local, progress);
|
|
4798
|
+
return;
|
|
4799
|
+
}
|
|
4800
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
|
|
4801
|
+
}
|
|
4492
4802
|
function observeStream() {
|
|
4493
4803
|
const stream = options.adapter.roomStream;
|
|
4494
4804
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -4504,6 +4814,7 @@ export function composeRuntime(options) {
|
|
|
4504
4814
|
connectionLive();
|
|
4505
4815
|
generation += 1;
|
|
4506
4816
|
conversations.reopened();
|
|
4817
|
+
lastAnswers.reopened();
|
|
4507
4818
|
recovery.resume();
|
|
4508
4819
|
}
|
|
4509
4820
|
else if (event.kind === 'discovered') {
|
|
@@ -4520,8 +4831,11 @@ export function composeRuntime(options) {
|
|
|
4520
4831
|
closedSocket(event.revoked, event.lost, event.authorityChanged);
|
|
4521
4832
|
else if (event.kind === 'caught-up')
|
|
4522
4833
|
void caughtUpRooms(event.expired);
|
|
4523
|
-
else if (event.kind === 'authority-changed')
|
|
4834
|
+
else if (event.kind === 'authority-changed') {
|
|
4835
|
+
if (announced && socketOpen)
|
|
4836
|
+
fenceLive(event.left);
|
|
4524
4837
|
void changeAuthority(event);
|
|
4838
|
+
}
|
|
4525
4839
|
else
|
|
4526
4840
|
void applyRoom(event);
|
|
4527
4841
|
});
|
|
@@ -4554,10 +4868,7 @@ export function composeRuntime(options) {
|
|
|
4554
4868
|
withdrawConversations = stream.observe((event) => {
|
|
4555
4869
|
if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
|
|
4556
4870
|
queueReadiness();
|
|
4557
|
-
|
|
4558
|
-
void ready.then(scheduleCurrency);
|
|
4559
|
-
return;
|
|
4560
|
-
}
|
|
4871
|
+
conversations.receive(event);
|
|
4561
4872
|
if (event.kind !== 'gap' || conversations.current())
|
|
4562
4873
|
return;
|
|
4563
4874
|
show();
|
|
@@ -4581,7 +4892,7 @@ export function composeRuntime(options) {
|
|
|
4581
4892
|
return;
|
|
4582
4893
|
}
|
|
4583
4894
|
const progress = { complete: false, reachable: !status.providerUnreachable };
|
|
4584
|
-
moveTo(transferring(
|
|
4895
|
+
moveTo(transferring());
|
|
4585
4896
|
try {
|
|
4586
4897
|
await conversations.catchUp(progress);
|
|
4587
4898
|
}
|
|
@@ -4595,6 +4906,7 @@ export function composeRuntime(options) {
|
|
|
4595
4906
|
return;
|
|
4596
4907
|
closed = true;
|
|
4597
4908
|
notifyDiscovered();
|
|
4909
|
+
unreadied();
|
|
4598
4910
|
if (probing !== null)
|
|
4599
4911
|
clearInterval(probing);
|
|
4600
4912
|
connectionLive();
|
|
@@ -4616,12 +4928,14 @@ export function composeRuntime(options) {
|
|
|
4616
4928
|
memberListeners.clear();
|
|
4617
4929
|
authorityChangeListeners.clear();
|
|
4618
4930
|
liveQueries.close();
|
|
4931
|
+
figures.close();
|
|
4619
4932
|
queueSubscriptions.clear();
|
|
4620
4933
|
roomDraftListeners.clear();
|
|
4621
4934
|
presentationListeners.clear();
|
|
4622
4935
|
}
|
|
4623
4936
|
async function stop() {
|
|
4624
4937
|
stopping = true;
|
|
4938
|
+
unreadied();
|
|
4625
4939
|
inFlight.abort();
|
|
4626
4940
|
await recovery.retire();
|
|
4627
4941
|
await Promise.all(working);
|
|
@@ -4634,16 +4948,18 @@ export function composeRuntime(options) {
|
|
|
4634
4948
|
if (options.signal?.aborted)
|
|
4635
4949
|
close();
|
|
4636
4950
|
observeStream();
|
|
4637
|
-
void openLocal()
|
|
4638
|
-
|
|
4951
|
+
void openLocal()
|
|
4952
|
+
.then(async (local) => {
|
|
4953
|
+
if ((await local.facts()).capabilities.length > 0)
|
|
4639
4954
|
readyFromStore();
|
|
4640
|
-
}
|
|
4955
|
+
})
|
|
4956
|
+
.catch(() => {
|
|
4641
4957
|
readyFromStore();
|
|
4642
4958
|
});
|
|
4643
4959
|
if (options.adapter.roomStream === undefined)
|
|
4644
4960
|
void sync('unstreamed', null);
|
|
4645
|
-
function planned(input, reach) {
|
|
4646
|
-
const plan = planWithin(input, roomFacts(reach), planReach(reach));
|
|
4961
|
+
async function planned(input, reach) {
|
|
4962
|
+
const plan = planWithin(input, await roomFacts(reach), planReach(reach));
|
|
4647
4963
|
const named = plan.paths !== null &&
|
|
4648
4964
|
plan.rooms !== null &&
|
|
4649
4965
|
plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
|
|
@@ -4662,10 +4978,10 @@ export function composeRuntime(options) {
|
|
|
4662
4978
|
const watchers = seedWatchers.get(plan.key) ?? new Set();
|
|
4663
4979
|
seedWatchers.set(plan.key, watchers);
|
|
4664
4980
|
watchers.add(shown);
|
|
4665
|
-
const stop = liveQueries.watch(plan, identityOf(plan), () => {
|
|
4666
|
-
const cover = covered(plan);
|
|
4981
|
+
const stop = liveQueries.watch(plan, identityOf(plan), (held) => {
|
|
4982
|
+
const cover = covered(plan, held);
|
|
4667
4983
|
awaitPreparation(cover);
|
|
4668
|
-
return seedBehind(plan, cover);
|
|
4984
|
+
return seedBehind(plan, cover, held);
|
|
4669
4985
|
}, (message) => {
|
|
4670
4986
|
const answered = message;
|
|
4671
4987
|
pending = whole(answered) && 'pending' in answered.result;
|
|
@@ -4683,6 +4999,21 @@ export function composeRuntime(options) {
|
|
|
4683
4999
|
stop();
|
|
4684
5000
|
};
|
|
4685
5001
|
}
|
|
5002
|
+
function watchPlanned(planning, deliver, fail) {
|
|
5003
|
+
let stop = null;
|
|
5004
|
+
let stopped = false;
|
|
5005
|
+
planning.then((plan) => {
|
|
5006
|
+
if (!stopped)
|
|
5007
|
+
stop = watchPlan(plan, deliver, fail);
|
|
5008
|
+
}, (error) => {
|
|
5009
|
+
if (!stopped)
|
|
5010
|
+
fail(error);
|
|
5011
|
+
});
|
|
5012
|
+
return () => {
|
|
5013
|
+
stopped = true;
|
|
5014
|
+
stop?.();
|
|
5015
|
+
};
|
|
5016
|
+
}
|
|
4686
5017
|
async function searched(input, reach) {
|
|
4687
5018
|
const asked = searchPlanOf(input);
|
|
4688
5019
|
await ready;
|
|
@@ -4695,7 +5026,7 @@ export function composeRuntime(options) {
|
|
|
4695
5026
|
retryable: false,
|
|
4696
5027
|
};
|
|
4697
5028
|
}
|
|
4698
|
-
if (closed || refusal !== null ||
|
|
5029
|
+
if (closed || refusal !== null || (await connectionNow()) !== 'connected')
|
|
4699
5030
|
return OFFLINE;
|
|
4700
5031
|
const reached = reach === null ? null : new Set(reach.rooms);
|
|
4701
5032
|
const named = asked.rooms === null
|
|
@@ -4703,7 +5034,8 @@ export function composeRuntime(options) {
|
|
|
4703
5034
|
? null
|
|
4704
5035
|
: [...reached]
|
|
4705
5036
|
: asked.rooms.filter((room) => reached === null || reached.has(room));
|
|
4706
|
-
const
|
|
5037
|
+
const held = named === null ? null : await storedRooms(named);
|
|
5038
|
+
const rooms = named === null ? null : named.filter((room) => held.has(room)).sort();
|
|
4707
5039
|
if (rooms !== null && rooms.length === 0)
|
|
4708
5040
|
return { kind: 'found', documents: [] };
|
|
4709
5041
|
try {
|
|
@@ -4741,18 +5073,15 @@ export function composeRuntime(options) {
|
|
|
4741
5073
|
roomDraftListeners.add(listener);
|
|
4742
5074
|
void exclusive(async () => {
|
|
4743
5075
|
await openLocal();
|
|
4744
|
-
}).then(() => {
|
|
5076
|
+
}).then(() => publication(async () => {
|
|
4745
5077
|
if (closed || !roomDraftListeners.has(listener))
|
|
4746
5078
|
return;
|
|
4747
|
-
const
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
listener.notify(value);
|
|
4754
|
-
});
|
|
4755
|
-
}, () => { });
|
|
5079
|
+
const value = await snapshotRoomDrafts();
|
|
5080
|
+
if (closed || !roomDraftListeners.has(listener))
|
|
5081
|
+
return;
|
|
5082
|
+
listener.ready = true;
|
|
5083
|
+
listener.notify(value);
|
|
5084
|
+
}), () => { });
|
|
4756
5085
|
return () => {
|
|
4757
5086
|
roomDraftListeners.delete(listener);
|
|
4758
5087
|
};
|
|
@@ -4764,6 +5093,10 @@ export function composeRuntime(options) {
|
|
|
4764
5093
|
return conversations.watchList(deliver);
|
|
4765
5094
|
if (topic === 'agents')
|
|
4766
5095
|
return conversations.watchAgents(deliver);
|
|
5096
|
+
if (topic === 'figures')
|
|
5097
|
+
return figures.watch(deliver, fail);
|
|
5098
|
+
if (topic === 'read')
|
|
5099
|
+
return lastAnswers.watch(input[0], input[1], deliver);
|
|
4767
5100
|
if (topic === 'presentation') {
|
|
4768
5101
|
const listener = {
|
|
4769
5102
|
app: input[0],
|
|
@@ -4772,13 +5105,12 @@ export function composeRuntime(options) {
|
|
|
4772
5105
|
};
|
|
4773
5106
|
presentationListeners.add(listener);
|
|
4774
5107
|
void exclusive(async () => {
|
|
4775
|
-
await
|
|
4776
|
-
}).then(() => {
|
|
5108
|
+
const value = await presentationOf(listener.app);
|
|
4777
5109
|
if (closed || !presentationListeners.has(listener))
|
|
4778
5110
|
return;
|
|
4779
5111
|
listener.ready = true;
|
|
4780
|
-
listener.notify(
|
|
4781
|
-
}
|
|
5112
|
+
listener.notify(value);
|
|
5113
|
+
}).catch(() => { });
|
|
4782
5114
|
return () => {
|
|
4783
5115
|
presentationListeners.delete(listener);
|
|
4784
5116
|
};
|
|
@@ -4792,6 +5124,8 @@ export function composeRuntime(options) {
|
|
|
4792
5124
|
return () => {
|
|
4793
5125
|
unlistedRooms.delete(deliver);
|
|
4794
5126
|
roomListeners.delete(deliver);
|
|
5127
|
+
if (roomListeners.size === 0)
|
|
5128
|
+
roomsShown = null;
|
|
4795
5129
|
};
|
|
4796
5130
|
}
|
|
4797
5131
|
if (topic === 'connection-refusal') {
|
|
@@ -4824,7 +5158,12 @@ export function composeRuntime(options) {
|
|
|
4824
5158
|
}
|
|
4825
5159
|
if (topic === 'member') {
|
|
4826
5160
|
memberListeners.add(deliver);
|
|
4827
|
-
|
|
5161
|
+
void openLocal()
|
|
5162
|
+
.then((local) => local.facts())
|
|
5163
|
+
.then(({ member }) => {
|
|
5164
|
+
if (memberListeners.has(deliver))
|
|
5165
|
+
deliver(member);
|
|
5166
|
+
}, () => { });
|
|
4828
5167
|
return () => {
|
|
4829
5168
|
memberListeners.delete(deliver);
|
|
4830
5169
|
};
|
|
@@ -4839,12 +5178,18 @@ export function composeRuntime(options) {
|
|
|
4839
5178
|
if (topic === 'queue') {
|
|
4840
5179
|
const queue = { locator: input[0], notify: deliver };
|
|
4841
5180
|
queueSubscriptions.add(queue);
|
|
4842
|
-
|
|
5181
|
+
void publication(async () => {
|
|
5182
|
+
if (!queueSubscriptions.has(queue))
|
|
5183
|
+
return;
|
|
5184
|
+
const entries = await entriesOf(queue.locator);
|
|
5185
|
+
if (queueSubscriptions.has(queue))
|
|
5186
|
+
queue.notify(entries);
|
|
5187
|
+
});
|
|
4843
5188
|
return () => {
|
|
4844
5189
|
queueSubscriptions.delete(queue);
|
|
4845
5190
|
};
|
|
4846
5191
|
}
|
|
4847
|
-
return
|
|
5192
|
+
return watchPlanned(planned(input[0], null), deliver, fail);
|
|
4848
5193
|
};
|
|
4849
5194
|
const md = {
|
|
4850
5195
|
loaded() {
|
|
@@ -4863,15 +5208,14 @@ export function composeRuntime(options) {
|
|
|
4863
5208
|
emitKnownRooms();
|
|
4864
5209
|
});
|
|
4865
5210
|
},
|
|
4866
|
-
query: ((input) =>
|
|
5211
|
+
query: ((input) => planned(input, null).then(rowsQuery)),
|
|
4867
5212
|
search: (input) => searched(input, null),
|
|
4868
|
-
types(locator) {
|
|
5213
|
+
async types(locator) {
|
|
5214
|
+
const held = (await storedRooms([locator])).get(locator);
|
|
5215
|
+
const room = isFenced(locator) ? undefined : declarationOf(locator, held)?.room;
|
|
4869
5216
|
return {
|
|
4870
|
-
collection: modelTypes(
|
|
4871
|
-
typedSelection:
|
|
4872
|
-
rooms.get(locator)?.typedSelection === null
|
|
4873
|
-
? null
|
|
4874
|
-
: modelTypes(builtIn),
|
|
5217
|
+
collection: modelTypes(isFenced(locator) ? NO_MODEL : modelIn(locator, held)),
|
|
5218
|
+
typedSelection: (room?.typedSelection ?? null) === null ? null : modelTypes(builtIn),
|
|
4875
5219
|
};
|
|
4876
5220
|
},
|
|
4877
5221
|
roomDraft: {
|
|
@@ -4885,7 +5229,7 @@ export function composeRuntime(options) {
|
|
|
4885
5229
|
});
|
|
4886
5230
|
},
|
|
4887
5231
|
async read(room, path) {
|
|
4888
|
-
const record =
|
|
5232
|
+
const record = await draftOf(room, path);
|
|
4889
5233
|
return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
|
|
4890
5234
|
},
|
|
4891
5235
|
write(room, path, revision, input, next = roomDraftRevision()) {
|
|
@@ -4903,14 +5247,15 @@ export function composeRuntime(options) {
|
|
|
4903
5247
|
discard(room, path, revision) {
|
|
4904
5248
|
return exclusive(async () => {
|
|
4905
5249
|
const local = await openLocal();
|
|
4906
|
-
const current =
|
|
5250
|
+
const current = await draftOf(room, path);
|
|
4907
5251
|
if (current === undefined || current.revision !== revision) {
|
|
4908
5252
|
return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
|
|
4909
5253
|
}
|
|
4910
5254
|
await local.discardRoomDraft(room, path);
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
5255
|
+
await Promise.all([
|
|
5256
|
+
publishChangeSet(changeSet(room, { drafts: [path] })),
|
|
5257
|
+
moveTo(status),
|
|
5258
|
+
]);
|
|
4914
5259
|
return { outcome: 'written', roomDraft: null };
|
|
4915
5260
|
});
|
|
4916
5261
|
},
|
|
@@ -4919,10 +5264,7 @@ export function composeRuntime(options) {
|
|
|
4919
5264
|
chatDraft: conversations.chatDraft,
|
|
4920
5265
|
presentation: {
|
|
4921
5266
|
read(app) {
|
|
4922
|
-
return exclusive(
|
|
4923
|
-
await openLocal();
|
|
4924
|
-
return presentationOf(app);
|
|
4925
|
-
});
|
|
5267
|
+
return exclusive(() => presentationOf(app));
|
|
4926
5268
|
},
|
|
4927
5269
|
write(app, value) {
|
|
4928
5270
|
return exclusive(() => keptPresentation(app, () => structuredClone(value)));
|
|
@@ -4955,7 +5297,8 @@ export function composeRuntime(options) {
|
|
|
4955
5297
|
},
|
|
4956
5298
|
async call(name, input) {
|
|
4957
5299
|
await ready;
|
|
4958
|
-
const
|
|
5300
|
+
const lockedNow = await lockedRooms();
|
|
5301
|
+
const room = (await calledRooms(name, input)).find((one) => lockedNow.includes(one));
|
|
4959
5302
|
return room === undefined ? recoveredAction(name, input) : lockedRoom(room);
|
|
4960
5303
|
},
|
|
4961
5304
|
async reached(locator, roomVersion, path) {
|
|
@@ -4969,6 +5312,9 @@ export function composeRuntime(options) {
|
|
|
4969
5312
|
cleanUp,
|
|
4970
5313
|
unsent: () => database.ask({ kind: 'authored.unsent' }),
|
|
4971
5314
|
reset: () => options.shell.reset(),
|
|
5315
|
+
async readAgain() {
|
|
5316
|
+
lastAnswers.again();
|
|
5317
|
+
},
|
|
4972
5318
|
persistence() {
|
|
4973
5319
|
return persistence === 'granted';
|
|
4974
5320
|
},
|
|
@@ -5008,7 +5354,7 @@ export function composeRuntime(options) {
|
|
|
5008
5354
|
lockedRooms,
|
|
5009
5355
|
watchLocks,
|
|
5010
5356
|
located,
|
|
5011
|
-
room: (locator) =>
|
|
5357
|
+
room: async (locator) => shownRooms(await listingNow()).find((one) => one.locator === locator),
|
|
5012
5358
|
};
|
|
5013
5359
|
return {
|
|
5014
5360
|
md,
|