@agentshouse/mdruntime 0.1.0-cedb03eaa48b6 → 0.1.0-cee9c8aa4cd86
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 +86 -71
- package/README.md +9 -8
- package/dist/adapter.d.ts +44 -18
- 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/bootstrap.d.ts +13 -6
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +90 -61
- 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 +9 -16
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +11 -89
- 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 +2 -1
- package/dist/chat/list.d.ts.map +1 -1
- package/dist/chat/list.js +7 -3
- package/dist/chat/markdown.d.ts +1 -0
- package/dist/chat/markdown.d.ts.map +1 -1
- package/dist/chat/markdown.js +78 -20
- package/dist/chat/message.d.ts +1 -0
- package/dist/chat/message.d.ts.map +1 -1
- package/dist/chat/message.js +73 -11
- package/dist/chat/messages.d.ts +14 -1
- package/dist/chat/messages.d.ts.map +1 -1
- package/dist/chat/messages.js +79 -24
- 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 +1 -0
- package/dist/chat/overlay.d.ts.map +1 -1
- package/dist/chat/overlay.js +11 -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/pieces.d.ts +62 -0
- package/dist/chat/pieces.d.ts.map +1 -0
- package/dist/chat/pieces.js +1110 -0
- package/dist/chat/provider.d.ts +8 -1
- package/dist/chat/provider.d.ts.map +1 -1
- package/dist/chat/provider.js +88 -81
- package/dist/chat/ready.d.ts +5 -2
- 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/steady.d.ts +3 -0
- package/dist/chat/steady.d.ts.map +1 -0
- package/dist/chat/steady.js +26 -0
- package/dist/chat/style.d.ts.map +1 -1
- package/dist/chat/style.js +5 -0
- package/dist/chat.d.ts +3 -2
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +2 -2
- package/dist/client.d.ts +6 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +29 -2
- package/dist/collection.d.ts +6 -19
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +30 -317
- package/dist/compile.d.ts +16 -0
- package/dist/compile.d.ts.map +1 -0
- package/dist/compile.js +718 -0
- package/dist/connection.d.ts +11 -5
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +1931 -706
- package/dist/conversation.d.ts +9 -3
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +85 -61
- 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 +2 -1
- package/dist/durability.d.ts.map +1 -1
- 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 +0 -1
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +10 -3
- 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 +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- 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 +22 -6
- 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 +29 -4
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +25 -0
- package/dist/overlay.d.ts +2 -5
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +86 -171
- package/dist/query.d.ts +2 -1
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +1 -0
- package/dist/reach.d.ts +5 -1
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +158 -52
- package/dist/readiness.d.ts +3 -3
- package/dist/readiness.d.ts.map +1 -1
- package/dist/reads.js +1 -1
- 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 +10 -13
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +74 -51
- package/dist/replica.d.ts +54 -36
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +277 -184
- package/dist/room-draft.d.ts +3 -1
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +29 -22
- 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 +50 -14
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +502 -178
- 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 +8 -3
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +129 -66
- package/dist/store.d.ts +79 -93
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +318 -687
- package/dist/stored.d.ts +138 -0
- package/dist/stored.d.ts.map +1 -0
- package/dist/stored.js +544 -0
- package/dist/tables.d.ts +332 -0
- package/dist/tables.d.ts.map +1 -0
- package/dist/tables.js +570 -0
- package/dist/turn.d.ts +4 -2
- package/dist/turn.d.ts.map +1 -1
- package/dist/turn.js +46 -22
- 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 +16 -0
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +4 -0
- package/dist/words.d.ts +19 -0
- package/dist/words.d.ts.map +1 -0
- package/dist/words.js +150 -0
- package/package.json +10 -4
- package/dist/read-side.d.ts +0 -27
- package/dist/read-side.d.ts.map +0 -1
- package/dist/read-side.js +0 -824
package/dist/connection.js
CHANGED
|
@@ -1,47 +1,42 @@
|
|
|
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, absentReference, 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
28
|
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
27
29
|
import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
|
|
28
30
|
import { FencedMap } from './fence.js';
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
+
import { fingerprint } from './stored.js';
|
|
32
|
+
import { sameBinary } from './overlay.js';
|
|
33
|
+
import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
|
|
31
34
|
import { identityTime, roomDraftRevision } from './identity.js';
|
|
32
35
|
import { beneath, normalize } from './paths.js';
|
|
33
36
|
import { openRuntimeStore, } from './store.js';
|
|
37
|
+
import { changedCleanly, sameComposition, validatedCollection, } from './validation.js';
|
|
34
38
|
import { unconfirmed } from './transfer.js';
|
|
35
39
|
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
40
|
function within(root, path) {
|
|
46
41
|
return root === '' || beneath(root, path);
|
|
47
42
|
}
|
|
@@ -59,24 +54,18 @@ const OPENING_LANE = 'opening';
|
|
|
59
54
|
const CONVERSATION_LANE = 'conversations';
|
|
60
55
|
const ROOM_READS = 256;
|
|
61
56
|
const ROOM_ATTEMPTS = 16;
|
|
57
|
+
const ERASE_BATCH = 512;
|
|
58
|
+
const REDERIVE_BATCH = 256;
|
|
59
|
+
const WALK_BYTES = 1 << 20;
|
|
60
|
+
const QUERY_BYTES = 1 << 20;
|
|
61
|
+
const STAGE_BYTES = 1 << 20;
|
|
62
|
+
const CLEAN_CHANGES = 2_000;
|
|
63
|
+
// A Room that locks offline stays readable this long after the connection drops, so a laptop waking or changing network keeps working while a disconnected device locks soon after.
|
|
64
|
+
const LOCK_GRACE_MS = 120_000;
|
|
62
65
|
function operationOf(input) {
|
|
63
66
|
const operation = input?.operation_id;
|
|
64
67
|
return typeof operation === 'string' ? operation : null;
|
|
65
68
|
}
|
|
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
69
|
function abandonedOn(signal) {
|
|
81
70
|
return new Promise((resolve) => {
|
|
82
71
|
if (signal.aborted)
|
|
@@ -88,11 +77,14 @@ function abandonedOn(signal) {
|
|
|
88
77
|
function roomLane(locator) {
|
|
89
78
|
return `room ${locator}`;
|
|
90
79
|
}
|
|
80
|
+
function pendingKey(locator, id) {
|
|
81
|
+
return `${locator}\u0000${id}`;
|
|
82
|
+
}
|
|
91
83
|
export function composeRuntime(options) {
|
|
92
84
|
const localSaves = options.adapter.localSaves === true;
|
|
85
|
+
const roomTypes = roomTypesOf(options.adapter.roomTypes);
|
|
93
86
|
let authorization = options.authorization;
|
|
94
87
|
let opening;
|
|
95
|
-
let store;
|
|
96
88
|
const fences = new Map();
|
|
97
89
|
const isFenced = (locator) => fences.has(locator);
|
|
98
90
|
let fencings = 0;
|
|
@@ -164,6 +156,7 @@ export function composeRuntime(options) {
|
|
|
164
156
|
let discovering = null;
|
|
165
157
|
let opened = null;
|
|
166
158
|
const owed = new Set();
|
|
159
|
+
const reloadsDue = new Map();
|
|
167
160
|
const lanes = new Map();
|
|
168
161
|
let roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
169
162
|
const working = new Set();
|
|
@@ -175,10 +168,13 @@ export function composeRuntime(options) {
|
|
|
175
168
|
const queueSubscriptions = new Set();
|
|
176
169
|
const roomDraftListeners = new Set();
|
|
177
170
|
const presentationListeners = new Set();
|
|
178
|
-
const
|
|
171
|
+
const liveQueries = createLiveQueries((plan, cover) => answeredPlan(plan, cover));
|
|
179
172
|
let withdrawConversations;
|
|
180
173
|
let currencyQueued = null;
|
|
181
174
|
let socketOpen = false;
|
|
175
|
+
const confirmed = new Set();
|
|
176
|
+
let lockGrace = null;
|
|
177
|
+
const lockListeners = new Set();
|
|
182
178
|
let serviceMode = 'open';
|
|
183
179
|
let haltedRefusal = false;
|
|
184
180
|
let openSignals = 0;
|
|
@@ -186,9 +182,8 @@ export function composeRuntime(options) {
|
|
|
186
182
|
let member = null;
|
|
187
183
|
const memberListeners = new Set();
|
|
188
184
|
const prepared = new Set();
|
|
189
|
-
const projecting = new Set();
|
|
190
185
|
const unpreparedDemand = new Set();
|
|
191
|
-
const
|
|
186
|
+
const unpreparable = new Set();
|
|
192
187
|
let preparing = false;
|
|
193
188
|
let targets = {};
|
|
194
189
|
let persistence = options.shell.persistence;
|
|
@@ -201,7 +196,19 @@ export function composeRuntime(options) {
|
|
|
201
196
|
const transfers = new FencedMap(isFenced);
|
|
202
197
|
const conversations = createConversationStore({
|
|
203
198
|
agents: declaredAgents,
|
|
204
|
-
documents: (
|
|
199
|
+
documents: (declaration, after) => rowsQuery(planOf({
|
|
200
|
+
rooms: [declaration.locator],
|
|
201
|
+
types: [declaration.type],
|
|
202
|
+
select: {
|
|
203
|
+
fields: [
|
|
204
|
+
declaration.identity,
|
|
205
|
+
declaration.name,
|
|
206
|
+
...(declaration.retired === undefined ? [] : [declaration.retired]),
|
|
207
|
+
].map((field) => `/${escapePointer(field)}`),
|
|
208
|
+
},
|
|
209
|
+
limit: MAXIMUM_WINDOW,
|
|
210
|
+
...(after === undefined ? {} : { after }),
|
|
211
|
+
}, roomFacts())),
|
|
205
212
|
call: (name, input) => invokeAction(name, input),
|
|
206
213
|
durably,
|
|
207
214
|
stream: () => options.adapter.conversations,
|
|
@@ -211,14 +218,8 @@ export function composeRuntime(options) {
|
|
|
211
218
|
unanswered: resendLater,
|
|
212
219
|
expired: (operation) => pastHorizon(operation),
|
|
213
220
|
});
|
|
214
|
-
const searchIndex = createSearchIndex({
|
|
215
|
-
room: (room) => readSide.room(room),
|
|
216
|
-
document: (room, path) => readSide.document(room, path),
|
|
217
|
-
});
|
|
218
|
-
const roomsToIndex = new Set();
|
|
219
|
-
const conversationsToIndex = new Set();
|
|
220
|
-
let indexing = false;
|
|
221
221
|
const journal = createLog();
|
|
222
|
+
const keptFiles = openedFiles(options.database, isFenced);
|
|
222
223
|
let notifyReady = () => { };
|
|
223
224
|
const ready = new Promise((resolve) => {
|
|
224
225
|
notifyReady = resolve;
|
|
@@ -236,16 +237,36 @@ export function composeRuntime(options) {
|
|
|
236
237
|
if (room.size === 0)
|
|
237
238
|
roomDrafts.delete(locator);
|
|
238
239
|
}
|
|
239
|
-
function
|
|
240
|
+
async function visibleOf(locator, paths) {
|
|
241
|
+
const held = replicas.get(locator);
|
|
242
|
+
if (held === undefined)
|
|
243
|
+
return undefined;
|
|
244
|
+
const local = await openLocal();
|
|
245
|
+
const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
|
|
246
|
+
return { ...slice, collection: collectionOf(slice.documents, compositionOf(held.record)) };
|
|
247
|
+
}
|
|
248
|
+
function roomDraftOf(record, visible) {
|
|
240
249
|
return structuredClone({
|
|
241
250
|
room: record.locator,
|
|
242
251
|
path: record.path,
|
|
243
252
|
revision: record.revision,
|
|
244
253
|
parts: record.parts,
|
|
245
|
-
changedUnderneath: underneath(record.parts, record.path,
|
|
254
|
+
changedUnderneath: underneath(record.parts, record.path, visible),
|
|
246
255
|
returned: record.returned,
|
|
247
256
|
});
|
|
248
257
|
}
|
|
258
|
+
async function roomDraftsOf(records) {
|
|
259
|
+
const rooms = new Map();
|
|
260
|
+
for (const record of records) {
|
|
261
|
+
const paths = rooms.get(record.locator) ?? [];
|
|
262
|
+
paths.push(record.path);
|
|
263
|
+
rooms.set(record.locator, paths);
|
|
264
|
+
}
|
|
265
|
+
const layers = new Map();
|
|
266
|
+
for (const [locator, paths] of rooms)
|
|
267
|
+
layers.set(locator, await visibleOf(locator, paths));
|
|
268
|
+
return records.map((record) => roomDraftOf(record, layers.get(record.locator)));
|
|
269
|
+
}
|
|
249
270
|
function snapshotRoomDrafts() {
|
|
250
271
|
const records = [...roomDrafts.values()].flatMap((room) => [...room.values()]);
|
|
251
272
|
records.sort((left, right) => {
|
|
@@ -255,18 +276,24 @@ export function composeRuntime(options) {
|
|
|
255
276
|
return left.path < right.path ? -1 : 1;
|
|
256
277
|
return 0;
|
|
257
278
|
});
|
|
258
|
-
return records
|
|
279
|
+
return roomDraftsOf(records);
|
|
259
280
|
}
|
|
281
|
+
let draftsShown = Promise.resolve();
|
|
260
282
|
function publishRoomDrafts(waitingOnly) {
|
|
261
283
|
if (closed)
|
|
262
284
|
return;
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
listener
|
|
269
|
-
|
|
285
|
+
const snapshot = snapshotRoomDrafts();
|
|
286
|
+
draftsShown = draftsShown.then(async () => {
|
|
287
|
+
const value = await snapshot.catch(() => null);
|
|
288
|
+
if (value === null || closed)
|
|
289
|
+
return;
|
|
290
|
+
for (const listener of roomDraftListeners) {
|
|
291
|
+
if (waitingOnly && listener.ready)
|
|
292
|
+
continue;
|
|
293
|
+
listener.ready = true;
|
|
294
|
+
listener.notify(value);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
270
297
|
}
|
|
271
298
|
function presentationOf(app) {
|
|
272
299
|
const value = presentations.get(app);
|
|
@@ -278,7 +305,7 @@ export function composeRuntime(options) {
|
|
|
278
305
|
const stored = next(presentations.get(app));
|
|
279
306
|
await local.present({ app, value: stored });
|
|
280
307
|
presentations.set(app, stored);
|
|
281
|
-
publishChangeSet(changeSet('',
|
|
308
|
+
publishChangeSet(changeSet('', { presentation: app }));
|
|
282
309
|
});
|
|
283
310
|
}
|
|
284
311
|
function emitPresentation(app) {
|
|
@@ -329,21 +356,69 @@ export function composeRuntime(options) {
|
|
|
329
356
|
return { attentionRequired, reset };
|
|
330
357
|
}
|
|
331
358
|
function documentsIn(locator) {
|
|
332
|
-
|
|
333
|
-
if (replica === undefined)
|
|
334
|
-
return 0;
|
|
335
|
-
const roots = replica.confirmed.record.typedSelection?.roots ?? [];
|
|
336
|
-
const weighed = outsideOf(roots);
|
|
337
|
-
return (tally(replica.visible.documents, weighed.documents) +
|
|
338
|
-
tally(replica.visible.binaries, weighed.binaries));
|
|
359
|
+
return replicas.get(locator)?.count ?? 0;
|
|
339
360
|
}
|
|
340
361
|
function listed() {
|
|
341
362
|
return structuredClone([...rooms.values()].map((room) => {
|
|
342
363
|
const documents = documentsIn(room.locator);
|
|
343
364
|
counted.set(room.locator, documents);
|
|
344
|
-
return
|
|
365
|
+
return locked(room.locator)
|
|
366
|
+
? {
|
|
367
|
+
...room,
|
|
368
|
+
model: modelTypes(NO_MODEL),
|
|
369
|
+
typedSelection: null,
|
|
370
|
+
documents: 0,
|
|
371
|
+
locked: true,
|
|
372
|
+
}
|
|
373
|
+
: { ...room, documents, locked: false };
|
|
345
374
|
}));
|
|
346
375
|
}
|
|
376
|
+
function locked(locator) {
|
|
377
|
+
return rooms.get(locator)?.replica.offline === 'locked' && !confirmed.has(locator);
|
|
378
|
+
}
|
|
379
|
+
function lockedRooms() {
|
|
380
|
+
return [...rooms.keys()].filter(locked).sort();
|
|
381
|
+
}
|
|
382
|
+
function relocked(before) {
|
|
383
|
+
const after = lockedRooms();
|
|
384
|
+
if (JSON.stringify(after) === JSON.stringify(before))
|
|
385
|
+
return;
|
|
386
|
+
journal.record(`locked ${after.length}`);
|
|
387
|
+
emitRooms();
|
|
388
|
+
for (const notify of [...lockListeners])
|
|
389
|
+
notify();
|
|
390
|
+
}
|
|
391
|
+
function confirmLocks(listed) {
|
|
392
|
+
if (!socketOpen)
|
|
393
|
+
return;
|
|
394
|
+
for (const room of listed) {
|
|
395
|
+
if (room.replica.admission === 'admitted')
|
|
396
|
+
confirmed.add(room.locator);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function connectionLost() {
|
|
400
|
+
if (lockGrace !== null || closed)
|
|
401
|
+
return;
|
|
402
|
+
lockGrace = setTimeout(() => {
|
|
403
|
+
lockGrace = null;
|
|
404
|
+
if (socketOpen || closed)
|
|
405
|
+
return;
|
|
406
|
+
const before = lockedRooms();
|
|
407
|
+
confirmed.clear();
|
|
408
|
+
relocked(before);
|
|
409
|
+
}, LOCK_GRACE_MS);
|
|
410
|
+
}
|
|
411
|
+
function connectionLive() {
|
|
412
|
+
if (lockGrace !== null)
|
|
413
|
+
clearTimeout(lockGrace);
|
|
414
|
+
lockGrace = null;
|
|
415
|
+
}
|
|
416
|
+
function watchLocks(notify) {
|
|
417
|
+
lockListeners.add(notify);
|
|
418
|
+
return () => {
|
|
419
|
+
lockListeners.delete(notify);
|
|
420
|
+
};
|
|
421
|
+
}
|
|
347
422
|
function roomFacts(reach = null) {
|
|
348
423
|
const held = [...rooms.values()]
|
|
349
424
|
.filter((room) => holds(reach, room.locator))
|
|
@@ -362,7 +437,7 @@ export function composeRuntime(options) {
|
|
|
362
437
|
return { listed: roomsListed, rooms: held };
|
|
363
438
|
}
|
|
364
439
|
function emitRooms() {
|
|
365
|
-
|
|
440
|
+
liveQueries.recover();
|
|
366
441
|
queueReadiness();
|
|
367
442
|
const value = listed();
|
|
368
443
|
for (const listener of roomListeners)
|
|
@@ -401,17 +476,17 @@ export function composeRuntime(options) {
|
|
|
401
476
|
for (const listener of statusListeners)
|
|
402
477
|
listener(shown);
|
|
403
478
|
}
|
|
404
|
-
function entryOf(batch) {
|
|
479
|
+
function entryOf(locator, batch) {
|
|
405
480
|
if (localSaves)
|
|
406
481
|
return Object.freeze({ batch, state: 'local' });
|
|
407
|
-
const state = pendings.get(batch.id);
|
|
482
|
+
const state = pendings.get(pendingKey(locator, batch.id));
|
|
408
483
|
if (state?.accepted !== undefined) {
|
|
409
484
|
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
410
485
|
}
|
|
411
486
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
412
487
|
}
|
|
413
488
|
function entriesOf(locator) {
|
|
414
|
-
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
|
|
489
|
+
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map((batch) => entryOf(locator, batch));
|
|
415
490
|
}
|
|
416
491
|
function emitQueue(locator) {
|
|
417
492
|
const entries = entriesOf(locator);
|
|
@@ -421,66 +496,35 @@ export function composeRuntime(options) {
|
|
|
421
496
|
}
|
|
422
497
|
}
|
|
423
498
|
function applyChangeSet(set) {
|
|
424
|
-
|
|
425
|
-
...set,
|
|
426
|
-
documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
|
|
427
|
-
};
|
|
428
|
-
const applied = readSide.apply(held, () => searchIndex.changed(held));
|
|
429
|
-
if (applied instanceof Promise)
|
|
430
|
-
void applied.catch(unpublished);
|
|
499
|
+
liveQueries.touched(set);
|
|
431
500
|
}
|
|
432
501
|
function unpublished(error) {
|
|
433
502
|
journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
|
|
434
503
|
}
|
|
435
|
-
async function referenced(locator) {
|
|
436
|
-
for (;;) {
|
|
437
|
-
const record = replicas.get(locator)?.confirmed.record;
|
|
438
|
-
if (record === undefined || closed)
|
|
439
|
-
return;
|
|
440
|
-
const held = { text: record.documents, binaries: record.binaries };
|
|
441
|
-
const composition = compositionOf(record);
|
|
442
|
-
if (referencesIndexed(held, composition))
|
|
443
|
-
return;
|
|
444
|
-
await indexReferrers(held, composition);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
504
|
async function prepareReplica(locator) {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
return;
|
|
452
|
-
const visible = replica.visible;
|
|
453
|
-
const paths = [
|
|
454
|
-
...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
|
|
455
|
-
];
|
|
456
|
-
const entered = [];
|
|
457
|
-
projecting.add(locator);
|
|
458
|
-
try {
|
|
459
|
-
await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
|
|
460
|
-
const current = replicas.get(locator);
|
|
461
|
-
if (current === undefined || closed)
|
|
462
|
-
return;
|
|
463
|
-
const set = entering(locator, current, chunk);
|
|
464
|
-
entered.push(set);
|
|
465
|
-
return readSide.apply(set);
|
|
466
|
-
});
|
|
467
|
-
if (!replicas.has(locator) || closed)
|
|
505
|
+
for (;;) {
|
|
506
|
+
const held = replicas.get(locator);
|
|
507
|
+
if (held === undefined || closed || prepared.has(locator))
|
|
468
508
|
return;
|
|
509
|
+
const key = held.record.key;
|
|
510
|
+
const done = await exclusive(async () => (await openLocal()).rederive(key, REDERIVE_BATCH));
|
|
511
|
+
if (replicas.get(locator)?.record.key !== key)
|
|
512
|
+
continue;
|
|
513
|
+
if (!done)
|
|
514
|
+
continue;
|
|
469
515
|
prepared.add(locator);
|
|
516
|
+
publishChangeSet(changeSet(locator, { documents: null }));
|
|
517
|
+
liveQueries.recover();
|
|
518
|
+
queueReadiness();
|
|
519
|
+
return;
|
|
470
520
|
}
|
|
471
|
-
finally {
|
|
472
|
-
projecting.delete(locator);
|
|
473
|
-
}
|
|
474
|
-
conversations.changed(mergeChangeSets(entered));
|
|
475
|
-
readSide.recover();
|
|
476
|
-
queueReadiness();
|
|
477
521
|
}
|
|
478
522
|
function unprepared() {
|
|
479
|
-
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !
|
|
523
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unpreparable.has(locator));
|
|
480
524
|
}
|
|
481
|
-
function
|
|
482
|
-
|
|
483
|
-
journal.record(`
|
|
525
|
+
function unpreparedBy(locator, error) {
|
|
526
|
+
unpreparable.add(locator);
|
|
527
|
+
journal.record(`preparation failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
|
|
484
528
|
queueReadiness();
|
|
485
529
|
}
|
|
486
530
|
function prepare(locators = []) {
|
|
@@ -496,7 +540,7 @@ export function composeRuntime(options) {
|
|
|
496
540
|
if (closed)
|
|
497
541
|
return;
|
|
498
542
|
unpreparedDemand.delete(waiting);
|
|
499
|
-
await prepareReplica(waiting).catch((error) =>
|
|
543
|
+
await prepareReplica(waiting).catch((error) => unpreparedBy(waiting, error));
|
|
500
544
|
}
|
|
501
545
|
}
|
|
502
546
|
finally {
|
|
@@ -504,14 +548,18 @@ export function composeRuntime(options) {
|
|
|
504
548
|
}
|
|
505
549
|
})();
|
|
506
550
|
}
|
|
551
|
+
function roomsOfReach(reach) {
|
|
552
|
+
return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
553
|
+
}
|
|
507
554
|
function reachedRooms(plan) {
|
|
508
|
-
const
|
|
509
|
-
const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
555
|
+
const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
|
|
510
556
|
const named = plan.rooms ??
|
|
511
557
|
(reached === null
|
|
512
558
|
? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
|
|
513
559
|
: [...reached]);
|
|
514
|
-
return [...new Set(named)]
|
|
560
|
+
return [...new Set(named)]
|
|
561
|
+
.filter((locator) => (reached === null || reached.has(locator)) && !plan.locked.includes(locator))
|
|
562
|
+
.sort();
|
|
515
563
|
}
|
|
516
564
|
function awaited(locator) {
|
|
517
565
|
return (!roomsListed ||
|
|
@@ -525,6 +573,7 @@ export function composeRuntime(options) {
|
|
|
525
573
|
const pending = [];
|
|
526
574
|
const coverage = [];
|
|
527
575
|
const overlay = [];
|
|
576
|
+
const read = [];
|
|
528
577
|
for (const locator of locators) {
|
|
529
578
|
const replica = replicas.get(locator);
|
|
530
579
|
if (replica === undefined) {
|
|
@@ -538,15 +587,16 @@ export function composeRuntime(options) {
|
|
|
538
587
|
}
|
|
539
588
|
coverage.push({
|
|
540
589
|
room: locator,
|
|
541
|
-
generation: replica.
|
|
590
|
+
generation: replica.record.transfer,
|
|
542
591
|
current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
|
|
543
592
|
});
|
|
593
|
+
read.push({ locator, key: replica.record.key });
|
|
544
594
|
for (const batch of replica.queue)
|
|
545
595
|
overlay.push(batch.id);
|
|
546
596
|
}
|
|
547
597
|
if (pending.length > 0)
|
|
548
598
|
return { pending };
|
|
549
|
-
return { coverage: { rooms: coverage, indexes: [], overlay } };
|
|
599
|
+
return { coverage: { rooms: coverage, indexes: [], overlay }, rooms: read };
|
|
550
600
|
}
|
|
551
601
|
function awaitPreparation(cover) {
|
|
552
602
|
if (!('pending' in cover))
|
|
@@ -555,15 +605,16 @@ export function composeRuntime(options) {
|
|
|
555
605
|
if (waiting.length > 0)
|
|
556
606
|
prepare(waiting.map(({ room }) => room));
|
|
557
607
|
}
|
|
558
|
-
function requiredRooms(
|
|
608
|
+
function requiredRooms(given) {
|
|
609
|
+
const reach = planReach(given);
|
|
559
610
|
if (reach === null)
|
|
560
611
|
return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
|
|
561
|
-
const named = [...
|
|
612
|
+
const named = [...roomsOfReach(reach)].sort();
|
|
562
613
|
if (roomsListed)
|
|
563
614
|
return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
|
|
564
615
|
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
565
616
|
}
|
|
566
|
-
function roomState(locator
|
|
617
|
+
function roomState(locator) {
|
|
567
618
|
if (sourceRefusals.has(locator))
|
|
568
619
|
return 'refused';
|
|
569
620
|
const listedRoom = discovered.get(locator);
|
|
@@ -576,25 +627,15 @@ export function composeRuntime(options) {
|
|
|
576
627
|
return 'acquiring';
|
|
577
628
|
if (!prepared.has(locator))
|
|
578
629
|
return 'preparing';
|
|
579
|
-
if (search && !searchIndex.rooms().has(locator))
|
|
580
|
-
return 'indexing';
|
|
581
630
|
return 'complete';
|
|
582
631
|
}
|
|
583
|
-
function conversationCoverage(reach
|
|
632
|
+
function conversationCoverage(reach) {
|
|
584
633
|
const declared = reach === null || reach.conversations;
|
|
585
634
|
if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
|
|
586
635
|
return 'undeclared';
|
|
587
636
|
if (!conversations.known() || !conversations.current())
|
|
588
637
|
return 'acquiring';
|
|
589
|
-
|
|
590
|
-
return 'complete';
|
|
591
|
-
const unindexed = conversations
|
|
592
|
-
.searchable()
|
|
593
|
-
.filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
|
|
594
|
-
if (unindexed.length === 0)
|
|
595
|
-
return 'complete';
|
|
596
|
-
index([], unindexed);
|
|
597
|
-
return 'indexing';
|
|
638
|
+
return 'complete';
|
|
598
639
|
}
|
|
599
640
|
function connectionOf() {
|
|
600
641
|
if (socketOpen)
|
|
@@ -624,22 +665,18 @@ export function composeRuntime(options) {
|
|
|
624
665
|
function readinessFor(port, reach) {
|
|
625
666
|
const required = requiredRooms(reach);
|
|
626
667
|
const search = port.search === 'required';
|
|
627
|
-
const coverage = required.rooms.map((room) => ({ room, state: roomState(room
|
|
628
|
-
const
|
|
629
|
-
.filter((room) => room.state ===
|
|
668
|
+
const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
|
|
669
|
+
const preparing = coverage
|
|
670
|
+
.filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
|
|
630
671
|
.map(({ room }) => room);
|
|
631
|
-
const preparing = building('preparing');
|
|
632
672
|
if (preparing.length > 0)
|
|
633
673
|
prepare(preparing);
|
|
634
|
-
const indexing = building('indexing');
|
|
635
|
-
if (indexing.length > 0)
|
|
636
|
-
index(indexing, []);
|
|
637
674
|
return readinessOf({
|
|
638
675
|
code: port.code,
|
|
639
676
|
coverage: {
|
|
640
677
|
listed: required.listed,
|
|
641
678
|
rooms: coverage,
|
|
642
|
-
conversations: conversationCoverage(reach
|
|
679
|
+
conversations: conversationCoverage(reach),
|
|
643
680
|
search: search ? 'required' : 'unused',
|
|
644
681
|
},
|
|
645
682
|
persistence,
|
|
@@ -657,7 +694,7 @@ export function composeRuntime(options) {
|
|
|
657
694
|
function establish(name) {
|
|
658
695
|
targets = { ...targets, [name]: { established: true } };
|
|
659
696
|
queueReadiness();
|
|
660
|
-
void durably((local) => local.
|
|
697
|
+
void durably((local) => local.fact('targets', targets)).catch(() => {
|
|
661
698
|
journal.record(`target record failed ${name}`);
|
|
662
699
|
});
|
|
663
700
|
}
|
|
@@ -694,18 +731,12 @@ export function composeRuntime(options) {
|
|
|
694
731
|
readinessWatches.delete(watch);
|
|
695
732
|
};
|
|
696
733
|
}
|
|
697
|
-
function
|
|
734
|
+
function pageBytes(page) {
|
|
698
735
|
let bytes = 0;
|
|
699
|
-
for (const
|
|
700
|
-
bytes += (path.length +
|
|
736
|
+
for (const { path, content } of page.entries)
|
|
737
|
+
bytes += (path.length + (content?.length ?? 0)) * 2;
|
|
701
738
|
return bytes;
|
|
702
739
|
}
|
|
703
|
-
function grownBy(next, previous) {
|
|
704
|
-
if (previous === undefined)
|
|
705
|
-
return layerBytes(next);
|
|
706
|
-
const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
|
|
707
|
-
return layerBytes({ ...next, documents });
|
|
708
|
-
}
|
|
709
740
|
function exceeded(error) {
|
|
710
741
|
return (typeof error === 'object' &&
|
|
711
742
|
error !== null &&
|
|
@@ -713,7 +744,7 @@ export function composeRuntime(options) {
|
|
|
713
744
|
}
|
|
714
745
|
async function estimated() {
|
|
715
746
|
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
716
|
-
return estimate === null ? null : Math.max(0, estimate.
|
|
747
|
+
return estimate === null ? null : Math.max(0, estimate.total - estimate.usage);
|
|
717
748
|
}
|
|
718
749
|
function pause(locator, cause) {
|
|
719
750
|
paused.add(locator);
|
|
@@ -734,7 +765,8 @@ export function composeRuntime(options) {
|
|
|
734
765
|
const available = await estimated().catch(() => null);
|
|
735
766
|
if (available === null || available < (held.needed ?? 0))
|
|
736
767
|
return;
|
|
737
|
-
if (held.cause === '
|
|
768
|
+
if (held.cause === 'storage-exceeded' &&
|
|
769
|
+
(held.available === null || available <= held.available))
|
|
738
770
|
return;
|
|
739
771
|
resume();
|
|
740
772
|
}
|
|
@@ -776,7 +808,7 @@ export function composeRuntime(options) {
|
|
|
776
808
|
}
|
|
777
809
|
async function exceededOn(locator, bytes) {
|
|
778
810
|
const available = await estimated().catch(() => null);
|
|
779
|
-
pause(locator, { cause: '
|
|
811
|
+
pause(locator, { cause: 'storage-exceeded', needed: bytes, available });
|
|
780
812
|
}
|
|
781
813
|
function seedRoomHeld(room, version) {
|
|
782
814
|
return prepared.has(room) && reachedBy(room, version);
|
|
@@ -813,6 +845,75 @@ export function composeRuntime(options) {
|
|
|
813
845
|
shown();
|
|
814
846
|
}
|
|
815
847
|
}
|
|
848
|
+
function sameCover(left, right) {
|
|
849
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
850
|
+
}
|
|
851
|
+
async function coveredRows(plan, cover) {
|
|
852
|
+
for (;;) {
|
|
853
|
+
const before = cover();
|
|
854
|
+
if ('pending' in before)
|
|
855
|
+
return { pending: before };
|
|
856
|
+
const local = await openLocal();
|
|
857
|
+
const keys = before.rooms.map(({ key }) => key);
|
|
858
|
+
const changes = local.changes(keys);
|
|
859
|
+
const current = () => local.changes(keys) === changes;
|
|
860
|
+
const rows = await windowRows(local, plan, before.rooms, current);
|
|
861
|
+
const after = cover();
|
|
862
|
+
if (rows !== null && sameCover(before, after))
|
|
863
|
+
return { rows, cover: before, current };
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
async function windowRows(local, plan, rooms, unchanged) {
|
|
867
|
+
const first = await local.query(plan, rooms, QUERY_BYTES);
|
|
868
|
+
if (first.more === undefined || !('documents' in first)) {
|
|
869
|
+
const { more: _more, ...rows } = first;
|
|
870
|
+
return rows;
|
|
871
|
+
}
|
|
872
|
+
const documents = [...first.documents];
|
|
873
|
+
for (let more = first.more; more !== undefined;) {
|
|
874
|
+
if (!unchanged())
|
|
875
|
+
return null;
|
|
876
|
+
const page = await local.query({
|
|
877
|
+
...plan,
|
|
878
|
+
after: { text: more, values: JSON.parse(more) },
|
|
879
|
+
limit: plan.limit === null ? null : plan.limit - documents.length,
|
|
880
|
+
total: false,
|
|
881
|
+
}, rooms, QUERY_BYTES);
|
|
882
|
+
if (!('documents' in page))
|
|
883
|
+
return null;
|
|
884
|
+
documents.push(...page.documents);
|
|
885
|
+
more = page.more;
|
|
886
|
+
if (more === undefined) {
|
|
887
|
+
return {
|
|
888
|
+
documents,
|
|
889
|
+
...(page.next === undefined ? {} : { next: page.next }),
|
|
890
|
+
...(first.total === undefined ? {} : { total: first.total }),
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
function keyedOf(rows, coverage) {
|
|
897
|
+
if ('groups' in rows) {
|
|
898
|
+
return {
|
|
899
|
+
result: { ...rows, coverage },
|
|
900
|
+
keys: rows.groups.map((group) => JSON.stringify(group.key)),
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
return {
|
|
904
|
+
result: { ...rows, coverage },
|
|
905
|
+
keys: rows.documents.map((document) => documentKey(document.room, document.path)),
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
async function answeredPlan(plan, cover) {
|
|
909
|
+
const answered = await coveredRows(plan, cover);
|
|
910
|
+
if ('pending' in answered) {
|
|
911
|
+
const pending = answered.pending;
|
|
912
|
+
return { result: pending, keys: [], current: () => true };
|
|
913
|
+
}
|
|
914
|
+
const held = answered.cover;
|
|
915
|
+
return { ...keyedOf(answered.rows, held.coverage), current: answered.current };
|
|
916
|
+
}
|
|
816
917
|
async function rowsQuery(plan) {
|
|
817
918
|
const before = covered(plan);
|
|
818
919
|
awaitPreparation(before);
|
|
@@ -821,59 +922,15 @@ export function composeRuntime(options) {
|
|
|
821
922
|
return seededResult(seeded).result;
|
|
822
923
|
if ('pending' in before)
|
|
823
924
|
return before;
|
|
824
|
-
const
|
|
825
|
-
|
|
826
|
-
|
|
925
|
+
const answered = await coveredRows(plan, () => {
|
|
926
|
+
const cover = covered(plan);
|
|
927
|
+
awaitPreparation(cover);
|
|
827
928
|
return cover;
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
for (;;) {
|
|
834
|
-
const batch = await durably((local) => local.messagesAfter(conversation, after));
|
|
835
|
-
if (closed)
|
|
836
|
-
return;
|
|
837
|
-
searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
|
|
838
|
-
if (batch.length === 0)
|
|
839
|
-
return;
|
|
840
|
-
after = batch[batch.length - 1].identity;
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
function indexNext() {
|
|
844
|
-
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
|
|
845
|
-
if (room !== undefined) {
|
|
846
|
-
roomsToIndex.delete(room);
|
|
847
|
-
return () => prepared.has(room)
|
|
848
|
-
? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
|
|
849
|
-
: undefined;
|
|
850
|
-
}
|
|
851
|
-
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
852
|
-
if (conversation === undefined)
|
|
853
|
-
return null;
|
|
854
|
-
conversationsToIndex.delete(conversation);
|
|
855
|
-
return () => indexConversation(conversation);
|
|
856
|
-
}
|
|
857
|
-
function index(rooms, named) {
|
|
858
|
-
for (const room of rooms)
|
|
859
|
-
roomsToIndex.add(room);
|
|
860
|
-
for (const conversation of named)
|
|
861
|
-
conversationsToIndex.add(conversation);
|
|
862
|
-
if (indexing)
|
|
863
|
-
return;
|
|
864
|
-
indexing = true;
|
|
865
|
-
void (async () => {
|
|
866
|
-
try {
|
|
867
|
-
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
868
|
-
await yielded();
|
|
869
|
-
await unit();
|
|
870
|
-
queueReadiness();
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
finally {
|
|
874
|
-
indexing = false;
|
|
875
|
-
}
|
|
876
|
-
})().catch(() => { });
|
|
929
|
+
});
|
|
930
|
+
if ('pending' in answered)
|
|
931
|
+
return answered.pending;
|
|
932
|
+
const held = answered.cover;
|
|
933
|
+
return { ...answered.rows, coverage: held.coverage };
|
|
877
934
|
}
|
|
878
935
|
function publishChangeSet(set) {
|
|
879
936
|
applyChangeSet(set);
|
|
@@ -883,7 +940,10 @@ export function composeRuntime(options) {
|
|
|
883
940
|
for (const locator of set.queues)
|
|
884
941
|
emitQueue(locator);
|
|
885
942
|
if (set.drafts.length > 0 ||
|
|
886
|
-
set.documents.some((change) =>
|
|
943
|
+
set.documents.some((change) => {
|
|
944
|
+
const held = roomDrafts.get(change.locator);
|
|
945
|
+
return held !== undefined && (change.path === null || held.has(change.path));
|
|
946
|
+
})) {
|
|
887
947
|
publishRoomDrafts(false);
|
|
888
948
|
}
|
|
889
949
|
for (const app of set.presentations)
|
|
@@ -896,9 +956,21 @@ export function composeRuntime(options) {
|
|
|
896
956
|
if (reason === 'revoked-authorization') {
|
|
897
957
|
await exclusive(async () => {
|
|
898
958
|
const revoked = { status: 'unavailable', reason: 'revoked' };
|
|
959
|
+
const records = [...replicas].map(([locator, held]) => ({
|
|
960
|
+
locator,
|
|
961
|
+
held,
|
|
962
|
+
record: { ...held.record, model: revoked, selectionModel: revoked },
|
|
963
|
+
}));
|
|
899
964
|
try {
|
|
900
965
|
const local = await openLocal();
|
|
901
|
-
await local.
|
|
966
|
+
await local.commit({
|
|
967
|
+
rooms: records.map(({ locator, record }) => ({
|
|
968
|
+
locator,
|
|
969
|
+
key: record.key,
|
|
970
|
+
record: storedRecord(record),
|
|
971
|
+
composition: compositionOf(record),
|
|
972
|
+
})),
|
|
973
|
+
});
|
|
902
974
|
}
|
|
903
975
|
catch {
|
|
904
976
|
journal.record('model revocation persistence failed');
|
|
@@ -914,16 +986,16 @@ export function composeRuntime(options) {
|
|
|
914
986
|
liveModels.delete(key);
|
|
915
987
|
}
|
|
916
988
|
const changed = [];
|
|
917
|
-
for (const
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
989
|
+
for (const { locator, held, record } of records) {
|
|
990
|
+
if (replicas.get(locator) !== held)
|
|
991
|
+
continue;
|
|
992
|
+
recomposed(locator, held.record, record);
|
|
993
|
+
replicas.set(locator, { ...held, record });
|
|
994
|
+
}
|
|
995
|
+
for (const locator of [...rooms.keys()]) {
|
|
996
|
+
const room = rooms.get(locator);
|
|
997
|
+
if (room === undefined)
|
|
998
|
+
continue;
|
|
927
999
|
rooms.set(locator, {
|
|
928
1000
|
...room,
|
|
929
1001
|
model: modelTypes(revoked),
|
|
@@ -931,9 +1003,10 @@ export function composeRuntime(options) {
|
|
|
931
1003
|
? null
|
|
932
1004
|
: { ...room.typedSelection, model: modelTypes(revoked) },
|
|
933
1005
|
});
|
|
934
|
-
changed.push(changeSet(locator,
|
|
1006
|
+
changed.push(changeSet(locator, { documents: null, room: true }));
|
|
935
1007
|
}
|
|
936
1008
|
publishChangeSet(mergeChangeSets(changed));
|
|
1009
|
+
prepare();
|
|
937
1010
|
});
|
|
938
1011
|
}
|
|
939
1012
|
}
|
|
@@ -964,12 +1037,13 @@ export function composeRuntime(options) {
|
|
|
964
1037
|
model: modelTypes(modelOf(room.locator, 'typed-selection')),
|
|
965
1038
|
},
|
|
966
1039
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
967
|
-
roomVersion: replicas.get(room.locator)?.
|
|
1040
|
+
roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
|
|
968
1041
|
synchronizationAvailable,
|
|
969
1042
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
970
1043
|
});
|
|
971
1044
|
}
|
|
972
1045
|
async function applyDiscovery(found, started, covered) {
|
|
1046
|
+
const lockedBefore = lockedRooms();
|
|
973
1047
|
for (const room of found) {
|
|
974
1048
|
const fence = fences.get(room.locator);
|
|
975
1049
|
if (fence !== undefined && fence.erased && fence.at < started)
|
|
@@ -995,15 +1069,17 @@ export function composeRuntime(options) {
|
|
|
995
1069
|
else
|
|
996
1070
|
unlistedKept(unlisted);
|
|
997
1071
|
for (const room of listed) {
|
|
998
|
-
const held = replicas.get(room.locator)?.
|
|
1072
|
+
const held = replicas.get(room.locator)?.record;
|
|
999
1073
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
1000
1074
|
list(held, false);
|
|
1001
1075
|
continue;
|
|
1002
1076
|
}
|
|
1003
1077
|
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
1004
1078
|
}
|
|
1079
|
+
confirmLocks(listed);
|
|
1005
1080
|
roomsListed = true;
|
|
1006
1081
|
emitRooms();
|
|
1082
|
+
relocked(lockedBefore);
|
|
1007
1083
|
return listed;
|
|
1008
1084
|
}
|
|
1009
1085
|
function heldLocally(locator) {
|
|
@@ -1024,7 +1100,7 @@ export function composeRuntime(options) {
|
|
|
1024
1100
|
}
|
|
1025
1101
|
function unlistedKept(unlisted) {
|
|
1026
1102
|
for (const locator of unlisted) {
|
|
1027
|
-
const held = replicas.get(locator)?.
|
|
1103
|
+
const held = replicas.get(locator)?.record;
|
|
1028
1104
|
if (held !== undefined)
|
|
1029
1105
|
list(held, false);
|
|
1030
1106
|
else
|
|
@@ -1033,16 +1109,16 @@ export function composeRuntime(options) {
|
|
|
1033
1109
|
}
|
|
1034
1110
|
function fence(locators) {
|
|
1035
1111
|
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
1036
|
-
const removed = fresh.map((locator) =>
|
|
1037
|
-
|
|
1038
|
-
queue: true,
|
|
1112
|
+
const removed = fresh.map((locator) => ({
|
|
1113
|
+
locator,
|
|
1039
1114
|
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
1040
1115
|
}));
|
|
1041
1116
|
for (const locator of fresh) {
|
|
1042
1117
|
fences.set(locator, { at: discoveries, erased: false });
|
|
1043
1118
|
lastFenced.set(locator, ++fencings);
|
|
1119
|
+
confirmed.delete(locator);
|
|
1044
1120
|
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
1045
|
-
pendings.delete(batch.id);
|
|
1121
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
1046
1122
|
replicas.delete(locator);
|
|
1047
1123
|
queuesWithoutReplica.delete(locator);
|
|
1048
1124
|
rooms.delete(locator);
|
|
@@ -1054,8 +1130,9 @@ export function composeRuntime(options) {
|
|
|
1054
1130
|
unreadable.delete(locator);
|
|
1055
1131
|
stale.delete(locator);
|
|
1056
1132
|
repairable.delete(locator);
|
|
1057
|
-
|
|
1133
|
+
unpreparable.delete(locator);
|
|
1058
1134
|
paused.delete(locator);
|
|
1135
|
+
prepared.delete(locator);
|
|
1059
1136
|
scopedRefusals.delete(locator);
|
|
1060
1137
|
sourceRefusals.delete(locator);
|
|
1061
1138
|
declarationRefusals.delete(locator);
|
|
@@ -1069,26 +1146,48 @@ export function composeRuntime(options) {
|
|
|
1069
1146
|
}
|
|
1070
1147
|
if (fresh.length === 0)
|
|
1071
1148
|
return fresh;
|
|
1072
|
-
publishChangeSet(mergeChangeSets(removed));
|
|
1073
|
-
|
|
1074
|
-
prepared.delete(locator);
|
|
1075
|
-
searchIndex.forget(fresh);
|
|
1076
|
-
readSide.recover();
|
|
1149
|
+
publishChangeSet(mergeChangeSets(removed.map(({ locator, drafts }) => changeSet(locator, { documents: null, room: true, queue: true, drafts }))));
|
|
1150
|
+
liveQueries.recover();
|
|
1077
1151
|
moveTo({ ...status, ...attention() });
|
|
1078
1152
|
return fresh;
|
|
1079
1153
|
}
|
|
1080
|
-
async function
|
|
1081
|
-
await local.erase(locators);
|
|
1154
|
+
async function eraseFenced(locators) {
|
|
1082
1155
|
for (const locator of locators) {
|
|
1156
|
+
while (await durably((local) => local.erase(locator, ERASE_BATCH)))
|
|
1157
|
+
await yielded();
|
|
1083
1158
|
const held = fences.get(locator);
|
|
1084
1159
|
if (held !== undefined)
|
|
1085
1160
|
held.erased = true;
|
|
1086
1161
|
}
|
|
1087
1162
|
}
|
|
1163
|
+
let sweeping = false;
|
|
1164
|
+
function sweep() {
|
|
1165
|
+
if (sweeping || closed)
|
|
1166
|
+
return;
|
|
1167
|
+
sweeping = true;
|
|
1168
|
+
void (async () => {
|
|
1169
|
+
try {
|
|
1170
|
+
while (!closed && (await durably((local) => local.erase(null, ERASE_BATCH)))) {
|
|
1171
|
+
await yielded();
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
catch {
|
|
1175
|
+
journal.record('erasure failed');
|
|
1176
|
+
}
|
|
1177
|
+
finally {
|
|
1178
|
+
sweeping = false;
|
|
1179
|
+
}
|
|
1180
|
+
})();
|
|
1181
|
+
}
|
|
1088
1182
|
async function loseRooms(locators) {
|
|
1089
1183
|
const fresh = fence(locators);
|
|
1090
|
-
if (fresh.length
|
|
1091
|
-
|
|
1184
|
+
if (fresh.length === 0)
|
|
1185
|
+
return;
|
|
1186
|
+
await durably(async (local) => {
|
|
1187
|
+
await local.fence(fresh);
|
|
1188
|
+
await unpair(local);
|
|
1189
|
+
});
|
|
1190
|
+
await eraseFenced(fresh);
|
|
1092
1191
|
}
|
|
1093
1192
|
function restored(key, persisted) {
|
|
1094
1193
|
const prior = models.get(key);
|
|
@@ -1109,54 +1208,58 @@ export function composeRuntime(options) {
|
|
|
1109
1208
|
return model;
|
|
1110
1209
|
}
|
|
1111
1210
|
async function hydrate(opened) {
|
|
1112
|
-
const [
|
|
1113
|
-
opened.
|
|
1211
|
+
const [held, queues, kept, heldConversations, chatDrafts] = await Promise.all([
|
|
1212
|
+
opened.opened(),
|
|
1114
1213
|
opened.queues(),
|
|
1115
1214
|
opened.roomDrafts(),
|
|
1116
|
-
opened.presentation(),
|
|
1117
1215
|
opened.conversations(),
|
|
1118
1216
|
opened.chatDrafts(),
|
|
1119
|
-
opened.lastCompleted(),
|
|
1120
|
-
opened.providerLimits(),
|
|
1121
|
-
opened.capabilities(),
|
|
1122
|
-
opened.member(),
|
|
1123
|
-
opened.transfers(),
|
|
1124
|
-
opened.targets(),
|
|
1125
1217
|
]);
|
|
1126
|
-
|
|
1218
|
+
const records = [...held.rooms.values()];
|
|
1219
|
+
const counts = await opened.counts(records.map(({ key, typedSelection }) => ({ key, roots: typedSelection?.roots ?? [] })));
|
|
1220
|
+
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1221
|
+
targets = held.targets;
|
|
1127
1222
|
transfers.clear();
|
|
1128
|
-
for (const record of
|
|
1129
|
-
transfers.set(
|
|
1130
|
-
limits =
|
|
1131
|
-
capabilities =
|
|
1132
|
-
member =
|
|
1223
|
+
for (const [locator, record] of held.transfers)
|
|
1224
|
+
transfers.set(locator, record);
|
|
1225
|
+
limits = held.providerLimits;
|
|
1226
|
+
capabilities = held.capabilities;
|
|
1227
|
+
member = held.member;
|
|
1133
1228
|
roomDrafts.clear();
|
|
1134
1229
|
for (const record of kept)
|
|
1135
1230
|
rememberRoomDraft(record);
|
|
1136
1231
|
presentations.clear();
|
|
1137
|
-
for (const record of
|
|
1232
|
+
for (const record of held.presentation)
|
|
1138
1233
|
presentations.set(record.app, record.value);
|
|
1139
|
-
conversations.hydrate(
|
|
1140
|
-
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1234
|
+
conversations.hydrate(heldConversations, chatDrafts);
|
|
1141
1235
|
pendings.clear();
|
|
1142
1236
|
for (const queue of queues) {
|
|
1143
|
-
for (const { batch, state } of queue.pending)
|
|
1144
|
-
pendings.set(batch.id, state);
|
|
1237
|
+
for (const { batch, state } of queue.pending) {
|
|
1238
|
+
pendings.set(pendingKey(queue.locator, batch.id), state);
|
|
1239
|
+
}
|
|
1145
1240
|
}
|
|
1146
|
-
const previousReplicas = new Map(replicas);
|
|
1147
1241
|
replicas.clear();
|
|
1242
|
+
prepared.clear();
|
|
1148
1243
|
queuesWithoutReplica.clear();
|
|
1149
|
-
const replicated = new Set(records.map((record) => record.locator));
|
|
1150
1244
|
for (const [locator, queue] of queuedBatches) {
|
|
1151
|
-
if (!
|
|
1245
|
+
if (!held.rooms.has(locator))
|
|
1152
1246
|
queuesWithoutReplica.set(locator, queue);
|
|
1153
1247
|
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
const
|
|
1158
|
-
const
|
|
1159
|
-
|
|
1248
|
+
records.forEach((stored, index) => {
|
|
1249
|
+
if (fences.has(stored.locator))
|
|
1250
|
+
return;
|
|
1251
|
+
const model = restored(modelKey(stored.locator, 'collection'), stored.model);
|
|
1252
|
+
const selectionModel = restored(modelKey(stored.locator, 'typed-selection'), stored.selectionModel);
|
|
1253
|
+
const record = { ...stored, model, selectionModel };
|
|
1254
|
+
replicas.set(record.locator, {
|
|
1255
|
+
record,
|
|
1256
|
+
queue: queuedBatches.get(record.locator) ?? [],
|
|
1257
|
+
overlaid: held.overlaid.get(record.key) ?? new Set(),
|
|
1258
|
+
count: counts[index] ?? 0,
|
|
1259
|
+
confirmed: 0,
|
|
1260
|
+
});
|
|
1261
|
+
if (!held.unprepared.has(record.key))
|
|
1262
|
+
prepared.add(record.locator);
|
|
1160
1263
|
const existing = rooms.get(record.locator);
|
|
1161
1264
|
if (existing === undefined)
|
|
1162
1265
|
list(record, false);
|
|
@@ -1175,11 +1278,15 @@ export function composeRuntime(options) {
|
|
|
1175
1278
|
},
|
|
1176
1279
|
});
|
|
1177
1280
|
}
|
|
1178
|
-
}
|
|
1281
|
+
});
|
|
1282
|
+
for (const key of held.orphans)
|
|
1283
|
+
await opened.release(key);
|
|
1284
|
+
if (held.erasing || held.orphans.length > 0)
|
|
1285
|
+
queueMicrotask(sweep);
|
|
1179
1286
|
return {
|
|
1180
|
-
lastCompleted,
|
|
1287
|
+
lastCompleted: held.lastCompleted,
|
|
1181
1288
|
locators: new Set([...queuedBatches.keys(), ...replicas.keys()]),
|
|
1182
|
-
read: mergeChangeSets([...
|
|
1289
|
+
read: mergeChangeSets([...replicas.keys()].map((locator) => changeSet(locator, { documents: null }))),
|
|
1183
1290
|
};
|
|
1184
1291
|
}
|
|
1185
1292
|
async function readState(opened) {
|
|
@@ -1198,8 +1305,7 @@ export function composeRuntime(options) {
|
|
|
1198
1305
|
async function open() {
|
|
1199
1306
|
if (closed)
|
|
1200
1307
|
throw new Error('runtime closed');
|
|
1201
|
-
const opened =
|
|
1202
|
-
store = opened;
|
|
1308
|
+
const opened = openRuntimeStore(isFenced, options.database);
|
|
1203
1309
|
const interrupted = await opened.fences();
|
|
1204
1310
|
for (const locator of interrupted) {
|
|
1205
1311
|
fences.set(locator, { at: discoveries, erased: false });
|
|
@@ -1218,9 +1324,17 @@ export function composeRuntime(options) {
|
|
|
1218
1324
|
emitStorageLoss();
|
|
1219
1325
|
}
|
|
1220
1326
|
const lastCompleted = await readState(opened);
|
|
1221
|
-
if (interrupted.length > 0)
|
|
1222
|
-
|
|
1327
|
+
if (interrupted.length > 0) {
|
|
1328
|
+
queueMicrotask(() => {
|
|
1329
|
+
void eraseFenced(interrupted).catch(() => {
|
|
1330
|
+
journal.record('erasure failed');
|
|
1331
|
+
});
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1223
1334
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
1335
|
+
void exclusive(() => unpair(opened)).catch(() => {
|
|
1336
|
+
journal.record('unpair failed');
|
|
1337
|
+
});
|
|
1224
1338
|
return opened;
|
|
1225
1339
|
}
|
|
1226
1340
|
function durably(work) {
|
|
@@ -1241,80 +1355,272 @@ export function composeRuntime(options) {
|
|
|
1241
1355
|
mutations = next.then(() => undefined, () => undefined);
|
|
1242
1356
|
return next;
|
|
1243
1357
|
}
|
|
1244
|
-
function withdrawable(
|
|
1245
|
-
|
|
1246
|
-
|
|
1358
|
+
function withdrawable(locator) {
|
|
1359
|
+
return (batch) => {
|
|
1360
|
+
const state = pendings.get(pendingKey(locator, batch.id));
|
|
1361
|
+
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
const REWRITE_LINKS = ['field', 'body'];
|
|
1365
|
+
const CHECKED_LINKS = ['field', 'section'];
|
|
1366
|
+
async function sliceAt(record, view, queue, extra = [], moved = []) {
|
|
1367
|
+
const local = await openLocal();
|
|
1368
|
+
const needed = new Set([...touchedBy(queue), ...extra]);
|
|
1369
|
+
const movedPaths = [...new Set([...movedBy(queue, record.locator), ...moved])];
|
|
1370
|
+
if (movedPaths.length > 0) {
|
|
1371
|
+
for (const { path } of await local.referrers(record.key, view, movedPaths, REWRITE_LINKS)) {
|
|
1372
|
+
needed.add(path);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
const paths = [...needed];
|
|
1376
|
+
return stepped(slicing(await local.entries(record.key, view, paths), paths));
|
|
1247
1377
|
}
|
|
1248
|
-
async function
|
|
1378
|
+
async function landedWhole(locator, batch, confirmedLayer, current) {
|
|
1379
|
+
if (!landed(batch, confirmedLayer, current))
|
|
1380
|
+
return false;
|
|
1381
|
+
const partner = partnerOf(batch, locator);
|
|
1382
|
+
if (partner === null)
|
|
1383
|
+
return true;
|
|
1384
|
+
const other = replicas.get(partner);
|
|
1385
|
+
const half = other?.queue.find((one) => one.id === batch.id);
|
|
1386
|
+
if (other === undefined || half === undefined)
|
|
1387
|
+
return false;
|
|
1388
|
+
return landed(half, await sliceAt(other.record, 'confirmed', [half]));
|
|
1389
|
+
}
|
|
1390
|
+
function carriedBack(locator, unlanded, written) {
|
|
1391
|
+
const back = new Map();
|
|
1392
|
+
const taken = [];
|
|
1393
|
+
for (const batch of unlanded) {
|
|
1394
|
+
const between = batch.between;
|
|
1395
|
+
if (between === undefined)
|
|
1396
|
+
continue;
|
|
1397
|
+
const source = between.source.room;
|
|
1398
|
+
const destination = between.destination.room;
|
|
1399
|
+
for (const { path: to } of pendings.get(pendingKey(destination, batch.id))?.drafts ?? []) {
|
|
1400
|
+
const record = roomDrafts.get(destination)?.get(to);
|
|
1401
|
+
const path = between.moves.find((move) => move.to === to)?.path;
|
|
1402
|
+
if (record === undefined || path === undefined)
|
|
1403
|
+
continue;
|
|
1404
|
+
const [into, key] = source === locator ? [written, path] : [back, documentKey(source, path)];
|
|
1405
|
+
const existing = into.get(key) ?? roomDrafts.get(source)?.get(path);
|
|
1406
|
+
taken.push({ locator: destination, path: to });
|
|
1407
|
+
into.set(key, {
|
|
1408
|
+
locator: source,
|
|
1409
|
+
path,
|
|
1410
|
+
revision: roomDraftRevision(),
|
|
1411
|
+
parts: returnedInto(existing?.parts, record.parts),
|
|
1412
|
+
returned: existing?.returned ?? null,
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
const records = [...back.values()];
|
|
1417
|
+
const elsewhere = taken.filter((one) => one.locator !== locator);
|
|
1418
|
+
const rooms = new Set([...records, ...elsewhere].map((one) => one.locator));
|
|
1419
|
+
return {
|
|
1420
|
+
taken: taken.filter((one) => one.locator === locator).map(({ path }) => path),
|
|
1421
|
+
writes: [...rooms].map((room) => ({
|
|
1422
|
+
locator: room,
|
|
1423
|
+
roomDrafts: records.filter((record) => record.locator === room),
|
|
1424
|
+
consumedRoomDrafts: elsewhere.filter((one) => one.locator === room).map(({ path }) => path),
|
|
1425
|
+
})),
|
|
1426
|
+
changes: [...rooms].map((room) => changeSet(room, {
|
|
1427
|
+
drafts: [...records, ...elsewhere]
|
|
1428
|
+
.filter((one) => one.locator === room)
|
|
1429
|
+
.map(({ path }) => path),
|
|
1430
|
+
})),
|
|
1431
|
+
remember() {
|
|
1432
|
+
for (const one of taken)
|
|
1433
|
+
forgetRoomDraft(one.locator, one.path);
|
|
1434
|
+
for (const record of records)
|
|
1435
|
+
rememberRoomDraft(record);
|
|
1436
|
+
},
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
function recomposed(locator, before, after) {
|
|
1440
|
+
if (before !== undefined &&
|
|
1441
|
+
before.key === after.key &&
|
|
1442
|
+
fingerprint(compositionOf(before)) === fingerprint(compositionOf(after))) {
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
if (before !== undefined && before.key === after.key)
|
|
1446
|
+
prepared.delete(locator);
|
|
1447
|
+
}
|
|
1448
|
+
function committedRoom(locator, record, answer) {
|
|
1449
|
+
if (answer.prepared[locator] === false)
|
|
1450
|
+
prepared.delete(locator);
|
|
1451
|
+
else if (answer.prepared[locator] === true)
|
|
1452
|
+
prepared.add(locator);
|
|
1453
|
+
if (!prepared.has(locator))
|
|
1454
|
+
prepare([locator]);
|
|
1455
|
+
return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0 };
|
|
1456
|
+
}
|
|
1457
|
+
function replayed(record, confirmedLayer, queue) {
|
|
1458
|
+
return stepped(replaying(confirmedLayer, queue, record.locator, compositionOf(record)));
|
|
1459
|
+
}
|
|
1460
|
+
async function overlayDiff(record, before, visible, touched) {
|
|
1461
|
+
const kept = before !== undefined && before.record.key === record.key
|
|
1462
|
+
? before.overlaid
|
|
1463
|
+
: new Set();
|
|
1464
|
+
return {
|
|
1465
|
+
overlay: {
|
|
1466
|
+
set: await stepped(overlayOf(visible, touched)),
|
|
1467
|
+
cleared: [...kept].filter((path) => !touched.has(path)),
|
|
1468
|
+
},
|
|
1469
|
+
touched,
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
async function settle(local, locator, before, record, queue, settling = {}) {
|
|
1473
|
+
return together(local, [await settlement(local, locator, before, record, queue, settling)]);
|
|
1474
|
+
}
|
|
1475
|
+
function unpairedIn(locator, batch) {
|
|
1476
|
+
if (!queueIn(locator).some((held) => held.id === batch.id))
|
|
1477
|
+
return false;
|
|
1478
|
+
if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
|
|
1479
|
+
return false;
|
|
1480
|
+
return !leavingIn(batch, locator) || withdrawable(locator)(batch);
|
|
1481
|
+
}
|
|
1482
|
+
async function together(local, settlements) {
|
|
1483
|
+
const settled = new Set(settlements.map(({ locator }) => locator));
|
|
1484
|
+
const all = [...settlements];
|
|
1485
|
+
for (let wave = settlements; wave.length > 0;) {
|
|
1486
|
+
const partnered = new Map();
|
|
1487
|
+
for (const { locator, left } of wave) {
|
|
1488
|
+
for (const batch of left) {
|
|
1489
|
+
const partner = partnerOf(batch, locator);
|
|
1490
|
+
if (partner === null || settled.has(partner) || !unpairedIn(partner, batch))
|
|
1491
|
+
continue;
|
|
1492
|
+
partnered.set(partner, [...(partnered.get(partner) ?? []), batch]);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
const partners = [];
|
|
1496
|
+
for (const [partner, batches] of partnered) {
|
|
1497
|
+
settled.add(partner);
|
|
1498
|
+
partners.push(await unpairing(local, partner, batches));
|
|
1499
|
+
}
|
|
1500
|
+
all.push(...partners);
|
|
1501
|
+
wave = partners;
|
|
1502
|
+
}
|
|
1503
|
+
const answer = await local.commit({
|
|
1504
|
+
rooms: all.flatMap(({ commit }) => commit.rooms),
|
|
1505
|
+
writes: all.flatMap(({ commit }) => commit.writes),
|
|
1506
|
+
});
|
|
1507
|
+
return mergeChangeSets(all.map((one) => one.apply(answer)));
|
|
1508
|
+
}
|
|
1509
|
+
async function settlement(local, locator, before, record, queue, settling = {}) {
|
|
1510
|
+
const view = settling.view ?? 'confirmed';
|
|
1249
1511
|
const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
1250
|
-
const
|
|
1512
|
+
const overrides = settling.changes;
|
|
1513
|
+
const read = await sliceAt(record, view, queue, [
|
|
1514
|
+
...(before !== undefined && before.record.key === record.key ? before.overlaid : []),
|
|
1515
|
+
...(overrides?.set.map(({ path }) => path) ?? []),
|
|
1516
|
+
...(overrides?.removed ?? []),
|
|
1517
|
+
]);
|
|
1518
|
+
const confirmedLayer = overrides === undefined
|
|
1519
|
+
? read
|
|
1520
|
+
: await stepped(slicingWith(read, overrides.set, overrides.removed));
|
|
1521
|
+
const composition = compositionOf(record);
|
|
1522
|
+
const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
|
|
1251
1523
|
const founded = kept.filter((batch) => !queue.includes(batch));
|
|
1252
1524
|
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
1253
|
-
const unqueued =
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
}
|
|
1525
|
+
const unqueued = [];
|
|
1526
|
+
for (const batch of withdrawn) {
|
|
1527
|
+
unqueued.push({ batch, landed: await landedWhole(locator, batch, confirmedLayer) });
|
|
1528
|
+
}
|
|
1257
1529
|
const returning = [
|
|
1258
|
-
...[...unqueued].reverse(),
|
|
1259
|
-
|
|
1530
|
+
...[...unqueued].reverse().map(({ batch, landed: held }) => ({
|
|
1531
|
+
batch,
|
|
1532
|
+
returned: localSaves || held ? null : returnedBy('withdrawn'),
|
|
1533
|
+
})),
|
|
1534
|
+
...(settling.leaving ?? []),
|
|
1260
1535
|
];
|
|
1261
1536
|
const written = new Map();
|
|
1262
|
-
for (const { batch, returned } of returning) {
|
|
1263
|
-
if (
|
|
1537
|
+
for (const { batch, returned: reason } of returning) {
|
|
1538
|
+
if (reason === null || batch.between?.destination.room === locator)
|
|
1264
1539
|
continue;
|
|
1265
|
-
for (const saved of pendings.get(batch.id)?.drafts ?? []) {
|
|
1540
|
+
for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
|
|
1266
1541
|
const existing = written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path);
|
|
1267
1542
|
written.set(saved.path, {
|
|
1268
1543
|
locator,
|
|
1269
1544
|
path: saved.path,
|
|
1270
1545
|
revision: roomDraftRevision(),
|
|
1271
1546
|
parts: returnedInto(existing?.parts, saved.parts),
|
|
1272
|
-
returned,
|
|
1547
|
+
returned: reason,
|
|
1273
1548
|
});
|
|
1274
1549
|
}
|
|
1275
1550
|
}
|
|
1276
|
-
|
|
1551
|
+
const carried = carriedBack(locator, [
|
|
1552
|
+
...[...unqueued].reverse().flatMap(({ batch, landed: held }) => (held ? [] : [batch])),
|
|
1553
|
+
...(settling.leaving ?? []).flatMap(({ batch, returned }) => returned === null ? [] : [batch]),
|
|
1554
|
+
], written);
|
|
1555
|
+
const replayedLayer = await replayed(record, confirmedLayer, kept);
|
|
1556
|
+
const { overlay, touched } = await overlayDiff(record, before, replayedLayer.visible, replayedLayer.touched);
|
|
1557
|
+
const commit = {
|
|
1558
|
+
rooms: [
|
|
1559
|
+
{
|
|
1560
|
+
locator,
|
|
1561
|
+
key: record.key,
|
|
1562
|
+
record: storedRecord(record),
|
|
1563
|
+
roots: record.typedSelection?.roots ?? [],
|
|
1564
|
+
...settling.commit,
|
|
1565
|
+
...(overrides === undefined
|
|
1566
|
+
? {}
|
|
1567
|
+
: { confirmed: { set: overrides.set, removed: overrides.removed } }),
|
|
1568
|
+
overlay,
|
|
1569
|
+
},
|
|
1570
|
+
],
|
|
1571
|
+
writes: [
|
|
1572
|
+
{
|
|
1573
|
+
locator,
|
|
1574
|
+
queue: kept,
|
|
1575
|
+
founded,
|
|
1576
|
+
roomDrafts: [...written.values()],
|
|
1577
|
+
consumedRoomDrafts: carried.taken,
|
|
1578
|
+
},
|
|
1579
|
+
...carried.writes,
|
|
1580
|
+
],
|
|
1581
|
+
};
|
|
1277
1582
|
const staying = new Set(kept.map((batch) => batch.id));
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
catch (error) {
|
|
1292
|
-
const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
|
|
1293
|
-
if (held !== null)
|
|
1294
|
-
replicas.set(locator, held);
|
|
1295
|
-
else if (kept.length > 0)
|
|
1296
|
-
queuesWithoutReplica.set(locator, kept);
|
|
1297
|
-
else
|
|
1583
|
+
return {
|
|
1584
|
+
locator,
|
|
1585
|
+
left: previousQueue.filter((batch) => !staying.has(batch.id)),
|
|
1586
|
+
commit,
|
|
1587
|
+
apply: (answer) => {
|
|
1588
|
+
for (const batch of previousQueue) {
|
|
1589
|
+
if (!staying.has(batch.id))
|
|
1590
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
1591
|
+
}
|
|
1592
|
+
for (const draft of written.values())
|
|
1593
|
+
rememberRoomDraft(draft);
|
|
1594
|
+
carried.remember();
|
|
1595
|
+
const drafts = [...written.keys(), ...carried.taken];
|
|
1298
1596
|
queuesWithoutReplica.delete(locator);
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1597
|
+
for (const { batch, landed: held } of unqueued) {
|
|
1598
|
+
const outcome = localSaves ? 'dropped' : held ? 'landed' : 'withdrawn';
|
|
1599
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
1600
|
+
}
|
|
1601
|
+
const changed = answer.changed[locator];
|
|
1602
|
+
const confirmedChanged = settling.commit?.published !== undefined ||
|
|
1603
|
+
settling.commit?.refreshed === true ||
|
|
1604
|
+
overrides !== undefined;
|
|
1605
|
+
recomposed(locator, before?.record, record);
|
|
1606
|
+
const { count } = committedRoom(locator, record, answer);
|
|
1607
|
+
replicas.set(locator, {
|
|
1608
|
+
record,
|
|
1609
|
+
queue: kept,
|
|
1610
|
+
overlaid: touched,
|
|
1611
|
+
count,
|
|
1612
|
+
confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
|
|
1613
|
+
});
|
|
1614
|
+
return mergeChangeSets([
|
|
1615
|
+
changeSet(locator, {
|
|
1616
|
+
documents: changed === undefined ? [] : changed,
|
|
1617
|
+
room: settling.room,
|
|
1618
|
+
queue: queueChanged,
|
|
1619
|
+
drafts,
|
|
1620
|
+
}),
|
|
1621
|
+
...carried.changes,
|
|
1622
|
+
]);
|
|
1623
|
+
},
|
|
1318
1624
|
};
|
|
1319
1625
|
}
|
|
1320
1626
|
function adoptModels(locator, record) {
|
|
@@ -1335,7 +1641,7 @@ export function composeRuntime(options) {
|
|
|
1335
1641
|
const taken = (answer, previous, attached) => {
|
|
1336
1642
|
if (answer === undefined)
|
|
1337
1643
|
return previous;
|
|
1338
|
-
const { state, refused } = attachedModel(answer, previous);
|
|
1644
|
+
const { state, refused } = attachedModel(answer, previous, attached === 'collection' ? roomTypes : NO_ROOM_TYPES);
|
|
1339
1645
|
if (refused !== null) {
|
|
1340
1646
|
const where = refused.location === undefined ? '' : ` ${refused.location}`;
|
|
1341
1647
|
journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
|
|
@@ -1348,65 +1654,76 @@ export function composeRuntime(options) {
|
|
|
1348
1654
|
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
1349
1655
|
};
|
|
1350
1656
|
}
|
|
1351
|
-
function publish(local, room,
|
|
1352
|
-
const { base, stored } = landing;
|
|
1657
|
+
function publish(local, room, record, landing, live) {
|
|
1353
1658
|
return exclusive(async () => {
|
|
1354
|
-
if (stored !== undefined) {
|
|
1355
|
-
await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
|
|
1356
|
-
}
|
|
1357
1659
|
if (!live()) {
|
|
1358
1660
|
caughtUpTo(room.locator);
|
|
1359
1661
|
return true;
|
|
1360
1662
|
}
|
|
1361
1663
|
const prior = replicas.get(room.locator);
|
|
1362
|
-
if (
|
|
1363
|
-
(prior
|
|
1364
|
-
|
|
1664
|
+
if (landing.kind === 'refreshed' &&
|
|
1665
|
+
(prior === undefined || prior.record.key !== record.key || prior.confirmed !== landing.base)) {
|
|
1666
|
+
await local.unrefresh(record.key);
|
|
1365
1667
|
return false;
|
|
1366
1668
|
}
|
|
1367
|
-
adoptModels(room.locator,
|
|
1669
|
+
adoptModels(room.locator, record);
|
|
1368
1670
|
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
1369
|
-
const roomVersion =
|
|
1671
|
+
const roomVersion = record.roomVersion;
|
|
1370
1672
|
const accepted = queue.filter((batch) => {
|
|
1371
|
-
const
|
|
1372
|
-
return
|
|
1673
|
+
const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
|
|
1674
|
+
return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
|
|
1373
1675
|
});
|
|
1374
|
-
let
|
|
1676
|
+
let set;
|
|
1375
1677
|
try {
|
|
1376
|
-
|
|
1678
|
+
set = await settle(local, room.locator, prior, record, queue.filter((batch) => !accepted.includes(batch)), landing.kind === 'refreshed'
|
|
1679
|
+
? {
|
|
1680
|
+
room: true,
|
|
1681
|
+
view: 'refreshed',
|
|
1682
|
+
commit: { refreshed: true, composition: compositionOf(record) },
|
|
1683
|
+
}
|
|
1684
|
+
: {
|
|
1685
|
+
room: true,
|
|
1686
|
+
view: landing.refreshed ? 'refreshed' : 'confirmed',
|
|
1687
|
+
commit: {
|
|
1688
|
+
published: { from: prior?.record.key ?? null },
|
|
1689
|
+
composition: compositionOf(record),
|
|
1690
|
+
...(landing.refreshed ? { refreshed: true } : {}),
|
|
1691
|
+
},
|
|
1692
|
+
});
|
|
1377
1693
|
}
|
|
1378
1694
|
catch (error) {
|
|
1379
1695
|
if (!exceeded(error))
|
|
1380
1696
|
throw error;
|
|
1381
|
-
await exceededOn(room.locator,
|
|
1697
|
+
await exceededOn(room.locator, null);
|
|
1382
1698
|
caughtUpTo(room.locator);
|
|
1383
1699
|
return true;
|
|
1384
1700
|
}
|
|
1385
|
-
|
|
1386
|
-
if (stored !== undefined) {
|
|
1701
|
+
if (landing.kind === 'bootstrapped') {
|
|
1387
1702
|
transfers.delete(room.locator);
|
|
1388
1703
|
catching.delete(room.locator);
|
|
1704
|
+
sweep();
|
|
1389
1705
|
}
|
|
1390
1706
|
for (const batch of accepted)
|
|
1391
|
-
pendings.delete(batch.id);
|
|
1707
|
+
pendings.delete(pendingKey(room.locator, batch.id));
|
|
1392
1708
|
stale.delete(room.locator);
|
|
1393
|
-
|
|
1709
|
+
unpreparable.delete(room.locator);
|
|
1394
1710
|
if (unreadable.delete(room.locator))
|
|
1395
1711
|
scopedRefusals.delete(room.locator);
|
|
1396
1712
|
sourceRefusals.delete(room.locator);
|
|
1397
1713
|
declarationRefusals.delete(room.locator);
|
|
1398
1714
|
list(room, true);
|
|
1399
|
-
if (prior === undefined)
|
|
1400
|
-
prepared.add(room.locator);
|
|
1401
1715
|
publishChangeSet(set);
|
|
1716
|
+
await unpair(local);
|
|
1402
1717
|
moveTo({ ...status, ...attention() });
|
|
1403
1718
|
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
1719
|
+
if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1720
|
+
submitLater(room.locator);
|
|
1721
|
+
}
|
|
1404
1722
|
return true;
|
|
1405
1723
|
});
|
|
1406
1724
|
}
|
|
1407
1725
|
async function savedRoom(locator) {
|
|
1408
1726
|
const local = await openLocal();
|
|
1409
|
-
await referenced(locator);
|
|
1410
1727
|
const replica = replicas.get(locator);
|
|
1411
1728
|
const room = rooms.get(locator);
|
|
1412
1729
|
if (replica === undefined || room === undefined) {
|
|
@@ -1436,13 +1753,19 @@ export function composeRuntime(options) {
|
|
|
1436
1753
|
}
|
|
1437
1754
|
const target = await savedRoom(locator);
|
|
1438
1755
|
const saved = [];
|
|
1756
|
+
const files = [];
|
|
1757
|
+
const filed = read.filter(({ parts }) => parts.file !== undefined).map(({ path }) => path);
|
|
1758
|
+
const visible = filed.length === 0 ? undefined : await visibleOf(locator, filed);
|
|
1439
1759
|
for (const { path, parts } of read) {
|
|
1440
1760
|
const file = parts.file === undefined
|
|
1441
1761
|
? undefined
|
|
1442
|
-
: await heldFile(parts.file.blob, binaryBase(
|
|
1762
|
+
: await heldFile(parts.file.blob, binaryBase(visible, path));
|
|
1763
|
+
if (parts.file !== undefined && file !== undefined) {
|
|
1764
|
+
files.push({ key: file.localId, blob: parts.file.blob });
|
|
1765
|
+
}
|
|
1443
1766
|
saved.push({ path, parts: held(undefined, parts, file) });
|
|
1444
1767
|
}
|
|
1445
|
-
return committed(locator, target, saved, new Set(), writtenBy);
|
|
1768
|
+
return committed(locator, target, saved, new Set(), writtenBy, files);
|
|
1446
1769
|
}
|
|
1447
1770
|
function carriedDrafts(locator, saved, consumed) {
|
|
1448
1771
|
const holding = roomDrafts.get(locator);
|
|
@@ -1456,13 +1779,359 @@ export function composeRuntime(options) {
|
|
|
1456
1779
|
return [[path, { ...record, path: to, revision: roomDraftRevision() }]];
|
|
1457
1780
|
}));
|
|
1458
1781
|
}
|
|
1459
|
-
async function
|
|
1460
|
-
const
|
|
1782
|
+
async function savingIn(replica, changes, extra, leaving = []) {
|
|
1783
|
+
const record = replica.record;
|
|
1784
|
+
const local = await openLocal();
|
|
1785
|
+
const paths = [
|
|
1786
|
+
...extra,
|
|
1787
|
+
...changes.flatMap((change) => change.kind === 'move' ? [change.path, change.to] : [change.path]),
|
|
1788
|
+
];
|
|
1789
|
+
const moved = [
|
|
1790
|
+
...leaving,
|
|
1791
|
+
...changes.flatMap((change) => (change.kind === 'move' ? [change.path] : [])),
|
|
1792
|
+
];
|
|
1793
|
+
const removed = changes.flatMap((change) => (change.kind === 'remove' ? [change.path] : []));
|
|
1794
|
+
const referring = removed.length === 0
|
|
1795
|
+
? []
|
|
1796
|
+
: (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
|
|
1797
|
+
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
|
|
1798
|
+
const { visible } = await replayed(record, confirmedLayer, replica.queue);
|
|
1799
|
+
const composition = compositionOf(record);
|
|
1800
|
+
return {
|
|
1801
|
+
record,
|
|
1802
|
+
confirmed: confirmedLayer,
|
|
1803
|
+
visible,
|
|
1804
|
+
*visibleOver(queue) {
|
|
1805
|
+
return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
|
|
1806
|
+
},
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
async function unmodeledIn(record) {
|
|
1810
|
+
const composition = compositionFor(record);
|
|
1811
|
+
if (available(composition))
|
|
1812
|
+
return false;
|
|
1813
|
+
const local = await openLocal();
|
|
1814
|
+
const roots = record.typedSelection?.roots ?? [];
|
|
1815
|
+
if (composition.model.status === 'unavailable' &&
|
|
1816
|
+
(await local.typedWithin(record.key, 'visible', roots, false))) {
|
|
1817
|
+
return true;
|
|
1818
|
+
}
|
|
1819
|
+
return (composition.selection !== null &&
|
|
1820
|
+
composition.selection.model.status === 'unavailable' &&
|
|
1821
|
+
(await local.typedWithin(record.key, 'visible', roots, true)));
|
|
1822
|
+
}
|
|
1823
|
+
function textDiffers(left, right) {
|
|
1824
|
+
return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
|
|
1825
|
+
!sameBinary(left?.binary ?? undefined, right?.binary ?? undefined));
|
|
1826
|
+
}
|
|
1827
|
+
function textOf(entry) {
|
|
1828
|
+
if (entry === null)
|
|
1829
|
+
return undefined;
|
|
1830
|
+
return entry.binary !== null ? null : entry.content;
|
|
1831
|
+
}
|
|
1832
|
+
function walkOf(local, key, view) {
|
|
1833
|
+
return async (after) => {
|
|
1834
|
+
const page = await local.walk(key, view, after, WALK_BYTES);
|
|
1835
|
+
return {
|
|
1836
|
+
entries: page.entries.map(({ path, content, binary }) => ({
|
|
1837
|
+
path,
|
|
1838
|
+
content: binary !== null ? null : content,
|
|
1839
|
+
})),
|
|
1840
|
+
next: page.next,
|
|
1841
|
+
};
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
function lookupOf(local, key, view) {
|
|
1845
|
+
return async (paths) => {
|
|
1846
|
+
if (paths.length === 0)
|
|
1847
|
+
return new Map();
|
|
1848
|
+
return new Map((await local.typed(key, view, paths)).map(({ path, type, binary }) => [
|
|
1849
|
+
path,
|
|
1850
|
+
{ type: type ?? undefined, binary },
|
|
1851
|
+
]));
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
function declaredOf(entry) {
|
|
1855
|
+
if (entry.binary !== null)
|
|
1856
|
+
return { type: undefined, binary: true };
|
|
1857
|
+
const read = parseFrontmatter(entry.content ?? '');
|
|
1858
|
+
return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
|
|
1859
|
+
}
|
|
1860
|
+
function overriddenWalk(walk, overrides) {
|
|
1861
|
+
const sorted = [...overrides.keys()].sort();
|
|
1862
|
+
const beyond = (path) => {
|
|
1863
|
+
if (path === null)
|
|
1864
|
+
return 0;
|
|
1865
|
+
let low = 0;
|
|
1866
|
+
let high = sorted.length;
|
|
1867
|
+
while (low < high) {
|
|
1868
|
+
const middle = (low + high) >>> 1;
|
|
1869
|
+
if (sorted[middle] <= path)
|
|
1870
|
+
low = middle + 1;
|
|
1871
|
+
else
|
|
1872
|
+
high = middle;
|
|
1873
|
+
}
|
|
1874
|
+
return low;
|
|
1875
|
+
};
|
|
1876
|
+
return async (after) => {
|
|
1877
|
+
const page = await walk(after);
|
|
1878
|
+
const end = page.next;
|
|
1879
|
+
const added = sorted
|
|
1880
|
+
.slice(beyond(after), end === null ? sorted.length : beyond(end))
|
|
1881
|
+
.flatMap((path) => {
|
|
1882
|
+
const entry = overrides.get(path);
|
|
1883
|
+
return entry === null || entry === undefined
|
|
1884
|
+
? []
|
|
1885
|
+
: [{ path, content: entry.binary !== null ? null : entry.content }];
|
|
1886
|
+
});
|
|
1887
|
+
const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
|
|
1888
|
+
return { entries, next: end };
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
function overriddenLookup(lookup, overrides) {
|
|
1892
|
+
return async (paths) => {
|
|
1893
|
+
const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
|
|
1894
|
+
for (const path of paths) {
|
|
1895
|
+
const entry = overrides.get(path);
|
|
1896
|
+
if (entry === undefined)
|
|
1897
|
+
continue;
|
|
1898
|
+
if (entry === null)
|
|
1899
|
+
known.delete(path);
|
|
1900
|
+
else
|
|
1901
|
+
known.set(path, declaredOf(entry));
|
|
1902
|
+
}
|
|
1903
|
+
return known;
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
function validatorOf(record, confirmedLayer) {
|
|
1907
|
+
return async (resulting, touched) => {
|
|
1908
|
+
const composition = compositionFor(record);
|
|
1909
|
+
const local = await openLocal();
|
|
1910
|
+
const changed = [];
|
|
1911
|
+
const overrides = new Map();
|
|
1912
|
+
const pace = paced();
|
|
1913
|
+
for (const path of new Set([...confirmedLayer.held, ...touched])) {
|
|
1914
|
+
await pace(ROW_WEIGHT);
|
|
1915
|
+
const was = entryAt(confirmedLayer, path);
|
|
1916
|
+
const now = entryAt(resulting, path);
|
|
1917
|
+
if (!textDiffers(was, now))
|
|
1918
|
+
continue;
|
|
1919
|
+
overrides.set(path, now);
|
|
1920
|
+
if (textOf(was) !== textOf(now))
|
|
1921
|
+
changed.push({ path, was: textOf(was), now: textOf(now) });
|
|
1922
|
+
}
|
|
1923
|
+
const lookup = overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides);
|
|
1924
|
+
const clean = available(composition) &&
|
|
1925
|
+
changed.length <= CLEAN_CHANGES &&
|
|
1926
|
+
(await changedCleanly(changed, compositionOf(record), composition, {
|
|
1927
|
+
lookup,
|
|
1928
|
+
texts: textsOf(local, record.key, 'confirmed'),
|
|
1929
|
+
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
1930
|
+
}));
|
|
1931
|
+
if (clean)
|
|
1932
|
+
return { verdict: 'accept' };
|
|
1933
|
+
return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
function textsOf(local, key, view) {
|
|
1937
|
+
return async (paths) => new Map((await local.entries(key, view, paths)).map(({ path, content, binary }) => [
|
|
1938
|
+
path,
|
|
1939
|
+
binary !== null ? null : content,
|
|
1940
|
+
]));
|
|
1941
|
+
}
|
|
1942
|
+
function referrersOf(local, key, view) {
|
|
1943
|
+
return async (targets) => [
|
|
1944
|
+
...new Set((await local.referrers(key, view, targets, CHECKED_LINKS)).map(({ path }) => path)),
|
|
1945
|
+
];
|
|
1946
|
+
}
|
|
1947
|
+
async function sideOf(locator, replica, room, saving) {
|
|
1948
|
+
const record = replica.record;
|
|
1949
|
+
return {
|
|
1950
|
+
room: locator,
|
|
1951
|
+
handle: room.handle,
|
|
1952
|
+
baseRoomVersion: record.roomVersion,
|
|
1953
|
+
composition: compositionFor(record),
|
|
1954
|
+
operations: room.operations,
|
|
1955
|
+
providerOwnedPaths: room.providerOwnedPaths,
|
|
1956
|
+
visible: saving.visible,
|
|
1957
|
+
queued: queuedDocuments(replica.queue),
|
|
1958
|
+
unmodeled: await unmodeledIn(record),
|
|
1959
|
+
validate: validatorOf(record, saving.confirmed),
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
function movedInto(locator, saved) {
|
|
1963
|
+
const into = saved.find(({ parts }) => parts.move?.room !== undefined && parts.move.room !== locator);
|
|
1964
|
+
return into?.parts.move?.room ?? null;
|
|
1965
|
+
}
|
|
1966
|
+
async function saveCommit(local, files, rooms) {
|
|
1967
|
+
const touched = new Map();
|
|
1968
|
+
const commits = [];
|
|
1969
|
+
for (const { locator, saving, before, queue } of rooms) {
|
|
1970
|
+
const replayedLayer = await replayed(saving.record, saving.confirmed, queue);
|
|
1971
|
+
const diff = await overlayDiff(saving.record, before, replayedLayer.visible, replayedLayer.touched);
|
|
1972
|
+
touched.set(locator, diff.touched);
|
|
1973
|
+
commits.push({
|
|
1974
|
+
locator,
|
|
1975
|
+
key: saving.record.key,
|
|
1976
|
+
roots: saving.record.typedSelection?.roots ?? [],
|
|
1977
|
+
overlay: diff.overlay,
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
try {
|
|
1981
|
+
const answer = await local.commit({
|
|
1982
|
+
rooms: commits,
|
|
1983
|
+
writes: rooms.map(({ write }) => write),
|
|
1984
|
+
files,
|
|
1985
|
+
whole: true,
|
|
1986
|
+
});
|
|
1987
|
+
return { answer, touched };
|
|
1988
|
+
}
|
|
1989
|
+
catch {
|
|
1990
|
+
return null;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
function savedReplica(locator, before, queue, touched, answer) {
|
|
1994
|
+
replicas.set(locator, {
|
|
1995
|
+
...before,
|
|
1996
|
+
queue,
|
|
1997
|
+
overlaid: touched,
|
|
1998
|
+
count: answer.counts[locator] ?? before.count,
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
async function committedMove(locator, destination, { local, replica, room }, saved, consumed, writtenBy, files) {
|
|
2002
|
+
const mixed = saved.find(({ parts }) => Object.keys(parts).length !== 1 || parts.move?.room !== destination);
|
|
2003
|
+
if (mixed !== undefined) {
|
|
2004
|
+
return { outcome: 'refused', code: 'mixed-room-move', retryable: false, file: mixed.path };
|
|
2005
|
+
}
|
|
2006
|
+
const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
|
|
2007
|
+
const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
|
|
2008
|
+
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, sourceSaving.visible)));
|
|
2009
|
+
if (changed.length > 0)
|
|
2010
|
+
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2011
|
+
const target = replicas.get(destination);
|
|
2012
|
+
const listedRoom = rooms.get(destination);
|
|
2013
|
+
if (target === undefined || listedRoom === undefined) {
|
|
2014
|
+
throw runtimeError('room-not-held', `No Local Replica for ${destination}`);
|
|
2015
|
+
}
|
|
2016
|
+
const arriving = await savingIn(target, [], moves.map(({ to }) => to));
|
|
2017
|
+
const built = await buildMove({
|
|
2018
|
+
source: await sideOf(locator, replica, room, sourceSaving),
|
|
2019
|
+
destination: await sideOf(destination, target, listedRoom, arriving),
|
|
2020
|
+
moves,
|
|
2021
|
+
providerLimits: limits,
|
|
2022
|
+
local: localSaves,
|
|
2023
|
+
...(writtenBy === undefined ? {} : { writtenBy }),
|
|
2024
|
+
});
|
|
2025
|
+
if (built.kind === 'refused')
|
|
2026
|
+
return { outcome: 'refused', ...built.refusal };
|
|
2027
|
+
const id = built.leaving.id;
|
|
2028
|
+
const carrying = saved.flatMap(({ path, parts }) => {
|
|
2029
|
+
const record = roomDrafts.get(locator)?.get(path);
|
|
2030
|
+
const to = moveTarget(parts.move);
|
|
2031
|
+
if (record === undefined || consumed.has(path) || roomDrafts.get(destination)?.has(to)) {
|
|
2032
|
+
return [];
|
|
2033
|
+
}
|
|
2034
|
+
const moved = {
|
|
2035
|
+
...record,
|
|
2036
|
+
locator: destination,
|
|
2037
|
+
path: to,
|
|
2038
|
+
revision: roomDraftRevision(),
|
|
2039
|
+
};
|
|
2040
|
+
return [{ from: path, record: moved }];
|
|
2041
|
+
});
|
|
2042
|
+
const carried = carrying.map(({ record }) => record);
|
|
2043
|
+
const marked = carried.map(({ path }) => ({ path, parts: {} }));
|
|
2044
|
+
const left = [...consumed, ...carrying.map(({ from }) => from)];
|
|
2045
|
+
const leavingQueue = [...replica.queue, built.leaving];
|
|
2046
|
+
const arrivingQueue = [...target.queue, built.arriving];
|
|
2047
|
+
const committedBoth = await saveCommit(local, files, [
|
|
2048
|
+
{
|
|
2049
|
+
locator,
|
|
2050
|
+
saving: sourceSaving,
|
|
2051
|
+
before: replica,
|
|
2052
|
+
queue: leavingQueue,
|
|
2053
|
+
write: {
|
|
2054
|
+
locator,
|
|
2055
|
+
queue: leavingQueue,
|
|
2056
|
+
saved: new Map([[id, saved]]),
|
|
2057
|
+
consumedRoomDrafts: left,
|
|
2058
|
+
},
|
|
2059
|
+
},
|
|
2060
|
+
{
|
|
2061
|
+
locator: destination,
|
|
2062
|
+
saving: arriving,
|
|
2063
|
+
before: target,
|
|
2064
|
+
queue: arrivingQueue,
|
|
2065
|
+
write: {
|
|
2066
|
+
locator: destination,
|
|
2067
|
+
queue: arrivingQueue,
|
|
2068
|
+
saved: new Map([[id, marked]]),
|
|
2069
|
+
roomDrafts: carried,
|
|
2070
|
+
},
|
|
2071
|
+
},
|
|
2072
|
+
]);
|
|
2073
|
+
if (committedBoth === null)
|
|
2074
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2075
|
+
const { answer, touched } = committedBoth;
|
|
2076
|
+
if (answer.fenced !== null) {
|
|
2077
|
+
throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
|
|
2078
|
+
}
|
|
2079
|
+
if (fences.has(locator) || fences.has(destination)) {
|
|
2080
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
2081
|
+
}
|
|
2082
|
+
pendings.set(pendingKey(locator, id), {
|
|
2083
|
+
drafts: saved,
|
|
2084
|
+
submission: { state: 'waiting' },
|
|
2085
|
+
lost: false,
|
|
2086
|
+
});
|
|
2087
|
+
pendings.set(pendingKey(destination, id), {
|
|
2088
|
+
drafts: marked,
|
|
2089
|
+
submission: { state: 'waiting' },
|
|
2090
|
+
lost: false,
|
|
2091
|
+
});
|
|
2092
|
+
for (const path of left)
|
|
2093
|
+
forgetRoomDraft(locator, path);
|
|
2094
|
+
for (const record of carried)
|
|
2095
|
+
rememberRoomDraft(record);
|
|
2096
|
+
savedReplica(locator, replica, leavingQueue, touched.get(locator), answer);
|
|
2097
|
+
savedReplica(destination, target, arrivingQueue, touched.get(destination), answer);
|
|
2098
|
+
publishChangeSet(mergeChangeSets([
|
|
2099
|
+
changeSet(locator, {
|
|
2100
|
+
documents: answer.changed[locator] ?? [],
|
|
2101
|
+
queue: true,
|
|
2102
|
+
drafts: left,
|
|
2103
|
+
}),
|
|
2104
|
+
changeSet(destination, {
|
|
2105
|
+
documents: answer.changed[destination] ?? [],
|
|
2106
|
+
queue: true,
|
|
2107
|
+
drafts: carried.map(({ path }) => path),
|
|
2108
|
+
}),
|
|
2109
|
+
]));
|
|
2110
|
+
queuedSave(locator);
|
|
2111
|
+
return { outcome: 'saved', identifier: id };
|
|
2112
|
+
}
|
|
2113
|
+
function queuedSave(locator) {
|
|
2114
|
+
if (!localSaves)
|
|
2115
|
+
recorded = false;
|
|
2116
|
+
moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
|
|
2117
|
+
submitLater(locator);
|
|
2118
|
+
}
|
|
2119
|
+
async function committed(locator, target, saved, consumed, writtenBy, files = []) {
|
|
2120
|
+
const into = movedInto(locator, saved);
|
|
2121
|
+
if (into !== null) {
|
|
2122
|
+
return committedMove(locator, into, target, saved, consumed, writtenBy, files);
|
|
2123
|
+
}
|
|
2124
|
+
const { local, replica, room } = target;
|
|
2125
|
+
const parts = saved.map(({ path, parts: held }) => changesOf(path, held));
|
|
2126
|
+
const changes = parts.flatMap(({ changes: made }) => made);
|
|
2127
|
+
const saving = await savingIn(replica, changes, [
|
|
2128
|
+
...saved.map(({ path }) => path),
|
|
2129
|
+
...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
|
|
2130
|
+
]);
|
|
2131
|
+
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, underneath(held, path, saving.visible)));
|
|
1461
2132
|
if (changed.length > 0)
|
|
1462
2133
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
1463
|
-
const
|
|
1464
|
-
const changes = parts.flatMap(({ changes }) => changes);
|
|
1465
|
-
const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
|
|
2134
|
+
const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
|
|
1466
2135
|
if (requeue.kind === 'refused')
|
|
1467
2136
|
return { outcome: 'refused', ...requeue.refusal };
|
|
1468
2137
|
const foreign = [...requeue.withdrawn.keys()].some((origin) => {
|
|
@@ -1480,25 +2149,22 @@ export function composeRuntime(options) {
|
|
|
1480
2149
|
const withdrawing = requeue.withdrawn.size > 0;
|
|
1481
2150
|
const input = {
|
|
1482
2151
|
local: localSaves,
|
|
1483
|
-
baseRoomVersion: replica.
|
|
1484
|
-
composition: compositionFor(replica.
|
|
2152
|
+
baseRoomVersion: replica.record.roomVersion,
|
|
2153
|
+
composition: compositionFor(replica.record),
|
|
1485
2154
|
operations: room.operations,
|
|
1486
2155
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
1487
2156
|
providerLimits: limits,
|
|
1488
2157
|
handle: room.handle,
|
|
1489
|
-
visible:
|
|
2158
|
+
visible: saving.visible,
|
|
1490
2159
|
changes,
|
|
1491
2160
|
bases: parts.flatMap(({ bases }) => bases),
|
|
1492
2161
|
queued: queuedDocuments(replica.queue),
|
|
1493
2162
|
writtenBy,
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
binaries: replica.confirmed.record.binaries,
|
|
1497
|
-
composition: compositionOf(replica.confirmed.record),
|
|
1498
|
-
},
|
|
2163
|
+
unmodeled: await unmodeledIn(replica.record),
|
|
2164
|
+
validate: validatorOf(replica.record, saving.confirmed),
|
|
1499
2165
|
};
|
|
1500
2166
|
const checked = withdrawing
|
|
1501
|
-
? buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
|
|
2167
|
+
? await buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
|
|
1502
2168
|
: null;
|
|
1503
2169
|
if (checked?.kind === 'refused')
|
|
1504
2170
|
return { outcome: 'refused', ...checked.refusal };
|
|
@@ -1515,12 +2181,12 @@ export function composeRuntime(options) {
|
|
|
1515
2181
|
};
|
|
1516
2182
|
}
|
|
1517
2183
|
const built = !withdrawing
|
|
1518
|
-
? buildBatch(input)
|
|
2184
|
+
? await buildBatch(input)
|
|
1519
2185
|
: requeue.changes.length === 0
|
|
1520
2186
|
? null
|
|
1521
|
-
: buildBatch({
|
|
2187
|
+
: await buildBatch({
|
|
1522
2188
|
...input,
|
|
1523
|
-
visible:
|
|
2189
|
+
visible: await stepped(saving.visibleOver(requeue.queue)),
|
|
1524
2190
|
changes: requeue.changes,
|
|
1525
2191
|
queued: queuedDocuments(requeue.queue),
|
|
1526
2192
|
});
|
|
@@ -1539,7 +2205,7 @@ export function composeRuntime(options) {
|
|
|
1539
2205
|
for (const [origin, replaced] of withdrawn) {
|
|
1540
2206
|
if (replaced === null)
|
|
1541
2207
|
continue;
|
|
1542
|
-
const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
|
|
2208
|
+
const carried = withdrawnDrafts(pendings.get(pendingKey(locator, origin))?.drafts ?? [], changes);
|
|
1543
2209
|
drafted.set(replaced.id, replaced === join?.batch
|
|
1544
2210
|
? [
|
|
1545
2211
|
...carried.map((held) => joinedDraft(held, saved)),
|
|
@@ -1553,64 +2219,77 @@ export function composeRuntime(options) {
|
|
|
1553
2219
|
for (const replaced of withdrawn.values()) {
|
|
1554
2220
|
if (replaced === null || !batches.includes(replaced))
|
|
1555
2221
|
continue;
|
|
1556
|
-
const limited =
|
|
2222
|
+
const limited = await stepped(limiting(batches, replaced, limits, saving.visibleOver));
|
|
1557
2223
|
if (limited !== null)
|
|
1558
2224
|
return { outcome: 'refused', ...limited };
|
|
1559
2225
|
}
|
|
1560
2226
|
const carried = carriedDrafts(locator, saved, consumed);
|
|
1561
2227
|
const left = [...consumed, ...carried.keys()];
|
|
1562
2228
|
const arrived = [...carried.values()];
|
|
1563
|
-
|
|
1564
|
-
|
|
2229
|
+
const done = await saveCommit(local, files, [
|
|
2230
|
+
{
|
|
1565
2231
|
locator,
|
|
2232
|
+
saving,
|
|
2233
|
+
before: replica,
|
|
1566
2234
|
queue: batches,
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
2235
|
+
write: {
|
|
2236
|
+
locator,
|
|
2237
|
+
queue: batches,
|
|
2238
|
+
saved: drafted,
|
|
2239
|
+
consumedRoomDrafts: left,
|
|
2240
|
+
roomDrafts: arrived,
|
|
2241
|
+
},
|
|
2242
|
+
},
|
|
2243
|
+
]);
|
|
2244
|
+
if (done === null)
|
|
1573
2245
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2246
|
+
const { answer, touched } = done;
|
|
2247
|
+
if (answer.fenced !== null) {
|
|
2248
|
+
throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
|
|
1574
2249
|
}
|
|
1575
2250
|
if (fences.has(locator))
|
|
1576
2251
|
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1577
2252
|
for (const origin of withdrawn.keys())
|
|
1578
|
-
pendings.delete(origin);
|
|
2253
|
+
pendings.delete(pendingKey(locator, origin));
|
|
1579
2254
|
for (const [id, drafts] of drafted) {
|
|
1580
|
-
pendings.set(
|
|
2255
|
+
pendings.set(pendingKey(locator, id), {
|
|
2256
|
+
drafts,
|
|
2257
|
+
submission: { state: 'waiting' },
|
|
2258
|
+
lost: false,
|
|
2259
|
+
});
|
|
1581
2260
|
}
|
|
1582
|
-
if (!localSaves)
|
|
1583
|
-
recorded = false;
|
|
1584
2261
|
for (const path of left)
|
|
1585
2262
|
forgetRoomDraft(locator, path);
|
|
1586
2263
|
for (const record of arrived)
|
|
1587
2264
|
rememberRoomDraft(record);
|
|
1588
|
-
|
|
1589
|
-
publishChangeSet(changeSet(locator,
|
|
2265
|
+
savedReplica(locator, replica, batches, touched.get(locator), answer);
|
|
2266
|
+
publishChangeSet(changeSet(locator, {
|
|
2267
|
+
documents: answer.changed[locator] ?? [],
|
|
1590
2268
|
queue: true,
|
|
1591
2269
|
drafts: [...left, ...arrived.map(({ path }) => path)],
|
|
1592
2270
|
}));
|
|
1593
|
-
|
|
1594
|
-
submitLater(locator);
|
|
2271
|
+
queuedSave(locator);
|
|
1595
2272
|
const identifier = join?.batch.id ??
|
|
1596
2273
|
built?.batch.id ??
|
|
1597
2274
|
[...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
|
|
1598
2275
|
return { outcome: 'saved', identifier };
|
|
1599
2276
|
}
|
|
1600
|
-
async function mark(local, locator,
|
|
1601
|
-
const
|
|
1602
|
-
|
|
1603
|
-
state
|
|
1604
|
-
(
|
|
1605
|
-
|
|
2277
|
+
async function mark(local, locator, batch, submission, lost) {
|
|
2278
|
+
const partner = partnerOf(batch, locator);
|
|
2279
|
+
for (const held of partner === null ? [locator] : [locator, partner]) {
|
|
2280
|
+
const state = pendings.get(pendingKey(held, batch.id));
|
|
2281
|
+
if (!continuing() ||
|
|
2282
|
+
state === undefined ||
|
|
2283
|
+
(JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
|
|
2284
|
+
continue;
|
|
2285
|
+
}
|
|
2286
|
+
await local.submit(held, batch.id, submission, lost);
|
|
2287
|
+
if (fences.has(held))
|
|
2288
|
+
continue;
|
|
2289
|
+
pendings.set(pendingKey(held, batch.id), { ...state, submission, lost });
|
|
2290
|
+
if (replicas.has(held))
|
|
2291
|
+
publishChangeSet(changeSet(held, { queue: true }));
|
|
1606
2292
|
}
|
|
1607
|
-
await local.submit(locator, identifier, submission, lost);
|
|
1608
|
-
if (fences.has(locator))
|
|
1609
|
-
return;
|
|
1610
|
-
pendings.set(identifier, { ...state, submission, lost });
|
|
1611
|
-
const replica = replicas.get(locator);
|
|
1612
|
-
if (replica !== undefined)
|
|
1613
|
-
publishChangeSet(changeSet(locator, replica, replica, { queue: true }));
|
|
1614
2293
|
moveTo({ ...status, ...attention() });
|
|
1615
2294
|
}
|
|
1616
2295
|
async function keepUpload(local, locator, identifier, upload) {
|
|
@@ -1619,7 +2298,8 @@ export function composeRuntime(options) {
|
|
|
1619
2298
|
await local.keepUpload(locator, identifier, upload);
|
|
1620
2299
|
if (fences.has(locator))
|
|
1621
2300
|
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1622
|
-
|
|
2301
|
+
const key = pendingKey(locator, identifier);
|
|
2302
|
+
pendings.set(key, { ...pendings.get(key), upload });
|
|
1623
2303
|
if (!continuing())
|
|
1624
2304
|
throw new Error('connection refused');
|
|
1625
2305
|
}
|
|
@@ -1652,7 +2332,7 @@ export function composeRuntime(options) {
|
|
|
1652
2332
|
if (fencedSince(locator, since))
|
|
1653
2333
|
return failure({ kind: 'room-not-readable', locator });
|
|
1654
2334
|
if (result.kind === 'scoped-refusal') {
|
|
1655
|
-
const room = replicas.get(result.locator)?.
|
|
2335
|
+
const room = replicas.get(result.locator)?.record;
|
|
1656
2336
|
if (room !== undefined)
|
|
1657
2337
|
await scoped(result, room, await openLocal());
|
|
1658
2338
|
}
|
|
@@ -1665,19 +2345,60 @@ export function composeRuntime(options) {
|
|
|
1665
2345
|
return UNCERTAIN;
|
|
1666
2346
|
}
|
|
1667
2347
|
}
|
|
1668
|
-
function
|
|
2348
|
+
function located(room, path, destination) {
|
|
2349
|
+
const crossing = roomTarget(destination);
|
|
2350
|
+
if (crossing === null) {
|
|
2351
|
+
const named = target(path, destination);
|
|
2352
|
+
return named === null || !rooms.has(room) ? null : { room, path: named };
|
|
2353
|
+
}
|
|
2354
|
+
const held = [...rooms.values()].find((listed) => listed.handle === crossing.handle);
|
|
2355
|
+
return held === undefined ? null : { room: held.locator, path: crossing.path };
|
|
2356
|
+
}
|
|
2357
|
+
async function resolveReference(room, path, destination) {
|
|
2358
|
+
if (roomTarget(destination) === null && target(path, destination) === null) {
|
|
2359
|
+
return ABSENT_TARGET;
|
|
2360
|
+
}
|
|
2361
|
+
const at = located(room, path, destination);
|
|
2362
|
+
if (at === null)
|
|
2363
|
+
return UNREADABLE;
|
|
2364
|
+
if (locked(at.room))
|
|
2365
|
+
return LOCKED_UNREADABLE;
|
|
2366
|
+
const held = replicas.get(at.room);
|
|
2367
|
+
if (held !== undefined &&
|
|
2368
|
+
(await (await openLocal()).entries(held.record.key, 'visible', [at.path])).length > 0) {
|
|
2369
|
+
return { kind: 'document', ...at };
|
|
2370
|
+
}
|
|
2371
|
+
const history = await readHistory(at.room, at.path);
|
|
2372
|
+
if (history.kind === 'no-history' || history.kind === 'revision')
|
|
2373
|
+
return ABSENT_TARGET;
|
|
2374
|
+
if (history.kind !== 'history')
|
|
2375
|
+
return history;
|
|
2376
|
+
const [last] = history.revisions;
|
|
2377
|
+
if (last === undefined)
|
|
2378
|
+
return ABSENT_TARGET;
|
|
2379
|
+
return history.deleted === true
|
|
2380
|
+
? { kind: 'deleted', ...at, revision: last }
|
|
2381
|
+
: { kind: 'document', ...at };
|
|
2382
|
+
}
|
|
2383
|
+
function queuedFacts(locator, path, localId) {
|
|
1669
2384
|
for (const batch of replicas.get(locator)?.queue ?? []) {
|
|
1670
2385
|
const change = batch.changes[queuedFile(batch, path, localId)];
|
|
1671
2386
|
if (change?.kind === 'binary')
|
|
1672
|
-
return change
|
|
2387
|
+
return change;
|
|
1673
2388
|
}
|
|
1674
|
-
|
|
2389
|
+
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2390
|
+
return drafted?.localId === localId ? drafted : null;
|
|
2391
|
+
}
|
|
2392
|
+
function batchFile(batch) {
|
|
2393
|
+
return async (localId) => {
|
|
2394
|
+
const change = batch.changes.find((held) => held.kind === 'binary' && held.localId === localId);
|
|
2395
|
+
return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
|
|
2396
|
+
};
|
|
1675
2397
|
}
|
|
1676
2398
|
function binaryRoom(request) {
|
|
1677
2399
|
if (request.kind === 'path')
|
|
1678
2400
|
return request.locator;
|
|
1679
|
-
|
|
1680
|
-
return scope?.kind === 'room' ? scope.locator : null;
|
|
2401
|
+
return options.adapter.referrerScope(request.referrer)?.locator ?? null;
|
|
1681
2402
|
}
|
|
1682
2403
|
async function transferBinary(request, transfer, answer) {
|
|
1683
2404
|
if (closed || refusal !== null)
|
|
@@ -1702,7 +2423,7 @@ export function composeRuntime(options) {
|
|
|
1702
2423
|
if (locator !== null && fencedSince(locator, since))
|
|
1703
2424
|
return failure({ kind: 'room-not-readable', locator });
|
|
1704
2425
|
if (settled.kind === 'scoped-refusal') {
|
|
1705
|
-
const room = replicas.get(settled.locator)?.
|
|
2426
|
+
const room = replicas.get(settled.locator)?.record;
|
|
1706
2427
|
if (room !== undefined)
|
|
1707
2428
|
await scoped(settled, room, await openLocal());
|
|
1708
2429
|
}
|
|
@@ -1715,68 +2436,96 @@ export function composeRuntime(options) {
|
|
|
1715
2436
|
return UNCERTAIN;
|
|
1716
2437
|
}
|
|
1717
2438
|
}
|
|
1718
|
-
function readBinary(request) {
|
|
1719
|
-
|
|
2439
|
+
async function readBinary(request, name) {
|
|
2440
|
+
if (closed)
|
|
2441
|
+
return UNCERTAIN;
|
|
2442
|
+
const held = await keptFiles.find(name);
|
|
2443
|
+
if (held !== null)
|
|
2444
|
+
return held;
|
|
2445
|
+
return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
|
|
1720
2446
|
}
|
|
1721
|
-
function binaryPath(locator, path, revision) {
|
|
2447
|
+
async function binaryPath(locator, path, revision) {
|
|
1722
2448
|
if (revision !== undefined)
|
|
1723
2449
|
return { kind: 'path', locator, path, revision };
|
|
1724
|
-
const
|
|
1725
|
-
|
|
1726
|
-
|
|
2450
|
+
const replica = replicas.get(locator);
|
|
2451
|
+
if (replica === undefined)
|
|
2452
|
+
return null;
|
|
2453
|
+
const local = await openLocal();
|
|
2454
|
+
const [held] = await local.entries(replica.record.key, 'visible', [path]);
|
|
2455
|
+
if (held?.binary === undefined || held.binary === null)
|
|
1727
2456
|
return null;
|
|
1728
|
-
if (held.state === 'pending')
|
|
1729
|
-
return { kind: 'pending', localId: held.localId };
|
|
1730
|
-
const record = binaryRecord(path, held,
|
|
2457
|
+
if (held.binary.state === 'pending')
|
|
2458
|
+
return { kind: 'pending', localId: held.binary.localId };
|
|
2459
|
+
const record = binaryRecord(path, held.binary, held.revision ?? undefined);
|
|
1731
2460
|
if (record.state !== 'accepted')
|
|
1732
2461
|
return null;
|
|
1733
|
-
const
|
|
1734
|
-
const
|
|
2462
|
+
const queued = replica.queue.flatMap((batch) => batch.changes);
|
|
2463
|
+
const sources = queued.flatMap((change) => (change.kind === 'move' ? [change.path] : []));
|
|
2464
|
+
const confirmedLayer = await stepped(slicing(await local.entries(replica.record.key, 'confirmed', [path, ...sources]), [
|
|
2465
|
+
path,
|
|
2466
|
+
...sources,
|
|
2467
|
+
]));
|
|
1735
2468
|
const holds = (at) => {
|
|
1736
|
-
const kept =
|
|
2469
|
+
const kept = confirmedLayer.binaries[at];
|
|
1737
2470
|
return (kept?.state === 'accepted' &&
|
|
1738
2471
|
kept.fileRef === record.fileRef &&
|
|
1739
|
-
|
|
2472
|
+
confirmedLayer.revisions[at] === record.revision);
|
|
1740
2473
|
};
|
|
1741
|
-
let
|
|
1742
|
-
for (let index = queued.length - 1; index >= 0 && !holds(
|
|
2474
|
+
let confirmedAt = path;
|
|
2475
|
+
for (let index = queued.length - 1; index >= 0 && !holds(confirmedAt); index -= 1) {
|
|
1743
2476
|
const change = queued[index];
|
|
1744
|
-
if (change.kind === 'move' && change.to ===
|
|
1745
|
-
|
|
2477
|
+
if (change.kind === 'move' && change.to === confirmedAt)
|
|
2478
|
+
confirmedAt = change.path;
|
|
1746
2479
|
}
|
|
1747
2480
|
return {
|
|
1748
2481
|
kind: 'path',
|
|
1749
2482
|
locator,
|
|
1750
|
-
path:
|
|
2483
|
+
path: confirmedAt,
|
|
1751
2484
|
revision: record.revision,
|
|
1752
2485
|
fileRef: record.fileRef,
|
|
1753
2486
|
};
|
|
1754
2487
|
}
|
|
1755
2488
|
async function readBinaryPath(locator, path, revision) {
|
|
1756
2489
|
await ready;
|
|
1757
|
-
const
|
|
2490
|
+
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2491
|
+
const asked = drafted !== undefined && drafted.localId === revision
|
|
2492
|
+
? { kind: 'pending', localId: revision }
|
|
2493
|
+
: await binaryPath(locator, path, revision);
|
|
1758
2494
|
if (asked === null)
|
|
1759
2495
|
return absentBinary(path);
|
|
1760
2496
|
if (asked.kind === 'pending') {
|
|
1761
|
-
const
|
|
1762
|
-
return
|
|
2497
|
+
const facts = queuedFacts(locator, path, asked.localId);
|
|
2498
|
+
return facts === null
|
|
2499
|
+
? absentBinary(path)
|
|
2500
|
+
: {
|
|
2501
|
+
kind: 'binary',
|
|
2502
|
+
file: asked.localId,
|
|
2503
|
+
byteLength: facts.byteLength,
|
|
2504
|
+
mediaType: facts.mediaType,
|
|
2505
|
+
};
|
|
1763
2506
|
}
|
|
1764
|
-
return readBinary(asked);
|
|
2507
|
+
return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
|
|
1765
2508
|
}
|
|
1766
2509
|
async function linkBinaryPath(locator, path, revision) {
|
|
1767
2510
|
await ready;
|
|
1768
|
-
const asked = binaryPath(locator, path, revision);
|
|
2511
|
+
const asked = await binaryPath(locator, path, revision);
|
|
1769
2512
|
if (asked === null || asked.kind === 'pending')
|
|
1770
2513
|
return absentBinary(path);
|
|
1771
2514
|
return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
|
|
1772
2515
|
}
|
|
1773
2516
|
async function readBinaryReference(reference) {
|
|
1774
2517
|
await ready;
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
2518
|
+
const scope = options.adapter.referrerScope(reference.referrer);
|
|
2519
|
+
if (scope === null)
|
|
2520
|
+
return absentReference(reference.fileRef);
|
|
2521
|
+
return readBinary({ kind: 'reference', fileRef: reference.fileRef, referrer: reference.referrer }, { locator: scope.locator, path: scope.path ?? '', revision: reference.fileRef });
|
|
2522
|
+
}
|
|
2523
|
+
function readPart(file, part) {
|
|
2524
|
+
return options.database.ask({ kind: 'file.part', key: file, part });
|
|
2525
|
+
}
|
|
2526
|
+
async function cleanUp() {
|
|
2527
|
+
const freed = await options.database.ask({ kind: 'opened.clean' });
|
|
2528
|
+
return freed + ((await options.shell.cleanUp?.()) ?? 0);
|
|
1780
2529
|
}
|
|
1781
2530
|
function declaredAgents() {
|
|
1782
2531
|
for (const capability of capabilities) {
|
|
@@ -1794,6 +2543,13 @@ export function composeRuntime(options) {
|
|
|
1794
2543
|
}
|
|
1795
2544
|
return undefined;
|
|
1796
2545
|
}
|
|
2546
|
+
function calledRooms(name, input) {
|
|
2547
|
+
const member = declaredAction(name)?.room;
|
|
2548
|
+
if (member === undefined || input === null || typeof input !== 'object')
|
|
2549
|
+
return [];
|
|
2550
|
+
const named = input[member];
|
|
2551
|
+
return (Array.isArray(named) ? named : [named]).filter((room) => typeof room === 'string');
|
|
2552
|
+
}
|
|
1797
2553
|
async function invokeAction(name, input, dispatching) {
|
|
1798
2554
|
await ready;
|
|
1799
2555
|
if (closed || refusal !== null)
|
|
@@ -1813,7 +2569,7 @@ export function composeRuntime(options) {
|
|
|
1813
2569
|
try {
|
|
1814
2570
|
const result = await options.adapter.action(authorization, { name, input });
|
|
1815
2571
|
if (result.kind === 'scoped-refusal') {
|
|
1816
|
-
const room = replicas.get(result.locator)?.
|
|
2572
|
+
const room = replicas.get(result.locator)?.record;
|
|
1817
2573
|
if (room !== undefined)
|
|
1818
2574
|
await scoped(result, room, await openLocal());
|
|
1819
2575
|
}
|
|
@@ -1888,85 +2644,277 @@ export function composeRuntime(options) {
|
|
|
1888
2644
|
}
|
|
1889
2645
|
function reachedBy(locator, roomVersion) {
|
|
1890
2646
|
const held = replicas.get(locator);
|
|
1891
|
-
return (held !== undefined &&
|
|
1892
|
-
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion));
|
|
2647
|
+
return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
|
|
1893
2648
|
}
|
|
1894
|
-
function writtenAfter(locator, roomVersion, path) {
|
|
1895
|
-
const
|
|
2649
|
+
async function writtenAfter(locator, roomVersion, path) {
|
|
2650
|
+
const held = replicas.get(locator);
|
|
2651
|
+
if (held === undefined)
|
|
2652
|
+
return false;
|
|
2653
|
+
const [entry] = await (await openLocal()).entries(held.record.key, 'confirmed', [path]);
|
|
2654
|
+
const written = entry?.written ?? undefined;
|
|
1896
2655
|
return (written !== undefined &&
|
|
1897
2656
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
1898
2657
|
!options.adapter.reached(locator, roomVersion, written));
|
|
1899
2658
|
}
|
|
1900
|
-
|
|
1901
|
-
|
|
2659
|
+
function overriddenTexts(texts, overrides) {
|
|
2660
|
+
return async (paths) => {
|
|
2661
|
+
const known = new Map(await texts(paths.filter((path) => !overrides.has(path))));
|
|
2662
|
+
for (const path of paths) {
|
|
2663
|
+
const entry = overrides.get(path);
|
|
2664
|
+
if (entry === undefined)
|
|
2665
|
+
continue;
|
|
2666
|
+
if (entry === null)
|
|
2667
|
+
known.delete(path);
|
|
2668
|
+
else
|
|
2669
|
+
known.set(path, entry.binary !== null ? null : entry.content);
|
|
2670
|
+
}
|
|
2671
|
+
return known;
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2674
|
+
async function unreadableRoom(texts, walk, paths) {
|
|
2675
|
+
if (paths !== null) {
|
|
2676
|
+
const text = {};
|
|
2677
|
+
for (const [path, content] of await texts(paths))
|
|
2678
|
+
if (content !== null)
|
|
2679
|
+
text[path] = content;
|
|
2680
|
+
return stepped(unreadableIn(text, Object.keys(text)));
|
|
2681
|
+
}
|
|
2682
|
+
for (let after = null;;) {
|
|
2683
|
+
const page = await walk(after);
|
|
2684
|
+
const text = {};
|
|
2685
|
+
for (const { path, content } of page.entries)
|
|
2686
|
+
if (content !== null)
|
|
2687
|
+
text[path] = content;
|
|
2688
|
+
const refused = await stepped(unreadableIn(text, Object.keys(text)));
|
|
2689
|
+
if (refused !== null)
|
|
2690
|
+
return refused;
|
|
2691
|
+
if (page.next === null)
|
|
2692
|
+
return null;
|
|
2693
|
+
after = page.next;
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
async function validatedRoom(local, record, view, previous, overrides = null) {
|
|
2697
|
+
const composition = compositionOf(record);
|
|
2698
|
+
const modeled = record.model.status !== 'unavailable' || record.selectionModel.status !== 'unavailable';
|
|
2699
|
+
const lookup = overrides === null
|
|
2700
|
+
? lookupOf(local, record.key, view)
|
|
2701
|
+
: overriddenLookup(lookupOf(local, record.key, view), overrides);
|
|
2702
|
+
const texts = overrides === null
|
|
2703
|
+
? textsOf(local, record.key, view)
|
|
2704
|
+
: overriddenTexts(textsOf(local, record.key, view), overrides);
|
|
2705
|
+
const walk = overrides === null
|
|
2706
|
+
? walkOf(local, record.key, view)
|
|
2707
|
+
: overriddenWalk(walkOf(local, record.key, view), overrides);
|
|
2708
|
+
let changes = null;
|
|
2709
|
+
let reshaped = false;
|
|
2710
|
+
if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
|
|
2711
|
+
const [was, now] = await Promise.all([
|
|
2712
|
+
local.entries(record.key, 'confirmed', previous.changed),
|
|
2713
|
+
overrides === null
|
|
2714
|
+
? local.entries(record.key, view, previous.changed)
|
|
2715
|
+
: Promise.resolve([]),
|
|
2716
|
+
]);
|
|
2717
|
+
const before = new Map(was.map((entry) => [entry.path, entry]));
|
|
2718
|
+
const after = overrides === null
|
|
2719
|
+
? new Map(now.map((entry) => [entry.path, entry]))
|
|
2720
|
+
: overrides;
|
|
2721
|
+
changes = previous.changed.flatMap((path) => {
|
|
2722
|
+
const one = before.get(path) ?? null;
|
|
2723
|
+
const other = after.get(path) ?? null;
|
|
2724
|
+
reshaped ||= textDiffers(one, other);
|
|
2725
|
+
const held = textOf(one);
|
|
2726
|
+
const next = textOf(other);
|
|
2727
|
+
return held === next ? [] : [{ path, was: held, now: next }];
|
|
2728
|
+
});
|
|
2729
|
+
}
|
|
2730
|
+
const named = changes === null ? null : changes.map(({ path }) => path);
|
|
2731
|
+
const textChanged = changes === null || changes.length > 0 || reshaped;
|
|
2732
|
+
const modelsChanged = previous === null || !sameComposition(compositionOf(previous.record), composition);
|
|
2733
|
+
if (modeled && (textChanged || modelsChanged)) {
|
|
2734
|
+
const clean = previous !== null &&
|
|
2735
|
+
changes !== null &&
|
|
2736
|
+
(await changedCleanly(changes, compositionOf(previous.record), composition, {
|
|
2737
|
+
lookup,
|
|
2738
|
+
texts,
|
|
2739
|
+
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2740
|
+
}));
|
|
2741
|
+
if (clean)
|
|
2742
|
+
return null;
|
|
2743
|
+
const verdict = await validatedCollection(walk, lookup, composition);
|
|
2744
|
+
if (verdict.verdict === 'accept')
|
|
2745
|
+
return null;
|
|
2746
|
+
if (verdict.rule !== 'model-unavailable')
|
|
2747
|
+
return { kind: 'refused', refusal: verdict };
|
|
2748
|
+
return unreadableRoom(texts, walk, named);
|
|
2749
|
+
}
|
|
2750
|
+
if (textChanged)
|
|
2751
|
+
return unreadableRoom(texts, walk, named);
|
|
2752
|
+
return null;
|
|
2753
|
+
}
|
|
2754
|
+
function refreshingOf(local, key, liveness) {
|
|
2755
|
+
return {
|
|
2756
|
+
prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
|
|
2757
|
+
async stage(page) {
|
|
2758
|
+
const batches = batched(page.entries);
|
|
2759
|
+
for (const [index, entries] of batches.entries()) {
|
|
2760
|
+
const last = index === batches.length - 1;
|
|
2761
|
+
const staged = await exclusive(async () => {
|
|
2762
|
+
if (!liveness())
|
|
2763
|
+
return false;
|
|
2764
|
+
await local.refresh(key, last
|
|
2765
|
+
? { ...page, entries }
|
|
2766
|
+
: { entries, removals: [], folders: [], folderRemovals: [] });
|
|
2767
|
+
return true;
|
|
2768
|
+
});
|
|
2769
|
+
if (!staged)
|
|
2770
|
+
return false;
|
|
2771
|
+
}
|
|
2772
|
+
return true;
|
|
2773
|
+
},
|
|
2774
|
+
};
|
|
2775
|
+
}
|
|
2776
|
+
function batched(entries) {
|
|
2777
|
+
const batches = [[]];
|
|
2778
|
+
let spent = 0;
|
|
2779
|
+
for (const entry of entries) {
|
|
2780
|
+
const size = (entry.content?.length ?? 0) + ROW_WEIGHT;
|
|
2781
|
+
if (spent + size > STAGE_BYTES && batches.at(-1).length > 0) {
|
|
2782
|
+
batches.push([]);
|
|
2783
|
+
spent = 0;
|
|
2784
|
+
}
|
|
2785
|
+
batches.at(-1).push(entry);
|
|
2786
|
+
spent += size;
|
|
2787
|
+
}
|
|
2788
|
+
return batches;
|
|
2789
|
+
}
|
|
2790
|
+
async function pullRefresh(authorized, held, liveness) {
|
|
2791
|
+
if (!liveness())
|
|
1902
2792
|
return unconfirmed;
|
|
1903
2793
|
const turn = generation;
|
|
2794
|
+
const { record } = held;
|
|
2795
|
+
const base = held.confirmed;
|
|
2796
|
+
const local = await openLocal();
|
|
1904
2797
|
try {
|
|
1905
|
-
|
|
1906
|
-
const
|
|
1907
|
-
|
|
1908
|
-
|
|
2798
|
+
await exclusive(() => local.unrefresh(record.key));
|
|
2799
|
+
const result = await advancement(reads, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, liveness));
|
|
2800
|
+
if (result.kind !== 'advanced') {
|
|
2801
|
+
journal.record(`transfer refresh ${record.locator} ${result.kind}`);
|
|
2802
|
+
return result;
|
|
2803
|
+
}
|
|
2804
|
+
const refused = await validatedRoom(local, result.record, 'refreshed', {
|
|
2805
|
+
record,
|
|
2806
|
+
changed: [...result.changed],
|
|
2807
|
+
});
|
|
2808
|
+
if (refused !== null) {
|
|
2809
|
+
journal.record(`transfer refresh ${record.locator} refused`);
|
|
2810
|
+
return refused;
|
|
2811
|
+
}
|
|
2812
|
+
journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
|
|
2813
|
+
return { kind: 'advanced', record: result.record, base };
|
|
1909
2814
|
}
|
|
1910
2815
|
catch (error) {
|
|
1911
|
-
journal.record(`transfer refresh ${
|
|
2816
|
+
journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
|
|
1912
2817
|
throw error;
|
|
1913
2818
|
}
|
|
1914
2819
|
finally {
|
|
1915
|
-
reads.finished(
|
|
1916
|
-
}
|
|
2820
|
+
reads.finished(record.locator, turn);
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
const heldContents = new Map();
|
|
2824
|
+
function heldContentOf(local, key) {
|
|
2825
|
+
const known = heldContents.get(key);
|
|
2826
|
+
if (known !== undefined)
|
|
2827
|
+
return known;
|
|
2828
|
+
if (heldContents.size > ROOM_READS)
|
|
2829
|
+
heldContents.clear();
|
|
2830
|
+
const made = {
|
|
2831
|
+
files: (after, limit) => local.files(key, 'confirmed', after, limit),
|
|
2832
|
+
async contents(paths) {
|
|
2833
|
+
return new Map((await local.entries(key, 'confirmed', paths)).flatMap(({ path, content, revision }) => content === null || revision === null ? [] : [[path, { content, revision }]]));
|
|
2834
|
+
},
|
|
2835
|
+
};
|
|
2836
|
+
heldContents.set(key, made);
|
|
2837
|
+
return made;
|
|
1917
2838
|
}
|
|
1918
|
-
function staging(local,
|
|
2839
|
+
function staging(local, liveness) {
|
|
1919
2840
|
return {
|
|
1920
2841
|
held: (locator) => transfers.get(locator) ?? null,
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
const
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
2842
|
+
async stage(record, page, composition) {
|
|
2843
|
+
const bytes = pageBytes(page);
|
|
2844
|
+
const batches = batched(page.entries);
|
|
2845
|
+
let key = record.key;
|
|
2846
|
+
for (const [index, entries] of batches.entries()) {
|
|
2847
|
+
const last = index === batches.length - 1;
|
|
2848
|
+
const staged = await exclusive(async () => {
|
|
2849
|
+
if (!liveness())
|
|
2850
|
+
return null;
|
|
2851
|
+
if (index === 0 && !(await admitted(local, record.locator, bytes)))
|
|
2852
|
+
return null;
|
|
2853
|
+
try {
|
|
2854
|
+
return await local.stage(record, composition, { entries, folders: last ? page.folders : [] }, key, last);
|
|
2855
|
+
}
|
|
2856
|
+
catch (error) {
|
|
2857
|
+
if (!exceeded(error))
|
|
2858
|
+
throw error;
|
|
2859
|
+
await exceededOn(record.locator, bytes);
|
|
2860
|
+
return null;
|
|
2861
|
+
}
|
|
2862
|
+
});
|
|
2863
|
+
if (staged === null)
|
|
2864
|
+
return null;
|
|
2865
|
+
key = staged;
|
|
1936
2866
|
}
|
|
1937
|
-
transfers.set(record.locator, record);
|
|
1938
|
-
|
|
1939
|
-
return
|
|
1940
|
-
}
|
|
2867
|
+
transfers.set(record.locator, { ...record, key });
|
|
2868
|
+
liveQueries.recover();
|
|
2869
|
+
return key;
|
|
2870
|
+
},
|
|
2871
|
+
async refreshing(key) {
|
|
2872
|
+
await exclusive(() => local.unrefresh(key));
|
|
2873
|
+
return refreshingOf(local, key, liveness);
|
|
2874
|
+
},
|
|
1941
2875
|
abandon: (locator) => exclusive(async () => {
|
|
1942
|
-
if (!
|
|
2876
|
+
if (!liveness())
|
|
1943
2877
|
return;
|
|
1944
2878
|
await local.abandon(locator);
|
|
1945
2879
|
transfers.delete(locator);
|
|
1946
|
-
|
|
2880
|
+
liveQueries.recover();
|
|
2881
|
+
sweep();
|
|
2882
|
+
}),
|
|
2883
|
+
setAside: (locator) => exclusive(async () => {
|
|
2884
|
+
if (!liveness())
|
|
2885
|
+
return;
|
|
2886
|
+
await local.abandon(locator, true);
|
|
2887
|
+
transfers.delete(locator);
|
|
2888
|
+
liveQueries.recover();
|
|
1947
2889
|
}),
|
|
2890
|
+
release: (key) => exclusive(async () => {
|
|
2891
|
+
await local.release(key);
|
|
2892
|
+
sweep();
|
|
2893
|
+
}),
|
|
2894
|
+
content: (key) => heldContentOf(local, key),
|
|
2895
|
+
validate: (record, view) => validatedRoom(local, record, view, null),
|
|
1948
2896
|
};
|
|
1949
2897
|
}
|
|
1950
|
-
function reusable(room) {
|
|
1951
|
-
const record = replicas.get(room.locator)?.
|
|
2898
|
+
function reusable(room, local) {
|
|
2899
|
+
const record = replicas.get(room.locator)?.record;
|
|
1952
2900
|
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1953
2901
|
return null;
|
|
1954
|
-
return
|
|
2902
|
+
return heldContentOf(local, record.key);
|
|
1955
2903
|
}
|
|
1956
|
-
async function pullBootstrap(authorized, room, local,
|
|
1957
|
-
if (!
|
|
2904
|
+
async function pullBootstrap(authorized, room, local, liveness) {
|
|
2905
|
+
if (!liveness())
|
|
1958
2906
|
return unconfirmed;
|
|
1959
2907
|
const turn = generation;
|
|
1960
2908
|
if (replicas.has(room.locator)) {
|
|
1961
2909
|
catching.add(room.locator);
|
|
1962
|
-
|
|
2910
|
+
liveQueries.recover();
|
|
1963
2911
|
}
|
|
1964
2912
|
try {
|
|
1965
2913
|
const result = await bootstrapRoom(reads, authorized, room, {
|
|
1966
2914
|
collection: modelOf(room.locator),
|
|
1967
2915
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
1968
|
-
}, (models, held) => (
|
|
1969
|
-
const detail = result.kind === 'staged' ? result.
|
|
2916
|
+
}, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
|
|
2917
|
+
const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
|
|
1970
2918
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1971
2919
|
if (result.kind !== 'staged')
|
|
1972
2920
|
caughtUpTo(room.locator);
|
|
@@ -1983,10 +2931,10 @@ export function composeRuntime(options) {
|
|
|
1983
2931
|
}
|
|
1984
2932
|
function caughtUpTo(locator) {
|
|
1985
2933
|
if (catching.delete(locator))
|
|
1986
|
-
|
|
2934
|
+
liveQueries.recover();
|
|
1987
2935
|
}
|
|
1988
2936
|
function behind(room) {
|
|
1989
|
-
const held = replicas.get(room.locator)?.
|
|
2937
|
+
const held = replicas.get(room.locator)?.record.roomVersion;
|
|
1990
2938
|
return (held !== undefined &&
|
|
1991
2939
|
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
1992
2940
|
}
|
|
@@ -1999,12 +2947,11 @@ export function composeRuntime(options) {
|
|
|
1999
2947
|
return advanceOnce(room, local, authorized, live);
|
|
2000
2948
|
}
|
|
2001
2949
|
async function advanceOnce(room, local, authorized, live) {
|
|
2002
|
-
await referenced(room.locator);
|
|
2003
2950
|
if (!live() || paused.has(room.locator))
|
|
2004
2951
|
return unconfirmed;
|
|
2005
2952
|
const held = replicas.get(room.locator);
|
|
2006
2953
|
if (held !== undefined) {
|
|
2007
|
-
const refreshed = await pullRefresh(authorized, held
|
|
2954
|
+
const refreshed = await pullRefresh(authorized, held, live);
|
|
2008
2955
|
if (!live())
|
|
2009
2956
|
return unconfirmed;
|
|
2010
2957
|
if (refreshed.kind === 'still-current') {
|
|
@@ -2019,10 +2966,11 @@ export function composeRuntime(options) {
|
|
|
2019
2966
|
return null;
|
|
2020
2967
|
}
|
|
2021
2968
|
if (refreshed.kind === 'advanced') {
|
|
2022
|
-
|
|
2023
|
-
|
|
2969
|
+
const landing = { kind: 'refreshed', base: refreshed.base };
|
|
2970
|
+
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
2971
|
+
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
2024
2972
|
const reached = now !== undefined &&
|
|
2025
|
-
options.adapter.reached(room.locator, now, refreshed.
|
|
2973
|
+
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
2026
2974
|
return reached ? null : advance(room, local, authorized, live);
|
|
2027
2975
|
}
|
|
2028
2976
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
@@ -2032,16 +2980,18 @@ export function composeRuntime(options) {
|
|
|
2032
2980
|
return refreshed;
|
|
2033
2981
|
if (opened !== null) {
|
|
2034
2982
|
owed.add(room.locator);
|
|
2035
|
-
|
|
2983
|
+
liveQueries.recover();
|
|
2036
2984
|
return unconfirmed;
|
|
2037
2985
|
}
|
|
2986
|
+
if (reloadsDue.get(room.locator) === generation)
|
|
2987
|
+
return unconfirmed;
|
|
2038
2988
|
}
|
|
2039
2989
|
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
2040
2990
|
if (!live())
|
|
2041
2991
|
return unconfirmed;
|
|
2042
2992
|
if (bootstrapped.kind !== 'staged')
|
|
2043
2993
|
return bootstrapped;
|
|
2044
|
-
await publish(local, room, bootstrapped.
|
|
2994
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
2045
2995
|
return null;
|
|
2046
2996
|
}
|
|
2047
2997
|
async function withhold(withheld, room, local, progress) {
|
|
@@ -2114,57 +3064,254 @@ export function composeRuntime(options) {
|
|
|
2114
3064
|
(accepted.author === undefined ? '' : ` author ${accepted.author}`) +
|
|
2115
3065
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
2116
3066
|
}
|
|
2117
|
-
|
|
2118
|
-
|
|
3067
|
+
function settling(acceptance) {
|
|
3068
|
+
return acceptance !== null && 'apply' in acceptance;
|
|
3069
|
+
}
|
|
3070
|
+
function refusalOf(acceptance) {
|
|
3071
|
+
return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
|
|
3072
|
+
}
|
|
3073
|
+
async function acceptance(room, local, submitted, accepted) {
|
|
2119
3074
|
if (!continuing())
|
|
2120
3075
|
return unconfirmed;
|
|
2121
3076
|
const current = replicas.get(room.locator);
|
|
2122
3077
|
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
2123
3078
|
return null;
|
|
2124
3079
|
}
|
|
2125
|
-
const held = current.
|
|
3080
|
+
const held = current.record.roomVersion;
|
|
2126
3081
|
logAccepted(room.locator, submitted, accepted);
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
3082
|
+
let record = current.record;
|
|
3083
|
+
let changes;
|
|
3084
|
+
if (!options.adapter.reached(room.locator, held, accepted.roomVersion)) {
|
|
3085
|
+
const paths = [...returnedPaths(submitted, accepted)];
|
|
3086
|
+
const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
|
|
3087
|
+
const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
|
|
3088
|
+
record = {
|
|
3089
|
+
...record,
|
|
3090
|
+
roomVersion: options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
3091
|
+
? accepted.roomVersion
|
|
3092
|
+
: held,
|
|
3093
|
+
};
|
|
3094
|
+
const overrides = new Map([
|
|
3095
|
+
...back.set.map((entry) => [entry.path, entry]),
|
|
3096
|
+
...back.removed.map((path) => [path, null]),
|
|
3097
|
+
]);
|
|
3098
|
+
const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
|
|
3099
|
+
if (refused !== null) {
|
|
3100
|
+
const key = pendingKey(room.locator, submitted.id);
|
|
3101
|
+
return {
|
|
3102
|
+
locator: room.locator,
|
|
3103
|
+
left: [],
|
|
3104
|
+
commit: {
|
|
3105
|
+
rooms: [],
|
|
3106
|
+
writes: [
|
|
3107
|
+
{
|
|
3108
|
+
locator: room.locator,
|
|
3109
|
+
accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
|
|
3110
|
+
},
|
|
3111
|
+
],
|
|
3112
|
+
},
|
|
3113
|
+
refused,
|
|
3114
|
+
apply() {
|
|
3115
|
+
const state = pendings.get(key);
|
|
3116
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
3117
|
+
pendings.set(key, { ...state, accepted: accepted.roomVersion });
|
|
3118
|
+
return changeSet(room.locator, { queue: true });
|
|
3119
|
+
},
|
|
3120
|
+
};
|
|
3121
|
+
}
|
|
3122
|
+
changes = back;
|
|
3123
|
+
}
|
|
3124
|
+
const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
|
|
3125
|
+
return {
|
|
3126
|
+
...settled,
|
|
3127
|
+
apply(answer) {
|
|
3128
|
+
const set = settled.apply(answer);
|
|
3129
|
+
pendings.delete(pendingKey(room.locator, submitted.id));
|
|
3130
|
+
unreadable.delete(room.locator);
|
|
3131
|
+
sourceRefusals.delete(room.locator);
|
|
3132
|
+
declarationRefusals.delete(room.locator);
|
|
3133
|
+
list(room, true);
|
|
3134
|
+
return set;
|
|
3135
|
+
},
|
|
3136
|
+
};
|
|
3137
|
+
}
|
|
3138
|
+
async function adoptedTogether(local, acceptances) {
|
|
3139
|
+
const settlements = acceptances.filter(settling);
|
|
3140
|
+
if (settlements.length > 0) {
|
|
3141
|
+
publishChangeSet(await together(local, settlements));
|
|
3142
|
+
moveTo({ ...status, ...attention() });
|
|
3143
|
+
}
|
|
3144
|
+
await unpair(local);
|
|
2150
3145
|
}
|
|
2151
3146
|
async function adopted(room, local, submitted, accepted) {
|
|
2152
|
-
const failed = await exclusive(() =>
|
|
3147
|
+
const failed = await exclusive(async () => {
|
|
3148
|
+
const adopting = await acceptance(room, local, submitted, accepted);
|
|
3149
|
+
await adoptedTogether(local, [adopting]);
|
|
3150
|
+
return refusalOf(adopting);
|
|
3151
|
+
});
|
|
3152
|
+
if (failed === null)
|
|
3153
|
+
return true;
|
|
3154
|
+
if (failed.kind === 'refused' && continuing())
|
|
3155
|
+
refusedSource(room, failed);
|
|
3156
|
+
return false;
|
|
3157
|
+
}
|
|
3158
|
+
async function adoptedMove(room, partner, local, submitted, accepted) {
|
|
3159
|
+
const other = discovered.get(partner);
|
|
3160
|
+
const failed = await exclusive(async () => {
|
|
3161
|
+
const left = await acceptance(room, local, submitted, accepted);
|
|
3162
|
+
const arriving = replicas.get(partner)?.queue.find((batch) => batch.id === submitted.id);
|
|
3163
|
+
const arrived = other === undefined || arriving === undefined || accepted.destination === undefined
|
|
3164
|
+
? null
|
|
3165
|
+
: await acceptance(other, local, arriving, {
|
|
3166
|
+
kind: 'accepted',
|
|
3167
|
+
...accepted.destination,
|
|
3168
|
+
});
|
|
3169
|
+
await adoptedTogether(local, [left, arrived]);
|
|
3170
|
+
const refusedArrival = refusalOf(arrived);
|
|
3171
|
+
if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
|
|
3172
|
+
refusedSource(other, refusedArrival);
|
|
3173
|
+
}
|
|
3174
|
+
return refusalOf(left);
|
|
3175
|
+
});
|
|
3176
|
+
submitLater(partner);
|
|
2153
3177
|
if (failed === null)
|
|
2154
3178
|
return true;
|
|
2155
3179
|
if (failed.kind === 'refused' && continuing())
|
|
2156
3180
|
refusedSource(room, failed);
|
|
2157
3181
|
return false;
|
|
2158
3182
|
}
|
|
3183
|
+
function queueIn(locator) {
|
|
3184
|
+
return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
3185
|
+
}
|
|
3186
|
+
function behindIn(locator, batch) {
|
|
3187
|
+
const held = replicas.get(locator)?.queue;
|
|
3188
|
+
if (held === undefined)
|
|
3189
|
+
return queuesWithoutReplica.get(locator) !== undefined;
|
|
3190
|
+
return held.findIndex((one) => one.id === batch.id) > 0;
|
|
3191
|
+
}
|
|
3192
|
+
function unpaired(skipped) {
|
|
3193
|
+
for (const locator of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
|
|
3194
|
+
for (const batch of queueIn(locator)) {
|
|
3195
|
+
const partner = partnerOf(batch, locator);
|
|
3196
|
+
if (partner === null || skipped.has(pendingKey(locator, batch.id)))
|
|
3197
|
+
continue;
|
|
3198
|
+
if (queueIn(partner).some((held) => held.id === batch.id))
|
|
3199
|
+
continue;
|
|
3200
|
+
if (unpairedIn(locator, batch))
|
|
3201
|
+
return { locator, batch };
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
return null;
|
|
3205
|
+
}
|
|
3206
|
+
async function unpairing(local, locator, batches) {
|
|
3207
|
+
const ids = new Set(batches.map(({ id }) => id));
|
|
3208
|
+
const leaving = batches.map((batch) => ({
|
|
3209
|
+
batch,
|
|
3210
|
+
returned: !localSaves && leavingIn(batch, locator) ? returnedBy('withdrawn') : null,
|
|
3211
|
+
}));
|
|
3212
|
+
const outcome = localSaves ? 'dropped' : 'unpaired';
|
|
3213
|
+
const current = replicas.get(locator);
|
|
3214
|
+
if (current !== undefined) {
|
|
3215
|
+
const settled = await settlement(local, locator, current, current.record, current.queue.filter((one) => !ids.has(one.id)), { leaving });
|
|
3216
|
+
return {
|
|
3217
|
+
...settled,
|
|
3218
|
+
apply(answer) {
|
|
3219
|
+
for (const batch of batches)
|
|
3220
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
3221
|
+
const set = settled.apply(answer);
|
|
3222
|
+
submitLater(locator);
|
|
3223
|
+
return set;
|
|
3224
|
+
},
|
|
3225
|
+
};
|
|
3226
|
+
}
|
|
3227
|
+
const queue = queueIn(locator).filter((one) => !ids.has(one.id));
|
|
3228
|
+
const written = new Map();
|
|
3229
|
+
for (const { batch, returned } of leaving) {
|
|
3230
|
+
if (returned === null)
|
|
3231
|
+
continue;
|
|
3232
|
+
for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
|
|
3233
|
+
written.set(saved.path, {
|
|
3234
|
+
locator,
|
|
3235
|
+
path: saved.path,
|
|
3236
|
+
revision: roomDraftRevision(),
|
|
3237
|
+
parts: returnedInto((written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path))?.parts, saved.parts),
|
|
3238
|
+
returned,
|
|
3239
|
+
});
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
const carried = carriedBack(locator, leaving.flatMap(({ batch, returned }) => (returned === null ? [] : [batch])), written);
|
|
3243
|
+
return {
|
|
3244
|
+
locator,
|
|
3245
|
+
left: batches,
|
|
3246
|
+
commit: {
|
|
3247
|
+
rooms: [],
|
|
3248
|
+
writes: [
|
|
3249
|
+
{
|
|
3250
|
+
locator,
|
|
3251
|
+
queue,
|
|
3252
|
+
roomDrafts: [...written.values()],
|
|
3253
|
+
consumedRoomDrafts: carried.taken,
|
|
3254
|
+
},
|
|
3255
|
+
...carried.writes,
|
|
3256
|
+
],
|
|
3257
|
+
},
|
|
3258
|
+
apply() {
|
|
3259
|
+
for (const batch of batches) {
|
|
3260
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
3261
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
3262
|
+
}
|
|
3263
|
+
if (queue.length > 0)
|
|
3264
|
+
queuesWithoutReplica.set(locator, queue);
|
|
3265
|
+
else
|
|
3266
|
+
queuesWithoutReplica.delete(locator);
|
|
3267
|
+
for (const record of written.values())
|
|
3268
|
+
rememberRoomDraft(record);
|
|
3269
|
+
carried.remember();
|
|
3270
|
+
submitLater(locator);
|
|
3271
|
+
return mergeChangeSets([
|
|
3272
|
+
changeSet(locator, {
|
|
3273
|
+
queue: true,
|
|
3274
|
+
drafts: [...written.keys(), ...carried.taken],
|
|
3275
|
+
}),
|
|
3276
|
+
...carried.changes,
|
|
3277
|
+
]);
|
|
3278
|
+
},
|
|
3279
|
+
};
|
|
3280
|
+
}
|
|
3281
|
+
async function unpair(local) {
|
|
3282
|
+
const handled = new Set();
|
|
3283
|
+
for (let held = unpaired(handled); held !== null && continuing(); held = unpaired(handled)) {
|
|
3284
|
+
const { locator, batch } = held;
|
|
3285
|
+
handled.add(pendingKey(locator, batch.id));
|
|
3286
|
+
publishChangeSet(await together(local, [await unpairing(local, locator, [batch])]));
|
|
3287
|
+
}
|
|
3288
|
+
if (handled.size === 0)
|
|
3289
|
+
return;
|
|
3290
|
+
moveTo({ ...status, ...attention() });
|
|
3291
|
+
}
|
|
3292
|
+
async function discardHeld(head, room, local, signal) {
|
|
3293
|
+
const partner = partnerOf(head, room.locator);
|
|
3294
|
+
const other = partner === null ? undefined : discovered.get(partner);
|
|
3295
|
+
await discardPending(other === undefined ? [room] : [room, other], local, signal);
|
|
3296
|
+
}
|
|
2159
3297
|
async function submitRoom(room, local, authorized, progress, signal) {
|
|
2160
3298
|
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
2161
3299
|
const replica = replicas.get(room.locator);
|
|
2162
3300
|
const head = replica?.queue[0];
|
|
2163
3301
|
if (replica === undefined || head === undefined)
|
|
2164
3302
|
return 'done';
|
|
2165
|
-
|
|
3303
|
+
const state = pendings.get(pendingKey(room.locator, head.id));
|
|
3304
|
+
if (state?.accepted !== undefined)
|
|
3305
|
+
return 'done';
|
|
3306
|
+
const partner = partnerOf(head, room.locator);
|
|
3307
|
+
if (partner !== null && !leavingIn(head, room.locator)) {
|
|
3308
|
+
submitLater(partner);
|
|
3309
|
+
return 'done';
|
|
3310
|
+
}
|
|
3311
|
+
if (partner !== null && behindIn(partner, head)) {
|
|
3312
|
+
progress.complete = false;
|
|
2166
3313
|
return 'done';
|
|
2167
|
-
|
|
3314
|
+
}
|
|
2168
3315
|
const { submission, lost: earlier } = state ?? {
|
|
2169
3316
|
submission: { state: 'waiting' },
|
|
2170
3317
|
lost: false,
|
|
@@ -2180,16 +3327,16 @@ export function composeRuntime(options) {
|
|
|
2180
3327
|
}
|
|
2181
3328
|
const lost = earlier || submission.state === 'submitted';
|
|
2182
3329
|
if (lost && pastHorizon(head.id)) {
|
|
2183
|
-
await
|
|
3330
|
+
await discardHeld(head, room, local, signal);
|
|
2184
3331
|
return 'done';
|
|
2185
3332
|
}
|
|
2186
3333
|
let result;
|
|
2187
3334
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
2188
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3335
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
|
|
2189
3336
|
if (discoveryPaused || fences.has(room.locator))
|
|
2190
3337
|
return 'done';
|
|
2191
3338
|
if (signal.aborted || writesHeld()) {
|
|
2192
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3339
|
+
await exclusive(() => mark(local, room.locator, head, submission, earlier));
|
|
2193
3340
|
if (signal.aborted)
|
|
2194
3341
|
return 'done';
|
|
2195
3342
|
progress.complete = false;
|
|
@@ -2197,11 +3344,17 @@ export function composeRuntime(options) {
|
|
|
2197
3344
|
return 'done';
|
|
2198
3345
|
}
|
|
2199
3346
|
const opensBefore = openSignals;
|
|
2200
|
-
const
|
|
3347
|
+
const answeredHere = awaitingAnswer(room.locator);
|
|
3348
|
+
const answeredThere = partner === null ? () => { } : awaitingAnswer(partner);
|
|
3349
|
+
const answered = () => {
|
|
3350
|
+
answeredHere();
|
|
3351
|
+
answeredThere();
|
|
3352
|
+
};
|
|
2201
3353
|
try {
|
|
2202
3354
|
result = await options.adapter.mutate(authorized, {
|
|
2203
3355
|
locator: room.locator,
|
|
2204
3356
|
changeBatch: head,
|
|
3357
|
+
file: batchFile(head),
|
|
2205
3358
|
...(state?.upload === undefined ? {} : { upload: state.upload }),
|
|
2206
3359
|
}, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
|
|
2207
3360
|
}
|
|
@@ -2226,9 +3379,10 @@ export function composeRuntime(options) {
|
|
|
2226
3379
|
return 'again';
|
|
2227
3380
|
}
|
|
2228
3381
|
if (result.kind === 'room-not-readable') {
|
|
3382
|
+
const unread = discovered.get(result.locator) ?? room;
|
|
2229
3383
|
progress.complete = false;
|
|
2230
|
-
unreadable.add(
|
|
2231
|
-
list(
|
|
3384
|
+
unreadable.add(unread.locator);
|
|
3385
|
+
list(unread, false);
|
|
2232
3386
|
emitRooms();
|
|
2233
3387
|
return 'done';
|
|
2234
3388
|
}
|
|
@@ -2243,24 +3397,30 @@ export function composeRuntime(options) {
|
|
|
2243
3397
|
}
|
|
2244
3398
|
if (result.kind === 'write-halted') {
|
|
2245
3399
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
2246
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3400
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
|
|
2247
3401
|
if (serviceMode === 'open' && openSignals !== opensBefore)
|
|
2248
3402
|
continue;
|
|
2249
3403
|
haltedRefusal = true;
|
|
2250
3404
|
progress.complete = false;
|
|
2251
3405
|
return 'done';
|
|
2252
3406
|
}
|
|
3407
|
+
const other = partner === null ? undefined : discovered.get(partner);
|
|
2253
3408
|
if (result.kind === 'conflicted' ||
|
|
2254
3409
|
result.kind === 'rejected' ||
|
|
2255
3410
|
result.kind === 'expired' ||
|
|
2256
3411
|
result.kind === 'retryable-refusal') {
|
|
2257
3412
|
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
2258
3413
|
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
2259
|
-
await
|
|
3414
|
+
await discardHeld(head, room, local, signal);
|
|
2260
3415
|
return 'done';
|
|
2261
3416
|
}
|
|
2262
3417
|
if (result.kind === 'expired') {
|
|
2263
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3418
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
|
|
3419
|
+
const there = other === undefined ? null : await advance(other, local, authorized, continuing);
|
|
3420
|
+
if (other !== undefined && there !== null) {
|
|
3421
|
+
await withhold(there, other, local, progress);
|
|
3422
|
+
return 'done';
|
|
3423
|
+
}
|
|
2264
3424
|
const withheld = await advance(room, local, authorized, continuing);
|
|
2265
3425
|
if (withheld !== null) {
|
|
2266
3426
|
await withhold(withheld, room, local, progress);
|
|
@@ -2277,11 +3437,25 @@ export function composeRuntime(options) {
|
|
|
2277
3437
|
if (result.kind === 'retryable-refusal') {
|
|
2278
3438
|
const reset = result.reset;
|
|
2279
3439
|
progress.complete = false;
|
|
2280
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3440
|
+
await exclusive(() => mark(local, room.locator, head, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
2281
3441
|
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
2282
3442
|
}
|
|
2283
3443
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
2284
3444
|
const left = leaving(head, result);
|
|
3445
|
+
const refreshed = async () => ({
|
|
3446
|
+
there: other === undefined ? null : await advance(other, local, authorized, continuing),
|
|
3447
|
+
here: await advance(room, local, authorized, continuing),
|
|
3448
|
+
});
|
|
3449
|
+
const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
|
|
3450
|
+
if (judged !== null && (judged.there !== null || judged.here !== null)) {
|
|
3451
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
|
|
3452
|
+
if (other !== undefined && judged.there !== null) {
|
|
3453
|
+
await withhold(judged.there, other, local, progress);
|
|
3454
|
+
}
|
|
3455
|
+
if (judged.here !== null)
|
|
3456
|
+
await withhold(judged.here, room, local, progress);
|
|
3457
|
+
return 'done';
|
|
3458
|
+
}
|
|
2285
3459
|
await exclusive(async () => {
|
|
2286
3460
|
if (!continuing())
|
|
2287
3461
|
return;
|
|
@@ -2289,25 +3463,29 @@ export function composeRuntime(options) {
|
|
|
2289
3463
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
2290
3464
|
return;
|
|
2291
3465
|
}
|
|
2292
|
-
const
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
});
|
|
2298
|
-
|
|
3466
|
+
const confirmedLayer = await sliceAt(current.record, 'confirmed', [head]);
|
|
3467
|
+
const held = (result.kind === 'expired' &&
|
|
3468
|
+
(await landedWhole(room.locator, head, confirmedLayer))) ||
|
|
3469
|
+
(result.kind === 'conflicted' &&
|
|
3470
|
+
(await landedWhole(room.locator, head, confirmedLayer, result.affected)));
|
|
3471
|
+
publishChangeSet(await settle(local, room.locator, current, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
|
|
3472
|
+
await unpair(local);
|
|
2299
3473
|
moveTo({ ...status, ...attention() });
|
|
2300
3474
|
});
|
|
2301
3475
|
if (result.kind === 'expired')
|
|
2302
3476
|
continue;
|
|
2303
|
-
const
|
|
2304
|
-
if (
|
|
2305
|
-
await withhold(
|
|
3477
|
+
const { there, here } = judged ?? (await refreshed());
|
|
3478
|
+
if (other !== undefined && there !== null)
|
|
3479
|
+
await withhold(there, other, local, progress);
|
|
3480
|
+
if (here !== null) {
|
|
3481
|
+
await withhold(here, room, local, progress);
|
|
2306
3482
|
return 'done';
|
|
2307
3483
|
}
|
|
2308
3484
|
continue;
|
|
2309
3485
|
}
|
|
2310
|
-
const kept = await
|
|
3486
|
+
const kept = await (partner === null
|
|
3487
|
+
? adopted(room, local, head, result)
|
|
3488
|
+
: adoptedMove(room, partner, local, head, result)).finally(answered);
|
|
2311
3489
|
if (!kept) {
|
|
2312
3490
|
progress.complete = false;
|
|
2313
3491
|
return 'done';
|
|
@@ -2318,44 +3496,67 @@ export function composeRuntime(options) {
|
|
|
2318
3496
|
function pastHorizon(identity) {
|
|
2319
3497
|
return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
|
|
2320
3498
|
}
|
|
2321
|
-
async function discardPending(
|
|
2322
|
-
const { locator } = room;
|
|
3499
|
+
async function discardPending(rooms, local, signal) {
|
|
2323
3500
|
const discarded = await exclusive(async () => {
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
3501
|
+
const held = rooms
|
|
3502
|
+
.filter(({ locator }) => !fences.has(locator))
|
|
3503
|
+
.map((room) => ({
|
|
3504
|
+
room,
|
|
3505
|
+
before: replicas.get(room.locator),
|
|
3506
|
+
queue: queueIn(room.locator),
|
|
3507
|
+
}));
|
|
3508
|
+
if (held.length === 0)
|
|
3509
|
+
return [];
|
|
3510
|
+
const answer = await local.commit({
|
|
3511
|
+
rooms: held.flatMap(({ room, before }) => before === undefined
|
|
3512
|
+
? []
|
|
3513
|
+
: [
|
|
3514
|
+
{
|
|
3515
|
+
locator: room.locator,
|
|
3516
|
+
key: before.record.key,
|
|
3517
|
+
roots: before.record.typedSelection?.roots ?? [],
|
|
3518
|
+
overlay: { set: [], cleared: [...before.overlaid] },
|
|
3519
|
+
},
|
|
3520
|
+
]),
|
|
3521
|
+
discards: held.map(({ room }) => room.locator),
|
|
3522
|
+
});
|
|
3523
|
+
for (const { room, before, queue } of held) {
|
|
3524
|
+
const { locator } = room;
|
|
3525
|
+
lastFenced.set(locator, ++fencings);
|
|
3526
|
+
for (const batch of queue)
|
|
3527
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
3528
|
+
queuesWithoutReplica.delete(locator);
|
|
3529
|
+
if (before !== undefined) {
|
|
3530
|
+
replicas.set(locator, {
|
|
3531
|
+
...before,
|
|
3532
|
+
queue: [],
|
|
3533
|
+
overlaid: new Set(),
|
|
3534
|
+
count: answer.counts[locator] ?? before.count,
|
|
3535
|
+
});
|
|
3536
|
+
}
|
|
3537
|
+
journal.record(`room discarded ${locator}`);
|
|
2335
3538
|
}
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
if (after !== null)
|
|
2341
|
-
replicas.set(locator, after);
|
|
2342
|
-
journal.record(`room discarded ${locator}`);
|
|
2343
|
-
publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
|
|
3539
|
+
publishChangeSet(mergeChangeSets(held.map(({ room }) => changeSet(room.locator, {
|
|
3540
|
+
documents: answer.changed[room.locator] ?? [],
|
|
3541
|
+
queue: true,
|
|
3542
|
+
}))));
|
|
2344
3543
|
moveTo({ ...status, ...attention() });
|
|
2345
|
-
return
|
|
2346
|
-
});
|
|
2347
|
-
if (!discarded)
|
|
2348
|
-
return;
|
|
2349
|
-
const progress = { complete: true, reachable: true };
|
|
2350
|
-
await roomTask(locator, async (live) => {
|
|
2351
|
-
await Promise.race([
|
|
2352
|
-
reload(room, local, progress, () => live() && !signal.aborted),
|
|
2353
|
-
abandonedOn(signal),
|
|
2354
|
-
]);
|
|
3544
|
+
return held.map(({ room }) => room);
|
|
2355
3545
|
});
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
3546
|
+
for (const room of discarded) {
|
|
3547
|
+
if (signal.aborted)
|
|
3548
|
+
return;
|
|
3549
|
+
const progress = { complete: true, reachable: true };
|
|
3550
|
+
await roomTask(room.locator, async (live) => {
|
|
3551
|
+
await Promise.race([
|
|
3552
|
+
reload(room, local, progress, () => live() && !signal.aborted),
|
|
3553
|
+
abandonedOn(signal),
|
|
3554
|
+
]);
|
|
3555
|
+
});
|
|
3556
|
+
if (signal.aborted)
|
|
3557
|
+
return;
|
|
3558
|
+
await rest(progress);
|
|
3559
|
+
}
|
|
2359
3560
|
}
|
|
2360
3561
|
function awaitingAnswer(locator) {
|
|
2361
3562
|
let release = () => { };
|
|
@@ -2372,7 +3573,7 @@ export function composeRuntime(options) {
|
|
|
2372
3573
|
async function retainModel(local, room, attached, next) {
|
|
2373
3574
|
const key = modelKey(room.locator, attached);
|
|
2374
3575
|
const previous = modelOf(room.locator, attached);
|
|
2375
|
-
const heldBefore = replicas.get(room.locator)?.
|
|
3576
|
+
const heldBefore = replicas.get(room.locator)?.record;
|
|
2376
3577
|
if (JSON.stringify(previous) === JSON.stringify(next) &&
|
|
2377
3578
|
(heldBefore === undefined ||
|
|
2378
3579
|
(JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
|
|
@@ -2388,36 +3589,57 @@ export function composeRuntime(options) {
|
|
|
2388
3589
|
if (!continuing())
|
|
2389
3590
|
return;
|
|
2390
3591
|
const held = replicas.get(room.locator);
|
|
3592
|
+
let changed = null;
|
|
2391
3593
|
if (held !== undefined) {
|
|
2392
3594
|
const declared = {
|
|
2393
|
-
...held.
|
|
3595
|
+
...held.record,
|
|
2394
3596
|
operations: room.operations,
|
|
2395
3597
|
replica: room.replica,
|
|
2396
3598
|
typedSelection: room.typedSelection ?? null,
|
|
2397
3599
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
2398
3600
|
};
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
3601
|
+
let read = 'staged';
|
|
3602
|
+
if (!sameMembership(declared, held.record)) {
|
|
3603
|
+
const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), compositionOf(declared));
|
|
3604
|
+
read =
|
|
3605
|
+
verdict.verdict !== 'refuse'
|
|
3606
|
+
? 'staged'
|
|
3607
|
+
: verdict.rule === 'model-unavailable'
|
|
3608
|
+
? 'deferred'
|
|
3609
|
+
: { kind: 'refused', refusal: verdict };
|
|
3610
|
+
}
|
|
3611
|
+
const record = read === 'staged'
|
|
2403
3612
|
? declared
|
|
2404
|
-
: {
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
const
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
3613
|
+
: { ...declared, typedSelection: held.record.typedSelection };
|
|
3614
|
+
const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
|
|
3615
|
+
const replayedLayer = await replayed(record, confirmedLayer, held.queue);
|
|
3616
|
+
const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
|
|
3617
|
+
const answer = await local.commit({
|
|
3618
|
+
rooms: [
|
|
3619
|
+
{
|
|
3620
|
+
locator: room.locator,
|
|
3621
|
+
key: record.key,
|
|
3622
|
+
record: storedRecord(record),
|
|
3623
|
+
roots: record.typedSelection?.roots ?? [],
|
|
3624
|
+
composition: compositionOf(record),
|
|
3625
|
+
overlay: diff.overlay,
|
|
3626
|
+
},
|
|
3627
|
+
],
|
|
3628
|
+
});
|
|
3629
|
+
recomposed(room.locator, held.record, record);
|
|
3630
|
+
const { count } = committedRoom(room.locator, record, answer);
|
|
3631
|
+
replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count });
|
|
3632
|
+
changed = changeSet(room.locator, {
|
|
3633
|
+
documents: answer.changed[room.locator] ?? [],
|
|
3634
|
+
room: true,
|
|
3635
|
+
});
|
|
3636
|
+
if (read === 'staged' || read === 'deferred') {
|
|
2414
3637
|
if (declarationRefusals.delete(room.locator)) {
|
|
2415
3638
|
sourceRefusals.delete(room.locator);
|
|
2416
3639
|
unreadable.delete(room.locator);
|
|
2417
3640
|
}
|
|
2418
3641
|
}
|
|
2419
|
-
else
|
|
2420
|
-
replicas.set(room.locator, { ...held, confirmed: { ...held.confirmed, record } });
|
|
3642
|
+
else {
|
|
2421
3643
|
refusedSource(room, read);
|
|
2422
3644
|
declarationRefusals.add(room.locator);
|
|
2423
3645
|
}
|
|
@@ -2428,14 +3650,14 @@ export function composeRuntime(options) {
|
|
|
2428
3650
|
invalidModels.delete(key);
|
|
2429
3651
|
}
|
|
2430
3652
|
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
2431
|
-
if (
|
|
3653
|
+
if (changed === null)
|
|
2432
3654
|
emitRooms();
|
|
2433
3655
|
else
|
|
2434
|
-
publishChangeSet(
|
|
3656
|
+
publishChangeSet(changed);
|
|
2435
3657
|
});
|
|
2436
3658
|
}
|
|
2437
3659
|
async function scoped(result, room, local) {
|
|
2438
|
-
if (
|
|
3660
|
+
if (result.scope === 'content' && !result.retryable) {
|
|
2439
3661
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
2440
3662
|
await loseRooms([room.locator]);
|
|
2441
3663
|
return;
|
|
@@ -2453,15 +3675,7 @@ export function composeRuntime(options) {
|
|
|
2453
3675
|
});
|
|
2454
3676
|
else if (result.scope !== 'operation') {
|
|
2455
3677
|
unreadable.add(room.locator);
|
|
2456
|
-
scopedAway = {
|
|
2457
|
-
...room,
|
|
2458
|
-
replica: { ...room.replica, admission: 'unavailable' },
|
|
2459
|
-
operations: {
|
|
2460
|
-
...room.operations,
|
|
2461
|
-
...(result.scope === 'content' ? { read: [] } : {}),
|
|
2462
|
-
...(result.scope === 'sync' ? { sync: [] } : {}),
|
|
2463
|
-
},
|
|
2464
|
-
};
|
|
3678
|
+
scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
|
|
2465
3679
|
await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
|
|
2466
3680
|
}
|
|
2467
3681
|
if (!continuing())
|
|
@@ -2472,17 +3686,10 @@ export function composeRuntime(options) {
|
|
|
2472
3686
|
}
|
|
2473
3687
|
function heldRooms(listed) {
|
|
2474
3688
|
return listed.flatMap((room) => {
|
|
2475
|
-
const record = replicas.get(room.locator)?.
|
|
3689
|
+
const record = replicas.get(room.locator)?.record;
|
|
2476
3690
|
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
2477
3691
|
return [];
|
|
2478
|
-
return [
|
|
2479
|
-
{
|
|
2480
|
-
locator: room.locator,
|
|
2481
|
-
roomVersion: record.roomVersion,
|
|
2482
|
-
documents: record.documents,
|
|
2483
|
-
revisions: record.revisions,
|
|
2484
|
-
},
|
|
2485
|
-
];
|
|
3692
|
+
return [{ locator: room.locator, roomVersion: record.roomVersion }];
|
|
2486
3693
|
});
|
|
2487
3694
|
}
|
|
2488
3695
|
async function synchronize(listed, local, progress, attempt, live) {
|
|
@@ -2510,7 +3717,7 @@ export function composeRuntime(options) {
|
|
|
2510
3717
|
progress.complete = false;
|
|
2511
3718
|
return false;
|
|
2512
3719
|
}
|
|
2513
|
-
const prior = replicas.get(room.locator)?.
|
|
3720
|
+
const prior = replicas.get(room.locator)?.record;
|
|
2514
3721
|
if (prior !== undefined && prior.replica.selection !== room.replica.selection) {
|
|
2515
3722
|
list(prior, false);
|
|
2516
3723
|
emitRooms();
|
|
@@ -2553,6 +3760,8 @@ export function composeRuntime(options) {
|
|
|
2553
3760
|
}
|
|
2554
3761
|
async function reload(room, local, progress, live) {
|
|
2555
3762
|
try {
|
|
3763
|
+
if (live())
|
|
3764
|
+
reloadsDue.delete(room.locator);
|
|
2556
3765
|
if (!live() || authorization === undefined)
|
|
2557
3766
|
return;
|
|
2558
3767
|
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
@@ -2562,7 +3771,7 @@ export function composeRuntime(options) {
|
|
|
2562
3771
|
await withhold(bootstrapped, room, local, progress);
|
|
2563
3772
|
return;
|
|
2564
3773
|
}
|
|
2565
|
-
await publish(local, room, bootstrapped.
|
|
3774
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
2566
3775
|
await caughtUp(room, local, progress, live);
|
|
2567
3776
|
}
|
|
2568
3777
|
finally {
|
|
@@ -2619,12 +3828,13 @@ export function composeRuntime(options) {
|
|
|
2619
3828
|
}
|
|
2620
3829
|
for (const room of result.rooms) {
|
|
2621
3830
|
if (room.operations === undefined ||
|
|
2622
|
-
!['
|
|
3831
|
+
!['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
2623
3832
|
room.operations[operation].every((scope) => typeof scope === 'string')) ||
|
|
2624
3833
|
room.replica === undefined ||
|
|
2625
3834
|
typeof room.replica.selection !== 'string' ||
|
|
2626
3835
|
room.replica.selection.length === 0 ||
|
|
2627
|
-
!['admitted', 'unavailable'].includes(room.replica.admission)
|
|
3836
|
+
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
3837
|
+
!['locked', 'open'].includes(room.replica.offline)) {
|
|
2628
3838
|
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
2629
3839
|
await refuse('missing-required-guarantee');
|
|
2630
3840
|
notifyReady();
|
|
@@ -2648,11 +3858,12 @@ export function composeRuntime(options) {
|
|
|
2648
3858
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
2649
3859
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
2650
3860
|
limits = result.providerLimits;
|
|
2651
|
-
await local.
|
|
3861
|
+
await local.fact('providerLimits', limits);
|
|
3862
|
+
await local.fact('capabilities', capabilities);
|
|
2652
3863
|
}
|
|
2653
3864
|
const signedIn = result.member ?? null;
|
|
2654
3865
|
if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
|
|
2655
|
-
await local.
|
|
3866
|
+
await local.fact('member', signedIn);
|
|
2656
3867
|
member = signedIn;
|
|
2657
3868
|
emitMember();
|
|
2658
3869
|
}
|
|
@@ -2786,6 +3997,7 @@ export function composeRuntime(options) {
|
|
|
2786
3997
|
function closedSocket(revoked, lost) {
|
|
2787
3998
|
const opened = socketOpen;
|
|
2788
3999
|
socketOpen = false;
|
|
4000
|
+
connectionLost();
|
|
2789
4001
|
if (lost) {
|
|
2790
4002
|
reads.lost();
|
|
2791
4003
|
conversations.lost();
|
|
@@ -2855,17 +4067,20 @@ export function composeRuntime(options) {
|
|
|
2855
4067
|
owed.clear();
|
|
2856
4068
|
for (const locator of reloading)
|
|
2857
4069
|
catching.add(locator);
|
|
2858
|
-
|
|
4070
|
+
liveQueries.recover();
|
|
2859
4071
|
return reloading;
|
|
2860
4072
|
}
|
|
2861
4073
|
async function reloadNow(replaced, progress) {
|
|
4074
|
+
const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
|
|
4075
|
+
for (const room of reloaded)
|
|
4076
|
+
reloadsDue.set(room.locator, generation);
|
|
2862
4077
|
try {
|
|
2863
4078
|
const local = await openLocal();
|
|
2864
|
-
return await Promise.allSettled(
|
|
2865
|
-
.filter((room) => replicas.has(room.locator) && replaced(room))
|
|
2866
|
-
.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
4079
|
+
return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
2867
4080
|
}
|
|
2868
4081
|
catch {
|
|
4082
|
+
for (const room of reloaded)
|
|
4083
|
+
reloadsDue.delete(room.locator);
|
|
2869
4084
|
progress.reachable = false;
|
|
2870
4085
|
return [];
|
|
2871
4086
|
}
|
|
@@ -3019,7 +4234,7 @@ export function composeRuntime(options) {
|
|
|
3019
4234
|
if (synced && !recorded) {
|
|
3020
4235
|
const completedAt = new Date().toISOString();
|
|
3021
4236
|
try {
|
|
3022
|
-
await (await openLocal()).
|
|
4237
|
+
await (await openLocal()).fact('lastCompleted', completedAt);
|
|
3023
4238
|
if (!live())
|
|
3024
4239
|
return;
|
|
3025
4240
|
recorded = true;
|
|
@@ -3059,8 +4274,8 @@ export function composeRuntime(options) {
|
|
|
3059
4274
|
if (room === undefined || room.replica.admission !== 'admitted')
|
|
3060
4275
|
return;
|
|
3061
4276
|
if (held !== undefined &&
|
|
3062
|
-
(held.
|
|
3063
|
-
options.adapter.reached(locator, held.
|
|
4277
|
+
(held.record.replica.selection !== room.replica.selection ||
|
|
4278
|
+
options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
|
|
3064
4279
|
return;
|
|
3065
4280
|
}
|
|
3066
4281
|
const during = underway?.generation === generation ? underway.progress : null;
|
|
@@ -3107,6 +4322,7 @@ export function composeRuntime(options) {
|
|
|
3107
4322
|
return;
|
|
3108
4323
|
if (event.kind === 'opened') {
|
|
3109
4324
|
socketOpen = true;
|
|
4325
|
+
connectionLive();
|
|
3110
4326
|
generation += 1;
|
|
3111
4327
|
reads.opened();
|
|
3112
4328
|
conversations.reopened();
|
|
@@ -3121,7 +4337,14 @@ export function composeRuntime(options) {
|
|
|
3121
4337
|
void rediscover(event.scope);
|
|
3122
4338
|
else
|
|
3123
4339
|
void applyRoom(event.locator, event.roomVersion);
|
|
3124
|
-
}, receipts);
|
|
4340
|
+
}, receipts, heldRevisions);
|
|
4341
|
+
}
|
|
4342
|
+
async function heldRevisions(locator, paths) {
|
|
4343
|
+
const held = replicas.get(locator);
|
|
4344
|
+
if (held === undefined || paths.length === 0)
|
|
4345
|
+
return new Map();
|
|
4346
|
+
const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
|
|
4347
|
+
return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
|
|
3125
4348
|
}
|
|
3126
4349
|
function writesHeld() {
|
|
3127
4350
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -3193,6 +4416,8 @@ export function composeRuntime(options) {
|
|
|
3193
4416
|
closed = true;
|
|
3194
4417
|
if (rechecking !== null)
|
|
3195
4418
|
clearInterval(rechecking);
|
|
4419
|
+
connectionLive();
|
|
4420
|
+
lockListeners.clear();
|
|
3196
4421
|
readinessWatches.clear();
|
|
3197
4422
|
withdrawStream?.();
|
|
3198
4423
|
withdrawStream = undefined;
|
|
@@ -3201,17 +4426,13 @@ export function composeRuntime(options) {
|
|
|
3201
4426
|
withdrawServiceMode?.();
|
|
3202
4427
|
conversations.close();
|
|
3203
4428
|
void recovery.retire();
|
|
3204
|
-
store?.close();
|
|
3205
|
-
void opening?.then((opened) => {
|
|
3206
|
-
opened.close();
|
|
3207
|
-
}, () => { });
|
|
3208
4429
|
roomListeners.clear();
|
|
3209
4430
|
refusalListeners.clear();
|
|
3210
4431
|
storageLossListeners.clear();
|
|
3211
4432
|
statusListeners.clear();
|
|
3212
4433
|
serviceModeListeners.clear();
|
|
3213
4434
|
memberListeners.clear();
|
|
3214
|
-
|
|
4435
|
+
liveQueries.close();
|
|
3215
4436
|
queueSubscriptions.clear();
|
|
3216
4437
|
roomDraftListeners.clear();
|
|
3217
4438
|
presentationListeners.clear();
|
|
@@ -3257,7 +4478,7 @@ export function composeRuntime(options) {
|
|
|
3257
4478
|
const watchers = seedWatchers.get(plan.key) ?? new Set();
|
|
3258
4479
|
seedWatchers.set(plan.key, watchers);
|
|
3259
4480
|
watchers.add(shown);
|
|
3260
|
-
const stop =
|
|
4481
|
+
const stop = liveQueries.watch(plan, identityOf(plan), () => {
|
|
3261
4482
|
const cover = covered(plan);
|
|
3262
4483
|
awaitPreparation(cover);
|
|
3263
4484
|
return seedBehind(plan, cover);
|
|
@@ -3278,66 +4499,53 @@ export function composeRuntime(options) {
|
|
|
3278
4499
|
stop();
|
|
3279
4500
|
};
|
|
3280
4501
|
}
|
|
3281
|
-
async function searched(input, reach) {
|
|
4502
|
+
async function searched(input, reach, locked) {
|
|
3282
4503
|
const asked = searchPlanOf(input);
|
|
3283
|
-
const
|
|
3284
|
-
|
|
4504
|
+
const filter = {
|
|
4505
|
+
...filterOf(asked.documents ?? {}, roomFacts(reach), planReach(reach)),
|
|
4506
|
+
locked,
|
|
4507
|
+
};
|
|
4508
|
+
const unlocking = (filter.rooms ?? [])
|
|
4509
|
+
.filter((room) => locked.includes(room))
|
|
4510
|
+
.map((room) => ({ room, awaits: 'unlock' }));
|
|
3285
4511
|
for (;;) {
|
|
3286
4512
|
const cover = covered(filter);
|
|
3287
4513
|
awaitPreparation(cover);
|
|
3288
|
-
const pending = 'pending' in cover ?
|
|
3289
|
-
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
3290
|
-
const held = covered({
|
|
3291
|
-
...filter,
|
|
3292
|
-
rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
|
|
3293
|
-
});
|
|
3294
|
-
const covering = 'pending' in held ? [] : held.coverage.rooms;
|
|
3295
|
-
const unindexed = covering
|
|
3296
|
-
.filter(({ room }) => !searchIndex.rooms().has(room))
|
|
3297
|
-
.map(({ room }) => room);
|
|
3298
|
-
for (const room of unindexed)
|
|
3299
|
-
pending.push({ room, awaits: 'index' });
|
|
3300
|
-
const listed = conversations.searchable();
|
|
3301
|
-
const named = reach !== null && !reach.conversations
|
|
3302
|
-
? []
|
|
3303
|
-
: (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
|
|
3304
|
-
const waiting = named.filter((one) => !searchIndex.conversations().has(one));
|
|
3305
|
-
for (const conversation of waiting)
|
|
3306
|
-
pending.push({ conversation, awaits: 'index' });
|
|
3307
|
-
if (unindexed.length > 0 || waiting.length > 0)
|
|
3308
|
-
index(unindexed, waiting);
|
|
4514
|
+
const pending = [...unlocking, ...('pending' in cover ? cover.pending : [])];
|
|
3309
4515
|
const complete = pending.length === 0 && !('pending' in cover);
|
|
4516
|
+
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
3310
4517
|
const answering = complete
|
|
3311
4518
|
? filter
|
|
3312
|
-
: {
|
|
3313
|
-
...filter,
|
|
3314
|
-
rooms: covering
|
|
3315
|
-
.filter(({ room }) => searchIndex.rooms().has(room))
|
|
3316
|
-
.map(({ room }) => room),
|
|
3317
|
-
};
|
|
4519
|
+
: { ...filter, rooms: reachedRooms(filter).filter((room) => !awaited.has(room)) };
|
|
3318
4520
|
const answered = covered(answering);
|
|
3319
4521
|
if ('pending' in answered)
|
|
3320
4522
|
continue;
|
|
3321
|
-
const
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
const
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
4523
|
+
const listed = conversations.searchable();
|
|
4524
|
+
const named = reach !== null && !reach.conversations
|
|
4525
|
+
? []
|
|
4526
|
+
: (asked.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
|
|
4527
|
+
const hits = await (await openLocal()).search({
|
|
4528
|
+
text: asked.text,
|
|
4529
|
+
limit: capped(asked.limit),
|
|
4530
|
+
after: asked.after,
|
|
4531
|
+
total: asked.total,
|
|
4532
|
+
documents: {
|
|
4533
|
+
plan: answering,
|
|
4534
|
+
rooms: answered.rooms,
|
|
4535
|
+
beneath: asked.collection
|
|
4536
|
+
? answered.rooms.map(({ locator, key }) => ({
|
|
4537
|
+
key,
|
|
4538
|
+
roots: replicas.get(locator)?.record.typedSelection?.roots ?? [],
|
|
4539
|
+
}))
|
|
4540
|
+
: [],
|
|
4541
|
+
},
|
|
4542
|
+
conversations: named,
|
|
4543
|
+
});
|
|
4544
|
+
if (JSON.stringify(covered(answering)) !== JSON.stringify(answered))
|
|
3330
4545
|
continue;
|
|
3331
|
-
const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms === null ? null : new Set(answering.rooms), plan.collection
|
|
3332
|
-
? new Map(answered.coverage.rooms.map(({ room }) => [
|
|
3333
|
-
room,
|
|
3334
|
-
replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
|
|
3335
|
-
]))
|
|
3336
|
-
: null);
|
|
3337
4546
|
const coverage = {
|
|
3338
4547
|
rooms: answered.coverage.rooms,
|
|
3339
|
-
conversations:
|
|
3340
|
-
index: searchIndex.generation(),
|
|
4548
|
+
conversations: named,
|
|
3341
4549
|
overlay: answered.coverage.overlay,
|
|
3342
4550
|
};
|
|
3343
4551
|
return complete
|
|
@@ -3351,13 +4559,17 @@ export function composeRuntime(options) {
|
|
|
3351
4559
|
roomDraftListeners.add(listener);
|
|
3352
4560
|
void exclusive(async () => {
|
|
3353
4561
|
await openLocal();
|
|
3354
|
-
if (closed || !roomDraftListeners.has(listener))
|
|
3355
|
-
return;
|
|
3356
|
-
listener.ready = true;
|
|
3357
4562
|
}).then(() => {
|
|
3358
|
-
if (closed || !
|
|
4563
|
+
if (closed || !roomDraftListeners.has(listener))
|
|
3359
4564
|
return;
|
|
3360
|
-
|
|
4565
|
+
const snapshot = snapshotRoomDrafts();
|
|
4566
|
+
draftsShown = draftsShown.then(async () => {
|
|
4567
|
+
const value = await snapshot.catch(() => null);
|
|
4568
|
+
if (value === null || closed || !roomDraftListeners.has(listener))
|
|
4569
|
+
return;
|
|
4570
|
+
listener.ready = true;
|
|
4571
|
+
listener.notify(value);
|
|
4572
|
+
});
|
|
3361
4573
|
}, () => { });
|
|
3362
4574
|
return () => {
|
|
3363
4575
|
roomDraftListeners.delete(listener);
|
|
@@ -3452,7 +4664,7 @@ export function composeRuntime(options) {
|
|
|
3452
4664
|
return listed();
|
|
3453
4665
|
},
|
|
3454
4666
|
query: ((input) => rowsQuery(planned(input, null))),
|
|
3455
|
-
search: (input) => searched(input, null),
|
|
4667
|
+
search: (input) => searched(input, null, []),
|
|
3456
4668
|
types(locator) {
|
|
3457
4669
|
return {
|
|
3458
4670
|
collection: modelTypes(modelOf(locator)),
|
|
@@ -3472,9 +4684,9 @@ export function composeRuntime(options) {
|
|
|
3472
4684
|
return listed;
|
|
3473
4685
|
});
|
|
3474
4686
|
},
|
|
3475
|
-
read(room, path) {
|
|
4687
|
+
async read(room, path) {
|
|
3476
4688
|
const record = roomDrafts.get(room)?.get(path);
|
|
3477
|
-
return record === undefined ? null :
|
|
4689
|
+
return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
|
|
3478
4690
|
},
|
|
3479
4691
|
write(room, path, revision, input) {
|
|
3480
4692
|
return exclusive(async () => {
|
|
@@ -3484,11 +4696,11 @@ export function composeRuntime(options) {
|
|
|
3484
4696
|
const local = await openLocal();
|
|
3485
4697
|
const current = roomDrafts.get(room)?.get(path);
|
|
3486
4698
|
if ((current?.revision ?? null) !== revision) {
|
|
3487
|
-
return staleWrite(current === undefined ? null :
|
|
4699
|
+
return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
|
|
3488
4700
|
}
|
|
3489
4701
|
const file = parts.file === undefined
|
|
3490
4702
|
? undefined
|
|
3491
|
-
: await heldFile(parts.file.blob, binaryBase(
|
|
4703
|
+
: await heldFile(parts.file.blob, binaryBase(await visibleOf(room, [path]), path));
|
|
3492
4704
|
const record = {
|
|
3493
4705
|
locator: room,
|
|
3494
4706
|
path,
|
|
@@ -3496,10 +4708,12 @@ export function composeRuntime(options) {
|
|
|
3496
4708
|
parts: held(current?.parts, parts, file),
|
|
3497
4709
|
returned: current?.returned ?? null,
|
|
3498
4710
|
};
|
|
3499
|
-
await local.keepRoomDraft(record
|
|
4711
|
+
await local.keepRoomDraft(record, parts.file === undefined || file === undefined
|
|
4712
|
+
? []
|
|
4713
|
+
: [{ key: file.localId, blob: parts.file.blob }]);
|
|
3500
4714
|
rememberRoomDraft(record);
|
|
3501
|
-
publishChangeSet(changeSet(room,
|
|
3502
|
-
return { outcome: 'written', roomDraft:
|
|
4715
|
+
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
4716
|
+
return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
|
|
3503
4717
|
});
|
|
3504
4718
|
},
|
|
3505
4719
|
discard(room, path, revision) {
|
|
@@ -3507,11 +4721,11 @@ export function composeRuntime(options) {
|
|
|
3507
4721
|
const local = await openLocal();
|
|
3508
4722
|
const current = roomDrafts.get(room)?.get(path);
|
|
3509
4723
|
if (current === undefined || current.revision !== revision) {
|
|
3510
|
-
return staleWrite(current === undefined ? null :
|
|
4724
|
+
return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
|
|
3511
4725
|
}
|
|
3512
4726
|
await local.discardRoomDraft(room, path);
|
|
3513
4727
|
forgetRoomDraft(room, path);
|
|
3514
|
-
publishChangeSet(changeSet(room,
|
|
4728
|
+
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
3515
4729
|
moveTo({ ...status, ...attention() });
|
|
3516
4730
|
return { outcome: 'written', roomDraft: null };
|
|
3517
4731
|
});
|
|
@@ -3545,15 +4759,21 @@ export function composeRuntime(options) {
|
|
|
3545
4759
|
history(locator, path, query) {
|
|
3546
4760
|
return readHistory(locator, path, query);
|
|
3547
4761
|
},
|
|
4762
|
+
resolve(room, path, destination) {
|
|
4763
|
+
return resolveReference(room, path, destination);
|
|
4764
|
+
},
|
|
3548
4765
|
binaryFile: {
|
|
4766
|
+
part: readPart,
|
|
3549
4767
|
read: readBinaryPath,
|
|
3550
4768
|
link: linkBinaryPath,
|
|
3551
4769
|
readReference: readBinaryReference,
|
|
3552
4770
|
},
|
|
3553
|
-
call(name, input) {
|
|
3554
|
-
|
|
4771
|
+
async call(name, input) {
|
|
4772
|
+
await ready;
|
|
4773
|
+
const room = calledRooms(name, input).find((one) => locked(one));
|
|
4774
|
+
return room === undefined ? recoveredAction(name, input) : lockedRoom(room);
|
|
3555
4775
|
},
|
|
3556
|
-
reached(locator, roomVersion, path) {
|
|
4776
|
+
async reached(locator, roomVersion, path) {
|
|
3557
4777
|
return path === undefined
|
|
3558
4778
|
? reachedBy(locator, roomVersion)
|
|
3559
4779
|
: writtenAfter(locator, roomVersion, path);
|
|
@@ -3561,12 +4781,13 @@ export function composeRuntime(options) {
|
|
|
3561
4781
|
log() {
|
|
3562
4782
|
return journal.export();
|
|
3563
4783
|
},
|
|
4784
|
+
cleanUp,
|
|
3564
4785
|
persistence() {
|
|
3565
4786
|
return persistence === 'granted';
|
|
3566
4787
|
},
|
|
3567
4788
|
async retry() {
|
|
3568
4789
|
journal.record('retry');
|
|
3569
|
-
|
|
4790
|
+
unpreparable.clear();
|
|
3570
4791
|
queueReadiness();
|
|
3571
4792
|
resume();
|
|
3572
4793
|
},
|
|
@@ -3596,6 +4817,10 @@ export function composeRuntime(options) {
|
|
|
3596
4817
|
watchPlan,
|
|
3597
4818
|
search: searched,
|
|
3598
4819
|
watchReadiness,
|
|
4820
|
+
locked,
|
|
4821
|
+
lockedRooms,
|
|
4822
|
+
watchLocks,
|
|
4823
|
+
located,
|
|
3599
4824
|
};
|
|
3600
4825
|
return {
|
|
3601
4826
|
md,
|