@agentshouse/mdruntime 0.1.0-c1ffd2bc60355 → 0.1.0-c219104d36ac5
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 +10 -8
- package/dist/adapter.d.ts +25 -7
- package/dist/adapter.d.ts.map +1 -1
- package/dist/attached-model.d.ts +2 -1
- package/dist/attached-model.d.ts.map +1 -1
- package/dist/attached-model.js +28 -4
- package/dist/bootstrap.d.ts +8 -8
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +90 -84
- package/dist/chain.d.ts.map +1 -1
- package/dist/chain.js +7 -3
- package/dist/client.d.ts +4 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +52 -5
- package/dist/collection.d.ts +5 -0
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +23 -2
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +74 -24
- package/dist/connection.d.ts +5 -2
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +654 -359
- package/dist/conversation.d.ts +5 -5
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +126 -33
- package/dist/entry.d.ts +1 -0
- package/dist/entry.d.ts.map +1 -1
- package/dist/entry.js +1 -1
- package/dist/file.d.ts +2 -11
- package/dist/file.d.ts.map +1 -1
- package/dist/file.js +6 -4
- package/dist/fulltext.d.ts.map +1 -1
- package/dist/fulltext.js +8 -6
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +4 -2
- package/dist/index.d.ts +3 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/live.d.ts.map +1 -1
- package/dist/live.js +2 -1
- package/dist/media.d.ts +8 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +208 -0
- package/dist/memory.d.ts +2 -2
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +47 -9
- package/dist/model.d.ts +6 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +7 -1
- package/dist/observation.d.ts +1 -0
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +1 -1
- package/dist/outcome.d.ts +13 -2
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +16 -2
- package/dist/paths.d.ts +1 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +4 -0
- package/dist/query.d.ts +6 -0
- package/dist/query.d.ts.map +1 -1
- package/dist/reach.d.ts +8 -1
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +51 -17
- package/dist/readiness.d.ts +1 -2
- package/dist/readiness.d.ts.map +1 -1
- package/dist/reads.d.ts.map +1 -1
- package/dist/reads.js +10 -19
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +1 -5
- package/dist/replica.d.ts +3 -4
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +4 -3
- package/dist/room-draft.d.ts +17 -2
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +40 -6
- package/dist/rows.d.ts +15 -6
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +102 -74
- package/dist/save.d.ts +2 -0
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +11 -5
- package/dist/status.d.ts +5 -0
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +3 -1
- package/dist/store.d.ts +16 -19
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +23 -29
- package/dist/stored.d.ts +9 -19
- package/dist/stored.d.ts.map +1 -1
- package/dist/stored.js +197 -196
- package/dist/tables.d.ts +14 -16
- package/dist/tables.d.ts.map +1 -1
- package/dist/tables.js +6 -9
- package/dist/wire.d.ts +8 -4
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +2 -1
- package/dist/words.d.ts.map +1 -1
- package/dist/words.js +8 -2
- package/package.json +10 -17
- package/AGENTS.md +0 -317
- package/dist/chat/acts.d.ts +0 -33
- package/dist/chat/acts.d.ts.map +0 -1
- package/dist/chat/acts.js +0 -36
- package/dist/chat/context.d.ts +0 -3
- package/dist/chat/context.d.ts.map +0 -1
- package/dist/chat/context.js +0 -10
- package/dist/chat/earlier.d.ts +0 -10
- package/dist/chat/earlier.d.ts.map +0 -1
- package/dist/chat/earlier.js +0 -54
- package/dist/chat/house.d.ts +0 -25
- package/dist/chat/house.d.ts.map +0 -1
- package/dist/chat/house.js +0 -38
- package/dist/chat/input.d.ts +0 -11
- package/dist/chat/input.d.ts.map +0 -1
- package/dist/chat/input.js +0 -92
- package/dist/chat/kit.d.ts +0 -61
- package/dist/chat/kit.d.ts.map +0 -1
- package/dist/chat/kit.js +0 -123
- package/dist/chat/lasting.d.ts +0 -6
- package/dist/chat/lasting.d.ts.map +0 -1
- package/dist/chat/lasting.js +0 -19
- package/dist/chat/list.d.ts +0 -28
- package/dist/chat/list.d.ts.map +0 -1
- package/dist/chat/list.js +0 -103
- package/dist/chat/markdown.d.ts +0 -20
- package/dist/chat/markdown.d.ts.map +0 -1
- package/dist/chat/markdown.js +0 -229
- package/dist/chat/message.d.ts +0 -7
- package/dist/chat/message.d.ts.map +0 -1
- package/dist/chat/message.js +0 -254
- package/dist/chat/messages.d.ts +0 -35
- package/dist/chat/messages.d.ts.map +0 -1
- package/dist/chat/messages.js +0 -210
- package/dist/chat/name.d.ts +0 -7
- package/dist/chat/name.d.ts.map +0 -1
- package/dist/chat/name.js +0 -37
- package/dist/chat/opened.d.ts +0 -12
- package/dist/chat/opened.d.ts.map +0 -1
- package/dist/chat/opened.js +0 -19
- package/dist/chat/overlay.d.ts +0 -32
- package/dist/chat/overlay.d.ts.map +0 -1
- package/dist/chat/overlay.js +0 -103
- package/dist/chat/parts.d.ts +0 -58
- package/dist/chat/parts.d.ts.map +0 -1
- package/dist/chat/parts.js +0 -120
- package/dist/chat/pieces.d.ts +0 -62
- package/dist/chat/pieces.d.ts.map +0 -1
- package/dist/chat/pieces.js +0 -1110
- package/dist/chat/process.d.ts +0 -20
- package/dist/chat/process.d.ts.map +0 -1
- package/dist/chat/process.js +0 -32
- package/dist/chat/provider.d.ts +0 -70
- package/dist/chat/provider.d.ts.map +0 -1
- package/dist/chat/provider.js +0 -635
- package/dist/chat/question.d.ts +0 -9
- package/dist/chat/question.d.ts.map +0 -1
- package/dist/chat/question.js +0 -69
- package/dist/chat/questions.d.ts +0 -31
- package/dist/chat/questions.d.ts.map +0 -1
- package/dist/chat/questions.js +0 -111
- package/dist/chat/read.d.ts +0 -5
- package/dist/chat/read.d.ts.map +0 -1
- package/dist/chat/read.js +0 -13
- package/dist/chat/ready.d.ts +0 -37
- package/dist/chat/ready.d.ts.map +0 -1
- package/dist/chat/ready.js +0 -93
- package/dist/chat/room-file.d.ts +0 -8
- package/dist/chat/room-file.d.ts.map +0 -1
- package/dist/chat/room-file.js +0 -72
- package/dist/chat/steady.d.ts +0 -3
- package/dist/chat/steady.d.ts.map +0 -1
- package/dist/chat/steady.js +0 -26
- package/dist/chat/style.d.ts +0 -4
- package/dist/chat/style.d.ts.map +0 -1
- package/dist/chat/style.js +0 -307
- package/dist/chat.d.ts +0 -15
- package/dist/chat.d.ts.map +0 -1
- package/dist/chat.js +0 -8
package/dist/connection.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { escapePointer, parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
|
|
2
|
-
import { attachedModel,
|
|
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
5
|
import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
|
|
@@ -12,18 +12,19 @@ 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, unreadable as unreadableIn, validRoot, } from './collection.js';
|
|
15
|
+
import { available, collectionOf, composedOf, unreadable as unreadableIn, validRoot, } from './collection.js';
|
|
16
16
|
import { runtimeError } from './failure.js';
|
|
17
17
|
import { createLog } from './log.js';
|
|
18
18
|
import { binaryRecord } from './binary.js';
|
|
19
|
+
import { attachedFile } from './file.js';
|
|
19
20
|
import { openedFiles } from './opened.js';
|
|
20
|
-
import { ABSENT_TARGET, absentBinary, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
|
|
21
|
+
import { ABSENT_TARGET, absentBinary, attachAnswer, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, OFFLINE, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
|
|
21
22
|
import { createRecovery } from './recovery.js';
|
|
22
23
|
import { doorReads } from './reads.js';
|
|
23
24
|
import { advancement } from './refresh.js';
|
|
24
25
|
import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
|
|
25
26
|
import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, sameMembership, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
|
|
26
|
-
import { binaryBase, changedParts, changesOf, gatheredParts, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
27
|
+
import { binaryBase, changedParts, changesOf, gatheredParts, held, heldFile, joinedDraft, moveTarget, readParts, readSavedParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
27
28
|
import { same, saving, STARTING, transferring, } from './status.js';
|
|
28
29
|
import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
|
|
29
30
|
import { readinessOf, } from './readiness.js';
|
|
@@ -31,7 +32,7 @@ import { FencedMap } from './fence.js';
|
|
|
31
32
|
import { sameBinary } from './overlay.js';
|
|
32
33
|
import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
|
|
33
34
|
import { identityTime, roomDraftRevision } from './identity.js';
|
|
34
|
-
import { beneath, normalize } from './paths.js';
|
|
35
|
+
import { ancestors, beneath, normalize } from './paths.js';
|
|
35
36
|
import { openRuntimeStore, } from './store.js';
|
|
36
37
|
import { changedCleanly, sameComposition, validatedCollection, } from './validation.js';
|
|
37
38
|
import { unconfirmed } from './transfer.js';
|
|
@@ -54,22 +55,23 @@ const CONVERSATION_LANE = 'conversations';
|
|
|
54
55
|
const ROOM_READS = 256;
|
|
55
56
|
const ROOM_ATTEMPTS = 16;
|
|
56
57
|
const ERASE_BATCH = 512;
|
|
57
|
-
const
|
|
58
|
+
const DERIVE_BYTES = 1 << 23;
|
|
59
|
+
const DERIVE_FLOOR = 1 << 18;
|
|
60
|
+
// 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
|
+
const DERIVE_AIM_MS = 250;
|
|
62
|
+
// A build past its floor of text stops after a second and a half of work, so a Room whose text costs ten times the usual still reports progress within the 2 s bound.
|
|
63
|
+
const DERIVE_LONGEST_MS = 1_500;
|
|
58
64
|
const WALK_BYTES = 1 << 20;
|
|
59
65
|
const QUERY_BYTES = 1 << 20;
|
|
60
66
|
const STAGE_BYTES = 1 << 20;
|
|
61
67
|
const STAGE_FLOOR = 1 << 16;
|
|
62
68
|
// A batch aims at this much database work, so a Save or a query waits behind at most one short write.
|
|
63
69
|
const STAGE_AIM_MS = 50;
|
|
64
|
-
const
|
|
70
|
+
const STREAMS_HELD = 4;
|
|
71
|
+
const NOTHING_HELD = { parts: 0, bytes: 0 };
|
|
72
|
+
const QUEUED_BYTES = 1 << 23;
|
|
65
73
|
// Bytes still arriving are reported at most this often, well inside the 2 s progress bound, so readiness is not rebuilt for every chunk the network hands over.
|
|
66
74
|
const ARRIVAL_REPORT_MS = 250;
|
|
67
|
-
const INSTALL_STEPS = [
|
|
68
|
-
'writing',
|
|
69
|
-
'downloading',
|
|
70
|
-
'replaying',
|
|
71
|
-
'listing',
|
|
72
|
-
];
|
|
73
75
|
const CLEAN_CHANGES = 2_000;
|
|
74
76
|
// A Room that locks offline stays readable this long after the connection drops, so a laptop waking or changing network keeps working while a disconnected device locks soon after.
|
|
75
77
|
const LOCK_GRACE_MS = 120_000;
|
|
@@ -93,7 +95,7 @@ function pendingKey(locator, id) {
|
|
|
93
95
|
}
|
|
94
96
|
export function composeRuntime(options) {
|
|
95
97
|
const localSaves = options.adapter.localSaves === true;
|
|
96
|
-
const roomTypes = roomTypesOf(options.adapter.roomTypes);
|
|
98
|
+
const roomTypes = roomTypesOf(options.adapter.roomTypes, options.adapter.nameableRoomTypes);
|
|
97
99
|
let authorization = options.authorization;
|
|
98
100
|
let opening;
|
|
99
101
|
const fences = new Map();
|
|
@@ -103,20 +105,23 @@ export function composeRuntime(options) {
|
|
|
103
105
|
const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
|
|
104
106
|
let discoveries = 0;
|
|
105
107
|
const replicas = new FencedMap(isFenced);
|
|
108
|
+
const reopened = new Set();
|
|
109
|
+
const usableWhenHeld = options.usableWhenHeld ?? [];
|
|
106
110
|
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
107
111
|
const pendings = new Map();
|
|
108
112
|
const seeds = new Map();
|
|
109
113
|
const seedWatchers = new Map();
|
|
110
114
|
const roomDrafts = new FencedMap(isFenced);
|
|
115
|
+
const attachedFiles = new FencedMap(isFenced);
|
|
111
116
|
const presentations = new Map();
|
|
112
117
|
let limits = [];
|
|
113
118
|
let capabilities = [];
|
|
114
119
|
const rooms = new FencedMap(isFenced);
|
|
115
120
|
const counted = new Map();
|
|
116
121
|
const unreadable = new Set();
|
|
117
|
-
const
|
|
118
|
-
const models = new FencedMap(
|
|
119
|
-
const liveModels = new FencedMap(
|
|
122
|
+
const builtIn = builtInModel(roomTypes);
|
|
123
|
+
const models = new FencedMap(isFenced);
|
|
124
|
+
const liveModels = new FencedMap(isFenced);
|
|
120
125
|
const invalidModels = new Set();
|
|
121
126
|
const discovered = new Map();
|
|
122
127
|
const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
|
|
@@ -124,23 +129,19 @@ export function composeRuntime(options) {
|
|
|
124
129
|
const scopedRefusals = new Map();
|
|
125
130
|
const sourceRefusals = new Map();
|
|
126
131
|
const declarationRefusals = new Set();
|
|
127
|
-
function
|
|
128
|
-
return
|
|
132
|
+
function modelOf(locator) {
|
|
133
|
+
return models.get(locator) ?? NO_MODEL;
|
|
129
134
|
}
|
|
130
|
-
function
|
|
131
|
-
return
|
|
132
|
-
}
|
|
133
|
-
function savedModel(locator, attached) {
|
|
134
|
-
return invalidModels.has(modelKey(locator, attached))
|
|
135
|
+
function savedModel(locator) {
|
|
136
|
+
return invalidModels.has(locator)
|
|
135
137
|
? { status: 'unavailable', reason: 'revoked' }
|
|
136
|
-
: modelOf(locator
|
|
138
|
+
: modelOf(locator);
|
|
139
|
+
}
|
|
140
|
+
function composed(record) {
|
|
141
|
+
return compositionOf(record, builtIn);
|
|
137
142
|
}
|
|
138
143
|
function compositionFor(record) {
|
|
139
|
-
return
|
|
140
|
-
...record,
|
|
141
|
-
model: savedModel(record.locator, 'collection'),
|
|
142
|
-
selectionModel: savedModel(record.locator, 'typed-selection'),
|
|
143
|
-
});
|
|
144
|
+
return composed({ ...record, model: savedModel(record.locator) });
|
|
144
145
|
}
|
|
145
146
|
let refusal = null;
|
|
146
147
|
let storageLoss = null;
|
|
@@ -174,6 +175,7 @@ export function composeRuntime(options) {
|
|
|
174
175
|
let mutations = Promise.resolve();
|
|
175
176
|
let gathering = null;
|
|
176
177
|
const roomListeners = new Set();
|
|
178
|
+
const unlistedRooms = new Set();
|
|
177
179
|
const refusalListeners = new Set();
|
|
178
180
|
const storageLossListeners = new Set();
|
|
179
181
|
const statusListeners = new Set();
|
|
@@ -217,15 +219,31 @@ export function composeRuntime(options) {
|
|
|
217
219
|
let readinessQueued = false;
|
|
218
220
|
const catching = new Set();
|
|
219
221
|
const transfers = new FencedMap(isFenced);
|
|
220
|
-
const installs = new
|
|
222
|
+
const installs = new Set();
|
|
221
223
|
const arrivals = new Map();
|
|
224
|
+
const streaming = new Set();
|
|
225
|
+
const underived = new Map();
|
|
226
|
+
const unindexed = new Map();
|
|
227
|
+
const unchecked = new Map();
|
|
222
228
|
let arrivalReported = 0;
|
|
223
229
|
let onScreen = options.shell.onScreen;
|
|
224
230
|
const shownWaiters = [];
|
|
225
|
-
let
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
231
|
+
let streams = { generation, free: STREAMS_HELD };
|
|
232
|
+
let queuedBytes = 0;
|
|
233
|
+
const installWaiters = [];
|
|
234
|
+
const stageBudget = {
|
|
235
|
+
bytes: STAGE_FLOOR * 4,
|
|
236
|
+
floor: STAGE_FLOOR,
|
|
237
|
+
ceiling: STAGE_BYTES,
|
|
238
|
+
aim: STAGE_AIM_MS,
|
|
239
|
+
};
|
|
240
|
+
const deriveBudget = {
|
|
241
|
+
bytes: DERIVE_FLOOR * 4,
|
|
242
|
+
floor: DERIVE_FLOOR,
|
|
243
|
+
ceiling: DERIVE_BYTES,
|
|
244
|
+
aim: DERIVE_AIM_MS,
|
|
245
|
+
};
|
|
246
|
+
const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
|
|
229
247
|
const conversations = createConversationStore({
|
|
230
248
|
agents: declaredAgents,
|
|
231
249
|
documents: (declaration, after) => rowsQuery(planOf({
|
|
@@ -246,6 +264,7 @@ export function composeRuntime(options) {
|
|
|
246
264
|
stream: () => options.adapter.conversations,
|
|
247
265
|
publish: publishChangeSet,
|
|
248
266
|
online: () => connectionOf() !== 'unreachable' && refusal === null,
|
|
267
|
+
listed: () => roomsListed,
|
|
249
268
|
full: () => full,
|
|
250
269
|
closed: () => closed,
|
|
251
270
|
unanswered: resendLater,
|
|
@@ -253,10 +272,19 @@ export function composeRuntime(options) {
|
|
|
253
272
|
});
|
|
254
273
|
const journal = createLog();
|
|
255
274
|
const keptFiles = openedFiles(database, isFenced);
|
|
256
|
-
let
|
|
275
|
+
let readyFromStore = () => { };
|
|
257
276
|
const ready = new Promise((resolve) => {
|
|
258
|
-
|
|
277
|
+
readyFromStore = resolve;
|
|
278
|
+
});
|
|
279
|
+
let notifyDiscovered = () => { };
|
|
280
|
+
const firstDiscovery = new Promise((resolve) => {
|
|
281
|
+
notifyDiscovered = resolve;
|
|
259
282
|
});
|
|
283
|
+
let roomsNamed = false;
|
|
284
|
+
function notifyReady() {
|
|
285
|
+
notifyDiscovered();
|
|
286
|
+
readyFromStore();
|
|
287
|
+
}
|
|
260
288
|
function rememberRoomDraft(record) {
|
|
261
289
|
const room = roomDrafts.get(record.locator) ?? new Map();
|
|
262
290
|
room.set(record.path, record);
|
|
@@ -276,7 +304,7 @@ export function composeRuntime(options) {
|
|
|
276
304
|
return undefined;
|
|
277
305
|
const local = await openLocal();
|
|
278
306
|
const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
|
|
279
|
-
return { ...slice, collection: collectionOf(slice.documents,
|
|
307
|
+
return { ...slice, collection: collectionOf(slice.documents, composed(held.record)) };
|
|
280
308
|
}
|
|
281
309
|
function roomDraftOf(record, visible) {
|
|
282
310
|
return structuredClone({
|
|
@@ -343,7 +371,9 @@ export function composeRuntime(options) {
|
|
|
343
371
|
}
|
|
344
372
|
const file = parts.file === undefined
|
|
345
373
|
? undefined
|
|
346
|
-
: await heldFile(parts.file.blob,
|
|
374
|
+
: await heldFile(parts.file.blob, parts.file.base === undefined
|
|
375
|
+
? binaryBase(await visibleOf(room, [path]), path)
|
|
376
|
+
: parts.file.base);
|
|
347
377
|
const record = {
|
|
348
378
|
locator: room,
|
|
349
379
|
path,
|
|
@@ -378,6 +408,20 @@ export function composeRuntime(options) {
|
|
|
378
408
|
(queuesWithoutReplica.size > 0 ||
|
|
379
409
|
[...replicas.values()].some((replica) => replica.queue.length > 0)));
|
|
380
410
|
}
|
|
411
|
+
function unsent() {
|
|
412
|
+
const changed = new Map();
|
|
413
|
+
for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
|
|
414
|
+
for (const { batch, state } of entriesOf(room)) {
|
|
415
|
+
if (state === 'accepted' || state === 'local')
|
|
416
|
+
continue;
|
|
417
|
+
for (const { path } of batch.changes)
|
|
418
|
+
changed.set(`${room}\u0000${path}`, { room, path });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return [...changed]
|
|
422
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
423
|
+
.map(([, change]) => change);
|
|
424
|
+
}
|
|
381
425
|
function attention() {
|
|
382
426
|
const attentionRequired = [];
|
|
383
427
|
for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
|
|
@@ -410,7 +454,7 @@ export function composeRuntime(options) {
|
|
|
410
454
|
if (reset === null || submission.reset < reset)
|
|
411
455
|
reset = submission.reset;
|
|
412
456
|
}
|
|
413
|
-
return { attentionRequired, reset };
|
|
457
|
+
return { attentionRequired, unsent: unsent(), reset };
|
|
414
458
|
}
|
|
415
459
|
function documentsIn(locator) {
|
|
416
460
|
return replicas.get(locator)?.count ?? 0;
|
|
@@ -483,7 +527,7 @@ export function composeRuntime(options) {
|
|
|
483
527
|
const governed = governs(reach, room);
|
|
484
528
|
const attached = [
|
|
485
529
|
...(governed.collection ? [modelOf(room.locator)] : []),
|
|
486
|
-
...(governed.selection ? [
|
|
530
|
+
...(governed.selection ? [builtIn] : []),
|
|
487
531
|
];
|
|
488
532
|
return {
|
|
489
533
|
locator: room.locator,
|
|
@@ -499,11 +543,26 @@ export function composeRuntime(options) {
|
|
|
499
543
|
const value = listed();
|
|
500
544
|
for (const listener of roomListeners)
|
|
501
545
|
listener(value);
|
|
546
|
+
emitKnownRooms();
|
|
547
|
+
}
|
|
548
|
+
function emitKnownRooms() {
|
|
549
|
+
conversations.learned();
|
|
550
|
+
if (unlistedRooms.size === 0)
|
|
551
|
+
return;
|
|
552
|
+
if (!roomsListed && rooms.size === 0 && refusal === null && !status.providerUnreachable)
|
|
553
|
+
return;
|
|
554
|
+
const value = listed();
|
|
555
|
+
for (const listener of [...unlistedRooms]) {
|
|
556
|
+
unlistedRooms.delete(listener);
|
|
557
|
+
roomListeners.add(listener);
|
|
558
|
+
listener(value);
|
|
559
|
+
}
|
|
502
560
|
}
|
|
503
561
|
function emitRefusal() {
|
|
504
562
|
queueReadiness();
|
|
505
563
|
for (const listener of refusalListeners)
|
|
506
564
|
listener(refusal);
|
|
565
|
+
emitKnownRooms();
|
|
507
566
|
}
|
|
508
567
|
function emitStorageLoss() {
|
|
509
568
|
queueReadiness();
|
|
@@ -524,6 +583,7 @@ export function composeRuntime(options) {
|
|
|
524
583
|
if (closed)
|
|
525
584
|
return;
|
|
526
585
|
queueReadiness();
|
|
586
|
+
emitKnownRooms();
|
|
527
587
|
const next = status.state === 'synced' && !conversations.current()
|
|
528
588
|
? { ...status, state: 'syncing' }
|
|
529
589
|
: status;
|
|
@@ -567,11 +627,14 @@ export function composeRuntime(options) {
|
|
|
567
627
|
if (held === undefined || closed || held.indexed)
|
|
568
628
|
return;
|
|
569
629
|
const key = held.record.key;
|
|
570
|
-
const done = await exclusive(async () =>
|
|
630
|
+
const done = await exclusive(async () => {
|
|
631
|
+
const local = await openLocal();
|
|
632
|
+
return timed(deriveBudget, () => local.rederive(key, composed(held.record), deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR));
|
|
633
|
+
});
|
|
571
634
|
const now = replicas.get(locator);
|
|
572
635
|
if (now?.record.key !== key)
|
|
573
636
|
continue;
|
|
574
|
-
if (!done)
|
|
637
|
+
if (!done.done)
|
|
575
638
|
continue;
|
|
576
639
|
replicas.set(locator, { ...now, indexed: true });
|
|
577
640
|
publishChangeSet(changeSet(locator, { documents: null }));
|
|
@@ -610,7 +673,11 @@ export function composeRuntime(options) {
|
|
|
610
673
|
})();
|
|
611
674
|
}
|
|
612
675
|
function roomsOfReach(reach) {
|
|
613
|
-
return new Set([
|
|
676
|
+
return new Set([
|
|
677
|
+
...reach.rooms,
|
|
678
|
+
...reach.documents.map(roomOfKey),
|
|
679
|
+
...reach.areas.flatMap((area) => (area.types.length === 0 ? [] : [area.room])),
|
|
680
|
+
]);
|
|
614
681
|
}
|
|
615
682
|
function reachedRooms(plan) {
|
|
616
683
|
const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
|
|
@@ -728,9 +795,9 @@ export function composeRuntime(options) {
|
|
|
728
795
|
const rooms = { done: 0, total: 0 };
|
|
729
796
|
const parts = { done: 0, total: 0 };
|
|
730
797
|
const bytes = { done: 0, total: 0 };
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
let
|
|
798
|
+
let installing = 0;
|
|
799
|
+
let downloading = false;
|
|
800
|
+
let checking = 0;
|
|
734
801
|
for (const { room, state } of coverage) {
|
|
735
802
|
if (state === 'unadmitted')
|
|
736
803
|
continue;
|
|
@@ -738,35 +805,43 @@ export function composeRuntime(options) {
|
|
|
738
805
|
if (state === 'complete')
|
|
739
806
|
rooms.done += 1;
|
|
740
807
|
const transfer = transfers.get(room);
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
808
|
+
const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
|
|
809
|
+
if (underway)
|
|
810
|
+
installing += 1;
|
|
811
|
+
if (underway && (streaming.has(room) || !arrivals.has(room)))
|
|
812
|
+
downloading = true;
|
|
813
|
+
if (transfer !== undefined) {
|
|
814
|
+
const check = unchecked.get(room);
|
|
815
|
+
const unchecking = check === undefined ? 0 : check.bytes * (1 - Math.min(1, check.done / check.text));
|
|
816
|
+
const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
|
|
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;
|
|
757
824
|
}
|
|
758
|
-
const
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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;
|
|
831
|
+
}
|
|
832
|
+
const step = installing === 0
|
|
833
|
+
? null
|
|
834
|
+
: downloading
|
|
835
|
+
? 'downloading'
|
|
836
|
+
: bytes.done + checking < bytes.total
|
|
837
|
+
? 'writing'
|
|
838
|
+
: 'replaying';
|
|
763
839
|
return {
|
|
764
|
-
step
|
|
765
|
-
paused:
|
|
840
|
+
step,
|
|
841
|
+
paused: installing > 0 && !onScreen ? 'hidden' : null,
|
|
766
842
|
rooms,
|
|
767
843
|
parts,
|
|
768
844
|
bytes,
|
|
769
|
-
received,
|
|
770
845
|
};
|
|
771
846
|
}
|
|
772
847
|
function readinessFor(port, reach) {
|
|
@@ -793,7 +868,8 @@ export function composeRuntime(options) {
|
|
|
793
868
|
installation: installationOf(coverage),
|
|
794
869
|
refused: refusedIn(required.rooms),
|
|
795
870
|
lost: storageLoss !== null,
|
|
796
|
-
established: targets[port.target]?.established === true
|
|
871
|
+
established: targets[port.target]?.established === true ||
|
|
872
|
+
(usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
|
|
797
873
|
});
|
|
798
874
|
}
|
|
799
875
|
function establish(name) {
|
|
@@ -846,7 +922,8 @@ export function composeRuntime(options) {
|
|
|
846
922
|
probeEachMinute();
|
|
847
923
|
}
|
|
848
924
|
function installedTarget() {
|
|
849
|
-
return
|
|
925
|
+
return ((usableWhenHeld.length > 0 && reopened.size > 0) ||
|
|
926
|
+
Object.values(targets).some((target) => target.established));
|
|
850
927
|
}
|
|
851
928
|
function probeEachMinute() {
|
|
852
929
|
if (!full || probing !== null || closed || !installedTarget())
|
|
@@ -1004,6 +1081,10 @@ export function composeRuntime(options) {
|
|
|
1004
1081
|
}
|
|
1005
1082
|
function publishChangeSet(set) {
|
|
1006
1083
|
applyChangeSet(set);
|
|
1084
|
+
for (const { locator, path } of set.documents) {
|
|
1085
|
+
if (path !== null)
|
|
1086
|
+
attachedFiles.get(locator)?.delete(path);
|
|
1087
|
+
}
|
|
1007
1088
|
if (set.rooms.length > 0 ||
|
|
1008
1089
|
[...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
|
|
1009
1090
|
emitRooms();
|
|
@@ -1029,7 +1110,7 @@ export function composeRuntime(options) {
|
|
|
1029
1110
|
const records = [...replicas].map(([locator, held]) => ({
|
|
1030
1111
|
locator,
|
|
1031
1112
|
held,
|
|
1032
|
-
record: { ...held.record, model: revoked
|
|
1113
|
+
record: { ...held.record, model: revoked },
|
|
1033
1114
|
}));
|
|
1034
1115
|
let answer = null;
|
|
1035
1116
|
try {
|
|
@@ -1039,18 +1120,14 @@ export function composeRuntime(options) {
|
|
|
1039
1120
|
locator,
|
|
1040
1121
|
key: record.key,
|
|
1041
1122
|
record: storedRecord(record),
|
|
1042
|
-
|
|
1123
|
+
composed: composedOf(composed(record)),
|
|
1043
1124
|
})),
|
|
1044
1125
|
});
|
|
1045
1126
|
}
|
|
1046
1127
|
catch {
|
|
1047
1128
|
journal.record('model revocation persistence failed');
|
|
1048
1129
|
}
|
|
1049
|
-
const keys = new Set(models.keys());
|
|
1050
|
-
for (const locator of rooms.keys()) {
|
|
1051
|
-
keys.add(modelKey(locator, 'collection'));
|
|
1052
|
-
keys.add(modelKey(locator, 'typed-selection'));
|
|
1053
|
-
}
|
|
1130
|
+
const keys = new Set([...models.keys(), ...rooms.keys()]);
|
|
1054
1131
|
for (const key of keys) {
|
|
1055
1132
|
invalidModels.add(key);
|
|
1056
1133
|
models.set(key, revoked);
|
|
@@ -1105,7 +1182,7 @@ export function composeRuntime(options) {
|
|
|
1105
1182
|
? null
|
|
1106
1183
|
: {
|
|
1107
1184
|
roots: selection.roots,
|
|
1108
|
-
model: modelTypes(
|
|
1185
|
+
model: modelTypes(builtIn),
|
|
1109
1186
|
},
|
|
1110
1187
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
1111
1188
|
roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
|
|
@@ -1194,7 +1271,8 @@ export function composeRuntime(options) {
|
|
|
1194
1271
|
queuesWithoutReplica.delete(locator);
|
|
1195
1272
|
rooms.delete(locator);
|
|
1196
1273
|
roomDrafts.delete(locator);
|
|
1197
|
-
|
|
1274
|
+
attachedFiles.delete(locator);
|
|
1275
|
+
untransferred(locator);
|
|
1198
1276
|
catching.delete(locator);
|
|
1199
1277
|
discovered.delete(locator);
|
|
1200
1278
|
announced.delete(locator);
|
|
@@ -1205,12 +1283,9 @@ export function composeRuntime(options) {
|
|
|
1205
1283
|
scopedRefusals.delete(locator);
|
|
1206
1284
|
sourceRefusals.delete(locator);
|
|
1207
1285
|
declarationRefusals.delete(locator);
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
liveModels.delete(key);
|
|
1212
|
-
invalidModels.delete(key);
|
|
1213
|
-
}
|
|
1286
|
+
models.delete(locator);
|
|
1287
|
+
liveModels.delete(locator);
|
|
1288
|
+
invalidModels.delete(locator);
|
|
1214
1289
|
journal.record(`room lost ${locator}`);
|
|
1215
1290
|
}
|
|
1216
1291
|
if (fresh.length === 0)
|
|
@@ -1258,7 +1333,14 @@ export function composeRuntime(options) {
|
|
|
1258
1333
|
});
|
|
1259
1334
|
await eraseFenced(fresh);
|
|
1260
1335
|
}
|
|
1261
|
-
function
|
|
1336
|
+
function retyped(persisted) {
|
|
1337
|
+
if (persisted.status === 'unavailable')
|
|
1338
|
+
return persisted;
|
|
1339
|
+
const { state } = attachedModel({ kind: 'model', snapshot: persisted.snapshot }, NO_MODEL, roomTypes);
|
|
1340
|
+
return state.status === 'unavailable' ? state : { ...persisted, types: state.types };
|
|
1341
|
+
}
|
|
1342
|
+
function restored(key, held) {
|
|
1343
|
+
const persisted = retyped(held);
|
|
1262
1344
|
const prior = models.get(key);
|
|
1263
1345
|
if (prior !== undefined &&
|
|
1264
1346
|
prior.status !== 'unavailable' &&
|
|
@@ -1289,8 +1371,8 @@ export function composeRuntime(options) {
|
|
|
1289
1371
|
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1290
1372
|
targets = held.targets;
|
|
1291
1373
|
transfers.clear();
|
|
1292
|
-
|
|
1293
|
-
|
|
1374
|
+
arrivals.clear();
|
|
1375
|
+
streaming.clear();
|
|
1294
1376
|
limits = held.providerLimits;
|
|
1295
1377
|
capabilities = held.capabilities;
|
|
1296
1378
|
member = held.member;
|
|
@@ -1300,7 +1382,7 @@ export function composeRuntime(options) {
|
|
|
1300
1382
|
presentations.clear();
|
|
1301
1383
|
for (const record of held.presentation)
|
|
1302
1384
|
presentations.set(record.app, record.value);
|
|
1303
|
-
conversations.hydrate(heldConversations, chatDrafts);
|
|
1385
|
+
conversations.hydrate(heldConversations, chatDrafts, held.conversationList);
|
|
1304
1386
|
pendings.clear();
|
|
1305
1387
|
for (const queue of queues) {
|
|
1306
1388
|
for (const { batch, state } of queue.pending) {
|
|
@@ -1308,6 +1390,7 @@ export function composeRuntime(options) {
|
|
|
1308
1390
|
}
|
|
1309
1391
|
}
|
|
1310
1392
|
replicas.clear();
|
|
1393
|
+
reopened.clear();
|
|
1311
1394
|
queuesWithoutReplica.clear();
|
|
1312
1395
|
for (const [locator, queue] of queuedBatches) {
|
|
1313
1396
|
if (!held.rooms.has(locator))
|
|
@@ -1316,9 +1399,8 @@ export function composeRuntime(options) {
|
|
|
1316
1399
|
records.forEach((stored, index) => {
|
|
1317
1400
|
if (fences.has(stored.locator))
|
|
1318
1401
|
return;
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1321
|
-
const record = { ...stored, model, selectionModel };
|
|
1402
|
+
const record = { ...stored, model: restored(stored.locator, stored.model) };
|
|
1403
|
+
reopened.add(record.locator);
|
|
1322
1404
|
replicas.set(record.locator, {
|
|
1323
1405
|
record,
|
|
1324
1406
|
queue: queuedBatches.get(record.locator) ?? [],
|
|
@@ -1329,25 +1411,28 @@ export function composeRuntime(options) {
|
|
|
1329
1411
|
});
|
|
1330
1412
|
const existing = rooms.get(record.locator);
|
|
1331
1413
|
if (existing === undefined)
|
|
1332
|
-
list(record,
|
|
1414
|
+
list(record, record.roomVersion !== null);
|
|
1333
1415
|
else {
|
|
1334
1416
|
rooms.set(record.locator, {
|
|
1335
1417
|
...existing,
|
|
1336
1418
|
roomVersion: record.roomVersion,
|
|
1337
1419
|
operations: record.operations,
|
|
1338
1420
|
replica: record.replica,
|
|
1339
|
-
model: modelTypes(model),
|
|
1421
|
+
model: modelTypes(record.model),
|
|
1340
1422
|
typedSelection: record.typedSelection === null
|
|
1341
1423
|
? null
|
|
1342
|
-
: {
|
|
1343
|
-
roots: record.typedSelection.roots,
|
|
1344
|
-
model: modelTypes(selectionModel),
|
|
1345
|
-
},
|
|
1424
|
+
: { roots: record.typedSelection.roots, model: modelTypes(builtIn) },
|
|
1346
1425
|
});
|
|
1347
1426
|
}
|
|
1348
1427
|
});
|
|
1349
|
-
|
|
1350
|
-
|
|
1428
|
+
try {
|
|
1429
|
+
for (const key of held.orphans)
|
|
1430
|
+
await opened.release(key);
|
|
1431
|
+
}
|
|
1432
|
+
catch (error) {
|
|
1433
|
+
if (!refusedForSpace(error))
|
|
1434
|
+
throw error;
|
|
1435
|
+
}
|
|
1351
1436
|
if (held.erasing || held.orphans.length > 0)
|
|
1352
1437
|
queueMicrotask(sweep);
|
|
1353
1438
|
return {
|
|
@@ -1529,7 +1614,7 @@ export function composeRuntime(options) {
|
|
|
1529
1614
|
return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
|
|
1530
1615
|
}
|
|
1531
1616
|
function replayed(record, confirmedLayer, queue) {
|
|
1532
|
-
return stepped(replaying(confirmedLayer, queue, record.locator,
|
|
1617
|
+
return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
|
|
1533
1618
|
}
|
|
1534
1619
|
async function overlayDiff(record, before, visible, touched) {
|
|
1535
1620
|
const kept = before !== undefined && before.record.key === record.key
|
|
@@ -1592,7 +1677,7 @@ export function composeRuntime(options) {
|
|
|
1592
1677
|
const confirmedLayer = overrides === undefined
|
|
1593
1678
|
? read
|
|
1594
1679
|
: await stepped(slicingWith(read, overrides.set, overrides.removed));
|
|
1595
|
-
const composition =
|
|
1680
|
+
const composition = composed(record);
|
|
1596
1681
|
const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
|
|
1597
1682
|
const founded = kept.filter((batch) => !queue.includes(batch));
|
|
1598
1683
|
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
@@ -1635,6 +1720,7 @@ export function composeRuntime(options) {
|
|
|
1635
1720
|
key: record.key,
|
|
1636
1721
|
record: storedRecord(record),
|
|
1637
1722
|
roots: record.typedSelection?.roots ?? [],
|
|
1723
|
+
composed: composedOf(composition),
|
|
1638
1724
|
...settling.commit,
|
|
1639
1725
|
...(overrides === undefined
|
|
1640
1726
|
? {}
|
|
@@ -1698,35 +1784,25 @@ export function composeRuntime(options) {
|
|
|
1698
1784
|
};
|
|
1699
1785
|
}
|
|
1700
1786
|
function adoptModels(locator, record) {
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
if (model.status === 'current')
|
|
1709
|
-
liveModels.set(key, JSON.stringify(model.snapshot));
|
|
1710
|
-
else if (model.status === 'unavailable')
|
|
1711
|
-
liveModels.delete(key);
|
|
1712
|
-
}
|
|
1787
|
+
const model = record.model;
|
|
1788
|
+
models.set(locator, model);
|
|
1789
|
+
invalidModels.delete(locator);
|
|
1790
|
+
if (model.status === 'current')
|
|
1791
|
+
liveModels.set(locator, JSON.stringify(model.snapshot));
|
|
1792
|
+
else if (model.status === 'unavailable')
|
|
1793
|
+
liveModels.delete(locator);
|
|
1713
1794
|
}
|
|
1714
1795
|
function remodel(locator, answers, held) {
|
|
1715
|
-
const
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
};
|
|
1726
|
-
return {
|
|
1727
|
-
collection: taken(answers?.collection, held.collection, 'collection'),
|
|
1728
|
-
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
1729
|
-
};
|
|
1796
|
+
const answer = answers?.collection;
|
|
1797
|
+
if (answer === undefined)
|
|
1798
|
+
return held;
|
|
1799
|
+
const { state, refused } = attachedModel(answer, held.collection, roomTypes);
|
|
1800
|
+
if (refused !== null) {
|
|
1801
|
+
const where = refused.location === undefined ? '' : ` ${refused.location}`;
|
|
1802
|
+
journal.record(`model invalid ${locator} collection ${refused.identity} ${refused.rule}${where}`);
|
|
1803
|
+
}
|
|
1804
|
+
models.set(locator, state);
|
|
1805
|
+
return { collection: state };
|
|
1730
1806
|
}
|
|
1731
1807
|
function publish(local, room, record, landing, live) {
|
|
1732
1808
|
const landed = exclusive(async () => {
|
|
@@ -1753,14 +1829,13 @@ export function composeRuntime(options) {
|
|
|
1753
1829
|
? {
|
|
1754
1830
|
room: true,
|
|
1755
1831
|
view: 'refreshed',
|
|
1756
|
-
commit: { refreshed: true
|
|
1832
|
+
commit: { refreshed: true },
|
|
1757
1833
|
}
|
|
1758
1834
|
: {
|
|
1759
1835
|
room: true,
|
|
1760
1836
|
view: landing.refreshed ? 'refreshed' : 'confirmed',
|
|
1761
1837
|
commit: {
|
|
1762
1838
|
published: { from: prior?.record.key ?? null },
|
|
1763
|
-
composition: compositionOf(record),
|
|
1764
1839
|
...(landing.refreshed ? { refreshed: true } : {}),
|
|
1765
1840
|
},
|
|
1766
1841
|
});
|
|
@@ -1772,7 +1847,7 @@ export function composeRuntime(options) {
|
|
|
1772
1847
|
return true;
|
|
1773
1848
|
}
|
|
1774
1849
|
if (landing.kind === 'bootstrapped') {
|
|
1775
|
-
|
|
1850
|
+
untransferred(room.locator);
|
|
1776
1851
|
catching.delete(room.locator);
|
|
1777
1852
|
sweep();
|
|
1778
1853
|
}
|
|
@@ -1819,12 +1894,7 @@ export function composeRuntime(options) {
|
|
|
1819
1894
|
return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
|
|
1820
1895
|
}
|
|
1821
1896
|
async function saveParts(locator, named, writtenBy) {
|
|
1822
|
-
const read = named
|
|
1823
|
-
if (read.some(({ path }, at) => path === '' ||
|
|
1824
|
-
normalize(path) !== path ||
|
|
1825
|
-
read.findIndex((one) => one.path === path) !== at)) {
|
|
1826
|
-
throw runtimeError('room-draft-malformed');
|
|
1827
|
-
}
|
|
1897
|
+
const read = readSavedParts(named);
|
|
1828
1898
|
const target = await savedRoom(locator);
|
|
1829
1899
|
const saved = [];
|
|
1830
1900
|
const files = [];
|
|
@@ -1833,7 +1903,7 @@ export function composeRuntime(options) {
|
|
|
1833
1903
|
for (const { path, parts } of read) {
|
|
1834
1904
|
const file = parts.file === undefined
|
|
1835
1905
|
? undefined
|
|
1836
|
-
: await heldFile(parts.file.blob, binaryBase(visible, path));
|
|
1906
|
+
: await heldFile(parts.file.blob, parts.file.base === undefined ? binaryBase(visible, path) : parts.file.base);
|
|
1837
1907
|
if (parts.file !== undefined && file !== undefined) {
|
|
1838
1908
|
files.push({ key: file.localId, blob: parts.file.blob });
|
|
1839
1909
|
}
|
|
@@ -1868,13 +1938,14 @@ export function composeRuntime(options) {
|
|
|
1868
1938
|
const referring = removed.length === 0
|
|
1869
1939
|
? []
|
|
1870
1940
|
: (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
|
|
1871
|
-
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
|
|
1941
|
+
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
|
|
1872
1942
|
const { visible } = await replayed(record, confirmedLayer, replica.queue);
|
|
1873
|
-
const composition =
|
|
1943
|
+
const composition = composed(record);
|
|
1874
1944
|
return {
|
|
1875
1945
|
record,
|
|
1876
1946
|
confirmed: confirmedLayer,
|
|
1877
1947
|
visible,
|
|
1948
|
+
folders: new Set(await local.folders(record.key, 'visible', paths)),
|
|
1878
1949
|
*visibleOver(queue) {
|
|
1879
1950
|
return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
|
|
1880
1951
|
},
|
|
@@ -1886,13 +1957,7 @@ export function composeRuntime(options) {
|
|
|
1886
1957
|
return false;
|
|
1887
1958
|
const local = await openLocal();
|
|
1888
1959
|
const roots = record.typedSelection?.roots ?? [];
|
|
1889
|
-
|
|
1890
|
-
(await local.typedWithin(record.key, 'visible', roots, false))) {
|
|
1891
|
-
return true;
|
|
1892
|
-
}
|
|
1893
|
-
return (composition.selection !== null &&
|
|
1894
|
-
composition.selection.model.status === 'unavailable' &&
|
|
1895
|
-
(await local.typedWithin(record.key, 'visible', roots, true)));
|
|
1960
|
+
return local.typedWithin(record.key, 'visible', roots, false);
|
|
1896
1961
|
}
|
|
1897
1962
|
function textDiffers(left, right) {
|
|
1898
1963
|
return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
|
|
@@ -1997,7 +2062,7 @@ export function composeRuntime(options) {
|
|
|
1997
2062
|
const lookup = overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides);
|
|
1998
2063
|
const clean = available(composition) &&
|
|
1999
2064
|
changed.length <= CLEAN_CHANGES &&
|
|
2000
|
-
(await changedCleanly(changed,
|
|
2065
|
+
(await changedCleanly(changed, composed(record), composition, {
|
|
2001
2066
|
lookup,
|
|
2002
2067
|
texts: textsOf(local, record.key, 'confirmed'),
|
|
2003
2068
|
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
@@ -2028,6 +2093,7 @@ export function composeRuntime(options) {
|
|
|
2028
2093
|
operations: room.operations,
|
|
2029
2094
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
2030
2095
|
visible: saving.visible,
|
|
2096
|
+
folders: saving.folders,
|
|
2031
2097
|
queued: queuedDocuments(replica.queue),
|
|
2032
2098
|
unmodeled: await unmodeledIn(record),
|
|
2033
2099
|
validate: validatorOf(record, saving.confirmed),
|
|
@@ -2048,6 +2114,7 @@ export function composeRuntime(options) {
|
|
|
2048
2114
|
locator,
|
|
2049
2115
|
key: saving.record.key,
|
|
2050
2116
|
roots: saving.record.typedSelection?.roots ?? [],
|
|
2117
|
+
composed: composedOf(composed(saving.record)),
|
|
2051
2118
|
overlay: diff.overlay,
|
|
2052
2119
|
});
|
|
2053
2120
|
}
|
|
@@ -2079,7 +2146,7 @@ export function composeRuntime(options) {
|
|
|
2079
2146
|
}
|
|
2080
2147
|
const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
|
|
2081
2148
|
const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
|
|
2082
|
-
const changed = saved.flatMap(({ path, parts }) => changedParts(path,
|
|
2149
|
+
const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
|
|
2083
2150
|
if (changed.length > 0)
|
|
2084
2151
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2085
2152
|
const target = replicas.get(destination);
|
|
@@ -2204,7 +2271,7 @@ export function composeRuntime(options) {
|
|
|
2204
2271
|
...saved.map(({ path }) => path),
|
|
2205
2272
|
...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
|
|
2206
2273
|
]);
|
|
2207
|
-
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path,
|
|
2274
|
+
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
|
|
2208
2275
|
if (changed.length > 0)
|
|
2209
2276
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2210
2277
|
const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
|
|
@@ -2232,6 +2299,7 @@ export function composeRuntime(options) {
|
|
|
2232
2299
|
providerLimits: limits,
|
|
2233
2300
|
handle: room.handle,
|
|
2234
2301
|
visible: saving.visible,
|
|
2302
|
+
folders: saving.folders,
|
|
2235
2303
|
changes,
|
|
2236
2304
|
bases: parts.flatMap(({ bases }) => bases),
|
|
2237
2305
|
queued: queuedDocuments(replica.queue),
|
|
@@ -2382,6 +2450,9 @@ export function composeRuntime(options) {
|
|
|
2382
2450
|
async function readHistory(locator, path, query = {}) {
|
|
2383
2451
|
if (closed || refusal !== null)
|
|
2384
2452
|
return UNCERTAIN;
|
|
2453
|
+
await firstDiscovery;
|
|
2454
|
+
if (!roomsNamed)
|
|
2455
|
+
return UNCERTAIN;
|
|
2385
2456
|
try {
|
|
2386
2457
|
await ensureAuthorization();
|
|
2387
2458
|
}
|
|
@@ -2474,6 +2545,9 @@ export function composeRuntime(options) {
|
|
|
2474
2545
|
async function transferBinary(request, transfer, answer) {
|
|
2475
2546
|
if (closed || refusal !== null)
|
|
2476
2547
|
return UNCERTAIN;
|
|
2548
|
+
await firstDiscovery;
|
|
2549
|
+
if (!roomsNamed)
|
|
2550
|
+
return UNCERTAIN;
|
|
2477
2551
|
try {
|
|
2478
2552
|
await ensureAuthorization();
|
|
2479
2553
|
}
|
|
@@ -2504,23 +2578,27 @@ export function composeRuntime(options) {
|
|
|
2504
2578
|
return UNCERTAIN;
|
|
2505
2579
|
}
|
|
2506
2580
|
}
|
|
2581
|
+
function fetchBinary(request, name) {
|
|
2582
|
+
return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
|
|
2583
|
+
}
|
|
2507
2584
|
async function readBinary(request, name) {
|
|
2508
2585
|
if (closed)
|
|
2509
2586
|
return UNCERTAIN;
|
|
2510
2587
|
const held = await keptFiles.find(name);
|
|
2511
2588
|
if (held !== null)
|
|
2512
2589
|
return held;
|
|
2513
|
-
return
|
|
2590
|
+
return fetchBinary(request, name);
|
|
2514
2591
|
}
|
|
2515
2592
|
async function binaryPath(locator, path, revision) {
|
|
2516
2593
|
if (revision !== undefined)
|
|
2517
2594
|
return { kind: 'path', locator, path, revision };
|
|
2518
2595
|
const replica = replicas.get(locator);
|
|
2519
|
-
if (replica === undefined)
|
|
2520
|
-
return null;
|
|
2521
2596
|
const local = await openLocal();
|
|
2522
|
-
const [held] = await local.entries(replica.record.key, 'visible', [path]);
|
|
2523
|
-
|
|
2597
|
+
const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
|
|
2598
|
+
const attached = held === undefined ? attachedFiles.get(locator)?.get(path) : undefined;
|
|
2599
|
+
if (attached !== undefined)
|
|
2600
|
+
return { kind: 'path', locator, path, revision: attached.revision };
|
|
2601
|
+
if (replica === undefined || held?.binary === undefined || held.binary === null)
|
|
2524
2602
|
return null;
|
|
2525
2603
|
if (held.binary.state === 'pending')
|
|
2526
2604
|
return { kind: 'pending', localId: held.binary.localId };
|
|
@@ -2552,33 +2630,83 @@ export function composeRuntime(options) {
|
|
|
2552
2630
|
revision: record.revision,
|
|
2553
2631
|
};
|
|
2554
2632
|
}
|
|
2555
|
-
async function
|
|
2633
|
+
async function askedBinary(locator, path, revision) {
|
|
2556
2634
|
await ready;
|
|
2557
2635
|
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2558
|
-
|
|
2636
|
+
return drafted !== undefined && drafted.localId === revision
|
|
2559
2637
|
? { kind: 'pending', localId: revision }
|
|
2560
|
-
:
|
|
2638
|
+
: binaryPath(locator, path, revision);
|
|
2639
|
+
}
|
|
2640
|
+
function pendingBinary(locator, path, localId) {
|
|
2641
|
+
const facts = queuedFacts(locator, path, localId);
|
|
2642
|
+
return facts === null
|
|
2643
|
+
? absentBinary(path)
|
|
2644
|
+
: {
|
|
2645
|
+
kind: 'binary',
|
|
2646
|
+
file: localId,
|
|
2647
|
+
byteLength: facts.byteLength,
|
|
2648
|
+
mediaType: facts.mediaType,
|
|
2649
|
+
};
|
|
2650
|
+
}
|
|
2651
|
+
async function readBinaryPath(locator, path, revision) {
|
|
2652
|
+
const asked = await askedBinary(locator, path, revision);
|
|
2561
2653
|
if (asked === null)
|
|
2562
2654
|
return absentBinary(path);
|
|
2563
|
-
if (asked.kind === 'pending')
|
|
2564
|
-
|
|
2565
|
-
return facts === null
|
|
2566
|
-
? absentBinary(path)
|
|
2567
|
-
: {
|
|
2568
|
-
kind: 'binary',
|
|
2569
|
-
file: asked.localId,
|
|
2570
|
-
byteLength: facts.byteLength,
|
|
2571
|
-
mediaType: facts.mediaType,
|
|
2572
|
-
};
|
|
2573
|
-
}
|
|
2655
|
+
if (asked.kind === 'pending')
|
|
2656
|
+
return pendingBinary(locator, path, asked.localId);
|
|
2574
2657
|
return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
|
|
2575
2658
|
}
|
|
2659
|
+
async function heldMediaType(locator, path) {
|
|
2660
|
+
const replica = replicas.get(locator);
|
|
2661
|
+
const [held] = replica === undefined
|
|
2662
|
+
? []
|
|
2663
|
+
: await (await openLocal()).entries(replica.record.key, 'visible', [path]);
|
|
2664
|
+
return held === undefined
|
|
2665
|
+
? attachedFiles.get(locator)?.get(path)?.mediaType
|
|
2666
|
+
: held.binary?.mediaType;
|
|
2667
|
+
}
|
|
2668
|
+
function linkOf(asked, held) {
|
|
2669
|
+
return transferBinary(asked, (authorization) => options.adapter.link(authorization, asked), (result) => binaryLinkAnswer(result, held));
|
|
2670
|
+
}
|
|
2576
2671
|
async function linkBinaryPath(locator, path, revision) {
|
|
2577
2672
|
await ready;
|
|
2578
2673
|
const asked = await binaryPath(locator, path, revision);
|
|
2579
2674
|
if (asked === null || asked.kind === 'pending')
|
|
2580
2675
|
return absentBinary(path);
|
|
2581
|
-
return
|
|
2676
|
+
return linkOf(asked, revision === undefined ? await heldMediaType(locator, path) : undefined);
|
|
2677
|
+
}
|
|
2678
|
+
async function showBinaryPath(locator, path, revision) {
|
|
2679
|
+
const asked = await askedBinary(locator, path, revision);
|
|
2680
|
+
if (asked === null)
|
|
2681
|
+
return absentBinary(path);
|
|
2682
|
+
if (asked.kind === 'pending')
|
|
2683
|
+
return pendingBinary(locator, path, asked.localId);
|
|
2684
|
+
if (closed)
|
|
2685
|
+
return UNCERTAIN;
|
|
2686
|
+
const name = { locator, path: asked.path, revision: asked.revision };
|
|
2687
|
+
const kept = await keptFiles.find(name);
|
|
2688
|
+
if (kept !== null)
|
|
2689
|
+
return kept;
|
|
2690
|
+
const mediaType = await heldMediaType(locator, path);
|
|
2691
|
+
if (mediaType?.startsWith('image/') === true)
|
|
2692
|
+
return fetchBinary(asked, name);
|
|
2693
|
+
return linkOf(asked, revision === undefined ? mediaType : undefined);
|
|
2694
|
+
}
|
|
2695
|
+
async function attachBinary(locator, file) {
|
|
2696
|
+
await ready;
|
|
2697
|
+
await firstDiscovery;
|
|
2698
|
+
if (closed || !roomsNamed || connectionOf() === 'unreachable' || refusal !== null)
|
|
2699
|
+
return OFFLINE;
|
|
2700
|
+
const request = await attachedFile(locator, file);
|
|
2701
|
+
return transferBinary({ locator, path: file.name }, async (held) => {
|
|
2702
|
+
const result = await options.adapter.attach(held, request);
|
|
2703
|
+
if (result.kind === 'attached') {
|
|
2704
|
+
const landed = attachedFiles.get(locator) ?? new Map();
|
|
2705
|
+
landed.set(result.path, { revision: result.revision, mediaType: request.mediaType });
|
|
2706
|
+
attachedFiles.set(locator, landed);
|
|
2707
|
+
}
|
|
2708
|
+
return result;
|
|
2709
|
+
}, attachAnswer);
|
|
2582
2710
|
}
|
|
2583
2711
|
function readPart(file, part) {
|
|
2584
2712
|
return database.ask({ kind: 'file.part', key: file, part });
|
|
@@ -2753,18 +2881,25 @@ export function composeRuntime(options) {
|
|
|
2753
2881
|
after = page.next;
|
|
2754
2882
|
}
|
|
2755
2883
|
}
|
|
2756
|
-
async function validatedRoom(local, record, view, previous, overrides = null) {
|
|
2757
|
-
const composition =
|
|
2758
|
-
const modeled = record.model.status !== 'unavailable' || record.
|
|
2884
|
+
async function validatedRoom(local, record, view, previous, overrides = null, checked = null) {
|
|
2885
|
+
const composition = composed(record);
|
|
2886
|
+
const modeled = record.model.status !== 'unavailable' || record.typedSelection !== null;
|
|
2759
2887
|
const lookup = overrides === null
|
|
2760
2888
|
? lookupOf(local, record.key, view)
|
|
2761
2889
|
: overriddenLookup(lookupOf(local, record.key, view), overrides);
|
|
2762
2890
|
const texts = overrides === null
|
|
2763
2891
|
? textsOf(local, record.key, view)
|
|
2764
2892
|
: overriddenTexts(textsOf(local, record.key, view), overrides);
|
|
2765
|
-
const
|
|
2893
|
+
const walking = overrides === null
|
|
2766
2894
|
? walkOf(local, record.key, view)
|
|
2767
2895
|
: overriddenWalk(walkOf(local, record.key, view), overrides);
|
|
2896
|
+
const walk = checked === null
|
|
2897
|
+
? walking
|
|
2898
|
+
: async (after) => {
|
|
2899
|
+
const page = await walking(after);
|
|
2900
|
+
checked(page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0));
|
|
2901
|
+
return page;
|
|
2902
|
+
};
|
|
2768
2903
|
let changes = null;
|
|
2769
2904
|
let reshaped = false;
|
|
2770
2905
|
if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
|
|
@@ -2789,11 +2924,11 @@ export function composeRuntime(options) {
|
|
|
2789
2924
|
}
|
|
2790
2925
|
const named = changes === null ? null : changes.map(({ path }) => path);
|
|
2791
2926
|
const textChanged = changes === null || changes.length > 0 || reshaped;
|
|
2792
|
-
const modelsChanged = previous === null || !sameComposition(
|
|
2927
|
+
const modelsChanged = previous === null || !sameComposition(composed(previous.record), composition);
|
|
2793
2928
|
if (modeled && (textChanged || modelsChanged)) {
|
|
2794
2929
|
const clean = previous !== null &&
|
|
2795
2930
|
changes !== null &&
|
|
2796
|
-
(await changedCleanly(changes,
|
|
2931
|
+
(await changedCleanly(changes, composed(previous.record), composition, {
|
|
2797
2932
|
lookup,
|
|
2798
2933
|
texts,
|
|
2799
2934
|
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
@@ -2811,13 +2946,13 @@ export function composeRuntime(options) {
|
|
|
2811
2946
|
return unreadableRoom(texts, walk, named);
|
|
2812
2947
|
return null;
|
|
2813
2948
|
}
|
|
2814
|
-
function refreshingOf(local, key, liveness, replaying = null) {
|
|
2815
|
-
const write = replaying === null ? exclusive :
|
|
2949
|
+
function refreshingOf(local, key, composition, liveness, replaying = null) {
|
|
2950
|
+
const write = replaying === null ? exclusive : finishingWrite;
|
|
2816
2951
|
return {
|
|
2817
2952
|
async turn() {
|
|
2818
2953
|
if (replaying === null)
|
|
2819
2954
|
return;
|
|
2820
|
-
installing(replaying
|
|
2955
|
+
installing(replaying);
|
|
2821
2956
|
await screened();
|
|
2822
2957
|
},
|
|
2823
2958
|
prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
|
|
@@ -2831,7 +2966,7 @@ export function composeRuntime(options) {
|
|
|
2831
2966
|
const staged = await write(async () => {
|
|
2832
2967
|
if (!liveness())
|
|
2833
2968
|
return false;
|
|
2834
|
-
await timed(() => local.refresh(key, last
|
|
2969
|
+
await timed(stageBudget, () => local.refresh(key, composition, last
|
|
2835
2970
|
? { ...page, entries }
|
|
2836
2971
|
: { entries, removals: [], folders: [], folderRemovals: [] }));
|
|
2837
2972
|
return true;
|
|
@@ -2850,28 +2985,79 @@ export function composeRuntime(options) {
|
|
|
2850
2985
|
let spent = 0;
|
|
2851
2986
|
while (end < entries.length) {
|
|
2852
2987
|
const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
|
|
2853
|
-
if (spent + size >
|
|
2988
|
+
if (spent + size > stageBudget.bytes && end > from)
|
|
2854
2989
|
break;
|
|
2855
2990
|
spent += size;
|
|
2856
2991
|
end += 1;
|
|
2857
2992
|
}
|
|
2858
2993
|
return end;
|
|
2859
2994
|
}
|
|
2860
|
-
async function timed(work) {
|
|
2995
|
+
async function timed(budget, work) {
|
|
2861
2996
|
const started = performance.now();
|
|
2862
2997
|
const done = await work();
|
|
2863
2998
|
const spent = performance.now() - started;
|
|
2864
|
-
if (spent <
|
|
2865
|
-
|
|
2866
|
-
else if (spent >
|
|
2867
|
-
|
|
2999
|
+
if (spent < budget.aim / 2)
|
|
3000
|
+
budget.bytes = Math.min(budget.ceiling, Math.round(budget.bytes * 1.5));
|
|
3001
|
+
else if (spent > budget.aim * 2) {
|
|
3002
|
+
budget.bytes = Math.max(budget.floor, Math.round(budget.bytes / 1.5));
|
|
2868
3003
|
}
|
|
2869
3004
|
return done;
|
|
2870
3005
|
}
|
|
2871
3006
|
function installWrite(work) {
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
3007
|
+
return installTurn(installTurns.starting, work);
|
|
3008
|
+
}
|
|
3009
|
+
function finishingWrite(work) {
|
|
3010
|
+
return installTurn(installTurns.finishing, work);
|
|
3011
|
+
}
|
|
3012
|
+
function installTurn(queue, work) {
|
|
3013
|
+
return new Promise((resolve, reject) => {
|
|
3014
|
+
queue.push(() => void exclusive(work)
|
|
3015
|
+
.then(resolve, reject)
|
|
3016
|
+
.finally(() => {
|
|
3017
|
+
installTurns.busy = false;
|
|
3018
|
+
nextInstallTurn();
|
|
3019
|
+
}));
|
|
3020
|
+
nextInstallTurn();
|
|
3021
|
+
});
|
|
3022
|
+
}
|
|
3023
|
+
function nextInstallTurn() {
|
|
3024
|
+
if (installTurns.busy || installTurns.ahead > 0)
|
|
3025
|
+
return;
|
|
3026
|
+
const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
|
|
3027
|
+
if (turn === undefined)
|
|
3028
|
+
return;
|
|
3029
|
+
installTurns.busy = true;
|
|
3030
|
+
turn();
|
|
3031
|
+
}
|
|
3032
|
+
async function ahead(work) {
|
|
3033
|
+
let taken = false;
|
|
3034
|
+
const hold = {
|
|
3035
|
+
take() {
|
|
3036
|
+
if (taken)
|
|
3037
|
+
return;
|
|
3038
|
+
taken = true;
|
|
3039
|
+
installTurns.ahead += 1;
|
|
3040
|
+
},
|
|
3041
|
+
release() {
|
|
3042
|
+
if (!taken)
|
|
3043
|
+
return;
|
|
3044
|
+
taken = false;
|
|
3045
|
+
installTurns.ahead -= 1;
|
|
3046
|
+
resumeInstall();
|
|
3047
|
+
},
|
|
3048
|
+
};
|
|
3049
|
+
hold.take();
|
|
3050
|
+
try {
|
|
3051
|
+
return await work(hold);
|
|
3052
|
+
}
|
|
3053
|
+
finally {
|
|
3054
|
+
hold.release();
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
function resumeInstall() {
|
|
3058
|
+
if (installTurns.ahead > 0)
|
|
3059
|
+
return;
|
|
3060
|
+
void yielded().then(nextInstallTurn);
|
|
2875
3061
|
}
|
|
2876
3062
|
function screened() {
|
|
2877
3063
|
if (onScreen || closed)
|
|
@@ -2879,24 +3065,73 @@ export function composeRuntime(options) {
|
|
|
2879
3065
|
queueReadiness();
|
|
2880
3066
|
return new Promise((resolve) => shownWaiters.push(resolve));
|
|
2881
3067
|
}
|
|
2882
|
-
async function
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
3068
|
+
async function heldStream() {
|
|
3069
|
+
for (;;) {
|
|
3070
|
+
if (streams.generation !== generation)
|
|
3071
|
+
streams = { generation, free: STREAMS_HELD };
|
|
3072
|
+
const held = streams;
|
|
3073
|
+
if (held.free > 0) {
|
|
3074
|
+
held.free -= 1;
|
|
3075
|
+
return once(() => {
|
|
3076
|
+
held.free += 1;
|
|
3077
|
+
});
|
|
3078
|
+
}
|
|
3079
|
+
await new Promise((admit) => installWaiters.push(admit));
|
|
3080
|
+
}
|
|
2887
3081
|
}
|
|
2888
|
-
function
|
|
3082
|
+
async function unqueued() {
|
|
3083
|
+
while (queuedBytes >= QUEUED_BYTES) {
|
|
3084
|
+
await new Promise((admit) => installWaiters.push(admit));
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
function once(release) {
|
|
2889
3088
|
let released = false;
|
|
2890
3089
|
return () => {
|
|
2891
3090
|
if (released)
|
|
2892
3091
|
return;
|
|
2893
3092
|
released = true;
|
|
2894
|
-
|
|
2895
|
-
|
|
3093
|
+
release();
|
|
3094
|
+
for (const admit of installWaiters.splice(0))
|
|
3095
|
+
admit();
|
|
2896
3096
|
};
|
|
2897
3097
|
}
|
|
2898
|
-
function installing(locator
|
|
2899
|
-
installs.
|
|
3098
|
+
function installing(locator) {
|
|
3099
|
+
installs.add(locator);
|
|
3100
|
+
queueReadiness();
|
|
3101
|
+
}
|
|
3102
|
+
function untransferred(locator) {
|
|
3103
|
+
transfers.delete(locator);
|
|
3104
|
+
arrivals.delete(locator);
|
|
3105
|
+
streaming.delete(locator);
|
|
3106
|
+
underived.delete(locator);
|
|
3107
|
+
unindexed.delete(locator);
|
|
3108
|
+
unchecked.delete(locator);
|
|
3109
|
+
}
|
|
3110
|
+
function owedIndexes(locator, bytes, text) {
|
|
3111
|
+
if (bytes <= 0 || text <= 0)
|
|
3112
|
+
return;
|
|
3113
|
+
unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes: bytes / 2, text }]);
|
|
3114
|
+
const check = unchecked.get(locator) ?? { bytes: 0, text: 0, done: 0 };
|
|
3115
|
+
unchecked.set(locator, { ...check, bytes: check.bytes + bytes / 2, text: check.text + text });
|
|
3116
|
+
}
|
|
3117
|
+
function built(locator, { done, text }) {
|
|
3118
|
+
const owed = unindexed.get(locator) ?? [];
|
|
3119
|
+
let left = text;
|
|
3120
|
+
while (!done && left > 0 && owed.length > 0) {
|
|
3121
|
+
const [first] = owed;
|
|
3122
|
+
if (left >= first.text) {
|
|
3123
|
+
left -= first.text;
|
|
3124
|
+
owed.shift();
|
|
3125
|
+
continue;
|
|
3126
|
+
}
|
|
3127
|
+
owed[0] = {
|
|
3128
|
+
bytes: (first.bytes * (first.text - left)) / first.text,
|
|
3129
|
+
text: first.text - left,
|
|
3130
|
+
};
|
|
3131
|
+
left = 0;
|
|
3132
|
+
}
|
|
3133
|
+
if (done || owed.length === 0)
|
|
3134
|
+
unindexed.delete(locator);
|
|
2900
3135
|
queueReadiness();
|
|
2901
3136
|
}
|
|
2902
3137
|
function installed(locator) {
|
|
@@ -2914,7 +3149,7 @@ export function composeRuntime(options) {
|
|
|
2914
3149
|
for (const resolve of shownWaiters.splice(0))
|
|
2915
3150
|
resolve();
|
|
2916
3151
|
}
|
|
2917
|
-
async function pullRefresh(authorized, held, liveness) {
|
|
3152
|
+
async function pullRefresh(authorized, held, liveness, hold) {
|
|
2918
3153
|
if (!liveness())
|
|
2919
3154
|
return unconfirmed;
|
|
2920
3155
|
const turn = generation;
|
|
@@ -2923,7 +3158,18 @@ export function composeRuntime(options) {
|
|
|
2923
3158
|
const local = await openLocal();
|
|
2924
3159
|
try {
|
|
2925
3160
|
await exclusive(() => local.unrefresh(record.key));
|
|
2926
|
-
const result = await advancement(
|
|
3161
|
+
const result = await advancement({
|
|
3162
|
+
...reads,
|
|
3163
|
+
async refresh(authorization, request) {
|
|
3164
|
+
hold.release();
|
|
3165
|
+
try {
|
|
3166
|
+
return await reads.refresh(authorization, request);
|
|
3167
|
+
}
|
|
3168
|
+
finally {
|
|
3169
|
+
hold.take();
|
|
3170
|
+
}
|
|
3171
|
+
},
|
|
3172
|
+
}, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, composed(record), liveness));
|
|
2927
3173
|
if (result.kind !== 'advanced') {
|
|
2928
3174
|
journal.record(`transfer refresh ${record.locator} ${result.kind}`);
|
|
2929
3175
|
return result;
|
|
@@ -2966,129 +3212,142 @@ export function composeRuntime(options) {
|
|
|
2966
3212
|
return made;
|
|
2967
3213
|
}
|
|
2968
3214
|
function staging(local, liveness) {
|
|
3215
|
+
const derived = async (locator, key, composition) => {
|
|
3216
|
+
underived.delete(locator);
|
|
3217
|
+
for (;;) {
|
|
3218
|
+
await screened();
|
|
3219
|
+
if (!liveness())
|
|
3220
|
+
return false;
|
|
3221
|
+
const done = await finishingWrite(async () => liveness()
|
|
3222
|
+
? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
|
|
3223
|
+
: null);
|
|
3224
|
+
if (done === null)
|
|
3225
|
+
return false;
|
|
3226
|
+
built(locator, done);
|
|
3227
|
+
if (done.done)
|
|
3228
|
+
return true;
|
|
3229
|
+
}
|
|
3230
|
+
};
|
|
2969
3231
|
return {
|
|
2970
3232
|
held: (locator) => transfers.get(locator) ?? null,
|
|
2971
3233
|
async turn(locator, step) {
|
|
2972
|
-
installing(locator
|
|
3234
|
+
installing(locator);
|
|
2973
3235
|
for (;;) {
|
|
2974
3236
|
await screened();
|
|
2975
|
-
const release = step === 'downloading' ? await
|
|
3237
|
+
const release = step === 'downloading' ? await heldStream() : () => { };
|
|
2976
3238
|
if (onScreen || closed)
|
|
2977
3239
|
return release;
|
|
2978
3240
|
release();
|
|
2979
3241
|
}
|
|
2980
3242
|
},
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
3243
|
+
unqueued,
|
|
3244
|
+
queued(bytes) {
|
|
3245
|
+
queuedBytes += bytes;
|
|
3246
|
+
return once(() => {
|
|
3247
|
+
queuedBytes -= bytes;
|
|
3248
|
+
});
|
|
2986
3249
|
},
|
|
2987
3250
|
arriving(locator) {
|
|
2988
3251
|
let ended = false;
|
|
2989
3252
|
return {
|
|
2990
|
-
at(
|
|
3253
|
+
at(bytes) {
|
|
2991
3254
|
if (ended)
|
|
2992
3255
|
return;
|
|
2993
|
-
|
|
2994
|
-
|
|
3256
|
+
arrivals.set(locator, { parts: arrivals.get(locator)?.parts ?? 0, bytes });
|
|
3257
|
+
streaming.add(locator);
|
|
2995
3258
|
const now = performance.now();
|
|
2996
3259
|
if (now - arrivalReported < ARRIVAL_REPORT_MS)
|
|
2997
3260
|
return;
|
|
2998
3261
|
arrivalReported = now;
|
|
2999
3262
|
queueReadiness();
|
|
3000
3263
|
},
|
|
3264
|
+
part() {
|
|
3265
|
+
const arrived = arrivals.get(locator);
|
|
3266
|
+
arrivals.set(locator, {
|
|
3267
|
+
parts: (arrived?.parts ?? 0) + 1,
|
|
3268
|
+
bytes: arrived?.bytes ?? 0,
|
|
3269
|
+
});
|
|
3270
|
+
queueReadiness();
|
|
3271
|
+
},
|
|
3001
3272
|
end() {
|
|
3273
|
+
if (ended)
|
|
3274
|
+
return;
|
|
3002
3275
|
ended = true;
|
|
3003
|
-
if (
|
|
3276
|
+
if (streaming.delete(locator))
|
|
3004
3277
|
queueReadiness();
|
|
3005
3278
|
},
|
|
3006
3279
|
};
|
|
3007
3280
|
},
|
|
3008
|
-
received: (locator, token) => local.transferPart(locator, token),
|
|
3009
|
-
keep: (locator, token, received) => installWrite(async () => {
|
|
3010
|
-
if (liveness())
|
|
3011
|
-
await local.keepPart(locator, token, received);
|
|
3012
|
-
}),
|
|
3013
3281
|
announced(locator) {
|
|
3014
3282
|
const later = announced.get(locator);
|
|
3015
3283
|
return later === undefined ? [] : [later];
|
|
3016
3284
|
},
|
|
3017
|
-
async stage(record, page, composition
|
|
3285
|
+
async stage(record, page, composition) {
|
|
3018
3286
|
const { locator } = record;
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
if (token !== null)
|
|
3025
|
-
arrivals.get(locator)?.delete(token);
|
|
3026
|
-
installing(locator, 'writing', { written: from, entries, bytes: whole });
|
|
3027
|
-
for (let written = from;;) {
|
|
3028
|
-
await screened();
|
|
3287
|
+
installing(locator);
|
|
3288
|
+
await screened();
|
|
3289
|
+
if (!liveness())
|
|
3290
|
+
return null;
|
|
3291
|
+
const key = await installWrite(async () => {
|
|
3029
3292
|
if (!liveness())
|
|
3030
3293
|
return null;
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
});
|
|
3051
|
-
if (key === null)
|
|
3052
|
-
return null;
|
|
3053
|
-
const arrived = !transfers.has(locator);
|
|
3054
|
-
held = { ...next, key };
|
|
3055
|
-
transfers.set(locator, held);
|
|
3056
|
-
if (arrived)
|
|
3057
|
-
liveQueries.recover();
|
|
3058
|
-
if (last) {
|
|
3059
|
-
installing(locator, held.pageToken === null ? 'replaying' : 'downloading');
|
|
3060
|
-
liveQueries.recover();
|
|
3061
|
-
return held;
|
|
3062
|
-
}
|
|
3063
|
-
installing(locator, 'writing', { written: end, entries, bytes: whole });
|
|
3064
|
-
written = end;
|
|
3065
|
-
}
|
|
3294
|
+
return local.stage(record, composition, page);
|
|
3295
|
+
});
|
|
3296
|
+
if (key === null || !liveness())
|
|
3297
|
+
return null;
|
|
3298
|
+
const arrived = !transfers.has(locator);
|
|
3299
|
+
const prior = transfers.get(locator)?.done.bytes ?? 0;
|
|
3300
|
+
const held = { ...record, key };
|
|
3301
|
+
transfers.set(locator, held);
|
|
3302
|
+
if (arrived)
|
|
3303
|
+
liveQueries.recover();
|
|
3304
|
+
installing(locator);
|
|
3305
|
+
liveQueries.recover();
|
|
3306
|
+
const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
|
|
3307
|
+
owedIndexes(locator, record.done.bytes - prior, text);
|
|
3308
|
+
const owed = text + (underived.get(locator) ?? 0);
|
|
3309
|
+
underived.set(locator, owed);
|
|
3310
|
+
if (owed < deriveBudget.bytes)
|
|
3311
|
+
return held;
|
|
3312
|
+
return (await derived(locator, key, composition)) ? held : null;
|
|
3066
3313
|
},
|
|
3067
|
-
|
|
3314
|
+
derive: derived,
|
|
3315
|
+
async refreshing(locator, key, composition) {
|
|
3068
3316
|
await exclusive(() => local.unrefresh(key));
|
|
3069
|
-
return refreshingOf(local, key, liveness, locator);
|
|
3317
|
+
return refreshingOf(local, key, composition, liveness, locator);
|
|
3070
3318
|
},
|
|
3071
3319
|
abandon: (locator) => exclusive(async () => {
|
|
3072
3320
|
if (!liveness())
|
|
3073
3321
|
return;
|
|
3074
3322
|
await local.abandon(locator);
|
|
3075
|
-
|
|
3323
|
+
untransferred(locator);
|
|
3076
3324
|
liveQueries.recover();
|
|
3077
3325
|
sweep();
|
|
3078
3326
|
}),
|
|
3079
|
-
setAside:
|
|
3327
|
+
setAside: async (locator) => {
|
|
3080
3328
|
if (!liveness())
|
|
3081
3329
|
return;
|
|
3082
|
-
|
|
3083
|
-
transfers.delete(locator);
|
|
3330
|
+
untransferred(locator);
|
|
3084
3331
|
liveQueries.recover();
|
|
3085
|
-
}
|
|
3332
|
+
},
|
|
3086
3333
|
release: (key) => exclusive(async () => {
|
|
3087
3334
|
await local.release(key);
|
|
3088
3335
|
sweep();
|
|
3089
3336
|
}),
|
|
3090
3337
|
content: (key) => heldContentOf(local, key),
|
|
3091
|
-
validate
|
|
3338
|
+
async validate(record, view) {
|
|
3339
|
+
const { locator } = record;
|
|
3340
|
+
const refused = await validatedRoom(local, record, view, null, null, (text) => {
|
|
3341
|
+
const check = unchecked.get(locator);
|
|
3342
|
+
if (check === undefined)
|
|
3343
|
+
return;
|
|
3344
|
+
unchecked.set(locator, { ...check, done: check.done + text });
|
|
3345
|
+
queueReadiness();
|
|
3346
|
+
});
|
|
3347
|
+
unchecked.delete(locator);
|
|
3348
|
+
queueReadiness();
|
|
3349
|
+
return refused;
|
|
3350
|
+
},
|
|
3092
3351
|
};
|
|
3093
3352
|
}
|
|
3094
3353
|
function reusable(room, local) {
|
|
@@ -3106,10 +3365,7 @@ export function composeRuntime(options) {
|
|
|
3106
3365
|
liveQueries.recover();
|
|
3107
3366
|
}
|
|
3108
3367
|
try {
|
|
3109
|
-
const result = await bootstrapRoom(reads, authorized, room, {
|
|
3110
|
-
collection: modelOf(room.locator),
|
|
3111
|
-
selection: modelOf(room.locator, 'typed-selection'),
|
|
3112
|
-
}, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
|
|
3368
|
+
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));
|
|
3113
3369
|
const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
|
|
3114
3370
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
3115
3371
|
if (result.kind !== 'staged') {
|
|
@@ -3153,32 +3409,10 @@ export function composeRuntime(options) {
|
|
|
3153
3409
|
return unconfirmed;
|
|
3154
3410
|
const held = replicas.get(room.locator);
|
|
3155
3411
|
if (held !== undefined) {
|
|
3156
|
-
const refreshed = await
|
|
3157
|
-
if (
|
|
3158
|
-
return
|
|
3159
|
-
if (refreshed
|
|
3160
|
-
stale.delete(room.locator);
|
|
3161
|
-
if (sourceRefusals.has(room.locator))
|
|
3162
|
-
return null;
|
|
3163
|
-
if (unreadable.delete(room.locator)) {
|
|
3164
|
-
scopedRefusals.delete(room.locator);
|
|
3165
|
-
list(room, true);
|
|
3166
|
-
emitRooms();
|
|
3167
|
-
}
|
|
3168
|
-
return null;
|
|
3169
|
-
}
|
|
3170
|
-
if (refreshed.kind === 'advanced') {
|
|
3171
|
-
const landing = { kind: 'refreshed', base: refreshed.base };
|
|
3172
|
-
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3173
|
-
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
3174
|
-
const reached = now !== undefined &&
|
|
3175
|
-
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
3176
|
-
return reached ? null : advance(room, local, authorized, live);
|
|
3177
|
-
}
|
|
3178
|
-
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
3179
|
-
return null;
|
|
3180
|
-
}
|
|
3181
|
-
if (refreshed.kind !== 'beyond-horizon')
|
|
3412
|
+
const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold));
|
|
3413
|
+
if (refreshed === 'behind')
|
|
3414
|
+
return advance(room, local, authorized, live);
|
|
3415
|
+
if (refreshed !== 'beyond-horizon')
|
|
3182
3416
|
return refreshed;
|
|
3183
3417
|
if (opened !== null) {
|
|
3184
3418
|
owed.add(room.locator);
|
|
@@ -3195,6 +3429,34 @@ export function composeRuntime(options) {
|
|
|
3195
3429
|
}
|
|
3196
3430
|
return live() ? bootstrapped : unconfirmed;
|
|
3197
3431
|
}
|
|
3432
|
+
async function refreshedRoom(room, local, authorized, held, live, hold) {
|
|
3433
|
+
const refreshed = await pullRefresh(authorized, held, live, hold);
|
|
3434
|
+
if (!live())
|
|
3435
|
+
return unconfirmed;
|
|
3436
|
+
if (refreshed.kind === 'still-current') {
|
|
3437
|
+
stale.delete(room.locator);
|
|
3438
|
+
if (sourceRefusals.has(room.locator))
|
|
3439
|
+
return null;
|
|
3440
|
+
if (unreadable.delete(room.locator)) {
|
|
3441
|
+
scopedRefusals.delete(room.locator);
|
|
3442
|
+
list(room, true);
|
|
3443
|
+
emitRooms();
|
|
3444
|
+
}
|
|
3445
|
+
return null;
|
|
3446
|
+
}
|
|
3447
|
+
if (refreshed.kind === 'advanced') {
|
|
3448
|
+
const landing = { kind: 'refreshed', base: refreshed.base };
|
|
3449
|
+
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3450
|
+
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
3451
|
+
const reached = now !== undefined &&
|
|
3452
|
+
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
3453
|
+
return reached ? null : 'behind';
|
|
3454
|
+
}
|
|
3455
|
+
await retainModel(local, room, modelOf(room.locator));
|
|
3456
|
+
return null;
|
|
3457
|
+
}
|
|
3458
|
+
return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
|
|
3459
|
+
}
|
|
3198
3460
|
async function withhold(withheld, room, local, progress) {
|
|
3199
3461
|
if (withheld.kind === 'unconfirmed')
|
|
3200
3462
|
return;
|
|
@@ -3716,6 +3978,7 @@ export function composeRuntime(options) {
|
|
|
3716
3978
|
locator: room.locator,
|
|
3717
3979
|
key: before.record.key,
|
|
3718
3980
|
roots: before.record.typedSelection?.roots ?? [],
|
|
3981
|
+
composed: composedOf(composed(before.record)),
|
|
3719
3982
|
overlay: { set: [], cleared: [...before.overlaid] },
|
|
3720
3983
|
},
|
|
3721
3984
|
]),
|
|
@@ -3771,16 +4034,17 @@ export function composeRuntime(options) {
|
|
|
3771
4034
|
release();
|
|
3772
4035
|
};
|
|
3773
4036
|
}
|
|
3774
|
-
|
|
3775
|
-
const
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
4037
|
+
function declaredAsHeld(room) {
|
|
4038
|
+
const held = replicas.get(room.locator)?.record;
|
|
4039
|
+
return (held === undefined ||
|
|
4040
|
+
(JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
|
|
4041
|
+
JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
|
|
4042
|
+
JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
|
|
4043
|
+
}
|
|
4044
|
+
async function retainModel(local, room, next) {
|
|
4045
|
+
const key = room.locator;
|
|
4046
|
+
const previous = modelOf(room.locator);
|
|
4047
|
+
if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(room))
|
|
3784
4048
|
return;
|
|
3785
4049
|
if (next.status === 'unavailable') {
|
|
3786
4050
|
invalidModels.add(key);
|
|
@@ -3797,11 +4061,11 @@ export function composeRuntime(options) {
|
|
|
3797
4061
|
operations: room.operations,
|
|
3798
4062
|
replica: room.replica,
|
|
3799
4063
|
typedSelection: room.typedSelection ?? null,
|
|
3800
|
-
|
|
4064
|
+
model: next,
|
|
3801
4065
|
};
|
|
3802
4066
|
let read = 'staged';
|
|
3803
4067
|
if (!sameMembership(declared, held.record)) {
|
|
3804
|
-
const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'),
|
|
4068
|
+
const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), composed(declared));
|
|
3805
4069
|
read =
|
|
3806
4070
|
verdict.verdict !== 'refuse'
|
|
3807
4071
|
? 'staged'
|
|
@@ -3822,7 +4086,7 @@ export function composeRuntime(options) {
|
|
|
3822
4086
|
key: record.key,
|
|
3823
4087
|
record: storedRecord(record),
|
|
3824
4088
|
roots: record.typedSelection?.roots ?? [],
|
|
3825
|
-
|
|
4089
|
+
composed: composedOf(composed(record)),
|
|
3826
4090
|
overlay: diff.overlay,
|
|
3827
4091
|
},
|
|
3828
4092
|
],
|
|
@@ -3844,18 +4108,21 @@ export function composeRuntime(options) {
|
|
|
3844
4108
|
declarationRefusals.add(room.locator);
|
|
3845
4109
|
}
|
|
3846
4110
|
}
|
|
3847
|
-
|
|
3848
|
-
if (next.status === 'current') {
|
|
3849
|
-
liveModels.set(key, JSON.stringify(next.snapshot));
|
|
3850
|
-
invalidModels.delete(key);
|
|
3851
|
-
}
|
|
3852
|
-
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
4111
|
+
heldModel(room, next);
|
|
3853
4112
|
if (changed === null)
|
|
3854
4113
|
emitRooms();
|
|
3855
4114
|
else
|
|
3856
4115
|
publishChangeSet(changed);
|
|
3857
4116
|
});
|
|
3858
4117
|
}
|
|
4118
|
+
function heldModel(room, next) {
|
|
4119
|
+
models.set(room.locator, next);
|
|
4120
|
+
if (next.status === 'current') {
|
|
4121
|
+
liveModels.set(room.locator, JSON.stringify(next.snapshot));
|
|
4122
|
+
invalidModels.delete(room.locator);
|
|
4123
|
+
}
|
|
4124
|
+
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
4125
|
+
}
|
|
3859
4126
|
async function scoped(result, room, local) {
|
|
3860
4127
|
if (result.scope === 'content' && !result.retryable) {
|
|
3861
4128
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
@@ -3869,14 +4136,14 @@ export function composeRuntime(options) {
|
|
|
3869
4136
|
});
|
|
3870
4137
|
let scopedAway = room;
|
|
3871
4138
|
if (result.scope === 'model')
|
|
3872
|
-
await retainModel(local, room,
|
|
4139
|
+
await retainModel(local, room, {
|
|
3873
4140
|
status: 'unavailable',
|
|
3874
4141
|
reason: 'not-readable',
|
|
3875
4142
|
});
|
|
3876
4143
|
else if (result.scope !== 'operation') {
|
|
3877
4144
|
unreadable.add(room.locator);
|
|
3878
4145
|
scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
|
|
3879
|
-
await retainModel(local, scopedAway,
|
|
4146
|
+
await retainModel(local, scopedAway, modelOf(room.locator));
|
|
3880
4147
|
}
|
|
3881
4148
|
if (!continuing())
|
|
3882
4149
|
return;
|
|
@@ -3926,7 +4193,7 @@ export function composeRuntime(options) {
|
|
|
3926
4193
|
return false;
|
|
3927
4194
|
}
|
|
3928
4195
|
if (prior !== undefined)
|
|
3929
|
-
await retainModel(local, room,
|
|
4196
|
+
await retainModel(local, room, modelOf(room.locator));
|
|
3930
4197
|
if (discoveryPaused || authorization === undefined)
|
|
3931
4198
|
return false;
|
|
3932
4199
|
return room.replica.admission === 'admitted';
|
|
@@ -3939,10 +4206,8 @@ export function composeRuntime(options) {
|
|
|
3939
4206
|
await withhold(withheld, room, local, progress);
|
|
3940
4207
|
}
|
|
3941
4208
|
function revoked(locator) {
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
3945
|
-
});
|
|
4209
|
+
const model = modelOf(locator);
|
|
4210
|
+
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
3946
4211
|
}
|
|
3947
4212
|
async function loadRoom(room, local, progress, live) {
|
|
3948
4213
|
if (!live() || authorization === undefined)
|
|
@@ -3992,6 +4257,8 @@ export function composeRuntime(options) {
|
|
|
3992
4257
|
catch {
|
|
3993
4258
|
result = { kind: 'transport-uncertainty' };
|
|
3994
4259
|
}
|
|
4260
|
+
if (result.kind === 'discovery')
|
|
4261
|
+
roomsNamed = true;
|
|
3995
4262
|
if (!live()) {
|
|
3996
4263
|
progress.complete = false;
|
|
3997
4264
|
notifyReady();
|
|
@@ -3999,12 +4266,7 @@ export function composeRuntime(options) {
|
|
|
3999
4266
|
}
|
|
4000
4267
|
journal.record(`connection discover ${result.kind}`);
|
|
4001
4268
|
if (result.kind === 'transport-uncertainty') {
|
|
4002
|
-
|
|
4003
|
-
await retainModel(local, room, 'collection', cached(modelOf(room.locator)));
|
|
4004
|
-
if (room.typedSelection !== null) {
|
|
4005
|
-
await retainModel(local, room, 'typed-selection', cached(modelOf(room.locator, 'typed-selection')));
|
|
4006
|
-
}
|
|
4007
|
-
}
|
|
4269
|
+
await settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
|
|
4008
4270
|
progress.reachable = false;
|
|
4009
4271
|
progress.complete = false;
|
|
4010
4272
|
notifyReady();
|
|
@@ -4059,6 +4321,7 @@ export function composeRuntime(options) {
|
|
|
4059
4321
|
limits = result.providerLimits;
|
|
4060
4322
|
await local.fact('providerLimits', limits);
|
|
4061
4323
|
await local.fact('capabilities', capabilities);
|
|
4324
|
+
conversations.declared();
|
|
4062
4325
|
}
|
|
4063
4326
|
const signedIn = result.member ?? null;
|
|
4064
4327
|
if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
|
|
@@ -4116,7 +4379,7 @@ export function composeRuntime(options) {
|
|
|
4116
4379
|
function sync(attempt) {
|
|
4117
4380
|
return task(OPENING_LANE, async (live) => {
|
|
4118
4381
|
if (!live()) {
|
|
4119
|
-
|
|
4382
|
+
readyFromStore();
|
|
4120
4383
|
return;
|
|
4121
4384
|
}
|
|
4122
4385
|
const progress = { complete: true, reachable: true };
|
|
@@ -4194,6 +4457,7 @@ export function composeRuntime(options) {
|
|
|
4194
4457
|
});
|
|
4195
4458
|
}
|
|
4196
4459
|
function closedSocket(revoked, lost) {
|
|
4460
|
+
notifyDiscovered();
|
|
4197
4461
|
const opened = socketOpen;
|
|
4198
4462
|
socketOpen = false;
|
|
4199
4463
|
connectionLost();
|
|
@@ -4237,13 +4501,9 @@ export function composeRuntime(options) {
|
|
|
4237
4501
|
const local = await openLocal();
|
|
4238
4502
|
const reloaded = await replacing;
|
|
4239
4503
|
if (progress.reachable) {
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
!reloading.includes(room.locator)) {
|
|
4244
|
-
await confirmModels(room, local);
|
|
4245
|
-
}
|
|
4246
|
-
}
|
|
4504
|
+
await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
|
|
4505
|
+
!expired.includes(room.locator) &&
|
|
4506
|
+
!reloading.includes(room.locator)), 'current', local);
|
|
4247
4507
|
}
|
|
4248
4508
|
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
4249
4509
|
if (failed !== undefined)
|
|
@@ -4284,12 +4544,30 @@ export function composeRuntime(options) {
|
|
|
4284
4544
|
return [];
|
|
4285
4545
|
}
|
|
4286
4546
|
}
|
|
4287
|
-
async function
|
|
4288
|
-
|
|
4289
|
-
const model = modelOf(room.locator
|
|
4290
|
-
|
|
4291
|
-
|
|
4547
|
+
async function settleModels(held, status, local) {
|
|
4548
|
+
const owed = (room) => {
|
|
4549
|
+
const model = modelOf(room.locator);
|
|
4550
|
+
return model.status !== 'unavailable' && model.status !== status;
|
|
4551
|
+
};
|
|
4552
|
+
const settled = await exclusive(async () => {
|
|
4553
|
+
if (!continuing())
|
|
4554
|
+
return [];
|
|
4555
|
+
const unchanged = held.filter((room) => owed(room) && !declarationRefusals.has(room.locator) && declaredAsHeld(room));
|
|
4556
|
+
for (const room of unchanged) {
|
|
4557
|
+
const next = { ...modelOf(room.locator), status };
|
|
4558
|
+
const replica = replicas.get(room.locator);
|
|
4559
|
+
if (replica !== undefined)
|
|
4560
|
+
replicas.set(room.locator, { ...replica, record: { ...replica.record, model: next } });
|
|
4561
|
+
heldModel(room, next);
|
|
4292
4562
|
}
|
|
4563
|
+
if (unchanged.length > 0)
|
|
4564
|
+
emitRooms();
|
|
4565
|
+
return unchanged;
|
|
4566
|
+
});
|
|
4567
|
+
for (const room of held) {
|
|
4568
|
+
if (settled.includes(room) || !owed(room))
|
|
4569
|
+
continue;
|
|
4570
|
+
await retainModel(local, room, { ...modelOf(room.locator), status });
|
|
4293
4571
|
}
|
|
4294
4572
|
}
|
|
4295
4573
|
function submitLater(locator) {
|
|
@@ -4402,8 +4680,6 @@ export function composeRuntime(options) {
|
|
|
4402
4680
|
if (repairable.has(locator))
|
|
4403
4681
|
repairLater(locator, {});
|
|
4404
4682
|
await conversations.reconcile();
|
|
4405
|
-
if (status.providerUnreachable)
|
|
4406
|
-
conversations.reconnected();
|
|
4407
4683
|
const reading = { complete: true, reachable: true };
|
|
4408
4684
|
await conversations.catchUp(reading);
|
|
4409
4685
|
if (progress.reachable && reading.reachable)
|
|
@@ -4511,6 +4787,7 @@ export function composeRuntime(options) {
|
|
|
4511
4787
|
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
4512
4788
|
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
4513
4789
|
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
4790
|
+
ahead,
|
|
4514
4791
|
};
|
|
4515
4792
|
function observeStream() {
|
|
4516
4793
|
const stream = options.adapter.roomStream;
|
|
@@ -4523,6 +4800,8 @@ export function composeRuntime(options) {
|
|
|
4523
4800
|
socketOpen = true;
|
|
4524
4801
|
connectionLive();
|
|
4525
4802
|
generation += 1;
|
|
4803
|
+
for (const admit of installWaiters.splice(0))
|
|
4804
|
+
admit();
|
|
4526
4805
|
reads.opened();
|
|
4527
4806
|
conversations.reopened();
|
|
4528
4807
|
recovery.resume();
|
|
@@ -4542,7 +4821,7 @@ export function composeRuntime(options) {
|
|
|
4542
4821
|
const held = replicas.get(locator);
|
|
4543
4822
|
if (held === undefined || paths.length === 0)
|
|
4544
4823
|
return new Map();
|
|
4545
|
-
const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
|
|
4824
|
+
const entries = await ahead(async () => (await openLocal()).entries(held.record.key, 'confirmed', paths));
|
|
4546
4825
|
return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
|
|
4547
4826
|
}
|
|
4548
4827
|
function writesHeld() {
|
|
@@ -4613,6 +4892,7 @@ export function composeRuntime(options) {
|
|
|
4613
4892
|
if (closed)
|
|
4614
4893
|
return;
|
|
4615
4894
|
closed = true;
|
|
4895
|
+
notifyDiscovered();
|
|
4616
4896
|
if (probing !== null)
|
|
4617
4897
|
clearInterval(probing);
|
|
4618
4898
|
for (const resolve of shownWaiters.splice(0))
|
|
@@ -4628,6 +4908,7 @@ export function composeRuntime(options) {
|
|
|
4628
4908
|
conversations.close();
|
|
4629
4909
|
void recovery.retire();
|
|
4630
4910
|
roomListeners.clear();
|
|
4911
|
+
unlistedRooms.clear();
|
|
4631
4912
|
refusalListeners.clear();
|
|
4632
4913
|
storageLossListeners.clear();
|
|
4633
4914
|
statusListeners.clear();
|
|
@@ -4653,9 +4934,9 @@ export function composeRuntime(options) {
|
|
|
4653
4934
|
observeStream();
|
|
4654
4935
|
void openLocal().then(() => {
|
|
4655
4936
|
if (capabilities.length > 0)
|
|
4656
|
-
|
|
4937
|
+
readyFromStore();
|
|
4657
4938
|
}, () => {
|
|
4658
|
-
|
|
4939
|
+
readyFromStore();
|
|
4659
4940
|
});
|
|
4660
4941
|
if (options.adapter.roomStream === undefined)
|
|
4661
4942
|
void sync('unstreamed');
|
|
@@ -4806,9 +5087,10 @@ export function composeRuntime(options) {
|
|
|
4806
5087
|
return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
|
|
4807
5088
|
}
|
|
4808
5089
|
if (topic === 'rooms') {
|
|
4809
|
-
|
|
4810
|
-
|
|
5090
|
+
unlistedRooms.add(deliver);
|
|
5091
|
+
emitKnownRooms();
|
|
4811
5092
|
return () => {
|
|
5093
|
+
unlistedRooms.delete(deliver);
|
|
4812
5094
|
roomListeners.delete(deliver);
|
|
4813
5095
|
};
|
|
4814
5096
|
}
|
|
@@ -4862,7 +5144,17 @@ export function composeRuntime(options) {
|
|
|
4862
5144
|
return openLocal().then(() => undefined);
|
|
4863
5145
|
},
|
|
4864
5146
|
rooms() {
|
|
4865
|
-
|
|
5147
|
+
if (closed)
|
|
5148
|
+
return Promise.resolve([]);
|
|
5149
|
+
return new Promise((settle) => {
|
|
5150
|
+
const answer = (value) => {
|
|
5151
|
+
unlistedRooms.delete(answer);
|
|
5152
|
+
roomListeners.delete(answer);
|
|
5153
|
+
settle(value);
|
|
5154
|
+
};
|
|
5155
|
+
unlistedRooms.add(answer);
|
|
5156
|
+
emitKnownRooms();
|
|
5157
|
+
});
|
|
4866
5158
|
},
|
|
4867
5159
|
query: ((input) => rowsQuery(planned(input, null))),
|
|
4868
5160
|
search: (input) => searched(input, null, []),
|
|
@@ -4872,7 +5164,7 @@ export function composeRuntime(options) {
|
|
|
4872
5164
|
typedSelection: rooms.get(locator)?.typedSelection === undefined ||
|
|
4873
5165
|
rooms.get(locator)?.typedSelection === null
|
|
4874
5166
|
? null
|
|
4875
|
-
: modelTypes(
|
|
5167
|
+
: modelTypes(builtIn),
|
|
4876
5168
|
};
|
|
4877
5169
|
},
|
|
4878
5170
|
roomDraft: {
|
|
@@ -4951,6 +5243,8 @@ export function composeRuntime(options) {
|
|
|
4951
5243
|
part: readPart,
|
|
4952
5244
|
read: readBinaryPath,
|
|
4953
5245
|
link: linkBinaryPath,
|
|
5246
|
+
show: showBinaryPath,
|
|
5247
|
+
attach: attachBinary,
|
|
4954
5248
|
},
|
|
4955
5249
|
async call(name, input) {
|
|
4956
5250
|
await ready;
|
|
@@ -5005,6 +5299,7 @@ export function composeRuntime(options) {
|
|
|
5005
5299
|
lockedRooms,
|
|
5006
5300
|
watchLocks,
|
|
5007
5301
|
located,
|
|
5302
|
+
room: (locator) => listed().find((one) => one.locator === locator),
|
|
5008
5303
|
};
|
|
5009
5304
|
return {
|
|
5010
5305
|
md,
|