@agentshouse/mdruntime 0.1.0-c1ff51b87bc3d → 0.1.0-c20970f71a6a9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/adapter.d.ts +68 -48
- package/dist/adapter.d.ts.map +1 -1
- package/dist/bootstrap.d.ts +4 -7
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +19 -14
- package/dist/client.d.ts +4 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +0 -1
- package/dist/collection.d.ts +0 -3
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +1 -17
- package/dist/compile.d.ts +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +69 -45
- package/dist/connection.d.ts +3 -5
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +867 -851
- package/dist/conversation.d.ts +14 -14
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +153 -145
- package/dist/durability.d.ts +0 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/entry.d.ts.map +1 -1
- package/dist/entry.js +1 -0
- package/dist/host.d.ts +3 -6
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +8 -17
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/live.d.ts.map +1 -1
- package/dist/live.js +1 -2
- package/dist/media.d.ts +1 -1
- package/dist/media.d.ts.map +1 -1
- package/dist/media.js +19 -9
- package/dist/memory.d.ts +9 -5
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +134 -82
- package/dist/messages.d.ts +2 -2
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +15 -16
- package/dist/model.d.ts +0 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +0 -9
- package/dist/outcome.d.ts +0 -3
- package/dist/outcome.d.ts.map +1 -1
- package/dist/phrase.d.ts +3 -0
- package/dist/phrase.d.ts.map +1 -0
- package/dist/phrase.js +8 -0
- package/dist/query.d.ts +5 -1
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +20 -1
- package/dist/reach.d.ts +3 -3
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +19 -29
- package/dist/react.d.ts +1 -0
- package/dist/react.d.ts.map +1 -1
- package/dist/readiness.d.ts +2 -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 +7 -5
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +46 -6
- package/dist/replica.d.ts +0 -1
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +3 -4
- package/dist/room-draft.d.ts +1 -1
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +61 -48
- package/dist/rows.d.ts +3 -2
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +34 -29
- package/dist/search.d.ts +12 -63
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +9 -52
- 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 +0 -4
- package/dist/status.d.ts.map +1 -1
- package/dist/store.d.ts +15 -19
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +40 -39
- package/dist/stored.d.ts +33 -6
- package/dist/stored.d.ts.map +1 -1
- package/dist/stored.js +97 -122
- package/dist/tables.d.ts +34 -33
- package/dist/tables.d.ts.map +1 -1
- package/dist/tables.js +18 -54
- 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 +1 -4
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +1 -1
- package/package.json +2 -2
- package/dist/fulltext.d.ts +0 -27
- package/dist/fulltext.d.ts.map +0 -1
- package/dist/fulltext.js +0 -368
- package/dist/reads.d.ts +0 -11
- package/dist/reads.d.ts.map +0 -1
- package/dist/reads.js +0 -91
- package/dist/words.d.ts +0 -19
- package/dist/words.d.ts.map +0 -1
- package/dist/words.js +0 -156
package/dist/connection.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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';
|
|
5
|
-
import {
|
|
5
|
+
import { bodied, identityOf, planOf, plain, queryKey, } from './query.js';
|
|
6
6
|
import { createLiveQueries } from './live.js';
|
|
7
7
|
import { seededResult } from './seed.js';
|
|
8
8
|
import { documentKey, roomOfKey } from './rows.js';
|
|
@@ -12,7 +12,7 @@ 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, composedOf,
|
|
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';
|
|
@@ -20,10 +20,9 @@ import { attachedFile } from './file.js';
|
|
|
20
20
|
import { openedFiles } from './opened.js';
|
|
21
21
|
import { ABSENT_TARGET, absentBinary, attachAnswer, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, OFFLINE, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
|
|
22
22
|
import { createRecovery } from './recovery.js';
|
|
23
|
-
import { doorReads } from './reads.js';
|
|
24
23
|
import { advancement } from './refresh.js';
|
|
25
24
|
import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
|
|
26
|
-
import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths,
|
|
25
|
+
import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
|
|
27
26
|
import { binaryBase, changedParts, changesOf, gatheredParts, held, heldFile, joinedDraft, moveTarget, readParts, readSavedParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
28
27
|
import { same, saving, STARTING, transferring, } from './status.js';
|
|
29
28
|
import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
|
|
@@ -34,7 +33,7 @@ import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
|
|
|
34
33
|
import { identityTime, roomDraftRevision } from './identity.js';
|
|
35
34
|
import { ancestors, beneath, normalize } from './paths.js';
|
|
36
35
|
import { openRuntimeStore, } from './store.js';
|
|
37
|
-
import {
|
|
36
|
+
import { validatedNeighbourhood } from './validation.js';
|
|
38
37
|
import { unconfirmed } from './transfer.js';
|
|
39
38
|
import { governs, holds, narrowed, planReach, } from './reach.js';
|
|
40
39
|
function within(root, path) {
|
|
@@ -59,37 +58,34 @@ const DERIVE_BYTES = 1 << 23;
|
|
|
59
58
|
const DERIVE_FLOOR = 1 << 18;
|
|
60
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.
|
|
61
60
|
const DERIVE_AIM_MS = 250;
|
|
62
|
-
// A build past its floor of text stops after a second and a half of work, so a Room whose text costs ten times the usual
|
|
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.
|
|
63
62
|
const DERIVE_LONGEST_MS = 1_500;
|
|
64
|
-
const WALK_BYTES = 1 << 20;
|
|
65
63
|
const QUERY_BYTES = 1 << 20;
|
|
66
64
|
const STAGE_BYTES = 1 << 20;
|
|
67
65
|
const STAGE_FLOOR = 1 << 16;
|
|
68
66
|
// A batch aims at this much database work, so a Save or a query waits behind at most one short write.
|
|
69
67
|
const STAGE_AIM_MS = 50;
|
|
70
68
|
const STREAMS_HELD = 4;
|
|
71
|
-
const NOTHING_HELD = {
|
|
72
|
-
const
|
|
73
|
-
// Bytes still arriving are reported at most this often,
|
|
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.
|
|
74
72
|
const ARRIVAL_REPORT_MS = 250;
|
|
75
|
-
const CLEAN_CHANGES = 2_000;
|
|
76
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.
|
|
77
74
|
const LOCK_GRACE_MS = 120_000;
|
|
78
75
|
function operationOf(input) {
|
|
79
76
|
const operation = input?.operation_id;
|
|
80
77
|
return typeof operation === 'string' ? operation : null;
|
|
81
78
|
}
|
|
82
|
-
function
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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));
|
|
88
86
|
});
|
|
89
87
|
}
|
|
90
|
-
|
|
91
|
-
return `room ${locator}`;
|
|
92
|
-
}
|
|
88
|
+
const TEXT_READS = 4;
|
|
93
89
|
function pendingKey(locator, id) {
|
|
94
90
|
return `${locator}\u0000${id}`;
|
|
95
91
|
}
|
|
@@ -124,11 +120,19 @@ export function composeRuntime(options) {
|
|
|
124
120
|
const liveModels = new FencedMap(isFenced);
|
|
125
121
|
const invalidModels = new Set();
|
|
126
122
|
const discovered = new Map();
|
|
127
|
-
|
|
123
|
+
let inFlight = new AbortController();
|
|
124
|
+
const refreshed = options.adapter.refresh?.bind(options.adapter);
|
|
125
|
+
const reads = {
|
|
126
|
+
bootstrap: (authorized, request) => readUntil(options.adapter.bootstrap(authorized, request), inFlight.signal),
|
|
127
|
+
...(refreshed === undefined
|
|
128
|
+
? {}
|
|
129
|
+
: {
|
|
130
|
+
refresh: (authorized, request) => readUntil(refreshed(authorized, request), inFlight.signal),
|
|
131
|
+
}),
|
|
132
|
+
reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
|
|
133
|
+
};
|
|
128
134
|
let roomsListed = false;
|
|
129
135
|
const scopedRefusals = new Map();
|
|
130
|
-
const sourceRefusals = new Map();
|
|
131
|
-
const declarationRefusals = new Set();
|
|
132
136
|
function modelOf(locator) {
|
|
133
137
|
return models.get(locator) ?? NO_MODEL;
|
|
134
138
|
}
|
|
@@ -151,10 +155,10 @@ export function composeRuntime(options) {
|
|
|
151
155
|
let closed = false;
|
|
152
156
|
let stopping = false;
|
|
153
157
|
let withdrawStream;
|
|
154
|
-
const announced = new Map();
|
|
155
158
|
let underway = null;
|
|
156
159
|
let streamed = null;
|
|
157
160
|
let handed = false;
|
|
161
|
+
let naming = 0;
|
|
158
162
|
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
159
163
|
const repairing = new Set();
|
|
160
164
|
const calling = new Set();
|
|
@@ -165,12 +169,13 @@ export function composeRuntime(options) {
|
|
|
165
169
|
let recorded = false;
|
|
166
170
|
const stale = new Set();
|
|
167
171
|
let generation = 0;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
const reloadsDue = new Map();
|
|
172
|
+
const reloadsDue = new Set();
|
|
173
|
+
const highest = new Map();
|
|
174
|
+
const beyondHorizon = new Set();
|
|
172
175
|
const lanes = new Map();
|
|
173
|
-
|
|
176
|
+
const roomLanes = new Map();
|
|
177
|
+
let roomWork = 0;
|
|
178
|
+
const roomReads = { free: ROOM_READS, waiting: [] };
|
|
174
179
|
const working = new Set();
|
|
175
180
|
let mutations = Promise.resolve();
|
|
176
181
|
let gathering = null;
|
|
@@ -186,6 +191,8 @@ export function composeRuntime(options) {
|
|
|
186
191
|
let withdrawConversations;
|
|
187
192
|
let currencyQueued = null;
|
|
188
193
|
let socketOpen = false;
|
|
194
|
+
let announced = false;
|
|
195
|
+
let missedChange = false;
|
|
189
196
|
const confirmed = new Set();
|
|
190
197
|
let lockGrace = null;
|
|
191
198
|
const lockListeners = new Set();
|
|
@@ -195,6 +202,8 @@ export function composeRuntime(options) {
|
|
|
195
202
|
const serviceModeListeners = new Set();
|
|
196
203
|
let member = null;
|
|
197
204
|
const memberListeners = new Set();
|
|
205
|
+
let authorityChanges = 0;
|
|
206
|
+
const authorityChangeListeners = new Set();
|
|
198
207
|
const unpreparedDemand = new Set();
|
|
199
208
|
const unpreparable = new Set();
|
|
200
209
|
let preparing = false;
|
|
@@ -224,12 +233,10 @@ export function composeRuntime(options) {
|
|
|
224
233
|
const streaming = new Set();
|
|
225
234
|
const underived = new Map();
|
|
226
235
|
const unindexed = new Map();
|
|
227
|
-
const
|
|
236
|
+
const startedOver = new Map();
|
|
228
237
|
let arrivalReported = 0;
|
|
229
|
-
let
|
|
230
|
-
|
|
231
|
-
let streams = { generation, free: STREAMS_HELD };
|
|
232
|
-
let queuedBytes = 0;
|
|
238
|
+
let streamsFree = STREAMS_HELD;
|
|
239
|
+
let queuedText = 0;
|
|
233
240
|
const installWaiters = [];
|
|
234
241
|
const stageBudget = {
|
|
235
242
|
bytes: STAGE_FLOOR * 4,
|
|
@@ -243,28 +250,14 @@ export function composeRuntime(options) {
|
|
|
243
250
|
ceiling: DERIVE_BYTES,
|
|
244
251
|
aim: DERIVE_AIM_MS,
|
|
245
252
|
};
|
|
246
|
-
|
|
253
|
+
const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
|
|
247
254
|
const conversations = createConversationStore({
|
|
248
|
-
agents: declaredAgents,
|
|
249
|
-
documents: (declaration, after) => rowsQuery(planOf({
|
|
250
|
-
rooms: [declaration.locator],
|
|
251
|
-
types: [declaration.type],
|
|
252
|
-
select: {
|
|
253
|
-
fields: [
|
|
254
|
-
declaration.identity,
|
|
255
|
-
declaration.name,
|
|
256
|
-
...(declaration.retired === undefined ? [] : [declaration.retired]),
|
|
257
|
-
].map((field) => `/${escapePointer(field)}`),
|
|
258
|
-
},
|
|
259
|
-
limit: MAXIMUM_WINDOW,
|
|
260
|
-
...(after === undefined ? {} : { after }),
|
|
261
|
-
}, roomFacts())),
|
|
262
255
|
call: (name, input) => invokeAction(name, input),
|
|
256
|
+
history: (request) => readConversationHistory(request),
|
|
263
257
|
durably,
|
|
264
258
|
stream: () => options.adapter.conversations,
|
|
265
259
|
publish: publishChangeSet,
|
|
266
260
|
online: () => connectionOf() !== 'unreachable' && refusal === null,
|
|
267
|
-
listed: () => roomsListed,
|
|
268
261
|
full: () => full,
|
|
269
262
|
closed: () => closed,
|
|
270
263
|
unanswered: resendLater,
|
|
@@ -272,6 +265,8 @@ export function composeRuntime(options) {
|
|
|
272
265
|
});
|
|
273
266
|
const journal = createLog();
|
|
274
267
|
const keptFiles = openedFiles(database, isFenced);
|
|
268
|
+
const holding = new Map();
|
|
269
|
+
const unholdable = new Map();
|
|
275
270
|
let readyFromStore = () => { };
|
|
276
271
|
const ready = new Promise((resolve) => {
|
|
277
272
|
readyFromStore = resolve;
|
|
@@ -361,9 +356,9 @@ export function composeRuntime(options) {
|
|
|
361
356
|
return value === undefined ? null : structuredClone(value);
|
|
362
357
|
}
|
|
363
358
|
async function writtenRoomDraft(room, path, revision, input, next) {
|
|
364
|
-
const parts = readParts(input);
|
|
359
|
+
const parts = readParts(input, path);
|
|
365
360
|
if (path === '' || normalize(path) !== path)
|
|
366
|
-
throw runtimeError('room-draft-malformed');
|
|
361
|
+
throw runtimeError('room-draft-malformed', `room-draft-malformed at ${path}: a path inside the Room, such as notes/a.md`);
|
|
367
362
|
const local = await openLocal();
|
|
368
363
|
const current = roomDrafts.get(room)?.get(path);
|
|
369
364
|
if ((current?.revision ?? null) !== revision) {
|
|
@@ -412,7 +407,7 @@ export function composeRuntime(options) {
|
|
|
412
407
|
const changed = new Map();
|
|
413
408
|
for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
|
|
414
409
|
for (const { batch, state } of entriesOf(room)) {
|
|
415
|
-
if (state === '
|
|
410
|
+
if (state === 'local')
|
|
416
411
|
continue;
|
|
417
412
|
for (const { path } of batch.changes)
|
|
418
413
|
changed.set(`${room}\u0000${path}`, { room, path });
|
|
@@ -433,16 +428,9 @@ export function composeRuntime(options) {
|
|
|
433
428
|
returned: structuredClone(record.returned),
|
|
434
429
|
});
|
|
435
430
|
}
|
|
436
|
-
for (const [room, refusal] of sourceRefusals)
|
|
437
|
-
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
438
431
|
attentionRequired.sort((left, right) => {
|
|
439
432
|
if (left.room !== right.room)
|
|
440
433
|
return left.room < right.room ? -1 : 1;
|
|
441
|
-
if ('refusal' in left || 'refusal' in right) {
|
|
442
|
-
if ('refusal' in left && 'refusal' in right)
|
|
443
|
-
return 0;
|
|
444
|
-
return 'refusal' in left ? -1 : 1;
|
|
445
|
-
}
|
|
446
434
|
if (left.path !== right.path)
|
|
447
435
|
return left.path < right.path ? -1 : 1;
|
|
448
436
|
return 0;
|
|
@@ -597,9 +585,6 @@ export function composeRuntime(options) {
|
|
|
597
585
|
if (localSaves)
|
|
598
586
|
return Object.freeze({ batch, state: 'local' });
|
|
599
587
|
const state = pendings.get(pendingKey(locator, batch.id));
|
|
600
|
-
if (state?.accepted !== undefined) {
|
|
601
|
-
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
602
|
-
}
|
|
603
588
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
604
589
|
}
|
|
605
590
|
function entriesOf(locator) {
|
|
@@ -673,11 +658,7 @@ export function composeRuntime(options) {
|
|
|
673
658
|
})();
|
|
674
659
|
}
|
|
675
660
|
function roomsOfReach(reach) {
|
|
676
|
-
return new Set([
|
|
677
|
-
...reach.rooms,
|
|
678
|
-
...reach.documents.map(roomOfKey),
|
|
679
|
-
...reach.areas.flatMap((area) => (area.types.length === 0 ? [] : [area.room])),
|
|
680
|
-
]);
|
|
661
|
+
return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
681
662
|
}
|
|
682
663
|
function reachedRooms(plan) {
|
|
683
664
|
const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
|
|
@@ -716,7 +697,7 @@ export function composeRuntime(options) {
|
|
|
716
697
|
coverage.push({
|
|
717
698
|
room: locator,
|
|
718
699
|
generation: replica.record.transfer,
|
|
719
|
-
current: !(catching.has(locator) ||
|
|
700
|
+
current: !(catching.has(locator) || transfers.has(locator)),
|
|
720
701
|
});
|
|
721
702
|
read.push({ locator, key: replica.record.key });
|
|
722
703
|
for (const batch of replica.queue)
|
|
@@ -743,8 +724,6 @@ export function composeRuntime(options) {
|
|
|
743
724
|
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
744
725
|
}
|
|
745
726
|
function roomState(locator) {
|
|
746
|
-
if (sourceRefusals.has(locator))
|
|
747
|
-
return 'refused';
|
|
748
727
|
const listedRoom = discovered.get(locator);
|
|
749
728
|
if (roomsListed &&
|
|
750
729
|
(listedRoom === undefined ||
|
|
@@ -759,7 +738,7 @@ export function composeRuntime(options) {
|
|
|
759
738
|
}
|
|
760
739
|
function conversationCoverage(reach) {
|
|
761
740
|
const declared = reach === null || reach.conversations;
|
|
762
|
-
if (!declared || options.adapter.conversations === undefined
|
|
741
|
+
if (!declared || options.adapter.conversations === undefined)
|
|
763
742
|
return 'undeclared';
|
|
764
743
|
if (!conversations.known() || !conversations.current())
|
|
765
744
|
return 'acquiring';
|
|
@@ -774,79 +753,50 @@ export function composeRuntime(options) {
|
|
|
774
753
|
? 'connected'
|
|
775
754
|
: 'connecting';
|
|
776
755
|
}
|
|
777
|
-
function
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
rule: held.code,
|
|
789
|
-
location: typeof location === 'string' ? location : null,
|
|
790
|
-
},
|
|
791
|
-
];
|
|
792
|
-
});
|
|
756
|
+
function bytesOf(room, underway) {
|
|
757
|
+
const transfer = transfers.get(room);
|
|
758
|
+
if (transfer !== undefined) {
|
|
759
|
+
const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
|
|
760
|
+
return {
|
|
761
|
+
done: transfer.done.bytes - Math.ceil(owed),
|
|
762
|
+
total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
|
|
766
|
+
return { done: held.bytes, total: arrivals.get(room) ?? held.bytes };
|
|
793
767
|
}
|
|
794
768
|
function installationOf(coverage) {
|
|
795
769
|
const rooms = { done: 0, total: 0 };
|
|
796
|
-
const parts = { done: 0, total: 0 };
|
|
797
770
|
const bytes = { done: 0, total: 0 };
|
|
798
771
|
let installing = 0;
|
|
799
772
|
let downloading = false;
|
|
800
|
-
let checking = 0;
|
|
801
773
|
for (const { room, state } of coverage) {
|
|
802
774
|
if (state === 'unadmitted')
|
|
803
775
|
continue;
|
|
804
776
|
rooms.total += 1;
|
|
805
777
|
if (state === 'complete')
|
|
806
778
|
rooms.done += 1;
|
|
807
|
-
const transfer = transfers.get(room);
|
|
808
779
|
const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
|
|
809
780
|
if (underway)
|
|
810
781
|
installing += 1;
|
|
811
782
|
if (underway && (streaming.has(room) || !arrivals.has(room)))
|
|
812
783
|
downloading = true;
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
const arrived = arrivals.get(room);
|
|
818
|
-
parts.done += transfer.done.parts;
|
|
819
|
-
parts.total += Math.max(arrived?.parts ?? 0, transfer.done.parts);
|
|
820
|
-
bytes.done += transfer.done.bytes - Math.ceil(owed + unchecking);
|
|
821
|
-
checking += Math.ceil(owed + unchecking) - Math.ceil(owed);
|
|
822
|
-
bytes.total += Math.max(arrived?.bytes ?? 0, transfer.done.bytes);
|
|
823
|
-
continue;
|
|
824
|
-
}
|
|
825
|
-
const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
|
|
826
|
-
const arrived = arrivals.get(room) ?? held;
|
|
827
|
-
parts.done += held.parts;
|
|
828
|
-
parts.total += arrived.parts;
|
|
829
|
-
bytes.done += held.bytes;
|
|
830
|
-
bytes.total += arrived.bytes;
|
|
784
|
+
const now = bytesOf(room, underway);
|
|
785
|
+
const earlier = underway ? startedOver.get(room) : undefined;
|
|
786
|
+
bytes.done += Math.max(now.done, earlier?.done ?? 0);
|
|
787
|
+
bytes.total += Math.max(now.total, earlier?.total ?? 0);
|
|
831
788
|
}
|
|
832
789
|
const step = installing === 0
|
|
833
790
|
? null
|
|
834
791
|
: downloading
|
|
835
792
|
? 'downloading'
|
|
836
|
-
: bytes.done
|
|
793
|
+
: bytes.done < bytes.total
|
|
837
794
|
? 'writing'
|
|
838
795
|
: 'replaying';
|
|
839
|
-
return {
|
|
840
|
-
step,
|
|
841
|
-
paused: installing > 0 && !onScreen ? 'hidden' : null,
|
|
842
|
-
rooms,
|
|
843
|
-
parts,
|
|
844
|
-
bytes,
|
|
845
|
-
};
|
|
796
|
+
return { step, rooms, bytes };
|
|
846
797
|
}
|
|
847
798
|
function readinessFor(port, reach) {
|
|
848
799
|
const required = requiredRooms(reach);
|
|
849
|
-
const search = port.search === 'required';
|
|
850
800
|
const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
|
|
851
801
|
const preparing = coverage
|
|
852
802
|
.filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
|
|
@@ -859,14 +809,12 @@ export function composeRuntime(options) {
|
|
|
859
809
|
listed: required.listed,
|
|
860
810
|
rooms: coverage,
|
|
861
811
|
conversations: conversationCoverage(reach),
|
|
862
|
-
search: search ? 'required' : 'unused',
|
|
863
812
|
},
|
|
864
813
|
persistence,
|
|
865
814
|
connection: connectionOf(),
|
|
866
815
|
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
867
816
|
space: full ? 'full' : port.space,
|
|
868
817
|
installation: installationOf(coverage),
|
|
869
|
-
refused: refusedIn(required.rooms),
|
|
870
818
|
lost: storageLoss !== null,
|
|
871
819
|
established: targets[port.target]?.established === true ||
|
|
872
820
|
(usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
|
|
@@ -954,8 +902,12 @@ export function composeRuntime(options) {
|
|
|
954
902
|
unpreparable.clear();
|
|
955
903
|
journal.record('space freed');
|
|
956
904
|
queueReadiness();
|
|
957
|
-
if (
|
|
958
|
-
|
|
905
|
+
if (closed || authorization === undefined)
|
|
906
|
+
return;
|
|
907
|
+
if (options.adapter.roomStream === undefined)
|
|
908
|
+
void sync('unstreamed', null);
|
|
909
|
+
else
|
|
910
|
+
void resynchronize();
|
|
959
911
|
}
|
|
960
912
|
function seedRoomHeld(room, version) {
|
|
961
913
|
return indexed(room) && reachedBy(room, version);
|
|
@@ -995,7 +947,24 @@ export function composeRuntime(options) {
|
|
|
995
947
|
function sameCover(left, right) {
|
|
996
948
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
997
949
|
}
|
|
950
|
+
async function unheldRows(local, plan, rows, rooms) {
|
|
951
|
+
if (plan.shape.kind !== 'documents' || !bodied(plan.shape))
|
|
952
|
+
return [];
|
|
953
|
+
if (!('documents' in rows) || !textOnline())
|
|
954
|
+
return [];
|
|
955
|
+
const named = new Map();
|
|
956
|
+
for (const { room, path, textHeld } of rows.documents) {
|
|
957
|
+
if (textHeld === false)
|
|
958
|
+
named.set(room, [...(named.get(room) ?? []), path]);
|
|
959
|
+
}
|
|
960
|
+
const unheld = [];
|
|
961
|
+
for (const { locator, key } of rooms) {
|
|
962
|
+
unheld.push(...(await unheldIn(local, locator, key, named.get(locator) ?? [])));
|
|
963
|
+
}
|
|
964
|
+
return unheld.filter((one) => unholdable.get(textKey(one)) !== one.revision);
|
|
965
|
+
}
|
|
998
966
|
async function coveredRows(plan, cover) {
|
|
967
|
+
const asked = new Set();
|
|
999
968
|
for (;;) {
|
|
1000
969
|
const before = cover();
|
|
1001
970
|
if ('pending' in before)
|
|
@@ -1006,8 +975,14 @@ export function composeRuntime(options) {
|
|
|
1006
975
|
const current = () => local.changes(keys) === changes;
|
|
1007
976
|
const rows = await windowRows(local, plan, before.rooms, current);
|
|
1008
977
|
const after = cover();
|
|
1009
|
-
if (rows
|
|
978
|
+
if (rows === null || !sameCover(before, after))
|
|
979
|
+
continue;
|
|
980
|
+
const unheld = (await unheldRows(local, plan, rows, before.rooms)).filter((one) => !asked.has(textKey(one)));
|
|
981
|
+
if (unheld.length === 0)
|
|
1010
982
|
return { rows, cover: before, current };
|
|
983
|
+
for (const one of unheld)
|
|
984
|
+
asked.add(textKey(one));
|
|
985
|
+
await holdTexts(unheld);
|
|
1011
986
|
}
|
|
1012
987
|
}
|
|
1013
988
|
async function windowRows(local, plan, rooms, unchanged) {
|
|
@@ -1103,6 +1078,8 @@ export function composeRuntime(options) {
|
|
|
1103
1078
|
}
|
|
1104
1079
|
async function refuse(reason, provider = null) {
|
|
1105
1080
|
discoveryPaused = true;
|
|
1081
|
+
inFlight.abort();
|
|
1082
|
+
inFlight = new AbortController();
|
|
1106
1083
|
try {
|
|
1107
1084
|
if (reason === 'revoked-authorization') {
|
|
1108
1085
|
await exclusive(async () => {
|
|
@@ -1187,10 +1164,10 @@ export function composeRuntime(options) {
|
|
|
1187
1164
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
1188
1165
|
roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
|
|
1189
1166
|
synchronizationAvailable,
|
|
1190
|
-
refusal: scopedRefusals.get(room.locator) ??
|
|
1167
|
+
refusal: scopedRefusals.get(room.locator) ?? null,
|
|
1191
1168
|
});
|
|
1192
1169
|
}
|
|
1193
|
-
async function applyDiscovery(found, started,
|
|
1170
|
+
async function applyDiscovery(found, started, left) {
|
|
1194
1171
|
const lockedBefore = lockedRooms();
|
|
1195
1172
|
for (const room of found) {
|
|
1196
1173
|
const fence = fences.get(room.locator);
|
|
@@ -1204,18 +1181,11 @@ export function composeRuntime(options) {
|
|
|
1204
1181
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
1205
1182
|
for (const locator of listedLocators)
|
|
1206
1183
|
scopedRefusals.delete(locator);
|
|
1207
|
-
if (
|
|
1184
|
+
if (left === undefined)
|
|
1208
1185
|
discovered.clear();
|
|
1209
|
-
else
|
|
1210
|
-
for (const locator of covered)
|
|
1211
|
-
discovered.delete(locator);
|
|
1212
1186
|
for (const room of listed)
|
|
1213
1187
|
discovered.set(room.locator, room);
|
|
1214
|
-
|
|
1215
|
-
if (options.adapter.exhaustiveDiscovery === true)
|
|
1216
|
-
await loseRooms(unlisted);
|
|
1217
|
-
else
|
|
1218
|
-
unlistedKept(unlisted);
|
|
1188
|
+
await loseRooms(left ?? unlistedHeld(new Set(found.map((room) => room.locator))));
|
|
1219
1189
|
for (const room of listed) {
|
|
1220
1190
|
const held = replicas.get(room.locator)?.record;
|
|
1221
1191
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -1246,15 +1216,6 @@ export function composeRuntime(options) {
|
|
|
1246
1216
|
]),
|
|
1247
1217
|
].filter((locator) => !listed.has(locator));
|
|
1248
1218
|
}
|
|
1249
|
-
function unlistedKept(unlisted) {
|
|
1250
|
-
for (const locator of unlisted) {
|
|
1251
|
-
const held = replicas.get(locator)?.record;
|
|
1252
|
-
if (held !== undefined)
|
|
1253
|
-
list(held, false);
|
|
1254
|
-
else
|
|
1255
|
-
rooms.delete(locator);
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
1219
|
function fence(locators) {
|
|
1259
1220
|
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
1260
1221
|
const removed = fresh.map((locator) => ({
|
|
@@ -1268,21 +1229,21 @@ export function composeRuntime(options) {
|
|
|
1268
1229
|
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
1269
1230
|
pendings.delete(pendingKey(locator, batch.id));
|
|
1270
1231
|
replicas.delete(locator);
|
|
1232
|
+
highest.delete(locator);
|
|
1233
|
+
beyondHorizon.delete(locator);
|
|
1271
1234
|
queuesWithoutReplica.delete(locator);
|
|
1272
1235
|
rooms.delete(locator);
|
|
1273
1236
|
roomDrafts.delete(locator);
|
|
1274
1237
|
attachedFiles.delete(locator);
|
|
1275
1238
|
untransferred(locator);
|
|
1239
|
+
startedOver.delete(locator);
|
|
1276
1240
|
catching.delete(locator);
|
|
1277
1241
|
discovered.delete(locator);
|
|
1278
|
-
announced.delete(locator);
|
|
1279
1242
|
unreadable.delete(locator);
|
|
1280
1243
|
stale.delete(locator);
|
|
1281
1244
|
repairable.delete(locator);
|
|
1282
1245
|
unpreparable.delete(locator);
|
|
1283
1246
|
scopedRefusals.delete(locator);
|
|
1284
|
-
sourceRefusals.delete(locator);
|
|
1285
|
-
declarationRefusals.delete(locator);
|
|
1286
1247
|
models.delete(locator);
|
|
1287
1248
|
liveModels.delete(locator);
|
|
1288
1249
|
invalidModels.delete(locator);
|
|
@@ -1373,6 +1334,7 @@ export function composeRuntime(options) {
|
|
|
1373
1334
|
transfers.clear();
|
|
1374
1335
|
arrivals.clear();
|
|
1375
1336
|
streaming.clear();
|
|
1337
|
+
startedOver.clear();
|
|
1376
1338
|
limits = held.providerLimits;
|
|
1377
1339
|
capabilities = held.capabilities;
|
|
1378
1340
|
member = held.member;
|
|
@@ -1382,7 +1344,7 @@ export function composeRuntime(options) {
|
|
|
1382
1344
|
presentations.clear();
|
|
1383
1345
|
for (const record of held.presentation)
|
|
1384
1346
|
presentations.set(record.app, record.value);
|
|
1385
|
-
conversations.hydrate(heldConversations, chatDrafts, held.conversationList);
|
|
1347
|
+
conversations.hydrate(heldConversations, chatDrafts, held.conversationList, held.conversationCursor);
|
|
1386
1348
|
pendings.clear();
|
|
1387
1349
|
for (const queue of queues) {
|
|
1388
1350
|
for (const { batch, state } of queue.pending) {
|
|
@@ -1534,6 +1496,16 @@ export function composeRuntime(options) {
|
|
|
1534
1496
|
}
|
|
1535
1497
|
const REWRITE_LINKS = ['field', 'body'];
|
|
1536
1498
|
const CHECKED_LINKS = ['field', 'section'];
|
|
1499
|
+
const BODY_LINKS = ['body'];
|
|
1500
|
+
async function textsNeeded(local, record, named) {
|
|
1501
|
+
const paths = [...new Set(named.map((one) => (typeof one === 'string' ? one : one.path)))];
|
|
1502
|
+
const unheld = await unheldIn(local, record.locator, record.key, paths);
|
|
1503
|
+
if (unheld.length === 0)
|
|
1504
|
+
return null;
|
|
1505
|
+
await holdTexts(unheld);
|
|
1506
|
+
const [first] = await unheldIn(local, record.locator, record.key, unheld.map(({ path }) => path));
|
|
1507
|
+
return first?.path ?? null;
|
|
1508
|
+
}
|
|
1537
1509
|
async function sliceAt(record, view, queue, extra = [], moved = []) {
|
|
1538
1510
|
const local = await openLocal();
|
|
1539
1511
|
const needed = new Set([...touchedBy(queue), ...extra]);
|
|
@@ -1634,8 +1606,6 @@ export function composeRuntime(options) {
|
|
|
1634
1606
|
function unpairedIn(locator, batch) {
|
|
1635
1607
|
if (!queueIn(locator).some((held) => held.id === batch.id))
|
|
1636
1608
|
return false;
|
|
1637
|
-
if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
|
|
1638
|
-
return false;
|
|
1639
1609
|
return !leavingIn(batch, locator) || withdrawable(locator)(batch);
|
|
1640
1610
|
}
|
|
1641
1611
|
async function together(local, settlements) {
|
|
@@ -1804,28 +1774,34 @@ export function composeRuntime(options) {
|
|
|
1804
1774
|
models.set(locator, state);
|
|
1805
1775
|
return { collection: state };
|
|
1806
1776
|
}
|
|
1807
|
-
function publish(local, room,
|
|
1777
|
+
function publish(local, room, reached, landing, live) {
|
|
1808
1778
|
const landed = exclusive(async () => {
|
|
1809
1779
|
if (!live()) {
|
|
1810
1780
|
caughtUpTo(room.locator);
|
|
1811
1781
|
return true;
|
|
1812
1782
|
}
|
|
1813
1783
|
const prior = replicas.get(room.locator);
|
|
1814
|
-
if (landing.
|
|
1815
|
-
(
|
|
1816
|
-
|
|
1784
|
+
if (!stands(room.locator, landing.from)) {
|
|
1785
|
+
if (landing.kind === 'refreshed') {
|
|
1786
|
+
await local.unrefresh(reached.key);
|
|
1787
|
+
return false;
|
|
1788
|
+
}
|
|
1789
|
+
caughtUpTo(room.locator);
|
|
1790
|
+
await abandoned(local, room.locator);
|
|
1817
1791
|
return false;
|
|
1818
1792
|
}
|
|
1793
|
+
const record = landing.kind === 'refreshed' && prior !== undefined
|
|
1794
|
+
? {
|
|
1795
|
+
...prior.record,
|
|
1796
|
+
roomVersion: reached.roomVersion,
|
|
1797
|
+
...(landing.remodelled ? { model: reached.model } : {}),
|
|
1798
|
+
}
|
|
1799
|
+
: reached;
|
|
1819
1800
|
adoptModels(room.locator, record);
|
|
1820
1801
|
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
1821
|
-
const roomVersion = record.roomVersion;
|
|
1822
|
-
const accepted = queue.filter((batch) => {
|
|
1823
|
-
const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
|
|
1824
|
-
return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
|
|
1825
|
-
});
|
|
1826
1802
|
let set;
|
|
1827
1803
|
try {
|
|
1828
|
-
set = await settle(local, room.locator, prior, record, queue
|
|
1804
|
+
set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
|
|
1829
1805
|
? {
|
|
1830
1806
|
room: true,
|
|
1831
1807
|
view: 'refreshed',
|
|
@@ -1847,24 +1823,26 @@ export function composeRuntime(options) {
|
|
|
1847
1823
|
return true;
|
|
1848
1824
|
}
|
|
1849
1825
|
if (landing.kind === 'bootstrapped') {
|
|
1826
|
+
highest.delete(room.locator);
|
|
1827
|
+
beyondHorizon.delete(room.locator);
|
|
1850
1828
|
untransferred(room.locator);
|
|
1829
|
+
startedOver.delete(room.locator);
|
|
1851
1830
|
catching.delete(room.locator);
|
|
1852
|
-
|
|
1831
|
+
if (prior !== undefined && prior.record.key !== record.key)
|
|
1832
|
+
sweep();
|
|
1833
|
+
name(room.locator);
|
|
1853
1834
|
}
|
|
1854
|
-
for (const batch of accepted)
|
|
1855
|
-
pendings.delete(pendingKey(room.locator, batch.id));
|
|
1856
1835
|
stale.delete(room.locator);
|
|
1857
1836
|
unpreparable.delete(room.locator);
|
|
1858
1837
|
if (unreadable.delete(room.locator))
|
|
1859
1838
|
scopedRefusals.delete(room.locator);
|
|
1860
|
-
sourceRefusals.delete(room.locator);
|
|
1861
|
-
declarationRefusals.delete(room.locator);
|
|
1862
1839
|
list(room, true);
|
|
1863
1840
|
publishChangeSet(set);
|
|
1841
|
+
if (landing.kind === 'bootstrapped')
|
|
1842
|
+
void heldAgain(room.locator);
|
|
1864
1843
|
await unpair(local);
|
|
1865
1844
|
moveTo({ ...status, ...attention() });
|
|
1866
|
-
|
|
1867
|
-
if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1845
|
+
if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1868
1846
|
submitLater(room.locator);
|
|
1869
1847
|
}
|
|
1870
1848
|
return true;
|
|
@@ -1876,7 +1854,7 @@ export function composeRuntime(options) {
|
|
|
1876
1854
|
const replica = replicas.get(locator);
|
|
1877
1855
|
const room = rooms.get(locator);
|
|
1878
1856
|
if (replica === undefined || room === undefined) {
|
|
1879
|
-
throw runtimeError('room
|
|
1857
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
1880
1858
|
}
|
|
1881
1859
|
return { local, replica, room };
|
|
1882
1860
|
}
|
|
@@ -1938,6 +1916,19 @@ export function composeRuntime(options) {
|
|
|
1938
1916
|
const referring = removed.length === 0
|
|
1939
1917
|
? []
|
|
1940
1918
|
: (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
|
|
1919
|
+
const unheld = await textsNeeded(local, record, [
|
|
1920
|
+
...paths,
|
|
1921
|
+
...referring,
|
|
1922
|
+
...(moved.length === 0
|
|
1923
|
+
? []
|
|
1924
|
+
: await local.referrers(record.key, 'confirmed', moved, REWRITE_LINKS)),
|
|
1925
|
+
...(removed.length === 0
|
|
1926
|
+
? []
|
|
1927
|
+
: await local.referrers(record.key, 'confirmed', removed, BODY_LINKS)),
|
|
1928
|
+
]);
|
|
1929
|
+
if (unheld !== null) {
|
|
1930
|
+
return { refusal: { code: 'transport-uncertainty', retryable: true, file: unheld } };
|
|
1931
|
+
}
|
|
1941
1932
|
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
|
|
1942
1933
|
const { visible } = await replayed(record, confirmedLayer, replica.queue);
|
|
1943
1934
|
const composition = composed(record);
|
|
@@ -1968,18 +1959,6 @@ export function composeRuntime(options) {
|
|
|
1968
1959
|
return undefined;
|
|
1969
1960
|
return entry.binary !== null ? null : entry.content;
|
|
1970
1961
|
}
|
|
1971
|
-
function walkOf(local, key, view) {
|
|
1972
|
-
return async (after) => {
|
|
1973
|
-
const page = await local.walk(key, view, after, WALK_BYTES);
|
|
1974
|
-
return {
|
|
1975
|
-
entries: page.entries.map(({ path, content, binary }) => ({
|
|
1976
|
-
path,
|
|
1977
|
-
content: binary !== null ? null : content,
|
|
1978
|
-
})),
|
|
1979
|
-
next: page.next,
|
|
1980
|
-
};
|
|
1981
|
-
};
|
|
1982
|
-
}
|
|
1983
1962
|
function lookupOf(local, key, view) {
|
|
1984
1963
|
return async (paths) => {
|
|
1985
1964
|
if (paths.length === 0)
|
|
@@ -1996,37 +1975,6 @@ export function composeRuntime(options) {
|
|
|
1996
1975
|
const read = parseFrontmatter(entry.content ?? '');
|
|
1997
1976
|
return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
|
|
1998
1977
|
}
|
|
1999
|
-
function overriddenWalk(walk, overrides) {
|
|
2000
|
-
const sorted = [...overrides.keys()].sort();
|
|
2001
|
-
const beyond = (path) => {
|
|
2002
|
-
if (path === null)
|
|
2003
|
-
return 0;
|
|
2004
|
-
let low = 0;
|
|
2005
|
-
let high = sorted.length;
|
|
2006
|
-
while (low < high) {
|
|
2007
|
-
const middle = (low + high) >>> 1;
|
|
2008
|
-
if (sorted[middle] <= path)
|
|
2009
|
-
low = middle + 1;
|
|
2010
|
-
else
|
|
2011
|
-
high = middle;
|
|
2012
|
-
}
|
|
2013
|
-
return low;
|
|
2014
|
-
};
|
|
2015
|
-
return async (after) => {
|
|
2016
|
-
const page = await walk(after);
|
|
2017
|
-
const end = page.next;
|
|
2018
|
-
const added = sorted
|
|
2019
|
-
.slice(beyond(after), end === null ? sorted.length : beyond(end))
|
|
2020
|
-
.flatMap((path) => {
|
|
2021
|
-
const entry = overrides.get(path);
|
|
2022
|
-
return entry === null || entry === undefined
|
|
2023
|
-
? []
|
|
2024
|
-
: [{ path, content: entry.binary !== null ? null : entry.content }];
|
|
2025
|
-
});
|
|
2026
|
-
const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
|
|
2027
|
-
return { entries, next: end };
|
|
2028
|
-
};
|
|
2029
|
-
}
|
|
2030
1978
|
function overriddenLookup(lookup, overrides) {
|
|
2031
1979
|
return async (paths) => {
|
|
2032
1980
|
const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
|
|
@@ -2044,9 +1992,8 @@ export function composeRuntime(options) {
|
|
|
2044
1992
|
}
|
|
2045
1993
|
function validatorOf(record, confirmedLayer) {
|
|
2046
1994
|
return async (resulting, touched) => {
|
|
2047
|
-
const composition = compositionFor(record);
|
|
2048
1995
|
const local = await openLocal();
|
|
2049
|
-
const
|
|
1996
|
+
const changes = [];
|
|
2050
1997
|
const overrides = new Map();
|
|
2051
1998
|
const pace = paced();
|
|
2052
1999
|
for (const path of new Set([...confirmedLayer.held, ...touched])) {
|
|
@@ -2057,19 +2004,13 @@ export function composeRuntime(options) {
|
|
|
2057
2004
|
continue;
|
|
2058
2005
|
overrides.set(path, now);
|
|
2059
2006
|
if (textOf(was) !== textOf(now))
|
|
2060
|
-
|
|
2007
|
+
changes.push({ path, was: textOf(was), now: textOf(now) });
|
|
2061
2008
|
}
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
texts: textsOf(local, record.key, 'confirmed'),
|
|
2068
|
-
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2069
|
-
}));
|
|
2070
|
-
if (clean)
|
|
2071
|
-
return { verdict: 'accept' };
|
|
2072
|
-
return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
|
|
2009
|
+
return validatedNeighbourhood(changes, compositionFor(record), {
|
|
2010
|
+
lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
|
|
2011
|
+
texts: textsOf(local, record.key, 'confirmed'),
|
|
2012
|
+
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2013
|
+
});
|
|
2073
2014
|
};
|
|
2074
2015
|
}
|
|
2075
2016
|
function textsOf(local, key, view) {
|
|
@@ -2146,15 +2087,19 @@ export function composeRuntime(options) {
|
|
|
2146
2087
|
}
|
|
2147
2088
|
const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
|
|
2148
2089
|
const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
|
|
2090
|
+
if ('refusal' in sourceSaving)
|
|
2091
|
+
return { outcome: 'refused', ...sourceSaving.refusal };
|
|
2149
2092
|
const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
|
|
2150
2093
|
if (changed.length > 0)
|
|
2151
2094
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2152
2095
|
const target = replicas.get(destination);
|
|
2153
2096
|
const listedRoom = rooms.get(destination);
|
|
2154
2097
|
if (target === undefined || listedRoom === undefined) {
|
|
2155
|
-
throw runtimeError('room
|
|
2098
|
+
throw runtimeError('unknown-room', `Unknown Room ${destination}`);
|
|
2156
2099
|
}
|
|
2157
2100
|
const arriving = await savingIn(target, [], moves.map(({ to }) => to));
|
|
2101
|
+
if ('refusal' in arriving)
|
|
2102
|
+
return { outcome: 'refused', ...arriving.refusal };
|
|
2158
2103
|
const built = await buildMove({
|
|
2159
2104
|
source: await sideOf(locator, replica, room, sourceSaving),
|
|
2160
2105
|
destination: await sideOf(destination, target, listedRoom, arriving),
|
|
@@ -2215,10 +2160,10 @@ export function composeRuntime(options) {
|
|
|
2215
2160
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2216
2161
|
const { answer, touched } = committedBoth;
|
|
2217
2162
|
if (answer.fenced !== null) {
|
|
2218
|
-
throw runtimeError('room
|
|
2163
|
+
throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
|
|
2219
2164
|
}
|
|
2220
2165
|
if (fences.has(locator) || fences.has(destination)) {
|
|
2221
|
-
throw runtimeError('room
|
|
2166
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
2222
2167
|
}
|
|
2223
2168
|
pendings.set(pendingKey(locator, id), {
|
|
2224
2169
|
drafts: saved,
|
|
@@ -2271,6 +2216,8 @@ export function composeRuntime(options) {
|
|
|
2271
2216
|
...saved.map(({ path }) => path),
|
|
2272
2217
|
...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
|
|
2273
2218
|
]);
|
|
2219
|
+
if ('refusal' in saving)
|
|
2220
|
+
return { outcome: 'refused', ...saving.refusal };
|
|
2274
2221
|
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
|
|
2275
2222
|
if (changed.length > 0)
|
|
2276
2223
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
@@ -2389,10 +2336,10 @@ export function composeRuntime(options) {
|
|
|
2389
2336
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2390
2337
|
const { answer, touched } = done;
|
|
2391
2338
|
if (answer.fenced !== null) {
|
|
2392
|
-
throw runtimeError('room
|
|
2339
|
+
throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
|
|
2393
2340
|
}
|
|
2394
2341
|
if (fences.has(locator))
|
|
2395
|
-
throw runtimeError('room
|
|
2342
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
2396
2343
|
for (const origin of withdrawn.keys())
|
|
2397
2344
|
pendings.delete(pendingKey(locator, origin));
|
|
2398
2345
|
for (const [id, drafts] of drafted) {
|
|
@@ -2441,7 +2388,7 @@ export function composeRuntime(options) {
|
|
|
2441
2388
|
throw new Error('connection refused');
|
|
2442
2389
|
await local.keepUpload(locator, identifier, upload);
|
|
2443
2390
|
if (fences.has(locator))
|
|
2444
|
-
throw runtimeError('room
|
|
2391
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
2445
2392
|
const key = pendingKey(locator, identifier);
|
|
2446
2393
|
pendings.set(key, { ...pendings.get(key), upload });
|
|
2447
2394
|
if (!continuing())
|
|
@@ -2578,6 +2525,83 @@ export function composeRuntime(options) {
|
|
|
2578
2525
|
return UNCERTAIN;
|
|
2579
2526
|
}
|
|
2580
2527
|
}
|
|
2528
|
+
async function heldAgain(locator) {
|
|
2529
|
+
const replica = replicas.get(locator);
|
|
2530
|
+
if (replica === undefined || !textOnline())
|
|
2531
|
+
return;
|
|
2532
|
+
try {
|
|
2533
|
+
const wanted = await (await openLocal()).wanted(locator, replica.record.key);
|
|
2534
|
+
await holdTexts(wanted.map(({ path, revision }) => ({ locator, path, revision })));
|
|
2535
|
+
}
|
|
2536
|
+
catch {
|
|
2537
|
+
journal.record(`hold text ${locator} unwritten`);
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
function textOnline() {
|
|
2541
|
+
return !closed && refusal === null && connectionOf() === 'connected';
|
|
2542
|
+
}
|
|
2543
|
+
function textKey({ locator, path }) {
|
|
2544
|
+
return documentKey(locator, path);
|
|
2545
|
+
}
|
|
2546
|
+
async function readText({ locator, path, revision }) {
|
|
2547
|
+
const replica = replicas.get(locator);
|
|
2548
|
+
if (replica === undefined || !textOnline())
|
|
2549
|
+
return;
|
|
2550
|
+
try {
|
|
2551
|
+
await ensureAuthorization();
|
|
2552
|
+
}
|
|
2553
|
+
catch {
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
2557
|
+
return;
|
|
2558
|
+
const since = fencings;
|
|
2559
|
+
journal.record(`hold text ${locator} ${path}`);
|
|
2560
|
+
const read = await reads
|
|
2561
|
+
.bootstrap(authorization, { locator, selection: replica.record.replica.selection, path })
|
|
2562
|
+
.catch(() => null);
|
|
2563
|
+
journal.record(`outcome text ${read?.kind ?? 'transport-uncertainty'}`);
|
|
2564
|
+
if (read === null || read.kind !== 'bootstrap' || fencedSince(locator, since))
|
|
2565
|
+
return;
|
|
2566
|
+
const content = read.documents[path];
|
|
2567
|
+
const now = replicas.get(locator);
|
|
2568
|
+
if (content === undefined || read.revisions[path] !== revision || now === undefined) {
|
|
2569
|
+
unholdable.set(textKey({ locator, path, revision }), revision);
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
const held = await (await openLocal()).hold(locator, now.record.key, compositionFor(now.record), [{ path, revision, content }]);
|
|
2573
|
+
if (held.length === 0)
|
|
2574
|
+
unholdable.set(textKey({ locator, path, revision }), revision);
|
|
2575
|
+
else
|
|
2576
|
+
publishChangeSet(changeSet(locator, { documents: held }));
|
|
2577
|
+
}
|
|
2578
|
+
function holdText(unheld) {
|
|
2579
|
+
const key = textKey(unheld);
|
|
2580
|
+
const running = holding.get(key);
|
|
2581
|
+
if (running !== undefined)
|
|
2582
|
+
return running;
|
|
2583
|
+
const started = readText(unheld).finally(() => holding.delete(key));
|
|
2584
|
+
holding.set(key, started);
|
|
2585
|
+
return started;
|
|
2586
|
+
}
|
|
2587
|
+
async function holdTexts(wanted) {
|
|
2588
|
+
const queue = wanted.filter((one) => unholdable.get(textKey(one)) !== one.revision);
|
|
2589
|
+
await Promise.all(Array.from({ length: Math.min(TEXT_READS, queue.length) }, async () => {
|
|
2590
|
+
for (let next = queue.shift(); next !== undefined; next = queue.shift()) {
|
|
2591
|
+
await holdText(next);
|
|
2592
|
+
}
|
|
2593
|
+
}));
|
|
2594
|
+
}
|
|
2595
|
+
async function unheldIn(local, locator, key, paths) {
|
|
2596
|
+
if (paths.length === 0)
|
|
2597
|
+
return [];
|
|
2598
|
+
return (await local.entries(key, 'confirmed', paths)).flatMap((entry) => entry.content === null &&
|
|
2599
|
+
entry.binary === null &&
|
|
2600
|
+
entry.outline !== null &&
|
|
2601
|
+
entry.revision !== null
|
|
2602
|
+
? [{ locator, path: entry.path, revision: entry.revision }]
|
|
2603
|
+
: []);
|
|
2604
|
+
}
|
|
2581
2605
|
function fetchBinary(request, name) {
|
|
2582
2606
|
return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
|
|
2583
2607
|
}
|
|
@@ -2713,14 +2737,16 @@ export function composeRuntime(options) {
|
|
|
2713
2737
|
}
|
|
2714
2738
|
async function cleanUp() {
|
|
2715
2739
|
const freed = await database.ask({ kind: 'opened.clean' });
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2740
|
+
const local = await openLocal();
|
|
2741
|
+
let texts = 0;
|
|
2742
|
+
for (const [locator, replica] of replicas) {
|
|
2743
|
+
const released = await local.unhold(locator, replica.record.key, compositionFor(replica.record));
|
|
2744
|
+
texts += released.bytes;
|
|
2745
|
+
if (released.paths.length > 0) {
|
|
2746
|
+
publishChangeSet(changeSet(locator, { documents: released.paths }));
|
|
2747
|
+
}
|
|
2722
2748
|
}
|
|
2723
|
-
return
|
|
2749
|
+
return freed + texts + ((await options.shell.cleanUp?.()) ?? 0);
|
|
2724
2750
|
}
|
|
2725
2751
|
function declaredAction(name) {
|
|
2726
2752
|
for (const capability of capabilities) {
|
|
@@ -2773,6 +2799,24 @@ export function composeRuntime(options) {
|
|
|
2773
2799
|
return UNCERTAIN;
|
|
2774
2800
|
}
|
|
2775
2801
|
}
|
|
2802
|
+
async function readConversationHistory(request) {
|
|
2803
|
+
await ready;
|
|
2804
|
+
const stream = options.adapter.conversations;
|
|
2805
|
+
if (closed || refusal !== null || stream?.history === undefined)
|
|
2806
|
+
return UNCERTAIN;
|
|
2807
|
+
journal.record('transfer conversation history');
|
|
2808
|
+
try {
|
|
2809
|
+
const result = await stream.history(request);
|
|
2810
|
+
if (result.kind === 'revoked-authorization')
|
|
2811
|
+
await refuse('revoked-authorization');
|
|
2812
|
+
journal.record(`outcome conversation history ${result.kind}`);
|
|
2813
|
+
return calledResult(result);
|
|
2814
|
+
}
|
|
2815
|
+
catch {
|
|
2816
|
+
journal.record('outcome conversation history transport-uncertainty');
|
|
2817
|
+
return UNCERTAIN;
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2776
2820
|
function unsettled(result) {
|
|
2777
2821
|
return result.kind !== 'action' && result.retryable;
|
|
2778
2822
|
}
|
|
@@ -2834,6 +2878,33 @@ export function composeRuntime(options) {
|
|
|
2834
2878
|
const held = replicas.get(locator);
|
|
2835
2879
|
return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
|
|
2836
2880
|
}
|
|
2881
|
+
function shownAt(locator) {
|
|
2882
|
+
const held = replicas.get(locator)?.record.roomVersion;
|
|
2883
|
+
const raised = highest.get(locator);
|
|
2884
|
+
if (held === undefined || raised === undefined)
|
|
2885
|
+
return held ?? raised;
|
|
2886
|
+
return options.adapter.reached(locator, held, raised) ? held : raised;
|
|
2887
|
+
}
|
|
2888
|
+
function showing(locator, roomVersion) {
|
|
2889
|
+
const at = shownAt(locator);
|
|
2890
|
+
if (at === undefined || !options.adapter.reached(locator, at, roomVersion)) {
|
|
2891
|
+
highest.set(locator, roomVersion);
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
function older(locator, roomVersion) {
|
|
2895
|
+
if (beyondHorizon.has(locator))
|
|
2896
|
+
return false;
|
|
2897
|
+
const at = shownAt(locator);
|
|
2898
|
+
return (at !== undefined &&
|
|
2899
|
+
options.adapter.reached(locator, at, roomVersion) &&
|
|
2900
|
+
!options.adapter.reached(locator, roomVersion, at));
|
|
2901
|
+
}
|
|
2902
|
+
function replaced(locators) {
|
|
2903
|
+
for (const locator of locators) {
|
|
2904
|
+
highest.delete(locator);
|
|
2905
|
+
beyondHorizon.add(locator);
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2837
2908
|
async function writtenAfter(locator, roomVersion, path) {
|
|
2838
2909
|
const held = replicas.get(locator);
|
|
2839
2910
|
if (held === undefined)
|
|
@@ -2844,122 +2915,27 @@ export function composeRuntime(options) {
|
|
|
2844
2915
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
2845
2916
|
!options.adapter.reached(locator, roomVersion, written));
|
|
2846
2917
|
}
|
|
2847
|
-
function
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
else
|
|
2857
|
-
known.set(path, entry.binary !== null ? null : entry.content);
|
|
2858
|
-
}
|
|
2859
|
-
return known;
|
|
2860
|
-
};
|
|
2861
|
-
}
|
|
2862
|
-
async function unreadableRoom(texts, walk, paths) {
|
|
2863
|
-
if (paths !== null) {
|
|
2864
|
-
const text = {};
|
|
2865
|
-
for (const [path, content] of await texts(paths))
|
|
2866
|
-
if (content !== null)
|
|
2867
|
-
text[path] = content;
|
|
2868
|
-
return stepped(unreadableIn(text, Object.keys(text)));
|
|
2869
|
-
}
|
|
2870
|
-
for (let after = null;;) {
|
|
2871
|
-
const page = await walk(after);
|
|
2872
|
-
const text = {};
|
|
2873
|
-
for (const { path, content } of page.entries)
|
|
2874
|
-
if (content !== null)
|
|
2875
|
-
text[path] = content;
|
|
2876
|
-
const refused = await stepped(unreadableIn(text, Object.keys(text)));
|
|
2877
|
-
if (refused !== null)
|
|
2878
|
-
return refused;
|
|
2879
|
-
if (page.next === null)
|
|
2880
|
-
return null;
|
|
2881
|
-
after = page.next;
|
|
2882
|
-
}
|
|
2883
|
-
}
|
|
2884
|
-
async function validatedRoom(local, record, view, previous, overrides = null, checked = null) {
|
|
2885
|
-
const composition = composed(record);
|
|
2886
|
-
const modeled = record.model.status !== 'unavailable' || record.typedSelection !== null;
|
|
2887
|
-
const lookup = overrides === null
|
|
2888
|
-
? lookupOf(local, record.key, view)
|
|
2889
|
-
: overriddenLookup(lookupOf(local, record.key, view), overrides);
|
|
2890
|
-
const texts = overrides === null
|
|
2891
|
-
? textsOf(local, record.key, view)
|
|
2892
|
-
: overriddenTexts(textsOf(local, record.key, view), overrides);
|
|
2893
|
-
const walking = overrides === null
|
|
2894
|
-
? walkOf(local, record.key, view)
|
|
2895
|
-
: overriddenWalk(walkOf(local, record.key, view), overrides);
|
|
2896
|
-
const walk = checked === null
|
|
2897
|
-
? walking
|
|
2898
|
-
: async (after) => {
|
|
2899
|
-
const page = await walking(after);
|
|
2900
|
-
checked(page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0));
|
|
2901
|
-
return page;
|
|
2902
|
-
};
|
|
2903
|
-
let changes = null;
|
|
2904
|
-
let reshaped = false;
|
|
2905
|
-
if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
|
|
2906
|
-
const [was, now] = await Promise.all([
|
|
2907
|
-
local.entries(record.key, 'confirmed', previous.changed),
|
|
2908
|
-
overrides === null
|
|
2909
|
-
? local.entries(record.key, view, previous.changed)
|
|
2910
|
-
: Promise.resolve([]),
|
|
2911
|
-
]);
|
|
2912
|
-
const before = new Map(was.map((entry) => [entry.path, entry]));
|
|
2913
|
-
const after = overrides === null
|
|
2914
|
-
? new Map(now.map((entry) => [entry.path, entry]))
|
|
2915
|
-
: overrides;
|
|
2916
|
-
changes = previous.changed.flatMap((path) => {
|
|
2917
|
-
const one = before.get(path) ?? null;
|
|
2918
|
-
const other = after.get(path) ?? null;
|
|
2919
|
-
reshaped ||= textDiffers(one, other);
|
|
2920
|
-
const held = textOf(one);
|
|
2921
|
-
const next = textOf(other);
|
|
2922
|
-
return held === next ? [] : [{ path, was: held, now: next }];
|
|
2923
|
-
});
|
|
2924
|
-
}
|
|
2925
|
-
const named = changes === null ? null : changes.map(({ path }) => path);
|
|
2926
|
-
const textChanged = changes === null || changes.length > 0 || reshaped;
|
|
2927
|
-
const modelsChanged = previous === null || !sameComposition(composed(previous.record), composition);
|
|
2928
|
-
if (modeled && (textChanged || modelsChanged)) {
|
|
2929
|
-
const clean = previous !== null &&
|
|
2930
|
-
changes !== null &&
|
|
2931
|
-
(await changedCleanly(changes, composed(previous.record), composition, {
|
|
2932
|
-
lookup,
|
|
2933
|
-
texts,
|
|
2934
|
-
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2935
|
-
}));
|
|
2936
|
-
if (clean)
|
|
2937
|
-
return null;
|
|
2938
|
-
const verdict = await validatedCollection(walk, lookup, composition);
|
|
2939
|
-
if (verdict.verdict === 'accept')
|
|
2940
|
-
return null;
|
|
2941
|
-
if (verdict.rule !== 'model-unavailable')
|
|
2942
|
-
return { kind: 'refused', refusal: verdict };
|
|
2943
|
-
return unreadableRoom(texts, walk, named);
|
|
2944
|
-
}
|
|
2945
|
-
if (textChanged)
|
|
2946
|
-
return unreadableRoom(texts, walk, named);
|
|
2947
|
-
return null;
|
|
2918
|
+
async function writtenLater(local, locator, key, paths, roomVersion) {
|
|
2919
|
+
if (paths.length === 0 || !older(locator, roomVersion))
|
|
2920
|
+
return new Set();
|
|
2921
|
+
const entries = await local.entries(key, 'confirmed', paths);
|
|
2922
|
+
return new Set(entries.flatMap(({ path, written }) => written !== null &&
|
|
2923
|
+
options.adapter.reached(locator, written, roomVersion) &&
|
|
2924
|
+
!options.adapter.reached(locator, roomVersion, written)
|
|
2925
|
+
? [path]
|
|
2926
|
+
: []));
|
|
2948
2927
|
}
|
|
2949
|
-
function refreshingOf(local, key, composition, liveness, replaying =
|
|
2950
|
-
const write = replaying
|
|
2928
|
+
function refreshingOf(local, locator, key, composition, liveness, replaying = false) {
|
|
2929
|
+
const write = replaying ? finishingWrite : exclusive;
|
|
2951
2930
|
return {
|
|
2952
|
-
|
|
2953
|
-
if (replaying
|
|
2954
|
-
|
|
2955
|
-
installing(replaying);
|
|
2956
|
-
await screened();
|
|
2931
|
+
turn() {
|
|
2932
|
+
if (replaying)
|
|
2933
|
+
installing(locator);
|
|
2957
2934
|
},
|
|
2958
2935
|
prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
|
|
2936
|
+
later: (paths, roomVersion) => writtenLater(local, locator, key, paths, roomVersion),
|
|
2959
2937
|
async stage(page) {
|
|
2960
2938
|
for (let written = 0;;) {
|
|
2961
|
-
if (replaying !== null)
|
|
2962
|
-
await screened();
|
|
2963
2939
|
const end = batchEnd(page.entries, written);
|
|
2964
2940
|
const last = end === page.entries.length;
|
|
2965
2941
|
const entries = page.entries.slice(written, end);
|
|
@@ -3004,32 +2980,71 @@ export function composeRuntime(options) {
|
|
|
3004
2980
|
return done;
|
|
3005
2981
|
}
|
|
3006
2982
|
function installWrite(work) {
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
2983
|
+
return installTurn(installTurns.starting, work);
|
|
2984
|
+
}
|
|
2985
|
+
function finishingWrite(work) {
|
|
2986
|
+
return installTurn(installTurns.finishing, work);
|
|
2987
|
+
}
|
|
2988
|
+
function installTurn(queue, work) {
|
|
2989
|
+
return new Promise((resolve, reject) => {
|
|
2990
|
+
queue.push(() => void exclusive(work)
|
|
2991
|
+
.then(resolve, reject)
|
|
2992
|
+
.finally(() => {
|
|
2993
|
+
installTurns.busy = false;
|
|
2994
|
+
nextInstallTurn();
|
|
2995
|
+
}));
|
|
2996
|
+
nextInstallTurn();
|
|
2997
|
+
});
|
|
3010
2998
|
}
|
|
3011
|
-
function
|
|
3012
|
-
if (
|
|
3013
|
-
return
|
|
3014
|
-
|
|
3015
|
-
|
|
2999
|
+
function nextInstallTurn() {
|
|
3000
|
+
if (installTurns.busy || installTurns.ahead > 0)
|
|
3001
|
+
return;
|
|
3002
|
+
const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
|
|
3003
|
+
if (turn === undefined)
|
|
3004
|
+
return;
|
|
3005
|
+
installTurns.busy = true;
|
|
3006
|
+
turn();
|
|
3007
|
+
}
|
|
3008
|
+
async function ahead(work) {
|
|
3009
|
+
let taken = false;
|
|
3010
|
+
const hold = {
|
|
3011
|
+
take() {
|
|
3012
|
+
if (taken)
|
|
3013
|
+
return;
|
|
3014
|
+
taken = true;
|
|
3015
|
+
installTurns.ahead += 1;
|
|
3016
|
+
},
|
|
3017
|
+
release() {
|
|
3018
|
+
if (!taken)
|
|
3019
|
+
return;
|
|
3020
|
+
taken = false;
|
|
3021
|
+
installTurns.ahead -= 1;
|
|
3022
|
+
resumeInstall();
|
|
3023
|
+
},
|
|
3024
|
+
};
|
|
3025
|
+
hold.take();
|
|
3026
|
+
try {
|
|
3027
|
+
return await work(hold);
|
|
3028
|
+
}
|
|
3029
|
+
finally {
|
|
3030
|
+
hold.release();
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
function resumeInstall() {
|
|
3034
|
+
if (installTurns.ahead > 0)
|
|
3035
|
+
return;
|
|
3036
|
+
void yielded().then(nextInstallTurn);
|
|
3016
3037
|
}
|
|
3017
3038
|
async function heldStream() {
|
|
3018
|
-
|
|
3019
|
-
if (streams.generation !== generation)
|
|
3020
|
-
streams = { generation, free: STREAMS_HELD };
|
|
3021
|
-
const held = streams;
|
|
3022
|
-
if (held.free > 0) {
|
|
3023
|
-
held.free -= 1;
|
|
3024
|
-
return once(() => {
|
|
3025
|
-
held.free += 1;
|
|
3026
|
-
});
|
|
3027
|
-
}
|
|
3039
|
+
while (streamsFree === 0)
|
|
3028
3040
|
await new Promise((admit) => installWaiters.push(admit));
|
|
3029
|
-
|
|
3041
|
+
streamsFree -= 1;
|
|
3042
|
+
return once(() => {
|
|
3043
|
+
streamsFree += 1;
|
|
3044
|
+
});
|
|
3030
3045
|
}
|
|
3031
3046
|
async function unqueued() {
|
|
3032
|
-
while (
|
|
3047
|
+
while (queuedText >= QUEUED_TEXT) {
|
|
3033
3048
|
await new Promise((admit) => installWaiters.push(admit));
|
|
3034
3049
|
}
|
|
3035
3050
|
}
|
|
@@ -3054,14 +3069,19 @@ export function composeRuntime(options) {
|
|
|
3054
3069
|
streaming.delete(locator);
|
|
3055
3070
|
underived.delete(locator);
|
|
3056
3071
|
unindexed.delete(locator);
|
|
3057
|
-
|
|
3072
|
+
}
|
|
3073
|
+
function setBack(locator) {
|
|
3074
|
+
const now = bytesOf(locator, true);
|
|
3075
|
+
const earlier = startedOver.get(locator);
|
|
3076
|
+
startedOver.set(locator, {
|
|
3077
|
+
done: Math.max(now.done, earlier?.done ?? 0),
|
|
3078
|
+
total: Math.max(now.total, earlier?.total ?? 0),
|
|
3079
|
+
});
|
|
3058
3080
|
}
|
|
3059
3081
|
function owedIndexes(locator, bytes, text) {
|
|
3060
3082
|
if (bytes <= 0 || text <= 0)
|
|
3061
3083
|
return;
|
|
3062
|
-
unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes
|
|
3063
|
-
const check = unchecked.get(locator) ?? { bytes: 0, text: 0, done: 0 };
|
|
3064
|
-
unchecked.set(locator, { ...check, bytes: check.bytes + bytes / 2, text: check.text + text });
|
|
3084
|
+
unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes, text }]);
|
|
3065
3085
|
}
|
|
3066
3086
|
function built(locator, { done, text }) {
|
|
3067
3087
|
const owed = unindexed.get(locator) ?? [];
|
|
@@ -3087,41 +3107,34 @@ export function composeRuntime(options) {
|
|
|
3087
3107
|
if (installs.delete(locator))
|
|
3088
3108
|
queueReadiness();
|
|
3089
3109
|
}
|
|
3090
|
-
function
|
|
3091
|
-
if (value === onScreen)
|
|
3092
|
-
return;
|
|
3093
|
-
onScreen = value;
|
|
3094
|
-
journal.record(`installation ${value ? 'shown' : 'hidden'}`);
|
|
3095
|
-
queueReadiness();
|
|
3096
|
-
if (!value)
|
|
3097
|
-
return;
|
|
3098
|
-
for (const resolve of shownWaiters.splice(0))
|
|
3099
|
-
resolve();
|
|
3100
|
-
}
|
|
3101
|
-
async function pullRefresh(authorized, held, liveness) {
|
|
3110
|
+
async function pullRefresh(authorized, held, liveness, hold, refresh) {
|
|
3102
3111
|
if (!liveness())
|
|
3103
3112
|
return unconfirmed;
|
|
3104
|
-
const turn = generation;
|
|
3105
3113
|
const { record } = held;
|
|
3106
|
-
const
|
|
3114
|
+
const from = standingOf(record.locator);
|
|
3107
3115
|
const local = await openLocal();
|
|
3108
3116
|
try {
|
|
3109
3117
|
await exclusive(() => local.unrefresh(record.key));
|
|
3110
|
-
const result = await advancement(
|
|
3118
|
+
const result = await advancement(async (authorization, request) => {
|
|
3119
|
+
hold.release();
|
|
3120
|
+
try {
|
|
3121
|
+
return await refresh(authorization, request);
|
|
3122
|
+
}
|
|
3123
|
+
finally {
|
|
3124
|
+
hold.take();
|
|
3125
|
+
}
|
|
3126
|
+
}, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.locator, record.key, composed(record), liveness));
|
|
3111
3127
|
if (result.kind !== 'advanced') {
|
|
3112
3128
|
journal.record(`transfer refresh ${record.locator} ${result.kind}`);
|
|
3113
3129
|
return result;
|
|
3114
3130
|
}
|
|
3115
|
-
const refused = await validatedRoom(local, result.record, 'refreshed', {
|
|
3116
|
-
record,
|
|
3117
|
-
changed: [...result.changed],
|
|
3118
|
-
});
|
|
3119
|
-
if (refused !== null) {
|
|
3120
|
-
journal.record(`transfer refresh ${record.locator} refused`);
|
|
3121
|
-
return refused;
|
|
3122
|
-
}
|
|
3123
3131
|
journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
|
|
3124
|
-
return {
|
|
3132
|
+
return {
|
|
3133
|
+
kind: 'advanced',
|
|
3134
|
+
record: result.record,
|
|
3135
|
+
from,
|
|
3136
|
+
remodelled: result.record.model !== record.model,
|
|
3137
|
+
};
|
|
3125
3138
|
}
|
|
3126
3139
|
catch (error) {
|
|
3127
3140
|
if (refusedForSpace(error))
|
|
@@ -3129,9 +3142,6 @@ export function composeRuntime(options) {
|
|
|
3129
3142
|
journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
|
|
3130
3143
|
throw error;
|
|
3131
3144
|
}
|
|
3132
|
-
finally {
|
|
3133
|
-
reads.finished(record.locator, turn);
|
|
3134
|
-
}
|
|
3135
3145
|
}
|
|
3136
3146
|
const heldContents = new Map();
|
|
3137
3147
|
function heldContentOf(local, key) {
|
|
@@ -3149,14 +3159,20 @@ export function composeRuntime(options) {
|
|
|
3149
3159
|
heldContents.set(key, made);
|
|
3150
3160
|
return made;
|
|
3151
3161
|
}
|
|
3162
|
+
async function abandoned(local, locator) {
|
|
3163
|
+
await local.abandon(locator);
|
|
3164
|
+
setBack(locator);
|
|
3165
|
+
untransferred(locator);
|
|
3166
|
+
liveQueries.recover();
|
|
3167
|
+
sweep();
|
|
3168
|
+
}
|
|
3152
3169
|
function staging(local, liveness) {
|
|
3153
3170
|
const derived = async (locator, key, composition) => {
|
|
3154
3171
|
underived.delete(locator);
|
|
3155
3172
|
for (;;) {
|
|
3156
|
-
await screened();
|
|
3157
3173
|
if (!liveness())
|
|
3158
3174
|
return false;
|
|
3159
|
-
const done = await
|
|
3175
|
+
const done = await finishingWrite(async () => liveness()
|
|
3160
3176
|
? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
|
|
3161
3177
|
: null);
|
|
3162
3178
|
if (done === null)
|
|
@@ -3170,19 +3186,13 @@ export function composeRuntime(options) {
|
|
|
3170
3186
|
held: (locator) => transfers.get(locator) ?? null,
|
|
3171
3187
|
async turn(locator, step) {
|
|
3172
3188
|
installing(locator);
|
|
3173
|
-
|
|
3174
|
-
await screened();
|
|
3175
|
-
const release = step === 'downloading' ? await heldStream() : () => { };
|
|
3176
|
-
if (onScreen || closed)
|
|
3177
|
-
return release;
|
|
3178
|
-
release();
|
|
3179
|
-
}
|
|
3189
|
+
return step === 'downloading' ? heldStream() : () => { };
|
|
3180
3190
|
},
|
|
3181
3191
|
unqueued,
|
|
3182
|
-
queued(
|
|
3183
|
-
|
|
3192
|
+
queued(text) {
|
|
3193
|
+
queuedText += text;
|
|
3184
3194
|
return once(() => {
|
|
3185
|
-
|
|
3195
|
+
queuedText -= text;
|
|
3186
3196
|
});
|
|
3187
3197
|
},
|
|
3188
3198
|
arriving(locator) {
|
|
@@ -3191,7 +3201,7 @@ export function composeRuntime(options) {
|
|
|
3191
3201
|
at(bytes) {
|
|
3192
3202
|
if (ended)
|
|
3193
3203
|
return;
|
|
3194
|
-
arrivals.set(locator,
|
|
3204
|
+
arrivals.set(locator, bytes);
|
|
3195
3205
|
streaming.add(locator);
|
|
3196
3206
|
const now = performance.now();
|
|
3197
3207
|
if (now - arrivalReported < ARRIVAL_REPORT_MS)
|
|
@@ -3199,14 +3209,6 @@ export function composeRuntime(options) {
|
|
|
3199
3209
|
arrivalReported = now;
|
|
3200
3210
|
queueReadiness();
|
|
3201
3211
|
},
|
|
3202
|
-
part() {
|
|
3203
|
-
const arrived = arrivals.get(locator);
|
|
3204
|
-
arrivals.set(locator, {
|
|
3205
|
-
parts: (arrived?.parts ?? 0) + 1,
|
|
3206
|
-
bytes: arrived?.bytes ?? 0,
|
|
3207
|
-
});
|
|
3208
|
-
queueReadiness();
|
|
3209
|
-
},
|
|
3210
3212
|
end() {
|
|
3211
3213
|
if (ended)
|
|
3212
3214
|
return;
|
|
@@ -3216,14 +3218,10 @@ export function composeRuntime(options) {
|
|
|
3216
3218
|
},
|
|
3217
3219
|
};
|
|
3218
3220
|
},
|
|
3219
|
-
|
|
3220
|
-
const later = announced.get(locator);
|
|
3221
|
-
return later === undefined ? [] : [later];
|
|
3222
|
-
},
|
|
3221
|
+
streamed: options.adapter.roomStream !== undefined,
|
|
3223
3222
|
async stage(record, page, composition) {
|
|
3224
3223
|
const { locator } = record;
|
|
3225
3224
|
installing(locator);
|
|
3226
|
-
await screened();
|
|
3227
3225
|
if (!liveness())
|
|
3228
3226
|
return null;
|
|
3229
3227
|
const key = await installWrite(async () => {
|
|
@@ -3252,19 +3250,16 @@ export function composeRuntime(options) {
|
|
|
3252
3250
|
derive: derived,
|
|
3253
3251
|
async refreshing(locator, key, composition) {
|
|
3254
3252
|
await exclusive(() => local.unrefresh(key));
|
|
3255
|
-
return refreshingOf(local, key, composition, liveness,
|
|
3253
|
+
return refreshingOf(local, locator, key, composition, liveness, true);
|
|
3256
3254
|
},
|
|
3257
3255
|
abandon: (locator) => exclusive(async () => {
|
|
3258
|
-
if (
|
|
3259
|
-
|
|
3260
|
-
await local.abandon(locator);
|
|
3261
|
-
untransferred(locator);
|
|
3262
|
-
liveQueries.recover();
|
|
3263
|
-
sweep();
|
|
3256
|
+
if (liveness())
|
|
3257
|
+
await abandoned(local, locator);
|
|
3264
3258
|
}),
|
|
3265
3259
|
setAside: async (locator) => {
|
|
3266
3260
|
if (!liveness())
|
|
3267
3261
|
return;
|
|
3262
|
+
setBack(locator);
|
|
3268
3263
|
untransferred(locator);
|
|
3269
3264
|
liveQueries.recover();
|
|
3270
3265
|
},
|
|
@@ -3273,19 +3268,6 @@ export function composeRuntime(options) {
|
|
|
3273
3268
|
sweep();
|
|
3274
3269
|
}),
|
|
3275
3270
|
content: (key) => heldContentOf(local, key),
|
|
3276
|
-
async validate(record, view) {
|
|
3277
|
-
const { locator } = record;
|
|
3278
|
-
const refused = await validatedRoom(local, record, view, null, null, (text) => {
|
|
3279
|
-
const check = unchecked.get(locator);
|
|
3280
|
-
if (check === undefined)
|
|
3281
|
-
return;
|
|
3282
|
-
unchecked.set(locator, { ...check, done: check.done + text });
|
|
3283
|
-
queueReadiness();
|
|
3284
|
-
});
|
|
3285
|
-
unchecked.delete(locator);
|
|
3286
|
-
queueReadiness();
|
|
3287
|
-
return refused;
|
|
3288
|
-
},
|
|
3289
3271
|
};
|
|
3290
3272
|
}
|
|
3291
3273
|
function reusable(room, local) {
|
|
@@ -3297,7 +3279,6 @@ export function composeRuntime(options) {
|
|
|
3297
3279
|
async function pullBootstrap(authorized, room, local, liveness) {
|
|
3298
3280
|
if (!liveness())
|
|
3299
3281
|
return unconfirmed;
|
|
3300
|
-
const turn = generation;
|
|
3301
3282
|
if (replicas.has(room.locator)) {
|
|
3302
3283
|
catching.add(room.locator);
|
|
3303
3284
|
liveQueries.recover();
|
|
@@ -3306,6 +3287,13 @@ export function composeRuntime(options) {
|
|
|
3306
3287
|
const result = await bootstrapRoom(reads, authorized, room, { collection: modelOf(room.locator) }, builtIn, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
|
|
3307
3288
|
const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
|
|
3308
3289
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
3290
|
+
if (result.kind === 'staged' && older(room.locator, result.record.roomVersion)) {
|
|
3291
|
+
caughtUpTo(room.locator);
|
|
3292
|
+
installed(room.locator);
|
|
3293
|
+
await abandoned(local, room.locator);
|
|
3294
|
+
journal.record(`transfer bootstrap ${room.locator} older than shown`);
|
|
3295
|
+
return { kind: 'transport-uncertainty' };
|
|
3296
|
+
}
|
|
3309
3297
|
if (result.kind !== 'staged') {
|
|
3310
3298
|
caughtUpTo(room.locator);
|
|
3311
3299
|
installed(room.locator);
|
|
@@ -3320,9 +3308,6 @@ export function composeRuntime(options) {
|
|
|
3320
3308
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
3321
3309
|
throw error;
|
|
3322
3310
|
}
|
|
3323
|
-
finally {
|
|
3324
|
-
reads.finished(room.locator, turn);
|
|
3325
|
-
}
|
|
3326
3311
|
}
|
|
3327
3312
|
function caughtUpTo(locator) {
|
|
3328
3313
|
if (catching.delete(locator))
|
|
@@ -3330,72 +3315,83 @@ export function composeRuntime(options) {
|
|
|
3330
3315
|
}
|
|
3331
3316
|
function behind(room) {
|
|
3332
3317
|
const held = replicas.get(room.locator)?.record.roomVersion;
|
|
3333
|
-
return
|
|
3334
|
-
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
3318
|
+
return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
|
|
3335
3319
|
}
|
|
3336
|
-
|
|
3337
|
-
const
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
return
|
|
3320
|
+
function standingOf(locator) {
|
|
3321
|
+
const held = replicas.get(locator);
|
|
3322
|
+
return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
|
|
3323
|
+
}
|
|
3324
|
+
function stands(locator, from) {
|
|
3325
|
+
const now = standingOf(locator);
|
|
3326
|
+
return now.key === from.key && now.confirmed === from.confirmed;
|
|
3343
3327
|
}
|
|
3344
|
-
async function
|
|
3328
|
+
async function advance(room, local, authorized, running) {
|
|
3345
3329
|
const live = () => running() && !full;
|
|
3346
3330
|
if (!live())
|
|
3347
3331
|
return unconfirmed;
|
|
3348
3332
|
const held = replicas.get(room.locator);
|
|
3349
|
-
if (held !== undefined) {
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
emitRooms();
|
|
3361
|
-
}
|
|
3362
|
-
return null;
|
|
3363
|
-
}
|
|
3364
|
-
if (refreshed.kind === 'advanced') {
|
|
3365
|
-
const landing = { kind: 'refreshed', base: refreshed.base };
|
|
3366
|
-
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3367
|
-
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
3368
|
-
const reached = now !== undefined &&
|
|
3369
|
-
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
3370
|
-
return reached ? null : advance(room, local, authorized, live);
|
|
3371
|
-
}
|
|
3372
|
-
await retainModel(local, room, modelOf(room.locator));
|
|
3373
|
-
return null;
|
|
3374
|
-
}
|
|
3375
|
-
if (refreshed.kind !== 'beyond-horizon')
|
|
3333
|
+
if (held !== undefined && options.adapter.roomStream !== undefined) {
|
|
3334
|
+
stale.delete(room.locator);
|
|
3335
|
+
name(room.locator);
|
|
3336
|
+
return null;
|
|
3337
|
+
}
|
|
3338
|
+
const refresh = reads.refresh;
|
|
3339
|
+
if (held !== undefined && refresh !== undefined) {
|
|
3340
|
+
const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
|
|
3341
|
+
if (refreshed === 'behind')
|
|
3342
|
+
return advance(room, local, authorized, live);
|
|
3343
|
+
if (refreshed !== 'beyond-horizon')
|
|
3376
3344
|
return refreshed;
|
|
3377
|
-
if (
|
|
3378
|
-
owed.add(room.locator);
|
|
3379
|
-
liveQueries.recover();
|
|
3380
|
-
return unconfirmed;
|
|
3381
|
-
}
|
|
3382
|
-
if (reloadsDue.get(room.locator) === generation)
|
|
3345
|
+
if (reloadsDue.has(room.locator))
|
|
3383
3346
|
return unconfirmed;
|
|
3347
|
+
replaced([room.locator]);
|
|
3384
3348
|
}
|
|
3349
|
+
const from = standingOf(room.locator);
|
|
3385
3350
|
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
3386
|
-
if (bootstrapped.kind
|
|
3387
|
-
|
|
3351
|
+
if (bootstrapped.kind !== 'staged')
|
|
3352
|
+
return live() ? bootstrapped : unconfirmed;
|
|
3353
|
+
const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
|
|
3354
|
+
if (await publish(local, room, bootstrapped.record, landing, live)) {
|
|
3388
3355
|
return live() ? null : unconfirmed;
|
|
3389
3356
|
}
|
|
3390
|
-
return
|
|
3357
|
+
return reachedBy(room.locator, bootstrapped.record.roomVersion)
|
|
3358
|
+
? null
|
|
3359
|
+
: advance(room, local, authorized, live);
|
|
3360
|
+
}
|
|
3361
|
+
async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
|
|
3362
|
+
const refreshed = await pullRefresh(authorized, held, live, hold, refresh);
|
|
3363
|
+
if (!live())
|
|
3364
|
+
return unconfirmed;
|
|
3365
|
+
if (refreshed.kind === 'still-current') {
|
|
3366
|
+
stale.delete(room.locator);
|
|
3367
|
+
if (unreadable.delete(room.locator)) {
|
|
3368
|
+
scopedRefusals.delete(room.locator);
|
|
3369
|
+
list(room, true);
|
|
3370
|
+
emitRooms();
|
|
3371
|
+
}
|
|
3372
|
+
return null;
|
|
3373
|
+
}
|
|
3374
|
+
if (refreshed.kind === 'advanced') {
|
|
3375
|
+
const landing = {
|
|
3376
|
+
kind: 'refreshed',
|
|
3377
|
+
from: refreshed.from,
|
|
3378
|
+
remodelled: refreshed.remodelled,
|
|
3379
|
+
};
|
|
3380
|
+
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3381
|
+
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
3382
|
+
const reached = now !== undefined &&
|
|
3383
|
+
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
3384
|
+
return reached ? null : 'behind';
|
|
3385
|
+
}
|
|
3386
|
+
await retainModel(local, room, modelOf(room.locator));
|
|
3387
|
+
return null;
|
|
3388
|
+
}
|
|
3389
|
+
return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
|
|
3391
3390
|
}
|
|
3392
3391
|
async function withhold(withheld, room, local, progress) {
|
|
3393
|
-
if (withheld.kind === 'unconfirmed')
|
|
3392
|
+
if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
|
|
3394
3393
|
return;
|
|
3395
|
-
|
|
3396
|
-
stale.delete(room.locator);
|
|
3397
|
-
else
|
|
3398
|
-
stale.add(room.locator);
|
|
3394
|
+
stale.add(room.locator);
|
|
3399
3395
|
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
3400
3396
|
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
3401
3397
|
repairable.set(room.locator, reset);
|
|
@@ -3417,30 +3413,10 @@ export function composeRuntime(options) {
|
|
|
3417
3413
|
list(room, false);
|
|
3418
3414
|
emitRooms();
|
|
3419
3415
|
}
|
|
3420
|
-
else if (withheld.kind === 'refused') {
|
|
3421
|
-
refusedSource(room, withheld);
|
|
3422
|
-
}
|
|
3423
3416
|
else if (withheld.kind === 'revoked-authorization') {
|
|
3424
3417
|
await refuse('revoked-authorization');
|
|
3425
3418
|
}
|
|
3426
3419
|
}
|
|
3427
|
-
function refusedSource(room, { refusal }) {
|
|
3428
|
-
sourceRefusals.set(room.locator, {
|
|
3429
|
-
code: refusal.rule,
|
|
3430
|
-
retryable: false,
|
|
3431
|
-
fields: {
|
|
3432
|
-
file: refusal.file,
|
|
3433
|
-
...(refusal.location === undefined ? {} : { location: refusal.location }),
|
|
3434
|
-
...(refusal.referrers === undefined ? {} : { referrers: refusal.referrers }),
|
|
3435
|
-
},
|
|
3436
|
-
});
|
|
3437
|
-
declarationRefusals.delete(room.locator);
|
|
3438
|
-
unreadable.add(room.locator);
|
|
3439
|
-
list(room, false);
|
|
3440
|
-
emitRooms();
|
|
3441
|
-
moveTo({ ...status, ...attention() });
|
|
3442
|
-
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
3443
|
-
}
|
|
3444
3420
|
function leaving(batch, result) {
|
|
3445
3421
|
if (result.kind === 'expired')
|
|
3446
3422
|
return { batch, returned: returnedBy('expired') };
|
|
@@ -3462,8 +3438,8 @@ export function composeRuntime(options) {
|
|
|
3462
3438
|
function settling(acceptance) {
|
|
3463
3439
|
return acceptance !== null && 'apply' in acceptance;
|
|
3464
3440
|
}
|
|
3465
|
-
function
|
|
3466
|
-
return settling(acceptance) ?
|
|
3441
|
+
function failureOf(acceptance) {
|
|
3442
|
+
return settling(acceptance) ? null : acceptance;
|
|
3467
3443
|
}
|
|
3468
3444
|
async function acceptance(room, local, submitted, accepted) {
|
|
3469
3445
|
if (!continuing())
|
|
@@ -3476,7 +3452,8 @@ export function composeRuntime(options) {
|
|
|
3476
3452
|
logAccepted(room.locator, submitted, accepted);
|
|
3477
3453
|
let record = current.record;
|
|
3478
3454
|
let changes;
|
|
3479
|
-
if (!options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
3455
|
+
if (!options.adapter.reached(room.locator, held, accepted.roomVersion) &&
|
|
3456
|
+
!older(room.locator, accepted.roomVersion)) {
|
|
3480
3457
|
const paths = [...returnedPaths(submitted, accepted)];
|
|
3481
3458
|
const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
|
|
3482
3459
|
const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
|
|
@@ -3486,34 +3463,6 @@ export function composeRuntime(options) {
|
|
|
3486
3463
|
? accepted.roomVersion
|
|
3487
3464
|
: held,
|
|
3488
3465
|
};
|
|
3489
|
-
const overrides = new Map([
|
|
3490
|
-
...back.set.map((entry) => [entry.path, entry]),
|
|
3491
|
-
...back.removed.map((path) => [path, null]),
|
|
3492
|
-
]);
|
|
3493
|
-
const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
|
|
3494
|
-
if (refused !== null) {
|
|
3495
|
-
const key = pendingKey(room.locator, submitted.id);
|
|
3496
|
-
return {
|
|
3497
|
-
locator: room.locator,
|
|
3498
|
-
left: [],
|
|
3499
|
-
commit: {
|
|
3500
|
-
rooms: [],
|
|
3501
|
-
writes: [
|
|
3502
|
-
{
|
|
3503
|
-
locator: room.locator,
|
|
3504
|
-
accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
|
|
3505
|
-
},
|
|
3506
|
-
],
|
|
3507
|
-
},
|
|
3508
|
-
refused,
|
|
3509
|
-
apply() {
|
|
3510
|
-
const state = pendings.get(key);
|
|
3511
|
-
if (state !== undefined && !fences.has(room.locator))
|
|
3512
|
-
pendings.set(key, { ...state, accepted: accepted.roomVersion });
|
|
3513
|
-
return changeSet(room.locator, { queue: true });
|
|
3514
|
-
},
|
|
3515
|
-
};
|
|
3516
|
-
}
|
|
3517
3466
|
changes = back;
|
|
3518
3467
|
}
|
|
3519
3468
|
const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
|
|
@@ -3521,10 +3470,9 @@ export function composeRuntime(options) {
|
|
|
3521
3470
|
...settled,
|
|
3522
3471
|
apply(answer) {
|
|
3523
3472
|
const set = settled.apply(answer);
|
|
3473
|
+
showing(room.locator, accepted.roomVersion);
|
|
3524
3474
|
pendings.delete(pendingKey(room.locator, submitted.id));
|
|
3525
3475
|
unreadable.delete(room.locator);
|
|
3526
|
-
sourceRefusals.delete(room.locator);
|
|
3527
|
-
declarationRefusals.delete(room.locator);
|
|
3528
3476
|
list(room, true);
|
|
3529
3477
|
return set;
|
|
3530
3478
|
},
|
|
@@ -3542,13 +3490,9 @@ export function composeRuntime(options) {
|
|
|
3542
3490
|
const failed = await exclusive(async () => {
|
|
3543
3491
|
const adopting = await acceptance(room, local, submitted, accepted);
|
|
3544
3492
|
await adoptedTogether(local, [adopting]);
|
|
3545
|
-
return
|
|
3493
|
+
return failureOf(adopting);
|
|
3546
3494
|
});
|
|
3547
|
-
|
|
3548
|
-
return true;
|
|
3549
|
-
if (failed.kind === 'refused' && continuing())
|
|
3550
|
-
refusedSource(room, failed);
|
|
3551
|
-
return false;
|
|
3495
|
+
return failed === null;
|
|
3552
3496
|
}
|
|
3553
3497
|
async function adoptedMove(room, partner, local, submitted, accepted) {
|
|
3554
3498
|
const other = discovered.get(partner);
|
|
@@ -3562,18 +3506,10 @@ export function composeRuntime(options) {
|
|
|
3562
3506
|
...accepted.destination,
|
|
3563
3507
|
});
|
|
3564
3508
|
await adoptedTogether(local, [left, arrived]);
|
|
3565
|
-
|
|
3566
|
-
if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
|
|
3567
|
-
refusedSource(other, refusedArrival);
|
|
3568
|
-
}
|
|
3569
|
-
return refusalOf(left);
|
|
3509
|
+
return failureOf(left);
|
|
3570
3510
|
});
|
|
3571
3511
|
submitLater(partner);
|
|
3572
|
-
|
|
3573
|
-
return true;
|
|
3574
|
-
if (failed.kind === 'refused' && continuing())
|
|
3575
|
-
refusedSource(room, failed);
|
|
3576
|
-
return false;
|
|
3512
|
+
return failed === null;
|
|
3577
3513
|
}
|
|
3578
3514
|
function queueIn(locator) {
|
|
3579
3515
|
return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
@@ -3696,8 +3632,6 @@ export function composeRuntime(options) {
|
|
|
3696
3632
|
if (replica === undefined || head === undefined)
|
|
3697
3633
|
return 'done';
|
|
3698
3634
|
const state = pendings.get(pendingKey(room.locator, head.id));
|
|
3699
|
-
if (state?.accepted !== undefined)
|
|
3700
|
-
return 'done';
|
|
3701
3635
|
const partner = partnerOf(head, room.locator);
|
|
3702
3636
|
if (partner !== null && !leavingIn(head, room.locator)) {
|
|
3703
3637
|
submitLater(partner);
|
|
@@ -3811,12 +3745,12 @@ export function composeRuntime(options) {
|
|
|
3811
3745
|
}
|
|
3812
3746
|
if (result.kind === 'expired') {
|
|
3813
3747
|
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
|
|
3814
|
-
const there = other === undefined ? null : await
|
|
3748
|
+
const there = other === undefined ? null : await advanced(other, local, authorized);
|
|
3815
3749
|
if (other !== undefined && there !== null) {
|
|
3816
3750
|
await withhold(there, other, local, progress);
|
|
3817
3751
|
return 'done';
|
|
3818
3752
|
}
|
|
3819
|
-
const withheld = await
|
|
3753
|
+
const withheld = await advanced(room, local, authorized);
|
|
3820
3754
|
if (withheld !== null) {
|
|
3821
3755
|
await withhold(withheld, room, local, progress);
|
|
3822
3756
|
return 'done';
|
|
@@ -3838,8 +3772,8 @@ export function composeRuntime(options) {
|
|
|
3838
3772
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
3839
3773
|
const left = leaving(head, result);
|
|
3840
3774
|
const refreshed = async () => ({
|
|
3841
|
-
there: other === undefined ? null : await
|
|
3842
|
-
here: await
|
|
3775
|
+
there: other === undefined ? null : await advanced(other, local, authorized),
|
|
3776
|
+
here: await advanced(room, local, authorized),
|
|
3843
3777
|
});
|
|
3844
3778
|
const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
|
|
3845
3779
|
if (judged !== null && (judged.there !== null || judged.here !== null)) {
|
|
@@ -3943,25 +3877,22 @@ export function composeRuntime(options) {
|
|
|
3943
3877
|
if (signal.aborted)
|
|
3944
3878
|
return;
|
|
3945
3879
|
const progress = { complete: true, reachable: true };
|
|
3946
|
-
await roomTask(room.locator,
|
|
3947
|
-
await Promise.race([
|
|
3948
|
-
reload(room, local, progress, () => live() && !signal.aborted),
|
|
3949
|
-
abandonedOn(signal),
|
|
3950
|
-
]);
|
|
3951
|
-
});
|
|
3880
|
+
await roomTask(room.locator, (live) => reload(room, local, progress, live));
|
|
3952
3881
|
if (signal.aborted)
|
|
3953
3882
|
return;
|
|
3954
3883
|
await rest(progress);
|
|
3955
3884
|
}
|
|
3956
3885
|
}
|
|
3957
3886
|
function awaitingAnswer(locator) {
|
|
3887
|
+
answering.get(locator)?.release();
|
|
3958
3888
|
let release = () => { };
|
|
3959
|
-
const
|
|
3889
|
+
const answered = new Promise((resolve) => {
|
|
3960
3890
|
release = resolve;
|
|
3961
3891
|
});
|
|
3962
|
-
|
|
3892
|
+
const awaited = { answered, release };
|
|
3893
|
+
answering.set(locator, awaited);
|
|
3963
3894
|
return () => {
|
|
3964
|
-
if (answering.get(locator)
|
|
3895
|
+
if (answering.get(locator) === awaited)
|
|
3965
3896
|
answering.delete(locator);
|
|
3966
3897
|
release();
|
|
3967
3898
|
};
|
|
@@ -3973,6 +3904,30 @@ export function composeRuntime(options) {
|
|
|
3973
3904
|
JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
|
|
3974
3905
|
JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
|
|
3975
3906
|
}
|
|
3907
|
+
async function keepNaming(local, room) {
|
|
3908
|
+
const held = replicas.get(room.locator)?.record;
|
|
3909
|
+
if (held === undefined || (held.name === room.name && held.handle === room.handle))
|
|
3910
|
+
return;
|
|
3911
|
+
await exclusive(async () => {
|
|
3912
|
+
if (!continuing())
|
|
3913
|
+
return;
|
|
3914
|
+
const replica = replicas.get(room.locator);
|
|
3915
|
+
if (replica === undefined)
|
|
3916
|
+
return;
|
|
3917
|
+
const record = { ...replica.record, name: room.name, handle: room.handle };
|
|
3918
|
+
await local.commit({
|
|
3919
|
+
rooms: [
|
|
3920
|
+
{
|
|
3921
|
+
locator: room.locator,
|
|
3922
|
+
key: record.key,
|
|
3923
|
+
record: storedRecord(record),
|
|
3924
|
+
composed: composedOf(composed(record)),
|
|
3925
|
+
},
|
|
3926
|
+
],
|
|
3927
|
+
});
|
|
3928
|
+
replicas.set(room.locator, { ...replica, record });
|
|
3929
|
+
});
|
|
3930
|
+
}
|
|
3976
3931
|
async function retainModel(local, room, next) {
|
|
3977
3932
|
const key = room.locator;
|
|
3978
3933
|
const previous = modelOf(room.locator);
|
|
@@ -3988,26 +3943,15 @@ export function composeRuntime(options) {
|
|
|
3988
3943
|
const held = replicas.get(room.locator);
|
|
3989
3944
|
let changed = null;
|
|
3990
3945
|
if (held !== undefined) {
|
|
3991
|
-
const
|
|
3946
|
+
const record = {
|
|
3992
3947
|
...held.record,
|
|
3948
|
+
name: room.name,
|
|
3949
|
+
handle: room.handle,
|
|
3993
3950
|
operations: room.operations,
|
|
3994
3951
|
replica: room.replica,
|
|
3995
3952
|
typedSelection: room.typedSelection ?? null,
|
|
3996
3953
|
model: next,
|
|
3997
3954
|
};
|
|
3998
|
-
let read = 'staged';
|
|
3999
|
-
if (!sameMembership(declared, held.record)) {
|
|
4000
|
-
const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), composed(declared));
|
|
4001
|
-
read =
|
|
4002
|
-
verdict.verdict !== 'refuse'
|
|
4003
|
-
? 'staged'
|
|
4004
|
-
: verdict.rule === 'model-unavailable'
|
|
4005
|
-
? 'deferred'
|
|
4006
|
-
: { kind: 'refused', refusal: verdict };
|
|
4007
|
-
}
|
|
4008
|
-
const record = read === 'staged'
|
|
4009
|
-
? declared
|
|
4010
|
-
: { ...declared, typedSelection: held.record.typedSelection };
|
|
4011
3955
|
const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
|
|
4012
3956
|
const replayedLayer = await replayed(record, confirmedLayer, held.queue);
|
|
4013
3957
|
const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
|
|
@@ -4029,16 +3973,6 @@ export function composeRuntime(options) {
|
|
|
4029
3973
|
documents: answer.changed[room.locator] ?? [],
|
|
4030
3974
|
room: true,
|
|
4031
3975
|
});
|
|
4032
|
-
if (read === 'staged' || read === 'deferred') {
|
|
4033
|
-
if (declarationRefusals.delete(room.locator)) {
|
|
4034
|
-
sourceRefusals.delete(room.locator);
|
|
4035
|
-
unreadable.delete(room.locator);
|
|
4036
|
-
}
|
|
4037
|
-
}
|
|
4038
|
-
else {
|
|
4039
|
-
refusedSource(room, read);
|
|
4040
|
-
declarationRefusals.add(room.locator);
|
|
4041
|
-
}
|
|
4042
3976
|
}
|
|
4043
3977
|
heldModel(room, next);
|
|
4044
3978
|
if (changed === null)
|
|
@@ -4083,6 +4017,17 @@ export function composeRuntime(options) {
|
|
|
4083
4017
|
emitRooms();
|
|
4084
4018
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
4085
4019
|
}
|
|
4020
|
+
function name(locator) {
|
|
4021
|
+
const stream = options.adapter.roomStream;
|
|
4022
|
+
const record = replicas.get(locator)?.record;
|
|
4023
|
+
const room = discovered.get(locator);
|
|
4024
|
+
if (stream === undefined || record === undefined || room === undefined)
|
|
4025
|
+
return;
|
|
4026
|
+
if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
|
|
4027
|
+
return;
|
|
4028
|
+
naming += 1;
|
|
4029
|
+
stream.hold([{ locator, roomVersion: record.roomVersion }]);
|
|
4030
|
+
}
|
|
4086
4031
|
function heldRooms(listed) {
|
|
4087
4032
|
return listed.flatMap((room) => {
|
|
4088
4033
|
const record = replicas.get(room.locator)?.record;
|
|
@@ -4100,14 +4045,19 @@ export function composeRuntime(options) {
|
|
|
4100
4045
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
4101
4046
|
});
|
|
4102
4047
|
if (attempt === 'opened') {
|
|
4048
|
+
naming += 1;
|
|
4103
4049
|
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
4104
4050
|
handed = true;
|
|
4105
4051
|
}
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4052
|
+
for (const room of ready) {
|
|
4053
|
+
if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
|
|
4054
|
+
if (attempt === 'authorized')
|
|
4055
|
+
name(room.locator);
|
|
4056
|
+
continue;
|
|
4057
|
+
}
|
|
4058
|
+
void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
|
|
4059
|
+
}
|
|
4060
|
+
const failed = prepared.find((room) => room.status === 'rejected');
|
|
4111
4061
|
if (failed !== undefined)
|
|
4112
4062
|
throw failed.reason;
|
|
4113
4063
|
}
|
|
@@ -4126,12 +4076,16 @@ export function composeRuntime(options) {
|
|
|
4126
4076
|
}
|
|
4127
4077
|
if (prior !== undefined)
|
|
4128
4078
|
await retainModel(local, room, modelOf(room.locator));
|
|
4079
|
+
if (prior !== undefined)
|
|
4080
|
+
await keepNaming(local, room);
|
|
4129
4081
|
if (discoveryPaused || authorization === undefined)
|
|
4130
4082
|
return false;
|
|
4131
4083
|
return room.replica.admission === 'admitted';
|
|
4132
4084
|
}
|
|
4133
4085
|
async function caughtUp(room, local, progress, live) {
|
|
4134
|
-
if (
|
|
4086
|
+
if (options.adapter.roomStream !== undefined || authorization === undefined)
|
|
4087
|
+
return;
|
|
4088
|
+
if (!live() || !behind(room))
|
|
4135
4089
|
return;
|
|
4136
4090
|
const withheld = await advance(room, local, authorization, live);
|
|
4137
4091
|
if (withheld !== null)
|
|
@@ -4144,6 +4098,8 @@ export function composeRuntime(options) {
|
|
|
4144
4098
|
async function loadRoom(room, local, progress, live) {
|
|
4145
4099
|
if (!live() || authorization === undefined)
|
|
4146
4100
|
return;
|
|
4101
|
+
if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
|
|
4102
|
+
return;
|
|
4147
4103
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
4148
4104
|
await reload(room, local, progress, live);
|
|
4149
4105
|
return;
|
|
@@ -4161,20 +4117,53 @@ export function composeRuntime(options) {
|
|
|
4161
4117
|
reloadsDue.delete(room.locator);
|
|
4162
4118
|
if (!live() || authorization === undefined)
|
|
4163
4119
|
return;
|
|
4120
|
+
const from = standingOf(room.locator);
|
|
4164
4121
|
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
4165
4122
|
if (bootstrapped.kind !== 'staged') {
|
|
4166
4123
|
if (live())
|
|
4167
4124
|
await withhold(bootstrapped, room, local, progress);
|
|
4168
4125
|
return;
|
|
4169
4126
|
}
|
|
4170
|
-
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
4127
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
|
|
4171
4128
|
await caughtUp(room, local, progress, live);
|
|
4172
4129
|
}
|
|
4173
4130
|
finally {
|
|
4174
4131
|
caughtUpTo(room.locator);
|
|
4175
4132
|
}
|
|
4176
4133
|
}
|
|
4177
|
-
async function
|
|
4134
|
+
async function asked(authorized) {
|
|
4135
|
+
try {
|
|
4136
|
+
return (await options.adapter.discover?.(authorized)) ?? { kind: 'transport-uncertainty' };
|
|
4137
|
+
}
|
|
4138
|
+
catch {
|
|
4139
|
+
return { kind: 'transport-uncertainty' };
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
function malformed(listed) {
|
|
4143
|
+
for (const room of listed) {
|
|
4144
|
+
if (room.operations === undefined ||
|
|
4145
|
+
!['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
4146
|
+
room.operations[operation].every((scope) => typeof scope === 'string')) ||
|
|
4147
|
+
room.replica === undefined ||
|
|
4148
|
+
typeof room.replica.selection !== 'string' ||
|
|
4149
|
+
room.replica.selection.length === 0 ||
|
|
4150
|
+
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
4151
|
+
!['locked', 'open'].includes(room.replica.offline)) {
|
|
4152
|
+
return `invalid model or replica declaration ${room.locator}`;
|
|
4153
|
+
}
|
|
4154
|
+
if (!declaredSelection(room))
|
|
4155
|
+
return `invalid typed selection declaration ${room.locator}`;
|
|
4156
|
+
}
|
|
4157
|
+
return null;
|
|
4158
|
+
}
|
|
4159
|
+
async function signIn(local, signedIn) {
|
|
4160
|
+
if (JSON.stringify(signedIn) === JSON.stringify(member))
|
|
4161
|
+
return;
|
|
4162
|
+
await local.fact('member', signedIn);
|
|
4163
|
+
member = signedIn;
|
|
4164
|
+
emitMember();
|
|
4165
|
+
}
|
|
4166
|
+
async function discoverRooms(local, progress, attempt, live, found) {
|
|
4178
4167
|
if (closed || authorization === undefined) {
|
|
4179
4168
|
notifyReady();
|
|
4180
4169
|
return;
|
|
@@ -4182,13 +4171,7 @@ export function composeRuntime(options) {
|
|
|
4182
4171
|
moveTo(transferring(status));
|
|
4183
4172
|
discoveries += 1;
|
|
4184
4173
|
const started = discoveries;
|
|
4185
|
-
|
|
4186
|
-
try {
|
|
4187
|
-
result = await options.adapter.discover(authorization, scope);
|
|
4188
|
-
}
|
|
4189
|
-
catch {
|
|
4190
|
-
result = { kind: 'transport-uncertainty' };
|
|
4191
|
-
}
|
|
4174
|
+
const result = found ?? (await asked(authorization));
|
|
4192
4175
|
if (result.kind === 'discovery')
|
|
4193
4176
|
roomsNamed = true;
|
|
4194
4177
|
if (!live()) {
|
|
@@ -4198,7 +4181,7 @@ export function composeRuntime(options) {
|
|
|
4198
4181
|
}
|
|
4199
4182
|
journal.record(`connection discover ${result.kind}`);
|
|
4200
4183
|
if (result.kind === 'transport-uncertainty') {
|
|
4201
|
-
await
|
|
4184
|
+
await cachedModels(local);
|
|
4202
4185
|
progress.reachable = false;
|
|
4203
4186
|
progress.complete = false;
|
|
4204
4187
|
notifyReady();
|
|
@@ -4219,26 +4202,12 @@ export function composeRuntime(options) {
|
|
|
4219
4202
|
notifyReady();
|
|
4220
4203
|
return;
|
|
4221
4204
|
}
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
room.replica.selection.length === 0 ||
|
|
4229
|
-
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
4230
|
-
!['locked', 'open'].includes(room.replica.offline)) {
|
|
4231
|
-
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
4232
|
-
await refuse('missing-required-guarantee');
|
|
4233
|
-
notifyReady();
|
|
4234
|
-
return;
|
|
4235
|
-
}
|
|
4236
|
-
if (!declaredSelection(room)) {
|
|
4237
|
-
journal.record(`invalid typed selection declaration ${room.locator}`);
|
|
4238
|
-
await refuse('missing-required-guarantee');
|
|
4239
|
-
notifyReady();
|
|
4240
|
-
return;
|
|
4241
|
-
}
|
|
4205
|
+
const invalid = malformed(result.rooms);
|
|
4206
|
+
if (invalid !== null) {
|
|
4207
|
+
journal.record(invalid);
|
|
4208
|
+
await refuse('missing-required-guarantee');
|
|
4209
|
+
notifyReady();
|
|
4210
|
+
return;
|
|
4242
4211
|
}
|
|
4243
4212
|
if (refusal !== null) {
|
|
4244
4213
|
refusal = null;
|
|
@@ -4253,15 +4222,9 @@ export function composeRuntime(options) {
|
|
|
4253
4222
|
limits = result.providerLimits;
|
|
4254
4223
|
await local.fact('providerLimits', limits);
|
|
4255
4224
|
await local.fact('capabilities', capabilities);
|
|
4256
|
-
conversations.declared();
|
|
4257
|
-
}
|
|
4258
|
-
const signedIn = result.member ?? null;
|
|
4259
|
-
if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
|
|
4260
|
-
await local.fact('member', signedIn);
|
|
4261
|
-
member = signedIn;
|
|
4262
|
-
emitMember();
|
|
4263
4225
|
}
|
|
4264
|
-
await
|
|
4226
|
+
await signIn(local, result.member ?? null);
|
|
4227
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
4265
4228
|
}
|
|
4266
4229
|
async function ensureAuthorization() {
|
|
4267
4230
|
if (authorization !== undefined)
|
|
@@ -4276,23 +4239,45 @@ export function composeRuntime(options) {
|
|
|
4276
4239
|
}
|
|
4277
4240
|
}
|
|
4278
4241
|
async function admittedRead(work) {
|
|
4279
|
-
|
|
4280
|
-
|
|
4242
|
+
while (roomReads.free === 0) {
|
|
4243
|
+
await new Promise((admit) => roomReads.waiting.push(admit));
|
|
4281
4244
|
}
|
|
4282
|
-
|
|
4283
|
-
while (reads.free === 0)
|
|
4284
|
-
await new Promise((admit) => reads.waiting.push(admit));
|
|
4285
|
-
reads.free -= 1;
|
|
4245
|
+
roomReads.free -= 1;
|
|
4286
4246
|
try {
|
|
4287
4247
|
await work();
|
|
4288
4248
|
}
|
|
4289
4249
|
finally {
|
|
4290
|
-
|
|
4291
|
-
|
|
4250
|
+
roomReads.free += 1;
|
|
4251
|
+
roomReads.waiting.shift()?.();
|
|
4292
4252
|
}
|
|
4293
4253
|
}
|
|
4254
|
+
function inRoom(locator, work) {
|
|
4255
|
+
roomWork += 1;
|
|
4256
|
+
const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
|
|
4257
|
+
try {
|
|
4258
|
+
return await work(continuing);
|
|
4259
|
+
}
|
|
4260
|
+
finally {
|
|
4261
|
+
roomWork -= 1;
|
|
4262
|
+
}
|
|
4263
|
+
});
|
|
4264
|
+
const chain = ran.then(() => undefined, () => undefined);
|
|
4265
|
+
roomLanes.set(locator, chain);
|
|
4266
|
+
working.add(chain);
|
|
4267
|
+
void chain.finally(() => {
|
|
4268
|
+
working.delete(chain);
|
|
4269
|
+
if (roomLanes.get(locator) === chain)
|
|
4270
|
+
roomLanes.delete(locator);
|
|
4271
|
+
});
|
|
4272
|
+
return ran;
|
|
4273
|
+
}
|
|
4294
4274
|
function roomTask(locator, work) {
|
|
4295
|
-
|
|
4275
|
+
if (stopping)
|
|
4276
|
+
return Promise.resolve();
|
|
4277
|
+
return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
|
|
4278
|
+
}
|
|
4279
|
+
function advanced(room, local, authorized) {
|
|
4280
|
+
return inRoom(room.locator, (live) => advance(room, local, authorized, live));
|
|
4296
4281
|
}
|
|
4297
4282
|
function task(lane, work) {
|
|
4298
4283
|
if (stopping)
|
|
@@ -4308,23 +4293,19 @@ export function composeRuntime(options) {
|
|
|
4308
4293
|
void run.finally(() => working.delete(run));
|
|
4309
4294
|
return run;
|
|
4310
4295
|
}
|
|
4311
|
-
function sync(attempt) {
|
|
4296
|
+
function sync(attempt, found) {
|
|
4312
4297
|
return task(OPENING_LANE, async (live) => {
|
|
4313
4298
|
if (!live()) {
|
|
4314
4299
|
readyFromStore();
|
|
4315
4300
|
return;
|
|
4316
4301
|
}
|
|
4317
4302
|
const progress = { complete: true, reachable: true };
|
|
4318
|
-
|
|
4319
|
-
if (attempt === 'opened')
|
|
4320
|
-
opened = generation;
|
|
4321
|
-
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
4303
|
+
underway = attempt === 'opened' ? progress : null;
|
|
4322
4304
|
if (attempt === 'opened')
|
|
4323
4305
|
streamed = progress;
|
|
4324
4306
|
complete = false;
|
|
4325
4307
|
recorded = false;
|
|
4326
4308
|
moveTo(transferring(status));
|
|
4327
|
-
handed = false;
|
|
4328
4309
|
try {
|
|
4329
4310
|
const local = await openLocal();
|
|
4330
4311
|
if (!live())
|
|
@@ -4332,7 +4313,7 @@ export function composeRuntime(options) {
|
|
|
4332
4313
|
await ensureAuthorization();
|
|
4333
4314
|
if (!live())
|
|
4334
4315
|
return;
|
|
4335
|
-
await discoverRooms(local, progress, attempt, live);
|
|
4316
|
+
await discoverRooms(local, progress, attempt, live, found);
|
|
4336
4317
|
}
|
|
4337
4318
|
catch {
|
|
4338
4319
|
progress.complete = false;
|
|
@@ -4340,7 +4321,6 @@ export function composeRuntime(options) {
|
|
|
4340
4321
|
progress.reachable = false;
|
|
4341
4322
|
}
|
|
4342
4323
|
finally {
|
|
4343
|
-
gate.release();
|
|
4344
4324
|
notifyReady();
|
|
4345
4325
|
}
|
|
4346
4326
|
if (!live())
|
|
@@ -4352,35 +4332,59 @@ export function composeRuntime(options) {
|
|
|
4352
4332
|
if (handed || closed || progress.reachable)
|
|
4353
4333
|
return;
|
|
4354
4334
|
underway = null;
|
|
4355
|
-
opened = null;
|
|
4356
|
-
journal.record('connection reopen');
|
|
4357
4335
|
await rest(progress, live);
|
|
4358
|
-
options.adapter.roomStream?.reopen();
|
|
4359
4336
|
});
|
|
4360
4337
|
}
|
|
4361
|
-
function
|
|
4338
|
+
function resynchronize() {
|
|
4362
4339
|
return task(OPENING_LANE, async (live) => {
|
|
4363
|
-
if (!live() || !
|
|
4340
|
+
if (!live() || !announced)
|
|
4364
4341
|
return;
|
|
4365
4342
|
const progress = { complete: true, reachable: true };
|
|
4366
|
-
const gate = discover();
|
|
4367
4343
|
moveTo(transferring(status));
|
|
4368
4344
|
try {
|
|
4369
4345
|
const local = await openLocal();
|
|
4370
4346
|
if (!live())
|
|
4371
4347
|
return;
|
|
4372
|
-
await
|
|
4348
|
+
await synchronize([...discovered.values()], local, progress, 'unstreamed', live);
|
|
4349
|
+
}
|
|
4350
|
+
catch {
|
|
4351
|
+
progress.complete = false;
|
|
4352
|
+
}
|
|
4353
|
+
if (live())
|
|
4354
|
+
await finishOpening(progress, live);
|
|
4355
|
+
});
|
|
4356
|
+
}
|
|
4357
|
+
function countAuthorityChange() {
|
|
4358
|
+
authorityChanges += 1;
|
|
4359
|
+
for (const listener of authorityChangeListeners)
|
|
4360
|
+
listener(authorityChanges);
|
|
4361
|
+
}
|
|
4362
|
+
function changeAuthority(change) {
|
|
4363
|
+
return task(OPENING_LANE, async (live) => {
|
|
4364
|
+
if (!live() || !announced)
|
|
4365
|
+
return;
|
|
4366
|
+
countAuthorityChange();
|
|
4367
|
+
const progress = { complete: true, reachable: true };
|
|
4368
|
+
moveTo(transferring(status));
|
|
4369
|
+
try {
|
|
4370
|
+
const local = await openLocal();
|
|
4373
4371
|
if (!live())
|
|
4374
4372
|
return;
|
|
4375
|
-
|
|
4373
|
+
const invalid = malformed(change.rooms);
|
|
4374
|
+
if (invalid !== null) {
|
|
4375
|
+
journal.record(invalid);
|
|
4376
|
+
await refuse('missing-required-guarantee');
|
|
4377
|
+
return;
|
|
4378
|
+
}
|
|
4379
|
+
if (change.member !== undefined)
|
|
4380
|
+
await signIn(local, change.member);
|
|
4381
|
+
discoveries += 1;
|
|
4382
|
+
const listed = await applyDiscovery(change.rooms, discoveries, change.left);
|
|
4383
|
+
await synchronize(listed, local, progress, 'authorized', live);
|
|
4376
4384
|
}
|
|
4377
4385
|
catch {
|
|
4378
4386
|
progress.complete = false;
|
|
4379
4387
|
}
|
|
4380
|
-
finally {
|
|
4381
|
-
gate.release();
|
|
4382
|
-
notifyReady();
|
|
4383
|
-
}
|
|
4384
4388
|
if (!live())
|
|
4385
4389
|
return;
|
|
4386
4390
|
if (!progress.complete)
|
|
@@ -4388,15 +4392,17 @@ export function composeRuntime(options) {
|
|
|
4388
4392
|
await rest(progress, live);
|
|
4389
4393
|
});
|
|
4390
4394
|
}
|
|
4391
|
-
function closedSocket(revoked, lost) {
|
|
4392
|
-
|
|
4393
|
-
const opened =
|
|
4395
|
+
function closedSocket(revoked, lost, authorityChanged) {
|
|
4396
|
+
notifyReady();
|
|
4397
|
+
const opened = announced;
|
|
4394
4398
|
socketOpen = false;
|
|
4399
|
+
announced = false;
|
|
4400
|
+
missedChange ||= authorityChanged;
|
|
4401
|
+
handed = false;
|
|
4402
|
+
naming = 0;
|
|
4395
4403
|
connectionLost();
|
|
4396
|
-
if (lost)
|
|
4397
|
-
reads.lost();
|
|
4404
|
+
if (lost)
|
|
4398
4405
|
conversations.lost();
|
|
4399
|
-
}
|
|
4400
4406
|
if (revoked) {
|
|
4401
4407
|
void refuse('revoked-authorization');
|
|
4402
4408
|
return;
|
|
@@ -4407,18 +4413,32 @@ export function composeRuntime(options) {
|
|
|
4407
4413
|
moveTo({
|
|
4408
4414
|
...status,
|
|
4409
4415
|
state: status.state === 'synced' ? 'syncing' : status.state,
|
|
4410
|
-
providerUnreachable: status.providerUnreachable || !opened,
|
|
4416
|
+
providerUnreachable: status.providerUnreachable || (!opened && lost),
|
|
4417
|
+
});
|
|
4418
|
+
void closing(opened);
|
|
4419
|
+
}
|
|
4420
|
+
function closing(opened) {
|
|
4421
|
+
return task(OPENING_LANE, async (live) => {
|
|
4422
|
+
if (!live())
|
|
4423
|
+
return;
|
|
4424
|
+
const local = await openLocal();
|
|
4425
|
+
if (!opened && !socketOpen)
|
|
4426
|
+
await cachedModels(local);
|
|
4427
|
+
await rest(null, live);
|
|
4411
4428
|
});
|
|
4412
4429
|
}
|
|
4413
|
-
function
|
|
4414
|
-
|
|
4430
|
+
function cachedModels(local) {
|
|
4431
|
+
return settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
|
|
4432
|
+
}
|
|
4433
|
+
function caughtUpRooms(expired) {
|
|
4434
|
+
const progress = underway;
|
|
4435
|
+
underway = null;
|
|
4436
|
+
naming = Math.max(0, naming - 1);
|
|
4415
4437
|
const cycle = progress ?? { complete: true, reachable: true };
|
|
4416
|
-
|
|
4417
|
-
opened = null;
|
|
4438
|
+
replaced(expired);
|
|
4418
4439
|
const reloading = progress === null ? [] : owedReloads(expired);
|
|
4419
4440
|
const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
|
|
4420
4441
|
return task(OPENING_LANE, async (live) => {
|
|
4421
|
-
underway = null;
|
|
4422
4442
|
if (!live())
|
|
4423
4443
|
return;
|
|
4424
4444
|
if (progress === null) {
|
|
@@ -4426,20 +4446,14 @@ export function composeRuntime(options) {
|
|
|
4426
4446
|
await rest(cycle, live);
|
|
4427
4447
|
return;
|
|
4428
4448
|
}
|
|
4429
|
-
await roomsCarried([...expired, ...reloading]);
|
|
4430
|
-
if (!live())
|
|
4431
|
-
return;
|
|
4432
4449
|
try {
|
|
4433
4450
|
const local = await openLocal();
|
|
4434
|
-
|
|
4451
|
+
await replacing;
|
|
4435
4452
|
if (progress.reachable) {
|
|
4436
4453
|
await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
|
|
4437
4454
|
!expired.includes(room.locator) &&
|
|
4438
4455
|
!reloading.includes(room.locator)), 'current', local);
|
|
4439
4456
|
}
|
|
4440
|
-
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
4441
|
-
if (failed !== undefined)
|
|
4442
|
-
throw failed.reason;
|
|
4443
4457
|
}
|
|
4444
4458
|
catch {
|
|
4445
4459
|
progress.complete = false;
|
|
@@ -4451,11 +4465,8 @@ export function composeRuntime(options) {
|
|
|
4451
4465
|
}
|
|
4452
4466
|
function owedReloads(expired) {
|
|
4453
4467
|
const reloading = [...discovered.values()]
|
|
4454
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
4455
|
-
!expired.includes(room.locator) &&
|
|
4456
|
-
(owed.has(room.locator) || revoked(room.locator)))
|
|
4468
|
+
.filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
|
|
4457
4469
|
.map((room) => room.locator);
|
|
4458
|
-
owed.clear();
|
|
4459
4470
|
for (const locator of reloading)
|
|
4460
4471
|
catching.add(locator);
|
|
4461
4472
|
liveQueries.recover();
|
|
@@ -4464,16 +4475,17 @@ export function composeRuntime(options) {
|
|
|
4464
4475
|
async function reloadNow(replaced, progress) {
|
|
4465
4476
|
const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
|
|
4466
4477
|
for (const room of reloaded)
|
|
4467
|
-
reloadsDue.
|
|
4478
|
+
reloadsDue.add(room.locator);
|
|
4468
4479
|
try {
|
|
4469
4480
|
const local = await openLocal();
|
|
4470
|
-
|
|
4481
|
+
for (const room of reloaded) {
|
|
4482
|
+
void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
|
|
4483
|
+
}
|
|
4471
4484
|
}
|
|
4472
4485
|
catch {
|
|
4473
4486
|
for (const room of reloaded)
|
|
4474
4487
|
reloadsDue.delete(room.locator);
|
|
4475
4488
|
progress.reachable = false;
|
|
4476
|
-
return [];
|
|
4477
4489
|
}
|
|
4478
4490
|
}
|
|
4479
4491
|
async function settleModels(held, status, local) {
|
|
@@ -4484,7 +4496,7 @@ export function composeRuntime(options) {
|
|
|
4484
4496
|
const settled = await exclusive(async () => {
|
|
4485
4497
|
if (!continuing())
|
|
4486
4498
|
return [];
|
|
4487
|
-
const unchanged = held.filter((room) => owed(room) &&
|
|
4499
|
+
const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
|
|
4488
4500
|
for (const room of unchanged) {
|
|
4489
4501
|
const next = { ...modelOf(room.locator), status };
|
|
4490
4502
|
const replica = replicas.get(room.locator);
|
|
@@ -4544,14 +4556,10 @@ export function composeRuntime(options) {
|
|
|
4544
4556
|
try {
|
|
4545
4557
|
const local = await openLocal();
|
|
4546
4558
|
await roomTask(locator, async (live) => {
|
|
4547
|
-
|
|
4548
|
-
if (!current() || authorization === undefined)
|
|
4559
|
+
if (!live() || authorization === undefined)
|
|
4549
4560
|
return;
|
|
4550
|
-
const withheld = await
|
|
4551
|
-
|
|
4552
|
-
abandonedOn(signal),
|
|
4553
|
-
]);
|
|
4554
|
-
if (withheld !== null && current())
|
|
4561
|
+
const withheld = await advance(room, local, authorization, live);
|
|
4562
|
+
if (withheld !== null && live())
|
|
4555
4563
|
await withhold(withheld, room, local, progress);
|
|
4556
4564
|
});
|
|
4557
4565
|
}
|
|
@@ -4606,14 +4614,11 @@ export function composeRuntime(options) {
|
|
|
4606
4614
|
progress.complete = false;
|
|
4607
4615
|
return;
|
|
4608
4616
|
}
|
|
4609
|
-
reads.applied();
|
|
4610
4617
|
submitAll();
|
|
4611
4618
|
for (const locator of stale)
|
|
4612
4619
|
if (repairable.has(locator))
|
|
4613
4620
|
repairLater(locator, {});
|
|
4614
4621
|
await conversations.reconcile();
|
|
4615
|
-
if (status.providerUnreachable)
|
|
4616
|
-
conversations.reconnected();
|
|
4617
4622
|
const reading = { complete: true, reachable: true };
|
|
4618
4623
|
await conversations.catchUp(reading);
|
|
4619
4624
|
if (progress.reachable && reading.reachable)
|
|
@@ -4639,7 +4644,7 @@ export function composeRuntime(options) {
|
|
|
4639
4644
|
if (!live())
|
|
4640
4645
|
return;
|
|
4641
4646
|
let lastCompleted = status.lastCompleted;
|
|
4642
|
-
let synced = complete && stale.size === 0 && !pending();
|
|
4647
|
+
let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
|
|
4643
4648
|
if (synced && !recorded) {
|
|
4644
4649
|
const completedAt = new Date().toISOString();
|
|
4645
4650
|
try {
|
|
@@ -4654,74 +4659,78 @@ export function composeRuntime(options) {
|
|
|
4654
4659
|
}
|
|
4655
4660
|
}
|
|
4656
4661
|
moveTo({
|
|
4657
|
-
state: pending()
|
|
4662
|
+
state: pending()
|
|
4663
|
+
? 'pending'
|
|
4664
|
+
: synced && complete && roomWork === 0 && naming === 0
|
|
4665
|
+
? 'synced'
|
|
4666
|
+
: 'syncing',
|
|
4658
4667
|
lastCompleted,
|
|
4659
|
-
providerUnreachable: !progress.reachable,
|
|
4668
|
+
providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
|
|
4660
4669
|
...attention(),
|
|
4661
4670
|
});
|
|
4662
4671
|
}
|
|
4663
|
-
function
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
}
|
|
4671
|
-
function applyRoom(locator, roomVersion) {
|
|
4672
|
-
announced.set(locator, roomVersion);
|
|
4673
|
-
return task(roomLane(locator), async (live) => {
|
|
4674
|
-
if (discovering?.generation === generation)
|
|
4675
|
-
await discovering.done;
|
|
4676
|
-
const saving = answering.get(locator);
|
|
4677
|
-
if (saving?.generation === generation)
|
|
4678
|
-
await saving.answered;
|
|
4679
|
-
if (!live() || authorization === undefined)
|
|
4680
|
-
return;
|
|
4681
|
-
const room = discovered.get(locator);
|
|
4682
|
-
const held = replicas.get(locator);
|
|
4683
|
-
if (room === undefined || room.replica.admission !== 'admitted')
|
|
4684
|
-
return;
|
|
4685
|
-
if (held !== undefined &&
|
|
4686
|
-
(held.record.replica.selection !== room.replica.selection ||
|
|
4687
|
-
options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
|
|
4688
|
-
return;
|
|
4689
|
-
}
|
|
4690
|
-
const during = underway?.generation === generation ? underway.progress : null;
|
|
4691
|
-
const progress = during ?? { complete: true, reachable: true };
|
|
4692
|
-
if (during === null)
|
|
4693
|
-
moveTo(transferring(status));
|
|
4672
|
+
function applyRoom(change) {
|
|
4673
|
+
const { locator } = change;
|
|
4674
|
+
if (stopping)
|
|
4675
|
+
return Promise.resolve();
|
|
4676
|
+
return inRoom(locator, async (live) => {
|
|
4677
|
+
await answering.get(locator)?.answered;
|
|
4678
|
+
const progress = underway ?? { complete: true, reachable: true };
|
|
4694
4679
|
try {
|
|
4695
4680
|
const local = await openLocal();
|
|
4696
|
-
|
|
4697
|
-
if (!live()
|
|
4698
|
-
return;
|
|
4699
|
-
const
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4681
|
+
for (;;) {
|
|
4682
|
+
if (!live())
|
|
4683
|
+
return null;
|
|
4684
|
+
const room = discovered.get(locator);
|
|
4685
|
+
const held = replicas.get(locator);
|
|
4686
|
+
if (room === undefined ||
|
|
4687
|
+
held === undefined ||
|
|
4688
|
+
room.replica.admission !== 'admitted' ||
|
|
4689
|
+
held.record.replica.selection !== room.replica.selection) {
|
|
4690
|
+
return null;
|
|
4691
|
+
}
|
|
4692
|
+
const at = held.record.roomVersion;
|
|
4693
|
+
if (options.adapter.reached(locator, at, change.roomVersion))
|
|
4694
|
+
return progress;
|
|
4695
|
+
if (!options.adapter.reached(locator, at, change.from)) {
|
|
4696
|
+
name(locator);
|
|
4697
|
+
return progress;
|
|
4698
|
+
}
|
|
4699
|
+
if (underway === null)
|
|
4700
|
+
moveTo(transferring(status));
|
|
4701
|
+
const writing = () => live() && !full;
|
|
4702
|
+
let outlined = false;
|
|
4703
|
+
const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, async () => {
|
|
4704
|
+
const page = await change.read();
|
|
4705
|
+
const paths = Object.keys(page.outlines ?? {});
|
|
4706
|
+
outlined = paths.length > 0;
|
|
4707
|
+
await local.remember(locator, held.record.key, paths);
|
|
4708
|
+
return page;
|
|
4709
|
+
}));
|
|
4710
|
+
if (refreshed.kind !== 'advanced')
|
|
4711
|
+
return progress;
|
|
4712
|
+
const landing = {
|
|
4713
|
+
kind: 'refreshed',
|
|
4714
|
+
from: refreshed.from,
|
|
4715
|
+
remodelled: refreshed.remodelled,
|
|
4716
|
+
};
|
|
4717
|
+
if (await publish(local, room, refreshed.record, landing, writing)) {
|
|
4718
|
+
stale.delete(locator);
|
|
4719
|
+
await retainModel(local, room, modelOf(locator));
|
|
4720
|
+
if (outlined)
|
|
4721
|
+
void heldAgain(locator);
|
|
4722
|
+
return progress;
|
|
4723
|
+
}
|
|
4724
|
+
}
|
|
4703
4725
|
}
|
|
4704
4726
|
catch {
|
|
4727
|
+
if (!stale.has(locator))
|
|
4728
|
+
name(locator);
|
|
4705
4729
|
stale.add(locator);
|
|
4730
|
+
return progress;
|
|
4706
4731
|
}
|
|
4707
|
-
|
|
4708
|
-
if (during === null)
|
|
4709
|
-
await rest(progress, live);
|
|
4710
|
-
}
|
|
4711
|
-
});
|
|
4732
|
+
}).then((progress) => rest(progress), () => undefined);
|
|
4712
4733
|
}
|
|
4713
|
-
function roomsCarried(replacing) {
|
|
4714
|
-
const replaced = new Set(replacing.map(roomLane));
|
|
4715
|
-
return Promise.all([...lanes]
|
|
4716
|
-
.filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
|
|
4717
|
-
.map(([, held]) => held.chain));
|
|
4718
|
-
}
|
|
4719
|
-
const receipts = {
|
|
4720
|
-
heads: async () => (await openLocal()).receivedHeads(),
|
|
4721
|
-
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
4722
|
-
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
4723
|
-
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
4724
|
-
};
|
|
4725
4734
|
function observeStream() {
|
|
4726
4735
|
const stream = options.adapter.roomStream;
|
|
4727
4736
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -4731,31 +4740,35 @@ export function composeRuntime(options) {
|
|
|
4731
4740
|
return;
|
|
4732
4741
|
if (event.kind === 'opened') {
|
|
4733
4742
|
socketOpen = true;
|
|
4743
|
+
announced = false;
|
|
4744
|
+
handed = false;
|
|
4745
|
+
naming = 0;
|
|
4734
4746
|
connectionLive();
|
|
4735
4747
|
generation += 1;
|
|
4736
|
-
for (const admit of installWaiters.splice(0))
|
|
4737
|
-
admit();
|
|
4738
|
-
reads.opened();
|
|
4739
4748
|
conversations.reopened();
|
|
4740
4749
|
recovery.resume();
|
|
4741
|
-
|
|
4750
|
+
}
|
|
4751
|
+
else if (event.kind === 'discovered') {
|
|
4752
|
+
if (!socketOpen)
|
|
4753
|
+
return;
|
|
4754
|
+
announced = true;
|
|
4755
|
+
if (missedChange) {
|
|
4756
|
+
missedChange = false;
|
|
4757
|
+
countAuthorityChange();
|
|
4758
|
+
}
|
|
4759
|
+
void sync('opened', event.discovery);
|
|
4760
|
+
for (const locator of replicas.keys())
|
|
4761
|
+
void heldAgain(locator);
|
|
4742
4762
|
}
|
|
4743
4763
|
else if (event.kind === 'closed')
|
|
4744
|
-
closedSocket(event.revoked, event.lost);
|
|
4745
|
-
else if (event.kind === '
|
|
4746
|
-
void
|
|
4764
|
+
closedSocket(event.revoked, event.lost, event.authorityChanged);
|
|
4765
|
+
else if (event.kind === 'caught-up')
|
|
4766
|
+
void caughtUpRooms(event.expired);
|
|
4747
4767
|
else if (event.kind === 'authority-changed')
|
|
4748
|
-
void
|
|
4768
|
+
void changeAuthority(event);
|
|
4749
4769
|
else
|
|
4750
|
-
void applyRoom(event
|
|
4751
|
-
}
|
|
4752
|
-
}
|
|
4753
|
-
async function heldRevisions(locator, paths) {
|
|
4754
|
-
const held = replicas.get(locator);
|
|
4755
|
-
if (held === undefined || paths.length === 0)
|
|
4756
|
-
return new Map();
|
|
4757
|
-
const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
|
|
4758
|
-
return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
|
|
4770
|
+
void applyRoom(event);
|
|
4771
|
+
});
|
|
4759
4772
|
}
|
|
4760
4773
|
function writesHeld() {
|
|
4761
4774
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -4828,8 +4841,6 @@ export function composeRuntime(options) {
|
|
|
4828
4841
|
notifyDiscovered();
|
|
4829
4842
|
if (probing !== null)
|
|
4830
4843
|
clearInterval(probing);
|
|
4831
|
-
for (const resolve of shownWaiters.splice(0))
|
|
4832
|
-
resolve();
|
|
4833
4844
|
connectionLive();
|
|
4834
4845
|
lockListeners.clear();
|
|
4835
4846
|
readinessWatches.clear();
|
|
@@ -4847,6 +4858,7 @@ export function composeRuntime(options) {
|
|
|
4847
4858
|
statusListeners.clear();
|
|
4848
4859
|
serviceModeListeners.clear();
|
|
4849
4860
|
memberListeners.clear();
|
|
4861
|
+
authorityChangeListeners.clear();
|
|
4850
4862
|
liveQueries.close();
|
|
4851
4863
|
queueSubscriptions.clear();
|
|
4852
4864
|
roomDraftListeners.clear();
|
|
@@ -4854,6 +4866,7 @@ export function composeRuntime(options) {
|
|
|
4854
4866
|
}
|
|
4855
4867
|
async function stop() {
|
|
4856
4868
|
stopping = true;
|
|
4869
|
+
inFlight.abort();
|
|
4857
4870
|
await recovery.retire();
|
|
4858
4871
|
await Promise.all(working);
|
|
4859
4872
|
await mutations;
|
|
@@ -4872,7 +4885,7 @@ export function composeRuntime(options) {
|
|
|
4872
4885
|
readyFromStore();
|
|
4873
4886
|
});
|
|
4874
4887
|
if (options.adapter.roomStream === undefined)
|
|
4875
|
-
void sync('unstreamed');
|
|
4888
|
+
void sync('unstreamed', null);
|
|
4876
4889
|
function planned(input, reach) {
|
|
4877
4890
|
const plan = planOf(input, roomFacts(reach), planReach(reach));
|
|
4878
4891
|
const named = plan.paths !== null &&
|
|
@@ -4914,58 +4927,56 @@ export function composeRuntime(options) {
|
|
|
4914
4927
|
stop();
|
|
4915
4928
|
};
|
|
4916
4929
|
}
|
|
4917
|
-
async function searched(input, reach
|
|
4930
|
+
async function searched(input, reach) {
|
|
4918
4931
|
const asked = searchPlanOf(input);
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
rooms: answered.coverage.rooms,
|
|
4963
|
-
conversations: named,
|
|
4964
|
-
overlay: answered.coverage.overlay,
|
|
4932
|
+
await ready;
|
|
4933
|
+
const search = options.adapter.search;
|
|
4934
|
+
if (search === undefined) {
|
|
4935
|
+
return {
|
|
4936
|
+
kind: 'undeclared',
|
|
4937
|
+
code: 'action-undeclared',
|
|
4938
|
+
fields: { name: 'search' },
|
|
4939
|
+
retryable: false,
|
|
4940
|
+
};
|
|
4941
|
+
}
|
|
4942
|
+
if (closed || refusal !== null || connectionOf() !== 'connected')
|
|
4943
|
+
return OFFLINE;
|
|
4944
|
+
const reached = reach === null ? null : new Set(reach.rooms);
|
|
4945
|
+
const named = asked.rooms === null
|
|
4946
|
+
? reached === null
|
|
4947
|
+
? null
|
|
4948
|
+
: [...reached]
|
|
4949
|
+
: asked.rooms.filter((room) => reached === null || reached.has(room));
|
|
4950
|
+
const rooms = named === null ? null : named.filter((room) => replicas.has(room)).sort();
|
|
4951
|
+
if (rooms !== null && rooms.length === 0)
|
|
4952
|
+
return { kind: 'found', documents: [] };
|
|
4953
|
+
try {
|
|
4954
|
+
await ensureAuthorization();
|
|
4955
|
+
}
|
|
4956
|
+
catch {
|
|
4957
|
+
return UNCERTAIN;
|
|
4958
|
+
}
|
|
4959
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
4960
|
+
return UNCERTAIN;
|
|
4961
|
+
journal.record('transfer search');
|
|
4962
|
+
try {
|
|
4963
|
+
const result = await search(authorization, { text: asked.text, rooms });
|
|
4964
|
+
if (result.kind === 'revoked-authorization')
|
|
4965
|
+
await refuse('revoked-authorization');
|
|
4966
|
+
journal.record(`outcome search ${result.kind}`);
|
|
4967
|
+
if (result.kind === 'rejected')
|
|
4968
|
+
return result;
|
|
4969
|
+
if (result.kind !== 'found')
|
|
4970
|
+
return failure(result);
|
|
4971
|
+
const kept = rooms === null ? null : new Set(rooms);
|
|
4972
|
+
return {
|
|
4973
|
+
kind: 'found',
|
|
4974
|
+
documents: result.documents.filter((hit) => kept === null || kept.has(hit.room)),
|
|
4965
4975
|
};
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4976
|
+
}
|
|
4977
|
+
catch {
|
|
4978
|
+
journal.record('outcome search transport-uncertainty');
|
|
4979
|
+
return UNCERTAIN;
|
|
4969
4980
|
}
|
|
4970
4981
|
}
|
|
4971
4982
|
const watch = (topic, input, deliver) => {
|
|
@@ -5017,7 +5028,7 @@ export function composeRuntime(options) {
|
|
|
5017
5028
|
};
|
|
5018
5029
|
}
|
|
5019
5030
|
if (topic === 'readiness') {
|
|
5020
|
-
return watchReadiness({ target: '', code: 'stored', space: null
|
|
5031
|
+
return watchReadiness({ target: '', code: 'stored', space: null }, null, deliver);
|
|
5021
5032
|
}
|
|
5022
5033
|
if (topic === 'rooms') {
|
|
5023
5034
|
unlistedRooms.add(deliver);
|
|
@@ -5062,6 +5073,13 @@ export function composeRuntime(options) {
|
|
|
5062
5073
|
memberListeners.delete(deliver);
|
|
5063
5074
|
};
|
|
5064
5075
|
}
|
|
5076
|
+
if (topic === 'authority-changes') {
|
|
5077
|
+
authorityChangeListeners.add(deliver);
|
|
5078
|
+
deliver(authorityChanges);
|
|
5079
|
+
return () => {
|
|
5080
|
+
authorityChangeListeners.delete(deliver);
|
|
5081
|
+
};
|
|
5082
|
+
}
|
|
5065
5083
|
if (topic === 'queue') {
|
|
5066
5084
|
const queue = { locator: input[0], notify: deliver };
|
|
5067
5085
|
queueSubscriptions.add(queue);
|
|
@@ -5090,7 +5108,7 @@ export function composeRuntime(options) {
|
|
|
5090
5108
|
});
|
|
5091
5109
|
},
|
|
5092
5110
|
query: ((input) => rowsQuery(planned(input, null))),
|
|
5093
|
-
search: (input) => searched(input, null
|
|
5111
|
+
search: (input) => searched(input, null),
|
|
5094
5112
|
types(locator) {
|
|
5095
5113
|
return {
|
|
5096
5114
|
collection: modelTypes(modelOf(locator)),
|
|
@@ -5215,7 +5233,7 @@ export function composeRuntime(options) {
|
|
|
5215
5233
|
authorization = undefined;
|
|
5216
5234
|
discoveryPaused = false;
|
|
5217
5235
|
if (options.adapter.roomStream === undefined)
|
|
5218
|
-
await sync('unstreamed');
|
|
5236
|
+
await sync('unstreamed', null);
|
|
5219
5237
|
else
|
|
5220
5238
|
await task(OPENING_LANE, ensureAuthorization);
|
|
5221
5239
|
},
|
|
@@ -5238,7 +5256,6 @@ export function composeRuntime(options) {
|
|
|
5238
5256
|
md,
|
|
5239
5257
|
seed,
|
|
5240
5258
|
stop,
|
|
5241
|
-
lose: loseRooms,
|
|
5242
5259
|
watch,
|
|
5243
5260
|
persisted(answer) {
|
|
5244
5261
|
if (answer === persistence)
|
|
@@ -5247,7 +5264,6 @@ export function composeRuntime(options) {
|
|
|
5247
5264
|
journal.record(`persistence ${answer}`);
|
|
5248
5265
|
queueReadiness();
|
|
5249
5266
|
},
|
|
5250
|
-
shown: screen,
|
|
5251
5267
|
within: (reach, port) => {
|
|
5252
5268
|
if (full)
|
|
5253
5269
|
void probed();
|