@agentshouse/mdruntime 0.1.0-cefbc8bb8a1c0 → 0.1.0-cf1e7fdb9c600
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 +69 -49
- package/dist/adapter.d.ts.map +1 -1
- package/dist/bootstrap.d.ts +5 -8
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +20 -21
- package/dist/chain.d.ts +2 -1
- package/dist/chain.d.ts.map +1 -1
- package/dist/client.d.ts +6 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +0 -1
- package/dist/collection.d.ts +2 -4
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +1 -17
- package/dist/compile.d.ts +1 -6
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +97 -281
- package/dist/connection.d.ts +5 -7
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +816 -846
- package/dist/context.d.ts +2 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +7 -0
- package/dist/conversation.d.ts +15 -13
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +133 -142
- package/dist/durability.d.ts +0 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/entry.d.ts +2 -2
- package/dist/entry.d.ts.map +1 -1
- package/dist/entry.js +14 -5
- package/dist/host.d.ts +3 -7
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +12 -26
- package/dist/index.d.ts +9 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/job.d.ts +14 -0
- package/dist/job.d.ts.map +1 -0
- package/dist/job.js +28 -0
- package/dist/live.d.ts +2 -1
- package/dist/live.d.ts.map +1 -1
- package/dist/live.js +10 -7
- package/dist/memory.d.ts +9 -4
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +132 -81
- 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 +1 -3
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +0 -12
- package/dist/observation.d.ts +2 -2
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +7 -2
- package/dist/outcome.d.ts +0 -3
- package/dist/outcome.d.ts.map +1 -1
- package/dist/query.d.ts +5 -209
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +3 -505
- package/dist/reach.d.ts +7 -7
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +31 -44
- package/dist/react.d.ts +3 -1
- 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/refresh.d.ts +7 -5
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +46 -6
- package/dist/replica.d.ts +2 -2
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +5 -5
- package/dist/room-draft.d.ts +2 -1
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +65 -52
- package/dist/rows.d.ts +3 -19
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +36 -164
- 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 +87 -124
- package/dist/tables.d.ts +34 -33
- package/dist/tables.d.ts.map +1 -1
- package/dist/tables.js +21 -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 +39 -97
- package/dist/windows.d.ts +1 -12
- package/dist/windows.d.ts.map +1 -1
- package/dist/windows.js +2 -75
- 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 -370
- 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 { bodied, identityOf, parseFrontmatter, plain, queryKey, 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 { planWithin, } 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, its full-text writes included, 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 = {
|
|
69
|
+
const NOTHING_HELD = { bytes: 0 };
|
|
72
70
|
const QUEUED_TEXT = 1 << 23;
|
|
73
|
-
// Bytes still arriving are reported at most this often,
|
|
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
|
}
|
|
@@ -109,6 +105,7 @@ export function composeRuntime(options) {
|
|
|
109
105
|
const usableWhenHeld = options.usableWhenHeld ?? [];
|
|
110
106
|
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
111
107
|
const pendings = new Map();
|
|
108
|
+
const landedAhead = new Map();
|
|
112
109
|
const seeds = new Map();
|
|
113
110
|
const seedWatchers = new Map();
|
|
114
111
|
const roomDrafts = new FencedMap(isFenced);
|
|
@@ -120,15 +117,24 @@ export function composeRuntime(options) {
|
|
|
120
117
|
const counted = new Map();
|
|
121
118
|
const unreadable = new Set();
|
|
122
119
|
const builtIn = builtInModel(roomTypes);
|
|
120
|
+
const typedPaths = options.adapter.typedPaths ?? [];
|
|
123
121
|
const models = new FencedMap(isFenced);
|
|
124
122
|
const liveModels = new FencedMap(isFenced);
|
|
125
123
|
const invalidModels = new Set();
|
|
126
124
|
const discovered = new Map();
|
|
127
|
-
|
|
125
|
+
let inFlight = new AbortController();
|
|
126
|
+
const refreshed = options.adapter.refresh?.bind(options.adapter);
|
|
127
|
+
const reads = {
|
|
128
|
+
bootstrap: (authorized, request) => readUntil(options.adapter.bootstrap(authorized, request), inFlight.signal),
|
|
129
|
+
...(refreshed === undefined
|
|
130
|
+
? {}
|
|
131
|
+
: {
|
|
132
|
+
refresh: (authorized, request) => readUntil(refreshed(authorized, request), inFlight.signal),
|
|
133
|
+
}),
|
|
134
|
+
reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
|
|
135
|
+
};
|
|
128
136
|
let roomsListed = false;
|
|
129
137
|
const scopedRefusals = new Map();
|
|
130
|
-
const sourceRefusals = new Map();
|
|
131
|
-
const declarationRefusals = new Set();
|
|
132
138
|
function modelOf(locator) {
|
|
133
139
|
return models.get(locator) ?? NO_MODEL;
|
|
134
140
|
}
|
|
@@ -138,7 +144,7 @@ export function composeRuntime(options) {
|
|
|
138
144
|
: modelOf(locator);
|
|
139
145
|
}
|
|
140
146
|
function composed(record) {
|
|
141
|
-
return compositionOf(record, builtIn);
|
|
147
|
+
return compositionOf(record, builtIn, typedPaths);
|
|
142
148
|
}
|
|
143
149
|
function compositionFor(record) {
|
|
144
150
|
return composed({ ...record, model: savedModel(record.locator) });
|
|
@@ -151,10 +157,10 @@ export function composeRuntime(options) {
|
|
|
151
157
|
let closed = false;
|
|
152
158
|
let stopping = false;
|
|
153
159
|
let withdrawStream;
|
|
154
|
-
const announced = new Map();
|
|
155
160
|
let underway = null;
|
|
156
161
|
let streamed = null;
|
|
157
162
|
let handed = false;
|
|
163
|
+
let naming = 0;
|
|
158
164
|
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
159
165
|
const repairing = new Set();
|
|
160
166
|
const calling = new Set();
|
|
@@ -165,12 +171,13 @@ export function composeRuntime(options) {
|
|
|
165
171
|
let recorded = false;
|
|
166
172
|
const stale = new Set();
|
|
167
173
|
let generation = 0;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
const reloadsDue = new Map();
|
|
174
|
+
const reloadsDue = new Set();
|
|
175
|
+
const highest = new Map();
|
|
176
|
+
const beyondHorizon = new Set();
|
|
172
177
|
const lanes = new Map();
|
|
173
|
-
|
|
178
|
+
const roomLanes = new Map();
|
|
179
|
+
let roomWork = 0;
|
|
180
|
+
const roomReads = { free: ROOM_READS, waiting: [] };
|
|
174
181
|
const working = new Set();
|
|
175
182
|
let mutations = Promise.resolve();
|
|
176
183
|
let gathering = null;
|
|
@@ -186,6 +193,8 @@ export function composeRuntime(options) {
|
|
|
186
193
|
let withdrawConversations;
|
|
187
194
|
let currencyQueued = null;
|
|
188
195
|
let socketOpen = false;
|
|
196
|
+
let announced = false;
|
|
197
|
+
let missedChange = false;
|
|
189
198
|
const confirmed = new Set();
|
|
190
199
|
let lockGrace = null;
|
|
191
200
|
const lockListeners = new Set();
|
|
@@ -195,6 +204,8 @@ export function composeRuntime(options) {
|
|
|
195
204
|
const serviceModeListeners = new Set();
|
|
196
205
|
let member = null;
|
|
197
206
|
const memberListeners = new Set();
|
|
207
|
+
let authorityChanges = 0;
|
|
208
|
+
const authorityChangeListeners = new Set();
|
|
198
209
|
const unpreparedDemand = new Set();
|
|
199
210
|
const unpreparable = new Set();
|
|
200
211
|
let preparing = false;
|
|
@@ -224,11 +235,9 @@ export function composeRuntime(options) {
|
|
|
224
235
|
const streaming = new Set();
|
|
225
236
|
const underived = new Map();
|
|
226
237
|
const unindexed = new Map();
|
|
227
|
-
const
|
|
238
|
+
const startedOver = new Map();
|
|
228
239
|
let arrivalReported = 0;
|
|
229
|
-
let
|
|
230
|
-
const shownWaiters = [];
|
|
231
|
-
let streams = { generation, free: STREAMS_HELD };
|
|
240
|
+
let streamsFree = STREAMS_HELD;
|
|
232
241
|
let queuedText = 0;
|
|
233
242
|
const installWaiters = [];
|
|
234
243
|
const stageBudget = {
|
|
@@ -245,26 +254,12 @@ export function composeRuntime(options) {
|
|
|
245
254
|
};
|
|
246
255
|
const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
|
|
247
256
|
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
257
|
call: (name, input) => invokeAction(name, input),
|
|
258
|
+
history: (request) => readConversationHistory(request),
|
|
263
259
|
durably,
|
|
264
260
|
stream: () => options.adapter.conversations,
|
|
265
261
|
publish: publishChangeSet,
|
|
266
262
|
online: () => connectionOf() !== 'unreachable' && refusal === null,
|
|
267
|
-
listed: () => roomsListed,
|
|
268
263
|
full: () => full,
|
|
269
264
|
closed: () => closed,
|
|
270
265
|
unanswered: resendLater,
|
|
@@ -272,6 +267,8 @@ export function composeRuntime(options) {
|
|
|
272
267
|
});
|
|
273
268
|
const journal = createLog();
|
|
274
269
|
const keptFiles = openedFiles(database, isFenced);
|
|
270
|
+
const holding = new Map();
|
|
271
|
+
const unholdable = new Map();
|
|
275
272
|
let readyFromStore = () => { };
|
|
276
273
|
const ready = new Promise((resolve) => {
|
|
277
274
|
readyFromStore = resolve;
|
|
@@ -361,9 +358,9 @@ export function composeRuntime(options) {
|
|
|
361
358
|
return value === undefined ? null : structuredClone(value);
|
|
362
359
|
}
|
|
363
360
|
async function writtenRoomDraft(room, path, revision, input, next) {
|
|
364
|
-
const parts = readParts(input);
|
|
361
|
+
const parts = readParts(input, path);
|
|
365
362
|
if (path === '' || normalize(path) !== path)
|
|
366
|
-
throw runtimeError('room-draft-malformed');
|
|
363
|
+
throw runtimeError('room-draft-malformed', `room-draft-malformed at ${path}: a path inside the Room, such as notes/a.md`);
|
|
367
364
|
const local = await openLocal();
|
|
368
365
|
const current = roomDrafts.get(room)?.get(path);
|
|
369
366
|
if ((current?.revision ?? null) !== revision) {
|
|
@@ -412,7 +409,7 @@ export function composeRuntime(options) {
|
|
|
412
409
|
const changed = new Map();
|
|
413
410
|
for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
|
|
414
411
|
for (const { batch, state } of entriesOf(room)) {
|
|
415
|
-
if (state === '
|
|
412
|
+
if (state === 'local')
|
|
416
413
|
continue;
|
|
417
414
|
for (const { path } of batch.changes)
|
|
418
415
|
changed.set(`${room}\u0000${path}`, { room, path });
|
|
@@ -433,16 +430,9 @@ export function composeRuntime(options) {
|
|
|
433
430
|
returned: structuredClone(record.returned),
|
|
434
431
|
});
|
|
435
432
|
}
|
|
436
|
-
for (const [room, refusal] of sourceRefusals)
|
|
437
|
-
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
438
433
|
attentionRequired.sort((left, right) => {
|
|
439
434
|
if (left.room !== right.room)
|
|
440
435
|
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
436
|
if (left.path !== right.path)
|
|
447
437
|
return left.path < right.path ? -1 : 1;
|
|
448
438
|
return 0;
|
|
@@ -597,9 +587,6 @@ export function composeRuntime(options) {
|
|
|
597
587
|
if (localSaves)
|
|
598
588
|
return Object.freeze({ batch, state: 'local' });
|
|
599
589
|
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
590
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
604
591
|
}
|
|
605
592
|
function entriesOf(locator) {
|
|
@@ -673,11 +660,7 @@ export function composeRuntime(options) {
|
|
|
673
660
|
})();
|
|
674
661
|
}
|
|
675
662
|
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
|
-
]);
|
|
663
|
+
return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
681
664
|
}
|
|
682
665
|
function reachedRooms(plan) {
|
|
683
666
|
const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
|
|
@@ -716,7 +699,7 @@ export function composeRuntime(options) {
|
|
|
716
699
|
coverage.push({
|
|
717
700
|
room: locator,
|
|
718
701
|
generation: replica.record.transfer,
|
|
719
|
-
current: !(catching.has(locator) ||
|
|
702
|
+
current: !(catching.has(locator) || transfers.has(locator)),
|
|
720
703
|
});
|
|
721
704
|
read.push({ locator, key: replica.record.key });
|
|
722
705
|
for (const batch of replica.queue)
|
|
@@ -743,8 +726,6 @@ export function composeRuntime(options) {
|
|
|
743
726
|
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
744
727
|
}
|
|
745
728
|
function roomState(locator) {
|
|
746
|
-
if (sourceRefusals.has(locator))
|
|
747
|
-
return 'refused';
|
|
748
729
|
const listedRoom = discovered.get(locator);
|
|
749
730
|
if (roomsListed &&
|
|
750
731
|
(listedRoom === undefined ||
|
|
@@ -759,7 +740,7 @@ export function composeRuntime(options) {
|
|
|
759
740
|
}
|
|
760
741
|
function conversationCoverage(reach) {
|
|
761
742
|
const declared = reach === null || reach.conversations;
|
|
762
|
-
if (!declared || options.adapter.conversations === undefined
|
|
743
|
+
if (!declared || options.adapter.conversations === undefined)
|
|
763
744
|
return 'undeclared';
|
|
764
745
|
if (!conversations.known() || !conversations.current())
|
|
765
746
|
return 'acquiring';
|
|
@@ -774,79 +755,50 @@ export function composeRuntime(options) {
|
|
|
774
755
|
? 'connected'
|
|
775
756
|
: 'connecting';
|
|
776
757
|
}
|
|
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
|
-
});
|
|
758
|
+
function bytesOf(room, underway) {
|
|
759
|
+
const transfer = transfers.get(room);
|
|
760
|
+
if (transfer !== undefined) {
|
|
761
|
+
const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
|
|
762
|
+
return {
|
|
763
|
+
done: transfer.done.bytes - Math.ceil(owed),
|
|
764
|
+
total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
|
|
768
|
+
return { done: held.bytes, total: arrivals.get(room) ?? held.bytes };
|
|
793
769
|
}
|
|
794
770
|
function installationOf(coverage) {
|
|
795
771
|
const rooms = { done: 0, total: 0 };
|
|
796
|
-
const parts = { done: 0, total: 0 };
|
|
797
772
|
const bytes = { done: 0, total: 0 };
|
|
798
773
|
let installing = 0;
|
|
799
774
|
let downloading = false;
|
|
800
|
-
let checking = 0;
|
|
801
775
|
for (const { room, state } of coverage) {
|
|
802
776
|
if (state === 'unadmitted')
|
|
803
777
|
continue;
|
|
804
778
|
rooms.total += 1;
|
|
805
779
|
if (state === 'complete')
|
|
806
780
|
rooms.done += 1;
|
|
807
|
-
const transfer = transfers.get(room);
|
|
808
781
|
const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
|
|
809
782
|
if (underway)
|
|
810
783
|
installing += 1;
|
|
811
784
|
if (underway && (streaming.has(room) || !arrivals.has(room)))
|
|
812
785
|
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;
|
|
786
|
+
const now = bytesOf(room, underway);
|
|
787
|
+
const earlier = underway ? startedOver.get(room) : undefined;
|
|
788
|
+
bytes.done += Math.max(now.done, earlier?.done ?? 0);
|
|
789
|
+
bytes.total += Math.max(now.total, earlier?.total ?? 0);
|
|
831
790
|
}
|
|
832
791
|
const step = installing === 0
|
|
833
792
|
? null
|
|
834
793
|
: downloading
|
|
835
794
|
? 'downloading'
|
|
836
|
-
: bytes.done
|
|
795
|
+
: bytes.done < bytes.total
|
|
837
796
|
? 'writing'
|
|
838
797
|
: 'replaying';
|
|
839
|
-
return {
|
|
840
|
-
step,
|
|
841
|
-
paused: installing > 0 && !onScreen ? 'hidden' : null,
|
|
842
|
-
rooms,
|
|
843
|
-
parts,
|
|
844
|
-
bytes,
|
|
845
|
-
};
|
|
798
|
+
return { step, rooms, bytes };
|
|
846
799
|
}
|
|
847
800
|
function readinessFor(port, reach) {
|
|
848
801
|
const required = requiredRooms(reach);
|
|
849
|
-
const search = port.search === 'required';
|
|
850
802
|
const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
|
|
851
803
|
const preparing = coverage
|
|
852
804
|
.filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
|
|
@@ -859,14 +811,12 @@ export function composeRuntime(options) {
|
|
|
859
811
|
listed: required.listed,
|
|
860
812
|
rooms: coverage,
|
|
861
813
|
conversations: conversationCoverage(reach),
|
|
862
|
-
search: search ? 'required' : 'unused',
|
|
863
814
|
},
|
|
864
815
|
persistence,
|
|
865
816
|
connection: connectionOf(),
|
|
866
817
|
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
867
|
-
space: full ? 'full' :
|
|
818
|
+
space: full ? 'full' : null,
|
|
868
819
|
installation: installationOf(coverage),
|
|
869
|
-
refused: refusedIn(required.rooms),
|
|
870
820
|
lost: storageLoss !== null,
|
|
871
821
|
established: targets[port.target]?.established === true ||
|
|
872
822
|
(usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
|
|
@@ -954,8 +904,12 @@ export function composeRuntime(options) {
|
|
|
954
904
|
unpreparable.clear();
|
|
955
905
|
journal.record('space freed');
|
|
956
906
|
queueReadiness();
|
|
957
|
-
if (
|
|
958
|
-
|
|
907
|
+
if (closed || authorization === undefined)
|
|
908
|
+
return;
|
|
909
|
+
if (options.adapter.roomStream === undefined)
|
|
910
|
+
void sync('unstreamed', null);
|
|
911
|
+
else
|
|
912
|
+
void resynchronize();
|
|
959
913
|
}
|
|
960
914
|
function seedRoomHeld(room, version) {
|
|
961
915
|
return indexed(room) && reachedBy(room, version);
|
|
@@ -995,7 +949,24 @@ export function composeRuntime(options) {
|
|
|
995
949
|
function sameCover(left, right) {
|
|
996
950
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
997
951
|
}
|
|
952
|
+
async function unheldRows(local, plan, rows, rooms) {
|
|
953
|
+
if (plan.shape.kind !== 'documents' || !bodied(plan.shape))
|
|
954
|
+
return [];
|
|
955
|
+
if (!('documents' in rows) || !textOnline())
|
|
956
|
+
return [];
|
|
957
|
+
const named = new Map();
|
|
958
|
+
for (const { room, path, textHeld } of rows.documents) {
|
|
959
|
+
if (textHeld === false)
|
|
960
|
+
named.set(room, [...(named.get(room) ?? []), path]);
|
|
961
|
+
}
|
|
962
|
+
const unheld = [];
|
|
963
|
+
for (const { locator, key } of rooms) {
|
|
964
|
+
unheld.push(...(await unheldIn(local, locator, key, named.get(locator) ?? [])));
|
|
965
|
+
}
|
|
966
|
+
return unheld.filter((one) => unholdable.get(textKey(one)) !== one.revision);
|
|
967
|
+
}
|
|
998
968
|
async function coveredRows(plan, cover) {
|
|
969
|
+
const asked = new Set();
|
|
999
970
|
for (;;) {
|
|
1000
971
|
const before = cover();
|
|
1001
972
|
if ('pending' in before)
|
|
@@ -1006,8 +977,14 @@ export function composeRuntime(options) {
|
|
|
1006
977
|
const current = () => local.changes(keys) === changes;
|
|
1007
978
|
const rows = await windowRows(local, plan, before.rooms, current);
|
|
1008
979
|
const after = cover();
|
|
1009
|
-
if (rows
|
|
980
|
+
if (rows === null || !sameCover(before, after))
|
|
981
|
+
continue;
|
|
982
|
+
const unheld = (await unheldRows(local, plan, rows, before.rooms)).filter((one) => !asked.has(textKey(one)));
|
|
983
|
+
if (unheld.length === 0)
|
|
1010
984
|
return { rows, cover: before, current };
|
|
985
|
+
for (const one of unheld)
|
|
986
|
+
asked.add(textKey(one));
|
|
987
|
+
await holdTexts(unheld);
|
|
1011
988
|
}
|
|
1012
989
|
}
|
|
1013
990
|
async function windowRows(local, plan, rooms, unchanged) {
|
|
@@ -1103,6 +1080,8 @@ export function composeRuntime(options) {
|
|
|
1103
1080
|
}
|
|
1104
1081
|
async function refuse(reason, provider = null) {
|
|
1105
1082
|
discoveryPaused = true;
|
|
1083
|
+
inFlight.abort();
|
|
1084
|
+
inFlight = new AbortController();
|
|
1106
1085
|
try {
|
|
1107
1086
|
if (reason === 'revoked-authorization') {
|
|
1108
1087
|
await exclusive(async () => {
|
|
@@ -1187,10 +1166,10 @@ export function composeRuntime(options) {
|
|
|
1187
1166
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
1188
1167
|
roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
|
|
1189
1168
|
synchronizationAvailable,
|
|
1190
|
-
refusal: scopedRefusals.get(room.locator) ??
|
|
1169
|
+
refusal: scopedRefusals.get(room.locator) ?? null,
|
|
1191
1170
|
});
|
|
1192
1171
|
}
|
|
1193
|
-
async function applyDiscovery(found, started,
|
|
1172
|
+
async function applyDiscovery(found, started, left) {
|
|
1194
1173
|
const lockedBefore = lockedRooms();
|
|
1195
1174
|
for (const room of found) {
|
|
1196
1175
|
const fence = fences.get(room.locator);
|
|
@@ -1204,18 +1183,11 @@ export function composeRuntime(options) {
|
|
|
1204
1183
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
1205
1184
|
for (const locator of listedLocators)
|
|
1206
1185
|
scopedRefusals.delete(locator);
|
|
1207
|
-
if (
|
|
1186
|
+
if (left === undefined)
|
|
1208
1187
|
discovered.clear();
|
|
1209
|
-
else
|
|
1210
|
-
for (const locator of covered)
|
|
1211
|
-
discovered.delete(locator);
|
|
1212
1188
|
for (const room of listed)
|
|
1213
1189
|
discovered.set(room.locator, room);
|
|
1214
|
-
|
|
1215
|
-
if (options.adapter.exhaustiveDiscovery === true)
|
|
1216
|
-
await loseRooms(unlisted);
|
|
1217
|
-
else
|
|
1218
|
-
unlistedKept(unlisted);
|
|
1190
|
+
await loseRooms(left ?? unlistedHeld(new Set(found.map((room) => room.locator))));
|
|
1219
1191
|
for (const room of listed) {
|
|
1220
1192
|
const held = replicas.get(room.locator)?.record;
|
|
1221
1193
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -1246,15 +1218,6 @@ export function composeRuntime(options) {
|
|
|
1246
1218
|
]),
|
|
1247
1219
|
].filter((locator) => !listed.has(locator));
|
|
1248
1220
|
}
|
|
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
1221
|
function fence(locators) {
|
|
1259
1222
|
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
1260
1223
|
const removed = fresh.map((locator) => ({
|
|
@@ -1265,24 +1228,26 @@ export function composeRuntime(options) {
|
|
|
1265
1228
|
fences.set(locator, { at: discoveries, erased: false });
|
|
1266
1229
|
lastFenced.set(locator, ++fencings);
|
|
1267
1230
|
confirmed.delete(locator);
|
|
1268
|
-
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
1231
|
+
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []) {
|
|
1269
1232
|
pendings.delete(pendingKey(locator, batch.id));
|
|
1233
|
+
landedAhead.delete(pendingKey(locator, batch.id));
|
|
1234
|
+
}
|
|
1270
1235
|
replicas.delete(locator);
|
|
1236
|
+
highest.delete(locator);
|
|
1237
|
+
beyondHorizon.delete(locator);
|
|
1271
1238
|
queuesWithoutReplica.delete(locator);
|
|
1272
1239
|
rooms.delete(locator);
|
|
1273
1240
|
roomDrafts.delete(locator);
|
|
1274
1241
|
attachedFiles.delete(locator);
|
|
1275
1242
|
untransferred(locator);
|
|
1243
|
+
startedOver.delete(locator);
|
|
1276
1244
|
catching.delete(locator);
|
|
1277
1245
|
discovered.delete(locator);
|
|
1278
|
-
announced.delete(locator);
|
|
1279
1246
|
unreadable.delete(locator);
|
|
1280
1247
|
stale.delete(locator);
|
|
1281
1248
|
repairable.delete(locator);
|
|
1282
1249
|
unpreparable.delete(locator);
|
|
1283
1250
|
scopedRefusals.delete(locator);
|
|
1284
|
-
sourceRefusals.delete(locator);
|
|
1285
|
-
declarationRefusals.delete(locator);
|
|
1286
1251
|
models.delete(locator);
|
|
1287
1252
|
liveModels.delete(locator);
|
|
1288
1253
|
invalidModels.delete(locator);
|
|
@@ -1373,6 +1338,7 @@ export function composeRuntime(options) {
|
|
|
1373
1338
|
transfers.clear();
|
|
1374
1339
|
arrivals.clear();
|
|
1375
1340
|
streaming.clear();
|
|
1341
|
+
startedOver.clear();
|
|
1376
1342
|
limits = held.providerLimits;
|
|
1377
1343
|
capabilities = held.capabilities;
|
|
1378
1344
|
member = held.member;
|
|
@@ -1382,8 +1348,9 @@ export function composeRuntime(options) {
|
|
|
1382
1348
|
presentations.clear();
|
|
1383
1349
|
for (const record of held.presentation)
|
|
1384
1350
|
presentations.set(record.app, record.value);
|
|
1385
|
-
conversations.hydrate(heldConversations, chatDrafts, held.conversationList);
|
|
1351
|
+
conversations.hydrate(heldConversations, chatDrafts, held.conversationList, held.conversationCursor);
|
|
1386
1352
|
pendings.clear();
|
|
1353
|
+
landedAhead.clear();
|
|
1387
1354
|
for (const queue of queues) {
|
|
1388
1355
|
for (const { batch, state } of queue.pending) {
|
|
1389
1356
|
pendings.set(pendingKey(queue.locator, batch.id), state);
|
|
@@ -1534,6 +1501,16 @@ export function composeRuntime(options) {
|
|
|
1534
1501
|
}
|
|
1535
1502
|
const REWRITE_LINKS = ['field', 'body'];
|
|
1536
1503
|
const CHECKED_LINKS = ['field', 'section'];
|
|
1504
|
+
const BODY_LINKS = ['body'];
|
|
1505
|
+
async function textsNeeded(local, record, named) {
|
|
1506
|
+
const paths = [...new Set(named.map((one) => (typeof one === 'string' ? one : one.path)))];
|
|
1507
|
+
const unheld = await unheldIn(local, record.locator, record.key, paths);
|
|
1508
|
+
if (unheld.length === 0)
|
|
1509
|
+
return null;
|
|
1510
|
+
await holdTexts(unheld);
|
|
1511
|
+
const [first] = await unheldIn(local, record.locator, record.key, unheld.map(({ path }) => path));
|
|
1512
|
+
return first?.path ?? null;
|
|
1513
|
+
}
|
|
1537
1514
|
async function sliceAt(record, view, queue, extra = [], moved = []) {
|
|
1538
1515
|
const local = await openLocal();
|
|
1539
1516
|
const needed = new Set([...touchedBy(queue), ...extra]);
|
|
@@ -1634,8 +1611,6 @@ export function composeRuntime(options) {
|
|
|
1634
1611
|
function unpairedIn(locator, batch) {
|
|
1635
1612
|
if (!queueIn(locator).some((held) => held.id === batch.id))
|
|
1636
1613
|
return false;
|
|
1637
|
-
if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
|
|
1638
|
-
return false;
|
|
1639
1614
|
return !leavingIn(batch, locator) || withdrawable(locator)(batch);
|
|
1640
1615
|
}
|
|
1641
1616
|
async function together(local, settlements) {
|
|
@@ -1678,10 +1653,16 @@ export function composeRuntime(options) {
|
|
|
1678
1653
|
? read
|
|
1679
1654
|
: await stepped(slicingWith(read, overrides.set, overrides.removed));
|
|
1680
1655
|
const composition = composed(record);
|
|
1681
|
-
const
|
|
1682
|
-
|
|
1656
|
+
const shown = queue.filter((batch) => {
|
|
1657
|
+
const held = landedAhead.get(pendingKey(locator, batch.id));
|
|
1658
|
+
return (held !== undefined &&
|
|
1659
|
+
Object.entries(held).some(([path, revision]) => confirmedLayer.revisions[path] !== revision));
|
|
1660
|
+
});
|
|
1661
|
+
const unshown = queue.filter((batch) => !shown.includes(batch));
|
|
1662
|
+
const { withdrawn, kept } = await stepped(standing(confirmedLayer, unshown, withdrawable(locator), localSaves, locator, composition));
|
|
1663
|
+
const founded = kept.filter((batch) => !unshown.includes(batch));
|
|
1683
1664
|
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
1684
|
-
const unqueued =
|
|
1665
|
+
const unqueued = shown.map((batch) => ({ batch, landed: true }));
|
|
1685
1666
|
for (const batch of withdrawn) {
|
|
1686
1667
|
unqueued.push({ batch, landed: await landedWhole(locator, batch, confirmedLayer) });
|
|
1687
1668
|
}
|
|
@@ -1746,8 +1727,10 @@ export function composeRuntime(options) {
|
|
|
1746
1727
|
commit,
|
|
1747
1728
|
apply: (answer) => {
|
|
1748
1729
|
for (const batch of previousQueue) {
|
|
1749
|
-
if (
|
|
1750
|
-
|
|
1730
|
+
if (staying.has(batch.id))
|
|
1731
|
+
continue;
|
|
1732
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
1733
|
+
landedAhead.delete(pendingKey(locator, batch.id));
|
|
1751
1734
|
}
|
|
1752
1735
|
for (const draft of written.values())
|
|
1753
1736
|
rememberRoomDraft(draft);
|
|
@@ -1804,28 +1787,34 @@ export function composeRuntime(options) {
|
|
|
1804
1787
|
models.set(locator, state);
|
|
1805
1788
|
return { collection: state };
|
|
1806
1789
|
}
|
|
1807
|
-
function publish(local, room,
|
|
1790
|
+
function publish(local, room, reached, landing, live) {
|
|
1808
1791
|
const landed = exclusive(async () => {
|
|
1809
1792
|
if (!live()) {
|
|
1810
1793
|
caughtUpTo(room.locator);
|
|
1811
1794
|
return true;
|
|
1812
1795
|
}
|
|
1813
1796
|
const prior = replicas.get(room.locator);
|
|
1814
|
-
if (landing.
|
|
1815
|
-
(
|
|
1816
|
-
|
|
1797
|
+
if (!stands(room.locator, landing.from)) {
|
|
1798
|
+
if (landing.kind === 'refreshed') {
|
|
1799
|
+
await local.unrefresh(reached.key);
|
|
1800
|
+
return false;
|
|
1801
|
+
}
|
|
1802
|
+
caughtUpTo(room.locator);
|
|
1803
|
+
await abandoned(local, room.locator);
|
|
1817
1804
|
return false;
|
|
1818
1805
|
}
|
|
1806
|
+
const record = landing.kind === 'refreshed' && prior !== undefined
|
|
1807
|
+
? {
|
|
1808
|
+
...prior.record,
|
|
1809
|
+
roomVersion: reached.roomVersion,
|
|
1810
|
+
...(landing.remodelled ? { model: reached.model } : {}),
|
|
1811
|
+
}
|
|
1812
|
+
: reached;
|
|
1819
1813
|
adoptModels(room.locator, record);
|
|
1820
1814
|
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
1815
|
let set;
|
|
1827
1816
|
try {
|
|
1828
|
-
set = await settle(local, room.locator, prior, record, queue
|
|
1817
|
+
set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
|
|
1829
1818
|
? {
|
|
1830
1819
|
room: true,
|
|
1831
1820
|
view: 'refreshed',
|
|
@@ -1847,25 +1836,26 @@ export function composeRuntime(options) {
|
|
|
1847
1836
|
return true;
|
|
1848
1837
|
}
|
|
1849
1838
|
if (landing.kind === 'bootstrapped') {
|
|
1839
|
+
highest.delete(room.locator);
|
|
1840
|
+
beyondHorizon.delete(room.locator);
|
|
1850
1841
|
untransferred(room.locator);
|
|
1842
|
+
startedOver.delete(room.locator);
|
|
1851
1843
|
catching.delete(room.locator);
|
|
1852
1844
|
if (prior !== undefined && prior.record.key !== record.key)
|
|
1853
1845
|
sweep();
|
|
1846
|
+
name(room.locator);
|
|
1854
1847
|
}
|
|
1855
|
-
for (const batch of accepted)
|
|
1856
|
-
pendings.delete(pendingKey(room.locator, batch.id));
|
|
1857
1848
|
stale.delete(room.locator);
|
|
1858
1849
|
unpreparable.delete(room.locator);
|
|
1859
1850
|
if (unreadable.delete(room.locator))
|
|
1860
1851
|
scopedRefusals.delete(room.locator);
|
|
1861
|
-
sourceRefusals.delete(room.locator);
|
|
1862
|
-
declarationRefusals.delete(room.locator);
|
|
1863
1852
|
list(room, true);
|
|
1864
1853
|
publishChangeSet(set);
|
|
1854
|
+
if (landing.kind === 'bootstrapped')
|
|
1855
|
+
void heldAgain(room.locator);
|
|
1865
1856
|
await unpair(local);
|
|
1866
1857
|
moveTo({ ...status, ...attention() });
|
|
1867
|
-
|
|
1868
|
-
if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1858
|
+
if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1869
1859
|
submitLater(room.locator);
|
|
1870
1860
|
}
|
|
1871
1861
|
return true;
|
|
@@ -1877,7 +1867,7 @@ export function composeRuntime(options) {
|
|
|
1877
1867
|
const replica = replicas.get(locator);
|
|
1878
1868
|
const room = rooms.get(locator);
|
|
1879
1869
|
if (replica === undefined || room === undefined) {
|
|
1880
|
-
throw runtimeError('room
|
|
1870
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
1881
1871
|
}
|
|
1882
1872
|
return { local, replica, room };
|
|
1883
1873
|
}
|
|
@@ -1939,6 +1929,19 @@ export function composeRuntime(options) {
|
|
|
1939
1929
|
const referring = removed.length === 0
|
|
1940
1930
|
? []
|
|
1941
1931
|
: (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
|
|
1932
|
+
const unheld = await textsNeeded(local, record, [
|
|
1933
|
+
...paths,
|
|
1934
|
+
...referring,
|
|
1935
|
+
...(moved.length === 0
|
|
1936
|
+
? []
|
|
1937
|
+
: await local.referrers(record.key, 'confirmed', moved, REWRITE_LINKS)),
|
|
1938
|
+
...(removed.length === 0
|
|
1939
|
+
? []
|
|
1940
|
+
: await local.referrers(record.key, 'confirmed', removed, BODY_LINKS)),
|
|
1941
|
+
]);
|
|
1942
|
+
if (unheld !== null) {
|
|
1943
|
+
return { refusal: { code: 'transport-uncertainty', retryable: true, file: unheld } };
|
|
1944
|
+
}
|
|
1942
1945
|
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
|
|
1943
1946
|
const { visible } = await replayed(record, confirmedLayer, replica.queue);
|
|
1944
1947
|
const composition = composed(record);
|
|
@@ -1969,18 +1972,6 @@ export function composeRuntime(options) {
|
|
|
1969
1972
|
return undefined;
|
|
1970
1973
|
return entry.binary !== null ? null : entry.content;
|
|
1971
1974
|
}
|
|
1972
|
-
function walkOf(local, key, view) {
|
|
1973
|
-
return async (after) => {
|
|
1974
|
-
const page = await local.walk(key, view, after, WALK_BYTES);
|
|
1975
|
-
return {
|
|
1976
|
-
entries: page.entries.map(({ path, content, binary }) => ({
|
|
1977
|
-
path,
|
|
1978
|
-
content: binary !== null ? null : content,
|
|
1979
|
-
})),
|
|
1980
|
-
next: page.next,
|
|
1981
|
-
};
|
|
1982
|
-
};
|
|
1983
|
-
}
|
|
1984
1975
|
function lookupOf(local, key, view) {
|
|
1985
1976
|
return async (paths) => {
|
|
1986
1977
|
if (paths.length === 0)
|
|
@@ -1997,37 +1988,6 @@ export function composeRuntime(options) {
|
|
|
1997
1988
|
const read = parseFrontmatter(entry.content ?? '');
|
|
1998
1989
|
return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
|
|
1999
1990
|
}
|
|
2000
|
-
function overriddenWalk(walk, overrides) {
|
|
2001
|
-
const sorted = [...overrides.keys()].sort();
|
|
2002
|
-
const beyond = (path) => {
|
|
2003
|
-
if (path === null)
|
|
2004
|
-
return 0;
|
|
2005
|
-
let low = 0;
|
|
2006
|
-
let high = sorted.length;
|
|
2007
|
-
while (low < high) {
|
|
2008
|
-
const middle = (low + high) >>> 1;
|
|
2009
|
-
if (sorted[middle] <= path)
|
|
2010
|
-
low = middle + 1;
|
|
2011
|
-
else
|
|
2012
|
-
high = middle;
|
|
2013
|
-
}
|
|
2014
|
-
return low;
|
|
2015
|
-
};
|
|
2016
|
-
return async (after) => {
|
|
2017
|
-
const page = await walk(after);
|
|
2018
|
-
const end = page.next;
|
|
2019
|
-
const added = sorted
|
|
2020
|
-
.slice(beyond(after), end === null ? sorted.length : beyond(end))
|
|
2021
|
-
.flatMap((path) => {
|
|
2022
|
-
const entry = overrides.get(path);
|
|
2023
|
-
return entry === null || entry === undefined
|
|
2024
|
-
? []
|
|
2025
|
-
: [{ path, content: entry.binary !== null ? null : entry.content }];
|
|
2026
|
-
});
|
|
2027
|
-
const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
|
|
2028
|
-
return { entries, next: end };
|
|
2029
|
-
};
|
|
2030
|
-
}
|
|
2031
1991
|
function overriddenLookup(lookup, overrides) {
|
|
2032
1992
|
return async (paths) => {
|
|
2033
1993
|
const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
|
|
@@ -2045,9 +2005,8 @@ export function composeRuntime(options) {
|
|
|
2045
2005
|
}
|
|
2046
2006
|
function validatorOf(record, confirmedLayer) {
|
|
2047
2007
|
return async (resulting, touched) => {
|
|
2048
|
-
const composition = compositionFor(record);
|
|
2049
2008
|
const local = await openLocal();
|
|
2050
|
-
const
|
|
2009
|
+
const changes = [];
|
|
2051
2010
|
const overrides = new Map();
|
|
2052
2011
|
const pace = paced();
|
|
2053
2012
|
for (const path of new Set([...confirmedLayer.held, ...touched])) {
|
|
@@ -2058,19 +2017,13 @@ export function composeRuntime(options) {
|
|
|
2058
2017
|
continue;
|
|
2059
2018
|
overrides.set(path, now);
|
|
2060
2019
|
if (textOf(was) !== textOf(now))
|
|
2061
|
-
|
|
2020
|
+
changes.push({ path, was: textOf(was), now: textOf(now) });
|
|
2062
2021
|
}
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
texts: textsOf(local, record.key, 'confirmed'),
|
|
2069
|
-
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2070
|
-
}));
|
|
2071
|
-
if (clean)
|
|
2072
|
-
return { verdict: 'accept' };
|
|
2073
|
-
return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
|
|
2022
|
+
return validatedNeighbourhood(changes, compositionFor(record), {
|
|
2023
|
+
lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
|
|
2024
|
+
texts: textsOf(local, record.key, 'confirmed'),
|
|
2025
|
+
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2026
|
+
});
|
|
2074
2027
|
};
|
|
2075
2028
|
}
|
|
2076
2029
|
function textsOf(local, key, view) {
|
|
@@ -2147,15 +2100,19 @@ export function composeRuntime(options) {
|
|
|
2147
2100
|
}
|
|
2148
2101
|
const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
|
|
2149
2102
|
const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
|
|
2103
|
+
if ('refusal' in sourceSaving)
|
|
2104
|
+
return { outcome: 'refused', ...sourceSaving.refusal };
|
|
2150
2105
|
const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
|
|
2151
2106
|
if (changed.length > 0)
|
|
2152
2107
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2153
2108
|
const target = replicas.get(destination);
|
|
2154
2109
|
const listedRoom = rooms.get(destination);
|
|
2155
2110
|
if (target === undefined || listedRoom === undefined) {
|
|
2156
|
-
throw runtimeError('room
|
|
2111
|
+
throw runtimeError('unknown-room', `Unknown Room ${destination}`);
|
|
2157
2112
|
}
|
|
2158
2113
|
const arriving = await savingIn(target, [], moves.map(({ to }) => to));
|
|
2114
|
+
if ('refusal' in arriving)
|
|
2115
|
+
return { outcome: 'refused', ...arriving.refusal };
|
|
2159
2116
|
const built = await buildMove({
|
|
2160
2117
|
source: await sideOf(locator, replica, room, sourceSaving),
|
|
2161
2118
|
destination: await sideOf(destination, target, listedRoom, arriving),
|
|
@@ -2216,10 +2173,10 @@ export function composeRuntime(options) {
|
|
|
2216
2173
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2217
2174
|
const { answer, touched } = committedBoth;
|
|
2218
2175
|
if (answer.fenced !== null) {
|
|
2219
|
-
throw runtimeError('room
|
|
2176
|
+
throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
|
|
2220
2177
|
}
|
|
2221
2178
|
if (fences.has(locator) || fences.has(destination)) {
|
|
2222
|
-
throw runtimeError('room
|
|
2179
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
2223
2180
|
}
|
|
2224
2181
|
pendings.set(pendingKey(locator, id), {
|
|
2225
2182
|
drafts: saved,
|
|
@@ -2272,6 +2229,8 @@ export function composeRuntime(options) {
|
|
|
2272
2229
|
...saved.map(({ path }) => path),
|
|
2273
2230
|
...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
|
|
2274
2231
|
]);
|
|
2232
|
+
if ('refusal' in saving)
|
|
2233
|
+
return { outcome: 'refused', ...saving.refusal };
|
|
2275
2234
|
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
|
|
2276
2235
|
if (changed.length > 0)
|
|
2277
2236
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
@@ -2390,10 +2349,10 @@ export function composeRuntime(options) {
|
|
|
2390
2349
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2391
2350
|
const { answer, touched } = done;
|
|
2392
2351
|
if (answer.fenced !== null) {
|
|
2393
|
-
throw runtimeError('room
|
|
2352
|
+
throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
|
|
2394
2353
|
}
|
|
2395
2354
|
if (fences.has(locator))
|
|
2396
|
-
throw runtimeError('room
|
|
2355
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
2397
2356
|
for (const origin of withdrawn.keys())
|
|
2398
2357
|
pendings.delete(pendingKey(locator, origin));
|
|
2399
2358
|
for (const [id, drafts] of drafted) {
|
|
@@ -2442,7 +2401,7 @@ export function composeRuntime(options) {
|
|
|
2442
2401
|
throw new Error('connection refused');
|
|
2443
2402
|
await local.keepUpload(locator, identifier, upload);
|
|
2444
2403
|
if (fences.has(locator))
|
|
2445
|
-
throw runtimeError('room
|
|
2404
|
+
throw runtimeError('unknown-room', `Unknown Room ${locator}`);
|
|
2446
2405
|
const key = pendingKey(locator, identifier);
|
|
2447
2406
|
pendings.set(key, { ...pendings.get(key), upload });
|
|
2448
2407
|
if (!continuing())
|
|
@@ -2579,6 +2538,83 @@ export function composeRuntime(options) {
|
|
|
2579
2538
|
return UNCERTAIN;
|
|
2580
2539
|
}
|
|
2581
2540
|
}
|
|
2541
|
+
async function heldAgain(locator) {
|
|
2542
|
+
const replica = replicas.get(locator);
|
|
2543
|
+
if (replica === undefined || !textOnline())
|
|
2544
|
+
return;
|
|
2545
|
+
try {
|
|
2546
|
+
const wanted = await (await openLocal()).wanted(locator, replica.record.key);
|
|
2547
|
+
await holdTexts(wanted.map(({ path, revision }) => ({ locator, path, revision })));
|
|
2548
|
+
}
|
|
2549
|
+
catch {
|
|
2550
|
+
journal.record(`hold text ${locator} unwritten`);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
function textOnline() {
|
|
2554
|
+
return !closed && refusal === null && connectionOf() === 'connected';
|
|
2555
|
+
}
|
|
2556
|
+
function textKey({ locator, path }) {
|
|
2557
|
+
return documentKey(locator, path);
|
|
2558
|
+
}
|
|
2559
|
+
async function readText({ locator, path, revision }) {
|
|
2560
|
+
const replica = replicas.get(locator);
|
|
2561
|
+
if (replica === undefined || !textOnline())
|
|
2562
|
+
return;
|
|
2563
|
+
try {
|
|
2564
|
+
await ensureAuthorization();
|
|
2565
|
+
}
|
|
2566
|
+
catch {
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
2570
|
+
return;
|
|
2571
|
+
const since = fencings;
|
|
2572
|
+
journal.record(`hold text ${locator} ${path}`);
|
|
2573
|
+
const read = await reads
|
|
2574
|
+
.bootstrap(authorization, { locator, selection: replica.record.replica.selection, path })
|
|
2575
|
+
.catch(() => null);
|
|
2576
|
+
journal.record(`outcome text ${read?.kind ?? 'transport-uncertainty'}`);
|
|
2577
|
+
if (read === null || read.kind !== 'bootstrap' || fencedSince(locator, since))
|
|
2578
|
+
return;
|
|
2579
|
+
const content = read.documents[path];
|
|
2580
|
+
const now = replicas.get(locator);
|
|
2581
|
+
if (content === undefined || read.revisions[path] !== revision || now === undefined) {
|
|
2582
|
+
unholdable.set(textKey({ locator, path, revision }), revision);
|
|
2583
|
+
return;
|
|
2584
|
+
}
|
|
2585
|
+
const held = await (await openLocal()).hold(locator, now.record.key, compositionFor(now.record), [{ path, revision, content }]);
|
|
2586
|
+
if (held.length === 0)
|
|
2587
|
+
unholdable.set(textKey({ locator, path, revision }), revision);
|
|
2588
|
+
else
|
|
2589
|
+
publishChangeSet(changeSet(locator, { documents: held }));
|
|
2590
|
+
}
|
|
2591
|
+
function holdText(unheld) {
|
|
2592
|
+
const key = textKey(unheld);
|
|
2593
|
+
const running = holding.get(key);
|
|
2594
|
+
if (running !== undefined)
|
|
2595
|
+
return running;
|
|
2596
|
+
const started = readText(unheld).finally(() => holding.delete(key));
|
|
2597
|
+
holding.set(key, started);
|
|
2598
|
+
return started;
|
|
2599
|
+
}
|
|
2600
|
+
async function holdTexts(wanted) {
|
|
2601
|
+
const queue = wanted.filter((one) => unholdable.get(textKey(one)) !== one.revision);
|
|
2602
|
+
await Promise.all(Array.from({ length: Math.min(TEXT_READS, queue.length) }, async () => {
|
|
2603
|
+
for (let next = queue.shift(); next !== undefined; next = queue.shift()) {
|
|
2604
|
+
await holdText(next);
|
|
2605
|
+
}
|
|
2606
|
+
}));
|
|
2607
|
+
}
|
|
2608
|
+
async function unheldIn(local, locator, key, paths) {
|
|
2609
|
+
if (paths.length === 0)
|
|
2610
|
+
return [];
|
|
2611
|
+
return (await local.entries(key, 'confirmed', paths)).flatMap((entry) => entry.content === null &&
|
|
2612
|
+
entry.binary === null &&
|
|
2613
|
+
entry.outline !== null &&
|
|
2614
|
+
entry.revision !== null
|
|
2615
|
+
? [{ locator, path: entry.path, revision: entry.revision }]
|
|
2616
|
+
: []);
|
|
2617
|
+
}
|
|
2582
2618
|
function fetchBinary(request, name) {
|
|
2583
2619
|
return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
|
|
2584
2620
|
}
|
|
@@ -2714,14 +2750,16 @@ export function composeRuntime(options) {
|
|
|
2714
2750
|
}
|
|
2715
2751
|
async function cleanUp() {
|
|
2716
2752
|
const freed = await database.ask({ kind: 'opened.clean' });
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2753
|
+
const local = await openLocal();
|
|
2754
|
+
let texts = 0;
|
|
2755
|
+
for (const [locator, replica] of replicas) {
|
|
2756
|
+
const released = await local.unhold(locator, replica.record.key, compositionFor(replica.record));
|
|
2757
|
+
texts += released.bytes;
|
|
2758
|
+
if (released.paths.length > 0) {
|
|
2759
|
+
publishChangeSet(changeSet(locator, { documents: released.paths }));
|
|
2760
|
+
}
|
|
2723
2761
|
}
|
|
2724
|
-
return
|
|
2762
|
+
return freed + texts + ((await options.shell.cleanUp?.()) ?? 0);
|
|
2725
2763
|
}
|
|
2726
2764
|
function declaredAction(name) {
|
|
2727
2765
|
for (const capability of capabilities) {
|
|
@@ -2774,6 +2812,24 @@ export function composeRuntime(options) {
|
|
|
2774
2812
|
return UNCERTAIN;
|
|
2775
2813
|
}
|
|
2776
2814
|
}
|
|
2815
|
+
async function readConversationHistory(request) {
|
|
2816
|
+
await ready;
|
|
2817
|
+
const stream = options.adapter.conversations;
|
|
2818
|
+
if (closed || refusal !== null || stream?.history === undefined)
|
|
2819
|
+
return UNCERTAIN;
|
|
2820
|
+
journal.record('transfer conversation history');
|
|
2821
|
+
try {
|
|
2822
|
+
const result = await stream.history(request);
|
|
2823
|
+
if (result.kind === 'revoked-authorization')
|
|
2824
|
+
await refuse('revoked-authorization');
|
|
2825
|
+
journal.record(`outcome conversation history ${result.kind}`);
|
|
2826
|
+
return calledResult(result);
|
|
2827
|
+
}
|
|
2828
|
+
catch {
|
|
2829
|
+
journal.record('outcome conversation history transport-uncertainty');
|
|
2830
|
+
return UNCERTAIN;
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2777
2833
|
function unsettled(result) {
|
|
2778
2834
|
return result.kind !== 'action' && result.retryable;
|
|
2779
2835
|
}
|
|
@@ -2835,6 +2891,33 @@ export function composeRuntime(options) {
|
|
|
2835
2891
|
const held = replicas.get(locator);
|
|
2836
2892
|
return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
|
|
2837
2893
|
}
|
|
2894
|
+
function shownAt(locator) {
|
|
2895
|
+
const held = replicas.get(locator)?.record.roomVersion;
|
|
2896
|
+
const raised = highest.get(locator);
|
|
2897
|
+
if (held === undefined || raised === undefined)
|
|
2898
|
+
return held ?? raised;
|
|
2899
|
+
return options.adapter.reached(locator, held, raised) ? held : raised;
|
|
2900
|
+
}
|
|
2901
|
+
function showing(locator, roomVersion) {
|
|
2902
|
+
const at = shownAt(locator);
|
|
2903
|
+
if (at === undefined || !options.adapter.reached(locator, at, roomVersion)) {
|
|
2904
|
+
highest.set(locator, roomVersion);
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
function older(locator, roomVersion) {
|
|
2908
|
+
if (beyondHorizon.has(locator))
|
|
2909
|
+
return false;
|
|
2910
|
+
const at = shownAt(locator);
|
|
2911
|
+
return (at !== undefined &&
|
|
2912
|
+
options.adapter.reached(locator, at, roomVersion) &&
|
|
2913
|
+
!options.adapter.reached(locator, roomVersion, at));
|
|
2914
|
+
}
|
|
2915
|
+
function replaced(locators) {
|
|
2916
|
+
for (const locator of locators) {
|
|
2917
|
+
highest.delete(locator);
|
|
2918
|
+
beyondHorizon.add(locator);
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2838
2921
|
async function writtenAfter(locator, roomVersion, path) {
|
|
2839
2922
|
const held = replicas.get(locator);
|
|
2840
2923
|
if (held === undefined)
|
|
@@ -2845,122 +2928,27 @@ export function composeRuntime(options) {
|
|
|
2845
2928
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
2846
2929
|
!options.adapter.reached(locator, roomVersion, written));
|
|
2847
2930
|
}
|
|
2848
|
-
function
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
else
|
|
2858
|
-
known.set(path, entry.binary !== null ? null : entry.content);
|
|
2859
|
-
}
|
|
2860
|
-
return known;
|
|
2861
|
-
};
|
|
2862
|
-
}
|
|
2863
|
-
async function unreadableRoom(texts, walk, paths) {
|
|
2864
|
-
if (paths !== null) {
|
|
2865
|
-
const text = {};
|
|
2866
|
-
for (const [path, content] of await texts(paths))
|
|
2867
|
-
if (content !== null)
|
|
2868
|
-
text[path] = content;
|
|
2869
|
-
return stepped(unreadableIn(text, Object.keys(text)));
|
|
2870
|
-
}
|
|
2871
|
-
for (let after = null;;) {
|
|
2872
|
-
const page = await walk(after);
|
|
2873
|
-
const text = {};
|
|
2874
|
-
for (const { path, content } of page.entries)
|
|
2875
|
-
if (content !== null)
|
|
2876
|
-
text[path] = content;
|
|
2877
|
-
const refused = await stepped(unreadableIn(text, Object.keys(text)));
|
|
2878
|
-
if (refused !== null)
|
|
2879
|
-
return refused;
|
|
2880
|
-
if (page.next === null)
|
|
2881
|
-
return null;
|
|
2882
|
-
after = page.next;
|
|
2883
|
-
}
|
|
2884
|
-
}
|
|
2885
|
-
async function validatedRoom(local, record, view, previous, overrides = null, checked = null) {
|
|
2886
|
-
const composition = composed(record);
|
|
2887
|
-
const modeled = record.model.status !== 'unavailable' || record.typedSelection !== null;
|
|
2888
|
-
const lookup = overrides === null
|
|
2889
|
-
? lookupOf(local, record.key, view)
|
|
2890
|
-
: overriddenLookup(lookupOf(local, record.key, view), overrides);
|
|
2891
|
-
const texts = overrides === null
|
|
2892
|
-
? textsOf(local, record.key, view)
|
|
2893
|
-
: overriddenTexts(textsOf(local, record.key, view), overrides);
|
|
2894
|
-
const walking = overrides === null
|
|
2895
|
-
? walkOf(local, record.key, view)
|
|
2896
|
-
: overriddenWalk(walkOf(local, record.key, view), overrides);
|
|
2897
|
-
const walk = checked === null
|
|
2898
|
-
? walking
|
|
2899
|
-
: async (after) => {
|
|
2900
|
-
const page = await walking(after);
|
|
2901
|
-
checked(page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0));
|
|
2902
|
-
return page;
|
|
2903
|
-
};
|
|
2904
|
-
let changes = null;
|
|
2905
|
-
let reshaped = false;
|
|
2906
|
-
if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
|
|
2907
|
-
const [was, now] = await Promise.all([
|
|
2908
|
-
local.entries(record.key, 'confirmed', previous.changed),
|
|
2909
|
-
overrides === null
|
|
2910
|
-
? local.entries(record.key, view, previous.changed)
|
|
2911
|
-
: Promise.resolve([]),
|
|
2912
|
-
]);
|
|
2913
|
-
const before = new Map(was.map((entry) => [entry.path, entry]));
|
|
2914
|
-
const after = overrides === null
|
|
2915
|
-
? new Map(now.map((entry) => [entry.path, entry]))
|
|
2916
|
-
: overrides;
|
|
2917
|
-
changes = previous.changed.flatMap((path) => {
|
|
2918
|
-
const one = before.get(path) ?? null;
|
|
2919
|
-
const other = after.get(path) ?? null;
|
|
2920
|
-
reshaped ||= textDiffers(one, other);
|
|
2921
|
-
const held = textOf(one);
|
|
2922
|
-
const next = textOf(other);
|
|
2923
|
-
return held === next ? [] : [{ path, was: held, now: next }];
|
|
2924
|
-
});
|
|
2925
|
-
}
|
|
2926
|
-
const named = changes === null ? null : changes.map(({ path }) => path);
|
|
2927
|
-
const textChanged = changes === null || changes.length > 0 || reshaped;
|
|
2928
|
-
const modelsChanged = previous === null || !sameComposition(composed(previous.record), composition);
|
|
2929
|
-
if (modeled && (textChanged || modelsChanged)) {
|
|
2930
|
-
const clean = previous !== null &&
|
|
2931
|
-
changes !== null &&
|
|
2932
|
-
(await changedCleanly(changes, composed(previous.record), composition, {
|
|
2933
|
-
lookup,
|
|
2934
|
-
texts,
|
|
2935
|
-
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2936
|
-
}));
|
|
2937
|
-
if (clean)
|
|
2938
|
-
return null;
|
|
2939
|
-
const verdict = await validatedCollection(walk, lookup, composition);
|
|
2940
|
-
if (verdict.verdict === 'accept')
|
|
2941
|
-
return null;
|
|
2942
|
-
if (verdict.rule !== 'model-unavailable')
|
|
2943
|
-
return { kind: 'refused', refusal: verdict };
|
|
2944
|
-
return unreadableRoom(texts, walk, named);
|
|
2945
|
-
}
|
|
2946
|
-
if (textChanged)
|
|
2947
|
-
return unreadableRoom(texts, walk, named);
|
|
2948
|
-
return null;
|
|
2931
|
+
async function writtenLater(local, locator, key, paths, roomVersion) {
|
|
2932
|
+
if (paths.length === 0 || !older(locator, roomVersion))
|
|
2933
|
+
return new Set();
|
|
2934
|
+
const entries = await local.entries(key, 'confirmed', paths);
|
|
2935
|
+
return new Set(entries.flatMap(({ path, written }) => written !== null &&
|
|
2936
|
+
options.adapter.reached(locator, written, roomVersion) &&
|
|
2937
|
+
!options.adapter.reached(locator, roomVersion, written)
|
|
2938
|
+
? [path]
|
|
2939
|
+
: []));
|
|
2949
2940
|
}
|
|
2950
|
-
function refreshingOf(local, key, composition, liveness, replaying =
|
|
2951
|
-
const write = replaying
|
|
2941
|
+
function refreshingOf(local, locator, key, composition, liveness, replaying = false) {
|
|
2942
|
+
const write = replaying ? finishingWrite : exclusive;
|
|
2952
2943
|
return {
|
|
2953
|
-
|
|
2954
|
-
if (replaying
|
|
2955
|
-
|
|
2956
|
-
installing(replaying);
|
|
2957
|
-
await screened();
|
|
2944
|
+
turn() {
|
|
2945
|
+
if (replaying)
|
|
2946
|
+
installing(locator);
|
|
2958
2947
|
},
|
|
2959
2948
|
prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
|
|
2949
|
+
later: (paths, roomVersion) => writtenLater(local, locator, key, paths, roomVersion),
|
|
2960
2950
|
async stage(page) {
|
|
2961
2951
|
for (let written = 0;;) {
|
|
2962
|
-
if (replaying !== null)
|
|
2963
|
-
await screened();
|
|
2964
2952
|
const end = batchEnd(page.entries, written);
|
|
2965
2953
|
const last = end === page.entries.length;
|
|
2966
2954
|
const entries = page.entries.slice(written, end);
|
|
@@ -3060,25 +3048,13 @@ export function composeRuntime(options) {
|
|
|
3060
3048
|
return;
|
|
3061
3049
|
void yielded().then(nextInstallTurn);
|
|
3062
3050
|
}
|
|
3063
|
-
function screened() {
|
|
3064
|
-
if (onScreen || closed)
|
|
3065
|
-
return Promise.resolve();
|
|
3066
|
-
queueReadiness();
|
|
3067
|
-
return new Promise((resolve) => shownWaiters.push(resolve));
|
|
3068
|
-
}
|
|
3069
3051
|
async function heldStream() {
|
|
3070
|
-
|
|
3071
|
-
if (streams.generation !== generation)
|
|
3072
|
-
streams = { generation, free: STREAMS_HELD };
|
|
3073
|
-
const held = streams;
|
|
3074
|
-
if (held.free > 0) {
|
|
3075
|
-
held.free -= 1;
|
|
3076
|
-
return once(() => {
|
|
3077
|
-
held.free += 1;
|
|
3078
|
-
});
|
|
3079
|
-
}
|
|
3052
|
+
while (streamsFree === 0)
|
|
3080
3053
|
await new Promise((admit) => installWaiters.push(admit));
|
|
3081
|
-
|
|
3054
|
+
streamsFree -= 1;
|
|
3055
|
+
return once(() => {
|
|
3056
|
+
streamsFree += 1;
|
|
3057
|
+
});
|
|
3082
3058
|
}
|
|
3083
3059
|
async function unqueued() {
|
|
3084
3060
|
while (queuedText >= QUEUED_TEXT) {
|
|
@@ -3106,14 +3082,19 @@ export function composeRuntime(options) {
|
|
|
3106
3082
|
streaming.delete(locator);
|
|
3107
3083
|
underived.delete(locator);
|
|
3108
3084
|
unindexed.delete(locator);
|
|
3109
|
-
|
|
3085
|
+
}
|
|
3086
|
+
function setBack(locator) {
|
|
3087
|
+
const now = bytesOf(locator, true);
|
|
3088
|
+
const earlier = startedOver.get(locator);
|
|
3089
|
+
startedOver.set(locator, {
|
|
3090
|
+
done: Math.max(now.done, earlier?.done ?? 0),
|
|
3091
|
+
total: Math.max(now.total, earlier?.total ?? 0),
|
|
3092
|
+
});
|
|
3110
3093
|
}
|
|
3111
3094
|
function owedIndexes(locator, bytes, text) {
|
|
3112
3095
|
if (bytes <= 0 || text <= 0)
|
|
3113
3096
|
return;
|
|
3114
|
-
unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes
|
|
3115
|
-
const check = unchecked.get(locator) ?? { bytes: 0, text: 0, done: 0 };
|
|
3116
|
-
unchecked.set(locator, { ...check, bytes: check.bytes + bytes / 2, text: check.text + text });
|
|
3097
|
+
unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes, text }]);
|
|
3117
3098
|
}
|
|
3118
3099
|
function built(locator, { done, text }) {
|
|
3119
3100
|
const owed = unindexed.get(locator) ?? [];
|
|
@@ -3139,52 +3120,34 @@ export function composeRuntime(options) {
|
|
|
3139
3120
|
if (installs.delete(locator))
|
|
3140
3121
|
queueReadiness();
|
|
3141
3122
|
}
|
|
3142
|
-
function
|
|
3143
|
-
if (value === onScreen)
|
|
3144
|
-
return;
|
|
3145
|
-
onScreen = value;
|
|
3146
|
-
journal.record(`installation ${value ? 'shown' : 'hidden'}`);
|
|
3147
|
-
queueReadiness();
|
|
3148
|
-
if (!value)
|
|
3149
|
-
return;
|
|
3150
|
-
for (const resolve of shownWaiters.splice(0))
|
|
3151
|
-
resolve();
|
|
3152
|
-
}
|
|
3153
|
-
async function pullRefresh(authorized, held, liveness, hold) {
|
|
3123
|
+
async function pullRefresh(authorized, held, liveness, hold, refresh) {
|
|
3154
3124
|
if (!liveness())
|
|
3155
3125
|
return unconfirmed;
|
|
3156
|
-
const turn = generation;
|
|
3157
3126
|
const { record } = held;
|
|
3158
|
-
const
|
|
3127
|
+
const from = standingOf(record.locator);
|
|
3159
3128
|
const local = await openLocal();
|
|
3160
3129
|
try {
|
|
3161
3130
|
await exclusive(() => local.unrefresh(record.key));
|
|
3162
|
-
const result = await advancement({
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
}
|
|
3172
|
-
},
|
|
3173
|
-
}, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, composed(record), liveness));
|
|
3131
|
+
const result = await advancement(async (authorization, request) => {
|
|
3132
|
+
hold.release();
|
|
3133
|
+
try {
|
|
3134
|
+
return await refresh(authorization, request);
|
|
3135
|
+
}
|
|
3136
|
+
finally {
|
|
3137
|
+
hold.take();
|
|
3138
|
+
}
|
|
3139
|
+
}, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.locator, record.key, composed(record), liveness));
|
|
3174
3140
|
if (result.kind !== 'advanced') {
|
|
3175
3141
|
journal.record(`transfer refresh ${record.locator} ${result.kind}`);
|
|
3176
3142
|
return result;
|
|
3177
3143
|
}
|
|
3178
|
-
const refused = await validatedRoom(local, result.record, 'refreshed', {
|
|
3179
|
-
record,
|
|
3180
|
-
changed: [...result.changed],
|
|
3181
|
-
});
|
|
3182
|
-
if (refused !== null) {
|
|
3183
|
-
journal.record(`transfer refresh ${record.locator} refused`);
|
|
3184
|
-
return refused;
|
|
3185
|
-
}
|
|
3186
3144
|
journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
|
|
3187
|
-
return {
|
|
3145
|
+
return {
|
|
3146
|
+
kind: 'advanced',
|
|
3147
|
+
record: result.record,
|
|
3148
|
+
from,
|
|
3149
|
+
remodelled: result.record.model !== record.model,
|
|
3150
|
+
};
|
|
3188
3151
|
}
|
|
3189
3152
|
catch (error) {
|
|
3190
3153
|
if (refusedForSpace(error))
|
|
@@ -3192,9 +3155,6 @@ export function composeRuntime(options) {
|
|
|
3192
3155
|
journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
|
|
3193
3156
|
throw error;
|
|
3194
3157
|
}
|
|
3195
|
-
finally {
|
|
3196
|
-
reads.finished(record.locator, turn);
|
|
3197
|
-
}
|
|
3198
3158
|
}
|
|
3199
3159
|
const heldContents = new Map();
|
|
3200
3160
|
function heldContentOf(local, key) {
|
|
@@ -3212,11 +3172,17 @@ export function composeRuntime(options) {
|
|
|
3212
3172
|
heldContents.set(key, made);
|
|
3213
3173
|
return made;
|
|
3214
3174
|
}
|
|
3175
|
+
async function abandoned(local, locator) {
|
|
3176
|
+
await local.abandon(locator);
|
|
3177
|
+
setBack(locator);
|
|
3178
|
+
untransferred(locator);
|
|
3179
|
+
liveQueries.recover();
|
|
3180
|
+
sweep();
|
|
3181
|
+
}
|
|
3215
3182
|
function staging(local, liveness) {
|
|
3216
3183
|
const derived = async (locator, key, composition) => {
|
|
3217
3184
|
underived.delete(locator);
|
|
3218
3185
|
for (;;) {
|
|
3219
|
-
await screened();
|
|
3220
3186
|
if (!liveness())
|
|
3221
3187
|
return false;
|
|
3222
3188
|
const done = await finishingWrite(async () => liveness()
|
|
@@ -3233,13 +3199,7 @@ export function composeRuntime(options) {
|
|
|
3233
3199
|
held: (locator) => transfers.get(locator) ?? null,
|
|
3234
3200
|
async turn(locator, step) {
|
|
3235
3201
|
installing(locator);
|
|
3236
|
-
|
|
3237
|
-
await screened();
|
|
3238
|
-
const release = step === 'downloading' ? await heldStream() : () => { };
|
|
3239
|
-
if (onScreen || closed)
|
|
3240
|
-
return release;
|
|
3241
|
-
release();
|
|
3242
|
-
}
|
|
3202
|
+
return step === 'downloading' ? heldStream() : () => { };
|
|
3243
3203
|
},
|
|
3244
3204
|
unqueued,
|
|
3245
3205
|
queued(text) {
|
|
@@ -3254,7 +3214,7 @@ export function composeRuntime(options) {
|
|
|
3254
3214
|
at(bytes) {
|
|
3255
3215
|
if (ended)
|
|
3256
3216
|
return;
|
|
3257
|
-
arrivals.set(locator,
|
|
3217
|
+
arrivals.set(locator, bytes);
|
|
3258
3218
|
streaming.add(locator);
|
|
3259
3219
|
const now = performance.now();
|
|
3260
3220
|
if (now - arrivalReported < ARRIVAL_REPORT_MS)
|
|
@@ -3262,14 +3222,6 @@ export function composeRuntime(options) {
|
|
|
3262
3222
|
arrivalReported = now;
|
|
3263
3223
|
queueReadiness();
|
|
3264
3224
|
},
|
|
3265
|
-
part() {
|
|
3266
|
-
const arrived = arrivals.get(locator);
|
|
3267
|
-
arrivals.set(locator, {
|
|
3268
|
-
parts: (arrived?.parts ?? 0) + 1,
|
|
3269
|
-
bytes: arrived?.bytes ?? 0,
|
|
3270
|
-
});
|
|
3271
|
-
queueReadiness();
|
|
3272
|
-
},
|
|
3273
3225
|
end() {
|
|
3274
3226
|
if (ended)
|
|
3275
3227
|
return;
|
|
@@ -3279,14 +3231,10 @@ export function composeRuntime(options) {
|
|
|
3279
3231
|
},
|
|
3280
3232
|
};
|
|
3281
3233
|
},
|
|
3282
|
-
|
|
3283
|
-
const later = announced.get(locator);
|
|
3284
|
-
return later === undefined ? [] : [later];
|
|
3285
|
-
},
|
|
3234
|
+
streamed: options.adapter.roomStream !== undefined,
|
|
3286
3235
|
async stage(record, page, composition) {
|
|
3287
3236
|
const { locator } = record;
|
|
3288
3237
|
installing(locator);
|
|
3289
|
-
await screened();
|
|
3290
3238
|
if (!liveness())
|
|
3291
3239
|
return null;
|
|
3292
3240
|
const key = await installWrite(async () => {
|
|
@@ -3315,19 +3263,16 @@ export function composeRuntime(options) {
|
|
|
3315
3263
|
derive: derived,
|
|
3316
3264
|
async refreshing(locator, key, composition) {
|
|
3317
3265
|
await exclusive(() => local.unrefresh(key));
|
|
3318
|
-
return refreshingOf(local, key, composition, liveness,
|
|
3266
|
+
return refreshingOf(local, locator, key, composition, liveness, true);
|
|
3319
3267
|
},
|
|
3320
3268
|
abandon: (locator) => exclusive(async () => {
|
|
3321
|
-
if (
|
|
3322
|
-
|
|
3323
|
-
await local.abandon(locator);
|
|
3324
|
-
untransferred(locator);
|
|
3325
|
-
liveQueries.recover();
|
|
3326
|
-
sweep();
|
|
3269
|
+
if (liveness())
|
|
3270
|
+
await abandoned(local, locator);
|
|
3327
3271
|
}),
|
|
3328
3272
|
setAside: async (locator) => {
|
|
3329
3273
|
if (!liveness())
|
|
3330
3274
|
return;
|
|
3275
|
+
setBack(locator);
|
|
3331
3276
|
untransferred(locator);
|
|
3332
3277
|
liveQueries.recover();
|
|
3333
3278
|
},
|
|
@@ -3336,19 +3281,6 @@ export function composeRuntime(options) {
|
|
|
3336
3281
|
sweep();
|
|
3337
3282
|
}),
|
|
3338
3283
|
content: (key) => heldContentOf(local, key),
|
|
3339
|
-
async validate(record, view) {
|
|
3340
|
-
const { locator } = record;
|
|
3341
|
-
const refused = await validatedRoom(local, record, view, null, null, (text) => {
|
|
3342
|
-
const check = unchecked.get(locator);
|
|
3343
|
-
if (check === undefined)
|
|
3344
|
-
return;
|
|
3345
|
-
unchecked.set(locator, { ...check, done: check.done + text });
|
|
3346
|
-
queueReadiness();
|
|
3347
|
-
});
|
|
3348
|
-
unchecked.delete(locator);
|
|
3349
|
-
queueReadiness();
|
|
3350
|
-
return refused;
|
|
3351
|
-
},
|
|
3352
3284
|
};
|
|
3353
3285
|
}
|
|
3354
3286
|
function reusable(room, local) {
|
|
@@ -3360,15 +3292,21 @@ export function composeRuntime(options) {
|
|
|
3360
3292
|
async function pullBootstrap(authorized, room, local, liveness) {
|
|
3361
3293
|
if (!liveness())
|
|
3362
3294
|
return unconfirmed;
|
|
3363
|
-
const turn = generation;
|
|
3364
3295
|
if (replicas.has(room.locator)) {
|
|
3365
3296
|
catching.add(room.locator);
|
|
3366
3297
|
liveQueries.recover();
|
|
3367
3298
|
}
|
|
3368
3299
|
try {
|
|
3369
|
-
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));
|
|
3300
|
+
const result = await bootstrapRoom(reads, authorized, room, { collection: modelOf(room.locator) }, builtIn, typedPaths, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
|
|
3370
3301
|
const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
|
|
3371
3302
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
3303
|
+
if (result.kind === 'staged' && older(room.locator, result.record.roomVersion)) {
|
|
3304
|
+
caughtUpTo(room.locator);
|
|
3305
|
+
installed(room.locator);
|
|
3306
|
+
await abandoned(local, room.locator);
|
|
3307
|
+
journal.record(`transfer bootstrap ${room.locator} older than shown`);
|
|
3308
|
+
return { kind: 'transport-uncertainty' };
|
|
3309
|
+
}
|
|
3372
3310
|
if (result.kind !== 'staged') {
|
|
3373
3311
|
caughtUpTo(room.locator);
|
|
3374
3312
|
installed(room.locator);
|
|
@@ -3383,9 +3321,6 @@ export function composeRuntime(options) {
|
|
|
3383
3321
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
3384
3322
|
throw error;
|
|
3385
3323
|
}
|
|
3386
|
-
finally {
|
|
3387
|
-
reads.finished(room.locator, turn);
|
|
3388
|
-
}
|
|
3389
3324
|
}
|
|
3390
3325
|
function caughtUpTo(locator) {
|
|
3391
3326
|
if (catching.delete(locator))
|
|
@@ -3393,51 +3328,55 @@ export function composeRuntime(options) {
|
|
|
3393
3328
|
}
|
|
3394
3329
|
function behind(room) {
|
|
3395
3330
|
const held = replicas.get(room.locator)?.record.roomVersion;
|
|
3396
|
-
return
|
|
3397
|
-
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
3331
|
+
return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
|
|
3398
3332
|
}
|
|
3399
|
-
|
|
3400
|
-
const
|
|
3401
|
-
|
|
3402
|
-
if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
|
|
3403
|
-
return withheld;
|
|
3404
|
-
}
|
|
3405
|
-
return advanceOnce(room, local, authorized, live);
|
|
3333
|
+
function standingOf(locator) {
|
|
3334
|
+
const held = replicas.get(locator);
|
|
3335
|
+
return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
|
|
3406
3336
|
}
|
|
3407
|
-
|
|
3337
|
+
function stands(locator, from) {
|
|
3338
|
+
const now = standingOf(locator);
|
|
3339
|
+
return now.key === from.key && now.confirmed === from.confirmed;
|
|
3340
|
+
}
|
|
3341
|
+
async function advance(room, local, authorized, running) {
|
|
3408
3342
|
const live = () => running() && !full;
|
|
3409
3343
|
if (!live())
|
|
3410
3344
|
return unconfirmed;
|
|
3411
3345
|
const held = replicas.get(room.locator);
|
|
3412
|
-
if (held !== undefined) {
|
|
3413
|
-
|
|
3346
|
+
if (held !== undefined && options.adapter.roomStream !== undefined) {
|
|
3347
|
+
stale.delete(room.locator);
|
|
3348
|
+
name(room.locator);
|
|
3349
|
+
return null;
|
|
3350
|
+
}
|
|
3351
|
+
const refresh = reads.refresh;
|
|
3352
|
+
if (held !== undefined && refresh !== undefined) {
|
|
3353
|
+
const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
|
|
3414
3354
|
if (refreshed === 'behind')
|
|
3415
3355
|
return advance(room, local, authorized, live);
|
|
3416
3356
|
if (refreshed !== 'beyond-horizon')
|
|
3417
3357
|
return refreshed;
|
|
3418
|
-
if (
|
|
3419
|
-
owed.add(room.locator);
|
|
3420
|
-
liveQueries.recover();
|
|
3421
|
-
return unconfirmed;
|
|
3422
|
-
}
|
|
3423
|
-
if (reloadsDue.get(room.locator) === generation)
|
|
3358
|
+
if (reloadsDue.has(room.locator))
|
|
3424
3359
|
return unconfirmed;
|
|
3360
|
+
replaced([room.locator]);
|
|
3425
3361
|
}
|
|
3362
|
+
const from = standingOf(room.locator);
|
|
3426
3363
|
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
3427
|
-
if (bootstrapped.kind
|
|
3428
|
-
|
|
3364
|
+
if (bootstrapped.kind !== 'staged')
|
|
3365
|
+
return live() ? bootstrapped : unconfirmed;
|
|
3366
|
+
const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
|
|
3367
|
+
if (await publish(local, room, bootstrapped.record, landing, live)) {
|
|
3429
3368
|
return live() ? null : unconfirmed;
|
|
3430
3369
|
}
|
|
3431
|
-
return
|
|
3370
|
+
return reachedBy(room.locator, bootstrapped.record.roomVersion)
|
|
3371
|
+
? null
|
|
3372
|
+
: advance(room, local, authorized, live);
|
|
3432
3373
|
}
|
|
3433
|
-
async function refreshedRoom(room, local, authorized, held, live, hold) {
|
|
3434
|
-
const refreshed = await pullRefresh(authorized, held, live, hold);
|
|
3374
|
+
async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
|
|
3375
|
+
const refreshed = await pullRefresh(authorized, held, live, hold, refresh);
|
|
3435
3376
|
if (!live())
|
|
3436
3377
|
return unconfirmed;
|
|
3437
3378
|
if (refreshed.kind === 'still-current') {
|
|
3438
3379
|
stale.delete(room.locator);
|
|
3439
|
-
if (sourceRefusals.has(room.locator))
|
|
3440
|
-
return null;
|
|
3441
3380
|
if (unreadable.delete(room.locator)) {
|
|
3442
3381
|
scopedRefusals.delete(room.locator);
|
|
3443
3382
|
list(room, true);
|
|
@@ -3446,7 +3385,11 @@ export function composeRuntime(options) {
|
|
|
3446
3385
|
return null;
|
|
3447
3386
|
}
|
|
3448
3387
|
if (refreshed.kind === 'advanced') {
|
|
3449
|
-
const landing = {
|
|
3388
|
+
const landing = {
|
|
3389
|
+
kind: 'refreshed',
|
|
3390
|
+
from: refreshed.from,
|
|
3391
|
+
remodelled: refreshed.remodelled,
|
|
3392
|
+
};
|
|
3450
3393
|
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3451
3394
|
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
3452
3395
|
const reached = now !== undefined &&
|
|
@@ -3459,12 +3402,9 @@ export function composeRuntime(options) {
|
|
|
3459
3402
|
return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
|
|
3460
3403
|
}
|
|
3461
3404
|
async function withhold(withheld, room, local, progress) {
|
|
3462
|
-
if (withheld.kind === 'unconfirmed')
|
|
3405
|
+
if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
|
|
3463
3406
|
return;
|
|
3464
|
-
|
|
3465
|
-
stale.delete(room.locator);
|
|
3466
|
-
else
|
|
3467
|
-
stale.add(room.locator);
|
|
3407
|
+
stale.add(room.locator);
|
|
3468
3408
|
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
3469
3409
|
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
3470
3410
|
repairable.set(room.locator, reset);
|
|
@@ -3486,30 +3426,10 @@ export function composeRuntime(options) {
|
|
|
3486
3426
|
list(room, false);
|
|
3487
3427
|
emitRooms();
|
|
3488
3428
|
}
|
|
3489
|
-
else if (withheld.kind === 'refused') {
|
|
3490
|
-
refusedSource(room, withheld);
|
|
3491
|
-
}
|
|
3492
3429
|
else if (withheld.kind === 'revoked-authorization') {
|
|
3493
3430
|
await refuse('revoked-authorization');
|
|
3494
3431
|
}
|
|
3495
3432
|
}
|
|
3496
|
-
function refusedSource(room, { refusal }) {
|
|
3497
|
-
sourceRefusals.set(room.locator, {
|
|
3498
|
-
code: refusal.rule,
|
|
3499
|
-
retryable: false,
|
|
3500
|
-
fields: {
|
|
3501
|
-
file: refusal.file,
|
|
3502
|
-
...(refusal.location === undefined ? {} : { location: refusal.location }),
|
|
3503
|
-
...(refusal.referrers === undefined ? {} : { referrers: refusal.referrers }),
|
|
3504
|
-
},
|
|
3505
|
-
});
|
|
3506
|
-
declarationRefusals.delete(room.locator);
|
|
3507
|
-
unreadable.add(room.locator);
|
|
3508
|
-
list(room, false);
|
|
3509
|
-
emitRooms();
|
|
3510
|
-
moveTo({ ...status, ...attention() });
|
|
3511
|
-
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
3512
|
-
}
|
|
3513
3433
|
function leaving(batch, result) {
|
|
3514
3434
|
if (result.kind === 'expired')
|
|
3515
3435
|
return { batch, returned: returnedBy('expired') };
|
|
@@ -3531,8 +3451,8 @@ export function composeRuntime(options) {
|
|
|
3531
3451
|
function settling(acceptance) {
|
|
3532
3452
|
return acceptance !== null && 'apply' in acceptance;
|
|
3533
3453
|
}
|
|
3534
|
-
function
|
|
3535
|
-
return settling(acceptance) ?
|
|
3454
|
+
function failureOf(acceptance) {
|
|
3455
|
+
return settling(acceptance) ? null : acceptance;
|
|
3536
3456
|
}
|
|
3537
3457
|
async function acceptance(room, local, submitted, accepted) {
|
|
3538
3458
|
if (!continuing())
|
|
@@ -3545,7 +3465,8 @@ export function composeRuntime(options) {
|
|
|
3545
3465
|
logAccepted(room.locator, submitted, accepted);
|
|
3546
3466
|
let record = current.record;
|
|
3547
3467
|
let changes;
|
|
3548
|
-
if (!options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
3468
|
+
if (!options.adapter.reached(room.locator, held, accepted.roomVersion) &&
|
|
3469
|
+
!older(room.locator, accepted.roomVersion)) {
|
|
3549
3470
|
const paths = [...returnedPaths(submitted, accepted)];
|
|
3550
3471
|
const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
|
|
3551
3472
|
const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
|
|
@@ -3555,34 +3476,6 @@ export function composeRuntime(options) {
|
|
|
3555
3476
|
? accepted.roomVersion
|
|
3556
3477
|
: held,
|
|
3557
3478
|
};
|
|
3558
|
-
const overrides = new Map([
|
|
3559
|
-
...back.set.map((entry) => [entry.path, entry]),
|
|
3560
|
-
...back.removed.map((path) => [path, null]),
|
|
3561
|
-
]);
|
|
3562
|
-
const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
|
|
3563
|
-
if (refused !== null) {
|
|
3564
|
-
const key = pendingKey(room.locator, submitted.id);
|
|
3565
|
-
return {
|
|
3566
|
-
locator: room.locator,
|
|
3567
|
-
left: [],
|
|
3568
|
-
commit: {
|
|
3569
|
-
rooms: [],
|
|
3570
|
-
writes: [
|
|
3571
|
-
{
|
|
3572
|
-
locator: room.locator,
|
|
3573
|
-
accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
|
|
3574
|
-
},
|
|
3575
|
-
],
|
|
3576
|
-
},
|
|
3577
|
-
refused,
|
|
3578
|
-
apply() {
|
|
3579
|
-
const state = pendings.get(key);
|
|
3580
|
-
if (state !== undefined && !fences.has(room.locator))
|
|
3581
|
-
pendings.set(key, { ...state, accepted: accepted.roomVersion });
|
|
3582
|
-
return changeSet(room.locator, { queue: true });
|
|
3583
|
-
},
|
|
3584
|
-
};
|
|
3585
|
-
}
|
|
3586
3479
|
changes = back;
|
|
3587
3480
|
}
|
|
3588
3481
|
const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
|
|
@@ -3590,10 +3483,9 @@ export function composeRuntime(options) {
|
|
|
3590
3483
|
...settled,
|
|
3591
3484
|
apply(answer) {
|
|
3592
3485
|
const set = settled.apply(answer);
|
|
3486
|
+
showing(room.locator, accepted.roomVersion);
|
|
3593
3487
|
pendings.delete(pendingKey(room.locator, submitted.id));
|
|
3594
3488
|
unreadable.delete(room.locator);
|
|
3595
|
-
sourceRefusals.delete(room.locator);
|
|
3596
|
-
declarationRefusals.delete(room.locator);
|
|
3597
3489
|
list(room, true);
|
|
3598
3490
|
return set;
|
|
3599
3491
|
},
|
|
@@ -3611,13 +3503,9 @@ export function composeRuntime(options) {
|
|
|
3611
3503
|
const failed = await exclusive(async () => {
|
|
3612
3504
|
const adopting = await acceptance(room, local, submitted, accepted);
|
|
3613
3505
|
await adoptedTogether(local, [adopting]);
|
|
3614
|
-
return
|
|
3506
|
+
return failureOf(adopting);
|
|
3615
3507
|
});
|
|
3616
|
-
|
|
3617
|
-
return true;
|
|
3618
|
-
if (failed.kind === 'refused' && continuing())
|
|
3619
|
-
refusedSource(room, failed);
|
|
3620
|
-
return false;
|
|
3508
|
+
return failed === null;
|
|
3621
3509
|
}
|
|
3622
3510
|
async function adoptedMove(room, partner, local, submitted, accepted) {
|
|
3623
3511
|
const other = discovered.get(partner);
|
|
@@ -3631,18 +3519,10 @@ export function composeRuntime(options) {
|
|
|
3631
3519
|
...accepted.destination,
|
|
3632
3520
|
});
|
|
3633
3521
|
await adoptedTogether(local, [left, arrived]);
|
|
3634
|
-
|
|
3635
|
-
if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
|
|
3636
|
-
refusedSource(other, refusedArrival);
|
|
3637
|
-
}
|
|
3638
|
-
return refusalOf(left);
|
|
3522
|
+
return failureOf(left);
|
|
3639
3523
|
});
|
|
3640
3524
|
submitLater(partner);
|
|
3641
|
-
|
|
3642
|
-
return true;
|
|
3643
|
-
if (failed.kind === 'refused' && continuing())
|
|
3644
|
-
refusedSource(room, failed);
|
|
3645
|
-
return false;
|
|
3525
|
+
return failed === null;
|
|
3646
3526
|
}
|
|
3647
3527
|
function queueIn(locator) {
|
|
3648
3528
|
return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
@@ -3761,12 +3641,10 @@ export function composeRuntime(options) {
|
|
|
3761
3641
|
async function submitRoom(room, local, authorized, progress, signal) {
|
|
3762
3642
|
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
3763
3643
|
const replica = replicas.get(room.locator);
|
|
3764
|
-
const head = replica?.queue
|
|
3644
|
+
const head = replica?.queue.find((batch) => !landedAhead.has(pendingKey(room.locator, batch.id)));
|
|
3765
3645
|
if (replica === undefined || head === undefined)
|
|
3766
3646
|
return 'done';
|
|
3767
3647
|
const state = pendings.get(pendingKey(room.locator, head.id));
|
|
3768
|
-
if (state?.accepted !== undefined)
|
|
3769
|
-
return 'done';
|
|
3770
3648
|
const partner = partnerOf(head, room.locator);
|
|
3771
3649
|
if (partner !== null && !leavingIn(head, room.locator)) {
|
|
3772
3650
|
submitLater(partner);
|
|
@@ -3880,12 +3758,12 @@ export function composeRuntime(options) {
|
|
|
3880
3758
|
}
|
|
3881
3759
|
if (result.kind === 'expired') {
|
|
3882
3760
|
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
|
|
3883
|
-
const there = other === undefined ? null : await
|
|
3761
|
+
const there = other === undefined ? null : await advanced(other, local, authorized);
|
|
3884
3762
|
if (other !== undefined && there !== null) {
|
|
3885
3763
|
await withhold(there, other, local, progress);
|
|
3886
3764
|
return 'done';
|
|
3887
3765
|
}
|
|
3888
|
-
const withheld = await
|
|
3766
|
+
const withheld = await advanced(room, local, authorized);
|
|
3889
3767
|
if (withheld !== null) {
|
|
3890
3768
|
await withhold(withheld, room, local, progress);
|
|
3891
3769
|
return 'done';
|
|
@@ -3907,8 +3785,8 @@ export function composeRuntime(options) {
|
|
|
3907
3785
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
3908
3786
|
const left = leaving(head, result);
|
|
3909
3787
|
const refreshed = async () => ({
|
|
3910
|
-
there: other === undefined ? null : await
|
|
3911
|
-
here: await
|
|
3788
|
+
there: other === undefined ? null : await advanced(other, local, authorized),
|
|
3789
|
+
here: await advanced(room, local, authorized),
|
|
3912
3790
|
});
|
|
3913
3791
|
const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
|
|
3914
3792
|
if (judged !== null && (judged.there !== null || judged.here !== null)) {
|
|
@@ -3932,6 +3810,13 @@ export function composeRuntime(options) {
|
|
|
3932
3810
|
(await landedWhole(room.locator, head, confirmedLayer))) ||
|
|
3933
3811
|
(result.kind === 'conflicted' &&
|
|
3934
3812
|
(await landedWhole(room.locator, head, confirmedLayer, result.affected)));
|
|
3813
|
+
if (held &&
|
|
3814
|
+
result.kind === 'conflicted' &&
|
|
3815
|
+
partner === null &&
|
|
3816
|
+
!landed(head, confirmedLayer)) {
|
|
3817
|
+
landedAhead.set(pendingKey(room.locator, head.id), Object.fromEntries([...touchedBy([head])].map((path) => [path, confirmedLayer.revisions[path]])));
|
|
3818
|
+
return;
|
|
3819
|
+
}
|
|
3935
3820
|
publishChangeSet(await settle(local, room.locator, current, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
|
|
3936
3821
|
await unpair(local);
|
|
3937
3822
|
moveTo({ ...status, ...attention() });
|
|
@@ -3988,8 +3873,10 @@ export function composeRuntime(options) {
|
|
|
3988
3873
|
for (const { room, before, queue } of held) {
|
|
3989
3874
|
const { locator } = room;
|
|
3990
3875
|
lastFenced.set(locator, ++fencings);
|
|
3991
|
-
for (const batch of queue)
|
|
3876
|
+
for (const batch of queue) {
|
|
3992
3877
|
pendings.delete(pendingKey(locator, batch.id));
|
|
3878
|
+
landedAhead.delete(pendingKey(locator, batch.id));
|
|
3879
|
+
}
|
|
3993
3880
|
queuesWithoutReplica.delete(locator);
|
|
3994
3881
|
if (before !== undefined) {
|
|
3995
3882
|
replicas.set(locator, {
|
|
@@ -4012,25 +3899,22 @@ export function composeRuntime(options) {
|
|
|
4012
3899
|
if (signal.aborted)
|
|
4013
3900
|
return;
|
|
4014
3901
|
const progress = { complete: true, reachable: true };
|
|
4015
|
-
await roomTask(room.locator,
|
|
4016
|
-
await Promise.race([
|
|
4017
|
-
reload(room, local, progress, () => live() && !signal.aborted),
|
|
4018
|
-
abandonedOn(signal),
|
|
4019
|
-
]);
|
|
4020
|
-
});
|
|
3902
|
+
await roomTask(room.locator, (live) => reload(room, local, progress, live));
|
|
4021
3903
|
if (signal.aborted)
|
|
4022
3904
|
return;
|
|
4023
3905
|
await rest(progress);
|
|
4024
3906
|
}
|
|
4025
3907
|
}
|
|
4026
3908
|
function awaitingAnswer(locator) {
|
|
3909
|
+
answering.get(locator)?.release();
|
|
4027
3910
|
let release = () => { };
|
|
4028
|
-
const
|
|
3911
|
+
const answered = new Promise((resolve) => {
|
|
4029
3912
|
release = resolve;
|
|
4030
3913
|
});
|
|
4031
|
-
|
|
3914
|
+
const awaited = { answered, release };
|
|
3915
|
+
answering.set(locator, awaited);
|
|
4032
3916
|
return () => {
|
|
4033
|
-
if (answering.get(locator)
|
|
3917
|
+
if (answering.get(locator) === awaited)
|
|
4034
3918
|
answering.delete(locator);
|
|
4035
3919
|
release();
|
|
4036
3920
|
};
|
|
@@ -4042,6 +3926,30 @@ export function composeRuntime(options) {
|
|
|
4042
3926
|
JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
|
|
4043
3927
|
JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
|
|
4044
3928
|
}
|
|
3929
|
+
async function keepNaming(local, room) {
|
|
3930
|
+
const held = replicas.get(room.locator)?.record;
|
|
3931
|
+
if (held === undefined || (held.name === room.name && held.handle === room.handle))
|
|
3932
|
+
return;
|
|
3933
|
+
await exclusive(async () => {
|
|
3934
|
+
if (!continuing())
|
|
3935
|
+
return;
|
|
3936
|
+
const replica = replicas.get(room.locator);
|
|
3937
|
+
if (replica === undefined)
|
|
3938
|
+
return;
|
|
3939
|
+
const record = { ...replica.record, name: room.name, handle: room.handle };
|
|
3940
|
+
await local.commit({
|
|
3941
|
+
rooms: [
|
|
3942
|
+
{
|
|
3943
|
+
locator: room.locator,
|
|
3944
|
+
key: record.key,
|
|
3945
|
+
record: storedRecord(record),
|
|
3946
|
+
composed: composedOf(composed(record)),
|
|
3947
|
+
},
|
|
3948
|
+
],
|
|
3949
|
+
});
|
|
3950
|
+
replicas.set(room.locator, { ...replica, record });
|
|
3951
|
+
});
|
|
3952
|
+
}
|
|
4045
3953
|
async function retainModel(local, room, next) {
|
|
4046
3954
|
const key = room.locator;
|
|
4047
3955
|
const previous = modelOf(room.locator);
|
|
@@ -4057,26 +3965,15 @@ export function composeRuntime(options) {
|
|
|
4057
3965
|
const held = replicas.get(room.locator);
|
|
4058
3966
|
let changed = null;
|
|
4059
3967
|
if (held !== undefined) {
|
|
4060
|
-
const
|
|
3968
|
+
const record = {
|
|
4061
3969
|
...held.record,
|
|
3970
|
+
name: room.name,
|
|
3971
|
+
handle: room.handle,
|
|
4062
3972
|
operations: room.operations,
|
|
4063
3973
|
replica: room.replica,
|
|
4064
3974
|
typedSelection: room.typedSelection ?? null,
|
|
4065
3975
|
model: next,
|
|
4066
3976
|
};
|
|
4067
|
-
let read = 'staged';
|
|
4068
|
-
if (!sameMembership(declared, held.record)) {
|
|
4069
|
-
const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), composed(declared));
|
|
4070
|
-
read =
|
|
4071
|
-
verdict.verdict !== 'refuse'
|
|
4072
|
-
? 'staged'
|
|
4073
|
-
: verdict.rule === 'model-unavailable'
|
|
4074
|
-
? 'deferred'
|
|
4075
|
-
: { kind: 'refused', refusal: verdict };
|
|
4076
|
-
}
|
|
4077
|
-
const record = read === 'staged'
|
|
4078
|
-
? declared
|
|
4079
|
-
: { ...declared, typedSelection: held.record.typedSelection };
|
|
4080
3977
|
const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
|
|
4081
3978
|
const replayedLayer = await replayed(record, confirmedLayer, held.queue);
|
|
4082
3979
|
const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
|
|
@@ -4098,16 +3995,6 @@ export function composeRuntime(options) {
|
|
|
4098
3995
|
documents: answer.changed[room.locator] ?? [],
|
|
4099
3996
|
room: true,
|
|
4100
3997
|
});
|
|
4101
|
-
if (read === 'staged' || read === 'deferred') {
|
|
4102
|
-
if (declarationRefusals.delete(room.locator)) {
|
|
4103
|
-
sourceRefusals.delete(room.locator);
|
|
4104
|
-
unreadable.delete(room.locator);
|
|
4105
|
-
}
|
|
4106
|
-
}
|
|
4107
|
-
else {
|
|
4108
|
-
refusedSource(room, read);
|
|
4109
|
-
declarationRefusals.add(room.locator);
|
|
4110
|
-
}
|
|
4111
3998
|
}
|
|
4112
3999
|
heldModel(room, next);
|
|
4113
4000
|
if (changed === null)
|
|
@@ -4152,6 +4039,17 @@ export function composeRuntime(options) {
|
|
|
4152
4039
|
emitRooms();
|
|
4153
4040
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
4154
4041
|
}
|
|
4042
|
+
function name(locator) {
|
|
4043
|
+
const stream = options.adapter.roomStream;
|
|
4044
|
+
const record = replicas.get(locator)?.record;
|
|
4045
|
+
const room = discovered.get(locator);
|
|
4046
|
+
if (stream === undefined || record === undefined || room === undefined)
|
|
4047
|
+
return;
|
|
4048
|
+
if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
|
|
4049
|
+
return;
|
|
4050
|
+
naming += 1;
|
|
4051
|
+
stream.hold([{ locator, roomVersion: record.roomVersion }]);
|
|
4052
|
+
}
|
|
4155
4053
|
function heldRooms(listed) {
|
|
4156
4054
|
return listed.flatMap((room) => {
|
|
4157
4055
|
const record = replicas.get(room.locator)?.record;
|
|
@@ -4169,14 +4067,19 @@ export function composeRuntime(options) {
|
|
|
4169
4067
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
4170
4068
|
});
|
|
4171
4069
|
if (attempt === 'opened') {
|
|
4070
|
+
naming += 1;
|
|
4172
4071
|
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
4173
4072
|
handed = true;
|
|
4174
4073
|
}
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4074
|
+
for (const room of ready) {
|
|
4075
|
+
if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
|
|
4076
|
+
if (attempt === 'authorized')
|
|
4077
|
+
name(room.locator);
|
|
4078
|
+
continue;
|
|
4079
|
+
}
|
|
4080
|
+
void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
|
|
4081
|
+
}
|
|
4082
|
+
const failed = prepared.find((room) => room.status === 'rejected');
|
|
4180
4083
|
if (failed !== undefined)
|
|
4181
4084
|
throw failed.reason;
|
|
4182
4085
|
}
|
|
@@ -4195,12 +4098,16 @@ export function composeRuntime(options) {
|
|
|
4195
4098
|
}
|
|
4196
4099
|
if (prior !== undefined)
|
|
4197
4100
|
await retainModel(local, room, modelOf(room.locator));
|
|
4101
|
+
if (prior !== undefined)
|
|
4102
|
+
await keepNaming(local, room);
|
|
4198
4103
|
if (discoveryPaused || authorization === undefined)
|
|
4199
4104
|
return false;
|
|
4200
4105
|
return room.replica.admission === 'admitted';
|
|
4201
4106
|
}
|
|
4202
4107
|
async function caughtUp(room, local, progress, live) {
|
|
4203
|
-
if (
|
|
4108
|
+
if (options.adapter.roomStream !== undefined || authorization === undefined)
|
|
4109
|
+
return;
|
|
4110
|
+
if (!live() || !behind(room))
|
|
4204
4111
|
return;
|
|
4205
4112
|
const withheld = await advance(room, local, authorization, live);
|
|
4206
4113
|
if (withheld !== null)
|
|
@@ -4213,6 +4120,8 @@ export function composeRuntime(options) {
|
|
|
4213
4120
|
async function loadRoom(room, local, progress, live) {
|
|
4214
4121
|
if (!live() || authorization === undefined)
|
|
4215
4122
|
return;
|
|
4123
|
+
if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
|
|
4124
|
+
return;
|
|
4216
4125
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
4217
4126
|
await reload(room, local, progress, live);
|
|
4218
4127
|
return;
|
|
@@ -4230,20 +4139,53 @@ export function composeRuntime(options) {
|
|
|
4230
4139
|
reloadsDue.delete(room.locator);
|
|
4231
4140
|
if (!live() || authorization === undefined)
|
|
4232
4141
|
return;
|
|
4142
|
+
const from = standingOf(room.locator);
|
|
4233
4143
|
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
4234
4144
|
if (bootstrapped.kind !== 'staged') {
|
|
4235
4145
|
if (live())
|
|
4236
4146
|
await withhold(bootstrapped, room, local, progress);
|
|
4237
4147
|
return;
|
|
4238
4148
|
}
|
|
4239
|
-
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
4149
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
|
|
4240
4150
|
await caughtUp(room, local, progress, live);
|
|
4241
4151
|
}
|
|
4242
4152
|
finally {
|
|
4243
4153
|
caughtUpTo(room.locator);
|
|
4244
4154
|
}
|
|
4245
4155
|
}
|
|
4246
|
-
async function
|
|
4156
|
+
async function asked(authorized) {
|
|
4157
|
+
try {
|
|
4158
|
+
return (await options.adapter.discover?.(authorized)) ?? { kind: 'transport-uncertainty' };
|
|
4159
|
+
}
|
|
4160
|
+
catch {
|
|
4161
|
+
return { kind: 'transport-uncertainty' };
|
|
4162
|
+
}
|
|
4163
|
+
}
|
|
4164
|
+
function malformed(listed) {
|
|
4165
|
+
for (const room of listed) {
|
|
4166
|
+
if (room.operations === undefined ||
|
|
4167
|
+
!['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
4168
|
+
room.operations[operation].every((scope) => typeof scope === 'string')) ||
|
|
4169
|
+
room.replica === undefined ||
|
|
4170
|
+
typeof room.replica.selection !== 'string' ||
|
|
4171
|
+
room.replica.selection.length === 0 ||
|
|
4172
|
+
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
4173
|
+
!['locked', 'open'].includes(room.replica.offline)) {
|
|
4174
|
+
return `invalid model or replica declaration ${room.locator}`;
|
|
4175
|
+
}
|
|
4176
|
+
if (!declaredSelection(room))
|
|
4177
|
+
return `invalid typed selection declaration ${room.locator}`;
|
|
4178
|
+
}
|
|
4179
|
+
return null;
|
|
4180
|
+
}
|
|
4181
|
+
async function signIn(local, signedIn) {
|
|
4182
|
+
if (JSON.stringify(signedIn) === JSON.stringify(member))
|
|
4183
|
+
return;
|
|
4184
|
+
await local.fact('member', signedIn);
|
|
4185
|
+
member = signedIn;
|
|
4186
|
+
emitMember();
|
|
4187
|
+
}
|
|
4188
|
+
async function discoverRooms(local, progress, attempt, live, found) {
|
|
4247
4189
|
if (closed || authorization === undefined) {
|
|
4248
4190
|
notifyReady();
|
|
4249
4191
|
return;
|
|
@@ -4251,13 +4193,7 @@ export function composeRuntime(options) {
|
|
|
4251
4193
|
moveTo(transferring(status));
|
|
4252
4194
|
discoveries += 1;
|
|
4253
4195
|
const started = discoveries;
|
|
4254
|
-
|
|
4255
|
-
try {
|
|
4256
|
-
result = await options.adapter.discover(authorization, scope);
|
|
4257
|
-
}
|
|
4258
|
-
catch {
|
|
4259
|
-
result = { kind: 'transport-uncertainty' };
|
|
4260
|
-
}
|
|
4196
|
+
const result = found ?? (await asked(authorization));
|
|
4261
4197
|
if (result.kind === 'discovery')
|
|
4262
4198
|
roomsNamed = true;
|
|
4263
4199
|
if (!live()) {
|
|
@@ -4267,7 +4203,7 @@ export function composeRuntime(options) {
|
|
|
4267
4203
|
}
|
|
4268
4204
|
journal.record(`connection discover ${result.kind}`);
|
|
4269
4205
|
if (result.kind === 'transport-uncertainty') {
|
|
4270
|
-
await
|
|
4206
|
+
await cachedModels(local);
|
|
4271
4207
|
progress.reachable = false;
|
|
4272
4208
|
progress.complete = false;
|
|
4273
4209
|
notifyReady();
|
|
@@ -4288,26 +4224,12 @@ export function composeRuntime(options) {
|
|
|
4288
4224
|
notifyReady();
|
|
4289
4225
|
return;
|
|
4290
4226
|
}
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
room.replica.selection.length === 0 ||
|
|
4298
|
-
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
4299
|
-
!['locked', 'open'].includes(room.replica.offline)) {
|
|
4300
|
-
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
4301
|
-
await refuse('missing-required-guarantee');
|
|
4302
|
-
notifyReady();
|
|
4303
|
-
return;
|
|
4304
|
-
}
|
|
4305
|
-
if (!declaredSelection(room)) {
|
|
4306
|
-
journal.record(`invalid typed selection declaration ${room.locator}`);
|
|
4307
|
-
await refuse('missing-required-guarantee');
|
|
4308
|
-
notifyReady();
|
|
4309
|
-
return;
|
|
4310
|
-
}
|
|
4227
|
+
const invalid = malformed(result.rooms);
|
|
4228
|
+
if (invalid !== null) {
|
|
4229
|
+
journal.record(invalid);
|
|
4230
|
+
await refuse('missing-required-guarantee');
|
|
4231
|
+
notifyReady();
|
|
4232
|
+
return;
|
|
4311
4233
|
}
|
|
4312
4234
|
if (refusal !== null) {
|
|
4313
4235
|
refusal = null;
|
|
@@ -4322,15 +4244,9 @@ export function composeRuntime(options) {
|
|
|
4322
4244
|
limits = result.providerLimits;
|
|
4323
4245
|
await local.fact('providerLimits', limits);
|
|
4324
4246
|
await local.fact('capabilities', capabilities);
|
|
4325
|
-
conversations.declared();
|
|
4326
|
-
}
|
|
4327
|
-
const signedIn = result.member ?? null;
|
|
4328
|
-
if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
|
|
4329
|
-
await local.fact('member', signedIn);
|
|
4330
|
-
member = signedIn;
|
|
4331
|
-
emitMember();
|
|
4332
4247
|
}
|
|
4333
|
-
await
|
|
4248
|
+
await signIn(local, result.member ?? null);
|
|
4249
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
4334
4250
|
}
|
|
4335
4251
|
async function ensureAuthorization() {
|
|
4336
4252
|
if (authorization !== undefined)
|
|
@@ -4345,23 +4261,45 @@ export function composeRuntime(options) {
|
|
|
4345
4261
|
}
|
|
4346
4262
|
}
|
|
4347
4263
|
async function admittedRead(work) {
|
|
4348
|
-
|
|
4349
|
-
|
|
4264
|
+
while (roomReads.free === 0) {
|
|
4265
|
+
await new Promise((admit) => roomReads.waiting.push(admit));
|
|
4350
4266
|
}
|
|
4351
|
-
|
|
4352
|
-
while (reads.free === 0)
|
|
4353
|
-
await new Promise((admit) => reads.waiting.push(admit));
|
|
4354
|
-
reads.free -= 1;
|
|
4267
|
+
roomReads.free -= 1;
|
|
4355
4268
|
try {
|
|
4356
4269
|
await work();
|
|
4357
4270
|
}
|
|
4358
4271
|
finally {
|
|
4359
|
-
|
|
4360
|
-
|
|
4272
|
+
roomReads.free += 1;
|
|
4273
|
+
roomReads.waiting.shift()?.();
|
|
4361
4274
|
}
|
|
4362
4275
|
}
|
|
4276
|
+
function inRoom(locator, work) {
|
|
4277
|
+
roomWork += 1;
|
|
4278
|
+
const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
|
|
4279
|
+
try {
|
|
4280
|
+
return await work(continuing);
|
|
4281
|
+
}
|
|
4282
|
+
finally {
|
|
4283
|
+
roomWork -= 1;
|
|
4284
|
+
}
|
|
4285
|
+
});
|
|
4286
|
+
const chain = ran.then(() => undefined, () => undefined);
|
|
4287
|
+
roomLanes.set(locator, chain);
|
|
4288
|
+
working.add(chain);
|
|
4289
|
+
void chain.finally(() => {
|
|
4290
|
+
working.delete(chain);
|
|
4291
|
+
if (roomLanes.get(locator) === chain)
|
|
4292
|
+
roomLanes.delete(locator);
|
|
4293
|
+
});
|
|
4294
|
+
return ran;
|
|
4295
|
+
}
|
|
4363
4296
|
function roomTask(locator, work) {
|
|
4364
|
-
|
|
4297
|
+
if (stopping)
|
|
4298
|
+
return Promise.resolve();
|
|
4299
|
+
return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
|
|
4300
|
+
}
|
|
4301
|
+
function advanced(room, local, authorized) {
|
|
4302
|
+
return inRoom(room.locator, (live) => advance(room, local, authorized, live));
|
|
4365
4303
|
}
|
|
4366
4304
|
function task(lane, work) {
|
|
4367
4305
|
if (stopping)
|
|
@@ -4377,23 +4315,19 @@ export function composeRuntime(options) {
|
|
|
4377
4315
|
void run.finally(() => working.delete(run));
|
|
4378
4316
|
return run;
|
|
4379
4317
|
}
|
|
4380
|
-
function sync(attempt) {
|
|
4318
|
+
function sync(attempt, found) {
|
|
4381
4319
|
return task(OPENING_LANE, async (live) => {
|
|
4382
4320
|
if (!live()) {
|
|
4383
4321
|
readyFromStore();
|
|
4384
4322
|
return;
|
|
4385
4323
|
}
|
|
4386
4324
|
const progress = { complete: true, reachable: true };
|
|
4387
|
-
|
|
4388
|
-
if (attempt === 'opened')
|
|
4389
|
-
opened = generation;
|
|
4390
|
-
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
4325
|
+
underway = attempt === 'opened' ? progress : null;
|
|
4391
4326
|
if (attempt === 'opened')
|
|
4392
4327
|
streamed = progress;
|
|
4393
4328
|
complete = false;
|
|
4394
4329
|
recorded = false;
|
|
4395
4330
|
moveTo(transferring(status));
|
|
4396
|
-
handed = false;
|
|
4397
4331
|
try {
|
|
4398
4332
|
const local = await openLocal();
|
|
4399
4333
|
if (!live())
|
|
@@ -4401,7 +4335,7 @@ export function composeRuntime(options) {
|
|
|
4401
4335
|
await ensureAuthorization();
|
|
4402
4336
|
if (!live())
|
|
4403
4337
|
return;
|
|
4404
|
-
await discoverRooms(local, progress, attempt, live);
|
|
4338
|
+
await discoverRooms(local, progress, attempt, live, found);
|
|
4405
4339
|
}
|
|
4406
4340
|
catch {
|
|
4407
4341
|
progress.complete = false;
|
|
@@ -4409,7 +4343,6 @@ export function composeRuntime(options) {
|
|
|
4409
4343
|
progress.reachable = false;
|
|
4410
4344
|
}
|
|
4411
4345
|
finally {
|
|
4412
|
-
gate.release();
|
|
4413
4346
|
notifyReady();
|
|
4414
4347
|
}
|
|
4415
4348
|
if (!live())
|
|
@@ -4421,35 +4354,59 @@ export function composeRuntime(options) {
|
|
|
4421
4354
|
if (handed || closed || progress.reachable)
|
|
4422
4355
|
return;
|
|
4423
4356
|
underway = null;
|
|
4424
|
-
opened = null;
|
|
4425
|
-
journal.record('connection reopen');
|
|
4426
4357
|
await rest(progress, live);
|
|
4427
|
-
options.adapter.roomStream?.reopen();
|
|
4428
4358
|
});
|
|
4429
4359
|
}
|
|
4430
|
-
function
|
|
4360
|
+
function resynchronize() {
|
|
4431
4361
|
return task(OPENING_LANE, async (live) => {
|
|
4432
|
-
if (!live() || !
|
|
4362
|
+
if (!live() || !announced)
|
|
4433
4363
|
return;
|
|
4434
4364
|
const progress = { complete: true, reachable: true };
|
|
4435
|
-
const gate = discover();
|
|
4436
4365
|
moveTo(transferring(status));
|
|
4437
4366
|
try {
|
|
4438
4367
|
const local = await openLocal();
|
|
4439
4368
|
if (!live())
|
|
4440
4369
|
return;
|
|
4441
|
-
await
|
|
4370
|
+
await synchronize([...discovered.values()], local, progress, 'unstreamed', live);
|
|
4371
|
+
}
|
|
4372
|
+
catch {
|
|
4373
|
+
progress.complete = false;
|
|
4374
|
+
}
|
|
4375
|
+
if (live())
|
|
4376
|
+
await finishOpening(progress, live);
|
|
4377
|
+
});
|
|
4378
|
+
}
|
|
4379
|
+
function countAuthorityChange() {
|
|
4380
|
+
authorityChanges += 1;
|
|
4381
|
+
for (const listener of authorityChangeListeners)
|
|
4382
|
+
listener(authorityChanges);
|
|
4383
|
+
}
|
|
4384
|
+
function changeAuthority(change) {
|
|
4385
|
+
return task(OPENING_LANE, async (live) => {
|
|
4386
|
+
if (!live() || !announced)
|
|
4387
|
+
return;
|
|
4388
|
+
countAuthorityChange();
|
|
4389
|
+
const progress = { complete: true, reachable: true };
|
|
4390
|
+
moveTo(transferring(status));
|
|
4391
|
+
try {
|
|
4392
|
+
const local = await openLocal();
|
|
4442
4393
|
if (!live())
|
|
4443
4394
|
return;
|
|
4444
|
-
|
|
4395
|
+
const invalid = malformed(change.rooms);
|
|
4396
|
+
if (invalid !== null) {
|
|
4397
|
+
journal.record(invalid);
|
|
4398
|
+
await refuse('missing-required-guarantee');
|
|
4399
|
+
return;
|
|
4400
|
+
}
|
|
4401
|
+
if (change.member !== undefined)
|
|
4402
|
+
await signIn(local, change.member);
|
|
4403
|
+
discoveries += 1;
|
|
4404
|
+
const listed = await applyDiscovery(change.rooms, discoveries, change.left);
|
|
4405
|
+
await synchronize(listed, local, progress, 'authorized', live);
|
|
4445
4406
|
}
|
|
4446
4407
|
catch {
|
|
4447
4408
|
progress.complete = false;
|
|
4448
4409
|
}
|
|
4449
|
-
finally {
|
|
4450
|
-
gate.release();
|
|
4451
|
-
notifyReady();
|
|
4452
|
-
}
|
|
4453
4410
|
if (!live())
|
|
4454
4411
|
return;
|
|
4455
4412
|
if (!progress.complete)
|
|
@@ -4457,15 +4414,17 @@ export function composeRuntime(options) {
|
|
|
4457
4414
|
await rest(progress, live);
|
|
4458
4415
|
});
|
|
4459
4416
|
}
|
|
4460
|
-
function closedSocket(revoked, lost) {
|
|
4461
|
-
|
|
4462
|
-
const opened =
|
|
4417
|
+
function closedSocket(revoked, lost, authorityChanged) {
|
|
4418
|
+
notifyReady();
|
|
4419
|
+
const opened = announced;
|
|
4463
4420
|
socketOpen = false;
|
|
4421
|
+
announced = false;
|
|
4422
|
+
missedChange ||= authorityChanged;
|
|
4423
|
+
handed = false;
|
|
4424
|
+
naming = 0;
|
|
4464
4425
|
connectionLost();
|
|
4465
|
-
if (lost)
|
|
4466
|
-
reads.lost();
|
|
4426
|
+
if (lost)
|
|
4467
4427
|
conversations.lost();
|
|
4468
|
-
}
|
|
4469
4428
|
if (revoked) {
|
|
4470
4429
|
void refuse('revoked-authorization');
|
|
4471
4430
|
return;
|
|
@@ -4476,18 +4435,32 @@ export function composeRuntime(options) {
|
|
|
4476
4435
|
moveTo({
|
|
4477
4436
|
...status,
|
|
4478
4437
|
state: status.state === 'synced' ? 'syncing' : status.state,
|
|
4479
|
-
providerUnreachable: status.providerUnreachable || !opened,
|
|
4438
|
+
providerUnreachable: status.providerUnreachable || (!opened && lost),
|
|
4480
4439
|
});
|
|
4440
|
+
void closing(opened);
|
|
4481
4441
|
}
|
|
4482
|
-
function
|
|
4483
|
-
|
|
4442
|
+
function closing(opened) {
|
|
4443
|
+
return task(OPENING_LANE, async (live) => {
|
|
4444
|
+
if (!live())
|
|
4445
|
+
return;
|
|
4446
|
+
const local = await openLocal();
|
|
4447
|
+
if (!opened && !socketOpen)
|
|
4448
|
+
await cachedModels(local);
|
|
4449
|
+
await rest(null, live);
|
|
4450
|
+
});
|
|
4451
|
+
}
|
|
4452
|
+
function cachedModels(local) {
|
|
4453
|
+
return settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
|
|
4454
|
+
}
|
|
4455
|
+
function caughtUpRooms(expired) {
|
|
4456
|
+
const progress = underway;
|
|
4457
|
+
underway = null;
|
|
4458
|
+
naming = Math.max(0, naming - 1);
|
|
4484
4459
|
const cycle = progress ?? { complete: true, reachable: true };
|
|
4485
|
-
|
|
4486
|
-
opened = null;
|
|
4460
|
+
replaced(expired);
|
|
4487
4461
|
const reloading = progress === null ? [] : owedReloads(expired);
|
|
4488
4462
|
const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
|
|
4489
4463
|
return task(OPENING_LANE, async (live) => {
|
|
4490
|
-
underway = null;
|
|
4491
4464
|
if (!live())
|
|
4492
4465
|
return;
|
|
4493
4466
|
if (progress === null) {
|
|
@@ -4495,20 +4468,14 @@ export function composeRuntime(options) {
|
|
|
4495
4468
|
await rest(cycle, live);
|
|
4496
4469
|
return;
|
|
4497
4470
|
}
|
|
4498
|
-
await roomsCarried([...expired, ...reloading]);
|
|
4499
|
-
if (!live())
|
|
4500
|
-
return;
|
|
4501
4471
|
try {
|
|
4502
4472
|
const local = await openLocal();
|
|
4503
|
-
|
|
4473
|
+
await replacing;
|
|
4504
4474
|
if (progress.reachable) {
|
|
4505
4475
|
await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
|
|
4506
4476
|
!expired.includes(room.locator) &&
|
|
4507
4477
|
!reloading.includes(room.locator)), 'current', local);
|
|
4508
4478
|
}
|
|
4509
|
-
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
4510
|
-
if (failed !== undefined)
|
|
4511
|
-
throw failed.reason;
|
|
4512
4479
|
}
|
|
4513
4480
|
catch {
|
|
4514
4481
|
progress.complete = false;
|
|
@@ -4520,11 +4487,8 @@ export function composeRuntime(options) {
|
|
|
4520
4487
|
}
|
|
4521
4488
|
function owedReloads(expired) {
|
|
4522
4489
|
const reloading = [...discovered.values()]
|
|
4523
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
4524
|
-
!expired.includes(room.locator) &&
|
|
4525
|
-
(owed.has(room.locator) || revoked(room.locator)))
|
|
4490
|
+
.filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
|
|
4526
4491
|
.map((room) => room.locator);
|
|
4527
|
-
owed.clear();
|
|
4528
4492
|
for (const locator of reloading)
|
|
4529
4493
|
catching.add(locator);
|
|
4530
4494
|
liveQueries.recover();
|
|
@@ -4533,16 +4497,17 @@ export function composeRuntime(options) {
|
|
|
4533
4497
|
async function reloadNow(replaced, progress) {
|
|
4534
4498
|
const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
|
|
4535
4499
|
for (const room of reloaded)
|
|
4536
|
-
reloadsDue.
|
|
4500
|
+
reloadsDue.add(room.locator);
|
|
4537
4501
|
try {
|
|
4538
4502
|
const local = await openLocal();
|
|
4539
|
-
|
|
4503
|
+
for (const room of reloaded) {
|
|
4504
|
+
void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
|
|
4505
|
+
}
|
|
4540
4506
|
}
|
|
4541
4507
|
catch {
|
|
4542
4508
|
for (const room of reloaded)
|
|
4543
4509
|
reloadsDue.delete(room.locator);
|
|
4544
4510
|
progress.reachable = false;
|
|
4545
|
-
return [];
|
|
4546
4511
|
}
|
|
4547
4512
|
}
|
|
4548
4513
|
async function settleModels(held, status, local) {
|
|
@@ -4553,7 +4518,7 @@ export function composeRuntime(options) {
|
|
|
4553
4518
|
const settled = await exclusive(async () => {
|
|
4554
4519
|
if (!continuing())
|
|
4555
4520
|
return [];
|
|
4556
|
-
const unchanged = held.filter((room) => owed(room) &&
|
|
4521
|
+
const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
|
|
4557
4522
|
for (const room of unchanged) {
|
|
4558
4523
|
const next = { ...modelOf(room.locator), status };
|
|
4559
4524
|
const replica = replicas.get(room.locator);
|
|
@@ -4613,14 +4578,10 @@ export function composeRuntime(options) {
|
|
|
4613
4578
|
try {
|
|
4614
4579
|
const local = await openLocal();
|
|
4615
4580
|
await roomTask(locator, async (live) => {
|
|
4616
|
-
|
|
4617
|
-
if (!current() || authorization === undefined)
|
|
4581
|
+
if (!live() || authorization === undefined)
|
|
4618
4582
|
return;
|
|
4619
|
-
const withheld = await
|
|
4620
|
-
|
|
4621
|
-
abandonedOn(signal),
|
|
4622
|
-
]);
|
|
4623
|
-
if (withheld !== null && current())
|
|
4583
|
+
const withheld = await advance(room, local, authorization, live);
|
|
4584
|
+
if (withheld !== null && live())
|
|
4624
4585
|
await withhold(withheld, room, local, progress);
|
|
4625
4586
|
});
|
|
4626
4587
|
}
|
|
@@ -4675,7 +4636,6 @@ export function composeRuntime(options) {
|
|
|
4675
4636
|
progress.complete = false;
|
|
4676
4637
|
return;
|
|
4677
4638
|
}
|
|
4678
|
-
reads.applied();
|
|
4679
4639
|
submitAll();
|
|
4680
4640
|
for (const locator of stale)
|
|
4681
4641
|
if (repairable.has(locator))
|
|
@@ -4706,7 +4666,7 @@ export function composeRuntime(options) {
|
|
|
4706
4666
|
if (!live())
|
|
4707
4667
|
return;
|
|
4708
4668
|
let lastCompleted = status.lastCompleted;
|
|
4709
|
-
let synced = complete && stale.size === 0 && !pending();
|
|
4669
|
+
let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
|
|
4710
4670
|
if (synced && !recorded) {
|
|
4711
4671
|
const completedAt = new Date().toISOString();
|
|
4712
4672
|
try {
|
|
@@ -4721,75 +4681,78 @@ export function composeRuntime(options) {
|
|
|
4721
4681
|
}
|
|
4722
4682
|
}
|
|
4723
4683
|
moveTo({
|
|
4724
|
-
state: pending()
|
|
4684
|
+
state: pending()
|
|
4685
|
+
? 'pending'
|
|
4686
|
+
: synced && complete && roomWork === 0 && naming === 0
|
|
4687
|
+
? 'synced'
|
|
4688
|
+
: 'syncing',
|
|
4725
4689
|
lastCompleted,
|
|
4726
|
-
providerUnreachable: !progress.reachable,
|
|
4690
|
+
providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
|
|
4727
4691
|
...attention(),
|
|
4728
4692
|
});
|
|
4729
4693
|
}
|
|
4730
|
-
function
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
}
|
|
4738
|
-
function applyRoom(locator, roomVersion) {
|
|
4739
|
-
announced.set(locator, roomVersion);
|
|
4740
|
-
return task(roomLane(locator), async (live) => {
|
|
4741
|
-
if (discovering?.generation === generation)
|
|
4742
|
-
await discovering.done;
|
|
4743
|
-
const saving = answering.get(locator);
|
|
4744
|
-
if (saving?.generation === generation)
|
|
4745
|
-
await saving.answered;
|
|
4746
|
-
if (!live() || authorization === undefined)
|
|
4747
|
-
return;
|
|
4748
|
-
const room = discovered.get(locator);
|
|
4749
|
-
const held = replicas.get(locator);
|
|
4750
|
-
if (room === undefined || room.replica.admission !== 'admitted')
|
|
4751
|
-
return;
|
|
4752
|
-
if (held !== undefined &&
|
|
4753
|
-
(held.record.replica.selection !== room.replica.selection ||
|
|
4754
|
-
options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
|
|
4755
|
-
return;
|
|
4756
|
-
}
|
|
4757
|
-
const during = underway?.generation === generation ? underway.progress : null;
|
|
4758
|
-
const progress = during ?? { complete: true, reachable: true };
|
|
4759
|
-
if (during === null)
|
|
4760
|
-
moveTo(transferring(status));
|
|
4694
|
+
function applyRoom(change) {
|
|
4695
|
+
const { locator } = change;
|
|
4696
|
+
if (stopping)
|
|
4697
|
+
return Promise.resolve();
|
|
4698
|
+
return inRoom(locator, async (live) => {
|
|
4699
|
+
await answering.get(locator)?.answered;
|
|
4700
|
+
const progress = underway ?? { complete: true, reachable: true };
|
|
4761
4701
|
try {
|
|
4762
4702
|
const local = await openLocal();
|
|
4763
|
-
|
|
4764
|
-
if (!live()
|
|
4765
|
-
return;
|
|
4766
|
-
const
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4703
|
+
for (;;) {
|
|
4704
|
+
if (!live())
|
|
4705
|
+
return null;
|
|
4706
|
+
const room = discovered.get(locator);
|
|
4707
|
+
const held = replicas.get(locator);
|
|
4708
|
+
if (room === undefined ||
|
|
4709
|
+
held === undefined ||
|
|
4710
|
+
room.replica.admission !== 'admitted' ||
|
|
4711
|
+
held.record.replica.selection !== room.replica.selection) {
|
|
4712
|
+
return null;
|
|
4713
|
+
}
|
|
4714
|
+
const at = held.record.roomVersion;
|
|
4715
|
+
if (options.adapter.reached(locator, at, change.roomVersion))
|
|
4716
|
+
return progress;
|
|
4717
|
+
if (!options.adapter.reached(locator, at, change.from)) {
|
|
4718
|
+
name(locator);
|
|
4719
|
+
return progress;
|
|
4720
|
+
}
|
|
4721
|
+
if (underway === null)
|
|
4722
|
+
moveTo(transferring(status));
|
|
4723
|
+
const writing = () => live() && !full;
|
|
4724
|
+
let outlined = false;
|
|
4725
|
+
const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, async () => {
|
|
4726
|
+
const page = await change.read();
|
|
4727
|
+
const paths = Object.keys(page.outlines ?? {});
|
|
4728
|
+
outlined = paths.length > 0;
|
|
4729
|
+
await local.remember(locator, held.record.key, paths);
|
|
4730
|
+
return page;
|
|
4731
|
+
}));
|
|
4732
|
+
if (refreshed.kind !== 'advanced')
|
|
4733
|
+
return progress;
|
|
4734
|
+
const landing = {
|
|
4735
|
+
kind: 'refreshed',
|
|
4736
|
+
from: refreshed.from,
|
|
4737
|
+
remodelled: refreshed.remodelled,
|
|
4738
|
+
};
|
|
4739
|
+
if (await publish(local, room, refreshed.record, landing, writing)) {
|
|
4740
|
+
stale.delete(locator);
|
|
4741
|
+
await retainModel(local, room, modelOf(locator));
|
|
4742
|
+
if (outlined)
|
|
4743
|
+
void heldAgain(locator);
|
|
4744
|
+
return progress;
|
|
4745
|
+
}
|
|
4746
|
+
}
|
|
4770
4747
|
}
|
|
4771
4748
|
catch {
|
|
4749
|
+
if (!stale.has(locator))
|
|
4750
|
+
name(locator);
|
|
4772
4751
|
stale.add(locator);
|
|
4752
|
+
return progress;
|
|
4773
4753
|
}
|
|
4774
|
-
|
|
4775
|
-
if (during === null)
|
|
4776
|
-
await rest(progress, live);
|
|
4777
|
-
}
|
|
4778
|
-
});
|
|
4754
|
+
}).then((progress) => rest(progress), () => undefined);
|
|
4779
4755
|
}
|
|
4780
|
-
function roomsCarried(replacing) {
|
|
4781
|
-
const replaced = new Set(replacing.map(roomLane));
|
|
4782
|
-
return Promise.all([...lanes]
|
|
4783
|
-
.filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
|
|
4784
|
-
.map(([, held]) => held.chain));
|
|
4785
|
-
}
|
|
4786
|
-
const receipts = {
|
|
4787
|
-
heads: async () => (await openLocal()).receivedHeads(),
|
|
4788
|
-
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
4789
|
-
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
4790
|
-
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
4791
|
-
ahead,
|
|
4792
|
-
};
|
|
4793
4756
|
function observeStream() {
|
|
4794
4757
|
const stream = options.adapter.roomStream;
|
|
4795
4758
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -4799,31 +4762,35 @@ export function composeRuntime(options) {
|
|
|
4799
4762
|
return;
|
|
4800
4763
|
if (event.kind === 'opened') {
|
|
4801
4764
|
socketOpen = true;
|
|
4765
|
+
announced = false;
|
|
4766
|
+
handed = false;
|
|
4767
|
+
naming = 0;
|
|
4802
4768
|
connectionLive();
|
|
4803
4769
|
generation += 1;
|
|
4804
|
-
for (const admit of installWaiters.splice(0))
|
|
4805
|
-
admit();
|
|
4806
|
-
reads.opened();
|
|
4807
4770
|
conversations.reopened();
|
|
4808
4771
|
recovery.resume();
|
|
4809
|
-
|
|
4772
|
+
}
|
|
4773
|
+
else if (event.kind === 'discovered') {
|
|
4774
|
+
if (!socketOpen)
|
|
4775
|
+
return;
|
|
4776
|
+
announced = true;
|
|
4777
|
+
if (missedChange) {
|
|
4778
|
+
missedChange = false;
|
|
4779
|
+
countAuthorityChange();
|
|
4780
|
+
}
|
|
4781
|
+
void sync('opened', event.discovery);
|
|
4782
|
+
for (const locator of replicas.keys())
|
|
4783
|
+
void heldAgain(locator);
|
|
4810
4784
|
}
|
|
4811
4785
|
else if (event.kind === 'closed')
|
|
4812
|
-
closedSocket(event.revoked, event.lost);
|
|
4813
|
-
else if (event.kind === '
|
|
4814
|
-
void
|
|
4786
|
+
closedSocket(event.revoked, event.lost, event.authorityChanged);
|
|
4787
|
+
else if (event.kind === 'caught-up')
|
|
4788
|
+
void caughtUpRooms(event.expired);
|
|
4815
4789
|
else if (event.kind === 'authority-changed')
|
|
4816
|
-
void
|
|
4790
|
+
void changeAuthority(event);
|
|
4817
4791
|
else
|
|
4818
|
-
void applyRoom(event
|
|
4819
|
-
}
|
|
4820
|
-
}
|
|
4821
|
-
async function heldRevisions(locator, paths) {
|
|
4822
|
-
const held = replicas.get(locator);
|
|
4823
|
-
if (held === undefined || paths.length === 0)
|
|
4824
|
-
return new Map();
|
|
4825
|
-
const entries = await ahead(async () => (await openLocal()).entries(held.record.key, 'confirmed', paths));
|
|
4826
|
-
return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
|
|
4792
|
+
void applyRoom(event);
|
|
4793
|
+
});
|
|
4827
4794
|
}
|
|
4828
4795
|
function writesHeld() {
|
|
4829
4796
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -4896,8 +4863,6 @@ export function composeRuntime(options) {
|
|
|
4896
4863
|
notifyDiscovered();
|
|
4897
4864
|
if (probing !== null)
|
|
4898
4865
|
clearInterval(probing);
|
|
4899
|
-
for (const resolve of shownWaiters.splice(0))
|
|
4900
|
-
resolve();
|
|
4901
4866
|
connectionLive();
|
|
4902
4867
|
lockListeners.clear();
|
|
4903
4868
|
readinessWatches.clear();
|
|
@@ -4915,6 +4880,7 @@ export function composeRuntime(options) {
|
|
|
4915
4880
|
statusListeners.clear();
|
|
4916
4881
|
serviceModeListeners.clear();
|
|
4917
4882
|
memberListeners.clear();
|
|
4883
|
+
authorityChangeListeners.clear();
|
|
4918
4884
|
liveQueries.close();
|
|
4919
4885
|
queueSubscriptions.clear();
|
|
4920
4886
|
roomDraftListeners.clear();
|
|
@@ -4922,6 +4888,7 @@ export function composeRuntime(options) {
|
|
|
4922
4888
|
}
|
|
4923
4889
|
async function stop() {
|
|
4924
4890
|
stopping = true;
|
|
4891
|
+
inFlight.abort();
|
|
4925
4892
|
await recovery.retire();
|
|
4926
4893
|
await Promise.all(working);
|
|
4927
4894
|
await mutations;
|
|
@@ -4940,15 +4907,15 @@ export function composeRuntime(options) {
|
|
|
4940
4907
|
readyFromStore();
|
|
4941
4908
|
});
|
|
4942
4909
|
if (options.adapter.roomStream === undefined)
|
|
4943
|
-
void sync('unstreamed');
|
|
4910
|
+
void sync('unstreamed', null);
|
|
4944
4911
|
function planned(input, reach) {
|
|
4945
|
-
const plan =
|
|
4912
|
+
const plan = planWithin(input, roomFacts(reach), planReach(reach));
|
|
4946
4913
|
const named = plan.paths !== null &&
|
|
4947
4914
|
plan.rooms !== null &&
|
|
4948
4915
|
plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
|
|
4949
4916
|
return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
|
|
4950
4917
|
}
|
|
4951
|
-
function watchPlan(plan, deliver) {
|
|
4918
|
+
function watchPlan(plan, deliver, fail) {
|
|
4952
4919
|
let seeded = false;
|
|
4953
4920
|
let pending = true;
|
|
4954
4921
|
const shown = () => {
|
|
@@ -4974,7 +4941,7 @@ export function composeRuntime(options) {
|
|
|
4974
4941
|
}
|
|
4975
4942
|
seeded = false;
|
|
4976
4943
|
deliver(message);
|
|
4977
|
-
});
|
|
4944
|
+
}, fail);
|
|
4978
4945
|
return () => {
|
|
4979
4946
|
watchers.delete(shown);
|
|
4980
4947
|
if (watchers.size === 0)
|
|
@@ -4982,61 +4949,59 @@ export function composeRuntime(options) {
|
|
|
4982
4949
|
stop();
|
|
4983
4950
|
};
|
|
4984
4951
|
}
|
|
4985
|
-
async function searched(input, reach
|
|
4952
|
+
async function searched(input, reach) {
|
|
4986
4953
|
const asked = searchPlanOf(input);
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
rooms: answered.coverage.rooms,
|
|
5031
|
-
conversations: named,
|
|
5032
|
-
overlay: answered.coverage.overlay,
|
|
4954
|
+
await ready;
|
|
4955
|
+
const search = options.adapter.search;
|
|
4956
|
+
if (search === undefined) {
|
|
4957
|
+
return {
|
|
4958
|
+
kind: 'undeclared',
|
|
4959
|
+
code: 'action-undeclared',
|
|
4960
|
+
fields: { name: 'search' },
|
|
4961
|
+
retryable: false,
|
|
4962
|
+
};
|
|
4963
|
+
}
|
|
4964
|
+
if (closed || refusal !== null || connectionOf() !== 'connected')
|
|
4965
|
+
return OFFLINE;
|
|
4966
|
+
const reached = reach === null ? null : new Set(reach.rooms);
|
|
4967
|
+
const named = asked.rooms === null
|
|
4968
|
+
? reached === null
|
|
4969
|
+
? null
|
|
4970
|
+
: [...reached]
|
|
4971
|
+
: asked.rooms.filter((room) => reached === null || reached.has(room));
|
|
4972
|
+
const rooms = named === null ? null : named.filter((room) => replicas.has(room)).sort();
|
|
4973
|
+
if (rooms !== null && rooms.length === 0)
|
|
4974
|
+
return { kind: 'found', documents: [] };
|
|
4975
|
+
try {
|
|
4976
|
+
await ensureAuthorization();
|
|
4977
|
+
}
|
|
4978
|
+
catch {
|
|
4979
|
+
return UNCERTAIN;
|
|
4980
|
+
}
|
|
4981
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
4982
|
+
return UNCERTAIN;
|
|
4983
|
+
journal.record('transfer search');
|
|
4984
|
+
try {
|
|
4985
|
+
const result = await search(authorization, { text: asked.text, rooms });
|
|
4986
|
+
if (result.kind === 'revoked-authorization')
|
|
4987
|
+
await refuse('revoked-authorization');
|
|
4988
|
+
journal.record(`outcome search ${result.kind}`);
|
|
4989
|
+
if (result.kind === 'rejected')
|
|
4990
|
+
return result;
|
|
4991
|
+
if (result.kind !== 'found')
|
|
4992
|
+
return failure(result);
|
|
4993
|
+
const kept = rooms === null ? null : new Set(rooms);
|
|
4994
|
+
return {
|
|
4995
|
+
kind: 'found',
|
|
4996
|
+
documents: result.documents.filter((hit) => kept === null || kept.has(hit.room)),
|
|
5033
4997
|
};
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
4998
|
+
}
|
|
4999
|
+
catch {
|
|
5000
|
+
journal.record('outcome search transport-uncertainty');
|
|
5001
|
+
return UNCERTAIN;
|
|
5037
5002
|
}
|
|
5038
5003
|
}
|
|
5039
|
-
const watch = (topic, input, deliver) => {
|
|
5004
|
+
const watch = (topic, input, deliver, fail) => {
|
|
5040
5005
|
if (topic === 'room-drafts') {
|
|
5041
5006
|
const listener = { notify: deliver, ready: false };
|
|
5042
5007
|
roomDraftListeners.add(listener);
|
|
@@ -5085,7 +5050,7 @@ export function composeRuntime(options) {
|
|
|
5085
5050
|
};
|
|
5086
5051
|
}
|
|
5087
5052
|
if (topic === 'readiness') {
|
|
5088
|
-
return watchReadiness({ target: '', code: 'stored'
|
|
5053
|
+
return watchReadiness({ target: '', code: 'stored' }, null, deliver);
|
|
5089
5054
|
}
|
|
5090
5055
|
if (topic === 'rooms') {
|
|
5091
5056
|
unlistedRooms.add(deliver);
|
|
@@ -5130,6 +5095,13 @@ export function composeRuntime(options) {
|
|
|
5130
5095
|
memberListeners.delete(deliver);
|
|
5131
5096
|
};
|
|
5132
5097
|
}
|
|
5098
|
+
if (topic === 'authority-changes') {
|
|
5099
|
+
authorityChangeListeners.add(deliver);
|
|
5100
|
+
deliver(authorityChanges);
|
|
5101
|
+
return () => {
|
|
5102
|
+
authorityChangeListeners.delete(deliver);
|
|
5103
|
+
};
|
|
5104
|
+
}
|
|
5133
5105
|
if (topic === 'queue') {
|
|
5134
5106
|
const queue = { locator: input[0], notify: deliver };
|
|
5135
5107
|
queueSubscriptions.add(queue);
|
|
@@ -5138,7 +5110,7 @@ export function composeRuntime(options) {
|
|
|
5138
5110
|
queueSubscriptions.delete(queue);
|
|
5139
5111
|
};
|
|
5140
5112
|
}
|
|
5141
|
-
return watchPlan(planned(input[0], null), deliver);
|
|
5113
|
+
return watchPlan(planned(input[0], null), deliver, fail);
|
|
5142
5114
|
};
|
|
5143
5115
|
const md = {
|
|
5144
5116
|
loaded() {
|
|
@@ -5158,7 +5130,7 @@ export function composeRuntime(options) {
|
|
|
5158
5130
|
});
|
|
5159
5131
|
},
|
|
5160
5132
|
query: ((input) => rowsQuery(planned(input, null))),
|
|
5161
|
-
search: (input) => searched(input, null
|
|
5133
|
+
search: (input) => searched(input, null),
|
|
5162
5134
|
types(locator) {
|
|
5163
5135
|
return {
|
|
5164
5136
|
collection: modelTypes(modelOf(locator)),
|
|
@@ -5283,7 +5255,7 @@ export function composeRuntime(options) {
|
|
|
5283
5255
|
authorization = undefined;
|
|
5284
5256
|
discoveryPaused = false;
|
|
5285
5257
|
if (options.adapter.roomStream === undefined)
|
|
5286
|
-
await sync('unstreamed');
|
|
5258
|
+
await sync('unstreamed', null);
|
|
5287
5259
|
else
|
|
5288
5260
|
await task(OPENING_LANE, ensureAuthorization);
|
|
5289
5261
|
},
|
|
@@ -5306,7 +5278,6 @@ export function composeRuntime(options) {
|
|
|
5306
5278
|
md,
|
|
5307
5279
|
seed,
|
|
5308
5280
|
stop,
|
|
5309
|
-
lose: loseRooms,
|
|
5310
5281
|
watch,
|
|
5311
5282
|
persisted(answer) {
|
|
5312
5283
|
if (answer === persistence)
|
|
@@ -5315,7 +5286,6 @@ export function composeRuntime(options) {
|
|
|
5315
5286
|
journal.record(`persistence ${answer}`);
|
|
5316
5287
|
queueReadiness();
|
|
5317
5288
|
},
|
|
5318
|
-
shown: screen,
|
|
5319
5289
|
within: (reach, port) => {
|
|
5320
5290
|
if (full)
|
|
5321
5291
|
void probed();
|