@agentshouse/mdruntime 0.1.0-cee9c8aa4cd86 → 0.1.0-cefbc8bb8a1c0
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 +31 -18
- 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/binary.d.ts +0 -5
- package/dist/binary.d.ts.map +1 -1
- package/dist/binary.js +1 -1
- package/dist/bootstrap.d.ts +15 -4
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +140 -90
- package/dist/client.d.ts +6 -7
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +54 -7
- 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 +85 -30
- package/dist/connection.d.ts +9 -7
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +961 -476
- package/dist/conversation.d.ts +7 -5
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +166 -41
- package/dist/durability.d.ts +2 -5
- package/dist/durability.d.ts.map +1 -1
- package/dist/durability.js +5 -0
- 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 +4 -3
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +43 -15
- package/dist/index.d.ts +9 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- 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 +218 -0
- package/dist/memory.d.ts +2 -2
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +46 -19
- 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 -3
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +16 -5
- package/dist/overlay.js +1 -1
- 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 +9 -0
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +12 -1
- package/dist/reach.d.ts +8 -3
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +68 -45
- package/dist/readiness.d.ts +17 -7
- package/dist/readiness.d.ts.map +1 -1
- package/dist/readiness.js +6 -6
- package/dist/reads.d.ts.map +1 -1
- package/dist/reads.js +10 -17
- package/dist/recovery.d.ts +1 -0
- package/dist/recovery.d.ts.map +1 -1
- package/dist/recovery.js +26 -0
- package/dist/refresh.d.ts +6 -2
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +61 -27
- package/dist/replica.d.ts +5 -4
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +4 -3
- package/dist/room-draft.d.ts +18 -2
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +73 -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 +13 -7
- 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 +18 -12
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +25 -20
- package/dist/stored.d.ts +10 -13
- package/dist/stored.d.ts.map +1 -1
- package/dist/stored.js +210 -174
- package/dist/tables.d.ts +26 -6
- package/dist/tables.d.ts.map +1 -1
- package/dist/tables.js +57 -6
- package/dist/wire.d.ts +5 -5
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +2 -2
- package/dist/words.d.ts.map +1 -1
- package/dist/words.js +8 -2
- package/package.json +11 -18
- package/AGENTS.md +0 -320
- 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 -21
- package/dist/chat/markdown.d.ts.map +0 -1
- package/dist/chat/markdown.js +0 -246
- 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,27 +12,27 @@ 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,
|
|
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, 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
|
-
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
29
|
-
import { readinessOf,
|
|
29
|
+
import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
|
|
30
|
+
import { readinessOf, } from './readiness.js';
|
|
30
31
|
import { FencedMap } from './fence.js';
|
|
31
|
-
import { fingerprint } from './stored.js';
|
|
32
32
|
import { sameBinary } from './overlay.js';
|
|
33
33
|
import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
|
|
34
34
|
import { identityTime, roomDraftRevision } from './identity.js';
|
|
35
|
-
import { beneath, normalize } from './paths.js';
|
|
35
|
+
import { ancestors, beneath, normalize } from './paths.js';
|
|
36
36
|
import { openRuntimeStore, } from './store.js';
|
|
37
37
|
import { changedCleanly, sameComposition, validatedCollection, } from './validation.js';
|
|
38
38
|
import { unconfirmed } from './transfer.js';
|
|
@@ -55,10 +55,23 @@ const CONVERSATION_LANE = 'conversations';
|
|
|
55
55
|
const ROOM_READS = 256;
|
|
56
56
|
const ROOM_ATTEMPTS = 16;
|
|
57
57
|
const ERASE_BATCH = 512;
|
|
58
|
-
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, its full-text writes included, 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;
|
|
59
64
|
const WALK_BYTES = 1 << 20;
|
|
60
65
|
const QUERY_BYTES = 1 << 20;
|
|
61
66
|
const STAGE_BYTES = 1 << 20;
|
|
67
|
+
const STAGE_FLOOR = 1 << 16;
|
|
68
|
+
// A batch aims at this much database work, so a Save or a query waits behind at most one short write.
|
|
69
|
+
const STAGE_AIM_MS = 50;
|
|
70
|
+
const STREAMS_HELD = 4;
|
|
71
|
+
const NOTHING_HELD = { parts: 0, bytes: 0 };
|
|
72
|
+
const QUEUED_TEXT = 1 << 23;
|
|
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.
|
|
74
|
+
const ARRIVAL_REPORT_MS = 250;
|
|
62
75
|
const CLEAN_CHANGES = 2_000;
|
|
63
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.
|
|
64
77
|
const LOCK_GRACE_MS = 120_000;
|
|
@@ -82,7 +95,7 @@ function pendingKey(locator, id) {
|
|
|
82
95
|
}
|
|
83
96
|
export function composeRuntime(options) {
|
|
84
97
|
const localSaves = options.adapter.localSaves === true;
|
|
85
|
-
const roomTypes = roomTypesOf(options.adapter.roomTypes);
|
|
98
|
+
const roomTypes = roomTypesOf(options.adapter.roomTypes, options.adapter.nameableRoomTypes);
|
|
86
99
|
let authorization = options.authorization;
|
|
87
100
|
let opening;
|
|
88
101
|
const fences = new Map();
|
|
@@ -92,20 +105,23 @@ export function composeRuntime(options) {
|
|
|
92
105
|
const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
|
|
93
106
|
let discoveries = 0;
|
|
94
107
|
const replicas = new FencedMap(isFenced);
|
|
108
|
+
const reopened = new Set();
|
|
109
|
+
const usableWhenHeld = options.usableWhenHeld ?? [];
|
|
95
110
|
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
96
111
|
const pendings = new Map();
|
|
97
112
|
const seeds = new Map();
|
|
98
113
|
const seedWatchers = new Map();
|
|
99
114
|
const roomDrafts = new FencedMap(isFenced);
|
|
115
|
+
const attachedFiles = new FencedMap(isFenced);
|
|
100
116
|
const presentations = new Map();
|
|
101
117
|
let limits = [];
|
|
102
118
|
let capabilities = [];
|
|
103
119
|
const rooms = new FencedMap(isFenced);
|
|
104
120
|
const counted = new Map();
|
|
105
121
|
const unreadable = new Set();
|
|
106
|
-
const
|
|
107
|
-
const models = new FencedMap(
|
|
108
|
-
const liveModels = new FencedMap(
|
|
122
|
+
const builtIn = builtInModel(roomTypes);
|
|
123
|
+
const models = new FencedMap(isFenced);
|
|
124
|
+
const liveModels = new FencedMap(isFenced);
|
|
109
125
|
const invalidModels = new Set();
|
|
110
126
|
const discovered = new Map();
|
|
111
127
|
const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
|
|
@@ -113,23 +129,19 @@ export function composeRuntime(options) {
|
|
|
113
129
|
const scopedRefusals = new Map();
|
|
114
130
|
const sourceRefusals = new Map();
|
|
115
131
|
const declarationRefusals = new Set();
|
|
116
|
-
function
|
|
117
|
-
return
|
|
132
|
+
function modelOf(locator) {
|
|
133
|
+
return models.get(locator) ?? NO_MODEL;
|
|
118
134
|
}
|
|
119
|
-
function
|
|
120
|
-
return
|
|
121
|
-
}
|
|
122
|
-
function savedModel(locator, attached) {
|
|
123
|
-
return invalidModels.has(modelKey(locator, attached))
|
|
135
|
+
function savedModel(locator) {
|
|
136
|
+
return invalidModels.has(locator)
|
|
124
137
|
? { status: 'unavailable', reason: 'revoked' }
|
|
125
|
-
: modelOf(locator
|
|
138
|
+
: modelOf(locator);
|
|
139
|
+
}
|
|
140
|
+
function composed(record) {
|
|
141
|
+
return compositionOf(record, builtIn);
|
|
126
142
|
}
|
|
127
143
|
function compositionFor(record) {
|
|
128
|
-
return
|
|
129
|
-
...record,
|
|
130
|
-
model: savedModel(record.locator, 'collection'),
|
|
131
|
-
selectionModel: savedModel(record.locator, 'typed-selection'),
|
|
132
|
-
});
|
|
144
|
+
return composed({ ...record, model: savedModel(record.locator) });
|
|
133
145
|
}
|
|
134
146
|
let refusal = null;
|
|
135
147
|
let storageLoss = null;
|
|
@@ -161,7 +173,9 @@ export function composeRuntime(options) {
|
|
|
161
173
|
let roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
162
174
|
const working = new Set();
|
|
163
175
|
let mutations = Promise.resolve();
|
|
176
|
+
let gathering = null;
|
|
164
177
|
const roomListeners = new Set();
|
|
178
|
+
const unlistedRooms = new Set();
|
|
165
179
|
const refusalListeners = new Set();
|
|
166
180
|
const storageLossListeners = new Set();
|
|
167
181
|
const statusListeners = new Set();
|
|
@@ -181,19 +195,55 @@ export function composeRuntime(options) {
|
|
|
181
195
|
const serviceModeListeners = new Set();
|
|
182
196
|
let member = null;
|
|
183
197
|
const memberListeners = new Set();
|
|
184
|
-
const prepared = new Set();
|
|
185
198
|
const unpreparedDemand = new Set();
|
|
186
199
|
const unpreparable = new Set();
|
|
187
200
|
let preparing = false;
|
|
188
201
|
let targets = {};
|
|
189
202
|
let persistence = options.shell.persistence;
|
|
190
|
-
let
|
|
191
|
-
|
|
192
|
-
let
|
|
203
|
+
let full = false;
|
|
204
|
+
let probing = null;
|
|
205
|
+
let testing = null;
|
|
206
|
+
const database = {
|
|
207
|
+
async ask(request) {
|
|
208
|
+
try {
|
|
209
|
+
return await options.database.ask(request);
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
if (refusedForSpace(error))
|
|
213
|
+
filled();
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
};
|
|
193
218
|
const readinessWatches = new Set();
|
|
194
219
|
let readinessQueued = false;
|
|
195
220
|
const catching = new Set();
|
|
196
221
|
const transfers = new FencedMap(isFenced);
|
|
222
|
+
const installs = new Set();
|
|
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();
|
|
228
|
+
let arrivalReported = 0;
|
|
229
|
+
let onScreen = options.shell.onScreen;
|
|
230
|
+
const shownWaiters = [];
|
|
231
|
+
let streams = { generation, free: STREAMS_HELD };
|
|
232
|
+
let queuedText = 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: [] };
|
|
197
247
|
const conversations = createConversationStore({
|
|
198
248
|
agents: declaredAgents,
|
|
199
249
|
documents: (declaration, after) => rowsQuery(planOf({
|
|
@@ -214,16 +264,27 @@ export function composeRuntime(options) {
|
|
|
214
264
|
stream: () => options.adapter.conversations,
|
|
215
265
|
publish: publishChangeSet,
|
|
216
266
|
online: () => connectionOf() !== 'unreachable' && refusal === null,
|
|
267
|
+
listed: () => roomsListed,
|
|
268
|
+
full: () => full,
|
|
217
269
|
closed: () => closed,
|
|
218
270
|
unanswered: resendLater,
|
|
219
271
|
expired: (operation) => pastHorizon(operation),
|
|
220
272
|
});
|
|
221
273
|
const journal = createLog();
|
|
222
|
-
const keptFiles = openedFiles(
|
|
223
|
-
let
|
|
274
|
+
const keptFiles = openedFiles(database, isFenced);
|
|
275
|
+
let readyFromStore = () => { };
|
|
224
276
|
const ready = new Promise((resolve) => {
|
|
225
|
-
|
|
277
|
+
readyFromStore = resolve;
|
|
226
278
|
});
|
|
279
|
+
let notifyDiscovered = () => { };
|
|
280
|
+
const firstDiscovery = new Promise((resolve) => {
|
|
281
|
+
notifyDiscovered = resolve;
|
|
282
|
+
});
|
|
283
|
+
let roomsNamed = false;
|
|
284
|
+
function notifyReady() {
|
|
285
|
+
notifyDiscovered();
|
|
286
|
+
readyFromStore();
|
|
287
|
+
}
|
|
227
288
|
function rememberRoomDraft(record) {
|
|
228
289
|
const room = roomDrafts.get(record.locator) ?? new Map();
|
|
229
290
|
room.set(record.path, record);
|
|
@@ -243,7 +304,7 @@ export function composeRuntime(options) {
|
|
|
243
304
|
return undefined;
|
|
244
305
|
const local = await openLocal();
|
|
245
306
|
const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
|
|
246
|
-
return { ...slice, collection: collectionOf(slice.documents,
|
|
307
|
+
return { ...slice, collection: collectionOf(slice.documents, composed(held.record)) };
|
|
247
308
|
}
|
|
248
309
|
function roomDraftOf(record, visible) {
|
|
249
310
|
return structuredClone({
|
|
@@ -299,14 +360,40 @@ export function composeRuntime(options) {
|
|
|
299
360
|
const value = presentations.get(app);
|
|
300
361
|
return value === undefined ? null : structuredClone(value);
|
|
301
362
|
}
|
|
302
|
-
function
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
363
|
+
async function writtenRoomDraft(room, path, revision, input, next) {
|
|
364
|
+
const parts = readParts(input);
|
|
365
|
+
if (path === '' || normalize(path) !== path)
|
|
366
|
+
throw runtimeError('room-draft-malformed');
|
|
367
|
+
const local = await openLocal();
|
|
368
|
+
const current = roomDrafts.get(room)?.get(path);
|
|
369
|
+
if ((current?.revision ?? null) !== revision) {
|
|
370
|
+
return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
|
|
371
|
+
}
|
|
372
|
+
const file = parts.file === undefined
|
|
373
|
+
? undefined
|
|
374
|
+
: await heldFile(parts.file.blob, parts.file.base === undefined
|
|
375
|
+
? binaryBase(await visibleOf(room, [path]), path)
|
|
376
|
+
: parts.file.base);
|
|
377
|
+
const record = {
|
|
378
|
+
locator: room,
|
|
379
|
+
path,
|
|
380
|
+
revision: next,
|
|
381
|
+
parts: held(current?.parts, parts, file),
|
|
382
|
+
returned: current?.returned ?? null,
|
|
383
|
+
};
|
|
384
|
+
await local.keepRoomDraft(record, parts.file === undefined || file === undefined
|
|
385
|
+
? []
|
|
386
|
+
: [{ key: file.localId, blob: parts.file.blob }]);
|
|
387
|
+
rememberRoomDraft(record);
|
|
388
|
+
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
389
|
+
return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
|
|
390
|
+
}
|
|
391
|
+
async function keptPresentation(app, next) {
|
|
392
|
+
const local = await openLocal();
|
|
393
|
+
const stored = next(presentations.get(app));
|
|
394
|
+
await local.present({ app, value: stored });
|
|
395
|
+
presentations.set(app, stored);
|
|
396
|
+
publishChangeSet(changeSet('', { presentation: app }));
|
|
310
397
|
}
|
|
311
398
|
function emitPresentation(app) {
|
|
312
399
|
if (closed)
|
|
@@ -321,6 +408,20 @@ export function composeRuntime(options) {
|
|
|
321
408
|
(queuesWithoutReplica.size > 0 ||
|
|
322
409
|
[...replicas.values()].some((replica) => replica.queue.length > 0)));
|
|
323
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
|
+
}
|
|
324
425
|
function attention() {
|
|
325
426
|
const attentionRequired = [];
|
|
326
427
|
for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
|
|
@@ -353,7 +454,7 @@ export function composeRuntime(options) {
|
|
|
353
454
|
if (reset === null || submission.reset < reset)
|
|
354
455
|
reset = submission.reset;
|
|
355
456
|
}
|
|
356
|
-
return { attentionRequired, reset };
|
|
457
|
+
return { attentionRequired, unsent: unsent(), reset };
|
|
357
458
|
}
|
|
358
459
|
function documentsIn(locator) {
|
|
359
460
|
return replicas.get(locator)?.count ?? 0;
|
|
@@ -426,7 +527,7 @@ export function composeRuntime(options) {
|
|
|
426
527
|
const governed = governs(reach, room);
|
|
427
528
|
const attached = [
|
|
428
529
|
...(governed.collection ? [modelOf(room.locator)] : []),
|
|
429
|
-
...(governed.selection ? [
|
|
530
|
+
...(governed.selection ? [builtIn] : []),
|
|
430
531
|
];
|
|
431
532
|
return {
|
|
432
533
|
locator: room.locator,
|
|
@@ -442,11 +543,26 @@ export function composeRuntime(options) {
|
|
|
442
543
|
const value = listed();
|
|
443
544
|
for (const listener of roomListeners)
|
|
444
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
|
+
}
|
|
445
560
|
}
|
|
446
561
|
function emitRefusal() {
|
|
447
562
|
queueReadiness();
|
|
448
563
|
for (const listener of refusalListeners)
|
|
449
564
|
listener(refusal);
|
|
565
|
+
emitKnownRooms();
|
|
450
566
|
}
|
|
451
567
|
function emitStorageLoss() {
|
|
452
568
|
queueReadiness();
|
|
@@ -467,6 +583,7 @@ export function composeRuntime(options) {
|
|
|
467
583
|
if (closed)
|
|
468
584
|
return;
|
|
469
585
|
queueReadiness();
|
|
586
|
+
emitKnownRooms();
|
|
470
587
|
const next = status.state === 'synced' && !conversations.current()
|
|
471
588
|
? { ...status, state: 'syncing' }
|
|
472
589
|
: status;
|
|
@@ -501,18 +618,25 @@ export function composeRuntime(options) {
|
|
|
501
618
|
function unpublished(error) {
|
|
502
619
|
journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
|
|
503
620
|
}
|
|
621
|
+
function indexed(locator) {
|
|
622
|
+
return replicas.get(locator)?.indexed === true;
|
|
623
|
+
}
|
|
504
624
|
async function prepareReplica(locator) {
|
|
505
625
|
for (;;) {
|
|
506
626
|
const held = replicas.get(locator);
|
|
507
|
-
if (held === undefined || closed ||
|
|
627
|
+
if (held === undefined || closed || held.indexed)
|
|
508
628
|
return;
|
|
509
629
|
const key = held.record.key;
|
|
510
|
-
const done = await exclusive(async () =>
|
|
511
|
-
|
|
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
|
+
});
|
|
634
|
+
const now = replicas.get(locator);
|
|
635
|
+
if (now?.record.key !== key)
|
|
512
636
|
continue;
|
|
513
|
-
if (!done)
|
|
637
|
+
if (!done.done)
|
|
514
638
|
continue;
|
|
515
|
-
|
|
639
|
+
replicas.set(locator, { ...now, indexed: true });
|
|
516
640
|
publishChangeSet(changeSet(locator, { documents: null }));
|
|
517
641
|
liveQueries.recover();
|
|
518
642
|
queueReadiness();
|
|
@@ -520,7 +644,7 @@ export function composeRuntime(options) {
|
|
|
520
644
|
}
|
|
521
645
|
}
|
|
522
646
|
function unprepared() {
|
|
523
|
-
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !
|
|
647
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
|
|
524
648
|
}
|
|
525
649
|
function unpreparedBy(locator, error) {
|
|
526
650
|
unpreparable.add(locator);
|
|
@@ -549,7 +673,11 @@ export function composeRuntime(options) {
|
|
|
549
673
|
})();
|
|
550
674
|
}
|
|
551
675
|
function roomsOfReach(reach) {
|
|
552
|
-
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
|
+
]);
|
|
553
681
|
}
|
|
554
682
|
function reachedRooms(plan) {
|
|
555
683
|
const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
|
|
@@ -581,7 +709,7 @@ export function composeRuntime(options) {
|
|
|
581
709
|
pending.push({ room: locator, awaits: 'acquisition' });
|
|
582
710
|
continue;
|
|
583
711
|
}
|
|
584
|
-
if (!
|
|
712
|
+
if (!replica.indexed) {
|
|
585
713
|
pending.push({ room: locator, awaits: 'preparation' });
|
|
586
714
|
continue;
|
|
587
715
|
}
|
|
@@ -625,7 +753,7 @@ export function composeRuntime(options) {
|
|
|
625
753
|
return 'unadmitted';
|
|
626
754
|
if (!replicas.has(locator))
|
|
627
755
|
return 'acquiring';
|
|
628
|
-
if (!
|
|
756
|
+
if (!indexed(locator))
|
|
629
757
|
return 'preparing';
|
|
630
758
|
return 'complete';
|
|
631
759
|
}
|
|
@@ -655,6 +783,7 @@ export function composeRuntime(options) {
|
|
|
655
783
|
return [
|
|
656
784
|
{
|
|
657
785
|
room,
|
|
786
|
+
name: discovered.get(room)?.name ?? room,
|
|
658
787
|
file: typeof file === 'string' ? file : '',
|
|
659
788
|
rule: held.code,
|
|
660
789
|
location: typeof location === 'string' ? location : null,
|
|
@@ -662,6 +791,59 @@ export function composeRuntime(options) {
|
|
|
662
791
|
];
|
|
663
792
|
});
|
|
664
793
|
}
|
|
794
|
+
function installationOf(coverage) {
|
|
795
|
+
const rooms = { done: 0, total: 0 };
|
|
796
|
+
const parts = { done: 0, total: 0 };
|
|
797
|
+
const bytes = { done: 0, total: 0 };
|
|
798
|
+
let installing = 0;
|
|
799
|
+
let downloading = false;
|
|
800
|
+
let checking = 0;
|
|
801
|
+
for (const { room, state } of coverage) {
|
|
802
|
+
if (state === 'unadmitted')
|
|
803
|
+
continue;
|
|
804
|
+
rooms.total += 1;
|
|
805
|
+
if (state === 'complete')
|
|
806
|
+
rooms.done += 1;
|
|
807
|
+
const transfer = transfers.get(room);
|
|
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;
|
|
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;
|
|
831
|
+
}
|
|
832
|
+
const step = installing === 0
|
|
833
|
+
? null
|
|
834
|
+
: downloading
|
|
835
|
+
? 'downloading'
|
|
836
|
+
: bytes.done + checking < bytes.total
|
|
837
|
+
? 'writing'
|
|
838
|
+
: 'replaying';
|
|
839
|
+
return {
|
|
840
|
+
step,
|
|
841
|
+
paused: installing > 0 && !onScreen ? 'hidden' : null,
|
|
842
|
+
rooms,
|
|
843
|
+
parts,
|
|
844
|
+
bytes,
|
|
845
|
+
};
|
|
846
|
+
}
|
|
665
847
|
function readinessFor(port, reach) {
|
|
666
848
|
const required = requiredRooms(reach);
|
|
667
849
|
const search = port.search === 'required';
|
|
@@ -682,18 +864,18 @@ export function composeRuntime(options) {
|
|
|
682
864
|
persistence,
|
|
683
865
|
connection: connectionOf(),
|
|
684
866
|
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
? capacity
|
|
688
|
-
: port.capacity,
|
|
867
|
+
space: full ? 'full' : port.space,
|
|
868
|
+
installation: installationOf(coverage),
|
|
689
869
|
refused: refusedIn(required.rooms),
|
|
690
870
|
lost: storageLoss !== null,
|
|
691
|
-
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))),
|
|
692
873
|
});
|
|
693
874
|
}
|
|
694
875
|
function establish(name) {
|
|
695
876
|
targets = { ...targets, [name]: { established: true } };
|
|
696
877
|
queueReadiness();
|
|
878
|
+
probeEachMinute();
|
|
697
879
|
void durably((local) => local.fact('targets', targets)).catch(() => {
|
|
698
880
|
journal.record(`target record failed ${name}`);
|
|
699
881
|
});
|
|
@@ -731,87 +913,52 @@ export function composeRuntime(options) {
|
|
|
731
913
|
readinessWatches.delete(watch);
|
|
732
914
|
};
|
|
733
915
|
}
|
|
734
|
-
function
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
}
|
|
740
|
-
function exceeded(error) {
|
|
741
|
-
return (typeof error === 'object' &&
|
|
742
|
-
error !== null &&
|
|
743
|
-
error.name === 'QuotaExceededError');
|
|
744
|
-
}
|
|
745
|
-
async function estimated() {
|
|
746
|
-
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
747
|
-
return estimate === null ? null : Math.max(0, estimate.total - estimate.usage);
|
|
748
|
-
}
|
|
749
|
-
function pause(locator, cause) {
|
|
750
|
-
paused.add(locator);
|
|
751
|
-
capacity = cause;
|
|
752
|
-
journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
|
|
916
|
+
function filled() {
|
|
917
|
+
if (full || closed)
|
|
918
|
+
return;
|
|
919
|
+
full = true;
|
|
920
|
+
journal.record('space full');
|
|
753
921
|
queueReadiness();
|
|
754
|
-
|
|
922
|
+
probeEachMinute();
|
|
923
|
+
}
|
|
924
|
+
function installedTarget() {
|
|
925
|
+
return ((usableWhenHeld.length > 0 && reopened.size > 0) ||
|
|
926
|
+
Object.values(targets).some((target) => target.established));
|
|
927
|
+
}
|
|
928
|
+
function probeEachMinute() {
|
|
929
|
+
if (!full || probing !== null || closed || !installedTarget())
|
|
755
930
|
return;
|
|
756
|
-
// A
|
|
757
|
-
|
|
758
|
-
void
|
|
931
|
+
// A device whose storage filled after its install tries a write each minute, so space freed elsewhere ends its read-only mode without a Retry.
|
|
932
|
+
probing = setInterval(() => {
|
|
933
|
+
void probed();
|
|
759
934
|
}, 60_000);
|
|
760
935
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
resume();
|
|
936
|
+
function probed() {
|
|
937
|
+
testing ??= database.ask({ kind: 'space.probe' }).then(() => {
|
|
938
|
+
testing = null;
|
|
939
|
+
spaceFreed();
|
|
940
|
+
return true;
|
|
941
|
+
}, () => {
|
|
942
|
+
testing = null;
|
|
943
|
+
return false;
|
|
944
|
+
});
|
|
945
|
+
return testing;
|
|
772
946
|
}
|
|
773
|
-
function
|
|
774
|
-
if (
|
|
775
|
-
clearInterval(rechecking);
|
|
776
|
-
rechecking = null;
|
|
777
|
-
if (paused.size === 0 && capacity === null)
|
|
947
|
+
function spaceFreed() {
|
|
948
|
+
if (!full)
|
|
778
949
|
return;
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
950
|
+
full = false;
|
|
951
|
+
if (probing !== null)
|
|
952
|
+
clearInterval(probing);
|
|
953
|
+
probing = null;
|
|
954
|
+
unpreparable.clear();
|
|
955
|
+
journal.record('space freed');
|
|
782
956
|
queueReadiness();
|
|
783
957
|
if (!closed && authorization !== undefined)
|
|
784
958
|
void sync('unstreamed');
|
|
785
959
|
}
|
|
786
|
-
async function reclaimed(local) {
|
|
787
|
-
const unreferenced = [...transfers.keys()].filter((locator) => !discovered.has(locator));
|
|
788
|
-
for (const locator of unreferenced) {
|
|
789
|
-
await local.abandon(locator);
|
|
790
|
-
transfers.delete(locator);
|
|
791
|
-
}
|
|
792
|
-
if (unreferenced.length > 0)
|
|
793
|
-
journal.record(`capacity reclaimed ${unreferenced.length}`);
|
|
794
|
-
return unreferenced.length > 0;
|
|
795
|
-
}
|
|
796
|
-
async function admitted(local, locator, needed) {
|
|
797
|
-
if (paused.has(locator))
|
|
798
|
-
return false;
|
|
799
|
-
let available = await estimated().catch(() => null);
|
|
800
|
-
if (available === null || available >= needed)
|
|
801
|
-
return true;
|
|
802
|
-
if (await reclaimed(local))
|
|
803
|
-
available = await estimated().catch(() => null);
|
|
804
|
-
if (available === null || available >= needed)
|
|
805
|
-
return true;
|
|
806
|
-
pause(locator, { cause: 'insufficient-space', needed, available });
|
|
807
|
-
return false;
|
|
808
|
-
}
|
|
809
|
-
async function exceededOn(locator, bytes) {
|
|
810
|
-
const available = await estimated().catch(() => null);
|
|
811
|
-
pause(locator, { cause: 'storage-exceeded', needed: bytes, available });
|
|
812
|
-
}
|
|
813
960
|
function seedRoomHeld(room, version) {
|
|
814
|
-
return
|
|
961
|
+
return indexed(room) && reachedBy(room, version);
|
|
815
962
|
}
|
|
816
963
|
function seedHeld(at) {
|
|
817
964
|
return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
|
|
@@ -934,6 +1081,10 @@ export function composeRuntime(options) {
|
|
|
934
1081
|
}
|
|
935
1082
|
function publishChangeSet(set) {
|
|
936
1083
|
applyChangeSet(set);
|
|
1084
|
+
for (const { locator, path } of set.documents) {
|
|
1085
|
+
if (path !== null)
|
|
1086
|
+
attachedFiles.get(locator)?.delete(path);
|
|
1087
|
+
}
|
|
937
1088
|
if (set.rooms.length > 0 ||
|
|
938
1089
|
[...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
|
|
939
1090
|
emitRooms();
|
|
@@ -959,27 +1110,24 @@ export function composeRuntime(options) {
|
|
|
959
1110
|
const records = [...replicas].map(([locator, held]) => ({
|
|
960
1111
|
locator,
|
|
961
1112
|
held,
|
|
962
|
-
record: { ...held.record, model: revoked
|
|
1113
|
+
record: { ...held.record, model: revoked },
|
|
963
1114
|
}));
|
|
1115
|
+
let answer = null;
|
|
964
1116
|
try {
|
|
965
1117
|
const local = await openLocal();
|
|
966
|
-
await local.commit({
|
|
1118
|
+
answer = await local.commit({
|
|
967
1119
|
rooms: records.map(({ locator, record }) => ({
|
|
968
1120
|
locator,
|
|
969
1121
|
key: record.key,
|
|
970
1122
|
record: storedRecord(record),
|
|
971
|
-
|
|
1123
|
+
composed: composedOf(composed(record)),
|
|
972
1124
|
})),
|
|
973
1125
|
});
|
|
974
1126
|
}
|
|
975
1127
|
catch {
|
|
976
1128
|
journal.record('model revocation persistence failed');
|
|
977
1129
|
}
|
|
978
|
-
const keys = new Set(models.keys());
|
|
979
|
-
for (const locator of rooms.keys()) {
|
|
980
|
-
keys.add(modelKey(locator, 'collection'));
|
|
981
|
-
keys.add(modelKey(locator, 'typed-selection'));
|
|
982
|
-
}
|
|
1130
|
+
const keys = new Set([...models.keys(), ...rooms.keys()]);
|
|
983
1131
|
for (const key of keys) {
|
|
984
1132
|
invalidModels.add(key);
|
|
985
1133
|
models.set(key, revoked);
|
|
@@ -989,8 +1137,8 @@ export function composeRuntime(options) {
|
|
|
989
1137
|
for (const { locator, held, record } of records) {
|
|
990
1138
|
if (replicas.get(locator) !== held)
|
|
991
1139
|
continue;
|
|
992
|
-
|
|
993
|
-
replicas.set(locator, { ...held, record });
|
|
1140
|
+
const indexed = answer === null ? held.indexed : committedRoom(locator, answer).indexed;
|
|
1141
|
+
replicas.set(locator, { ...held, record, indexed });
|
|
994
1142
|
}
|
|
995
1143
|
for (const locator of [...rooms.keys()]) {
|
|
996
1144
|
const room = rooms.get(locator);
|
|
@@ -1034,7 +1182,7 @@ export function composeRuntime(options) {
|
|
|
1034
1182
|
? null
|
|
1035
1183
|
: {
|
|
1036
1184
|
roots: selection.roots,
|
|
1037
|
-
model: modelTypes(
|
|
1185
|
+
model: modelTypes(builtIn),
|
|
1038
1186
|
},
|
|
1039
1187
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
1040
1188
|
roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
|
|
@@ -1123,7 +1271,8 @@ export function composeRuntime(options) {
|
|
|
1123
1271
|
queuesWithoutReplica.delete(locator);
|
|
1124
1272
|
rooms.delete(locator);
|
|
1125
1273
|
roomDrafts.delete(locator);
|
|
1126
|
-
|
|
1274
|
+
attachedFiles.delete(locator);
|
|
1275
|
+
untransferred(locator);
|
|
1127
1276
|
catching.delete(locator);
|
|
1128
1277
|
discovered.delete(locator);
|
|
1129
1278
|
announced.delete(locator);
|
|
@@ -1131,17 +1280,12 @@ export function composeRuntime(options) {
|
|
|
1131
1280
|
stale.delete(locator);
|
|
1132
1281
|
repairable.delete(locator);
|
|
1133
1282
|
unpreparable.delete(locator);
|
|
1134
|
-
paused.delete(locator);
|
|
1135
|
-
prepared.delete(locator);
|
|
1136
1283
|
scopedRefusals.delete(locator);
|
|
1137
1284
|
sourceRefusals.delete(locator);
|
|
1138
1285
|
declarationRefusals.delete(locator);
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
liveModels.delete(key);
|
|
1143
|
-
invalidModels.delete(key);
|
|
1144
|
-
}
|
|
1286
|
+
models.delete(locator);
|
|
1287
|
+
liveModels.delete(locator);
|
|
1288
|
+
invalidModels.delete(locator);
|
|
1145
1289
|
journal.record(`room lost ${locator}`);
|
|
1146
1290
|
}
|
|
1147
1291
|
if (fresh.length === 0)
|
|
@@ -1189,7 +1333,14 @@ export function composeRuntime(options) {
|
|
|
1189
1333
|
});
|
|
1190
1334
|
await eraseFenced(fresh);
|
|
1191
1335
|
}
|
|
1192
|
-
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);
|
|
1193
1344
|
const prior = models.get(key);
|
|
1194
1345
|
if (prior !== undefined &&
|
|
1195
1346
|
prior.status !== 'unavailable' &&
|
|
@@ -1220,8 +1371,8 @@ export function composeRuntime(options) {
|
|
|
1220
1371
|
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1221
1372
|
targets = held.targets;
|
|
1222
1373
|
transfers.clear();
|
|
1223
|
-
|
|
1224
|
-
|
|
1374
|
+
arrivals.clear();
|
|
1375
|
+
streaming.clear();
|
|
1225
1376
|
limits = held.providerLimits;
|
|
1226
1377
|
capabilities = held.capabilities;
|
|
1227
1378
|
member = held.member;
|
|
@@ -1231,7 +1382,7 @@ export function composeRuntime(options) {
|
|
|
1231
1382
|
presentations.clear();
|
|
1232
1383
|
for (const record of held.presentation)
|
|
1233
1384
|
presentations.set(record.app, record.value);
|
|
1234
|
-
conversations.hydrate(heldConversations, chatDrafts);
|
|
1385
|
+
conversations.hydrate(heldConversations, chatDrafts, held.conversationList);
|
|
1235
1386
|
pendings.clear();
|
|
1236
1387
|
for (const queue of queues) {
|
|
1237
1388
|
for (const { batch, state } of queue.pending) {
|
|
@@ -1239,7 +1390,7 @@ export function composeRuntime(options) {
|
|
|
1239
1390
|
}
|
|
1240
1391
|
}
|
|
1241
1392
|
replicas.clear();
|
|
1242
|
-
|
|
1393
|
+
reopened.clear();
|
|
1243
1394
|
queuesWithoutReplica.clear();
|
|
1244
1395
|
for (const [locator, queue] of queuedBatches) {
|
|
1245
1396
|
if (!held.rooms.has(locator))
|
|
@@ -1248,39 +1399,40 @@ export function composeRuntime(options) {
|
|
|
1248
1399
|
records.forEach((stored, index) => {
|
|
1249
1400
|
if (fences.has(stored.locator))
|
|
1250
1401
|
return;
|
|
1251
|
-
const
|
|
1252
|
-
|
|
1253
|
-
const record = { ...stored, model, selectionModel };
|
|
1402
|
+
const record = { ...stored, model: restored(stored.locator, stored.model) };
|
|
1403
|
+
reopened.add(record.locator);
|
|
1254
1404
|
replicas.set(record.locator, {
|
|
1255
1405
|
record,
|
|
1256
1406
|
queue: queuedBatches.get(record.locator) ?? [],
|
|
1257
1407
|
overlaid: held.overlaid.get(record.key) ?? new Set(),
|
|
1258
1408
|
count: counts[index] ?? 0,
|
|
1259
1409
|
confirmed: 0,
|
|
1410
|
+
indexed: !held.unindexed.has(record.key),
|
|
1260
1411
|
});
|
|
1261
|
-
if (!held.unprepared.has(record.key))
|
|
1262
|
-
prepared.add(record.locator);
|
|
1263
1412
|
const existing = rooms.get(record.locator);
|
|
1264
1413
|
if (existing === undefined)
|
|
1265
|
-
list(record,
|
|
1414
|
+
list(record, record.roomVersion !== null);
|
|
1266
1415
|
else {
|
|
1267
1416
|
rooms.set(record.locator, {
|
|
1268
1417
|
...existing,
|
|
1269
1418
|
roomVersion: record.roomVersion,
|
|
1270
1419
|
operations: record.operations,
|
|
1271
1420
|
replica: record.replica,
|
|
1272
|
-
model: modelTypes(model),
|
|
1421
|
+
model: modelTypes(record.model),
|
|
1273
1422
|
typedSelection: record.typedSelection === null
|
|
1274
1423
|
? null
|
|
1275
|
-
: {
|
|
1276
|
-
roots: record.typedSelection.roots,
|
|
1277
|
-
model: modelTypes(selectionModel),
|
|
1278
|
-
},
|
|
1424
|
+
: { roots: record.typedSelection.roots, model: modelTypes(builtIn) },
|
|
1279
1425
|
});
|
|
1280
1426
|
}
|
|
1281
1427
|
});
|
|
1282
|
-
|
|
1283
|
-
|
|
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
|
+
}
|
|
1284
1436
|
if (held.erasing || held.orphans.length > 0)
|
|
1285
1437
|
queueMicrotask(sweep);
|
|
1286
1438
|
return {
|
|
@@ -1305,7 +1457,7 @@ export function composeRuntime(options) {
|
|
|
1305
1457
|
async function open() {
|
|
1306
1458
|
if (closed)
|
|
1307
1459
|
throw new Error('runtime closed');
|
|
1308
|
-
const opened = openRuntimeStore(isFenced,
|
|
1460
|
+
const opened = openRuntimeStore(isFenced, database);
|
|
1309
1461
|
const interrupted = await opened.fences();
|
|
1310
1462
|
for (const locator of interrupted) {
|
|
1311
1463
|
fences.set(locator, { at: discoveries, erased: false });
|
|
@@ -1351,10 +1503,29 @@ export function composeRuntime(options) {
|
|
|
1351
1503
|
return opening;
|
|
1352
1504
|
}
|
|
1353
1505
|
function exclusive(work) {
|
|
1506
|
+
gathering = null;
|
|
1354
1507
|
const next = mutations.then(work, work);
|
|
1355
1508
|
mutations = next.then(() => undefined, () => undefined);
|
|
1356
1509
|
return next;
|
|
1357
1510
|
}
|
|
1511
|
+
function gathered(key, input, merge, work) {
|
|
1512
|
+
const tail = gathering;
|
|
1513
|
+
if (tail !== null && tail.key === key) {
|
|
1514
|
+
const merged = merge(tail.input, input);
|
|
1515
|
+
if (merged !== null) {
|
|
1516
|
+
tail.input = merged;
|
|
1517
|
+
return tail.done;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
const entry = { key, input, done: Promise.resolve() };
|
|
1521
|
+
entry.done = exclusive(() => {
|
|
1522
|
+
if (gathering === entry)
|
|
1523
|
+
gathering = null;
|
|
1524
|
+
return work(entry.input);
|
|
1525
|
+
});
|
|
1526
|
+
gathering = entry;
|
|
1527
|
+
return entry.done;
|
|
1528
|
+
}
|
|
1358
1529
|
function withdrawable(locator) {
|
|
1359
1530
|
return (batch) => {
|
|
1360
1531
|
const state = pendings.get(pendingKey(locator, batch.id));
|
|
@@ -1436,26 +1607,14 @@ export function composeRuntime(options) {
|
|
|
1436
1607
|
},
|
|
1437
1608
|
};
|
|
1438
1609
|
}
|
|
1439
|
-
function
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
fingerprint(compositionOf(before)) === fingerprint(compositionOf(after))) {
|
|
1443
|
-
return;
|
|
1444
|
-
}
|
|
1445
|
-
if (before !== undefined && before.key === after.key)
|
|
1446
|
-
prepared.delete(locator);
|
|
1447
|
-
}
|
|
1448
|
-
function committedRoom(locator, record, answer) {
|
|
1449
|
-
if (answer.prepared[locator] === false)
|
|
1450
|
-
prepared.delete(locator);
|
|
1451
|
-
else if (answer.prepared[locator] === true)
|
|
1452
|
-
prepared.add(locator);
|
|
1453
|
-
if (!prepared.has(locator))
|
|
1610
|
+
function committedRoom(locator, answer) {
|
|
1611
|
+
const indexed = answer.indexed[locator] ?? replicas.get(locator)?.indexed ?? false;
|
|
1612
|
+
if (!indexed)
|
|
1454
1613
|
prepare([locator]);
|
|
1455
|
-
return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0 };
|
|
1614
|
+
return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
|
|
1456
1615
|
}
|
|
1457
1616
|
function replayed(record, confirmedLayer, queue) {
|
|
1458
|
-
return stepped(replaying(confirmedLayer, queue, record.locator,
|
|
1617
|
+
return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
|
|
1459
1618
|
}
|
|
1460
1619
|
async function overlayDiff(record, before, visible, touched) {
|
|
1461
1620
|
const kept = before !== undefined && before.record.key === record.key
|
|
@@ -1518,7 +1677,7 @@ export function composeRuntime(options) {
|
|
|
1518
1677
|
const confirmedLayer = overrides === undefined
|
|
1519
1678
|
? read
|
|
1520
1679
|
: await stepped(slicingWith(read, overrides.set, overrides.removed));
|
|
1521
|
-
const composition =
|
|
1680
|
+
const composition = composed(record);
|
|
1522
1681
|
const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
|
|
1523
1682
|
const founded = kept.filter((batch) => !queue.includes(batch));
|
|
1524
1683
|
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
@@ -1561,6 +1720,7 @@ export function composeRuntime(options) {
|
|
|
1561
1720
|
key: record.key,
|
|
1562
1721
|
record: storedRecord(record),
|
|
1563
1722
|
roots: record.typedSelection?.roots ?? [],
|
|
1723
|
+
composed: composedOf(composition),
|
|
1564
1724
|
...settling.commit,
|
|
1565
1725
|
...(overrides === undefined
|
|
1566
1726
|
? {}
|
|
@@ -1602,14 +1762,14 @@ export function composeRuntime(options) {
|
|
|
1602
1762
|
const confirmedChanged = settling.commit?.published !== undefined ||
|
|
1603
1763
|
settling.commit?.refreshed === true ||
|
|
1604
1764
|
overrides !== undefined;
|
|
1605
|
-
|
|
1606
|
-
const { count } = committedRoom(locator, record, answer);
|
|
1765
|
+
const { count, indexed } = committedRoom(locator, answer);
|
|
1607
1766
|
replicas.set(locator, {
|
|
1608
1767
|
record,
|
|
1609
1768
|
queue: kept,
|
|
1610
1769
|
overlaid: touched,
|
|
1611
1770
|
count,
|
|
1612
1771
|
confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
|
|
1772
|
+
indexed,
|
|
1613
1773
|
});
|
|
1614
1774
|
return mergeChangeSets([
|
|
1615
1775
|
changeSet(locator, {
|
|
@@ -1624,38 +1784,28 @@ export function composeRuntime(options) {
|
|
|
1624
1784
|
};
|
|
1625
1785
|
}
|
|
1626
1786
|
function adoptModels(locator, record) {
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
if (model.status === 'current')
|
|
1635
|
-
liveModels.set(key, JSON.stringify(model.snapshot));
|
|
1636
|
-
else if (model.status === 'unavailable')
|
|
1637
|
-
liveModels.delete(key);
|
|
1638
|
-
}
|
|
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);
|
|
1639
1794
|
}
|
|
1640
1795
|
function remodel(locator, answers, held) {
|
|
1641
|
-
const
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
};
|
|
1652
|
-
return {
|
|
1653
|
-
collection: taken(answers?.collection, held.collection, 'collection'),
|
|
1654
|
-
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
1655
|
-
};
|
|
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 };
|
|
1656
1806
|
}
|
|
1657
1807
|
function publish(local, room, record, landing, live) {
|
|
1658
|
-
|
|
1808
|
+
const landed = exclusive(async () => {
|
|
1659
1809
|
if (!live()) {
|
|
1660
1810
|
caughtUpTo(room.locator);
|
|
1661
1811
|
return true;
|
|
@@ -1679,29 +1829,28 @@ export function composeRuntime(options) {
|
|
|
1679
1829
|
? {
|
|
1680
1830
|
room: true,
|
|
1681
1831
|
view: 'refreshed',
|
|
1682
|
-
commit: { refreshed: true
|
|
1832
|
+
commit: { refreshed: true },
|
|
1683
1833
|
}
|
|
1684
1834
|
: {
|
|
1685
1835
|
room: true,
|
|
1686
1836
|
view: landing.refreshed ? 'refreshed' : 'confirmed',
|
|
1687
1837
|
commit: {
|
|
1688
1838
|
published: { from: prior?.record.key ?? null },
|
|
1689
|
-
composition: compositionOf(record),
|
|
1690
1839
|
...(landing.refreshed ? { refreshed: true } : {}),
|
|
1691
1840
|
},
|
|
1692
1841
|
});
|
|
1693
1842
|
}
|
|
1694
1843
|
catch (error) {
|
|
1695
|
-
if (!
|
|
1844
|
+
if (!refusedForSpace(error))
|
|
1696
1845
|
throw error;
|
|
1697
|
-
await exceededOn(room.locator, null);
|
|
1698
1846
|
caughtUpTo(room.locator);
|
|
1699
1847
|
return true;
|
|
1700
1848
|
}
|
|
1701
1849
|
if (landing.kind === 'bootstrapped') {
|
|
1702
|
-
|
|
1850
|
+
untransferred(room.locator);
|
|
1703
1851
|
catching.delete(room.locator);
|
|
1704
|
-
|
|
1852
|
+
if (prior !== undefined && prior.record.key !== record.key)
|
|
1853
|
+
sweep();
|
|
1705
1854
|
}
|
|
1706
1855
|
for (const batch of accepted)
|
|
1707
1856
|
pendings.delete(pendingKey(room.locator, batch.id));
|
|
@@ -1721,6 +1870,7 @@ export function composeRuntime(options) {
|
|
|
1721
1870
|
}
|
|
1722
1871
|
return true;
|
|
1723
1872
|
});
|
|
1873
|
+
return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
|
|
1724
1874
|
}
|
|
1725
1875
|
async function savedRoom(locator) {
|
|
1726
1876
|
const local = await openLocal();
|
|
@@ -1745,12 +1895,7 @@ export function composeRuntime(options) {
|
|
|
1745
1895
|
return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
|
|
1746
1896
|
}
|
|
1747
1897
|
async function saveParts(locator, named, writtenBy) {
|
|
1748
|
-
const read = named
|
|
1749
|
-
if (read.some(({ path }, at) => path === '' ||
|
|
1750
|
-
normalize(path) !== path ||
|
|
1751
|
-
read.findIndex((one) => one.path === path) !== at)) {
|
|
1752
|
-
throw runtimeError('room-draft-malformed');
|
|
1753
|
-
}
|
|
1898
|
+
const read = readSavedParts(named);
|
|
1754
1899
|
const target = await savedRoom(locator);
|
|
1755
1900
|
const saved = [];
|
|
1756
1901
|
const files = [];
|
|
@@ -1759,7 +1904,7 @@ export function composeRuntime(options) {
|
|
|
1759
1904
|
for (const { path, parts } of read) {
|
|
1760
1905
|
const file = parts.file === undefined
|
|
1761
1906
|
? undefined
|
|
1762
|
-
: await heldFile(parts.file.blob, binaryBase(visible, path));
|
|
1907
|
+
: await heldFile(parts.file.blob, parts.file.base === undefined ? binaryBase(visible, path) : parts.file.base);
|
|
1763
1908
|
if (parts.file !== undefined && file !== undefined) {
|
|
1764
1909
|
files.push({ key: file.localId, blob: parts.file.blob });
|
|
1765
1910
|
}
|
|
@@ -1794,13 +1939,14 @@ export function composeRuntime(options) {
|
|
|
1794
1939
|
const referring = removed.length === 0
|
|
1795
1940
|
? []
|
|
1796
1941
|
: (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
|
|
1797
|
-
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
|
|
1942
|
+
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
|
|
1798
1943
|
const { visible } = await replayed(record, confirmedLayer, replica.queue);
|
|
1799
|
-
const composition =
|
|
1944
|
+
const composition = composed(record);
|
|
1800
1945
|
return {
|
|
1801
1946
|
record,
|
|
1802
1947
|
confirmed: confirmedLayer,
|
|
1803
1948
|
visible,
|
|
1949
|
+
folders: new Set(await local.folders(record.key, 'visible', paths)),
|
|
1804
1950
|
*visibleOver(queue) {
|
|
1805
1951
|
return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
|
|
1806
1952
|
},
|
|
@@ -1812,13 +1958,7 @@ export function composeRuntime(options) {
|
|
|
1812
1958
|
return false;
|
|
1813
1959
|
const local = await openLocal();
|
|
1814
1960
|
const roots = record.typedSelection?.roots ?? [];
|
|
1815
|
-
|
|
1816
|
-
(await local.typedWithin(record.key, 'visible', roots, false))) {
|
|
1817
|
-
return true;
|
|
1818
|
-
}
|
|
1819
|
-
return (composition.selection !== null &&
|
|
1820
|
-
composition.selection.model.status === 'unavailable' &&
|
|
1821
|
-
(await local.typedWithin(record.key, 'visible', roots, true)));
|
|
1961
|
+
return local.typedWithin(record.key, 'visible', roots, false);
|
|
1822
1962
|
}
|
|
1823
1963
|
function textDiffers(left, right) {
|
|
1824
1964
|
return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
|
|
@@ -1923,7 +2063,7 @@ export function composeRuntime(options) {
|
|
|
1923
2063
|
const lookup = overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides);
|
|
1924
2064
|
const clean = available(composition) &&
|
|
1925
2065
|
changed.length <= CLEAN_CHANGES &&
|
|
1926
|
-
(await changedCleanly(changed,
|
|
2066
|
+
(await changedCleanly(changed, composed(record), composition, {
|
|
1927
2067
|
lookup,
|
|
1928
2068
|
texts: textsOf(local, record.key, 'confirmed'),
|
|
1929
2069
|
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
@@ -1954,6 +2094,7 @@ export function composeRuntime(options) {
|
|
|
1954
2094
|
operations: room.operations,
|
|
1955
2095
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
1956
2096
|
visible: saving.visible,
|
|
2097
|
+
folders: saving.folders,
|
|
1957
2098
|
queued: queuedDocuments(replica.queue),
|
|
1958
2099
|
unmodeled: await unmodeledIn(record),
|
|
1959
2100
|
validate: validatorOf(record, saving.confirmed),
|
|
@@ -1974,6 +2115,7 @@ export function composeRuntime(options) {
|
|
|
1974
2115
|
locator,
|
|
1975
2116
|
key: saving.record.key,
|
|
1976
2117
|
roots: saving.record.typedSelection?.roots ?? [],
|
|
2118
|
+
composed: composedOf(composed(saving.record)),
|
|
1977
2119
|
overlay: diff.overlay,
|
|
1978
2120
|
});
|
|
1979
2121
|
}
|
|
@@ -2005,7 +2147,7 @@ export function composeRuntime(options) {
|
|
|
2005
2147
|
}
|
|
2006
2148
|
const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
|
|
2007
2149
|
const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
|
|
2008
|
-
const changed = saved.flatMap(({ path, parts }) => changedParts(path,
|
|
2150
|
+
const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
|
|
2009
2151
|
if (changed.length > 0)
|
|
2010
2152
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2011
2153
|
const target = replicas.get(destination);
|
|
@@ -2117,6 +2259,8 @@ export function composeRuntime(options) {
|
|
|
2117
2259
|
submitLater(locator);
|
|
2118
2260
|
}
|
|
2119
2261
|
async function committed(locator, target, saved, consumed, writtenBy, files = []) {
|
|
2262
|
+
if (full)
|
|
2263
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2120
2264
|
const into = movedInto(locator, saved);
|
|
2121
2265
|
if (into !== null) {
|
|
2122
2266
|
return committedMove(locator, into, target, saved, consumed, writtenBy, files);
|
|
@@ -2128,7 +2272,7 @@ export function composeRuntime(options) {
|
|
|
2128
2272
|
...saved.map(({ path }) => path),
|
|
2129
2273
|
...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
|
|
2130
2274
|
]);
|
|
2131
|
-
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path,
|
|
2275
|
+
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
|
|
2132
2276
|
if (changed.length > 0)
|
|
2133
2277
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2134
2278
|
const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
|
|
@@ -2156,6 +2300,7 @@ export function composeRuntime(options) {
|
|
|
2156
2300
|
providerLimits: limits,
|
|
2157
2301
|
handle: room.handle,
|
|
2158
2302
|
visible: saving.visible,
|
|
2303
|
+
folders: saving.folders,
|
|
2159
2304
|
changes,
|
|
2160
2305
|
bases: parts.flatMap(({ bases }) => bases),
|
|
2161
2306
|
queued: queuedDocuments(replica.queue),
|
|
@@ -2306,6 +2451,9 @@ export function composeRuntime(options) {
|
|
|
2306
2451
|
async function readHistory(locator, path, query = {}) {
|
|
2307
2452
|
if (closed || refusal !== null)
|
|
2308
2453
|
return UNCERTAIN;
|
|
2454
|
+
await firstDiscovery;
|
|
2455
|
+
if (!roomsNamed)
|
|
2456
|
+
return UNCERTAIN;
|
|
2309
2457
|
try {
|
|
2310
2458
|
await ensureAuthorization();
|
|
2311
2459
|
}
|
|
@@ -2395,14 +2543,12 @@ export function composeRuntime(options) {
|
|
|
2395
2543
|
return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
|
|
2396
2544
|
};
|
|
2397
2545
|
}
|
|
2398
|
-
function binaryRoom(request) {
|
|
2399
|
-
if (request.kind === 'path')
|
|
2400
|
-
return request.locator;
|
|
2401
|
-
return options.adapter.referrerScope(request.referrer)?.locator ?? null;
|
|
2402
|
-
}
|
|
2403
2546
|
async function transferBinary(request, transfer, answer) {
|
|
2404
2547
|
if (closed || refusal !== null)
|
|
2405
2548
|
return UNCERTAIN;
|
|
2549
|
+
await firstDiscovery;
|
|
2550
|
+
if (!roomsNamed)
|
|
2551
|
+
return UNCERTAIN;
|
|
2406
2552
|
try {
|
|
2407
2553
|
await ensureAuthorization();
|
|
2408
2554
|
}
|
|
@@ -2412,16 +2558,13 @@ export function composeRuntime(options) {
|
|
|
2412
2558
|
if (closed || authorization === undefined || refusal !== null)
|
|
2413
2559
|
return UNCERTAIN;
|
|
2414
2560
|
const since = fencings;
|
|
2415
|
-
journal.record(request.
|
|
2416
|
-
? `transfer binary ${request.locator} ${request.path}`
|
|
2417
|
-
: `transfer binary ${request.fileRef}`);
|
|
2561
|
+
journal.record(`transfer binary ${request.locator} ${request.path}`);
|
|
2418
2562
|
try {
|
|
2419
2563
|
const result = await transfer(authorization);
|
|
2420
2564
|
const settled = result;
|
|
2421
2565
|
journal.record(`outcome binary ${settled.kind}`);
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
return failure({ kind: 'room-not-readable', locator });
|
|
2566
|
+
if (fencedSince(request.locator, since))
|
|
2567
|
+
return failure({ kind: 'room-not-readable', locator: request.locator });
|
|
2425
2568
|
if (settled.kind === 'scoped-refusal') {
|
|
2426
2569
|
const room = replicas.get(settled.locator)?.record;
|
|
2427
2570
|
if (room !== undefined)
|
|
@@ -2436,23 +2579,27 @@ export function composeRuntime(options) {
|
|
|
2436
2579
|
return UNCERTAIN;
|
|
2437
2580
|
}
|
|
2438
2581
|
}
|
|
2582
|
+
function fetchBinary(request, name) {
|
|
2583
|
+
return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
|
|
2584
|
+
}
|
|
2439
2585
|
async function readBinary(request, name) {
|
|
2440
2586
|
if (closed)
|
|
2441
2587
|
return UNCERTAIN;
|
|
2442
2588
|
const held = await keptFiles.find(name);
|
|
2443
2589
|
if (held !== null)
|
|
2444
2590
|
return held;
|
|
2445
|
-
return
|
|
2591
|
+
return fetchBinary(request, name);
|
|
2446
2592
|
}
|
|
2447
2593
|
async function binaryPath(locator, path, revision) {
|
|
2448
2594
|
if (revision !== undefined)
|
|
2449
2595
|
return { kind: 'path', locator, path, revision };
|
|
2450
2596
|
const replica = replicas.get(locator);
|
|
2451
|
-
if (replica === undefined)
|
|
2452
|
-
return null;
|
|
2453
2597
|
const local = await openLocal();
|
|
2454
|
-
const [held] = await local.entries(replica.record.key, 'visible', [path]);
|
|
2455
|
-
|
|
2598
|
+
const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
|
|
2599
|
+
const attached = held === undefined ? attachedFiles.get(locator)?.get(path) : undefined;
|
|
2600
|
+
if (attached !== undefined)
|
|
2601
|
+
return { kind: 'path', locator, path, revision: attached.revision };
|
|
2602
|
+
if (replica === undefined || held?.binary === undefined || held.binary === null)
|
|
2456
2603
|
return null;
|
|
2457
2604
|
if (held.binary.state === 'pending')
|
|
2458
2605
|
return { kind: 'pending', localId: held.binary.localId };
|
|
@@ -2468,7 +2615,7 @@ export function composeRuntime(options) {
|
|
|
2468
2615
|
const holds = (at) => {
|
|
2469
2616
|
const kept = confirmedLayer.binaries[at];
|
|
2470
2617
|
return (kept?.state === 'accepted' &&
|
|
2471
|
-
kept.
|
|
2618
|
+
kept.sha256 === record.sha256 &&
|
|
2472
2619
|
confirmedLayer.revisions[at] === record.revision);
|
|
2473
2620
|
};
|
|
2474
2621
|
let confirmedAt = path;
|
|
@@ -2482,49 +2629,91 @@ export function composeRuntime(options) {
|
|
|
2482
2629
|
locator,
|
|
2483
2630
|
path: confirmedAt,
|
|
2484
2631
|
revision: record.revision,
|
|
2485
|
-
fileRef: record.fileRef,
|
|
2486
2632
|
};
|
|
2487
2633
|
}
|
|
2488
|
-
async function
|
|
2634
|
+
async function askedBinary(locator, path, revision) {
|
|
2489
2635
|
await ready;
|
|
2490
2636
|
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2491
|
-
|
|
2637
|
+
return drafted !== undefined && drafted.localId === revision
|
|
2492
2638
|
? { kind: 'pending', localId: revision }
|
|
2493
|
-
:
|
|
2639
|
+
: binaryPath(locator, path, revision);
|
|
2640
|
+
}
|
|
2641
|
+
function pendingBinary(locator, path, localId) {
|
|
2642
|
+
const facts = queuedFacts(locator, path, localId);
|
|
2643
|
+
return facts === null
|
|
2644
|
+
? absentBinary(path)
|
|
2645
|
+
: {
|
|
2646
|
+
kind: 'binary',
|
|
2647
|
+
file: localId,
|
|
2648
|
+
byteLength: facts.byteLength,
|
|
2649
|
+
mediaType: facts.mediaType,
|
|
2650
|
+
};
|
|
2651
|
+
}
|
|
2652
|
+
async function readBinaryPath(locator, path, revision) {
|
|
2653
|
+
const asked = await askedBinary(locator, path, revision);
|
|
2494
2654
|
if (asked === null)
|
|
2495
2655
|
return absentBinary(path);
|
|
2496
|
-
if (asked.kind === 'pending')
|
|
2497
|
-
|
|
2498
|
-
return facts === null
|
|
2499
|
-
? absentBinary(path)
|
|
2500
|
-
: {
|
|
2501
|
-
kind: 'binary',
|
|
2502
|
-
file: asked.localId,
|
|
2503
|
-
byteLength: facts.byteLength,
|
|
2504
|
-
mediaType: facts.mediaType,
|
|
2505
|
-
};
|
|
2506
|
-
}
|
|
2656
|
+
if (asked.kind === 'pending')
|
|
2657
|
+
return pendingBinary(locator, path, asked.localId);
|
|
2507
2658
|
return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
|
|
2508
2659
|
}
|
|
2660
|
+
async function heldMediaType(locator, path) {
|
|
2661
|
+
const replica = replicas.get(locator);
|
|
2662
|
+
const [held] = replica === undefined
|
|
2663
|
+
? []
|
|
2664
|
+
: await (await openLocal()).entries(replica.record.key, 'visible', [path]);
|
|
2665
|
+
return held === undefined
|
|
2666
|
+
? attachedFiles.get(locator)?.get(path)?.mediaType
|
|
2667
|
+
: held.binary?.mediaType;
|
|
2668
|
+
}
|
|
2669
|
+
function linkOf(asked, held) {
|
|
2670
|
+
return transferBinary(asked, (authorization) => options.adapter.link(authorization, asked), (result) => binaryLinkAnswer(result, held));
|
|
2671
|
+
}
|
|
2509
2672
|
async function linkBinaryPath(locator, path, revision) {
|
|
2510
2673
|
await ready;
|
|
2511
2674
|
const asked = await binaryPath(locator, path, revision);
|
|
2512
2675
|
if (asked === null || asked.kind === 'pending')
|
|
2513
2676
|
return absentBinary(path);
|
|
2514
|
-
return
|
|
2677
|
+
return linkOf(asked, revision === undefined ? await heldMediaType(locator, path) : undefined);
|
|
2515
2678
|
}
|
|
2516
|
-
async function
|
|
2679
|
+
async function showBinaryPath(locator, path, revision) {
|
|
2680
|
+
const asked = await askedBinary(locator, path, revision);
|
|
2681
|
+
if (asked === null)
|
|
2682
|
+
return absentBinary(path);
|
|
2683
|
+
if (asked.kind === 'pending')
|
|
2684
|
+
return pendingBinary(locator, path, asked.localId);
|
|
2685
|
+
if (closed)
|
|
2686
|
+
return UNCERTAIN;
|
|
2687
|
+
const name = { locator, path: asked.path, revision: asked.revision };
|
|
2688
|
+
const kept = await keptFiles.find(name);
|
|
2689
|
+
if (kept !== null)
|
|
2690
|
+
return kept;
|
|
2691
|
+
const mediaType = await heldMediaType(locator, path);
|
|
2692
|
+
if (mediaType?.startsWith('image/') === true)
|
|
2693
|
+
return fetchBinary(asked, name);
|
|
2694
|
+
return linkOf(asked, revision === undefined ? mediaType : undefined);
|
|
2695
|
+
}
|
|
2696
|
+
async function attachBinary(locator, file) {
|
|
2517
2697
|
await ready;
|
|
2518
|
-
|
|
2519
|
-
if (
|
|
2520
|
-
return
|
|
2521
|
-
|
|
2698
|
+
await firstDiscovery;
|
|
2699
|
+
if (closed || !roomsNamed || connectionOf() === 'unreachable' || refusal !== null)
|
|
2700
|
+
return OFFLINE;
|
|
2701
|
+
const request = await attachedFile(locator, file);
|
|
2702
|
+
return transferBinary({ locator, path: file.name }, async (held) => {
|
|
2703
|
+
const result = await options.adapter.attach(held, request);
|
|
2704
|
+
if (result.kind === 'attached') {
|
|
2705
|
+
const landed = attachedFiles.get(locator) ?? new Map();
|
|
2706
|
+
landed.set(result.path, { revision: result.revision, mediaType: request.mediaType });
|
|
2707
|
+
attachedFiles.set(locator, landed);
|
|
2708
|
+
}
|
|
2709
|
+
return result;
|
|
2710
|
+
}, attachAnswer);
|
|
2522
2711
|
}
|
|
2523
2712
|
function readPart(file, part) {
|
|
2524
|
-
return
|
|
2713
|
+
return database.ask({ kind: 'file.part', key: file, part });
|
|
2525
2714
|
}
|
|
2526
2715
|
async function cleanUp() {
|
|
2527
|
-
const freed = await
|
|
2716
|
+
const freed = await database.ask({ kind: 'opened.clean' });
|
|
2528
2717
|
return freed + ((await options.shell.cleanUp?.()) ?? 0);
|
|
2529
2718
|
}
|
|
2530
2719
|
function declaredAgents() {
|
|
@@ -2693,18 +2882,25 @@ export function composeRuntime(options) {
|
|
|
2693
2882
|
after = page.next;
|
|
2694
2883
|
}
|
|
2695
2884
|
}
|
|
2696
|
-
async function validatedRoom(local, record, view, previous, overrides = null) {
|
|
2697
|
-
const composition =
|
|
2698
|
-
const modeled = record.model.status !== 'unavailable' || record.
|
|
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;
|
|
2699
2888
|
const lookup = overrides === null
|
|
2700
2889
|
? lookupOf(local, record.key, view)
|
|
2701
2890
|
: overriddenLookup(lookupOf(local, record.key, view), overrides);
|
|
2702
2891
|
const texts = overrides === null
|
|
2703
2892
|
? textsOf(local, record.key, view)
|
|
2704
2893
|
: overriddenTexts(textsOf(local, record.key, view), overrides);
|
|
2705
|
-
const
|
|
2894
|
+
const walking = overrides === null
|
|
2706
2895
|
? walkOf(local, record.key, view)
|
|
2707
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
|
+
};
|
|
2708
2904
|
let changes = null;
|
|
2709
2905
|
let reshaped = false;
|
|
2710
2906
|
if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
|
|
@@ -2729,11 +2925,11 @@ export function composeRuntime(options) {
|
|
|
2729
2925
|
}
|
|
2730
2926
|
const named = changes === null ? null : changes.map(({ path }) => path);
|
|
2731
2927
|
const textChanged = changes === null || changes.length > 0 || reshaped;
|
|
2732
|
-
const modelsChanged = previous === null || !sameComposition(
|
|
2928
|
+
const modelsChanged = previous === null || !sameComposition(composed(previous.record), composition);
|
|
2733
2929
|
if (modeled && (textChanged || modelsChanged)) {
|
|
2734
2930
|
const clean = previous !== null &&
|
|
2735
2931
|
changes !== null &&
|
|
2736
|
-
(await changedCleanly(changes,
|
|
2932
|
+
(await changedCleanly(changes, composed(previous.record), composition, {
|
|
2737
2933
|
lookup,
|
|
2738
2934
|
texts,
|
|
2739
2935
|
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
@@ -2751,43 +2947,210 @@ export function composeRuntime(options) {
|
|
|
2751
2947
|
return unreadableRoom(texts, walk, named);
|
|
2752
2948
|
return null;
|
|
2753
2949
|
}
|
|
2754
|
-
function refreshingOf(local, key, liveness) {
|
|
2950
|
+
function refreshingOf(local, key, composition, liveness, replaying = null) {
|
|
2951
|
+
const write = replaying === null ? exclusive : finishingWrite;
|
|
2755
2952
|
return {
|
|
2953
|
+
async turn() {
|
|
2954
|
+
if (replaying === null)
|
|
2955
|
+
return;
|
|
2956
|
+
installing(replaying);
|
|
2957
|
+
await screened();
|
|
2958
|
+
},
|
|
2756
2959
|
prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
|
|
2757
2960
|
async stage(page) {
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
const
|
|
2961
|
+
for (let written = 0;;) {
|
|
2962
|
+
if (replaying !== null)
|
|
2963
|
+
await screened();
|
|
2964
|
+
const end = batchEnd(page.entries, written);
|
|
2965
|
+
const last = end === page.entries.length;
|
|
2966
|
+
const entries = page.entries.slice(written, end);
|
|
2967
|
+
const staged = await write(async () => {
|
|
2762
2968
|
if (!liveness())
|
|
2763
2969
|
return false;
|
|
2764
|
-
await local.refresh(key, last
|
|
2970
|
+
await timed(stageBudget, () => local.refresh(key, composition, last
|
|
2765
2971
|
? { ...page, entries }
|
|
2766
|
-
: { entries, removals: [], folders: [], folderRemovals: [] });
|
|
2972
|
+
: { entries, removals: [], folders: [], folderRemovals: [] }));
|
|
2767
2973
|
return true;
|
|
2768
2974
|
});
|
|
2769
2975
|
if (!staged)
|
|
2770
2976
|
return false;
|
|
2977
|
+
if (last)
|
|
2978
|
+
return true;
|
|
2979
|
+
written = end;
|
|
2771
2980
|
}
|
|
2772
|
-
return true;
|
|
2773
2981
|
},
|
|
2774
2982
|
};
|
|
2775
2983
|
}
|
|
2776
|
-
function
|
|
2777
|
-
|
|
2984
|
+
function batchEnd(entries, from) {
|
|
2985
|
+
let end = from;
|
|
2778
2986
|
let spent = 0;
|
|
2779
|
-
|
|
2780
|
-
const size = (
|
|
2781
|
-
if (spent + size >
|
|
2782
|
-
|
|
2783
|
-
spent = 0;
|
|
2784
|
-
}
|
|
2785
|
-
batches.at(-1).push(entry);
|
|
2987
|
+
while (end < entries.length) {
|
|
2988
|
+
const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
|
|
2989
|
+
if (spent + size > stageBudget.bytes && end > from)
|
|
2990
|
+
break;
|
|
2786
2991
|
spent += size;
|
|
2992
|
+
end += 1;
|
|
2993
|
+
}
|
|
2994
|
+
return end;
|
|
2995
|
+
}
|
|
2996
|
+
async function timed(budget, work) {
|
|
2997
|
+
const started = performance.now();
|
|
2998
|
+
const done = await work();
|
|
2999
|
+
const spent = performance.now() - started;
|
|
3000
|
+
if (spent < budget.aim / 2)
|
|
3001
|
+
budget.bytes = Math.min(budget.ceiling, Math.round(budget.bytes * 1.5));
|
|
3002
|
+
else if (spent > budget.aim * 2) {
|
|
3003
|
+
budget.bytes = Math.max(budget.floor, Math.round(budget.bytes / 1.5));
|
|
3004
|
+
}
|
|
3005
|
+
return done;
|
|
3006
|
+
}
|
|
3007
|
+
function installWrite(work) {
|
|
3008
|
+
return installTurn(installTurns.starting, work);
|
|
3009
|
+
}
|
|
3010
|
+
function finishingWrite(work) {
|
|
3011
|
+
return installTurn(installTurns.finishing, work);
|
|
3012
|
+
}
|
|
3013
|
+
function installTurn(queue, work) {
|
|
3014
|
+
return new Promise((resolve, reject) => {
|
|
3015
|
+
queue.push(() => void exclusive(work)
|
|
3016
|
+
.then(resolve, reject)
|
|
3017
|
+
.finally(() => {
|
|
3018
|
+
installTurns.busy = false;
|
|
3019
|
+
nextInstallTurn();
|
|
3020
|
+
}));
|
|
3021
|
+
nextInstallTurn();
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
function nextInstallTurn() {
|
|
3025
|
+
if (installTurns.busy || installTurns.ahead > 0)
|
|
3026
|
+
return;
|
|
3027
|
+
const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
|
|
3028
|
+
if (turn === undefined)
|
|
3029
|
+
return;
|
|
3030
|
+
installTurns.busy = true;
|
|
3031
|
+
turn();
|
|
3032
|
+
}
|
|
3033
|
+
async function ahead(work) {
|
|
3034
|
+
let taken = false;
|
|
3035
|
+
const hold = {
|
|
3036
|
+
take() {
|
|
3037
|
+
if (taken)
|
|
3038
|
+
return;
|
|
3039
|
+
taken = true;
|
|
3040
|
+
installTurns.ahead += 1;
|
|
3041
|
+
},
|
|
3042
|
+
release() {
|
|
3043
|
+
if (!taken)
|
|
3044
|
+
return;
|
|
3045
|
+
taken = false;
|
|
3046
|
+
installTurns.ahead -= 1;
|
|
3047
|
+
resumeInstall();
|
|
3048
|
+
},
|
|
3049
|
+
};
|
|
3050
|
+
hold.take();
|
|
3051
|
+
try {
|
|
3052
|
+
return await work(hold);
|
|
3053
|
+
}
|
|
3054
|
+
finally {
|
|
3055
|
+
hold.release();
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
function resumeInstall() {
|
|
3059
|
+
if (installTurns.ahead > 0)
|
|
3060
|
+
return;
|
|
3061
|
+
void yielded().then(nextInstallTurn);
|
|
3062
|
+
}
|
|
3063
|
+
function screened() {
|
|
3064
|
+
if (onScreen || closed)
|
|
3065
|
+
return Promise.resolve();
|
|
3066
|
+
queueReadiness();
|
|
3067
|
+
return new Promise((resolve) => shownWaiters.push(resolve));
|
|
3068
|
+
}
|
|
3069
|
+
async function heldStream() {
|
|
3070
|
+
for (;;) {
|
|
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
|
+
}
|
|
3080
|
+
await new Promise((admit) => installWaiters.push(admit));
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
async function unqueued() {
|
|
3084
|
+
while (queuedText >= QUEUED_TEXT) {
|
|
3085
|
+
await new Promise((admit) => installWaiters.push(admit));
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
function once(release) {
|
|
3089
|
+
let released = false;
|
|
3090
|
+
return () => {
|
|
3091
|
+
if (released)
|
|
3092
|
+
return;
|
|
3093
|
+
released = true;
|
|
3094
|
+
release();
|
|
3095
|
+
for (const admit of installWaiters.splice(0))
|
|
3096
|
+
admit();
|
|
3097
|
+
};
|
|
3098
|
+
}
|
|
3099
|
+
function installing(locator) {
|
|
3100
|
+
installs.add(locator);
|
|
3101
|
+
queueReadiness();
|
|
3102
|
+
}
|
|
3103
|
+
function untransferred(locator) {
|
|
3104
|
+
transfers.delete(locator);
|
|
3105
|
+
arrivals.delete(locator);
|
|
3106
|
+
streaming.delete(locator);
|
|
3107
|
+
underived.delete(locator);
|
|
3108
|
+
unindexed.delete(locator);
|
|
3109
|
+
unchecked.delete(locator);
|
|
3110
|
+
}
|
|
3111
|
+
function owedIndexes(locator, bytes, text) {
|
|
3112
|
+
if (bytes <= 0 || text <= 0)
|
|
3113
|
+
return;
|
|
3114
|
+
unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes: bytes / 2, text }]);
|
|
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 });
|
|
3117
|
+
}
|
|
3118
|
+
function built(locator, { done, text }) {
|
|
3119
|
+
const owed = unindexed.get(locator) ?? [];
|
|
3120
|
+
let left = text;
|
|
3121
|
+
while (!done && left > 0 && owed.length > 0) {
|
|
3122
|
+
const [first] = owed;
|
|
3123
|
+
if (left >= first.text) {
|
|
3124
|
+
left -= first.text;
|
|
3125
|
+
owed.shift();
|
|
3126
|
+
continue;
|
|
3127
|
+
}
|
|
3128
|
+
owed[0] = {
|
|
3129
|
+
bytes: (first.bytes * (first.text - left)) / first.text,
|
|
3130
|
+
text: first.text - left,
|
|
3131
|
+
};
|
|
3132
|
+
left = 0;
|
|
2787
3133
|
}
|
|
2788
|
-
|
|
3134
|
+
if (done || owed.length === 0)
|
|
3135
|
+
unindexed.delete(locator);
|
|
3136
|
+
queueReadiness();
|
|
3137
|
+
}
|
|
3138
|
+
function installed(locator) {
|
|
3139
|
+
if (installs.delete(locator))
|
|
3140
|
+
queueReadiness();
|
|
2789
3141
|
}
|
|
2790
|
-
|
|
3142
|
+
function screen(value) {
|
|
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) {
|
|
2791
3154
|
if (!liveness())
|
|
2792
3155
|
return unconfirmed;
|
|
2793
3156
|
const turn = generation;
|
|
@@ -2796,7 +3159,18 @@ export function composeRuntime(options) {
|
|
|
2796
3159
|
const local = await openLocal();
|
|
2797
3160
|
try {
|
|
2798
3161
|
await exclusive(() => local.unrefresh(record.key));
|
|
2799
|
-
const result = await advancement(
|
|
3162
|
+
const result = await advancement({
|
|
3163
|
+
...reads,
|
|
3164
|
+
async refresh(authorization, request) {
|
|
3165
|
+
hold.release();
|
|
3166
|
+
try {
|
|
3167
|
+
return await reads.refresh(authorization, request);
|
|
3168
|
+
}
|
|
3169
|
+
finally {
|
|
3170
|
+
hold.take();
|
|
3171
|
+
}
|
|
3172
|
+
},
|
|
3173
|
+
}, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, composed(record), liveness));
|
|
2800
3174
|
if (result.kind !== 'advanced') {
|
|
2801
3175
|
journal.record(`transfer refresh ${record.locator} ${result.kind}`);
|
|
2802
3176
|
return result;
|
|
@@ -2813,6 +3187,8 @@ export function composeRuntime(options) {
|
|
|
2813
3187
|
return { kind: 'advanced', record: result.record, base };
|
|
2814
3188
|
}
|
|
2815
3189
|
catch (error) {
|
|
3190
|
+
if (refusedForSpace(error))
|
|
3191
|
+
return unconfirmed;
|
|
2816
3192
|
journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
|
|
2817
3193
|
throw error;
|
|
2818
3194
|
}
|
|
@@ -2837,62 +3213,142 @@ export function composeRuntime(options) {
|
|
|
2837
3213
|
return made;
|
|
2838
3214
|
}
|
|
2839
3215
|
function staging(local, liveness) {
|
|
3216
|
+
const derived = async (locator, key, composition) => {
|
|
3217
|
+
underived.delete(locator);
|
|
3218
|
+
for (;;) {
|
|
3219
|
+
await screened();
|
|
3220
|
+
if (!liveness())
|
|
3221
|
+
return false;
|
|
3222
|
+
const done = await finishingWrite(async () => liveness()
|
|
3223
|
+
? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
|
|
3224
|
+
: null);
|
|
3225
|
+
if (done === null)
|
|
3226
|
+
return false;
|
|
3227
|
+
built(locator, done);
|
|
3228
|
+
if (done.done)
|
|
3229
|
+
return true;
|
|
3230
|
+
}
|
|
3231
|
+
};
|
|
2840
3232
|
return {
|
|
2841
3233
|
held: (locator) => transfers.get(locator) ?? null,
|
|
3234
|
+
async turn(locator, step) {
|
|
3235
|
+
installing(locator);
|
|
3236
|
+
for (;;) {
|
|
3237
|
+
await screened();
|
|
3238
|
+
const release = step === 'downloading' ? await heldStream() : () => { };
|
|
3239
|
+
if (onScreen || closed)
|
|
3240
|
+
return release;
|
|
3241
|
+
release();
|
|
3242
|
+
}
|
|
3243
|
+
},
|
|
3244
|
+
unqueued,
|
|
3245
|
+
queued(text) {
|
|
3246
|
+
queuedText += text;
|
|
3247
|
+
return once(() => {
|
|
3248
|
+
queuedText -= text;
|
|
3249
|
+
});
|
|
3250
|
+
},
|
|
3251
|
+
arriving(locator) {
|
|
3252
|
+
let ended = false;
|
|
3253
|
+
return {
|
|
3254
|
+
at(bytes) {
|
|
3255
|
+
if (ended)
|
|
3256
|
+
return;
|
|
3257
|
+
arrivals.set(locator, { parts: arrivals.get(locator)?.parts ?? 0, bytes });
|
|
3258
|
+
streaming.add(locator);
|
|
3259
|
+
const now = performance.now();
|
|
3260
|
+
if (now - arrivalReported < ARRIVAL_REPORT_MS)
|
|
3261
|
+
return;
|
|
3262
|
+
arrivalReported = now;
|
|
3263
|
+
queueReadiness();
|
|
3264
|
+
},
|
|
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
|
+
end() {
|
|
3274
|
+
if (ended)
|
|
3275
|
+
return;
|
|
3276
|
+
ended = true;
|
|
3277
|
+
if (streaming.delete(locator))
|
|
3278
|
+
queueReadiness();
|
|
3279
|
+
},
|
|
3280
|
+
};
|
|
3281
|
+
},
|
|
3282
|
+
announced(locator) {
|
|
3283
|
+
const later = announced.get(locator);
|
|
3284
|
+
return later === undefined ? [] : [later];
|
|
3285
|
+
},
|
|
2842
3286
|
async stage(record, page, composition) {
|
|
2843
|
-
const
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
return null;
|
|
2851
|
-
if (index === 0 && !(await admitted(local, record.locator, bytes)))
|
|
2852
|
-
return null;
|
|
2853
|
-
try {
|
|
2854
|
-
return await local.stage(record, composition, { entries, folders: last ? page.folders : [] }, key, last);
|
|
2855
|
-
}
|
|
2856
|
-
catch (error) {
|
|
2857
|
-
if (!exceeded(error))
|
|
2858
|
-
throw error;
|
|
2859
|
-
await exceededOn(record.locator, bytes);
|
|
2860
|
-
return null;
|
|
2861
|
-
}
|
|
2862
|
-
});
|
|
2863
|
-
if (staged === null)
|
|
3287
|
+
const { locator } = record;
|
|
3288
|
+
installing(locator);
|
|
3289
|
+
await screened();
|
|
3290
|
+
if (!liveness())
|
|
3291
|
+
return null;
|
|
3292
|
+
const key = await installWrite(async () => {
|
|
3293
|
+
if (!liveness())
|
|
2864
3294
|
return null;
|
|
2865
|
-
|
|
2866
|
-
}
|
|
2867
|
-
|
|
3295
|
+
return local.stage(record, composition, page);
|
|
3296
|
+
});
|
|
3297
|
+
if (key === null || !liveness())
|
|
3298
|
+
return null;
|
|
3299
|
+
const arrived = !transfers.has(locator);
|
|
3300
|
+
const prior = transfers.get(locator)?.done.bytes ?? 0;
|
|
3301
|
+
const held = { ...record, key };
|
|
3302
|
+
transfers.set(locator, held);
|
|
3303
|
+
if (arrived)
|
|
3304
|
+
liveQueries.recover();
|
|
3305
|
+
installing(locator);
|
|
2868
3306
|
liveQueries.recover();
|
|
2869
|
-
|
|
3307
|
+
const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
|
|
3308
|
+
owedIndexes(locator, record.done.bytes - prior, text);
|
|
3309
|
+
const owed = text + (underived.get(locator) ?? 0);
|
|
3310
|
+
underived.set(locator, owed);
|
|
3311
|
+
if (owed < deriveBudget.bytes)
|
|
3312
|
+
return held;
|
|
3313
|
+
return (await derived(locator, key, composition)) ? held : null;
|
|
2870
3314
|
},
|
|
2871
|
-
|
|
3315
|
+
derive: derived,
|
|
3316
|
+
async refreshing(locator, key, composition) {
|
|
2872
3317
|
await exclusive(() => local.unrefresh(key));
|
|
2873
|
-
return refreshingOf(local, key, liveness);
|
|
3318
|
+
return refreshingOf(local, key, composition, liveness, locator);
|
|
2874
3319
|
},
|
|
2875
3320
|
abandon: (locator) => exclusive(async () => {
|
|
2876
3321
|
if (!liveness())
|
|
2877
3322
|
return;
|
|
2878
3323
|
await local.abandon(locator);
|
|
2879
|
-
|
|
3324
|
+
untransferred(locator);
|
|
2880
3325
|
liveQueries.recover();
|
|
2881
3326
|
sweep();
|
|
2882
3327
|
}),
|
|
2883
|
-
setAside:
|
|
3328
|
+
setAside: async (locator) => {
|
|
2884
3329
|
if (!liveness())
|
|
2885
3330
|
return;
|
|
2886
|
-
|
|
2887
|
-
transfers.delete(locator);
|
|
3331
|
+
untransferred(locator);
|
|
2888
3332
|
liveQueries.recover();
|
|
2889
|
-
}
|
|
3333
|
+
},
|
|
2890
3334
|
release: (key) => exclusive(async () => {
|
|
2891
3335
|
await local.release(key);
|
|
2892
3336
|
sweep();
|
|
2893
3337
|
}),
|
|
2894
3338
|
content: (key) => heldContentOf(local, key),
|
|
2895
|
-
validate
|
|
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
|
+
},
|
|
2896
3352
|
};
|
|
2897
3353
|
}
|
|
2898
3354
|
function reusable(room, local) {
|
|
@@ -2910,19 +3366,21 @@ export function composeRuntime(options) {
|
|
|
2910
3366
|
liveQueries.recover();
|
|
2911
3367
|
}
|
|
2912
3368
|
try {
|
|
2913
|
-
const result = await bootstrapRoom(reads, authorized, room, {
|
|
2914
|
-
collection: modelOf(room.locator),
|
|
2915
|
-
selection: modelOf(room.locator, 'typed-selection'),
|
|
2916
|
-
}, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
|
|
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));
|
|
2917
3370
|
const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
|
|
2918
3371
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
2919
|
-
if (result.kind !== 'staged')
|
|
3372
|
+
if (result.kind !== 'staged') {
|
|
2920
3373
|
caughtUpTo(room.locator);
|
|
3374
|
+
installed(room.locator);
|
|
3375
|
+
}
|
|
2921
3376
|
return result;
|
|
2922
3377
|
}
|
|
2923
3378
|
catch (error) {
|
|
2924
|
-
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
2925
3379
|
caughtUpTo(room.locator);
|
|
3380
|
+
installed(room.locator);
|
|
3381
|
+
if (refusedForSpace(error))
|
|
3382
|
+
return unconfirmed;
|
|
3383
|
+
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
2926
3384
|
throw error;
|
|
2927
3385
|
}
|
|
2928
3386
|
finally {
|
|
@@ -2946,37 +3404,16 @@ export function composeRuntime(options) {
|
|
|
2946
3404
|
}
|
|
2947
3405
|
return advanceOnce(room, local, authorized, live);
|
|
2948
3406
|
}
|
|
2949
|
-
async function advanceOnce(room, local, authorized,
|
|
2950
|
-
|
|
3407
|
+
async function advanceOnce(room, local, authorized, running) {
|
|
3408
|
+
const live = () => running() && !full;
|
|
3409
|
+
if (!live())
|
|
2951
3410
|
return unconfirmed;
|
|
2952
3411
|
const held = replicas.get(room.locator);
|
|
2953
3412
|
if (held !== undefined) {
|
|
2954
|
-
const refreshed = await
|
|
2955
|
-
if (
|
|
2956
|
-
return
|
|
2957
|
-
if (refreshed
|
|
2958
|
-
stale.delete(room.locator);
|
|
2959
|
-
if (sourceRefusals.has(room.locator))
|
|
2960
|
-
return null;
|
|
2961
|
-
if (unreadable.delete(room.locator)) {
|
|
2962
|
-
scopedRefusals.delete(room.locator);
|
|
2963
|
-
list(room, true);
|
|
2964
|
-
emitRooms();
|
|
2965
|
-
}
|
|
2966
|
-
return null;
|
|
2967
|
-
}
|
|
2968
|
-
if (refreshed.kind === 'advanced') {
|
|
2969
|
-
const landing = { kind: 'refreshed', base: refreshed.base };
|
|
2970
|
-
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
2971
|
-
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
2972
|
-
const reached = now !== undefined &&
|
|
2973
|
-
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
2974
|
-
return reached ? null : advance(room, local, authorized, live);
|
|
2975
|
-
}
|
|
2976
|
-
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
2977
|
-
return null;
|
|
2978
|
-
}
|
|
2979
|
-
if (refreshed.kind !== 'beyond-horizon')
|
|
3413
|
+
const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold));
|
|
3414
|
+
if (refreshed === 'behind')
|
|
3415
|
+
return advance(room, local, authorized, live);
|
|
3416
|
+
if (refreshed !== 'beyond-horizon')
|
|
2980
3417
|
return refreshed;
|
|
2981
3418
|
if (opened !== null) {
|
|
2982
3419
|
owed.add(room.locator);
|
|
@@ -2987,12 +3424,39 @@ export function composeRuntime(options) {
|
|
|
2987
3424
|
return unconfirmed;
|
|
2988
3425
|
}
|
|
2989
3426
|
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
3427
|
+
if (bootstrapped.kind === 'staged') {
|
|
3428
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
3429
|
+
return live() ? null : unconfirmed;
|
|
3430
|
+
}
|
|
3431
|
+
return live() ? bootstrapped : unconfirmed;
|
|
3432
|
+
}
|
|
3433
|
+
async function refreshedRoom(room, local, authorized, held, live, hold) {
|
|
3434
|
+
const refreshed = await pullRefresh(authorized, held, live, hold);
|
|
2990
3435
|
if (!live())
|
|
2991
3436
|
return unconfirmed;
|
|
2992
|
-
if (
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
3437
|
+
if (refreshed.kind === 'still-current') {
|
|
3438
|
+
stale.delete(room.locator);
|
|
3439
|
+
if (sourceRefusals.has(room.locator))
|
|
3440
|
+
return null;
|
|
3441
|
+
if (unreadable.delete(room.locator)) {
|
|
3442
|
+
scopedRefusals.delete(room.locator);
|
|
3443
|
+
list(room, true);
|
|
3444
|
+
emitRooms();
|
|
3445
|
+
}
|
|
3446
|
+
return null;
|
|
3447
|
+
}
|
|
3448
|
+
if (refreshed.kind === 'advanced') {
|
|
3449
|
+
const landing = { kind: 'refreshed', base: refreshed.base };
|
|
3450
|
+
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3451
|
+
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
3452
|
+
const reached = now !== undefined &&
|
|
3453
|
+
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
3454
|
+
return reached ? null : 'behind';
|
|
3455
|
+
}
|
|
3456
|
+
await retainModel(local, room, modelOf(room.locator));
|
|
3457
|
+
return null;
|
|
3458
|
+
}
|
|
3459
|
+
return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
|
|
2996
3460
|
}
|
|
2997
3461
|
async function withhold(withheld, room, local, progress) {
|
|
2998
3462
|
if (withheld.kind === 'unconfirmed')
|
|
@@ -3515,6 +3979,7 @@ export function composeRuntime(options) {
|
|
|
3515
3979
|
locator: room.locator,
|
|
3516
3980
|
key: before.record.key,
|
|
3517
3981
|
roots: before.record.typedSelection?.roots ?? [],
|
|
3982
|
+
composed: composedOf(composed(before.record)),
|
|
3518
3983
|
overlay: { set: [], cleared: [...before.overlaid] },
|
|
3519
3984
|
},
|
|
3520
3985
|
]),
|
|
@@ -3570,16 +4035,17 @@ export function composeRuntime(options) {
|
|
|
3570
4035
|
release();
|
|
3571
4036
|
};
|
|
3572
4037
|
}
|
|
3573
|
-
|
|
3574
|
-
const
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
4038
|
+
function declaredAsHeld(room) {
|
|
4039
|
+
const held = replicas.get(room.locator)?.record;
|
|
4040
|
+
return (held === undefined ||
|
|
4041
|
+
(JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
|
|
4042
|
+
JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
|
|
4043
|
+
JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
|
|
4044
|
+
}
|
|
4045
|
+
async function retainModel(local, room, next) {
|
|
4046
|
+
const key = room.locator;
|
|
4047
|
+
const previous = modelOf(room.locator);
|
|
4048
|
+
if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(room))
|
|
3583
4049
|
return;
|
|
3584
4050
|
if (next.status === 'unavailable') {
|
|
3585
4051
|
invalidModels.add(key);
|
|
@@ -3596,11 +4062,11 @@ export function composeRuntime(options) {
|
|
|
3596
4062
|
operations: room.operations,
|
|
3597
4063
|
replica: room.replica,
|
|
3598
4064
|
typedSelection: room.typedSelection ?? null,
|
|
3599
|
-
|
|
4065
|
+
model: next,
|
|
3600
4066
|
};
|
|
3601
4067
|
let read = 'staged';
|
|
3602
4068
|
if (!sameMembership(declared, held.record)) {
|
|
3603
|
-
const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'),
|
|
4069
|
+
const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), composed(declared));
|
|
3604
4070
|
read =
|
|
3605
4071
|
verdict.verdict !== 'refuse'
|
|
3606
4072
|
? 'staged'
|
|
@@ -3621,14 +4087,13 @@ export function composeRuntime(options) {
|
|
|
3621
4087
|
key: record.key,
|
|
3622
4088
|
record: storedRecord(record),
|
|
3623
4089
|
roots: record.typedSelection?.roots ?? [],
|
|
3624
|
-
|
|
4090
|
+
composed: composedOf(composed(record)),
|
|
3625
4091
|
overlay: diff.overlay,
|
|
3626
4092
|
},
|
|
3627
4093
|
],
|
|
3628
4094
|
});
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count });
|
|
4095
|
+
const { count, indexed } = committedRoom(room.locator, answer);
|
|
4096
|
+
replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
|
|
3632
4097
|
changed = changeSet(room.locator, {
|
|
3633
4098
|
documents: answer.changed[room.locator] ?? [],
|
|
3634
4099
|
room: true,
|
|
@@ -3644,18 +4109,21 @@ export function composeRuntime(options) {
|
|
|
3644
4109
|
declarationRefusals.add(room.locator);
|
|
3645
4110
|
}
|
|
3646
4111
|
}
|
|
3647
|
-
|
|
3648
|
-
if (next.status === 'current') {
|
|
3649
|
-
liveModels.set(key, JSON.stringify(next.snapshot));
|
|
3650
|
-
invalidModels.delete(key);
|
|
3651
|
-
}
|
|
3652
|
-
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
4112
|
+
heldModel(room, next);
|
|
3653
4113
|
if (changed === null)
|
|
3654
4114
|
emitRooms();
|
|
3655
4115
|
else
|
|
3656
4116
|
publishChangeSet(changed);
|
|
3657
4117
|
});
|
|
3658
4118
|
}
|
|
4119
|
+
function heldModel(room, next) {
|
|
4120
|
+
models.set(room.locator, next);
|
|
4121
|
+
if (next.status === 'current') {
|
|
4122
|
+
liveModels.set(room.locator, JSON.stringify(next.snapshot));
|
|
4123
|
+
invalidModels.delete(room.locator);
|
|
4124
|
+
}
|
|
4125
|
+
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
4126
|
+
}
|
|
3659
4127
|
async function scoped(result, room, local) {
|
|
3660
4128
|
if (result.scope === 'content' && !result.retryable) {
|
|
3661
4129
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
@@ -3669,14 +4137,14 @@ export function composeRuntime(options) {
|
|
|
3669
4137
|
});
|
|
3670
4138
|
let scopedAway = room;
|
|
3671
4139
|
if (result.scope === 'model')
|
|
3672
|
-
await retainModel(local, room,
|
|
4140
|
+
await retainModel(local, room, {
|
|
3673
4141
|
status: 'unavailable',
|
|
3674
4142
|
reason: 'not-readable',
|
|
3675
4143
|
});
|
|
3676
4144
|
else if (result.scope !== 'operation') {
|
|
3677
4145
|
unreadable.add(room.locator);
|
|
3678
4146
|
scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
|
|
3679
|
-
await retainModel(local, scopedAway,
|
|
4147
|
+
await retainModel(local, scopedAway, modelOf(room.locator));
|
|
3680
4148
|
}
|
|
3681
4149
|
if (!continuing())
|
|
3682
4150
|
return;
|
|
@@ -3726,7 +4194,7 @@ export function composeRuntime(options) {
|
|
|
3726
4194
|
return false;
|
|
3727
4195
|
}
|
|
3728
4196
|
if (prior !== undefined)
|
|
3729
|
-
await retainModel(local, room,
|
|
4197
|
+
await retainModel(local, room, modelOf(room.locator));
|
|
3730
4198
|
if (discoveryPaused || authorization === undefined)
|
|
3731
4199
|
return false;
|
|
3732
4200
|
return room.replica.admission === 'admitted';
|
|
@@ -3739,10 +4207,8 @@ export function composeRuntime(options) {
|
|
|
3739
4207
|
await withhold(withheld, room, local, progress);
|
|
3740
4208
|
}
|
|
3741
4209
|
function revoked(locator) {
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
3745
|
-
});
|
|
4210
|
+
const model = modelOf(locator);
|
|
4211
|
+
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
3746
4212
|
}
|
|
3747
4213
|
async function loadRoom(room, local, progress, live) {
|
|
3748
4214
|
if (!live() || authorization === undefined)
|
|
@@ -3765,10 +4231,9 @@ export function composeRuntime(options) {
|
|
|
3765
4231
|
if (!live() || authorization === undefined)
|
|
3766
4232
|
return;
|
|
3767
4233
|
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
3768
|
-
if (!live())
|
|
3769
|
-
return;
|
|
3770
4234
|
if (bootstrapped.kind !== 'staged') {
|
|
3771
|
-
|
|
4235
|
+
if (live())
|
|
4236
|
+
await withhold(bootstrapped, room, local, progress);
|
|
3772
4237
|
return;
|
|
3773
4238
|
}
|
|
3774
4239
|
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
@@ -3793,6 +4258,8 @@ export function composeRuntime(options) {
|
|
|
3793
4258
|
catch {
|
|
3794
4259
|
result = { kind: 'transport-uncertainty' };
|
|
3795
4260
|
}
|
|
4261
|
+
if (result.kind === 'discovery')
|
|
4262
|
+
roomsNamed = true;
|
|
3796
4263
|
if (!live()) {
|
|
3797
4264
|
progress.complete = false;
|
|
3798
4265
|
notifyReady();
|
|
@@ -3800,12 +4267,7 @@ export function composeRuntime(options) {
|
|
|
3800
4267
|
}
|
|
3801
4268
|
journal.record(`connection discover ${result.kind}`);
|
|
3802
4269
|
if (result.kind === 'transport-uncertainty') {
|
|
3803
|
-
|
|
3804
|
-
await retainModel(local, room, 'collection', cached(modelOf(room.locator)));
|
|
3805
|
-
if (room.typedSelection !== null) {
|
|
3806
|
-
await retainModel(local, room, 'typed-selection', cached(modelOf(room.locator, 'typed-selection')));
|
|
3807
|
-
}
|
|
3808
|
-
}
|
|
4270
|
+
await settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
|
|
3809
4271
|
progress.reachable = false;
|
|
3810
4272
|
progress.complete = false;
|
|
3811
4273
|
notifyReady();
|
|
@@ -3860,6 +4322,7 @@ export function composeRuntime(options) {
|
|
|
3860
4322
|
limits = result.providerLimits;
|
|
3861
4323
|
await local.fact('providerLimits', limits);
|
|
3862
4324
|
await local.fact('capabilities', capabilities);
|
|
4325
|
+
conversations.declared();
|
|
3863
4326
|
}
|
|
3864
4327
|
const signedIn = result.member ?? null;
|
|
3865
4328
|
if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
|
|
@@ -3917,7 +4380,7 @@ export function composeRuntime(options) {
|
|
|
3917
4380
|
function sync(attempt) {
|
|
3918
4381
|
return task(OPENING_LANE, async (live) => {
|
|
3919
4382
|
if (!live()) {
|
|
3920
|
-
|
|
4383
|
+
readyFromStore();
|
|
3921
4384
|
return;
|
|
3922
4385
|
}
|
|
3923
4386
|
const progress = { complete: true, reachable: true };
|
|
@@ -3995,6 +4458,7 @@ export function composeRuntime(options) {
|
|
|
3995
4458
|
});
|
|
3996
4459
|
}
|
|
3997
4460
|
function closedSocket(revoked, lost) {
|
|
4461
|
+
notifyDiscovered();
|
|
3998
4462
|
const opened = socketOpen;
|
|
3999
4463
|
socketOpen = false;
|
|
4000
4464
|
connectionLost();
|
|
@@ -4038,13 +4502,9 @@ export function composeRuntime(options) {
|
|
|
4038
4502
|
const local = await openLocal();
|
|
4039
4503
|
const reloaded = await replacing;
|
|
4040
4504
|
if (progress.reachable) {
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
!reloading.includes(room.locator)) {
|
|
4045
|
-
await confirmModels(room, local);
|
|
4046
|
-
}
|
|
4047
|
-
}
|
|
4505
|
+
await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
|
|
4506
|
+
!expired.includes(room.locator) &&
|
|
4507
|
+
!reloading.includes(room.locator)), 'current', local);
|
|
4048
4508
|
}
|
|
4049
4509
|
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
4050
4510
|
if (failed !== undefined)
|
|
@@ -4085,12 +4545,30 @@ export function composeRuntime(options) {
|
|
|
4085
4545
|
return [];
|
|
4086
4546
|
}
|
|
4087
4547
|
}
|
|
4088
|
-
async function
|
|
4089
|
-
|
|
4090
|
-
const model = modelOf(room.locator
|
|
4091
|
-
|
|
4092
|
-
|
|
4548
|
+
async function settleModels(held, status, local) {
|
|
4549
|
+
const owed = (room) => {
|
|
4550
|
+
const model = modelOf(room.locator);
|
|
4551
|
+
return model.status !== 'unavailable' && model.status !== status;
|
|
4552
|
+
};
|
|
4553
|
+
const settled = await exclusive(async () => {
|
|
4554
|
+
if (!continuing())
|
|
4555
|
+
return [];
|
|
4556
|
+
const unchanged = held.filter((room) => owed(room) && !declarationRefusals.has(room.locator) && declaredAsHeld(room));
|
|
4557
|
+
for (const room of unchanged) {
|
|
4558
|
+
const next = { ...modelOf(room.locator), status };
|
|
4559
|
+
const replica = replicas.get(room.locator);
|
|
4560
|
+
if (replica !== undefined)
|
|
4561
|
+
replicas.set(room.locator, { ...replica, record: { ...replica.record, model: next } });
|
|
4562
|
+
heldModel(room, next);
|
|
4093
4563
|
}
|
|
4564
|
+
if (unchanged.length > 0)
|
|
4565
|
+
emitRooms();
|
|
4566
|
+
return unchanged;
|
|
4567
|
+
});
|
|
4568
|
+
for (const room of held) {
|
|
4569
|
+
if (settled.includes(room) || !owed(room))
|
|
4570
|
+
continue;
|
|
4571
|
+
await retainModel(local, room, { ...modelOf(room.locator), status });
|
|
4094
4572
|
}
|
|
4095
4573
|
}
|
|
4096
4574
|
function submitLater(locator) {
|
|
@@ -4203,8 +4681,6 @@ export function composeRuntime(options) {
|
|
|
4203
4681
|
if (repairable.has(locator))
|
|
4204
4682
|
repairLater(locator, {});
|
|
4205
4683
|
await conversations.reconcile();
|
|
4206
|
-
if (status.providerUnreachable)
|
|
4207
|
-
conversations.reconnected();
|
|
4208
4684
|
const reading = { complete: true, reachable: true };
|
|
4209
4685
|
await conversations.catchUp(reading);
|
|
4210
4686
|
if (progress.reachable && reading.reachable)
|
|
@@ -4312,6 +4788,7 @@ export function composeRuntime(options) {
|
|
|
4312
4788
|
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
4313
4789
|
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
4314
4790
|
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
4791
|
+
ahead,
|
|
4315
4792
|
};
|
|
4316
4793
|
function observeStream() {
|
|
4317
4794
|
const stream = options.adapter.roomStream;
|
|
@@ -4324,6 +4801,8 @@ export function composeRuntime(options) {
|
|
|
4324
4801
|
socketOpen = true;
|
|
4325
4802
|
connectionLive();
|
|
4326
4803
|
generation += 1;
|
|
4804
|
+
for (const admit of installWaiters.splice(0))
|
|
4805
|
+
admit();
|
|
4327
4806
|
reads.opened();
|
|
4328
4807
|
conversations.reopened();
|
|
4329
4808
|
recovery.resume();
|
|
@@ -4343,7 +4822,7 @@ export function composeRuntime(options) {
|
|
|
4343
4822
|
const held = replicas.get(locator);
|
|
4344
4823
|
if (held === undefined || paths.length === 0)
|
|
4345
4824
|
return new Map();
|
|
4346
|
-
const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
|
|
4825
|
+
const entries = await ahead(async () => (await openLocal()).entries(held.record.key, 'confirmed', paths));
|
|
4347
4826
|
return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
|
|
4348
4827
|
}
|
|
4349
4828
|
function writesHeld() {
|
|
@@ -4414,8 +4893,11 @@ export function composeRuntime(options) {
|
|
|
4414
4893
|
if (closed)
|
|
4415
4894
|
return;
|
|
4416
4895
|
closed = true;
|
|
4417
|
-
|
|
4418
|
-
|
|
4896
|
+
notifyDiscovered();
|
|
4897
|
+
if (probing !== null)
|
|
4898
|
+
clearInterval(probing);
|
|
4899
|
+
for (const resolve of shownWaiters.splice(0))
|
|
4900
|
+
resolve();
|
|
4419
4901
|
connectionLive();
|
|
4420
4902
|
lockListeners.clear();
|
|
4421
4903
|
readinessWatches.clear();
|
|
@@ -4427,6 +4909,7 @@ export function composeRuntime(options) {
|
|
|
4427
4909
|
conversations.close();
|
|
4428
4910
|
void recovery.retire();
|
|
4429
4911
|
roomListeners.clear();
|
|
4912
|
+
unlistedRooms.clear();
|
|
4430
4913
|
refusalListeners.clear();
|
|
4431
4914
|
storageLossListeners.clear();
|
|
4432
4915
|
statusListeners.clear();
|
|
@@ -4452,9 +4935,9 @@ export function composeRuntime(options) {
|
|
|
4452
4935
|
observeStream();
|
|
4453
4936
|
void openLocal().then(() => {
|
|
4454
4937
|
if (capabilities.length > 0)
|
|
4455
|
-
|
|
4938
|
+
readyFromStore();
|
|
4456
4939
|
}, () => {
|
|
4457
|
-
|
|
4940
|
+
readyFromStore();
|
|
4458
4941
|
});
|
|
4459
4942
|
if (options.adapter.roomStream === undefined)
|
|
4460
4943
|
void sync('unstreamed');
|
|
@@ -4602,12 +5085,13 @@ export function composeRuntime(options) {
|
|
|
4602
5085
|
};
|
|
4603
5086
|
}
|
|
4604
5087
|
if (topic === 'readiness') {
|
|
4605
|
-
return watchReadiness({ target: '', code: 'stored',
|
|
5088
|
+
return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
|
|
4606
5089
|
}
|
|
4607
5090
|
if (topic === 'rooms') {
|
|
4608
|
-
|
|
4609
|
-
|
|
5091
|
+
unlistedRooms.add(deliver);
|
|
5092
|
+
emitKnownRooms();
|
|
4610
5093
|
return () => {
|
|
5094
|
+
unlistedRooms.delete(deliver);
|
|
4611
5095
|
roomListeners.delete(deliver);
|
|
4612
5096
|
};
|
|
4613
5097
|
}
|
|
@@ -4661,7 +5145,17 @@ export function composeRuntime(options) {
|
|
|
4661
5145
|
return openLocal().then(() => undefined);
|
|
4662
5146
|
},
|
|
4663
5147
|
rooms() {
|
|
4664
|
-
|
|
5148
|
+
if (closed)
|
|
5149
|
+
return Promise.resolve([]);
|
|
5150
|
+
return new Promise((settle) => {
|
|
5151
|
+
const answer = (value) => {
|
|
5152
|
+
unlistedRooms.delete(answer);
|
|
5153
|
+
roomListeners.delete(answer);
|
|
5154
|
+
settle(value);
|
|
5155
|
+
};
|
|
5156
|
+
unlistedRooms.add(answer);
|
|
5157
|
+
emitKnownRooms();
|
|
5158
|
+
});
|
|
4665
5159
|
},
|
|
4666
5160
|
query: ((input) => rowsQuery(planned(input, null))),
|
|
4667
5161
|
search: (input) => searched(input, null, []),
|
|
@@ -4671,7 +5165,7 @@ export function composeRuntime(options) {
|
|
|
4671
5165
|
typedSelection: rooms.get(locator)?.typedSelection === undefined ||
|
|
4672
5166
|
rooms.get(locator)?.typedSelection === null
|
|
4673
5167
|
? null
|
|
4674
|
-
: modelTypes(
|
|
5168
|
+
: modelTypes(builtIn),
|
|
4675
5169
|
};
|
|
4676
5170
|
},
|
|
4677
5171
|
roomDraft: {
|
|
@@ -4688,33 +5182,17 @@ export function composeRuntime(options) {
|
|
|
4688
5182
|
const record = roomDrafts.get(room)?.get(path);
|
|
4689
5183
|
return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
|
|
4690
5184
|
},
|
|
4691
|
-
write(room, path, revision, input) {
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
const
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
? undefined
|
|
4703
|
-
: await heldFile(parts.file.blob, binaryBase(await visibleOf(room, [path]), path));
|
|
4704
|
-
const record = {
|
|
4705
|
-
locator: room,
|
|
4706
|
-
path,
|
|
4707
|
-
revision: roomDraftRevision(),
|
|
4708
|
-
parts: held(current?.parts, parts, file),
|
|
4709
|
-
returned: current?.returned ?? null,
|
|
4710
|
-
};
|
|
4711
|
-
await local.keepRoomDraft(record, parts.file === undefined || file === undefined
|
|
4712
|
-
? []
|
|
4713
|
-
: [{ key: file.localId, blob: parts.file.blob }]);
|
|
4714
|
-
rememberRoomDraft(record);
|
|
4715
|
-
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
4716
|
-
return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
|
|
4717
|
-
});
|
|
5185
|
+
write(room, path, revision, input, next = roomDraftRevision()) {
|
|
5186
|
+
if (typeof next !== 'string' || next === '')
|
|
5187
|
+
return Promise.reject(runtimeError('room-draft-malformed'));
|
|
5188
|
+
return gathered(`room-draft\u0000${room}\u0000${path}`, { revision, input, next }, (earlier, later) => {
|
|
5189
|
+
if (later.revision !== earlier.next)
|
|
5190
|
+
return null;
|
|
5191
|
+
const parts = gatheredParts(earlier.input, later.input);
|
|
5192
|
+
return parts === null
|
|
5193
|
+
? null
|
|
5194
|
+
: { revision: earlier.revision, input: parts, next: later.next };
|
|
5195
|
+
}, (written) => writtenRoomDraft(room, path, written.revision, written.input, written.next));
|
|
4718
5196
|
},
|
|
4719
5197
|
discard(room, path, revision) {
|
|
4720
5198
|
return exclusive(async () => {
|
|
@@ -4741,13 +5219,13 @@ export function composeRuntime(options) {
|
|
|
4741
5219
|
});
|
|
4742
5220
|
},
|
|
4743
5221
|
write(app, value) {
|
|
4744
|
-
return
|
|
5222
|
+
return exclusive(() => keptPresentation(app, () => structuredClone(value)));
|
|
4745
5223
|
},
|
|
4746
5224
|
edit(app, members) {
|
|
4747
|
-
return
|
|
4748
|
-
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(
|
|
4749
|
-
...Object.entries(
|
|
4750
|
-
]));
|
|
5225
|
+
return gathered(`presentation\u0000${app}`, structuredClone(members), (earlier, later) => ({ ...earlier, ...later }), (edited) => keptPresentation(app, (held) => Object.fromEntries([
|
|
5226
|
+
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(edited, member)),
|
|
5227
|
+
...Object.entries(edited).filter(([, value]) => value !== null),
|
|
5228
|
+
])));
|
|
4751
5229
|
},
|
|
4752
5230
|
},
|
|
4753
5231
|
save(room, named, writtenBy) {
|
|
@@ -4766,7 +5244,8 @@ export function composeRuntime(options) {
|
|
|
4766
5244
|
part: readPart,
|
|
4767
5245
|
read: readBinaryPath,
|
|
4768
5246
|
link: linkBinaryPath,
|
|
4769
|
-
|
|
5247
|
+
show: showBinaryPath,
|
|
5248
|
+
attach: attachBinary,
|
|
4770
5249
|
},
|
|
4771
5250
|
async call(name, input) {
|
|
4772
5251
|
await ready;
|
|
@@ -4789,7 +5268,8 @@ export function composeRuntime(options) {
|
|
|
4789
5268
|
journal.record('retry');
|
|
4790
5269
|
unpreparable.clear();
|
|
4791
5270
|
queueReadiness();
|
|
4792
|
-
|
|
5271
|
+
if (full)
|
|
5272
|
+
await probed();
|
|
4793
5273
|
},
|
|
4794
5274
|
observe: observing(watch),
|
|
4795
5275
|
async reauthorize() {
|
|
@@ -4811,7 +5291,6 @@ export function composeRuntime(options) {
|
|
|
4811
5291
|
const reachable = {
|
|
4812
5292
|
md,
|
|
4813
5293
|
watch,
|
|
4814
|
-
referrerScope: (referrer) => options.adapter.referrerScope(referrer),
|
|
4815
5294
|
plan: planned,
|
|
4816
5295
|
query: (plan) => rowsQuery(plan),
|
|
4817
5296
|
watchPlan,
|
|
@@ -4821,6 +5300,7 @@ export function composeRuntime(options) {
|
|
|
4821
5300
|
lockedRooms,
|
|
4822
5301
|
watchLocks,
|
|
4823
5302
|
located,
|
|
5303
|
+
room: (locator) => listed().find((one) => one.locator === locator),
|
|
4824
5304
|
};
|
|
4825
5305
|
return {
|
|
4826
5306
|
md,
|
|
@@ -4835,6 +5315,11 @@ export function composeRuntime(options) {
|
|
|
4835
5315
|
journal.record(`persistence ${answer}`);
|
|
4836
5316
|
queueReadiness();
|
|
4837
5317
|
},
|
|
4838
|
-
|
|
5318
|
+
shown: screen,
|
|
5319
|
+
within: (reach, port) => {
|
|
5320
|
+
if (full)
|
|
5321
|
+
void probed();
|
|
5322
|
+
return narrowed(reachable, reach, port);
|
|
5323
|
+
},
|
|
4839
5324
|
};
|
|
4840
5325
|
}
|