@agentshouse/mdruntime 0.1.0-cc5745cd9af96 → 0.1.0-ccbe92df1c4f6
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/AGENTS.md +96 -78
- package/README.md +9 -8
- package/dist/adapter.d.ts +40 -32
- package/dist/adapter.d.ts.map +1 -1
- package/dist/attached-model.d.ts +4 -1
- package/dist/attached-model.d.ts.map +1 -1
- package/dist/attached-model.js +13 -3
- 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 +24 -6
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +148 -82
- package/dist/chain.d.ts +11 -0
- package/dist/chain.d.ts.map +1 -0
- package/dist/chain.js +64 -0
- package/dist/change-set.d.ts +6 -16
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +11 -94
- package/dist/change.d.ts +31 -1
- package/dist/change.d.ts.map +1 -1
- package/dist/change.js +10 -1
- package/dist/chat/house.d.ts +25 -0
- package/dist/chat/house.d.ts.map +1 -0
- package/dist/chat/house.js +38 -0
- package/dist/chat/input.d.ts +3 -1
- package/dist/chat/input.d.ts.map +1 -1
- package/dist/chat/input.js +11 -18
- package/dist/chat/list.d.ts.map +1 -1
- package/dist/chat/list.js +1 -1
- package/dist/chat/markdown.d.ts +1 -2
- package/dist/chat/markdown.d.ts.map +1 -1
- package/dist/chat/markdown.js +2 -19
- package/dist/chat/message.d.ts.map +1 -1
- package/dist/chat/message.js +63 -11
- package/dist/chat/messages.d.ts +12 -1
- package/dist/chat/messages.d.ts.map +1 -1
- package/dist/chat/messages.js +36 -13
- package/dist/chat/name.d.ts +1 -0
- package/dist/chat/name.d.ts.map +1 -1
- package/dist/chat/name.js +1 -1
- package/dist/chat/opened.d.ts +1 -0
- package/dist/chat/opened.d.ts.map +1 -1
- package/dist/chat/opened.js +2 -0
- package/dist/chat/overlay.d.ts.map +1 -1
- package/dist/chat/overlay.js +1 -5
- package/dist/chat/parts.d.ts +1 -0
- package/dist/chat/parts.d.ts.map +1 -1
- package/dist/chat/parts.js +3 -2
- package/dist/chat/provider.d.ts +7 -1
- package/dist/chat/provider.d.ts.map +1 -1
- package/dist/chat/provider.js +75 -78
- package/dist/chat/ready.d.ts +4 -1
- package/dist/chat/ready.d.ts.map +1 -1
- package/dist/chat/ready.js +7 -5
- package/dist/chat/room-file.d.ts +8 -0
- package/dist/chat/room-file.d.ts.map +1 -0
- package/dist/chat/room-file.js +72 -0
- package/dist/chat/style.d.ts.map +1 -1
- package/dist/chat/style.js +5 -0
- package/dist/chat.d.ts +1 -0
- package/dist/chat.d.ts.map +1 -1
- package/dist/client.d.ts +6 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +28 -2
- package/dist/collection.d.ts +6 -20
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +30 -352
- package/dist/compile.d.ts +16 -0
- package/dist/compile.d.ts.map +1 -0
- package/dist/compile.js +721 -0
- package/dist/connection.d.ts +13 -8
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +2179 -817
- package/dist/conversation.d.ts +5 -3
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +84 -65
- package/dist/digest.d.ts +7 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +113 -0
- package/dist/durability.d.ts +3 -5
- package/dist/durability.d.ts.map +1 -1
- package/dist/durability.js +5 -0
- package/dist/entry.d.ts +3 -1
- package/dist/entry.d.ts.map +1 -1
- package/dist/entry.js +18 -24
- package/dist/file.d.ts +2 -10
- package/dist/file.d.ts.map +1 -1
- package/dist/file.js +3 -120
- package/dist/fulltext.d.ts +27 -0
- package/dist/fulltext.d.ts.map +1 -0
- package/dist/fulltext.js +368 -0
- package/dist/host.d.ts +4 -4
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +16 -8
- package/dist/identity.d.ts +0 -1
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +0 -3
- package/dist/index.d.ts +12 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/live.d.ts +22 -0
- package/dist/live.d.ts.map +1 -0
- package/dist/live.js +123 -0
- package/dist/memory.d.ts +1 -0
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +20 -15
- package/dist/messages.d.ts +28 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +90 -0
- package/dist/name.d.ts +4 -0
- package/dist/name.d.ts.map +1 -0
- package/dist/name.js +122 -0
- package/dist/observation.d.ts +12 -0
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +84 -0
- package/dist/opened.d.ts +22 -0
- package/dist/opened.d.ts.map +1 -0
- package/dist/opened.js +107 -0
- package/dist/outcome.d.ts +28 -4
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +22 -0
- package/dist/overlay.d.ts +1 -4
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +22 -148
- package/dist/query.d.ts +5 -1
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +13 -1
- package/dist/reach.d.ts +5 -3
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +149 -61
- package/dist/readiness.d.ts +19 -9
- package/dist/readiness.d.ts.map +1 -1
- package/dist/readiness.js +2 -2
- package/dist/reads.d.ts.map +1 -1
- package/dist/reads.js +4 -2
- package/dist/recovery.d.ts +2 -2
- package/dist/recovery.d.ts.map +1 -1
- package/dist/recovery.js +8 -7
- package/dist/refresh.d.ts +15 -14
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +112 -70
- package/dist/replica.d.ts +57 -39
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +275 -195
- package/dist/room-draft.d.ts +3 -1
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +21 -13
- package/dist/rows.d.ts +29 -31
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +113 -69
- package/dist/save.d.ts +49 -14
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +352 -98
- package/dist/search.d.ts +11 -43
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +0 -213
- package/dist/shared.d.ts +4 -3
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +67 -34
- package/dist/store.d.ts +87 -94
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +314 -720
- package/dist/stored.d.ts +145 -0
- package/dist/stored.d.ts.map +1 -0
- package/dist/stored.js +567 -0
- package/dist/tables.d.ts +344 -0
- package/dist/tables.d.ts.map +1 -0
- package/dist/tables.js +580 -0
- package/dist/turn.d.ts +1 -1
- package/dist/turn.d.ts.map +1 -1
- package/dist/turn.js +31 -20
- package/dist/validation.d.ts +30 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +162 -0
- package/dist/windows.d.ts +2 -6
- package/dist/windows.d.ts.map +1 -1
- package/dist/windows.js +30 -34
- package/dist/wire.d.ts +13 -1
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +4 -1
- package/dist/words.d.ts +19 -0
- package/dist/words.d.ts.map +1 -0
- package/dist/words.js +150 -0
- package/package.json +11 -5
- package/dist/read-side.d.ts +0 -27
- package/dist/read-side.d.ts.map +0 -1
- package/dist/read-side.js +0 -826
package/dist/connection.js
CHANGED
|
@@ -1,47 +1,41 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { escapePointer, parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
|
|
2
|
+
import { attachedModel, NO_ROOM_TYPES, roomTypesOf } from './attached-model.js';
|
|
2
3
|
import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
|
|
3
4
|
import { observing, whole } from './observation.js';
|
|
4
5
|
import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
|
|
5
|
-
import {
|
|
6
|
+
import { createLiveQueries } from './live.js';
|
|
6
7
|
import { seededResult } from './seed.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
8
|
+
import { documentKey, roomOfKey } from './rows.js';
|
|
9
|
+
import { searchPlanOf } from './search.js';
|
|
9
10
|
import { capped, MAXIMUM_WINDOW } from './windows.js';
|
|
10
11
|
import { bootstrapRoom } from './bootstrap.js';
|
|
11
|
-
import { changeSet,
|
|
12
|
+
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
12
13
|
import { createConversationStore, } from './conversation.js';
|
|
13
|
-
import { queuedFile } from './change.js';
|
|
14
|
-
import {
|
|
14
|
+
import { leaving as leavingIn, partnerOf, queuedFile, } from './change.js';
|
|
15
|
+
import { available, collectionOf, unreadable as unreadableIn, validRoot, } from './collection.js';
|
|
15
16
|
import { runtimeError } from './failure.js';
|
|
16
17
|
import { createLog } from './log.js';
|
|
17
18
|
import { binaryRecord } from './binary.js';
|
|
18
|
-
import {
|
|
19
|
+
import { openedFiles } from './opened.js';
|
|
20
|
+
import { ABSENT_TARGET, absentBinary, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
|
|
19
21
|
import { createRecovery } from './recovery.js';
|
|
20
22
|
import { doorReads } from './reads.js';
|
|
21
|
-
import {
|
|
22
|
-
import { buildBatch, joined,
|
|
23
|
-
import {
|
|
23
|
+
import { advancement } from './refresh.js';
|
|
24
|
+
import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
|
|
25
|
+
import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, sameMembership, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
|
|
24
26
|
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
25
27
|
import { same, saving, STARTING, transferring, } from './status.js';
|
|
26
|
-
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
27
|
-
import { readinessOf,
|
|
28
|
+
import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
|
|
29
|
+
import { readinessOf, } from './readiness.js';
|
|
28
30
|
import { FencedMap } from './fence.js';
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
+
import { sameBinary } from './overlay.js';
|
|
32
|
+
import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
|
|
31
33
|
import { identityTime, roomDraftRevision } from './identity.js';
|
|
32
34
|
import { beneath, normalize } from './paths.js';
|
|
33
35
|
import { openRuntimeStore, } from './store.js';
|
|
36
|
+
import { changedCleanly, sameComposition, validatedCollection, } from './validation.js';
|
|
34
37
|
import { unconfirmed } from './transfer.js';
|
|
35
38
|
import { governs, holds, narrowed, planReach, } from './reach.js';
|
|
36
|
-
function roomsAlone(plan) {
|
|
37
|
-
return (plan.reach === null &&
|
|
38
|
-
plan.root.types === null &&
|
|
39
|
-
plan.root.where === null &&
|
|
40
|
-
plan.root.pathPrefix === null &&
|
|
41
|
-
plan.paths === null &&
|
|
42
|
-
plan.hop === null &&
|
|
43
|
-
plan.backlinks === null);
|
|
44
|
-
}
|
|
45
39
|
function within(root, path) {
|
|
46
40
|
return root === '' || beneath(root, path);
|
|
47
41
|
}
|
|
@@ -59,24 +53,30 @@ const OPENING_LANE = 'opening';
|
|
|
59
53
|
const CONVERSATION_LANE = 'conversations';
|
|
60
54
|
const ROOM_READS = 256;
|
|
61
55
|
const ROOM_ATTEMPTS = 16;
|
|
56
|
+
const ERASE_BATCH = 512;
|
|
57
|
+
const REDERIVE_BATCH = 256;
|
|
58
|
+
const WALK_BYTES = 1 << 20;
|
|
59
|
+
const QUERY_BYTES = 1 << 20;
|
|
60
|
+
const STAGE_BYTES = 1 << 20;
|
|
61
|
+
const STAGE_FLOOR = 1 << 16;
|
|
62
|
+
// A batch aims at this much database work, so a Save or a query waits behind at most one short write.
|
|
63
|
+
const STAGE_AIM_MS = 50;
|
|
64
|
+
const PARTS_HELD = 4;
|
|
65
|
+
// Bytes still arriving are reported at most this often, well inside the 2 s progress bound, so readiness is not rebuilt for every chunk the network hands over.
|
|
66
|
+
const ARRIVAL_REPORT_MS = 250;
|
|
67
|
+
const INSTALL_STEPS = [
|
|
68
|
+
'writing',
|
|
69
|
+
'downloading',
|
|
70
|
+
'replaying',
|
|
71
|
+
'listing',
|
|
72
|
+
];
|
|
73
|
+
const CLEAN_CHANGES = 2_000;
|
|
74
|
+
// A Room that locks offline stays readable this long after the connection drops, so a laptop waking or changing network keeps working while a disconnected device locks soon after.
|
|
75
|
+
const LOCK_GRACE_MS = 120_000;
|
|
62
76
|
function operationOf(input) {
|
|
63
77
|
const operation = input?.operation_id;
|
|
64
78
|
return typeof operation === 'string' ? operation : null;
|
|
65
79
|
}
|
|
66
|
-
const outside = new Map();
|
|
67
|
-
function outsideOf(roots) {
|
|
68
|
-
const key = JSON.stringify(roots);
|
|
69
|
-
const held = outside.get(key);
|
|
70
|
-
if (held !== undefined)
|
|
71
|
-
return held;
|
|
72
|
-
const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
|
|
73
|
-
const made = {
|
|
74
|
-
documents: (path) => (markdownPath(path) ? counted(path) : 0),
|
|
75
|
-
binaries: counted,
|
|
76
|
-
};
|
|
77
|
-
outside.set(key, made);
|
|
78
|
-
return made;
|
|
79
|
-
}
|
|
80
80
|
function abandonedOn(signal) {
|
|
81
81
|
return new Promise((resolve) => {
|
|
82
82
|
if (signal.aborted)
|
|
@@ -88,11 +88,14 @@ function abandonedOn(signal) {
|
|
|
88
88
|
function roomLane(locator) {
|
|
89
89
|
return `room ${locator}`;
|
|
90
90
|
}
|
|
91
|
+
function pendingKey(locator, id) {
|
|
92
|
+
return `${locator}\u0000${id}`;
|
|
93
|
+
}
|
|
91
94
|
export function composeRuntime(options) {
|
|
92
95
|
const localSaves = options.adapter.localSaves === true;
|
|
96
|
+
const roomTypes = roomTypesOf(options.adapter.roomTypes);
|
|
93
97
|
let authorization = options.authorization;
|
|
94
98
|
let opening;
|
|
95
|
-
let store;
|
|
96
99
|
const fences = new Map();
|
|
97
100
|
const isFenced = (locator) => fences.has(locator);
|
|
98
101
|
let fencings = 0;
|
|
@@ -176,50 +179,79 @@ export function composeRuntime(options) {
|
|
|
176
179
|
const queueSubscriptions = new Set();
|
|
177
180
|
const roomDraftListeners = new Set();
|
|
178
181
|
const presentationListeners = new Set();
|
|
179
|
-
const
|
|
182
|
+
const liveQueries = createLiveQueries((plan, cover) => answeredPlan(plan, cover));
|
|
180
183
|
let withdrawConversations;
|
|
181
184
|
let currencyQueued = null;
|
|
182
185
|
let socketOpen = false;
|
|
186
|
+
const confirmed = new Set();
|
|
187
|
+
let lockGrace = null;
|
|
188
|
+
const lockListeners = new Set();
|
|
183
189
|
let serviceMode = 'open';
|
|
184
190
|
let haltedRefusal = false;
|
|
185
191
|
let openSignals = 0;
|
|
186
192
|
const serviceModeListeners = new Set();
|
|
187
193
|
let member = null;
|
|
188
194
|
const memberListeners = new Set();
|
|
189
|
-
const prepared = new Set();
|
|
190
|
-
const projecting = new Set();
|
|
191
195
|
const unpreparedDemand = new Set();
|
|
192
|
-
const
|
|
196
|
+
const unpreparable = new Set();
|
|
193
197
|
let preparing = false;
|
|
194
198
|
let targets = {};
|
|
195
199
|
let persistence = options.shell.persistence;
|
|
196
|
-
let
|
|
197
|
-
|
|
198
|
-
let
|
|
200
|
+
let full = false;
|
|
201
|
+
let probing = null;
|
|
202
|
+
let testing = null;
|
|
203
|
+
const database = {
|
|
204
|
+
async ask(request) {
|
|
205
|
+
try {
|
|
206
|
+
return await options.database.ask(request);
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
if (refusedForSpace(error))
|
|
210
|
+
filled();
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
};
|
|
199
215
|
const readinessWatches = new Set();
|
|
200
216
|
let readinessQueued = false;
|
|
201
217
|
const catching = new Set();
|
|
202
218
|
const transfers = new FencedMap(isFenced);
|
|
219
|
+
const installs = new Map();
|
|
220
|
+
const arrivals = new Map();
|
|
221
|
+
let arrivalReported = 0;
|
|
222
|
+
let onScreen = options.shell.onScreen;
|
|
223
|
+
const shownWaiters = [];
|
|
224
|
+
let partsFree = PARTS_HELD;
|
|
225
|
+
const partWaiters = [];
|
|
226
|
+
let stageBytes = STAGE_FLOOR * 4;
|
|
227
|
+
let installWrites = Promise.resolve();
|
|
203
228
|
const conversations = createConversationStore({
|
|
204
229
|
agents: declaredAgents,
|
|
205
|
-
documents: (
|
|
230
|
+
documents: (declaration, after) => rowsQuery(planOf({
|
|
231
|
+
rooms: [declaration.locator],
|
|
232
|
+
types: [declaration.type],
|
|
233
|
+
select: {
|
|
234
|
+
fields: [
|
|
235
|
+
declaration.identity,
|
|
236
|
+
declaration.name,
|
|
237
|
+
...(declaration.retired === undefined ? [] : [declaration.retired]),
|
|
238
|
+
].map((field) => `/${escapePointer(field)}`),
|
|
239
|
+
},
|
|
240
|
+
limit: MAXIMUM_WINDOW,
|
|
241
|
+
...(after === undefined ? {} : { after }),
|
|
242
|
+
}, roomFacts())),
|
|
206
243
|
call: (name, input) => invokeAction(name, input),
|
|
207
244
|
durably,
|
|
208
245
|
stream: () => options.adapter.conversations,
|
|
209
246
|
publish: publishChangeSet,
|
|
210
247
|
online: () => connectionOf() !== 'unreachable' && refusal === null,
|
|
248
|
+
full: () => full,
|
|
211
249
|
closed: () => closed,
|
|
212
250
|
unanswered: resendLater,
|
|
213
251
|
expired: (operation) => pastHorizon(operation),
|
|
214
252
|
});
|
|
215
|
-
const searchIndex = createSearchIndex({
|
|
216
|
-
room: (room) => readSide.room(room),
|
|
217
|
-
document: (room, path) => readSide.document(room, path),
|
|
218
|
-
});
|
|
219
|
-
const roomsToIndex = new Set();
|
|
220
|
-
const conversationsToIndex = new Set();
|
|
221
|
-
let indexing = false;
|
|
222
253
|
const journal = createLog();
|
|
254
|
+
const keptFiles = openedFiles(database, isFenced);
|
|
223
255
|
let notifyReady = () => { };
|
|
224
256
|
const ready = new Promise((resolve) => {
|
|
225
257
|
notifyReady = resolve;
|
|
@@ -237,16 +269,36 @@ export function composeRuntime(options) {
|
|
|
237
269
|
if (room.size === 0)
|
|
238
270
|
roomDrafts.delete(locator);
|
|
239
271
|
}
|
|
240
|
-
function
|
|
272
|
+
async function visibleOf(locator, paths) {
|
|
273
|
+
const held = replicas.get(locator);
|
|
274
|
+
if (held === undefined)
|
|
275
|
+
return undefined;
|
|
276
|
+
const local = await openLocal();
|
|
277
|
+
const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
|
|
278
|
+
return { ...slice, collection: collectionOf(slice.documents, compositionOf(held.record)) };
|
|
279
|
+
}
|
|
280
|
+
function roomDraftOf(record, visible) {
|
|
241
281
|
return structuredClone({
|
|
242
282
|
room: record.locator,
|
|
243
283
|
path: record.path,
|
|
244
284
|
revision: record.revision,
|
|
245
285
|
parts: record.parts,
|
|
246
|
-
changedUnderneath: underneath(record.parts, record.path,
|
|
286
|
+
changedUnderneath: underneath(record.parts, record.path, visible),
|
|
247
287
|
returned: record.returned,
|
|
248
288
|
});
|
|
249
289
|
}
|
|
290
|
+
async function roomDraftsOf(records) {
|
|
291
|
+
const rooms = new Map();
|
|
292
|
+
for (const record of records) {
|
|
293
|
+
const paths = rooms.get(record.locator) ?? [];
|
|
294
|
+
paths.push(record.path);
|
|
295
|
+
rooms.set(record.locator, paths);
|
|
296
|
+
}
|
|
297
|
+
const layers = new Map();
|
|
298
|
+
for (const [locator, paths] of rooms)
|
|
299
|
+
layers.set(locator, await visibleOf(locator, paths));
|
|
300
|
+
return records.map((record) => roomDraftOf(record, layers.get(record.locator)));
|
|
301
|
+
}
|
|
250
302
|
function snapshotRoomDrafts() {
|
|
251
303
|
const records = [...roomDrafts.values()].flatMap((room) => [...room.values()]);
|
|
252
304
|
records.sort((left, right) => {
|
|
@@ -256,18 +308,24 @@ export function composeRuntime(options) {
|
|
|
256
308
|
return left.path < right.path ? -1 : 1;
|
|
257
309
|
return 0;
|
|
258
310
|
});
|
|
259
|
-
return records
|
|
311
|
+
return roomDraftsOf(records);
|
|
260
312
|
}
|
|
313
|
+
let draftsShown = Promise.resolve();
|
|
261
314
|
function publishRoomDrafts(waitingOnly) {
|
|
262
315
|
if (closed)
|
|
263
316
|
return;
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
listener
|
|
270
|
-
|
|
317
|
+
const snapshot = snapshotRoomDrafts();
|
|
318
|
+
draftsShown = draftsShown.then(async () => {
|
|
319
|
+
const value = await snapshot.catch(() => null);
|
|
320
|
+
if (value === null || closed)
|
|
321
|
+
return;
|
|
322
|
+
for (const listener of roomDraftListeners) {
|
|
323
|
+
if (waitingOnly && listener.ready)
|
|
324
|
+
continue;
|
|
325
|
+
listener.ready = true;
|
|
326
|
+
listener.notify(value);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
271
329
|
}
|
|
272
330
|
function presentationOf(app) {
|
|
273
331
|
const value = presentations.get(app);
|
|
@@ -279,7 +337,7 @@ export function composeRuntime(options) {
|
|
|
279
337
|
const stored = next(presentations.get(app));
|
|
280
338
|
await local.present({ app, value: stored });
|
|
281
339
|
presentations.set(app, stored);
|
|
282
|
-
publishChangeSet(changeSet('',
|
|
340
|
+
publishChangeSet(changeSet('', { presentation: app }));
|
|
283
341
|
});
|
|
284
342
|
}
|
|
285
343
|
function emitPresentation(app) {
|
|
@@ -330,21 +388,69 @@ export function composeRuntime(options) {
|
|
|
330
388
|
return { attentionRequired, reset };
|
|
331
389
|
}
|
|
332
390
|
function documentsIn(locator) {
|
|
333
|
-
|
|
334
|
-
if (replica === undefined)
|
|
335
|
-
return 0;
|
|
336
|
-
const roots = replica.confirmed.record.typedSelection?.roots ?? [];
|
|
337
|
-
const weighed = outsideOf(roots);
|
|
338
|
-
return (tally(replica.visible.documents, weighed.documents) +
|
|
339
|
-
tally(replica.visible.binaries, weighed.binaries));
|
|
391
|
+
return replicas.get(locator)?.count ?? 0;
|
|
340
392
|
}
|
|
341
393
|
function listed() {
|
|
342
394
|
return structuredClone([...rooms.values()].map((room) => {
|
|
343
395
|
const documents = documentsIn(room.locator);
|
|
344
396
|
counted.set(room.locator, documents);
|
|
345
|
-
return
|
|
397
|
+
return locked(room.locator)
|
|
398
|
+
? {
|
|
399
|
+
...room,
|
|
400
|
+
model: modelTypes(NO_MODEL),
|
|
401
|
+
typedSelection: null,
|
|
402
|
+
documents: 0,
|
|
403
|
+
locked: true,
|
|
404
|
+
}
|
|
405
|
+
: { ...room, documents, locked: false };
|
|
346
406
|
}));
|
|
347
407
|
}
|
|
408
|
+
function locked(locator) {
|
|
409
|
+
return rooms.get(locator)?.replica.offline === 'locked' && !confirmed.has(locator);
|
|
410
|
+
}
|
|
411
|
+
function lockedRooms() {
|
|
412
|
+
return [...rooms.keys()].filter(locked).sort();
|
|
413
|
+
}
|
|
414
|
+
function relocked(before) {
|
|
415
|
+
const after = lockedRooms();
|
|
416
|
+
if (JSON.stringify(after) === JSON.stringify(before))
|
|
417
|
+
return;
|
|
418
|
+
journal.record(`locked ${after.length}`);
|
|
419
|
+
emitRooms();
|
|
420
|
+
for (const notify of [...lockListeners])
|
|
421
|
+
notify();
|
|
422
|
+
}
|
|
423
|
+
function confirmLocks(listed) {
|
|
424
|
+
if (!socketOpen)
|
|
425
|
+
return;
|
|
426
|
+
for (const room of listed) {
|
|
427
|
+
if (room.replica.admission === 'admitted')
|
|
428
|
+
confirmed.add(room.locator);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function connectionLost() {
|
|
432
|
+
if (lockGrace !== null || closed)
|
|
433
|
+
return;
|
|
434
|
+
lockGrace = setTimeout(() => {
|
|
435
|
+
lockGrace = null;
|
|
436
|
+
if (socketOpen || closed)
|
|
437
|
+
return;
|
|
438
|
+
const before = lockedRooms();
|
|
439
|
+
confirmed.clear();
|
|
440
|
+
relocked(before);
|
|
441
|
+
}, LOCK_GRACE_MS);
|
|
442
|
+
}
|
|
443
|
+
function connectionLive() {
|
|
444
|
+
if (lockGrace !== null)
|
|
445
|
+
clearTimeout(lockGrace);
|
|
446
|
+
lockGrace = null;
|
|
447
|
+
}
|
|
448
|
+
function watchLocks(notify) {
|
|
449
|
+
lockListeners.add(notify);
|
|
450
|
+
return () => {
|
|
451
|
+
lockListeners.delete(notify);
|
|
452
|
+
};
|
|
453
|
+
}
|
|
348
454
|
function roomFacts(reach = null) {
|
|
349
455
|
const held = [...rooms.values()]
|
|
350
456
|
.filter((room) => holds(reach, room.locator))
|
|
@@ -363,7 +469,7 @@ export function composeRuntime(options) {
|
|
|
363
469
|
return { listed: roomsListed, rooms: held };
|
|
364
470
|
}
|
|
365
471
|
function emitRooms() {
|
|
366
|
-
|
|
472
|
+
liveQueries.recover();
|
|
367
473
|
queueReadiness();
|
|
368
474
|
const value = listed();
|
|
369
475
|
for (const listener of roomListeners)
|
|
@@ -402,17 +508,17 @@ export function composeRuntime(options) {
|
|
|
402
508
|
for (const listener of statusListeners)
|
|
403
509
|
listener(shown);
|
|
404
510
|
}
|
|
405
|
-
function entryOf(batch) {
|
|
511
|
+
function entryOf(locator, batch) {
|
|
406
512
|
if (localSaves)
|
|
407
513
|
return Object.freeze({ batch, state: 'local' });
|
|
408
|
-
const state = pendings.get(batch.id);
|
|
514
|
+
const state = pendings.get(pendingKey(locator, batch.id));
|
|
409
515
|
if (state?.accepted !== undefined) {
|
|
410
516
|
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
411
517
|
}
|
|
412
518
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
413
519
|
}
|
|
414
520
|
function entriesOf(locator) {
|
|
415
|
-
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
|
|
521
|
+
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map((batch) => entryOf(locator, batch));
|
|
416
522
|
}
|
|
417
523
|
function emitQueue(locator) {
|
|
418
524
|
const entries = entriesOf(locator);
|
|
@@ -422,66 +528,39 @@ export function composeRuntime(options) {
|
|
|
422
528
|
}
|
|
423
529
|
}
|
|
424
530
|
function applyChangeSet(set) {
|
|
425
|
-
|
|
426
|
-
...set,
|
|
427
|
-
documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
|
|
428
|
-
};
|
|
429
|
-
const applied = readSide.apply(held, () => searchIndex.changed(held));
|
|
430
|
-
if (applied instanceof Promise)
|
|
431
|
-
void applied.catch(unpublished);
|
|
531
|
+
liveQueries.touched(set);
|
|
432
532
|
}
|
|
433
533
|
function unpublished(error) {
|
|
434
534
|
journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
|
|
435
535
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
const record = replicas.get(locator)?.confirmed.record;
|
|
439
|
-
if (record === undefined || closed)
|
|
440
|
-
return;
|
|
441
|
-
const held = { text: record.documents, binaries: record.binaries };
|
|
442
|
-
const composition = compositionOf(record);
|
|
443
|
-
if (referencesIndexed(held, composition))
|
|
444
|
-
return;
|
|
445
|
-
await indexReferrers(held, composition);
|
|
446
|
-
}
|
|
536
|
+
function indexed(locator) {
|
|
537
|
+
return replicas.get(locator)?.indexed === true;
|
|
447
538
|
}
|
|
448
539
|
async function prepareReplica(locator) {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
return;
|
|
453
|
-
const visible = replica.visible;
|
|
454
|
-
const paths = [
|
|
455
|
-
...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
|
|
456
|
-
];
|
|
457
|
-
const entered = [];
|
|
458
|
-
projecting.add(locator);
|
|
459
|
-
try {
|
|
460
|
-
await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
|
|
461
|
-
const current = replicas.get(locator);
|
|
462
|
-
if (current === undefined || closed)
|
|
463
|
-
return;
|
|
464
|
-
const set = entering(locator, current, chunk);
|
|
465
|
-
entered.push(set);
|
|
466
|
-
return readSide.apply(set);
|
|
467
|
-
});
|
|
468
|
-
if (!replicas.has(locator) || closed)
|
|
540
|
+
for (;;) {
|
|
541
|
+
const held = replicas.get(locator);
|
|
542
|
+
if (held === undefined || closed || held.indexed)
|
|
469
543
|
return;
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
544
|
+
const key = held.record.key;
|
|
545
|
+
const done = await exclusive(async () => (await openLocal()).rederive(key, REDERIVE_BATCH));
|
|
546
|
+
const now = replicas.get(locator);
|
|
547
|
+
if (now?.record.key !== key)
|
|
548
|
+
continue;
|
|
549
|
+
if (!done)
|
|
550
|
+
continue;
|
|
551
|
+
replicas.set(locator, { ...now, indexed: true });
|
|
552
|
+
publishChangeSet(changeSet(locator, { documents: null }));
|
|
553
|
+
liveQueries.recover();
|
|
554
|
+
queueReadiness();
|
|
555
|
+
return;
|
|
474
556
|
}
|
|
475
|
-
conversations.changed(mergeChangeSets(entered));
|
|
476
|
-
readSide.recover();
|
|
477
|
-
queueReadiness();
|
|
478
557
|
}
|
|
479
558
|
function unprepared() {
|
|
480
|
-
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !
|
|
559
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
|
|
481
560
|
}
|
|
482
|
-
function
|
|
483
|
-
|
|
484
|
-
journal.record(`
|
|
561
|
+
function unpreparedBy(locator, error) {
|
|
562
|
+
unpreparable.add(locator);
|
|
563
|
+
journal.record(`preparation failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
|
|
485
564
|
queueReadiness();
|
|
486
565
|
}
|
|
487
566
|
function prepare(locators = []) {
|
|
@@ -497,7 +576,7 @@ export function composeRuntime(options) {
|
|
|
497
576
|
if (closed)
|
|
498
577
|
return;
|
|
499
578
|
unpreparedDemand.delete(waiting);
|
|
500
|
-
await prepareReplica(waiting).catch((error) =>
|
|
579
|
+
await prepareReplica(waiting).catch((error) => unpreparedBy(waiting, error));
|
|
501
580
|
}
|
|
502
581
|
}
|
|
503
582
|
finally {
|
|
@@ -514,7 +593,9 @@ export function composeRuntime(options) {
|
|
|
514
593
|
(reached === null
|
|
515
594
|
? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
|
|
516
595
|
: [...reached]);
|
|
517
|
-
return [...new Set(named)]
|
|
596
|
+
return [...new Set(named)]
|
|
597
|
+
.filter((locator) => (reached === null || reached.has(locator)) && !plan.locked.includes(locator))
|
|
598
|
+
.sort();
|
|
518
599
|
}
|
|
519
600
|
function awaited(locator) {
|
|
520
601
|
return (!roomsListed ||
|
|
@@ -528,6 +609,7 @@ export function composeRuntime(options) {
|
|
|
528
609
|
const pending = [];
|
|
529
610
|
const coverage = [];
|
|
530
611
|
const overlay = [];
|
|
612
|
+
const read = [];
|
|
531
613
|
for (const locator of locators) {
|
|
532
614
|
const replica = replicas.get(locator);
|
|
533
615
|
if (replica === undefined) {
|
|
@@ -535,21 +617,22 @@ export function composeRuntime(options) {
|
|
|
535
617
|
pending.push({ room: locator, awaits: 'acquisition' });
|
|
536
618
|
continue;
|
|
537
619
|
}
|
|
538
|
-
if (!
|
|
620
|
+
if (!replica.indexed) {
|
|
539
621
|
pending.push({ room: locator, awaits: 'preparation' });
|
|
540
622
|
continue;
|
|
541
623
|
}
|
|
542
624
|
coverage.push({
|
|
543
625
|
room: locator,
|
|
544
|
-
generation: replica.
|
|
626
|
+
generation: replica.record.transfer,
|
|
545
627
|
current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
|
|
546
628
|
});
|
|
629
|
+
read.push({ locator, key: replica.record.key });
|
|
547
630
|
for (const batch of replica.queue)
|
|
548
631
|
overlay.push(batch.id);
|
|
549
632
|
}
|
|
550
633
|
if (pending.length > 0)
|
|
551
634
|
return { pending };
|
|
552
|
-
return { coverage: { rooms: coverage, indexes: [], overlay } };
|
|
635
|
+
return { coverage: { rooms: coverage, indexes: [], overlay }, rooms: read };
|
|
553
636
|
}
|
|
554
637
|
function awaitPreparation(cover) {
|
|
555
638
|
if (!('pending' in cover))
|
|
@@ -567,7 +650,7 @@ export function composeRuntime(options) {
|
|
|
567
650
|
return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
|
|
568
651
|
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
569
652
|
}
|
|
570
|
-
function roomState(locator
|
|
653
|
+
function roomState(locator) {
|
|
571
654
|
if (sourceRefusals.has(locator))
|
|
572
655
|
return 'refused';
|
|
573
656
|
const listedRoom = discovered.get(locator);
|
|
@@ -578,27 +661,17 @@ export function composeRuntime(options) {
|
|
|
578
661
|
return 'unadmitted';
|
|
579
662
|
if (!replicas.has(locator))
|
|
580
663
|
return 'acquiring';
|
|
581
|
-
if (!
|
|
664
|
+
if (!indexed(locator))
|
|
582
665
|
return 'preparing';
|
|
583
|
-
if (search && !searchIndex.rooms().has(locator))
|
|
584
|
-
return 'indexing';
|
|
585
666
|
return 'complete';
|
|
586
667
|
}
|
|
587
|
-
function conversationCoverage(reach
|
|
668
|
+
function conversationCoverage(reach) {
|
|
588
669
|
const declared = reach === null || reach.conversations;
|
|
589
670
|
if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
|
|
590
671
|
return 'undeclared';
|
|
591
672
|
if (!conversations.known() || !conversations.current())
|
|
592
673
|
return 'acquiring';
|
|
593
|
-
|
|
594
|
-
return 'complete';
|
|
595
|
-
const unindexed = conversations
|
|
596
|
-
.searchable()
|
|
597
|
-
.filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
|
|
598
|
-
if (unindexed.length === 0)
|
|
599
|
-
return 'complete';
|
|
600
|
-
index([], unindexed);
|
|
601
|
-
return 'indexing';
|
|
674
|
+
return 'complete';
|
|
602
675
|
}
|
|
603
676
|
function connectionOf() {
|
|
604
677
|
if (socketOpen)
|
|
@@ -625,34 +698,73 @@ export function composeRuntime(options) {
|
|
|
625
698
|
];
|
|
626
699
|
});
|
|
627
700
|
}
|
|
701
|
+
function installationOf(coverage) {
|
|
702
|
+
const rooms = { done: 0, total: 0 };
|
|
703
|
+
const parts = { done: 0, total: 0 };
|
|
704
|
+
const bytes = { done: 0, total: 0 };
|
|
705
|
+
const received = { done: 0, total: 0 };
|
|
706
|
+
const steps = new Set();
|
|
707
|
+
let waiting = false;
|
|
708
|
+
for (const { room, state } of coverage) {
|
|
709
|
+
if (state === 'unadmitted')
|
|
710
|
+
continue;
|
|
711
|
+
rooms.total += 1;
|
|
712
|
+
if (state === 'complete')
|
|
713
|
+
rooms.done += 1;
|
|
714
|
+
const transfer = transfers.get(room);
|
|
715
|
+
const held = installs.get(room);
|
|
716
|
+
const inventory = transfer?.inventory ?? replicas.get(room)?.record.inventory ?? null;
|
|
717
|
+
if (inventory !== null) {
|
|
718
|
+
const done = transfer?.done ?? inventory;
|
|
719
|
+
const share = held?.share ?? null;
|
|
720
|
+
parts.total += inventory.parts;
|
|
721
|
+
bytes.total += inventory.bytes;
|
|
722
|
+
parts.done += done.parts;
|
|
723
|
+
bytes.done += done.bytes;
|
|
724
|
+
if (share !== null && share.entries > 0) {
|
|
725
|
+
bytes.done += Math.floor((share.bytes * share.written) / share.entries);
|
|
726
|
+
}
|
|
727
|
+
received.total += inventory.bytes;
|
|
728
|
+
received.done += done.bytes + (share?.bytes ?? 0);
|
|
729
|
+
for (const arrived of arrivals.get(room)?.values() ?? [])
|
|
730
|
+
received.done += arrived;
|
|
731
|
+
}
|
|
732
|
+
const installing = held !== undefined && state !== 'complete';
|
|
733
|
+
if (installing)
|
|
734
|
+
steps.add(held.step);
|
|
735
|
+
waiting ||= installing || state === 'acquiring';
|
|
736
|
+
}
|
|
737
|
+
return {
|
|
738
|
+
step: INSTALL_STEPS.find((step) => steps.has(step)) ?? null,
|
|
739
|
+
paused: waiting && !onScreen ? 'hidden' : null,
|
|
740
|
+
rooms,
|
|
741
|
+
parts,
|
|
742
|
+
bytes,
|
|
743
|
+
received,
|
|
744
|
+
};
|
|
745
|
+
}
|
|
628
746
|
function readinessFor(port, reach) {
|
|
629
747
|
const required = requiredRooms(reach);
|
|
630
748
|
const search = port.search === 'required';
|
|
631
|
-
const coverage = required.rooms.map((room) => ({ room, state: roomState(room
|
|
632
|
-
const
|
|
633
|
-
.filter((room) => room.state ===
|
|
749
|
+
const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
|
|
750
|
+
const preparing = coverage
|
|
751
|
+
.filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
|
|
634
752
|
.map(({ room }) => room);
|
|
635
|
-
const preparing = building('preparing');
|
|
636
753
|
if (preparing.length > 0)
|
|
637
754
|
prepare(preparing);
|
|
638
|
-
const indexing = building('indexing');
|
|
639
|
-
if (indexing.length > 0)
|
|
640
|
-
index(indexing, []);
|
|
641
755
|
return readinessOf({
|
|
642
756
|
code: port.code,
|
|
643
757
|
coverage: {
|
|
644
758
|
listed: required.listed,
|
|
645
759
|
rooms: coverage,
|
|
646
|
-
conversations: conversationCoverage(reach
|
|
760
|
+
conversations: conversationCoverage(reach),
|
|
647
761
|
search: search ? 'required' : 'unused',
|
|
648
762
|
},
|
|
649
763
|
persistence,
|
|
650
764
|
connection: connectionOf(),
|
|
651
765
|
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
? capacity
|
|
655
|
-
: port.capacity,
|
|
766
|
+
space: full ? 'full' : port.space,
|
|
767
|
+
installation: installationOf(coverage),
|
|
656
768
|
refused: refusedIn(required.rooms),
|
|
657
769
|
lost: storageLoss !== null,
|
|
658
770
|
established: targets[port.target]?.established === true,
|
|
@@ -661,7 +773,8 @@ export function composeRuntime(options) {
|
|
|
661
773
|
function establish(name) {
|
|
662
774
|
targets = { ...targets, [name]: { established: true } };
|
|
663
775
|
queueReadiness();
|
|
664
|
-
|
|
776
|
+
probeEachMinute();
|
|
777
|
+
void durably((local) => local.fact('targets', targets)).catch(() => {
|
|
665
778
|
journal.record(`target record failed ${name}`);
|
|
666
779
|
});
|
|
667
780
|
}
|
|
@@ -698,92 +811,51 @@ export function composeRuntime(options) {
|
|
|
698
811
|
readinessWatches.delete(watch);
|
|
699
812
|
};
|
|
700
813
|
}
|
|
701
|
-
function
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
}
|
|
707
|
-
function grownBy(next, previous) {
|
|
708
|
-
if (previous === undefined)
|
|
709
|
-
return layerBytes(next);
|
|
710
|
-
const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
|
|
711
|
-
return layerBytes({ ...next, documents });
|
|
712
|
-
}
|
|
713
|
-
function exceeded(error) {
|
|
714
|
-
return (typeof error === 'object' &&
|
|
715
|
-
error !== null &&
|
|
716
|
-
error.name === 'QuotaExceededError');
|
|
717
|
-
}
|
|
718
|
-
async function estimated() {
|
|
719
|
-
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
720
|
-
return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
|
|
721
|
-
}
|
|
722
|
-
function pause(locator, cause) {
|
|
723
|
-
paused.add(locator);
|
|
724
|
-
capacity = cause;
|
|
725
|
-
journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
|
|
814
|
+
function filled() {
|
|
815
|
+
if (full || closed)
|
|
816
|
+
return;
|
|
817
|
+
full = true;
|
|
818
|
+
journal.record('space full');
|
|
726
819
|
queueReadiness();
|
|
727
|
-
|
|
820
|
+
probeEachMinute();
|
|
821
|
+
}
|
|
822
|
+
function installedTarget() {
|
|
823
|
+
return Object.values(targets).some((target) => target.established);
|
|
824
|
+
}
|
|
825
|
+
function probeEachMinute() {
|
|
826
|
+
if (!full || probing !== null || closed || !installedTarget())
|
|
728
827
|
return;
|
|
729
|
-
// A
|
|
730
|
-
|
|
731
|
-
void
|
|
828
|
+
// 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.
|
|
829
|
+
probing = setInterval(() => {
|
|
830
|
+
void probed();
|
|
732
831
|
}, 60_000);
|
|
733
832
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
833
|
+
function probed() {
|
|
834
|
+
testing ??= database.ask({ kind: 'space.probe' }).then(() => {
|
|
835
|
+
testing = null;
|
|
836
|
+
spaceFreed();
|
|
837
|
+
return true;
|
|
838
|
+
}, () => {
|
|
839
|
+
testing = null;
|
|
840
|
+
return false;
|
|
841
|
+
});
|
|
842
|
+
return testing;
|
|
744
843
|
}
|
|
745
|
-
function
|
|
746
|
-
if (
|
|
747
|
-
clearInterval(rechecking);
|
|
748
|
-
rechecking = null;
|
|
749
|
-
if (paused.size === 0 && capacity === null)
|
|
844
|
+
function spaceFreed() {
|
|
845
|
+
if (!full)
|
|
750
846
|
return;
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
847
|
+
full = false;
|
|
848
|
+
if (probing !== null)
|
|
849
|
+
clearInterval(probing);
|
|
850
|
+
probing = null;
|
|
851
|
+
unpreparable.clear();
|
|
852
|
+
journal.record('space freed');
|
|
754
853
|
queueReadiness();
|
|
755
854
|
if (!closed && authorization !== undefined)
|
|
756
855
|
void sync('unstreamed');
|
|
757
856
|
}
|
|
758
|
-
async function reclaimed(local) {
|
|
759
|
-
const unreferenced = [...transfers.keys()].filter((locator) => !discovered.has(locator));
|
|
760
|
-
for (const locator of unreferenced) {
|
|
761
|
-
await local.abandon(locator);
|
|
762
|
-
transfers.delete(locator);
|
|
763
|
-
}
|
|
764
|
-
if (unreferenced.length > 0)
|
|
765
|
-
journal.record(`capacity reclaimed ${unreferenced.length}`);
|
|
766
|
-
return unreferenced.length > 0;
|
|
767
|
-
}
|
|
768
|
-
async function admitted(local, locator, needed) {
|
|
769
|
-
if (paused.has(locator))
|
|
770
|
-
return false;
|
|
771
|
-
let available = await estimated().catch(() => null);
|
|
772
|
-
if (available === null || available >= needed)
|
|
773
|
-
return true;
|
|
774
|
-
if (await reclaimed(local))
|
|
775
|
-
available = await estimated().catch(() => null);
|
|
776
|
-
if (available === null || available >= needed)
|
|
777
|
-
return true;
|
|
778
|
-
pause(locator, { cause: 'insufficient-space', needed, available });
|
|
779
|
-
return false;
|
|
780
|
-
}
|
|
781
|
-
async function exceededOn(locator, bytes) {
|
|
782
|
-
const available = await estimated().catch(() => null);
|
|
783
|
-
pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
|
|
784
|
-
}
|
|
785
857
|
function seedRoomHeld(room, version) {
|
|
786
|
-
return
|
|
858
|
+
return indexed(room) && reachedBy(room, version);
|
|
787
859
|
}
|
|
788
860
|
function seedHeld(at) {
|
|
789
861
|
return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
|
|
@@ -817,6 +889,75 @@ export function composeRuntime(options) {
|
|
|
817
889
|
shown();
|
|
818
890
|
}
|
|
819
891
|
}
|
|
892
|
+
function sameCover(left, right) {
|
|
893
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
894
|
+
}
|
|
895
|
+
async function coveredRows(plan, cover) {
|
|
896
|
+
for (;;) {
|
|
897
|
+
const before = cover();
|
|
898
|
+
if ('pending' in before)
|
|
899
|
+
return { pending: before };
|
|
900
|
+
const local = await openLocal();
|
|
901
|
+
const keys = before.rooms.map(({ key }) => key);
|
|
902
|
+
const changes = local.changes(keys);
|
|
903
|
+
const current = () => local.changes(keys) === changes;
|
|
904
|
+
const rows = await windowRows(local, plan, before.rooms, current);
|
|
905
|
+
const after = cover();
|
|
906
|
+
if (rows !== null && sameCover(before, after))
|
|
907
|
+
return { rows, cover: before, current };
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
async function windowRows(local, plan, rooms, unchanged) {
|
|
911
|
+
const first = await local.query(plan, rooms, QUERY_BYTES);
|
|
912
|
+
if (first.more === undefined || !('documents' in first)) {
|
|
913
|
+
const { more: _more, ...rows } = first;
|
|
914
|
+
return rows;
|
|
915
|
+
}
|
|
916
|
+
const documents = [...first.documents];
|
|
917
|
+
for (let more = first.more; more !== undefined;) {
|
|
918
|
+
if (!unchanged())
|
|
919
|
+
return null;
|
|
920
|
+
const page = await local.query({
|
|
921
|
+
...plan,
|
|
922
|
+
after: { text: more, values: JSON.parse(more) },
|
|
923
|
+
limit: plan.limit === null ? null : plan.limit - documents.length,
|
|
924
|
+
total: false,
|
|
925
|
+
}, rooms, QUERY_BYTES);
|
|
926
|
+
if (!('documents' in page))
|
|
927
|
+
return null;
|
|
928
|
+
documents.push(...page.documents);
|
|
929
|
+
more = page.more;
|
|
930
|
+
if (more === undefined) {
|
|
931
|
+
return {
|
|
932
|
+
documents,
|
|
933
|
+
...(page.next === undefined ? {} : { next: page.next }),
|
|
934
|
+
...(first.total === undefined ? {} : { total: first.total }),
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
function keyedOf(rows, coverage) {
|
|
941
|
+
if ('groups' in rows) {
|
|
942
|
+
return {
|
|
943
|
+
result: { ...rows, coverage },
|
|
944
|
+
keys: rows.groups.map((group) => JSON.stringify(group.key)),
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
return {
|
|
948
|
+
result: { ...rows, coverage },
|
|
949
|
+
keys: rows.documents.map((document) => documentKey(document.room, document.path)),
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
async function answeredPlan(plan, cover) {
|
|
953
|
+
const answered = await coveredRows(plan, cover);
|
|
954
|
+
if ('pending' in answered) {
|
|
955
|
+
const pending = answered.pending;
|
|
956
|
+
return { result: pending, keys: [], current: () => true };
|
|
957
|
+
}
|
|
958
|
+
const held = answered.cover;
|
|
959
|
+
return { ...keyedOf(answered.rows, held.coverage), current: answered.current };
|
|
960
|
+
}
|
|
820
961
|
async function rowsQuery(plan) {
|
|
821
962
|
const before = covered(plan);
|
|
822
963
|
awaitPreparation(before);
|
|
@@ -825,59 +966,15 @@ export function composeRuntime(options) {
|
|
|
825
966
|
return seededResult(seeded).result;
|
|
826
967
|
if ('pending' in before)
|
|
827
968
|
return before;
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
969
|
+
const answered = await coveredRows(plan, () => {
|
|
970
|
+
const cover = covered(plan);
|
|
971
|
+
awaitPreparation(cover);
|
|
831
972
|
return cover;
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
for (;;) {
|
|
838
|
-
const batch = await durably((local) => local.messagesAfter(conversation, after));
|
|
839
|
-
if (closed)
|
|
840
|
-
return;
|
|
841
|
-
searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
|
|
842
|
-
if (batch.length === 0)
|
|
843
|
-
return;
|
|
844
|
-
after = batch[batch.length - 1].identity;
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
function indexNext() {
|
|
848
|
-
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
|
|
849
|
-
if (room !== undefined) {
|
|
850
|
-
roomsToIndex.delete(room);
|
|
851
|
-
return () => prepared.has(room)
|
|
852
|
-
? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
|
|
853
|
-
: undefined;
|
|
854
|
-
}
|
|
855
|
-
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
856
|
-
if (conversation === undefined)
|
|
857
|
-
return null;
|
|
858
|
-
conversationsToIndex.delete(conversation);
|
|
859
|
-
return () => indexConversation(conversation);
|
|
860
|
-
}
|
|
861
|
-
function index(rooms, named) {
|
|
862
|
-
for (const room of rooms)
|
|
863
|
-
roomsToIndex.add(room);
|
|
864
|
-
for (const conversation of named)
|
|
865
|
-
conversationsToIndex.add(conversation);
|
|
866
|
-
if (indexing)
|
|
867
|
-
return;
|
|
868
|
-
indexing = true;
|
|
869
|
-
void (async () => {
|
|
870
|
-
try {
|
|
871
|
-
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
872
|
-
await yielded();
|
|
873
|
-
await unit();
|
|
874
|
-
queueReadiness();
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
finally {
|
|
878
|
-
indexing = false;
|
|
879
|
-
}
|
|
880
|
-
})().catch(() => { });
|
|
973
|
+
});
|
|
974
|
+
if ('pending' in answered)
|
|
975
|
+
return answered.pending;
|
|
976
|
+
const held = answered.cover;
|
|
977
|
+
return { ...answered.rows, coverage: held.coverage };
|
|
881
978
|
}
|
|
882
979
|
function publishChangeSet(set) {
|
|
883
980
|
applyChangeSet(set);
|
|
@@ -887,7 +984,10 @@ export function composeRuntime(options) {
|
|
|
887
984
|
for (const locator of set.queues)
|
|
888
985
|
emitQueue(locator);
|
|
889
986
|
if (set.drafts.length > 0 ||
|
|
890
|
-
set.documents.some((change) =>
|
|
987
|
+
set.documents.some((change) => {
|
|
988
|
+
const held = roomDrafts.get(change.locator);
|
|
989
|
+
return held !== undefined && (change.path === null || held.has(change.path));
|
|
990
|
+
})) {
|
|
891
991
|
publishRoomDrafts(false);
|
|
892
992
|
}
|
|
893
993
|
for (const app of set.presentations)
|
|
@@ -900,9 +1000,22 @@ export function composeRuntime(options) {
|
|
|
900
1000
|
if (reason === 'revoked-authorization') {
|
|
901
1001
|
await exclusive(async () => {
|
|
902
1002
|
const revoked = { status: 'unavailable', reason: 'revoked' };
|
|
1003
|
+
const records = [...replicas].map(([locator, held]) => ({
|
|
1004
|
+
locator,
|
|
1005
|
+
held,
|
|
1006
|
+
record: { ...held.record, model: revoked, selectionModel: revoked },
|
|
1007
|
+
}));
|
|
1008
|
+
let answer = null;
|
|
903
1009
|
try {
|
|
904
1010
|
const local = await openLocal();
|
|
905
|
-
await local.
|
|
1011
|
+
answer = await local.commit({
|
|
1012
|
+
rooms: records.map(({ locator, record }) => ({
|
|
1013
|
+
locator,
|
|
1014
|
+
key: record.key,
|
|
1015
|
+
record: storedRecord(record),
|
|
1016
|
+
composition: compositionOf(record),
|
|
1017
|
+
})),
|
|
1018
|
+
});
|
|
906
1019
|
}
|
|
907
1020
|
catch {
|
|
908
1021
|
journal.record('model revocation persistence failed');
|
|
@@ -918,16 +1031,16 @@ export function composeRuntime(options) {
|
|
|
918
1031
|
liveModels.delete(key);
|
|
919
1032
|
}
|
|
920
1033
|
const changed = [];
|
|
921
|
-
for (const
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1034
|
+
for (const { locator, held, record } of records) {
|
|
1035
|
+
if (replicas.get(locator) !== held)
|
|
1036
|
+
continue;
|
|
1037
|
+
const indexed = answer === null ? held.indexed : committedRoom(locator, answer).indexed;
|
|
1038
|
+
replicas.set(locator, { ...held, record, indexed });
|
|
1039
|
+
}
|
|
1040
|
+
for (const locator of [...rooms.keys()]) {
|
|
1041
|
+
const room = rooms.get(locator);
|
|
1042
|
+
if (room === undefined)
|
|
1043
|
+
continue;
|
|
931
1044
|
rooms.set(locator, {
|
|
932
1045
|
...room,
|
|
933
1046
|
model: modelTypes(revoked),
|
|
@@ -935,9 +1048,10 @@ export function composeRuntime(options) {
|
|
|
935
1048
|
? null
|
|
936
1049
|
: { ...room.typedSelection, model: modelTypes(revoked) },
|
|
937
1050
|
});
|
|
938
|
-
changed.push(changeSet(locator,
|
|
1051
|
+
changed.push(changeSet(locator, { documents: null, room: true }));
|
|
939
1052
|
}
|
|
940
1053
|
publishChangeSet(mergeChangeSets(changed));
|
|
1054
|
+
prepare();
|
|
941
1055
|
});
|
|
942
1056
|
}
|
|
943
1057
|
}
|
|
@@ -968,12 +1082,13 @@ export function composeRuntime(options) {
|
|
|
968
1082
|
model: modelTypes(modelOf(room.locator, 'typed-selection')),
|
|
969
1083
|
},
|
|
970
1084
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
971
|
-
roomVersion: replicas.get(room.locator)?.
|
|
1085
|
+
roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
|
|
972
1086
|
synchronizationAvailable,
|
|
973
1087
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
974
1088
|
});
|
|
975
1089
|
}
|
|
976
1090
|
async function applyDiscovery(found, started, covered) {
|
|
1091
|
+
const lockedBefore = lockedRooms();
|
|
977
1092
|
for (const room of found) {
|
|
978
1093
|
const fence = fences.get(room.locator);
|
|
979
1094
|
if (fence !== undefined && fence.erased && fence.at < started)
|
|
@@ -999,15 +1114,17 @@ export function composeRuntime(options) {
|
|
|
999
1114
|
else
|
|
1000
1115
|
unlistedKept(unlisted);
|
|
1001
1116
|
for (const room of listed) {
|
|
1002
|
-
const held = replicas.get(room.locator)?.
|
|
1117
|
+
const held = replicas.get(room.locator)?.record;
|
|
1003
1118
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
1004
1119
|
list(held, false);
|
|
1005
1120
|
continue;
|
|
1006
1121
|
}
|
|
1007
1122
|
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
1008
1123
|
}
|
|
1124
|
+
confirmLocks(listed);
|
|
1009
1125
|
roomsListed = true;
|
|
1010
1126
|
emitRooms();
|
|
1127
|
+
relocked(lockedBefore);
|
|
1011
1128
|
return listed;
|
|
1012
1129
|
}
|
|
1013
1130
|
function heldLocally(locator) {
|
|
@@ -1028,7 +1145,7 @@ export function composeRuntime(options) {
|
|
|
1028
1145
|
}
|
|
1029
1146
|
function unlistedKept(unlisted) {
|
|
1030
1147
|
for (const locator of unlisted) {
|
|
1031
|
-
const held = replicas.get(locator)?.
|
|
1148
|
+
const held = replicas.get(locator)?.record;
|
|
1032
1149
|
if (held !== undefined)
|
|
1033
1150
|
list(held, false);
|
|
1034
1151
|
else
|
|
@@ -1037,16 +1154,16 @@ export function composeRuntime(options) {
|
|
|
1037
1154
|
}
|
|
1038
1155
|
function fence(locators) {
|
|
1039
1156
|
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
1040
|
-
const removed = fresh.map((locator) =>
|
|
1041
|
-
|
|
1042
|
-
queue: true,
|
|
1157
|
+
const removed = fresh.map((locator) => ({
|
|
1158
|
+
locator,
|
|
1043
1159
|
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
1044
1160
|
}));
|
|
1045
1161
|
for (const locator of fresh) {
|
|
1046
1162
|
fences.set(locator, { at: discoveries, erased: false });
|
|
1047
1163
|
lastFenced.set(locator, ++fencings);
|
|
1164
|
+
confirmed.delete(locator);
|
|
1048
1165
|
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
1049
|
-
pendings.delete(batch.id);
|
|
1166
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
1050
1167
|
replicas.delete(locator);
|
|
1051
1168
|
queuesWithoutReplica.delete(locator);
|
|
1052
1169
|
rooms.delete(locator);
|
|
@@ -1058,8 +1175,7 @@ export function composeRuntime(options) {
|
|
|
1058
1175
|
unreadable.delete(locator);
|
|
1059
1176
|
stale.delete(locator);
|
|
1060
1177
|
repairable.delete(locator);
|
|
1061
|
-
|
|
1062
|
-
paused.delete(locator);
|
|
1178
|
+
unpreparable.delete(locator);
|
|
1063
1179
|
scopedRefusals.delete(locator);
|
|
1064
1180
|
sourceRefusals.delete(locator);
|
|
1065
1181
|
declarationRefusals.delete(locator);
|
|
@@ -1073,26 +1189,48 @@ export function composeRuntime(options) {
|
|
|
1073
1189
|
}
|
|
1074
1190
|
if (fresh.length === 0)
|
|
1075
1191
|
return fresh;
|
|
1076
|
-
publishChangeSet(mergeChangeSets(removed));
|
|
1077
|
-
|
|
1078
|
-
prepared.delete(locator);
|
|
1079
|
-
searchIndex.forget(fresh);
|
|
1080
|
-
readSide.recover();
|
|
1192
|
+
publishChangeSet(mergeChangeSets(removed.map(({ locator, drafts }) => changeSet(locator, { documents: null, room: true, queue: true, drafts }))));
|
|
1193
|
+
liveQueries.recover();
|
|
1081
1194
|
moveTo({ ...status, ...attention() });
|
|
1082
1195
|
return fresh;
|
|
1083
1196
|
}
|
|
1084
|
-
async function
|
|
1085
|
-
await local.erase(locators);
|
|
1197
|
+
async function eraseFenced(locators) {
|
|
1086
1198
|
for (const locator of locators) {
|
|
1199
|
+
while (await durably((local) => local.erase(locator, ERASE_BATCH)))
|
|
1200
|
+
await yielded();
|
|
1087
1201
|
const held = fences.get(locator);
|
|
1088
1202
|
if (held !== undefined)
|
|
1089
1203
|
held.erased = true;
|
|
1090
1204
|
}
|
|
1091
1205
|
}
|
|
1206
|
+
let sweeping = false;
|
|
1207
|
+
function sweep() {
|
|
1208
|
+
if (sweeping || closed)
|
|
1209
|
+
return;
|
|
1210
|
+
sweeping = true;
|
|
1211
|
+
void (async () => {
|
|
1212
|
+
try {
|
|
1213
|
+
while (!closed && (await durably((local) => local.erase(null, ERASE_BATCH)))) {
|
|
1214
|
+
await yielded();
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
catch {
|
|
1218
|
+
journal.record('erasure failed');
|
|
1219
|
+
}
|
|
1220
|
+
finally {
|
|
1221
|
+
sweeping = false;
|
|
1222
|
+
}
|
|
1223
|
+
})();
|
|
1224
|
+
}
|
|
1092
1225
|
async function loseRooms(locators) {
|
|
1093
1226
|
const fresh = fence(locators);
|
|
1094
|
-
if (fresh.length
|
|
1095
|
-
|
|
1227
|
+
if (fresh.length === 0)
|
|
1228
|
+
return;
|
|
1229
|
+
await durably(async (local) => {
|
|
1230
|
+
await local.fence(fresh);
|
|
1231
|
+
await unpair(local);
|
|
1232
|
+
});
|
|
1233
|
+
await eraseFenced(fresh);
|
|
1096
1234
|
}
|
|
1097
1235
|
function restored(key, persisted) {
|
|
1098
1236
|
const prior = models.get(key);
|
|
@@ -1113,54 +1251,56 @@ export function composeRuntime(options) {
|
|
|
1113
1251
|
return model;
|
|
1114
1252
|
}
|
|
1115
1253
|
async function hydrate(opened) {
|
|
1116
|
-
const [
|
|
1117
|
-
opened.
|
|
1254
|
+
const [held, queues, kept, heldConversations, chatDrafts] = await Promise.all([
|
|
1255
|
+
opened.opened(),
|
|
1118
1256
|
opened.queues(),
|
|
1119
1257
|
opened.roomDrafts(),
|
|
1120
|
-
opened.presentation(),
|
|
1121
1258
|
opened.conversations(),
|
|
1122
1259
|
opened.chatDrafts(),
|
|
1123
|
-
opened.lastCompleted(),
|
|
1124
|
-
opened.providerLimits(),
|
|
1125
|
-
opened.capabilities(),
|
|
1126
|
-
opened.member(),
|
|
1127
|
-
opened.transfers(),
|
|
1128
|
-
opened.targets(),
|
|
1129
1260
|
]);
|
|
1130
|
-
|
|
1261
|
+
const records = [...held.rooms.values()];
|
|
1262
|
+
const counts = await opened.counts(records.map(({ key, typedSelection }) => ({ key, roots: typedSelection?.roots ?? [] })));
|
|
1263
|
+
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1264
|
+
targets = held.targets;
|
|
1131
1265
|
transfers.clear();
|
|
1132
|
-
for (const record of
|
|
1133
|
-
transfers.set(
|
|
1134
|
-
limits =
|
|
1135
|
-
capabilities =
|
|
1136
|
-
member =
|
|
1266
|
+
for (const [locator, record] of held.transfers)
|
|
1267
|
+
transfers.set(locator, record);
|
|
1268
|
+
limits = held.providerLimits;
|
|
1269
|
+
capabilities = held.capabilities;
|
|
1270
|
+
member = held.member;
|
|
1137
1271
|
roomDrafts.clear();
|
|
1138
1272
|
for (const record of kept)
|
|
1139
1273
|
rememberRoomDraft(record);
|
|
1140
1274
|
presentations.clear();
|
|
1141
|
-
for (const record of
|
|
1275
|
+
for (const record of held.presentation)
|
|
1142
1276
|
presentations.set(record.app, record.value);
|
|
1143
|
-
conversations.hydrate(
|
|
1144
|
-
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1277
|
+
conversations.hydrate(heldConversations, chatDrafts);
|
|
1145
1278
|
pendings.clear();
|
|
1146
1279
|
for (const queue of queues) {
|
|
1147
|
-
for (const { batch, state } of queue.pending)
|
|
1148
|
-
pendings.set(batch.id, state);
|
|
1280
|
+
for (const { batch, state } of queue.pending) {
|
|
1281
|
+
pendings.set(pendingKey(queue.locator, batch.id), state);
|
|
1282
|
+
}
|
|
1149
1283
|
}
|
|
1150
|
-
const previousReplicas = new Map(replicas);
|
|
1151
1284
|
replicas.clear();
|
|
1152
1285
|
queuesWithoutReplica.clear();
|
|
1153
|
-
const replicated = new Set(records.map((record) => record.locator));
|
|
1154
1286
|
for (const [locator, queue] of queuedBatches) {
|
|
1155
|
-
if (!
|
|
1287
|
+
if (!held.rooms.has(locator))
|
|
1156
1288
|
queuesWithoutReplica.set(locator, queue);
|
|
1157
1289
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
const
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1290
|
+
records.forEach((stored, index) => {
|
|
1291
|
+
if (fences.has(stored.locator))
|
|
1292
|
+
return;
|
|
1293
|
+
const model = restored(modelKey(stored.locator, 'collection'), stored.model);
|
|
1294
|
+
const selectionModel = restored(modelKey(stored.locator, 'typed-selection'), stored.selectionModel);
|
|
1295
|
+
const record = { ...stored, model, selectionModel };
|
|
1296
|
+
replicas.set(record.locator, {
|
|
1297
|
+
record,
|
|
1298
|
+
queue: queuedBatches.get(record.locator) ?? [],
|
|
1299
|
+
overlaid: held.overlaid.get(record.key) ?? new Set(),
|
|
1300
|
+
count: counts[index] ?? 0,
|
|
1301
|
+
confirmed: 0,
|
|
1302
|
+
indexed: !held.unindexed.has(record.key),
|
|
1303
|
+
});
|
|
1164
1304
|
const existing = rooms.get(record.locator);
|
|
1165
1305
|
if (existing === undefined)
|
|
1166
1306
|
list(record, false);
|
|
@@ -1179,11 +1319,15 @@ export function composeRuntime(options) {
|
|
|
1179
1319
|
},
|
|
1180
1320
|
});
|
|
1181
1321
|
}
|
|
1182
|
-
}
|
|
1322
|
+
});
|
|
1323
|
+
for (const key of held.orphans)
|
|
1324
|
+
await opened.release(key);
|
|
1325
|
+
if (held.erasing || held.orphans.length > 0)
|
|
1326
|
+
queueMicrotask(sweep);
|
|
1183
1327
|
return {
|
|
1184
|
-
lastCompleted,
|
|
1328
|
+
lastCompleted: held.lastCompleted,
|
|
1185
1329
|
locators: new Set([...queuedBatches.keys(), ...replicas.keys()]),
|
|
1186
|
-
read: mergeChangeSets([...
|
|
1330
|
+
read: mergeChangeSets([...replicas.keys()].map((locator) => changeSet(locator, { documents: null }))),
|
|
1187
1331
|
};
|
|
1188
1332
|
}
|
|
1189
1333
|
async function readState(opened) {
|
|
@@ -1202,8 +1346,7 @@ export function composeRuntime(options) {
|
|
|
1202
1346
|
async function open() {
|
|
1203
1347
|
if (closed)
|
|
1204
1348
|
throw new Error('runtime closed');
|
|
1205
|
-
const opened =
|
|
1206
|
-
store = opened;
|
|
1349
|
+
const opened = openRuntimeStore(isFenced, database);
|
|
1207
1350
|
const interrupted = await opened.fences();
|
|
1208
1351
|
for (const locator of interrupted) {
|
|
1209
1352
|
fences.set(locator, { at: discoveries, erased: false });
|
|
@@ -1222,9 +1365,17 @@ export function composeRuntime(options) {
|
|
|
1222
1365
|
emitStorageLoss();
|
|
1223
1366
|
}
|
|
1224
1367
|
const lastCompleted = await readState(opened);
|
|
1225
|
-
if (interrupted.length > 0)
|
|
1226
|
-
|
|
1368
|
+
if (interrupted.length > 0) {
|
|
1369
|
+
queueMicrotask(() => {
|
|
1370
|
+
void eraseFenced(interrupted).catch(() => {
|
|
1371
|
+
journal.record('erasure failed');
|
|
1372
|
+
});
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1227
1375
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
1376
|
+
void exclusive(() => unpair(opened)).catch(() => {
|
|
1377
|
+
journal.record('unpair failed');
|
|
1378
|
+
});
|
|
1228
1379
|
return opened;
|
|
1229
1380
|
}
|
|
1230
1381
|
function durably(work) {
|
|
@@ -1245,86 +1396,262 @@ export function composeRuntime(options) {
|
|
|
1245
1396
|
mutations = next.then(() => undefined, () => undefined);
|
|
1246
1397
|
return next;
|
|
1247
1398
|
}
|
|
1248
|
-
function withdrawable(
|
|
1249
|
-
|
|
1250
|
-
|
|
1399
|
+
function withdrawable(locator) {
|
|
1400
|
+
return (batch) => {
|
|
1401
|
+
const state = pendings.get(pendingKey(locator, batch.id));
|
|
1402
|
+
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
const REWRITE_LINKS = ['field', 'body'];
|
|
1406
|
+
const CHECKED_LINKS = ['field', 'section'];
|
|
1407
|
+
async function sliceAt(record, view, queue, extra = [], moved = []) {
|
|
1408
|
+
const local = await openLocal();
|
|
1409
|
+
const needed = new Set([...touchedBy(queue), ...extra]);
|
|
1410
|
+
const movedPaths = [...new Set([...movedBy(queue, record.locator), ...moved])];
|
|
1411
|
+
if (movedPaths.length > 0) {
|
|
1412
|
+
for (const { path } of await local.referrers(record.key, view, movedPaths, REWRITE_LINKS)) {
|
|
1413
|
+
needed.add(path);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
const paths = [...needed];
|
|
1417
|
+
return stepped(slicing(await local.entries(record.key, view, paths), paths));
|
|
1418
|
+
}
|
|
1419
|
+
async function landedWhole(locator, batch, confirmedLayer, current) {
|
|
1420
|
+
if (!landed(batch, confirmedLayer, current))
|
|
1421
|
+
return false;
|
|
1422
|
+
const partner = partnerOf(batch, locator);
|
|
1423
|
+
if (partner === null)
|
|
1424
|
+
return true;
|
|
1425
|
+
const other = replicas.get(partner);
|
|
1426
|
+
const half = other?.queue.find((one) => one.id === batch.id);
|
|
1427
|
+
if (other === undefined || half === undefined)
|
|
1428
|
+
return false;
|
|
1429
|
+
return landed(half, await sliceAt(other.record, 'confirmed', [half]));
|
|
1430
|
+
}
|
|
1431
|
+
function carriedBack(locator, unlanded, written) {
|
|
1432
|
+
const back = new Map();
|
|
1433
|
+
const taken = [];
|
|
1434
|
+
for (const batch of unlanded) {
|
|
1435
|
+
const between = batch.between;
|
|
1436
|
+
if (between === undefined)
|
|
1437
|
+
continue;
|
|
1438
|
+
const source = between.source.room;
|
|
1439
|
+
const destination = between.destination.room;
|
|
1440
|
+
for (const { path: to } of pendings.get(pendingKey(destination, batch.id))?.drafts ?? []) {
|
|
1441
|
+
const record = roomDrafts.get(destination)?.get(to);
|
|
1442
|
+
const path = between.moves.find((move) => move.to === to)?.path;
|
|
1443
|
+
if (record === undefined || path === undefined)
|
|
1444
|
+
continue;
|
|
1445
|
+
const [into, key] = source === locator ? [written, path] : [back, documentKey(source, path)];
|
|
1446
|
+
const existing = into.get(key) ?? roomDrafts.get(source)?.get(path);
|
|
1447
|
+
taken.push({ locator: destination, path: to });
|
|
1448
|
+
into.set(key, {
|
|
1449
|
+
locator: source,
|
|
1450
|
+
path,
|
|
1451
|
+
revision: roomDraftRevision(),
|
|
1452
|
+
parts: returnedInto(existing?.parts, record.parts),
|
|
1453
|
+
returned: existing?.returned ?? null,
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
const records = [...back.values()];
|
|
1458
|
+
const elsewhere = taken.filter((one) => one.locator !== locator);
|
|
1459
|
+
const rooms = new Set([...records, ...elsewhere].map((one) => one.locator));
|
|
1460
|
+
return {
|
|
1461
|
+
taken: taken.filter((one) => one.locator === locator).map(({ path }) => path),
|
|
1462
|
+
writes: [...rooms].map((room) => ({
|
|
1463
|
+
locator: room,
|
|
1464
|
+
roomDrafts: records.filter((record) => record.locator === room),
|
|
1465
|
+
consumedRoomDrafts: elsewhere.filter((one) => one.locator === room).map(({ path }) => path),
|
|
1466
|
+
})),
|
|
1467
|
+
changes: [...rooms].map((room) => changeSet(room, {
|
|
1468
|
+
drafts: [...records, ...elsewhere]
|
|
1469
|
+
.filter((one) => one.locator === room)
|
|
1470
|
+
.map(({ path }) => path),
|
|
1471
|
+
})),
|
|
1472
|
+
remember() {
|
|
1473
|
+
for (const one of taken)
|
|
1474
|
+
forgetRoomDraft(one.locator, one.path);
|
|
1475
|
+
for (const record of records)
|
|
1476
|
+
rememberRoomDraft(record);
|
|
1477
|
+
},
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
function committedRoom(locator, answer) {
|
|
1481
|
+
const indexed = answer.indexed[locator] ?? replicas.get(locator)?.indexed ?? false;
|
|
1482
|
+
if (!indexed)
|
|
1483
|
+
prepare([locator]);
|
|
1484
|
+
return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
|
|
1485
|
+
}
|
|
1486
|
+
function replayed(record, confirmedLayer, queue) {
|
|
1487
|
+
return stepped(replaying(confirmedLayer, queue, record.locator, compositionOf(record)));
|
|
1251
1488
|
}
|
|
1252
|
-
async function
|
|
1489
|
+
async function overlayDiff(record, before, visible, touched) {
|
|
1490
|
+
const kept = before !== undefined && before.record.key === record.key
|
|
1491
|
+
? before.overlaid
|
|
1492
|
+
: new Set();
|
|
1493
|
+
return {
|
|
1494
|
+
overlay: {
|
|
1495
|
+
set: await stepped(overlayOf(visible, touched)),
|
|
1496
|
+
cleared: [...kept].filter((path) => !touched.has(path)),
|
|
1497
|
+
},
|
|
1498
|
+
touched,
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
async function settle(local, locator, before, record, queue, settling = {}) {
|
|
1502
|
+
return together(local, [await settlement(local, locator, before, record, queue, settling)]);
|
|
1503
|
+
}
|
|
1504
|
+
function unpairedIn(locator, batch) {
|
|
1505
|
+
if (!queueIn(locator).some((held) => held.id === batch.id))
|
|
1506
|
+
return false;
|
|
1507
|
+
if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
|
|
1508
|
+
return false;
|
|
1509
|
+
return !leavingIn(batch, locator) || withdrawable(locator)(batch);
|
|
1510
|
+
}
|
|
1511
|
+
async function together(local, settlements) {
|
|
1512
|
+
const settled = new Set(settlements.map(({ locator }) => locator));
|
|
1513
|
+
const all = [...settlements];
|
|
1514
|
+
for (let wave = settlements; wave.length > 0;) {
|
|
1515
|
+
const partnered = new Map();
|
|
1516
|
+
for (const { locator, left } of wave) {
|
|
1517
|
+
for (const batch of left) {
|
|
1518
|
+
const partner = partnerOf(batch, locator);
|
|
1519
|
+
if (partner === null || settled.has(partner) || !unpairedIn(partner, batch))
|
|
1520
|
+
continue;
|
|
1521
|
+
partnered.set(partner, [...(partnered.get(partner) ?? []), batch]);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
const partners = [];
|
|
1525
|
+
for (const [partner, batches] of partnered) {
|
|
1526
|
+
settled.add(partner);
|
|
1527
|
+
partners.push(await unpairing(local, partner, batches));
|
|
1528
|
+
}
|
|
1529
|
+
all.push(...partners);
|
|
1530
|
+
wave = partners;
|
|
1531
|
+
}
|
|
1532
|
+
const answer = await local.commit({
|
|
1533
|
+
rooms: all.flatMap(({ commit }) => commit.rooms),
|
|
1534
|
+
writes: all.flatMap(({ commit }) => commit.writes),
|
|
1535
|
+
});
|
|
1536
|
+
return mergeChangeSets(all.map((one) => one.apply(answer)));
|
|
1537
|
+
}
|
|
1538
|
+
async function settlement(local, locator, before, record, queue, settling = {}) {
|
|
1539
|
+
const view = settling.view ?? 'confirmed';
|
|
1253
1540
|
const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
1254
|
-
const
|
|
1541
|
+
const overrides = settling.changes;
|
|
1542
|
+
const read = await sliceAt(record, view, queue, [
|
|
1543
|
+
...(before !== undefined && before.record.key === record.key ? before.overlaid : []),
|
|
1544
|
+
...(overrides?.set.map(({ path }) => path) ?? []),
|
|
1545
|
+
...(overrides?.removed ?? []),
|
|
1546
|
+
]);
|
|
1547
|
+
const confirmedLayer = overrides === undefined
|
|
1548
|
+
? read
|
|
1549
|
+
: await stepped(slicingWith(read, overrides.set, overrides.removed));
|
|
1550
|
+
const composition = compositionOf(record);
|
|
1551
|
+
const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
|
|
1255
1552
|
const founded = kept.filter((batch) => !queue.includes(batch));
|
|
1256
1553
|
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
1257
|
-
const unqueued =
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
}
|
|
1554
|
+
const unqueued = [];
|
|
1555
|
+
for (const batch of withdrawn) {
|
|
1556
|
+
unqueued.push({ batch, landed: await landedWhole(locator, batch, confirmedLayer) });
|
|
1557
|
+
}
|
|
1261
1558
|
const returning = [
|
|
1262
|
-
...[...unqueued].reverse(),
|
|
1263
|
-
|
|
1559
|
+
...[...unqueued].reverse().map(({ batch, landed: held }) => ({
|
|
1560
|
+
batch,
|
|
1561
|
+
returned: localSaves || held ? null : returnedBy('withdrawn'),
|
|
1562
|
+
})),
|
|
1563
|
+
...(settling.leaving ?? []),
|
|
1264
1564
|
];
|
|
1265
1565
|
const written = new Map();
|
|
1266
|
-
for (const { batch, returned } of returning) {
|
|
1267
|
-
if (
|
|
1566
|
+
for (const { batch, returned: reason } of returning) {
|
|
1567
|
+
if (reason === null || batch.between?.destination.room === locator)
|
|
1268
1568
|
continue;
|
|
1269
|
-
for (const saved of pendings.get(batch.id)?.drafts ?? []) {
|
|
1569
|
+
for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
|
|
1270
1570
|
const existing = written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path);
|
|
1271
1571
|
written.set(saved.path, {
|
|
1272
1572
|
locator,
|
|
1273
1573
|
path: saved.path,
|
|
1274
1574
|
revision: roomDraftRevision(),
|
|
1275
1575
|
parts: returnedInto(existing?.parts, saved.parts),
|
|
1276
|
-
returned,
|
|
1576
|
+
returned: reason,
|
|
1277
1577
|
});
|
|
1278
1578
|
}
|
|
1279
1579
|
}
|
|
1280
|
-
|
|
1580
|
+
const carried = carriedBack(locator, [
|
|
1581
|
+
...[...unqueued].reverse().flatMap(({ batch, landed: held }) => (held ? [] : [batch])),
|
|
1582
|
+
...(settling.leaving ?? []).flatMap(({ batch, returned }) => returned === null ? [] : [batch]),
|
|
1583
|
+
], written);
|
|
1584
|
+
const replayedLayer = await replayed(record, confirmedLayer, kept);
|
|
1585
|
+
const { overlay, touched } = await overlayDiff(record, before, replayedLayer.visible, replayedLayer.touched);
|
|
1586
|
+
const commit = {
|
|
1587
|
+
rooms: [
|
|
1588
|
+
{
|
|
1589
|
+
locator,
|
|
1590
|
+
key: record.key,
|
|
1591
|
+
record: storedRecord(record),
|
|
1592
|
+
roots: record.typedSelection?.roots ?? [],
|
|
1593
|
+
...settling.commit,
|
|
1594
|
+
...(overrides === undefined
|
|
1595
|
+
? {}
|
|
1596
|
+
: { confirmed: { set: overrides.set, removed: overrides.removed } }),
|
|
1597
|
+
overlay,
|
|
1598
|
+
},
|
|
1599
|
+
],
|
|
1600
|
+
writes: [
|
|
1601
|
+
{
|
|
1602
|
+
locator,
|
|
1603
|
+
queue: kept,
|
|
1604
|
+
founded,
|
|
1605
|
+
roomDrafts: [...written.values()],
|
|
1606
|
+
consumedRoomDrafts: carried.taken,
|
|
1607
|
+
},
|
|
1608
|
+
...carried.writes,
|
|
1609
|
+
],
|
|
1610
|
+
};
|
|
1281
1611
|
const staying = new Set(kept.map((batch) => batch.id));
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
catch (error) {
|
|
1296
|
-
const held = before === undefined ? null : await stepped(replicating(before.confirmed, kept, before));
|
|
1297
|
-
const set = await stepped(changing(locator, before ?? null, held, {
|
|
1298
|
-
queue: queueChanged,
|
|
1299
|
-
drafts: [...written.keys()],
|
|
1300
|
-
}));
|
|
1301
|
-
if (held !== null)
|
|
1302
|
-
replicas.set(locator, held);
|
|
1303
|
-
else if (kept.length > 0)
|
|
1304
|
-
queuesWithoutReplica.set(locator, kept);
|
|
1305
|
-
else
|
|
1612
|
+
return {
|
|
1613
|
+
locator,
|
|
1614
|
+
left: previousQueue.filter((batch) => !staying.has(batch.id)),
|
|
1615
|
+
commit,
|
|
1616
|
+
apply: (answer) => {
|
|
1617
|
+
for (const batch of previousQueue) {
|
|
1618
|
+
if (!staying.has(batch.id))
|
|
1619
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
1620
|
+
}
|
|
1621
|
+
for (const draft of written.values())
|
|
1622
|
+
rememberRoomDraft(draft);
|
|
1623
|
+
carried.remember();
|
|
1624
|
+
const drafts = [...written.keys(), ...carried.taken];
|
|
1306
1625
|
queuesWithoutReplica.delete(locator);
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1626
|
+
for (const { batch, landed: held } of unqueued) {
|
|
1627
|
+
const outcome = localSaves ? 'dropped' : held ? 'landed' : 'withdrawn';
|
|
1628
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
1629
|
+
}
|
|
1630
|
+
const changed = answer.changed[locator];
|
|
1631
|
+
const confirmedChanged = settling.commit?.published !== undefined ||
|
|
1632
|
+
settling.commit?.refreshed === true ||
|
|
1633
|
+
overrides !== undefined;
|
|
1634
|
+
const { count, indexed } = committedRoom(locator, answer);
|
|
1635
|
+
replicas.set(locator, {
|
|
1636
|
+
record,
|
|
1637
|
+
queue: kept,
|
|
1638
|
+
overlaid: touched,
|
|
1639
|
+
count,
|
|
1640
|
+
confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
|
|
1641
|
+
indexed,
|
|
1642
|
+
});
|
|
1643
|
+
return mergeChangeSets([
|
|
1644
|
+
changeSet(locator, {
|
|
1645
|
+
documents: changed === undefined ? [] : changed,
|
|
1646
|
+
room: settling.room,
|
|
1647
|
+
queue: queueChanged,
|
|
1648
|
+
drafts,
|
|
1649
|
+
}),
|
|
1650
|
+
...carried.changes,
|
|
1651
|
+
]);
|
|
1652
|
+
},
|
|
1322
1653
|
};
|
|
1323
1654
|
}
|
|
1324
|
-
function heldChange(locator, before, next, set, changed) {
|
|
1325
|
-
const held = replicas.get(locator) ?? null;
|
|
1326
|
-
return held === next ? set : changeSet(locator, before, held, changed);
|
|
1327
|
-
}
|
|
1328
1655
|
function adoptModels(locator, record) {
|
|
1329
1656
|
for (const [attached, model] of [
|
|
1330
1657
|
['collection', record.model],
|
|
@@ -1343,7 +1670,7 @@ export function composeRuntime(options) {
|
|
|
1343
1670
|
const taken = (answer, previous, attached) => {
|
|
1344
1671
|
if (answer === undefined)
|
|
1345
1672
|
return previous;
|
|
1346
|
-
const { state, refused } = attachedModel(answer, previous);
|
|
1673
|
+
const { state, refused } = attachedModel(answer, previous, attached === 'collection' ? roomTypes : NO_ROOM_TYPES);
|
|
1347
1674
|
if (refused !== null) {
|
|
1348
1675
|
const where = refused.location === undefined ? '' : ` ${refused.location}`;
|
|
1349
1676
|
journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
|
|
@@ -1356,65 +1683,76 @@ export function composeRuntime(options) {
|
|
|
1356
1683
|
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
1357
1684
|
};
|
|
1358
1685
|
}
|
|
1359
|
-
function publish(local, room,
|
|
1360
|
-
const
|
|
1361
|
-
return exclusive(async () => {
|
|
1362
|
-
if (stored !== undefined) {
|
|
1363
|
-
await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
|
|
1364
|
-
}
|
|
1686
|
+
function publish(local, room, record, landing, live) {
|
|
1687
|
+
const landed = exclusive(async () => {
|
|
1365
1688
|
if (!live()) {
|
|
1366
1689
|
caughtUpTo(room.locator);
|
|
1367
1690
|
return true;
|
|
1368
1691
|
}
|
|
1369
1692
|
const prior = replicas.get(room.locator);
|
|
1370
|
-
if (
|
|
1371
|
-
(prior
|
|
1372
|
-
|
|
1693
|
+
if (landing.kind === 'refreshed' &&
|
|
1694
|
+
(prior === undefined || prior.record.key !== record.key || prior.confirmed !== landing.base)) {
|
|
1695
|
+
await local.unrefresh(record.key);
|
|
1373
1696
|
return false;
|
|
1374
1697
|
}
|
|
1375
|
-
adoptModels(room.locator,
|
|
1698
|
+
adoptModels(room.locator, record);
|
|
1376
1699
|
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
1377
|
-
const roomVersion =
|
|
1700
|
+
const roomVersion = record.roomVersion;
|
|
1378
1701
|
const accepted = queue.filter((batch) => {
|
|
1379
|
-
const
|
|
1380
|
-
return
|
|
1702
|
+
const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
|
|
1703
|
+
return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
|
|
1381
1704
|
});
|
|
1382
|
-
let
|
|
1705
|
+
let set;
|
|
1383
1706
|
try {
|
|
1384
|
-
|
|
1707
|
+
set = await settle(local, room.locator, prior, record, queue.filter((batch) => !accepted.includes(batch)), landing.kind === 'refreshed'
|
|
1708
|
+
? {
|
|
1709
|
+
room: true,
|
|
1710
|
+
view: 'refreshed',
|
|
1711
|
+
commit: { refreshed: true, composition: compositionOf(record) },
|
|
1712
|
+
}
|
|
1713
|
+
: {
|
|
1714
|
+
room: true,
|
|
1715
|
+
view: landing.refreshed ? 'refreshed' : 'confirmed',
|
|
1716
|
+
commit: {
|
|
1717
|
+
published: { from: prior?.record.key ?? null },
|
|
1718
|
+
composition: compositionOf(record),
|
|
1719
|
+
...(landing.refreshed ? { refreshed: true } : {}),
|
|
1720
|
+
},
|
|
1721
|
+
});
|
|
1385
1722
|
}
|
|
1386
1723
|
catch (error) {
|
|
1387
|
-
if (!
|
|
1724
|
+
if (!refusedForSpace(error))
|
|
1388
1725
|
throw error;
|
|
1389
|
-
await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
|
|
1390
1726
|
caughtUpTo(room.locator);
|
|
1391
1727
|
return true;
|
|
1392
1728
|
}
|
|
1393
|
-
|
|
1394
|
-
if (stored !== undefined) {
|
|
1729
|
+
if (landing.kind === 'bootstrapped') {
|
|
1395
1730
|
transfers.delete(room.locator);
|
|
1396
1731
|
catching.delete(room.locator);
|
|
1732
|
+
sweep();
|
|
1397
1733
|
}
|
|
1398
1734
|
for (const batch of accepted)
|
|
1399
|
-
pendings.delete(batch.id);
|
|
1735
|
+
pendings.delete(pendingKey(room.locator, batch.id));
|
|
1400
1736
|
stale.delete(room.locator);
|
|
1401
|
-
|
|
1737
|
+
unpreparable.delete(room.locator);
|
|
1402
1738
|
if (unreadable.delete(room.locator))
|
|
1403
1739
|
scopedRefusals.delete(room.locator);
|
|
1404
1740
|
sourceRefusals.delete(room.locator);
|
|
1405
1741
|
declarationRefusals.delete(room.locator);
|
|
1406
1742
|
list(room, true);
|
|
1407
|
-
if (prior === undefined)
|
|
1408
|
-
prepared.add(room.locator);
|
|
1409
1743
|
publishChangeSet(set);
|
|
1744
|
+
await unpair(local);
|
|
1410
1745
|
moveTo({ ...status, ...attention() });
|
|
1411
1746
|
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
1747
|
+
if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1748
|
+
submitLater(room.locator);
|
|
1749
|
+
}
|
|
1412
1750
|
return true;
|
|
1413
1751
|
});
|
|
1752
|
+
return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
|
|
1414
1753
|
}
|
|
1415
1754
|
async function savedRoom(locator) {
|
|
1416
1755
|
const local = await openLocal();
|
|
1417
|
-
await referenced(locator);
|
|
1418
1756
|
const replica = replicas.get(locator);
|
|
1419
1757
|
const room = rooms.get(locator);
|
|
1420
1758
|
if (replica === undefined || room === undefined) {
|
|
@@ -1444,13 +1782,19 @@ export function composeRuntime(options) {
|
|
|
1444
1782
|
}
|
|
1445
1783
|
const target = await savedRoom(locator);
|
|
1446
1784
|
const saved = [];
|
|
1785
|
+
const files = [];
|
|
1786
|
+
const filed = read.filter(({ parts }) => parts.file !== undefined).map(({ path }) => path);
|
|
1787
|
+
const visible = filed.length === 0 ? undefined : await visibleOf(locator, filed);
|
|
1447
1788
|
for (const { path, parts } of read) {
|
|
1448
1789
|
const file = parts.file === undefined
|
|
1449
1790
|
? undefined
|
|
1450
|
-
: await heldFile(parts.file.blob, binaryBase(
|
|
1791
|
+
: await heldFile(parts.file.blob, binaryBase(visible, path));
|
|
1792
|
+
if (parts.file !== undefined && file !== undefined) {
|
|
1793
|
+
files.push({ key: file.localId, blob: parts.file.blob });
|
|
1794
|
+
}
|
|
1451
1795
|
saved.push({ path, parts: held(undefined, parts, file) });
|
|
1452
1796
|
}
|
|
1453
|
-
return committed(locator, target, saved, new Set(), writtenBy);
|
|
1797
|
+
return committed(locator, target, saved, new Set(), writtenBy, files);
|
|
1454
1798
|
}
|
|
1455
1799
|
function carriedDrafts(locator, saved, consumed) {
|
|
1456
1800
|
const holding = roomDrafts.get(locator);
|
|
@@ -1464,13 +1808,361 @@ export function composeRuntime(options) {
|
|
|
1464
1808
|
return [[path, { ...record, path: to, revision: roomDraftRevision() }]];
|
|
1465
1809
|
}));
|
|
1466
1810
|
}
|
|
1467
|
-
async function
|
|
1468
|
-
const
|
|
1811
|
+
async function savingIn(replica, changes, extra, leaving = []) {
|
|
1812
|
+
const record = replica.record;
|
|
1813
|
+
const local = await openLocal();
|
|
1814
|
+
const paths = [
|
|
1815
|
+
...extra,
|
|
1816
|
+
...changes.flatMap((change) => change.kind === 'move' ? [change.path, change.to] : [change.path]),
|
|
1817
|
+
];
|
|
1818
|
+
const moved = [
|
|
1819
|
+
...leaving,
|
|
1820
|
+
...changes.flatMap((change) => (change.kind === 'move' ? [change.path] : [])),
|
|
1821
|
+
];
|
|
1822
|
+
const removed = changes.flatMap((change) => (change.kind === 'remove' ? [change.path] : []));
|
|
1823
|
+
const referring = removed.length === 0
|
|
1824
|
+
? []
|
|
1825
|
+
: (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
|
|
1826
|
+
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
|
|
1827
|
+
const { visible } = await replayed(record, confirmedLayer, replica.queue);
|
|
1828
|
+
const composition = compositionOf(record);
|
|
1829
|
+
return {
|
|
1830
|
+
record,
|
|
1831
|
+
confirmed: confirmedLayer,
|
|
1832
|
+
visible,
|
|
1833
|
+
*visibleOver(queue) {
|
|
1834
|
+
return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
|
|
1835
|
+
},
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
async function unmodeledIn(record) {
|
|
1839
|
+
const composition = compositionFor(record);
|
|
1840
|
+
if (available(composition))
|
|
1841
|
+
return false;
|
|
1842
|
+
const local = await openLocal();
|
|
1843
|
+
const roots = record.typedSelection?.roots ?? [];
|
|
1844
|
+
if (composition.model.status === 'unavailable' &&
|
|
1845
|
+
(await local.typedWithin(record.key, 'visible', roots, false))) {
|
|
1846
|
+
return true;
|
|
1847
|
+
}
|
|
1848
|
+
return (composition.selection !== null &&
|
|
1849
|
+
composition.selection.model.status === 'unavailable' &&
|
|
1850
|
+
(await local.typedWithin(record.key, 'visible', roots, true)));
|
|
1851
|
+
}
|
|
1852
|
+
function textDiffers(left, right) {
|
|
1853
|
+
return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
|
|
1854
|
+
!sameBinary(left?.binary ?? undefined, right?.binary ?? undefined));
|
|
1855
|
+
}
|
|
1856
|
+
function textOf(entry) {
|
|
1857
|
+
if (entry === null)
|
|
1858
|
+
return undefined;
|
|
1859
|
+
return entry.binary !== null ? null : entry.content;
|
|
1860
|
+
}
|
|
1861
|
+
function walkOf(local, key, view) {
|
|
1862
|
+
return async (after) => {
|
|
1863
|
+
const page = await local.walk(key, view, after, WALK_BYTES);
|
|
1864
|
+
return {
|
|
1865
|
+
entries: page.entries.map(({ path, content, binary }) => ({
|
|
1866
|
+
path,
|
|
1867
|
+
content: binary !== null ? null : content,
|
|
1868
|
+
})),
|
|
1869
|
+
next: page.next,
|
|
1870
|
+
};
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
function lookupOf(local, key, view) {
|
|
1874
|
+
return async (paths) => {
|
|
1875
|
+
if (paths.length === 0)
|
|
1876
|
+
return new Map();
|
|
1877
|
+
return new Map((await local.typed(key, view, paths)).map(({ path, type, binary }) => [
|
|
1878
|
+
path,
|
|
1879
|
+
{ type: type ?? undefined, binary },
|
|
1880
|
+
]));
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
function declaredOf(entry) {
|
|
1884
|
+
if (entry.binary !== null)
|
|
1885
|
+
return { type: undefined, binary: true };
|
|
1886
|
+
const read = parseFrontmatter(entry.content ?? '');
|
|
1887
|
+
return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
|
|
1888
|
+
}
|
|
1889
|
+
function overriddenWalk(walk, overrides) {
|
|
1890
|
+
const sorted = [...overrides.keys()].sort();
|
|
1891
|
+
const beyond = (path) => {
|
|
1892
|
+
if (path === null)
|
|
1893
|
+
return 0;
|
|
1894
|
+
let low = 0;
|
|
1895
|
+
let high = sorted.length;
|
|
1896
|
+
while (low < high) {
|
|
1897
|
+
const middle = (low + high) >>> 1;
|
|
1898
|
+
if (sorted[middle] <= path)
|
|
1899
|
+
low = middle + 1;
|
|
1900
|
+
else
|
|
1901
|
+
high = middle;
|
|
1902
|
+
}
|
|
1903
|
+
return low;
|
|
1904
|
+
};
|
|
1905
|
+
return async (after) => {
|
|
1906
|
+
const page = await walk(after);
|
|
1907
|
+
const end = page.next;
|
|
1908
|
+
const added = sorted
|
|
1909
|
+
.slice(beyond(after), end === null ? sorted.length : beyond(end))
|
|
1910
|
+
.flatMap((path) => {
|
|
1911
|
+
const entry = overrides.get(path);
|
|
1912
|
+
return entry === null || entry === undefined
|
|
1913
|
+
? []
|
|
1914
|
+
: [{ path, content: entry.binary !== null ? null : entry.content }];
|
|
1915
|
+
});
|
|
1916
|
+
const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
|
|
1917
|
+
return { entries, next: end };
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
function overriddenLookup(lookup, overrides) {
|
|
1921
|
+
return async (paths) => {
|
|
1922
|
+
const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
|
|
1923
|
+
for (const path of paths) {
|
|
1924
|
+
const entry = overrides.get(path);
|
|
1925
|
+
if (entry === undefined)
|
|
1926
|
+
continue;
|
|
1927
|
+
if (entry === null)
|
|
1928
|
+
known.delete(path);
|
|
1929
|
+
else
|
|
1930
|
+
known.set(path, declaredOf(entry));
|
|
1931
|
+
}
|
|
1932
|
+
return known;
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1935
|
+
function validatorOf(record, confirmedLayer) {
|
|
1936
|
+
return async (resulting, touched) => {
|
|
1937
|
+
const composition = compositionFor(record);
|
|
1938
|
+
const local = await openLocal();
|
|
1939
|
+
const changed = [];
|
|
1940
|
+
const overrides = new Map();
|
|
1941
|
+
const pace = paced();
|
|
1942
|
+
for (const path of new Set([...confirmedLayer.held, ...touched])) {
|
|
1943
|
+
await pace(ROW_WEIGHT);
|
|
1944
|
+
const was = entryAt(confirmedLayer, path);
|
|
1945
|
+
const now = entryAt(resulting, path);
|
|
1946
|
+
if (!textDiffers(was, now))
|
|
1947
|
+
continue;
|
|
1948
|
+
overrides.set(path, now);
|
|
1949
|
+
if (textOf(was) !== textOf(now))
|
|
1950
|
+
changed.push({ path, was: textOf(was), now: textOf(now) });
|
|
1951
|
+
}
|
|
1952
|
+
const lookup = overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides);
|
|
1953
|
+
const clean = available(composition) &&
|
|
1954
|
+
changed.length <= CLEAN_CHANGES &&
|
|
1955
|
+
(await changedCleanly(changed, compositionOf(record), composition, {
|
|
1956
|
+
lookup,
|
|
1957
|
+
texts: textsOf(local, record.key, 'confirmed'),
|
|
1958
|
+
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
1959
|
+
}));
|
|
1960
|
+
if (clean)
|
|
1961
|
+
return { verdict: 'accept' };
|
|
1962
|
+
return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
function textsOf(local, key, view) {
|
|
1966
|
+
return async (paths) => new Map((await local.entries(key, view, paths)).map(({ path, content, binary }) => [
|
|
1967
|
+
path,
|
|
1968
|
+
binary !== null ? null : content,
|
|
1969
|
+
]));
|
|
1970
|
+
}
|
|
1971
|
+
function referrersOf(local, key, view) {
|
|
1972
|
+
return async (targets) => [
|
|
1973
|
+
...new Set((await local.referrers(key, view, targets, CHECKED_LINKS)).map(({ path }) => path)),
|
|
1974
|
+
];
|
|
1975
|
+
}
|
|
1976
|
+
async function sideOf(locator, replica, room, saving) {
|
|
1977
|
+
const record = replica.record;
|
|
1978
|
+
return {
|
|
1979
|
+
room: locator,
|
|
1980
|
+
handle: room.handle,
|
|
1981
|
+
baseRoomVersion: record.roomVersion,
|
|
1982
|
+
composition: compositionFor(record),
|
|
1983
|
+
operations: room.operations,
|
|
1984
|
+
providerOwnedPaths: room.providerOwnedPaths,
|
|
1985
|
+
visible: saving.visible,
|
|
1986
|
+
queued: queuedDocuments(replica.queue),
|
|
1987
|
+
unmodeled: await unmodeledIn(record),
|
|
1988
|
+
validate: validatorOf(record, saving.confirmed),
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
function movedInto(locator, saved) {
|
|
1992
|
+
const into = saved.find(({ parts }) => parts.move?.room !== undefined && parts.move.room !== locator);
|
|
1993
|
+
return into?.parts.move?.room ?? null;
|
|
1994
|
+
}
|
|
1995
|
+
async function saveCommit(local, files, rooms) {
|
|
1996
|
+
const touched = new Map();
|
|
1997
|
+
const commits = [];
|
|
1998
|
+
for (const { locator, saving, before, queue } of rooms) {
|
|
1999
|
+
const replayedLayer = await replayed(saving.record, saving.confirmed, queue);
|
|
2000
|
+
const diff = await overlayDiff(saving.record, before, replayedLayer.visible, replayedLayer.touched);
|
|
2001
|
+
touched.set(locator, diff.touched);
|
|
2002
|
+
commits.push({
|
|
2003
|
+
locator,
|
|
2004
|
+
key: saving.record.key,
|
|
2005
|
+
roots: saving.record.typedSelection?.roots ?? [],
|
|
2006
|
+
overlay: diff.overlay,
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
try {
|
|
2010
|
+
const answer = await local.commit({
|
|
2011
|
+
rooms: commits,
|
|
2012
|
+
writes: rooms.map(({ write }) => write),
|
|
2013
|
+
files,
|
|
2014
|
+
whole: true,
|
|
2015
|
+
});
|
|
2016
|
+
return { answer, touched };
|
|
2017
|
+
}
|
|
2018
|
+
catch {
|
|
2019
|
+
return null;
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
function savedReplica(locator, before, queue, touched, answer) {
|
|
2023
|
+
replicas.set(locator, {
|
|
2024
|
+
...before,
|
|
2025
|
+
queue,
|
|
2026
|
+
overlaid: touched,
|
|
2027
|
+
count: answer.counts[locator] ?? before.count,
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
2030
|
+
async function committedMove(locator, destination, { local, replica, room }, saved, consumed, writtenBy, files) {
|
|
2031
|
+
const mixed = saved.find(({ parts }) => Object.keys(parts).length !== 1 || parts.move?.room !== destination);
|
|
2032
|
+
if (mixed !== undefined) {
|
|
2033
|
+
return { outcome: 'refused', code: 'mixed-room-move', retryable: false, file: mixed.path };
|
|
2034
|
+
}
|
|
2035
|
+
const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
|
|
2036
|
+
const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
|
|
2037
|
+
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, sourceSaving.visible)));
|
|
1469
2038
|
if (changed.length > 0)
|
|
1470
2039
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
1471
|
-
const
|
|
1472
|
-
const
|
|
1473
|
-
|
|
2040
|
+
const target = replicas.get(destination);
|
|
2041
|
+
const listedRoom = rooms.get(destination);
|
|
2042
|
+
if (target === undefined || listedRoom === undefined) {
|
|
2043
|
+
throw runtimeError('room-not-held', `No Local Replica for ${destination}`);
|
|
2044
|
+
}
|
|
2045
|
+
const arriving = await savingIn(target, [], moves.map(({ to }) => to));
|
|
2046
|
+
const built = await buildMove({
|
|
2047
|
+
source: await sideOf(locator, replica, room, sourceSaving),
|
|
2048
|
+
destination: await sideOf(destination, target, listedRoom, arriving),
|
|
2049
|
+
moves,
|
|
2050
|
+
providerLimits: limits,
|
|
2051
|
+
local: localSaves,
|
|
2052
|
+
...(writtenBy === undefined ? {} : { writtenBy }),
|
|
2053
|
+
});
|
|
2054
|
+
if (built.kind === 'refused')
|
|
2055
|
+
return { outcome: 'refused', ...built.refusal };
|
|
2056
|
+
const id = built.leaving.id;
|
|
2057
|
+
const carrying = saved.flatMap(({ path, parts }) => {
|
|
2058
|
+
const record = roomDrafts.get(locator)?.get(path);
|
|
2059
|
+
const to = moveTarget(parts.move);
|
|
2060
|
+
if (record === undefined || consumed.has(path) || roomDrafts.get(destination)?.has(to)) {
|
|
2061
|
+
return [];
|
|
2062
|
+
}
|
|
2063
|
+
const moved = {
|
|
2064
|
+
...record,
|
|
2065
|
+
locator: destination,
|
|
2066
|
+
path: to,
|
|
2067
|
+
revision: roomDraftRevision(),
|
|
2068
|
+
};
|
|
2069
|
+
return [{ from: path, record: moved }];
|
|
2070
|
+
});
|
|
2071
|
+
const carried = carrying.map(({ record }) => record);
|
|
2072
|
+
const marked = carried.map(({ path }) => ({ path, parts: {} }));
|
|
2073
|
+
const left = [...consumed, ...carrying.map(({ from }) => from)];
|
|
2074
|
+
const leavingQueue = [...replica.queue, built.leaving];
|
|
2075
|
+
const arrivingQueue = [...target.queue, built.arriving];
|
|
2076
|
+
const committedBoth = await saveCommit(local, files, [
|
|
2077
|
+
{
|
|
2078
|
+
locator,
|
|
2079
|
+
saving: sourceSaving,
|
|
2080
|
+
before: replica,
|
|
2081
|
+
queue: leavingQueue,
|
|
2082
|
+
write: {
|
|
2083
|
+
locator,
|
|
2084
|
+
queue: leavingQueue,
|
|
2085
|
+
saved: new Map([[id, saved]]),
|
|
2086
|
+
consumedRoomDrafts: left,
|
|
2087
|
+
},
|
|
2088
|
+
},
|
|
2089
|
+
{
|
|
2090
|
+
locator: destination,
|
|
2091
|
+
saving: arriving,
|
|
2092
|
+
before: target,
|
|
2093
|
+
queue: arrivingQueue,
|
|
2094
|
+
write: {
|
|
2095
|
+
locator: destination,
|
|
2096
|
+
queue: arrivingQueue,
|
|
2097
|
+
saved: new Map([[id, marked]]),
|
|
2098
|
+
roomDrafts: carried,
|
|
2099
|
+
},
|
|
2100
|
+
},
|
|
2101
|
+
]);
|
|
2102
|
+
if (committedBoth === null)
|
|
2103
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2104
|
+
const { answer, touched } = committedBoth;
|
|
2105
|
+
if (answer.fenced !== null) {
|
|
2106
|
+
throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
|
|
2107
|
+
}
|
|
2108
|
+
if (fences.has(locator) || fences.has(destination)) {
|
|
2109
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
2110
|
+
}
|
|
2111
|
+
pendings.set(pendingKey(locator, id), {
|
|
2112
|
+
drafts: saved,
|
|
2113
|
+
submission: { state: 'waiting' },
|
|
2114
|
+
lost: false,
|
|
2115
|
+
});
|
|
2116
|
+
pendings.set(pendingKey(destination, id), {
|
|
2117
|
+
drafts: marked,
|
|
2118
|
+
submission: { state: 'waiting' },
|
|
2119
|
+
lost: false,
|
|
2120
|
+
});
|
|
2121
|
+
for (const path of left)
|
|
2122
|
+
forgetRoomDraft(locator, path);
|
|
2123
|
+
for (const record of carried)
|
|
2124
|
+
rememberRoomDraft(record);
|
|
2125
|
+
savedReplica(locator, replica, leavingQueue, touched.get(locator), answer);
|
|
2126
|
+
savedReplica(destination, target, arrivingQueue, touched.get(destination), answer);
|
|
2127
|
+
publishChangeSet(mergeChangeSets([
|
|
2128
|
+
changeSet(locator, {
|
|
2129
|
+
documents: answer.changed[locator] ?? [],
|
|
2130
|
+
queue: true,
|
|
2131
|
+
drafts: left,
|
|
2132
|
+
}),
|
|
2133
|
+
changeSet(destination, {
|
|
2134
|
+
documents: answer.changed[destination] ?? [],
|
|
2135
|
+
queue: true,
|
|
2136
|
+
drafts: carried.map(({ path }) => path),
|
|
2137
|
+
}),
|
|
2138
|
+
]));
|
|
2139
|
+
queuedSave(locator);
|
|
2140
|
+
return { outcome: 'saved', identifier: id };
|
|
2141
|
+
}
|
|
2142
|
+
function queuedSave(locator) {
|
|
2143
|
+
if (!localSaves)
|
|
2144
|
+
recorded = false;
|
|
2145
|
+
moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
|
|
2146
|
+
submitLater(locator);
|
|
2147
|
+
}
|
|
2148
|
+
async function committed(locator, target, saved, consumed, writtenBy, files = []) {
|
|
2149
|
+
if (full)
|
|
2150
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2151
|
+
const into = movedInto(locator, saved);
|
|
2152
|
+
if (into !== null) {
|
|
2153
|
+
return committedMove(locator, into, target, saved, consumed, writtenBy, files);
|
|
2154
|
+
}
|
|
2155
|
+
const { local, replica, room } = target;
|
|
2156
|
+
const parts = saved.map(({ path, parts: held }) => changesOf(path, held));
|
|
2157
|
+
const changes = parts.flatMap(({ changes: made }) => made);
|
|
2158
|
+
const saving = await savingIn(replica, changes, [
|
|
2159
|
+
...saved.map(({ path }) => path),
|
|
2160
|
+
...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
|
|
2161
|
+
]);
|
|
2162
|
+
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, underneath(held, path, saving.visible)));
|
|
2163
|
+
if (changed.length > 0)
|
|
2164
|
+
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2165
|
+
const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
|
|
1474
2166
|
if (requeue.kind === 'refused')
|
|
1475
2167
|
return { outcome: 'refused', ...requeue.refusal };
|
|
1476
2168
|
const foreign = [...requeue.withdrawn.keys()].some((origin) => {
|
|
@@ -1488,22 +2180,19 @@ export function composeRuntime(options) {
|
|
|
1488
2180
|
const withdrawing = requeue.withdrawn.size > 0;
|
|
1489
2181
|
const input = {
|
|
1490
2182
|
local: localSaves,
|
|
1491
|
-
baseRoomVersion: replica.
|
|
1492
|
-
composition: compositionFor(replica.
|
|
2183
|
+
baseRoomVersion: replica.record.roomVersion,
|
|
2184
|
+
composition: compositionFor(replica.record),
|
|
1493
2185
|
operations: room.operations,
|
|
1494
2186
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
1495
2187
|
providerLimits: limits,
|
|
1496
2188
|
handle: room.handle,
|
|
1497
|
-
visible:
|
|
2189
|
+
visible: saving.visible,
|
|
1498
2190
|
changes,
|
|
1499
2191
|
bases: parts.flatMap(({ bases }) => bases),
|
|
1500
2192
|
queued: queuedDocuments(replica.queue),
|
|
1501
2193
|
writtenBy,
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
binaries: replica.confirmed.record.binaries,
|
|
1505
|
-
composition: compositionOf(replica.confirmed.record),
|
|
1506
|
-
},
|
|
2194
|
+
unmodeled: await unmodeledIn(replica.record),
|
|
2195
|
+
validate: validatorOf(replica.record, saving.confirmed),
|
|
1507
2196
|
};
|
|
1508
2197
|
const checked = withdrawing
|
|
1509
2198
|
? await buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
|
|
@@ -1528,8 +2217,7 @@ export function composeRuntime(options) {
|
|
|
1528
2217
|
? null
|
|
1529
2218
|
: await buildBatch({
|
|
1530
2219
|
...input,
|
|
1531
|
-
visible:
|
|
1532
|
-
.visible,
|
|
2220
|
+
visible: await stepped(saving.visibleOver(requeue.queue)),
|
|
1533
2221
|
changes: requeue.changes,
|
|
1534
2222
|
queued: queuedDocuments(requeue.queue),
|
|
1535
2223
|
});
|
|
@@ -1548,7 +2236,7 @@ export function composeRuntime(options) {
|
|
|
1548
2236
|
for (const [origin, replaced] of withdrawn) {
|
|
1549
2237
|
if (replaced === null)
|
|
1550
2238
|
continue;
|
|
1551
|
-
const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
|
|
2239
|
+
const carried = withdrawnDrafts(pendings.get(pendingKey(locator, origin))?.drafts ?? [], changes);
|
|
1552
2240
|
drafted.set(replaced.id, replaced === join?.batch
|
|
1553
2241
|
? [
|
|
1554
2242
|
...carried.map((held) => joinedDraft(held, saved)),
|
|
@@ -1562,66 +2250,77 @@ export function composeRuntime(options) {
|
|
|
1562
2250
|
for (const replaced of withdrawn.values()) {
|
|
1563
2251
|
if (replaced === null || !batches.includes(replaced))
|
|
1564
2252
|
continue;
|
|
1565
|
-
const limited =
|
|
2253
|
+
const limited = await stepped(limiting(batches, replaced, limits, saving.visibleOver));
|
|
1566
2254
|
if (limited !== null)
|
|
1567
2255
|
return { outcome: 'refused', ...limited };
|
|
1568
2256
|
}
|
|
1569
2257
|
const carried = carriedDrafts(locator, saved, consumed);
|
|
1570
2258
|
const left = [...consumed, ...carried.keys()];
|
|
1571
2259
|
const arrived = [...carried.values()];
|
|
1572
|
-
|
|
1573
|
-
|
|
2260
|
+
const done = await saveCommit(local, files, [
|
|
2261
|
+
{
|
|
1574
2262
|
locator,
|
|
2263
|
+
saving,
|
|
2264
|
+
before: replica,
|
|
1575
2265
|
queue: batches,
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
2266
|
+
write: {
|
|
2267
|
+
locator,
|
|
2268
|
+
queue: batches,
|
|
2269
|
+
saved: drafted,
|
|
2270
|
+
consumedRoomDrafts: left,
|
|
2271
|
+
roomDrafts: arrived,
|
|
2272
|
+
},
|
|
2273
|
+
},
|
|
2274
|
+
]);
|
|
2275
|
+
if (done === null)
|
|
1582
2276
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2277
|
+
const { answer, touched } = done;
|
|
2278
|
+
if (answer.fenced !== null) {
|
|
2279
|
+
throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
|
|
1583
2280
|
}
|
|
1584
|
-
const next = await stepped(replicating(replica.confirmed, batches, replica));
|
|
1585
|
-
const set = await stepped(changing(locator, replica, next, {
|
|
1586
|
-
queue: true,
|
|
1587
|
-
drafts: [...left, ...arrived.map(({ path }) => path)],
|
|
1588
|
-
}));
|
|
1589
2281
|
if (fences.has(locator))
|
|
1590
2282
|
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1591
2283
|
for (const origin of withdrawn.keys())
|
|
1592
|
-
pendings.delete(origin);
|
|
2284
|
+
pendings.delete(pendingKey(locator, origin));
|
|
1593
2285
|
for (const [id, drafts] of drafted) {
|
|
1594
|
-
pendings.set(
|
|
2286
|
+
pendings.set(pendingKey(locator, id), {
|
|
2287
|
+
drafts,
|
|
2288
|
+
submission: { state: 'waiting' },
|
|
2289
|
+
lost: false,
|
|
2290
|
+
});
|
|
1595
2291
|
}
|
|
1596
|
-
if (!localSaves)
|
|
1597
|
-
recorded = false;
|
|
1598
2292
|
for (const path of left)
|
|
1599
2293
|
forgetRoomDraft(locator, path);
|
|
1600
2294
|
for (const record of arrived)
|
|
1601
2295
|
rememberRoomDraft(record);
|
|
1602
|
-
|
|
1603
|
-
publishChangeSet(
|
|
1604
|
-
|
|
1605
|
-
|
|
2296
|
+
savedReplica(locator, replica, batches, touched.get(locator), answer);
|
|
2297
|
+
publishChangeSet(changeSet(locator, {
|
|
2298
|
+
documents: answer.changed[locator] ?? [],
|
|
2299
|
+
queue: true,
|
|
2300
|
+
drafts: [...left, ...arrived.map(({ path }) => path)],
|
|
2301
|
+
}));
|
|
2302
|
+
queuedSave(locator);
|
|
1606
2303
|
const identifier = join?.batch.id ??
|
|
1607
2304
|
built?.batch.id ??
|
|
1608
2305
|
[...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
|
|
1609
2306
|
return { outcome: 'saved', identifier };
|
|
1610
2307
|
}
|
|
1611
|
-
async function mark(local, locator,
|
|
1612
|
-
const
|
|
1613
|
-
|
|
1614
|
-
state
|
|
1615
|
-
(
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
2308
|
+
async function mark(local, locator, batch, submission, lost) {
|
|
2309
|
+
const partner = partnerOf(batch, locator);
|
|
2310
|
+
for (const held of partner === null ? [locator] : [locator, partner]) {
|
|
2311
|
+
const state = pendings.get(pendingKey(held, batch.id));
|
|
2312
|
+
if (!continuing() ||
|
|
2313
|
+
state === undefined ||
|
|
2314
|
+
(JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
|
|
2315
|
+
continue;
|
|
2316
|
+
}
|
|
2317
|
+
await local.submit(held, batch.id, submission, lost);
|
|
2318
|
+
if (fences.has(held))
|
|
2319
|
+
continue;
|
|
2320
|
+
pendings.set(pendingKey(held, batch.id), { ...state, submission, lost });
|
|
2321
|
+
if (replicas.has(held))
|
|
2322
|
+
publishChangeSet(changeSet(held, { queue: true }));
|
|
2323
|
+
}
|
|
1625
2324
|
moveTo({ ...status, ...attention() });
|
|
1626
2325
|
}
|
|
1627
2326
|
async function keepUpload(local, locator, identifier, upload) {
|
|
@@ -1630,7 +2329,8 @@ export function composeRuntime(options) {
|
|
|
1630
2329
|
await local.keepUpload(locator, identifier, upload);
|
|
1631
2330
|
if (fences.has(locator))
|
|
1632
2331
|
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1633
|
-
|
|
2332
|
+
const key = pendingKey(locator, identifier);
|
|
2333
|
+
pendings.set(key, { ...pendings.get(key), upload });
|
|
1634
2334
|
if (!continuing())
|
|
1635
2335
|
throw new Error('connection refused');
|
|
1636
2336
|
}
|
|
@@ -1663,7 +2363,7 @@ export function composeRuntime(options) {
|
|
|
1663
2363
|
if (fencedSince(locator, since))
|
|
1664
2364
|
return failure({ kind: 'room-not-readable', locator });
|
|
1665
2365
|
if (result.kind === 'scoped-refusal') {
|
|
1666
|
-
const room = replicas.get(result.locator)?.
|
|
2366
|
+
const room = replicas.get(result.locator)?.record;
|
|
1667
2367
|
if (room !== undefined)
|
|
1668
2368
|
await scoped(result, room, await openLocal());
|
|
1669
2369
|
}
|
|
@@ -1676,19 +2376,55 @@ export function composeRuntime(options) {
|
|
|
1676
2376
|
return UNCERTAIN;
|
|
1677
2377
|
}
|
|
1678
2378
|
}
|
|
1679
|
-
function
|
|
2379
|
+
function located(room, path, destination) {
|
|
2380
|
+
const crossing = roomTarget(destination);
|
|
2381
|
+
if (crossing === null) {
|
|
2382
|
+
const named = target(path, destination);
|
|
2383
|
+
return named === null || !rooms.has(room) ? null : { room, path: named };
|
|
2384
|
+
}
|
|
2385
|
+
const held = [...rooms.values()].find((listed) => listed.handle === crossing.handle);
|
|
2386
|
+
return held === undefined ? null : { room: held.locator, path: crossing.path };
|
|
2387
|
+
}
|
|
2388
|
+
async function resolveReference(room, path, destination) {
|
|
2389
|
+
if (roomTarget(destination) === null && target(path, destination) === null) {
|
|
2390
|
+
return ABSENT_TARGET;
|
|
2391
|
+
}
|
|
2392
|
+
const at = located(room, path, destination);
|
|
2393
|
+
if (at === null)
|
|
2394
|
+
return UNREADABLE;
|
|
2395
|
+
if (locked(at.room))
|
|
2396
|
+
return LOCKED_UNREADABLE;
|
|
2397
|
+
const held = replicas.get(at.room);
|
|
2398
|
+
if (held !== undefined &&
|
|
2399
|
+
(await (await openLocal()).entries(held.record.key, 'visible', [at.path])).length > 0) {
|
|
2400
|
+
return { kind: 'document', ...at };
|
|
2401
|
+
}
|
|
2402
|
+
const history = await readHistory(at.room, at.path);
|
|
2403
|
+
if (history.kind === 'no-history' || history.kind === 'revision')
|
|
2404
|
+
return ABSENT_TARGET;
|
|
2405
|
+
if (history.kind !== 'history')
|
|
2406
|
+
return history;
|
|
2407
|
+
const [last] = history.revisions;
|
|
2408
|
+
if (last === undefined)
|
|
2409
|
+
return ABSENT_TARGET;
|
|
2410
|
+
return history.deleted === true
|
|
2411
|
+
? { kind: 'deleted', ...at, revision: last }
|
|
2412
|
+
: { kind: 'document', ...at };
|
|
2413
|
+
}
|
|
2414
|
+
function queuedFacts(locator, path, localId) {
|
|
1680
2415
|
for (const batch of replicas.get(locator)?.queue ?? []) {
|
|
1681
2416
|
const change = batch.changes[queuedFile(batch, path, localId)];
|
|
1682
2417
|
if (change?.kind === 'binary')
|
|
1683
|
-
return change
|
|
2418
|
+
return change;
|
|
1684
2419
|
}
|
|
1685
|
-
|
|
2420
|
+
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2421
|
+
return drafted?.localId === localId ? drafted : null;
|
|
1686
2422
|
}
|
|
1687
|
-
function
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
2423
|
+
function batchFile(batch) {
|
|
2424
|
+
return async (localId) => {
|
|
2425
|
+
const change = batch.changes.find((held) => held.kind === 'binary' && held.localId === localId);
|
|
2426
|
+
return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
|
|
2427
|
+
};
|
|
1692
2428
|
}
|
|
1693
2429
|
async function transferBinary(request, transfer, answer) {
|
|
1694
2430
|
if (closed || refusal !== null)
|
|
@@ -1702,18 +2438,15 @@ export function composeRuntime(options) {
|
|
|
1702
2438
|
if (closed || authorization === undefined || refusal !== null)
|
|
1703
2439
|
return UNCERTAIN;
|
|
1704
2440
|
const since = fencings;
|
|
1705
|
-
journal.record(request.
|
|
1706
|
-
? `transfer binary ${request.locator} ${request.path}`
|
|
1707
|
-
: `transfer binary ${request.fileRef}`);
|
|
2441
|
+
journal.record(`transfer binary ${request.locator} ${request.path}`);
|
|
1708
2442
|
try {
|
|
1709
2443
|
const result = await transfer(authorization);
|
|
1710
2444
|
const settled = result;
|
|
1711
2445
|
journal.record(`outcome binary ${settled.kind}`);
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
return failure({ kind: 'room-not-readable', locator });
|
|
2446
|
+
if (fencedSince(request.locator, since))
|
|
2447
|
+
return failure({ kind: 'room-not-readable', locator: request.locator });
|
|
1715
2448
|
if (settled.kind === 'scoped-refusal') {
|
|
1716
|
-
const room = replicas.get(settled.locator)?.
|
|
2449
|
+
const room = replicas.get(settled.locator)?.record;
|
|
1717
2450
|
if (room !== undefined)
|
|
1718
2451
|
await scoped(settled, room, await openLocal());
|
|
1719
2452
|
}
|
|
@@ -1726,68 +2459,88 @@ export function composeRuntime(options) {
|
|
|
1726
2459
|
return UNCERTAIN;
|
|
1727
2460
|
}
|
|
1728
2461
|
}
|
|
1729
|
-
function readBinary(request) {
|
|
1730
|
-
|
|
2462
|
+
async function readBinary(request, name) {
|
|
2463
|
+
if (closed)
|
|
2464
|
+
return UNCERTAIN;
|
|
2465
|
+
const held = await keptFiles.find(name);
|
|
2466
|
+
if (held !== null)
|
|
2467
|
+
return held;
|
|
2468
|
+
return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
|
|
1731
2469
|
}
|
|
1732
|
-
function binaryPath(locator, path, revision) {
|
|
2470
|
+
async function binaryPath(locator, path, revision) {
|
|
1733
2471
|
if (revision !== undefined)
|
|
1734
2472
|
return { kind: 'path', locator, path, revision };
|
|
1735
|
-
const
|
|
1736
|
-
|
|
1737
|
-
|
|
2473
|
+
const replica = replicas.get(locator);
|
|
2474
|
+
if (replica === undefined)
|
|
2475
|
+
return null;
|
|
2476
|
+
const local = await openLocal();
|
|
2477
|
+
const [held] = await local.entries(replica.record.key, 'visible', [path]);
|
|
2478
|
+
if (held?.binary === undefined || held.binary === null)
|
|
1738
2479
|
return null;
|
|
1739
|
-
if (held.state === 'pending')
|
|
1740
|
-
return { kind: 'pending', localId: held.localId };
|
|
1741
|
-
const record = binaryRecord(path, held,
|
|
2480
|
+
if (held.binary.state === 'pending')
|
|
2481
|
+
return { kind: 'pending', localId: held.binary.localId };
|
|
2482
|
+
const record = binaryRecord(path, held.binary, held.revision ?? undefined);
|
|
1742
2483
|
if (record.state !== 'accepted')
|
|
1743
2484
|
return null;
|
|
1744
|
-
const
|
|
1745
|
-
const
|
|
2485
|
+
const queued = replica.queue.flatMap((batch) => batch.changes);
|
|
2486
|
+
const sources = queued.flatMap((change) => (change.kind === 'move' ? [change.path] : []));
|
|
2487
|
+
const confirmedLayer = await stepped(slicing(await local.entries(replica.record.key, 'confirmed', [path, ...sources]), [
|
|
2488
|
+
path,
|
|
2489
|
+
...sources,
|
|
2490
|
+
]));
|
|
1746
2491
|
const holds = (at) => {
|
|
1747
|
-
const kept =
|
|
2492
|
+
const kept = confirmedLayer.binaries[at];
|
|
1748
2493
|
return (kept?.state === 'accepted' &&
|
|
1749
|
-
kept.
|
|
1750
|
-
|
|
2494
|
+
kept.sha256 === record.sha256 &&
|
|
2495
|
+
confirmedLayer.revisions[at] === record.revision);
|
|
1751
2496
|
};
|
|
1752
|
-
let
|
|
1753
|
-
for (let index = queued.length - 1; index >= 0 && !holds(
|
|
2497
|
+
let confirmedAt = path;
|
|
2498
|
+
for (let index = queued.length - 1; index >= 0 && !holds(confirmedAt); index -= 1) {
|
|
1754
2499
|
const change = queued[index];
|
|
1755
|
-
if (change.kind === 'move' && change.to ===
|
|
1756
|
-
|
|
2500
|
+
if (change.kind === 'move' && change.to === confirmedAt)
|
|
2501
|
+
confirmedAt = change.path;
|
|
1757
2502
|
}
|
|
1758
2503
|
return {
|
|
1759
2504
|
kind: 'path',
|
|
1760
2505
|
locator,
|
|
1761
|
-
path:
|
|
2506
|
+
path: confirmedAt,
|
|
1762
2507
|
revision: record.revision,
|
|
1763
|
-
fileRef: record.fileRef,
|
|
1764
2508
|
};
|
|
1765
2509
|
}
|
|
1766
2510
|
async function readBinaryPath(locator, path, revision) {
|
|
1767
2511
|
await ready;
|
|
1768
|
-
const
|
|
2512
|
+
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2513
|
+
const asked = drafted !== undefined && drafted.localId === revision
|
|
2514
|
+
? { kind: 'pending', localId: revision }
|
|
2515
|
+
: await binaryPath(locator, path, revision);
|
|
1769
2516
|
if (asked === null)
|
|
1770
2517
|
return absentBinary(path);
|
|
1771
2518
|
if (asked.kind === 'pending') {
|
|
1772
|
-
const
|
|
1773
|
-
return
|
|
2519
|
+
const facts = queuedFacts(locator, path, asked.localId);
|
|
2520
|
+
return facts === null
|
|
2521
|
+
? absentBinary(path)
|
|
2522
|
+
: {
|
|
2523
|
+
kind: 'binary',
|
|
2524
|
+
file: asked.localId,
|
|
2525
|
+
byteLength: facts.byteLength,
|
|
2526
|
+
mediaType: facts.mediaType,
|
|
2527
|
+
};
|
|
1774
2528
|
}
|
|
1775
|
-
return readBinary(asked);
|
|
2529
|
+
return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
|
|
1776
2530
|
}
|
|
1777
2531
|
async function linkBinaryPath(locator, path, revision) {
|
|
1778
2532
|
await ready;
|
|
1779
|
-
const asked = binaryPath(locator, path, revision);
|
|
2533
|
+
const asked = await binaryPath(locator, path, revision);
|
|
1780
2534
|
if (asked === null || asked.kind === 'pending')
|
|
1781
2535
|
return absentBinary(path);
|
|
1782
2536
|
return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
|
|
1783
2537
|
}
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
});
|
|
2538
|
+
function readPart(file, part) {
|
|
2539
|
+
return database.ask({ kind: 'file.part', key: file, part });
|
|
2540
|
+
}
|
|
2541
|
+
async function cleanUp() {
|
|
2542
|
+
const freed = await database.ask({ kind: 'opened.clean' });
|
|
2543
|
+
return freed + ((await options.shell.cleanUp?.()) ?? 0);
|
|
1791
2544
|
}
|
|
1792
2545
|
function declaredAgents() {
|
|
1793
2546
|
for (const capability of capabilities) {
|
|
@@ -1805,6 +2558,13 @@ export function composeRuntime(options) {
|
|
|
1805
2558
|
}
|
|
1806
2559
|
return undefined;
|
|
1807
2560
|
}
|
|
2561
|
+
function calledRooms(name, input) {
|
|
2562
|
+
const member = declaredAction(name)?.room;
|
|
2563
|
+
if (member === undefined || input === null || typeof input !== 'object')
|
|
2564
|
+
return [];
|
|
2565
|
+
const named = input[member];
|
|
2566
|
+
return (Array.isArray(named) ? named : [named]).filter((room) => typeof room === 'string');
|
|
2567
|
+
}
|
|
1808
2568
|
async function invokeAction(name, input, dispatching) {
|
|
1809
2569
|
await ready;
|
|
1810
2570
|
if (closed || refusal !== null)
|
|
@@ -1824,7 +2584,7 @@ export function composeRuntime(options) {
|
|
|
1824
2584
|
try {
|
|
1825
2585
|
const result = await options.adapter.action(authorization, { name, input });
|
|
1826
2586
|
if (result.kind === 'scoped-refusal') {
|
|
1827
|
-
const room = replicas.get(result.locator)?.
|
|
2587
|
+
const room = replicas.get(result.locator)?.record;
|
|
1828
2588
|
if (room !== undefined)
|
|
1829
2589
|
await scoped(result, room, await openLocal());
|
|
1830
2590
|
}
|
|
@@ -1899,93 +2659,426 @@ export function composeRuntime(options) {
|
|
|
1899
2659
|
}
|
|
1900
2660
|
function reachedBy(locator, roomVersion) {
|
|
1901
2661
|
const held = replicas.get(locator);
|
|
1902
|
-
return (held !== undefined &&
|
|
1903
|
-
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion));
|
|
2662
|
+
return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
|
|
1904
2663
|
}
|
|
1905
|
-
function writtenAfter(locator, roomVersion, path) {
|
|
1906
|
-
const
|
|
2664
|
+
async function writtenAfter(locator, roomVersion, path) {
|
|
2665
|
+
const held = replicas.get(locator);
|
|
2666
|
+
if (held === undefined)
|
|
2667
|
+
return false;
|
|
2668
|
+
const [entry] = await (await openLocal()).entries(held.record.key, 'confirmed', [path]);
|
|
2669
|
+
const written = entry?.written ?? undefined;
|
|
1907
2670
|
return (written !== undefined &&
|
|
1908
2671
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
1909
2672
|
!options.adapter.reached(locator, roomVersion, written));
|
|
1910
2673
|
}
|
|
1911
|
-
|
|
1912
|
-
|
|
2674
|
+
function overriddenTexts(texts, overrides) {
|
|
2675
|
+
return async (paths) => {
|
|
2676
|
+
const known = new Map(await texts(paths.filter((path) => !overrides.has(path))));
|
|
2677
|
+
for (const path of paths) {
|
|
2678
|
+
const entry = overrides.get(path);
|
|
2679
|
+
if (entry === undefined)
|
|
2680
|
+
continue;
|
|
2681
|
+
if (entry === null)
|
|
2682
|
+
known.delete(path);
|
|
2683
|
+
else
|
|
2684
|
+
known.set(path, entry.binary !== null ? null : entry.content);
|
|
2685
|
+
}
|
|
2686
|
+
return known;
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
async function unreadableRoom(texts, walk, paths) {
|
|
2690
|
+
if (paths !== null) {
|
|
2691
|
+
const text = {};
|
|
2692
|
+
for (const [path, content] of await texts(paths))
|
|
2693
|
+
if (content !== null)
|
|
2694
|
+
text[path] = content;
|
|
2695
|
+
return stepped(unreadableIn(text, Object.keys(text)));
|
|
2696
|
+
}
|
|
2697
|
+
for (let after = null;;) {
|
|
2698
|
+
const page = await walk(after);
|
|
2699
|
+
const text = {};
|
|
2700
|
+
for (const { path, content } of page.entries)
|
|
2701
|
+
if (content !== null)
|
|
2702
|
+
text[path] = content;
|
|
2703
|
+
const refused = await stepped(unreadableIn(text, Object.keys(text)));
|
|
2704
|
+
if (refused !== null)
|
|
2705
|
+
return refused;
|
|
2706
|
+
if (page.next === null)
|
|
2707
|
+
return null;
|
|
2708
|
+
after = page.next;
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
async function validatedRoom(local, record, view, previous, overrides = null) {
|
|
2712
|
+
const composition = compositionOf(record);
|
|
2713
|
+
const modeled = record.model.status !== 'unavailable' || record.selectionModel.status !== 'unavailable';
|
|
2714
|
+
const lookup = overrides === null
|
|
2715
|
+
? lookupOf(local, record.key, view)
|
|
2716
|
+
: overriddenLookup(lookupOf(local, record.key, view), overrides);
|
|
2717
|
+
const texts = overrides === null
|
|
2718
|
+
? textsOf(local, record.key, view)
|
|
2719
|
+
: overriddenTexts(textsOf(local, record.key, view), overrides);
|
|
2720
|
+
const walk = overrides === null
|
|
2721
|
+
? walkOf(local, record.key, view)
|
|
2722
|
+
: overriddenWalk(walkOf(local, record.key, view), overrides);
|
|
2723
|
+
let changes = null;
|
|
2724
|
+
let reshaped = false;
|
|
2725
|
+
if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
|
|
2726
|
+
const [was, now] = await Promise.all([
|
|
2727
|
+
local.entries(record.key, 'confirmed', previous.changed),
|
|
2728
|
+
overrides === null
|
|
2729
|
+
? local.entries(record.key, view, previous.changed)
|
|
2730
|
+
: Promise.resolve([]),
|
|
2731
|
+
]);
|
|
2732
|
+
const before = new Map(was.map((entry) => [entry.path, entry]));
|
|
2733
|
+
const after = overrides === null
|
|
2734
|
+
? new Map(now.map((entry) => [entry.path, entry]))
|
|
2735
|
+
: overrides;
|
|
2736
|
+
changes = previous.changed.flatMap((path) => {
|
|
2737
|
+
const one = before.get(path) ?? null;
|
|
2738
|
+
const other = after.get(path) ?? null;
|
|
2739
|
+
reshaped ||= textDiffers(one, other);
|
|
2740
|
+
const held = textOf(one);
|
|
2741
|
+
const next = textOf(other);
|
|
2742
|
+
return held === next ? [] : [{ path, was: held, now: next }];
|
|
2743
|
+
});
|
|
2744
|
+
}
|
|
2745
|
+
const named = changes === null ? null : changes.map(({ path }) => path);
|
|
2746
|
+
const textChanged = changes === null || changes.length > 0 || reshaped;
|
|
2747
|
+
const modelsChanged = previous === null || !sameComposition(compositionOf(previous.record), composition);
|
|
2748
|
+
if (modeled && (textChanged || modelsChanged)) {
|
|
2749
|
+
const clean = previous !== null &&
|
|
2750
|
+
changes !== null &&
|
|
2751
|
+
(await changedCleanly(changes, compositionOf(previous.record), composition, {
|
|
2752
|
+
lookup,
|
|
2753
|
+
texts,
|
|
2754
|
+
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2755
|
+
}));
|
|
2756
|
+
if (clean)
|
|
2757
|
+
return null;
|
|
2758
|
+
const verdict = await validatedCollection(walk, lookup, composition);
|
|
2759
|
+
if (verdict.verdict === 'accept')
|
|
2760
|
+
return null;
|
|
2761
|
+
if (verdict.rule !== 'model-unavailable')
|
|
2762
|
+
return { kind: 'refused', refusal: verdict };
|
|
2763
|
+
return unreadableRoom(texts, walk, named);
|
|
2764
|
+
}
|
|
2765
|
+
if (textChanged)
|
|
2766
|
+
return unreadableRoom(texts, walk, named);
|
|
2767
|
+
return null;
|
|
2768
|
+
}
|
|
2769
|
+
function refreshingOf(local, key, liveness, replaying = null) {
|
|
2770
|
+
const write = replaying === null ? exclusive : installWrite;
|
|
2771
|
+
return {
|
|
2772
|
+
async turn() {
|
|
2773
|
+
if (replaying === null)
|
|
2774
|
+
return;
|
|
2775
|
+
installing(replaying, 'replaying');
|
|
2776
|
+
await screened();
|
|
2777
|
+
},
|
|
2778
|
+
prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
|
|
2779
|
+
async stage(page) {
|
|
2780
|
+
for (let written = 0;;) {
|
|
2781
|
+
if (replaying !== null)
|
|
2782
|
+
await screened();
|
|
2783
|
+
const end = batchEnd(page.entries, written);
|
|
2784
|
+
const last = end === page.entries.length;
|
|
2785
|
+
const entries = page.entries.slice(written, end);
|
|
2786
|
+
const staged = await write(async () => {
|
|
2787
|
+
if (!liveness())
|
|
2788
|
+
return false;
|
|
2789
|
+
await timed(() => local.refresh(key, last
|
|
2790
|
+
? { ...page, entries }
|
|
2791
|
+
: { entries, removals: [], folders: [], folderRemovals: [] }));
|
|
2792
|
+
return true;
|
|
2793
|
+
});
|
|
2794
|
+
if (!staged)
|
|
2795
|
+
return false;
|
|
2796
|
+
if (last)
|
|
2797
|
+
return true;
|
|
2798
|
+
written = end;
|
|
2799
|
+
}
|
|
2800
|
+
},
|
|
2801
|
+
};
|
|
2802
|
+
}
|
|
2803
|
+
function batchEnd(entries, from) {
|
|
2804
|
+
let end = from;
|
|
2805
|
+
let spent = 0;
|
|
2806
|
+
while (end < entries.length) {
|
|
2807
|
+
const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
|
|
2808
|
+
if (spent + size > stageBytes && end > from)
|
|
2809
|
+
break;
|
|
2810
|
+
spent += size;
|
|
2811
|
+
end += 1;
|
|
2812
|
+
}
|
|
2813
|
+
return end;
|
|
2814
|
+
}
|
|
2815
|
+
async function timed(work) {
|
|
2816
|
+
const started = performance.now();
|
|
2817
|
+
const done = await work();
|
|
2818
|
+
const spent = performance.now() - started;
|
|
2819
|
+
if (spent < STAGE_AIM_MS / 2)
|
|
2820
|
+
stageBytes = Math.min(STAGE_BYTES, Math.round(stageBytes * 1.5));
|
|
2821
|
+
else if (spent > STAGE_AIM_MS * 2) {
|
|
2822
|
+
stageBytes = Math.max(STAGE_FLOOR, Math.round(stageBytes / 1.5));
|
|
2823
|
+
}
|
|
2824
|
+
return done;
|
|
2825
|
+
}
|
|
2826
|
+
function installWrite(work) {
|
|
2827
|
+
const next = installWrites.then(() => exclusive(work), () => exclusive(work));
|
|
2828
|
+
installWrites = next.then(() => undefined, () => undefined);
|
|
2829
|
+
return next;
|
|
2830
|
+
}
|
|
2831
|
+
function screened() {
|
|
2832
|
+
if (onScreen || closed)
|
|
2833
|
+
return Promise.resolve();
|
|
2834
|
+
queueReadiness();
|
|
2835
|
+
return new Promise((resolve) => shownWaiters.push(resolve));
|
|
2836
|
+
}
|
|
2837
|
+
async function heldPart() {
|
|
2838
|
+
while (partsFree === 0)
|
|
2839
|
+
await new Promise((admit) => partWaiters.push(admit));
|
|
2840
|
+
partsFree -= 1;
|
|
2841
|
+
return freed();
|
|
2842
|
+
}
|
|
2843
|
+
function freed() {
|
|
2844
|
+
let released = false;
|
|
2845
|
+
return () => {
|
|
2846
|
+
if (released)
|
|
2847
|
+
return;
|
|
2848
|
+
released = true;
|
|
2849
|
+
partsFree += 1;
|
|
2850
|
+
partWaiters.shift()?.();
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
2853
|
+
function installing(locator, step, share = null) {
|
|
2854
|
+
installs.set(locator, { step, share });
|
|
2855
|
+
queueReadiness();
|
|
2856
|
+
}
|
|
2857
|
+
function installed(locator) {
|
|
2858
|
+
if (installs.delete(locator))
|
|
2859
|
+
queueReadiness();
|
|
2860
|
+
}
|
|
2861
|
+
function screen(value) {
|
|
2862
|
+
if (value === onScreen)
|
|
2863
|
+
return;
|
|
2864
|
+
onScreen = value;
|
|
2865
|
+
journal.record(`installation ${value ? 'shown' : 'hidden'}`);
|
|
2866
|
+
queueReadiness();
|
|
2867
|
+
if (!value)
|
|
2868
|
+
return;
|
|
2869
|
+
for (const resolve of shownWaiters.splice(0))
|
|
2870
|
+
resolve();
|
|
2871
|
+
}
|
|
2872
|
+
async function pullRefresh(authorized, held, liveness) {
|
|
2873
|
+
if (!liveness())
|
|
1913
2874
|
return unconfirmed;
|
|
1914
2875
|
const turn = generation;
|
|
2876
|
+
const { record } = held;
|
|
2877
|
+
const base = held.confirmed;
|
|
2878
|
+
const local = await openLocal();
|
|
1915
2879
|
try {
|
|
1916
|
-
|
|
1917
|
-
const
|
|
1918
|
-
|
|
1919
|
-
|
|
2880
|
+
await exclusive(() => local.unrefresh(record.key));
|
|
2881
|
+
const result = await advancement(reads, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, liveness));
|
|
2882
|
+
if (result.kind !== 'advanced') {
|
|
2883
|
+
journal.record(`transfer refresh ${record.locator} ${result.kind}`);
|
|
2884
|
+
return result;
|
|
2885
|
+
}
|
|
2886
|
+
const refused = await validatedRoom(local, result.record, 'refreshed', {
|
|
2887
|
+
record,
|
|
2888
|
+
changed: [...result.changed],
|
|
2889
|
+
});
|
|
2890
|
+
if (refused !== null) {
|
|
2891
|
+
journal.record(`transfer refresh ${record.locator} refused`);
|
|
2892
|
+
return refused;
|
|
2893
|
+
}
|
|
2894
|
+
journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
|
|
2895
|
+
return { kind: 'advanced', record: result.record, base };
|
|
1920
2896
|
}
|
|
1921
2897
|
catch (error) {
|
|
1922
|
-
|
|
2898
|
+
if (refusedForSpace(error))
|
|
2899
|
+
return unconfirmed;
|
|
2900
|
+
journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
|
|
1923
2901
|
throw error;
|
|
1924
2902
|
}
|
|
1925
2903
|
finally {
|
|
1926
|
-
reads.finished(
|
|
1927
|
-
}
|
|
2904
|
+
reads.finished(record.locator, turn);
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
const heldContents = new Map();
|
|
2908
|
+
function heldContentOf(local, key) {
|
|
2909
|
+
const known = heldContents.get(key);
|
|
2910
|
+
if (known !== undefined)
|
|
2911
|
+
return known;
|
|
2912
|
+
if (heldContents.size > ROOM_READS)
|
|
2913
|
+
heldContents.clear();
|
|
2914
|
+
const made = {
|
|
2915
|
+
files: (after, limit) => local.files(key, 'confirmed', after, limit),
|
|
2916
|
+
async contents(paths) {
|
|
2917
|
+
return new Map((await local.entries(key, 'confirmed', paths)).flatMap(({ path, content, revision }) => content === null || revision === null ? [] : [[path, { content, revision }]]));
|
|
2918
|
+
},
|
|
2919
|
+
};
|
|
2920
|
+
heldContents.set(key, made);
|
|
2921
|
+
return made;
|
|
1928
2922
|
}
|
|
1929
|
-
function staging(local,
|
|
2923
|
+
function staging(local, liveness) {
|
|
1930
2924
|
return {
|
|
1931
2925
|
held: (locator) => transfers.get(locator) ?? null,
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
await local.stage(record, page);
|
|
1941
|
-
}
|
|
1942
|
-
catch (error) {
|
|
1943
|
-
if (!exceeded(error))
|
|
1944
|
-
throw error;
|
|
1945
|
-
await exceededOn(record.locator, bytes);
|
|
1946
|
-
return false;
|
|
2926
|
+
async turn(locator, step) {
|
|
2927
|
+
installing(locator, step);
|
|
2928
|
+
for (;;) {
|
|
2929
|
+
await screened();
|
|
2930
|
+
const release = step === 'downloading' ? await heldPart() : () => { };
|
|
2931
|
+
if (onScreen || closed)
|
|
2932
|
+
return release;
|
|
2933
|
+
release();
|
|
1947
2934
|
}
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
2935
|
+
},
|
|
2936
|
+
spare() {
|
|
2937
|
+
if (!onScreen || closed || partsFree === 0)
|
|
2938
|
+
return null;
|
|
2939
|
+
partsFree -= 1;
|
|
2940
|
+
return freed();
|
|
2941
|
+
},
|
|
2942
|
+
arriving(locator) {
|
|
2943
|
+
let ended = false;
|
|
2944
|
+
return {
|
|
2945
|
+
at(token, bytes) {
|
|
2946
|
+
if (ended)
|
|
2947
|
+
return;
|
|
2948
|
+
const held = arrivals.get(locator) ?? new Map();
|
|
2949
|
+
arrivals.set(locator, held.set(token, bytes));
|
|
2950
|
+
const now = performance.now();
|
|
2951
|
+
if (now - arrivalReported < ARRIVAL_REPORT_MS)
|
|
2952
|
+
return;
|
|
2953
|
+
arrivalReported = now;
|
|
2954
|
+
queueReadiness();
|
|
2955
|
+
},
|
|
2956
|
+
end() {
|
|
2957
|
+
ended = true;
|
|
2958
|
+
if (arrivals.delete(locator))
|
|
2959
|
+
queueReadiness();
|
|
2960
|
+
},
|
|
2961
|
+
};
|
|
2962
|
+
},
|
|
2963
|
+
received: (locator, token) => local.transferPart(locator, token),
|
|
2964
|
+
keep: (locator, token, received) => installWrite(async () => {
|
|
2965
|
+
if (liveness())
|
|
2966
|
+
await local.keepPart(locator, token, received);
|
|
1951
2967
|
}),
|
|
2968
|
+
announced(locator) {
|
|
2969
|
+
const later = announced.get(locator);
|
|
2970
|
+
return later === undefined ? [] : [later];
|
|
2971
|
+
},
|
|
2972
|
+
async stage(record, page, composition, received) {
|
|
2973
|
+
const { locator } = record;
|
|
2974
|
+
const whole = received?.byteLength ?? 0;
|
|
2975
|
+
const from = record.writing?.written ?? 0;
|
|
2976
|
+
const token = record.writing?.token ?? null;
|
|
2977
|
+
const entries = page.entries.length;
|
|
2978
|
+
let held = record;
|
|
2979
|
+
if (token !== null)
|
|
2980
|
+
arrivals.get(locator)?.delete(token);
|
|
2981
|
+
installing(locator, 'writing', { written: from, entries, bytes: whole });
|
|
2982
|
+
for (let written = from;;) {
|
|
2983
|
+
await screened();
|
|
2984
|
+
if (!liveness())
|
|
2985
|
+
return null;
|
|
2986
|
+
const end = batchEnd(page.entries, written);
|
|
2987
|
+
const last = end === entries;
|
|
2988
|
+
const next = last
|
|
2989
|
+
? {
|
|
2990
|
+
...held,
|
|
2991
|
+
writing: null,
|
|
2992
|
+
done: {
|
|
2993
|
+
parts: held.done.parts + (received === null ? 0 : 1),
|
|
2994
|
+
bytes: held.done.bytes + whole,
|
|
2995
|
+
},
|
|
2996
|
+
}
|
|
2997
|
+
: { ...held, writing: { token, written: end } };
|
|
2998
|
+
const key = await installWrite(async () => {
|
|
2999
|
+
if (!liveness())
|
|
3000
|
+
return null;
|
|
3001
|
+
return timed(() => local.stage(next, composition, {
|
|
3002
|
+
entries: page.entries.slice(written, end),
|
|
3003
|
+
folders: last ? page.folders : [],
|
|
3004
|
+
}, last && received !== null && token !== null ? token : undefined));
|
|
3005
|
+
});
|
|
3006
|
+
if (key === null)
|
|
3007
|
+
return null;
|
|
3008
|
+
const arrived = !transfers.has(locator);
|
|
3009
|
+
held = { ...next, key };
|
|
3010
|
+
transfers.set(locator, held);
|
|
3011
|
+
if (arrived)
|
|
3012
|
+
liveQueries.recover();
|
|
3013
|
+
if (last) {
|
|
3014
|
+
installing(locator, held.pageToken === null ? 'replaying' : 'downloading');
|
|
3015
|
+
liveQueries.recover();
|
|
3016
|
+
return held;
|
|
3017
|
+
}
|
|
3018
|
+
installing(locator, 'writing', { written: end, entries, bytes: whole });
|
|
3019
|
+
written = end;
|
|
3020
|
+
}
|
|
3021
|
+
},
|
|
3022
|
+
async refreshing(locator, key) {
|
|
3023
|
+
await exclusive(() => local.unrefresh(key));
|
|
3024
|
+
return refreshingOf(local, key, liveness, locator);
|
|
3025
|
+
},
|
|
1952
3026
|
abandon: (locator) => exclusive(async () => {
|
|
1953
|
-
if (!
|
|
3027
|
+
if (!liveness())
|
|
1954
3028
|
return;
|
|
1955
3029
|
await local.abandon(locator);
|
|
1956
3030
|
transfers.delete(locator);
|
|
1957
|
-
|
|
3031
|
+
liveQueries.recover();
|
|
3032
|
+
sweep();
|
|
1958
3033
|
}),
|
|
3034
|
+
setAside: (locator) => exclusive(async () => {
|
|
3035
|
+
if (!liveness())
|
|
3036
|
+
return;
|
|
3037
|
+
await local.abandon(locator, true);
|
|
3038
|
+
transfers.delete(locator);
|
|
3039
|
+
liveQueries.recover();
|
|
3040
|
+
}),
|
|
3041
|
+
release: (key) => exclusive(async () => {
|
|
3042
|
+
await local.release(key);
|
|
3043
|
+
sweep();
|
|
3044
|
+
}),
|
|
3045
|
+
content: (key) => heldContentOf(local, key),
|
|
3046
|
+
validate: (record, view) => validatedRoom(local, record, view, null),
|
|
1959
3047
|
};
|
|
1960
3048
|
}
|
|
1961
|
-
function reusable(room) {
|
|
1962
|
-
const record = replicas.get(room.locator)?.
|
|
3049
|
+
function reusable(room, local) {
|
|
3050
|
+
const record = replicas.get(room.locator)?.record;
|
|
1963
3051
|
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1964
3052
|
return null;
|
|
1965
|
-
return
|
|
3053
|
+
return heldContentOf(local, record.key);
|
|
1966
3054
|
}
|
|
1967
|
-
async function pullBootstrap(authorized, room, local,
|
|
1968
|
-
if (!
|
|
3055
|
+
async function pullBootstrap(authorized, room, local, liveness) {
|
|
3056
|
+
if (!liveness())
|
|
1969
3057
|
return unconfirmed;
|
|
1970
3058
|
const turn = generation;
|
|
1971
3059
|
if (replicas.has(room.locator)) {
|
|
1972
3060
|
catching.add(room.locator);
|
|
1973
|
-
|
|
3061
|
+
liveQueries.recover();
|
|
1974
3062
|
}
|
|
1975
3063
|
try {
|
|
1976
3064
|
const result = await bootstrapRoom(reads, authorized, room, {
|
|
1977
3065
|
collection: modelOf(room.locator),
|
|
1978
3066
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
1979
|
-
}, (models, held) => (
|
|
1980
|
-
const detail = result.kind === 'staged' ? result.
|
|
3067
|
+
}, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
|
|
3068
|
+
const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
|
|
1981
3069
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1982
|
-
if (result.kind !== 'staged')
|
|
3070
|
+
if (result.kind !== 'staged') {
|
|
1983
3071
|
caughtUpTo(room.locator);
|
|
3072
|
+
installed(room.locator);
|
|
3073
|
+
}
|
|
1984
3074
|
return result;
|
|
1985
3075
|
}
|
|
1986
3076
|
catch (error) {
|
|
1987
|
-
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
1988
3077
|
caughtUpTo(room.locator);
|
|
3078
|
+
installed(room.locator);
|
|
3079
|
+
if (refusedForSpace(error))
|
|
3080
|
+
return unconfirmed;
|
|
3081
|
+
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
1989
3082
|
throw error;
|
|
1990
3083
|
}
|
|
1991
3084
|
finally {
|
|
@@ -1994,10 +3087,10 @@ export function composeRuntime(options) {
|
|
|
1994
3087
|
}
|
|
1995
3088
|
function caughtUpTo(locator) {
|
|
1996
3089
|
if (catching.delete(locator))
|
|
1997
|
-
|
|
3090
|
+
liveQueries.recover();
|
|
1998
3091
|
}
|
|
1999
3092
|
function behind(room) {
|
|
2000
|
-
const held = replicas.get(room.locator)?.
|
|
3093
|
+
const held = replicas.get(room.locator)?.record.roomVersion;
|
|
2001
3094
|
return (held !== undefined &&
|
|
2002
3095
|
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
2003
3096
|
}
|
|
@@ -2009,13 +3102,13 @@ export function composeRuntime(options) {
|
|
|
2009
3102
|
}
|
|
2010
3103
|
return advanceOnce(room, local, authorized, live);
|
|
2011
3104
|
}
|
|
2012
|
-
async function advanceOnce(room, local, authorized,
|
|
2013
|
-
|
|
2014
|
-
if (!live()
|
|
3105
|
+
async function advanceOnce(room, local, authorized, running) {
|
|
3106
|
+
const live = () => running() && !full;
|
|
3107
|
+
if (!live())
|
|
2015
3108
|
return unconfirmed;
|
|
2016
3109
|
const held = replicas.get(room.locator);
|
|
2017
3110
|
if (held !== undefined) {
|
|
2018
|
-
const refreshed = await pullRefresh(authorized, held
|
|
3111
|
+
const refreshed = await pullRefresh(authorized, held, live);
|
|
2019
3112
|
if (!live())
|
|
2020
3113
|
return unconfirmed;
|
|
2021
3114
|
if (refreshed.kind === 'still-current') {
|
|
@@ -2030,10 +3123,11 @@ export function composeRuntime(options) {
|
|
|
2030
3123
|
return null;
|
|
2031
3124
|
}
|
|
2032
3125
|
if (refreshed.kind === 'advanced') {
|
|
2033
|
-
|
|
2034
|
-
|
|
3126
|
+
const landing = { kind: 'refreshed', base: refreshed.base };
|
|
3127
|
+
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3128
|
+
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
2035
3129
|
const reached = now !== undefined &&
|
|
2036
|
-
options.adapter.reached(room.locator, now, refreshed.
|
|
3130
|
+
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
2037
3131
|
return reached ? null : advance(room, local, authorized, live);
|
|
2038
3132
|
}
|
|
2039
3133
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
@@ -2043,19 +3137,18 @@ export function composeRuntime(options) {
|
|
|
2043
3137
|
return refreshed;
|
|
2044
3138
|
if (opened !== null) {
|
|
2045
3139
|
owed.add(room.locator);
|
|
2046
|
-
|
|
3140
|
+
liveQueries.recover();
|
|
2047
3141
|
return unconfirmed;
|
|
2048
3142
|
}
|
|
2049
3143
|
if (reloadsDue.get(room.locator) === generation)
|
|
2050
3144
|
return unconfirmed;
|
|
2051
3145
|
}
|
|
2052
3146
|
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
2053
|
-
if (
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
return null;
|
|
3147
|
+
if (bootstrapped.kind === 'staged') {
|
|
3148
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
3149
|
+
return live() ? null : unconfirmed;
|
|
3150
|
+
}
|
|
3151
|
+
return live() ? bootstrapped : unconfirmed;
|
|
2059
3152
|
}
|
|
2060
3153
|
async function withhold(withheld, room, local, progress) {
|
|
2061
3154
|
if (withheld.kind === 'unconfirmed')
|
|
@@ -2127,57 +3220,254 @@ export function composeRuntime(options) {
|
|
|
2127
3220
|
(accepted.author === undefined ? '' : ` author ${accepted.author}`) +
|
|
2128
3221
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
2129
3222
|
}
|
|
2130
|
-
|
|
2131
|
-
|
|
3223
|
+
function settling(acceptance) {
|
|
3224
|
+
return acceptance !== null && 'apply' in acceptance;
|
|
3225
|
+
}
|
|
3226
|
+
function refusalOf(acceptance) {
|
|
3227
|
+
return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
|
|
3228
|
+
}
|
|
3229
|
+
async function acceptance(room, local, submitted, accepted) {
|
|
2132
3230
|
if (!continuing())
|
|
2133
3231
|
return unconfirmed;
|
|
2134
3232
|
const current = replicas.get(room.locator);
|
|
2135
3233
|
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
2136
3234
|
return null;
|
|
2137
3235
|
}
|
|
2138
|
-
const held = current.
|
|
3236
|
+
const held = current.record.roomVersion;
|
|
2139
3237
|
logAccepted(room.locator, submitted, accepted);
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
3238
|
+
let record = current.record;
|
|
3239
|
+
let changes;
|
|
3240
|
+
if (!options.adapter.reached(room.locator, held, accepted.roomVersion)) {
|
|
3241
|
+
const paths = [...returnedPaths(submitted, accepted)];
|
|
3242
|
+
const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
|
|
3243
|
+
const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
|
|
3244
|
+
record = {
|
|
3245
|
+
...record,
|
|
3246
|
+
roomVersion: options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
3247
|
+
? accepted.roomVersion
|
|
3248
|
+
: held,
|
|
3249
|
+
};
|
|
3250
|
+
const overrides = new Map([
|
|
3251
|
+
...back.set.map((entry) => [entry.path, entry]),
|
|
3252
|
+
...back.removed.map((path) => [path, null]),
|
|
3253
|
+
]);
|
|
3254
|
+
const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
|
|
3255
|
+
if (refused !== null) {
|
|
3256
|
+
const key = pendingKey(room.locator, submitted.id);
|
|
3257
|
+
return {
|
|
3258
|
+
locator: room.locator,
|
|
3259
|
+
left: [],
|
|
3260
|
+
commit: {
|
|
3261
|
+
rooms: [],
|
|
3262
|
+
writes: [
|
|
3263
|
+
{
|
|
3264
|
+
locator: room.locator,
|
|
3265
|
+
accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
|
|
3266
|
+
},
|
|
3267
|
+
],
|
|
3268
|
+
},
|
|
3269
|
+
refused,
|
|
3270
|
+
apply() {
|
|
3271
|
+
const state = pendings.get(key);
|
|
3272
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
3273
|
+
pendings.set(key, { ...state, accepted: accepted.roomVersion });
|
|
3274
|
+
return changeSet(room.locator, { queue: true });
|
|
3275
|
+
},
|
|
3276
|
+
};
|
|
3277
|
+
}
|
|
3278
|
+
changes = back;
|
|
3279
|
+
}
|
|
3280
|
+
const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
|
|
3281
|
+
return {
|
|
3282
|
+
...settled,
|
|
3283
|
+
apply(answer) {
|
|
3284
|
+
const set = settled.apply(answer);
|
|
3285
|
+
pendings.delete(pendingKey(room.locator, submitted.id));
|
|
3286
|
+
unreadable.delete(room.locator);
|
|
3287
|
+
sourceRefusals.delete(room.locator);
|
|
3288
|
+
declarationRefusals.delete(room.locator);
|
|
3289
|
+
list(room, true);
|
|
3290
|
+
return set;
|
|
3291
|
+
},
|
|
3292
|
+
};
|
|
3293
|
+
}
|
|
3294
|
+
async function adoptedTogether(local, acceptances) {
|
|
3295
|
+
const settlements = acceptances.filter(settling);
|
|
3296
|
+
if (settlements.length > 0) {
|
|
3297
|
+
publishChangeSet(await together(local, settlements));
|
|
3298
|
+
moveTo({ ...status, ...attention() });
|
|
3299
|
+
}
|
|
3300
|
+
await unpair(local);
|
|
2163
3301
|
}
|
|
2164
3302
|
async function adopted(room, local, submitted, accepted) {
|
|
2165
|
-
const failed = await exclusive(() =>
|
|
3303
|
+
const failed = await exclusive(async () => {
|
|
3304
|
+
const adopting = await acceptance(room, local, submitted, accepted);
|
|
3305
|
+
await adoptedTogether(local, [adopting]);
|
|
3306
|
+
return refusalOf(adopting);
|
|
3307
|
+
});
|
|
3308
|
+
if (failed === null)
|
|
3309
|
+
return true;
|
|
3310
|
+
if (failed.kind === 'refused' && continuing())
|
|
3311
|
+
refusedSource(room, failed);
|
|
3312
|
+
return false;
|
|
3313
|
+
}
|
|
3314
|
+
async function adoptedMove(room, partner, local, submitted, accepted) {
|
|
3315
|
+
const other = discovered.get(partner);
|
|
3316
|
+
const failed = await exclusive(async () => {
|
|
3317
|
+
const left = await acceptance(room, local, submitted, accepted);
|
|
3318
|
+
const arriving = replicas.get(partner)?.queue.find((batch) => batch.id === submitted.id);
|
|
3319
|
+
const arrived = other === undefined || arriving === undefined || accepted.destination === undefined
|
|
3320
|
+
? null
|
|
3321
|
+
: await acceptance(other, local, arriving, {
|
|
3322
|
+
kind: 'accepted',
|
|
3323
|
+
...accepted.destination,
|
|
3324
|
+
});
|
|
3325
|
+
await adoptedTogether(local, [left, arrived]);
|
|
3326
|
+
const refusedArrival = refusalOf(arrived);
|
|
3327
|
+
if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
|
|
3328
|
+
refusedSource(other, refusedArrival);
|
|
3329
|
+
}
|
|
3330
|
+
return refusalOf(left);
|
|
3331
|
+
});
|
|
3332
|
+
submitLater(partner);
|
|
2166
3333
|
if (failed === null)
|
|
2167
3334
|
return true;
|
|
2168
3335
|
if (failed.kind === 'refused' && continuing())
|
|
2169
3336
|
refusedSource(room, failed);
|
|
2170
3337
|
return false;
|
|
2171
3338
|
}
|
|
3339
|
+
function queueIn(locator) {
|
|
3340
|
+
return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
3341
|
+
}
|
|
3342
|
+
function behindIn(locator, batch) {
|
|
3343
|
+
const held = replicas.get(locator)?.queue;
|
|
3344
|
+
if (held === undefined)
|
|
3345
|
+
return queuesWithoutReplica.get(locator) !== undefined;
|
|
3346
|
+
return held.findIndex((one) => one.id === batch.id) > 0;
|
|
3347
|
+
}
|
|
3348
|
+
function unpaired(skipped) {
|
|
3349
|
+
for (const locator of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
|
|
3350
|
+
for (const batch of queueIn(locator)) {
|
|
3351
|
+
const partner = partnerOf(batch, locator);
|
|
3352
|
+
if (partner === null || skipped.has(pendingKey(locator, batch.id)))
|
|
3353
|
+
continue;
|
|
3354
|
+
if (queueIn(partner).some((held) => held.id === batch.id))
|
|
3355
|
+
continue;
|
|
3356
|
+
if (unpairedIn(locator, batch))
|
|
3357
|
+
return { locator, batch };
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
return null;
|
|
3361
|
+
}
|
|
3362
|
+
async function unpairing(local, locator, batches) {
|
|
3363
|
+
const ids = new Set(batches.map(({ id }) => id));
|
|
3364
|
+
const leaving = batches.map((batch) => ({
|
|
3365
|
+
batch,
|
|
3366
|
+
returned: !localSaves && leavingIn(batch, locator) ? returnedBy('withdrawn') : null,
|
|
3367
|
+
}));
|
|
3368
|
+
const outcome = localSaves ? 'dropped' : 'unpaired';
|
|
3369
|
+
const current = replicas.get(locator);
|
|
3370
|
+
if (current !== undefined) {
|
|
3371
|
+
const settled = await settlement(local, locator, current, current.record, current.queue.filter((one) => !ids.has(one.id)), { leaving });
|
|
3372
|
+
return {
|
|
3373
|
+
...settled,
|
|
3374
|
+
apply(answer) {
|
|
3375
|
+
for (const batch of batches)
|
|
3376
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
3377
|
+
const set = settled.apply(answer);
|
|
3378
|
+
submitLater(locator);
|
|
3379
|
+
return set;
|
|
3380
|
+
},
|
|
3381
|
+
};
|
|
3382
|
+
}
|
|
3383
|
+
const queue = queueIn(locator).filter((one) => !ids.has(one.id));
|
|
3384
|
+
const written = new Map();
|
|
3385
|
+
for (const { batch, returned } of leaving) {
|
|
3386
|
+
if (returned === null)
|
|
3387
|
+
continue;
|
|
3388
|
+
for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
|
|
3389
|
+
written.set(saved.path, {
|
|
3390
|
+
locator,
|
|
3391
|
+
path: saved.path,
|
|
3392
|
+
revision: roomDraftRevision(),
|
|
3393
|
+
parts: returnedInto((written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path))?.parts, saved.parts),
|
|
3394
|
+
returned,
|
|
3395
|
+
});
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
const carried = carriedBack(locator, leaving.flatMap(({ batch, returned }) => (returned === null ? [] : [batch])), written);
|
|
3399
|
+
return {
|
|
3400
|
+
locator,
|
|
3401
|
+
left: batches,
|
|
3402
|
+
commit: {
|
|
3403
|
+
rooms: [],
|
|
3404
|
+
writes: [
|
|
3405
|
+
{
|
|
3406
|
+
locator,
|
|
3407
|
+
queue,
|
|
3408
|
+
roomDrafts: [...written.values()],
|
|
3409
|
+
consumedRoomDrafts: carried.taken,
|
|
3410
|
+
},
|
|
3411
|
+
...carried.writes,
|
|
3412
|
+
],
|
|
3413
|
+
},
|
|
3414
|
+
apply() {
|
|
3415
|
+
for (const batch of batches) {
|
|
3416
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
3417
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
3418
|
+
}
|
|
3419
|
+
if (queue.length > 0)
|
|
3420
|
+
queuesWithoutReplica.set(locator, queue);
|
|
3421
|
+
else
|
|
3422
|
+
queuesWithoutReplica.delete(locator);
|
|
3423
|
+
for (const record of written.values())
|
|
3424
|
+
rememberRoomDraft(record);
|
|
3425
|
+
carried.remember();
|
|
3426
|
+
submitLater(locator);
|
|
3427
|
+
return mergeChangeSets([
|
|
3428
|
+
changeSet(locator, {
|
|
3429
|
+
queue: true,
|
|
3430
|
+
drafts: [...written.keys(), ...carried.taken],
|
|
3431
|
+
}),
|
|
3432
|
+
...carried.changes,
|
|
3433
|
+
]);
|
|
3434
|
+
},
|
|
3435
|
+
};
|
|
3436
|
+
}
|
|
3437
|
+
async function unpair(local) {
|
|
3438
|
+
const handled = new Set();
|
|
3439
|
+
for (let held = unpaired(handled); held !== null && continuing(); held = unpaired(handled)) {
|
|
3440
|
+
const { locator, batch } = held;
|
|
3441
|
+
handled.add(pendingKey(locator, batch.id));
|
|
3442
|
+
publishChangeSet(await together(local, [await unpairing(local, locator, [batch])]));
|
|
3443
|
+
}
|
|
3444
|
+
if (handled.size === 0)
|
|
3445
|
+
return;
|
|
3446
|
+
moveTo({ ...status, ...attention() });
|
|
3447
|
+
}
|
|
3448
|
+
async function discardHeld(head, room, local, signal) {
|
|
3449
|
+
const partner = partnerOf(head, room.locator);
|
|
3450
|
+
const other = partner === null ? undefined : discovered.get(partner);
|
|
3451
|
+
await discardPending(other === undefined ? [room] : [room, other], local, signal);
|
|
3452
|
+
}
|
|
2172
3453
|
async function submitRoom(room, local, authorized, progress, signal) {
|
|
2173
3454
|
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
2174
3455
|
const replica = replicas.get(room.locator);
|
|
2175
3456
|
const head = replica?.queue[0];
|
|
2176
3457
|
if (replica === undefined || head === undefined)
|
|
2177
3458
|
return 'done';
|
|
2178
|
-
|
|
3459
|
+
const state = pendings.get(pendingKey(room.locator, head.id));
|
|
3460
|
+
if (state?.accepted !== undefined)
|
|
3461
|
+
return 'done';
|
|
3462
|
+
const partner = partnerOf(head, room.locator);
|
|
3463
|
+
if (partner !== null && !leavingIn(head, room.locator)) {
|
|
3464
|
+
submitLater(partner);
|
|
2179
3465
|
return 'done';
|
|
2180
|
-
|
|
3466
|
+
}
|
|
3467
|
+
if (partner !== null && behindIn(partner, head)) {
|
|
3468
|
+
progress.complete = false;
|
|
3469
|
+
return 'done';
|
|
3470
|
+
}
|
|
2181
3471
|
const { submission, lost: earlier } = state ?? {
|
|
2182
3472
|
submission: { state: 'waiting' },
|
|
2183
3473
|
lost: false,
|
|
@@ -2193,16 +3483,16 @@ export function composeRuntime(options) {
|
|
|
2193
3483
|
}
|
|
2194
3484
|
const lost = earlier || submission.state === 'submitted';
|
|
2195
3485
|
if (lost && pastHorizon(head.id)) {
|
|
2196
|
-
await
|
|
3486
|
+
await discardHeld(head, room, local, signal);
|
|
2197
3487
|
return 'done';
|
|
2198
3488
|
}
|
|
2199
3489
|
let result;
|
|
2200
3490
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
2201
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3491
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
|
|
2202
3492
|
if (discoveryPaused || fences.has(room.locator))
|
|
2203
3493
|
return 'done';
|
|
2204
3494
|
if (signal.aborted || writesHeld()) {
|
|
2205
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3495
|
+
await exclusive(() => mark(local, room.locator, head, submission, earlier));
|
|
2206
3496
|
if (signal.aborted)
|
|
2207
3497
|
return 'done';
|
|
2208
3498
|
progress.complete = false;
|
|
@@ -2210,11 +3500,17 @@ export function composeRuntime(options) {
|
|
|
2210
3500
|
return 'done';
|
|
2211
3501
|
}
|
|
2212
3502
|
const opensBefore = openSignals;
|
|
2213
|
-
const
|
|
3503
|
+
const answeredHere = awaitingAnswer(room.locator);
|
|
3504
|
+
const answeredThere = partner === null ? () => { } : awaitingAnswer(partner);
|
|
3505
|
+
const answered = () => {
|
|
3506
|
+
answeredHere();
|
|
3507
|
+
answeredThere();
|
|
3508
|
+
};
|
|
2214
3509
|
try {
|
|
2215
3510
|
result = await options.adapter.mutate(authorized, {
|
|
2216
3511
|
locator: room.locator,
|
|
2217
3512
|
changeBatch: head,
|
|
3513
|
+
file: batchFile(head),
|
|
2218
3514
|
...(state?.upload === undefined ? {} : { upload: state.upload }),
|
|
2219
3515
|
}, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
|
|
2220
3516
|
}
|
|
@@ -2239,9 +3535,10 @@ export function composeRuntime(options) {
|
|
|
2239
3535
|
return 'again';
|
|
2240
3536
|
}
|
|
2241
3537
|
if (result.kind === 'room-not-readable') {
|
|
3538
|
+
const unread = discovered.get(result.locator) ?? room;
|
|
2242
3539
|
progress.complete = false;
|
|
2243
|
-
unreadable.add(
|
|
2244
|
-
list(
|
|
3540
|
+
unreadable.add(unread.locator);
|
|
3541
|
+
list(unread, false);
|
|
2245
3542
|
emitRooms();
|
|
2246
3543
|
return 'done';
|
|
2247
3544
|
}
|
|
@@ -2256,24 +3553,30 @@ export function composeRuntime(options) {
|
|
|
2256
3553
|
}
|
|
2257
3554
|
if (result.kind === 'write-halted') {
|
|
2258
3555
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
2259
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3556
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
|
|
2260
3557
|
if (serviceMode === 'open' && openSignals !== opensBefore)
|
|
2261
3558
|
continue;
|
|
2262
3559
|
haltedRefusal = true;
|
|
2263
3560
|
progress.complete = false;
|
|
2264
3561
|
return 'done';
|
|
2265
3562
|
}
|
|
3563
|
+
const other = partner === null ? undefined : discovered.get(partner);
|
|
2266
3564
|
if (result.kind === 'conflicted' ||
|
|
2267
3565
|
result.kind === 'rejected' ||
|
|
2268
3566
|
result.kind === 'expired' ||
|
|
2269
3567
|
result.kind === 'retryable-refusal') {
|
|
2270
3568
|
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
2271
3569
|
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
2272
|
-
await
|
|
3570
|
+
await discardHeld(head, room, local, signal);
|
|
2273
3571
|
return 'done';
|
|
2274
3572
|
}
|
|
2275
3573
|
if (result.kind === 'expired') {
|
|
2276
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3574
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
|
|
3575
|
+
const there = other === undefined ? null : await advance(other, local, authorized, continuing);
|
|
3576
|
+
if (other !== undefined && there !== null) {
|
|
3577
|
+
await withhold(there, other, local, progress);
|
|
3578
|
+
return 'done';
|
|
3579
|
+
}
|
|
2277
3580
|
const withheld = await advance(room, local, authorized, continuing);
|
|
2278
3581
|
if (withheld !== null) {
|
|
2279
3582
|
await withhold(withheld, room, local, progress);
|
|
@@ -2290,11 +3593,25 @@ export function composeRuntime(options) {
|
|
|
2290
3593
|
if (result.kind === 'retryable-refusal') {
|
|
2291
3594
|
const reset = result.reset;
|
|
2292
3595
|
progress.complete = false;
|
|
2293
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3596
|
+
await exclusive(() => mark(local, room.locator, head, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
2294
3597
|
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
2295
3598
|
}
|
|
2296
3599
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
2297
3600
|
const left = leaving(head, result);
|
|
3601
|
+
const refreshed = async () => ({
|
|
3602
|
+
there: other === undefined ? null : await advance(other, local, authorized, continuing),
|
|
3603
|
+
here: await advance(room, local, authorized, continuing),
|
|
3604
|
+
});
|
|
3605
|
+
const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
|
|
3606
|
+
if (judged !== null && (judged.there !== null || judged.here !== null)) {
|
|
3607
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
|
|
3608
|
+
if (other !== undefined && judged.there !== null) {
|
|
3609
|
+
await withhold(judged.there, other, local, progress);
|
|
3610
|
+
}
|
|
3611
|
+
if (judged.here !== null)
|
|
3612
|
+
await withhold(judged.here, room, local, progress);
|
|
3613
|
+
return 'done';
|
|
3614
|
+
}
|
|
2298
3615
|
await exclusive(async () => {
|
|
2299
3616
|
if (!continuing())
|
|
2300
3617
|
return;
|
|
@@ -2302,25 +3619,29 @@ export function composeRuntime(options) {
|
|
|
2302
3619
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
2303
3620
|
return;
|
|
2304
3621
|
}
|
|
2305
|
-
const
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
});
|
|
2311
|
-
|
|
3622
|
+
const confirmedLayer = await sliceAt(current.record, 'confirmed', [head]);
|
|
3623
|
+
const held = (result.kind === 'expired' &&
|
|
3624
|
+
(await landedWhole(room.locator, head, confirmedLayer))) ||
|
|
3625
|
+
(result.kind === 'conflicted' &&
|
|
3626
|
+
(await landedWhole(room.locator, head, confirmedLayer, result.affected)));
|
|
3627
|
+
publishChangeSet(await settle(local, room.locator, current, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
|
|
3628
|
+
await unpair(local);
|
|
2312
3629
|
moveTo({ ...status, ...attention() });
|
|
2313
3630
|
});
|
|
2314
3631
|
if (result.kind === 'expired')
|
|
2315
3632
|
continue;
|
|
2316
|
-
const
|
|
2317
|
-
if (
|
|
2318
|
-
await withhold(
|
|
3633
|
+
const { there, here } = judged ?? (await refreshed());
|
|
3634
|
+
if (other !== undefined && there !== null)
|
|
3635
|
+
await withhold(there, other, local, progress);
|
|
3636
|
+
if (here !== null) {
|
|
3637
|
+
await withhold(here, room, local, progress);
|
|
2319
3638
|
return 'done';
|
|
2320
3639
|
}
|
|
2321
3640
|
continue;
|
|
2322
3641
|
}
|
|
2323
|
-
const kept = await
|
|
3642
|
+
const kept = await (partner === null
|
|
3643
|
+
? adopted(room, local, head, result)
|
|
3644
|
+
: adoptedMove(room, partner, local, head, result)).finally(answered);
|
|
2324
3645
|
if (!kept) {
|
|
2325
3646
|
progress.complete = false;
|
|
2326
3647
|
return 'done';
|
|
@@ -2331,44 +3652,67 @@ export function composeRuntime(options) {
|
|
|
2331
3652
|
function pastHorizon(identity) {
|
|
2332
3653
|
return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
|
|
2333
3654
|
}
|
|
2334
|
-
async function discardPending(
|
|
2335
|
-
const { locator } = room;
|
|
3655
|
+
async function discardPending(rooms, local, signal) {
|
|
2336
3656
|
const discarded = await exclusive(async () => {
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
3657
|
+
const held = rooms
|
|
3658
|
+
.filter(({ locator }) => !fences.has(locator))
|
|
3659
|
+
.map((room) => ({
|
|
3660
|
+
room,
|
|
3661
|
+
before: replicas.get(room.locator),
|
|
3662
|
+
queue: queueIn(room.locator),
|
|
3663
|
+
}));
|
|
3664
|
+
if (held.length === 0)
|
|
3665
|
+
return [];
|
|
3666
|
+
const answer = await local.commit({
|
|
3667
|
+
rooms: held.flatMap(({ room, before }) => before === undefined
|
|
3668
|
+
? []
|
|
3669
|
+
: [
|
|
3670
|
+
{
|
|
3671
|
+
locator: room.locator,
|
|
3672
|
+
key: before.record.key,
|
|
3673
|
+
roots: before.record.typedSelection?.roots ?? [],
|
|
3674
|
+
overlay: { set: [], cleared: [...before.overlaid] },
|
|
3675
|
+
},
|
|
3676
|
+
]),
|
|
3677
|
+
discards: held.map(({ room }) => room.locator),
|
|
3678
|
+
});
|
|
3679
|
+
for (const { room, before, queue } of held) {
|
|
3680
|
+
const { locator } = room;
|
|
3681
|
+
lastFenced.set(locator, ++fencings);
|
|
3682
|
+
for (const batch of queue)
|
|
3683
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
3684
|
+
queuesWithoutReplica.delete(locator);
|
|
3685
|
+
if (before !== undefined) {
|
|
3686
|
+
replicas.set(locator, {
|
|
3687
|
+
...before,
|
|
3688
|
+
queue: [],
|
|
3689
|
+
overlaid: new Set(),
|
|
3690
|
+
count: answer.counts[locator] ?? before.count,
|
|
3691
|
+
});
|
|
3692
|
+
}
|
|
3693
|
+
journal.record(`room discarded ${locator}`);
|
|
2348
3694
|
}
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
if (after !== null)
|
|
2354
|
-
replicas.set(locator, after);
|
|
2355
|
-
journal.record(`room discarded ${locator}`);
|
|
2356
|
-
publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
|
|
3695
|
+
publishChangeSet(mergeChangeSets(held.map(({ room }) => changeSet(room.locator, {
|
|
3696
|
+
documents: answer.changed[room.locator] ?? [],
|
|
3697
|
+
queue: true,
|
|
3698
|
+
}))));
|
|
2357
3699
|
moveTo({ ...status, ...attention() });
|
|
2358
|
-
return
|
|
3700
|
+
return held.map(({ room }) => room);
|
|
2359
3701
|
});
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
await
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
3702
|
+
for (const room of discarded) {
|
|
3703
|
+
if (signal.aborted)
|
|
3704
|
+
return;
|
|
3705
|
+
const progress = { complete: true, reachable: true };
|
|
3706
|
+
await roomTask(room.locator, async (live) => {
|
|
3707
|
+
await Promise.race([
|
|
3708
|
+
reload(room, local, progress, () => live() && !signal.aborted),
|
|
3709
|
+
abandonedOn(signal),
|
|
3710
|
+
]);
|
|
3711
|
+
});
|
|
3712
|
+
if (signal.aborted)
|
|
3713
|
+
return;
|
|
3714
|
+
await rest(progress);
|
|
3715
|
+
}
|
|
2372
3716
|
}
|
|
2373
3717
|
function awaitingAnswer(locator) {
|
|
2374
3718
|
let release = () => { };
|
|
@@ -2385,7 +3729,7 @@ export function composeRuntime(options) {
|
|
|
2385
3729
|
async function retainModel(local, room, attached, next) {
|
|
2386
3730
|
const key = modelKey(room.locator, attached);
|
|
2387
3731
|
const previous = modelOf(room.locator, attached);
|
|
2388
|
-
const heldBefore = replicas.get(room.locator)?.
|
|
3732
|
+
const heldBefore = replicas.get(room.locator)?.record;
|
|
2389
3733
|
if (JSON.stringify(previous) === JSON.stringify(next) &&
|
|
2390
3734
|
(heldBefore === undefined ||
|
|
2391
3735
|
(JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
|
|
@@ -2404,32 +3748,47 @@ export function composeRuntime(options) {
|
|
|
2404
3748
|
let changed = null;
|
|
2405
3749
|
if (held !== undefined) {
|
|
2406
3750
|
const declared = {
|
|
2407
|
-
...held.
|
|
3751
|
+
...held.record,
|
|
2408
3752
|
operations: room.operations,
|
|
2409
3753
|
replica: room.replica,
|
|
2410
3754
|
typedSelection: room.typedSelection ?? null,
|
|
2411
3755
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
2412
3756
|
};
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
3757
|
+
let read = 'staged';
|
|
3758
|
+
if (!sameMembership(declared, held.record)) {
|
|
3759
|
+
const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), compositionOf(declared));
|
|
3760
|
+
read =
|
|
3761
|
+
verdict.verdict !== 'refuse'
|
|
3762
|
+
? 'staged'
|
|
3763
|
+
: verdict.rule === 'model-unavailable'
|
|
3764
|
+
? 'deferred'
|
|
3765
|
+
: { kind: 'refused', refusal: verdict };
|
|
3766
|
+
}
|
|
3767
|
+
const record = read === 'staged'
|
|
2417
3768
|
? declared
|
|
2418
|
-
: {
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
const
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
3769
|
+
: { ...declared, typedSelection: held.record.typedSelection };
|
|
3770
|
+
const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
|
|
3771
|
+
const replayedLayer = await replayed(record, confirmedLayer, held.queue);
|
|
3772
|
+
const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
|
|
3773
|
+
const answer = await local.commit({
|
|
3774
|
+
rooms: [
|
|
3775
|
+
{
|
|
3776
|
+
locator: room.locator,
|
|
3777
|
+
key: record.key,
|
|
3778
|
+
record: storedRecord(record),
|
|
3779
|
+
roots: record.typedSelection?.roots ?? [],
|
|
3780
|
+
composition: compositionOf(record),
|
|
3781
|
+
overlay: diff.overlay,
|
|
3782
|
+
},
|
|
3783
|
+
],
|
|
3784
|
+
});
|
|
3785
|
+
const { count, indexed } = committedRoom(room.locator, answer);
|
|
3786
|
+
replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
|
|
3787
|
+
changed = changeSet(room.locator, {
|
|
3788
|
+
documents: answer.changed[room.locator] ?? [],
|
|
3789
|
+
room: true,
|
|
3790
|
+
});
|
|
3791
|
+
if (read === 'staged' || read === 'deferred') {
|
|
2433
3792
|
if (declarationRefusals.delete(room.locator)) {
|
|
2434
3793
|
sourceRefusals.delete(room.locator);
|
|
2435
3794
|
unreadable.delete(room.locator);
|
|
@@ -2453,7 +3812,7 @@ export function composeRuntime(options) {
|
|
|
2453
3812
|
});
|
|
2454
3813
|
}
|
|
2455
3814
|
async function scoped(result, room, local) {
|
|
2456
|
-
if (
|
|
3815
|
+
if (result.scope === 'content' && !result.retryable) {
|
|
2457
3816
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
2458
3817
|
await loseRooms([room.locator]);
|
|
2459
3818
|
return;
|
|
@@ -2471,15 +3830,7 @@ export function composeRuntime(options) {
|
|
|
2471
3830
|
});
|
|
2472
3831
|
else if (result.scope !== 'operation') {
|
|
2473
3832
|
unreadable.add(room.locator);
|
|
2474
|
-
scopedAway = {
|
|
2475
|
-
...room,
|
|
2476
|
-
replica: { ...room.replica, admission: 'unavailable' },
|
|
2477
|
-
operations: {
|
|
2478
|
-
...room.operations,
|
|
2479
|
-
...(result.scope === 'content' ? { read: [] } : {}),
|
|
2480
|
-
...(result.scope === 'sync' ? { sync: [] } : {}),
|
|
2481
|
-
},
|
|
2482
|
-
};
|
|
3833
|
+
scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
|
|
2483
3834
|
await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
|
|
2484
3835
|
}
|
|
2485
3836
|
if (!continuing())
|
|
@@ -2490,17 +3841,10 @@ export function composeRuntime(options) {
|
|
|
2490
3841
|
}
|
|
2491
3842
|
function heldRooms(listed) {
|
|
2492
3843
|
return listed.flatMap((room) => {
|
|
2493
|
-
const record = replicas.get(room.locator)?.
|
|
3844
|
+
const record = replicas.get(room.locator)?.record;
|
|
2494
3845
|
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
2495
3846
|
return [];
|
|
2496
|
-
return [
|
|
2497
|
-
{
|
|
2498
|
-
locator: room.locator,
|
|
2499
|
-
roomVersion: record.roomVersion,
|
|
2500
|
-
documents: record.documents,
|
|
2501
|
-
revisions: record.revisions,
|
|
2502
|
-
},
|
|
2503
|
-
];
|
|
3847
|
+
return [{ locator: room.locator, roomVersion: record.roomVersion }];
|
|
2504
3848
|
});
|
|
2505
3849
|
}
|
|
2506
3850
|
async function synchronize(listed, local, progress, attempt, live) {
|
|
@@ -2528,7 +3872,7 @@ export function composeRuntime(options) {
|
|
|
2528
3872
|
progress.complete = false;
|
|
2529
3873
|
return false;
|
|
2530
3874
|
}
|
|
2531
|
-
const prior = replicas.get(room.locator)?.
|
|
3875
|
+
const prior = replicas.get(room.locator)?.record;
|
|
2532
3876
|
if (prior !== undefined && prior.replica.selection !== room.replica.selection) {
|
|
2533
3877
|
list(prior, false);
|
|
2534
3878
|
emitRooms();
|
|
@@ -2576,13 +3920,12 @@ export function composeRuntime(options) {
|
|
|
2576
3920
|
if (!live() || authorization === undefined)
|
|
2577
3921
|
return;
|
|
2578
3922
|
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
2579
|
-
if (!live())
|
|
2580
|
-
return;
|
|
2581
3923
|
if (bootstrapped.kind !== 'staged') {
|
|
2582
|
-
|
|
3924
|
+
if (live())
|
|
3925
|
+
await withhold(bootstrapped, room, local, progress);
|
|
2583
3926
|
return;
|
|
2584
3927
|
}
|
|
2585
|
-
await publish(local, room, bootstrapped.
|
|
3928
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
2586
3929
|
await caughtUp(room, local, progress, live);
|
|
2587
3930
|
}
|
|
2588
3931
|
finally {
|
|
@@ -2639,12 +3982,13 @@ export function composeRuntime(options) {
|
|
|
2639
3982
|
}
|
|
2640
3983
|
for (const room of result.rooms) {
|
|
2641
3984
|
if (room.operations === undefined ||
|
|
2642
|
-
!['
|
|
3985
|
+
!['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
2643
3986
|
room.operations[operation].every((scope) => typeof scope === 'string')) ||
|
|
2644
3987
|
room.replica === undefined ||
|
|
2645
3988
|
typeof room.replica.selection !== 'string' ||
|
|
2646
3989
|
room.replica.selection.length === 0 ||
|
|
2647
|
-
!['admitted', 'unavailable'].includes(room.replica.admission)
|
|
3990
|
+
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
3991
|
+
!['locked', 'open'].includes(room.replica.offline)) {
|
|
2648
3992
|
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
2649
3993
|
await refuse('missing-required-guarantee');
|
|
2650
3994
|
notifyReady();
|
|
@@ -2668,11 +4012,12 @@ export function composeRuntime(options) {
|
|
|
2668
4012
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
2669
4013
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
2670
4014
|
limits = result.providerLimits;
|
|
2671
|
-
await local.
|
|
4015
|
+
await local.fact('providerLimits', limits);
|
|
4016
|
+
await local.fact('capabilities', capabilities);
|
|
2672
4017
|
}
|
|
2673
4018
|
const signedIn = result.member ?? null;
|
|
2674
4019
|
if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
|
|
2675
|
-
await local.
|
|
4020
|
+
await local.fact('member', signedIn);
|
|
2676
4021
|
member = signedIn;
|
|
2677
4022
|
emitMember();
|
|
2678
4023
|
}
|
|
@@ -2806,6 +4151,7 @@ export function composeRuntime(options) {
|
|
|
2806
4151
|
function closedSocket(revoked, lost) {
|
|
2807
4152
|
const opened = socketOpen;
|
|
2808
4153
|
socketOpen = false;
|
|
4154
|
+
connectionLost();
|
|
2809
4155
|
if (lost) {
|
|
2810
4156
|
reads.lost();
|
|
2811
4157
|
conversations.lost();
|
|
@@ -2875,7 +4221,7 @@ export function composeRuntime(options) {
|
|
|
2875
4221
|
owed.clear();
|
|
2876
4222
|
for (const locator of reloading)
|
|
2877
4223
|
catching.add(locator);
|
|
2878
|
-
|
|
4224
|
+
liveQueries.recover();
|
|
2879
4225
|
return reloading;
|
|
2880
4226
|
}
|
|
2881
4227
|
async function reloadNow(replaced, progress) {
|
|
@@ -3042,7 +4388,7 @@ export function composeRuntime(options) {
|
|
|
3042
4388
|
if (synced && !recorded) {
|
|
3043
4389
|
const completedAt = new Date().toISOString();
|
|
3044
4390
|
try {
|
|
3045
|
-
await (await openLocal()).
|
|
4391
|
+
await (await openLocal()).fact('lastCompleted', completedAt);
|
|
3046
4392
|
if (!live())
|
|
3047
4393
|
return;
|
|
3048
4394
|
recorded = true;
|
|
@@ -3082,8 +4428,8 @@ export function composeRuntime(options) {
|
|
|
3082
4428
|
if (room === undefined || room.replica.admission !== 'admitted')
|
|
3083
4429
|
return;
|
|
3084
4430
|
if (held !== undefined &&
|
|
3085
|
-
(held.
|
|
3086
|
-
options.adapter.reached(locator, held.
|
|
4431
|
+
(held.record.replica.selection !== room.replica.selection ||
|
|
4432
|
+
options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
|
|
3087
4433
|
return;
|
|
3088
4434
|
}
|
|
3089
4435
|
const during = underway?.generation === generation ? underway.progress : null;
|
|
@@ -3130,6 +4476,7 @@ export function composeRuntime(options) {
|
|
|
3130
4476
|
return;
|
|
3131
4477
|
if (event.kind === 'opened') {
|
|
3132
4478
|
socketOpen = true;
|
|
4479
|
+
connectionLive();
|
|
3133
4480
|
generation += 1;
|
|
3134
4481
|
reads.opened();
|
|
3135
4482
|
conversations.reopened();
|
|
@@ -3144,7 +4491,14 @@ export function composeRuntime(options) {
|
|
|
3144
4491
|
void rediscover(event.scope);
|
|
3145
4492
|
else
|
|
3146
4493
|
void applyRoom(event.locator, event.roomVersion);
|
|
3147
|
-
}, receipts);
|
|
4494
|
+
}, receipts, heldRevisions);
|
|
4495
|
+
}
|
|
4496
|
+
async function heldRevisions(locator, paths) {
|
|
4497
|
+
const held = replicas.get(locator);
|
|
4498
|
+
if (held === undefined || paths.length === 0)
|
|
4499
|
+
return new Map();
|
|
4500
|
+
const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
|
|
4501
|
+
return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
|
|
3148
4502
|
}
|
|
3149
4503
|
function writesHeld() {
|
|
3150
4504
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -3214,8 +4568,12 @@ export function composeRuntime(options) {
|
|
|
3214
4568
|
if (closed)
|
|
3215
4569
|
return;
|
|
3216
4570
|
closed = true;
|
|
3217
|
-
if (
|
|
3218
|
-
clearInterval(
|
|
4571
|
+
if (probing !== null)
|
|
4572
|
+
clearInterval(probing);
|
|
4573
|
+
for (const resolve of shownWaiters.splice(0))
|
|
4574
|
+
resolve();
|
|
4575
|
+
connectionLive();
|
|
4576
|
+
lockListeners.clear();
|
|
3219
4577
|
readinessWatches.clear();
|
|
3220
4578
|
withdrawStream?.();
|
|
3221
4579
|
withdrawStream = undefined;
|
|
@@ -3224,17 +4582,13 @@ export function composeRuntime(options) {
|
|
|
3224
4582
|
withdrawServiceMode?.();
|
|
3225
4583
|
conversations.close();
|
|
3226
4584
|
void recovery.retire();
|
|
3227
|
-
store?.close();
|
|
3228
|
-
void opening?.then((opened) => {
|
|
3229
|
-
opened.close();
|
|
3230
|
-
}, () => { });
|
|
3231
4585
|
roomListeners.clear();
|
|
3232
4586
|
refusalListeners.clear();
|
|
3233
4587
|
storageLossListeners.clear();
|
|
3234
4588
|
statusListeners.clear();
|
|
3235
4589
|
serviceModeListeners.clear();
|
|
3236
4590
|
memberListeners.clear();
|
|
3237
|
-
|
|
4591
|
+
liveQueries.close();
|
|
3238
4592
|
queueSubscriptions.clear();
|
|
3239
4593
|
roomDraftListeners.clear();
|
|
3240
4594
|
presentationListeners.clear();
|
|
@@ -3280,7 +4634,7 @@ export function composeRuntime(options) {
|
|
|
3280
4634
|
const watchers = seedWatchers.get(plan.key) ?? new Set();
|
|
3281
4635
|
seedWatchers.set(plan.key, watchers);
|
|
3282
4636
|
watchers.add(shown);
|
|
3283
|
-
const stop =
|
|
4637
|
+
const stop = liveQueries.watch(plan, identityOf(plan), () => {
|
|
3284
4638
|
const cover = covered(plan);
|
|
3285
4639
|
awaitPreparation(cover);
|
|
3286
4640
|
return seedBehind(plan, cover);
|
|
@@ -3301,66 +4655,53 @@ export function composeRuntime(options) {
|
|
|
3301
4655
|
stop();
|
|
3302
4656
|
};
|
|
3303
4657
|
}
|
|
3304
|
-
async function searched(input, reach) {
|
|
4658
|
+
async function searched(input, reach, locked) {
|
|
3305
4659
|
const asked = searchPlanOf(input);
|
|
3306
|
-
const
|
|
3307
|
-
|
|
4660
|
+
const filter = {
|
|
4661
|
+
...filterOf(asked.documents ?? {}, roomFacts(reach), planReach(reach)),
|
|
4662
|
+
locked,
|
|
4663
|
+
};
|
|
4664
|
+
const unlocking = (filter.rooms ?? [])
|
|
4665
|
+
.filter((room) => locked.includes(room))
|
|
4666
|
+
.map((room) => ({ room, awaits: 'unlock' }));
|
|
3308
4667
|
for (;;) {
|
|
3309
4668
|
const cover = covered(filter);
|
|
3310
4669
|
awaitPreparation(cover);
|
|
3311
|
-
const pending = 'pending' in cover ?
|
|
3312
|
-
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
3313
|
-
const held = covered({
|
|
3314
|
-
...filter,
|
|
3315
|
-
rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
|
|
3316
|
-
});
|
|
3317
|
-
const covering = 'pending' in held ? [] : held.coverage.rooms;
|
|
3318
|
-
const unindexed = covering
|
|
3319
|
-
.filter(({ room }) => !searchIndex.rooms().has(room))
|
|
3320
|
-
.map(({ room }) => room);
|
|
3321
|
-
for (const room of unindexed)
|
|
3322
|
-
pending.push({ room, awaits: 'index' });
|
|
3323
|
-
const listed = conversations.searchable();
|
|
3324
|
-
const named = reach !== null && !reach.conversations
|
|
3325
|
-
? []
|
|
3326
|
-
: (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
|
|
3327
|
-
const waiting = named.filter((one) => !searchIndex.conversations().has(one));
|
|
3328
|
-
for (const conversation of waiting)
|
|
3329
|
-
pending.push({ conversation, awaits: 'index' });
|
|
3330
|
-
if (unindexed.length > 0 || waiting.length > 0)
|
|
3331
|
-
index(unindexed, waiting);
|
|
4670
|
+
const pending = [...unlocking, ...('pending' in cover ? cover.pending : [])];
|
|
3332
4671
|
const complete = pending.length === 0 && !('pending' in cover);
|
|
4672
|
+
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
3333
4673
|
const answering = complete
|
|
3334
4674
|
? filter
|
|
3335
|
-
: {
|
|
3336
|
-
...filter,
|
|
3337
|
-
rooms: covering
|
|
3338
|
-
.filter(({ room }) => searchIndex.rooms().has(room))
|
|
3339
|
-
.map(({ room }) => room),
|
|
3340
|
-
};
|
|
4675
|
+
: { ...filter, rooms: reachedRooms(filter).filter((room) => !awaited.has(room)) };
|
|
3341
4676
|
const answered = covered(answering);
|
|
3342
4677
|
if ('pending' in answered)
|
|
3343
4678
|
continue;
|
|
3344
|
-
const
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
const
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
4679
|
+
const listed = conversations.searchable();
|
|
4680
|
+
const named = reach !== null && !reach.conversations
|
|
4681
|
+
? []
|
|
4682
|
+
: (asked.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
|
|
4683
|
+
const hits = await (await openLocal()).search({
|
|
4684
|
+
text: asked.text,
|
|
4685
|
+
limit: capped(asked.limit),
|
|
4686
|
+
after: asked.after,
|
|
4687
|
+
total: asked.total,
|
|
4688
|
+
documents: {
|
|
4689
|
+
plan: answering,
|
|
4690
|
+
rooms: answered.rooms,
|
|
4691
|
+
beneath: asked.collection
|
|
4692
|
+
? answered.rooms.map(({ locator, key }) => ({
|
|
4693
|
+
key,
|
|
4694
|
+
roots: replicas.get(locator)?.record.typedSelection?.roots ?? [],
|
|
4695
|
+
}))
|
|
4696
|
+
: [],
|
|
4697
|
+
},
|
|
4698
|
+
conversations: named,
|
|
4699
|
+
});
|
|
4700
|
+
if (JSON.stringify(covered(answering)) !== JSON.stringify(answered))
|
|
3353
4701
|
continue;
|
|
3354
|
-
const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms === null ? null : new Set(answering.rooms), plan.collection
|
|
3355
|
-
? new Map(answered.coverage.rooms.map(({ room }) => [
|
|
3356
|
-
room,
|
|
3357
|
-
replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
|
|
3358
|
-
]))
|
|
3359
|
-
: null);
|
|
3360
4702
|
const coverage = {
|
|
3361
4703
|
rooms: answered.coverage.rooms,
|
|
3362
|
-
conversations:
|
|
3363
|
-
index: searchIndex.generation(),
|
|
4704
|
+
conversations: named,
|
|
3364
4705
|
overlay: answered.coverage.overlay,
|
|
3365
4706
|
};
|
|
3366
4707
|
return complete
|
|
@@ -3374,13 +4715,17 @@ export function composeRuntime(options) {
|
|
|
3374
4715
|
roomDraftListeners.add(listener);
|
|
3375
4716
|
void exclusive(async () => {
|
|
3376
4717
|
await openLocal();
|
|
3377
|
-
if (closed || !roomDraftListeners.has(listener))
|
|
3378
|
-
return;
|
|
3379
|
-
listener.ready = true;
|
|
3380
4718
|
}).then(() => {
|
|
3381
|
-
if (closed || !
|
|
4719
|
+
if (closed || !roomDraftListeners.has(listener))
|
|
3382
4720
|
return;
|
|
3383
|
-
|
|
4721
|
+
const snapshot = snapshotRoomDrafts();
|
|
4722
|
+
draftsShown = draftsShown.then(async () => {
|
|
4723
|
+
const value = await snapshot.catch(() => null);
|
|
4724
|
+
if (value === null || closed || !roomDraftListeners.has(listener))
|
|
4725
|
+
return;
|
|
4726
|
+
listener.ready = true;
|
|
4727
|
+
listener.notify(value);
|
|
4728
|
+
});
|
|
3384
4729
|
}, () => { });
|
|
3385
4730
|
return () => {
|
|
3386
4731
|
roomDraftListeners.delete(listener);
|
|
@@ -3413,7 +4758,7 @@ export function composeRuntime(options) {
|
|
|
3413
4758
|
};
|
|
3414
4759
|
}
|
|
3415
4760
|
if (topic === 'readiness') {
|
|
3416
|
-
return watchReadiness({ target: '', code: 'stored',
|
|
4761
|
+
return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
|
|
3417
4762
|
}
|
|
3418
4763
|
if (topic === 'rooms') {
|
|
3419
4764
|
roomListeners.add(deliver);
|
|
@@ -3475,7 +4820,7 @@ export function composeRuntime(options) {
|
|
|
3475
4820
|
return listed();
|
|
3476
4821
|
},
|
|
3477
4822
|
query: ((input) => rowsQuery(planned(input, null))),
|
|
3478
|
-
search: (input) => searched(input, null),
|
|
4823
|
+
search: (input) => searched(input, null, []),
|
|
3479
4824
|
types(locator) {
|
|
3480
4825
|
return {
|
|
3481
4826
|
collection: modelTypes(modelOf(locator)),
|
|
@@ -3495,9 +4840,9 @@ export function composeRuntime(options) {
|
|
|
3495
4840
|
return listed;
|
|
3496
4841
|
});
|
|
3497
4842
|
},
|
|
3498
|
-
read(room, path) {
|
|
4843
|
+
async read(room, path) {
|
|
3499
4844
|
const record = roomDrafts.get(room)?.get(path);
|
|
3500
|
-
return record === undefined ? null :
|
|
4845
|
+
return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
|
|
3501
4846
|
},
|
|
3502
4847
|
write(room, path, revision, input) {
|
|
3503
4848
|
return exclusive(async () => {
|
|
@@ -3507,11 +4852,11 @@ export function composeRuntime(options) {
|
|
|
3507
4852
|
const local = await openLocal();
|
|
3508
4853
|
const current = roomDrafts.get(room)?.get(path);
|
|
3509
4854
|
if ((current?.revision ?? null) !== revision) {
|
|
3510
|
-
return staleWrite(current === undefined ? null :
|
|
4855
|
+
return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
|
|
3511
4856
|
}
|
|
3512
4857
|
const file = parts.file === undefined
|
|
3513
4858
|
? undefined
|
|
3514
|
-
: await heldFile(parts.file.blob, binaryBase(
|
|
4859
|
+
: await heldFile(parts.file.blob, binaryBase(await visibleOf(room, [path]), path));
|
|
3515
4860
|
const record = {
|
|
3516
4861
|
locator: room,
|
|
3517
4862
|
path,
|
|
@@ -3519,10 +4864,12 @@ export function composeRuntime(options) {
|
|
|
3519
4864
|
parts: held(current?.parts, parts, file),
|
|
3520
4865
|
returned: current?.returned ?? null,
|
|
3521
4866
|
};
|
|
3522
|
-
await local.keepRoomDraft(record
|
|
4867
|
+
await local.keepRoomDraft(record, parts.file === undefined || file === undefined
|
|
4868
|
+
? []
|
|
4869
|
+
: [{ key: file.localId, blob: parts.file.blob }]);
|
|
3523
4870
|
rememberRoomDraft(record);
|
|
3524
|
-
publishChangeSet(changeSet(room,
|
|
3525
|
-
return { outcome: 'written', roomDraft:
|
|
4871
|
+
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
4872
|
+
return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
|
|
3526
4873
|
});
|
|
3527
4874
|
},
|
|
3528
4875
|
discard(room, path, revision) {
|
|
@@ -3530,11 +4877,11 @@ export function composeRuntime(options) {
|
|
|
3530
4877
|
const local = await openLocal();
|
|
3531
4878
|
const current = roomDrafts.get(room)?.get(path);
|
|
3532
4879
|
if (current === undefined || current.revision !== revision) {
|
|
3533
|
-
return staleWrite(current === undefined ? null :
|
|
4880
|
+
return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
|
|
3534
4881
|
}
|
|
3535
4882
|
await local.discardRoomDraft(room, path);
|
|
3536
4883
|
forgetRoomDraft(room, path);
|
|
3537
|
-
publishChangeSet(changeSet(room,
|
|
4884
|
+
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
3538
4885
|
moveTo({ ...status, ...attention() });
|
|
3539
4886
|
return { outcome: 'written', roomDraft: null };
|
|
3540
4887
|
});
|
|
@@ -3568,15 +4915,20 @@ export function composeRuntime(options) {
|
|
|
3568
4915
|
history(locator, path, query) {
|
|
3569
4916
|
return readHistory(locator, path, query);
|
|
3570
4917
|
},
|
|
4918
|
+
resolve(room, path, destination) {
|
|
4919
|
+
return resolveReference(room, path, destination);
|
|
4920
|
+
},
|
|
3571
4921
|
binaryFile: {
|
|
4922
|
+
part: readPart,
|
|
3572
4923
|
read: readBinaryPath,
|
|
3573
4924
|
link: linkBinaryPath,
|
|
3574
|
-
readReference: readBinaryReference,
|
|
3575
4925
|
},
|
|
3576
|
-
call(name, input) {
|
|
3577
|
-
|
|
4926
|
+
async call(name, input) {
|
|
4927
|
+
await ready;
|
|
4928
|
+
const room = calledRooms(name, input).find((one) => locked(one));
|
|
4929
|
+
return room === undefined ? recoveredAction(name, input) : lockedRoom(room);
|
|
3578
4930
|
},
|
|
3579
|
-
reached(locator, roomVersion, path) {
|
|
4931
|
+
async reached(locator, roomVersion, path) {
|
|
3580
4932
|
return path === undefined
|
|
3581
4933
|
? reachedBy(locator, roomVersion)
|
|
3582
4934
|
: writtenAfter(locator, roomVersion, path);
|
|
@@ -3584,14 +4936,16 @@ export function composeRuntime(options) {
|
|
|
3584
4936
|
log() {
|
|
3585
4937
|
return journal.export();
|
|
3586
4938
|
},
|
|
4939
|
+
cleanUp,
|
|
3587
4940
|
persistence() {
|
|
3588
4941
|
return persistence === 'granted';
|
|
3589
4942
|
},
|
|
3590
4943
|
async retry() {
|
|
3591
4944
|
journal.record('retry');
|
|
3592
|
-
|
|
4945
|
+
unpreparable.clear();
|
|
3593
4946
|
queueReadiness();
|
|
3594
|
-
|
|
4947
|
+
if (full)
|
|
4948
|
+
await probed();
|
|
3595
4949
|
},
|
|
3596
4950
|
observe: observing(watch),
|
|
3597
4951
|
async reauthorize() {
|
|
@@ -3613,12 +4967,15 @@ export function composeRuntime(options) {
|
|
|
3613
4967
|
const reachable = {
|
|
3614
4968
|
md,
|
|
3615
4969
|
watch,
|
|
3616
|
-
referrerScope: (referrer) => options.adapter.referrerScope(referrer),
|
|
3617
4970
|
plan: planned,
|
|
3618
4971
|
query: (plan) => rowsQuery(plan),
|
|
3619
4972
|
watchPlan,
|
|
3620
4973
|
search: searched,
|
|
3621
4974
|
watchReadiness,
|
|
4975
|
+
locked,
|
|
4976
|
+
lockedRooms,
|
|
4977
|
+
watchLocks,
|
|
4978
|
+
located,
|
|
3622
4979
|
};
|
|
3623
4980
|
return {
|
|
3624
4981
|
md,
|
|
@@ -3633,6 +4990,11 @@ export function composeRuntime(options) {
|
|
|
3633
4990
|
journal.record(`persistence ${answer}`);
|
|
3634
4991
|
queueReadiness();
|
|
3635
4992
|
},
|
|
3636
|
-
|
|
4993
|
+
shown: screen,
|
|
4994
|
+
within: (reach, port) => {
|
|
4995
|
+
if (full)
|
|
4996
|
+
void probed();
|
|
4997
|
+
return narrowed(reachable, reach, port);
|
|
4998
|
+
},
|
|
3637
4999
|
};
|
|
3638
5000
|
}
|