@agentshouse/mdruntime 0.1.0-cfcbb238a5fa0 → 0.1.0-cfe148b32e3d8
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 +229 -211
- package/README.md +9 -8
- package/dist/adapter.d.ts +69 -35
- package/dist/adapter.d.ts.map +1 -1
- package/dist/attached-model.d.ts +4 -1
- package/dist/attached-model.d.ts.map +1 -1
- package/dist/attached-model.js +13 -3
- package/dist/binary.d.ts +0 -5
- package/dist/binary.d.ts.map +1 -1
- package/dist/binary.js +1 -1
- package/dist/bootstrap.d.ts +24 -6
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +167 -82
- package/dist/chain.d.ts +11 -0
- package/dist/chain.d.ts.map +1 -0
- package/dist/chain.js +64 -0
- package/dist/change-set.d.ts +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/earlier.d.ts +10 -0
- package/dist/chat/earlier.d.ts.map +1 -0
- package/dist/chat/earlier.js +54 -0
- 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 +45 -5
- package/dist/chat/markdown.d.ts +2 -2
- package/dist/chat/markdown.d.ts.map +1 -1
- package/dist/chat/markdown.js +80 -39
- 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 +98 -92
- package/dist/chat/question.d.ts +1 -1
- package/dist/chat/question.d.ts.map +1 -1
- package/dist/chat/question.js +7 -18
- 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 +6 -1
- 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 +11 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +35 -5
- package/dist/collection.d.ts +6 -19
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +30 -287
- package/dist/compile.d.ts +16 -0
- package/dist/compile.d.ts.map +1 -0
- package/dist/compile.js +721 -0
- package/dist/connection.d.ts +19 -9
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +2392 -848
- package/dist/conversation.d.ts +14 -3
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +198 -89
- package/dist/digest.d.ts +7 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +113 -0
- package/dist/durability.d.ts +3 -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 -23
- 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 +1 -4
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +21 -8
- package/dist/identity.d.ts +0 -1
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +0 -3
- package/dist/index.d.ts +11 -8
- 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 +3 -2
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +30 -20
- 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 +14 -0
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +93 -3
- 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 +34 -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 +87 -172
- package/dist/query.d.ts +26 -2
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +41 -6
- package/dist/reach.d.ts +5 -3
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +158 -57
- package/dist/react.d.ts +1 -2
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +2 -14
- package/dist/readiness.d.ts +18 -3
- package/dist/readiness.d.ts.map +1 -1
- package/dist/readiness.js +1 -1
- package/dist/reads.d.ts.map +1 -1
- package/dist/reads.js +4 -2
- package/dist/recovery.d.ts +2 -2
- package/dist/recovery.d.ts.map +1 -1
- package/dist/recovery.js +8 -7
- package/dist/refresh.d.ts +11 -13
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +78 -51
- package/dist/replica.d.ts +57 -33
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +295 -186
- 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 -30
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +113 -64
- package/dist/save.d.ts +51 -14
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +518 -180
- package/dist/search.d.ts +23 -42
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +28 -182
- package/dist/seed.d.ts.map +1 -1
- package/dist/seed.js +18 -4
- 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 +97 -88
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +335 -659
- package/dist/stored.d.ts +145 -0
- package/dist/stored.d.ts.map +1 -0
- package/dist/stored.js +567 -0
- package/dist/tables.d.ts +342 -0
- package/dist/tables.d.ts.map +1 -0
- package/dist/tables.js +576 -0
- package/dist/turn.d.ts +5 -3
- package/dist/turn.d.ts.map +1 -1
- package/dist/turn.js +51 -21
- 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 +22 -0
- package/dist/windows.d.ts.map +1 -0
- package/dist/windows.js +89 -0
- package/dist/wire.d.ts +29 -1
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +8 -1
- package/dist/words.d.ts +19 -0
- package/dist/words.d.ts.map +1 -0
- package/dist/words.js +150 -0
- package/package.json +10 -4
- package/dist/acquisition.d.ts +0 -14
- package/dist/acquisition.d.ts.map +0 -1
- package/dist/acquisition.js +0 -24
- package/dist/read-side.d.ts +0 -25
- package/dist/read-side.d.ts.map +0 -1
- package/dist/read-side.js +0 -642
package/dist/connection.js
CHANGED
|
@@ -1,46 +1,41 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { escapePointer, parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
|
|
2
|
+
import { attachedModel, NO_ROOM_TYPES, roomTypesOf } from './attached-model.js';
|
|
2
3
|
import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
|
|
3
4
|
import { observing, whole } from './observation.js';
|
|
4
|
-
import { filterOf, identityOf, planOf, queryKey, } from './query.js';
|
|
5
|
-
import {
|
|
5
|
+
import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
|
|
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';
|
|
10
|
+
import { capped, MAXIMUM_WINDOW } from './windows.js';
|
|
9
11
|
import { bootstrapRoom } from './bootstrap.js';
|
|
10
|
-
import { changeSet,
|
|
12
|
+
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
11
13
|
import { createConversationStore, } from './conversation.js';
|
|
12
|
-
import { queuedFile } from './change.js';
|
|
13
|
-
import {
|
|
14
|
+
import { leaving as leavingIn, partnerOf, queuedFile, } from './change.js';
|
|
15
|
+
import { available, collectionOf, unreadable as unreadableIn, validRoot, } from './collection.js';
|
|
14
16
|
import { runtimeError } from './failure.js';
|
|
15
17
|
import { createLog } from './log.js';
|
|
16
18
|
import { binaryRecord } from './binary.js';
|
|
17
|
-
import {
|
|
19
|
+
import { openedFiles } from './opened.js';
|
|
20
|
+
import { ABSENT_TARGET, absentBinary, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
|
|
18
21
|
import { createRecovery } from './recovery.js';
|
|
19
22
|
import { doorReads } from './reads.js';
|
|
20
|
-
import {
|
|
21
|
-
import { buildBatch, joined,
|
|
22
|
-
import {
|
|
23
|
-
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
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';
|
|
26
|
+
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
24
27
|
import { same, saving, STARTING, transferring, } from './status.js';
|
|
25
28
|
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
26
|
-
import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
|
|
27
29
|
import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
|
|
28
30
|
import { FencedMap } from './fence.js';
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
+
import { sameBinary } from './overlay.js';
|
|
32
|
+
import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
|
|
31
33
|
import { identityTime, roomDraftRevision } from './identity.js';
|
|
32
34
|
import { beneath, normalize } from './paths.js';
|
|
33
35
|
import { openRuntimeStore, } from './store.js';
|
|
36
|
+
import { changedCleanly, sameComposition, validatedCollection, } from './validation.js';
|
|
34
37
|
import { unconfirmed } from './transfer.js';
|
|
35
38
|
import { governs, holds, narrowed, planReach, } from './reach.js';
|
|
36
|
-
function roomsAlone(plan) {
|
|
37
|
-
return (plan.reach === null &&
|
|
38
|
-
plan.root.types === null &&
|
|
39
|
-
plan.root.where === null &&
|
|
40
|
-
plan.root.pathPrefix === null &&
|
|
41
|
-
plan.hop === null &&
|
|
42
|
-
plan.backlinks === null);
|
|
43
|
-
}
|
|
44
39
|
function within(root, path) {
|
|
45
40
|
return root === '' || beneath(root, path);
|
|
46
41
|
}
|
|
@@ -58,19 +53,29 @@ const OPENING_LANE = 'opening';
|
|
|
58
53
|
const CONVERSATION_LANE = 'conversations';
|
|
59
54
|
const ROOM_READS = 256;
|
|
60
55
|
const ROOM_ATTEMPTS = 16;
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
56
|
+
const ERASE_BATCH = 512;
|
|
57
|
+
const REDERIVE_BATCH = 256;
|
|
58
|
+
const WALK_BYTES = 1 << 20;
|
|
59
|
+
const QUERY_BYTES = 1 << 20;
|
|
60
|
+
const STAGE_BYTES = 1 << 20;
|
|
61
|
+
const STAGE_FLOOR = 1 << 16;
|
|
62
|
+
// A batch aims at this much database work, so a Save or a query waits behind at most one short write.
|
|
63
|
+
const STAGE_AIM_MS = 50;
|
|
64
|
+
const PARTS_HELD = 4;
|
|
65
|
+
// Bytes still arriving are reported at most this often, well inside the 2 s progress bound, so readiness is not rebuilt for every chunk the network hands over.
|
|
66
|
+
const ARRIVAL_REPORT_MS = 250;
|
|
67
|
+
const INSTALL_STEPS = [
|
|
68
|
+
'writing',
|
|
69
|
+
'downloading',
|
|
70
|
+
'replaying',
|
|
71
|
+
'listing',
|
|
72
|
+
];
|
|
73
|
+
const CLEAN_CHANGES = 2_000;
|
|
74
|
+
// A Room that locks offline stays readable this long after the connection drops, so a laptop waking or changing network keeps working while a disconnected device locks soon after.
|
|
75
|
+
const LOCK_GRACE_MS = 120_000;
|
|
76
|
+
function operationOf(input) {
|
|
77
|
+
const operation = input?.operation_id;
|
|
78
|
+
return typeof operation === 'string' ? operation : null;
|
|
74
79
|
}
|
|
75
80
|
function abandonedOn(signal) {
|
|
76
81
|
return new Promise((resolve) => {
|
|
@@ -83,11 +88,14 @@ function abandonedOn(signal) {
|
|
|
83
88
|
function roomLane(locator) {
|
|
84
89
|
return `room ${locator}`;
|
|
85
90
|
}
|
|
91
|
+
function pendingKey(locator, id) {
|
|
92
|
+
return `${locator}\u0000${id}`;
|
|
93
|
+
}
|
|
86
94
|
export function composeRuntime(options) {
|
|
87
95
|
const localSaves = options.adapter.localSaves === true;
|
|
96
|
+
const roomTypes = roomTypesOf(options.adapter.roomTypes);
|
|
88
97
|
let authorization = options.authorization;
|
|
89
98
|
let opening;
|
|
90
|
-
let store;
|
|
91
99
|
const fences = new Map();
|
|
92
100
|
const isFenced = (locator) => fences.has(locator);
|
|
93
101
|
let fencings = 0;
|
|
@@ -148,6 +156,7 @@ export function composeRuntime(options) {
|
|
|
148
156
|
let handed = false;
|
|
149
157
|
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
150
158
|
const repairing = new Set();
|
|
159
|
+
const calling = new Set();
|
|
151
160
|
const repairable = new Map();
|
|
152
161
|
let outcomeRetention = null;
|
|
153
162
|
const answering = new Map();
|
|
@@ -158,6 +167,7 @@ export function composeRuntime(options) {
|
|
|
158
167
|
let discovering = null;
|
|
159
168
|
let opened = null;
|
|
160
169
|
const owed = new Set();
|
|
170
|
+
const reloadsDue = new Map();
|
|
161
171
|
const lanes = new Map();
|
|
162
172
|
let roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
163
173
|
const working = new Set();
|
|
@@ -169,23 +179,22 @@ export function composeRuntime(options) {
|
|
|
169
179
|
const queueSubscriptions = new Set();
|
|
170
180
|
const roomDraftListeners = new Set();
|
|
171
181
|
const presentationListeners = new Set();
|
|
172
|
-
const
|
|
182
|
+
const liveQueries = createLiveQueries((plan, cover) => answeredPlan(plan, cover));
|
|
173
183
|
let withdrawConversations;
|
|
174
184
|
let currencyQueued = null;
|
|
175
185
|
let socketOpen = false;
|
|
186
|
+
const confirmed = new Set();
|
|
187
|
+
let lockGrace = null;
|
|
188
|
+
const lockListeners = new Set();
|
|
176
189
|
let serviceMode = 'open';
|
|
177
190
|
let haltedRefusal = false;
|
|
178
191
|
let openSignals = 0;
|
|
179
192
|
const serviceModeListeners = new Set();
|
|
180
193
|
let member = null;
|
|
181
194
|
const memberListeners = new Set();
|
|
182
|
-
const prepared = new Set();
|
|
183
|
-
const projecting = new Set();
|
|
184
195
|
const unpreparedDemand = new Set();
|
|
185
|
-
const
|
|
196
|
+
const unpreparable = new Set();
|
|
186
197
|
let preparing = false;
|
|
187
|
-
let demand = WHOLE_DEMAND;
|
|
188
|
-
let narrowing = null;
|
|
189
198
|
let targets = {};
|
|
190
199
|
let persistence = options.shell.persistence;
|
|
191
200
|
let capacity = null;
|
|
@@ -195,26 +204,41 @@ export function composeRuntime(options) {
|
|
|
195
204
|
let readinessQueued = false;
|
|
196
205
|
const catching = new Set();
|
|
197
206
|
const transfers = new FencedMap(isFenced);
|
|
207
|
+
const installs = new Map();
|
|
208
|
+
const arrivals = new Map();
|
|
209
|
+
let arrivalReported = 0;
|
|
210
|
+
let onScreen = options.shell.onScreen;
|
|
211
|
+
const shownWaiters = [];
|
|
212
|
+
let partsFree = PARTS_HELD;
|
|
213
|
+
const partWaiters = [];
|
|
214
|
+
let stageBytes = STAGE_FLOOR * 4;
|
|
215
|
+
let installWrites = Promise.resolve();
|
|
198
216
|
const conversations = createConversationStore({
|
|
199
217
|
agents: declaredAgents,
|
|
200
|
-
documents: (
|
|
218
|
+
documents: (declaration, after) => rowsQuery(planOf({
|
|
219
|
+
rooms: [declaration.locator],
|
|
220
|
+
types: [declaration.type],
|
|
221
|
+
select: {
|
|
222
|
+
fields: [
|
|
223
|
+
declaration.identity,
|
|
224
|
+
declaration.name,
|
|
225
|
+
...(declaration.retired === undefined ? [] : [declaration.retired]),
|
|
226
|
+
].map((field) => `/${escapePointer(field)}`),
|
|
227
|
+
},
|
|
228
|
+
limit: MAXIMUM_WINDOW,
|
|
229
|
+
...(after === undefined ? {} : { after }),
|
|
230
|
+
}, roomFacts())),
|
|
201
231
|
call: (name, input) => invokeAction(name, input),
|
|
202
232
|
durably,
|
|
203
233
|
stream: () => options.adapter.conversations,
|
|
204
234
|
publish: publishChangeSet,
|
|
205
|
-
online: () =>
|
|
235
|
+
online: () => connectionOf() !== 'unreachable' && refusal === null,
|
|
206
236
|
closed: () => closed,
|
|
207
237
|
unanswered: resendLater,
|
|
208
238
|
expired: (operation) => pastHorizon(operation),
|
|
209
239
|
});
|
|
210
|
-
const searchIndex = createSearchIndex({
|
|
211
|
-
room: (room) => readSide.room(room),
|
|
212
|
-
document: (room, path) => readSide.document(room, path),
|
|
213
|
-
});
|
|
214
|
-
const roomsToIndex = new Set();
|
|
215
|
-
const conversationsToIndex = new Set();
|
|
216
|
-
let indexing = false;
|
|
217
240
|
const journal = createLog();
|
|
241
|
+
const keptFiles = openedFiles(options.database, isFenced);
|
|
218
242
|
let notifyReady = () => { };
|
|
219
243
|
const ready = new Promise((resolve) => {
|
|
220
244
|
notifyReady = resolve;
|
|
@@ -232,16 +256,36 @@ export function composeRuntime(options) {
|
|
|
232
256
|
if (room.size === 0)
|
|
233
257
|
roomDrafts.delete(locator);
|
|
234
258
|
}
|
|
235
|
-
function
|
|
259
|
+
async function visibleOf(locator, paths) {
|
|
260
|
+
const held = replicas.get(locator);
|
|
261
|
+
if (held === undefined)
|
|
262
|
+
return undefined;
|
|
263
|
+
const local = await openLocal();
|
|
264
|
+
const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
|
|
265
|
+
return { ...slice, collection: collectionOf(slice.documents, compositionOf(held.record)) };
|
|
266
|
+
}
|
|
267
|
+
function roomDraftOf(record, visible) {
|
|
236
268
|
return structuredClone({
|
|
237
269
|
room: record.locator,
|
|
238
270
|
path: record.path,
|
|
239
271
|
revision: record.revision,
|
|
240
272
|
parts: record.parts,
|
|
241
|
-
changedUnderneath: underneath(record.parts, record.path,
|
|
273
|
+
changedUnderneath: underneath(record.parts, record.path, visible),
|
|
242
274
|
returned: record.returned,
|
|
243
275
|
});
|
|
244
276
|
}
|
|
277
|
+
async function roomDraftsOf(records) {
|
|
278
|
+
const rooms = new Map();
|
|
279
|
+
for (const record of records) {
|
|
280
|
+
const paths = rooms.get(record.locator) ?? [];
|
|
281
|
+
paths.push(record.path);
|
|
282
|
+
rooms.set(record.locator, paths);
|
|
283
|
+
}
|
|
284
|
+
const layers = new Map();
|
|
285
|
+
for (const [locator, paths] of rooms)
|
|
286
|
+
layers.set(locator, await visibleOf(locator, paths));
|
|
287
|
+
return records.map((record) => roomDraftOf(record, layers.get(record.locator)));
|
|
288
|
+
}
|
|
245
289
|
function snapshotRoomDrafts() {
|
|
246
290
|
const records = [...roomDrafts.values()].flatMap((room) => [...room.values()]);
|
|
247
291
|
records.sort((left, right) => {
|
|
@@ -251,23 +295,38 @@ export function composeRuntime(options) {
|
|
|
251
295
|
return left.path < right.path ? -1 : 1;
|
|
252
296
|
return 0;
|
|
253
297
|
});
|
|
254
|
-
return records
|
|
298
|
+
return roomDraftsOf(records);
|
|
255
299
|
}
|
|
300
|
+
let draftsShown = Promise.resolve();
|
|
256
301
|
function publishRoomDrafts(waitingOnly) {
|
|
257
302
|
if (closed)
|
|
258
303
|
return;
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
listener
|
|
265
|
-
|
|
304
|
+
const snapshot = snapshotRoomDrafts();
|
|
305
|
+
draftsShown = draftsShown.then(async () => {
|
|
306
|
+
const value = await snapshot.catch(() => null);
|
|
307
|
+
if (value === null || closed)
|
|
308
|
+
return;
|
|
309
|
+
for (const listener of roomDraftListeners) {
|
|
310
|
+
if (waitingOnly && listener.ready)
|
|
311
|
+
continue;
|
|
312
|
+
listener.ready = true;
|
|
313
|
+
listener.notify(value);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
266
316
|
}
|
|
267
317
|
function presentationOf(app) {
|
|
268
318
|
const value = presentations.get(app);
|
|
269
319
|
return value === undefined ? null : structuredClone(value);
|
|
270
320
|
}
|
|
321
|
+
function keepPresentation(app, next) {
|
|
322
|
+
return exclusive(async () => {
|
|
323
|
+
const local = await openLocal();
|
|
324
|
+
const stored = next(presentations.get(app));
|
|
325
|
+
await local.present({ app, value: stored });
|
|
326
|
+
presentations.set(app, stored);
|
|
327
|
+
publishChangeSet(changeSet('', { presentation: app }));
|
|
328
|
+
});
|
|
329
|
+
}
|
|
271
330
|
function emitPresentation(app) {
|
|
272
331
|
if (closed)
|
|
273
332
|
return;
|
|
@@ -316,21 +375,69 @@ export function composeRuntime(options) {
|
|
|
316
375
|
return { attentionRequired, reset };
|
|
317
376
|
}
|
|
318
377
|
function documentsIn(locator) {
|
|
319
|
-
|
|
320
|
-
if (replica === undefined)
|
|
321
|
-
return 0;
|
|
322
|
-
const roots = replica.confirmed.record.typedSelection?.roots ?? [];
|
|
323
|
-
const weighed = outsideOf(roots);
|
|
324
|
-
return (tally(replica.visible.documents, weighed.documents) +
|
|
325
|
-
tally(replica.visible.binaries, weighed.binaries));
|
|
378
|
+
return replicas.get(locator)?.count ?? 0;
|
|
326
379
|
}
|
|
327
380
|
function listed() {
|
|
328
381
|
return structuredClone([...rooms.values()].map((room) => {
|
|
329
382
|
const documents = documentsIn(room.locator);
|
|
330
383
|
counted.set(room.locator, documents);
|
|
331
|
-
return
|
|
384
|
+
return locked(room.locator)
|
|
385
|
+
? {
|
|
386
|
+
...room,
|
|
387
|
+
model: modelTypes(NO_MODEL),
|
|
388
|
+
typedSelection: null,
|
|
389
|
+
documents: 0,
|
|
390
|
+
locked: true,
|
|
391
|
+
}
|
|
392
|
+
: { ...room, documents, locked: false };
|
|
332
393
|
}));
|
|
333
394
|
}
|
|
395
|
+
function locked(locator) {
|
|
396
|
+
return rooms.get(locator)?.replica.offline === 'locked' && !confirmed.has(locator);
|
|
397
|
+
}
|
|
398
|
+
function lockedRooms() {
|
|
399
|
+
return [...rooms.keys()].filter(locked).sort();
|
|
400
|
+
}
|
|
401
|
+
function relocked(before) {
|
|
402
|
+
const after = lockedRooms();
|
|
403
|
+
if (JSON.stringify(after) === JSON.stringify(before))
|
|
404
|
+
return;
|
|
405
|
+
journal.record(`locked ${after.length}`);
|
|
406
|
+
emitRooms();
|
|
407
|
+
for (const notify of [...lockListeners])
|
|
408
|
+
notify();
|
|
409
|
+
}
|
|
410
|
+
function confirmLocks(listed) {
|
|
411
|
+
if (!socketOpen)
|
|
412
|
+
return;
|
|
413
|
+
for (const room of listed) {
|
|
414
|
+
if (room.replica.admission === 'admitted')
|
|
415
|
+
confirmed.add(room.locator);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function connectionLost() {
|
|
419
|
+
if (lockGrace !== null || closed)
|
|
420
|
+
return;
|
|
421
|
+
lockGrace = setTimeout(() => {
|
|
422
|
+
lockGrace = null;
|
|
423
|
+
if (socketOpen || closed)
|
|
424
|
+
return;
|
|
425
|
+
const before = lockedRooms();
|
|
426
|
+
confirmed.clear();
|
|
427
|
+
relocked(before);
|
|
428
|
+
}, LOCK_GRACE_MS);
|
|
429
|
+
}
|
|
430
|
+
function connectionLive() {
|
|
431
|
+
if (lockGrace !== null)
|
|
432
|
+
clearTimeout(lockGrace);
|
|
433
|
+
lockGrace = null;
|
|
434
|
+
}
|
|
435
|
+
function watchLocks(notify) {
|
|
436
|
+
lockListeners.add(notify);
|
|
437
|
+
return () => {
|
|
438
|
+
lockListeners.delete(notify);
|
|
439
|
+
};
|
|
440
|
+
}
|
|
334
441
|
function roomFacts(reach = null) {
|
|
335
442
|
const held = [...rooms.values()]
|
|
336
443
|
.filter((room) => holds(reach, room.locator))
|
|
@@ -349,7 +456,7 @@ export function composeRuntime(options) {
|
|
|
349
456
|
return { listed: roomsListed, rooms: held };
|
|
350
457
|
}
|
|
351
458
|
function emitRooms() {
|
|
352
|
-
|
|
459
|
+
liveQueries.recover();
|
|
353
460
|
queueReadiness();
|
|
354
461
|
const value = listed();
|
|
355
462
|
for (const listener of roomListeners)
|
|
@@ -379,7 +486,7 @@ export function composeRuntime(options) {
|
|
|
379
486
|
if (closed)
|
|
380
487
|
return;
|
|
381
488
|
queueReadiness();
|
|
382
|
-
const next = status.state === 'synced' &&
|
|
489
|
+
const next = status.state === 'synced' && !conversations.current()
|
|
383
490
|
? { ...status, state: 'syncing' }
|
|
384
491
|
: status;
|
|
385
492
|
if (same(shown, next))
|
|
@@ -388,17 +495,17 @@ export function composeRuntime(options) {
|
|
|
388
495
|
for (const listener of statusListeners)
|
|
389
496
|
listener(shown);
|
|
390
497
|
}
|
|
391
|
-
function entryOf(batch) {
|
|
498
|
+
function entryOf(locator, batch) {
|
|
392
499
|
if (localSaves)
|
|
393
500
|
return Object.freeze({ batch, state: 'local' });
|
|
394
|
-
const state = pendings.get(batch.id);
|
|
501
|
+
const state = pendings.get(pendingKey(locator, batch.id));
|
|
395
502
|
if (state?.accepted !== undefined) {
|
|
396
503
|
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
397
504
|
}
|
|
398
505
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
399
506
|
}
|
|
400
507
|
function entriesOf(locator) {
|
|
401
|
-
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
|
|
508
|
+
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map((batch) => entryOf(locator, batch));
|
|
402
509
|
}
|
|
403
510
|
function emitQueue(locator) {
|
|
404
511
|
const entries = entriesOf(locator);
|
|
@@ -408,62 +515,39 @@ export function composeRuntime(options) {
|
|
|
408
515
|
}
|
|
409
516
|
}
|
|
410
517
|
function applyChangeSet(set) {
|
|
411
|
-
|
|
412
|
-
...set,
|
|
413
|
-
documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
|
|
414
|
-
};
|
|
415
|
-
readSide.apply(held);
|
|
416
|
-
searchIndex.changed(held);
|
|
518
|
+
liveQueries.touched(set);
|
|
417
519
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const held = { text: record.documents, binaries: record.binaries };
|
|
424
|
-
const composition = compositionOf(record);
|
|
425
|
-
if (referencesIndexed(held, composition))
|
|
426
|
-
return;
|
|
427
|
-
await indexReferrers(held, composition);
|
|
428
|
-
}
|
|
520
|
+
function unpublished(error) {
|
|
521
|
+
journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
|
|
522
|
+
}
|
|
523
|
+
function indexed(locator) {
|
|
524
|
+
return replicas.get(locator)?.indexed === true;
|
|
429
525
|
}
|
|
430
526
|
async function prepareReplica(locator) {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
return;
|
|
435
|
-
const visible = replica.visible;
|
|
436
|
-
const paths = [
|
|
437
|
-
...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
|
|
438
|
-
];
|
|
439
|
-
const entered = [];
|
|
440
|
-
projecting.add(locator);
|
|
441
|
-
try {
|
|
442
|
-
await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
|
|
443
|
-
const current = replicas.get(locator);
|
|
444
|
-
if (current === undefined || closed)
|
|
445
|
-
return;
|
|
446
|
-
const set = entering(locator, current, chunk);
|
|
447
|
-
readSide.apply(set);
|
|
448
|
-
entered.push(set);
|
|
449
|
-
});
|
|
450
|
-
if (!replicas.has(locator) || closed)
|
|
527
|
+
for (;;) {
|
|
528
|
+
const held = replicas.get(locator);
|
|
529
|
+
if (held === undefined || closed || held.indexed)
|
|
451
530
|
return;
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
531
|
+
const key = held.record.key;
|
|
532
|
+
const done = await exclusive(async () => (await openLocal()).rederive(key, REDERIVE_BATCH));
|
|
533
|
+
const now = replicas.get(locator);
|
|
534
|
+
if (now?.record.key !== key)
|
|
535
|
+
continue;
|
|
536
|
+
if (!done)
|
|
537
|
+
continue;
|
|
538
|
+
replicas.set(locator, { ...now, indexed: true });
|
|
539
|
+
publishChangeSet(changeSet(locator, { documents: null }));
|
|
540
|
+
liveQueries.recover();
|
|
541
|
+
queueReadiness();
|
|
542
|
+
return;
|
|
456
543
|
}
|
|
457
|
-
conversations.changed(mergeChangeSets(entered));
|
|
458
|
-
readSide.recover();
|
|
459
|
-
queueReadiness();
|
|
460
544
|
}
|
|
461
545
|
function unprepared() {
|
|
462
|
-
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !
|
|
546
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
|
|
463
547
|
}
|
|
464
|
-
function
|
|
465
|
-
|
|
466
|
-
journal.record(`
|
|
548
|
+
function unpreparedBy(locator, error) {
|
|
549
|
+
unpreparable.add(locator);
|
|
550
|
+
journal.record(`preparation failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
|
|
467
551
|
queueReadiness();
|
|
468
552
|
}
|
|
469
553
|
function prepare(locators = []) {
|
|
@@ -479,7 +563,7 @@ export function composeRuntime(options) {
|
|
|
479
563
|
if (closed)
|
|
480
564
|
return;
|
|
481
565
|
unpreparedDemand.delete(waiting);
|
|
482
|
-
await prepareReplica(waiting).catch((error) =>
|
|
566
|
+
await prepareReplica(waiting).catch((error) => unpreparedBy(waiting, error));
|
|
483
567
|
}
|
|
484
568
|
}
|
|
485
569
|
finally {
|
|
@@ -487,14 +571,18 @@ export function composeRuntime(options) {
|
|
|
487
571
|
}
|
|
488
572
|
})();
|
|
489
573
|
}
|
|
574
|
+
function roomsOfReach(reach) {
|
|
575
|
+
return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
576
|
+
}
|
|
490
577
|
function reachedRooms(plan) {
|
|
491
|
-
const
|
|
492
|
-
const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
578
|
+
const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
|
|
493
579
|
const named = plan.rooms ??
|
|
494
580
|
(reached === null
|
|
495
581
|
? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
|
|
496
582
|
: [...reached]);
|
|
497
|
-
return [...new Set(named)]
|
|
583
|
+
return [...new Set(named)]
|
|
584
|
+
.filter((locator) => (reached === null || reached.has(locator)) && !plan.locked.includes(locator))
|
|
585
|
+
.sort();
|
|
498
586
|
}
|
|
499
587
|
function awaited(locator) {
|
|
500
588
|
return (!roomsListed ||
|
|
@@ -508,6 +596,7 @@ export function composeRuntime(options) {
|
|
|
508
596
|
const pending = [];
|
|
509
597
|
const coverage = [];
|
|
510
598
|
const overlay = [];
|
|
599
|
+
const read = [];
|
|
511
600
|
for (const locator of locators) {
|
|
512
601
|
const replica = replicas.get(locator);
|
|
513
602
|
if (replica === undefined) {
|
|
@@ -515,21 +604,22 @@ export function composeRuntime(options) {
|
|
|
515
604
|
pending.push({ room: locator, awaits: 'acquisition' });
|
|
516
605
|
continue;
|
|
517
606
|
}
|
|
518
|
-
if (!
|
|
607
|
+
if (!replica.indexed) {
|
|
519
608
|
pending.push({ room: locator, awaits: 'preparation' });
|
|
520
609
|
continue;
|
|
521
610
|
}
|
|
522
611
|
coverage.push({
|
|
523
612
|
room: locator,
|
|
524
|
-
generation: replica.
|
|
613
|
+
generation: replica.record.transfer,
|
|
525
614
|
current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
|
|
526
615
|
});
|
|
616
|
+
read.push({ locator, key: replica.record.key });
|
|
527
617
|
for (const batch of replica.queue)
|
|
528
618
|
overlay.push(batch.id);
|
|
529
619
|
}
|
|
530
620
|
if (pending.length > 0)
|
|
531
621
|
return { pending };
|
|
532
|
-
return { coverage: { rooms: coverage, indexes: [], overlay } };
|
|
622
|
+
return { coverage: { rooms: coverage, indexes: [], overlay }, rooms: read };
|
|
533
623
|
}
|
|
534
624
|
function awaitPreparation(cover) {
|
|
535
625
|
if (!('pending' in cover))
|
|
@@ -538,66 +628,16 @@ export function composeRuntime(options) {
|
|
|
538
628
|
if (waiting.length > 0)
|
|
539
629
|
prepare(waiting.map(({ room }) => room));
|
|
540
630
|
}
|
|
541
|
-
function
|
|
542
|
-
|
|
543
|
-
(replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
|
|
544
|
-
queuesWithoutReplica.has(room.locator));
|
|
545
|
-
}
|
|
546
|
-
function acquiredLocators() {
|
|
547
|
-
return [...discovered.values()]
|
|
548
|
-
.filter(acquires)
|
|
549
|
-
.map((room) => room.locator)
|
|
550
|
-
.sort();
|
|
551
|
-
}
|
|
552
|
-
function heldDemand(acquired) {
|
|
553
|
-
if (demand.whole)
|
|
554
|
-
return null;
|
|
555
|
-
return { rooms: acquired.map((room) => room.locator), conversations: demand.conversations };
|
|
556
|
-
}
|
|
557
|
-
function applyDemand(next) {
|
|
558
|
-
const before = acquiredLocators();
|
|
559
|
-
const conversed = demand.conversations;
|
|
560
|
-
demand = next;
|
|
561
|
-
const after = acquiredLocators();
|
|
562
|
-
queueReadiness();
|
|
563
|
-
if (!conversed && next.conversations)
|
|
564
|
-
scheduleCurrency();
|
|
565
|
-
if (JSON.stringify(before) === JSON.stringify(after) && conversed === next.conversations)
|
|
566
|
-
return;
|
|
567
|
-
journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
|
|
568
|
-
if (options.adapter.roomStream === undefined)
|
|
569
|
-
void sync('unstreamed');
|
|
570
|
-
else if (socketOpen)
|
|
571
|
-
options.adapter.roomStream.reopen();
|
|
572
|
-
}
|
|
573
|
-
function acquire(next) {
|
|
574
|
-
if (closed)
|
|
575
|
-
return;
|
|
576
|
-
if (narrowing !== null)
|
|
577
|
-
clearTimeout(narrowing);
|
|
578
|
-
narrowing = null;
|
|
579
|
-
const widening = wider(next, demand);
|
|
580
|
-
if (!widening && !narrower(next, demand))
|
|
581
|
-
return;
|
|
582
|
-
if (widening || !roomsListed) {
|
|
583
|
-
applyDemand(next);
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
// A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
|
|
587
|
-
narrowing = setTimeout(() => {
|
|
588
|
-
narrowing = null;
|
|
589
|
-
applyDemand(next);
|
|
590
|
-
}, 5_000);
|
|
591
|
-
}
|
|
592
|
-
function requiredRooms(reach) {
|
|
631
|
+
function requiredRooms(given) {
|
|
632
|
+
const reach = planReach(given);
|
|
593
633
|
if (reach === null)
|
|
594
634
|
return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
|
|
595
|
-
const named = [...
|
|
635
|
+
const named = [...roomsOfReach(reach)].sort();
|
|
596
636
|
if (roomsListed)
|
|
597
637
|
return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
|
|
598
638
|
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
599
639
|
}
|
|
600
|
-
function roomState(locator
|
|
640
|
+
function roomState(locator) {
|
|
601
641
|
if (sourceRefusals.has(locator))
|
|
602
642
|
return 'refused';
|
|
603
643
|
const listedRoom = discovered.get(locator);
|
|
@@ -608,27 +648,17 @@ export function composeRuntime(options) {
|
|
|
608
648
|
return 'unadmitted';
|
|
609
649
|
if (!replicas.has(locator))
|
|
610
650
|
return 'acquiring';
|
|
611
|
-
if (!
|
|
651
|
+
if (!indexed(locator))
|
|
612
652
|
return 'preparing';
|
|
613
|
-
if (search && !searchIndex.rooms().has(locator))
|
|
614
|
-
return 'indexing';
|
|
615
653
|
return 'complete';
|
|
616
654
|
}
|
|
617
|
-
function conversationCoverage(reach
|
|
655
|
+
function conversationCoverage(reach) {
|
|
618
656
|
const declared = reach === null || reach.conversations;
|
|
619
657
|
if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
|
|
620
658
|
return 'undeclared';
|
|
621
659
|
if (!conversations.known() || !conversations.current())
|
|
622
660
|
return 'acquiring';
|
|
623
|
-
|
|
624
|
-
return 'complete';
|
|
625
|
-
const unindexed = conversations
|
|
626
|
-
.searchable()
|
|
627
|
-
.filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
|
|
628
|
-
if (unindexed.length === 0)
|
|
629
|
-
return 'complete';
|
|
630
|
-
index([], unindexed);
|
|
631
|
-
return 'indexing';
|
|
661
|
+
return 'complete';
|
|
632
662
|
}
|
|
633
663
|
function connectionOf() {
|
|
634
664
|
if (socketOpen)
|
|
@@ -655,25 +685,66 @@ export function composeRuntime(options) {
|
|
|
655
685
|
];
|
|
656
686
|
});
|
|
657
687
|
}
|
|
688
|
+
function installationOf(coverage) {
|
|
689
|
+
const rooms = { done: 0, total: 0 };
|
|
690
|
+
const parts = { done: 0, total: 0 };
|
|
691
|
+
const bytes = { done: 0, total: 0 };
|
|
692
|
+
const received = { done: 0, total: 0 };
|
|
693
|
+
const steps = new Set();
|
|
694
|
+
let waiting = false;
|
|
695
|
+
for (const { room, state } of coverage) {
|
|
696
|
+
if (state === 'unadmitted')
|
|
697
|
+
continue;
|
|
698
|
+
rooms.total += 1;
|
|
699
|
+
if (state === 'complete')
|
|
700
|
+
rooms.done += 1;
|
|
701
|
+
const transfer = transfers.get(room);
|
|
702
|
+
const held = installs.get(room);
|
|
703
|
+
const inventory = transfer?.inventory ?? replicas.get(room)?.record.inventory ?? null;
|
|
704
|
+
if (inventory !== null) {
|
|
705
|
+
const done = transfer?.done ?? inventory;
|
|
706
|
+
const share = held?.share ?? null;
|
|
707
|
+
parts.total += inventory.parts;
|
|
708
|
+
bytes.total += inventory.bytes;
|
|
709
|
+
parts.done += done.parts;
|
|
710
|
+
bytes.done += done.bytes;
|
|
711
|
+
if (share !== null && share.entries > 0) {
|
|
712
|
+
bytes.done += Math.floor((share.bytes * share.written) / share.entries);
|
|
713
|
+
}
|
|
714
|
+
received.total += inventory.bytes;
|
|
715
|
+
received.done += done.bytes + (share?.bytes ?? 0);
|
|
716
|
+
for (const arrived of arrivals.get(room)?.values() ?? [])
|
|
717
|
+
received.done += arrived;
|
|
718
|
+
}
|
|
719
|
+
const installing = held !== undefined && state !== 'complete';
|
|
720
|
+
if (installing)
|
|
721
|
+
steps.add(held.step);
|
|
722
|
+
waiting ||= installing || state === 'acquiring';
|
|
723
|
+
}
|
|
724
|
+
return {
|
|
725
|
+
step: INSTALL_STEPS.find((step) => steps.has(step)) ?? null,
|
|
726
|
+
paused: waiting && !onScreen ? 'hidden' : null,
|
|
727
|
+
rooms,
|
|
728
|
+
parts,
|
|
729
|
+
bytes,
|
|
730
|
+
received,
|
|
731
|
+
};
|
|
732
|
+
}
|
|
658
733
|
function readinessFor(port, reach) {
|
|
659
734
|
const required = requiredRooms(reach);
|
|
660
735
|
const search = port.search === 'required';
|
|
661
|
-
const coverage = required.rooms.map((room) => ({ room, state: roomState(room
|
|
662
|
-
const
|
|
663
|
-
.filter((room) => room.state ===
|
|
736
|
+
const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
|
|
737
|
+
const preparing = coverage
|
|
738
|
+
.filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
|
|
664
739
|
.map(({ room }) => room);
|
|
665
|
-
const preparing = building('preparing');
|
|
666
740
|
if (preparing.length > 0)
|
|
667
741
|
prepare(preparing);
|
|
668
|
-
const indexing = building('indexing');
|
|
669
|
-
if (indexing.length > 0)
|
|
670
|
-
index(indexing, []);
|
|
671
742
|
return readinessOf({
|
|
672
743
|
code: port.code,
|
|
673
744
|
coverage: {
|
|
674
745
|
listed: required.listed,
|
|
675
746
|
rooms: coverage,
|
|
676
|
-
conversations: conversationCoverage(reach
|
|
747
|
+
conversations: conversationCoverage(reach),
|
|
677
748
|
search: search ? 'required' : 'unused',
|
|
678
749
|
},
|
|
679
750
|
persistence,
|
|
@@ -683,6 +754,7 @@ export function composeRuntime(options) {
|
|
|
683
754
|
coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
|
|
684
755
|
? capacity
|
|
685
756
|
: port.capacity,
|
|
757
|
+
installation: installationOf(coverage),
|
|
686
758
|
refused: refusedIn(required.rooms),
|
|
687
759
|
lost: storageLoss !== null,
|
|
688
760
|
established: targets[port.target]?.established === true,
|
|
@@ -691,7 +763,7 @@ export function composeRuntime(options) {
|
|
|
691
763
|
function establish(name) {
|
|
692
764
|
targets = { ...targets, [name]: { established: true } };
|
|
693
765
|
queueReadiness();
|
|
694
|
-
void durably((local) => local.
|
|
766
|
+
void durably((local) => local.fact('targets', targets)).catch(() => {
|
|
695
767
|
journal.record(`target record failed ${name}`);
|
|
696
768
|
});
|
|
697
769
|
}
|
|
@@ -728,18 +800,12 @@ export function composeRuntime(options) {
|
|
|
728
800
|
readinessWatches.delete(watch);
|
|
729
801
|
};
|
|
730
802
|
}
|
|
731
|
-
function
|
|
803
|
+
function pageBytes(page) {
|
|
732
804
|
let bytes = 0;
|
|
733
|
-
for (const
|
|
734
|
-
bytes += (path.length +
|
|
805
|
+
for (const { path, content } of page.entries)
|
|
806
|
+
bytes += (path.length + (content?.length ?? 0)) * 2;
|
|
735
807
|
return bytes;
|
|
736
808
|
}
|
|
737
|
-
function grownBy(next, previous) {
|
|
738
|
-
if (previous === undefined)
|
|
739
|
-
return layerBytes(next);
|
|
740
|
-
const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
|
|
741
|
-
return layerBytes({ ...next, documents });
|
|
742
|
-
}
|
|
743
809
|
function exceeded(error) {
|
|
744
810
|
return (typeof error === 'object' &&
|
|
745
811
|
error !== null &&
|
|
@@ -747,7 +813,7 @@ export function composeRuntime(options) {
|
|
|
747
813
|
}
|
|
748
814
|
async function estimated() {
|
|
749
815
|
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
750
|
-
return estimate === null ? null : Math.max(0, estimate.
|
|
816
|
+
return estimate === null ? null : Math.max(0, estimate.total - estimate.usage);
|
|
751
817
|
}
|
|
752
818
|
function pause(locator, cause) {
|
|
753
819
|
paused.add(locator);
|
|
@@ -768,7 +834,8 @@ export function composeRuntime(options) {
|
|
|
768
834
|
const available = await estimated().catch(() => null);
|
|
769
835
|
if (available === null || available < (held.needed ?? 0))
|
|
770
836
|
return;
|
|
771
|
-
if (held.cause === '
|
|
837
|
+
if (held.cause === 'storage-exceeded' &&
|
|
838
|
+
(held.available === null || available <= held.available))
|
|
772
839
|
return;
|
|
773
840
|
resume();
|
|
774
841
|
}
|
|
@@ -786,10 +853,7 @@ export function composeRuntime(options) {
|
|
|
786
853
|
void sync('unstreamed');
|
|
787
854
|
}
|
|
788
855
|
async function reclaimed(local) {
|
|
789
|
-
const unreferenced = [...transfers.keys()].filter((locator) =>
|
|
790
|
-
const room = discovered.get(locator);
|
|
791
|
-
return room === undefined || !acquires(room);
|
|
792
|
-
});
|
|
856
|
+
const unreferenced = [...transfers.keys()].filter((locator) => !discovered.has(locator));
|
|
793
857
|
for (const locator of unreferenced) {
|
|
794
858
|
await local.abandon(locator);
|
|
795
859
|
transfers.delete(locator);
|
|
@@ -813,10 +877,10 @@ export function composeRuntime(options) {
|
|
|
813
877
|
}
|
|
814
878
|
async function exceededOn(locator, bytes) {
|
|
815
879
|
const available = await estimated().catch(() => null);
|
|
816
|
-
pause(locator, { cause: '
|
|
880
|
+
pause(locator, { cause: 'storage-exceeded', needed: bytes, available });
|
|
817
881
|
}
|
|
818
882
|
function seedRoomHeld(room, version) {
|
|
819
|
-
return
|
|
883
|
+
return indexed(room) && reachedBy(room, version);
|
|
820
884
|
}
|
|
821
885
|
function seedHeld(at) {
|
|
822
886
|
return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
|
|
@@ -850,6 +914,75 @@ export function composeRuntime(options) {
|
|
|
850
914
|
shown();
|
|
851
915
|
}
|
|
852
916
|
}
|
|
917
|
+
function sameCover(left, right) {
|
|
918
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
919
|
+
}
|
|
920
|
+
async function coveredRows(plan, cover) {
|
|
921
|
+
for (;;) {
|
|
922
|
+
const before = cover();
|
|
923
|
+
if ('pending' in before)
|
|
924
|
+
return { pending: before };
|
|
925
|
+
const local = await openLocal();
|
|
926
|
+
const keys = before.rooms.map(({ key }) => key);
|
|
927
|
+
const changes = local.changes(keys);
|
|
928
|
+
const current = () => local.changes(keys) === changes;
|
|
929
|
+
const rows = await windowRows(local, plan, before.rooms, current);
|
|
930
|
+
const after = cover();
|
|
931
|
+
if (rows !== null && sameCover(before, after))
|
|
932
|
+
return { rows, cover: before, current };
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
async function windowRows(local, plan, rooms, unchanged) {
|
|
936
|
+
const first = await local.query(plan, rooms, QUERY_BYTES);
|
|
937
|
+
if (first.more === undefined || !('documents' in first)) {
|
|
938
|
+
const { more: _more, ...rows } = first;
|
|
939
|
+
return rows;
|
|
940
|
+
}
|
|
941
|
+
const documents = [...first.documents];
|
|
942
|
+
for (let more = first.more; more !== undefined;) {
|
|
943
|
+
if (!unchanged())
|
|
944
|
+
return null;
|
|
945
|
+
const page = await local.query({
|
|
946
|
+
...plan,
|
|
947
|
+
after: { text: more, values: JSON.parse(more) },
|
|
948
|
+
limit: plan.limit === null ? null : plan.limit - documents.length,
|
|
949
|
+
total: false,
|
|
950
|
+
}, rooms, QUERY_BYTES);
|
|
951
|
+
if (!('documents' in page))
|
|
952
|
+
return null;
|
|
953
|
+
documents.push(...page.documents);
|
|
954
|
+
more = page.more;
|
|
955
|
+
if (more === undefined) {
|
|
956
|
+
return {
|
|
957
|
+
documents,
|
|
958
|
+
...(page.next === undefined ? {} : { next: page.next }),
|
|
959
|
+
...(first.total === undefined ? {} : { total: first.total }),
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return null;
|
|
964
|
+
}
|
|
965
|
+
function keyedOf(rows, coverage) {
|
|
966
|
+
if ('groups' in rows) {
|
|
967
|
+
return {
|
|
968
|
+
result: { ...rows, coverage },
|
|
969
|
+
keys: rows.groups.map((group) => JSON.stringify(group.key)),
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
return {
|
|
973
|
+
result: { ...rows, coverage },
|
|
974
|
+
keys: rows.documents.map((document) => documentKey(document.room, document.path)),
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
async function answeredPlan(plan, cover) {
|
|
978
|
+
const answered = await coveredRows(plan, cover);
|
|
979
|
+
if ('pending' in answered) {
|
|
980
|
+
const pending = answered.pending;
|
|
981
|
+
return { result: pending, keys: [], current: () => true };
|
|
982
|
+
}
|
|
983
|
+
const held = answered.cover;
|
|
984
|
+
return { ...keyedOf(answered.rows, held.coverage), current: answered.current };
|
|
985
|
+
}
|
|
853
986
|
async function rowsQuery(plan) {
|
|
854
987
|
const before = covered(plan);
|
|
855
988
|
awaitPreparation(before);
|
|
@@ -858,61 +991,15 @@ export function composeRuntime(options) {
|
|
|
858
991
|
return seededResult(seeded).result;
|
|
859
992
|
if ('pending' in before)
|
|
860
993
|
return before;
|
|
861
|
-
const
|
|
862
|
-
|
|
863
|
-
|
|
994
|
+
const answered = await coveredRows(plan, () => {
|
|
995
|
+
const cover = covered(plan);
|
|
996
|
+
awaitPreparation(cover);
|
|
864
997
|
return cover;
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
searchIndex.indexMessages(conversation, [], false);
|
|
871
|
-
let after = null;
|
|
872
|
-
for (;;) {
|
|
873
|
-
const batch = await durably((local) => local.messagesAfter(conversation, after));
|
|
874
|
-
if (closed)
|
|
875
|
-
return;
|
|
876
|
-
searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
|
|
877
|
-
if (batch.length === 0)
|
|
878
|
-
return;
|
|
879
|
-
after = batch[batch.length - 1].identity;
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
function indexNext() {
|
|
883
|
-
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
|
|
884
|
-
if (room !== undefined) {
|
|
885
|
-
roomsToIndex.delete(room);
|
|
886
|
-
return () => prepared.has(room)
|
|
887
|
-
? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
|
|
888
|
-
: undefined;
|
|
889
|
-
}
|
|
890
|
-
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
891
|
-
if (conversation === undefined)
|
|
892
|
-
return null;
|
|
893
|
-
conversationsToIndex.delete(conversation);
|
|
894
|
-
return () => indexConversation(conversation);
|
|
895
|
-
}
|
|
896
|
-
function index(rooms, named) {
|
|
897
|
-
for (const room of rooms)
|
|
898
|
-
roomsToIndex.add(room);
|
|
899
|
-
for (const conversation of named)
|
|
900
|
-
conversationsToIndex.add(conversation);
|
|
901
|
-
if (indexing)
|
|
902
|
-
return;
|
|
903
|
-
indexing = true;
|
|
904
|
-
void (async () => {
|
|
905
|
-
try {
|
|
906
|
-
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
907
|
-
await yielded();
|
|
908
|
-
await unit();
|
|
909
|
-
queueReadiness();
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
finally {
|
|
913
|
-
indexing = false;
|
|
914
|
-
}
|
|
915
|
-
})().catch(() => { });
|
|
998
|
+
});
|
|
999
|
+
if ('pending' in answered)
|
|
1000
|
+
return answered.pending;
|
|
1001
|
+
const held = answered.cover;
|
|
1002
|
+
return { ...answered.rows, coverage: held.coverage };
|
|
916
1003
|
}
|
|
917
1004
|
function publishChangeSet(set) {
|
|
918
1005
|
applyChangeSet(set);
|
|
@@ -922,7 +1009,10 @@ export function composeRuntime(options) {
|
|
|
922
1009
|
for (const locator of set.queues)
|
|
923
1010
|
emitQueue(locator);
|
|
924
1011
|
if (set.drafts.length > 0 ||
|
|
925
|
-
set.documents.some((change) =>
|
|
1012
|
+
set.documents.some((change) => {
|
|
1013
|
+
const held = roomDrafts.get(change.locator);
|
|
1014
|
+
return held !== undefined && (change.path === null || held.has(change.path));
|
|
1015
|
+
})) {
|
|
926
1016
|
publishRoomDrafts(false);
|
|
927
1017
|
}
|
|
928
1018
|
for (const app of set.presentations)
|
|
@@ -935,9 +1025,22 @@ export function composeRuntime(options) {
|
|
|
935
1025
|
if (reason === 'revoked-authorization') {
|
|
936
1026
|
await exclusive(async () => {
|
|
937
1027
|
const revoked = { status: 'unavailable', reason: 'revoked' };
|
|
1028
|
+
const records = [...replicas].map(([locator, held]) => ({
|
|
1029
|
+
locator,
|
|
1030
|
+
held,
|
|
1031
|
+
record: { ...held.record, model: revoked, selectionModel: revoked },
|
|
1032
|
+
}));
|
|
1033
|
+
let answer = null;
|
|
938
1034
|
try {
|
|
939
1035
|
const local = await openLocal();
|
|
940
|
-
await local.
|
|
1036
|
+
answer = await local.commit({
|
|
1037
|
+
rooms: records.map(({ locator, record }) => ({
|
|
1038
|
+
locator,
|
|
1039
|
+
key: record.key,
|
|
1040
|
+
record: storedRecord(record),
|
|
1041
|
+
composition: compositionOf(record),
|
|
1042
|
+
})),
|
|
1043
|
+
});
|
|
941
1044
|
}
|
|
942
1045
|
catch {
|
|
943
1046
|
journal.record('model revocation persistence failed');
|
|
@@ -953,16 +1056,16 @@ export function composeRuntime(options) {
|
|
|
953
1056
|
liveModels.delete(key);
|
|
954
1057
|
}
|
|
955
1058
|
const changed = [];
|
|
956
|
-
for (const
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1059
|
+
for (const { locator, held, record } of records) {
|
|
1060
|
+
if (replicas.get(locator) !== held)
|
|
1061
|
+
continue;
|
|
1062
|
+
const indexed = answer === null ? held.indexed : committedRoom(locator, answer).indexed;
|
|
1063
|
+
replicas.set(locator, { ...held, record, indexed });
|
|
1064
|
+
}
|
|
1065
|
+
for (const locator of [...rooms.keys()]) {
|
|
1066
|
+
const room = rooms.get(locator);
|
|
1067
|
+
if (room === undefined)
|
|
1068
|
+
continue;
|
|
966
1069
|
rooms.set(locator, {
|
|
967
1070
|
...room,
|
|
968
1071
|
model: modelTypes(revoked),
|
|
@@ -970,9 +1073,10 @@ export function composeRuntime(options) {
|
|
|
970
1073
|
? null
|
|
971
1074
|
: { ...room.typedSelection, model: modelTypes(revoked) },
|
|
972
1075
|
});
|
|
973
|
-
changed.push(changeSet(locator,
|
|
1076
|
+
changed.push(changeSet(locator, { documents: null, room: true }));
|
|
974
1077
|
}
|
|
975
1078
|
publishChangeSet(mergeChangeSets(changed));
|
|
1079
|
+
prepare();
|
|
976
1080
|
});
|
|
977
1081
|
}
|
|
978
1082
|
}
|
|
@@ -1003,12 +1107,13 @@ export function composeRuntime(options) {
|
|
|
1003
1107
|
model: modelTypes(modelOf(room.locator, 'typed-selection')),
|
|
1004
1108
|
},
|
|
1005
1109
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
1006
|
-
roomVersion: replicas.get(room.locator)?.
|
|
1110
|
+
roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
|
|
1007
1111
|
synchronizationAvailable,
|
|
1008
1112
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
1009
1113
|
});
|
|
1010
1114
|
}
|
|
1011
|
-
async function applyDiscovery(found, started) {
|
|
1115
|
+
async function applyDiscovery(found, started, covered) {
|
|
1116
|
+
const lockedBefore = lockedRooms();
|
|
1012
1117
|
for (const room of found) {
|
|
1013
1118
|
const fence = fences.get(room.locator);
|
|
1014
1119
|
if (fence !== undefined && fence.erased && fence.at < started)
|
|
@@ -1021,24 +1126,30 @@ export function composeRuntime(options) {
|
|
|
1021
1126
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
1022
1127
|
for (const locator of listedLocators)
|
|
1023
1128
|
scopedRefusals.delete(locator);
|
|
1024
|
-
|
|
1129
|
+
if (covered === undefined)
|
|
1130
|
+
discovered.clear();
|
|
1131
|
+
else
|
|
1132
|
+
for (const locator of covered)
|
|
1133
|
+
discovered.delete(locator);
|
|
1025
1134
|
for (const room of listed)
|
|
1026
1135
|
discovered.set(room.locator, room);
|
|
1027
|
-
const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
|
|
1136
|
+
const unlisted = unlistedHeld(new Set(found.map((room) => room.locator))).filter((locator) => covered === undefined || covered.includes(locator));
|
|
1028
1137
|
if (options.adapter.exhaustiveDiscovery === true)
|
|
1029
1138
|
await loseRooms(unlisted);
|
|
1030
1139
|
else
|
|
1031
1140
|
unlistedKept(unlisted);
|
|
1032
1141
|
for (const room of listed) {
|
|
1033
|
-
const held = replicas.get(room.locator)?.
|
|
1142
|
+
const held = replicas.get(room.locator)?.record;
|
|
1034
1143
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
1035
1144
|
list(held, false);
|
|
1036
1145
|
continue;
|
|
1037
1146
|
}
|
|
1038
1147
|
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
1039
1148
|
}
|
|
1149
|
+
confirmLocks(listed);
|
|
1040
1150
|
roomsListed = true;
|
|
1041
1151
|
emitRooms();
|
|
1152
|
+
relocked(lockedBefore);
|
|
1042
1153
|
return listed;
|
|
1043
1154
|
}
|
|
1044
1155
|
function heldLocally(locator) {
|
|
@@ -1059,7 +1170,7 @@ export function composeRuntime(options) {
|
|
|
1059
1170
|
}
|
|
1060
1171
|
function unlistedKept(unlisted) {
|
|
1061
1172
|
for (const locator of unlisted) {
|
|
1062
|
-
const held = replicas.get(locator)?.
|
|
1173
|
+
const held = replicas.get(locator)?.record;
|
|
1063
1174
|
if (held !== undefined)
|
|
1064
1175
|
list(held, false);
|
|
1065
1176
|
else
|
|
@@ -1068,16 +1179,16 @@ export function composeRuntime(options) {
|
|
|
1068
1179
|
}
|
|
1069
1180
|
function fence(locators) {
|
|
1070
1181
|
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
1071
|
-
const removed = fresh.map((locator) =>
|
|
1072
|
-
|
|
1073
|
-
queue: true,
|
|
1182
|
+
const removed = fresh.map((locator) => ({
|
|
1183
|
+
locator,
|
|
1074
1184
|
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
1075
1185
|
}));
|
|
1076
1186
|
for (const locator of fresh) {
|
|
1077
1187
|
fences.set(locator, { at: discoveries, erased: false });
|
|
1078
1188
|
lastFenced.set(locator, ++fencings);
|
|
1189
|
+
confirmed.delete(locator);
|
|
1079
1190
|
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
1080
|
-
pendings.delete(batch.id);
|
|
1191
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
1081
1192
|
replicas.delete(locator);
|
|
1082
1193
|
queuesWithoutReplica.delete(locator);
|
|
1083
1194
|
rooms.delete(locator);
|
|
@@ -1089,7 +1200,7 @@ export function composeRuntime(options) {
|
|
|
1089
1200
|
unreadable.delete(locator);
|
|
1090
1201
|
stale.delete(locator);
|
|
1091
1202
|
repairable.delete(locator);
|
|
1092
|
-
|
|
1203
|
+
unpreparable.delete(locator);
|
|
1093
1204
|
paused.delete(locator);
|
|
1094
1205
|
scopedRefusals.delete(locator);
|
|
1095
1206
|
sourceRefusals.delete(locator);
|
|
@@ -1104,26 +1215,48 @@ export function composeRuntime(options) {
|
|
|
1104
1215
|
}
|
|
1105
1216
|
if (fresh.length === 0)
|
|
1106
1217
|
return fresh;
|
|
1107
|
-
publishChangeSet(mergeChangeSets(removed));
|
|
1108
|
-
|
|
1109
|
-
prepared.delete(locator);
|
|
1110
|
-
searchIndex.forget(fresh);
|
|
1111
|
-
readSide.recover();
|
|
1218
|
+
publishChangeSet(mergeChangeSets(removed.map(({ locator, drafts }) => changeSet(locator, { documents: null, room: true, queue: true, drafts }))));
|
|
1219
|
+
liveQueries.recover();
|
|
1112
1220
|
moveTo({ ...status, ...attention() });
|
|
1113
1221
|
return fresh;
|
|
1114
1222
|
}
|
|
1115
|
-
async function
|
|
1116
|
-
await local.erase(locators);
|
|
1223
|
+
async function eraseFenced(locators) {
|
|
1117
1224
|
for (const locator of locators) {
|
|
1225
|
+
while (await durably((local) => local.erase(locator, ERASE_BATCH)))
|
|
1226
|
+
await yielded();
|
|
1118
1227
|
const held = fences.get(locator);
|
|
1119
1228
|
if (held !== undefined)
|
|
1120
1229
|
held.erased = true;
|
|
1121
1230
|
}
|
|
1122
1231
|
}
|
|
1232
|
+
let sweeping = false;
|
|
1233
|
+
function sweep() {
|
|
1234
|
+
if (sweeping || closed)
|
|
1235
|
+
return;
|
|
1236
|
+
sweeping = true;
|
|
1237
|
+
void (async () => {
|
|
1238
|
+
try {
|
|
1239
|
+
while (!closed && (await durably((local) => local.erase(null, ERASE_BATCH)))) {
|
|
1240
|
+
await yielded();
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
catch {
|
|
1244
|
+
journal.record('erasure failed');
|
|
1245
|
+
}
|
|
1246
|
+
finally {
|
|
1247
|
+
sweeping = false;
|
|
1248
|
+
}
|
|
1249
|
+
})();
|
|
1250
|
+
}
|
|
1123
1251
|
async function loseRooms(locators) {
|
|
1124
1252
|
const fresh = fence(locators);
|
|
1125
|
-
if (fresh.length
|
|
1126
|
-
|
|
1253
|
+
if (fresh.length === 0)
|
|
1254
|
+
return;
|
|
1255
|
+
await durably(async (local) => {
|
|
1256
|
+
await local.fence(fresh);
|
|
1257
|
+
await unpair(local);
|
|
1258
|
+
});
|
|
1259
|
+
await eraseFenced(fresh);
|
|
1127
1260
|
}
|
|
1128
1261
|
function restored(key, persisted) {
|
|
1129
1262
|
const prior = models.get(key);
|
|
@@ -1144,54 +1277,56 @@ export function composeRuntime(options) {
|
|
|
1144
1277
|
return model;
|
|
1145
1278
|
}
|
|
1146
1279
|
async function hydrate(opened) {
|
|
1147
|
-
const [
|
|
1148
|
-
opened.
|
|
1280
|
+
const [held, queues, kept, heldConversations, chatDrafts] = await Promise.all([
|
|
1281
|
+
opened.opened(),
|
|
1149
1282
|
opened.queues(),
|
|
1150
1283
|
opened.roomDrafts(),
|
|
1151
|
-
opened.presentation(),
|
|
1152
1284
|
opened.conversations(),
|
|
1153
1285
|
opened.chatDrafts(),
|
|
1154
|
-
opened.lastCompleted(),
|
|
1155
|
-
opened.providerLimits(),
|
|
1156
|
-
opened.capabilities(),
|
|
1157
|
-
opened.member(),
|
|
1158
|
-
opened.transfers(),
|
|
1159
|
-
opened.targets(),
|
|
1160
1286
|
]);
|
|
1161
|
-
|
|
1287
|
+
const records = [...held.rooms.values()];
|
|
1288
|
+
const counts = await opened.counts(records.map(({ key, typedSelection }) => ({ key, roots: typedSelection?.roots ?? [] })));
|
|
1289
|
+
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1290
|
+
targets = held.targets;
|
|
1162
1291
|
transfers.clear();
|
|
1163
|
-
for (const record of
|
|
1164
|
-
transfers.set(
|
|
1165
|
-
limits =
|
|
1166
|
-
capabilities =
|
|
1167
|
-
member =
|
|
1292
|
+
for (const [locator, record] of held.transfers)
|
|
1293
|
+
transfers.set(locator, record);
|
|
1294
|
+
limits = held.providerLimits;
|
|
1295
|
+
capabilities = held.capabilities;
|
|
1296
|
+
member = held.member;
|
|
1168
1297
|
roomDrafts.clear();
|
|
1169
1298
|
for (const record of kept)
|
|
1170
1299
|
rememberRoomDraft(record);
|
|
1171
1300
|
presentations.clear();
|
|
1172
|
-
for (const record of
|
|
1301
|
+
for (const record of held.presentation)
|
|
1173
1302
|
presentations.set(record.app, record.value);
|
|
1174
|
-
conversations.hydrate(
|
|
1175
|
-
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1303
|
+
conversations.hydrate(heldConversations, chatDrafts);
|
|
1176
1304
|
pendings.clear();
|
|
1177
1305
|
for (const queue of queues) {
|
|
1178
|
-
for (const { batch, state } of queue.pending)
|
|
1179
|
-
pendings.set(batch.id, state);
|
|
1306
|
+
for (const { batch, state } of queue.pending) {
|
|
1307
|
+
pendings.set(pendingKey(queue.locator, batch.id), state);
|
|
1308
|
+
}
|
|
1180
1309
|
}
|
|
1181
|
-
const previousReplicas = new Map(replicas);
|
|
1182
1310
|
replicas.clear();
|
|
1183
1311
|
queuesWithoutReplica.clear();
|
|
1184
|
-
const replicated = new Set(records.map((record) => record.locator));
|
|
1185
1312
|
for (const [locator, queue] of queuedBatches) {
|
|
1186
|
-
if (!
|
|
1313
|
+
if (!held.rooms.has(locator))
|
|
1187
1314
|
queuesWithoutReplica.set(locator, queue);
|
|
1188
1315
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
const
|
|
1193
|
-
const
|
|
1194
|
-
|
|
1316
|
+
records.forEach((stored, index) => {
|
|
1317
|
+
if (fences.has(stored.locator))
|
|
1318
|
+
return;
|
|
1319
|
+
const model = restored(modelKey(stored.locator, 'collection'), stored.model);
|
|
1320
|
+
const selectionModel = restored(modelKey(stored.locator, 'typed-selection'), stored.selectionModel);
|
|
1321
|
+
const record = { ...stored, model, selectionModel };
|
|
1322
|
+
replicas.set(record.locator, {
|
|
1323
|
+
record,
|
|
1324
|
+
queue: queuedBatches.get(record.locator) ?? [],
|
|
1325
|
+
overlaid: held.overlaid.get(record.key) ?? new Set(),
|
|
1326
|
+
count: counts[index] ?? 0,
|
|
1327
|
+
confirmed: 0,
|
|
1328
|
+
indexed: !held.unindexed.has(record.key),
|
|
1329
|
+
});
|
|
1195
1330
|
const existing = rooms.get(record.locator);
|
|
1196
1331
|
if (existing === undefined)
|
|
1197
1332
|
list(record, false);
|
|
@@ -1210,11 +1345,15 @@ export function composeRuntime(options) {
|
|
|
1210
1345
|
},
|
|
1211
1346
|
});
|
|
1212
1347
|
}
|
|
1213
|
-
}
|
|
1348
|
+
});
|
|
1349
|
+
for (const key of held.orphans)
|
|
1350
|
+
await opened.release(key);
|
|
1351
|
+
if (held.erasing || held.orphans.length > 0)
|
|
1352
|
+
queueMicrotask(sweep);
|
|
1214
1353
|
return {
|
|
1215
|
-
lastCompleted,
|
|
1354
|
+
lastCompleted: held.lastCompleted,
|
|
1216
1355
|
locators: new Set([...queuedBatches.keys(), ...replicas.keys()]),
|
|
1217
|
-
read: mergeChangeSets([...
|
|
1356
|
+
read: mergeChangeSets([...replicas.keys()].map((locator) => changeSet(locator, { documents: null }))),
|
|
1218
1357
|
};
|
|
1219
1358
|
}
|
|
1220
1359
|
async function readState(opened) {
|
|
@@ -1233,8 +1372,7 @@ export function composeRuntime(options) {
|
|
|
1233
1372
|
async function open() {
|
|
1234
1373
|
if (closed)
|
|
1235
1374
|
throw new Error('runtime closed');
|
|
1236
|
-
const opened =
|
|
1237
|
-
store = opened;
|
|
1375
|
+
const opened = openRuntimeStore(isFenced, options.database);
|
|
1238
1376
|
const interrupted = await opened.fences();
|
|
1239
1377
|
for (const locator of interrupted) {
|
|
1240
1378
|
fences.set(locator, { at: discoveries, erased: false });
|
|
@@ -1253,9 +1391,17 @@ export function composeRuntime(options) {
|
|
|
1253
1391
|
emitStorageLoss();
|
|
1254
1392
|
}
|
|
1255
1393
|
const lastCompleted = await readState(opened);
|
|
1256
|
-
if (interrupted.length > 0)
|
|
1257
|
-
|
|
1394
|
+
if (interrupted.length > 0) {
|
|
1395
|
+
queueMicrotask(() => {
|
|
1396
|
+
void eraseFenced(interrupted).catch(() => {
|
|
1397
|
+
journal.record('erasure failed');
|
|
1398
|
+
});
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1258
1401
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
1402
|
+
void exclusive(() => unpair(opened)).catch(() => {
|
|
1403
|
+
journal.record('unpair failed');
|
|
1404
|
+
});
|
|
1259
1405
|
return opened;
|
|
1260
1406
|
}
|
|
1261
1407
|
function durably(work) {
|
|
@@ -1276,78 +1422,260 @@ export function composeRuntime(options) {
|
|
|
1276
1422
|
mutations = next.then(() => undefined, () => undefined);
|
|
1277
1423
|
return next;
|
|
1278
1424
|
}
|
|
1279
|
-
function withdrawable(
|
|
1280
|
-
|
|
1281
|
-
|
|
1425
|
+
function withdrawable(locator) {
|
|
1426
|
+
return (batch) => {
|
|
1427
|
+
const state = pendings.get(pendingKey(locator, batch.id));
|
|
1428
|
+
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
const REWRITE_LINKS = ['field', 'body'];
|
|
1432
|
+
const CHECKED_LINKS = ['field', 'section'];
|
|
1433
|
+
async function sliceAt(record, view, queue, extra = [], moved = []) {
|
|
1434
|
+
const local = await openLocal();
|
|
1435
|
+
const needed = new Set([...touchedBy(queue), ...extra]);
|
|
1436
|
+
const movedPaths = [...new Set([...movedBy(queue, record.locator), ...moved])];
|
|
1437
|
+
if (movedPaths.length > 0) {
|
|
1438
|
+
for (const { path } of await local.referrers(record.key, view, movedPaths, REWRITE_LINKS)) {
|
|
1439
|
+
needed.add(path);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
const paths = [...needed];
|
|
1443
|
+
return stepped(slicing(await local.entries(record.key, view, paths), paths));
|
|
1444
|
+
}
|
|
1445
|
+
async function landedWhole(locator, batch, confirmedLayer, current) {
|
|
1446
|
+
if (!landed(batch, confirmedLayer, current))
|
|
1447
|
+
return false;
|
|
1448
|
+
const partner = partnerOf(batch, locator);
|
|
1449
|
+
if (partner === null)
|
|
1450
|
+
return true;
|
|
1451
|
+
const other = replicas.get(partner);
|
|
1452
|
+
const half = other?.queue.find((one) => one.id === batch.id);
|
|
1453
|
+
if (other === undefined || half === undefined)
|
|
1454
|
+
return false;
|
|
1455
|
+
return landed(half, await sliceAt(other.record, 'confirmed', [half]));
|
|
1456
|
+
}
|
|
1457
|
+
function carriedBack(locator, unlanded, written) {
|
|
1458
|
+
const back = new Map();
|
|
1459
|
+
const taken = [];
|
|
1460
|
+
for (const batch of unlanded) {
|
|
1461
|
+
const between = batch.between;
|
|
1462
|
+
if (between === undefined)
|
|
1463
|
+
continue;
|
|
1464
|
+
const source = between.source.room;
|
|
1465
|
+
const destination = between.destination.room;
|
|
1466
|
+
for (const { path: to } of pendings.get(pendingKey(destination, batch.id))?.drafts ?? []) {
|
|
1467
|
+
const record = roomDrafts.get(destination)?.get(to);
|
|
1468
|
+
const path = between.moves.find((move) => move.to === to)?.path;
|
|
1469
|
+
if (record === undefined || path === undefined)
|
|
1470
|
+
continue;
|
|
1471
|
+
const [into, key] = source === locator ? [written, path] : [back, documentKey(source, path)];
|
|
1472
|
+
const existing = into.get(key) ?? roomDrafts.get(source)?.get(path);
|
|
1473
|
+
taken.push({ locator: destination, path: to });
|
|
1474
|
+
into.set(key, {
|
|
1475
|
+
locator: source,
|
|
1476
|
+
path,
|
|
1477
|
+
revision: roomDraftRevision(),
|
|
1478
|
+
parts: returnedInto(existing?.parts, record.parts),
|
|
1479
|
+
returned: existing?.returned ?? null,
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
const records = [...back.values()];
|
|
1484
|
+
const elsewhere = taken.filter((one) => one.locator !== locator);
|
|
1485
|
+
const rooms = new Set([...records, ...elsewhere].map((one) => one.locator));
|
|
1486
|
+
return {
|
|
1487
|
+
taken: taken.filter((one) => one.locator === locator).map(({ path }) => path),
|
|
1488
|
+
writes: [...rooms].map((room) => ({
|
|
1489
|
+
locator: room,
|
|
1490
|
+
roomDrafts: records.filter((record) => record.locator === room),
|
|
1491
|
+
consumedRoomDrafts: elsewhere.filter((one) => one.locator === room).map(({ path }) => path),
|
|
1492
|
+
})),
|
|
1493
|
+
changes: [...rooms].map((room) => changeSet(room, {
|
|
1494
|
+
drafts: [...records, ...elsewhere]
|
|
1495
|
+
.filter((one) => one.locator === room)
|
|
1496
|
+
.map(({ path }) => path),
|
|
1497
|
+
})),
|
|
1498
|
+
remember() {
|
|
1499
|
+
for (const one of taken)
|
|
1500
|
+
forgetRoomDraft(one.locator, one.path);
|
|
1501
|
+
for (const record of records)
|
|
1502
|
+
rememberRoomDraft(record);
|
|
1503
|
+
},
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
function committedRoom(locator, answer) {
|
|
1507
|
+
const indexed = answer.indexed[locator] ?? replicas.get(locator)?.indexed ?? false;
|
|
1508
|
+
if (!indexed)
|
|
1509
|
+
prepare([locator]);
|
|
1510
|
+
return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
|
|
1511
|
+
}
|
|
1512
|
+
function replayed(record, confirmedLayer, queue) {
|
|
1513
|
+
return stepped(replaying(confirmedLayer, queue, record.locator, compositionOf(record)));
|
|
1282
1514
|
}
|
|
1283
|
-
async function
|
|
1515
|
+
async function overlayDiff(record, before, visible, touched) {
|
|
1516
|
+
const kept = before !== undefined && before.record.key === record.key
|
|
1517
|
+
? before.overlaid
|
|
1518
|
+
: new Set();
|
|
1519
|
+
return {
|
|
1520
|
+
overlay: {
|
|
1521
|
+
set: await stepped(overlayOf(visible, touched)),
|
|
1522
|
+
cleared: [...kept].filter((path) => !touched.has(path)),
|
|
1523
|
+
},
|
|
1524
|
+
touched,
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
async function settle(local, locator, before, record, queue, settling = {}) {
|
|
1528
|
+
return together(local, [await settlement(local, locator, before, record, queue, settling)]);
|
|
1529
|
+
}
|
|
1530
|
+
function unpairedIn(locator, batch) {
|
|
1531
|
+
if (!queueIn(locator).some((held) => held.id === batch.id))
|
|
1532
|
+
return false;
|
|
1533
|
+
if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
|
|
1534
|
+
return false;
|
|
1535
|
+
return !leavingIn(batch, locator) || withdrawable(locator)(batch);
|
|
1536
|
+
}
|
|
1537
|
+
async function together(local, settlements) {
|
|
1538
|
+
const settled = new Set(settlements.map(({ locator }) => locator));
|
|
1539
|
+
const all = [...settlements];
|
|
1540
|
+
for (let wave = settlements; wave.length > 0;) {
|
|
1541
|
+
const partnered = new Map();
|
|
1542
|
+
for (const { locator, left } of wave) {
|
|
1543
|
+
for (const batch of left) {
|
|
1544
|
+
const partner = partnerOf(batch, locator);
|
|
1545
|
+
if (partner === null || settled.has(partner) || !unpairedIn(partner, batch))
|
|
1546
|
+
continue;
|
|
1547
|
+
partnered.set(partner, [...(partnered.get(partner) ?? []), batch]);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
const partners = [];
|
|
1551
|
+
for (const [partner, batches] of partnered) {
|
|
1552
|
+
settled.add(partner);
|
|
1553
|
+
partners.push(await unpairing(local, partner, batches));
|
|
1554
|
+
}
|
|
1555
|
+
all.push(...partners);
|
|
1556
|
+
wave = partners;
|
|
1557
|
+
}
|
|
1558
|
+
const answer = await local.commit({
|
|
1559
|
+
rooms: all.flatMap(({ commit }) => commit.rooms),
|
|
1560
|
+
writes: all.flatMap(({ commit }) => commit.writes),
|
|
1561
|
+
});
|
|
1562
|
+
return mergeChangeSets(all.map((one) => one.apply(answer)));
|
|
1563
|
+
}
|
|
1564
|
+
async function settlement(local, locator, before, record, queue, settling = {}) {
|
|
1565
|
+
const view = settling.view ?? 'confirmed';
|
|
1284
1566
|
const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
1285
|
-
const
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1567
|
+
const overrides = settling.changes;
|
|
1568
|
+
const read = await sliceAt(record, view, queue, [
|
|
1569
|
+
...(before !== undefined && before.record.key === record.key ? before.overlaid : []),
|
|
1570
|
+
...(overrides?.set.map(({ path }) => path) ?? []),
|
|
1571
|
+
...(overrides?.removed ?? []),
|
|
1572
|
+
]);
|
|
1573
|
+
const confirmedLayer = overrides === undefined
|
|
1574
|
+
? read
|
|
1575
|
+
: await stepped(slicingWith(read, overrides.set, overrides.removed));
|
|
1576
|
+
const composition = compositionOf(record);
|
|
1577
|
+
const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
|
|
1578
|
+
const founded = kept.filter((batch) => !queue.includes(batch));
|
|
1579
|
+
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
1580
|
+
const unqueued = [];
|
|
1581
|
+
for (const batch of withdrawn) {
|
|
1582
|
+
unqueued.push({ batch, landed: await landedWhole(locator, batch, confirmedLayer) });
|
|
1583
|
+
}
|
|
1291
1584
|
const returning = [
|
|
1292
|
-
...[...unqueued].reverse(),
|
|
1293
|
-
|
|
1585
|
+
...[...unqueued].reverse().map(({ batch, landed: held }) => ({
|
|
1586
|
+
batch,
|
|
1587
|
+
returned: localSaves || held ? null : returnedBy('withdrawn'),
|
|
1588
|
+
})),
|
|
1589
|
+
...(settling.leaving ?? []),
|
|
1294
1590
|
];
|
|
1295
1591
|
const written = new Map();
|
|
1296
|
-
for (const { batch, returned } of returning) {
|
|
1297
|
-
if (
|
|
1592
|
+
for (const { batch, returned: reason } of returning) {
|
|
1593
|
+
if (reason === null || batch.between?.destination.room === locator)
|
|
1298
1594
|
continue;
|
|
1299
|
-
for (const saved of pendings.get(batch.id)?.drafts ?? []) {
|
|
1595
|
+
for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
|
|
1300
1596
|
const existing = written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path);
|
|
1301
1597
|
written.set(saved.path, {
|
|
1302
1598
|
locator,
|
|
1303
1599
|
path: saved.path,
|
|
1304
1600
|
revision: roomDraftRevision(),
|
|
1305
1601
|
parts: returnedInto(existing?.parts, saved.parts),
|
|
1306
|
-
returned,
|
|
1602
|
+
returned: reason,
|
|
1307
1603
|
});
|
|
1308
1604
|
}
|
|
1309
1605
|
}
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1606
|
+
const carried = carriedBack(locator, [
|
|
1607
|
+
...[...unqueued].reverse().flatMap(({ batch, landed: held }) => (held ? [] : [batch])),
|
|
1608
|
+
...(settling.leaving ?? []).flatMap(({ batch, returned }) => returned === null ? [] : [batch]),
|
|
1609
|
+
], written);
|
|
1610
|
+
const replayedLayer = await replayed(record, confirmedLayer, kept);
|
|
1611
|
+
const { overlay, touched } = await overlayDiff(record, before, replayedLayer.visible, replayedLayer.touched);
|
|
1612
|
+
const commit = {
|
|
1613
|
+
rooms: [
|
|
1614
|
+
{
|
|
1615
|
+
locator,
|
|
1616
|
+
key: record.key,
|
|
1617
|
+
record: storedRecord(record),
|
|
1618
|
+
roots: record.typedSelection?.roots ?? [],
|
|
1619
|
+
...settling.commit,
|
|
1620
|
+
...(overrides === undefined
|
|
1621
|
+
? {}
|
|
1622
|
+
: { confirmed: { set: overrides.set, removed: overrides.removed } }),
|
|
1623
|
+
overlay,
|
|
1624
|
+
},
|
|
1625
|
+
],
|
|
1626
|
+
writes: [
|
|
1627
|
+
{
|
|
1628
|
+
locator,
|
|
1629
|
+
queue: kept,
|
|
1630
|
+
founded,
|
|
1631
|
+
roomDrafts: [...written.values()],
|
|
1632
|
+
consumedRoomDrafts: carried.taken,
|
|
1633
|
+
},
|
|
1634
|
+
...carried.writes,
|
|
1635
|
+
],
|
|
1636
|
+
};
|
|
1637
|
+
const staying = new Set(kept.map((batch) => batch.id));
|
|
1638
|
+
return {
|
|
1639
|
+
locator,
|
|
1640
|
+
left: previousQueue.filter((batch) => !staying.has(batch.id)),
|
|
1641
|
+
commit,
|
|
1642
|
+
apply: (answer) => {
|
|
1643
|
+
for (const batch of previousQueue) {
|
|
1644
|
+
if (!staying.has(batch.id))
|
|
1645
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
1646
|
+
}
|
|
1647
|
+
for (const draft of written.values())
|
|
1648
|
+
rememberRoomDraft(draft);
|
|
1649
|
+
carried.remember();
|
|
1650
|
+
const drafts = [...written.keys(), ...carried.taken];
|
|
1331
1651
|
queuesWithoutReplica.delete(locator);
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1652
|
+
for (const { batch, landed: held } of unqueued) {
|
|
1653
|
+
const outcome = localSaves ? 'dropped' : held ? 'landed' : 'withdrawn';
|
|
1654
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
1655
|
+
}
|
|
1656
|
+
const changed = answer.changed[locator];
|
|
1657
|
+
const confirmedChanged = settling.commit?.published !== undefined ||
|
|
1658
|
+
settling.commit?.refreshed === true ||
|
|
1659
|
+
overrides !== undefined;
|
|
1660
|
+
const { count, indexed } = committedRoom(locator, answer);
|
|
1661
|
+
replicas.set(locator, {
|
|
1662
|
+
record,
|
|
1663
|
+
queue: kept,
|
|
1664
|
+
overlaid: touched,
|
|
1665
|
+
count,
|
|
1666
|
+
confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
|
|
1667
|
+
indexed,
|
|
1668
|
+
});
|
|
1669
|
+
return mergeChangeSets([
|
|
1670
|
+
changeSet(locator, {
|
|
1671
|
+
documents: changed === undefined ? [] : changed,
|
|
1672
|
+
room: settling.room,
|
|
1673
|
+
queue: queueChanged,
|
|
1674
|
+
drafts,
|
|
1675
|
+
}),
|
|
1676
|
+
...carried.changes,
|
|
1677
|
+
]);
|
|
1678
|
+
},
|
|
1351
1679
|
};
|
|
1352
1680
|
}
|
|
1353
1681
|
function adoptModels(locator, record) {
|
|
@@ -1368,7 +1696,7 @@ export function composeRuntime(options) {
|
|
|
1368
1696
|
const taken = (answer, previous, attached) => {
|
|
1369
1697
|
if (answer === undefined)
|
|
1370
1698
|
return previous;
|
|
1371
|
-
const { state, refused } = attachedModel(answer, previous);
|
|
1699
|
+
const { state, refused } = attachedModel(answer, previous, attached === 'collection' ? roomTypes : NO_ROOM_TYPES);
|
|
1372
1700
|
if (refused !== null) {
|
|
1373
1701
|
const where = refused.location === undefined ? '' : ` ${refused.location}`;
|
|
1374
1702
|
journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
|
|
@@ -1381,70 +1709,86 @@ export function composeRuntime(options) {
|
|
|
1381
1709
|
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
1382
1710
|
};
|
|
1383
1711
|
}
|
|
1384
|
-
function publish(local, room,
|
|
1385
|
-
const
|
|
1386
|
-
return exclusive(async () => {
|
|
1387
|
-
if (stored !== undefined) {
|
|
1388
|
-
await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
|
|
1389
|
-
}
|
|
1712
|
+
function publish(local, room, record, landing, live) {
|
|
1713
|
+
const landed = exclusive(async () => {
|
|
1390
1714
|
if (!live()) {
|
|
1391
1715
|
caughtUpTo(room.locator);
|
|
1392
1716
|
return true;
|
|
1393
1717
|
}
|
|
1394
1718
|
const prior = replicas.get(room.locator);
|
|
1395
|
-
if (
|
|
1396
|
-
(prior
|
|
1397
|
-
|
|
1719
|
+
if (landing.kind === 'refreshed' &&
|
|
1720
|
+
(prior === undefined || prior.record.key !== record.key || prior.confirmed !== landing.base)) {
|
|
1721
|
+
await local.unrefresh(record.key);
|
|
1398
1722
|
return false;
|
|
1399
1723
|
}
|
|
1400
|
-
adoptModels(room.locator,
|
|
1724
|
+
adoptModels(room.locator, record);
|
|
1401
1725
|
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
1402
|
-
const roomVersion =
|
|
1726
|
+
const roomVersion = record.roomVersion;
|
|
1403
1727
|
const accepted = queue.filter((batch) => {
|
|
1404
|
-
const
|
|
1405
|
-
return
|
|
1728
|
+
const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
|
|
1729
|
+
return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
|
|
1406
1730
|
});
|
|
1407
|
-
let
|
|
1731
|
+
let set;
|
|
1408
1732
|
try {
|
|
1409
|
-
|
|
1733
|
+
set = await settle(local, room.locator, prior, record, queue.filter((batch) => !accepted.includes(batch)), landing.kind === 'refreshed'
|
|
1734
|
+
? {
|
|
1735
|
+
room: true,
|
|
1736
|
+
view: 'refreshed',
|
|
1737
|
+
commit: { refreshed: true, composition: compositionOf(record) },
|
|
1738
|
+
}
|
|
1739
|
+
: {
|
|
1740
|
+
room: true,
|
|
1741
|
+
view: landing.refreshed ? 'refreshed' : 'confirmed',
|
|
1742
|
+
commit: {
|
|
1743
|
+
published: { from: prior?.record.key ?? null },
|
|
1744
|
+
composition: compositionOf(record),
|
|
1745
|
+
...(landing.refreshed ? { refreshed: true } : {}),
|
|
1746
|
+
},
|
|
1747
|
+
});
|
|
1410
1748
|
}
|
|
1411
1749
|
catch (error) {
|
|
1412
1750
|
if (!exceeded(error))
|
|
1413
1751
|
throw error;
|
|
1414
|
-
await exceededOn(room.locator,
|
|
1752
|
+
await exceededOn(room.locator, null);
|
|
1415
1753
|
caughtUpTo(room.locator);
|
|
1416
1754
|
return true;
|
|
1417
1755
|
}
|
|
1418
|
-
|
|
1419
|
-
if (stored !== undefined) {
|
|
1756
|
+
if (landing.kind === 'bootstrapped') {
|
|
1420
1757
|
transfers.delete(room.locator);
|
|
1421
1758
|
catching.delete(room.locator);
|
|
1759
|
+
sweep();
|
|
1422
1760
|
}
|
|
1423
1761
|
for (const batch of accepted)
|
|
1424
|
-
pendings.delete(batch.id);
|
|
1762
|
+
pendings.delete(pendingKey(room.locator, batch.id));
|
|
1425
1763
|
stale.delete(room.locator);
|
|
1426
|
-
|
|
1764
|
+
unpreparable.delete(room.locator);
|
|
1427
1765
|
if (unreadable.delete(room.locator))
|
|
1428
1766
|
scopedRefusals.delete(room.locator);
|
|
1429
1767
|
sourceRefusals.delete(room.locator);
|
|
1430
1768
|
declarationRefusals.delete(room.locator);
|
|
1431
1769
|
list(room, true);
|
|
1432
|
-
if (prior === undefined)
|
|
1433
|
-
prepared.add(room.locator);
|
|
1434
1770
|
publishChangeSet(set);
|
|
1771
|
+
await unpair(local);
|
|
1435
1772
|
moveTo({ ...status, ...attention() });
|
|
1436
1773
|
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
1774
|
+
if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1775
|
+
submitLater(room.locator);
|
|
1776
|
+
}
|
|
1437
1777
|
return true;
|
|
1438
1778
|
});
|
|
1779
|
+
return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
|
|
1439
1780
|
}
|
|
1440
|
-
async function
|
|
1781
|
+
async function savedRoom(locator) {
|
|
1441
1782
|
const local = await openLocal();
|
|
1442
|
-
await referenced(locator);
|
|
1443
1783
|
const replica = replicas.get(locator);
|
|
1444
1784
|
const room = rooms.get(locator);
|
|
1445
1785
|
if (replica === undefined || room === undefined) {
|
|
1446
1786
|
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1447
1787
|
}
|
|
1788
|
+
return { local, replica, room };
|
|
1789
|
+
}
|
|
1790
|
+
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
1791
|
+
const target = await savedRoom(locator);
|
|
1448
1792
|
const saved = [];
|
|
1449
1793
|
for (const { path, revision } of named) {
|
|
1450
1794
|
const record = roomDrafts.get(locator)?.get(path);
|
|
@@ -1454,12 +1798,396 @@ export function composeRuntime(options) {
|
|
|
1454
1798
|
if (!saved.some((draft) => draft.path === path))
|
|
1455
1799
|
saved.push({ path, parts: record.parts });
|
|
1456
1800
|
}
|
|
1457
|
-
|
|
1801
|
+
return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
|
|
1802
|
+
}
|
|
1803
|
+
async function saveParts(locator, named, writtenBy) {
|
|
1804
|
+
const read = named.map(({ path, parts }) => ({ path, parts: readParts(parts) }));
|
|
1805
|
+
if (read.some(({ path }, at) => path === '' ||
|
|
1806
|
+
normalize(path) !== path ||
|
|
1807
|
+
read.findIndex((one) => one.path === path) !== at)) {
|
|
1808
|
+
throw runtimeError('room-draft-malformed');
|
|
1809
|
+
}
|
|
1810
|
+
const target = await savedRoom(locator);
|
|
1811
|
+
const saved = [];
|
|
1812
|
+
const files = [];
|
|
1813
|
+
const filed = read.filter(({ parts }) => parts.file !== undefined).map(({ path }) => path);
|
|
1814
|
+
const visible = filed.length === 0 ? undefined : await visibleOf(locator, filed);
|
|
1815
|
+
for (const { path, parts } of read) {
|
|
1816
|
+
const file = parts.file === undefined
|
|
1817
|
+
? undefined
|
|
1818
|
+
: await heldFile(parts.file.blob, binaryBase(visible, path));
|
|
1819
|
+
if (parts.file !== undefined && file !== undefined) {
|
|
1820
|
+
files.push({ key: file.localId, blob: parts.file.blob });
|
|
1821
|
+
}
|
|
1822
|
+
saved.push({ path, parts: held(undefined, parts, file) });
|
|
1823
|
+
}
|
|
1824
|
+
return committed(locator, target, saved, new Set(), writtenBy, files);
|
|
1825
|
+
}
|
|
1826
|
+
function carriedDrafts(locator, saved, consumed) {
|
|
1827
|
+
const holding = roomDrafts.get(locator);
|
|
1828
|
+
return new Map(saved.flatMap(({ path, parts }) => {
|
|
1829
|
+
const record = holding?.get(path);
|
|
1830
|
+
if (parts.move === undefined || record === undefined || consumed.has(path))
|
|
1831
|
+
return [];
|
|
1832
|
+
const to = moveTarget(parts.move);
|
|
1833
|
+
if (to === path || holding?.has(to) === true)
|
|
1834
|
+
return [];
|
|
1835
|
+
return [[path, { ...record, path: to, revision: roomDraftRevision() }]];
|
|
1836
|
+
}));
|
|
1837
|
+
}
|
|
1838
|
+
async function savingIn(replica, changes, extra, leaving = []) {
|
|
1839
|
+
const record = replica.record;
|
|
1840
|
+
const local = await openLocal();
|
|
1841
|
+
const paths = [
|
|
1842
|
+
...extra,
|
|
1843
|
+
...changes.flatMap((change) => change.kind === 'move' ? [change.path, change.to] : [change.path]),
|
|
1844
|
+
];
|
|
1845
|
+
const moved = [
|
|
1846
|
+
...leaving,
|
|
1847
|
+
...changes.flatMap((change) => (change.kind === 'move' ? [change.path] : [])),
|
|
1848
|
+
];
|
|
1849
|
+
const removed = changes.flatMap((change) => (change.kind === 'remove' ? [change.path] : []));
|
|
1850
|
+
const referring = removed.length === 0
|
|
1851
|
+
? []
|
|
1852
|
+
: (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
|
|
1853
|
+
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
|
|
1854
|
+
const { visible } = await replayed(record, confirmedLayer, replica.queue);
|
|
1855
|
+
const composition = compositionOf(record);
|
|
1856
|
+
return {
|
|
1857
|
+
record,
|
|
1858
|
+
confirmed: confirmedLayer,
|
|
1859
|
+
visible,
|
|
1860
|
+
*visibleOver(queue) {
|
|
1861
|
+
return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
|
|
1862
|
+
},
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
async function unmodeledIn(record) {
|
|
1866
|
+
const composition = compositionFor(record);
|
|
1867
|
+
if (available(composition))
|
|
1868
|
+
return false;
|
|
1869
|
+
const local = await openLocal();
|
|
1870
|
+
const roots = record.typedSelection?.roots ?? [];
|
|
1871
|
+
if (composition.model.status === 'unavailable' &&
|
|
1872
|
+
(await local.typedWithin(record.key, 'visible', roots, false))) {
|
|
1873
|
+
return true;
|
|
1874
|
+
}
|
|
1875
|
+
return (composition.selection !== null &&
|
|
1876
|
+
composition.selection.model.status === 'unavailable' &&
|
|
1877
|
+
(await local.typedWithin(record.key, 'visible', roots, true)));
|
|
1878
|
+
}
|
|
1879
|
+
function textDiffers(left, right) {
|
|
1880
|
+
return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
|
|
1881
|
+
!sameBinary(left?.binary ?? undefined, right?.binary ?? undefined));
|
|
1882
|
+
}
|
|
1883
|
+
function textOf(entry) {
|
|
1884
|
+
if (entry === null)
|
|
1885
|
+
return undefined;
|
|
1886
|
+
return entry.binary !== null ? null : entry.content;
|
|
1887
|
+
}
|
|
1888
|
+
function walkOf(local, key, view) {
|
|
1889
|
+
return async (after) => {
|
|
1890
|
+
const page = await local.walk(key, view, after, WALK_BYTES);
|
|
1891
|
+
return {
|
|
1892
|
+
entries: page.entries.map(({ path, content, binary }) => ({
|
|
1893
|
+
path,
|
|
1894
|
+
content: binary !== null ? null : content,
|
|
1895
|
+
})),
|
|
1896
|
+
next: page.next,
|
|
1897
|
+
};
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
function lookupOf(local, key, view) {
|
|
1901
|
+
return async (paths) => {
|
|
1902
|
+
if (paths.length === 0)
|
|
1903
|
+
return new Map();
|
|
1904
|
+
return new Map((await local.typed(key, view, paths)).map(({ path, type, binary }) => [
|
|
1905
|
+
path,
|
|
1906
|
+
{ type: type ?? undefined, binary },
|
|
1907
|
+
]));
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
function declaredOf(entry) {
|
|
1911
|
+
if (entry.binary !== null)
|
|
1912
|
+
return { type: undefined, binary: true };
|
|
1913
|
+
const read = parseFrontmatter(entry.content ?? '');
|
|
1914
|
+
return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
|
|
1915
|
+
}
|
|
1916
|
+
function overriddenWalk(walk, overrides) {
|
|
1917
|
+
const sorted = [...overrides.keys()].sort();
|
|
1918
|
+
const beyond = (path) => {
|
|
1919
|
+
if (path === null)
|
|
1920
|
+
return 0;
|
|
1921
|
+
let low = 0;
|
|
1922
|
+
let high = sorted.length;
|
|
1923
|
+
while (low < high) {
|
|
1924
|
+
const middle = (low + high) >>> 1;
|
|
1925
|
+
if (sorted[middle] <= path)
|
|
1926
|
+
low = middle + 1;
|
|
1927
|
+
else
|
|
1928
|
+
high = middle;
|
|
1929
|
+
}
|
|
1930
|
+
return low;
|
|
1931
|
+
};
|
|
1932
|
+
return async (after) => {
|
|
1933
|
+
const page = await walk(after);
|
|
1934
|
+
const end = page.next;
|
|
1935
|
+
const added = sorted
|
|
1936
|
+
.slice(beyond(after), end === null ? sorted.length : beyond(end))
|
|
1937
|
+
.flatMap((path) => {
|
|
1938
|
+
const entry = overrides.get(path);
|
|
1939
|
+
return entry === null || entry === undefined
|
|
1940
|
+
? []
|
|
1941
|
+
: [{ path, content: entry.binary !== null ? null : entry.content }];
|
|
1942
|
+
});
|
|
1943
|
+
const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
|
|
1944
|
+
return { entries, next: end };
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
function overriddenLookup(lookup, overrides) {
|
|
1948
|
+
return async (paths) => {
|
|
1949
|
+
const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
|
|
1950
|
+
for (const path of paths) {
|
|
1951
|
+
const entry = overrides.get(path);
|
|
1952
|
+
if (entry === undefined)
|
|
1953
|
+
continue;
|
|
1954
|
+
if (entry === null)
|
|
1955
|
+
known.delete(path);
|
|
1956
|
+
else
|
|
1957
|
+
known.set(path, declaredOf(entry));
|
|
1958
|
+
}
|
|
1959
|
+
return known;
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
function validatorOf(record, confirmedLayer) {
|
|
1963
|
+
return async (resulting, touched) => {
|
|
1964
|
+
const composition = compositionFor(record);
|
|
1965
|
+
const local = await openLocal();
|
|
1966
|
+
const changed = [];
|
|
1967
|
+
const overrides = new Map();
|
|
1968
|
+
const pace = paced();
|
|
1969
|
+
for (const path of new Set([...confirmedLayer.held, ...touched])) {
|
|
1970
|
+
await pace(ROW_WEIGHT);
|
|
1971
|
+
const was = entryAt(confirmedLayer, path);
|
|
1972
|
+
const now = entryAt(resulting, path);
|
|
1973
|
+
if (!textDiffers(was, now))
|
|
1974
|
+
continue;
|
|
1975
|
+
overrides.set(path, now);
|
|
1976
|
+
if (textOf(was) !== textOf(now))
|
|
1977
|
+
changed.push({ path, was: textOf(was), now: textOf(now) });
|
|
1978
|
+
}
|
|
1979
|
+
const lookup = overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides);
|
|
1980
|
+
const clean = available(composition) &&
|
|
1981
|
+
changed.length <= CLEAN_CHANGES &&
|
|
1982
|
+
(await changedCleanly(changed, compositionOf(record), composition, {
|
|
1983
|
+
lookup,
|
|
1984
|
+
texts: textsOf(local, record.key, 'confirmed'),
|
|
1985
|
+
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
1986
|
+
}));
|
|
1987
|
+
if (clean)
|
|
1988
|
+
return { verdict: 'accept' };
|
|
1989
|
+
return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
function textsOf(local, key, view) {
|
|
1993
|
+
return async (paths) => new Map((await local.entries(key, view, paths)).map(({ path, content, binary }) => [
|
|
1994
|
+
path,
|
|
1995
|
+
binary !== null ? null : content,
|
|
1996
|
+
]));
|
|
1997
|
+
}
|
|
1998
|
+
function referrersOf(local, key, view) {
|
|
1999
|
+
return async (targets) => [
|
|
2000
|
+
...new Set((await local.referrers(key, view, targets, CHECKED_LINKS)).map(({ path }) => path)),
|
|
2001
|
+
];
|
|
2002
|
+
}
|
|
2003
|
+
async function sideOf(locator, replica, room, saving) {
|
|
2004
|
+
const record = replica.record;
|
|
2005
|
+
return {
|
|
2006
|
+
room: locator,
|
|
2007
|
+
handle: room.handle,
|
|
2008
|
+
baseRoomVersion: record.roomVersion,
|
|
2009
|
+
composition: compositionFor(record),
|
|
2010
|
+
operations: room.operations,
|
|
2011
|
+
providerOwnedPaths: room.providerOwnedPaths,
|
|
2012
|
+
visible: saving.visible,
|
|
2013
|
+
queued: queuedDocuments(replica.queue),
|
|
2014
|
+
unmodeled: await unmodeledIn(record),
|
|
2015
|
+
validate: validatorOf(record, saving.confirmed),
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
function movedInto(locator, saved) {
|
|
2019
|
+
const into = saved.find(({ parts }) => parts.move?.room !== undefined && parts.move.room !== locator);
|
|
2020
|
+
return into?.parts.move?.room ?? null;
|
|
2021
|
+
}
|
|
2022
|
+
async function saveCommit(local, files, rooms) {
|
|
2023
|
+
const touched = new Map();
|
|
2024
|
+
const commits = [];
|
|
2025
|
+
for (const { locator, saving, before, queue } of rooms) {
|
|
2026
|
+
const replayedLayer = await replayed(saving.record, saving.confirmed, queue);
|
|
2027
|
+
const diff = await overlayDiff(saving.record, before, replayedLayer.visible, replayedLayer.touched);
|
|
2028
|
+
touched.set(locator, diff.touched);
|
|
2029
|
+
commits.push({
|
|
2030
|
+
locator,
|
|
2031
|
+
key: saving.record.key,
|
|
2032
|
+
roots: saving.record.typedSelection?.roots ?? [],
|
|
2033
|
+
overlay: diff.overlay,
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
try {
|
|
2037
|
+
const answer = await local.commit({
|
|
2038
|
+
rooms: commits,
|
|
2039
|
+
writes: rooms.map(({ write }) => write),
|
|
2040
|
+
files,
|
|
2041
|
+
whole: true,
|
|
2042
|
+
});
|
|
2043
|
+
return { answer, touched };
|
|
2044
|
+
}
|
|
2045
|
+
catch {
|
|
2046
|
+
return null;
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
function savedReplica(locator, before, queue, touched, answer) {
|
|
2050
|
+
replicas.set(locator, {
|
|
2051
|
+
...before,
|
|
2052
|
+
queue,
|
|
2053
|
+
overlaid: touched,
|
|
2054
|
+
count: answer.counts[locator] ?? before.count,
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
async function committedMove(locator, destination, { local, replica, room }, saved, consumed, writtenBy, files) {
|
|
2058
|
+
const mixed = saved.find(({ parts }) => Object.keys(parts).length !== 1 || parts.move?.room !== destination);
|
|
2059
|
+
if (mixed !== undefined) {
|
|
2060
|
+
return { outcome: 'refused', code: 'mixed-room-move', retryable: false, file: mixed.path };
|
|
2061
|
+
}
|
|
2062
|
+
const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
|
|
2063
|
+
const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
|
|
2064
|
+
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, sourceSaving.visible)));
|
|
1458
2065
|
if (changed.length > 0)
|
|
1459
2066
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
1460
|
-
const
|
|
1461
|
-
const
|
|
1462
|
-
|
|
2067
|
+
const target = replicas.get(destination);
|
|
2068
|
+
const listedRoom = rooms.get(destination);
|
|
2069
|
+
if (target === undefined || listedRoom === undefined) {
|
|
2070
|
+
throw runtimeError('room-not-held', `No Local Replica for ${destination}`);
|
|
2071
|
+
}
|
|
2072
|
+
const arriving = await savingIn(target, [], moves.map(({ to }) => to));
|
|
2073
|
+
const built = await buildMove({
|
|
2074
|
+
source: await sideOf(locator, replica, room, sourceSaving),
|
|
2075
|
+
destination: await sideOf(destination, target, listedRoom, arriving),
|
|
2076
|
+
moves,
|
|
2077
|
+
providerLimits: limits,
|
|
2078
|
+
local: localSaves,
|
|
2079
|
+
...(writtenBy === undefined ? {} : { writtenBy }),
|
|
2080
|
+
});
|
|
2081
|
+
if (built.kind === 'refused')
|
|
2082
|
+
return { outcome: 'refused', ...built.refusal };
|
|
2083
|
+
const id = built.leaving.id;
|
|
2084
|
+
const carrying = saved.flatMap(({ path, parts }) => {
|
|
2085
|
+
const record = roomDrafts.get(locator)?.get(path);
|
|
2086
|
+
const to = moveTarget(parts.move);
|
|
2087
|
+
if (record === undefined || consumed.has(path) || roomDrafts.get(destination)?.has(to)) {
|
|
2088
|
+
return [];
|
|
2089
|
+
}
|
|
2090
|
+
const moved = {
|
|
2091
|
+
...record,
|
|
2092
|
+
locator: destination,
|
|
2093
|
+
path: to,
|
|
2094
|
+
revision: roomDraftRevision(),
|
|
2095
|
+
};
|
|
2096
|
+
return [{ from: path, record: moved }];
|
|
2097
|
+
});
|
|
2098
|
+
const carried = carrying.map(({ record }) => record);
|
|
2099
|
+
const marked = carried.map(({ path }) => ({ path, parts: {} }));
|
|
2100
|
+
const left = [...consumed, ...carrying.map(({ from }) => from)];
|
|
2101
|
+
const leavingQueue = [...replica.queue, built.leaving];
|
|
2102
|
+
const arrivingQueue = [...target.queue, built.arriving];
|
|
2103
|
+
const committedBoth = await saveCommit(local, files, [
|
|
2104
|
+
{
|
|
2105
|
+
locator,
|
|
2106
|
+
saving: sourceSaving,
|
|
2107
|
+
before: replica,
|
|
2108
|
+
queue: leavingQueue,
|
|
2109
|
+
write: {
|
|
2110
|
+
locator,
|
|
2111
|
+
queue: leavingQueue,
|
|
2112
|
+
saved: new Map([[id, saved]]),
|
|
2113
|
+
consumedRoomDrafts: left,
|
|
2114
|
+
},
|
|
2115
|
+
},
|
|
2116
|
+
{
|
|
2117
|
+
locator: destination,
|
|
2118
|
+
saving: arriving,
|
|
2119
|
+
before: target,
|
|
2120
|
+
queue: arrivingQueue,
|
|
2121
|
+
write: {
|
|
2122
|
+
locator: destination,
|
|
2123
|
+
queue: arrivingQueue,
|
|
2124
|
+
saved: new Map([[id, marked]]),
|
|
2125
|
+
roomDrafts: carried,
|
|
2126
|
+
},
|
|
2127
|
+
},
|
|
2128
|
+
]);
|
|
2129
|
+
if (committedBoth === null)
|
|
2130
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2131
|
+
const { answer, touched } = committedBoth;
|
|
2132
|
+
if (answer.fenced !== null) {
|
|
2133
|
+
throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
|
|
2134
|
+
}
|
|
2135
|
+
if (fences.has(locator) || fences.has(destination)) {
|
|
2136
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
2137
|
+
}
|
|
2138
|
+
pendings.set(pendingKey(locator, id), {
|
|
2139
|
+
drafts: saved,
|
|
2140
|
+
submission: { state: 'waiting' },
|
|
2141
|
+
lost: false,
|
|
2142
|
+
});
|
|
2143
|
+
pendings.set(pendingKey(destination, id), {
|
|
2144
|
+
drafts: marked,
|
|
2145
|
+
submission: { state: 'waiting' },
|
|
2146
|
+
lost: false,
|
|
2147
|
+
});
|
|
2148
|
+
for (const path of left)
|
|
2149
|
+
forgetRoomDraft(locator, path);
|
|
2150
|
+
for (const record of carried)
|
|
2151
|
+
rememberRoomDraft(record);
|
|
2152
|
+
savedReplica(locator, replica, leavingQueue, touched.get(locator), answer);
|
|
2153
|
+
savedReplica(destination, target, arrivingQueue, touched.get(destination), answer);
|
|
2154
|
+
publishChangeSet(mergeChangeSets([
|
|
2155
|
+
changeSet(locator, {
|
|
2156
|
+
documents: answer.changed[locator] ?? [],
|
|
2157
|
+
queue: true,
|
|
2158
|
+
drafts: left,
|
|
2159
|
+
}),
|
|
2160
|
+
changeSet(destination, {
|
|
2161
|
+
documents: answer.changed[destination] ?? [],
|
|
2162
|
+
queue: true,
|
|
2163
|
+
drafts: carried.map(({ path }) => path),
|
|
2164
|
+
}),
|
|
2165
|
+
]));
|
|
2166
|
+
queuedSave(locator);
|
|
2167
|
+
return { outcome: 'saved', identifier: id };
|
|
2168
|
+
}
|
|
2169
|
+
function queuedSave(locator) {
|
|
2170
|
+
if (!localSaves)
|
|
2171
|
+
recorded = false;
|
|
2172
|
+
moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
|
|
2173
|
+
submitLater(locator);
|
|
2174
|
+
}
|
|
2175
|
+
async function committed(locator, target, saved, consumed, writtenBy, files = []) {
|
|
2176
|
+
const into = movedInto(locator, saved);
|
|
2177
|
+
if (into !== null) {
|
|
2178
|
+
return committedMove(locator, into, target, saved, consumed, writtenBy, files);
|
|
2179
|
+
}
|
|
2180
|
+
const { local, replica, room } = target;
|
|
2181
|
+
const parts = saved.map(({ path, parts: held }) => changesOf(path, held));
|
|
2182
|
+
const changes = parts.flatMap(({ changes: made }) => made);
|
|
2183
|
+
const saving = await savingIn(replica, changes, [
|
|
2184
|
+
...saved.map(({ path }) => path),
|
|
2185
|
+
...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
|
|
2186
|
+
]);
|
|
2187
|
+
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, underneath(held, path, saving.visible)));
|
|
2188
|
+
if (changed.length > 0)
|
|
2189
|
+
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2190
|
+
const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
|
|
1463
2191
|
if (requeue.kind === 'refused')
|
|
1464
2192
|
return { outcome: 'refused', ...requeue.refusal };
|
|
1465
2193
|
const foreign = [...requeue.withdrawn.keys()].some((origin) => {
|
|
@@ -1477,23 +2205,23 @@ export function composeRuntime(options) {
|
|
|
1477
2205
|
const withdrawing = requeue.withdrawn.size > 0;
|
|
1478
2206
|
const input = {
|
|
1479
2207
|
local: localSaves,
|
|
1480
|
-
baseRoomVersion: replica.
|
|
1481
|
-
composition: compositionFor(replica.
|
|
2208
|
+
baseRoomVersion: replica.record.roomVersion,
|
|
2209
|
+
composition: compositionFor(replica.record),
|
|
1482
2210
|
operations: room.operations,
|
|
1483
2211
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
1484
2212
|
providerLimits: limits,
|
|
1485
2213
|
handle: room.handle,
|
|
1486
|
-
visible:
|
|
2214
|
+
visible: saving.visible,
|
|
1487
2215
|
changes,
|
|
1488
2216
|
bases: parts.flatMap(({ bases }) => bases),
|
|
2217
|
+
queued: queuedDocuments(replica.queue),
|
|
1489
2218
|
writtenBy,
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
binaries: replica.confirmed.record.binaries,
|
|
1493
|
-
composition: compositionOf(replica.confirmed.record),
|
|
1494
|
-
},
|
|
2219
|
+
unmodeled: await unmodeledIn(replica.record),
|
|
2220
|
+
validate: validatorOf(replica.record, saving.confirmed),
|
|
1495
2221
|
};
|
|
1496
|
-
const checked = withdrawing
|
|
2222
|
+
const checked = withdrawing
|
|
2223
|
+
? await buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
|
|
2224
|
+
: null;
|
|
1497
2225
|
if (checked?.kind === 'refused')
|
|
1498
2226
|
return { outcome: 'refused', ...checked.refusal };
|
|
1499
2227
|
const owners = [...requeue.owners].flatMap((origin) => {
|
|
@@ -1509,13 +2237,14 @@ export function composeRuntime(options) {
|
|
|
1509
2237
|
};
|
|
1510
2238
|
}
|
|
1511
2239
|
const built = !withdrawing
|
|
1512
|
-
? buildBatch(input)
|
|
2240
|
+
? await buildBatch(input)
|
|
1513
2241
|
: requeue.changes.length === 0
|
|
1514
2242
|
? null
|
|
1515
|
-
: buildBatch({
|
|
2243
|
+
: await buildBatch({
|
|
1516
2244
|
...input,
|
|
1517
|
-
visible:
|
|
2245
|
+
visible: await stepped(saving.visibleOver(requeue.queue)),
|
|
1518
2246
|
changes: requeue.changes,
|
|
2247
|
+
queued: queuedDocuments(requeue.queue),
|
|
1519
2248
|
});
|
|
1520
2249
|
if (built?.kind === 'refused')
|
|
1521
2250
|
return { outcome: 'refused', ...built.refusal };
|
|
@@ -1532,7 +2261,7 @@ export function composeRuntime(options) {
|
|
|
1532
2261
|
for (const [origin, replaced] of withdrawn) {
|
|
1533
2262
|
if (replaced === null)
|
|
1534
2263
|
continue;
|
|
1535
|
-
const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
|
|
2264
|
+
const carried = withdrawnDrafts(pendings.get(pendingKey(locator, origin))?.drafts ?? [], changes);
|
|
1536
2265
|
drafted.set(replaced.id, replaced === join?.batch
|
|
1537
2266
|
? [
|
|
1538
2267
|
...carried.map((held) => joinedDraft(held, saved)),
|
|
@@ -1546,59 +2275,77 @@ export function composeRuntime(options) {
|
|
|
1546
2275
|
for (const replaced of withdrawn.values()) {
|
|
1547
2276
|
if (replaced === null || !batches.includes(replaced))
|
|
1548
2277
|
continue;
|
|
1549
|
-
const limited =
|
|
2278
|
+
const limited = await stepped(limiting(batches, replaced, limits, saving.visibleOver));
|
|
1550
2279
|
if (limited !== null)
|
|
1551
2280
|
return { outcome: 'refused', ...limited };
|
|
1552
2281
|
}
|
|
1553
|
-
const
|
|
1554
|
-
|
|
1555
|
-
|
|
2282
|
+
const carried = carriedDrafts(locator, saved, consumed);
|
|
2283
|
+
const left = [...consumed, ...carried.keys()];
|
|
2284
|
+
const arrived = [...carried.values()];
|
|
2285
|
+
const done = await saveCommit(local, files, [
|
|
2286
|
+
{
|
|
1556
2287
|
locator,
|
|
2288
|
+
saving,
|
|
2289
|
+
before: replica,
|
|
1557
2290
|
queue: batches,
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
2291
|
+
write: {
|
|
2292
|
+
locator,
|
|
2293
|
+
queue: batches,
|
|
2294
|
+
saved: drafted,
|
|
2295
|
+
consumedRoomDrafts: left,
|
|
2296
|
+
roomDrafts: arrived,
|
|
2297
|
+
},
|
|
2298
|
+
},
|
|
2299
|
+
]);
|
|
2300
|
+
if (done === null)
|
|
1563
2301
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2302
|
+
const { answer, touched } = done;
|
|
2303
|
+
if (answer.fenced !== null) {
|
|
2304
|
+
throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
|
|
1564
2305
|
}
|
|
1565
2306
|
if (fences.has(locator))
|
|
1566
2307
|
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1567
2308
|
for (const origin of withdrawn.keys())
|
|
1568
|
-
pendings.delete(origin);
|
|
2309
|
+
pendings.delete(pendingKey(locator, origin));
|
|
1569
2310
|
for (const [id, drafts] of drafted) {
|
|
1570
|
-
pendings.set(
|
|
2311
|
+
pendings.set(pendingKey(locator, id), {
|
|
2312
|
+
drafts,
|
|
2313
|
+
submission: { state: 'waiting' },
|
|
2314
|
+
lost: false,
|
|
2315
|
+
});
|
|
1571
2316
|
}
|
|
1572
|
-
|
|
1573
|
-
recorded = false;
|
|
1574
|
-
for (const path of consumed)
|
|
2317
|
+
for (const path of left)
|
|
1575
2318
|
forgetRoomDraft(locator, path);
|
|
1576
|
-
|
|
1577
|
-
|
|
2319
|
+
for (const record of arrived)
|
|
2320
|
+
rememberRoomDraft(record);
|
|
2321
|
+
savedReplica(locator, replica, batches, touched.get(locator), answer);
|
|
2322
|
+
publishChangeSet(changeSet(locator, {
|
|
2323
|
+
documents: answer.changed[locator] ?? [],
|
|
1578
2324
|
queue: true,
|
|
1579
|
-
drafts:
|
|
2325
|
+
drafts: [...left, ...arrived.map(({ path }) => path)],
|
|
1580
2326
|
}));
|
|
1581
|
-
|
|
1582
|
-
submitLater(locator);
|
|
2327
|
+
queuedSave(locator);
|
|
1583
2328
|
const identifier = join?.batch.id ??
|
|
1584
2329
|
built?.batch.id ??
|
|
1585
2330
|
[...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
|
|
1586
2331
|
return { outcome: 'saved', identifier };
|
|
1587
2332
|
}
|
|
1588
|
-
async function mark(local, locator,
|
|
1589
|
-
const
|
|
1590
|
-
|
|
1591
|
-
state
|
|
1592
|
-
(
|
|
1593
|
-
|
|
2333
|
+
async function mark(local, locator, batch, submission, lost) {
|
|
2334
|
+
const partner = partnerOf(batch, locator);
|
|
2335
|
+
for (const held of partner === null ? [locator] : [locator, partner]) {
|
|
2336
|
+
const state = pendings.get(pendingKey(held, batch.id));
|
|
2337
|
+
if (!continuing() ||
|
|
2338
|
+
state === undefined ||
|
|
2339
|
+
(JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
|
|
2340
|
+
continue;
|
|
2341
|
+
}
|
|
2342
|
+
await local.submit(held, batch.id, submission, lost);
|
|
2343
|
+
if (fences.has(held))
|
|
2344
|
+
continue;
|
|
2345
|
+
pendings.set(pendingKey(held, batch.id), { ...state, submission, lost });
|
|
2346
|
+
if (replicas.has(held))
|
|
2347
|
+
publishChangeSet(changeSet(held, { queue: true }));
|
|
1594
2348
|
}
|
|
1595
|
-
await local.submit(locator, identifier, submission, lost);
|
|
1596
|
-
if (fences.has(locator))
|
|
1597
|
-
return;
|
|
1598
|
-
pendings.set(identifier, { ...state, submission, lost });
|
|
1599
|
-
const replica = replicas.get(locator);
|
|
1600
|
-
if (replica !== undefined)
|
|
1601
|
-
publishChangeSet(changeSet(locator, replica, replica, { queue: true }));
|
|
1602
2349
|
moveTo({ ...status, ...attention() });
|
|
1603
2350
|
}
|
|
1604
2351
|
async function keepUpload(local, locator, identifier, upload) {
|
|
@@ -1607,7 +2354,8 @@ export function composeRuntime(options) {
|
|
|
1607
2354
|
await local.keepUpload(locator, identifier, upload);
|
|
1608
2355
|
if (fences.has(locator))
|
|
1609
2356
|
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1610
|
-
|
|
2357
|
+
const key = pendingKey(locator, identifier);
|
|
2358
|
+
pendings.set(key, { ...pendings.get(key), upload });
|
|
1611
2359
|
if (!continuing())
|
|
1612
2360
|
throw new Error('connection refused');
|
|
1613
2361
|
}
|
|
@@ -1640,7 +2388,7 @@ export function composeRuntime(options) {
|
|
|
1640
2388
|
if (fencedSince(locator, since))
|
|
1641
2389
|
return failure({ kind: 'room-not-readable', locator });
|
|
1642
2390
|
if (result.kind === 'scoped-refusal') {
|
|
1643
|
-
const room = replicas.get(result.locator)?.
|
|
2391
|
+
const room = replicas.get(result.locator)?.record;
|
|
1644
2392
|
if (room !== undefined)
|
|
1645
2393
|
await scoped(result, room, await openLocal());
|
|
1646
2394
|
}
|
|
@@ -1653,21 +2401,57 @@ export function composeRuntime(options) {
|
|
|
1653
2401
|
return UNCERTAIN;
|
|
1654
2402
|
}
|
|
1655
2403
|
}
|
|
1656
|
-
function
|
|
2404
|
+
function located(room, path, destination) {
|
|
2405
|
+
const crossing = roomTarget(destination);
|
|
2406
|
+
if (crossing === null) {
|
|
2407
|
+
const named = target(path, destination);
|
|
2408
|
+
return named === null || !rooms.has(room) ? null : { room, path: named };
|
|
2409
|
+
}
|
|
2410
|
+
const held = [...rooms.values()].find((listed) => listed.handle === crossing.handle);
|
|
2411
|
+
return held === undefined ? null : { room: held.locator, path: crossing.path };
|
|
2412
|
+
}
|
|
2413
|
+
async function resolveReference(room, path, destination) {
|
|
2414
|
+
if (roomTarget(destination) === null && target(path, destination) === null) {
|
|
2415
|
+
return ABSENT_TARGET;
|
|
2416
|
+
}
|
|
2417
|
+
const at = located(room, path, destination);
|
|
2418
|
+
if (at === null)
|
|
2419
|
+
return UNREADABLE;
|
|
2420
|
+
if (locked(at.room))
|
|
2421
|
+
return LOCKED_UNREADABLE;
|
|
2422
|
+
const held = replicas.get(at.room);
|
|
2423
|
+
if (held !== undefined &&
|
|
2424
|
+
(await (await openLocal()).entries(held.record.key, 'visible', [at.path])).length > 0) {
|
|
2425
|
+
return { kind: 'document', ...at };
|
|
2426
|
+
}
|
|
2427
|
+
const history = await readHistory(at.room, at.path);
|
|
2428
|
+
if (history.kind === 'no-history' || history.kind === 'revision')
|
|
2429
|
+
return ABSENT_TARGET;
|
|
2430
|
+
if (history.kind !== 'history')
|
|
2431
|
+
return history;
|
|
2432
|
+
const [last] = history.revisions;
|
|
2433
|
+
if (last === undefined)
|
|
2434
|
+
return ABSENT_TARGET;
|
|
2435
|
+
return history.deleted === true
|
|
2436
|
+
? { kind: 'deleted', ...at, revision: last }
|
|
2437
|
+
: { kind: 'document', ...at };
|
|
2438
|
+
}
|
|
2439
|
+
function queuedFacts(locator, path, localId) {
|
|
1657
2440
|
for (const batch of replicas.get(locator)?.queue ?? []) {
|
|
1658
2441
|
const change = batch.changes[queuedFile(batch, path, localId)];
|
|
1659
2442
|
if (change?.kind === 'binary')
|
|
1660
|
-
return change
|
|
2443
|
+
return change;
|
|
1661
2444
|
}
|
|
1662
|
-
|
|
2445
|
+
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2446
|
+
return drafted?.localId === localId ? drafted : null;
|
|
1663
2447
|
}
|
|
1664
|
-
function
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
2448
|
+
function batchFile(batch) {
|
|
2449
|
+
return async (localId) => {
|
|
2450
|
+
const change = batch.changes.find((held) => held.kind === 'binary' && held.localId === localId);
|
|
2451
|
+
return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
|
|
2452
|
+
};
|
|
1669
2453
|
}
|
|
1670
|
-
async function
|
|
2454
|
+
async function transferBinary(request, transfer, answer) {
|
|
1671
2455
|
if (closed || refusal !== null)
|
|
1672
2456
|
return UNCERTAIN;
|
|
1673
2457
|
try {
|
|
@@ -1679,73 +2463,109 @@ export function composeRuntime(options) {
|
|
|
1679
2463
|
if (closed || authorization === undefined || refusal !== null)
|
|
1680
2464
|
return UNCERTAIN;
|
|
1681
2465
|
const since = fencings;
|
|
1682
|
-
journal.record(request.
|
|
1683
|
-
? `transfer binary ${request.locator} ${request.path}`
|
|
1684
|
-
: `transfer binary ${request.fileRef}`);
|
|
2466
|
+
journal.record(`transfer binary ${request.locator} ${request.path}`);
|
|
1685
2467
|
try {
|
|
1686
|
-
const result = await
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
if (
|
|
1690
|
-
return failure({ kind: 'room-not-readable', locator });
|
|
1691
|
-
if (
|
|
1692
|
-
const room = replicas.get(
|
|
2468
|
+
const result = await transfer(authorization);
|
|
2469
|
+
const settled = result;
|
|
2470
|
+
journal.record(`outcome binary ${settled.kind}`);
|
|
2471
|
+
if (fencedSince(request.locator, since))
|
|
2472
|
+
return failure({ kind: 'room-not-readable', locator: request.locator });
|
|
2473
|
+
if (settled.kind === 'scoped-refusal') {
|
|
2474
|
+
const room = replicas.get(settled.locator)?.record;
|
|
1693
2475
|
if (room !== undefined)
|
|
1694
|
-
await scoped(
|
|
2476
|
+
await scoped(settled, room, await openLocal());
|
|
1695
2477
|
}
|
|
1696
|
-
if (
|
|
2478
|
+
if (settled.kind === 'revoked-authorization')
|
|
1697
2479
|
await refuse('revoked-authorization');
|
|
1698
|
-
return
|
|
2480
|
+
return answer(result);
|
|
1699
2481
|
}
|
|
1700
2482
|
catch {
|
|
1701
2483
|
journal.record('outcome binary transport-uncertainty');
|
|
1702
2484
|
return UNCERTAIN;
|
|
1703
2485
|
}
|
|
1704
2486
|
}
|
|
1705
|
-
async function
|
|
1706
|
-
|
|
2487
|
+
async function readBinary(request, name) {
|
|
2488
|
+
if (closed)
|
|
2489
|
+
return UNCERTAIN;
|
|
2490
|
+
const held = await keptFiles.find(name);
|
|
2491
|
+
if (held !== null)
|
|
2492
|
+
return held;
|
|
2493
|
+
return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
|
|
2494
|
+
}
|
|
2495
|
+
async function binaryPath(locator, path, revision) {
|
|
1707
2496
|
if (revision !== undefined)
|
|
1708
|
-
return
|
|
1709
|
-
const visible = replicas.get(locator)?.visible;
|
|
1710
|
-
const held = visible?.binaries[path];
|
|
1711
|
-
if (visible === undefined || held === undefined)
|
|
1712
|
-
return absentBinary(path);
|
|
1713
|
-
if (held.state === 'pending') {
|
|
1714
|
-
const blob = queuedBlob(locator, path, held.localId);
|
|
1715
|
-
return blob === null ? absentBinary(path) : { kind: 'binary', blob };
|
|
1716
|
-
}
|
|
1717
|
-
const record = binaryRecord(path, held, visible.revisions[path]);
|
|
1718
|
-
if (record.state !== 'accepted')
|
|
1719
|
-
return absentBinary(path);
|
|
2497
|
+
return { kind: 'path', locator, path, revision };
|
|
1720
2498
|
const replica = replicas.get(locator);
|
|
1721
|
-
|
|
2499
|
+
if (replica === undefined)
|
|
2500
|
+
return null;
|
|
2501
|
+
const local = await openLocal();
|
|
2502
|
+
const [held] = await local.entries(replica.record.key, 'visible', [path]);
|
|
2503
|
+
if (held?.binary === undefined || held.binary === null)
|
|
2504
|
+
return null;
|
|
2505
|
+
if (held.binary.state === 'pending')
|
|
2506
|
+
return { kind: 'pending', localId: held.binary.localId };
|
|
2507
|
+
const record = binaryRecord(path, held.binary, held.revision ?? undefined);
|
|
2508
|
+
if (record.state !== 'accepted')
|
|
2509
|
+
return null;
|
|
2510
|
+
const queued = replica.queue.flatMap((batch) => batch.changes);
|
|
2511
|
+
const sources = queued.flatMap((change) => (change.kind === 'move' ? [change.path] : []));
|
|
2512
|
+
const confirmedLayer = await stepped(slicing(await local.entries(replica.record.key, 'confirmed', [path, ...sources]), [
|
|
2513
|
+
path,
|
|
2514
|
+
...sources,
|
|
2515
|
+
]));
|
|
1722
2516
|
const holds = (at) => {
|
|
1723
|
-
const kept =
|
|
2517
|
+
const kept = confirmedLayer.binaries[at];
|
|
1724
2518
|
return (kept?.state === 'accepted' &&
|
|
1725
|
-
kept.
|
|
1726
|
-
|
|
2519
|
+
kept.sha256 === record.sha256 &&
|
|
2520
|
+
confirmedLayer.revisions[at] === record.revision);
|
|
1727
2521
|
};
|
|
1728
|
-
let
|
|
1729
|
-
for (let index = queued.length - 1; index >= 0 && !holds(
|
|
2522
|
+
let confirmedAt = path;
|
|
2523
|
+
for (let index = queued.length - 1; index >= 0 && !holds(confirmedAt); index -= 1) {
|
|
1730
2524
|
const change = queued[index];
|
|
1731
|
-
if (change.kind === 'move' && change.to ===
|
|
1732
|
-
|
|
2525
|
+
if (change.kind === 'move' && change.to === confirmedAt)
|
|
2526
|
+
confirmedAt = change.path;
|
|
1733
2527
|
}
|
|
1734
|
-
return
|
|
2528
|
+
return {
|
|
1735
2529
|
kind: 'path',
|
|
1736
2530
|
locator,
|
|
1737
|
-
path:
|
|
2531
|
+
path: confirmedAt,
|
|
1738
2532
|
revision: record.revision,
|
|
1739
|
-
|
|
1740
|
-
});
|
|
2533
|
+
};
|
|
1741
2534
|
}
|
|
1742
|
-
async function
|
|
2535
|
+
async function readBinaryPath(locator, path, revision) {
|
|
1743
2536
|
await ready;
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
2537
|
+
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2538
|
+
const asked = drafted !== undefined && drafted.localId === revision
|
|
2539
|
+
? { kind: 'pending', localId: revision }
|
|
2540
|
+
: await binaryPath(locator, path, revision);
|
|
2541
|
+
if (asked === null)
|
|
2542
|
+
return absentBinary(path);
|
|
2543
|
+
if (asked.kind === 'pending') {
|
|
2544
|
+
const facts = queuedFacts(locator, path, asked.localId);
|
|
2545
|
+
return facts === null
|
|
2546
|
+
? absentBinary(path)
|
|
2547
|
+
: {
|
|
2548
|
+
kind: 'binary',
|
|
2549
|
+
file: asked.localId,
|
|
2550
|
+
byteLength: facts.byteLength,
|
|
2551
|
+
mediaType: facts.mediaType,
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
|
|
2555
|
+
}
|
|
2556
|
+
async function linkBinaryPath(locator, path, revision) {
|
|
2557
|
+
await ready;
|
|
2558
|
+
const asked = await binaryPath(locator, path, revision);
|
|
2559
|
+
if (asked === null || asked.kind === 'pending')
|
|
2560
|
+
return absentBinary(path);
|
|
2561
|
+
return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
|
|
2562
|
+
}
|
|
2563
|
+
function readPart(file, part) {
|
|
2564
|
+
return options.database.ask({ kind: 'file.part', key: file, part });
|
|
2565
|
+
}
|
|
2566
|
+
async function cleanUp() {
|
|
2567
|
+
const freed = await options.database.ask({ kind: 'opened.clean' });
|
|
2568
|
+
return freed + ((await options.shell.cleanUp?.()) ?? 0);
|
|
1749
2569
|
}
|
|
1750
2570
|
function declaredAgents() {
|
|
1751
2571
|
for (const capability of capabilities) {
|
|
@@ -1763,7 +2583,14 @@ export function composeRuntime(options) {
|
|
|
1763
2583
|
}
|
|
1764
2584
|
return undefined;
|
|
1765
2585
|
}
|
|
1766
|
-
|
|
2586
|
+
function calledRooms(name, input) {
|
|
2587
|
+
const member = declaredAction(name)?.room;
|
|
2588
|
+
if (member === undefined || input === null || typeof input !== 'object')
|
|
2589
|
+
return [];
|
|
2590
|
+
const named = input[member];
|
|
2591
|
+
return (Array.isArray(named) ? named : [named]).filter((room) => typeof room === 'string');
|
|
2592
|
+
}
|
|
2593
|
+
async function invokeAction(name, input, dispatching) {
|
|
1767
2594
|
await ready;
|
|
1768
2595
|
if (closed || refusal !== null)
|
|
1769
2596
|
return UNCERTAIN;
|
|
@@ -1777,11 +2604,12 @@ export function composeRuntime(options) {
|
|
|
1777
2604
|
}
|
|
1778
2605
|
if (closed || authorization === undefined || refusal !== null)
|
|
1779
2606
|
return UNCERTAIN;
|
|
2607
|
+
await dispatching?.();
|
|
1780
2608
|
journal.record(`transfer action ${name}`);
|
|
1781
2609
|
try {
|
|
1782
2610
|
const result = await options.adapter.action(authorization, { name, input });
|
|
1783
2611
|
if (result.kind === 'scoped-refusal') {
|
|
1784
|
-
const room = replicas.get(result.locator)?.
|
|
2612
|
+
const room = replicas.get(result.locator)?.record;
|
|
1785
2613
|
if (room !== undefined)
|
|
1786
2614
|
await scoped(result, room, await openLocal());
|
|
1787
2615
|
}
|
|
@@ -1797,98 +2625,492 @@ export function composeRuntime(options) {
|
|
|
1797
2625
|
return UNCERTAIN;
|
|
1798
2626
|
}
|
|
1799
2627
|
}
|
|
1800
|
-
function
|
|
1801
|
-
return
|
|
2628
|
+
function unsettled(result) {
|
|
2629
|
+
return result.kind !== 'action' && result.retryable;
|
|
2630
|
+
}
|
|
2631
|
+
async function recoveredAction(name, input) {
|
|
2632
|
+
const operation = operationOf(input);
|
|
2633
|
+
if (operation === null)
|
|
2634
|
+
return invokeAction(name, input);
|
|
2635
|
+
const kept = async () => (await openLocal()).keepCall({ operation, name, input });
|
|
2636
|
+
calling.add(operation);
|
|
2637
|
+
try {
|
|
2638
|
+
let result = await invokeAction(name, input, kept);
|
|
2639
|
+
if (unsettled(result)) {
|
|
2640
|
+
await recovery.attempt(`action ${operation}`, async () => {
|
|
2641
|
+
if (closed || refusal !== null)
|
|
2642
|
+
return 'done';
|
|
2643
|
+
if (!socketOpen)
|
|
2644
|
+
return 'again';
|
|
2645
|
+
result = await invokeAction(name, input, kept);
|
|
2646
|
+
return unsettled(result) ? 'again' : 'done';
|
|
2647
|
+
}, { again: true });
|
|
2648
|
+
}
|
|
2649
|
+
if (!unsettled(result))
|
|
2650
|
+
await (await openLocal()).dropCall(operation);
|
|
2651
|
+
return result;
|
|
2652
|
+
}
|
|
2653
|
+
finally {
|
|
2654
|
+
calling.delete(operation);
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
async function resendCalls(signal) {
|
|
2658
|
+
if (!socketOpen || closed || stopping)
|
|
2659
|
+
return 'done';
|
|
2660
|
+
const local = await openLocal();
|
|
2661
|
+
for (const held of await local.calls()) {
|
|
2662
|
+
if (calling.has(held.operation))
|
|
2663
|
+
continue;
|
|
2664
|
+
if (closed || signal.aborted)
|
|
2665
|
+
return 'done';
|
|
2666
|
+
if (pastHorizon(held.operation)) {
|
|
2667
|
+
await local.dropCall(held.operation);
|
|
2668
|
+
continue;
|
|
2669
|
+
}
|
|
2670
|
+
calling.add(held.operation);
|
|
2671
|
+
try {
|
|
2672
|
+
if (unsettled(await invokeAction(held.name, held.input)))
|
|
2673
|
+
return 'again';
|
|
2674
|
+
await local.dropCall(held.operation);
|
|
2675
|
+
}
|
|
2676
|
+
finally {
|
|
2677
|
+
calling.delete(held.operation);
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
return 'done';
|
|
2681
|
+
}
|
|
2682
|
+
function continuing() {
|
|
2683
|
+
return !closed && !discoveryPaused;
|
|
2684
|
+
}
|
|
2685
|
+
function reachedBy(locator, roomVersion) {
|
|
2686
|
+
const held = replicas.get(locator);
|
|
2687
|
+
return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
|
|
2688
|
+
}
|
|
2689
|
+
async function writtenAfter(locator, roomVersion, path) {
|
|
2690
|
+
const held = replicas.get(locator);
|
|
2691
|
+
if (held === undefined)
|
|
2692
|
+
return false;
|
|
2693
|
+
const [entry] = await (await openLocal()).entries(held.record.key, 'confirmed', [path]);
|
|
2694
|
+
const written = entry?.written ?? undefined;
|
|
2695
|
+
return (written !== undefined &&
|
|
2696
|
+
options.adapter.reached(locator, written, roomVersion) &&
|
|
2697
|
+
!options.adapter.reached(locator, roomVersion, written));
|
|
2698
|
+
}
|
|
2699
|
+
function overriddenTexts(texts, overrides) {
|
|
2700
|
+
return async (paths) => {
|
|
2701
|
+
const known = new Map(await texts(paths.filter((path) => !overrides.has(path))));
|
|
2702
|
+
for (const path of paths) {
|
|
2703
|
+
const entry = overrides.get(path);
|
|
2704
|
+
if (entry === undefined)
|
|
2705
|
+
continue;
|
|
2706
|
+
if (entry === null)
|
|
2707
|
+
known.delete(path);
|
|
2708
|
+
else
|
|
2709
|
+
known.set(path, entry.binary !== null ? null : entry.content);
|
|
2710
|
+
}
|
|
2711
|
+
return known;
|
|
2712
|
+
};
|
|
2713
|
+
}
|
|
2714
|
+
async function unreadableRoom(texts, walk, paths) {
|
|
2715
|
+
if (paths !== null) {
|
|
2716
|
+
const text = {};
|
|
2717
|
+
for (const [path, content] of await texts(paths))
|
|
2718
|
+
if (content !== null)
|
|
2719
|
+
text[path] = content;
|
|
2720
|
+
return stepped(unreadableIn(text, Object.keys(text)));
|
|
2721
|
+
}
|
|
2722
|
+
for (let after = null;;) {
|
|
2723
|
+
const page = await walk(after);
|
|
2724
|
+
const text = {};
|
|
2725
|
+
for (const { path, content } of page.entries)
|
|
2726
|
+
if (content !== null)
|
|
2727
|
+
text[path] = content;
|
|
2728
|
+
const refused = await stepped(unreadableIn(text, Object.keys(text)));
|
|
2729
|
+
if (refused !== null)
|
|
2730
|
+
return refused;
|
|
2731
|
+
if (page.next === null)
|
|
2732
|
+
return null;
|
|
2733
|
+
after = page.next;
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
async function validatedRoom(local, record, view, previous, overrides = null) {
|
|
2737
|
+
const composition = compositionOf(record);
|
|
2738
|
+
const modeled = record.model.status !== 'unavailable' || record.selectionModel.status !== 'unavailable';
|
|
2739
|
+
const lookup = overrides === null
|
|
2740
|
+
? lookupOf(local, record.key, view)
|
|
2741
|
+
: overriddenLookup(lookupOf(local, record.key, view), overrides);
|
|
2742
|
+
const texts = overrides === null
|
|
2743
|
+
? textsOf(local, record.key, view)
|
|
2744
|
+
: overriddenTexts(textsOf(local, record.key, view), overrides);
|
|
2745
|
+
const walk = overrides === null
|
|
2746
|
+
? walkOf(local, record.key, view)
|
|
2747
|
+
: overriddenWalk(walkOf(local, record.key, view), overrides);
|
|
2748
|
+
let changes = null;
|
|
2749
|
+
let reshaped = false;
|
|
2750
|
+
if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
|
|
2751
|
+
const [was, now] = await Promise.all([
|
|
2752
|
+
local.entries(record.key, 'confirmed', previous.changed),
|
|
2753
|
+
overrides === null
|
|
2754
|
+
? local.entries(record.key, view, previous.changed)
|
|
2755
|
+
: Promise.resolve([]),
|
|
2756
|
+
]);
|
|
2757
|
+
const before = new Map(was.map((entry) => [entry.path, entry]));
|
|
2758
|
+
const after = overrides === null
|
|
2759
|
+
? new Map(now.map((entry) => [entry.path, entry]))
|
|
2760
|
+
: overrides;
|
|
2761
|
+
changes = previous.changed.flatMap((path) => {
|
|
2762
|
+
const one = before.get(path) ?? null;
|
|
2763
|
+
const other = after.get(path) ?? null;
|
|
2764
|
+
reshaped ||= textDiffers(one, other);
|
|
2765
|
+
const held = textOf(one);
|
|
2766
|
+
const next = textOf(other);
|
|
2767
|
+
return held === next ? [] : [{ path, was: held, now: next }];
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
const named = changes === null ? null : changes.map(({ path }) => path);
|
|
2771
|
+
const textChanged = changes === null || changes.length > 0 || reshaped;
|
|
2772
|
+
const modelsChanged = previous === null || !sameComposition(compositionOf(previous.record), composition);
|
|
2773
|
+
if (modeled && (textChanged || modelsChanged)) {
|
|
2774
|
+
const clean = previous !== null &&
|
|
2775
|
+
changes !== null &&
|
|
2776
|
+
(await changedCleanly(changes, compositionOf(previous.record), composition, {
|
|
2777
|
+
lookup,
|
|
2778
|
+
texts,
|
|
2779
|
+
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2780
|
+
}));
|
|
2781
|
+
if (clean)
|
|
2782
|
+
return null;
|
|
2783
|
+
const verdict = await validatedCollection(walk, lookup, composition);
|
|
2784
|
+
if (verdict.verdict === 'accept')
|
|
2785
|
+
return null;
|
|
2786
|
+
if (verdict.rule !== 'model-unavailable')
|
|
2787
|
+
return { kind: 'refused', refusal: verdict };
|
|
2788
|
+
return unreadableRoom(texts, walk, named);
|
|
2789
|
+
}
|
|
2790
|
+
if (textChanged)
|
|
2791
|
+
return unreadableRoom(texts, walk, named);
|
|
2792
|
+
return null;
|
|
2793
|
+
}
|
|
2794
|
+
function refreshingOf(local, key, liveness, replaying = null) {
|
|
2795
|
+
const write = replaying === null ? exclusive : installWrite;
|
|
2796
|
+
return {
|
|
2797
|
+
async turn() {
|
|
2798
|
+
if (replaying === null)
|
|
2799
|
+
return;
|
|
2800
|
+
installing(replaying, 'replaying');
|
|
2801
|
+
await screened();
|
|
2802
|
+
},
|
|
2803
|
+
prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
|
|
2804
|
+
async stage(page) {
|
|
2805
|
+
for (let written = 0;;) {
|
|
2806
|
+
if (replaying !== null)
|
|
2807
|
+
await screened();
|
|
2808
|
+
const end = batchEnd(page.entries, written);
|
|
2809
|
+
const last = end === page.entries.length;
|
|
2810
|
+
const entries = page.entries.slice(written, end);
|
|
2811
|
+
const staged = await write(async () => {
|
|
2812
|
+
if (!liveness())
|
|
2813
|
+
return false;
|
|
2814
|
+
await timed(() => local.refresh(key, last
|
|
2815
|
+
? { ...page, entries }
|
|
2816
|
+
: { entries, removals: [], folders: [], folderRemovals: [] }));
|
|
2817
|
+
return true;
|
|
2818
|
+
});
|
|
2819
|
+
if (!staged)
|
|
2820
|
+
return false;
|
|
2821
|
+
if (last)
|
|
2822
|
+
return true;
|
|
2823
|
+
written = end;
|
|
2824
|
+
}
|
|
2825
|
+
},
|
|
2826
|
+
};
|
|
2827
|
+
}
|
|
2828
|
+
function batchEnd(entries, from) {
|
|
2829
|
+
let end = from;
|
|
2830
|
+
let spent = 0;
|
|
2831
|
+
while (end < entries.length) {
|
|
2832
|
+
const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
|
|
2833
|
+
if (spent + size > stageBytes && end > from)
|
|
2834
|
+
break;
|
|
2835
|
+
spent += size;
|
|
2836
|
+
end += 1;
|
|
2837
|
+
}
|
|
2838
|
+
return end;
|
|
2839
|
+
}
|
|
2840
|
+
async function timed(work) {
|
|
2841
|
+
const started = performance.now();
|
|
2842
|
+
const done = await work();
|
|
2843
|
+
const spent = performance.now() - started;
|
|
2844
|
+
if (spent < STAGE_AIM_MS / 2)
|
|
2845
|
+
stageBytes = Math.min(STAGE_BYTES, Math.round(stageBytes * 1.5));
|
|
2846
|
+
else if (spent > STAGE_AIM_MS * 2) {
|
|
2847
|
+
stageBytes = Math.max(STAGE_FLOOR, Math.round(stageBytes / 1.5));
|
|
2848
|
+
}
|
|
2849
|
+
return done;
|
|
2850
|
+
}
|
|
2851
|
+
function installWrite(work) {
|
|
2852
|
+
const next = installWrites.then(() => exclusive(work), () => exclusive(work));
|
|
2853
|
+
installWrites = next.then(() => undefined, () => undefined);
|
|
2854
|
+
return next;
|
|
1802
2855
|
}
|
|
1803
|
-
function
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
2856
|
+
function screened() {
|
|
2857
|
+
if (onScreen || closed)
|
|
2858
|
+
return Promise.resolve();
|
|
2859
|
+
queueReadiness();
|
|
2860
|
+
return new Promise((resolve) => shownWaiters.push(resolve));
|
|
1807
2861
|
}
|
|
1808
|
-
function
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
2862
|
+
async function heldPart() {
|
|
2863
|
+
while (partsFree === 0)
|
|
2864
|
+
await new Promise((admit) => partWaiters.push(admit));
|
|
2865
|
+
partsFree -= 1;
|
|
2866
|
+
return freed();
|
|
1813
2867
|
}
|
|
1814
|
-
|
|
1815
|
-
|
|
2868
|
+
function freed() {
|
|
2869
|
+
let released = false;
|
|
2870
|
+
return () => {
|
|
2871
|
+
if (released)
|
|
2872
|
+
return;
|
|
2873
|
+
released = true;
|
|
2874
|
+
partsFree += 1;
|
|
2875
|
+
partWaiters.shift()?.();
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
function installing(locator, step, share = null) {
|
|
2879
|
+
installs.set(locator, { step, share });
|
|
2880
|
+
queueReadiness();
|
|
2881
|
+
}
|
|
2882
|
+
function installed(locator) {
|
|
2883
|
+
if (installs.delete(locator))
|
|
2884
|
+
queueReadiness();
|
|
2885
|
+
}
|
|
2886
|
+
function screen(value) {
|
|
2887
|
+
if (value === onScreen)
|
|
2888
|
+
return;
|
|
2889
|
+
onScreen = value;
|
|
2890
|
+
journal.record(`installation ${value ? 'shown' : 'hidden'}`);
|
|
2891
|
+
queueReadiness();
|
|
2892
|
+
if (!value)
|
|
2893
|
+
return;
|
|
2894
|
+
for (const resolve of shownWaiters.splice(0))
|
|
2895
|
+
resolve();
|
|
2896
|
+
}
|
|
2897
|
+
async function pullRefresh(authorized, held, liveness) {
|
|
2898
|
+
if (!liveness())
|
|
1816
2899
|
return unconfirmed;
|
|
1817
2900
|
const turn = generation;
|
|
2901
|
+
const { record } = held;
|
|
2902
|
+
const base = held.confirmed;
|
|
2903
|
+
const local = await openLocal();
|
|
1818
2904
|
try {
|
|
1819
|
-
|
|
1820
|
-
const
|
|
1821
|
-
|
|
1822
|
-
|
|
2905
|
+
await exclusive(() => local.unrefresh(record.key));
|
|
2906
|
+
const result = await advancement(reads, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, liveness));
|
|
2907
|
+
if (result.kind !== 'advanced') {
|
|
2908
|
+
journal.record(`transfer refresh ${record.locator} ${result.kind}`);
|
|
2909
|
+
return result;
|
|
2910
|
+
}
|
|
2911
|
+
const refused = await validatedRoom(local, result.record, 'refreshed', {
|
|
2912
|
+
record,
|
|
2913
|
+
changed: [...result.changed],
|
|
2914
|
+
});
|
|
2915
|
+
if (refused !== null) {
|
|
2916
|
+
journal.record(`transfer refresh ${record.locator} refused`);
|
|
2917
|
+
return refused;
|
|
2918
|
+
}
|
|
2919
|
+
journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
|
|
2920
|
+
return { kind: 'advanced', record: result.record, base };
|
|
1823
2921
|
}
|
|
1824
2922
|
catch (error) {
|
|
1825
|
-
journal.record(`transfer refresh ${
|
|
2923
|
+
journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
|
|
1826
2924
|
throw error;
|
|
1827
2925
|
}
|
|
1828
2926
|
finally {
|
|
1829
|
-
reads.finished(
|
|
1830
|
-
}
|
|
2927
|
+
reads.finished(record.locator, turn);
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
const heldContents = new Map();
|
|
2931
|
+
function heldContentOf(local, key) {
|
|
2932
|
+
const known = heldContents.get(key);
|
|
2933
|
+
if (known !== undefined)
|
|
2934
|
+
return known;
|
|
2935
|
+
if (heldContents.size > ROOM_READS)
|
|
2936
|
+
heldContents.clear();
|
|
2937
|
+
const made = {
|
|
2938
|
+
files: (after, limit) => local.files(key, 'confirmed', after, limit),
|
|
2939
|
+
async contents(paths) {
|
|
2940
|
+
return new Map((await local.entries(key, 'confirmed', paths)).flatMap(({ path, content, revision }) => content === null || revision === null ? [] : [[path, { content, revision }]]));
|
|
2941
|
+
},
|
|
2942
|
+
};
|
|
2943
|
+
heldContents.set(key, made);
|
|
2944
|
+
return made;
|
|
1831
2945
|
}
|
|
1832
|
-
function staging(local,
|
|
2946
|
+
function staging(local, liveness) {
|
|
1833
2947
|
return {
|
|
1834
2948
|
held: (locator) => transfers.get(locator) ?? null,
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
await local.stage(record, page);
|
|
1844
|
-
}
|
|
1845
|
-
catch (error) {
|
|
1846
|
-
if (!exceeded(error))
|
|
1847
|
-
throw error;
|
|
1848
|
-
await exceededOn(record.locator, bytes);
|
|
1849
|
-
return false;
|
|
2949
|
+
async turn(locator, step) {
|
|
2950
|
+
installing(locator, step);
|
|
2951
|
+
for (;;) {
|
|
2952
|
+
await screened();
|
|
2953
|
+
const release = step === 'downloading' ? await heldPart() : () => { };
|
|
2954
|
+
if (onScreen || closed)
|
|
2955
|
+
return release;
|
|
2956
|
+
release();
|
|
1850
2957
|
}
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
2958
|
+
},
|
|
2959
|
+
spare() {
|
|
2960
|
+
if (!onScreen || closed || partsFree === 0)
|
|
2961
|
+
return null;
|
|
2962
|
+
partsFree -= 1;
|
|
2963
|
+
return freed();
|
|
2964
|
+
},
|
|
2965
|
+
arriving(locator) {
|
|
2966
|
+
let ended = false;
|
|
2967
|
+
return {
|
|
2968
|
+
at(token, bytes) {
|
|
2969
|
+
if (ended)
|
|
2970
|
+
return;
|
|
2971
|
+
const held = arrivals.get(locator) ?? new Map();
|
|
2972
|
+
arrivals.set(locator, held.set(token, bytes));
|
|
2973
|
+
const now = performance.now();
|
|
2974
|
+
if (now - arrivalReported < ARRIVAL_REPORT_MS)
|
|
2975
|
+
return;
|
|
2976
|
+
arrivalReported = now;
|
|
2977
|
+
queueReadiness();
|
|
2978
|
+
},
|
|
2979
|
+
end() {
|
|
2980
|
+
ended = true;
|
|
2981
|
+
if (arrivals.delete(locator))
|
|
2982
|
+
queueReadiness();
|
|
2983
|
+
},
|
|
2984
|
+
};
|
|
2985
|
+
},
|
|
2986
|
+
received: (locator, token) => local.transferPart(locator, token),
|
|
2987
|
+
keep: (locator, token, received) => installWrite(async () => {
|
|
2988
|
+
if (liveness())
|
|
2989
|
+
await local.keepPart(locator, token, received);
|
|
1854
2990
|
}),
|
|
2991
|
+
announced(locator) {
|
|
2992
|
+
const later = announced.get(locator);
|
|
2993
|
+
return later === undefined ? [] : [later];
|
|
2994
|
+
},
|
|
2995
|
+
async stage(record, page, composition, received) {
|
|
2996
|
+
const { locator } = record;
|
|
2997
|
+
const bytes = pageBytes(page);
|
|
2998
|
+
const whole = received?.byteLength ?? 0;
|
|
2999
|
+
const from = record.writing?.written ?? 0;
|
|
3000
|
+
const token = record.writing?.token ?? null;
|
|
3001
|
+
const entries = page.entries.length;
|
|
3002
|
+
let held = record;
|
|
3003
|
+
if (token !== null)
|
|
3004
|
+
arrivals.get(locator)?.delete(token);
|
|
3005
|
+
installing(locator, 'writing', { written: from, entries, bytes: whole });
|
|
3006
|
+
for (let written = from;;) {
|
|
3007
|
+
await screened();
|
|
3008
|
+
if (!liveness())
|
|
3009
|
+
return null;
|
|
3010
|
+
const end = batchEnd(page.entries, written);
|
|
3011
|
+
const last = end === entries;
|
|
3012
|
+
const next = last
|
|
3013
|
+
? {
|
|
3014
|
+
...held,
|
|
3015
|
+
writing: null,
|
|
3016
|
+
done: {
|
|
3017
|
+
parts: held.done.parts + (received === null ? 0 : 1),
|
|
3018
|
+
bytes: held.done.bytes + whole,
|
|
3019
|
+
},
|
|
3020
|
+
}
|
|
3021
|
+
: { ...held, writing: { token, written: end } };
|
|
3022
|
+
const key = await installWrite(async () => {
|
|
3023
|
+
if (!liveness())
|
|
3024
|
+
return null;
|
|
3025
|
+
if (written === from && !(await admitted(local, locator, bytes)))
|
|
3026
|
+
return null;
|
|
3027
|
+
try {
|
|
3028
|
+
return await timed(() => local.stage(next, composition, {
|
|
3029
|
+
entries: page.entries.slice(written, end),
|
|
3030
|
+
folders: last ? page.folders : [],
|
|
3031
|
+
}, last && received !== null && token !== null ? token : undefined));
|
|
3032
|
+
}
|
|
3033
|
+
catch (error) {
|
|
3034
|
+
if (!exceeded(error))
|
|
3035
|
+
throw error;
|
|
3036
|
+
await exceededOn(locator, bytes);
|
|
3037
|
+
return null;
|
|
3038
|
+
}
|
|
3039
|
+
});
|
|
3040
|
+
if (key === null)
|
|
3041
|
+
return null;
|
|
3042
|
+
const arrived = !transfers.has(locator);
|
|
3043
|
+
held = { ...next, key };
|
|
3044
|
+
transfers.set(locator, held);
|
|
3045
|
+
if (arrived)
|
|
3046
|
+
liveQueries.recover();
|
|
3047
|
+
if (last) {
|
|
3048
|
+
installing(locator, held.pageToken === null ? 'replaying' : 'downloading');
|
|
3049
|
+
liveQueries.recover();
|
|
3050
|
+
return held;
|
|
3051
|
+
}
|
|
3052
|
+
installing(locator, 'writing', { written: end, entries, bytes: whole });
|
|
3053
|
+
written = end;
|
|
3054
|
+
}
|
|
3055
|
+
},
|
|
3056
|
+
async refreshing(locator, key) {
|
|
3057
|
+
await exclusive(() => local.unrefresh(key));
|
|
3058
|
+
return refreshingOf(local, key, liveness, locator);
|
|
3059
|
+
},
|
|
1855
3060
|
abandon: (locator) => exclusive(async () => {
|
|
1856
|
-
if (!
|
|
3061
|
+
if (!liveness())
|
|
1857
3062
|
return;
|
|
1858
3063
|
await local.abandon(locator);
|
|
1859
3064
|
transfers.delete(locator);
|
|
1860
|
-
|
|
3065
|
+
liveQueries.recover();
|
|
3066
|
+
sweep();
|
|
3067
|
+
}),
|
|
3068
|
+
setAside: (locator) => exclusive(async () => {
|
|
3069
|
+
if (!liveness())
|
|
3070
|
+
return;
|
|
3071
|
+
await local.abandon(locator, true);
|
|
3072
|
+
transfers.delete(locator);
|
|
3073
|
+
liveQueries.recover();
|
|
3074
|
+
}),
|
|
3075
|
+
release: (key) => exclusive(async () => {
|
|
3076
|
+
await local.release(key);
|
|
3077
|
+
sweep();
|
|
1861
3078
|
}),
|
|
3079
|
+
content: (key) => heldContentOf(local, key),
|
|
3080
|
+
validate: (record, view) => validatedRoom(local, record, view, null),
|
|
1862
3081
|
};
|
|
1863
3082
|
}
|
|
1864
|
-
function reusable(room) {
|
|
1865
|
-
const record = replicas.get(room.locator)?.
|
|
3083
|
+
function reusable(room, local) {
|
|
3084
|
+
const record = replicas.get(room.locator)?.record;
|
|
1866
3085
|
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1867
3086
|
return null;
|
|
1868
|
-
return
|
|
3087
|
+
return heldContentOf(local, record.key);
|
|
1869
3088
|
}
|
|
1870
|
-
async function pullBootstrap(authorized, room, local,
|
|
1871
|
-
if (!
|
|
3089
|
+
async function pullBootstrap(authorized, room, local, liveness) {
|
|
3090
|
+
if (!liveness())
|
|
1872
3091
|
return unconfirmed;
|
|
1873
3092
|
const turn = generation;
|
|
1874
3093
|
if (replicas.has(room.locator)) {
|
|
1875
3094
|
catching.add(room.locator);
|
|
1876
|
-
|
|
3095
|
+
liveQueries.recover();
|
|
1877
3096
|
}
|
|
1878
3097
|
try {
|
|
1879
3098
|
const result = await bootstrapRoom(reads, authorized, room, {
|
|
1880
3099
|
collection: modelOf(room.locator),
|
|
1881
3100
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
1882
|
-
}, (models, held) => (
|
|
1883
|
-
const detail = result.kind === 'staged' ? result.
|
|
3101
|
+
}, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
|
|
3102
|
+
const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
|
|
1884
3103
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1885
|
-
if (result.kind !== 'staged')
|
|
3104
|
+
if (result.kind !== 'staged') {
|
|
1886
3105
|
caughtUpTo(room.locator);
|
|
3106
|
+
installed(room.locator);
|
|
3107
|
+
}
|
|
1887
3108
|
return result;
|
|
1888
3109
|
}
|
|
1889
3110
|
catch (error) {
|
|
1890
3111
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
1891
3112
|
caughtUpTo(room.locator);
|
|
3113
|
+
installed(room.locator);
|
|
1892
3114
|
throw error;
|
|
1893
3115
|
}
|
|
1894
3116
|
finally {
|
|
@@ -1897,10 +3119,10 @@ export function composeRuntime(options) {
|
|
|
1897
3119
|
}
|
|
1898
3120
|
function caughtUpTo(locator) {
|
|
1899
3121
|
if (catching.delete(locator))
|
|
1900
|
-
|
|
3122
|
+
liveQueries.recover();
|
|
1901
3123
|
}
|
|
1902
3124
|
function behind(room) {
|
|
1903
|
-
const held = replicas.get(room.locator)?.
|
|
3125
|
+
const held = replicas.get(room.locator)?.record.roomVersion;
|
|
1904
3126
|
return (held !== undefined &&
|
|
1905
3127
|
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
1906
3128
|
}
|
|
@@ -1913,12 +3135,11 @@ export function composeRuntime(options) {
|
|
|
1913
3135
|
return advanceOnce(room, local, authorized, live);
|
|
1914
3136
|
}
|
|
1915
3137
|
async function advanceOnce(room, local, authorized, live) {
|
|
1916
|
-
await referenced(room.locator);
|
|
1917
3138
|
if (!live() || paused.has(room.locator))
|
|
1918
3139
|
return unconfirmed;
|
|
1919
3140
|
const held = replicas.get(room.locator);
|
|
1920
3141
|
if (held !== undefined) {
|
|
1921
|
-
const refreshed = await pullRefresh(authorized, held
|
|
3142
|
+
const refreshed = await pullRefresh(authorized, held, live);
|
|
1922
3143
|
if (!live())
|
|
1923
3144
|
return unconfirmed;
|
|
1924
3145
|
if (refreshed.kind === 'still-current') {
|
|
@@ -1933,10 +3154,11 @@ export function composeRuntime(options) {
|
|
|
1933
3154
|
return null;
|
|
1934
3155
|
}
|
|
1935
3156
|
if (refreshed.kind === 'advanced') {
|
|
1936
|
-
|
|
1937
|
-
|
|
3157
|
+
const landing = { kind: 'refreshed', base: refreshed.base };
|
|
3158
|
+
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3159
|
+
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
1938
3160
|
const reached = now !== undefined &&
|
|
1939
|
-
options.adapter.reached(room.locator, now, refreshed.
|
|
3161
|
+
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
1940
3162
|
return reached ? null : advance(room, local, authorized, live);
|
|
1941
3163
|
}
|
|
1942
3164
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
@@ -1946,17 +3168,18 @@ export function composeRuntime(options) {
|
|
|
1946
3168
|
return refreshed;
|
|
1947
3169
|
if (opened !== null) {
|
|
1948
3170
|
owed.add(room.locator);
|
|
1949
|
-
|
|
3171
|
+
liveQueries.recover();
|
|
1950
3172
|
return unconfirmed;
|
|
1951
3173
|
}
|
|
3174
|
+
if (reloadsDue.get(room.locator) === generation)
|
|
3175
|
+
return unconfirmed;
|
|
1952
3176
|
}
|
|
1953
3177
|
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
1954
|
-
if (
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
return null;
|
|
3178
|
+
if (bootstrapped.kind === 'staged') {
|
|
3179
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
3180
|
+
return live() ? null : unconfirmed;
|
|
3181
|
+
}
|
|
3182
|
+
return live() ? bootstrapped : unconfirmed;
|
|
1960
3183
|
}
|
|
1961
3184
|
async function withhold(withheld, room, local, progress) {
|
|
1962
3185
|
if (withheld.kind === 'unconfirmed')
|
|
@@ -2028,57 +3251,254 @@ export function composeRuntime(options) {
|
|
|
2028
3251
|
(accepted.author === undefined ? '' : ` author ${accepted.author}`) +
|
|
2029
3252
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
2030
3253
|
}
|
|
2031
|
-
|
|
2032
|
-
|
|
3254
|
+
function settling(acceptance) {
|
|
3255
|
+
return acceptance !== null && 'apply' in acceptance;
|
|
3256
|
+
}
|
|
3257
|
+
function refusalOf(acceptance) {
|
|
3258
|
+
return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
|
|
3259
|
+
}
|
|
3260
|
+
async function acceptance(room, local, submitted, accepted) {
|
|
2033
3261
|
if (!continuing())
|
|
2034
3262
|
return unconfirmed;
|
|
2035
3263
|
const current = replicas.get(room.locator);
|
|
2036
3264
|
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
2037
3265
|
return null;
|
|
2038
3266
|
}
|
|
2039
|
-
const held = current.
|
|
3267
|
+
const held = current.record.roomVersion;
|
|
2040
3268
|
logAccepted(room.locator, submitted, accepted);
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
3269
|
+
let record = current.record;
|
|
3270
|
+
let changes;
|
|
3271
|
+
if (!options.adapter.reached(room.locator, held, accepted.roomVersion)) {
|
|
3272
|
+
const paths = [...returnedPaths(submitted, accepted)];
|
|
3273
|
+
const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
|
|
3274
|
+
const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
|
|
3275
|
+
record = {
|
|
3276
|
+
...record,
|
|
3277
|
+
roomVersion: options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
3278
|
+
? accepted.roomVersion
|
|
3279
|
+
: held,
|
|
3280
|
+
};
|
|
3281
|
+
const overrides = new Map([
|
|
3282
|
+
...back.set.map((entry) => [entry.path, entry]),
|
|
3283
|
+
...back.removed.map((path) => [path, null]),
|
|
3284
|
+
]);
|
|
3285
|
+
const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
|
|
3286
|
+
if (refused !== null) {
|
|
3287
|
+
const key = pendingKey(room.locator, submitted.id);
|
|
3288
|
+
return {
|
|
3289
|
+
locator: room.locator,
|
|
3290
|
+
left: [],
|
|
3291
|
+
commit: {
|
|
3292
|
+
rooms: [],
|
|
3293
|
+
writes: [
|
|
3294
|
+
{
|
|
3295
|
+
locator: room.locator,
|
|
3296
|
+
accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
|
|
3297
|
+
},
|
|
3298
|
+
],
|
|
3299
|
+
},
|
|
3300
|
+
refused,
|
|
3301
|
+
apply() {
|
|
3302
|
+
const state = pendings.get(key);
|
|
3303
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
3304
|
+
pendings.set(key, { ...state, accepted: accepted.roomVersion });
|
|
3305
|
+
return changeSet(room.locator, { queue: true });
|
|
3306
|
+
},
|
|
3307
|
+
};
|
|
3308
|
+
}
|
|
3309
|
+
changes = back;
|
|
3310
|
+
}
|
|
3311
|
+
const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
|
|
3312
|
+
return {
|
|
3313
|
+
...settled,
|
|
3314
|
+
apply(answer) {
|
|
3315
|
+
const set = settled.apply(answer);
|
|
3316
|
+
pendings.delete(pendingKey(room.locator, submitted.id));
|
|
3317
|
+
unreadable.delete(room.locator);
|
|
3318
|
+
sourceRefusals.delete(room.locator);
|
|
3319
|
+
declarationRefusals.delete(room.locator);
|
|
3320
|
+
list(room, true);
|
|
3321
|
+
return set;
|
|
3322
|
+
},
|
|
3323
|
+
};
|
|
3324
|
+
}
|
|
3325
|
+
async function adoptedTogether(local, acceptances) {
|
|
3326
|
+
const settlements = acceptances.filter(settling);
|
|
3327
|
+
if (settlements.length > 0) {
|
|
3328
|
+
publishChangeSet(await together(local, settlements));
|
|
3329
|
+
moveTo({ ...status, ...attention() });
|
|
3330
|
+
}
|
|
3331
|
+
await unpair(local);
|
|
2064
3332
|
}
|
|
2065
3333
|
async function adopted(room, local, submitted, accepted) {
|
|
2066
|
-
const failed = await exclusive(() =>
|
|
3334
|
+
const failed = await exclusive(async () => {
|
|
3335
|
+
const adopting = await acceptance(room, local, submitted, accepted);
|
|
3336
|
+
await adoptedTogether(local, [adopting]);
|
|
3337
|
+
return refusalOf(adopting);
|
|
3338
|
+
});
|
|
3339
|
+
if (failed === null)
|
|
3340
|
+
return true;
|
|
3341
|
+
if (failed.kind === 'refused' && continuing())
|
|
3342
|
+
refusedSource(room, failed);
|
|
3343
|
+
return false;
|
|
3344
|
+
}
|
|
3345
|
+
async function adoptedMove(room, partner, local, submitted, accepted) {
|
|
3346
|
+
const other = discovered.get(partner);
|
|
3347
|
+
const failed = await exclusive(async () => {
|
|
3348
|
+
const left = await acceptance(room, local, submitted, accepted);
|
|
3349
|
+
const arriving = replicas.get(partner)?.queue.find((batch) => batch.id === submitted.id);
|
|
3350
|
+
const arrived = other === undefined || arriving === undefined || accepted.destination === undefined
|
|
3351
|
+
? null
|
|
3352
|
+
: await acceptance(other, local, arriving, {
|
|
3353
|
+
kind: 'accepted',
|
|
3354
|
+
...accepted.destination,
|
|
3355
|
+
});
|
|
3356
|
+
await adoptedTogether(local, [left, arrived]);
|
|
3357
|
+
const refusedArrival = refusalOf(arrived);
|
|
3358
|
+
if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
|
|
3359
|
+
refusedSource(other, refusedArrival);
|
|
3360
|
+
}
|
|
3361
|
+
return refusalOf(left);
|
|
3362
|
+
});
|
|
3363
|
+
submitLater(partner);
|
|
2067
3364
|
if (failed === null)
|
|
2068
3365
|
return true;
|
|
2069
3366
|
if (failed.kind === 'refused' && continuing())
|
|
2070
3367
|
refusedSource(room, failed);
|
|
2071
3368
|
return false;
|
|
2072
3369
|
}
|
|
3370
|
+
function queueIn(locator) {
|
|
3371
|
+
return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
3372
|
+
}
|
|
3373
|
+
function behindIn(locator, batch) {
|
|
3374
|
+
const held = replicas.get(locator)?.queue;
|
|
3375
|
+
if (held === undefined)
|
|
3376
|
+
return queuesWithoutReplica.get(locator) !== undefined;
|
|
3377
|
+
return held.findIndex((one) => one.id === batch.id) > 0;
|
|
3378
|
+
}
|
|
3379
|
+
function unpaired(skipped) {
|
|
3380
|
+
for (const locator of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
|
|
3381
|
+
for (const batch of queueIn(locator)) {
|
|
3382
|
+
const partner = partnerOf(batch, locator);
|
|
3383
|
+
if (partner === null || skipped.has(pendingKey(locator, batch.id)))
|
|
3384
|
+
continue;
|
|
3385
|
+
if (queueIn(partner).some((held) => held.id === batch.id))
|
|
3386
|
+
continue;
|
|
3387
|
+
if (unpairedIn(locator, batch))
|
|
3388
|
+
return { locator, batch };
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
return null;
|
|
3392
|
+
}
|
|
3393
|
+
async function unpairing(local, locator, batches) {
|
|
3394
|
+
const ids = new Set(batches.map(({ id }) => id));
|
|
3395
|
+
const leaving = batches.map((batch) => ({
|
|
3396
|
+
batch,
|
|
3397
|
+
returned: !localSaves && leavingIn(batch, locator) ? returnedBy('withdrawn') : null,
|
|
3398
|
+
}));
|
|
3399
|
+
const outcome = localSaves ? 'dropped' : 'unpaired';
|
|
3400
|
+
const current = replicas.get(locator);
|
|
3401
|
+
if (current !== undefined) {
|
|
3402
|
+
const settled = await settlement(local, locator, current, current.record, current.queue.filter((one) => !ids.has(one.id)), { leaving });
|
|
3403
|
+
return {
|
|
3404
|
+
...settled,
|
|
3405
|
+
apply(answer) {
|
|
3406
|
+
for (const batch of batches)
|
|
3407
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
3408
|
+
const set = settled.apply(answer);
|
|
3409
|
+
submitLater(locator);
|
|
3410
|
+
return set;
|
|
3411
|
+
},
|
|
3412
|
+
};
|
|
3413
|
+
}
|
|
3414
|
+
const queue = queueIn(locator).filter((one) => !ids.has(one.id));
|
|
3415
|
+
const written = new Map();
|
|
3416
|
+
for (const { batch, returned } of leaving) {
|
|
3417
|
+
if (returned === null)
|
|
3418
|
+
continue;
|
|
3419
|
+
for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
|
|
3420
|
+
written.set(saved.path, {
|
|
3421
|
+
locator,
|
|
3422
|
+
path: saved.path,
|
|
3423
|
+
revision: roomDraftRevision(),
|
|
3424
|
+
parts: returnedInto((written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path))?.parts, saved.parts),
|
|
3425
|
+
returned,
|
|
3426
|
+
});
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
const carried = carriedBack(locator, leaving.flatMap(({ batch, returned }) => (returned === null ? [] : [batch])), written);
|
|
3430
|
+
return {
|
|
3431
|
+
locator,
|
|
3432
|
+
left: batches,
|
|
3433
|
+
commit: {
|
|
3434
|
+
rooms: [],
|
|
3435
|
+
writes: [
|
|
3436
|
+
{
|
|
3437
|
+
locator,
|
|
3438
|
+
queue,
|
|
3439
|
+
roomDrafts: [...written.values()],
|
|
3440
|
+
consumedRoomDrafts: carried.taken,
|
|
3441
|
+
},
|
|
3442
|
+
...carried.writes,
|
|
3443
|
+
],
|
|
3444
|
+
},
|
|
3445
|
+
apply() {
|
|
3446
|
+
for (const batch of batches) {
|
|
3447
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
3448
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
3449
|
+
}
|
|
3450
|
+
if (queue.length > 0)
|
|
3451
|
+
queuesWithoutReplica.set(locator, queue);
|
|
3452
|
+
else
|
|
3453
|
+
queuesWithoutReplica.delete(locator);
|
|
3454
|
+
for (const record of written.values())
|
|
3455
|
+
rememberRoomDraft(record);
|
|
3456
|
+
carried.remember();
|
|
3457
|
+
submitLater(locator);
|
|
3458
|
+
return mergeChangeSets([
|
|
3459
|
+
changeSet(locator, {
|
|
3460
|
+
queue: true,
|
|
3461
|
+
drafts: [...written.keys(), ...carried.taken],
|
|
3462
|
+
}),
|
|
3463
|
+
...carried.changes,
|
|
3464
|
+
]);
|
|
3465
|
+
},
|
|
3466
|
+
};
|
|
3467
|
+
}
|
|
3468
|
+
async function unpair(local) {
|
|
3469
|
+
const handled = new Set();
|
|
3470
|
+
for (let held = unpaired(handled); held !== null && continuing(); held = unpaired(handled)) {
|
|
3471
|
+
const { locator, batch } = held;
|
|
3472
|
+
handled.add(pendingKey(locator, batch.id));
|
|
3473
|
+
publishChangeSet(await together(local, [await unpairing(local, locator, [batch])]));
|
|
3474
|
+
}
|
|
3475
|
+
if (handled.size === 0)
|
|
3476
|
+
return;
|
|
3477
|
+
moveTo({ ...status, ...attention() });
|
|
3478
|
+
}
|
|
3479
|
+
async function discardHeld(head, room, local, signal) {
|
|
3480
|
+
const partner = partnerOf(head, room.locator);
|
|
3481
|
+
const other = partner === null ? undefined : discovered.get(partner);
|
|
3482
|
+
await discardPending(other === undefined ? [room] : [room, other], local, signal);
|
|
3483
|
+
}
|
|
2073
3484
|
async function submitRoom(room, local, authorized, progress, signal) {
|
|
2074
3485
|
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
2075
3486
|
const replica = replicas.get(room.locator);
|
|
2076
3487
|
const head = replica?.queue[0];
|
|
2077
3488
|
if (replica === undefined || head === undefined)
|
|
2078
3489
|
return 'done';
|
|
2079
|
-
|
|
3490
|
+
const state = pendings.get(pendingKey(room.locator, head.id));
|
|
3491
|
+
if (state?.accepted !== undefined)
|
|
3492
|
+
return 'done';
|
|
3493
|
+
const partner = partnerOf(head, room.locator);
|
|
3494
|
+
if (partner !== null && !leavingIn(head, room.locator)) {
|
|
3495
|
+
submitLater(partner);
|
|
3496
|
+
return 'done';
|
|
3497
|
+
}
|
|
3498
|
+
if (partner !== null && behindIn(partner, head)) {
|
|
3499
|
+
progress.complete = false;
|
|
2080
3500
|
return 'done';
|
|
2081
|
-
|
|
3501
|
+
}
|
|
2082
3502
|
const { submission, lost: earlier } = state ?? {
|
|
2083
3503
|
submission: { state: 'waiting' },
|
|
2084
3504
|
lost: false,
|
|
@@ -2094,16 +3514,16 @@ export function composeRuntime(options) {
|
|
|
2094
3514
|
}
|
|
2095
3515
|
const lost = earlier || submission.state === 'submitted';
|
|
2096
3516
|
if (lost && pastHorizon(head.id)) {
|
|
2097
|
-
await
|
|
3517
|
+
await discardHeld(head, room, local, signal);
|
|
2098
3518
|
return 'done';
|
|
2099
3519
|
}
|
|
2100
3520
|
let result;
|
|
2101
3521
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
2102
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3522
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
|
|
2103
3523
|
if (discoveryPaused || fences.has(room.locator))
|
|
2104
3524
|
return 'done';
|
|
2105
3525
|
if (signal.aborted || writesHeld()) {
|
|
2106
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3526
|
+
await exclusive(() => mark(local, room.locator, head, submission, earlier));
|
|
2107
3527
|
if (signal.aborted)
|
|
2108
3528
|
return 'done';
|
|
2109
3529
|
progress.complete = false;
|
|
@@ -2111,11 +3531,17 @@ export function composeRuntime(options) {
|
|
|
2111
3531
|
return 'done';
|
|
2112
3532
|
}
|
|
2113
3533
|
const opensBefore = openSignals;
|
|
2114
|
-
const
|
|
3534
|
+
const answeredHere = awaitingAnswer(room.locator);
|
|
3535
|
+
const answeredThere = partner === null ? () => { } : awaitingAnswer(partner);
|
|
3536
|
+
const answered = () => {
|
|
3537
|
+
answeredHere();
|
|
3538
|
+
answeredThere();
|
|
3539
|
+
};
|
|
2115
3540
|
try {
|
|
2116
3541
|
result = await options.adapter.mutate(authorized, {
|
|
2117
3542
|
locator: room.locator,
|
|
2118
3543
|
changeBatch: head,
|
|
3544
|
+
file: batchFile(head),
|
|
2119
3545
|
...(state?.upload === undefined ? {} : { upload: state.upload }),
|
|
2120
3546
|
}, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
|
|
2121
3547
|
}
|
|
@@ -2140,9 +3566,10 @@ export function composeRuntime(options) {
|
|
|
2140
3566
|
return 'again';
|
|
2141
3567
|
}
|
|
2142
3568
|
if (result.kind === 'room-not-readable') {
|
|
3569
|
+
const unread = discovered.get(result.locator) ?? room;
|
|
2143
3570
|
progress.complete = false;
|
|
2144
|
-
unreadable.add(
|
|
2145
|
-
list(
|
|
3571
|
+
unreadable.add(unread.locator);
|
|
3572
|
+
list(unread, false);
|
|
2146
3573
|
emitRooms();
|
|
2147
3574
|
return 'done';
|
|
2148
3575
|
}
|
|
@@ -2157,24 +3584,30 @@ export function composeRuntime(options) {
|
|
|
2157
3584
|
}
|
|
2158
3585
|
if (result.kind === 'write-halted') {
|
|
2159
3586
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
2160
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3587
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
|
|
2161
3588
|
if (serviceMode === 'open' && openSignals !== opensBefore)
|
|
2162
3589
|
continue;
|
|
2163
3590
|
haltedRefusal = true;
|
|
2164
3591
|
progress.complete = false;
|
|
2165
3592
|
return 'done';
|
|
2166
3593
|
}
|
|
3594
|
+
const other = partner === null ? undefined : discovered.get(partner);
|
|
2167
3595
|
if (result.kind === 'conflicted' ||
|
|
2168
3596
|
result.kind === 'rejected' ||
|
|
2169
3597
|
result.kind === 'expired' ||
|
|
2170
3598
|
result.kind === 'retryable-refusal') {
|
|
2171
3599
|
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
2172
3600
|
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
2173
|
-
await
|
|
3601
|
+
await discardHeld(head, room, local, signal);
|
|
2174
3602
|
return 'done';
|
|
2175
3603
|
}
|
|
2176
3604
|
if (result.kind === 'expired') {
|
|
2177
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3605
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
|
|
3606
|
+
const there = other === undefined ? null : await advance(other, local, authorized, continuing);
|
|
3607
|
+
if (other !== undefined && there !== null) {
|
|
3608
|
+
await withhold(there, other, local, progress);
|
|
3609
|
+
return 'done';
|
|
3610
|
+
}
|
|
2178
3611
|
const withheld = await advance(room, local, authorized, continuing);
|
|
2179
3612
|
if (withheld !== null) {
|
|
2180
3613
|
await withhold(withheld, room, local, progress);
|
|
@@ -2191,11 +3624,25 @@ export function composeRuntime(options) {
|
|
|
2191
3624
|
if (result.kind === 'retryable-refusal') {
|
|
2192
3625
|
const reset = result.reset;
|
|
2193
3626
|
progress.complete = false;
|
|
2194
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3627
|
+
await exclusive(() => mark(local, room.locator, head, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
2195
3628
|
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
2196
3629
|
}
|
|
2197
3630
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
2198
3631
|
const left = leaving(head, result);
|
|
3632
|
+
const refreshed = async () => ({
|
|
3633
|
+
there: other === undefined ? null : await advance(other, local, authorized, continuing),
|
|
3634
|
+
here: await advance(room, local, authorized, continuing),
|
|
3635
|
+
});
|
|
3636
|
+
const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
|
|
3637
|
+
if (judged !== null && (judged.there !== null || judged.here !== null)) {
|
|
3638
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
|
|
3639
|
+
if (other !== undefined && judged.there !== null) {
|
|
3640
|
+
await withhold(judged.there, other, local, progress);
|
|
3641
|
+
}
|
|
3642
|
+
if (judged.here !== null)
|
|
3643
|
+
await withhold(judged.here, room, local, progress);
|
|
3644
|
+
return 'done';
|
|
3645
|
+
}
|
|
2199
3646
|
await exclusive(async () => {
|
|
2200
3647
|
if (!continuing())
|
|
2201
3648
|
return;
|
|
@@ -2203,25 +3650,29 @@ export function composeRuntime(options) {
|
|
|
2203
3650
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
2204
3651
|
return;
|
|
2205
3652
|
}
|
|
2206
|
-
const
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
});
|
|
2212
|
-
|
|
3653
|
+
const confirmedLayer = await sliceAt(current.record, 'confirmed', [head]);
|
|
3654
|
+
const held = (result.kind === 'expired' &&
|
|
3655
|
+
(await landedWhole(room.locator, head, confirmedLayer))) ||
|
|
3656
|
+
(result.kind === 'conflicted' &&
|
|
3657
|
+
(await landedWhole(room.locator, head, confirmedLayer, result.affected)));
|
|
3658
|
+
publishChangeSet(await settle(local, room.locator, current, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
|
|
3659
|
+
await unpair(local);
|
|
2213
3660
|
moveTo({ ...status, ...attention() });
|
|
2214
3661
|
});
|
|
2215
3662
|
if (result.kind === 'expired')
|
|
2216
3663
|
continue;
|
|
2217
|
-
const
|
|
2218
|
-
if (
|
|
2219
|
-
await withhold(
|
|
3664
|
+
const { there, here } = judged ?? (await refreshed());
|
|
3665
|
+
if (other !== undefined && there !== null)
|
|
3666
|
+
await withhold(there, other, local, progress);
|
|
3667
|
+
if (here !== null) {
|
|
3668
|
+
await withhold(here, room, local, progress);
|
|
2220
3669
|
return 'done';
|
|
2221
3670
|
}
|
|
2222
3671
|
continue;
|
|
2223
3672
|
}
|
|
2224
|
-
const kept = await
|
|
3673
|
+
const kept = await (partner === null
|
|
3674
|
+
? adopted(room, local, head, result)
|
|
3675
|
+
: adoptedMove(room, partner, local, head, result)).finally(answered);
|
|
2225
3676
|
if (!kept) {
|
|
2226
3677
|
progress.complete = false;
|
|
2227
3678
|
return 'done';
|
|
@@ -2232,44 +3683,67 @@ export function composeRuntime(options) {
|
|
|
2232
3683
|
function pastHorizon(identity) {
|
|
2233
3684
|
return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
|
|
2234
3685
|
}
|
|
2235
|
-
async function discardPending(
|
|
2236
|
-
const { locator } = room;
|
|
3686
|
+
async function discardPending(rooms, local, signal) {
|
|
2237
3687
|
const discarded = await exclusive(async () => {
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
3688
|
+
const held = rooms
|
|
3689
|
+
.filter(({ locator }) => !fences.has(locator))
|
|
3690
|
+
.map((room) => ({
|
|
3691
|
+
room,
|
|
3692
|
+
before: replicas.get(room.locator),
|
|
3693
|
+
queue: queueIn(room.locator),
|
|
3694
|
+
}));
|
|
3695
|
+
if (held.length === 0)
|
|
3696
|
+
return [];
|
|
3697
|
+
const answer = await local.commit({
|
|
3698
|
+
rooms: held.flatMap(({ room, before }) => before === undefined
|
|
3699
|
+
? []
|
|
3700
|
+
: [
|
|
3701
|
+
{
|
|
3702
|
+
locator: room.locator,
|
|
3703
|
+
key: before.record.key,
|
|
3704
|
+
roots: before.record.typedSelection?.roots ?? [],
|
|
3705
|
+
overlay: { set: [], cleared: [...before.overlaid] },
|
|
3706
|
+
},
|
|
3707
|
+
]),
|
|
3708
|
+
discards: held.map(({ room }) => room.locator),
|
|
3709
|
+
});
|
|
3710
|
+
for (const { room, before, queue } of held) {
|
|
3711
|
+
const { locator } = room;
|
|
3712
|
+
lastFenced.set(locator, ++fencings);
|
|
3713
|
+
for (const batch of queue)
|
|
3714
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
3715
|
+
queuesWithoutReplica.delete(locator);
|
|
3716
|
+
if (before !== undefined) {
|
|
3717
|
+
replicas.set(locator, {
|
|
3718
|
+
...before,
|
|
3719
|
+
queue: [],
|
|
3720
|
+
overlaid: new Set(),
|
|
3721
|
+
count: answer.counts[locator] ?? before.count,
|
|
3722
|
+
});
|
|
3723
|
+
}
|
|
3724
|
+
journal.record(`room discarded ${locator}`);
|
|
2249
3725
|
}
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
if (after !== null)
|
|
2255
|
-
replicas.set(locator, after);
|
|
2256
|
-
journal.record(`room discarded ${locator}`);
|
|
2257
|
-
publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
|
|
3726
|
+
publishChangeSet(mergeChangeSets(held.map(({ room }) => changeSet(room.locator, {
|
|
3727
|
+
documents: answer.changed[room.locator] ?? [],
|
|
3728
|
+
queue: true,
|
|
3729
|
+
}))));
|
|
2258
3730
|
moveTo({ ...status, ...attention() });
|
|
2259
|
-
return
|
|
2260
|
-
});
|
|
2261
|
-
if (!discarded)
|
|
2262
|
-
return;
|
|
2263
|
-
const progress = { complete: true, reachable: true };
|
|
2264
|
-
await roomTask(locator, async (live) => {
|
|
2265
|
-
await Promise.race([
|
|
2266
|
-
reload(room, local, progress, () => live() && !signal.aborted),
|
|
2267
|
-
abandonedOn(signal),
|
|
2268
|
-
]);
|
|
3731
|
+
return held.map(({ room }) => room);
|
|
2269
3732
|
});
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
3733
|
+
for (const room of discarded) {
|
|
3734
|
+
if (signal.aborted)
|
|
3735
|
+
return;
|
|
3736
|
+
const progress = { complete: true, reachable: true };
|
|
3737
|
+
await roomTask(room.locator, async (live) => {
|
|
3738
|
+
await Promise.race([
|
|
3739
|
+
reload(room, local, progress, () => live() && !signal.aborted),
|
|
3740
|
+
abandonedOn(signal),
|
|
3741
|
+
]);
|
|
3742
|
+
});
|
|
3743
|
+
if (signal.aborted)
|
|
3744
|
+
return;
|
|
3745
|
+
await rest(progress);
|
|
3746
|
+
}
|
|
2273
3747
|
}
|
|
2274
3748
|
function awaitingAnswer(locator) {
|
|
2275
3749
|
let release = () => { };
|
|
@@ -2286,7 +3760,7 @@ export function composeRuntime(options) {
|
|
|
2286
3760
|
async function retainModel(local, room, attached, next) {
|
|
2287
3761
|
const key = modelKey(room.locator, attached);
|
|
2288
3762
|
const previous = modelOf(room.locator, attached);
|
|
2289
|
-
const heldBefore = replicas.get(room.locator)?.
|
|
3763
|
+
const heldBefore = replicas.get(room.locator)?.record;
|
|
2290
3764
|
if (JSON.stringify(previous) === JSON.stringify(next) &&
|
|
2291
3765
|
(heldBefore === undefined ||
|
|
2292
3766
|
(JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
|
|
@@ -2302,36 +3776,56 @@ export function composeRuntime(options) {
|
|
|
2302
3776
|
if (!continuing())
|
|
2303
3777
|
return;
|
|
2304
3778
|
const held = replicas.get(room.locator);
|
|
3779
|
+
let changed = null;
|
|
2305
3780
|
if (held !== undefined) {
|
|
2306
3781
|
const declared = {
|
|
2307
|
-
...held.
|
|
3782
|
+
...held.record,
|
|
2308
3783
|
operations: room.operations,
|
|
2309
3784
|
replica: room.replica,
|
|
2310
3785
|
typedSelection: room.typedSelection ?? null,
|
|
2311
3786
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
2312
3787
|
};
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
3788
|
+
let read = 'staged';
|
|
3789
|
+
if (!sameMembership(declared, held.record)) {
|
|
3790
|
+
const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), compositionOf(declared));
|
|
3791
|
+
read =
|
|
3792
|
+
verdict.verdict !== 'refuse'
|
|
3793
|
+
? 'staged'
|
|
3794
|
+
: verdict.rule === 'model-unavailable'
|
|
3795
|
+
? 'deferred'
|
|
3796
|
+
: { kind: 'refused', refusal: verdict };
|
|
3797
|
+
}
|
|
3798
|
+
const record = read === 'staged'
|
|
2317
3799
|
? declared
|
|
2318
|
-
: {
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
const
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
3800
|
+
: { ...declared, typedSelection: held.record.typedSelection };
|
|
3801
|
+
const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
|
|
3802
|
+
const replayedLayer = await replayed(record, confirmedLayer, held.queue);
|
|
3803
|
+
const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
|
|
3804
|
+
const answer = await local.commit({
|
|
3805
|
+
rooms: [
|
|
3806
|
+
{
|
|
3807
|
+
locator: room.locator,
|
|
3808
|
+
key: record.key,
|
|
3809
|
+
record: storedRecord(record),
|
|
3810
|
+
roots: record.typedSelection?.roots ?? [],
|
|
3811
|
+
composition: compositionOf(record),
|
|
3812
|
+
overlay: diff.overlay,
|
|
3813
|
+
},
|
|
3814
|
+
],
|
|
3815
|
+
});
|
|
3816
|
+
const { count, indexed } = committedRoom(room.locator, answer);
|
|
3817
|
+
replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
|
|
3818
|
+
changed = changeSet(room.locator, {
|
|
3819
|
+
documents: answer.changed[room.locator] ?? [],
|
|
3820
|
+
room: true,
|
|
3821
|
+
});
|
|
3822
|
+
if (read === 'staged' || read === 'deferred') {
|
|
2328
3823
|
if (declarationRefusals.delete(room.locator)) {
|
|
2329
3824
|
sourceRefusals.delete(room.locator);
|
|
2330
3825
|
unreadable.delete(room.locator);
|
|
2331
3826
|
}
|
|
2332
3827
|
}
|
|
2333
|
-
else
|
|
2334
|
-
replicas.set(room.locator, { ...held, confirmed: { ...held.confirmed, record } });
|
|
3828
|
+
else {
|
|
2335
3829
|
refusedSource(room, read);
|
|
2336
3830
|
declarationRefusals.add(room.locator);
|
|
2337
3831
|
}
|
|
@@ -2342,14 +3836,14 @@ export function composeRuntime(options) {
|
|
|
2342
3836
|
invalidModels.delete(key);
|
|
2343
3837
|
}
|
|
2344
3838
|
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
2345
|
-
if (
|
|
3839
|
+
if (changed === null)
|
|
2346
3840
|
emitRooms();
|
|
2347
3841
|
else
|
|
2348
|
-
publishChangeSet(
|
|
3842
|
+
publishChangeSet(changed);
|
|
2349
3843
|
});
|
|
2350
3844
|
}
|
|
2351
3845
|
async function scoped(result, room, local) {
|
|
2352
|
-
if (
|
|
3846
|
+
if (result.scope === 'content' && !result.retryable) {
|
|
2353
3847
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
2354
3848
|
await loseRooms([room.locator]);
|
|
2355
3849
|
return;
|
|
@@ -2367,15 +3861,7 @@ export function composeRuntime(options) {
|
|
|
2367
3861
|
});
|
|
2368
3862
|
else if (result.scope !== 'operation') {
|
|
2369
3863
|
unreadable.add(room.locator);
|
|
2370
|
-
scopedAway = {
|
|
2371
|
-
...room,
|
|
2372
|
-
replica: { ...room.replica, admission: 'unavailable' },
|
|
2373
|
-
operations: {
|
|
2374
|
-
...room.operations,
|
|
2375
|
-
...(result.scope === 'content' ? { read: [] } : {}),
|
|
2376
|
-
...(result.scope === 'sync' ? { sync: [] } : {}),
|
|
2377
|
-
},
|
|
2378
|
-
};
|
|
3864
|
+
scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
|
|
2379
3865
|
await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
|
|
2380
3866
|
}
|
|
2381
3867
|
if (!continuing())
|
|
@@ -2386,30 +3872,22 @@ export function composeRuntime(options) {
|
|
|
2386
3872
|
}
|
|
2387
3873
|
function heldRooms(listed) {
|
|
2388
3874
|
return listed.flatMap((room) => {
|
|
2389
|
-
const record = replicas.get(room.locator)?.
|
|
3875
|
+
const record = replicas.get(room.locator)?.record;
|
|
2390
3876
|
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
2391
3877
|
return [];
|
|
2392
|
-
return [
|
|
2393
|
-
{
|
|
2394
|
-
locator: room.locator,
|
|
2395
|
-
roomVersion: record.roomVersion,
|
|
2396
|
-
documents: record.documents,
|
|
2397
|
-
revisions: record.revisions,
|
|
2398
|
-
},
|
|
2399
|
-
];
|
|
3878
|
+
return [{ locator: room.locator, roomVersion: record.roomVersion }];
|
|
2400
3879
|
});
|
|
2401
3880
|
}
|
|
2402
3881
|
async function synchronize(listed, local, progress, attempt, live) {
|
|
2403
|
-
const
|
|
2404
|
-
const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
|
|
3882
|
+
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
|
|
2405
3883
|
if (!live())
|
|
2406
3884
|
return;
|
|
2407
|
-
const ready =
|
|
3885
|
+
const ready = listed.filter((_, index) => {
|
|
2408
3886
|
const outcome = prepared[index];
|
|
2409
3887
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
2410
3888
|
});
|
|
2411
3889
|
if (attempt === 'opened') {
|
|
2412
|
-
options.adapter.roomStream?.hold(heldRooms(ready)
|
|
3890
|
+
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
2413
3891
|
handed = true;
|
|
2414
3892
|
}
|
|
2415
3893
|
discovering?.release();
|
|
@@ -2425,7 +3903,7 @@ export function composeRuntime(options) {
|
|
|
2425
3903
|
progress.complete = false;
|
|
2426
3904
|
return false;
|
|
2427
3905
|
}
|
|
2428
|
-
const prior = replicas.get(room.locator)?.
|
|
3906
|
+
const prior = replicas.get(room.locator)?.record;
|
|
2429
3907
|
if (prior !== undefined && prior.replica.selection !== room.replica.selection) {
|
|
2430
3908
|
list(prior, false);
|
|
2431
3909
|
emitRooms();
|
|
@@ -2468,23 +3946,24 @@ export function composeRuntime(options) {
|
|
|
2468
3946
|
}
|
|
2469
3947
|
async function reload(room, local, progress, live) {
|
|
2470
3948
|
try {
|
|
3949
|
+
if (live())
|
|
3950
|
+
reloadsDue.delete(room.locator);
|
|
2471
3951
|
if (!live() || authorization === undefined)
|
|
2472
3952
|
return;
|
|
2473
3953
|
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
2474
|
-
if (!live())
|
|
2475
|
-
return;
|
|
2476
3954
|
if (bootstrapped.kind !== 'staged') {
|
|
2477
|
-
|
|
3955
|
+
if (live())
|
|
3956
|
+
await withhold(bootstrapped, room, local, progress);
|
|
2478
3957
|
return;
|
|
2479
3958
|
}
|
|
2480
|
-
await publish(local, room, bootstrapped.
|
|
3959
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
2481
3960
|
await caughtUp(room, local, progress, live);
|
|
2482
3961
|
}
|
|
2483
3962
|
finally {
|
|
2484
3963
|
caughtUpTo(room.locator);
|
|
2485
3964
|
}
|
|
2486
3965
|
}
|
|
2487
|
-
async function discoverRooms(local, progress, attempt, live) {
|
|
3966
|
+
async function discoverRooms(local, progress, attempt, live, scope) {
|
|
2488
3967
|
if (closed || authorization === undefined) {
|
|
2489
3968
|
notifyReady();
|
|
2490
3969
|
return;
|
|
@@ -2494,7 +3973,7 @@ export function composeRuntime(options) {
|
|
|
2494
3973
|
const started = discoveries;
|
|
2495
3974
|
let result;
|
|
2496
3975
|
try {
|
|
2497
|
-
result = await options.adapter.discover(authorization);
|
|
3976
|
+
result = await options.adapter.discover(authorization, scope);
|
|
2498
3977
|
}
|
|
2499
3978
|
catch {
|
|
2500
3979
|
result = { kind: 'transport-uncertainty' };
|
|
@@ -2534,12 +4013,13 @@ export function composeRuntime(options) {
|
|
|
2534
4013
|
}
|
|
2535
4014
|
for (const room of result.rooms) {
|
|
2536
4015
|
if (room.operations === undefined ||
|
|
2537
|
-
!['
|
|
4016
|
+
!['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
2538
4017
|
room.operations[operation].every((scope) => typeof scope === 'string')) ||
|
|
2539
4018
|
room.replica === undefined ||
|
|
2540
4019
|
typeof room.replica.selection !== 'string' ||
|
|
2541
4020
|
room.replica.selection.length === 0 ||
|
|
2542
|
-
!['admitted', 'unavailable'].includes(room.replica.admission)
|
|
4021
|
+
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
4022
|
+
!['locked', 'open'].includes(room.replica.offline)) {
|
|
2543
4023
|
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
2544
4024
|
await refuse('missing-required-guarantee');
|
|
2545
4025
|
notifyReady();
|
|
@@ -2563,15 +4043,16 @@ export function composeRuntime(options) {
|
|
|
2563
4043
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
2564
4044
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
2565
4045
|
limits = result.providerLimits;
|
|
2566
|
-
await local.
|
|
4046
|
+
await local.fact('providerLimits', limits);
|
|
4047
|
+
await local.fact('capabilities', capabilities);
|
|
2567
4048
|
}
|
|
2568
4049
|
const signedIn = result.member ?? null;
|
|
2569
4050
|
if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
|
|
2570
|
-
await local.
|
|
4051
|
+
await local.fact('member', signedIn);
|
|
2571
4052
|
member = signedIn;
|
|
2572
4053
|
emitMember();
|
|
2573
4054
|
}
|
|
2574
|
-
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
4055
|
+
await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
|
|
2575
4056
|
}
|
|
2576
4057
|
async function ensureAuthorization() {
|
|
2577
4058
|
if (authorization !== undefined)
|
|
@@ -2668,9 +4149,40 @@ export function composeRuntime(options) {
|
|
|
2668
4149
|
options.adapter.roomStream?.reopen();
|
|
2669
4150
|
});
|
|
2670
4151
|
}
|
|
4152
|
+
function rediscover(scope) {
|
|
4153
|
+
return task(OPENING_LANE, async (live) => {
|
|
4154
|
+
if (!live() || !socketOpen)
|
|
4155
|
+
return;
|
|
4156
|
+
const progress = { complete: true, reachable: true };
|
|
4157
|
+
const gate = discover();
|
|
4158
|
+
moveTo(transferring(status));
|
|
4159
|
+
try {
|
|
4160
|
+
const local = await openLocal();
|
|
4161
|
+
if (!live())
|
|
4162
|
+
return;
|
|
4163
|
+
await ensureAuthorization();
|
|
4164
|
+
if (!live())
|
|
4165
|
+
return;
|
|
4166
|
+
await discoverRooms(local, progress, 'authorized', live, scope);
|
|
4167
|
+
}
|
|
4168
|
+
catch {
|
|
4169
|
+
progress.complete = false;
|
|
4170
|
+
}
|
|
4171
|
+
finally {
|
|
4172
|
+
gate.release();
|
|
4173
|
+
notifyReady();
|
|
4174
|
+
}
|
|
4175
|
+
if (!live())
|
|
4176
|
+
return;
|
|
4177
|
+
if (!progress.complete)
|
|
4178
|
+
complete = false;
|
|
4179
|
+
await rest(progress, live);
|
|
4180
|
+
});
|
|
4181
|
+
}
|
|
2671
4182
|
function closedSocket(revoked, lost) {
|
|
2672
4183
|
const opened = socketOpen;
|
|
2673
4184
|
socketOpen = false;
|
|
4185
|
+
connectionLost();
|
|
2674
4186
|
if (lost) {
|
|
2675
4187
|
reads.lost();
|
|
2676
4188
|
conversations.lost();
|
|
@@ -2691,9 +4203,10 @@ export function composeRuntime(options) {
|
|
|
2691
4203
|
function applyOpening(expired) {
|
|
2692
4204
|
const progress = underway?.generation === generation ? underway.progress : null;
|
|
2693
4205
|
const cycle = progress ?? { complete: true, reachable: true };
|
|
2694
|
-
const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
|
|
2695
4206
|
if (opened === generation)
|
|
2696
4207
|
opened = null;
|
|
4208
|
+
const reloading = progress === null ? [] : owedReloads(expired);
|
|
4209
|
+
const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
|
|
2697
4210
|
return task(OPENING_LANE, async (live) => {
|
|
2698
4211
|
underway = null;
|
|
2699
4212
|
if (!live())
|
|
@@ -2703,24 +4216,17 @@ export function composeRuntime(options) {
|
|
|
2703
4216
|
await rest(cycle, live);
|
|
2704
4217
|
return;
|
|
2705
4218
|
}
|
|
2706
|
-
await roomsCarried(expired);
|
|
4219
|
+
await roomsCarried([...expired, ...reloading]);
|
|
2707
4220
|
if (!live())
|
|
2708
4221
|
return;
|
|
2709
|
-
const replaced = new Set(owed);
|
|
2710
|
-
owed.clear();
|
|
2711
4222
|
try {
|
|
2712
4223
|
const local = await openLocal();
|
|
2713
|
-
const
|
|
2714
|
-
const reloading = held.filter((room) => !expired.includes(room.locator) &&
|
|
2715
|
-
(replaced.has(room.locator) || revoked(room.locator)));
|
|
2716
|
-
for (const room of reloading)
|
|
2717
|
-
catching.add(room.locator);
|
|
2718
|
-
readSide.recover();
|
|
2719
|
-
const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
|
|
2720
|
-
const reloaded = [...(await reloads), ...(await replacing)];
|
|
4224
|
+
const reloaded = await replacing;
|
|
2721
4225
|
if (progress.reachable) {
|
|
2722
|
-
for (const room of
|
|
2723
|
-
if (
|
|
4226
|
+
for (const room of discovered.values()) {
|
|
4227
|
+
if (replicas.has(room.locator) &&
|
|
4228
|
+
!expired.includes(room.locator) &&
|
|
4229
|
+
!reloading.includes(room.locator)) {
|
|
2724
4230
|
await confirmModels(room, local);
|
|
2725
4231
|
}
|
|
2726
4232
|
}
|
|
@@ -2737,14 +4243,29 @@ export function composeRuntime(options) {
|
|
|
2737
4243
|
await finishOpening(progress, live);
|
|
2738
4244
|
});
|
|
2739
4245
|
}
|
|
4246
|
+
function owedReloads(expired) {
|
|
4247
|
+
const reloading = [...discovered.values()]
|
|
4248
|
+
.filter((room) => replicas.has(room.locator) &&
|
|
4249
|
+
!expired.includes(room.locator) &&
|
|
4250
|
+
(owed.has(room.locator) || revoked(room.locator)))
|
|
4251
|
+
.map((room) => room.locator);
|
|
4252
|
+
owed.clear();
|
|
4253
|
+
for (const locator of reloading)
|
|
4254
|
+
catching.add(locator);
|
|
4255
|
+
liveQueries.recover();
|
|
4256
|
+
return reloading;
|
|
4257
|
+
}
|
|
2740
4258
|
async function reloadNow(replaced, progress) {
|
|
4259
|
+
const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
|
|
4260
|
+
for (const room of reloaded)
|
|
4261
|
+
reloadsDue.set(room.locator, generation);
|
|
2741
4262
|
try {
|
|
2742
4263
|
const local = await openLocal();
|
|
2743
|
-
return await Promise.allSettled(
|
|
2744
|
-
.filter((room) => replicas.has(room.locator) && replaced(room))
|
|
2745
|
-
.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
4264
|
+
return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
2746
4265
|
}
|
|
2747
4266
|
catch {
|
|
4267
|
+
for (const room of reloaded)
|
|
4268
|
+
reloadsDue.delete(room.locator);
|
|
2748
4269
|
progress.reachable = false;
|
|
2749
4270
|
return [];
|
|
2750
4271
|
}
|
|
@@ -2870,10 +4391,11 @@ export function composeRuntime(options) {
|
|
|
2870
4391
|
if (status.providerUnreachable)
|
|
2871
4392
|
conversations.reconnected();
|
|
2872
4393
|
const reading = { complete: true, reachable: true };
|
|
2873
|
-
|
|
2874
|
-
await conversations.catchUp(reading);
|
|
4394
|
+
await conversations.catchUp(reading);
|
|
2875
4395
|
if (progress.reachable && reading.reachable)
|
|
2876
4396
|
await conversations.resend(reading);
|
|
4397
|
+
if (progress.reachable)
|
|
4398
|
+
void recovery.attempt('calls', resendCalls);
|
|
2877
4399
|
if (!reading.reachable) {
|
|
2878
4400
|
progress.reachable = false;
|
|
2879
4401
|
resendLater();
|
|
@@ -2897,7 +4419,7 @@ export function composeRuntime(options) {
|
|
|
2897
4419
|
if (synced && !recorded) {
|
|
2898
4420
|
const completedAt = new Date().toISOString();
|
|
2899
4421
|
try {
|
|
2900
|
-
await (await openLocal()).
|
|
4422
|
+
await (await openLocal()).fact('lastCompleted', completedAt);
|
|
2901
4423
|
if (!live())
|
|
2902
4424
|
return;
|
|
2903
4425
|
recorded = true;
|
|
@@ -2934,11 +4456,11 @@ export function composeRuntime(options) {
|
|
|
2934
4456
|
return;
|
|
2935
4457
|
const room = discovered.get(locator);
|
|
2936
4458
|
const held = replicas.get(locator);
|
|
2937
|
-
if (room === undefined || room.replica.admission !== 'admitted'
|
|
4459
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2938
4460
|
return;
|
|
2939
4461
|
if (held !== undefined &&
|
|
2940
|
-
(held.
|
|
2941
|
-
options.adapter.reached(locator, held.
|
|
4462
|
+
(held.record.replica.selection !== room.replica.selection ||
|
|
4463
|
+
options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
|
|
2942
4464
|
return;
|
|
2943
4465
|
}
|
|
2944
4466
|
const during = underway?.generation === generation ? underway.progress : null;
|
|
@@ -2985,6 +4507,7 @@ export function composeRuntime(options) {
|
|
|
2985
4507
|
return;
|
|
2986
4508
|
if (event.kind === 'opened') {
|
|
2987
4509
|
socketOpen = true;
|
|
4510
|
+
connectionLive();
|
|
2988
4511
|
generation += 1;
|
|
2989
4512
|
reads.opened();
|
|
2990
4513
|
conversations.reopened();
|
|
@@ -2995,9 +4518,18 @@ export function composeRuntime(options) {
|
|
|
2995
4518
|
closedSocket(event.revoked, event.lost);
|
|
2996
4519
|
else if (event.kind === 'opening')
|
|
2997
4520
|
void applyOpening(event.expired);
|
|
4521
|
+
else if (event.kind === 'authority-changed')
|
|
4522
|
+
void rediscover(event.scope);
|
|
2998
4523
|
else
|
|
2999
4524
|
void applyRoom(event.locator, event.roomVersion);
|
|
3000
|
-
}, receipts);
|
|
4525
|
+
}, receipts, heldRevisions);
|
|
4526
|
+
}
|
|
4527
|
+
async function heldRevisions(locator, paths) {
|
|
4528
|
+
const held = replicas.get(locator);
|
|
4529
|
+
if (held === undefined || paths.length === 0)
|
|
4530
|
+
return new Map();
|
|
4531
|
+
const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
|
|
4532
|
+
return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
|
|
3001
4533
|
}
|
|
3002
4534
|
function writesHeld() {
|
|
3003
4535
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -3049,7 +4581,7 @@ export function composeRuntime(options) {
|
|
|
3049
4581
|
if (!live())
|
|
3050
4582
|
return;
|
|
3051
4583
|
await conversations.reconcile();
|
|
3052
|
-
if (conversations.current()
|
|
4584
|
+
if (conversations.current()) {
|
|
3053
4585
|
show();
|
|
3054
4586
|
return;
|
|
3055
4587
|
}
|
|
@@ -3067,10 +4599,12 @@ export function composeRuntime(options) {
|
|
|
3067
4599
|
if (closed)
|
|
3068
4600
|
return;
|
|
3069
4601
|
closed = true;
|
|
3070
|
-
if (narrowing !== null)
|
|
3071
|
-
clearTimeout(narrowing);
|
|
3072
4602
|
if (rechecking !== null)
|
|
3073
4603
|
clearInterval(rechecking);
|
|
4604
|
+
for (const resolve of shownWaiters.splice(0))
|
|
4605
|
+
resolve();
|
|
4606
|
+
connectionLive();
|
|
4607
|
+
lockListeners.clear();
|
|
3074
4608
|
readinessWatches.clear();
|
|
3075
4609
|
withdrawStream?.();
|
|
3076
4610
|
withdrawStream = undefined;
|
|
@@ -3079,17 +4613,13 @@ export function composeRuntime(options) {
|
|
|
3079
4613
|
withdrawServiceMode?.();
|
|
3080
4614
|
conversations.close();
|
|
3081
4615
|
void recovery.retire();
|
|
3082
|
-
store?.close();
|
|
3083
|
-
void opening?.then((opened) => {
|
|
3084
|
-
opened.close();
|
|
3085
|
-
}, () => { });
|
|
3086
4616
|
roomListeners.clear();
|
|
3087
4617
|
refusalListeners.clear();
|
|
3088
4618
|
storageLossListeners.clear();
|
|
3089
4619
|
statusListeners.clear();
|
|
3090
4620
|
serviceModeListeners.clear();
|
|
3091
4621
|
memberListeners.clear();
|
|
3092
|
-
|
|
4622
|
+
liveQueries.close();
|
|
3093
4623
|
queueSubscriptions.clear();
|
|
3094
4624
|
roomDraftListeners.clear();
|
|
3095
4625
|
presentationListeners.clear();
|
|
@@ -3116,7 +4646,11 @@ export function composeRuntime(options) {
|
|
|
3116
4646
|
if (options.adapter.roomStream === undefined)
|
|
3117
4647
|
void sync('unstreamed');
|
|
3118
4648
|
function planned(input, reach) {
|
|
3119
|
-
|
|
4649
|
+
const plan = planOf(input, roomFacts(reach), planReach(reach));
|
|
4650
|
+
const named = plan.paths !== null &&
|
|
4651
|
+
plan.rooms !== null &&
|
|
4652
|
+
plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
|
|
4653
|
+
return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
|
|
3120
4654
|
}
|
|
3121
4655
|
function watchPlan(plan, deliver) {
|
|
3122
4656
|
let seeded = false;
|
|
@@ -3131,7 +4665,7 @@ export function composeRuntime(options) {
|
|
|
3131
4665
|
const watchers = seedWatchers.get(plan.key) ?? new Set();
|
|
3132
4666
|
seedWatchers.set(plan.key, watchers);
|
|
3133
4667
|
watchers.add(shown);
|
|
3134
|
-
const stop =
|
|
4668
|
+
const stop = liveQueries.watch(plan, identityOf(plan), () => {
|
|
3135
4669
|
const cover = covered(plan);
|
|
3136
4670
|
awaitPreparation(cover);
|
|
3137
4671
|
return seedBehind(plan, cover);
|
|
@@ -3152,67 +4686,58 @@ export function composeRuntime(options) {
|
|
|
3152
4686
|
stop();
|
|
3153
4687
|
};
|
|
3154
4688
|
}
|
|
3155
|
-
async function searched(input, reach) {
|
|
3156
|
-
const
|
|
3157
|
-
const filter =
|
|
4689
|
+
async function searched(input, reach, locked) {
|
|
4690
|
+
const asked = searchPlanOf(input);
|
|
4691
|
+
const filter = {
|
|
4692
|
+
...filterOf(asked.documents ?? {}, roomFacts(reach), planReach(reach)),
|
|
4693
|
+
locked,
|
|
4694
|
+
};
|
|
4695
|
+
const unlocking = (filter.rooms ?? [])
|
|
4696
|
+
.filter((room) => locked.includes(room))
|
|
4697
|
+
.map((room) => ({ room, awaits: 'unlock' }));
|
|
3158
4698
|
for (;;) {
|
|
3159
4699
|
const cover = covered(filter);
|
|
3160
4700
|
awaitPreparation(cover);
|
|
3161
|
-
const pending = 'pending' in cover ?
|
|
3162
|
-
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
3163
|
-
const held = covered({
|
|
3164
|
-
...filter,
|
|
3165
|
-
rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
|
|
3166
|
-
});
|
|
3167
|
-
const covering = 'pending' in held ? [] : held.coverage.rooms;
|
|
3168
|
-
const unindexed = covering
|
|
3169
|
-
.filter(({ room }) => !searchIndex.rooms().has(room))
|
|
3170
|
-
.map(({ room }) => room);
|
|
3171
|
-
for (const room of unindexed)
|
|
3172
|
-
pending.push({ room, awaits: 'index' });
|
|
3173
|
-
const listed = conversations.searchable();
|
|
3174
|
-
const named = reach !== null && !reach.conversations
|
|
3175
|
-
? []
|
|
3176
|
-
: (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
|
|
3177
|
-
const waiting = named.filter((one) => !searchIndex.conversations().has(one));
|
|
3178
|
-
for (const conversation of waiting)
|
|
3179
|
-
pending.push({ conversation, awaits: 'index' });
|
|
3180
|
-
if (unindexed.length > 0 || waiting.length > 0)
|
|
3181
|
-
index(unindexed, waiting);
|
|
4701
|
+
const pending = [...unlocking, ...('pending' in cover ? cover.pending : [])];
|
|
3182
4702
|
const complete = pending.length === 0 && !('pending' in cover);
|
|
4703
|
+
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
3183
4704
|
const answering = complete
|
|
3184
4705
|
? filter
|
|
3185
|
-
: {
|
|
3186
|
-
...filter,
|
|
3187
|
-
rooms: covering
|
|
3188
|
-
.filter(({ room }) => searchIndex.rooms().has(room))
|
|
3189
|
-
.map(({ room }) => room),
|
|
3190
|
-
};
|
|
4706
|
+
: { ...filter, rooms: reachedRooms(filter).filter((room) => !awaited.has(room)) };
|
|
3191
4707
|
const answered = covered(answering);
|
|
3192
4708
|
if ('pending' in answered)
|
|
3193
4709
|
continue;
|
|
3194
|
-
const
|
|
3195
|
-
const
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
4710
|
+
const listed = conversations.searchable();
|
|
4711
|
+
const named = reach !== null && !reach.conversations
|
|
4712
|
+
? []
|
|
4713
|
+
: (asked.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
|
|
4714
|
+
const hits = await (await openLocal()).search({
|
|
4715
|
+
text: asked.text,
|
|
4716
|
+
limit: capped(asked.limit),
|
|
4717
|
+
after: asked.after,
|
|
4718
|
+
total: asked.total,
|
|
4719
|
+
documents: {
|
|
4720
|
+
plan: answering,
|
|
4721
|
+
rooms: answered.rooms,
|
|
4722
|
+
beneath: asked.collection
|
|
4723
|
+
? answered.rooms.map(({ locator, key }) => ({
|
|
4724
|
+
key,
|
|
4725
|
+
roots: replicas.get(locator)?.record.typedSelection?.roots ?? [],
|
|
4726
|
+
}))
|
|
4727
|
+
: [],
|
|
4728
|
+
},
|
|
4729
|
+
conversations: named,
|
|
4730
|
+
});
|
|
4731
|
+
if (JSON.stringify(covered(answering)) !== JSON.stringify(answered))
|
|
3202
4732
|
continue;
|
|
3203
|
-
const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms === null ? null : new Set(answering.rooms), plan.collection
|
|
3204
|
-
? new Map(answered.coverage.rooms.map(({ room }) => [
|
|
3205
|
-
room,
|
|
3206
|
-
replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
|
|
3207
|
-
]))
|
|
3208
|
-
: null);
|
|
3209
4733
|
const coverage = {
|
|
3210
4734
|
rooms: answered.coverage.rooms,
|
|
3211
|
-
conversations:
|
|
3212
|
-
index: searchIndex.generation(),
|
|
4735
|
+
conversations: named,
|
|
3213
4736
|
overlay: answered.coverage.overlay,
|
|
3214
4737
|
};
|
|
3215
|
-
return complete
|
|
4738
|
+
return complete
|
|
4739
|
+
? { ...hits, coverage }
|
|
4740
|
+
: { pending, documents: hits.documents, messages: hits.messages, coverage };
|
|
3216
4741
|
}
|
|
3217
4742
|
}
|
|
3218
4743
|
const watch = (topic, input, deliver) => {
|
|
@@ -3221,13 +4746,17 @@ export function composeRuntime(options) {
|
|
|
3221
4746
|
roomDraftListeners.add(listener);
|
|
3222
4747
|
void exclusive(async () => {
|
|
3223
4748
|
await openLocal();
|
|
3224
|
-
if (closed || !roomDraftListeners.has(listener))
|
|
3225
|
-
return;
|
|
3226
|
-
listener.ready = true;
|
|
3227
4749
|
}).then(() => {
|
|
3228
|
-
if (closed || !
|
|
4750
|
+
if (closed || !roomDraftListeners.has(listener))
|
|
3229
4751
|
return;
|
|
3230
|
-
|
|
4752
|
+
const snapshot = snapshotRoomDrafts();
|
|
4753
|
+
draftsShown = draftsShown.then(async () => {
|
|
4754
|
+
const value = await snapshot.catch(() => null);
|
|
4755
|
+
if (value === null || closed || !roomDraftListeners.has(listener))
|
|
4756
|
+
return;
|
|
4757
|
+
listener.ready = true;
|
|
4758
|
+
listener.notify(value);
|
|
4759
|
+
});
|
|
3231
4760
|
}, () => { });
|
|
3232
4761
|
return () => {
|
|
3233
4762
|
roomDraftListeners.delete(listener);
|
|
@@ -3322,7 +4851,7 @@ export function composeRuntime(options) {
|
|
|
3322
4851
|
return listed();
|
|
3323
4852
|
},
|
|
3324
4853
|
query: ((input) => rowsQuery(planned(input, null))),
|
|
3325
|
-
search: (input) => searched(input, null),
|
|
4854
|
+
search: (input) => searched(input, null, []),
|
|
3326
4855
|
types(locator) {
|
|
3327
4856
|
return {
|
|
3328
4857
|
collection: modelTypes(modelOf(locator)),
|
|
@@ -3342,9 +4871,9 @@ export function composeRuntime(options) {
|
|
|
3342
4871
|
return listed;
|
|
3343
4872
|
});
|
|
3344
4873
|
},
|
|
3345
|
-
read(room, path) {
|
|
4874
|
+
async read(room, path) {
|
|
3346
4875
|
const record = roomDrafts.get(room)?.get(path);
|
|
3347
|
-
return record === undefined ? null :
|
|
4876
|
+
return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
|
|
3348
4877
|
},
|
|
3349
4878
|
write(room, path, revision, input) {
|
|
3350
4879
|
return exclusive(async () => {
|
|
@@ -3354,11 +4883,11 @@ export function composeRuntime(options) {
|
|
|
3354
4883
|
const local = await openLocal();
|
|
3355
4884
|
const current = roomDrafts.get(room)?.get(path);
|
|
3356
4885
|
if ((current?.revision ?? null) !== revision) {
|
|
3357
|
-
return staleWrite(current === undefined ? null :
|
|
4886
|
+
return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
|
|
3358
4887
|
}
|
|
3359
4888
|
const file = parts.file === undefined
|
|
3360
4889
|
? undefined
|
|
3361
|
-
: await heldFile(parts.file.blob, binaryBase(
|
|
4890
|
+
: await heldFile(parts.file.blob, binaryBase(await visibleOf(room, [path]), path));
|
|
3362
4891
|
const record = {
|
|
3363
4892
|
locator: room,
|
|
3364
4893
|
path,
|
|
@@ -3366,10 +4895,12 @@ export function composeRuntime(options) {
|
|
|
3366
4895
|
parts: held(current?.parts, parts, file),
|
|
3367
4896
|
returned: current?.returned ?? null,
|
|
3368
4897
|
};
|
|
3369
|
-
await local.keepRoomDraft(record
|
|
4898
|
+
await local.keepRoomDraft(record, parts.file === undefined || file === undefined
|
|
4899
|
+
? []
|
|
4900
|
+
: [{ key: file.localId, blob: parts.file.blob }]);
|
|
3370
4901
|
rememberRoomDraft(record);
|
|
3371
|
-
publishChangeSet(changeSet(room,
|
|
3372
|
-
return { outcome: 'written', roomDraft:
|
|
4902
|
+
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
4903
|
+
return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
|
|
3373
4904
|
});
|
|
3374
4905
|
},
|
|
3375
4906
|
discard(room, path, revision) {
|
|
@@ -3377,11 +4908,11 @@ export function composeRuntime(options) {
|
|
|
3377
4908
|
const local = await openLocal();
|
|
3378
4909
|
const current = roomDrafts.get(room)?.get(path);
|
|
3379
4910
|
if (current === undefined || current.revision !== revision) {
|
|
3380
|
-
return staleWrite(current === undefined ? null :
|
|
4911
|
+
return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
|
|
3381
4912
|
}
|
|
3382
4913
|
await local.discardRoomDraft(room, path);
|
|
3383
4914
|
forgetRoomDraft(room, path);
|
|
3384
|
-
publishChangeSet(changeSet(room,
|
|
4915
|
+
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
3385
4916
|
moveTo({ ...status, ...attention() });
|
|
3386
4917
|
return { outcome: 'written', roomDraft: null };
|
|
3387
4918
|
});
|
|
@@ -3397,29 +4928,38 @@ export function composeRuntime(options) {
|
|
|
3397
4928
|
});
|
|
3398
4929
|
},
|
|
3399
4930
|
write(app, value) {
|
|
3400
|
-
return
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
4931
|
+
return keepPresentation(app, () => structuredClone(value));
|
|
4932
|
+
},
|
|
4933
|
+
edit(app, members) {
|
|
4934
|
+
return keepPresentation(app, (held) => Object.fromEntries([
|
|
4935
|
+
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(members, member)),
|
|
4936
|
+
...Object.entries(structuredClone(members)).filter(([, value]) => value !== null),
|
|
4937
|
+
]));
|
|
3407
4938
|
},
|
|
3408
4939
|
},
|
|
3409
4940
|
save(room, named, writtenBy) {
|
|
3410
4941
|
return exclusive(() => saveRoomDrafts(room, named, writtenBy));
|
|
3411
4942
|
},
|
|
4943
|
+
saveParts(room, parts, writtenBy) {
|
|
4944
|
+
return exclusive(() => saveParts(room, parts, writtenBy));
|
|
4945
|
+
},
|
|
3412
4946
|
history(locator, path, query) {
|
|
3413
4947
|
return readHistory(locator, path, query);
|
|
3414
4948
|
},
|
|
4949
|
+
resolve(room, path, destination) {
|
|
4950
|
+
return resolveReference(room, path, destination);
|
|
4951
|
+
},
|
|
3415
4952
|
binaryFile: {
|
|
4953
|
+
part: readPart,
|
|
3416
4954
|
read: readBinaryPath,
|
|
3417
|
-
|
|
4955
|
+
link: linkBinaryPath,
|
|
3418
4956
|
},
|
|
3419
|
-
call(name, input) {
|
|
3420
|
-
|
|
4957
|
+
async call(name, input) {
|
|
4958
|
+
await ready;
|
|
4959
|
+
const room = calledRooms(name, input).find((one) => locked(one));
|
|
4960
|
+
return room === undefined ? recoveredAction(name, input) : lockedRoom(room);
|
|
3421
4961
|
},
|
|
3422
|
-
reached(locator, roomVersion, path) {
|
|
4962
|
+
async reached(locator, roomVersion, path) {
|
|
3423
4963
|
return path === undefined
|
|
3424
4964
|
? reachedBy(locator, roomVersion)
|
|
3425
4965
|
: writtenAfter(locator, roomVersion, path);
|
|
@@ -3427,12 +4967,13 @@ export function composeRuntime(options) {
|
|
|
3427
4967
|
log() {
|
|
3428
4968
|
return journal.export();
|
|
3429
4969
|
},
|
|
4970
|
+
cleanUp,
|
|
3430
4971
|
persistence() {
|
|
3431
4972
|
return persistence === 'granted';
|
|
3432
4973
|
},
|
|
3433
4974
|
async retry() {
|
|
3434
4975
|
journal.record('retry');
|
|
3435
|
-
|
|
4976
|
+
unpreparable.clear();
|
|
3436
4977
|
queueReadiness();
|
|
3437
4978
|
resume();
|
|
3438
4979
|
},
|
|
@@ -3456,12 +4997,15 @@ export function composeRuntime(options) {
|
|
|
3456
4997
|
const reachable = {
|
|
3457
4998
|
md,
|
|
3458
4999
|
watch,
|
|
3459
|
-
referrerScope: (referrer) => options.adapter.referrerScope(referrer),
|
|
3460
5000
|
plan: planned,
|
|
3461
5001
|
query: (plan) => rowsQuery(plan),
|
|
3462
5002
|
watchPlan,
|
|
3463
5003
|
search: searched,
|
|
3464
5004
|
watchReadiness,
|
|
5005
|
+
locked,
|
|
5006
|
+
lockedRooms,
|
|
5007
|
+
watchLocks,
|
|
5008
|
+
located,
|
|
3465
5009
|
};
|
|
3466
5010
|
return {
|
|
3467
5011
|
md,
|
|
@@ -3469,7 +5013,6 @@ export function composeRuntime(options) {
|
|
|
3469
5013
|
stop,
|
|
3470
5014
|
lose: loseRooms,
|
|
3471
5015
|
watch,
|
|
3472
|
-
acquire,
|
|
3473
5016
|
persisted(answer) {
|
|
3474
5017
|
if (answer === persistence)
|
|
3475
5018
|
return;
|
|
@@ -3477,6 +5020,7 @@ export function composeRuntime(options) {
|
|
|
3477
5020
|
journal.record(`persistence ${answer}`);
|
|
3478
5021
|
queueReadiness();
|
|
3479
5022
|
},
|
|
5023
|
+
shown: screen,
|
|
3480
5024
|
within: (reach, port) => narrowed(reachable, reach, port),
|
|
3481
5025
|
};
|
|
3482
5026
|
}
|