@agentshouse/mdruntime 0.1.0-ccd098ff75bbf → 0.1.0-cce1d81ef3d6d
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/dist/adapter.d.ts +65 -48
- package/dist/adapter.d.ts.map +1 -1
- package/dist/attached-model.d.ts.map +1 -1
- package/dist/attached-model.js +18 -1
- package/dist/bootstrap.d.ts +9 -11
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +97 -85
- package/dist/client.d.ts +6 -7
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +52 -6
- package/dist/collection.d.ts +5 -3
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +23 -17
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +65 -19
- package/dist/connection.d.ts +5 -4
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +925 -900
- package/dist/conversation.d.ts +11 -11
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +204 -129
- package/dist/durability.d.ts +0 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/entry.d.ts +1 -0
- package/dist/entry.d.ts.map +1 -1
- package/dist/entry.js +1 -1
- package/dist/file.d.ts +2 -11
- package/dist/file.d.ts.map +1 -1
- package/dist/file.js +6 -4
- package/dist/fulltext.d.ts.map +1 -1
- package/dist/fulltext.js +8 -6
- package/dist/host.d.ts +0 -2
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +10 -11
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/media.d.ts +8 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +218 -0
- package/dist/memory.d.ts +8 -5
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +174 -82
- package/dist/model.d.ts +6 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +6 -9
- package/dist/observation.d.ts +1 -0
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +1 -1
- package/dist/outcome.d.ts +13 -5
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +16 -2
- package/dist/paths.d.ts +1 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +4 -0
- package/dist/query.d.ts +6 -0
- package/dist/query.d.ts.map +1 -1
- package/dist/reach.d.ts +8 -1
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +49 -22
- package/dist/readiness.d.ts +3 -15
- package/dist/readiness.d.ts.map +1 -1
- package/dist/readiness.js +4 -10
- package/dist/recovery.d.ts +1 -0
- package/dist/recovery.d.ts.map +1 -1
- package/dist/recovery.js +26 -0
- package/dist/refresh.d.ts +5 -4
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +4 -4
- package/dist/replica.d.ts +2 -3
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +2 -5
- package/dist/room-draft.d.ts +17 -2
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +40 -6
- package/dist/rows.d.ts +4 -2
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +29 -20
- package/dist/save.d.ts +2 -0
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +11 -5
- package/dist/shared.d.ts +0 -1
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +0 -10
- package/dist/status.d.ts +4 -3
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +3 -1
- package/dist/store.d.ts +18 -32
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +25 -60
- package/dist/stored.d.ts +9 -24
- package/dist/stored.d.ts.map +1 -1
- package/dist/stored.js +207 -204
- package/dist/tables.d.ts +16 -45
- package/dist/tables.d.ts.map +1 -1
- package/dist/tables.js +7 -53
- package/dist/transfer.d.ts +0 -5
- package/dist/transfer.d.ts.map +1 -1
- package/dist/validation.d.ts +3 -4
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +35 -96
- package/dist/wire.d.ts +8 -8
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +2 -2
- package/dist/words.d.ts.map +1 -1
- package/dist/words.js +8 -2
- package/package.json +2 -2
- package/dist/reads.d.ts +0 -11
- package/dist/reads.d.ts.map +0 -1
- package/dist/reads.js +0 -100
package/dist/connection.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
|
|
2
2
|
import { attachedModel, builtInModel, roomTypesOf } from './attached-model.js';
|
|
3
3
|
import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
|
|
4
4
|
import { observing, whole } from './observation.js';
|
|
@@ -12,18 +12,18 @@ import { bootstrapRoom } from './bootstrap.js';
|
|
|
12
12
|
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
13
13
|
import { createConversationStore, } from './conversation.js';
|
|
14
14
|
import { leaving as leavingIn, partnerOf, queuedFile, } from './change.js';
|
|
15
|
-
import { available, collectionOf,
|
|
15
|
+
import { available, collectionOf, composedOf, validRoot, } from './collection.js';
|
|
16
16
|
import { runtimeError } from './failure.js';
|
|
17
17
|
import { createLog } from './log.js';
|
|
18
18
|
import { binaryRecord } from './binary.js';
|
|
19
|
+
import { attachedFile } from './file.js';
|
|
19
20
|
import { openedFiles } from './opened.js';
|
|
20
|
-
import { ABSENT_TARGET, absentBinary, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
|
|
21
|
+
import { ABSENT_TARGET, absentBinary, attachAnswer, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, OFFLINE, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
|
|
21
22
|
import { createRecovery } from './recovery.js';
|
|
22
|
-
import { doorReads } from './reads.js';
|
|
23
23
|
import { advancement } from './refresh.js';
|
|
24
24
|
import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
|
|
25
|
-
import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths,
|
|
26
|
-
import { binaryBase, changedParts, changesOf, gatheredParts, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
25
|
+
import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
|
|
26
|
+
import { binaryBase, changedParts, changesOf, gatheredParts, held, heldFile, joinedDraft, moveTarget, readParts, readSavedParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
27
27
|
import { same, saving, STARTING, transferring, } from './status.js';
|
|
28
28
|
import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
|
|
29
29
|
import { readinessOf, } from './readiness.js';
|
|
@@ -31,9 +31,9 @@ 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 { identityTime, roomDraftRevision } from './identity.js';
|
|
34
|
-
import { beneath, normalize } from './paths.js';
|
|
34
|
+
import { ancestors, beneath, normalize } from './paths.js';
|
|
35
35
|
import { openRuntimeStore, } from './store.js';
|
|
36
|
-
import {
|
|
36
|
+
import { validatedNeighbourhood } from './validation.js';
|
|
37
37
|
import { unconfirmed } from './transfer.js';
|
|
38
38
|
import { governs, holds, narrowed, planReach, } from './reach.js';
|
|
39
39
|
function within(root, path) {
|
|
@@ -54,40 +54,37 @@ const CONVERSATION_LANE = 'conversations';
|
|
|
54
54
|
const ROOM_READS = 256;
|
|
55
55
|
const ROOM_ATTEMPTS = 16;
|
|
56
56
|
const ERASE_BATCH = 512;
|
|
57
|
-
const
|
|
58
|
-
const
|
|
57
|
+
const DERIVE_BYTES = 1 << 23;
|
|
58
|
+
const DERIVE_FLOOR = 1 << 18;
|
|
59
|
+
// An index build aims at this much database work, so a Save, a query or the next part's progress waits behind at most one short build.
|
|
60
|
+
const DERIVE_AIM_MS = 250;
|
|
61
|
+
// A build past its floor of text stops after a second and a half of work, its full-text writes included, so a Room whose text costs ten times the usual holds a Save, a query or the next part's progress behind no longer build.
|
|
62
|
+
const DERIVE_LONGEST_MS = 1_500;
|
|
59
63
|
const QUERY_BYTES = 1 << 20;
|
|
60
64
|
const STAGE_BYTES = 1 << 20;
|
|
61
65
|
const STAGE_FLOOR = 1 << 16;
|
|
62
66
|
// A batch aims at this much database work, so a Save or a query waits behind at most one short write.
|
|
63
67
|
const STAGE_AIM_MS = 50;
|
|
64
|
-
const
|
|
65
|
-
|
|
68
|
+
const STREAMS_HELD = 4;
|
|
69
|
+
const NOTHING_HELD = { bytes: 0 };
|
|
70
|
+
const QUEUED_TEXT = 1 << 23;
|
|
71
|
+
// 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.
|
|
66
72
|
const ARRIVAL_REPORT_MS = 250;
|
|
67
|
-
const INSTALL_STEPS = [
|
|
68
|
-
'writing',
|
|
69
|
-
'downloading',
|
|
70
|
-
'replaying',
|
|
71
|
-
'listing',
|
|
72
|
-
];
|
|
73
|
-
const CLEAN_CHANGES = 2_000;
|
|
74
73
|
// 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.
|
|
75
74
|
const LOCK_GRACE_MS = 120_000;
|
|
76
75
|
function operationOf(input) {
|
|
77
76
|
const operation = input?.operation_id;
|
|
78
77
|
return typeof operation === 'string' ? operation : null;
|
|
79
78
|
}
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
79
|
+
function readUntil(read, signal) {
|
|
80
|
+
if (signal.aborted)
|
|
81
|
+
return Promise.resolve({ kind: 'transport-uncertainty' });
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const stop = () => resolve({ kind: 'transport-uncertainty' });
|
|
84
|
+
signal.addEventListener('abort', stop, { once: true });
|
|
85
|
+
read.then(resolve, reject).finally(() => signal.removeEventListener('abort', stop));
|
|
86
86
|
});
|
|
87
87
|
}
|
|
88
|
-
function roomLane(locator) {
|
|
89
|
-
return `room ${locator}`;
|
|
90
|
-
}
|
|
91
88
|
function pendingKey(locator, id) {
|
|
92
89
|
return `${locator}\u0000${id}`;
|
|
93
90
|
}
|
|
@@ -103,11 +100,14 @@ export function composeRuntime(options) {
|
|
|
103
100
|
const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
|
|
104
101
|
let discoveries = 0;
|
|
105
102
|
const replicas = new FencedMap(isFenced);
|
|
103
|
+
const reopened = new Set();
|
|
104
|
+
const usableWhenHeld = options.usableWhenHeld ?? [];
|
|
106
105
|
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
107
106
|
const pendings = new Map();
|
|
108
107
|
const seeds = new Map();
|
|
109
108
|
const seedWatchers = new Map();
|
|
110
109
|
const roomDrafts = new FencedMap(isFenced);
|
|
110
|
+
const attachedFiles = new FencedMap(isFenced);
|
|
111
111
|
const presentations = new Map();
|
|
112
112
|
let limits = [];
|
|
113
113
|
let capabilities = [];
|
|
@@ -119,11 +119,19 @@ export function composeRuntime(options) {
|
|
|
119
119
|
const liveModels = new FencedMap(isFenced);
|
|
120
120
|
const invalidModels = new Set();
|
|
121
121
|
const discovered = new Map();
|
|
122
|
-
|
|
122
|
+
let inFlight = new AbortController();
|
|
123
|
+
const refreshed = options.adapter.refresh?.bind(options.adapter);
|
|
124
|
+
const reads = {
|
|
125
|
+
bootstrap: (authorized, request) => readUntil(options.adapter.bootstrap(authorized, request), inFlight.signal),
|
|
126
|
+
...(refreshed === undefined
|
|
127
|
+
? {}
|
|
128
|
+
: {
|
|
129
|
+
refresh: (authorized, request) => readUntil(refreshed(authorized, request), inFlight.signal),
|
|
130
|
+
}),
|
|
131
|
+
reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
|
|
132
|
+
};
|
|
123
133
|
let roomsListed = false;
|
|
124
134
|
const scopedRefusals = new Map();
|
|
125
|
-
const sourceRefusals = new Map();
|
|
126
|
-
const declarationRefusals = new Set();
|
|
127
135
|
function modelOf(locator) {
|
|
128
136
|
return models.get(locator) ?? NO_MODEL;
|
|
129
137
|
}
|
|
@@ -146,10 +154,10 @@ export function composeRuntime(options) {
|
|
|
146
154
|
let closed = false;
|
|
147
155
|
let stopping = false;
|
|
148
156
|
let withdrawStream;
|
|
149
|
-
const announced = new Map();
|
|
150
157
|
let underway = null;
|
|
151
158
|
let streamed = null;
|
|
152
159
|
let handed = false;
|
|
160
|
+
let naming = 0;
|
|
153
161
|
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
154
162
|
const repairing = new Set();
|
|
155
163
|
const calling = new Set();
|
|
@@ -160,16 +168,16 @@ export function composeRuntime(options) {
|
|
|
160
168
|
let recorded = false;
|
|
161
169
|
const stale = new Set();
|
|
162
170
|
let generation = 0;
|
|
163
|
-
|
|
164
|
-
let opened = null;
|
|
165
|
-
const owed = new Set();
|
|
166
|
-
const reloadsDue = new Map();
|
|
171
|
+
const reloadsDue = new Set();
|
|
167
172
|
const lanes = new Map();
|
|
168
|
-
|
|
173
|
+
const roomLanes = new Map();
|
|
174
|
+
let roomWork = 0;
|
|
175
|
+
const roomReads = { free: ROOM_READS, waiting: [] };
|
|
169
176
|
const working = new Set();
|
|
170
177
|
let mutations = Promise.resolve();
|
|
171
178
|
let gathering = null;
|
|
172
179
|
const roomListeners = new Set();
|
|
180
|
+
const unlistedRooms = new Set();
|
|
173
181
|
const refusalListeners = new Set();
|
|
174
182
|
const storageLossListeners = new Set();
|
|
175
183
|
const statusListeners = new Set();
|
|
@@ -180,6 +188,7 @@ export function composeRuntime(options) {
|
|
|
180
188
|
let withdrawConversations;
|
|
181
189
|
let currencyQueued = null;
|
|
182
190
|
let socketOpen = false;
|
|
191
|
+
let announced = false;
|
|
183
192
|
const confirmed = new Set();
|
|
184
193
|
let lockGrace = null;
|
|
185
194
|
const lockListeners = new Set();
|
|
@@ -213,31 +222,32 @@ export function composeRuntime(options) {
|
|
|
213
222
|
let readinessQueued = false;
|
|
214
223
|
const catching = new Set();
|
|
215
224
|
const transfers = new FencedMap(isFenced);
|
|
216
|
-
const installs = new
|
|
225
|
+
const installs = new Set();
|
|
217
226
|
const arrivals = new Map();
|
|
227
|
+
const streaming = new Set();
|
|
228
|
+
const underived = new Map();
|
|
229
|
+
const unindexed = new Map();
|
|
230
|
+
const startedOver = new Map();
|
|
218
231
|
let arrivalReported = 0;
|
|
219
|
-
let
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
232
|
+
let streamsFree = STREAMS_HELD;
|
|
233
|
+
let queuedText = 0;
|
|
234
|
+
const installWaiters = [];
|
|
235
|
+
const stageBudget = {
|
|
236
|
+
bytes: STAGE_FLOOR * 4,
|
|
237
|
+
floor: STAGE_FLOOR,
|
|
238
|
+
ceiling: STAGE_BYTES,
|
|
239
|
+
aim: STAGE_AIM_MS,
|
|
240
|
+
};
|
|
241
|
+
const deriveBudget = {
|
|
242
|
+
bytes: DERIVE_FLOOR * 4,
|
|
243
|
+
floor: DERIVE_FLOOR,
|
|
244
|
+
ceiling: DERIVE_BYTES,
|
|
245
|
+
aim: DERIVE_AIM_MS,
|
|
246
|
+
};
|
|
247
|
+
const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
|
|
225
248
|
const conversations = createConversationStore({
|
|
226
|
-
agents: declaredAgents,
|
|
227
|
-
documents: (declaration, after) => rowsQuery(planOf({
|
|
228
|
-
rooms: [declaration.locator],
|
|
229
|
-
types: [declaration.type],
|
|
230
|
-
select: {
|
|
231
|
-
fields: [
|
|
232
|
-
declaration.identity,
|
|
233
|
-
declaration.name,
|
|
234
|
-
...(declaration.retired === undefined ? [] : [declaration.retired]),
|
|
235
|
-
].map((field) => `/${escapePointer(field)}`),
|
|
236
|
-
},
|
|
237
|
-
limit: MAXIMUM_WINDOW,
|
|
238
|
-
...(after === undefined ? {} : { after }),
|
|
239
|
-
}, roomFacts())),
|
|
240
249
|
call: (name, input) => invokeAction(name, input),
|
|
250
|
+
history: (request) => readConversationHistory(request),
|
|
241
251
|
durably,
|
|
242
252
|
stream: () => options.adapter.conversations,
|
|
243
253
|
publish: publishChangeSet,
|
|
@@ -249,10 +259,19 @@ export function composeRuntime(options) {
|
|
|
249
259
|
});
|
|
250
260
|
const journal = createLog();
|
|
251
261
|
const keptFiles = openedFiles(database, isFenced);
|
|
252
|
-
let
|
|
262
|
+
let readyFromStore = () => { };
|
|
253
263
|
const ready = new Promise((resolve) => {
|
|
254
|
-
|
|
264
|
+
readyFromStore = resolve;
|
|
265
|
+
});
|
|
266
|
+
let notifyDiscovered = () => { };
|
|
267
|
+
const firstDiscovery = new Promise((resolve) => {
|
|
268
|
+
notifyDiscovered = resolve;
|
|
255
269
|
});
|
|
270
|
+
let roomsNamed = false;
|
|
271
|
+
function notifyReady() {
|
|
272
|
+
notifyDiscovered();
|
|
273
|
+
readyFromStore();
|
|
274
|
+
}
|
|
256
275
|
function rememberRoomDraft(record) {
|
|
257
276
|
const room = roomDrafts.get(record.locator) ?? new Map();
|
|
258
277
|
room.set(record.path, record);
|
|
@@ -339,7 +358,9 @@ export function composeRuntime(options) {
|
|
|
339
358
|
}
|
|
340
359
|
const file = parts.file === undefined
|
|
341
360
|
? undefined
|
|
342
|
-
: await heldFile(parts.file.blob,
|
|
361
|
+
: await heldFile(parts.file.blob, parts.file.base === undefined
|
|
362
|
+
? binaryBase(await visibleOf(room, [path]), path)
|
|
363
|
+
: parts.file.base);
|
|
343
364
|
const record = {
|
|
344
365
|
locator: room,
|
|
345
366
|
path,
|
|
@@ -374,6 +395,20 @@ export function composeRuntime(options) {
|
|
|
374
395
|
(queuesWithoutReplica.size > 0 ||
|
|
375
396
|
[...replicas.values()].some((replica) => replica.queue.length > 0)));
|
|
376
397
|
}
|
|
398
|
+
function unsent() {
|
|
399
|
+
const changed = new Map();
|
|
400
|
+
for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
|
|
401
|
+
for (const { batch, state } of entriesOf(room)) {
|
|
402
|
+
if (state === 'local')
|
|
403
|
+
continue;
|
|
404
|
+
for (const { path } of batch.changes)
|
|
405
|
+
changed.set(`${room}\u0000${path}`, { room, path });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return [...changed]
|
|
409
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
410
|
+
.map(([, change]) => change);
|
|
411
|
+
}
|
|
377
412
|
function attention() {
|
|
378
413
|
const attentionRequired = [];
|
|
379
414
|
for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
|
|
@@ -385,16 +420,9 @@ export function composeRuntime(options) {
|
|
|
385
420
|
returned: structuredClone(record.returned),
|
|
386
421
|
});
|
|
387
422
|
}
|
|
388
|
-
for (const [room, refusal] of sourceRefusals)
|
|
389
|
-
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
390
423
|
attentionRequired.sort((left, right) => {
|
|
391
424
|
if (left.room !== right.room)
|
|
392
425
|
return left.room < right.room ? -1 : 1;
|
|
393
|
-
if ('refusal' in left || 'refusal' in right) {
|
|
394
|
-
if ('refusal' in left && 'refusal' in right)
|
|
395
|
-
return 0;
|
|
396
|
-
return 'refusal' in left ? -1 : 1;
|
|
397
|
-
}
|
|
398
426
|
if (left.path !== right.path)
|
|
399
427
|
return left.path < right.path ? -1 : 1;
|
|
400
428
|
return 0;
|
|
@@ -406,7 +434,7 @@ export function composeRuntime(options) {
|
|
|
406
434
|
if (reset === null || submission.reset < reset)
|
|
407
435
|
reset = submission.reset;
|
|
408
436
|
}
|
|
409
|
-
return { attentionRequired, reset };
|
|
437
|
+
return { attentionRequired, unsent: unsent(), reset };
|
|
410
438
|
}
|
|
411
439
|
function documentsIn(locator) {
|
|
412
440
|
return replicas.get(locator)?.count ?? 0;
|
|
@@ -495,11 +523,26 @@ export function composeRuntime(options) {
|
|
|
495
523
|
const value = listed();
|
|
496
524
|
for (const listener of roomListeners)
|
|
497
525
|
listener(value);
|
|
526
|
+
emitKnownRooms();
|
|
527
|
+
}
|
|
528
|
+
function emitKnownRooms() {
|
|
529
|
+
conversations.learned();
|
|
530
|
+
if (unlistedRooms.size === 0)
|
|
531
|
+
return;
|
|
532
|
+
if (!roomsListed && rooms.size === 0 && refusal === null && !status.providerUnreachable)
|
|
533
|
+
return;
|
|
534
|
+
const value = listed();
|
|
535
|
+
for (const listener of [...unlistedRooms]) {
|
|
536
|
+
unlistedRooms.delete(listener);
|
|
537
|
+
roomListeners.add(listener);
|
|
538
|
+
listener(value);
|
|
539
|
+
}
|
|
498
540
|
}
|
|
499
541
|
function emitRefusal() {
|
|
500
542
|
queueReadiness();
|
|
501
543
|
for (const listener of refusalListeners)
|
|
502
544
|
listener(refusal);
|
|
545
|
+
emitKnownRooms();
|
|
503
546
|
}
|
|
504
547
|
function emitStorageLoss() {
|
|
505
548
|
queueReadiness();
|
|
@@ -520,6 +563,7 @@ export function composeRuntime(options) {
|
|
|
520
563
|
if (closed)
|
|
521
564
|
return;
|
|
522
565
|
queueReadiness();
|
|
566
|
+
emitKnownRooms();
|
|
523
567
|
const next = status.state === 'synced' && !conversations.current()
|
|
524
568
|
? { ...status, state: 'syncing' }
|
|
525
569
|
: status;
|
|
@@ -533,9 +577,6 @@ export function composeRuntime(options) {
|
|
|
533
577
|
if (localSaves)
|
|
534
578
|
return Object.freeze({ batch, state: 'local' });
|
|
535
579
|
const state = pendings.get(pendingKey(locator, batch.id));
|
|
536
|
-
if (state?.accepted !== undefined) {
|
|
537
|
-
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
538
|
-
}
|
|
539
580
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
540
581
|
}
|
|
541
582
|
function entriesOf(locator) {
|
|
@@ -563,11 +604,14 @@ export function composeRuntime(options) {
|
|
|
563
604
|
if (held === undefined || closed || held.indexed)
|
|
564
605
|
return;
|
|
565
606
|
const key = held.record.key;
|
|
566
|
-
const done = await exclusive(async () =>
|
|
607
|
+
const done = await exclusive(async () => {
|
|
608
|
+
const local = await openLocal();
|
|
609
|
+
return timed(deriveBudget, () => local.rederive(key, composed(held.record), deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR));
|
|
610
|
+
});
|
|
567
611
|
const now = replicas.get(locator);
|
|
568
612
|
if (now?.record.key !== key)
|
|
569
613
|
continue;
|
|
570
|
-
if (!done)
|
|
614
|
+
if (!done.done)
|
|
571
615
|
continue;
|
|
572
616
|
replicas.set(locator, { ...now, indexed: true });
|
|
573
617
|
publishChangeSet(changeSet(locator, { documents: null }));
|
|
@@ -645,7 +689,7 @@ export function composeRuntime(options) {
|
|
|
645
689
|
coverage.push({
|
|
646
690
|
room: locator,
|
|
647
691
|
generation: replica.record.transfer,
|
|
648
|
-
current: !(catching.has(locator) ||
|
|
692
|
+
current: !(catching.has(locator) || transfers.has(locator)),
|
|
649
693
|
});
|
|
650
694
|
read.push({ locator, key: replica.record.key });
|
|
651
695
|
for (const batch of replica.queue)
|
|
@@ -672,8 +716,6 @@ export function composeRuntime(options) {
|
|
|
672
716
|
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
673
717
|
}
|
|
674
718
|
function roomState(locator) {
|
|
675
|
-
if (sourceRefusals.has(locator))
|
|
676
|
-
return 'refused';
|
|
677
719
|
const listedRoom = discovered.get(locator);
|
|
678
720
|
if (roomsListed &&
|
|
679
721
|
(listedRoom === undefined ||
|
|
@@ -688,7 +730,7 @@ export function composeRuntime(options) {
|
|
|
688
730
|
}
|
|
689
731
|
function conversationCoverage(reach) {
|
|
690
732
|
const declared = reach === null || reach.conversations;
|
|
691
|
-
if (!declared || options.adapter.conversations === undefined
|
|
733
|
+
if (!declared || options.adapter.conversations === undefined)
|
|
692
734
|
return 'undeclared';
|
|
693
735
|
if (!conversations.known() || !conversations.current())
|
|
694
736
|
return 'acquiring';
|
|
@@ -703,67 +745,47 @@ export function composeRuntime(options) {
|
|
|
703
745
|
? 'connected'
|
|
704
746
|
: 'connecting';
|
|
705
747
|
}
|
|
706
|
-
function
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
rule: held.code,
|
|
718
|
-
location: typeof location === 'string' ? location : null,
|
|
719
|
-
},
|
|
720
|
-
];
|
|
721
|
-
});
|
|
748
|
+
function bytesOf(room, underway) {
|
|
749
|
+
const transfer = transfers.get(room);
|
|
750
|
+
if (transfer !== undefined) {
|
|
751
|
+
const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
|
|
752
|
+
return {
|
|
753
|
+
done: transfer.done.bytes - Math.ceil(owed),
|
|
754
|
+
total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
|
|
758
|
+
return { done: held.bytes, total: arrivals.get(room) ?? held.bytes };
|
|
722
759
|
}
|
|
723
760
|
function installationOf(coverage) {
|
|
724
761
|
const rooms = { done: 0, total: 0 };
|
|
725
|
-
const parts = { done: 0, total: 0 };
|
|
726
762
|
const bytes = { done: 0, total: 0 };
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
let waiting = false;
|
|
763
|
+
let installing = 0;
|
|
764
|
+
let downloading = false;
|
|
730
765
|
for (const { room, state } of coverage) {
|
|
731
766
|
if (state === 'unadmitted')
|
|
732
767
|
continue;
|
|
733
768
|
rooms.total += 1;
|
|
734
769
|
if (state === 'complete')
|
|
735
770
|
rooms.done += 1;
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
if (
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
const installing = held !== undefined && state !== 'complete';
|
|
755
|
-
if (installing)
|
|
756
|
-
steps.add(held.step);
|
|
757
|
-
waiting ||= installing || state === 'acquiring';
|
|
758
|
-
}
|
|
759
|
-
return {
|
|
760
|
-
step: INSTALL_STEPS.find((step) => steps.has(step)) ?? null,
|
|
761
|
-
paused: waiting && !onScreen ? 'hidden' : null,
|
|
762
|
-
rooms,
|
|
763
|
-
parts,
|
|
764
|
-
bytes,
|
|
765
|
-
received,
|
|
766
|
-
};
|
|
771
|
+
const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
|
|
772
|
+
if (underway)
|
|
773
|
+
installing += 1;
|
|
774
|
+
if (underway && (streaming.has(room) || !arrivals.has(room)))
|
|
775
|
+
downloading = true;
|
|
776
|
+
const now = bytesOf(room, underway);
|
|
777
|
+
const earlier = underway ? startedOver.get(room) : undefined;
|
|
778
|
+
bytes.done += Math.max(now.done, earlier?.done ?? 0);
|
|
779
|
+
bytes.total += Math.max(now.total, earlier?.total ?? 0);
|
|
780
|
+
}
|
|
781
|
+
const step = installing === 0
|
|
782
|
+
? null
|
|
783
|
+
: downloading
|
|
784
|
+
? 'downloading'
|
|
785
|
+
: bytes.done < bytes.total
|
|
786
|
+
? 'writing'
|
|
787
|
+
: 'replaying';
|
|
788
|
+
return { step, rooms, bytes };
|
|
767
789
|
}
|
|
768
790
|
function readinessFor(port, reach) {
|
|
769
791
|
const required = requiredRooms(reach);
|
|
@@ -787,9 +809,9 @@ export function composeRuntime(options) {
|
|
|
787
809
|
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
788
810
|
space: full ? 'full' : port.space,
|
|
789
811
|
installation: installationOf(coverage),
|
|
790
|
-
refused: refusedIn(required.rooms),
|
|
791
812
|
lost: storageLoss !== null,
|
|
792
|
-
established: targets[port.target]?.established === true
|
|
813
|
+
established: targets[port.target]?.established === true ||
|
|
814
|
+
(usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
|
|
793
815
|
});
|
|
794
816
|
}
|
|
795
817
|
function establish(name) {
|
|
@@ -842,7 +864,8 @@ export function composeRuntime(options) {
|
|
|
842
864
|
probeEachMinute();
|
|
843
865
|
}
|
|
844
866
|
function installedTarget() {
|
|
845
|
-
return
|
|
867
|
+
return ((usableWhenHeld.length > 0 && reopened.size > 0) ||
|
|
868
|
+
Object.values(targets).some((target) => target.established));
|
|
846
869
|
}
|
|
847
870
|
function probeEachMinute() {
|
|
848
871
|
if (!full || probing !== null || closed || !installedTarget())
|
|
@@ -873,8 +896,12 @@ export function composeRuntime(options) {
|
|
|
873
896
|
unpreparable.clear();
|
|
874
897
|
journal.record('space freed');
|
|
875
898
|
queueReadiness();
|
|
876
|
-
if (
|
|
877
|
-
|
|
899
|
+
if (closed || authorization === undefined)
|
|
900
|
+
return;
|
|
901
|
+
if (options.adapter.roomStream === undefined)
|
|
902
|
+
void sync('unstreamed', null);
|
|
903
|
+
else
|
|
904
|
+
void resynchronize();
|
|
878
905
|
}
|
|
879
906
|
function seedRoomHeld(room, version) {
|
|
880
907
|
return indexed(room) && reachedBy(room, version);
|
|
@@ -1000,6 +1027,10 @@ export function composeRuntime(options) {
|
|
|
1000
1027
|
}
|
|
1001
1028
|
function publishChangeSet(set) {
|
|
1002
1029
|
applyChangeSet(set);
|
|
1030
|
+
for (const { locator, path } of set.documents) {
|
|
1031
|
+
if (path !== null)
|
|
1032
|
+
attachedFiles.get(locator)?.delete(path);
|
|
1033
|
+
}
|
|
1003
1034
|
if (set.rooms.length > 0 ||
|
|
1004
1035
|
[...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
|
|
1005
1036
|
emitRooms();
|
|
@@ -1018,6 +1049,8 @@ export function composeRuntime(options) {
|
|
|
1018
1049
|
}
|
|
1019
1050
|
async function refuse(reason, provider = null) {
|
|
1020
1051
|
discoveryPaused = true;
|
|
1052
|
+
inFlight.abort();
|
|
1053
|
+
inFlight = new AbortController();
|
|
1021
1054
|
try {
|
|
1022
1055
|
if (reason === 'revoked-authorization') {
|
|
1023
1056
|
await exclusive(async () => {
|
|
@@ -1035,7 +1068,7 @@ export function composeRuntime(options) {
|
|
|
1035
1068
|
locator,
|
|
1036
1069
|
key: record.key,
|
|
1037
1070
|
record: storedRecord(record),
|
|
1038
|
-
|
|
1071
|
+
composed: composedOf(composed(record)),
|
|
1039
1072
|
})),
|
|
1040
1073
|
});
|
|
1041
1074
|
}
|
|
@@ -1102,10 +1135,10 @@ export function composeRuntime(options) {
|
|
|
1102
1135
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
1103
1136
|
roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
|
|
1104
1137
|
synchronizationAvailable,
|
|
1105
|
-
refusal: scopedRefusals.get(room.locator) ??
|
|
1138
|
+
refusal: scopedRefusals.get(room.locator) ?? null,
|
|
1106
1139
|
});
|
|
1107
1140
|
}
|
|
1108
|
-
async function applyDiscovery(found, started,
|
|
1141
|
+
async function applyDiscovery(found, started, left) {
|
|
1109
1142
|
const lockedBefore = lockedRooms();
|
|
1110
1143
|
for (const room of found) {
|
|
1111
1144
|
const fence = fences.get(room.locator);
|
|
@@ -1119,18 +1152,11 @@ export function composeRuntime(options) {
|
|
|
1119
1152
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
1120
1153
|
for (const locator of listedLocators)
|
|
1121
1154
|
scopedRefusals.delete(locator);
|
|
1122
|
-
if (
|
|
1155
|
+
if (left === undefined)
|
|
1123
1156
|
discovered.clear();
|
|
1124
|
-
else
|
|
1125
|
-
for (const locator of covered)
|
|
1126
|
-
discovered.delete(locator);
|
|
1127
1157
|
for (const room of listed)
|
|
1128
1158
|
discovered.set(room.locator, room);
|
|
1129
|
-
|
|
1130
|
-
if (options.adapter.exhaustiveDiscovery === true)
|
|
1131
|
-
await loseRooms(unlisted);
|
|
1132
|
-
else
|
|
1133
|
-
unlistedKept(unlisted);
|
|
1159
|
+
await loseRooms(left ?? unlistedHeld(new Set(found.map((room) => room.locator))));
|
|
1134
1160
|
for (const room of listed) {
|
|
1135
1161
|
const held = replicas.get(room.locator)?.record;
|
|
1136
1162
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -1161,15 +1187,6 @@ export function composeRuntime(options) {
|
|
|
1161
1187
|
]),
|
|
1162
1188
|
].filter((locator) => !listed.has(locator));
|
|
1163
1189
|
}
|
|
1164
|
-
function unlistedKept(unlisted) {
|
|
1165
|
-
for (const locator of unlisted) {
|
|
1166
|
-
const held = replicas.get(locator)?.record;
|
|
1167
|
-
if (held !== undefined)
|
|
1168
|
-
list(held, false);
|
|
1169
|
-
else
|
|
1170
|
-
rooms.delete(locator);
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
1190
|
function fence(locators) {
|
|
1174
1191
|
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
1175
1192
|
const removed = fresh.map((locator) => ({
|
|
@@ -1186,17 +1203,16 @@ export function composeRuntime(options) {
|
|
|
1186
1203
|
queuesWithoutReplica.delete(locator);
|
|
1187
1204
|
rooms.delete(locator);
|
|
1188
1205
|
roomDrafts.delete(locator);
|
|
1189
|
-
|
|
1206
|
+
attachedFiles.delete(locator);
|
|
1207
|
+
untransferred(locator);
|
|
1208
|
+
startedOver.delete(locator);
|
|
1190
1209
|
catching.delete(locator);
|
|
1191
1210
|
discovered.delete(locator);
|
|
1192
|
-
announced.delete(locator);
|
|
1193
1211
|
unreadable.delete(locator);
|
|
1194
1212
|
stale.delete(locator);
|
|
1195
1213
|
repairable.delete(locator);
|
|
1196
1214
|
unpreparable.delete(locator);
|
|
1197
1215
|
scopedRefusals.delete(locator);
|
|
1198
|
-
sourceRefusals.delete(locator);
|
|
1199
|
-
declarationRefusals.delete(locator);
|
|
1200
1216
|
models.delete(locator);
|
|
1201
1217
|
liveModels.delete(locator);
|
|
1202
1218
|
invalidModels.delete(locator);
|
|
@@ -1285,8 +1301,9 @@ export function composeRuntime(options) {
|
|
|
1285
1301
|
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1286
1302
|
targets = held.targets;
|
|
1287
1303
|
transfers.clear();
|
|
1288
|
-
|
|
1289
|
-
|
|
1304
|
+
arrivals.clear();
|
|
1305
|
+
streaming.clear();
|
|
1306
|
+
startedOver.clear();
|
|
1290
1307
|
limits = held.providerLimits;
|
|
1291
1308
|
capabilities = held.capabilities;
|
|
1292
1309
|
member = held.member;
|
|
@@ -1296,7 +1313,7 @@ export function composeRuntime(options) {
|
|
|
1296
1313
|
presentations.clear();
|
|
1297
1314
|
for (const record of held.presentation)
|
|
1298
1315
|
presentations.set(record.app, record.value);
|
|
1299
|
-
conversations.hydrate(heldConversations, chatDrafts);
|
|
1316
|
+
conversations.hydrate(heldConversations, chatDrafts, held.conversationList, held.conversationCursor);
|
|
1300
1317
|
pendings.clear();
|
|
1301
1318
|
for (const queue of queues) {
|
|
1302
1319
|
for (const { batch, state } of queue.pending) {
|
|
@@ -1304,6 +1321,7 @@ export function composeRuntime(options) {
|
|
|
1304
1321
|
}
|
|
1305
1322
|
}
|
|
1306
1323
|
replicas.clear();
|
|
1324
|
+
reopened.clear();
|
|
1307
1325
|
queuesWithoutReplica.clear();
|
|
1308
1326
|
for (const [locator, queue] of queuedBatches) {
|
|
1309
1327
|
if (!held.rooms.has(locator))
|
|
@@ -1313,6 +1331,7 @@ export function composeRuntime(options) {
|
|
|
1313
1331
|
if (fences.has(stored.locator))
|
|
1314
1332
|
return;
|
|
1315
1333
|
const record = { ...stored, model: restored(stored.locator, stored.model) };
|
|
1334
|
+
reopened.add(record.locator);
|
|
1316
1335
|
replicas.set(record.locator, {
|
|
1317
1336
|
record,
|
|
1318
1337
|
queue: queuedBatches.get(record.locator) ?? [],
|
|
@@ -1323,7 +1342,7 @@ export function composeRuntime(options) {
|
|
|
1323
1342
|
});
|
|
1324
1343
|
const existing = rooms.get(record.locator);
|
|
1325
1344
|
if (existing === undefined)
|
|
1326
|
-
list(record,
|
|
1345
|
+
list(record, record.roomVersion !== null);
|
|
1327
1346
|
else {
|
|
1328
1347
|
rooms.set(record.locator, {
|
|
1329
1348
|
...existing,
|
|
@@ -1337,8 +1356,14 @@ export function composeRuntime(options) {
|
|
|
1337
1356
|
});
|
|
1338
1357
|
}
|
|
1339
1358
|
});
|
|
1340
|
-
|
|
1341
|
-
|
|
1359
|
+
try {
|
|
1360
|
+
for (const key of held.orphans)
|
|
1361
|
+
await opened.release(key);
|
|
1362
|
+
}
|
|
1363
|
+
catch (error) {
|
|
1364
|
+
if (!refusedForSpace(error))
|
|
1365
|
+
throw error;
|
|
1366
|
+
}
|
|
1342
1367
|
if (held.erasing || held.orphans.length > 0)
|
|
1343
1368
|
queueMicrotask(sweep);
|
|
1344
1369
|
return {
|
|
@@ -1540,8 +1565,6 @@ export function composeRuntime(options) {
|
|
|
1540
1565
|
function unpairedIn(locator, batch) {
|
|
1541
1566
|
if (!queueIn(locator).some((held) => held.id === batch.id))
|
|
1542
1567
|
return false;
|
|
1543
|
-
if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
|
|
1544
|
-
return false;
|
|
1545
1568
|
return !leavingIn(batch, locator) || withdrawable(locator)(batch);
|
|
1546
1569
|
}
|
|
1547
1570
|
async function together(local, settlements) {
|
|
@@ -1626,6 +1649,7 @@ export function composeRuntime(options) {
|
|
|
1626
1649
|
key: record.key,
|
|
1627
1650
|
record: storedRecord(record),
|
|
1628
1651
|
roots: record.typedSelection?.roots ?? [],
|
|
1652
|
+
composed: composedOf(composition),
|
|
1629
1653
|
...settling.commit,
|
|
1630
1654
|
...(overrides === undefined
|
|
1631
1655
|
? {}
|
|
@@ -1709,39 +1733,44 @@ export function composeRuntime(options) {
|
|
|
1709
1733
|
models.set(locator, state);
|
|
1710
1734
|
return { collection: state };
|
|
1711
1735
|
}
|
|
1712
|
-
function publish(local, room,
|
|
1736
|
+
function publish(local, room, reached, landing, live) {
|
|
1713
1737
|
const landed = exclusive(async () => {
|
|
1714
1738
|
if (!live()) {
|
|
1715
1739
|
caughtUpTo(room.locator);
|
|
1716
1740
|
return true;
|
|
1717
1741
|
}
|
|
1718
1742
|
const prior = replicas.get(room.locator);
|
|
1719
|
-
if (landing.
|
|
1720
|
-
(
|
|
1721
|
-
|
|
1743
|
+
if (!stands(room.locator, landing.from)) {
|
|
1744
|
+
if (landing.kind === 'refreshed') {
|
|
1745
|
+
await local.unrefresh(reached.key);
|
|
1746
|
+
return false;
|
|
1747
|
+
}
|
|
1748
|
+
caughtUpTo(room.locator);
|
|
1749
|
+
await abandoned(local, room.locator);
|
|
1722
1750
|
return false;
|
|
1723
1751
|
}
|
|
1752
|
+
const record = landing.kind === 'refreshed' && prior !== undefined
|
|
1753
|
+
? {
|
|
1754
|
+
...prior.record,
|
|
1755
|
+
roomVersion: reached.roomVersion,
|
|
1756
|
+
...(landing.remodelled ? { model: reached.model } : {}),
|
|
1757
|
+
}
|
|
1758
|
+
: reached;
|
|
1724
1759
|
adoptModels(room.locator, record);
|
|
1725
1760
|
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
1726
|
-
const roomVersion = record.roomVersion;
|
|
1727
|
-
const accepted = queue.filter((batch) => {
|
|
1728
|
-
const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
|
|
1729
|
-
return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
|
|
1730
|
-
});
|
|
1731
1761
|
let set;
|
|
1732
1762
|
try {
|
|
1733
|
-
set = await settle(local, room.locator, prior, record, queue
|
|
1763
|
+
set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
|
|
1734
1764
|
? {
|
|
1735
1765
|
room: true,
|
|
1736
1766
|
view: 'refreshed',
|
|
1737
|
-
commit: { refreshed: true
|
|
1767
|
+
commit: { refreshed: true },
|
|
1738
1768
|
}
|
|
1739
1769
|
: {
|
|
1740
1770
|
room: true,
|
|
1741
1771
|
view: landing.refreshed ? 'refreshed' : 'confirmed',
|
|
1742
1772
|
commit: {
|
|
1743
1773
|
published: { from: prior?.record.key ?? null },
|
|
1744
|
-
composition: composed(record),
|
|
1745
1774
|
...(landing.refreshed ? { refreshed: true } : {}),
|
|
1746
1775
|
},
|
|
1747
1776
|
});
|
|
@@ -1753,24 +1782,22 @@ export function composeRuntime(options) {
|
|
|
1753
1782
|
return true;
|
|
1754
1783
|
}
|
|
1755
1784
|
if (landing.kind === 'bootstrapped') {
|
|
1756
|
-
|
|
1785
|
+
untransferred(room.locator);
|
|
1786
|
+
startedOver.delete(room.locator);
|
|
1757
1787
|
catching.delete(room.locator);
|
|
1758
|
-
|
|
1788
|
+
if (prior !== undefined && prior.record.key !== record.key)
|
|
1789
|
+
sweep();
|
|
1790
|
+
name(room.locator);
|
|
1759
1791
|
}
|
|
1760
|
-
for (const batch of accepted)
|
|
1761
|
-
pendings.delete(pendingKey(room.locator, batch.id));
|
|
1762
1792
|
stale.delete(room.locator);
|
|
1763
1793
|
unpreparable.delete(room.locator);
|
|
1764
1794
|
if (unreadable.delete(room.locator))
|
|
1765
1795
|
scopedRefusals.delete(room.locator);
|
|
1766
|
-
sourceRefusals.delete(room.locator);
|
|
1767
|
-
declarationRefusals.delete(room.locator);
|
|
1768
1796
|
list(room, true);
|
|
1769
1797
|
publishChangeSet(set);
|
|
1770
1798
|
await unpair(local);
|
|
1771
1799
|
moveTo({ ...status, ...attention() });
|
|
1772
|
-
|
|
1773
|
-
if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1800
|
+
if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1774
1801
|
submitLater(room.locator);
|
|
1775
1802
|
}
|
|
1776
1803
|
return true;
|
|
@@ -1800,12 +1827,7 @@ export function composeRuntime(options) {
|
|
|
1800
1827
|
return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
|
|
1801
1828
|
}
|
|
1802
1829
|
async function saveParts(locator, named, writtenBy) {
|
|
1803
|
-
const read = named
|
|
1804
|
-
if (read.some(({ path }, at) => path === '' ||
|
|
1805
|
-
normalize(path) !== path ||
|
|
1806
|
-
read.findIndex((one) => one.path === path) !== at)) {
|
|
1807
|
-
throw runtimeError('room-draft-malformed');
|
|
1808
|
-
}
|
|
1830
|
+
const read = readSavedParts(named);
|
|
1809
1831
|
const target = await savedRoom(locator);
|
|
1810
1832
|
const saved = [];
|
|
1811
1833
|
const files = [];
|
|
@@ -1814,7 +1836,7 @@ export function composeRuntime(options) {
|
|
|
1814
1836
|
for (const { path, parts } of read) {
|
|
1815
1837
|
const file = parts.file === undefined
|
|
1816
1838
|
? undefined
|
|
1817
|
-
: await heldFile(parts.file.blob, binaryBase(visible, path));
|
|
1839
|
+
: await heldFile(parts.file.blob, parts.file.base === undefined ? binaryBase(visible, path) : parts.file.base);
|
|
1818
1840
|
if (parts.file !== undefined && file !== undefined) {
|
|
1819
1841
|
files.push({ key: file.localId, blob: parts.file.blob });
|
|
1820
1842
|
}
|
|
@@ -1849,13 +1871,14 @@ export function composeRuntime(options) {
|
|
|
1849
1871
|
const referring = removed.length === 0
|
|
1850
1872
|
? []
|
|
1851
1873
|
: (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
|
|
1852
|
-
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
|
|
1874
|
+
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
|
|
1853
1875
|
const { visible } = await replayed(record, confirmedLayer, replica.queue);
|
|
1854
1876
|
const composition = composed(record);
|
|
1855
1877
|
return {
|
|
1856
1878
|
record,
|
|
1857
1879
|
confirmed: confirmedLayer,
|
|
1858
1880
|
visible,
|
|
1881
|
+
folders: new Set(await local.folders(record.key, 'visible', paths)),
|
|
1859
1882
|
*visibleOver(queue) {
|
|
1860
1883
|
return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
|
|
1861
1884
|
},
|
|
@@ -1878,18 +1901,6 @@ export function composeRuntime(options) {
|
|
|
1878
1901
|
return undefined;
|
|
1879
1902
|
return entry.binary !== null ? null : entry.content;
|
|
1880
1903
|
}
|
|
1881
|
-
function walkOf(local, key, view) {
|
|
1882
|
-
return async (after) => {
|
|
1883
|
-
const page = await local.walk(key, view, after, WALK_BYTES);
|
|
1884
|
-
return {
|
|
1885
|
-
entries: page.entries.map(({ path, content, binary }) => ({
|
|
1886
|
-
path,
|
|
1887
|
-
content: binary !== null ? null : content,
|
|
1888
|
-
})),
|
|
1889
|
-
next: page.next,
|
|
1890
|
-
};
|
|
1891
|
-
};
|
|
1892
|
-
}
|
|
1893
1904
|
function lookupOf(local, key, view) {
|
|
1894
1905
|
return async (paths) => {
|
|
1895
1906
|
if (paths.length === 0)
|
|
@@ -1906,37 +1917,6 @@ export function composeRuntime(options) {
|
|
|
1906
1917
|
const read = parseFrontmatter(entry.content ?? '');
|
|
1907
1918
|
return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
|
|
1908
1919
|
}
|
|
1909
|
-
function overriddenWalk(walk, overrides) {
|
|
1910
|
-
const sorted = [...overrides.keys()].sort();
|
|
1911
|
-
const beyond = (path) => {
|
|
1912
|
-
if (path === null)
|
|
1913
|
-
return 0;
|
|
1914
|
-
let low = 0;
|
|
1915
|
-
let high = sorted.length;
|
|
1916
|
-
while (low < high) {
|
|
1917
|
-
const middle = (low + high) >>> 1;
|
|
1918
|
-
if (sorted[middle] <= path)
|
|
1919
|
-
low = middle + 1;
|
|
1920
|
-
else
|
|
1921
|
-
high = middle;
|
|
1922
|
-
}
|
|
1923
|
-
return low;
|
|
1924
|
-
};
|
|
1925
|
-
return async (after) => {
|
|
1926
|
-
const page = await walk(after);
|
|
1927
|
-
const end = page.next;
|
|
1928
|
-
const added = sorted
|
|
1929
|
-
.slice(beyond(after), end === null ? sorted.length : beyond(end))
|
|
1930
|
-
.flatMap((path) => {
|
|
1931
|
-
const entry = overrides.get(path);
|
|
1932
|
-
return entry === null || entry === undefined
|
|
1933
|
-
? []
|
|
1934
|
-
: [{ path, content: entry.binary !== null ? null : entry.content }];
|
|
1935
|
-
});
|
|
1936
|
-
const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
|
|
1937
|
-
return { entries, next: end };
|
|
1938
|
-
};
|
|
1939
|
-
}
|
|
1940
1920
|
function overriddenLookup(lookup, overrides) {
|
|
1941
1921
|
return async (paths) => {
|
|
1942
1922
|
const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
|
|
@@ -1954,9 +1934,8 @@ export function composeRuntime(options) {
|
|
|
1954
1934
|
}
|
|
1955
1935
|
function validatorOf(record, confirmedLayer) {
|
|
1956
1936
|
return async (resulting, touched) => {
|
|
1957
|
-
const composition = compositionFor(record);
|
|
1958
1937
|
const local = await openLocal();
|
|
1959
|
-
const
|
|
1938
|
+
const changes = [];
|
|
1960
1939
|
const overrides = new Map();
|
|
1961
1940
|
const pace = paced();
|
|
1962
1941
|
for (const path of new Set([...confirmedLayer.held, ...touched])) {
|
|
@@ -1967,19 +1946,13 @@ export function composeRuntime(options) {
|
|
|
1967
1946
|
continue;
|
|
1968
1947
|
overrides.set(path, now);
|
|
1969
1948
|
if (textOf(was) !== textOf(now))
|
|
1970
|
-
|
|
1949
|
+
changes.push({ path, was: textOf(was), now: textOf(now) });
|
|
1971
1950
|
}
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
texts: textsOf(local, record.key, 'confirmed'),
|
|
1978
|
-
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
1979
|
-
}));
|
|
1980
|
-
if (clean)
|
|
1981
|
-
return { verdict: 'accept' };
|
|
1982
|
-
return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
|
|
1951
|
+
return validatedNeighbourhood(changes, compositionFor(record), {
|
|
1952
|
+
lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
|
|
1953
|
+
texts: textsOf(local, record.key, 'confirmed'),
|
|
1954
|
+
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
1955
|
+
});
|
|
1983
1956
|
};
|
|
1984
1957
|
}
|
|
1985
1958
|
function textsOf(local, key, view) {
|
|
@@ -2003,6 +1976,7 @@ export function composeRuntime(options) {
|
|
|
2003
1976
|
operations: room.operations,
|
|
2004
1977
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
2005
1978
|
visible: saving.visible,
|
|
1979
|
+
folders: saving.folders,
|
|
2006
1980
|
queued: queuedDocuments(replica.queue),
|
|
2007
1981
|
unmodeled: await unmodeledIn(record),
|
|
2008
1982
|
validate: validatorOf(record, saving.confirmed),
|
|
@@ -2023,6 +1997,7 @@ export function composeRuntime(options) {
|
|
|
2023
1997
|
locator,
|
|
2024
1998
|
key: saving.record.key,
|
|
2025
1999
|
roots: saving.record.typedSelection?.roots ?? [],
|
|
2000
|
+
composed: composedOf(composed(saving.record)),
|
|
2026
2001
|
overlay: diff.overlay,
|
|
2027
2002
|
});
|
|
2028
2003
|
}
|
|
@@ -2054,7 +2029,7 @@ export function composeRuntime(options) {
|
|
|
2054
2029
|
}
|
|
2055
2030
|
const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
|
|
2056
2031
|
const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
|
|
2057
|
-
const changed = saved.flatMap(({ path, parts }) => changedParts(path,
|
|
2032
|
+
const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
|
|
2058
2033
|
if (changed.length > 0)
|
|
2059
2034
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2060
2035
|
const target = replicas.get(destination);
|
|
@@ -2179,7 +2154,7 @@ export function composeRuntime(options) {
|
|
|
2179
2154
|
...saved.map(({ path }) => path),
|
|
2180
2155
|
...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
|
|
2181
2156
|
]);
|
|
2182
|
-
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path,
|
|
2157
|
+
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
|
|
2183
2158
|
if (changed.length > 0)
|
|
2184
2159
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2185
2160
|
const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
|
|
@@ -2207,6 +2182,7 @@ export function composeRuntime(options) {
|
|
|
2207
2182
|
providerLimits: limits,
|
|
2208
2183
|
handle: room.handle,
|
|
2209
2184
|
visible: saving.visible,
|
|
2185
|
+
folders: saving.folders,
|
|
2210
2186
|
changes,
|
|
2211
2187
|
bases: parts.flatMap(({ bases }) => bases),
|
|
2212
2188
|
queued: queuedDocuments(replica.queue),
|
|
@@ -2357,6 +2333,9 @@ export function composeRuntime(options) {
|
|
|
2357
2333
|
async function readHistory(locator, path, query = {}) {
|
|
2358
2334
|
if (closed || refusal !== null)
|
|
2359
2335
|
return UNCERTAIN;
|
|
2336
|
+
await firstDiscovery;
|
|
2337
|
+
if (!roomsNamed)
|
|
2338
|
+
return UNCERTAIN;
|
|
2360
2339
|
try {
|
|
2361
2340
|
await ensureAuthorization();
|
|
2362
2341
|
}
|
|
@@ -2449,6 +2428,9 @@ export function composeRuntime(options) {
|
|
|
2449
2428
|
async function transferBinary(request, transfer, answer) {
|
|
2450
2429
|
if (closed || refusal !== null)
|
|
2451
2430
|
return UNCERTAIN;
|
|
2431
|
+
await firstDiscovery;
|
|
2432
|
+
if (!roomsNamed)
|
|
2433
|
+
return UNCERTAIN;
|
|
2452
2434
|
try {
|
|
2453
2435
|
await ensureAuthorization();
|
|
2454
2436
|
}
|
|
@@ -2479,23 +2461,27 @@ export function composeRuntime(options) {
|
|
|
2479
2461
|
return UNCERTAIN;
|
|
2480
2462
|
}
|
|
2481
2463
|
}
|
|
2464
|
+
function fetchBinary(request, name) {
|
|
2465
|
+
return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
|
|
2466
|
+
}
|
|
2482
2467
|
async function readBinary(request, name) {
|
|
2483
2468
|
if (closed)
|
|
2484
2469
|
return UNCERTAIN;
|
|
2485
2470
|
const held = await keptFiles.find(name);
|
|
2486
2471
|
if (held !== null)
|
|
2487
2472
|
return held;
|
|
2488
|
-
return
|
|
2473
|
+
return fetchBinary(request, name);
|
|
2489
2474
|
}
|
|
2490
2475
|
async function binaryPath(locator, path, revision) {
|
|
2491
2476
|
if (revision !== undefined)
|
|
2492
2477
|
return { kind: 'path', locator, path, revision };
|
|
2493
2478
|
const replica = replicas.get(locator);
|
|
2494
|
-
if (replica === undefined)
|
|
2495
|
-
return null;
|
|
2496
2479
|
const local = await openLocal();
|
|
2497
|
-
const [held] = await local.entries(replica.record.key, 'visible', [path]);
|
|
2498
|
-
|
|
2480
|
+
const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
|
|
2481
|
+
const attached = held === undefined ? attachedFiles.get(locator)?.get(path) : undefined;
|
|
2482
|
+
if (attached !== undefined)
|
|
2483
|
+
return { kind: 'path', locator, path, revision: attached.revision };
|
|
2484
|
+
if (replica === undefined || held?.binary === undefined || held.binary === null)
|
|
2499
2485
|
return null;
|
|
2500
2486
|
if (held.binary.state === 'pending')
|
|
2501
2487
|
return { kind: 'pending', localId: held.binary.localId };
|
|
@@ -2527,33 +2513,83 @@ export function composeRuntime(options) {
|
|
|
2527
2513
|
revision: record.revision,
|
|
2528
2514
|
};
|
|
2529
2515
|
}
|
|
2530
|
-
async function
|
|
2516
|
+
async function askedBinary(locator, path, revision) {
|
|
2531
2517
|
await ready;
|
|
2532
2518
|
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2533
|
-
|
|
2519
|
+
return drafted !== undefined && drafted.localId === revision
|
|
2534
2520
|
? { kind: 'pending', localId: revision }
|
|
2535
|
-
:
|
|
2521
|
+
: binaryPath(locator, path, revision);
|
|
2522
|
+
}
|
|
2523
|
+
function pendingBinary(locator, path, localId) {
|
|
2524
|
+
const facts = queuedFacts(locator, path, localId);
|
|
2525
|
+
return facts === null
|
|
2526
|
+
? absentBinary(path)
|
|
2527
|
+
: {
|
|
2528
|
+
kind: 'binary',
|
|
2529
|
+
file: localId,
|
|
2530
|
+
byteLength: facts.byteLength,
|
|
2531
|
+
mediaType: facts.mediaType,
|
|
2532
|
+
};
|
|
2533
|
+
}
|
|
2534
|
+
async function readBinaryPath(locator, path, revision) {
|
|
2535
|
+
const asked = await askedBinary(locator, path, revision);
|
|
2536
2536
|
if (asked === null)
|
|
2537
2537
|
return absentBinary(path);
|
|
2538
|
-
if (asked.kind === 'pending')
|
|
2539
|
-
|
|
2540
|
-
return facts === null
|
|
2541
|
-
? absentBinary(path)
|
|
2542
|
-
: {
|
|
2543
|
-
kind: 'binary',
|
|
2544
|
-
file: asked.localId,
|
|
2545
|
-
byteLength: facts.byteLength,
|
|
2546
|
-
mediaType: facts.mediaType,
|
|
2547
|
-
};
|
|
2548
|
-
}
|
|
2538
|
+
if (asked.kind === 'pending')
|
|
2539
|
+
return pendingBinary(locator, path, asked.localId);
|
|
2549
2540
|
return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
|
|
2550
2541
|
}
|
|
2542
|
+
async function heldMediaType(locator, path) {
|
|
2543
|
+
const replica = replicas.get(locator);
|
|
2544
|
+
const [held] = replica === undefined
|
|
2545
|
+
? []
|
|
2546
|
+
: await (await openLocal()).entries(replica.record.key, 'visible', [path]);
|
|
2547
|
+
return held === undefined
|
|
2548
|
+
? attachedFiles.get(locator)?.get(path)?.mediaType
|
|
2549
|
+
: held.binary?.mediaType;
|
|
2550
|
+
}
|
|
2551
|
+
function linkOf(asked, held) {
|
|
2552
|
+
return transferBinary(asked, (authorization) => options.adapter.link(authorization, asked), (result) => binaryLinkAnswer(result, held));
|
|
2553
|
+
}
|
|
2551
2554
|
async function linkBinaryPath(locator, path, revision) {
|
|
2552
2555
|
await ready;
|
|
2553
2556
|
const asked = await binaryPath(locator, path, revision);
|
|
2554
2557
|
if (asked === null || asked.kind === 'pending')
|
|
2555
2558
|
return absentBinary(path);
|
|
2556
|
-
return
|
|
2559
|
+
return linkOf(asked, revision === undefined ? await heldMediaType(locator, path) : undefined);
|
|
2560
|
+
}
|
|
2561
|
+
async function showBinaryPath(locator, path, revision) {
|
|
2562
|
+
const asked = await askedBinary(locator, path, revision);
|
|
2563
|
+
if (asked === null)
|
|
2564
|
+
return absentBinary(path);
|
|
2565
|
+
if (asked.kind === 'pending')
|
|
2566
|
+
return pendingBinary(locator, path, asked.localId);
|
|
2567
|
+
if (closed)
|
|
2568
|
+
return UNCERTAIN;
|
|
2569
|
+
const name = { locator, path: asked.path, revision: asked.revision };
|
|
2570
|
+
const kept = await keptFiles.find(name);
|
|
2571
|
+
if (kept !== null)
|
|
2572
|
+
return kept;
|
|
2573
|
+
const mediaType = await heldMediaType(locator, path);
|
|
2574
|
+
if (mediaType?.startsWith('image/') === true)
|
|
2575
|
+
return fetchBinary(asked, name);
|
|
2576
|
+
return linkOf(asked, revision === undefined ? mediaType : undefined);
|
|
2577
|
+
}
|
|
2578
|
+
async function attachBinary(locator, file) {
|
|
2579
|
+
await ready;
|
|
2580
|
+
await firstDiscovery;
|
|
2581
|
+
if (closed || !roomsNamed || connectionOf() === 'unreachable' || refusal !== null)
|
|
2582
|
+
return OFFLINE;
|
|
2583
|
+
const request = await attachedFile(locator, file);
|
|
2584
|
+
return transferBinary({ locator, path: file.name }, async (held) => {
|
|
2585
|
+
const result = await options.adapter.attach(held, request);
|
|
2586
|
+
if (result.kind === 'attached') {
|
|
2587
|
+
const landed = attachedFiles.get(locator) ?? new Map();
|
|
2588
|
+
landed.set(result.path, { revision: result.revision, mediaType: request.mediaType });
|
|
2589
|
+
attachedFiles.set(locator, landed);
|
|
2590
|
+
}
|
|
2591
|
+
return result;
|
|
2592
|
+
}, attachAnswer);
|
|
2557
2593
|
}
|
|
2558
2594
|
function readPart(file, part) {
|
|
2559
2595
|
return database.ask({ kind: 'file.part', key: file, part });
|
|
@@ -2562,13 +2598,6 @@ export function composeRuntime(options) {
|
|
|
2562
2598
|
const freed = await database.ask({ kind: 'opened.clean' });
|
|
2563
2599
|
return freed + ((await options.shell.cleanUp?.()) ?? 0);
|
|
2564
2600
|
}
|
|
2565
|
-
function declaredAgents() {
|
|
2566
|
-
for (const capability of capabilities) {
|
|
2567
|
-
if (capability.conversationAgents !== undefined)
|
|
2568
|
-
return capability.conversationAgents;
|
|
2569
|
-
}
|
|
2570
|
-
return undefined;
|
|
2571
|
-
}
|
|
2572
2601
|
function declaredAction(name) {
|
|
2573
2602
|
for (const capability of capabilities) {
|
|
2574
2603
|
for (const action of capability.actions ?? []) {
|
|
@@ -2620,6 +2649,24 @@ export function composeRuntime(options) {
|
|
|
2620
2649
|
return UNCERTAIN;
|
|
2621
2650
|
}
|
|
2622
2651
|
}
|
|
2652
|
+
async function readConversationHistory(request) {
|
|
2653
|
+
await ready;
|
|
2654
|
+
const stream = options.adapter.conversations;
|
|
2655
|
+
if (closed || refusal !== null || stream?.history === undefined)
|
|
2656
|
+
return UNCERTAIN;
|
|
2657
|
+
journal.record('transfer conversation history');
|
|
2658
|
+
try {
|
|
2659
|
+
const result = await stream.history(request);
|
|
2660
|
+
if (result.kind === 'revoked-authorization')
|
|
2661
|
+
await refuse('revoked-authorization');
|
|
2662
|
+
journal.record(`outcome conversation history ${result.kind}`);
|
|
2663
|
+
return calledResult(result);
|
|
2664
|
+
}
|
|
2665
|
+
catch {
|
|
2666
|
+
journal.record('outcome conversation history transport-uncertainty');
|
|
2667
|
+
return UNCERTAIN;
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2623
2670
|
function unsettled(result) {
|
|
2624
2671
|
return result.kind !== 'action' && result.retryable;
|
|
2625
2672
|
}
|
|
@@ -2691,122 +2738,23 @@ export function composeRuntime(options) {
|
|
|
2691
2738
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
2692
2739
|
!options.adapter.reached(locator, roomVersion, written));
|
|
2693
2740
|
}
|
|
2694
|
-
function
|
|
2695
|
-
|
|
2696
|
-
const known = new Map(await texts(paths.filter((path) => !overrides.has(path))));
|
|
2697
|
-
for (const path of paths) {
|
|
2698
|
-
const entry = overrides.get(path);
|
|
2699
|
-
if (entry === undefined)
|
|
2700
|
-
continue;
|
|
2701
|
-
if (entry === null)
|
|
2702
|
-
known.delete(path);
|
|
2703
|
-
else
|
|
2704
|
-
known.set(path, entry.binary !== null ? null : entry.content);
|
|
2705
|
-
}
|
|
2706
|
-
return known;
|
|
2707
|
-
};
|
|
2708
|
-
}
|
|
2709
|
-
async function unreadableRoom(texts, walk, paths) {
|
|
2710
|
-
if (paths !== null) {
|
|
2711
|
-
const text = {};
|
|
2712
|
-
for (const [path, content] of await texts(paths))
|
|
2713
|
-
if (content !== null)
|
|
2714
|
-
text[path] = content;
|
|
2715
|
-
return stepped(unreadableIn(text, Object.keys(text)));
|
|
2716
|
-
}
|
|
2717
|
-
for (let after = null;;) {
|
|
2718
|
-
const page = await walk(after);
|
|
2719
|
-
const text = {};
|
|
2720
|
-
for (const { path, content } of page.entries)
|
|
2721
|
-
if (content !== null)
|
|
2722
|
-
text[path] = content;
|
|
2723
|
-
const refused = await stepped(unreadableIn(text, Object.keys(text)));
|
|
2724
|
-
if (refused !== null)
|
|
2725
|
-
return refused;
|
|
2726
|
-
if (page.next === null)
|
|
2727
|
-
return null;
|
|
2728
|
-
after = page.next;
|
|
2729
|
-
}
|
|
2730
|
-
}
|
|
2731
|
-
async function validatedRoom(local, record, view, previous, overrides = null) {
|
|
2732
|
-
const composition = composed(record);
|
|
2733
|
-
const modeled = record.model.status !== 'unavailable' || record.typedSelection !== null;
|
|
2734
|
-
const lookup = overrides === null
|
|
2735
|
-
? lookupOf(local, record.key, view)
|
|
2736
|
-
: overriddenLookup(lookupOf(local, record.key, view), overrides);
|
|
2737
|
-
const texts = overrides === null
|
|
2738
|
-
? textsOf(local, record.key, view)
|
|
2739
|
-
: overriddenTexts(textsOf(local, record.key, view), overrides);
|
|
2740
|
-
const walk = overrides === null
|
|
2741
|
-
? walkOf(local, record.key, view)
|
|
2742
|
-
: overriddenWalk(walkOf(local, record.key, view), overrides);
|
|
2743
|
-
let changes = null;
|
|
2744
|
-
let reshaped = false;
|
|
2745
|
-
if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
|
|
2746
|
-
const [was, now] = await Promise.all([
|
|
2747
|
-
local.entries(record.key, 'confirmed', previous.changed),
|
|
2748
|
-
overrides === null
|
|
2749
|
-
? local.entries(record.key, view, previous.changed)
|
|
2750
|
-
: Promise.resolve([]),
|
|
2751
|
-
]);
|
|
2752
|
-
const before = new Map(was.map((entry) => [entry.path, entry]));
|
|
2753
|
-
const after = overrides === null
|
|
2754
|
-
? new Map(now.map((entry) => [entry.path, entry]))
|
|
2755
|
-
: overrides;
|
|
2756
|
-
changes = previous.changed.flatMap((path) => {
|
|
2757
|
-
const one = before.get(path) ?? null;
|
|
2758
|
-
const other = after.get(path) ?? null;
|
|
2759
|
-
reshaped ||= textDiffers(one, other);
|
|
2760
|
-
const held = textOf(one);
|
|
2761
|
-
const next = textOf(other);
|
|
2762
|
-
return held === next ? [] : [{ path, was: held, now: next }];
|
|
2763
|
-
});
|
|
2764
|
-
}
|
|
2765
|
-
const named = changes === null ? null : changes.map(({ path }) => path);
|
|
2766
|
-
const textChanged = changes === null || changes.length > 0 || reshaped;
|
|
2767
|
-
const modelsChanged = previous === null || !sameComposition(composed(previous.record), composition);
|
|
2768
|
-
if (modeled && (textChanged || modelsChanged)) {
|
|
2769
|
-
const clean = previous !== null &&
|
|
2770
|
-
changes !== null &&
|
|
2771
|
-
(await changedCleanly(changes, composed(previous.record), composition, {
|
|
2772
|
-
lookup,
|
|
2773
|
-
texts,
|
|
2774
|
-
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2775
|
-
}));
|
|
2776
|
-
if (clean)
|
|
2777
|
-
return null;
|
|
2778
|
-
const verdict = await validatedCollection(walk, lookup, composition);
|
|
2779
|
-
if (verdict.verdict === 'accept')
|
|
2780
|
-
return null;
|
|
2781
|
-
if (verdict.rule !== 'model-unavailable')
|
|
2782
|
-
return { kind: 'refused', refusal: verdict };
|
|
2783
|
-
return unreadableRoom(texts, walk, named);
|
|
2784
|
-
}
|
|
2785
|
-
if (textChanged)
|
|
2786
|
-
return unreadableRoom(texts, walk, named);
|
|
2787
|
-
return null;
|
|
2788
|
-
}
|
|
2789
|
-
function refreshingOf(local, key, liveness, replaying = null) {
|
|
2790
|
-
const write = replaying === null ? exclusive : installWrite;
|
|
2741
|
+
function refreshingOf(local, key, composition, liveness, replaying = null) {
|
|
2742
|
+
const write = replaying === null ? exclusive : finishingWrite;
|
|
2791
2743
|
return {
|
|
2792
|
-
|
|
2793
|
-
if (replaying
|
|
2794
|
-
|
|
2795
|
-
installing(replaying, 'replaying');
|
|
2796
|
-
await screened();
|
|
2744
|
+
turn() {
|
|
2745
|
+
if (replaying !== null)
|
|
2746
|
+
installing(replaying);
|
|
2797
2747
|
},
|
|
2798
2748
|
prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
|
|
2799
2749
|
async stage(page) {
|
|
2800
2750
|
for (let written = 0;;) {
|
|
2801
|
-
if (replaying !== null)
|
|
2802
|
-
await screened();
|
|
2803
2751
|
const end = batchEnd(page.entries, written);
|
|
2804
2752
|
const last = end === page.entries.length;
|
|
2805
2753
|
const entries = page.entries.slice(written, end);
|
|
2806
2754
|
const staged = await write(async () => {
|
|
2807
2755
|
if (!liveness())
|
|
2808
2756
|
return false;
|
|
2809
|
-
await timed(() => local.refresh(key, last
|
|
2757
|
+
await timed(stageBudget, () => local.refresh(key, composition, last
|
|
2810
2758
|
? { ...page, entries }
|
|
2811
2759
|
: { entries, removals: [], folders: [], folderRemovals: [] }));
|
|
2812
2760
|
return true;
|
|
@@ -2825,94 +2773,180 @@ export function composeRuntime(options) {
|
|
|
2825
2773
|
let spent = 0;
|
|
2826
2774
|
while (end < entries.length) {
|
|
2827
2775
|
const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
|
|
2828
|
-
if (spent + size >
|
|
2776
|
+
if (spent + size > stageBudget.bytes && end > from)
|
|
2829
2777
|
break;
|
|
2830
2778
|
spent += size;
|
|
2831
2779
|
end += 1;
|
|
2832
2780
|
}
|
|
2833
2781
|
return end;
|
|
2834
2782
|
}
|
|
2835
|
-
async function timed(work) {
|
|
2783
|
+
async function timed(budget, work) {
|
|
2836
2784
|
const started = performance.now();
|
|
2837
2785
|
const done = await work();
|
|
2838
2786
|
const spent = performance.now() - started;
|
|
2839
|
-
if (spent <
|
|
2840
|
-
|
|
2841
|
-
else if (spent >
|
|
2842
|
-
|
|
2787
|
+
if (spent < budget.aim / 2)
|
|
2788
|
+
budget.bytes = Math.min(budget.ceiling, Math.round(budget.bytes * 1.5));
|
|
2789
|
+
else if (spent > budget.aim * 2) {
|
|
2790
|
+
budget.bytes = Math.max(budget.floor, Math.round(budget.bytes / 1.5));
|
|
2843
2791
|
}
|
|
2844
2792
|
return done;
|
|
2845
2793
|
}
|
|
2846
2794
|
function installWrite(work) {
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2795
|
+
return installTurn(installTurns.starting, work);
|
|
2796
|
+
}
|
|
2797
|
+
function finishingWrite(work) {
|
|
2798
|
+
return installTurn(installTurns.finishing, work);
|
|
2799
|
+
}
|
|
2800
|
+
function installTurn(queue, work) {
|
|
2801
|
+
return new Promise((resolve, reject) => {
|
|
2802
|
+
queue.push(() => void exclusive(work)
|
|
2803
|
+
.then(resolve, reject)
|
|
2804
|
+
.finally(() => {
|
|
2805
|
+
installTurns.busy = false;
|
|
2806
|
+
nextInstallTurn();
|
|
2807
|
+
}));
|
|
2808
|
+
nextInstallTurn();
|
|
2809
|
+
});
|
|
2850
2810
|
}
|
|
2851
|
-
function
|
|
2852
|
-
if (
|
|
2853
|
-
return
|
|
2854
|
-
|
|
2855
|
-
|
|
2811
|
+
function nextInstallTurn() {
|
|
2812
|
+
if (installTurns.busy || installTurns.ahead > 0)
|
|
2813
|
+
return;
|
|
2814
|
+
const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
|
|
2815
|
+
if (turn === undefined)
|
|
2816
|
+
return;
|
|
2817
|
+
installTurns.busy = true;
|
|
2818
|
+
turn();
|
|
2819
|
+
}
|
|
2820
|
+
async function ahead(work) {
|
|
2821
|
+
let taken = false;
|
|
2822
|
+
const hold = {
|
|
2823
|
+
take() {
|
|
2824
|
+
if (taken)
|
|
2825
|
+
return;
|
|
2826
|
+
taken = true;
|
|
2827
|
+
installTurns.ahead += 1;
|
|
2828
|
+
},
|
|
2829
|
+
release() {
|
|
2830
|
+
if (!taken)
|
|
2831
|
+
return;
|
|
2832
|
+
taken = false;
|
|
2833
|
+
installTurns.ahead -= 1;
|
|
2834
|
+
resumeInstall();
|
|
2835
|
+
},
|
|
2836
|
+
};
|
|
2837
|
+
hold.take();
|
|
2838
|
+
try {
|
|
2839
|
+
return await work(hold);
|
|
2840
|
+
}
|
|
2841
|
+
finally {
|
|
2842
|
+
hold.release();
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
function resumeInstall() {
|
|
2846
|
+
if (installTurns.ahead > 0)
|
|
2847
|
+
return;
|
|
2848
|
+
void yielded().then(nextInstallTurn);
|
|
2849
|
+
}
|
|
2850
|
+
async function heldStream() {
|
|
2851
|
+
while (streamsFree === 0)
|
|
2852
|
+
await new Promise((admit) => installWaiters.push(admit));
|
|
2853
|
+
streamsFree -= 1;
|
|
2854
|
+
return once(() => {
|
|
2855
|
+
streamsFree += 1;
|
|
2856
|
+
});
|
|
2856
2857
|
}
|
|
2857
|
-
async function
|
|
2858
|
-
while (
|
|
2859
|
-
await new Promise((admit) =>
|
|
2860
|
-
|
|
2861
|
-
return freed();
|
|
2858
|
+
async function unqueued() {
|
|
2859
|
+
while (queuedText >= QUEUED_TEXT) {
|
|
2860
|
+
await new Promise((admit) => installWaiters.push(admit));
|
|
2861
|
+
}
|
|
2862
2862
|
}
|
|
2863
|
-
function
|
|
2863
|
+
function once(release) {
|
|
2864
2864
|
let released = false;
|
|
2865
2865
|
return () => {
|
|
2866
2866
|
if (released)
|
|
2867
2867
|
return;
|
|
2868
2868
|
released = true;
|
|
2869
|
-
|
|
2870
|
-
|
|
2869
|
+
release();
|
|
2870
|
+
for (const admit of installWaiters.splice(0))
|
|
2871
|
+
admit();
|
|
2871
2872
|
};
|
|
2872
2873
|
}
|
|
2873
|
-
function installing(locator
|
|
2874
|
-
installs.
|
|
2874
|
+
function installing(locator) {
|
|
2875
|
+
installs.add(locator);
|
|
2875
2876
|
queueReadiness();
|
|
2876
2877
|
}
|
|
2877
|
-
function
|
|
2878
|
-
|
|
2879
|
-
|
|
2878
|
+
function untransferred(locator) {
|
|
2879
|
+
transfers.delete(locator);
|
|
2880
|
+
arrivals.delete(locator);
|
|
2881
|
+
streaming.delete(locator);
|
|
2882
|
+
underived.delete(locator);
|
|
2883
|
+
unindexed.delete(locator);
|
|
2884
|
+
}
|
|
2885
|
+
function setBack(locator) {
|
|
2886
|
+
const now = bytesOf(locator, true);
|
|
2887
|
+
const earlier = startedOver.get(locator);
|
|
2888
|
+
startedOver.set(locator, {
|
|
2889
|
+
done: Math.max(now.done, earlier?.done ?? 0),
|
|
2890
|
+
total: Math.max(now.total, earlier?.total ?? 0),
|
|
2891
|
+
});
|
|
2880
2892
|
}
|
|
2881
|
-
function
|
|
2882
|
-
if (
|
|
2893
|
+
function owedIndexes(locator, bytes, text) {
|
|
2894
|
+
if (bytes <= 0 || text <= 0)
|
|
2883
2895
|
return;
|
|
2884
|
-
|
|
2885
|
-
|
|
2896
|
+
unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes, text }]);
|
|
2897
|
+
}
|
|
2898
|
+
function built(locator, { done, text }) {
|
|
2899
|
+
const owed = unindexed.get(locator) ?? [];
|
|
2900
|
+
let left = text;
|
|
2901
|
+
while (!done && left > 0 && owed.length > 0) {
|
|
2902
|
+
const [first] = owed;
|
|
2903
|
+
if (left >= first.text) {
|
|
2904
|
+
left -= first.text;
|
|
2905
|
+
owed.shift();
|
|
2906
|
+
continue;
|
|
2907
|
+
}
|
|
2908
|
+
owed[0] = {
|
|
2909
|
+
bytes: (first.bytes * (first.text - left)) / first.text,
|
|
2910
|
+
text: first.text - left,
|
|
2911
|
+
};
|
|
2912
|
+
left = 0;
|
|
2913
|
+
}
|
|
2914
|
+
if (done || owed.length === 0)
|
|
2915
|
+
unindexed.delete(locator);
|
|
2886
2916
|
queueReadiness();
|
|
2887
|
-
if (!value)
|
|
2888
|
-
return;
|
|
2889
|
-
for (const resolve of shownWaiters.splice(0))
|
|
2890
|
-
resolve();
|
|
2891
2917
|
}
|
|
2892
|
-
|
|
2918
|
+
function installed(locator) {
|
|
2919
|
+
if (installs.delete(locator))
|
|
2920
|
+
queueReadiness();
|
|
2921
|
+
}
|
|
2922
|
+
async function pullRefresh(authorized, held, liveness, hold, refresh) {
|
|
2893
2923
|
if (!liveness())
|
|
2894
2924
|
return unconfirmed;
|
|
2895
|
-
const turn = generation;
|
|
2896
2925
|
const { record } = held;
|
|
2897
|
-
const
|
|
2926
|
+
const from = standingOf(record.locator);
|
|
2898
2927
|
const local = await openLocal();
|
|
2899
2928
|
try {
|
|
2900
2929
|
await exclusive(() => local.unrefresh(record.key));
|
|
2901
|
-
const result = await advancement(
|
|
2930
|
+
const result = await advancement(async (authorization, request) => {
|
|
2931
|
+
hold.release();
|
|
2932
|
+
try {
|
|
2933
|
+
return await refresh(authorization, request);
|
|
2934
|
+
}
|
|
2935
|
+
finally {
|
|
2936
|
+
hold.take();
|
|
2937
|
+
}
|
|
2938
|
+
}, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, composed(record), liveness));
|
|
2902
2939
|
if (result.kind !== 'advanced') {
|
|
2903
2940
|
journal.record(`transfer refresh ${record.locator} ${result.kind}`);
|
|
2904
2941
|
return result;
|
|
2905
2942
|
}
|
|
2906
|
-
const refused = await validatedRoom(local, result.record, 'refreshed', {
|
|
2907
|
-
record,
|
|
2908
|
-
changed: [...result.changed],
|
|
2909
|
-
});
|
|
2910
|
-
if (refused !== null) {
|
|
2911
|
-
journal.record(`transfer refresh ${record.locator} refused`);
|
|
2912
|
-
return refused;
|
|
2913
|
-
}
|
|
2914
2943
|
journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
|
|
2915
|
-
return {
|
|
2944
|
+
return {
|
|
2945
|
+
kind: 'advanced',
|
|
2946
|
+
record: result.record,
|
|
2947
|
+
from,
|
|
2948
|
+
remodelled: result.record.model !== record.model,
|
|
2949
|
+
};
|
|
2916
2950
|
}
|
|
2917
2951
|
catch (error) {
|
|
2918
2952
|
if (refusedForSpace(error))
|
|
@@ -2920,9 +2954,6 @@ export function composeRuntime(options) {
|
|
|
2920
2954
|
journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
|
|
2921
2955
|
throw error;
|
|
2922
2956
|
}
|
|
2923
|
-
finally {
|
|
2924
|
-
reads.finished(record.locator, turn);
|
|
2925
|
-
}
|
|
2926
2957
|
}
|
|
2927
2958
|
const heldContents = new Map();
|
|
2928
2959
|
function heldContentOf(local, key) {
|
|
@@ -2940,33 +2971,50 @@ export function composeRuntime(options) {
|
|
|
2940
2971
|
heldContents.set(key, made);
|
|
2941
2972
|
return made;
|
|
2942
2973
|
}
|
|
2974
|
+
async function abandoned(local, locator) {
|
|
2975
|
+
await local.abandon(locator);
|
|
2976
|
+
setBack(locator);
|
|
2977
|
+
untransferred(locator);
|
|
2978
|
+
liveQueries.recover();
|
|
2979
|
+
sweep();
|
|
2980
|
+
}
|
|
2943
2981
|
function staging(local, liveness) {
|
|
2982
|
+
const derived = async (locator, key, composition) => {
|
|
2983
|
+
underived.delete(locator);
|
|
2984
|
+
for (;;) {
|
|
2985
|
+
if (!liveness())
|
|
2986
|
+
return false;
|
|
2987
|
+
const done = await finishingWrite(async () => liveness()
|
|
2988
|
+
? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
|
|
2989
|
+
: null);
|
|
2990
|
+
if (done === null)
|
|
2991
|
+
return false;
|
|
2992
|
+
built(locator, done);
|
|
2993
|
+
if (done.done)
|
|
2994
|
+
return true;
|
|
2995
|
+
}
|
|
2996
|
+
};
|
|
2944
2997
|
return {
|
|
2945
2998
|
held: (locator) => transfers.get(locator) ?? null,
|
|
2946
2999
|
async turn(locator, step) {
|
|
2947
|
-
installing(locator
|
|
2948
|
-
|
|
2949
|
-
await screened();
|
|
2950
|
-
const release = step === 'downloading' ? await heldPart() : () => { };
|
|
2951
|
-
if (onScreen || closed)
|
|
2952
|
-
return release;
|
|
2953
|
-
release();
|
|
2954
|
-
}
|
|
3000
|
+
installing(locator);
|
|
3001
|
+
return step === 'downloading' ? heldStream() : () => { };
|
|
2955
3002
|
},
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
3003
|
+
unqueued,
|
|
3004
|
+
queued(text) {
|
|
3005
|
+
queuedText += text;
|
|
3006
|
+
return once(() => {
|
|
3007
|
+
queuedText -= text;
|
|
3008
|
+
});
|
|
2961
3009
|
},
|
|
2962
3010
|
arriving(locator) {
|
|
2963
3011
|
let ended = false;
|
|
2964
3012
|
return {
|
|
2965
|
-
at(
|
|
3013
|
+
at(bytes) {
|
|
2966
3014
|
if (ended)
|
|
2967
3015
|
return;
|
|
2968
|
-
|
|
2969
|
-
|
|
3016
|
+
arrivals.set(locator, bytes);
|
|
3017
|
+
streaming.add(locator);
|
|
2970
3018
|
const now = performance.now();
|
|
2971
3019
|
if (now - arrivalReported < ARRIVAL_REPORT_MS)
|
|
2972
3020
|
return;
|
|
@@ -2974,96 +3022,64 @@ export function composeRuntime(options) {
|
|
|
2974
3022
|
queueReadiness();
|
|
2975
3023
|
},
|
|
2976
3024
|
end() {
|
|
3025
|
+
if (ended)
|
|
3026
|
+
return;
|
|
2977
3027
|
ended = true;
|
|
2978
|
-
if (
|
|
3028
|
+
if (streaming.delete(locator))
|
|
2979
3029
|
queueReadiness();
|
|
2980
3030
|
},
|
|
2981
3031
|
};
|
|
2982
3032
|
},
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
if (liveness())
|
|
2986
|
-
await local.keepPart(locator, token, received);
|
|
2987
|
-
}),
|
|
2988
|
-
announced(locator) {
|
|
2989
|
-
const later = announced.get(locator);
|
|
2990
|
-
return later === undefined ? [] : [later];
|
|
2991
|
-
},
|
|
2992
|
-
async stage(record, page, composition, received) {
|
|
3033
|
+
streamed: options.adapter.roomStream !== undefined,
|
|
3034
|
+
async stage(record, page, composition) {
|
|
2993
3035
|
const { locator } = record;
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
const
|
|
2998
|
-
let held = record;
|
|
2999
|
-
if (token !== null)
|
|
3000
|
-
arrivals.get(locator)?.delete(token);
|
|
3001
|
-
installing(locator, 'writing', { written: from, entries, bytes: whole });
|
|
3002
|
-
for (let written = from;;) {
|
|
3003
|
-
await screened();
|
|
3036
|
+
installing(locator);
|
|
3037
|
+
if (!liveness())
|
|
3038
|
+
return null;
|
|
3039
|
+
const key = await installWrite(async () => {
|
|
3004
3040
|
if (!liveness())
|
|
3005
3041
|
return null;
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
});
|
|
3026
|
-
if (key === null)
|
|
3027
|
-
return null;
|
|
3028
|
-
const arrived = !transfers.has(locator);
|
|
3029
|
-
held = { ...next, key };
|
|
3030
|
-
transfers.set(locator, held);
|
|
3031
|
-
if (arrived)
|
|
3032
|
-
liveQueries.recover();
|
|
3033
|
-
if (last) {
|
|
3034
|
-
installing(locator, held.pageToken === null ? 'replaying' : 'downloading');
|
|
3035
|
-
liveQueries.recover();
|
|
3036
|
-
return held;
|
|
3037
|
-
}
|
|
3038
|
-
installing(locator, 'writing', { written: end, entries, bytes: whole });
|
|
3039
|
-
written = end;
|
|
3040
|
-
}
|
|
3042
|
+
return local.stage(record, composition, page);
|
|
3043
|
+
});
|
|
3044
|
+
if (key === null || !liveness())
|
|
3045
|
+
return null;
|
|
3046
|
+
const arrived = !transfers.has(locator);
|
|
3047
|
+
const prior = transfers.get(locator)?.done.bytes ?? 0;
|
|
3048
|
+
const held = { ...record, key };
|
|
3049
|
+
transfers.set(locator, held);
|
|
3050
|
+
if (arrived)
|
|
3051
|
+
liveQueries.recover();
|
|
3052
|
+
installing(locator);
|
|
3053
|
+
liveQueries.recover();
|
|
3054
|
+
const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
|
|
3055
|
+
owedIndexes(locator, record.done.bytes - prior, text);
|
|
3056
|
+
const owed = text + (underived.get(locator) ?? 0);
|
|
3057
|
+
underived.set(locator, owed);
|
|
3058
|
+
if (owed < deriveBudget.bytes)
|
|
3059
|
+
return held;
|
|
3060
|
+
return (await derived(locator, key, composition)) ? held : null;
|
|
3041
3061
|
},
|
|
3042
|
-
|
|
3062
|
+
derive: derived,
|
|
3063
|
+
async refreshing(locator, key, composition) {
|
|
3043
3064
|
await exclusive(() => local.unrefresh(key));
|
|
3044
|
-
return refreshingOf(local, key, liveness, locator);
|
|
3065
|
+
return refreshingOf(local, key, composition, liveness, locator);
|
|
3045
3066
|
},
|
|
3046
3067
|
abandon: (locator) => exclusive(async () => {
|
|
3047
|
-
if (
|
|
3048
|
-
|
|
3049
|
-
await local.abandon(locator);
|
|
3050
|
-
transfers.delete(locator);
|
|
3051
|
-
liveQueries.recover();
|
|
3052
|
-
sweep();
|
|
3068
|
+
if (liveness())
|
|
3069
|
+
await abandoned(local, locator);
|
|
3053
3070
|
}),
|
|
3054
|
-
setAside:
|
|
3071
|
+
setAside: async (locator) => {
|
|
3055
3072
|
if (!liveness())
|
|
3056
3073
|
return;
|
|
3057
|
-
|
|
3058
|
-
|
|
3074
|
+
setBack(locator);
|
|
3075
|
+
untransferred(locator);
|
|
3059
3076
|
liveQueries.recover();
|
|
3060
|
-
}
|
|
3077
|
+
},
|
|
3061
3078
|
release: (key) => exclusive(async () => {
|
|
3062
3079
|
await local.release(key);
|
|
3063
3080
|
sweep();
|
|
3064
3081
|
}),
|
|
3065
3082
|
content: (key) => heldContentOf(local, key),
|
|
3066
|
-
validate: (record, view) => validatedRoom(local, record, view, null),
|
|
3067
3083
|
};
|
|
3068
3084
|
}
|
|
3069
3085
|
function reusable(room, local) {
|
|
@@ -3075,7 +3091,6 @@ export function composeRuntime(options) {
|
|
|
3075
3091
|
async function pullBootstrap(authorized, room, local, liveness) {
|
|
3076
3092
|
if (!liveness())
|
|
3077
3093
|
return unconfirmed;
|
|
3078
|
-
const turn = generation;
|
|
3079
3094
|
if (replicas.has(room.locator)) {
|
|
3080
3095
|
catching.add(room.locator);
|
|
3081
3096
|
liveQueries.recover();
|
|
@@ -3098,9 +3113,6 @@ export function composeRuntime(options) {
|
|
|
3098
3113
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
3099
3114
|
throw error;
|
|
3100
3115
|
}
|
|
3101
|
-
finally {
|
|
3102
|
-
reads.finished(room.locator, turn);
|
|
3103
|
-
}
|
|
3104
3116
|
}
|
|
3105
3117
|
function caughtUpTo(locator) {
|
|
3106
3118
|
if (catching.delete(locator))
|
|
@@ -3108,72 +3120,82 @@ export function composeRuntime(options) {
|
|
|
3108
3120
|
}
|
|
3109
3121
|
function behind(room) {
|
|
3110
3122
|
const held = replicas.get(room.locator)?.record.roomVersion;
|
|
3111
|
-
return
|
|
3112
|
-
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
3123
|
+
return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
|
|
3113
3124
|
}
|
|
3114
|
-
|
|
3115
|
-
const
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
return
|
|
3125
|
+
function standingOf(locator) {
|
|
3126
|
+
const held = replicas.get(locator);
|
|
3127
|
+
return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
|
|
3128
|
+
}
|
|
3129
|
+
function stands(locator, from) {
|
|
3130
|
+
const now = standingOf(locator);
|
|
3131
|
+
return now.key === from.key && now.confirmed === from.confirmed;
|
|
3121
3132
|
}
|
|
3122
|
-
async function
|
|
3133
|
+
async function advance(room, local, authorized, running) {
|
|
3123
3134
|
const live = () => running() && !full;
|
|
3124
3135
|
if (!live())
|
|
3125
3136
|
return unconfirmed;
|
|
3126
3137
|
const held = replicas.get(room.locator);
|
|
3127
|
-
if (held !== undefined) {
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
emitRooms();
|
|
3139
|
-
}
|
|
3140
|
-
return null;
|
|
3141
|
-
}
|
|
3142
|
-
if (refreshed.kind === 'advanced') {
|
|
3143
|
-
const landing = { kind: 'refreshed', base: refreshed.base };
|
|
3144
|
-
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3145
|
-
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
3146
|
-
const reached = now !== undefined &&
|
|
3147
|
-
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
3148
|
-
return reached ? null : advance(room, local, authorized, live);
|
|
3149
|
-
}
|
|
3150
|
-
await retainModel(local, room, modelOf(room.locator));
|
|
3151
|
-
return null;
|
|
3152
|
-
}
|
|
3153
|
-
if (refreshed.kind !== 'beyond-horizon')
|
|
3138
|
+
if (held !== undefined && options.adapter.roomStream !== undefined) {
|
|
3139
|
+
stale.delete(room.locator);
|
|
3140
|
+
name(room.locator);
|
|
3141
|
+
return null;
|
|
3142
|
+
}
|
|
3143
|
+
const refresh = reads.refresh;
|
|
3144
|
+
if (held !== undefined && refresh !== undefined) {
|
|
3145
|
+
const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
|
|
3146
|
+
if (refreshed === 'behind')
|
|
3147
|
+
return advance(room, local, authorized, live);
|
|
3148
|
+
if (refreshed !== 'beyond-horizon')
|
|
3154
3149
|
return refreshed;
|
|
3155
|
-
if (
|
|
3156
|
-
owed.add(room.locator);
|
|
3157
|
-
liveQueries.recover();
|
|
3158
|
-
return unconfirmed;
|
|
3159
|
-
}
|
|
3160
|
-
if (reloadsDue.get(room.locator) === generation)
|
|
3150
|
+
if (reloadsDue.has(room.locator))
|
|
3161
3151
|
return unconfirmed;
|
|
3162
3152
|
}
|
|
3153
|
+
const from = standingOf(room.locator);
|
|
3163
3154
|
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
3164
|
-
if (bootstrapped.kind
|
|
3165
|
-
|
|
3155
|
+
if (bootstrapped.kind !== 'staged')
|
|
3156
|
+
return live() ? bootstrapped : unconfirmed;
|
|
3157
|
+
const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
|
|
3158
|
+
if (await publish(local, room, bootstrapped.record, landing, live)) {
|
|
3166
3159
|
return live() ? null : unconfirmed;
|
|
3167
3160
|
}
|
|
3168
|
-
return
|
|
3161
|
+
return reachedBy(room.locator, bootstrapped.record.roomVersion)
|
|
3162
|
+
? null
|
|
3163
|
+
: advance(room, local, authorized, live);
|
|
3164
|
+
}
|
|
3165
|
+
async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
|
|
3166
|
+
const refreshed = await pullRefresh(authorized, held, live, hold, refresh);
|
|
3167
|
+
if (!live())
|
|
3168
|
+
return unconfirmed;
|
|
3169
|
+
if (refreshed.kind === 'still-current') {
|
|
3170
|
+
stale.delete(room.locator);
|
|
3171
|
+
if (unreadable.delete(room.locator)) {
|
|
3172
|
+
scopedRefusals.delete(room.locator);
|
|
3173
|
+
list(room, true);
|
|
3174
|
+
emitRooms();
|
|
3175
|
+
}
|
|
3176
|
+
return null;
|
|
3177
|
+
}
|
|
3178
|
+
if (refreshed.kind === 'advanced') {
|
|
3179
|
+
const landing = {
|
|
3180
|
+
kind: 'refreshed',
|
|
3181
|
+
from: refreshed.from,
|
|
3182
|
+
remodelled: refreshed.remodelled,
|
|
3183
|
+
};
|
|
3184
|
+
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3185
|
+
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
3186
|
+
const reached = now !== undefined &&
|
|
3187
|
+
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
3188
|
+
return reached ? null : 'behind';
|
|
3189
|
+
}
|
|
3190
|
+
await retainModel(local, room, modelOf(room.locator));
|
|
3191
|
+
return null;
|
|
3192
|
+
}
|
|
3193
|
+
return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
|
|
3169
3194
|
}
|
|
3170
3195
|
async function withhold(withheld, room, local, progress) {
|
|
3171
|
-
if (withheld.kind === 'unconfirmed')
|
|
3196
|
+
if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
|
|
3172
3197
|
return;
|
|
3173
|
-
|
|
3174
|
-
stale.delete(room.locator);
|
|
3175
|
-
else
|
|
3176
|
-
stale.add(room.locator);
|
|
3198
|
+
stale.add(room.locator);
|
|
3177
3199
|
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
3178
3200
|
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
3179
3201
|
repairable.set(room.locator, reset);
|
|
@@ -3195,30 +3217,10 @@ export function composeRuntime(options) {
|
|
|
3195
3217
|
list(room, false);
|
|
3196
3218
|
emitRooms();
|
|
3197
3219
|
}
|
|
3198
|
-
else if (withheld.kind === 'refused') {
|
|
3199
|
-
refusedSource(room, withheld);
|
|
3200
|
-
}
|
|
3201
3220
|
else if (withheld.kind === 'revoked-authorization') {
|
|
3202
3221
|
await refuse('revoked-authorization');
|
|
3203
3222
|
}
|
|
3204
3223
|
}
|
|
3205
|
-
function refusedSource(room, { refusal }) {
|
|
3206
|
-
sourceRefusals.set(room.locator, {
|
|
3207
|
-
code: refusal.rule,
|
|
3208
|
-
retryable: false,
|
|
3209
|
-
fields: {
|
|
3210
|
-
file: refusal.file,
|
|
3211
|
-
...(refusal.location === undefined ? {} : { location: refusal.location }),
|
|
3212
|
-
...(refusal.referrers === undefined ? {} : { referrers: refusal.referrers }),
|
|
3213
|
-
},
|
|
3214
|
-
});
|
|
3215
|
-
declarationRefusals.delete(room.locator);
|
|
3216
|
-
unreadable.add(room.locator);
|
|
3217
|
-
list(room, false);
|
|
3218
|
-
emitRooms();
|
|
3219
|
-
moveTo({ ...status, ...attention() });
|
|
3220
|
-
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
3221
|
-
}
|
|
3222
3224
|
function leaving(batch, result) {
|
|
3223
3225
|
if (result.kind === 'expired')
|
|
3224
3226
|
return { batch, returned: returnedBy('expired') };
|
|
@@ -3240,8 +3242,8 @@ export function composeRuntime(options) {
|
|
|
3240
3242
|
function settling(acceptance) {
|
|
3241
3243
|
return acceptance !== null && 'apply' in acceptance;
|
|
3242
3244
|
}
|
|
3243
|
-
function
|
|
3244
|
-
return settling(acceptance) ?
|
|
3245
|
+
function failureOf(acceptance) {
|
|
3246
|
+
return settling(acceptance) ? null : acceptance;
|
|
3245
3247
|
}
|
|
3246
3248
|
async function acceptance(room, local, submitted, accepted) {
|
|
3247
3249
|
if (!continuing())
|
|
@@ -3264,34 +3266,6 @@ export function composeRuntime(options) {
|
|
|
3264
3266
|
? accepted.roomVersion
|
|
3265
3267
|
: held,
|
|
3266
3268
|
};
|
|
3267
|
-
const overrides = new Map([
|
|
3268
|
-
...back.set.map((entry) => [entry.path, entry]),
|
|
3269
|
-
...back.removed.map((path) => [path, null]),
|
|
3270
|
-
]);
|
|
3271
|
-
const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
|
|
3272
|
-
if (refused !== null) {
|
|
3273
|
-
const key = pendingKey(room.locator, submitted.id);
|
|
3274
|
-
return {
|
|
3275
|
-
locator: room.locator,
|
|
3276
|
-
left: [],
|
|
3277
|
-
commit: {
|
|
3278
|
-
rooms: [],
|
|
3279
|
-
writes: [
|
|
3280
|
-
{
|
|
3281
|
-
locator: room.locator,
|
|
3282
|
-
accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
|
|
3283
|
-
},
|
|
3284
|
-
],
|
|
3285
|
-
},
|
|
3286
|
-
refused,
|
|
3287
|
-
apply() {
|
|
3288
|
-
const state = pendings.get(key);
|
|
3289
|
-
if (state !== undefined && !fences.has(room.locator))
|
|
3290
|
-
pendings.set(key, { ...state, accepted: accepted.roomVersion });
|
|
3291
|
-
return changeSet(room.locator, { queue: true });
|
|
3292
|
-
},
|
|
3293
|
-
};
|
|
3294
|
-
}
|
|
3295
3269
|
changes = back;
|
|
3296
3270
|
}
|
|
3297
3271
|
const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
|
|
@@ -3301,8 +3275,6 @@ export function composeRuntime(options) {
|
|
|
3301
3275
|
const set = settled.apply(answer);
|
|
3302
3276
|
pendings.delete(pendingKey(room.locator, submitted.id));
|
|
3303
3277
|
unreadable.delete(room.locator);
|
|
3304
|
-
sourceRefusals.delete(room.locator);
|
|
3305
|
-
declarationRefusals.delete(room.locator);
|
|
3306
3278
|
list(room, true);
|
|
3307
3279
|
return set;
|
|
3308
3280
|
},
|
|
@@ -3320,13 +3292,9 @@ export function composeRuntime(options) {
|
|
|
3320
3292
|
const failed = await exclusive(async () => {
|
|
3321
3293
|
const adopting = await acceptance(room, local, submitted, accepted);
|
|
3322
3294
|
await adoptedTogether(local, [adopting]);
|
|
3323
|
-
return
|
|
3295
|
+
return failureOf(adopting);
|
|
3324
3296
|
});
|
|
3325
|
-
|
|
3326
|
-
return true;
|
|
3327
|
-
if (failed.kind === 'refused' && continuing())
|
|
3328
|
-
refusedSource(room, failed);
|
|
3329
|
-
return false;
|
|
3297
|
+
return failed === null;
|
|
3330
3298
|
}
|
|
3331
3299
|
async function adoptedMove(room, partner, local, submitted, accepted) {
|
|
3332
3300
|
const other = discovered.get(partner);
|
|
@@ -3340,18 +3308,10 @@ export function composeRuntime(options) {
|
|
|
3340
3308
|
...accepted.destination,
|
|
3341
3309
|
});
|
|
3342
3310
|
await adoptedTogether(local, [left, arrived]);
|
|
3343
|
-
|
|
3344
|
-
if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
|
|
3345
|
-
refusedSource(other, refusedArrival);
|
|
3346
|
-
}
|
|
3347
|
-
return refusalOf(left);
|
|
3311
|
+
return failureOf(left);
|
|
3348
3312
|
});
|
|
3349
3313
|
submitLater(partner);
|
|
3350
|
-
|
|
3351
|
-
return true;
|
|
3352
|
-
if (failed.kind === 'refused' && continuing())
|
|
3353
|
-
refusedSource(room, failed);
|
|
3354
|
-
return false;
|
|
3314
|
+
return failed === null;
|
|
3355
3315
|
}
|
|
3356
3316
|
function queueIn(locator) {
|
|
3357
3317
|
return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
@@ -3474,8 +3434,6 @@ export function composeRuntime(options) {
|
|
|
3474
3434
|
if (replica === undefined || head === undefined)
|
|
3475
3435
|
return 'done';
|
|
3476
3436
|
const state = pendings.get(pendingKey(room.locator, head.id));
|
|
3477
|
-
if (state?.accepted !== undefined)
|
|
3478
|
-
return 'done';
|
|
3479
3437
|
const partner = partnerOf(head, room.locator);
|
|
3480
3438
|
if (partner !== null && !leavingIn(head, room.locator)) {
|
|
3481
3439
|
submitLater(partner);
|
|
@@ -3589,12 +3547,12 @@ export function composeRuntime(options) {
|
|
|
3589
3547
|
}
|
|
3590
3548
|
if (result.kind === 'expired') {
|
|
3591
3549
|
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
|
|
3592
|
-
const there = other === undefined ? null : await
|
|
3550
|
+
const there = other === undefined ? null : await advanced(other, local, authorized);
|
|
3593
3551
|
if (other !== undefined && there !== null) {
|
|
3594
3552
|
await withhold(there, other, local, progress);
|
|
3595
3553
|
return 'done';
|
|
3596
3554
|
}
|
|
3597
|
-
const withheld = await
|
|
3555
|
+
const withheld = await advanced(room, local, authorized);
|
|
3598
3556
|
if (withheld !== null) {
|
|
3599
3557
|
await withhold(withheld, room, local, progress);
|
|
3600
3558
|
return 'done';
|
|
@@ -3616,8 +3574,8 @@ export function composeRuntime(options) {
|
|
|
3616
3574
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
3617
3575
|
const left = leaving(head, result);
|
|
3618
3576
|
const refreshed = async () => ({
|
|
3619
|
-
there: other === undefined ? null : await
|
|
3620
|
-
here: await
|
|
3577
|
+
there: other === undefined ? null : await advanced(other, local, authorized),
|
|
3578
|
+
here: await advanced(room, local, authorized),
|
|
3621
3579
|
});
|
|
3622
3580
|
const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
|
|
3623
3581
|
if (judged !== null && (judged.there !== null || judged.here !== null)) {
|
|
@@ -3688,6 +3646,7 @@ export function composeRuntime(options) {
|
|
|
3688
3646
|
locator: room.locator,
|
|
3689
3647
|
key: before.record.key,
|
|
3690
3648
|
roots: before.record.typedSelection?.roots ?? [],
|
|
3649
|
+
composed: composedOf(composed(before.record)),
|
|
3691
3650
|
overlay: { set: [], cleared: [...before.overlaid] },
|
|
3692
3651
|
},
|
|
3693
3652
|
]),
|
|
@@ -3720,39 +3679,37 @@ export function composeRuntime(options) {
|
|
|
3720
3679
|
if (signal.aborted)
|
|
3721
3680
|
return;
|
|
3722
3681
|
const progress = { complete: true, reachable: true };
|
|
3723
|
-
await roomTask(room.locator,
|
|
3724
|
-
await Promise.race([
|
|
3725
|
-
reload(room, local, progress, () => live() && !signal.aborted),
|
|
3726
|
-
abandonedOn(signal),
|
|
3727
|
-
]);
|
|
3728
|
-
});
|
|
3682
|
+
await roomTask(room.locator, (live) => reload(room, local, progress, live));
|
|
3729
3683
|
if (signal.aborted)
|
|
3730
3684
|
return;
|
|
3731
3685
|
await rest(progress);
|
|
3732
3686
|
}
|
|
3733
3687
|
}
|
|
3734
3688
|
function awaitingAnswer(locator) {
|
|
3689
|
+
answering.get(locator)?.release();
|
|
3735
3690
|
let release = () => { };
|
|
3736
|
-
const
|
|
3691
|
+
const answered = new Promise((resolve) => {
|
|
3737
3692
|
release = resolve;
|
|
3738
3693
|
});
|
|
3739
|
-
|
|
3694
|
+
const awaited = { answered, release };
|
|
3695
|
+
answering.set(locator, awaited);
|
|
3740
3696
|
return () => {
|
|
3741
|
-
if (answering.get(locator)
|
|
3697
|
+
if (answering.get(locator) === awaited)
|
|
3742
3698
|
answering.delete(locator);
|
|
3743
3699
|
release();
|
|
3744
3700
|
};
|
|
3745
3701
|
}
|
|
3702
|
+
function declaredAsHeld(room) {
|
|
3703
|
+
const held = replicas.get(room.locator)?.record;
|
|
3704
|
+
return (held === undefined ||
|
|
3705
|
+
(JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
|
|
3706
|
+
JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
|
|
3707
|
+
JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
|
|
3708
|
+
}
|
|
3746
3709
|
async function retainModel(local, room, next) {
|
|
3747
3710
|
const key = room.locator;
|
|
3748
3711
|
const previous = modelOf(room.locator);
|
|
3749
|
-
|
|
3750
|
-
if (JSON.stringify(previous) === JSON.stringify(next) &&
|
|
3751
|
-
(heldBefore === undefined ||
|
|
3752
|
-
(JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
|
|
3753
|
-
JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
|
|
3754
|
-
JSON.stringify(heldBefore.typedSelection) ===
|
|
3755
|
-
JSON.stringify(room.typedSelection ?? null))))
|
|
3712
|
+
if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(room))
|
|
3756
3713
|
return;
|
|
3757
3714
|
if (next.status === 'unavailable') {
|
|
3758
3715
|
invalidModels.add(key);
|
|
@@ -3764,26 +3721,13 @@ export function composeRuntime(options) {
|
|
|
3764
3721
|
const held = replicas.get(room.locator);
|
|
3765
3722
|
let changed = null;
|
|
3766
3723
|
if (held !== undefined) {
|
|
3767
|
-
const
|
|
3724
|
+
const record = {
|
|
3768
3725
|
...held.record,
|
|
3769
3726
|
operations: room.operations,
|
|
3770
3727
|
replica: room.replica,
|
|
3771
3728
|
typedSelection: room.typedSelection ?? null,
|
|
3772
3729
|
model: next,
|
|
3773
3730
|
};
|
|
3774
|
-
let read = 'staged';
|
|
3775
|
-
if (!sameMembership(declared, held.record)) {
|
|
3776
|
-
const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), composed(declared));
|
|
3777
|
-
read =
|
|
3778
|
-
verdict.verdict !== 'refuse'
|
|
3779
|
-
? 'staged'
|
|
3780
|
-
: verdict.rule === 'model-unavailable'
|
|
3781
|
-
? 'deferred'
|
|
3782
|
-
: { kind: 'refused', refusal: verdict };
|
|
3783
|
-
}
|
|
3784
|
-
const record = read === 'staged'
|
|
3785
|
-
? declared
|
|
3786
|
-
: { ...declared, typedSelection: held.record.typedSelection };
|
|
3787
3731
|
const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
|
|
3788
3732
|
const replayedLayer = await replayed(record, confirmedLayer, held.queue);
|
|
3789
3733
|
const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
|
|
@@ -3794,7 +3738,7 @@ export function composeRuntime(options) {
|
|
|
3794
3738
|
key: record.key,
|
|
3795
3739
|
record: storedRecord(record),
|
|
3796
3740
|
roots: record.typedSelection?.roots ?? [],
|
|
3797
|
-
|
|
3741
|
+
composed: composedOf(composed(record)),
|
|
3798
3742
|
overlay: diff.overlay,
|
|
3799
3743
|
},
|
|
3800
3744
|
],
|
|
@@ -3805,29 +3749,22 @@ export function composeRuntime(options) {
|
|
|
3805
3749
|
documents: answer.changed[room.locator] ?? [],
|
|
3806
3750
|
room: true,
|
|
3807
3751
|
});
|
|
3808
|
-
if (read === 'staged' || read === 'deferred') {
|
|
3809
|
-
if (declarationRefusals.delete(room.locator)) {
|
|
3810
|
-
sourceRefusals.delete(room.locator);
|
|
3811
|
-
unreadable.delete(room.locator);
|
|
3812
|
-
}
|
|
3813
|
-
}
|
|
3814
|
-
else {
|
|
3815
|
-
refusedSource(room, read);
|
|
3816
|
-
declarationRefusals.add(room.locator);
|
|
3817
|
-
}
|
|
3818
3752
|
}
|
|
3819
|
-
|
|
3820
|
-
if (next.status === 'current') {
|
|
3821
|
-
liveModels.set(key, JSON.stringify(next.snapshot));
|
|
3822
|
-
invalidModels.delete(key);
|
|
3823
|
-
}
|
|
3824
|
-
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
3753
|
+
heldModel(room, next);
|
|
3825
3754
|
if (changed === null)
|
|
3826
3755
|
emitRooms();
|
|
3827
3756
|
else
|
|
3828
3757
|
publishChangeSet(changed);
|
|
3829
3758
|
});
|
|
3830
3759
|
}
|
|
3760
|
+
function heldModel(room, next) {
|
|
3761
|
+
models.set(room.locator, next);
|
|
3762
|
+
if (next.status === 'current') {
|
|
3763
|
+
liveModels.set(room.locator, JSON.stringify(next.snapshot));
|
|
3764
|
+
invalidModels.delete(room.locator);
|
|
3765
|
+
}
|
|
3766
|
+
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
3767
|
+
}
|
|
3831
3768
|
async function scoped(result, room, local) {
|
|
3832
3769
|
if (result.scope === 'content' && !result.retryable) {
|
|
3833
3770
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
@@ -3856,6 +3793,17 @@ export function composeRuntime(options) {
|
|
|
3856
3793
|
emitRooms();
|
|
3857
3794
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
3858
3795
|
}
|
|
3796
|
+
function name(locator) {
|
|
3797
|
+
const stream = options.adapter.roomStream;
|
|
3798
|
+
const record = replicas.get(locator)?.record;
|
|
3799
|
+
const room = discovered.get(locator);
|
|
3800
|
+
if (stream === undefined || record === undefined || room === undefined)
|
|
3801
|
+
return;
|
|
3802
|
+
if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
|
|
3803
|
+
return;
|
|
3804
|
+
naming += 1;
|
|
3805
|
+
stream.hold([{ locator, roomVersion: record.roomVersion }]);
|
|
3806
|
+
}
|
|
3859
3807
|
function heldRooms(listed) {
|
|
3860
3808
|
return listed.flatMap((room) => {
|
|
3861
3809
|
const record = replicas.get(room.locator)?.record;
|
|
@@ -3873,14 +3821,19 @@ export function composeRuntime(options) {
|
|
|
3873
3821
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
3874
3822
|
});
|
|
3875
3823
|
if (attempt === 'opened') {
|
|
3824
|
+
naming += 1;
|
|
3876
3825
|
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
3877
3826
|
handed = true;
|
|
3878
3827
|
}
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3828
|
+
for (const room of ready) {
|
|
3829
|
+
if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
|
|
3830
|
+
if (attempt === 'authorized')
|
|
3831
|
+
name(room.locator);
|
|
3832
|
+
continue;
|
|
3833
|
+
}
|
|
3834
|
+
void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
|
|
3835
|
+
}
|
|
3836
|
+
const failed = prepared.find((room) => room.status === 'rejected');
|
|
3884
3837
|
if (failed !== undefined)
|
|
3885
3838
|
throw failed.reason;
|
|
3886
3839
|
}
|
|
@@ -3904,7 +3857,9 @@ export function composeRuntime(options) {
|
|
|
3904
3857
|
return room.replica.admission === 'admitted';
|
|
3905
3858
|
}
|
|
3906
3859
|
async function caughtUp(room, local, progress, live) {
|
|
3907
|
-
if (
|
|
3860
|
+
if (options.adapter.roomStream !== undefined || authorization === undefined)
|
|
3861
|
+
return;
|
|
3862
|
+
if (!live() || !behind(room))
|
|
3908
3863
|
return;
|
|
3909
3864
|
const withheld = await advance(room, local, authorization, live);
|
|
3910
3865
|
if (withheld !== null)
|
|
@@ -3917,6 +3872,8 @@ export function composeRuntime(options) {
|
|
|
3917
3872
|
async function loadRoom(room, local, progress, live) {
|
|
3918
3873
|
if (!live() || authorization === undefined)
|
|
3919
3874
|
return;
|
|
3875
|
+
if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
|
|
3876
|
+
return;
|
|
3920
3877
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
3921
3878
|
await reload(room, local, progress, live);
|
|
3922
3879
|
return;
|
|
@@ -3934,20 +3891,53 @@ export function composeRuntime(options) {
|
|
|
3934
3891
|
reloadsDue.delete(room.locator);
|
|
3935
3892
|
if (!live() || authorization === undefined)
|
|
3936
3893
|
return;
|
|
3894
|
+
const from = standingOf(room.locator);
|
|
3937
3895
|
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
3938
3896
|
if (bootstrapped.kind !== 'staged') {
|
|
3939
3897
|
if (live())
|
|
3940
3898
|
await withhold(bootstrapped, room, local, progress);
|
|
3941
3899
|
return;
|
|
3942
3900
|
}
|
|
3943
|
-
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
3901
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
|
|
3944
3902
|
await caughtUp(room, local, progress, live);
|
|
3945
3903
|
}
|
|
3946
3904
|
finally {
|
|
3947
3905
|
caughtUpTo(room.locator);
|
|
3948
3906
|
}
|
|
3949
3907
|
}
|
|
3950
|
-
async function
|
|
3908
|
+
async function asked(authorized) {
|
|
3909
|
+
try {
|
|
3910
|
+
return (await options.adapter.discover?.(authorized)) ?? { kind: 'transport-uncertainty' };
|
|
3911
|
+
}
|
|
3912
|
+
catch {
|
|
3913
|
+
return { kind: 'transport-uncertainty' };
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3916
|
+
function malformed(listed) {
|
|
3917
|
+
for (const room of listed) {
|
|
3918
|
+
if (room.operations === undefined ||
|
|
3919
|
+
!['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
3920
|
+
room.operations[operation].every((scope) => typeof scope === 'string')) ||
|
|
3921
|
+
room.replica === undefined ||
|
|
3922
|
+
typeof room.replica.selection !== 'string' ||
|
|
3923
|
+
room.replica.selection.length === 0 ||
|
|
3924
|
+
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
3925
|
+
!['locked', 'open'].includes(room.replica.offline)) {
|
|
3926
|
+
return `invalid model or replica declaration ${room.locator}`;
|
|
3927
|
+
}
|
|
3928
|
+
if (!declaredSelection(room))
|
|
3929
|
+
return `invalid typed selection declaration ${room.locator}`;
|
|
3930
|
+
}
|
|
3931
|
+
return null;
|
|
3932
|
+
}
|
|
3933
|
+
async function signIn(local, signedIn) {
|
|
3934
|
+
if (JSON.stringify(signedIn) === JSON.stringify(member))
|
|
3935
|
+
return;
|
|
3936
|
+
await local.fact('member', signedIn);
|
|
3937
|
+
member = signedIn;
|
|
3938
|
+
emitMember();
|
|
3939
|
+
}
|
|
3940
|
+
async function discoverRooms(local, progress, attempt, live, found) {
|
|
3951
3941
|
if (closed || authorization === undefined) {
|
|
3952
3942
|
notifyReady();
|
|
3953
3943
|
return;
|
|
@@ -3955,13 +3945,9 @@ export function composeRuntime(options) {
|
|
|
3955
3945
|
moveTo(transferring(status));
|
|
3956
3946
|
discoveries += 1;
|
|
3957
3947
|
const started = discoveries;
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
}
|
|
3962
|
-
catch {
|
|
3963
|
-
result = { kind: 'transport-uncertainty' };
|
|
3964
|
-
}
|
|
3948
|
+
const result = found ?? (await asked(authorization));
|
|
3949
|
+
if (result.kind === 'discovery')
|
|
3950
|
+
roomsNamed = true;
|
|
3965
3951
|
if (!live()) {
|
|
3966
3952
|
progress.complete = false;
|
|
3967
3953
|
notifyReady();
|
|
@@ -3969,9 +3955,7 @@ export function composeRuntime(options) {
|
|
|
3969
3955
|
}
|
|
3970
3956
|
journal.record(`connection discover ${result.kind}`);
|
|
3971
3957
|
if (result.kind === 'transport-uncertainty') {
|
|
3972
|
-
|
|
3973
|
-
await retainModel(local, room, cached(modelOf(room.locator)));
|
|
3974
|
-
}
|
|
3958
|
+
await cachedModels(local);
|
|
3975
3959
|
progress.reachable = false;
|
|
3976
3960
|
progress.complete = false;
|
|
3977
3961
|
notifyReady();
|
|
@@ -3992,26 +3976,12 @@ export function composeRuntime(options) {
|
|
|
3992
3976
|
notifyReady();
|
|
3993
3977
|
return;
|
|
3994
3978
|
}
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
room.replica.selection.length === 0 ||
|
|
4002
|
-
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
4003
|
-
!['locked', 'open'].includes(room.replica.offline)) {
|
|
4004
|
-
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
4005
|
-
await refuse('missing-required-guarantee');
|
|
4006
|
-
notifyReady();
|
|
4007
|
-
return;
|
|
4008
|
-
}
|
|
4009
|
-
if (!declaredSelection(room)) {
|
|
4010
|
-
journal.record(`invalid typed selection declaration ${room.locator}`);
|
|
4011
|
-
await refuse('missing-required-guarantee');
|
|
4012
|
-
notifyReady();
|
|
4013
|
-
return;
|
|
4014
|
-
}
|
|
3979
|
+
const invalid = malformed(result.rooms);
|
|
3980
|
+
if (invalid !== null) {
|
|
3981
|
+
journal.record(invalid);
|
|
3982
|
+
await refuse('missing-required-guarantee');
|
|
3983
|
+
notifyReady();
|
|
3984
|
+
return;
|
|
4015
3985
|
}
|
|
4016
3986
|
if (refusal !== null) {
|
|
4017
3987
|
refusal = null;
|
|
@@ -4027,13 +3997,8 @@ export function composeRuntime(options) {
|
|
|
4027
3997
|
await local.fact('providerLimits', limits);
|
|
4028
3998
|
await local.fact('capabilities', capabilities);
|
|
4029
3999
|
}
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
await local.fact('member', signedIn);
|
|
4033
|
-
member = signedIn;
|
|
4034
|
-
emitMember();
|
|
4035
|
-
}
|
|
4036
|
-
await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
|
|
4000
|
+
await signIn(local, result.member ?? null);
|
|
4001
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
4037
4002
|
}
|
|
4038
4003
|
async function ensureAuthorization() {
|
|
4039
4004
|
if (authorization !== undefined)
|
|
@@ -4048,23 +4013,45 @@ export function composeRuntime(options) {
|
|
|
4048
4013
|
}
|
|
4049
4014
|
}
|
|
4050
4015
|
async function admittedRead(work) {
|
|
4051
|
-
|
|
4052
|
-
|
|
4016
|
+
while (roomReads.free === 0) {
|
|
4017
|
+
await new Promise((admit) => roomReads.waiting.push(admit));
|
|
4053
4018
|
}
|
|
4054
|
-
|
|
4055
|
-
while (reads.free === 0)
|
|
4056
|
-
await new Promise((admit) => reads.waiting.push(admit));
|
|
4057
|
-
reads.free -= 1;
|
|
4019
|
+
roomReads.free -= 1;
|
|
4058
4020
|
try {
|
|
4059
4021
|
await work();
|
|
4060
4022
|
}
|
|
4061
4023
|
finally {
|
|
4062
|
-
|
|
4063
|
-
|
|
4024
|
+
roomReads.free += 1;
|
|
4025
|
+
roomReads.waiting.shift()?.();
|
|
4064
4026
|
}
|
|
4065
4027
|
}
|
|
4028
|
+
function inRoom(locator, work) {
|
|
4029
|
+
roomWork += 1;
|
|
4030
|
+
const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
|
|
4031
|
+
try {
|
|
4032
|
+
return await work(continuing);
|
|
4033
|
+
}
|
|
4034
|
+
finally {
|
|
4035
|
+
roomWork -= 1;
|
|
4036
|
+
}
|
|
4037
|
+
});
|
|
4038
|
+
const chain = ran.then(() => undefined, () => undefined);
|
|
4039
|
+
roomLanes.set(locator, chain);
|
|
4040
|
+
working.add(chain);
|
|
4041
|
+
void chain.finally(() => {
|
|
4042
|
+
working.delete(chain);
|
|
4043
|
+
if (roomLanes.get(locator) === chain)
|
|
4044
|
+
roomLanes.delete(locator);
|
|
4045
|
+
});
|
|
4046
|
+
return ran;
|
|
4047
|
+
}
|
|
4066
4048
|
function roomTask(locator, work) {
|
|
4067
|
-
|
|
4049
|
+
if (stopping)
|
|
4050
|
+
return Promise.resolve();
|
|
4051
|
+
return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
|
|
4052
|
+
}
|
|
4053
|
+
function advanced(room, local, authorized) {
|
|
4054
|
+
return inRoom(room.locator, (live) => advance(room, local, authorized, live));
|
|
4068
4055
|
}
|
|
4069
4056
|
function task(lane, work) {
|
|
4070
4057
|
if (stopping)
|
|
@@ -4080,23 +4067,19 @@ export function composeRuntime(options) {
|
|
|
4080
4067
|
void run.finally(() => working.delete(run));
|
|
4081
4068
|
return run;
|
|
4082
4069
|
}
|
|
4083
|
-
function sync(attempt) {
|
|
4070
|
+
function sync(attempt, found) {
|
|
4084
4071
|
return task(OPENING_LANE, async (live) => {
|
|
4085
4072
|
if (!live()) {
|
|
4086
|
-
|
|
4073
|
+
readyFromStore();
|
|
4087
4074
|
return;
|
|
4088
4075
|
}
|
|
4089
4076
|
const progress = { complete: true, reachable: true };
|
|
4090
|
-
|
|
4091
|
-
if (attempt === 'opened')
|
|
4092
|
-
opened = generation;
|
|
4093
|
-
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
4077
|
+
underway = attempt === 'opened' ? progress : null;
|
|
4094
4078
|
if (attempt === 'opened')
|
|
4095
4079
|
streamed = progress;
|
|
4096
4080
|
complete = false;
|
|
4097
4081
|
recorded = false;
|
|
4098
4082
|
moveTo(transferring(status));
|
|
4099
|
-
handed = false;
|
|
4100
4083
|
try {
|
|
4101
4084
|
const local = await openLocal();
|
|
4102
4085
|
if (!live())
|
|
@@ -4104,7 +4087,7 @@ export function composeRuntime(options) {
|
|
|
4104
4087
|
await ensureAuthorization();
|
|
4105
4088
|
if (!live())
|
|
4106
4089
|
return;
|
|
4107
|
-
await discoverRooms(local, progress, attempt, live);
|
|
4090
|
+
await discoverRooms(local, progress, attempt, live, found);
|
|
4108
4091
|
}
|
|
4109
4092
|
catch {
|
|
4110
4093
|
progress.complete = false;
|
|
@@ -4112,7 +4095,6 @@ export function composeRuntime(options) {
|
|
|
4112
4095
|
progress.reachable = false;
|
|
4113
4096
|
}
|
|
4114
4097
|
finally {
|
|
4115
|
-
gate.release();
|
|
4116
4098
|
notifyReady();
|
|
4117
4099
|
}
|
|
4118
4100
|
if (!live())
|
|
@@ -4124,35 +4106,53 @@ export function composeRuntime(options) {
|
|
|
4124
4106
|
if (handed || closed || progress.reachable)
|
|
4125
4107
|
return;
|
|
4126
4108
|
underway = null;
|
|
4127
|
-
opened = null;
|
|
4128
|
-
journal.record('connection reopen');
|
|
4129
4109
|
await rest(progress, live);
|
|
4130
|
-
options.adapter.roomStream?.reopen();
|
|
4131
4110
|
});
|
|
4132
4111
|
}
|
|
4133
|
-
function
|
|
4112
|
+
function resynchronize() {
|
|
4134
4113
|
return task(OPENING_LANE, async (live) => {
|
|
4135
|
-
if (!live() || !
|
|
4114
|
+
if (!live() || !announced)
|
|
4136
4115
|
return;
|
|
4137
4116
|
const progress = { complete: true, reachable: true };
|
|
4138
|
-
const gate = discover();
|
|
4139
4117
|
moveTo(transferring(status));
|
|
4140
4118
|
try {
|
|
4141
4119
|
const local = await openLocal();
|
|
4142
4120
|
if (!live())
|
|
4143
4121
|
return;
|
|
4144
|
-
await
|
|
4122
|
+
await synchronize([...discovered.values()], local, progress, 'unstreamed', live);
|
|
4123
|
+
}
|
|
4124
|
+
catch {
|
|
4125
|
+
progress.complete = false;
|
|
4126
|
+
}
|
|
4127
|
+
if (live())
|
|
4128
|
+
await finishOpening(progress, live);
|
|
4129
|
+
});
|
|
4130
|
+
}
|
|
4131
|
+
function changeAuthority(change) {
|
|
4132
|
+
return task(OPENING_LANE, async (live) => {
|
|
4133
|
+
if (!live() || !announced)
|
|
4134
|
+
return;
|
|
4135
|
+
const progress = { complete: true, reachable: true };
|
|
4136
|
+
moveTo(transferring(status));
|
|
4137
|
+
try {
|
|
4138
|
+
const local = await openLocal();
|
|
4145
4139
|
if (!live())
|
|
4146
4140
|
return;
|
|
4147
|
-
|
|
4141
|
+
const invalid = malformed(change.rooms);
|
|
4142
|
+
if (invalid !== null) {
|
|
4143
|
+
journal.record(invalid);
|
|
4144
|
+
await refuse('missing-required-guarantee');
|
|
4145
|
+
return;
|
|
4146
|
+
}
|
|
4147
|
+
if (change.member !== undefined)
|
|
4148
|
+
await signIn(local, change.member);
|
|
4149
|
+
discoveries += 1;
|
|
4150
|
+
const listed = await applyDiscovery(change.rooms, discoveries, change.left);
|
|
4151
|
+
await synchronize(listed, local, progress, 'authorized', live);
|
|
4148
4152
|
}
|
|
4149
4153
|
catch {
|
|
4150
4154
|
progress.complete = false;
|
|
4151
4155
|
}
|
|
4152
|
-
finally {
|
|
4153
|
-
gate.release();
|
|
4154
|
-
notifyReady();
|
|
4155
|
-
}
|
|
4156
4156
|
if (!live())
|
|
4157
4157
|
return;
|
|
4158
4158
|
if (!progress.complete)
|
|
@@ -4161,13 +4161,15 @@ export function composeRuntime(options) {
|
|
|
4161
4161
|
});
|
|
4162
4162
|
}
|
|
4163
4163
|
function closedSocket(revoked, lost) {
|
|
4164
|
-
|
|
4164
|
+
notifyReady();
|
|
4165
|
+
const opened = announced;
|
|
4165
4166
|
socketOpen = false;
|
|
4167
|
+
announced = false;
|
|
4168
|
+
handed = false;
|
|
4169
|
+
naming = 0;
|
|
4166
4170
|
connectionLost();
|
|
4167
|
-
if (lost)
|
|
4168
|
-
reads.lost();
|
|
4171
|
+
if (lost)
|
|
4169
4172
|
conversations.lost();
|
|
4170
|
-
}
|
|
4171
4173
|
if (revoked) {
|
|
4172
4174
|
void refuse('revoked-authorization');
|
|
4173
4175
|
return;
|
|
@@ -4180,16 +4182,29 @@ export function composeRuntime(options) {
|
|
|
4180
4182
|
state: status.state === 'synced' ? 'syncing' : status.state,
|
|
4181
4183
|
providerUnreachable: status.providerUnreachable || !opened,
|
|
4182
4184
|
});
|
|
4185
|
+
void closing(opened);
|
|
4183
4186
|
}
|
|
4184
|
-
function
|
|
4185
|
-
|
|
4187
|
+
function closing(opened) {
|
|
4188
|
+
return task(OPENING_LANE, async (live) => {
|
|
4189
|
+
if (!live())
|
|
4190
|
+
return;
|
|
4191
|
+
const local = await openLocal();
|
|
4192
|
+
if (!opened && !socketOpen)
|
|
4193
|
+
await cachedModels(local);
|
|
4194
|
+
await rest(null, live);
|
|
4195
|
+
});
|
|
4196
|
+
}
|
|
4197
|
+
function cachedModels(local) {
|
|
4198
|
+
return settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
|
|
4199
|
+
}
|
|
4200
|
+
function caughtUpRooms(expired) {
|
|
4201
|
+
const progress = underway;
|
|
4202
|
+
underway = null;
|
|
4203
|
+
naming = Math.max(0, naming - 1);
|
|
4186
4204
|
const cycle = progress ?? { complete: true, reachable: true };
|
|
4187
|
-
if (opened === generation)
|
|
4188
|
-
opened = null;
|
|
4189
4205
|
const reloading = progress === null ? [] : owedReloads(expired);
|
|
4190
4206
|
const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
|
|
4191
4207
|
return task(OPENING_LANE, async (live) => {
|
|
4192
|
-
underway = null;
|
|
4193
4208
|
if (!live())
|
|
4194
4209
|
return;
|
|
4195
4210
|
if (progress === null) {
|
|
@@ -4197,24 +4212,14 @@ export function composeRuntime(options) {
|
|
|
4197
4212
|
await rest(cycle, live);
|
|
4198
4213
|
return;
|
|
4199
4214
|
}
|
|
4200
|
-
await roomsCarried([...expired, ...reloading]);
|
|
4201
|
-
if (!live())
|
|
4202
|
-
return;
|
|
4203
4215
|
try {
|
|
4204
4216
|
const local = await openLocal();
|
|
4205
|
-
|
|
4217
|
+
await replacing;
|
|
4206
4218
|
if (progress.reachable) {
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
!reloading.includes(room.locator)) {
|
|
4211
|
-
await confirmModels(room, local);
|
|
4212
|
-
}
|
|
4213
|
-
}
|
|
4219
|
+
await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
|
|
4220
|
+
!expired.includes(room.locator) &&
|
|
4221
|
+
!reloading.includes(room.locator)), 'current', local);
|
|
4214
4222
|
}
|
|
4215
|
-
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
4216
|
-
if (failed !== undefined)
|
|
4217
|
-
throw failed.reason;
|
|
4218
4223
|
}
|
|
4219
4224
|
catch {
|
|
4220
4225
|
progress.complete = false;
|
|
@@ -4226,11 +4231,8 @@ export function composeRuntime(options) {
|
|
|
4226
4231
|
}
|
|
4227
4232
|
function owedReloads(expired) {
|
|
4228
4233
|
const reloading = [...discovered.values()]
|
|
4229
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
4230
|
-
!expired.includes(room.locator) &&
|
|
4231
|
-
(owed.has(room.locator) || revoked(room.locator)))
|
|
4234
|
+
.filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
|
|
4232
4235
|
.map((room) => room.locator);
|
|
4233
|
-
owed.clear();
|
|
4234
4236
|
for (const locator of reloading)
|
|
4235
4237
|
catching.add(locator);
|
|
4236
4238
|
liveQueries.recover();
|
|
@@ -4239,22 +4241,44 @@ export function composeRuntime(options) {
|
|
|
4239
4241
|
async function reloadNow(replaced, progress) {
|
|
4240
4242
|
const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
|
|
4241
4243
|
for (const room of reloaded)
|
|
4242
|
-
reloadsDue.
|
|
4244
|
+
reloadsDue.add(room.locator);
|
|
4243
4245
|
try {
|
|
4244
4246
|
const local = await openLocal();
|
|
4245
|
-
|
|
4247
|
+
for (const room of reloaded) {
|
|
4248
|
+
void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
|
|
4249
|
+
}
|
|
4246
4250
|
}
|
|
4247
4251
|
catch {
|
|
4248
4252
|
for (const room of reloaded)
|
|
4249
4253
|
reloadsDue.delete(room.locator);
|
|
4250
4254
|
progress.reachable = false;
|
|
4251
|
-
return [];
|
|
4252
4255
|
}
|
|
4253
4256
|
}
|
|
4254
|
-
async function
|
|
4255
|
-
const
|
|
4256
|
-
|
|
4257
|
-
|
|
4257
|
+
async function settleModels(held, status, local) {
|
|
4258
|
+
const owed = (room) => {
|
|
4259
|
+
const model = modelOf(room.locator);
|
|
4260
|
+
return model.status !== 'unavailable' && model.status !== status;
|
|
4261
|
+
};
|
|
4262
|
+
const settled = await exclusive(async () => {
|
|
4263
|
+
if (!continuing())
|
|
4264
|
+
return [];
|
|
4265
|
+
const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
|
|
4266
|
+
for (const room of unchanged) {
|
|
4267
|
+
const next = { ...modelOf(room.locator), status };
|
|
4268
|
+
const replica = replicas.get(room.locator);
|
|
4269
|
+
if (replica !== undefined)
|
|
4270
|
+
replicas.set(room.locator, { ...replica, record: { ...replica.record, model: next } });
|
|
4271
|
+
heldModel(room, next);
|
|
4272
|
+
}
|
|
4273
|
+
if (unchanged.length > 0)
|
|
4274
|
+
emitRooms();
|
|
4275
|
+
return unchanged;
|
|
4276
|
+
});
|
|
4277
|
+
for (const room of held) {
|
|
4278
|
+
if (settled.includes(room) || !owed(room))
|
|
4279
|
+
continue;
|
|
4280
|
+
await retainModel(local, room, { ...modelOf(room.locator), status });
|
|
4281
|
+
}
|
|
4258
4282
|
}
|
|
4259
4283
|
function submitLater(locator) {
|
|
4260
4284
|
if (localSaves)
|
|
@@ -4298,14 +4322,10 @@ export function composeRuntime(options) {
|
|
|
4298
4322
|
try {
|
|
4299
4323
|
const local = await openLocal();
|
|
4300
4324
|
await roomTask(locator, async (live) => {
|
|
4301
|
-
|
|
4302
|
-
if (!current() || authorization === undefined)
|
|
4325
|
+
if (!live() || authorization === undefined)
|
|
4303
4326
|
return;
|
|
4304
|
-
const withheld = await
|
|
4305
|
-
|
|
4306
|
-
abandonedOn(signal),
|
|
4307
|
-
]);
|
|
4308
|
-
if (withheld !== null && current())
|
|
4327
|
+
const withheld = await advance(room, local, authorization, live);
|
|
4328
|
+
if (withheld !== null && live())
|
|
4309
4329
|
await withhold(withheld, room, local, progress);
|
|
4310
4330
|
});
|
|
4311
4331
|
}
|
|
@@ -4360,14 +4380,11 @@ export function composeRuntime(options) {
|
|
|
4360
4380
|
progress.complete = false;
|
|
4361
4381
|
return;
|
|
4362
4382
|
}
|
|
4363
|
-
reads.applied();
|
|
4364
4383
|
submitAll();
|
|
4365
4384
|
for (const locator of stale)
|
|
4366
4385
|
if (repairable.has(locator))
|
|
4367
4386
|
repairLater(locator, {});
|
|
4368
4387
|
await conversations.reconcile();
|
|
4369
|
-
if (status.providerUnreachable)
|
|
4370
|
-
conversations.reconnected();
|
|
4371
4388
|
const reading = { complete: true, reachable: true };
|
|
4372
4389
|
await conversations.catchUp(reading);
|
|
4373
4390
|
if (progress.reachable && reading.reachable)
|
|
@@ -4393,7 +4410,7 @@ export function composeRuntime(options) {
|
|
|
4393
4410
|
if (!live())
|
|
4394
4411
|
return;
|
|
4395
4412
|
let lastCompleted = status.lastCompleted;
|
|
4396
|
-
let synced = complete && stale.size === 0 && !pending();
|
|
4413
|
+
let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
|
|
4397
4414
|
if (synced && !recorded) {
|
|
4398
4415
|
const completedAt = new Date().toISOString();
|
|
4399
4416
|
try {
|
|
@@ -4408,74 +4425,69 @@ export function composeRuntime(options) {
|
|
|
4408
4425
|
}
|
|
4409
4426
|
}
|
|
4410
4427
|
moveTo({
|
|
4411
|
-
state: pending()
|
|
4428
|
+
state: pending()
|
|
4429
|
+
? 'pending'
|
|
4430
|
+
: synced && complete && roomWork === 0 && naming === 0
|
|
4431
|
+
? 'synced'
|
|
4432
|
+
: 'syncing',
|
|
4412
4433
|
lastCompleted,
|
|
4413
|
-
providerUnreachable: !progress.reachable,
|
|
4434
|
+
providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
|
|
4414
4435
|
...attention(),
|
|
4415
4436
|
});
|
|
4416
4437
|
}
|
|
4417
|
-
function
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
}
|
|
4425
|
-
function applyRoom(locator, roomVersion) {
|
|
4426
|
-
announced.set(locator, roomVersion);
|
|
4427
|
-
return task(roomLane(locator), async (live) => {
|
|
4428
|
-
if (discovering?.generation === generation)
|
|
4429
|
-
await discovering.done;
|
|
4430
|
-
const saving = answering.get(locator);
|
|
4431
|
-
if (saving?.generation === generation)
|
|
4432
|
-
await saving.answered;
|
|
4433
|
-
if (!live() || authorization === undefined)
|
|
4434
|
-
return;
|
|
4435
|
-
const room = discovered.get(locator);
|
|
4436
|
-
const held = replicas.get(locator);
|
|
4437
|
-
if (room === undefined || room.replica.admission !== 'admitted')
|
|
4438
|
-
return;
|
|
4439
|
-
if (held !== undefined &&
|
|
4440
|
-
(held.record.replica.selection !== room.replica.selection ||
|
|
4441
|
-
options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
|
|
4442
|
-
return;
|
|
4443
|
-
}
|
|
4444
|
-
const during = underway?.generation === generation ? underway.progress : null;
|
|
4445
|
-
const progress = during ?? { complete: true, reachable: true };
|
|
4446
|
-
if (during === null)
|
|
4447
|
-
moveTo(transferring(status));
|
|
4438
|
+
function applyRoom(change) {
|
|
4439
|
+
const { locator } = change;
|
|
4440
|
+
if (stopping)
|
|
4441
|
+
return Promise.resolve();
|
|
4442
|
+
return inRoom(locator, async (live) => {
|
|
4443
|
+
await answering.get(locator)?.answered;
|
|
4444
|
+
const progress = underway ?? { complete: true, reachable: true };
|
|
4448
4445
|
try {
|
|
4449
4446
|
const local = await openLocal();
|
|
4450
|
-
|
|
4451
|
-
if (!live()
|
|
4452
|
-
return;
|
|
4453
|
-
const
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4447
|
+
for (;;) {
|
|
4448
|
+
if (!live())
|
|
4449
|
+
return null;
|
|
4450
|
+
const room = discovered.get(locator);
|
|
4451
|
+
const held = replicas.get(locator);
|
|
4452
|
+
if (room === undefined ||
|
|
4453
|
+
held === undefined ||
|
|
4454
|
+
room.replica.admission !== 'admitted' ||
|
|
4455
|
+
held.record.replica.selection !== room.replica.selection) {
|
|
4456
|
+
return null;
|
|
4457
|
+
}
|
|
4458
|
+
const at = held.record.roomVersion;
|
|
4459
|
+
if (options.adapter.reached(locator, at, change.roomVersion))
|
|
4460
|
+
return progress;
|
|
4461
|
+
if (!options.adapter.reached(locator, at, change.from)) {
|
|
4462
|
+
name(locator);
|
|
4463
|
+
return progress;
|
|
4464
|
+
}
|
|
4465
|
+
if (underway === null)
|
|
4466
|
+
moveTo(transferring(status));
|
|
4467
|
+
const writing = () => live() && !full;
|
|
4468
|
+
const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, () => change.read()));
|
|
4469
|
+
if (refreshed.kind !== 'advanced')
|
|
4470
|
+
return progress;
|
|
4471
|
+
const landing = {
|
|
4472
|
+
kind: 'refreshed',
|
|
4473
|
+
from: refreshed.from,
|
|
4474
|
+
remodelled: refreshed.remodelled,
|
|
4475
|
+
};
|
|
4476
|
+
if (await publish(local, room, refreshed.record, landing, writing)) {
|
|
4477
|
+
stale.delete(locator);
|
|
4478
|
+
await retainModel(local, room, modelOf(locator));
|
|
4479
|
+
return progress;
|
|
4480
|
+
}
|
|
4481
|
+
}
|
|
4457
4482
|
}
|
|
4458
4483
|
catch {
|
|
4484
|
+
if (!stale.has(locator))
|
|
4485
|
+
name(locator);
|
|
4459
4486
|
stale.add(locator);
|
|
4487
|
+
return progress;
|
|
4460
4488
|
}
|
|
4461
|
-
|
|
4462
|
-
if (during === null)
|
|
4463
|
-
await rest(progress, live);
|
|
4464
|
-
}
|
|
4465
|
-
});
|
|
4489
|
+
}).then((progress) => rest(progress), () => undefined);
|
|
4466
4490
|
}
|
|
4467
|
-
function roomsCarried(replacing) {
|
|
4468
|
-
const replaced = new Set(replacing.map(roomLane));
|
|
4469
|
-
return Promise.all([...lanes]
|
|
4470
|
-
.filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
|
|
4471
|
-
.map(([, held]) => held.chain));
|
|
4472
|
-
}
|
|
4473
|
-
const receipts = {
|
|
4474
|
-
heads: async () => (await openLocal()).receivedHeads(),
|
|
4475
|
-
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
4476
|
-
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
4477
|
-
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
4478
|
-
};
|
|
4479
4491
|
function observeStream() {
|
|
4480
4492
|
const stream = options.adapter.roomStream;
|
|
4481
4493
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -4485,29 +4497,29 @@ export function composeRuntime(options) {
|
|
|
4485
4497
|
return;
|
|
4486
4498
|
if (event.kind === 'opened') {
|
|
4487
4499
|
socketOpen = true;
|
|
4500
|
+
announced = false;
|
|
4501
|
+
handed = false;
|
|
4502
|
+
naming = 0;
|
|
4488
4503
|
connectionLive();
|
|
4489
4504
|
generation += 1;
|
|
4490
|
-
reads.opened();
|
|
4491
4505
|
conversations.reopened();
|
|
4492
4506
|
recovery.resume();
|
|
4493
|
-
|
|
4507
|
+
}
|
|
4508
|
+
else if (event.kind === 'discovered') {
|
|
4509
|
+
if (!socketOpen)
|
|
4510
|
+
return;
|
|
4511
|
+
announced = true;
|
|
4512
|
+
void sync('opened', event.discovery);
|
|
4494
4513
|
}
|
|
4495
4514
|
else if (event.kind === 'closed')
|
|
4496
4515
|
closedSocket(event.revoked, event.lost);
|
|
4497
|
-
else if (event.kind === '
|
|
4498
|
-
void
|
|
4516
|
+
else if (event.kind === 'caught-up')
|
|
4517
|
+
void caughtUpRooms(event.expired);
|
|
4499
4518
|
else if (event.kind === 'authority-changed')
|
|
4500
|
-
void
|
|
4519
|
+
void changeAuthority(event);
|
|
4501
4520
|
else
|
|
4502
|
-
void applyRoom(event
|
|
4503
|
-
}
|
|
4504
|
-
}
|
|
4505
|
-
async function heldRevisions(locator, paths) {
|
|
4506
|
-
const held = replicas.get(locator);
|
|
4507
|
-
if (held === undefined || paths.length === 0)
|
|
4508
|
-
return new Map();
|
|
4509
|
-
const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
|
|
4510
|
-
return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
|
|
4521
|
+
void applyRoom(event);
|
|
4522
|
+
});
|
|
4511
4523
|
}
|
|
4512
4524
|
function writesHeld() {
|
|
4513
4525
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -4577,10 +4589,9 @@ export function composeRuntime(options) {
|
|
|
4577
4589
|
if (closed)
|
|
4578
4590
|
return;
|
|
4579
4591
|
closed = true;
|
|
4592
|
+
notifyDiscovered();
|
|
4580
4593
|
if (probing !== null)
|
|
4581
4594
|
clearInterval(probing);
|
|
4582
|
-
for (const resolve of shownWaiters.splice(0))
|
|
4583
|
-
resolve();
|
|
4584
4595
|
connectionLive();
|
|
4585
4596
|
lockListeners.clear();
|
|
4586
4597
|
readinessWatches.clear();
|
|
@@ -4592,6 +4603,7 @@ export function composeRuntime(options) {
|
|
|
4592
4603
|
conversations.close();
|
|
4593
4604
|
void recovery.retire();
|
|
4594
4605
|
roomListeners.clear();
|
|
4606
|
+
unlistedRooms.clear();
|
|
4595
4607
|
refusalListeners.clear();
|
|
4596
4608
|
storageLossListeners.clear();
|
|
4597
4609
|
statusListeners.clear();
|
|
@@ -4604,6 +4616,7 @@ export function composeRuntime(options) {
|
|
|
4604
4616
|
}
|
|
4605
4617
|
async function stop() {
|
|
4606
4618
|
stopping = true;
|
|
4619
|
+
inFlight.abort();
|
|
4607
4620
|
await recovery.retire();
|
|
4608
4621
|
await Promise.all(working);
|
|
4609
4622
|
await mutations;
|
|
@@ -4617,12 +4630,12 @@ export function composeRuntime(options) {
|
|
|
4617
4630
|
observeStream();
|
|
4618
4631
|
void openLocal().then(() => {
|
|
4619
4632
|
if (capabilities.length > 0)
|
|
4620
|
-
|
|
4633
|
+
readyFromStore();
|
|
4621
4634
|
}, () => {
|
|
4622
|
-
|
|
4635
|
+
readyFromStore();
|
|
4623
4636
|
});
|
|
4624
4637
|
if (options.adapter.roomStream === undefined)
|
|
4625
|
-
void sync('unstreamed');
|
|
4638
|
+
void sync('unstreamed', null);
|
|
4626
4639
|
function planned(input, reach) {
|
|
4627
4640
|
const plan = planOf(input, roomFacts(reach), planReach(reach));
|
|
4628
4641
|
const named = plan.paths !== null &&
|
|
@@ -4770,9 +4783,10 @@ export function composeRuntime(options) {
|
|
|
4770
4783
|
return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
|
|
4771
4784
|
}
|
|
4772
4785
|
if (topic === 'rooms') {
|
|
4773
|
-
|
|
4774
|
-
|
|
4786
|
+
unlistedRooms.add(deliver);
|
|
4787
|
+
emitKnownRooms();
|
|
4775
4788
|
return () => {
|
|
4789
|
+
unlistedRooms.delete(deliver);
|
|
4776
4790
|
roomListeners.delete(deliver);
|
|
4777
4791
|
};
|
|
4778
4792
|
}
|
|
@@ -4826,7 +4840,17 @@ export function composeRuntime(options) {
|
|
|
4826
4840
|
return openLocal().then(() => undefined);
|
|
4827
4841
|
},
|
|
4828
4842
|
rooms() {
|
|
4829
|
-
|
|
4843
|
+
if (closed)
|
|
4844
|
+
return Promise.resolve([]);
|
|
4845
|
+
return new Promise((settle) => {
|
|
4846
|
+
const answer = (value) => {
|
|
4847
|
+
unlistedRooms.delete(answer);
|
|
4848
|
+
roomListeners.delete(answer);
|
|
4849
|
+
settle(value);
|
|
4850
|
+
};
|
|
4851
|
+
unlistedRooms.add(answer);
|
|
4852
|
+
emitKnownRooms();
|
|
4853
|
+
});
|
|
4830
4854
|
},
|
|
4831
4855
|
query: ((input) => rowsQuery(planned(input, null))),
|
|
4832
4856
|
search: (input) => searched(input, null, []),
|
|
@@ -4915,6 +4939,8 @@ export function composeRuntime(options) {
|
|
|
4915
4939
|
part: readPart,
|
|
4916
4940
|
read: readBinaryPath,
|
|
4917
4941
|
link: linkBinaryPath,
|
|
4942
|
+
show: showBinaryPath,
|
|
4943
|
+
attach: attachBinary,
|
|
4918
4944
|
},
|
|
4919
4945
|
async call(name, input) {
|
|
4920
4946
|
await ready;
|
|
@@ -4952,7 +4978,7 @@ export function composeRuntime(options) {
|
|
|
4952
4978
|
authorization = undefined;
|
|
4953
4979
|
discoveryPaused = false;
|
|
4954
4980
|
if (options.adapter.roomStream === undefined)
|
|
4955
|
-
await sync('unstreamed');
|
|
4981
|
+
await sync('unstreamed', null);
|
|
4956
4982
|
else
|
|
4957
4983
|
await task(OPENING_LANE, ensureAuthorization);
|
|
4958
4984
|
},
|
|
@@ -4969,12 +4995,12 @@ export function composeRuntime(options) {
|
|
|
4969
4995
|
lockedRooms,
|
|
4970
4996
|
watchLocks,
|
|
4971
4997
|
located,
|
|
4998
|
+
room: (locator) => listed().find((one) => one.locator === locator),
|
|
4972
4999
|
};
|
|
4973
5000
|
return {
|
|
4974
5001
|
md,
|
|
4975
5002
|
seed,
|
|
4976
5003
|
stop,
|
|
4977
|
-
lose: loseRooms,
|
|
4978
5004
|
watch,
|
|
4979
5005
|
persisted(answer) {
|
|
4980
5006
|
if (answer === persistence)
|
|
@@ -4983,7 +5009,6 @@ export function composeRuntime(options) {
|
|
|
4983
5009
|
journal.record(`persistence ${answer}`);
|
|
4984
5010
|
queueReadiness();
|
|
4985
5011
|
},
|
|
4986
|
-
shown: screen,
|
|
4987
5012
|
within: (reach, port) => {
|
|
4988
5013
|
if (full)
|
|
4989
5014
|
void probed();
|