@agentshouse/mdruntime 0.1.0-c09bf842921ff → 0.1.0-c0c1e81da9166
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 +14 -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 +2 -0
- package/dist/client.d.ts.map +1 -1
- 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 +4 -2
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +1362 -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/index.d.ts +8 -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/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 +151 -99
- package/dist/read.d.ts +7 -0
- package/dist/read.d.ts.map +1 -0
- package/dist/read.js +17 -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 +91 -19
- package/dist/tables.d.ts.map +1 -1
- package/dist/tables.js +160 -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 +1 -0
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +1 -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';
|
|
@@ -24,10 +25,9 @@ import { advancement } from './refresh.js';
|
|
|
24
25
|
import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
|
|
25
26
|
import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
|
|
26
27
|
import { binaryBase, changedParts, changesOf, gatheredParts, held, heldFile, joinedDraft, moveTarget, readParts, readSavedParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
27
|
-
import { same,
|
|
28
|
+
import { same, STARTING, } from './status.js';
|
|
28
29
|
import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
|
|
29
30
|
import { readinessOf, } from './readiness.js';
|
|
30
|
-
import { FencedMap } from './fence.js';
|
|
31
31
|
import { sameBinary } from './overlay.js';
|
|
32
32
|
import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
|
|
33
33
|
import { roomDraftRevision } from './identity.js';
|
|
@@ -68,6 +68,8 @@ const STAGE_AIM_MS = 50;
|
|
|
68
68
|
const STREAMS_HELD = 4;
|
|
69
69
|
const NOTHING_HELD = { bytes: 0 };
|
|
70
70
|
const QUEUED_TEXT = 1 << 23;
|
|
71
|
+
// 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.
|
|
72
|
+
const NOT_READY_MS = [1_000, 2_000, 3_000, 5_000, 8_000, 11_000, 15_000];
|
|
71
73
|
// 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
74
|
const ARRIVAL_REPORT_MS = 250;
|
|
73
75
|
// 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 +95,24 @@ export function composeRuntime(options) {
|
|
|
93
95
|
const roomTypes = roomTypesOf(options.adapter.roomTypes, options.adapter.nameableRoomTypes);
|
|
94
96
|
let authorization = options.authorization;
|
|
95
97
|
let opening;
|
|
96
|
-
const
|
|
97
|
-
const isFenced = (locator) =>
|
|
98
|
+
const fencedAt = new Map();
|
|
99
|
+
const isFenced = (locator) => fencedAt.has(locator);
|
|
98
100
|
let fencings = 0;
|
|
99
101
|
const lastFenced = new Map();
|
|
100
|
-
const fencedSince = (locator, since) =>
|
|
102
|
+
const fencedSince = (locator, since) => fencedAt.has(locator) || (lastFenced.get(locator) ?? 0) > since;
|
|
101
103
|
let discoveries = 0;
|
|
102
|
-
const
|
|
103
|
-
const reopened = new Set();
|
|
104
|
+
const confirmations = new Map();
|
|
104
105
|
const usableWhenHeld = options.usableWhenHeld ?? [];
|
|
105
|
-
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
106
|
-
const pendings = new Map();
|
|
107
106
|
const landedAhead = new Map();
|
|
108
107
|
const seeds = new Map();
|
|
109
108
|
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();
|
|
109
|
+
const attachedFiles = new Map();
|
|
110
|
+
const declared = new Map();
|
|
111
|
+
const unheldModels = new Map();
|
|
112
|
+
const typesHeld = new Map();
|
|
117
113
|
const unreadable = new Set();
|
|
118
114
|
const builtIn = builtInModel(roomTypes);
|
|
119
115
|
const typedPaths = options.adapter.typedPaths ?? [];
|
|
120
|
-
const models = new FencedMap(isFenced);
|
|
121
|
-
const liveModels = new FencedMap(isFenced);
|
|
122
|
-
const invalidModels = new Set();
|
|
123
116
|
const discovered = new Map();
|
|
124
117
|
let inFlight = new AbortController();
|
|
125
118
|
const refreshed = options.adapter.refresh?.bind(options.adapter);
|
|
@@ -134,23 +127,74 @@ export function composeRuntime(options) {
|
|
|
134
127
|
};
|
|
135
128
|
let roomsListed = false;
|
|
136
129
|
const scopedRefusals = new Map();
|
|
137
|
-
function
|
|
138
|
-
|
|
130
|
+
function typed(locator, persisted) {
|
|
131
|
+
if (persisted.status === 'unavailable')
|
|
132
|
+
return persisted;
|
|
133
|
+
const schema = persisted.snapshot.schema;
|
|
134
|
+
const held = typesHeld.get(locator);
|
|
135
|
+
if (held !== undefined && held.schema === schema)
|
|
136
|
+
return { ...persisted, types: held.types };
|
|
137
|
+
const types = schemaTypes(schema, roomTypes);
|
|
138
|
+
typesHeld.set(locator, { schema, types });
|
|
139
|
+
return { ...persisted, types };
|
|
139
140
|
}
|
|
140
|
-
function
|
|
141
|
-
return
|
|
142
|
-
? { status: 'unavailable', reason: 'revoked' }
|
|
143
|
-
: modelOf(locator);
|
|
141
|
+
function recordOf(stored) {
|
|
142
|
+
return { ...stored, model: typed(stored.locator, stored.model) };
|
|
144
143
|
}
|
|
145
144
|
function composed(record) {
|
|
146
145
|
return compositionOf(record, builtIn, typedPaths);
|
|
147
146
|
}
|
|
147
|
+
async function storedRooms(locators) {
|
|
148
|
+
const held = await (await openLocal()).rooms(locators);
|
|
149
|
+
return new Map(held
|
|
150
|
+
.filter(({ record }) => !isFenced(record.locator))
|
|
151
|
+
.map(({ record, indexed, documents }) => [
|
|
152
|
+
record.locator,
|
|
153
|
+
{ record: recordOf(record), indexed, documents },
|
|
154
|
+
]));
|
|
155
|
+
}
|
|
156
|
+
async function recordAt(locator) {
|
|
157
|
+
return (await storedRooms([locator])).get(locator)?.record;
|
|
158
|
+
}
|
|
159
|
+
async function workOf(locator) {
|
|
160
|
+
if (isFenced(locator))
|
|
161
|
+
return { replica: undefined, queue: [], states: new Map() };
|
|
162
|
+
const { room, queue } = await (await openLocal()).room(locator);
|
|
163
|
+
const batches = queue.map(({ batch }) => batch);
|
|
164
|
+
const states = new Map(queue.map(({ batch, state }) => [batch.id, state]));
|
|
165
|
+
return {
|
|
166
|
+
replica: room === null
|
|
167
|
+
? undefined
|
|
168
|
+
: {
|
|
169
|
+
record: recordOf(room.record),
|
|
170
|
+
queue: batches,
|
|
171
|
+
states,
|
|
172
|
+
overlaid: room.overlaid,
|
|
173
|
+
count: room.documents,
|
|
174
|
+
confirmed: confirmations.get(locator) ?? 0,
|
|
175
|
+
indexed: room.indexed,
|
|
176
|
+
},
|
|
177
|
+
queue: batches,
|
|
178
|
+
states,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
async function replicaOf(locator) {
|
|
182
|
+
return (await workOf(locator)).replica;
|
|
183
|
+
}
|
|
184
|
+
function modelIn(locator, stored) {
|
|
185
|
+
if (refusal?.code === 'revoked-authorization')
|
|
186
|
+
return { status: 'unavailable', reason: 'revoked' };
|
|
187
|
+
return stored?.record.model ?? unheldModels.get(locator) ?? NO_MODEL;
|
|
188
|
+
}
|
|
148
189
|
function compositionFor(record) {
|
|
149
|
-
return composed({
|
|
190
|
+
return composed({
|
|
191
|
+
...record,
|
|
192
|
+
model: modelIn(record.locator, { record, indexed: true, documents: 0 }),
|
|
193
|
+
});
|
|
150
194
|
}
|
|
151
195
|
let refusal = null;
|
|
152
196
|
let storageLoss = null;
|
|
153
|
-
let status =
|
|
197
|
+
let status = { state: 'syncing', providerUnreachable: false, resting: false };
|
|
154
198
|
let shown = STARTING;
|
|
155
199
|
let discoveryPaused = false;
|
|
156
200
|
let closed = false;
|
|
@@ -164,6 +208,7 @@ export function composeRuntime(options) {
|
|
|
164
208
|
const repairing = new Set();
|
|
165
209
|
const calling = new Set();
|
|
166
210
|
const repairable = new Map();
|
|
211
|
+
const unready = new Map();
|
|
167
212
|
const answering = new Map();
|
|
168
213
|
let complete = false;
|
|
169
214
|
let recorded = false;
|
|
@@ -187,7 +232,17 @@ export function composeRuntime(options) {
|
|
|
187
232
|
const queueSubscriptions = new Set();
|
|
188
233
|
const roomDraftListeners = new Set();
|
|
189
234
|
const presentationListeners = new Set();
|
|
190
|
-
const liveQueries = createLiveQueries((plan, cover) => answeredPlan(plan, cover));
|
|
235
|
+
const liveQueries = createLiveQueries((plan, cover) => answeredPlan(plan, cover), () => covering());
|
|
236
|
+
const figures = createFigures({
|
|
237
|
+
cover: figuresCover,
|
|
238
|
+
rooms: async (rooms, since) => database.ask({
|
|
239
|
+
kind: 'replica.tallies',
|
|
240
|
+
totals: { ...(await planned(ROOM_TOTALS, null)), limit: null },
|
|
241
|
+
rooms,
|
|
242
|
+
since,
|
|
243
|
+
}),
|
|
244
|
+
conversations: (conversations, since) => database.ask({ kind: 'conversation.tallies', conversations, since }),
|
|
245
|
+
});
|
|
191
246
|
let withdrawConversations;
|
|
192
247
|
let currencyQueued = null;
|
|
193
248
|
let socketOpen = false;
|
|
@@ -200,15 +255,14 @@ export function composeRuntime(options) {
|
|
|
200
255
|
let haltedRefusal = false;
|
|
201
256
|
let openSignals = 0;
|
|
202
257
|
const serviceModeListeners = new Set();
|
|
203
|
-
let member = null;
|
|
204
258
|
const memberListeners = new Set();
|
|
205
259
|
let authorityChanges = 0;
|
|
206
260
|
const authorityChangeListeners = new Set();
|
|
207
261
|
const unpreparedDemand = new Set();
|
|
208
262
|
const unpreparable = new Set();
|
|
209
263
|
let preparing = false;
|
|
210
|
-
let targets = {};
|
|
211
264
|
let persistence = options.shell.persistence;
|
|
265
|
+
let heldWhenOpened = false;
|
|
212
266
|
let full = false;
|
|
213
267
|
let probing = null;
|
|
214
268
|
let testing = null;
|
|
@@ -227,7 +281,7 @@ export function composeRuntime(options) {
|
|
|
227
281
|
const readinessWatches = new Set();
|
|
228
282
|
let readinessQueued = false;
|
|
229
283
|
const catching = new Set();
|
|
230
|
-
const transfers = new
|
|
284
|
+
const transfers = new Map();
|
|
231
285
|
const installs = new Set();
|
|
232
286
|
const arrivals = new Map();
|
|
233
287
|
const streaming = new Set();
|
|
@@ -255,9 +309,16 @@ export function composeRuntime(options) {
|
|
|
255
309
|
call: (name, input) => invokeAction(name, input),
|
|
256
310
|
history: (request) => readConversationHistory(request),
|
|
257
311
|
durably,
|
|
312
|
+
local: () => openLocal(),
|
|
313
|
+
again: () => {
|
|
314
|
+
void ready.then(scheduleCurrency);
|
|
315
|
+
},
|
|
316
|
+
settled: () => {
|
|
317
|
+
show();
|
|
318
|
+
},
|
|
258
319
|
stream: () => options.adapter.conversations,
|
|
259
320
|
publish: publishChangeSet,
|
|
260
|
-
online: () =>
|
|
321
|
+
online: () => (socketOpen || !status.providerUnreachable) && refusal === null,
|
|
261
322
|
full: () => full,
|
|
262
323
|
closed: () => closed,
|
|
263
324
|
unanswered: resendLater,
|
|
@@ -277,26 +338,13 @@ export function composeRuntime(options) {
|
|
|
277
338
|
notifyDiscovered();
|
|
278
339
|
readyFromStore();
|
|
279
340
|
}
|
|
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
341
|
async function visibleOf(locator, paths) {
|
|
294
|
-
const held =
|
|
342
|
+
const held = await recordAt(locator);
|
|
295
343
|
if (held === undefined)
|
|
296
344
|
return undefined;
|
|
297
345
|
const local = await openLocal();
|
|
298
|
-
const slice = await stepped(slicing(await local.entries(held.
|
|
299
|
-
return { ...slice, collection: collectionOf(slice.documents, composed(held
|
|
346
|
+
const slice = await stepped(slicing(await local.entries(held.key, 'visible', paths), paths));
|
|
347
|
+
return { ...slice, collection: collectionOf(slice.documents, composed(held)) };
|
|
300
348
|
}
|
|
301
349
|
function roomDraftOf(record, visible) {
|
|
302
350
|
return structuredClone({
|
|
@@ -320,25 +368,45 @@ export function composeRuntime(options) {
|
|
|
320
368
|
layers.set(locator, await visibleOf(locator, paths));
|
|
321
369
|
return records.map((record) => roomDraftOf(record, layers.get(record.locator)));
|
|
322
370
|
}
|
|
323
|
-
function
|
|
324
|
-
const
|
|
325
|
-
|
|
371
|
+
async function draftsOf(locator, paths) {
|
|
372
|
+
const held = await (await openLocal()).roomDrafts(locator, paths);
|
|
373
|
+
return held.filter((record) => !isFenced(record.locator));
|
|
374
|
+
}
|
|
375
|
+
async function draftOf(locator, path) {
|
|
376
|
+
return (await draftsOf(locator, [path]))[0];
|
|
377
|
+
}
|
|
378
|
+
function inOrder(records) {
|
|
379
|
+
return [...records].sort((left, right) => {
|
|
326
380
|
if (left.locator !== right.locator)
|
|
327
381
|
return left.locator < right.locator ? -1 : 1;
|
|
328
382
|
if (left.path !== right.path)
|
|
329
383
|
return left.path < right.path ? -1 : 1;
|
|
330
384
|
return 0;
|
|
331
385
|
});
|
|
332
|
-
return roomDraftsOf(records);
|
|
333
386
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
387
|
+
async function snapshotRoomDrafts() {
|
|
388
|
+
return roomDraftsOf(inOrder(await draftsOf()));
|
|
389
|
+
}
|
|
390
|
+
let published = Promise.resolve();
|
|
391
|
+
function publication(work) {
|
|
392
|
+
published = published.then(work).catch(unpublished);
|
|
393
|
+
return published;
|
|
394
|
+
}
|
|
395
|
+
function publishRoomDrafts(waitingOnly, touched = null) {
|
|
396
|
+
if (closed || roomDraftListeners.size === 0)
|
|
397
|
+
return Promise.resolve();
|
|
398
|
+
return publication(async () => {
|
|
399
|
+
if (closed || roomDraftListeners.size === 0)
|
|
400
|
+
return;
|
|
401
|
+
const records = await draftsOf();
|
|
402
|
+
if (closed)
|
|
403
|
+
return;
|
|
404
|
+
if (touched !== null &&
|
|
405
|
+
!records.some((record) => touched.some(({ locator, path }) => locator === record.locator && (path === null || path === record.path)))) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const value = await roomDraftsOf(inOrder(records));
|
|
409
|
+
if (closed)
|
|
342
410
|
return;
|
|
343
411
|
for (const listener of roomDraftListeners) {
|
|
344
412
|
if (waitingOnly && listener.ready)
|
|
@@ -348,8 +416,8 @@ export function composeRuntime(options) {
|
|
|
348
416
|
}
|
|
349
417
|
});
|
|
350
418
|
}
|
|
351
|
-
function presentationOf(app) {
|
|
352
|
-
const value =
|
|
419
|
+
async function presentationOf(app) {
|
|
420
|
+
const value = await (await openLocal()).presentation(app);
|
|
353
421
|
return value === undefined ? null : structuredClone(value);
|
|
354
422
|
}
|
|
355
423
|
async function writtenRoomDraft(room, path, revision, input, next) {
|
|
@@ -357,7 +425,7 @@ export function composeRuntime(options) {
|
|
|
357
425
|
if (path === '' || normalize(path) !== path)
|
|
358
426
|
throw runtimeError('room-draft-malformed', `room-draft-malformed at ${path}: a path inside the Room, such as notes/a.md`);
|
|
359
427
|
const local = await openLocal();
|
|
360
|
-
const current =
|
|
428
|
+
const current = await draftOf(room, path);
|
|
361
429
|
if ((current?.revision ?? null) !== revision) {
|
|
362
430
|
return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
|
|
363
431
|
}
|
|
@@ -376,55 +444,34 @@ export function composeRuntime(options) {
|
|
|
376
444
|
await local.keepRoomDraft(record, parts.file === undefined || file === undefined
|
|
377
445
|
? []
|
|
378
446
|
: [{ key: file.localId, blob: parts.file.blob }]);
|
|
379
|
-
|
|
380
|
-
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
447
|
+
await publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
381
448
|
return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
|
|
382
449
|
}
|
|
383
450
|
async function keptPresentation(app, next) {
|
|
384
451
|
const local = await openLocal();
|
|
385
|
-
const stored = next(
|
|
452
|
+
const stored = next(await local.presentation(app));
|
|
386
453
|
await local.present({ app, value: stored });
|
|
387
|
-
|
|
388
|
-
publishChangeSet(changeSet('', { presentation: app }));
|
|
454
|
+
await publishChangeSet(changeSet('', { presentation: app }));
|
|
389
455
|
}
|
|
390
456
|
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);
|
|
457
|
+
const listening = () => [...presentationListeners].filter((listener) => listener.app === app && listener.ready);
|
|
458
|
+
if (closed || listening().length === 0)
|
|
459
|
+
return Promise.resolve();
|
|
460
|
+
return publication(async () => {
|
|
461
|
+
if (closed || listening().length === 0)
|
|
462
|
+
return;
|
|
463
|
+
const value = await presentationOf(app);
|
|
464
|
+
if (closed)
|
|
465
|
+
return;
|
|
466
|
+
for (const listener of listening())
|
|
467
|
+
listener.notify(value);
|
|
468
|
+
});
|
|
416
469
|
}
|
|
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
|
-
}
|
|
470
|
+
async function attended() {
|
|
471
|
+
const held = await (await openLocal()).status();
|
|
472
|
+
const attentionRequired = held.returned.flatMap((record) => record.returned === null || isFenced(record.locator)
|
|
473
|
+
? []
|
|
474
|
+
: [{ room: record.locator, path: record.path, returned: structuredClone(record.returned) }]);
|
|
428
475
|
attentionRequired.sort((left, right) => {
|
|
429
476
|
if (left.room !== right.room)
|
|
430
477
|
return left.room < right.room ? -1 : 1;
|
|
@@ -432,41 +479,130 @@ export function composeRuntime(options) {
|
|
|
432
479
|
return left.path < right.path ? -1 : 1;
|
|
433
480
|
return 0;
|
|
434
481
|
});
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
482
|
+
const changed = new Map();
|
|
483
|
+
if (!localSaves) {
|
|
484
|
+
for (const change of held.unsent) {
|
|
485
|
+
if (!isFenced(change.room))
|
|
486
|
+
changed.set(`${change.room}\u0000${change.path}`, change);
|
|
487
|
+
}
|
|
441
488
|
}
|
|
442
|
-
|
|
489
|
+
const unsent = [...changed]
|
|
490
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
491
|
+
.map(([, change]) => change);
|
|
492
|
+
return {
|
|
493
|
+
queued: !localSaves && held.queued.some((locator) => !isFenced(locator)),
|
|
494
|
+
lastCompleted: held.lastCompleted,
|
|
495
|
+
attentionRequired,
|
|
496
|
+
unsent,
|
|
497
|
+
reset: held.resets.reduce((earliest, { locator, reset }) => isFenced(locator) || (earliest !== null && earliest <= reset) ? earliest : reset, null),
|
|
498
|
+
};
|
|
443
499
|
}
|
|
444
|
-
function
|
|
445
|
-
|
|
500
|
+
async function queuedWork() {
|
|
501
|
+
const held = await (await openLocal()).queued();
|
|
502
|
+
return !localSaves && held.some((locator) => !isFenced(locator));
|
|
446
503
|
}
|
|
447
|
-
function
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
return locked(room.locator)
|
|
452
|
-
? {
|
|
453
|
-
...room,
|
|
454
|
-
model: modelTypes(NO_MODEL),
|
|
455
|
-
typedSelection: null,
|
|
456
|
-
documents: 0,
|
|
457
|
-
locked: true,
|
|
458
|
-
}
|
|
459
|
-
: { ...room, documents, locked: false };
|
|
460
|
-
}));
|
|
504
|
+
function declarationOf(locator, stored) {
|
|
505
|
+
const held = declared.get(locator);
|
|
506
|
+
const room = stored?.record ?? held?.room ?? discovered.get(locator);
|
|
507
|
+
return room === undefined ? undefined : { room, available: held?.available ?? true };
|
|
461
508
|
}
|
|
462
|
-
function
|
|
463
|
-
return
|
|
509
|
+
function listedLocators(held) {
|
|
510
|
+
return [
|
|
511
|
+
...declared.keys(),
|
|
512
|
+
...[...held.keys()].filter((locator) => !declared.has(locator)),
|
|
513
|
+
].filter((locator) => !isFenced(locator));
|
|
464
514
|
}
|
|
465
|
-
function
|
|
466
|
-
return
|
|
515
|
+
function selectionModel() {
|
|
516
|
+
return refusal?.code === 'revoked-authorization'
|
|
517
|
+
? { status: 'unavailable', reason: 'revoked' }
|
|
518
|
+
: builtIn;
|
|
467
519
|
}
|
|
468
|
-
function
|
|
469
|
-
const
|
|
520
|
+
function listedRoom(locator, stored) {
|
|
521
|
+
const declaration = declarationOf(locator, stored);
|
|
522
|
+
if (declaration === undefined)
|
|
523
|
+
return undefined;
|
|
524
|
+
const { room, available } = declaration;
|
|
525
|
+
const selection = room.typedSelection ?? null;
|
|
526
|
+
return {
|
|
527
|
+
name: room.name,
|
|
528
|
+
locator,
|
|
529
|
+
handle: room.handle,
|
|
530
|
+
operations: room.operations,
|
|
531
|
+
replica: room.replica,
|
|
532
|
+
model: modelTypes(modelIn(locator, stored)),
|
|
533
|
+
typedSelection: selection === null ? null : { roots: selection.roots, model: modelTypes(selectionModel()) },
|
|
534
|
+
providerOwnedPaths: room.providerOwnedPaths,
|
|
535
|
+
roomVersion: stored?.record.roomVersion ?? null,
|
|
536
|
+
synchronizationAvailable: available,
|
|
537
|
+
refusal: scopedRefusals.get(locator) ?? null,
|
|
538
|
+
documents: stored?.documents ?? 0,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
function listingOf(held) {
|
|
542
|
+
const listing = new Map();
|
|
543
|
+
for (const locator of listedLocators(held)) {
|
|
544
|
+
const room = listedRoom(locator, held.get(locator));
|
|
545
|
+
if (room !== undefined)
|
|
546
|
+
listing.set(locator, room);
|
|
547
|
+
}
|
|
548
|
+
return listing;
|
|
549
|
+
}
|
|
550
|
+
async function patchedListing(changed) {
|
|
551
|
+
if (roomsShown === null)
|
|
552
|
+
return null;
|
|
553
|
+
const held = await storedRooms([...changed]);
|
|
554
|
+
const listing = new Map(roomsShown);
|
|
555
|
+
for (const locator of changed) {
|
|
556
|
+
const room = isFenced(locator) ? undefined : listedRoom(locator, held.get(locator));
|
|
557
|
+
if (room === undefined || !listing.has(locator))
|
|
558
|
+
return null;
|
|
559
|
+
listing.set(locator, room);
|
|
560
|
+
}
|
|
561
|
+
return listing;
|
|
562
|
+
}
|
|
563
|
+
async function listingNow() {
|
|
564
|
+
return listingOf(await storedRooms());
|
|
565
|
+
}
|
|
566
|
+
function lockedIn(listing, locator) {
|
|
567
|
+
return listing.get(locator)?.replica.offline === 'locked' && !confirmed.has(locator);
|
|
568
|
+
}
|
|
569
|
+
function shownRooms(listing) {
|
|
570
|
+
return structuredClone([...listing.values()].map(({ documents, ...room }) => lockedIn(listing, room.locator)
|
|
571
|
+
? {
|
|
572
|
+
...room,
|
|
573
|
+
model: modelTypes(NO_MODEL),
|
|
574
|
+
typedSelection: null,
|
|
575
|
+
documents: 0,
|
|
576
|
+
locked: true,
|
|
577
|
+
}
|
|
578
|
+
: { ...room, documents, locked: false }));
|
|
579
|
+
}
|
|
580
|
+
async function offlineModes() {
|
|
581
|
+
const modes = new Map();
|
|
582
|
+
for (const [locator, { room }] of declared) {
|
|
583
|
+
const declaration = room ?? discovered.get(locator);
|
|
584
|
+
if (declaration !== undefined)
|
|
585
|
+
modes.set(locator, declaration.replica.offline);
|
|
586
|
+
}
|
|
587
|
+
for (const [locator, { offline }] of (await covering()).rooms)
|
|
588
|
+
modes.set(locator, offline);
|
|
589
|
+
return modes;
|
|
590
|
+
}
|
|
591
|
+
async function locked(locator) {
|
|
592
|
+
if (isFenced(locator) || confirmed.has(locator))
|
|
593
|
+
return false;
|
|
594
|
+
const held = declared.get(locator)?.room ?? discovered.get(locator);
|
|
595
|
+
const mode = (await (await openLocal()).offline(locator)) ?? held?.replica.offline;
|
|
596
|
+
return mode === 'locked';
|
|
597
|
+
}
|
|
598
|
+
async function lockedRooms() {
|
|
599
|
+
return [...(await offlineModes())]
|
|
600
|
+
.filter(([locator, mode]) => mode === 'locked' && !confirmed.has(locator) && !isFenced(locator))
|
|
601
|
+
.map(([locator]) => locator)
|
|
602
|
+
.sort();
|
|
603
|
+
}
|
|
604
|
+
async function relocked(before) {
|
|
605
|
+
const after = await lockedRooms();
|
|
470
606
|
if (JSON.stringify(after) === JSON.stringify(before))
|
|
471
607
|
return;
|
|
472
608
|
journal.record(`locked ${after.length}`);
|
|
@@ -489,9 +625,10 @@ export function composeRuntime(options) {
|
|
|
489
625
|
lockGrace = null;
|
|
490
626
|
if (socketOpen || closed)
|
|
491
627
|
return;
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
628
|
+
void lockedRooms().then((before) => {
|
|
629
|
+
confirmed.clear();
|
|
630
|
+
return relocked(before);
|
|
631
|
+
}, () => { });
|
|
495
632
|
}, LOCK_GRACE_MS);
|
|
496
633
|
}
|
|
497
634
|
function connectionLive() {
|
|
@@ -505,13 +642,14 @@ export function composeRuntime(options) {
|
|
|
505
642
|
lockListeners.delete(notify);
|
|
506
643
|
};
|
|
507
644
|
}
|
|
508
|
-
function roomFacts(reach = null) {
|
|
509
|
-
const held =
|
|
645
|
+
async function roomFacts(reach = null) {
|
|
646
|
+
const held = await storedRooms();
|
|
647
|
+
const rooms = [...listingOf(held).values()]
|
|
510
648
|
.filter((room) => holds(reach, room.locator))
|
|
511
649
|
.map((room) => {
|
|
512
650
|
const governed = governs(reach, room);
|
|
513
651
|
const attached = [
|
|
514
|
-
...(governed.collection ? [
|
|
652
|
+
...(governed.collection ? [modelIn(room.locator, held.get(room.locator))] : []),
|
|
515
653
|
...(governed.selection ? [builtIn] : []),
|
|
516
654
|
];
|
|
517
655
|
return {
|
|
@@ -520,28 +658,86 @@ export function composeRuntime(options) {
|
|
|
520
658
|
types: attached.flatMap((model) => model.status === 'unavailable' ? [] : Object.values(model.types)),
|
|
521
659
|
};
|
|
522
660
|
});
|
|
523
|
-
return { listed: roomsListed, rooms
|
|
661
|
+
return { listed: roomsListed, rooms };
|
|
524
662
|
}
|
|
525
|
-
function emitRooms() {
|
|
526
|
-
|
|
663
|
+
function emitRooms(changed) {
|
|
664
|
+
recoverReads();
|
|
527
665
|
queueReadiness();
|
|
528
|
-
|
|
529
|
-
for (const listener of roomListeners)
|
|
530
|
-
listener(value);
|
|
531
|
-
emitKnownRooms();
|
|
666
|
+
return changed === undefined ? publishRooms('all') : publishRooms('changed', changed);
|
|
532
667
|
}
|
|
533
668
|
function emitKnownRooms() {
|
|
534
669
|
conversations.learned();
|
|
535
670
|
if (unlistedRooms.size === 0)
|
|
536
671
|
return;
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
672
|
+
void publishRooms('known');
|
|
673
|
+
}
|
|
674
|
+
let roomsOwed = null;
|
|
675
|
+
let roomsShown = null;
|
|
676
|
+
function publishRooms(mode, touched = []) {
|
|
677
|
+
if (closed)
|
|
678
|
+
return Promise.resolve();
|
|
679
|
+
if (roomsOwed !== null) {
|
|
680
|
+
roomsOwed.all ||= mode === 'all';
|
|
681
|
+
for (const locator of touched) {
|
|
682
|
+
(mode === 'changed' ? roomsOwed.changed : roomsOwed.counted).add(locator);
|
|
683
|
+
}
|
|
684
|
+
return roomsOwed.shown;
|
|
544
685
|
}
|
|
686
|
+
const owing = {
|
|
687
|
+
all: mode === 'all',
|
|
688
|
+
changed: new Set(mode === 'changed' ? touched : []),
|
|
689
|
+
counted: new Set(mode === 'changed' ? [] : touched),
|
|
690
|
+
shown: Promise.resolve(),
|
|
691
|
+
};
|
|
692
|
+
roomsOwed = owing;
|
|
693
|
+
owing.shown = publication(async () => {
|
|
694
|
+
const owed = owing;
|
|
695
|
+
if (roomsOwed === owing)
|
|
696
|
+
roomsOwed = null;
|
|
697
|
+
if (closed || (roomListeners.size === 0 && unlistedRooms.size === 0))
|
|
698
|
+
return;
|
|
699
|
+
const patched = owed.all || owed.changed.size === 0 || unlistedRooms.size > 0
|
|
700
|
+
? null
|
|
701
|
+
: await patchedListing(new Set([...owed.changed, ...owed.counted]));
|
|
702
|
+
if (patched === null && owed.changed.size > 0)
|
|
703
|
+
owed.all = true;
|
|
704
|
+
if (patched === null && !owed.all && owed.counted.size > 0) {
|
|
705
|
+
const held = await covering().catch(() => null);
|
|
706
|
+
if (held === null)
|
|
707
|
+
return;
|
|
708
|
+
owed.all = [...owed.counted].some((locator) => held.rooms.has(locator) &&
|
|
709
|
+
roomsShown?.get(locator)?.documents !== held.rooms.get(locator).documents);
|
|
710
|
+
if (owed.all) {
|
|
711
|
+
recoverReads();
|
|
712
|
+
queueReadiness();
|
|
713
|
+
}
|
|
714
|
+
else if (unlistedRooms.size === 0)
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const listing = patched ?? (await listingNow().catch(() => null));
|
|
718
|
+
if (listing === null || closed)
|
|
719
|
+
return;
|
|
720
|
+
const value = shownRooms(listing);
|
|
721
|
+
const joining = unlistedRooms.size > 0 &&
|
|
722
|
+
(roomsListed || listing.size > 0 || refusal !== null || status.providerUnreachable);
|
|
723
|
+
if (!owed.all && patched === null && !joining)
|
|
724
|
+
return;
|
|
725
|
+
const moved = owed.all ||
|
|
726
|
+
patched !== null ||
|
|
727
|
+
(roomsShown !== null && JSON.stringify(shownRooms(roomsShown)) !== JSON.stringify(value));
|
|
728
|
+
if (moved)
|
|
729
|
+
for (const listener of roomListeners)
|
|
730
|
+
listener(value);
|
|
731
|
+
if (joining) {
|
|
732
|
+
for (const listener of [...unlistedRooms]) {
|
|
733
|
+
unlistedRooms.delete(listener);
|
|
734
|
+
roomListeners.add(listener);
|
|
735
|
+
listener(value);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
roomsShown = roomListeners.size === 0 ? null : listing;
|
|
739
|
+
});
|
|
740
|
+
return owing.shown;
|
|
545
741
|
}
|
|
546
742
|
function emitRefusal() {
|
|
547
743
|
queueReadiness();
|
|
@@ -554,58 +750,98 @@ export function composeRuntime(options) {
|
|
|
554
750
|
for (const listener of storageLossListeners)
|
|
555
751
|
listener(storageLoss);
|
|
556
752
|
}
|
|
557
|
-
function emitMember() {
|
|
753
|
+
function emitMember(member) {
|
|
558
754
|
for (const listener of memberListeners)
|
|
559
755
|
listener(member);
|
|
560
756
|
}
|
|
561
757
|
function moveTo(next) {
|
|
562
758
|
if (closed)
|
|
563
|
-
return;
|
|
759
|
+
return Promise.resolve();
|
|
564
760
|
status = next;
|
|
565
|
-
show();
|
|
761
|
+
return show();
|
|
566
762
|
}
|
|
763
|
+
function transferring() {
|
|
764
|
+
return { ...status, state: 'syncing', resting: false };
|
|
765
|
+
}
|
|
766
|
+
let statusOwed = null;
|
|
767
|
+
let statusShown = Promise.resolve();
|
|
567
768
|
function show() {
|
|
568
769
|
if (closed)
|
|
569
|
-
return;
|
|
770
|
+
return Promise.resolve();
|
|
570
771
|
queueReadiness();
|
|
571
772
|
emitKnownRooms();
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
773
|
+
const moved = { moving: status, current: conversations.current() };
|
|
774
|
+
if (statusOwed !== null) {
|
|
775
|
+
statusOwed.push(moved);
|
|
776
|
+
return statusShown;
|
|
777
|
+
}
|
|
778
|
+
const owed = [moved];
|
|
779
|
+
statusOwed = owed;
|
|
780
|
+
statusShown = publication(async () => {
|
|
781
|
+
if (statusOwed === owed)
|
|
782
|
+
statusOwed = null;
|
|
783
|
+
const held = await attended();
|
|
784
|
+
if (closed)
|
|
785
|
+
return;
|
|
786
|
+
for (const { moving, current } of owed) {
|
|
787
|
+
const state = moving.resting && held.queued ? 'pending' : moving.state;
|
|
788
|
+
const next = {
|
|
789
|
+
state: state === 'synced' && !current ? 'syncing' : state,
|
|
790
|
+
lastCompleted: held.lastCompleted,
|
|
791
|
+
providerUnreachable: moving.providerUnreachable,
|
|
792
|
+
attentionRequired: held.attentionRequired,
|
|
793
|
+
unsent: held.unsent,
|
|
794
|
+
reset: held.reset,
|
|
795
|
+
};
|
|
796
|
+
if (same(shown, next))
|
|
797
|
+
continue;
|
|
798
|
+
shown = next;
|
|
799
|
+
for (const listener of statusListeners)
|
|
800
|
+
listener(shown);
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
return statusShown;
|
|
580
804
|
}
|
|
581
|
-
function entryOf(
|
|
805
|
+
function entryOf(states, batch) {
|
|
582
806
|
if (localSaves)
|
|
583
807
|
return Object.freeze({ batch, state: 'local' });
|
|
584
|
-
const state =
|
|
808
|
+
const state = states.get(batch.id);
|
|
585
809
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
586
810
|
}
|
|
587
|
-
function entriesOf(locator) {
|
|
588
|
-
|
|
811
|
+
async function entriesOf(locator) {
|
|
812
|
+
if (isFenced(locator))
|
|
813
|
+
return [];
|
|
814
|
+
const { queue, states } = await workOf(locator);
|
|
815
|
+
return queue.map((batch) => entryOf(states, batch));
|
|
589
816
|
}
|
|
590
817
|
function emitQueue(locator) {
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
818
|
+
const listening = () => [...queueSubscriptions].filter((subscription) => subscription.locator === locator);
|
|
819
|
+
if (closed || listening().length === 0)
|
|
820
|
+
return Promise.resolve();
|
|
821
|
+
return publication(async () => {
|
|
822
|
+
if (closed || listening().length === 0)
|
|
823
|
+
return;
|
|
824
|
+
const entries = await entriesOf(locator);
|
|
825
|
+
if (closed)
|
|
826
|
+
return;
|
|
827
|
+
for (const subscription of listening())
|
|
594
828
|
subscription.notify(entries);
|
|
595
|
-
}
|
|
829
|
+
});
|
|
596
830
|
}
|
|
597
831
|
function applyChangeSet(set) {
|
|
598
832
|
liveQueries.touched(set);
|
|
833
|
+
figures.touched(set);
|
|
834
|
+
}
|
|
835
|
+
function recoverReads() {
|
|
836
|
+
liveQueries.recover();
|
|
837
|
+
figures.recover();
|
|
599
838
|
}
|
|
600
839
|
function unpublished(error) {
|
|
601
840
|
journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
|
|
602
841
|
}
|
|
603
|
-
function indexed(locator) {
|
|
604
|
-
return replicas.get(locator)?.indexed === true;
|
|
605
|
-
}
|
|
606
842
|
async function prepareReplica(locator) {
|
|
607
843
|
for (;;) {
|
|
608
|
-
const held =
|
|
844
|
+
const held = (await storedRooms([locator])).get(locator);
|
|
609
845
|
if (held === undefined || closed || held.indexed)
|
|
610
846
|
return;
|
|
611
847
|
const key = held.record.key;
|
|
@@ -613,20 +849,19 @@ export function composeRuntime(options) {
|
|
|
613
849
|
const local = await openLocal();
|
|
614
850
|
return timed(deriveBudget, () => local.rederive(key, composed(held.record), deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR));
|
|
615
851
|
});
|
|
616
|
-
const now = replicas.get(locator);
|
|
617
|
-
if (now?.record.key !== key)
|
|
618
|
-
continue;
|
|
619
852
|
if (!done.done)
|
|
620
853
|
continue;
|
|
621
|
-
|
|
854
|
+
if ((await storedRooms([locator])).get(locator)?.record.key !== key)
|
|
855
|
+
continue;
|
|
622
856
|
publishChangeSet(changeSet(locator, { documents: null }));
|
|
623
|
-
|
|
857
|
+
recoverReads();
|
|
624
858
|
queueReadiness();
|
|
625
859
|
return;
|
|
626
860
|
}
|
|
627
861
|
}
|
|
628
|
-
function unprepared() {
|
|
629
|
-
|
|
862
|
+
async function unprepared() {
|
|
863
|
+
const held = (await covering()).rooms;
|
|
864
|
+
return [...unpreparedDemand, ...held.keys()].find((locator) => !(held.get(locator)?.indexed ?? false) && !unpreparable.has(locator));
|
|
630
865
|
}
|
|
631
866
|
function unpreparedBy(locator, error) {
|
|
632
867
|
unpreparable.add(locator);
|
|
@@ -641,7 +876,7 @@ export function composeRuntime(options) {
|
|
|
641
876
|
preparing = true;
|
|
642
877
|
void (async () => {
|
|
643
878
|
try {
|
|
644
|
-
for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
|
|
879
|
+
for (let waiting = await unprepared(); waiting !== undefined; waiting = await unprepared()) {
|
|
645
880
|
await yielded();
|
|
646
881
|
if (closed)
|
|
647
882
|
return;
|
|
@@ -649,6 +884,9 @@ export function composeRuntime(options) {
|
|
|
649
884
|
await prepareReplica(waiting).catch((error) => unpreparedBy(waiting, error));
|
|
650
885
|
}
|
|
651
886
|
}
|
|
887
|
+
catch {
|
|
888
|
+
journal.record('preparation failed');
|
|
889
|
+
}
|
|
652
890
|
finally {
|
|
653
891
|
preparing = false;
|
|
654
892
|
}
|
|
@@ -657,31 +895,48 @@ export function composeRuntime(options) {
|
|
|
657
895
|
function roomsOfReach(reach) {
|
|
658
896
|
return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
659
897
|
}
|
|
660
|
-
function
|
|
898
|
+
async function covering() {
|
|
899
|
+
const held = await (await openLocal()).covering();
|
|
900
|
+
return {
|
|
901
|
+
rooms: new Map([...held.rooms].filter(([locator]) => !isFenced(locator))),
|
|
902
|
+
queues: new Map([...held.queues].filter(([locator]) => !isFenced(locator))),
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
function everyRoom(held) {
|
|
906
|
+
return [...new Set([...declared.keys(), ...held.rooms.keys(), ...held.queues.keys()])];
|
|
907
|
+
}
|
|
908
|
+
function reachedRooms(plan, held) {
|
|
661
909
|
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]);
|
|
910
|
+
const named = plan.rooms ?? (reached === null ? everyRoom(held) : [...reached]);
|
|
666
911
|
return [...new Set(named)]
|
|
667
912
|
.filter((locator) => (reached === null || reached.has(locator)) && !plan.locked.includes(locator))
|
|
668
913
|
.sort();
|
|
669
914
|
}
|
|
670
915
|
function awaited(locator) {
|
|
671
916
|
return (!roomsListed ||
|
|
672
|
-
(
|
|
673
|
-
}
|
|
674
|
-
function covered(plan) {
|
|
675
|
-
|
|
676
|
-
|
|
917
|
+
(declared.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
|
|
918
|
+
}
|
|
919
|
+
function covered(plan, held) {
|
|
920
|
+
return coveredRooms(reachedRooms(plan, held), plan.rooms === null && plan.reach === null, held);
|
|
921
|
+
}
|
|
922
|
+
async function figuresCover() {
|
|
923
|
+
const held = await covering();
|
|
924
|
+
const locked = await lockedRooms();
|
|
925
|
+
const cover = coveredRooms(everyRoom(held)
|
|
926
|
+
.filter((locator) => !locked.includes(locator))
|
|
927
|
+
.sort(), true, held);
|
|
928
|
+
awaitPreparation(cover);
|
|
929
|
+
return 'pending' in cover ? cover : { rooms: cover.rooms };
|
|
930
|
+
}
|
|
931
|
+
function coveredRooms(locators, whole, held) {
|
|
932
|
+
if (locators.length === 0 && whole && !roomsListed)
|
|
677
933
|
return { pending: [] };
|
|
678
|
-
}
|
|
679
934
|
const pending = [];
|
|
680
935
|
const coverage = [];
|
|
681
936
|
const overlay = [];
|
|
682
937
|
const read = [];
|
|
683
938
|
for (const locator of locators) {
|
|
684
|
-
const replica =
|
|
939
|
+
const replica = held.rooms.get(locator);
|
|
685
940
|
if (replica === undefined) {
|
|
686
941
|
if (awaited(locator))
|
|
687
942
|
pending.push({ room: locator, awaits: 'acquisition' });
|
|
@@ -693,12 +948,12 @@ export function composeRuntime(options) {
|
|
|
693
948
|
}
|
|
694
949
|
coverage.push({
|
|
695
950
|
room: locator,
|
|
696
|
-
generation: replica.
|
|
951
|
+
generation: replica.transfer,
|
|
697
952
|
current: !(catching.has(locator) || transfers.has(locator)),
|
|
698
953
|
});
|
|
699
|
-
read.push({ locator, key: replica.
|
|
700
|
-
for (const
|
|
701
|
-
overlay.push(
|
|
954
|
+
read.push({ locator, key: replica.key });
|
|
955
|
+
for (const id of held.queues.get(locator) ?? [])
|
|
956
|
+
overlay.push(id);
|
|
702
957
|
}
|
|
703
958
|
if (pending.length > 0)
|
|
704
959
|
return { pending };
|
|
@@ -711,46 +966,54 @@ export function composeRuntime(options) {
|
|
|
711
966
|
if (waiting.length > 0)
|
|
712
967
|
prepare(waiting.map(({ room }) => room));
|
|
713
968
|
}
|
|
714
|
-
function requiredRooms(given) {
|
|
969
|
+
function requiredRooms(given, held) {
|
|
715
970
|
const reach = planReach(given);
|
|
971
|
+
const listed = new Set(listedLocators(held));
|
|
716
972
|
if (reach === null)
|
|
717
|
-
return { listed: roomsListed, rooms: [...
|
|
973
|
+
return { listed: roomsListed, rooms: [...listed].sort() };
|
|
718
974
|
const named = [...roomsOfReach(reach)].sort();
|
|
719
975
|
if (roomsListed)
|
|
720
|
-
return { listed: true, rooms: named.filter((room) =>
|
|
721
|
-
return { listed: named.every((room) =>
|
|
976
|
+
return { listed: true, rooms: named.filter((room) => listed.has(room)) };
|
|
977
|
+
return { listed: named.every((room) => held.has(room)), rooms: named };
|
|
722
978
|
}
|
|
723
|
-
function roomState(locator) {
|
|
979
|
+
function roomState(locator, held) {
|
|
724
980
|
const listedRoom = discovered.get(locator);
|
|
725
981
|
if (roomsListed &&
|
|
726
982
|
(listedRoom === undefined ||
|
|
727
983
|
listedRoom.replica.admission !== 'admitted' ||
|
|
728
984
|
unreadable.has(locator)))
|
|
729
985
|
return 'unadmitted';
|
|
730
|
-
|
|
986
|
+
const stored = held.get(locator);
|
|
987
|
+
if (stored === undefined)
|
|
731
988
|
return 'acquiring';
|
|
732
|
-
if (!indexed
|
|
989
|
+
if (!stored.indexed)
|
|
733
990
|
return 'preparing';
|
|
734
991
|
return 'complete';
|
|
735
992
|
}
|
|
736
993
|
function conversationCoverage(reach) {
|
|
737
|
-
const
|
|
738
|
-
if (!
|
|
994
|
+
const declaredConversations = reach === null || reach.conversations;
|
|
995
|
+
if (!declaredConversations || options.adapter.conversations === undefined)
|
|
739
996
|
return 'undeclared';
|
|
740
997
|
if (!conversations.known() || !conversations.current())
|
|
741
998
|
return 'acquiring';
|
|
742
999
|
return 'complete';
|
|
743
1000
|
}
|
|
744
|
-
function connectionOf() {
|
|
1001
|
+
function connectionOf(lastCompleted) {
|
|
745
1002
|
if (socketOpen)
|
|
746
1003
|
return 'connected';
|
|
747
1004
|
if (status.providerUnreachable)
|
|
748
1005
|
return 'unreachable';
|
|
749
|
-
return options.adapter.roomStream === undefined &&
|
|
1006
|
+
return options.adapter.roomStream === undefined && lastCompleted !== null
|
|
750
1007
|
? 'connected'
|
|
751
1008
|
: 'connecting';
|
|
752
1009
|
}
|
|
753
|
-
function
|
|
1010
|
+
async function connectionNow() {
|
|
1011
|
+
if (socketOpen || status.providerUnreachable || options.adapter.roomStream !== undefined) {
|
|
1012
|
+
return connectionOf(null);
|
|
1013
|
+
}
|
|
1014
|
+
return connectionOf((await (await openLocal()).facts()).lastCompleted);
|
|
1015
|
+
}
|
|
1016
|
+
function bytesOf(room, underway, held) {
|
|
754
1017
|
const transfer = transfers.get(room);
|
|
755
1018
|
if (transfer !== undefined) {
|
|
756
1019
|
const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
|
|
@@ -759,10 +1022,10 @@ export function composeRuntime(options) {
|
|
|
759
1022
|
total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
|
|
760
1023
|
};
|
|
761
1024
|
}
|
|
762
|
-
const
|
|
763
|
-
return { done:
|
|
1025
|
+
const kept = underway ? NOTHING_HELD.bytes : (held.get(room)?.bytes ?? NOTHING_HELD.bytes);
|
|
1026
|
+
return { done: kept, total: arrivals.get(room) ?? kept };
|
|
764
1027
|
}
|
|
765
|
-
function installationOf(coverage) {
|
|
1028
|
+
function installationOf(coverage, held) {
|
|
766
1029
|
const rooms = { done: 0, total: 0 };
|
|
767
1030
|
const bytes = { done: 0, total: 0 };
|
|
768
1031
|
let installing = 0;
|
|
@@ -778,7 +1041,7 @@ export function composeRuntime(options) {
|
|
|
778
1041
|
installing += 1;
|
|
779
1042
|
if (underway && (streaming.has(room) || !arrivals.has(room)))
|
|
780
1043
|
downloading = true;
|
|
781
|
-
const now = bytesOf(room, underway);
|
|
1044
|
+
const now = bytesOf(room, underway, held);
|
|
782
1045
|
const earlier = underway ? startedOver.get(room) : undefined;
|
|
783
1046
|
bytes.done += Math.max(now.done, earlier?.done ?? 0);
|
|
784
1047
|
bytes.total += Math.max(now.total, earlier?.total ?? 0);
|
|
@@ -792,9 +1055,14 @@ export function composeRuntime(options) {
|
|
|
792
1055
|
: 'replaying';
|
|
793
1056
|
return { step, rooms, bytes };
|
|
794
1057
|
}
|
|
795
|
-
function
|
|
796
|
-
const
|
|
797
|
-
const
|
|
1058
|
+
async function readinessHeld() {
|
|
1059
|
+
const local = await openLocal();
|
|
1060
|
+
const [held, facts] = await Promise.all([covering(), local.facts()]);
|
|
1061
|
+
return { held: held.rooms, facts };
|
|
1062
|
+
}
|
|
1063
|
+
function readinessFor(port, reach, { held, facts }) {
|
|
1064
|
+
const required = requiredRooms(reach, held);
|
|
1065
|
+
const coverage = required.rooms.map((room) => ({ room, state: roomState(room, held) }));
|
|
798
1066
|
const preparing = coverage
|
|
799
1067
|
.filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
|
|
800
1068
|
.map(({ room }) => room);
|
|
@@ -808,36 +1076,65 @@ export function composeRuntime(options) {
|
|
|
808
1076
|
conversations: conversationCoverage(reach),
|
|
809
1077
|
},
|
|
810
1078
|
persistence,
|
|
811
|
-
connection: connectionOf(),
|
|
1079
|
+
connection: connectionOf(facts.lastCompleted),
|
|
812
1080
|
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
813
1081
|
space: full ? 'full' : null,
|
|
814
|
-
installation: installationOf(coverage),
|
|
1082
|
+
installation: installationOf(coverage, held),
|
|
815
1083
|
lost: storageLoss !== null,
|
|
816
|
-
established: targets[port.target]?.established === true ||
|
|
817
|
-
(usableWhenHeld.includes(port.target)
|
|
1084
|
+
established: facts.targets[port.target]?.established === true ||
|
|
1085
|
+
(heldWhenOpened && usableWhenHeld.includes(port.target)),
|
|
818
1086
|
});
|
|
819
1087
|
}
|
|
1088
|
+
const establishing = new Set();
|
|
820
1089
|
function establish(name) {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
void durably((local) =>
|
|
1090
|
+
if (establishing.has(name))
|
|
1091
|
+
return;
|
|
1092
|
+
establishing.add(name);
|
|
1093
|
+
void durably(async (local) => {
|
|
1094
|
+
const { targets } = await local.facts();
|
|
1095
|
+
await local.fact('targets', { ...targets, [name]: { established: true } });
|
|
1096
|
+
})
|
|
1097
|
+
.then(() => {
|
|
1098
|
+
queueReadiness();
|
|
1099
|
+
void probeEachMinute();
|
|
1100
|
+
}, () => {
|
|
825
1101
|
journal.record(`target record failed ${name}`);
|
|
826
|
-
})
|
|
1102
|
+
})
|
|
1103
|
+
.finally(() => establishing.delete(name));
|
|
827
1104
|
}
|
|
828
|
-
function deliverReadiness(watch) {
|
|
1105
|
+
function deliverReadiness(watch, held) {
|
|
829
1106
|
if (!readinessWatches.has(watch))
|
|
830
1107
|
return;
|
|
831
|
-
const readiness = readinessFor(watch.port, watch.reach);
|
|
832
|
-
if (readiness.ready && targets[watch.port.target]?.established !== true) {
|
|
1108
|
+
const readiness = readinessFor(watch.port, watch.reach, held);
|
|
1109
|
+
if (readiness.ready && held.facts.targets[watch.port.target]?.established !== true) {
|
|
833
1110
|
establish(watch.port.target);
|
|
834
1111
|
}
|
|
835
|
-
const
|
|
836
|
-
if (
|
|
1112
|
+
const shownReadiness = JSON.stringify(readiness);
|
|
1113
|
+
if (shownReadiness === watch.last)
|
|
837
1114
|
return;
|
|
838
|
-
watch.last =
|
|
1115
|
+
watch.last = shownReadiness;
|
|
839
1116
|
watch.deliver(structuredClone(readiness));
|
|
840
1117
|
}
|
|
1118
|
+
let readinessShown = Promise.resolve();
|
|
1119
|
+
const readinessOwed = new Set();
|
|
1120
|
+
function deliverAll(watches) {
|
|
1121
|
+
const queued = readinessOwed.size > 0;
|
|
1122
|
+
for (const watch of watches)
|
|
1123
|
+
readinessOwed.add(watch);
|
|
1124
|
+
if (queued)
|
|
1125
|
+
return;
|
|
1126
|
+
readinessShown = readinessShown
|
|
1127
|
+
.then(async () => {
|
|
1128
|
+
const owed = [...readinessOwed];
|
|
1129
|
+
readinessOwed.clear();
|
|
1130
|
+
const held = await readinessHeld();
|
|
1131
|
+
if (closed)
|
|
1132
|
+
return;
|
|
1133
|
+
for (const watch of owed)
|
|
1134
|
+
deliverReadiness(watch, held);
|
|
1135
|
+
})
|
|
1136
|
+
.catch(unpublished);
|
|
1137
|
+
}
|
|
841
1138
|
function queueReadiness() {
|
|
842
1139
|
if (readinessQueued || closed || readinessWatches.size === 0)
|
|
843
1140
|
return;
|
|
@@ -846,14 +1143,13 @@ export function composeRuntime(options) {
|
|
|
846
1143
|
readinessQueued = false;
|
|
847
1144
|
if (closed)
|
|
848
1145
|
return;
|
|
849
|
-
|
|
850
|
-
deliverReadiness(watch);
|
|
1146
|
+
deliverAll([...readinessWatches]);
|
|
851
1147
|
});
|
|
852
1148
|
}
|
|
853
1149
|
function watchReadiness(port, reach, deliver) {
|
|
854
1150
|
const watch = { port, reach, deliver, last: null };
|
|
855
1151
|
readinessWatches.add(watch);
|
|
856
|
-
void openLocal().then(() =>
|
|
1152
|
+
void openLocal().then(() => deliverAll([watch]), () => deliverAll([watch]));
|
|
857
1153
|
return () => {
|
|
858
1154
|
readinessWatches.delete(watch);
|
|
859
1155
|
};
|
|
@@ -864,14 +1160,20 @@ export function composeRuntime(options) {
|
|
|
864
1160
|
full = true;
|
|
865
1161
|
journal.record('space full');
|
|
866
1162
|
queueReadiness();
|
|
867
|
-
probeEachMinute();
|
|
1163
|
+
void probeEachMinute();
|
|
868
1164
|
}
|
|
869
|
-
function installedTarget() {
|
|
870
|
-
|
|
871
|
-
|
|
1165
|
+
async function installedTarget() {
|
|
1166
|
+
if (heldWhenOpened && usableWhenHeld.length > 0)
|
|
1167
|
+
return true;
|
|
1168
|
+
const { targets } = await (await openLocal()).facts();
|
|
1169
|
+
return Object.values(targets).some((target) => target.established);
|
|
872
1170
|
}
|
|
873
|
-
function probeEachMinute() {
|
|
874
|
-
if (!full || probing !== null || closed
|
|
1171
|
+
async function probeEachMinute() {
|
|
1172
|
+
if (!full || probing !== null || closed)
|
|
1173
|
+
return;
|
|
1174
|
+
if (!(await installedTarget().catch(() => false)))
|
|
1175
|
+
return;
|
|
1176
|
+
if (!full || probing !== null || closed)
|
|
875
1177
|
return;
|
|
876
1178
|
// 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
1179
|
probing = setInterval(() => {
|
|
@@ -906,39 +1208,41 @@ export function composeRuntime(options) {
|
|
|
906
1208
|
else
|
|
907
1209
|
void resynchronize();
|
|
908
1210
|
}
|
|
909
|
-
function seedRoomHeld(room, version) {
|
|
910
|
-
|
|
1211
|
+
function seedRoomHeld(room, version, held) {
|
|
1212
|
+
const stored = held.rooms.get(room);
|
|
1213
|
+
return (stored !== undefined &&
|
|
1214
|
+
stored.indexed &&
|
|
1215
|
+
options.adapter.reached(room, stored.roomVersion, version));
|
|
911
1216
|
}
|
|
912
|
-
function seedHeld(at) {
|
|
913
|
-
return Object.entries(at).every(([room, version]) => (roomsListed && !
|
|
1217
|
+
function seedHeld(at, held) {
|
|
1218
|
+
return Object.entries(at).every(([room, version]) => (roomsListed && !declared.has(room) && !held.rooms.has(room)) ||
|
|
1219
|
+
seedRoomHeld(room, version, held));
|
|
914
1220
|
}
|
|
915
|
-
function seedApplies(plan) {
|
|
1221
|
+
function seedApplies(plan, held) {
|
|
916
1222
|
const seed = seeds.get(plan.key);
|
|
917
1223
|
if (seed === undefined)
|
|
918
1224
|
return null;
|
|
919
|
-
if (!seedHeld(seed.at))
|
|
1225
|
+
if (!seedHeld(seed.at, held))
|
|
920
1226
|
return seed;
|
|
921
1227
|
seeds.delete(plan.key);
|
|
922
1228
|
return null;
|
|
923
1229
|
}
|
|
924
|
-
function seedBehind(plan, cover) {
|
|
925
|
-
const seed = seedApplies(plan);
|
|
1230
|
+
function seedBehind(plan, cover, held) {
|
|
1231
|
+
const seed = seedApplies(plan, held);
|
|
926
1232
|
if (seed === null || 'pending' in cover)
|
|
927
1233
|
return cover;
|
|
928
1234
|
return {
|
|
929
1235
|
pending: Object.entries(seed.at)
|
|
930
|
-
.filter(([room, version]) => !seedRoomHeld(room, version))
|
|
1236
|
+
.filter(([room, version]) => !seedRoomHeld(room, version, held))
|
|
931
1237
|
.map(([room]) => ({ room, awaits: 'acquisition' })),
|
|
932
1238
|
};
|
|
933
1239
|
}
|
|
934
1240
|
function seed(entry) {
|
|
935
|
-
if (seedHeld(entry.at))
|
|
936
|
-
return;
|
|
937
1241
|
for (const { query, rows } of entry.queries) {
|
|
938
1242
|
const key = queryKey(query);
|
|
939
1243
|
seeds.set(key, { rows, at: entry.at });
|
|
940
|
-
for (const
|
|
941
|
-
|
|
1244
|
+
for (const shownSeed of seedWatchers.get(key) ?? [])
|
|
1245
|
+
shownSeed();
|
|
942
1246
|
}
|
|
943
1247
|
}
|
|
944
1248
|
function sameCover(left, right) {
|
|
@@ -946,7 +1250,7 @@ export function composeRuntime(options) {
|
|
|
946
1250
|
}
|
|
947
1251
|
async function coveredRows(plan, cover) {
|
|
948
1252
|
for (;;) {
|
|
949
|
-
const before = cover();
|
|
1253
|
+
const before = await cover();
|
|
950
1254
|
if ('pending' in before)
|
|
951
1255
|
return { pending: before };
|
|
952
1256
|
const local = await openLocal();
|
|
@@ -954,7 +1258,7 @@ export function composeRuntime(options) {
|
|
|
954
1258
|
const changes = local.changes(keys);
|
|
955
1259
|
const current = () => local.changes(keys) === changes;
|
|
956
1260
|
const rows = await windowRows(local, plan, before.rooms, current);
|
|
957
|
-
const after = cover();
|
|
1261
|
+
const after = await cover();
|
|
958
1262
|
if (rows !== null && sameCover(before, after))
|
|
959
1263
|
return { rows, cover: before, current };
|
|
960
1264
|
}
|
|
@@ -1010,23 +1314,25 @@ export function composeRuntime(options) {
|
|
|
1010
1314
|
const held = answered.cover;
|
|
1011
1315
|
return { ...keyedOf(answered.rows, held.coverage), current: answered.current };
|
|
1012
1316
|
}
|
|
1317
|
+
async function coverNow(plan) {
|
|
1318
|
+
const cover = covered(plan, await covering());
|
|
1319
|
+
awaitPreparation(cover);
|
|
1320
|
+
return cover;
|
|
1321
|
+
}
|
|
1013
1322
|
async function rowsQuery(plan) {
|
|
1014
|
-
const
|
|
1323
|
+
const held = await covering();
|
|
1324
|
+
const before = covered(plan, held);
|
|
1015
1325
|
awaitPreparation(before);
|
|
1016
|
-
const seeded = seedApplies(plan);
|
|
1326
|
+
const seeded = seedApplies(plan, held);
|
|
1017
1327
|
if (seeded !== null)
|
|
1018
1328
|
return seededResult(seeded).result;
|
|
1019
1329
|
if ('pending' in before)
|
|
1020
1330
|
return before;
|
|
1021
|
-
const answered = await coveredRows(plan, () =>
|
|
1022
|
-
const cover = covered(plan);
|
|
1023
|
-
awaitPreparation(cover);
|
|
1024
|
-
return cover;
|
|
1025
|
-
});
|
|
1331
|
+
const answered = await coveredRows(plan, () => coverNow(plan));
|
|
1026
1332
|
if ('pending' in answered)
|
|
1027
1333
|
return answered.pending;
|
|
1028
|
-
const
|
|
1029
|
-
return { ...answered.rows, coverage:
|
|
1334
|
+
const shownCover = answered.cover;
|
|
1335
|
+
return { ...answered.rows, coverage: shownCover.coverage };
|
|
1030
1336
|
}
|
|
1031
1337
|
function publishChangeSet(set) {
|
|
1032
1338
|
applyChangeSet(set);
|
|
@@ -1034,21 +1340,24 @@ export function composeRuntime(options) {
|
|
|
1034
1340
|
if (path !== null)
|
|
1035
1341
|
attachedFiles.get(locator)?.delete(path);
|
|
1036
1342
|
}
|
|
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);
|
|
1343
|
+
const shownNow = [];
|
|
1344
|
+
if (set.queues.length > 0 || set.drafts.length > 0)
|
|
1345
|
+
shownNow.push(show());
|
|
1346
|
+
if (set.rooms.length > 0)
|
|
1347
|
+
shownNow.push(emitRooms(set.rooms));
|
|
1348
|
+
else if (set.documents.length > 0) {
|
|
1349
|
+
shownNow.push(publishRooms('counted', [...new Set(set.documents.map(({ locator }) => locator))]));
|
|
1048
1350
|
}
|
|
1351
|
+
for (const locator of set.queues)
|
|
1352
|
+
shownNow.push(emitQueue(locator));
|
|
1353
|
+
if (set.drafts.length > 0)
|
|
1354
|
+
shownNow.push(publishRoomDrafts(false));
|
|
1355
|
+
else if (set.documents.length > 0)
|
|
1356
|
+
shownNow.push(publishRoomDrafts(false, set.documents));
|
|
1049
1357
|
for (const app of set.presentations)
|
|
1050
|
-
emitPresentation(app);
|
|
1358
|
+
shownNow.push(emitPresentation(app));
|
|
1051
1359
|
conversations.changed(set);
|
|
1360
|
+
return Promise.all(shownNow).then(() => undefined);
|
|
1052
1361
|
}
|
|
1053
1362
|
async function refuse(reason, provider = null) {
|
|
1054
1363
|
discoveryPaused = true;
|
|
@@ -1058,53 +1367,31 @@ export function composeRuntime(options) {
|
|
|
1058
1367
|
if (reason === 'revoked-authorization') {
|
|
1059
1368
|
await exclusive(async () => {
|
|
1060
1369
|
const revoked = { status: 'unavailable', reason: 'revoked' };
|
|
1061
|
-
|
|
1062
|
-
locator,
|
|
1063
|
-
held,
|
|
1064
|
-
record: { ...held.record, model: revoked },
|
|
1065
|
-
}));
|
|
1066
|
-
let answer = null;
|
|
1370
|
+
let held = new Map();
|
|
1067
1371
|
try {
|
|
1068
1372
|
const local = await openLocal();
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1373
|
+
held = await storedRooms();
|
|
1374
|
+
await local.commit({
|
|
1375
|
+
rooms: [...held.values()].map(({ record: kept }) => {
|
|
1376
|
+
const record = { ...kept, model: revoked };
|
|
1377
|
+
return {
|
|
1378
|
+
locator: record.locator,
|
|
1379
|
+
key: record.key,
|
|
1380
|
+
record: storedRecord(record),
|
|
1381
|
+
composed: composedOf(composed(record)),
|
|
1382
|
+
};
|
|
1383
|
+
}),
|
|
1076
1384
|
});
|
|
1077
1385
|
}
|
|
1078
1386
|
catch {
|
|
1079
1387
|
journal.record('model revocation persistence failed');
|
|
1080
1388
|
}
|
|
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 }));
|
|
1389
|
+
const listedNow = listedLocators(held);
|
|
1390
|
+
for (const locator of listedNow) {
|
|
1391
|
+
if (!held.has(locator))
|
|
1392
|
+
unheldModels.set(locator, revoked);
|
|
1106
1393
|
}
|
|
1107
|
-
publishChangeSet(mergeChangeSets(
|
|
1394
|
+
publishChangeSet(mergeChangeSets(listedNow.map((locator) => changeSet(locator, { documents: null, room: true }))));
|
|
1108
1395
|
prepare();
|
|
1109
1396
|
});
|
|
1110
1397
|
}
|
|
@@ -1121,49 +1408,66 @@ export function composeRuntime(options) {
|
|
|
1121
1408
|
}
|
|
1122
1409
|
}
|
|
1123
1410
|
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,
|
|
1411
|
+
if (isFenced(room.locator))
|
|
1412
|
+
return;
|
|
1413
|
+
declared.set(room.locator, {
|
|
1414
|
+
room: {
|
|
1415
|
+
name: room.name,
|
|
1416
|
+
locator: room.locator,
|
|
1417
|
+
handle: room.handle,
|
|
1418
|
+
operations: room.operations,
|
|
1419
|
+
replica: room.replica,
|
|
1420
|
+
typedSelection: room.typedSelection ?? null,
|
|
1421
|
+
providerOwnedPaths: room.providerOwnedPaths,
|
|
1422
|
+
},
|
|
1423
|
+
available: synchronizationAvailable,
|
|
1142
1424
|
});
|
|
1143
1425
|
}
|
|
1426
|
+
function listHeld(locator) {
|
|
1427
|
+
if (isFenced(locator))
|
|
1428
|
+
return;
|
|
1429
|
+
declared.set(locator, { room: null, available: false });
|
|
1430
|
+
}
|
|
1144
1431
|
async function applyDiscovery(found, started, left) {
|
|
1145
|
-
const lockedBefore = lockedRooms();
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1432
|
+
const lockedBefore = await lockedRooms();
|
|
1433
|
+
const local = await openLocal();
|
|
1434
|
+
const stillFenced = new Set(await local.fences());
|
|
1435
|
+
const relisting = found
|
|
1436
|
+
.map(({ locator }) => locator)
|
|
1437
|
+
.filter((locator) => {
|
|
1438
|
+
const fence = fencedAt.get(locator);
|
|
1439
|
+
return stillFenced.has(locator) && (fence === undefined || fence.at < started);
|
|
1440
|
+
});
|
|
1441
|
+
const relisted = relisting.length === 0 ? new Set() : await durably((held) => held.unfence(relisting));
|
|
1442
|
+
for (const { locator } of found) {
|
|
1443
|
+
const fence = fencedAt.get(locator);
|
|
1444
|
+
if (fence === undefined || fence.at >= started)
|
|
1445
|
+
continue;
|
|
1446
|
+
if (relisted.has(locator) || (!stillFenced.has(locator) && !fence.written)) {
|
|
1447
|
+
fencedAt.delete(locator);
|
|
1448
|
+
}
|
|
1150
1449
|
}
|
|
1450
|
+
const kept = await local.held();
|
|
1151
1451
|
await loseRooms(found
|
|
1152
|
-
.filter((room) => room.replica.admission !== 'admitted' &&
|
|
1452
|
+
.filter((room) => room.replica.admission !== 'admitted' &&
|
|
1453
|
+
(kept.held.has(room.locator) || transfers.has(room.locator)))
|
|
1153
1454
|
.map((room) => room.locator));
|
|
1154
|
-
const listed = found.filter((
|
|
1155
|
-
const
|
|
1156
|
-
for (const locator of
|
|
1455
|
+
const listed = found.filter(({ locator }) => !fencedAt.has(locator) && (!stillFenced.has(locator) || relisted.has(locator)));
|
|
1456
|
+
const listedLocatorsNow = new Set(listed.map((room) => room.locator));
|
|
1457
|
+
for (const locator of listedLocatorsNow)
|
|
1157
1458
|
scopedRefusals.delete(locator);
|
|
1158
1459
|
if (left === undefined)
|
|
1159
1460
|
discovered.clear();
|
|
1160
1461
|
for (const room of listed)
|
|
1161
1462
|
discovered.set(room.locator, room);
|
|
1162
|
-
|
|
1463
|
+
const named = new Set(found.map((room) => room.locator));
|
|
1464
|
+
await loseRooms(left ??
|
|
1465
|
+
[...new Set([...declared.keys(), ...kept.held, ...transfers.keys()])].filter((locator) => !named.has(locator)));
|
|
1466
|
+
const held = await storedRooms(listed.map((room) => room.locator));
|
|
1163
1467
|
for (const room of listed) {
|
|
1164
|
-
const
|
|
1165
|
-
if (
|
|
1166
|
-
|
|
1468
|
+
const record = held.get(room.locator)?.record;
|
|
1469
|
+
if (record !== undefined && record.replica.selection !== room.replica.selection) {
|
|
1470
|
+
listHeld(room.locator);
|
|
1167
1471
|
continue;
|
|
1168
1472
|
}
|
|
1169
1473
|
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
@@ -1171,74 +1475,36 @@ export function composeRuntime(options) {
|
|
|
1171
1475
|
confirmLocks(listed);
|
|
1172
1476
|
roomsListed = true;
|
|
1173
1477
|
emitRooms();
|
|
1174
|
-
relocked(lockedBefore);
|
|
1478
|
+
await relocked(lockedBefore);
|
|
1175
1479
|
return listed;
|
|
1176
1480
|
}
|
|
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;
|
|
1481
|
+
function forget(locator) {
|
|
1482
|
+
confirmed.delete(locator);
|
|
1483
|
+
for (const key of [...landedAhead.keys()]) {
|
|
1484
|
+
if (key.startsWith(`${locator}\u0000`))
|
|
1485
|
+
landedAhead.delete(key);
|
|
1486
|
+
}
|
|
1487
|
+
confirmations.delete(locator);
|
|
1488
|
+
highest.delete(locator);
|
|
1489
|
+
beyondHorizon.delete(locator);
|
|
1490
|
+
declared.delete(locator);
|
|
1491
|
+
attachedFiles.delete(locator);
|
|
1492
|
+
untransferred(locator);
|
|
1493
|
+
startedOver.delete(locator);
|
|
1494
|
+
catching.delete(locator);
|
|
1495
|
+
discovered.delete(locator);
|
|
1496
|
+
unreadable.delete(locator);
|
|
1497
|
+
stale.delete(locator);
|
|
1498
|
+
repairable.delete(locator);
|
|
1499
|
+
unpreparable.delete(locator);
|
|
1500
|
+
scopedRefusals.delete(locator);
|
|
1501
|
+
unheldModels.delete(locator);
|
|
1502
|
+
typesHeld.delete(locator);
|
|
1234
1503
|
}
|
|
1235
1504
|
async function eraseFenced(locators) {
|
|
1236
1505
|
for (const locator of locators) {
|
|
1237
1506
|
while (await durably((local) => local.erase(locator, ERASE_BATCH)))
|
|
1238
1507
|
await yielded();
|
|
1239
|
-
const held = fences.get(locator);
|
|
1240
|
-
if (held !== undefined)
|
|
1241
|
-
held.erased = true;
|
|
1242
1508
|
}
|
|
1243
1509
|
}
|
|
1244
1510
|
let sweeping = false;
|
|
@@ -1260,147 +1526,60 @@ export function composeRuntime(options) {
|
|
|
1260
1526
|
}
|
|
1261
1527
|
})();
|
|
1262
1528
|
}
|
|
1529
|
+
function fenceLive(locators) {
|
|
1530
|
+
for (const locator of new Set(locators)) {
|
|
1531
|
+
if (fencedAt.has(locator))
|
|
1532
|
+
continue;
|
|
1533
|
+
fencedAt.set(locator, { at: discoveries, written: false });
|
|
1534
|
+
lastFenced.set(locator, ++fencings);
|
|
1535
|
+
forget(locator);
|
|
1536
|
+
journal.record(`room lost ${locator}`);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1263
1539
|
async function loseRooms(locators) {
|
|
1264
|
-
|
|
1540
|
+
fenceLive(locators);
|
|
1541
|
+
const fresh = [...new Set(locators)].filter((locator) => {
|
|
1542
|
+
const fence = fencedAt.get(locator);
|
|
1543
|
+
if (fence === undefined || fence.written)
|
|
1544
|
+
return false;
|
|
1545
|
+
fence.written = true;
|
|
1546
|
+
return true;
|
|
1547
|
+
});
|
|
1265
1548
|
if (fresh.length === 0)
|
|
1266
1549
|
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
|
-
});
|
|
1550
|
+
let held = [];
|
|
1366
1551
|
try {
|
|
1367
|
-
|
|
1368
|
-
await
|
|
1552
|
+
await durably(async (local) => {
|
|
1553
|
+
held = await local.roomDrafts();
|
|
1554
|
+
await local.fence(fresh);
|
|
1555
|
+
});
|
|
1369
1556
|
}
|
|
1370
1557
|
catch (error) {
|
|
1371
|
-
|
|
1372
|
-
|
|
1558
|
+
for (const locator of fresh) {
|
|
1559
|
+
const fence = fencedAt.get(locator);
|
|
1560
|
+
if (fence !== undefined)
|
|
1561
|
+
fence.written = false;
|
|
1562
|
+
}
|
|
1563
|
+
throw error;
|
|
1373
1564
|
}
|
|
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;
|
|
1565
|
+
finally {
|
|
1566
|
+
publishChangeSet(mergeChangeSets(fresh.map((locator) => changeSet(locator, {
|
|
1567
|
+
documents: null,
|
|
1568
|
+
room: true,
|
|
1569
|
+
queue: true,
|
|
1570
|
+
drafts: held.filter((draft) => draft.locator === locator).map(({ path }) => path),
|
|
1571
|
+
}))));
|
|
1572
|
+
recoverReads();
|
|
1573
|
+
moveTo(status);
|
|
1574
|
+
}
|
|
1575
|
+
await durably(unpair);
|
|
1576
|
+
await eraseFenced(fresh);
|
|
1394
1577
|
}
|
|
1395
1578
|
async function open() {
|
|
1396
1579
|
if (closed)
|
|
1397
1580
|
throw new Error('runtime closed');
|
|
1398
|
-
const opened = openRuntimeStore(
|
|
1581
|
+
const opened = openRuntimeStore(database);
|
|
1399
1582
|
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
1583
|
const kept = options.shell.generation;
|
|
1405
1584
|
const storedGeneration = await opened.generation();
|
|
1406
1585
|
const loss = detectStorageLoss(storedGeneration, kept);
|
|
@@ -1413,7 +1592,20 @@ export function composeRuntime(options) {
|
|
|
1413
1592
|
storageLoss = loss;
|
|
1414
1593
|
emitStorageLoss();
|
|
1415
1594
|
}
|
|
1416
|
-
const
|
|
1595
|
+
const held = await opened.opened();
|
|
1596
|
+
heldWhenOpened = usableWhenHeld.length > 0 && (await opened.covering()).rooms.size > 0;
|
|
1597
|
+
try {
|
|
1598
|
+
await opened.demote();
|
|
1599
|
+
for (const key of held.orphans)
|
|
1600
|
+
await opened.release(key);
|
|
1601
|
+
}
|
|
1602
|
+
catch (error) {
|
|
1603
|
+
if (!refusedForSpace(error))
|
|
1604
|
+
throw error;
|
|
1605
|
+
}
|
|
1606
|
+
if (held.erasing || held.orphans.length > 0)
|
|
1607
|
+
queueMicrotask(sweep);
|
|
1608
|
+
await conversations.opened(opened);
|
|
1417
1609
|
if (interrupted.length > 0) {
|
|
1418
1610
|
queueMicrotask(() => {
|
|
1419
1611
|
void eraseFenced(interrupted).catch(() => {
|
|
@@ -1421,7 +1613,9 @@ export function composeRuntime(options) {
|
|
|
1421
1613
|
});
|
|
1422
1614
|
});
|
|
1423
1615
|
}
|
|
1424
|
-
moveTo(
|
|
1616
|
+
moveTo(status);
|
|
1617
|
+
prepare();
|
|
1618
|
+
emitRooms();
|
|
1425
1619
|
void exclusive(() => unpair(opened)).catch(() => {
|
|
1426
1620
|
journal.record('unpair failed');
|
|
1427
1621
|
});
|
|
@@ -1464,9 +1658,9 @@ export function composeRuntime(options) {
|
|
|
1464
1658
|
gathering = entry;
|
|
1465
1659
|
return entry.done;
|
|
1466
1660
|
}
|
|
1467
|
-
function withdrawable(
|
|
1661
|
+
function withdrawable(states) {
|
|
1468
1662
|
return (batch) => {
|
|
1469
|
-
const state =
|
|
1663
|
+
const state = states.get(batch.id);
|
|
1470
1664
|
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
1471
1665
|
};
|
|
1472
1666
|
}
|
|
@@ -1488,13 +1682,13 @@ export function composeRuntime(options) {
|
|
|
1488
1682
|
const partner = partnerOf(batch, locator);
|
|
1489
1683
|
if (partner === null)
|
|
1490
1684
|
return true;
|
|
1491
|
-
const other =
|
|
1685
|
+
const other = await replicaOf(partner);
|
|
1492
1686
|
const half = other?.queue.find((one) => one.id === batch.id);
|
|
1493
1687
|
if (other === undefined || half === undefined)
|
|
1494
1688
|
return false;
|
|
1495
1689
|
return landed(half, await sliceAt(other.record, 'confirmed', [half]));
|
|
1496
1690
|
}
|
|
1497
|
-
function carriedBack(locator, unlanded, written) {
|
|
1691
|
+
async function carriedBack(locator, unlanded, written) {
|
|
1498
1692
|
const back = new Map();
|
|
1499
1693
|
const taken = [];
|
|
1500
1694
|
for (const batch of unlanded) {
|
|
@@ -1503,13 +1697,15 @@ export function composeRuntime(options) {
|
|
|
1503
1697
|
continue;
|
|
1504
1698
|
const source = between.source.room;
|
|
1505
1699
|
const destination = between.destination.room;
|
|
1506
|
-
|
|
1507
|
-
|
|
1700
|
+
const local = await openLocal();
|
|
1701
|
+
const arrived = (await local.pending(destination, batch.id)).state?.drafts ?? [];
|
|
1702
|
+
for (const { path: to } of arrived) {
|
|
1703
|
+
const record = await draftOf(destination, to);
|
|
1508
1704
|
const path = between.moves.find((move) => move.to === to)?.path;
|
|
1509
1705
|
if (record === undefined || path === undefined)
|
|
1510
1706
|
continue;
|
|
1511
1707
|
const [into, key] = source === locator ? [written, path] : [back, documentKey(source, path)];
|
|
1512
|
-
const existing = into.get(key) ??
|
|
1708
|
+
const existing = into.get(key) ?? (await draftOf(source, path));
|
|
1513
1709
|
taken.push({ locator: destination, path: to });
|
|
1514
1710
|
into.set(key, {
|
|
1515
1711
|
locator: source,
|
|
@@ -1535,19 +1731,14 @@ export function composeRuntime(options) {
|
|
|
1535
1731
|
.filter((one) => one.locator === room)
|
|
1536
1732
|
.map(({ path }) => path),
|
|
1537
1733
|
})),
|
|
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
1734
|
};
|
|
1545
1735
|
}
|
|
1546
|
-
function
|
|
1547
|
-
|
|
1548
|
-
if (!indexed)
|
|
1736
|
+
function preparedAfter(locator, answer) {
|
|
1737
|
+
if (answer.indexed[locator] === false)
|
|
1549
1738
|
prepare([locator]);
|
|
1550
|
-
|
|
1739
|
+
}
|
|
1740
|
+
function confirmedAgain(locator) {
|
|
1741
|
+
confirmations.set(locator, (confirmations.get(locator) ?? 0) + 1);
|
|
1551
1742
|
}
|
|
1552
1743
|
function replayed(record, confirmedLayer, queue) {
|
|
1553
1744
|
return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
|
|
@@ -1564,13 +1755,14 @@ export function composeRuntime(options) {
|
|
|
1564
1755
|
touched,
|
|
1565
1756
|
};
|
|
1566
1757
|
}
|
|
1567
|
-
async function settle(local, locator,
|
|
1568
|
-
return together(local, [await settlement(local, locator,
|
|
1758
|
+
async function settle(local, locator, work, record, queue, settling = {}) {
|
|
1759
|
+
return together(local, [await settlement(local, locator, work, record, queue, settling)]);
|
|
1569
1760
|
}
|
|
1570
|
-
function unpairedIn(locator, batch) {
|
|
1571
|
-
|
|
1761
|
+
async function unpairedIn(locator, batch) {
|
|
1762
|
+
const { state } = await (await openLocal()).pending(locator, batch.id);
|
|
1763
|
+
if (state === undefined)
|
|
1572
1764
|
return false;
|
|
1573
|
-
return !leavingIn(batch, locator) || withdrawable(
|
|
1765
|
+
return !leavingIn(batch, locator) || withdrawable(new Map([[batch.id, state]]))(batch);
|
|
1574
1766
|
}
|
|
1575
1767
|
async function together(local, settlements) {
|
|
1576
1768
|
const settled = new Set(settlements.map(({ locator }) => locator));
|
|
@@ -1580,8 +1772,9 @@ export function composeRuntime(options) {
|
|
|
1580
1772
|
for (const { locator, left } of wave) {
|
|
1581
1773
|
for (const batch of left) {
|
|
1582
1774
|
const partner = partnerOf(batch, locator);
|
|
1583
|
-
if (partner === null || settled.has(partner) || !unpairedIn(partner, batch))
|
|
1775
|
+
if (partner === null || settled.has(partner) || !(await unpairedIn(partner, batch))) {
|
|
1584
1776
|
continue;
|
|
1777
|
+
}
|
|
1585
1778
|
partnered.set(partner, [...(partnered.get(partner) ?? []), batch]);
|
|
1586
1779
|
}
|
|
1587
1780
|
}
|
|
@@ -1599,9 +1792,10 @@ export function composeRuntime(options) {
|
|
|
1599
1792
|
});
|
|
1600
1793
|
return mergeChangeSets(all.map((one) => one.apply(answer)));
|
|
1601
1794
|
}
|
|
1602
|
-
async function settlement(local, locator,
|
|
1795
|
+
async function settlement(local, locator, work, record, queue, settling = {}) {
|
|
1796
|
+
const before = work.replica;
|
|
1603
1797
|
const view = settling.view ?? 'confirmed';
|
|
1604
|
-
const previousQueue =
|
|
1798
|
+
const previousQueue = work.queue;
|
|
1605
1799
|
const overrides = settling.changes;
|
|
1606
1800
|
const read = await sliceAt(record, view, queue, [
|
|
1607
1801
|
...(before !== undefined && before.record.key === record.key ? before.overlaid : []),
|
|
@@ -1618,7 +1812,7 @@ export function composeRuntime(options) {
|
|
|
1618
1812
|
Object.entries(held).some(([path, revision]) => confirmedLayer.revisions[path] !== revision));
|
|
1619
1813
|
});
|
|
1620
1814
|
const unshown = queue.filter((batch) => !shown.includes(batch));
|
|
1621
|
-
const { withdrawn, kept } = await stepped(standing(confirmedLayer, unshown, withdrawable(
|
|
1815
|
+
const { withdrawn, kept } = await stepped(standing(confirmedLayer, unshown, withdrawable(work.states), localSaves, locator, composition));
|
|
1622
1816
|
const founded = kept.filter((batch) => !unshown.includes(batch));
|
|
1623
1817
|
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
1624
1818
|
const unqueued = shown.map((batch) => ({ batch, landed: true }));
|
|
@@ -1636,8 +1830,8 @@ export function composeRuntime(options) {
|
|
|
1636
1830
|
for (const { batch, returned: reason } of returning) {
|
|
1637
1831
|
if (reason === null || batch.between?.destination.room === locator)
|
|
1638
1832
|
continue;
|
|
1639
|
-
for (const saved of
|
|
1640
|
-
const existing = written.get(saved.path) ??
|
|
1833
|
+
for (const saved of work.states.get(batch.id)?.drafts ?? []) {
|
|
1834
|
+
const existing = written.get(saved.path) ?? (await draftOf(locator, saved.path));
|
|
1641
1835
|
written.set(saved.path, {
|
|
1642
1836
|
locator,
|
|
1643
1837
|
path: saved.path,
|
|
@@ -1647,12 +1841,12 @@ export function composeRuntime(options) {
|
|
|
1647
1841
|
});
|
|
1648
1842
|
}
|
|
1649
1843
|
}
|
|
1650
|
-
const carried = carriedBack(locator, [
|
|
1844
|
+
const carried = await carriedBack(locator, [
|
|
1651
1845
|
...[...unqueued].reverse().flatMap(({ batch, landed: held }) => (held ? [] : [batch])),
|
|
1652
1846
|
...(settling.leaving ?? []).flatMap(({ batch, returned }) => returned === null ? [] : [batch]),
|
|
1653
1847
|
], written);
|
|
1654
1848
|
const replayedLayer = await replayed(record, confirmedLayer, kept);
|
|
1655
|
-
const { overlay
|
|
1849
|
+
const { overlay } = await overlayDiff(record, before, replayedLayer.visible, replayedLayer.touched);
|
|
1656
1850
|
const commit = {
|
|
1657
1851
|
rooms: [
|
|
1658
1852
|
{
|
|
@@ -1672,6 +1866,7 @@ export function composeRuntime(options) {
|
|
|
1672
1866
|
{
|
|
1673
1867
|
locator,
|
|
1674
1868
|
queue: kept,
|
|
1869
|
+
stored: previousQueue,
|
|
1675
1870
|
founded,
|
|
1676
1871
|
roomDrafts: [...written.values()],
|
|
1677
1872
|
consumedRoomDrafts: carried.taken,
|
|
@@ -1686,33 +1881,21 @@ export function composeRuntime(options) {
|
|
|
1686
1881
|
commit,
|
|
1687
1882
|
apply: (answer) => {
|
|
1688
1883
|
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));
|
|
1884
|
+
if (!staying.has(batch.id))
|
|
1885
|
+
landedAhead.delete(pendingKey(locator, batch.id));
|
|
1693
1886
|
}
|
|
1694
|
-
for (const draft of written.values())
|
|
1695
|
-
rememberRoomDraft(draft);
|
|
1696
|
-
carried.remember();
|
|
1697
1887
|
const drafts = [...written.keys(), ...carried.taken];
|
|
1698
|
-
queuesWithoutReplica.delete(locator);
|
|
1699
1888
|
for (const { batch, landed: held } of unqueued) {
|
|
1700
1889
|
const outcome = localSaves ? 'dropped' : held ? 'landed' : 'withdrawn';
|
|
1701
1890
|
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
1702
1891
|
}
|
|
1703
1892
|
const changed = answer.changed[locator];
|
|
1704
|
-
|
|
1893
|
+
if (settling.commit?.published !== undefined ||
|
|
1705
1894
|
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
|
-
});
|
|
1895
|
+
overrides !== undefined) {
|
|
1896
|
+
confirmedAgain(locator);
|
|
1897
|
+
}
|
|
1898
|
+
preparedAfter(locator, answer);
|
|
1716
1899
|
return mergeChangeSets([
|
|
1717
1900
|
changeSet(locator, {
|
|
1718
1901
|
documents: changed === undefined ? [] : changed,
|
|
@@ -1725,22 +1908,11 @@ export function composeRuntime(options) {
|
|
|
1725
1908
|
},
|
|
1726
1909
|
};
|
|
1727
1910
|
}
|
|
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) {
|
|
1911
|
+
function remodel(answers, held) {
|
|
1738
1912
|
const answer = answers?.collection;
|
|
1739
1913
|
if (answer === undefined)
|
|
1740
1914
|
return held;
|
|
1741
|
-
|
|
1742
|
-
models.set(locator, state);
|
|
1743
|
-
return { collection: state };
|
|
1915
|
+
return { collection: attachedModel(answer, roomTypes) };
|
|
1744
1916
|
}
|
|
1745
1917
|
function publish(local, room, reached, landing, live) {
|
|
1746
1918
|
const landed = exclusive(async () => {
|
|
@@ -1748,8 +1920,9 @@ export function composeRuntime(options) {
|
|
|
1748
1920
|
caughtUpTo(room.locator);
|
|
1749
1921
|
return true;
|
|
1750
1922
|
}
|
|
1751
|
-
const
|
|
1752
|
-
|
|
1923
|
+
const work = await workOf(room.locator);
|
|
1924
|
+
const prior = work.replica;
|
|
1925
|
+
if (!standsAt(room.locator, prior, landing.from)) {
|
|
1753
1926
|
if (landing.kind === 'refreshed') {
|
|
1754
1927
|
await local.unrefresh(reached.key);
|
|
1755
1928
|
return false;
|
|
@@ -1765,11 +1938,9 @@ export function composeRuntime(options) {
|
|
|
1765
1938
|
...(landing.remodelled ? { model: reached.model } : {}),
|
|
1766
1939
|
}
|
|
1767
1940
|
: reached;
|
|
1768
|
-
adoptModels(room.locator, record);
|
|
1769
|
-
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
1770
1941
|
let set;
|
|
1771
1942
|
try {
|
|
1772
|
-
set = await settle(local, room.locator,
|
|
1943
|
+
set = await settle(local, room.locator, work, record, work.queue, landing.kind === 'refreshed'
|
|
1773
1944
|
? {
|
|
1774
1945
|
room: true,
|
|
1775
1946
|
view: 'refreshed',
|
|
@@ -1791,14 +1962,16 @@ export function composeRuntime(options) {
|
|
|
1791
1962
|
return true;
|
|
1792
1963
|
}
|
|
1793
1964
|
if (landing.kind === 'bootstrapped') {
|
|
1965
|
+
readied(room.locator);
|
|
1794
1966
|
highest.delete(room.locator);
|
|
1795
1967
|
beyondHorizon.delete(room.locator);
|
|
1796
1968
|
untransferred(room.locator);
|
|
1797
1969
|
startedOver.delete(room.locator);
|
|
1798
1970
|
catching.delete(room.locator);
|
|
1971
|
+
unheldModels.delete(room.locator);
|
|
1799
1972
|
if (prior !== undefined && prior.record.key !== record.key)
|
|
1800
1973
|
sweep();
|
|
1801
|
-
name(room.locator);
|
|
1974
|
+
void name(room.locator);
|
|
1802
1975
|
}
|
|
1803
1976
|
stale.delete(room.locator);
|
|
1804
1977
|
unpreparable.delete(room.locator);
|
|
@@ -1807,28 +1980,27 @@ export function composeRuntime(options) {
|
|
|
1807
1980
|
list(room, true);
|
|
1808
1981
|
publishChangeSet(set);
|
|
1809
1982
|
await unpair(local);
|
|
1810
|
-
moveTo(
|
|
1811
|
-
if (landing.kind === 'bootstrapped' &&
|
|
1983
|
+
moveTo(status);
|
|
1984
|
+
if (landing.kind === 'bootstrapped' && work.queue.length > 0)
|
|
1812
1985
|
submitLater(room.locator);
|
|
1813
|
-
}
|
|
1814
1986
|
return true;
|
|
1815
1987
|
});
|
|
1816
1988
|
return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
|
|
1817
1989
|
}
|
|
1818
1990
|
async function savedRoom(locator) {
|
|
1819
1991
|
const local = await openLocal();
|
|
1820
|
-
const replica =
|
|
1821
|
-
|
|
1822
|
-
if (replica === undefined || room === undefined) {
|
|
1992
|
+
const replica = await replicaOf(locator);
|
|
1993
|
+
if (replica === undefined || isFenced(locator)) {
|
|
1823
1994
|
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
1824
1995
|
}
|
|
1825
|
-
return { local, replica
|
|
1996
|
+
return { local, replica };
|
|
1826
1997
|
}
|
|
1827
1998
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
1828
1999
|
const target = await savedRoom(locator);
|
|
2000
|
+
const drafts = new Map((await draftsOf(locator, named.map(({ path }) => path))).map((record) => [record.path, record]));
|
|
1829
2001
|
const saved = [];
|
|
1830
2002
|
for (const { path, revision } of named) {
|
|
1831
|
-
const record =
|
|
2003
|
+
const record = drafts.get(path);
|
|
1832
2004
|
if (record === undefined || record.revision !== revision) {
|
|
1833
2005
|
return { outcome: 'refused', code: 'stale', retryable: false, file: path };
|
|
1834
2006
|
}
|
|
@@ -1855,14 +2027,16 @@ export function composeRuntime(options) {
|
|
|
1855
2027
|
}
|
|
1856
2028
|
return committed(locator, target, saved, new Set(), writtenBy, files);
|
|
1857
2029
|
}
|
|
1858
|
-
function carriedDrafts(locator, saved, consumed) {
|
|
1859
|
-
|
|
2030
|
+
async function carriedDrafts(locator, saved, consumed) {
|
|
2031
|
+
if (!saved.some(({ parts }) => parts.move !== undefined))
|
|
2032
|
+
return new Map();
|
|
2033
|
+
const holding = new Map((await draftsOf(locator)).map((record) => [record.path, record]));
|
|
1860
2034
|
return new Map(saved.flatMap(({ path, parts }) => {
|
|
1861
|
-
const record = holding
|
|
2035
|
+
const record = holding.get(path);
|
|
1862
2036
|
if (parts.move === undefined || record === undefined || consumed.has(path))
|
|
1863
2037
|
return [];
|
|
1864
2038
|
const to = moveTarget(parts.move);
|
|
1865
|
-
if (to === path || holding
|
|
2039
|
+
if (to === path || holding.has(to))
|
|
1866
2040
|
return [];
|
|
1867
2041
|
return [[path, { ...record, path: to, revision: roomDraftRevision() }]];
|
|
1868
2042
|
}));
|
|
@@ -1977,15 +2151,15 @@ export function composeRuntime(options) {
|
|
|
1977
2151
|
...new Set((await local.referrers(key, view, targets)).map(({ path }) => path)),
|
|
1978
2152
|
];
|
|
1979
2153
|
}
|
|
1980
|
-
async function sideOf(locator, replica,
|
|
2154
|
+
async function sideOf(locator, replica, saving) {
|
|
1981
2155
|
const record = replica.record;
|
|
1982
2156
|
return {
|
|
1983
2157
|
room: locator,
|
|
1984
|
-
handle:
|
|
2158
|
+
handle: record.handle,
|
|
1985
2159
|
baseRoomVersion: record.roomVersion,
|
|
1986
2160
|
composition: compositionFor(record),
|
|
1987
|
-
operations:
|
|
1988
|
-
providerOwnedPaths:
|
|
2161
|
+
operations: record.operations,
|
|
2162
|
+
providerOwnedPaths: record.providerOwnedPaths,
|
|
1989
2163
|
visible: saving.visible,
|
|
1990
2164
|
folders: saving.folders,
|
|
1991
2165
|
queued: queuedDocuments(replica.queue),
|
|
@@ -1998,12 +2172,10 @@ export function composeRuntime(options) {
|
|
|
1998
2172
|
return into?.parts.move?.room ?? null;
|
|
1999
2173
|
}
|
|
2000
2174
|
async function saveCommit(local, files, rooms) {
|
|
2001
|
-
const touched = new Map();
|
|
2002
2175
|
const commits = [];
|
|
2003
2176
|
for (const { locator, saving, before, queue } of rooms) {
|
|
2004
2177
|
const replayedLayer = await replayed(saving.record, saving.confirmed, queue);
|
|
2005
2178
|
const diff = await overlayDiff(saving.record, before, replayedLayer.visible, replayedLayer.touched);
|
|
2006
|
-
touched.set(locator, diff.touched);
|
|
2007
2179
|
commits.push({
|
|
2008
2180
|
locator,
|
|
2009
2181
|
key: saving.record.key,
|
|
@@ -2013,27 +2185,18 @@ export function composeRuntime(options) {
|
|
|
2013
2185
|
});
|
|
2014
2186
|
}
|
|
2015
2187
|
try {
|
|
2016
|
-
|
|
2188
|
+
return await local.commit({
|
|
2017
2189
|
rooms: commits,
|
|
2018
2190
|
writes: rooms.map(({ write }) => write),
|
|
2019
2191
|
files,
|
|
2020
2192
|
whole: true,
|
|
2021
2193
|
});
|
|
2022
|
-
return { answer, touched };
|
|
2023
2194
|
}
|
|
2024
2195
|
catch {
|
|
2025
2196
|
return null;
|
|
2026
2197
|
}
|
|
2027
2198
|
}
|
|
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) {
|
|
2199
|
+
async function committedMove(locator, destination, { local, replica }, saved, consumed, writtenBy, files) {
|
|
2037
2200
|
const mixed = saved.find(({ parts }) => Object.keys(parts).length !== 1 || parts.move?.room !== destination);
|
|
2038
2201
|
if (mixed !== undefined) {
|
|
2039
2202
|
return { outcome: 'refused', code: 'mixed-room-move', retryable: false, file: mixed.path };
|
|
@@ -2043,27 +2206,28 @@ export function composeRuntime(options) {
|
|
|
2043
2206
|
const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
|
|
2044
2207
|
if (changed.length > 0)
|
|
2045
2208
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2046
|
-
const target =
|
|
2047
|
-
|
|
2048
|
-
if (target === undefined || listedRoom === undefined) {
|
|
2209
|
+
const target = await replicaOf(destination);
|
|
2210
|
+
if (target === undefined || isFenced(destination)) {
|
|
2049
2211
|
throw runtimeError('unknown-room', `Unknown Room ${destination}`);
|
|
2050
2212
|
}
|
|
2051
2213
|
const arriving = await savingIn(target, [], moves.map(({ to }) => to));
|
|
2052
2214
|
const built = await buildMove({
|
|
2053
|
-
source: await sideOf(locator, replica,
|
|
2054
|
-
destination: await sideOf(destination, target,
|
|
2215
|
+
source: await sideOf(locator, replica, sourceSaving),
|
|
2216
|
+
destination: await sideOf(destination, target, arriving),
|
|
2055
2217
|
moves,
|
|
2056
|
-
providerLimits:
|
|
2218
|
+
providerLimits: (await local.facts()).providerLimits,
|
|
2057
2219
|
local: localSaves,
|
|
2058
2220
|
...(writtenBy === undefined ? {} : { writtenBy }),
|
|
2059
2221
|
});
|
|
2060
2222
|
if (built.kind === 'refused')
|
|
2061
2223
|
return { outcome: 'refused', ...built.refusal };
|
|
2062
2224
|
const id = built.leaving.id;
|
|
2225
|
+
const leavingDrafts = new Map((await draftsOf(locator, saved.map(({ path }) => path))).map((record) => [record.path, record]));
|
|
2226
|
+
const arrivingDrafts = new Set((await draftsOf(destination, moves.map(({ to }) => to))).map(({ path }) => path));
|
|
2063
2227
|
const carrying = saved.flatMap(({ path, parts }) => {
|
|
2064
|
-
const record =
|
|
2228
|
+
const record = leavingDrafts.get(path);
|
|
2065
2229
|
const to = moveTarget(parts.move);
|
|
2066
|
-
if (record === undefined || consumed.has(path) ||
|
|
2230
|
+
if (record === undefined || consumed.has(path) || arrivingDrafts.has(to)) {
|
|
2067
2231
|
return [];
|
|
2068
2232
|
}
|
|
2069
2233
|
const moved = {
|
|
@@ -2079,7 +2243,7 @@ export function composeRuntime(options) {
|
|
|
2079
2243
|
const left = [...consumed, ...carrying.map(({ from }) => from)];
|
|
2080
2244
|
const leavingQueue = [...replica.queue, built.leaving];
|
|
2081
2245
|
const arrivingQueue = [...target.queue, built.arriving];
|
|
2082
|
-
const
|
|
2246
|
+
const answer = await saveCommit(local, files, [
|
|
2083
2247
|
{
|
|
2084
2248
|
locator,
|
|
2085
2249
|
saving: sourceSaving,
|
|
@@ -2088,6 +2252,7 @@ export function composeRuntime(options) {
|
|
|
2088
2252
|
write: {
|
|
2089
2253
|
locator,
|
|
2090
2254
|
queue: leavingQueue,
|
|
2255
|
+
stored: replica.queue,
|
|
2091
2256
|
saved: new Map([[id, saved]]),
|
|
2092
2257
|
consumedRoomDrafts: left,
|
|
2093
2258
|
},
|
|
@@ -2100,37 +2265,23 @@ export function composeRuntime(options) {
|
|
|
2100
2265
|
write: {
|
|
2101
2266
|
locator: destination,
|
|
2102
2267
|
queue: arrivingQueue,
|
|
2268
|
+
stored: target.queue,
|
|
2103
2269
|
saved: new Map([[id, marked]]),
|
|
2104
2270
|
roomDrafts: carried,
|
|
2105
2271
|
},
|
|
2106
2272
|
},
|
|
2107
2273
|
]);
|
|
2108
|
-
if (
|
|
2274
|
+
if (answer === null)
|
|
2109
2275
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2110
|
-
const { answer, touched } = committedBoth;
|
|
2111
2276
|
if (answer.fenced !== null) {
|
|
2112
2277
|
throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
|
|
2113
2278
|
}
|
|
2114
|
-
if (
|
|
2279
|
+
if (isFenced(locator) || isFenced(destination)) {
|
|
2115
2280
|
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
2116
2281
|
}
|
|
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([
|
|
2282
|
+
preparedAfter(locator, answer);
|
|
2283
|
+
preparedAfter(destination, answer);
|
|
2284
|
+
const shownNow = publishChangeSet(mergeChangeSets([
|
|
2134
2285
|
changeSet(locator, {
|
|
2135
2286
|
documents: answer.changed[locator] ?? [],
|
|
2136
2287
|
queue: true,
|
|
@@ -2142,14 +2293,15 @@ export function composeRuntime(options) {
|
|
|
2142
2293
|
drafts: carried.map(({ path }) => path),
|
|
2143
2294
|
}),
|
|
2144
2295
|
]));
|
|
2145
|
-
queuedSave(locator);
|
|
2296
|
+
await Promise.all([shownNow, queuedSave(locator)]);
|
|
2146
2297
|
return { outcome: 'saved', identifier: id };
|
|
2147
2298
|
}
|
|
2148
2299
|
function queuedSave(locator) {
|
|
2149
2300
|
if (!localSaves)
|
|
2150
2301
|
recorded = false;
|
|
2151
|
-
moveTo(
|
|
2302
|
+
const shownNow = moveTo(localSaves ? status : { ...status, state: 'pending', resting: false });
|
|
2152
2303
|
submitLater(locator);
|
|
2304
|
+
return shownNow;
|
|
2153
2305
|
}
|
|
2154
2306
|
async function committed(locator, target, saved, consumed, writtenBy, files = []) {
|
|
2155
2307
|
if (full)
|
|
@@ -2158,7 +2310,8 @@ export function composeRuntime(options) {
|
|
|
2158
2310
|
if (into !== null) {
|
|
2159
2311
|
return committedMove(locator, into, target, saved, consumed, writtenBy, files);
|
|
2160
2312
|
}
|
|
2161
|
-
const { local, replica
|
|
2313
|
+
const { local, replica } = target;
|
|
2314
|
+
const { providerLimits } = await local.facts();
|
|
2162
2315
|
const parts = saved.map(({ path, parts: held }) => changesOf(path, held));
|
|
2163
2316
|
const changes = parts.flatMap(({ changes: made }) => made);
|
|
2164
2317
|
const saving = await savingIn(replica, changes, [
|
|
@@ -2168,7 +2321,7 @@ export function composeRuntime(options) {
|
|
|
2168
2321
|
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
|
|
2169
2322
|
if (changed.length > 0)
|
|
2170
2323
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2171
|
-
const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(
|
|
2324
|
+
const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(replica.states), saving.visibleOver));
|
|
2172
2325
|
if (requeue.kind === 'refused')
|
|
2173
2326
|
return { outcome: 'refused', ...requeue.refusal };
|
|
2174
2327
|
const foreign = [...requeue.withdrawn.keys()].some((origin) => {
|
|
@@ -2188,10 +2341,10 @@ export function composeRuntime(options) {
|
|
|
2188
2341
|
local: localSaves,
|
|
2189
2342
|
baseRoomVersion: replica.record.roomVersion,
|
|
2190
2343
|
composition: compositionFor(replica.record),
|
|
2191
|
-
operations:
|
|
2192
|
-
providerOwnedPaths:
|
|
2193
|
-
providerLimits
|
|
2194
|
-
handle:
|
|
2344
|
+
operations: replica.record.operations,
|
|
2345
|
+
providerOwnedPaths: replica.record.providerOwnedPaths,
|
|
2346
|
+
providerLimits,
|
|
2347
|
+
handle: replica.record.handle,
|
|
2195
2348
|
visible: saving.visible,
|
|
2196
2349
|
folders: saving.folders,
|
|
2197
2350
|
changes,
|
|
@@ -2243,7 +2396,7 @@ export function composeRuntime(options) {
|
|
|
2243
2396
|
for (const [origin, replaced] of withdrawn) {
|
|
2244
2397
|
if (replaced === null)
|
|
2245
2398
|
continue;
|
|
2246
|
-
const carried = withdrawnDrafts(
|
|
2399
|
+
const carried = withdrawnDrafts(replica.states.get(origin)?.drafts ?? [], changes);
|
|
2247
2400
|
drafted.set(replaced.id, replaced === join?.batch
|
|
2248
2401
|
? [
|
|
2249
2402
|
...carried.map((held) => joinedDraft(held, saved)),
|
|
@@ -2257,14 +2410,14 @@ export function composeRuntime(options) {
|
|
|
2257
2410
|
for (const replaced of withdrawn.values()) {
|
|
2258
2411
|
if (replaced === null || !batches.includes(replaced))
|
|
2259
2412
|
continue;
|
|
2260
|
-
const limited = await stepped(limiting(batches, replaced,
|
|
2413
|
+
const limited = await stepped(limiting(batches, replaced, providerLimits, saving.visibleOver));
|
|
2261
2414
|
if (limited !== null)
|
|
2262
2415
|
return { outcome: 'refused', ...limited };
|
|
2263
2416
|
}
|
|
2264
|
-
const carried = carriedDrafts(locator, saved, consumed);
|
|
2417
|
+
const carried = await carriedDrafts(locator, saved, consumed);
|
|
2265
2418
|
const left = [...consumed, ...carried.keys()];
|
|
2266
2419
|
const arrived = [...carried.values()];
|
|
2267
|
-
const
|
|
2420
|
+
const answer = await saveCommit(local, files, [
|
|
2268
2421
|
{
|
|
2269
2422
|
locator,
|
|
2270
2423
|
saving,
|
|
@@ -2273,40 +2426,27 @@ export function composeRuntime(options) {
|
|
|
2273
2426
|
write: {
|
|
2274
2427
|
locator,
|
|
2275
2428
|
queue: batches,
|
|
2429
|
+
stored: replica.queue,
|
|
2276
2430
|
saved: drafted,
|
|
2277
2431
|
consumedRoomDrafts: left,
|
|
2278
2432
|
roomDrafts: arrived,
|
|
2279
2433
|
},
|
|
2280
2434
|
},
|
|
2281
2435
|
]);
|
|
2282
|
-
if (
|
|
2436
|
+
if (answer === null)
|
|
2283
2437
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2284
|
-
const { answer, touched } = done;
|
|
2285
2438
|
if (answer.fenced !== null) {
|
|
2286
2439
|
throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
|
|
2287
2440
|
}
|
|
2288
|
-
if (
|
|
2441
|
+
if (isFenced(locator))
|
|
2289
2442
|
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, {
|
|
2443
|
+
preparedAfter(locator, answer);
|
|
2444
|
+
const shownNow = publishChangeSet(changeSet(locator, {
|
|
2305
2445
|
documents: answer.changed[locator] ?? [],
|
|
2306
2446
|
queue: true,
|
|
2307
2447
|
drafts: [...left, ...arrived.map(({ path }) => path)],
|
|
2308
2448
|
}));
|
|
2309
|
-
queuedSave(locator);
|
|
2449
|
+
await Promise.all([shownNow, queuedSave(locator)]);
|
|
2310
2450
|
const identifier = join?.batch.id ??
|
|
2311
2451
|
built?.batch.id ??
|
|
2312
2452
|
[...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
|
|
@@ -2315,29 +2455,26 @@ export function composeRuntime(options) {
|
|
|
2315
2455
|
async function mark(local, locator, batch, submission, lost) {
|
|
2316
2456
|
const partner = partnerOf(batch, locator);
|
|
2317
2457
|
for (const held of partner === null ? [locator] : [locator, partner]) {
|
|
2318
|
-
const state =
|
|
2458
|
+
const { state, room } = await local.pending(held, batch.id);
|
|
2319
2459
|
if (!continuing() ||
|
|
2320
2460
|
state === undefined ||
|
|
2321
2461
|
(JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
|
|
2322
2462
|
continue;
|
|
2323
2463
|
}
|
|
2324
2464
|
await local.submit(held, batch.id, submission, lost);
|
|
2325
|
-
if (
|
|
2465
|
+
if (isFenced(held))
|
|
2326
2466
|
continue;
|
|
2327
|
-
|
|
2328
|
-
if (replicas.has(held))
|
|
2467
|
+
if (room)
|
|
2329
2468
|
publishChangeSet(changeSet(held, { queue: true }));
|
|
2330
2469
|
}
|
|
2331
|
-
moveTo(
|
|
2470
|
+
moveTo(status);
|
|
2332
2471
|
}
|
|
2333
2472
|
async function keepUpload(local, locator, identifier, upload) {
|
|
2334
2473
|
if (!continuing())
|
|
2335
2474
|
throw new Error('connection refused');
|
|
2336
2475
|
await local.keepUpload(locator, identifier, upload);
|
|
2337
|
-
if (
|
|
2476
|
+
if (isFenced(locator))
|
|
2338
2477
|
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
2339
|
-
const key = pendingKey(locator, identifier);
|
|
2340
|
-
pendings.set(key, { ...pendings.get(key), upload });
|
|
2341
2478
|
if (!continuing())
|
|
2342
2479
|
throw new Error('connection refused');
|
|
2343
2480
|
}
|
|
@@ -2373,7 +2510,7 @@ export function composeRuntime(options) {
|
|
|
2373
2510
|
if (fencedSince(locator, since))
|
|
2374
2511
|
return failure({ kind: 'room-not-readable', locator });
|
|
2375
2512
|
if (result.kind === 'scoped-refusal') {
|
|
2376
|
-
const room =
|
|
2513
|
+
const room = await recordAt(result.locator);
|
|
2377
2514
|
if (room !== undefined)
|
|
2378
2515
|
await scoped(result, room, await openLocal());
|
|
2379
2516
|
}
|
|
@@ -2386,27 +2523,28 @@ export function composeRuntime(options) {
|
|
|
2386
2523
|
return UNCERTAIN;
|
|
2387
2524
|
}
|
|
2388
2525
|
}
|
|
2389
|
-
function located(room, path, destination) {
|
|
2526
|
+
async function located(room, path, destination) {
|
|
2390
2527
|
const crossing = roomTarget(destination);
|
|
2528
|
+
const listing = await listingNow();
|
|
2391
2529
|
if (crossing === null) {
|
|
2392
2530
|
const named = target(path, destination);
|
|
2393
|
-
return named === null || !
|
|
2531
|
+
return named === null || !listing.has(room) ? null : { room, path: named };
|
|
2394
2532
|
}
|
|
2395
|
-
const held = [...
|
|
2533
|
+
const held = [...listing.values()].find((listed) => listed.handle === crossing.handle);
|
|
2396
2534
|
return held === undefined ? null : { room: held.locator, path: crossing.path };
|
|
2397
2535
|
}
|
|
2398
2536
|
async function resolveReference(room, path, destination) {
|
|
2399
2537
|
if (roomTarget(destination) === null && target(path, destination) === null) {
|
|
2400
2538
|
return ABSENT_TARGET;
|
|
2401
2539
|
}
|
|
2402
|
-
const at = located(room, path, destination);
|
|
2540
|
+
const at = await located(room, path, destination);
|
|
2403
2541
|
if (at === null)
|
|
2404
2542
|
return UNREADABLE;
|
|
2405
|
-
if (locked(at.room))
|
|
2543
|
+
if (await locked(at.room))
|
|
2406
2544
|
return LOCKED_UNREADABLE;
|
|
2407
|
-
const held =
|
|
2545
|
+
const held = await recordAt(at.room);
|
|
2408
2546
|
if (held !== undefined &&
|
|
2409
|
-
(await (await openLocal()).entries(held.
|
|
2547
|
+
(await (await openLocal()).entries(held.key, 'visible', [at.path])).length > 0) {
|
|
2410
2548
|
return { kind: 'document', ...at };
|
|
2411
2549
|
}
|
|
2412
2550
|
const history = await readHistory(at.room, at.path);
|
|
@@ -2421,13 +2559,13 @@ export function composeRuntime(options) {
|
|
|
2421
2559
|
? { kind: 'deleted', ...at, revision: last }
|
|
2422
2560
|
: { kind: 'document', ...at };
|
|
2423
2561
|
}
|
|
2424
|
-
function queuedFacts(locator, path, localId) {
|
|
2425
|
-
for (const batch of
|
|
2562
|
+
async function queuedFacts(locator, path, localId) {
|
|
2563
|
+
for (const batch of (await replicaOf(locator))?.queue ?? []) {
|
|
2426
2564
|
const change = batch.changes[queuedFile(batch, path, localId)];
|
|
2427
2565
|
if (change?.kind === 'binary')
|
|
2428
2566
|
return change;
|
|
2429
2567
|
}
|
|
2430
|
-
const drafted =
|
|
2568
|
+
const drafted = (await draftOf(locator, path))?.parts.file;
|
|
2431
2569
|
return drafted?.localId === localId ? drafted : null;
|
|
2432
2570
|
}
|
|
2433
2571
|
function batchFile(batch) {
|
|
@@ -2459,7 +2597,7 @@ export function composeRuntime(options) {
|
|
|
2459
2597
|
if (fencedSince(request.locator, since))
|
|
2460
2598
|
return failure({ kind: 'room-not-readable', locator: request.locator });
|
|
2461
2599
|
if (settled.kind === 'scoped-refusal') {
|
|
2462
|
-
const room =
|
|
2600
|
+
const room = await recordAt(settled.locator);
|
|
2463
2601
|
if (room !== undefined)
|
|
2464
2602
|
await scoped(settled, room, await openLocal());
|
|
2465
2603
|
}
|
|
@@ -2486,7 +2624,7 @@ export function composeRuntime(options) {
|
|
|
2486
2624
|
async function binaryPath(locator, path, revision) {
|
|
2487
2625
|
if (revision !== undefined)
|
|
2488
2626
|
return { kind: 'path', locator, path, revision };
|
|
2489
|
-
const replica =
|
|
2627
|
+
const replica = await replicaOf(locator);
|
|
2490
2628
|
const local = await openLocal();
|
|
2491
2629
|
const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
|
|
2492
2630
|
const attached = held === undefined ? attachedFiles.get(locator)?.get(path) : undefined;
|
|
@@ -2526,13 +2664,13 @@ export function composeRuntime(options) {
|
|
|
2526
2664
|
}
|
|
2527
2665
|
async function askedBinary(locator, path, revision) {
|
|
2528
2666
|
await ready;
|
|
2529
|
-
const drafted =
|
|
2667
|
+
const drafted = (await draftOf(locator, path))?.parts.file;
|
|
2530
2668
|
return drafted !== undefined && drafted.localId === revision
|
|
2531
2669
|
? { kind: 'pending', localId: revision }
|
|
2532
2670
|
: binaryPath(locator, path, revision);
|
|
2533
2671
|
}
|
|
2534
|
-
function pendingBinary(locator, path, localId) {
|
|
2535
|
-
const facts = queuedFacts(locator, path, localId);
|
|
2672
|
+
async function pendingBinary(locator, path, localId) {
|
|
2673
|
+
const facts = await queuedFacts(locator, path, localId);
|
|
2536
2674
|
return facts === null
|
|
2537
2675
|
? absentBinary(path)
|
|
2538
2676
|
: {
|
|
@@ -2551,10 +2689,10 @@ export function composeRuntime(options) {
|
|
|
2551
2689
|
return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
|
|
2552
2690
|
}
|
|
2553
2691
|
async function heldMediaType(locator, path) {
|
|
2554
|
-
const replica =
|
|
2692
|
+
const replica = await recordAt(locator);
|
|
2555
2693
|
const [held] = replica === undefined
|
|
2556
2694
|
? []
|
|
2557
|
-
: await (await openLocal()).entries(replica.
|
|
2695
|
+
: await (await openLocal()).entries(replica.key, 'visible', [path]);
|
|
2558
2696
|
return held === undefined
|
|
2559
2697
|
? attachedFiles.get(locator)?.get(path)?.mediaType
|
|
2560
2698
|
: held.binary?.mediaType;
|
|
@@ -2589,7 +2727,7 @@ export function composeRuntime(options) {
|
|
|
2589
2727
|
async function attachBinary(locator, file) {
|
|
2590
2728
|
await ready;
|
|
2591
2729
|
await firstDiscovery;
|
|
2592
|
-
if (closed || !roomsNamed ||
|
|
2730
|
+
if (closed || !roomsNamed || (await connectionNow()) === 'unreachable' || refusal !== null)
|
|
2593
2731
|
return OFFLINE;
|
|
2594
2732
|
const request = await attachedFile(locator, file);
|
|
2595
2733
|
return transferBinary({ locator, path: file.name }, async (held) => {
|
|
@@ -2609,8 +2747,8 @@ export function composeRuntime(options) {
|
|
|
2609
2747
|
const freed = await database.ask({ kind: 'opened.clean' });
|
|
2610
2748
|
return freed + ((await options.shell.cleanUp?.()) ?? 0);
|
|
2611
2749
|
}
|
|
2612
|
-
function declaredAction(name) {
|
|
2613
|
-
for (const capability of capabilities) {
|
|
2750
|
+
async function declaredAction(name) {
|
|
2751
|
+
for (const capability of (await (await openLocal()).facts()).capabilities) {
|
|
2614
2752
|
for (const action of capability.actions ?? []) {
|
|
2615
2753
|
if (action.name === name)
|
|
2616
2754
|
return action;
|
|
@@ -2618,8 +2756,8 @@ export function composeRuntime(options) {
|
|
|
2618
2756
|
}
|
|
2619
2757
|
return undefined;
|
|
2620
2758
|
}
|
|
2621
|
-
function calledRooms(name, input) {
|
|
2622
|
-
const member = declaredAction(name)?.room;
|
|
2759
|
+
async function calledRooms(name, input) {
|
|
2760
|
+
const member = (await declaredAction(name))?.room;
|
|
2623
2761
|
if (member === undefined || input === null || typeof input !== 'object')
|
|
2624
2762
|
return [];
|
|
2625
2763
|
const named = input[member];
|
|
@@ -2629,7 +2767,7 @@ export function composeRuntime(options) {
|
|
|
2629
2767
|
await ready;
|
|
2630
2768
|
if (closed || refusal !== null)
|
|
2631
2769
|
return UNCERTAIN;
|
|
2632
|
-
if (declaredAction(name) === undefined)
|
|
2770
|
+
if ((await declaredAction(name)) === undefined)
|
|
2633
2771
|
return undeclared(name);
|
|
2634
2772
|
try {
|
|
2635
2773
|
await ensureAuthorization();
|
|
@@ -2644,7 +2782,7 @@ export function composeRuntime(options) {
|
|
|
2644
2782
|
try {
|
|
2645
2783
|
const result = await options.adapter.action(authorization, { name, input });
|
|
2646
2784
|
if (result.kind === 'scoped-refusal') {
|
|
2647
|
-
const room =
|
|
2785
|
+
const room = await recordAt(result.locator);
|
|
2648
2786
|
if (room !== undefined)
|
|
2649
2787
|
await scoped(result, room, await openLocal());
|
|
2650
2788
|
}
|
|
@@ -2731,31 +2869,35 @@ export function composeRuntime(options) {
|
|
|
2731
2869
|
function continuing() {
|
|
2732
2870
|
return !closed && !discoveryPaused;
|
|
2733
2871
|
}
|
|
2734
|
-
function reachedBy(locator, roomVersion) {
|
|
2735
|
-
const held =
|
|
2736
|
-
return
|
|
2872
|
+
async function reachedBy(locator, roomVersion) {
|
|
2873
|
+
const held = await recordAt(locator);
|
|
2874
|
+
return held !== undefined && options.adapter.reached(locator, held.roomVersion, roomVersion);
|
|
2737
2875
|
}
|
|
2738
|
-
function shownAt(locator) {
|
|
2739
|
-
const held = replicas.get(locator)?.record.roomVersion;
|
|
2876
|
+
function shownAt(locator, held) {
|
|
2740
2877
|
const raised = highest.get(locator);
|
|
2741
2878
|
if (held === undefined || raised === undefined)
|
|
2742
2879
|
return held ?? raised;
|
|
2743
2880
|
return options.adapter.reached(locator, held, raised) ? held : raised;
|
|
2744
2881
|
}
|
|
2745
|
-
function showing(locator, roomVersion) {
|
|
2746
|
-
const at = shownAt(locator);
|
|
2882
|
+
function showing(locator, roomVersion, held) {
|
|
2883
|
+
const at = shownAt(locator, held);
|
|
2747
2884
|
if (at === undefined || !options.adapter.reached(locator, at, roomVersion)) {
|
|
2748
2885
|
highest.set(locator, roomVersion);
|
|
2749
2886
|
}
|
|
2750
2887
|
}
|
|
2751
|
-
function
|
|
2888
|
+
function olderThan(locator, roomVersion, held) {
|
|
2752
2889
|
if (beyondHorizon.has(locator))
|
|
2753
2890
|
return false;
|
|
2754
|
-
const at = shownAt(locator);
|
|
2891
|
+
const at = shownAt(locator, held);
|
|
2755
2892
|
return (at !== undefined &&
|
|
2756
2893
|
options.adapter.reached(locator, at, roomVersion) &&
|
|
2757
2894
|
!options.adapter.reached(locator, roomVersion, at));
|
|
2758
2895
|
}
|
|
2896
|
+
async function older(locator, roomVersion) {
|
|
2897
|
+
if (beyondHorizon.has(locator))
|
|
2898
|
+
return false;
|
|
2899
|
+
return olderThan(locator, roomVersion, (await recordAt(locator))?.roomVersion);
|
|
2900
|
+
}
|
|
2759
2901
|
function replaced(locators) {
|
|
2760
2902
|
for (const locator of locators) {
|
|
2761
2903
|
highest.delete(locator);
|
|
@@ -2763,17 +2905,22 @@ export function composeRuntime(options) {
|
|
|
2763
2905
|
}
|
|
2764
2906
|
}
|
|
2765
2907
|
async function writtenAfter(locator, roomVersion, path) {
|
|
2766
|
-
const held =
|
|
2908
|
+
const held = await recordAt(locator);
|
|
2767
2909
|
if (held === undefined)
|
|
2768
2910
|
return false;
|
|
2769
|
-
const [entry] = await (await openLocal()).entries(held.
|
|
2911
|
+
const [entry] = await (await openLocal()).entries(held.key, 'confirmed', [path]);
|
|
2770
2912
|
const written = entry?.written ?? undefined;
|
|
2771
2913
|
return (written !== undefined &&
|
|
2772
2914
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
2773
2915
|
!options.adapter.reached(locator, roomVersion, written));
|
|
2774
2916
|
}
|
|
2775
|
-
async function writtenLater(local, locator, key, paths, roomVersion) {
|
|
2776
|
-
if (paths.length === 0
|
|
2917
|
+
async function writtenLater(local, locator, key, paths, roomVersion, heldAt) {
|
|
2918
|
+
if (paths.length === 0)
|
|
2919
|
+
return new Set();
|
|
2920
|
+
const behindShown = heldAt === undefined
|
|
2921
|
+
? await older(locator, roomVersion)
|
|
2922
|
+
: olderThan(locator, roomVersion, heldAt);
|
|
2923
|
+
if (!behindShown)
|
|
2777
2924
|
return new Set();
|
|
2778
2925
|
const entries = await local.entries(key, 'confirmed', paths);
|
|
2779
2926
|
return new Set(entries.flatMap(({ path, written }) => written !== null &&
|
|
@@ -2782,7 +2929,7 @@ export function composeRuntime(options) {
|
|
|
2782
2929
|
? [path]
|
|
2783
2930
|
: []));
|
|
2784
2931
|
}
|
|
2785
|
-
function refreshingOf(local, locator, key, composition, liveness, replaying = false) {
|
|
2932
|
+
function refreshingOf(local, locator, key, composition, liveness, replaying = false, heldAt) {
|
|
2786
2933
|
const write = replaying ? finishingWrite : exclusive;
|
|
2787
2934
|
return {
|
|
2788
2935
|
turn() {
|
|
@@ -2790,7 +2937,7 @@ export function composeRuntime(options) {
|
|
|
2790
2937
|
installing(locator);
|
|
2791
2938
|
},
|
|
2792
2939
|
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),
|
|
2940
|
+
later: (paths, roomVersion) => writtenLater(local, locator, key, paths, roomVersion, heldAt),
|
|
2794
2941
|
async stage(page) {
|
|
2795
2942
|
for (let written = 0;;) {
|
|
2796
2943
|
const end = batchEnd(page.entries, written);
|
|
@@ -2928,7 +3075,7 @@ export function composeRuntime(options) {
|
|
|
2928
3075
|
unindexed.delete(locator);
|
|
2929
3076
|
}
|
|
2930
3077
|
function setBack(locator) {
|
|
2931
|
-
const now = bytesOf(locator, true);
|
|
3078
|
+
const now = bytesOf(locator, true, new Map());
|
|
2932
3079
|
const earlier = startedOver.get(locator);
|
|
2933
3080
|
startedOver.set(locator, {
|
|
2934
3081
|
done: Math.max(now.done, earlier?.done ?? 0),
|
|
@@ -2964,11 +3111,13 @@ export function composeRuntime(options) {
|
|
|
2964
3111
|
if (installs.delete(locator))
|
|
2965
3112
|
queueReadiness();
|
|
2966
3113
|
}
|
|
2967
|
-
async function pullRefresh(authorized,
|
|
3114
|
+
async function pullRefresh(authorized, record, liveness, hold, refresh) {
|
|
2968
3115
|
if (!liveness())
|
|
2969
3116
|
return unconfirmed;
|
|
2970
|
-
const
|
|
2971
|
-
|
|
3117
|
+
const from = {
|
|
3118
|
+
key: record.key,
|
|
3119
|
+
confirmed: confirmations.get(record.locator) ?? 0,
|
|
3120
|
+
};
|
|
2972
3121
|
const local = await openLocal();
|
|
2973
3122
|
try {
|
|
2974
3123
|
await exclusive(() => local.unrefresh(record.key));
|
|
@@ -2980,7 +3129,7 @@ export function composeRuntime(options) {
|
|
|
2980
3129
|
finally {
|
|
2981
3130
|
hold.take();
|
|
2982
3131
|
}
|
|
2983
|
-
}, authorized, record, (models, kept) => (liveness() ? remodel(
|
|
3132
|
+
}, authorized, record, (models, kept) => (liveness() ? remodel(models, kept) : kept), liveness, refreshingOf(local, record.locator, record.key, composed(record), liveness, false, record.roomVersion));
|
|
2984
3133
|
if (result.kind !== 'advanced') {
|
|
2985
3134
|
journal.record(`transfer refresh ${record.locator} ${result.kind}`);
|
|
2986
3135
|
return result;
|
|
@@ -3000,30 +3149,14 @@ export function composeRuntime(options) {
|
|
|
3000
3149
|
throw error;
|
|
3001
3150
|
}
|
|
3002
3151
|
}
|
|
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
3152
|
async function abandoned(local, locator) {
|
|
3020
3153
|
await local.abandon(locator);
|
|
3021
3154
|
setBack(locator);
|
|
3022
3155
|
untransferred(locator);
|
|
3023
|
-
|
|
3156
|
+
recoverReads();
|
|
3024
3157
|
sweep();
|
|
3025
3158
|
}
|
|
3026
|
-
function staging(local, liveness) {
|
|
3159
|
+
function staging(local, liveness, unheld) {
|
|
3027
3160
|
const derived = async (locator, key, composition) => {
|
|
3028
3161
|
underived.delete(locator);
|
|
3029
3162
|
for (;;) {
|
|
@@ -3093,9 +3226,9 @@ export function composeRuntime(options) {
|
|
|
3093
3226
|
const held = { ...record, key };
|
|
3094
3227
|
transfers.set(locator, held);
|
|
3095
3228
|
if (arrived)
|
|
3096
|
-
|
|
3229
|
+
recoverReads();
|
|
3097
3230
|
installing(locator);
|
|
3098
|
-
|
|
3231
|
+
recoverReads();
|
|
3099
3232
|
const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
|
|
3100
3233
|
owedIndexes(locator, record.done.bytes - prior, text);
|
|
3101
3234
|
const owed = text + (underived.get(locator) ?? 0);
|
|
@@ -3104,6 +3237,37 @@ export function composeRuntime(options) {
|
|
|
3104
3237
|
return held;
|
|
3105
3238
|
return (await derived(locator, key, composition)) ? held : null;
|
|
3106
3239
|
},
|
|
3240
|
+
async resume(record, parts, composition) {
|
|
3241
|
+
const { locator } = record;
|
|
3242
|
+
installing(locator);
|
|
3243
|
+
if (!liveness())
|
|
3244
|
+
return null;
|
|
3245
|
+
const prior = unheld === undefined ? await recordAt(locator) : undefined;
|
|
3246
|
+
const published = prior !== undefined && prior.replica.selection === record.selection ? prior.key : null;
|
|
3247
|
+
const kept = unheld?.staged ?? (await local.held()).staged.has(locator);
|
|
3248
|
+
if (published === null && !kept)
|
|
3249
|
+
return { record: null, staged: new Set() };
|
|
3250
|
+
const resumed = await installWrite(async () => {
|
|
3251
|
+
if (!liveness())
|
|
3252
|
+
return null;
|
|
3253
|
+
return local.resume(record, composition, parts.map((part) => part.id), published);
|
|
3254
|
+
});
|
|
3255
|
+
if (resumed === null || !liveness())
|
|
3256
|
+
return null;
|
|
3257
|
+
const parted = new Set(resumed.staged);
|
|
3258
|
+
if (resumed.key === null)
|
|
3259
|
+
return { record: null, staged: parted };
|
|
3260
|
+
const bytes = parts.reduce((sum, part) => sum + (parted.has(part.id) ? part.bytes : 0), 0);
|
|
3261
|
+
const held = {
|
|
3262
|
+
...record,
|
|
3263
|
+
transfer: resumed.transfer,
|
|
3264
|
+
key: resumed.key,
|
|
3265
|
+
done: { bytes },
|
|
3266
|
+
};
|
|
3267
|
+
transfers.set(locator, held);
|
|
3268
|
+
recoverReads();
|
|
3269
|
+
return { record: held, staged: parted };
|
|
3270
|
+
},
|
|
3107
3271
|
derive: derived,
|
|
3108
3272
|
async refreshing(locator, key, composition) {
|
|
3109
3273
|
await exclusive(() => local.unrefresh(key));
|
|
@@ -3118,33 +3282,26 @@ export function composeRuntime(options) {
|
|
|
3118
3282
|
return;
|
|
3119
3283
|
setBack(locator);
|
|
3120
3284
|
untransferred(locator);
|
|
3121
|
-
|
|
3285
|
+
recoverReads();
|
|
3122
3286
|
},
|
|
3123
3287
|
release: (key) => exclusive(async () => {
|
|
3124
3288
|
await local.release(key);
|
|
3125
3289
|
sweep();
|
|
3126
3290
|
}),
|
|
3127
|
-
content: (key) => heldContentOf(local, key),
|
|
3128
3291
|
};
|
|
3129
3292
|
}
|
|
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) {
|
|
3293
|
+
async function pullBootstrap(authorized, room, local, liveness, prior, from = reads, unheld) {
|
|
3137
3294
|
if (!liveness())
|
|
3138
3295
|
return unconfirmed;
|
|
3139
|
-
if (
|
|
3296
|
+
if (prior !== undefined) {
|
|
3140
3297
|
catching.add(room.locator);
|
|
3141
|
-
|
|
3298
|
+
recoverReads();
|
|
3142
3299
|
}
|
|
3143
3300
|
try {
|
|
3144
|
-
const result = await bootstrapRoom(
|
|
3301
|
+
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
3302
|
const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
|
|
3146
3303
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
3147
|
-
if (result.kind === 'staged' && older(room.locator, result.record.roomVersion)) {
|
|
3304
|
+
if (result.kind === 'staged' && (await older(room.locator, result.record.roomVersion))) {
|
|
3148
3305
|
caughtUpTo(room.locator);
|
|
3149
3306
|
installed(room.locator);
|
|
3150
3307
|
await abandoned(local, room.locator);
|
|
@@ -3168,28 +3325,31 @@ export function composeRuntime(options) {
|
|
|
3168
3325
|
}
|
|
3169
3326
|
function caughtUpTo(locator) {
|
|
3170
3327
|
if (catching.delete(locator))
|
|
3171
|
-
|
|
3328
|
+
recoverReads();
|
|
3172
3329
|
}
|
|
3173
|
-
function behind(room) {
|
|
3174
|
-
const held =
|
|
3330
|
+
async function behind(room) {
|
|
3331
|
+
const held = (await recordAt(room.locator))?.roomVersion;
|
|
3175
3332
|
return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
|
|
3176
3333
|
}
|
|
3177
|
-
function
|
|
3178
|
-
|
|
3179
|
-
|
|
3334
|
+
function standingFrom(locator, held) {
|
|
3335
|
+
return { key: held?.record.key ?? null, confirmed: confirmations.get(locator) ?? 0 };
|
|
3336
|
+
}
|
|
3337
|
+
function standingAt(locator, held) {
|
|
3338
|
+
return { key: held?.key ?? null, confirmed: confirmations.get(locator) ?? 0 };
|
|
3180
3339
|
}
|
|
3181
|
-
function
|
|
3182
|
-
const now =
|
|
3340
|
+
function standsAt(locator, held, from) {
|
|
3341
|
+
const now = standingFrom(locator, held);
|
|
3183
3342
|
return now.key === from.key && now.confirmed === from.confirmed;
|
|
3184
3343
|
}
|
|
3185
|
-
async function advance(room, local, authorized, running) {
|
|
3344
|
+
async function advance(room, local, authorized, running, unheld) {
|
|
3186
3345
|
const live = () => running() && !full;
|
|
3187
3346
|
if (!live())
|
|
3188
3347
|
return unconfirmed;
|
|
3189
|
-
const held =
|
|
3190
|
-
if (
|
|
3348
|
+
const held = unheld === undefined ? await recordAt(room.locator) : undefined;
|
|
3349
|
+
if (options.adapter.roomStream !== undefined &&
|
|
3350
|
+
(held !== undefined || !beyondHorizon.has(room.locator))) {
|
|
3191
3351
|
stale.delete(room.locator);
|
|
3192
|
-
name(room.locator);
|
|
3352
|
+
await name(room.locator);
|
|
3193
3353
|
return null;
|
|
3194
3354
|
}
|
|
3195
3355
|
const refresh = reads.refresh;
|
|
@@ -3203,15 +3363,16 @@ export function composeRuntime(options) {
|
|
|
3203
3363
|
return unconfirmed;
|
|
3204
3364
|
replaced([room.locator]);
|
|
3205
3365
|
}
|
|
3206
|
-
const
|
|
3207
|
-
const
|
|
3366
|
+
const prior = held === undefined ? undefined : await recordAt(room.locator);
|
|
3367
|
+
const from = standingAt(room.locator, prior);
|
|
3368
|
+
const bootstrapped = await pullBootstrap(authorized, room, local, live, prior, reads, unheld);
|
|
3208
3369
|
if (bootstrapped.kind !== 'staged')
|
|
3209
3370
|
return live() ? bootstrapped : unconfirmed;
|
|
3210
3371
|
const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
|
|
3211
3372
|
if (await publish(local, room, bootstrapped.record, landing, live)) {
|
|
3212
3373
|
return live() ? null : unconfirmed;
|
|
3213
3374
|
}
|
|
3214
|
-
return reachedBy(room.locator, bootstrapped.record.roomVersion)
|
|
3375
|
+
return (await reachedBy(room.locator, bootstrapped.record.roomVersion))
|
|
3215
3376
|
? null
|
|
3216
3377
|
: advance(room, local, authorized, live);
|
|
3217
3378
|
}
|
|
@@ -3235,12 +3396,12 @@ export function composeRuntime(options) {
|
|
|
3235
3396
|
remodelled: refreshed.remodelled,
|
|
3236
3397
|
};
|
|
3237
3398
|
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3238
|
-
const now =
|
|
3399
|
+
const now = (await recordAt(room.locator))?.roomVersion;
|
|
3239
3400
|
const reached = now !== undefined &&
|
|
3240
3401
|
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
3241
3402
|
return reached ? null : 'behind';
|
|
3242
3403
|
}
|
|
3243
|
-
await retainModel(local, room,
|
|
3404
|
+
await retainModel(local, room, null);
|
|
3244
3405
|
return null;
|
|
3245
3406
|
}
|
|
3246
3407
|
return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
|
|
@@ -3248,6 +3409,11 @@ export function composeRuntime(options) {
|
|
|
3248
3409
|
async function withhold(withheld, room, local, progress) {
|
|
3249
3410
|
if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
|
|
3250
3411
|
return;
|
|
3412
|
+
if (withheld.kind === 'not-ready') {
|
|
3413
|
+
progress.complete = false;
|
|
3414
|
+
askAgain(room.locator);
|
|
3415
|
+
return;
|
|
3416
|
+
}
|
|
3251
3417
|
stale.add(room.locator);
|
|
3252
3418
|
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
3253
3419
|
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
@@ -3274,6 +3440,37 @@ export function composeRuntime(options) {
|
|
|
3274
3440
|
await refuse('revoked-authorization');
|
|
3275
3441
|
}
|
|
3276
3442
|
}
|
|
3443
|
+
function askAgain(locator) {
|
|
3444
|
+
const held = unready.get(locator) ?? { tries: 0, timer: null };
|
|
3445
|
+
unready.set(locator, held);
|
|
3446
|
+
if (held.timer !== null)
|
|
3447
|
+
return;
|
|
3448
|
+
const wait = NOT_READY_MS[Math.min(held.tries, NOT_READY_MS.length - 1)];
|
|
3449
|
+
held.tries += 1;
|
|
3450
|
+
held.timer = setTimeout(() => {
|
|
3451
|
+
held.timer = null;
|
|
3452
|
+
const room = discovered.get(locator);
|
|
3453
|
+
const unstreamed = options.adapter.roomStream === undefined;
|
|
3454
|
+
if (closed || stopping || (!unstreamed && !socketOpen) || room === undefined)
|
|
3455
|
+
return;
|
|
3456
|
+
const progress = { complete: true, reachable: true };
|
|
3457
|
+
void openLocal()
|
|
3458
|
+
.then((local) => roomTask(locator, (live) => loadRoom(room, local, progress, live)))
|
|
3459
|
+
.then(() => rest(progress), () => undefined);
|
|
3460
|
+
}, wait);
|
|
3461
|
+
}
|
|
3462
|
+
function readied(locator) {
|
|
3463
|
+
const held = unready.get(locator);
|
|
3464
|
+
if (held === undefined)
|
|
3465
|
+
return;
|
|
3466
|
+
if (held.timer !== null)
|
|
3467
|
+
clearTimeout(held.timer);
|
|
3468
|
+
unready.delete(locator);
|
|
3469
|
+
}
|
|
3470
|
+
function unreadied() {
|
|
3471
|
+
for (const locator of [...unready.keys()])
|
|
3472
|
+
readied(locator);
|
|
3473
|
+
}
|
|
3277
3474
|
function leaving(batch, result) {
|
|
3278
3475
|
return {
|
|
3279
3476
|
batch,
|
|
@@ -3299,7 +3496,8 @@ export function composeRuntime(options) {
|
|
|
3299
3496
|
async function acceptance(room, local, submitted, accepted) {
|
|
3300
3497
|
if (!continuing())
|
|
3301
3498
|
return unconfirmed;
|
|
3302
|
-
const
|
|
3499
|
+
const work = await workOf(room.locator);
|
|
3500
|
+
const current = work.replica;
|
|
3303
3501
|
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
3304
3502
|
return null;
|
|
3305
3503
|
}
|
|
@@ -3308,7 +3506,7 @@ export function composeRuntime(options) {
|
|
|
3308
3506
|
let record = current.record;
|
|
3309
3507
|
let changes;
|
|
3310
3508
|
if (!options.adapter.reached(room.locator, held, accepted.roomVersion) &&
|
|
3311
|
-
!
|
|
3509
|
+
!olderThan(room.locator, accepted.roomVersion, held)) {
|
|
3312
3510
|
const paths = [...returnedPaths(submitted, accepted)];
|
|
3313
3511
|
const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
|
|
3314
3512
|
const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion, composed(record)));
|
|
@@ -3320,13 +3518,13 @@ export function composeRuntime(options) {
|
|
|
3320
3518
|
};
|
|
3321
3519
|
changes = back;
|
|
3322
3520
|
}
|
|
3323
|
-
const settled = await settlement(local, room.locator,
|
|
3521
|
+
const settled = await settlement(local, room.locator, work, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
|
|
3522
|
+
const reached = record.roomVersion;
|
|
3324
3523
|
return {
|
|
3325
3524
|
...settled,
|
|
3326
3525
|
apply(answer) {
|
|
3327
3526
|
const set = settled.apply(answer);
|
|
3328
|
-
showing(room.locator, accepted.roomVersion);
|
|
3329
|
-
pendings.delete(pendingKey(room.locator, submitted.id));
|
|
3527
|
+
showing(room.locator, accepted.roomVersion, reached);
|
|
3330
3528
|
unreadable.delete(room.locator);
|
|
3331
3529
|
list(room, true);
|
|
3332
3530
|
return set;
|
|
@@ -3336,8 +3534,9 @@ export function composeRuntime(options) {
|
|
|
3336
3534
|
async function adoptedTogether(local, acceptances) {
|
|
3337
3535
|
const settlements = acceptances.filter(settling);
|
|
3338
3536
|
if (settlements.length > 0) {
|
|
3339
|
-
|
|
3340
|
-
moveTo(
|
|
3537
|
+
const set = await together(local, settlements);
|
|
3538
|
+
void moveTo(status);
|
|
3539
|
+
void publishChangeSet(set);
|
|
3341
3540
|
}
|
|
3342
3541
|
await unpair(local);
|
|
3343
3542
|
}
|
|
@@ -3353,7 +3552,7 @@ export function composeRuntime(options) {
|
|
|
3353
3552
|
const other = discovered.get(partner);
|
|
3354
3553
|
const failed = await exclusive(async () => {
|
|
3355
3554
|
const left = await acceptance(room, local, submitted, accepted);
|
|
3356
|
-
const arriving =
|
|
3555
|
+
const arriving = (await replicaOf(partner))?.queue.find((batch) => batch.id === submitted.id);
|
|
3357
3556
|
const arrived = other === undefined || arriving === undefined || accepted.destination === undefined
|
|
3358
3557
|
? null
|
|
3359
3558
|
: await acceptance(other, local, arriving, {
|
|
@@ -3366,25 +3565,24 @@ export function composeRuntime(options) {
|
|
|
3366
3565
|
submitLater(partner);
|
|
3367
3566
|
return failed === null;
|
|
3368
3567
|
}
|
|
3369
|
-
function
|
|
3370
|
-
|
|
3568
|
+
async function behindIn(locator, batch) {
|
|
3569
|
+
const local = await openLocal();
|
|
3570
|
+
const [order, { room }] = await Promise.all([
|
|
3571
|
+
local.order(locator),
|
|
3572
|
+
local.pending(locator, batch.id),
|
|
3573
|
+
]);
|
|
3574
|
+
return room ? order.indexOf(batch.id) > 0 : order.length > 0;
|
|
3371
3575
|
}
|
|
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 };
|
|
3576
|
+
async function unpaired(local, skipped) {
|
|
3577
|
+
const { queued, moving } = await local.moving();
|
|
3578
|
+
for (const { locator, batch, state } of moving) {
|
|
3579
|
+
const partner = partnerOf(batch, locator);
|
|
3580
|
+
if (partner === null || skipped.has(pendingKey(locator, batch.id)))
|
|
3581
|
+
continue;
|
|
3582
|
+
if (queued.get(partner)?.includes(batch.id) === true)
|
|
3583
|
+
continue;
|
|
3584
|
+
if (!leavingIn(batch, locator) || withdrawable(new Map([[batch.id, state]]))(batch)) {
|
|
3585
|
+
return { locator, batch };
|
|
3388
3586
|
}
|
|
3389
3587
|
}
|
|
3390
3588
|
return null;
|
|
@@ -3396,9 +3594,10 @@ export function composeRuntime(options) {
|
|
|
3396
3594
|
returned: !localSaves && leavingIn(batch, locator) ? returnedBy('withdrawn') : null,
|
|
3397
3595
|
}));
|
|
3398
3596
|
const outcome = localSaves ? 'dropped' : 'unpaired';
|
|
3399
|
-
const
|
|
3597
|
+
const work = await workOf(locator);
|
|
3598
|
+
const current = work.replica;
|
|
3400
3599
|
if (current !== undefined) {
|
|
3401
|
-
const settled = await settlement(local, locator,
|
|
3600
|
+
const settled = await settlement(local, locator, work, current.record, current.queue.filter((one) => !ids.has(one.id)), { leaving });
|
|
3402
3601
|
return {
|
|
3403
3602
|
...settled,
|
|
3404
3603
|
apply(answer) {
|
|
@@ -3410,22 +3609,22 @@ export function composeRuntime(options) {
|
|
|
3410
3609
|
},
|
|
3411
3610
|
};
|
|
3412
3611
|
}
|
|
3413
|
-
const queue =
|
|
3612
|
+
const queue = work.queue.filter((one) => !ids.has(one.id));
|
|
3414
3613
|
const written = new Map();
|
|
3415
3614
|
for (const { batch, returned } of leaving) {
|
|
3416
3615
|
if (returned === null)
|
|
3417
3616
|
continue;
|
|
3418
|
-
for (const saved of
|
|
3617
|
+
for (const saved of work.states.get(batch.id)?.drafts ?? []) {
|
|
3419
3618
|
written.set(saved.path, {
|
|
3420
3619
|
locator,
|
|
3421
3620
|
path: saved.path,
|
|
3422
3621
|
revision: roomDraftRevision(),
|
|
3423
|
-
parts: returnedInto((written.get(saved.path) ??
|
|
3622
|
+
parts: returnedInto((written.get(saved.path) ?? (await draftOf(locator, saved.path)))?.parts, saved.parts),
|
|
3424
3623
|
returned,
|
|
3425
3624
|
});
|
|
3426
3625
|
}
|
|
3427
3626
|
}
|
|
3428
|
-
const carried = carriedBack(locator, leaving.flatMap(({ batch, returned }) => (returned === null ? [] : [batch])), written);
|
|
3627
|
+
const carried = await carriedBack(locator, leaving.flatMap(({ batch, returned }) => (returned === null ? [] : [batch])), written);
|
|
3429
3628
|
return {
|
|
3430
3629
|
locator,
|
|
3431
3630
|
left: batches,
|
|
@@ -3435,6 +3634,7 @@ export function composeRuntime(options) {
|
|
|
3435
3634
|
{
|
|
3436
3635
|
locator,
|
|
3437
3636
|
queue,
|
|
3637
|
+
stored: work.queue,
|
|
3438
3638
|
roomDrafts: [...written.values()],
|
|
3439
3639
|
consumedRoomDrafts: carried.taken,
|
|
3440
3640
|
},
|
|
@@ -3442,17 +3642,8 @@ export function composeRuntime(options) {
|
|
|
3442
3642
|
],
|
|
3443
3643
|
},
|
|
3444
3644
|
apply() {
|
|
3445
|
-
for (const batch of batches)
|
|
3645
|
+
for (const batch of batches)
|
|
3446
3646
|
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
3647
|
submitLater(locator);
|
|
3457
3648
|
return mergeChangeSets([
|
|
3458
3649
|
changeSet(locator, {
|
|
@@ -3466,28 +3657,33 @@ export function composeRuntime(options) {
|
|
|
3466
3657
|
}
|
|
3467
3658
|
async function unpair(local) {
|
|
3468
3659
|
const handled = new Set();
|
|
3469
|
-
for (let held = unpaired(handled); held !== null && continuing(); held = unpaired(handled)) {
|
|
3660
|
+
for (let held = await unpaired(local, handled); held !== null && continuing(); held = await unpaired(local, handled)) {
|
|
3470
3661
|
const { locator, batch } = held;
|
|
3471
3662
|
handled.add(pendingKey(locator, batch.id));
|
|
3472
3663
|
publishChangeSet(await together(local, [await unpairing(local, locator, [batch])]));
|
|
3473
3664
|
}
|
|
3474
3665
|
if (handled.size === 0)
|
|
3475
3666
|
return;
|
|
3476
|
-
moveTo(
|
|
3667
|
+
moveTo(status);
|
|
3477
3668
|
}
|
|
3478
3669
|
async function submitRoom(room, local, authorized, progress, signal) {
|
|
3479
3670
|
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
3480
|
-
const
|
|
3481
|
-
|
|
3482
|
-
|
|
3671
|
+
const headId = (await local.order(room.locator)).find((id) => !landedAhead.has(pendingKey(room.locator, id)));
|
|
3672
|
+
if (headId === undefined)
|
|
3673
|
+
return 'done';
|
|
3674
|
+
const [head, pending] = await Promise.all([
|
|
3675
|
+
local.batch(room.locator, headId),
|
|
3676
|
+
local.pending(room.locator, headId),
|
|
3677
|
+
]);
|
|
3678
|
+
if (head === null || !pending.room)
|
|
3483
3679
|
return 'done';
|
|
3484
|
-
const state =
|
|
3680
|
+
const { state } = pending;
|
|
3485
3681
|
const partner = partnerOf(head, room.locator);
|
|
3486
3682
|
if (partner !== null && !leavingIn(head, room.locator)) {
|
|
3487
3683
|
submitLater(partner);
|
|
3488
3684
|
return 'done';
|
|
3489
3685
|
}
|
|
3490
|
-
if (partner !== null && behindIn(partner, head)) {
|
|
3686
|
+
if (partner !== null && (await behindIn(partner, head))) {
|
|
3491
3687
|
progress.complete = false;
|
|
3492
3688
|
return 'done';
|
|
3493
3689
|
}
|
|
@@ -3508,7 +3704,7 @@ export function composeRuntime(options) {
|
|
|
3508
3704
|
let result;
|
|
3509
3705
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
3510
3706
|
await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
|
|
3511
|
-
if (discoveryPaused ||
|
|
3707
|
+
if (discoveryPaused || isFenced(room.locator))
|
|
3512
3708
|
return 'done';
|
|
3513
3709
|
if (signal.aborted || writesHeld()) {
|
|
3514
3710
|
await exclusive(() => mark(local, room.locator, head, submission, earlier));
|
|
@@ -3614,7 +3810,8 @@ export function composeRuntime(options) {
|
|
|
3614
3810
|
await exclusive(async () => {
|
|
3615
3811
|
if (!continuing())
|
|
3616
3812
|
return;
|
|
3617
|
-
const
|
|
3813
|
+
const work = await workOf(room.locator);
|
|
3814
|
+
const current = work.replica;
|
|
3618
3815
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
3619
3816
|
return;
|
|
3620
3817
|
}
|
|
@@ -3628,9 +3825,9 @@ export function composeRuntime(options) {
|
|
|
3628
3825
|
landedAhead.set(pendingKey(room.locator, head.id), Object.fromEntries([...touchedBy([head])].map((path) => [path, confirmedLayer.revisions[path]])));
|
|
3629
3826
|
return;
|
|
3630
3827
|
}
|
|
3631
|
-
publishChangeSet(await settle(local, room.locator,
|
|
3828
|
+
publishChangeSet(await settle(local, room.locator, work, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
|
|
3632
3829
|
await unpair(local);
|
|
3633
|
-
moveTo(
|
|
3830
|
+
moveTo(status);
|
|
3634
3831
|
});
|
|
3635
3832
|
const { there, here } = judged ?? (await refreshed());
|
|
3636
3833
|
if (other !== undefined && there !== null)
|
|
@@ -3665,24 +3862,24 @@ export function composeRuntime(options) {
|
|
|
3665
3862
|
release();
|
|
3666
3863
|
};
|
|
3667
3864
|
}
|
|
3668
|
-
function declaredAsHeld(room) {
|
|
3669
|
-
const held = replicas.get(room.locator)?.record;
|
|
3865
|
+
function declaredAsHeld(held, room) {
|
|
3670
3866
|
return (held === undefined ||
|
|
3671
3867
|
(JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
|
|
3672
3868
|
JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
|
|
3869
|
+
JSON.stringify(held.providerOwnedPaths) === JSON.stringify(room.providerOwnedPaths) &&
|
|
3673
3870
|
JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
|
|
3674
3871
|
}
|
|
3675
3872
|
async function keepNaming(local, room) {
|
|
3676
|
-
const held =
|
|
3873
|
+
const held = await recordAt(room.locator);
|
|
3677
3874
|
if (held === undefined || (held.name === room.name && held.handle === room.handle))
|
|
3678
3875
|
return;
|
|
3679
3876
|
await exclusive(async () => {
|
|
3680
3877
|
if (!continuing())
|
|
3681
3878
|
return;
|
|
3682
|
-
const
|
|
3683
|
-
if (
|
|
3879
|
+
const current = await recordAt(room.locator);
|
|
3880
|
+
if (current === undefined)
|
|
3684
3881
|
return;
|
|
3685
|
-
const record = { ...
|
|
3882
|
+
const record = { ...current, name: room.name, handle: room.handle };
|
|
3686
3883
|
await local.commit({
|
|
3687
3884
|
rooms: [
|
|
3688
3885
|
{
|
|
@@ -3693,36 +3890,38 @@ export function composeRuntime(options) {
|
|
|
3693
3890
|
},
|
|
3694
3891
|
],
|
|
3695
3892
|
});
|
|
3696
|
-
|
|
3893
|
+
emitRooms();
|
|
3697
3894
|
});
|
|
3698
3895
|
}
|
|
3699
|
-
async function
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3896
|
+
async function modelNow(locator) {
|
|
3897
|
+
return (await recordAt(locator))?.model ?? unheldModels.get(locator) ?? NO_MODEL;
|
|
3898
|
+
}
|
|
3899
|
+
async function retainModel(local, room, given) {
|
|
3900
|
+
const held = await recordAt(room.locator);
|
|
3901
|
+
const previous = held?.model ?? unheldModels.get(room.locator) ?? NO_MODEL;
|
|
3902
|
+
const next = given ?? previous;
|
|
3903
|
+
if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(held, room))
|
|
3703
3904
|
return;
|
|
3704
|
-
if (next.status === 'unavailable') {
|
|
3705
|
-
invalidModels.add(key);
|
|
3706
|
-
liveModels.delete(key);
|
|
3707
|
-
}
|
|
3708
3905
|
await exclusive(async () => {
|
|
3709
3906
|
if (!continuing())
|
|
3710
3907
|
return;
|
|
3711
|
-
const
|
|
3908
|
+
const work = await workOf(room.locator);
|
|
3909
|
+
const current = work.replica;
|
|
3712
3910
|
let changed = null;
|
|
3713
|
-
if (
|
|
3911
|
+
if (current !== undefined) {
|
|
3714
3912
|
const record = {
|
|
3715
|
-
...
|
|
3913
|
+
...current.record,
|
|
3716
3914
|
name: room.name,
|
|
3717
3915
|
handle: room.handle,
|
|
3718
3916
|
operations: room.operations,
|
|
3719
3917
|
replica: room.replica,
|
|
3918
|
+
providerOwnedPaths: room.providerOwnedPaths,
|
|
3720
3919
|
typedSelection: room.typedSelection ?? null,
|
|
3721
3920
|
model: next,
|
|
3722
3921
|
};
|
|
3723
|
-
const confirmedLayer = await sliceAt(record, 'confirmed',
|
|
3724
|
-
const replayedLayer = await replayed(record, confirmedLayer,
|
|
3725
|
-
const diff = await overlayDiff(record,
|
|
3922
|
+
const confirmedLayer = await sliceAt(record, 'confirmed', current.queue, current.overlaid);
|
|
3923
|
+
const replayedLayer = await replayed(record, confirmedLayer, current.queue);
|
|
3924
|
+
const diff = await overlayDiff(record, current, replayedLayer.visible, replayedLayer.touched);
|
|
3726
3925
|
const answer = await local.commit({
|
|
3727
3926
|
rooms: [
|
|
3728
3927
|
{
|
|
@@ -3735,28 +3934,21 @@ export function composeRuntime(options) {
|
|
|
3735
3934
|
},
|
|
3736
3935
|
],
|
|
3737
3936
|
});
|
|
3738
|
-
|
|
3739
|
-
replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
|
|
3937
|
+
preparedAfter(room.locator, answer);
|
|
3740
3938
|
changed = changeSet(room.locator, {
|
|
3741
3939
|
documents: answer.changed[room.locator] ?? [],
|
|
3742
3940
|
room: true,
|
|
3743
3941
|
});
|
|
3744
3942
|
}
|
|
3745
|
-
|
|
3943
|
+
else
|
|
3944
|
+
unheldModels.set(room.locator, next);
|
|
3945
|
+
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
3746
3946
|
if (changed === null)
|
|
3747
3947
|
emitRooms();
|
|
3748
3948
|
else
|
|
3749
3949
|
publishChangeSet(changed);
|
|
3750
3950
|
});
|
|
3751
3951
|
}
|
|
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
3952
|
async function scoped(result, room, local) {
|
|
3761
3953
|
if (result.scope === 'content' && !result.retryable) {
|
|
3762
3954
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
@@ -3777,7 +3969,7 @@ export function composeRuntime(options) {
|
|
|
3777
3969
|
else if (result.scope !== 'operation') {
|
|
3778
3970
|
unreadable.add(room.locator);
|
|
3779
3971
|
scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
|
|
3780
|
-
await retainModel(local, scopedAway,
|
|
3972
|
+
await retainModel(local, scopedAway, null);
|
|
3781
3973
|
}
|
|
3782
3974
|
if (!continuing())
|
|
3783
3975
|
return;
|
|
@@ -3785,42 +3977,55 @@ export function composeRuntime(options) {
|
|
|
3785
3977
|
emitRooms();
|
|
3786
3978
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
3787
3979
|
}
|
|
3788
|
-
function name(locator) {
|
|
3980
|
+
async function name(locator) {
|
|
3789
3981
|
const stream = options.adapter.roomStream;
|
|
3790
|
-
const record = replicas.get(locator)?.record;
|
|
3791
3982
|
const room = discovered.get(locator);
|
|
3792
|
-
if (stream === undefined ||
|
|
3983
|
+
if (stream === undefined || room === undefined || !socketOpen || !handed)
|
|
3793
3984
|
return;
|
|
3794
|
-
|
|
3985
|
+
const [held] = await heldRooms([room]);
|
|
3986
|
+
if (held === undefined || !socketOpen || !handed)
|
|
3795
3987
|
return;
|
|
3796
3988
|
naming += 1;
|
|
3797
|
-
stream.hold([
|
|
3989
|
+
stream.hold([held]);
|
|
3798
3990
|
}
|
|
3799
|
-
function heldRooms(listed) {
|
|
3991
|
+
async function heldRooms(listed) {
|
|
3992
|
+
const stored = await storedRooms(listed.map((room) => room.locator));
|
|
3800
3993
|
return listed.flatMap((room) => {
|
|
3801
|
-
const record =
|
|
3802
|
-
if (record === undefined
|
|
3994
|
+
const record = stored.get(room.locator)?.record;
|
|
3995
|
+
if (record === undefined) {
|
|
3996
|
+
return room.replica.admission === 'admitted'
|
|
3997
|
+
? [{ locator: room.locator, roomVersion: null }]
|
|
3998
|
+
: [];
|
|
3999
|
+
}
|
|
4000
|
+
if (record.replica.selection !== room.replica.selection)
|
|
3803
4001
|
return [];
|
|
3804
4002
|
return [{ locator: room.locator, roomVersion: record.roomVersion }];
|
|
3805
4003
|
});
|
|
3806
4004
|
}
|
|
3807
4005
|
async function synchronize(listed, local, progress, attempt, live) {
|
|
3808
|
-
const
|
|
4006
|
+
const heldNow = await storedRooms(listed.map(({ locator }) => locator));
|
|
4007
|
+
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress, heldNow.get(room.locator)?.record)));
|
|
3809
4008
|
if (!live())
|
|
3810
4009
|
return;
|
|
3811
4010
|
const ready = listed.filter((_, index) => {
|
|
3812
4011
|
const outcome = prepared[index];
|
|
3813
4012
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
3814
4013
|
});
|
|
4014
|
+
const held = await heldRooms(ready);
|
|
4015
|
+
if (!live())
|
|
4016
|
+
return;
|
|
3815
4017
|
if (attempt === 'opened') {
|
|
3816
4018
|
naming += 1;
|
|
3817
|
-
options.adapter.roomStream?.hold(
|
|
4019
|
+
options.adapter.roomStream?.hold(held);
|
|
3818
4020
|
handed = true;
|
|
3819
4021
|
}
|
|
4022
|
+
const kept = attempt === 'unstreamed'
|
|
4023
|
+
? new Set()
|
|
4024
|
+
: new Set((await storedRooms(ready.map(({ locator }) => locator))).keys());
|
|
3820
4025
|
for (const room of ready) {
|
|
3821
|
-
if (attempt
|
|
4026
|
+
if (attempt === 'opened' || kept.has(room.locator)) {
|
|
3822
4027
|
if (attempt === 'authorized')
|
|
3823
|
-
name(room.locator);
|
|
4028
|
+
await name(room.locator);
|
|
3824
4029
|
continue;
|
|
3825
4030
|
}
|
|
3826
4031
|
void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
|
|
@@ -3829,23 +4034,23 @@ export function composeRuntime(options) {
|
|
|
3829
4034
|
if (failed !== undefined)
|
|
3830
4035
|
throw failed.reason;
|
|
3831
4036
|
}
|
|
3832
|
-
async function prepareRoom(room, local, progress) {
|
|
4037
|
+
async function prepareRoom(room, local, progress, prior) {
|
|
3833
4038
|
if (closed || authorization === undefined) {
|
|
3834
4039
|
progress.complete = false;
|
|
3835
4040
|
return false;
|
|
3836
4041
|
}
|
|
3837
|
-
const prior = replicas.get(room.locator)?.record;
|
|
3838
4042
|
if (prior !== undefined && prior.replica.selection !== room.replica.selection) {
|
|
3839
|
-
|
|
4043
|
+
listHeld(room.locator);
|
|
3840
4044
|
emitRooms();
|
|
3841
4045
|
journal.record(`selection changed ${room.locator}`);
|
|
3842
4046
|
progress.complete = false;
|
|
3843
4047
|
return false;
|
|
3844
4048
|
}
|
|
3845
|
-
if (prior !== undefined)
|
|
3846
|
-
await retainModel(local, room,
|
|
3847
|
-
if (prior !== undefined)
|
|
4049
|
+
if (prior !== undefined && !declaredAsHeld(prior, room))
|
|
4050
|
+
await retainModel(local, room, null);
|
|
4051
|
+
if (prior !== undefined && (prior.name !== room.name || prior.handle !== room.handle)) {
|
|
3848
4052
|
await keepNaming(local, room);
|
|
4053
|
+
}
|
|
3849
4054
|
if (discoveryPaused || authorization === undefined)
|
|
3850
4055
|
return false;
|
|
3851
4056
|
return room.replica.admission === 'admitted';
|
|
@@ -3853,26 +4058,28 @@ export function composeRuntime(options) {
|
|
|
3853
4058
|
async function caughtUp(room, local, progress, live) {
|
|
3854
4059
|
if (options.adapter.roomStream !== undefined || authorization === undefined)
|
|
3855
4060
|
return;
|
|
3856
|
-
if (!live() || !behind(room))
|
|
4061
|
+
if (!live() || !(await behind(room)))
|
|
3857
4062
|
return;
|
|
3858
4063
|
const withheld = await advance(room, local, authorization, live);
|
|
3859
4064
|
if (withheld !== null)
|
|
3860
4065
|
await withhold(withheld, room, local, progress);
|
|
3861
4066
|
}
|
|
3862
|
-
function revoked(
|
|
3863
|
-
|
|
3864
|
-
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
4067
|
+
function revoked(model) {
|
|
4068
|
+
return model?.status === 'unavailable' && model.reason === 'revoked';
|
|
3865
4069
|
}
|
|
3866
|
-
async function loadRoom(room, local, progress, live) {
|
|
4070
|
+
async function loadRoom(room, local, progress, live, unheld) {
|
|
3867
4071
|
if (!live() || authorization === undefined)
|
|
3868
4072
|
return;
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
4073
|
+
const prior = unheld === undefined ? await recordAt(room.locator) : undefined;
|
|
4074
|
+
if (prior !== undefined) {
|
|
4075
|
+
if (!(await prepareRoom(room, local, progress, prior)))
|
|
4076
|
+
return;
|
|
4077
|
+
if (revoked(prior.model)) {
|
|
4078
|
+
await reload(room, local, progress, live);
|
|
4079
|
+
return;
|
|
4080
|
+
}
|
|
3874
4081
|
}
|
|
3875
|
-
const withheld = await advance(room, local, authorization, live);
|
|
4082
|
+
const withheld = await advance(room, local, authorization, live, unheld);
|
|
3876
4083
|
if (withheld !== null) {
|
|
3877
4084
|
await withhold(withheld, room, local, progress);
|
|
3878
4085
|
return;
|
|
@@ -3885,8 +4092,9 @@ export function composeRuntime(options) {
|
|
|
3885
4092
|
reloadsDue.delete(room.locator);
|
|
3886
4093
|
if (!live() || authorization === undefined)
|
|
3887
4094
|
return;
|
|
3888
|
-
const
|
|
3889
|
-
const
|
|
4095
|
+
const prior = await recordAt(room.locator);
|
|
4096
|
+
const from = standingAt(room.locator, prior);
|
|
4097
|
+
const bootstrapped = await pullBootstrap(authorization, room, local, live, prior);
|
|
3890
4098
|
if (bootstrapped.kind !== 'staged') {
|
|
3891
4099
|
if (live())
|
|
3892
4100
|
await withhold(bootstrapped, room, local, progress);
|
|
@@ -3910,7 +4118,7 @@ export function composeRuntime(options) {
|
|
|
3910
4118
|
function malformed(listed) {
|
|
3911
4119
|
for (const room of listed) {
|
|
3912
4120
|
if (room.operations === undefined ||
|
|
3913
|
-
!['write', '
|
|
4121
|
+
!['write', 'erase'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
3914
4122
|
room.operations[operation].every((scope) => typeof scope === 'string')) ||
|
|
3915
4123
|
room.replica === undefined ||
|
|
3916
4124
|
typeof room.replica.selection !== 'string' ||
|
|
@@ -3925,18 +4133,17 @@ export function composeRuntime(options) {
|
|
|
3925
4133
|
return null;
|
|
3926
4134
|
}
|
|
3927
4135
|
async function signIn(local, signedIn) {
|
|
3928
|
-
if (JSON.stringify(signedIn) === JSON.stringify(member))
|
|
4136
|
+
if (JSON.stringify(signedIn) === JSON.stringify((await local.facts()).member))
|
|
3929
4137
|
return;
|
|
3930
4138
|
await local.fact('member', signedIn);
|
|
3931
|
-
|
|
3932
|
-
emitMember();
|
|
4139
|
+
emitMember(signedIn);
|
|
3933
4140
|
}
|
|
3934
4141
|
async function discoverRooms(local, progress, attempt, live, found) {
|
|
3935
4142
|
if (closed || authorization === undefined) {
|
|
3936
4143
|
notifyReady();
|
|
3937
4144
|
return;
|
|
3938
4145
|
}
|
|
3939
|
-
moveTo(transferring(
|
|
4146
|
+
moveTo(transferring());
|
|
3940
4147
|
discoveries += 1;
|
|
3941
4148
|
const started = discoveries;
|
|
3942
4149
|
const result = found ?? (await asked(authorization));
|
|
@@ -3981,15 +4188,13 @@ export function composeRuntime(options) {
|
|
|
3981
4188
|
refusal = null;
|
|
3982
4189
|
emitRefusal();
|
|
3983
4190
|
}
|
|
3984
|
-
const
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
limits = result.providerLimits;
|
|
3990
|
-
await local.fact('providerLimits', limits);
|
|
3991
|
-
await local.fact('capabilities', capabilities);
|
|
4191
|
+
const held = await local.facts();
|
|
4192
|
+
if (JSON.stringify(result.providerLimits) !== JSON.stringify(held.providerLimits) ||
|
|
4193
|
+
JSON.stringify(result.capabilities) !== JSON.stringify(held.capabilities)) {
|
|
4194
|
+
await local.fact('providerLimits', result.providerLimits);
|
|
4195
|
+
await local.fact('capabilities', result.capabilities);
|
|
3992
4196
|
}
|
|
4197
|
+
notifyReady();
|
|
3993
4198
|
await signIn(local, result.member ?? null);
|
|
3994
4199
|
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
3995
4200
|
}
|
|
@@ -4072,7 +4277,7 @@ export function composeRuntime(options) {
|
|
|
4072
4277
|
streamed = progress;
|
|
4073
4278
|
complete = false;
|
|
4074
4279
|
recorded = false;
|
|
4075
|
-
moveTo(transferring(
|
|
4280
|
+
moveTo(transferring());
|
|
4076
4281
|
try {
|
|
4077
4282
|
const local = await openLocal();
|
|
4078
4283
|
if (!live())
|
|
@@ -4107,7 +4312,7 @@ export function composeRuntime(options) {
|
|
|
4107
4312
|
if (!live() || !announced)
|
|
4108
4313
|
return;
|
|
4109
4314
|
const progress = { complete: true, reachable: true };
|
|
4110
|
-
moveTo(transferring(
|
|
4315
|
+
moveTo(transferring());
|
|
4111
4316
|
try {
|
|
4112
4317
|
const local = await openLocal();
|
|
4113
4318
|
if (!live())
|
|
@@ -4132,7 +4337,7 @@ export function composeRuntime(options) {
|
|
|
4132
4337
|
return;
|
|
4133
4338
|
countAuthorityChange();
|
|
4134
4339
|
const progress = { complete: true, reachable: true };
|
|
4135
|
-
moveTo(transferring(
|
|
4340
|
+
moveTo(transferring());
|
|
4136
4341
|
try {
|
|
4137
4342
|
const local = await openLocal();
|
|
4138
4343
|
if (!live())
|
|
@@ -4167,6 +4372,7 @@ export function composeRuntime(options) {
|
|
|
4167
4372
|
missedChange ||= authorityChanged;
|
|
4168
4373
|
handed = false;
|
|
4169
4374
|
naming = 0;
|
|
4375
|
+
unreadied();
|
|
4170
4376
|
connectionLost();
|
|
4171
4377
|
if (lost)
|
|
4172
4378
|
conversations.lost();
|
|
@@ -4194,8 +4400,12 @@ export function composeRuntime(options) {
|
|
|
4194
4400
|
await rest(null, live);
|
|
4195
4401
|
});
|
|
4196
4402
|
}
|
|
4197
|
-
function cachedModels(local) {
|
|
4198
|
-
|
|
4403
|
+
async function cachedModels(local) {
|
|
4404
|
+
const held = await storedRooms();
|
|
4405
|
+
await settleModels(listedLocators(held).flatMap((locator) => {
|
|
4406
|
+
const room = discovered.get(locator) ?? declarationOf(locator, held.get(locator))?.room;
|
|
4407
|
+
return room === undefined ? [] : [room];
|
|
4408
|
+
}), 'cached', local);
|
|
4199
4409
|
}
|
|
4200
4410
|
function caughtUpRooms(expired) {
|
|
4201
4411
|
const progress = underway;
|
|
@@ -4203,8 +4413,12 @@ export function composeRuntime(options) {
|
|
|
4203
4413
|
naming = Math.max(0, naming - 1);
|
|
4204
4414
|
const cycle = progress ?? { complete: true, reachable: true };
|
|
4205
4415
|
replaced(expired);
|
|
4206
|
-
const
|
|
4207
|
-
const
|
|
4416
|
+
const installing = new Set(installs);
|
|
4417
|
+
const held = progress === null ? storedRooms(expired) : storedRooms();
|
|
4418
|
+
const reloading = progress === null
|
|
4419
|
+
? Promise.resolve([])
|
|
4420
|
+
: held.then((stored) => owedReloads(expired, stored));
|
|
4421
|
+
const replacing = Promise.all([held, reloading]).then(([stored, owed]) => reloadNow((room) => expired.includes(room.locator) || owed.includes(room.locator), cycle, stored, installing));
|
|
4208
4422
|
return task(OPENING_LANE, async (live) => {
|
|
4209
4423
|
if (!live())
|
|
4210
4424
|
return;
|
|
@@ -4217,9 +4431,11 @@ export function composeRuntime(options) {
|
|
|
4217
4431
|
const local = await openLocal();
|
|
4218
4432
|
await replacing;
|
|
4219
4433
|
if (progress.reachable) {
|
|
4220
|
-
|
|
4434
|
+
const stored = await held;
|
|
4435
|
+
const owed = await reloading;
|
|
4436
|
+
await settleModels([...discovered.values()].filter((room) => stored.has(room.locator) &&
|
|
4221
4437
|
!expired.includes(room.locator) &&
|
|
4222
|
-
!
|
|
4438
|
+
!owed.includes(room.locator)), 'current', local);
|
|
4223
4439
|
}
|
|
4224
4440
|
}
|
|
4225
4441
|
catch {
|
|
@@ -4230,24 +4446,34 @@ export function composeRuntime(options) {
|
|
|
4230
4446
|
await finishOpening(progress, live);
|
|
4231
4447
|
});
|
|
4232
4448
|
}
|
|
4233
|
-
function owedReloads(expired) {
|
|
4449
|
+
function owedReloads(expired, held) {
|
|
4234
4450
|
const reloading = [...discovered.values()]
|
|
4235
|
-
.filter((room) =>
|
|
4451
|
+
.filter((room) => !expired.includes(room.locator) && revoked(held.get(room.locator)?.record.model))
|
|
4236
4452
|
.map((room) => room.locator);
|
|
4237
4453
|
for (const locator of reloading)
|
|
4238
4454
|
catching.add(locator);
|
|
4239
|
-
|
|
4455
|
+
recoverReads();
|
|
4240
4456
|
return reloading;
|
|
4241
4457
|
}
|
|
4242
|
-
async function reloadNow(replaced, progress) {
|
|
4243
|
-
const reloaded = [...discovered.values()].filter((room) =>
|
|
4458
|
+
async function reloadNow(replaced, progress, held, installing) {
|
|
4459
|
+
const reloaded = [...discovered.values()].filter((room) => held.has(room.locator) && !installing.has(room.locator) && replaced(room));
|
|
4460
|
+
const acquired = [...discovered.values()].filter((room) => (!held.has(room.locator) || installing.has(room.locator)) &&
|
|
4461
|
+
room.replica.admission === 'admitted' &&
|
|
4462
|
+
replaced(room));
|
|
4244
4463
|
for (const room of reloaded)
|
|
4245
4464
|
reloadsDue.add(room.locator);
|
|
4246
4465
|
try {
|
|
4247
4466
|
const local = await openLocal();
|
|
4467
|
+
const kept = acquired.length === 0 ? new Set() : (await local.held()).staged;
|
|
4248
4468
|
for (const room of reloaded) {
|
|
4249
4469
|
void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
|
|
4250
4470
|
}
|
|
4471
|
+
for (const room of acquired) {
|
|
4472
|
+
const unheld = installing.has(room.locator)
|
|
4473
|
+
? undefined
|
|
4474
|
+
: { staged: kept.has(room.locator) };
|
|
4475
|
+
void roomTask(room.locator, (live) => loadRoom(room, local, progress, live, unheld)).then(() => rest(progress));
|
|
4476
|
+
}
|
|
4251
4477
|
}
|
|
4252
4478
|
catch {
|
|
4253
4479
|
for (const room of reloaded)
|
|
@@ -4256,29 +4482,56 @@ export function composeRuntime(options) {
|
|
|
4256
4482
|
}
|
|
4257
4483
|
}
|
|
4258
4484
|
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 () => {
|
|
4485
|
+
const owed = (model) => model.status !== 'unavailable' && model.status !== status;
|
|
4486
|
+
const declaredAnew = await exclusive(async () => {
|
|
4264
4487
|
if (!continuing())
|
|
4265
4488
|
return [];
|
|
4266
|
-
const
|
|
4489
|
+
const stored = await storedRooms(held.map(({ locator }) => locator));
|
|
4490
|
+
const unchanged = held.filter((room) => {
|
|
4491
|
+
const record = stored.get(room.locator)?.record;
|
|
4492
|
+
return (owed(record?.model ?? unheldModels.get(room.locator) ?? NO_MODEL) &&
|
|
4493
|
+
declaredAsHeld(record, room));
|
|
4494
|
+
});
|
|
4495
|
+
const records = unchanged.flatMap((room) => {
|
|
4496
|
+
const record = stored.get(room.locator)?.record;
|
|
4497
|
+
return record === undefined
|
|
4498
|
+
? []
|
|
4499
|
+
: [{ ...record, model: { ...record.model, status } }];
|
|
4500
|
+
});
|
|
4501
|
+
if (records.length > 0) {
|
|
4502
|
+
try {
|
|
4503
|
+
await local.commit({
|
|
4504
|
+
rooms: records.map((record) => ({
|
|
4505
|
+
locator: record.locator,
|
|
4506
|
+
key: record.key,
|
|
4507
|
+
record: storedRecord(record),
|
|
4508
|
+
composed: composedOf(composed(record)),
|
|
4509
|
+
})),
|
|
4510
|
+
});
|
|
4511
|
+
}
|
|
4512
|
+
catch {
|
|
4513
|
+
journal.record(`model ${status} persistence failed`);
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4267
4516
|
for (const room of unchanged) {
|
|
4268
|
-
const
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4517
|
+
const unheld = unheldModels.get(room.locator);
|
|
4518
|
+
if (!stored.has(room.locator) && unheld !== undefined) {
|
|
4519
|
+
unheldModels.set(room.locator, { ...unheld, status });
|
|
4520
|
+
}
|
|
4521
|
+
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
4273
4522
|
}
|
|
4274
4523
|
if (unchanged.length > 0)
|
|
4275
4524
|
emitRooms();
|
|
4276
|
-
return
|
|
4525
|
+
return held.filter((room) => {
|
|
4526
|
+
const record = stored.get(room.locator)?.record;
|
|
4527
|
+
return (!unchanged.includes(room) &&
|
|
4528
|
+
owed(record?.model ?? unheldModels.get(room.locator) ?? NO_MODEL));
|
|
4529
|
+
});
|
|
4277
4530
|
});
|
|
4278
|
-
for (const room of
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4531
|
+
for (const room of declaredAnew) {
|
|
4532
|
+
const model = await modelNow(room.locator);
|
|
4533
|
+
if (owed(model))
|
|
4534
|
+
await retainModel(local, room, { ...model, status });
|
|
4282
4535
|
}
|
|
4283
4536
|
}
|
|
4284
4537
|
function submitLater(locator) {
|
|
@@ -4341,7 +4594,7 @@ export function composeRuntime(options) {
|
|
|
4341
4594
|
return 'done';
|
|
4342
4595
|
await rest(progress);
|
|
4343
4596
|
if (!stale.has(locator)) {
|
|
4344
|
-
if ((
|
|
4597
|
+
if ((await (await openLocal()).order(locator)).length > 0)
|
|
4345
4598
|
submitLater(locator);
|
|
4346
4599
|
return 'done';
|
|
4347
4600
|
}
|
|
@@ -4369,9 +4622,11 @@ export function composeRuntime(options) {
|
|
|
4369
4622
|
return 'done';
|
|
4370
4623
|
return reading.reachable && reading.complete ? 'done' : 'again';
|
|
4371
4624
|
}
|
|
4372
|
-
function submitAll() {
|
|
4373
|
-
|
|
4374
|
-
|
|
4625
|
+
async function submitAll() {
|
|
4626
|
+
if (localSaves)
|
|
4627
|
+
return;
|
|
4628
|
+
for (const locator of await (await openLocal()).queued()) {
|
|
4629
|
+
if (!isFenced(locator))
|
|
4375
4630
|
submitLater(locator);
|
|
4376
4631
|
}
|
|
4377
4632
|
}
|
|
@@ -4381,7 +4636,7 @@ export function composeRuntime(options) {
|
|
|
4381
4636
|
progress.complete = false;
|
|
4382
4637
|
return;
|
|
4383
4638
|
}
|
|
4384
|
-
submitAll();
|
|
4639
|
+
await submitAll();
|
|
4385
4640
|
for (const locator of stale)
|
|
4386
4641
|
if (repairable.has(locator))
|
|
4387
4642
|
repairLater(locator, {});
|
|
@@ -4410,8 +4665,14 @@ export function composeRuntime(options) {
|
|
|
4410
4665
|
async function rest(progress, live = () => !closed) {
|
|
4411
4666
|
if (!live())
|
|
4412
4667
|
return;
|
|
4413
|
-
|
|
4414
|
-
let synced =
|
|
4668
|
+
const settled = () => complete && roomWork === 0 && naming === 0 && stale.size === 0;
|
|
4669
|
+
let synced = settled();
|
|
4670
|
+
if (synced && !recorded) {
|
|
4671
|
+
const queued = await queuedWork().catch(() => true);
|
|
4672
|
+
if (!live())
|
|
4673
|
+
return;
|
|
4674
|
+
synced = !queued && settled();
|
|
4675
|
+
}
|
|
4415
4676
|
if (synced && !recorded) {
|
|
4416
4677
|
const completedAt = new Date().toISOString();
|
|
4417
4678
|
try {
|
|
@@ -4419,21 +4680,15 @@ export function composeRuntime(options) {
|
|
|
4419
4680
|
if (!live())
|
|
4420
4681
|
return;
|
|
4421
4682
|
recorded = true;
|
|
4422
|
-
lastCompleted = completedAt;
|
|
4423
4683
|
}
|
|
4424
4684
|
catch {
|
|
4425
4685
|
synced = false;
|
|
4426
4686
|
}
|
|
4427
4687
|
}
|
|
4428
4688
|
moveTo({
|
|
4429
|
-
state:
|
|
4430
|
-
? 'pending'
|
|
4431
|
-
: synced && complete && roomWork === 0 && naming === 0
|
|
4432
|
-
? 'synced'
|
|
4433
|
-
: 'syncing',
|
|
4434
|
-
lastCompleted,
|
|
4689
|
+
state: synced && complete && roomWork === 0 && naming === 0 ? 'synced' : 'syncing',
|
|
4435
4690
|
providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
|
|
4436
|
-
|
|
4691
|
+
resting: true,
|
|
4437
4692
|
});
|
|
4438
4693
|
}
|
|
4439
4694
|
function applyRoom(change) {
|
|
@@ -4449,22 +4704,26 @@ export function composeRuntime(options) {
|
|
|
4449
4704
|
if (!live())
|
|
4450
4705
|
return null;
|
|
4451
4706
|
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) {
|
|
4707
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
4457
4708
|
return null;
|
|
4709
|
+
const held = await recordAt(locator);
|
|
4710
|
+
if (held === undefined) {
|
|
4711
|
+
if (change.from !== null)
|
|
4712
|
+
return null;
|
|
4713
|
+
await installCarried(room, local, change, live, progress);
|
|
4714
|
+
return progress;
|
|
4458
4715
|
}
|
|
4459
|
-
|
|
4716
|
+
if (held.replica.selection !== room.replica.selection)
|
|
4717
|
+
return null;
|
|
4718
|
+
const at = held.roomVersion;
|
|
4460
4719
|
if (options.adapter.reached(locator, at, change.roomVersion))
|
|
4461
4720
|
return progress;
|
|
4462
|
-
if (!options.adapter.reached(locator, at, change.from)) {
|
|
4463
|
-
name(locator);
|
|
4721
|
+
if (change.from === null || !options.adapter.reached(locator, at, change.from)) {
|
|
4722
|
+
await name(locator);
|
|
4464
4723
|
return progress;
|
|
4465
4724
|
}
|
|
4466
4725
|
if (underway === null)
|
|
4467
|
-
moveTo(transferring(
|
|
4726
|
+
moveTo(transferring());
|
|
4468
4727
|
const writing = () => live() && !full;
|
|
4469
4728
|
const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, () => change.read()));
|
|
4470
4729
|
if (refreshed.kind !== 'advanced')
|
|
@@ -4476,19 +4735,52 @@ export function composeRuntime(options) {
|
|
|
4476
4735
|
};
|
|
4477
4736
|
if (await publish(local, room, refreshed.record, landing, writing)) {
|
|
4478
4737
|
stale.delete(locator);
|
|
4479
|
-
|
|
4738
|
+
if (!declaredAsHeld(held, room))
|
|
4739
|
+
await retainModel(local, room, null);
|
|
4480
4740
|
return progress;
|
|
4481
4741
|
}
|
|
4482
4742
|
}
|
|
4483
4743
|
}
|
|
4484
4744
|
catch {
|
|
4485
4745
|
if (!stale.has(locator))
|
|
4486
|
-
name(locator);
|
|
4746
|
+
void name(locator);
|
|
4487
4747
|
stale.add(locator);
|
|
4488
4748
|
return progress;
|
|
4489
4749
|
}
|
|
4490
4750
|
}).then((progress) => rest(progress), () => undefined);
|
|
4491
4751
|
}
|
|
4752
|
+
async function installCarried(room, local, change, live, progress) {
|
|
4753
|
+
if (authorization === undefined)
|
|
4754
|
+
return;
|
|
4755
|
+
if (underway === null)
|
|
4756
|
+
moveTo(transferring());
|
|
4757
|
+
const prior = await recordAt(room.locator);
|
|
4758
|
+
const from = standingAt(room.locator, prior);
|
|
4759
|
+
const page = await change.read();
|
|
4760
|
+
const carried = {
|
|
4761
|
+
bootstrap: () => Promise.resolve({
|
|
4762
|
+
kind: 'bootstrap',
|
|
4763
|
+
roomVersion: page.roomVersion,
|
|
4764
|
+
...(page.models === undefined ? {} : { models: page.models }),
|
|
4765
|
+
documents: page.documents,
|
|
4766
|
+
facts: page.facts,
|
|
4767
|
+
revisions: page.revisions,
|
|
4768
|
+
...(page.fileIds === undefined ? {} : { fileIds: page.fileIds }),
|
|
4769
|
+
...(page.created === undefined ? {} : { created: page.created }),
|
|
4770
|
+
changes: page.changes,
|
|
4771
|
+
binaryFiles: page.binaryFiles,
|
|
4772
|
+
...(page.folders === undefined ? {} : { folders: page.folders }),
|
|
4773
|
+
}),
|
|
4774
|
+
reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
|
|
4775
|
+
};
|
|
4776
|
+
const bootstrapped = await pullBootstrap(authorization, room, local, live, prior, carried);
|
|
4777
|
+
if (bootstrapped.kind !== 'staged') {
|
|
4778
|
+
if (live())
|
|
4779
|
+
await withhold(bootstrapped, room, local, progress);
|
|
4780
|
+
return;
|
|
4781
|
+
}
|
|
4782
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
|
|
4783
|
+
}
|
|
4492
4784
|
function observeStream() {
|
|
4493
4785
|
const stream = options.adapter.roomStream;
|
|
4494
4786
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -4520,8 +4812,11 @@ export function composeRuntime(options) {
|
|
|
4520
4812
|
closedSocket(event.revoked, event.lost, event.authorityChanged);
|
|
4521
4813
|
else if (event.kind === 'caught-up')
|
|
4522
4814
|
void caughtUpRooms(event.expired);
|
|
4523
|
-
else if (event.kind === 'authority-changed')
|
|
4815
|
+
else if (event.kind === 'authority-changed') {
|
|
4816
|
+
if (announced && socketOpen)
|
|
4817
|
+
fenceLive(event.left);
|
|
4524
4818
|
void changeAuthority(event);
|
|
4819
|
+
}
|
|
4525
4820
|
else
|
|
4526
4821
|
void applyRoom(event);
|
|
4527
4822
|
});
|
|
@@ -4554,10 +4849,7 @@ export function composeRuntime(options) {
|
|
|
4554
4849
|
withdrawConversations = stream.observe((event) => {
|
|
4555
4850
|
if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
|
|
4556
4851
|
queueReadiness();
|
|
4557
|
-
|
|
4558
|
-
void ready.then(scheduleCurrency);
|
|
4559
|
-
return;
|
|
4560
|
-
}
|
|
4852
|
+
conversations.receive(event);
|
|
4561
4853
|
if (event.kind !== 'gap' || conversations.current())
|
|
4562
4854
|
return;
|
|
4563
4855
|
show();
|
|
@@ -4581,7 +4873,7 @@ export function composeRuntime(options) {
|
|
|
4581
4873
|
return;
|
|
4582
4874
|
}
|
|
4583
4875
|
const progress = { complete: false, reachable: !status.providerUnreachable };
|
|
4584
|
-
moveTo(transferring(
|
|
4876
|
+
moveTo(transferring());
|
|
4585
4877
|
try {
|
|
4586
4878
|
await conversations.catchUp(progress);
|
|
4587
4879
|
}
|
|
@@ -4595,6 +4887,7 @@ export function composeRuntime(options) {
|
|
|
4595
4887
|
return;
|
|
4596
4888
|
closed = true;
|
|
4597
4889
|
notifyDiscovered();
|
|
4890
|
+
unreadied();
|
|
4598
4891
|
if (probing !== null)
|
|
4599
4892
|
clearInterval(probing);
|
|
4600
4893
|
connectionLive();
|
|
@@ -4616,12 +4909,14 @@ export function composeRuntime(options) {
|
|
|
4616
4909
|
memberListeners.clear();
|
|
4617
4910
|
authorityChangeListeners.clear();
|
|
4618
4911
|
liveQueries.close();
|
|
4912
|
+
figures.close();
|
|
4619
4913
|
queueSubscriptions.clear();
|
|
4620
4914
|
roomDraftListeners.clear();
|
|
4621
4915
|
presentationListeners.clear();
|
|
4622
4916
|
}
|
|
4623
4917
|
async function stop() {
|
|
4624
4918
|
stopping = true;
|
|
4919
|
+
unreadied();
|
|
4625
4920
|
inFlight.abort();
|
|
4626
4921
|
await recovery.retire();
|
|
4627
4922
|
await Promise.all(working);
|
|
@@ -4634,16 +4929,18 @@ export function composeRuntime(options) {
|
|
|
4634
4929
|
if (options.signal?.aborted)
|
|
4635
4930
|
close();
|
|
4636
4931
|
observeStream();
|
|
4637
|
-
void openLocal()
|
|
4638
|
-
|
|
4932
|
+
void openLocal()
|
|
4933
|
+
.then(async (local) => {
|
|
4934
|
+
if ((await local.facts()).capabilities.length > 0)
|
|
4639
4935
|
readyFromStore();
|
|
4640
|
-
}
|
|
4936
|
+
})
|
|
4937
|
+
.catch(() => {
|
|
4641
4938
|
readyFromStore();
|
|
4642
4939
|
});
|
|
4643
4940
|
if (options.adapter.roomStream === undefined)
|
|
4644
4941
|
void sync('unstreamed', null);
|
|
4645
|
-
function planned(input, reach) {
|
|
4646
|
-
const plan = planWithin(input, roomFacts(reach), planReach(reach));
|
|
4942
|
+
async function planned(input, reach) {
|
|
4943
|
+
const plan = planWithin(input, await roomFacts(reach), planReach(reach));
|
|
4647
4944
|
const named = plan.paths !== null &&
|
|
4648
4945
|
plan.rooms !== null &&
|
|
4649
4946
|
plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
|
|
@@ -4662,10 +4959,10 @@ export function composeRuntime(options) {
|
|
|
4662
4959
|
const watchers = seedWatchers.get(plan.key) ?? new Set();
|
|
4663
4960
|
seedWatchers.set(plan.key, watchers);
|
|
4664
4961
|
watchers.add(shown);
|
|
4665
|
-
const stop = liveQueries.watch(plan, identityOf(plan), () => {
|
|
4666
|
-
const cover = covered(plan);
|
|
4962
|
+
const stop = liveQueries.watch(plan, identityOf(plan), (held) => {
|
|
4963
|
+
const cover = covered(plan, held);
|
|
4667
4964
|
awaitPreparation(cover);
|
|
4668
|
-
return seedBehind(plan, cover);
|
|
4965
|
+
return seedBehind(plan, cover, held);
|
|
4669
4966
|
}, (message) => {
|
|
4670
4967
|
const answered = message;
|
|
4671
4968
|
pending = whole(answered) && 'pending' in answered.result;
|
|
@@ -4683,6 +4980,21 @@ export function composeRuntime(options) {
|
|
|
4683
4980
|
stop();
|
|
4684
4981
|
};
|
|
4685
4982
|
}
|
|
4983
|
+
function watchPlanned(planning, deliver, fail) {
|
|
4984
|
+
let stop = null;
|
|
4985
|
+
let stopped = false;
|
|
4986
|
+
planning.then((plan) => {
|
|
4987
|
+
if (!stopped)
|
|
4988
|
+
stop = watchPlan(plan, deliver, fail);
|
|
4989
|
+
}, (error) => {
|
|
4990
|
+
if (!stopped)
|
|
4991
|
+
fail(error);
|
|
4992
|
+
});
|
|
4993
|
+
return () => {
|
|
4994
|
+
stopped = true;
|
|
4995
|
+
stop?.();
|
|
4996
|
+
};
|
|
4997
|
+
}
|
|
4686
4998
|
async function searched(input, reach) {
|
|
4687
4999
|
const asked = searchPlanOf(input);
|
|
4688
5000
|
await ready;
|
|
@@ -4695,7 +5007,7 @@ export function composeRuntime(options) {
|
|
|
4695
5007
|
retryable: false,
|
|
4696
5008
|
};
|
|
4697
5009
|
}
|
|
4698
|
-
if (closed || refusal !== null ||
|
|
5010
|
+
if (closed || refusal !== null || (await connectionNow()) !== 'connected')
|
|
4699
5011
|
return OFFLINE;
|
|
4700
5012
|
const reached = reach === null ? null : new Set(reach.rooms);
|
|
4701
5013
|
const named = asked.rooms === null
|
|
@@ -4703,7 +5015,8 @@ export function composeRuntime(options) {
|
|
|
4703
5015
|
? null
|
|
4704
5016
|
: [...reached]
|
|
4705
5017
|
: asked.rooms.filter((room) => reached === null || reached.has(room));
|
|
4706
|
-
const
|
|
5018
|
+
const held = named === null ? null : await storedRooms(named);
|
|
5019
|
+
const rooms = named === null ? null : named.filter((room) => held.has(room)).sort();
|
|
4707
5020
|
if (rooms !== null && rooms.length === 0)
|
|
4708
5021
|
return { kind: 'found', documents: [] };
|
|
4709
5022
|
try {
|
|
@@ -4741,18 +5054,15 @@ export function composeRuntime(options) {
|
|
|
4741
5054
|
roomDraftListeners.add(listener);
|
|
4742
5055
|
void exclusive(async () => {
|
|
4743
5056
|
await openLocal();
|
|
4744
|
-
}).then(() => {
|
|
5057
|
+
}).then(() => publication(async () => {
|
|
4745
5058
|
if (closed || !roomDraftListeners.has(listener))
|
|
4746
5059
|
return;
|
|
4747
|
-
const
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
listener.notify(value);
|
|
4754
|
-
});
|
|
4755
|
-
}, () => { });
|
|
5060
|
+
const value = await snapshotRoomDrafts();
|
|
5061
|
+
if (closed || !roomDraftListeners.has(listener))
|
|
5062
|
+
return;
|
|
5063
|
+
listener.ready = true;
|
|
5064
|
+
listener.notify(value);
|
|
5065
|
+
}), () => { });
|
|
4756
5066
|
return () => {
|
|
4757
5067
|
roomDraftListeners.delete(listener);
|
|
4758
5068
|
};
|
|
@@ -4764,6 +5074,8 @@ export function composeRuntime(options) {
|
|
|
4764
5074
|
return conversations.watchList(deliver);
|
|
4765
5075
|
if (topic === 'agents')
|
|
4766
5076
|
return conversations.watchAgents(deliver);
|
|
5077
|
+
if (topic === 'figures')
|
|
5078
|
+
return figures.watch(deliver, fail);
|
|
4767
5079
|
if (topic === 'presentation') {
|
|
4768
5080
|
const listener = {
|
|
4769
5081
|
app: input[0],
|
|
@@ -4772,13 +5084,12 @@ export function composeRuntime(options) {
|
|
|
4772
5084
|
};
|
|
4773
5085
|
presentationListeners.add(listener);
|
|
4774
5086
|
void exclusive(async () => {
|
|
4775
|
-
await
|
|
4776
|
-
}).then(() => {
|
|
5087
|
+
const value = await presentationOf(listener.app);
|
|
4777
5088
|
if (closed || !presentationListeners.has(listener))
|
|
4778
5089
|
return;
|
|
4779
5090
|
listener.ready = true;
|
|
4780
|
-
listener.notify(
|
|
4781
|
-
}
|
|
5091
|
+
listener.notify(value);
|
|
5092
|
+
}).catch(() => { });
|
|
4782
5093
|
return () => {
|
|
4783
5094
|
presentationListeners.delete(listener);
|
|
4784
5095
|
};
|
|
@@ -4792,6 +5103,8 @@ export function composeRuntime(options) {
|
|
|
4792
5103
|
return () => {
|
|
4793
5104
|
unlistedRooms.delete(deliver);
|
|
4794
5105
|
roomListeners.delete(deliver);
|
|
5106
|
+
if (roomListeners.size === 0)
|
|
5107
|
+
roomsShown = null;
|
|
4795
5108
|
};
|
|
4796
5109
|
}
|
|
4797
5110
|
if (topic === 'connection-refusal') {
|
|
@@ -4824,7 +5137,12 @@ export function composeRuntime(options) {
|
|
|
4824
5137
|
}
|
|
4825
5138
|
if (topic === 'member') {
|
|
4826
5139
|
memberListeners.add(deliver);
|
|
4827
|
-
|
|
5140
|
+
void openLocal()
|
|
5141
|
+
.then((local) => local.facts())
|
|
5142
|
+
.then(({ member }) => {
|
|
5143
|
+
if (memberListeners.has(deliver))
|
|
5144
|
+
deliver(member);
|
|
5145
|
+
}, () => { });
|
|
4828
5146
|
return () => {
|
|
4829
5147
|
memberListeners.delete(deliver);
|
|
4830
5148
|
};
|
|
@@ -4839,12 +5157,18 @@ export function composeRuntime(options) {
|
|
|
4839
5157
|
if (topic === 'queue') {
|
|
4840
5158
|
const queue = { locator: input[0], notify: deliver };
|
|
4841
5159
|
queueSubscriptions.add(queue);
|
|
4842
|
-
|
|
5160
|
+
void publication(async () => {
|
|
5161
|
+
if (!queueSubscriptions.has(queue))
|
|
5162
|
+
return;
|
|
5163
|
+
const entries = await entriesOf(queue.locator);
|
|
5164
|
+
if (queueSubscriptions.has(queue))
|
|
5165
|
+
queue.notify(entries);
|
|
5166
|
+
});
|
|
4843
5167
|
return () => {
|
|
4844
5168
|
queueSubscriptions.delete(queue);
|
|
4845
5169
|
};
|
|
4846
5170
|
}
|
|
4847
|
-
return
|
|
5171
|
+
return watchPlanned(planned(input[0], null), deliver, fail);
|
|
4848
5172
|
};
|
|
4849
5173
|
const md = {
|
|
4850
5174
|
loaded() {
|
|
@@ -4863,15 +5187,14 @@ export function composeRuntime(options) {
|
|
|
4863
5187
|
emitKnownRooms();
|
|
4864
5188
|
});
|
|
4865
5189
|
},
|
|
4866
|
-
query: ((input) =>
|
|
5190
|
+
query: ((input) => planned(input, null).then(rowsQuery)),
|
|
4867
5191
|
search: (input) => searched(input, null),
|
|
4868
|
-
types(locator) {
|
|
5192
|
+
async types(locator) {
|
|
5193
|
+
const held = (await storedRooms([locator])).get(locator);
|
|
5194
|
+
const room = isFenced(locator) ? undefined : declarationOf(locator, held)?.room;
|
|
4869
5195
|
return {
|
|
4870
|
-
collection: modelTypes(
|
|
4871
|
-
typedSelection:
|
|
4872
|
-
rooms.get(locator)?.typedSelection === null
|
|
4873
|
-
? null
|
|
4874
|
-
: modelTypes(builtIn),
|
|
5196
|
+
collection: modelTypes(isFenced(locator) ? NO_MODEL : modelIn(locator, held)),
|
|
5197
|
+
typedSelection: (room?.typedSelection ?? null) === null ? null : modelTypes(builtIn),
|
|
4875
5198
|
};
|
|
4876
5199
|
},
|
|
4877
5200
|
roomDraft: {
|
|
@@ -4885,7 +5208,7 @@ export function composeRuntime(options) {
|
|
|
4885
5208
|
});
|
|
4886
5209
|
},
|
|
4887
5210
|
async read(room, path) {
|
|
4888
|
-
const record =
|
|
5211
|
+
const record = await draftOf(room, path);
|
|
4889
5212
|
return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
|
|
4890
5213
|
},
|
|
4891
5214
|
write(room, path, revision, input, next = roomDraftRevision()) {
|
|
@@ -4903,14 +5226,15 @@ export function composeRuntime(options) {
|
|
|
4903
5226
|
discard(room, path, revision) {
|
|
4904
5227
|
return exclusive(async () => {
|
|
4905
5228
|
const local = await openLocal();
|
|
4906
|
-
const current =
|
|
5229
|
+
const current = await draftOf(room, path);
|
|
4907
5230
|
if (current === undefined || current.revision !== revision) {
|
|
4908
5231
|
return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
|
|
4909
5232
|
}
|
|
4910
5233
|
await local.discardRoomDraft(room, path);
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
5234
|
+
await Promise.all([
|
|
5235
|
+
publishChangeSet(changeSet(room, { drafts: [path] })),
|
|
5236
|
+
moveTo(status),
|
|
5237
|
+
]);
|
|
4914
5238
|
return { outcome: 'written', roomDraft: null };
|
|
4915
5239
|
});
|
|
4916
5240
|
},
|
|
@@ -4919,10 +5243,7 @@ export function composeRuntime(options) {
|
|
|
4919
5243
|
chatDraft: conversations.chatDraft,
|
|
4920
5244
|
presentation: {
|
|
4921
5245
|
read(app) {
|
|
4922
|
-
return exclusive(
|
|
4923
|
-
await openLocal();
|
|
4924
|
-
return presentationOf(app);
|
|
4925
|
-
});
|
|
5246
|
+
return exclusive(() => presentationOf(app));
|
|
4926
5247
|
},
|
|
4927
5248
|
write(app, value) {
|
|
4928
5249
|
return exclusive(() => keptPresentation(app, () => structuredClone(value)));
|
|
@@ -4955,7 +5276,8 @@ export function composeRuntime(options) {
|
|
|
4955
5276
|
},
|
|
4956
5277
|
async call(name, input) {
|
|
4957
5278
|
await ready;
|
|
4958
|
-
const
|
|
5279
|
+
const lockedNow = await lockedRooms();
|
|
5280
|
+
const room = (await calledRooms(name, input)).find((one) => lockedNow.includes(one));
|
|
4959
5281
|
return room === undefined ? recoveredAction(name, input) : lockedRoom(room);
|
|
4960
5282
|
},
|
|
4961
5283
|
async reached(locator, roomVersion, path) {
|
|
@@ -5008,7 +5330,7 @@ export function composeRuntime(options) {
|
|
|
5008
5330
|
lockedRooms,
|
|
5009
5331
|
watchLocks,
|
|
5010
5332
|
located,
|
|
5011
|
-
room: (locator) =>
|
|
5333
|
+
room: async (locator) => shownRooms(await listingNow()).find((one) => one.locator === locator),
|
|
5012
5334
|
};
|
|
5013
5335
|
return {
|
|
5014
5336
|
md,
|