@agentshouse/mdruntime 0.1.0-c5d3fa6fb67bc → 0.1.0-c63ad08fad0c3
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 +240 -220
- package/README.md +9 -8
- package/dist/adapter.d.ts +72 -24
- package/dist/adapter.d.ts.map +1 -1
- package/dist/attached-model.d.ts +4 -1
- package/dist/attached-model.d.ts.map +1 -1
- package/dist/attached-model.js +13 -3
- package/dist/bootstrap.d.ts +26 -7
- 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/kit.js +1 -1
- 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 +1 -0
- package/dist/chat/markdown.d.ts.map +1 -1
- package/dist/chat/markdown.js +78 -20
- package/dist/chat/message.d.ts +1 -0
- package/dist/chat/message.d.ts.map +1 -1
- package/dist/chat/message.js +73 -11
- package/dist/chat/messages.d.ts +14 -1
- package/dist/chat/messages.d.ts.map +1 -1
- package/dist/chat/messages.js +79 -24
- package/dist/chat/name.d.ts +1 -0
- package/dist/chat/name.d.ts.map +1 -1
- package/dist/chat/name.js +1 -1
- package/dist/chat/opened.d.ts +1 -0
- package/dist/chat/opened.d.ts.map +1 -1
- package/dist/chat/opened.js +2 -0
- package/dist/chat/overlay.d.ts +1 -0
- package/dist/chat/overlay.d.ts.map +1 -1
- package/dist/chat/overlay.js +11 -5
- package/dist/chat/parts.d.ts +1 -0
- package/dist/chat/parts.d.ts.map +1 -1
- package/dist/chat/parts.js +3 -2
- package/dist/chat/pieces.d.ts +62 -0
- package/dist/chat/pieces.d.ts.map +1 -0
- package/dist/chat/pieces.js +1110 -0
- package/dist/chat/provider.d.ts +8 -1
- package/dist/chat/provider.d.ts.map +1 -1
- package/dist/chat/provider.js +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 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +45 -7
- 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 +22 -8
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +2509 -830
- package/dist/conversation.d.ts +15 -3
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +238 -94
- 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 +5 -4
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +57 -7
- package/dist/identity.d.ts +0 -1
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +0 -3
- package/dist/index.d.ts +12 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -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 +7 -3
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +39 -17
- 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 +35 -4
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +28 -0
- package/dist/overlay.d.ts +2 -5
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +86 -171
- package/dist/query.d.ts +29 -2
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +46 -7
- package/dist/reach.d.ts +5 -1
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +167 -48
- package/dist/react.d.ts +3 -3
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +7 -17
- 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 +11 -0
- package/dist/reads.d.ts.map +1 -0
- package/dist/reads.js +100 -0
- 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 +13 -15
- 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 +15 -1
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +32 -25
- 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 +25 -42
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +30 -169
- package/dist/seed.d.ts +16 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +75 -0
- 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 +32 -0
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +8 -0
- package/dist/words.d.ts +19 -0
- package/dist/words.d.ts.map +1 -0
- package/dist/words.js +150 -0
- package/package.json +10 -4
- package/dist/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,34 +1,39 @@
|
|
|
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
|
-
import { observing } from './observation.js';
|
|
4
|
-
import { filterOf, identityOf, planOf, } from './query.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
4
|
+
import { observing, whole } from './observation.js';
|
|
5
|
+
import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
|
|
6
|
+
import { createLiveQueries } from './live.js';
|
|
7
|
+
import { seededResult } from './seed.js';
|
|
8
|
+
import { documentKey, roomOfKey } from './rows.js';
|
|
9
|
+
import { searchPlanOf } from './search.js';
|
|
10
|
+
import { capped, MAXIMUM_WINDOW } from './windows.js';
|
|
8
11
|
import { bootstrapRoom } from './bootstrap.js';
|
|
9
|
-
import { changeSet,
|
|
12
|
+
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
10
13
|
import { createConversationStore, } from './conversation.js';
|
|
11
|
-
import { queuedFile } from './change.js';
|
|
12
|
-
import {
|
|
14
|
+
import { leaving as leavingIn, partnerOf, queuedFile, } from './change.js';
|
|
15
|
+
import { available, collectionOf, unreadable as unreadableIn, validRoot, } from './collection.js';
|
|
13
16
|
import { runtimeError } from './failure.js';
|
|
14
17
|
import { createLog } from './log.js';
|
|
15
18
|
import { binaryRecord } from './binary.js';
|
|
16
|
-
import {
|
|
19
|
+
import { openedFiles } from './opened.js';
|
|
20
|
+
import { ABSENT_TARGET, absentBinary, absentReference, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
|
|
17
21
|
import { createRecovery } from './recovery.js';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
+
import { doorReads } from './reads.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';
|
|
22
27
|
import { same, saving, STARTING, transferring, } from './status.js';
|
|
23
28
|
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
24
|
-
import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
|
|
25
29
|
import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
|
|
26
30
|
import { FencedMap } from './fence.js';
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
31
|
+
import { sameBinary } from './overlay.js';
|
|
32
|
+
import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
|
|
29
33
|
import { identityTime, roomDraftRevision } from './identity.js';
|
|
30
34
|
import { beneath, normalize } from './paths.js';
|
|
31
35
|
import { openRuntimeStore, } from './store.js';
|
|
36
|
+
import { changedCleanly, sameComposition, validatedCollection, } from './validation.js';
|
|
32
37
|
import { unconfirmed } from './transfer.js';
|
|
33
38
|
import { governs, holds, narrowed, planReach, } from './reach.js';
|
|
34
39
|
function within(root, path) {
|
|
@@ -48,19 +53,29 @@ const OPENING_LANE = 'opening';
|
|
|
48
53
|
const CONVERSATION_LANE = 'conversations';
|
|
49
54
|
const ROOM_READS = 256;
|
|
50
55
|
const ROOM_ATTEMPTS = 16;
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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;
|
|
64
79
|
}
|
|
65
80
|
function abandonedOn(signal) {
|
|
66
81
|
return new Promise((resolve) => {
|
|
@@ -73,11 +88,14 @@ function abandonedOn(signal) {
|
|
|
73
88
|
function roomLane(locator) {
|
|
74
89
|
return `room ${locator}`;
|
|
75
90
|
}
|
|
91
|
+
function pendingKey(locator, id) {
|
|
92
|
+
return `${locator}\u0000${id}`;
|
|
93
|
+
}
|
|
76
94
|
export function composeRuntime(options) {
|
|
77
95
|
const localSaves = options.adapter.localSaves === true;
|
|
96
|
+
const roomTypes = roomTypesOf(options.adapter.roomTypes);
|
|
78
97
|
let authorization = options.authorization;
|
|
79
98
|
let opening;
|
|
80
|
-
let store;
|
|
81
99
|
const fences = new Map();
|
|
82
100
|
const isFenced = (locator) => fences.has(locator);
|
|
83
101
|
let fencings = 0;
|
|
@@ -87,6 +105,8 @@ export function composeRuntime(options) {
|
|
|
87
105
|
const replicas = new FencedMap(isFenced);
|
|
88
106
|
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
89
107
|
const pendings = new Map();
|
|
108
|
+
const seeds = new Map();
|
|
109
|
+
const seedWatchers = new Map();
|
|
90
110
|
const roomDrafts = new FencedMap(isFenced);
|
|
91
111
|
const presentations = new Map();
|
|
92
112
|
let limits = [];
|
|
@@ -99,6 +119,7 @@ export function composeRuntime(options) {
|
|
|
99
119
|
const liveModels = new FencedMap(isModelFenced);
|
|
100
120
|
const invalidModels = new Set();
|
|
101
121
|
const discovered = new Map();
|
|
122
|
+
const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
|
|
102
123
|
let roomsListed = false;
|
|
103
124
|
const scopedRefusals = new Map();
|
|
104
125
|
const sourceRefusals = new Map();
|
|
@@ -135,6 +156,7 @@ export function composeRuntime(options) {
|
|
|
135
156
|
let handed = false;
|
|
136
157
|
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
137
158
|
const repairing = new Set();
|
|
159
|
+
const calling = new Set();
|
|
138
160
|
const repairable = new Map();
|
|
139
161
|
let outcomeRetention = null;
|
|
140
162
|
const answering = new Map();
|
|
@@ -145,6 +167,7 @@ export function composeRuntime(options) {
|
|
|
145
167
|
let discovering = null;
|
|
146
168
|
let opened = null;
|
|
147
169
|
const owed = new Set();
|
|
170
|
+
const reloadsDue = new Map();
|
|
148
171
|
const lanes = new Map();
|
|
149
172
|
let roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
150
173
|
const working = new Set();
|
|
@@ -156,23 +179,22 @@ export function composeRuntime(options) {
|
|
|
156
179
|
const queueSubscriptions = new Set();
|
|
157
180
|
const roomDraftListeners = new Set();
|
|
158
181
|
const presentationListeners = new Set();
|
|
159
|
-
const
|
|
182
|
+
const liveQueries = createLiveQueries((plan, cover) => answeredPlan(plan, cover));
|
|
160
183
|
let withdrawConversations;
|
|
161
184
|
let currencyQueued = null;
|
|
162
185
|
let socketOpen = false;
|
|
186
|
+
const confirmed = new Set();
|
|
187
|
+
let lockGrace = null;
|
|
188
|
+
const lockListeners = new Set();
|
|
163
189
|
let serviceMode = 'open';
|
|
164
190
|
let haltedRefusal = false;
|
|
165
191
|
let openSignals = 0;
|
|
166
192
|
const serviceModeListeners = new Set();
|
|
167
193
|
let member = null;
|
|
168
194
|
const memberListeners = new Set();
|
|
169
|
-
const prepared = new Set();
|
|
170
|
-
const projecting = new Set();
|
|
171
195
|
const unpreparedDemand = new Set();
|
|
172
|
-
const
|
|
196
|
+
const unpreparable = new Set();
|
|
173
197
|
let preparing = false;
|
|
174
|
-
let demand = WHOLE_DEMAND;
|
|
175
|
-
let narrowing = null;
|
|
176
198
|
let targets = {};
|
|
177
199
|
let persistence = options.shell.persistence;
|
|
178
200
|
let capacity = null;
|
|
@@ -182,26 +204,41 @@ export function composeRuntime(options) {
|
|
|
182
204
|
let readinessQueued = false;
|
|
183
205
|
const catching = new Set();
|
|
184
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();
|
|
185
216
|
const conversations = createConversationStore({
|
|
186
217
|
agents: declaredAgents,
|
|
187
|
-
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())),
|
|
188
231
|
call: (name, input) => invokeAction(name, input),
|
|
189
232
|
durably,
|
|
190
233
|
stream: () => options.adapter.conversations,
|
|
191
234
|
publish: publishChangeSet,
|
|
192
|
-
online: () =>
|
|
235
|
+
online: () => connectionOf() !== 'unreachable' && refusal === null,
|
|
193
236
|
closed: () => closed,
|
|
194
237
|
unanswered: resendLater,
|
|
195
238
|
expired: (operation) => pastHorizon(operation),
|
|
196
239
|
});
|
|
197
|
-
const searchIndex = createSearchIndex({
|
|
198
|
-
room: (room) => readSide.room(room),
|
|
199
|
-
document: (room, path) => readSide.document(room, path),
|
|
200
|
-
});
|
|
201
|
-
const roomsToIndex = new Set();
|
|
202
|
-
const conversationsToIndex = new Set();
|
|
203
|
-
let indexing = false;
|
|
204
240
|
const journal = createLog();
|
|
241
|
+
const keptFiles = openedFiles(options.database, isFenced);
|
|
205
242
|
let notifyReady = () => { };
|
|
206
243
|
const ready = new Promise((resolve) => {
|
|
207
244
|
notifyReady = resolve;
|
|
@@ -219,16 +256,36 @@ export function composeRuntime(options) {
|
|
|
219
256
|
if (room.size === 0)
|
|
220
257
|
roomDrafts.delete(locator);
|
|
221
258
|
}
|
|
222
|
-
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) {
|
|
223
268
|
return structuredClone({
|
|
224
269
|
room: record.locator,
|
|
225
270
|
path: record.path,
|
|
226
271
|
revision: record.revision,
|
|
227
272
|
parts: record.parts,
|
|
228
|
-
changedUnderneath: underneath(record.parts, record.path,
|
|
273
|
+
changedUnderneath: underneath(record.parts, record.path, visible),
|
|
229
274
|
returned: record.returned,
|
|
230
275
|
});
|
|
231
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
|
+
}
|
|
232
289
|
function snapshotRoomDrafts() {
|
|
233
290
|
const records = [...roomDrafts.values()].flatMap((room) => [...room.values()]);
|
|
234
291
|
records.sort((left, right) => {
|
|
@@ -238,23 +295,38 @@ export function composeRuntime(options) {
|
|
|
238
295
|
return left.path < right.path ? -1 : 1;
|
|
239
296
|
return 0;
|
|
240
297
|
});
|
|
241
|
-
return records
|
|
298
|
+
return roomDraftsOf(records);
|
|
242
299
|
}
|
|
300
|
+
let draftsShown = Promise.resolve();
|
|
243
301
|
function publishRoomDrafts(waitingOnly) {
|
|
244
302
|
if (closed)
|
|
245
303
|
return;
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
listener
|
|
252
|
-
|
|
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
|
+
});
|
|
253
316
|
}
|
|
254
317
|
function presentationOf(app) {
|
|
255
318
|
const value = presentations.get(app);
|
|
256
319
|
return value === undefined ? null : structuredClone(value);
|
|
257
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
|
+
}
|
|
258
330
|
function emitPresentation(app) {
|
|
259
331
|
if (closed)
|
|
260
332
|
return;
|
|
@@ -303,21 +375,69 @@ export function composeRuntime(options) {
|
|
|
303
375
|
return { attentionRequired, reset };
|
|
304
376
|
}
|
|
305
377
|
function documentsIn(locator) {
|
|
306
|
-
|
|
307
|
-
if (replica === undefined)
|
|
308
|
-
return 0;
|
|
309
|
-
const roots = replica.confirmed.record.typedSelection?.roots ?? [];
|
|
310
|
-
const weighed = outsideOf(roots);
|
|
311
|
-
return (tally(replica.visible.documents, weighed.documents) +
|
|
312
|
-
tally(replica.visible.binaries, weighed.binaries));
|
|
378
|
+
return replicas.get(locator)?.count ?? 0;
|
|
313
379
|
}
|
|
314
380
|
function listed() {
|
|
315
381
|
return structuredClone([...rooms.values()].map((room) => {
|
|
316
382
|
const documents = documentsIn(room.locator);
|
|
317
383
|
counted.set(room.locator, documents);
|
|
318
|
-
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 };
|
|
319
393
|
}));
|
|
320
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
|
+
}
|
|
321
441
|
function roomFacts(reach = null) {
|
|
322
442
|
const held = [...rooms.values()]
|
|
323
443
|
.filter((room) => holds(reach, room.locator))
|
|
@@ -336,7 +456,7 @@ export function composeRuntime(options) {
|
|
|
336
456
|
return { listed: roomsListed, rooms: held };
|
|
337
457
|
}
|
|
338
458
|
function emitRooms() {
|
|
339
|
-
|
|
459
|
+
liveQueries.recover();
|
|
340
460
|
queueReadiness();
|
|
341
461
|
const value = listed();
|
|
342
462
|
for (const listener of roomListeners)
|
|
@@ -366,7 +486,7 @@ export function composeRuntime(options) {
|
|
|
366
486
|
if (closed)
|
|
367
487
|
return;
|
|
368
488
|
queueReadiness();
|
|
369
|
-
const next = status.state === 'synced' &&
|
|
489
|
+
const next = status.state === 'synced' && !conversations.current()
|
|
370
490
|
? { ...status, state: 'syncing' }
|
|
371
491
|
: status;
|
|
372
492
|
if (same(shown, next))
|
|
@@ -375,17 +495,17 @@ export function composeRuntime(options) {
|
|
|
375
495
|
for (const listener of statusListeners)
|
|
376
496
|
listener(shown);
|
|
377
497
|
}
|
|
378
|
-
function entryOf(batch) {
|
|
498
|
+
function entryOf(locator, batch) {
|
|
379
499
|
if (localSaves)
|
|
380
500
|
return Object.freeze({ batch, state: 'local' });
|
|
381
|
-
const state = pendings.get(batch.id);
|
|
501
|
+
const state = pendings.get(pendingKey(locator, batch.id));
|
|
382
502
|
if (state?.accepted !== undefined) {
|
|
383
503
|
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
384
504
|
}
|
|
385
505
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
386
506
|
}
|
|
387
507
|
function entriesOf(locator) {
|
|
388
|
-
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
|
|
508
|
+
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map((batch) => entryOf(locator, batch));
|
|
389
509
|
}
|
|
390
510
|
function emitQueue(locator) {
|
|
391
511
|
const entries = entriesOf(locator);
|
|
@@ -395,62 +515,39 @@ export function composeRuntime(options) {
|
|
|
395
515
|
}
|
|
396
516
|
}
|
|
397
517
|
function applyChangeSet(set) {
|
|
398
|
-
|
|
399
|
-
...set,
|
|
400
|
-
documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
|
|
401
|
-
};
|
|
402
|
-
readSide.apply(held);
|
|
403
|
-
searchIndex.changed(held);
|
|
518
|
+
liveQueries.touched(set);
|
|
404
519
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const held = { text: record.documents, binaries: record.binaries };
|
|
411
|
-
const composition = compositionOf(record);
|
|
412
|
-
if (referencesIndexed(held, composition))
|
|
413
|
-
return;
|
|
414
|
-
await indexReferrers(held, composition);
|
|
415
|
-
}
|
|
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;
|
|
416
525
|
}
|
|
417
526
|
async function prepareReplica(locator) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
return;
|
|
422
|
-
const visible = replica.visible;
|
|
423
|
-
const paths = [
|
|
424
|
-
...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
|
|
425
|
-
];
|
|
426
|
-
const entered = [];
|
|
427
|
-
projecting.add(locator);
|
|
428
|
-
try {
|
|
429
|
-
await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
|
|
430
|
-
const current = replicas.get(locator);
|
|
431
|
-
if (current === undefined || closed)
|
|
432
|
-
return;
|
|
433
|
-
const set = entering(locator, current, chunk);
|
|
434
|
-
readSide.apply(set);
|
|
435
|
-
entered.push(set);
|
|
436
|
-
});
|
|
437
|
-
if (!replicas.has(locator) || closed)
|
|
527
|
+
for (;;) {
|
|
528
|
+
const held = replicas.get(locator);
|
|
529
|
+
if (held === undefined || closed || held.indexed)
|
|
438
530
|
return;
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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;
|
|
443
543
|
}
|
|
444
|
-
conversations.changed(mergeChangeSets(entered));
|
|
445
|
-
readSide.recover();
|
|
446
|
-
queueReadiness();
|
|
447
544
|
}
|
|
448
545
|
function unprepared() {
|
|
449
|
-
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !
|
|
546
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
|
|
450
547
|
}
|
|
451
|
-
function
|
|
452
|
-
|
|
453
|
-
journal.record(`
|
|
548
|
+
function unpreparedBy(locator, error) {
|
|
549
|
+
unpreparable.add(locator);
|
|
550
|
+
journal.record(`preparation failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
|
|
454
551
|
queueReadiness();
|
|
455
552
|
}
|
|
456
553
|
function prepare(locators = []) {
|
|
@@ -466,7 +563,7 @@ export function composeRuntime(options) {
|
|
|
466
563
|
if (closed)
|
|
467
564
|
return;
|
|
468
565
|
unpreparedDemand.delete(waiting);
|
|
469
|
-
await prepareReplica(waiting).catch((error) =>
|
|
566
|
+
await prepareReplica(waiting).catch((error) => unpreparedBy(waiting, error));
|
|
470
567
|
}
|
|
471
568
|
}
|
|
472
569
|
finally {
|
|
@@ -474,17 +571,22 @@ export function composeRuntime(options) {
|
|
|
474
571
|
}
|
|
475
572
|
})();
|
|
476
573
|
}
|
|
574
|
+
function roomsOfReach(reach) {
|
|
575
|
+
return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
576
|
+
}
|
|
477
577
|
function reachedRooms(plan) {
|
|
478
|
-
const
|
|
479
|
-
const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
578
|
+
const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
|
|
480
579
|
const named = plan.rooms ??
|
|
481
580
|
(reached === null
|
|
482
581
|
? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
|
|
483
582
|
: [...reached]);
|
|
484
|
-
return [...new Set(named)]
|
|
583
|
+
return [...new Set(named)]
|
|
584
|
+
.filter((locator) => (reached === null || reached.has(locator)) && !plan.locked.includes(locator))
|
|
585
|
+
.sort();
|
|
485
586
|
}
|
|
486
587
|
function awaited(locator) {
|
|
487
|
-
return
|
|
588
|
+
return (!roomsListed ||
|
|
589
|
+
(rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
|
|
488
590
|
}
|
|
489
591
|
function covered(plan) {
|
|
490
592
|
const locators = reachedRooms(plan);
|
|
@@ -494,6 +596,7 @@ export function composeRuntime(options) {
|
|
|
494
596
|
const pending = [];
|
|
495
597
|
const coverage = [];
|
|
496
598
|
const overlay = [];
|
|
599
|
+
const read = [];
|
|
497
600
|
for (const locator of locators) {
|
|
498
601
|
const replica = replicas.get(locator);
|
|
499
602
|
if (replica === undefined) {
|
|
@@ -501,21 +604,22 @@ export function composeRuntime(options) {
|
|
|
501
604
|
pending.push({ room: locator, awaits: 'acquisition' });
|
|
502
605
|
continue;
|
|
503
606
|
}
|
|
504
|
-
if (!
|
|
607
|
+
if (!replica.indexed) {
|
|
505
608
|
pending.push({ room: locator, awaits: 'preparation' });
|
|
506
609
|
continue;
|
|
507
610
|
}
|
|
508
611
|
coverage.push({
|
|
509
612
|
room: locator,
|
|
510
|
-
generation: replica.
|
|
613
|
+
generation: replica.record.transfer,
|
|
511
614
|
current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
|
|
512
615
|
});
|
|
616
|
+
read.push({ locator, key: replica.record.key });
|
|
513
617
|
for (const batch of replica.queue)
|
|
514
618
|
overlay.push(batch.id);
|
|
515
619
|
}
|
|
516
620
|
if (pending.length > 0)
|
|
517
621
|
return { pending };
|
|
518
|
-
return { coverage: { rooms: coverage, indexes: [], overlay } };
|
|
622
|
+
return { coverage: { rooms: coverage, indexes: [], overlay }, rooms: read };
|
|
519
623
|
}
|
|
520
624
|
function awaitPreparation(cover) {
|
|
521
625
|
if (!('pending' in cover))
|
|
@@ -524,66 +628,16 @@ export function composeRuntime(options) {
|
|
|
524
628
|
if (waiting.length > 0)
|
|
525
629
|
prepare(waiting.map(({ room }) => room));
|
|
526
630
|
}
|
|
527
|
-
function
|
|
528
|
-
|
|
529
|
-
(replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
|
|
530
|
-
queuesWithoutReplica.has(room.locator));
|
|
531
|
-
}
|
|
532
|
-
function acquiredLocators() {
|
|
533
|
-
return [...discovered.values()]
|
|
534
|
-
.filter(acquires)
|
|
535
|
-
.map((room) => room.locator)
|
|
536
|
-
.sort();
|
|
537
|
-
}
|
|
538
|
-
function heldDemand(acquired) {
|
|
539
|
-
if (demand.whole)
|
|
540
|
-
return null;
|
|
541
|
-
return { rooms: acquired.map((room) => room.locator), conversations: demand.conversations };
|
|
542
|
-
}
|
|
543
|
-
function applyDemand(next) {
|
|
544
|
-
const before = acquiredLocators();
|
|
545
|
-
const conversed = demand.conversations;
|
|
546
|
-
demand = next;
|
|
547
|
-
const after = acquiredLocators();
|
|
548
|
-
queueReadiness();
|
|
549
|
-
if (!conversed && next.conversations)
|
|
550
|
-
scheduleCurrency();
|
|
551
|
-
if (JSON.stringify(before) === JSON.stringify(after) && conversed === next.conversations)
|
|
552
|
-
return;
|
|
553
|
-
journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
|
|
554
|
-
if (options.adapter.roomStream === undefined)
|
|
555
|
-
void sync('unstreamed');
|
|
556
|
-
else if (socketOpen)
|
|
557
|
-
options.adapter.roomStream.reopen();
|
|
558
|
-
}
|
|
559
|
-
function acquire(next) {
|
|
560
|
-
if (closed)
|
|
561
|
-
return;
|
|
562
|
-
if (narrowing !== null)
|
|
563
|
-
clearTimeout(narrowing);
|
|
564
|
-
narrowing = null;
|
|
565
|
-
const widening = wider(next, demand);
|
|
566
|
-
if (!widening && !narrower(next, demand))
|
|
567
|
-
return;
|
|
568
|
-
if (widening || !roomsListed) {
|
|
569
|
-
applyDemand(next);
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
// A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
|
|
573
|
-
narrowing = setTimeout(() => {
|
|
574
|
-
narrowing = null;
|
|
575
|
-
applyDemand(next);
|
|
576
|
-
}, 5_000);
|
|
577
|
-
}
|
|
578
|
-
function requiredRooms(reach) {
|
|
631
|
+
function requiredRooms(given) {
|
|
632
|
+
const reach = planReach(given);
|
|
579
633
|
if (reach === null)
|
|
580
634
|
return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
|
|
581
|
-
const named = [...
|
|
635
|
+
const named = [...roomsOfReach(reach)].sort();
|
|
582
636
|
if (roomsListed)
|
|
583
637
|
return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
|
|
584
638
|
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
585
639
|
}
|
|
586
|
-
function roomState(locator
|
|
640
|
+
function roomState(locator) {
|
|
587
641
|
if (sourceRefusals.has(locator))
|
|
588
642
|
return 'refused';
|
|
589
643
|
const listedRoom = discovered.get(locator);
|
|
@@ -594,27 +648,17 @@ export function composeRuntime(options) {
|
|
|
594
648
|
return 'unadmitted';
|
|
595
649
|
if (!replicas.has(locator))
|
|
596
650
|
return 'acquiring';
|
|
597
|
-
if (!
|
|
651
|
+
if (!indexed(locator))
|
|
598
652
|
return 'preparing';
|
|
599
|
-
if (search && !searchIndex.rooms().has(locator))
|
|
600
|
-
return 'indexing';
|
|
601
653
|
return 'complete';
|
|
602
654
|
}
|
|
603
|
-
function conversationCoverage(reach
|
|
655
|
+
function conversationCoverage(reach) {
|
|
604
656
|
const declared = reach === null || reach.conversations;
|
|
605
657
|
if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
|
|
606
658
|
return 'undeclared';
|
|
607
659
|
if (!conversations.known() || !conversations.current())
|
|
608
660
|
return 'acquiring';
|
|
609
|
-
|
|
610
|
-
return 'complete';
|
|
611
|
-
const unindexed = conversations
|
|
612
|
-
.searchable()
|
|
613
|
-
.filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
|
|
614
|
-
if (unindexed.length === 0)
|
|
615
|
-
return 'complete';
|
|
616
|
-
index([], unindexed);
|
|
617
|
-
return 'indexing';
|
|
661
|
+
return 'complete';
|
|
618
662
|
}
|
|
619
663
|
function connectionOf() {
|
|
620
664
|
if (socketOpen)
|
|
@@ -641,25 +685,66 @@ export function composeRuntime(options) {
|
|
|
641
685
|
];
|
|
642
686
|
});
|
|
643
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
|
+
}
|
|
644
733
|
function readinessFor(port, reach) {
|
|
645
734
|
const required = requiredRooms(reach);
|
|
646
735
|
const search = port.search === 'required';
|
|
647
|
-
const coverage = required.rooms.map((room) => ({ room, state: roomState(room
|
|
648
|
-
const
|
|
649
|
-
.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))
|
|
650
739
|
.map(({ room }) => room);
|
|
651
|
-
const preparing = building('preparing');
|
|
652
740
|
if (preparing.length > 0)
|
|
653
741
|
prepare(preparing);
|
|
654
|
-
const indexing = building('indexing');
|
|
655
|
-
if (indexing.length > 0)
|
|
656
|
-
index(indexing, []);
|
|
657
742
|
return readinessOf({
|
|
658
743
|
code: port.code,
|
|
659
744
|
coverage: {
|
|
660
745
|
listed: required.listed,
|
|
661
746
|
rooms: coverage,
|
|
662
|
-
conversations: conversationCoverage(reach
|
|
747
|
+
conversations: conversationCoverage(reach),
|
|
663
748
|
search: search ? 'required' : 'unused',
|
|
664
749
|
},
|
|
665
750
|
persistence,
|
|
@@ -669,6 +754,7 @@ export function composeRuntime(options) {
|
|
|
669
754
|
coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
|
|
670
755
|
? capacity
|
|
671
756
|
: port.capacity,
|
|
757
|
+
installation: installationOf(coverage),
|
|
672
758
|
refused: refusedIn(required.rooms),
|
|
673
759
|
lost: storageLoss !== null,
|
|
674
760
|
established: targets[port.target]?.established === true,
|
|
@@ -677,7 +763,7 @@ export function composeRuntime(options) {
|
|
|
677
763
|
function establish(name) {
|
|
678
764
|
targets = { ...targets, [name]: { established: true } };
|
|
679
765
|
queueReadiness();
|
|
680
|
-
void durably((local) => local.
|
|
766
|
+
void durably((local) => local.fact('targets', targets)).catch(() => {
|
|
681
767
|
journal.record(`target record failed ${name}`);
|
|
682
768
|
});
|
|
683
769
|
}
|
|
@@ -714,18 +800,12 @@ export function composeRuntime(options) {
|
|
|
714
800
|
readinessWatches.delete(watch);
|
|
715
801
|
};
|
|
716
802
|
}
|
|
717
|
-
function
|
|
803
|
+
function pageBytes(page) {
|
|
718
804
|
let bytes = 0;
|
|
719
|
-
for (const
|
|
720
|
-
bytes += (path.length +
|
|
805
|
+
for (const { path, content } of page.entries)
|
|
806
|
+
bytes += (path.length + (content?.length ?? 0)) * 2;
|
|
721
807
|
return bytes;
|
|
722
808
|
}
|
|
723
|
-
function grownBy(next, previous) {
|
|
724
|
-
if (previous === undefined)
|
|
725
|
-
return layerBytes(next);
|
|
726
|
-
const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
|
|
727
|
-
return layerBytes({ ...next, documents });
|
|
728
|
-
}
|
|
729
809
|
function exceeded(error) {
|
|
730
810
|
return (typeof error === 'object' &&
|
|
731
811
|
error !== null &&
|
|
@@ -733,7 +813,7 @@ export function composeRuntime(options) {
|
|
|
733
813
|
}
|
|
734
814
|
async function estimated() {
|
|
735
815
|
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
736
|
-
return estimate === null ? null : Math.max(0, estimate.
|
|
816
|
+
return estimate === null ? null : Math.max(0, estimate.total - estimate.usage);
|
|
737
817
|
}
|
|
738
818
|
function pause(locator, cause) {
|
|
739
819
|
paused.add(locator);
|
|
@@ -754,7 +834,8 @@ export function composeRuntime(options) {
|
|
|
754
834
|
const available = await estimated().catch(() => null);
|
|
755
835
|
if (available === null || available < (held.needed ?? 0))
|
|
756
836
|
return;
|
|
757
|
-
if (held.cause === '
|
|
837
|
+
if (held.cause === 'storage-exceeded' &&
|
|
838
|
+
(held.available === null || available <= held.available))
|
|
758
839
|
return;
|
|
759
840
|
resume();
|
|
760
841
|
}
|
|
@@ -772,10 +853,7 @@ export function composeRuntime(options) {
|
|
|
772
853
|
void sync('unstreamed');
|
|
773
854
|
}
|
|
774
855
|
async function reclaimed(local) {
|
|
775
|
-
const unreferenced = [...transfers.keys()].filter((locator) =>
|
|
776
|
-
const room = discovered.get(locator);
|
|
777
|
-
return room === undefined || !acquires(room);
|
|
778
|
-
});
|
|
856
|
+
const unreferenced = [...transfers.keys()].filter((locator) => !discovered.has(locator));
|
|
779
857
|
for (const locator of unreferenced) {
|
|
780
858
|
await local.abandon(locator);
|
|
781
859
|
transfers.delete(locator);
|
|
@@ -799,68 +877,129 @@ export function composeRuntime(options) {
|
|
|
799
877
|
}
|
|
800
878
|
async function exceededOn(locator, bytes) {
|
|
801
879
|
const available = await estimated().catch(() => null);
|
|
802
|
-
pause(locator, { cause: '
|
|
880
|
+
pause(locator, { cause: 'storage-exceeded', needed: bytes, available });
|
|
803
881
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
882
|
+
function seedRoomHeld(room, version) {
|
|
883
|
+
return indexed(room) && reachedBy(room, version);
|
|
884
|
+
}
|
|
885
|
+
function seedHeld(at) {
|
|
886
|
+
return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
|
|
887
|
+
}
|
|
888
|
+
function seedApplies(plan) {
|
|
889
|
+
const seed = seeds.get(plan.key);
|
|
890
|
+
if (seed === undefined)
|
|
891
|
+
return null;
|
|
892
|
+
if (!seedHeld(seed.at))
|
|
893
|
+
return seed;
|
|
894
|
+
seeds.delete(plan.key);
|
|
895
|
+
return null;
|
|
896
|
+
}
|
|
897
|
+
function seedBehind(plan, cover) {
|
|
898
|
+
const seed = seedApplies(plan);
|
|
899
|
+
if (seed === null || 'pending' in cover)
|
|
812
900
|
return cover;
|
|
813
|
-
return
|
|
814
|
-
|
|
815
|
-
|
|
901
|
+
return {
|
|
902
|
+
pending: Object.entries(seed.at)
|
|
903
|
+
.filter(([room, version]) => !seedRoomHeld(room, version))
|
|
904
|
+
.map(([room]) => ({ room, awaits: 'acquisition' })),
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
function seed(entry) {
|
|
908
|
+
if (seedHeld(entry.at))
|
|
909
|
+
return;
|
|
910
|
+
for (const { query, rows } of entry.queries) {
|
|
911
|
+
const key = queryKey(query);
|
|
912
|
+
seeds.set(key, { rows, at: entry.at });
|
|
913
|
+
for (const shown of seedWatchers.get(key) ?? [])
|
|
914
|
+
shown();
|
|
915
|
+
}
|
|
816
916
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
917
|
+
function sameCover(left, right) {
|
|
918
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
919
|
+
}
|
|
920
|
+
async function coveredRows(plan, cover) {
|
|
820
921
|
for (;;) {
|
|
821
|
-
const
|
|
822
|
-
if (
|
|
823
|
-
return;
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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
|
+
}
|
|
828
962
|
}
|
|
963
|
+
return null;
|
|
829
964
|
}
|
|
830
|
-
function
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
: undefined;
|
|
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
|
+
};
|
|
837
971
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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
|
+
}
|
|
986
|
+
async function rowsQuery(plan) {
|
|
987
|
+
const before = covered(plan);
|
|
988
|
+
awaitPreparation(before);
|
|
989
|
+
const seeded = seedApplies(plan);
|
|
990
|
+
if (seeded !== null)
|
|
991
|
+
return seededResult(seeded).result;
|
|
992
|
+
if ('pending' in before)
|
|
993
|
+
return before;
|
|
994
|
+
const answered = await coveredRows(plan, () => {
|
|
995
|
+
const cover = covered(plan);
|
|
996
|
+
awaitPreparation(cover);
|
|
997
|
+
return cover;
|
|
998
|
+
});
|
|
999
|
+
if ('pending' in answered)
|
|
1000
|
+
return answered.pending;
|
|
1001
|
+
const held = answered.cover;
|
|
1002
|
+
return { ...answered.rows, coverage: held.coverage };
|
|
864
1003
|
}
|
|
865
1004
|
function publishChangeSet(set) {
|
|
866
1005
|
applyChangeSet(set);
|
|
@@ -870,7 +1009,10 @@ export function composeRuntime(options) {
|
|
|
870
1009
|
for (const locator of set.queues)
|
|
871
1010
|
emitQueue(locator);
|
|
872
1011
|
if (set.drafts.length > 0 ||
|
|
873
|
-
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
|
+
})) {
|
|
874
1016
|
publishRoomDrafts(false);
|
|
875
1017
|
}
|
|
876
1018
|
for (const app of set.presentations)
|
|
@@ -883,9 +1025,22 @@ export function composeRuntime(options) {
|
|
|
883
1025
|
if (reason === 'revoked-authorization') {
|
|
884
1026
|
await exclusive(async () => {
|
|
885
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;
|
|
886
1034
|
try {
|
|
887
1035
|
const local = await openLocal();
|
|
888
|
-
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
|
+
});
|
|
889
1044
|
}
|
|
890
1045
|
catch {
|
|
891
1046
|
journal.record('model revocation persistence failed');
|
|
@@ -901,16 +1056,16 @@ export function composeRuntime(options) {
|
|
|
901
1056
|
liveModels.delete(key);
|
|
902
1057
|
}
|
|
903
1058
|
const changed = [];
|
|
904
|
-
for (const
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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;
|
|
914
1069
|
rooms.set(locator, {
|
|
915
1070
|
...room,
|
|
916
1071
|
model: modelTypes(revoked),
|
|
@@ -918,9 +1073,10 @@ export function composeRuntime(options) {
|
|
|
918
1073
|
? null
|
|
919
1074
|
: { ...room.typedSelection, model: modelTypes(revoked) },
|
|
920
1075
|
});
|
|
921
|
-
changed.push(changeSet(locator,
|
|
1076
|
+
changed.push(changeSet(locator, { documents: null, room: true }));
|
|
922
1077
|
}
|
|
923
1078
|
publishChangeSet(mergeChangeSets(changed));
|
|
1079
|
+
prepare();
|
|
924
1080
|
});
|
|
925
1081
|
}
|
|
926
1082
|
}
|
|
@@ -951,41 +1107,57 @@ export function composeRuntime(options) {
|
|
|
951
1107
|
model: modelTypes(modelOf(room.locator, 'typed-selection')),
|
|
952
1108
|
},
|
|
953
1109
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
954
|
-
roomVersion: replicas.get(room.locator)?.
|
|
1110
|
+
roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
|
|
955
1111
|
synchronizationAvailable,
|
|
956
1112
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
957
1113
|
});
|
|
958
1114
|
}
|
|
959
|
-
async function applyDiscovery(found, started) {
|
|
1115
|
+
async function applyDiscovery(found, started, covered) {
|
|
1116
|
+
const lockedBefore = lockedRooms();
|
|
960
1117
|
for (const room of found) {
|
|
961
1118
|
const fence = fences.get(room.locator);
|
|
962
1119
|
if (fence !== undefined && fence.erased && fence.at < started)
|
|
963
1120
|
fences.delete(room.locator);
|
|
964
1121
|
}
|
|
1122
|
+
await loseRooms(found
|
|
1123
|
+
.filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
|
|
1124
|
+
.map((room) => room.locator));
|
|
965
1125
|
const listed = found.filter((room) => !fences.has(room.locator));
|
|
966
1126
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
967
1127
|
for (const locator of listedLocators)
|
|
968
1128
|
scopedRefusals.delete(locator);
|
|
969
|
-
|
|
1129
|
+
if (covered === undefined)
|
|
1130
|
+
discovered.clear();
|
|
1131
|
+
else
|
|
1132
|
+
for (const locator of covered)
|
|
1133
|
+
discovered.delete(locator);
|
|
970
1134
|
for (const room of listed)
|
|
971
1135
|
discovered.set(room.locator, room);
|
|
972
|
-
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));
|
|
973
1137
|
if (options.adapter.exhaustiveDiscovery === true)
|
|
974
1138
|
await loseRooms(unlisted);
|
|
975
1139
|
else
|
|
976
1140
|
unlistedKept(unlisted);
|
|
977
1141
|
for (const room of listed) {
|
|
978
|
-
const held = replicas.get(room.locator)?.
|
|
1142
|
+
const held = replicas.get(room.locator)?.record;
|
|
979
1143
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
980
1144
|
list(held, false);
|
|
981
1145
|
continue;
|
|
982
1146
|
}
|
|
983
1147
|
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
984
1148
|
}
|
|
1149
|
+
confirmLocks(listed);
|
|
985
1150
|
roomsListed = true;
|
|
986
1151
|
emitRooms();
|
|
1152
|
+
relocked(lockedBefore);
|
|
987
1153
|
return listed;
|
|
988
1154
|
}
|
|
1155
|
+
function heldLocally(locator) {
|
|
1156
|
+
return (replicas.has(locator) ||
|
|
1157
|
+
roomDrafts.has(locator) ||
|
|
1158
|
+
transfers.has(locator) ||
|
|
1159
|
+
queuesWithoutReplica.has(locator));
|
|
1160
|
+
}
|
|
989
1161
|
function unlistedHeld(listed) {
|
|
990
1162
|
return [
|
|
991
1163
|
...new Set([
|
|
@@ -998,7 +1170,7 @@ export function composeRuntime(options) {
|
|
|
998
1170
|
}
|
|
999
1171
|
function unlistedKept(unlisted) {
|
|
1000
1172
|
for (const locator of unlisted) {
|
|
1001
|
-
const held = replicas.get(locator)?.
|
|
1173
|
+
const held = replicas.get(locator)?.record;
|
|
1002
1174
|
if (held !== undefined)
|
|
1003
1175
|
list(held, false);
|
|
1004
1176
|
else
|
|
@@ -1007,16 +1179,16 @@ export function composeRuntime(options) {
|
|
|
1007
1179
|
}
|
|
1008
1180
|
function fence(locators) {
|
|
1009
1181
|
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
1010
|
-
const removed = fresh.map((locator) =>
|
|
1011
|
-
|
|
1012
|
-
queue: true,
|
|
1182
|
+
const removed = fresh.map((locator) => ({
|
|
1183
|
+
locator,
|
|
1013
1184
|
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
1014
1185
|
}));
|
|
1015
1186
|
for (const locator of fresh) {
|
|
1016
1187
|
fences.set(locator, { at: discoveries, erased: false });
|
|
1017
1188
|
lastFenced.set(locator, ++fencings);
|
|
1189
|
+
confirmed.delete(locator);
|
|
1018
1190
|
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
1019
|
-
pendings.delete(batch.id);
|
|
1191
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
1020
1192
|
replicas.delete(locator);
|
|
1021
1193
|
queuesWithoutReplica.delete(locator);
|
|
1022
1194
|
rooms.delete(locator);
|
|
@@ -1028,7 +1200,7 @@ export function composeRuntime(options) {
|
|
|
1028
1200
|
unreadable.delete(locator);
|
|
1029
1201
|
stale.delete(locator);
|
|
1030
1202
|
repairable.delete(locator);
|
|
1031
|
-
|
|
1203
|
+
unpreparable.delete(locator);
|
|
1032
1204
|
paused.delete(locator);
|
|
1033
1205
|
scopedRefusals.delete(locator);
|
|
1034
1206
|
sourceRefusals.delete(locator);
|
|
@@ -1043,26 +1215,48 @@ export function composeRuntime(options) {
|
|
|
1043
1215
|
}
|
|
1044
1216
|
if (fresh.length === 0)
|
|
1045
1217
|
return fresh;
|
|
1046
|
-
publishChangeSet(mergeChangeSets(removed));
|
|
1047
|
-
|
|
1048
|
-
prepared.delete(locator);
|
|
1049
|
-
searchIndex.forget(fresh);
|
|
1050
|
-
readSide.recover();
|
|
1218
|
+
publishChangeSet(mergeChangeSets(removed.map(({ locator, drafts }) => changeSet(locator, { documents: null, room: true, queue: true, drafts }))));
|
|
1219
|
+
liveQueries.recover();
|
|
1051
1220
|
moveTo({ ...status, ...attention() });
|
|
1052
1221
|
return fresh;
|
|
1053
1222
|
}
|
|
1054
|
-
async function
|
|
1055
|
-
await local.erase(locators);
|
|
1223
|
+
async function eraseFenced(locators) {
|
|
1056
1224
|
for (const locator of locators) {
|
|
1225
|
+
while (await durably((local) => local.erase(locator, ERASE_BATCH)))
|
|
1226
|
+
await yielded();
|
|
1057
1227
|
const held = fences.get(locator);
|
|
1058
1228
|
if (held !== undefined)
|
|
1059
1229
|
held.erased = true;
|
|
1060
1230
|
}
|
|
1061
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
|
+
}
|
|
1062
1251
|
async function loseRooms(locators) {
|
|
1063
1252
|
const fresh = fence(locators);
|
|
1064
|
-
if (fresh.length
|
|
1065
|
-
|
|
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);
|
|
1066
1260
|
}
|
|
1067
1261
|
function restored(key, persisted) {
|
|
1068
1262
|
const prior = models.get(key);
|
|
@@ -1083,54 +1277,56 @@ export function composeRuntime(options) {
|
|
|
1083
1277
|
return model;
|
|
1084
1278
|
}
|
|
1085
1279
|
async function hydrate(opened) {
|
|
1086
|
-
const [
|
|
1087
|
-
opened.
|
|
1280
|
+
const [held, queues, kept, heldConversations, chatDrafts] = await Promise.all([
|
|
1281
|
+
opened.opened(),
|
|
1088
1282
|
opened.queues(),
|
|
1089
1283
|
opened.roomDrafts(),
|
|
1090
|
-
opened.presentation(),
|
|
1091
1284
|
opened.conversations(),
|
|
1092
1285
|
opened.chatDrafts(),
|
|
1093
|
-
opened.lastCompleted(),
|
|
1094
|
-
opened.providerLimits(),
|
|
1095
|
-
opened.capabilities(),
|
|
1096
|
-
opened.member(),
|
|
1097
|
-
opened.transfers(),
|
|
1098
|
-
opened.targets(),
|
|
1099
1286
|
]);
|
|
1100
|
-
|
|
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;
|
|
1101
1291
|
transfers.clear();
|
|
1102
|
-
for (const record of
|
|
1103
|
-
transfers.set(
|
|
1104
|
-
limits =
|
|
1105
|
-
capabilities =
|
|
1106
|
-
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;
|
|
1107
1297
|
roomDrafts.clear();
|
|
1108
1298
|
for (const record of kept)
|
|
1109
1299
|
rememberRoomDraft(record);
|
|
1110
1300
|
presentations.clear();
|
|
1111
|
-
for (const record of
|
|
1301
|
+
for (const record of held.presentation)
|
|
1112
1302
|
presentations.set(record.app, record.value);
|
|
1113
|
-
conversations.hydrate(
|
|
1114
|
-
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1303
|
+
conversations.hydrate(heldConversations, chatDrafts);
|
|
1115
1304
|
pendings.clear();
|
|
1116
1305
|
for (const queue of queues) {
|
|
1117
|
-
for (const { batch, state } of queue.pending)
|
|
1118
|
-
pendings.set(batch.id, state);
|
|
1306
|
+
for (const { batch, state } of queue.pending) {
|
|
1307
|
+
pendings.set(pendingKey(queue.locator, batch.id), state);
|
|
1308
|
+
}
|
|
1119
1309
|
}
|
|
1120
|
-
const previousReplicas = new Map(replicas);
|
|
1121
1310
|
replicas.clear();
|
|
1122
1311
|
queuesWithoutReplica.clear();
|
|
1123
|
-
const replicated = new Set(records.map((record) => record.locator));
|
|
1124
1312
|
for (const [locator, queue] of queuedBatches) {
|
|
1125
|
-
if (!
|
|
1313
|
+
if (!held.rooms.has(locator))
|
|
1126
1314
|
queuesWithoutReplica.set(locator, queue);
|
|
1127
1315
|
}
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
const
|
|
1132
|
-
const
|
|
1133
|
-
|
|
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
|
+
});
|
|
1134
1330
|
const existing = rooms.get(record.locator);
|
|
1135
1331
|
if (existing === undefined)
|
|
1136
1332
|
list(record, false);
|
|
@@ -1149,11 +1345,15 @@ export function composeRuntime(options) {
|
|
|
1149
1345
|
},
|
|
1150
1346
|
});
|
|
1151
1347
|
}
|
|
1152
|
-
}
|
|
1348
|
+
});
|
|
1349
|
+
for (const key of held.orphans)
|
|
1350
|
+
await opened.release(key);
|
|
1351
|
+
if (held.erasing || held.orphans.length > 0)
|
|
1352
|
+
queueMicrotask(sweep);
|
|
1153
1353
|
return {
|
|
1154
|
-
lastCompleted,
|
|
1354
|
+
lastCompleted: held.lastCompleted,
|
|
1155
1355
|
locators: new Set([...queuedBatches.keys(), ...replicas.keys()]),
|
|
1156
|
-
read: mergeChangeSets([...
|
|
1356
|
+
read: mergeChangeSets([...replicas.keys()].map((locator) => changeSet(locator, { documents: null }))),
|
|
1157
1357
|
};
|
|
1158
1358
|
}
|
|
1159
1359
|
async function readState(opened) {
|
|
@@ -1172,8 +1372,7 @@ export function composeRuntime(options) {
|
|
|
1172
1372
|
async function open() {
|
|
1173
1373
|
if (closed)
|
|
1174
1374
|
throw new Error('runtime closed');
|
|
1175
|
-
const opened =
|
|
1176
|
-
store = opened;
|
|
1375
|
+
const opened = openRuntimeStore(isFenced, options.database);
|
|
1177
1376
|
const interrupted = await opened.fences();
|
|
1178
1377
|
for (const locator of interrupted) {
|
|
1179
1378
|
fences.set(locator, { at: discoveries, erased: false });
|
|
@@ -1192,9 +1391,17 @@ export function composeRuntime(options) {
|
|
|
1192
1391
|
emitStorageLoss();
|
|
1193
1392
|
}
|
|
1194
1393
|
const lastCompleted = await readState(opened);
|
|
1195
|
-
if (interrupted.length > 0)
|
|
1196
|
-
|
|
1394
|
+
if (interrupted.length > 0) {
|
|
1395
|
+
queueMicrotask(() => {
|
|
1396
|
+
void eraseFenced(interrupted).catch(() => {
|
|
1397
|
+
journal.record('erasure failed');
|
|
1398
|
+
});
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1197
1401
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
1402
|
+
void exclusive(() => unpair(opened)).catch(() => {
|
|
1403
|
+
journal.record('unpair failed');
|
|
1404
|
+
});
|
|
1198
1405
|
return opened;
|
|
1199
1406
|
}
|
|
1200
1407
|
function durably(work) {
|
|
@@ -1215,78 +1422,260 @@ export function composeRuntime(options) {
|
|
|
1215
1422
|
mutations = next.then(() => undefined, () => undefined);
|
|
1216
1423
|
return next;
|
|
1217
1424
|
}
|
|
1218
|
-
function withdrawable(
|
|
1219
|
-
|
|
1220
|
-
|
|
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 };
|
|
1221
1511
|
}
|
|
1222
|
-
|
|
1512
|
+
function replayed(record, confirmedLayer, queue) {
|
|
1513
|
+
return stepped(replaying(confirmedLayer, queue, record.locator, compositionOf(record)));
|
|
1514
|
+
}
|
|
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';
|
|
1223
1566
|
const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
1224
|
-
const
|
|
1225
|
-
const
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
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
|
+
}
|
|
1230
1584
|
const returning = [
|
|
1231
|
-
...[...unqueued].reverse(),
|
|
1232
|
-
|
|
1585
|
+
...[...unqueued].reverse().map(({ batch, landed: held }) => ({
|
|
1586
|
+
batch,
|
|
1587
|
+
returned: localSaves || held ? null : returnedBy('withdrawn'),
|
|
1588
|
+
})),
|
|
1589
|
+
...(settling.leaving ?? []),
|
|
1233
1590
|
];
|
|
1234
1591
|
const written = new Map();
|
|
1235
|
-
for (const { batch, returned } of returning) {
|
|
1236
|
-
if (
|
|
1592
|
+
for (const { batch, returned: reason } of returning) {
|
|
1593
|
+
if (reason === null || batch.between?.destination.room === locator)
|
|
1237
1594
|
continue;
|
|
1238
|
-
for (const saved of pendings.get(batch.id)?.drafts ?? []) {
|
|
1595
|
+
for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
|
|
1239
1596
|
const existing = written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path);
|
|
1240
1597
|
written.set(saved.path, {
|
|
1241
1598
|
locator,
|
|
1242
1599
|
path: saved.path,
|
|
1243
1600
|
revision: roomDraftRevision(),
|
|
1244
1601
|
parts: returnedInto(existing?.parts, saved.parts),
|
|
1245
|
-
returned,
|
|
1602
|
+
returned: reason,
|
|
1246
1603
|
});
|
|
1247
1604
|
}
|
|
1248
1605
|
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
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];
|
|
1270
1651
|
queuesWithoutReplica.delete(locator);
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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
|
+
},
|
|
1290
1679
|
};
|
|
1291
1680
|
}
|
|
1292
1681
|
function adoptModels(locator, record) {
|
|
@@ -1307,7 +1696,7 @@ export function composeRuntime(options) {
|
|
|
1307
1696
|
const taken = (answer, previous, attached) => {
|
|
1308
1697
|
if (answer === undefined)
|
|
1309
1698
|
return previous;
|
|
1310
|
-
const { state, refused } = attachedModel(answer, previous);
|
|
1699
|
+
const { state, refused } = attachedModel(answer, previous, attached === 'collection' ? roomTypes : NO_ROOM_TYPES);
|
|
1311
1700
|
if (refused !== null) {
|
|
1312
1701
|
const where = refused.location === undefined ? '' : ` ${refused.location}`;
|
|
1313
1702
|
journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
|
|
@@ -1320,70 +1709,86 @@ export function composeRuntime(options) {
|
|
|
1320
1709
|
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
1321
1710
|
};
|
|
1322
1711
|
}
|
|
1323
|
-
function publish(local, room,
|
|
1324
|
-
const
|
|
1325
|
-
return exclusive(async () => {
|
|
1326
|
-
if (stored !== undefined) {
|
|
1327
|
-
await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
|
|
1328
|
-
}
|
|
1712
|
+
function publish(local, room, record, landing, live) {
|
|
1713
|
+
const landed = exclusive(async () => {
|
|
1329
1714
|
if (!live()) {
|
|
1330
1715
|
caughtUpTo(room.locator);
|
|
1331
1716
|
return true;
|
|
1332
1717
|
}
|
|
1333
1718
|
const prior = replicas.get(room.locator);
|
|
1334
|
-
if (
|
|
1335
|
-
(prior
|
|
1336
|
-
|
|
1719
|
+
if (landing.kind === 'refreshed' &&
|
|
1720
|
+
(prior === undefined || prior.record.key !== record.key || prior.confirmed !== landing.base)) {
|
|
1721
|
+
await local.unrefresh(record.key);
|
|
1337
1722
|
return false;
|
|
1338
1723
|
}
|
|
1339
|
-
adoptModels(room.locator,
|
|
1724
|
+
adoptModels(room.locator, record);
|
|
1340
1725
|
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
1341
|
-
const roomVersion =
|
|
1726
|
+
const roomVersion = record.roomVersion;
|
|
1342
1727
|
const accepted = queue.filter((batch) => {
|
|
1343
|
-
const
|
|
1344
|
-
return
|
|
1728
|
+
const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
|
|
1729
|
+
return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
|
|
1345
1730
|
});
|
|
1346
|
-
let
|
|
1731
|
+
let set;
|
|
1347
1732
|
try {
|
|
1348
|
-
|
|
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
|
+
});
|
|
1349
1748
|
}
|
|
1350
1749
|
catch (error) {
|
|
1351
1750
|
if (!exceeded(error))
|
|
1352
1751
|
throw error;
|
|
1353
|
-
await exceededOn(room.locator,
|
|
1752
|
+
await exceededOn(room.locator, null);
|
|
1354
1753
|
caughtUpTo(room.locator);
|
|
1355
1754
|
return true;
|
|
1356
1755
|
}
|
|
1357
|
-
|
|
1358
|
-
if (stored !== undefined) {
|
|
1756
|
+
if (landing.kind === 'bootstrapped') {
|
|
1359
1757
|
transfers.delete(room.locator);
|
|
1360
1758
|
catching.delete(room.locator);
|
|
1759
|
+
sweep();
|
|
1361
1760
|
}
|
|
1362
1761
|
for (const batch of accepted)
|
|
1363
|
-
pendings.delete(batch.id);
|
|
1762
|
+
pendings.delete(pendingKey(room.locator, batch.id));
|
|
1364
1763
|
stale.delete(room.locator);
|
|
1365
|
-
|
|
1764
|
+
unpreparable.delete(room.locator);
|
|
1366
1765
|
if (unreadable.delete(room.locator))
|
|
1367
1766
|
scopedRefusals.delete(room.locator);
|
|
1368
1767
|
sourceRefusals.delete(room.locator);
|
|
1369
1768
|
declarationRefusals.delete(room.locator);
|
|
1370
1769
|
list(room, true);
|
|
1371
|
-
if (prior === undefined)
|
|
1372
|
-
prepared.add(room.locator);
|
|
1373
1770
|
publishChangeSet(set);
|
|
1771
|
+
await unpair(local);
|
|
1374
1772
|
moveTo({ ...status, ...attention() });
|
|
1375
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
|
+
}
|
|
1376
1777
|
return true;
|
|
1377
1778
|
});
|
|
1779
|
+
return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
|
|
1378
1780
|
}
|
|
1379
|
-
async function
|
|
1781
|
+
async function savedRoom(locator) {
|
|
1380
1782
|
const local = await openLocal();
|
|
1381
|
-
await referenced(locator);
|
|
1382
1783
|
const replica = replicas.get(locator);
|
|
1383
1784
|
const room = rooms.get(locator);
|
|
1384
1785
|
if (replica === undefined || room === undefined) {
|
|
1385
1786
|
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1386
1787
|
}
|
|
1788
|
+
return { local, replica, room };
|
|
1789
|
+
}
|
|
1790
|
+
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
1791
|
+
const target = await savedRoom(locator);
|
|
1387
1792
|
const saved = [];
|
|
1388
1793
|
for (const { path, revision } of named) {
|
|
1389
1794
|
const record = roomDrafts.get(locator)?.get(path);
|
|
@@ -1393,12 +1798,396 @@ export function composeRuntime(options) {
|
|
|
1393
1798
|
if (!saved.some((draft) => draft.path === path))
|
|
1394
1799
|
saved.push({ path, parts: record.parts });
|
|
1395
1800
|
}
|
|
1396
|
-
|
|
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)));
|
|
2065
|
+
if (changed.length > 0)
|
|
2066
|
+
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
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)));
|
|
1397
2188
|
if (changed.length > 0)
|
|
1398
2189
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
1399
|
-
const
|
|
1400
|
-
const changes = parts.flatMap(({ changes }) => changes);
|
|
1401
|
-
const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
|
|
2190
|
+
const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
|
|
1402
2191
|
if (requeue.kind === 'refused')
|
|
1403
2192
|
return { outcome: 'refused', ...requeue.refusal };
|
|
1404
2193
|
const foreign = [...requeue.withdrawn.keys()].some((origin) => {
|
|
@@ -1416,23 +2205,23 @@ export function composeRuntime(options) {
|
|
|
1416
2205
|
const withdrawing = requeue.withdrawn.size > 0;
|
|
1417
2206
|
const input = {
|
|
1418
2207
|
local: localSaves,
|
|
1419
|
-
baseRoomVersion: replica.
|
|
1420
|
-
composition: compositionFor(replica.
|
|
2208
|
+
baseRoomVersion: replica.record.roomVersion,
|
|
2209
|
+
composition: compositionFor(replica.record),
|
|
1421
2210
|
operations: room.operations,
|
|
1422
2211
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
1423
2212
|
providerLimits: limits,
|
|
1424
2213
|
handle: room.handle,
|
|
1425
|
-
visible:
|
|
2214
|
+
visible: saving.visible,
|
|
1426
2215
|
changes,
|
|
1427
2216
|
bases: parts.flatMap(({ bases }) => bases),
|
|
2217
|
+
queued: queuedDocuments(replica.queue),
|
|
1428
2218
|
writtenBy,
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
binaries: replica.confirmed.record.binaries,
|
|
1432
|
-
composition: compositionOf(replica.confirmed.record),
|
|
1433
|
-
},
|
|
2219
|
+
unmodeled: await unmodeledIn(replica.record),
|
|
2220
|
+
validate: validatorOf(replica.record, saving.confirmed),
|
|
1434
2221
|
};
|
|
1435
|
-
const checked = withdrawing
|
|
2222
|
+
const checked = withdrawing
|
|
2223
|
+
? await buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
|
|
2224
|
+
: null;
|
|
1436
2225
|
if (checked?.kind === 'refused')
|
|
1437
2226
|
return { outcome: 'refused', ...checked.refusal };
|
|
1438
2227
|
const owners = [...requeue.owners].flatMap((origin) => {
|
|
@@ -1448,13 +2237,14 @@ export function composeRuntime(options) {
|
|
|
1448
2237
|
};
|
|
1449
2238
|
}
|
|
1450
2239
|
const built = !withdrawing
|
|
1451
|
-
? buildBatch(input)
|
|
2240
|
+
? await buildBatch(input)
|
|
1452
2241
|
: requeue.changes.length === 0
|
|
1453
2242
|
? null
|
|
1454
|
-
: buildBatch({
|
|
2243
|
+
: await buildBatch({
|
|
1455
2244
|
...input,
|
|
1456
|
-
visible:
|
|
2245
|
+
visible: await stepped(saving.visibleOver(requeue.queue)),
|
|
1457
2246
|
changes: requeue.changes,
|
|
2247
|
+
queued: queuedDocuments(requeue.queue),
|
|
1458
2248
|
});
|
|
1459
2249
|
if (built?.kind === 'refused')
|
|
1460
2250
|
return { outcome: 'refused', ...built.refusal };
|
|
@@ -1471,7 +2261,7 @@ export function composeRuntime(options) {
|
|
|
1471
2261
|
for (const [origin, replaced] of withdrawn) {
|
|
1472
2262
|
if (replaced === null)
|
|
1473
2263
|
continue;
|
|
1474
|
-
const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
|
|
2264
|
+
const carried = withdrawnDrafts(pendings.get(pendingKey(locator, origin))?.drafts ?? [], changes);
|
|
1475
2265
|
drafted.set(replaced.id, replaced === join?.batch
|
|
1476
2266
|
? [
|
|
1477
2267
|
...carried.map((held) => joinedDraft(held, saved)),
|
|
@@ -1485,59 +2275,77 @@ export function composeRuntime(options) {
|
|
|
1485
2275
|
for (const replaced of withdrawn.values()) {
|
|
1486
2276
|
if (replaced === null || !batches.includes(replaced))
|
|
1487
2277
|
continue;
|
|
1488
|
-
const limited =
|
|
2278
|
+
const limited = await stepped(limiting(batches, replaced, limits, saving.visibleOver));
|
|
1489
2279
|
if (limited !== null)
|
|
1490
2280
|
return { outcome: 'refused', ...limited };
|
|
1491
2281
|
}
|
|
1492
|
-
const
|
|
1493
|
-
|
|
1494
|
-
|
|
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
|
+
{
|
|
1495
2287
|
locator,
|
|
2288
|
+
saving,
|
|
2289
|
+
before: replica,
|
|
1496
2290
|
queue: batches,
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
2291
|
+
write: {
|
|
2292
|
+
locator,
|
|
2293
|
+
queue: batches,
|
|
2294
|
+
saved: drafted,
|
|
2295
|
+
consumedRoomDrafts: left,
|
|
2296
|
+
roomDrafts: arrived,
|
|
2297
|
+
},
|
|
2298
|
+
},
|
|
2299
|
+
]);
|
|
2300
|
+
if (done === null)
|
|
1502
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}`);
|
|
1503
2305
|
}
|
|
1504
2306
|
if (fences.has(locator))
|
|
1505
2307
|
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1506
2308
|
for (const origin of withdrawn.keys())
|
|
1507
|
-
pendings.delete(origin);
|
|
2309
|
+
pendings.delete(pendingKey(locator, origin));
|
|
1508
2310
|
for (const [id, drafts] of drafted) {
|
|
1509
|
-
pendings.set(
|
|
2311
|
+
pendings.set(pendingKey(locator, id), {
|
|
2312
|
+
drafts,
|
|
2313
|
+
submission: { state: 'waiting' },
|
|
2314
|
+
lost: false,
|
|
2315
|
+
});
|
|
1510
2316
|
}
|
|
1511
|
-
|
|
1512
|
-
recorded = false;
|
|
1513
|
-
for (const path of consumed)
|
|
2317
|
+
for (const path of left)
|
|
1514
2318
|
forgetRoomDraft(locator, path);
|
|
1515
|
-
|
|
1516
|
-
|
|
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] ?? [],
|
|
1517
2324
|
queue: true,
|
|
1518
|
-
drafts:
|
|
2325
|
+
drafts: [...left, ...arrived.map(({ path }) => path)],
|
|
1519
2326
|
}));
|
|
1520
|
-
|
|
1521
|
-
submitLater(locator);
|
|
2327
|
+
queuedSave(locator);
|
|
1522
2328
|
const identifier = join?.batch.id ??
|
|
1523
2329
|
built?.batch.id ??
|
|
1524
2330
|
[...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
|
|
1525
2331
|
return { outcome: 'saved', identifier };
|
|
1526
2332
|
}
|
|
1527
|
-
async function mark(local, locator,
|
|
1528
|
-
const
|
|
1529
|
-
|
|
1530
|
-
state
|
|
1531
|
-
(
|
|
1532
|
-
|
|
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 }));
|
|
1533
2348
|
}
|
|
1534
|
-
await local.submit(locator, identifier, submission, lost);
|
|
1535
|
-
if (fences.has(locator))
|
|
1536
|
-
return;
|
|
1537
|
-
pendings.set(identifier, { ...state, submission, lost });
|
|
1538
|
-
const replica = replicas.get(locator);
|
|
1539
|
-
if (replica !== undefined)
|
|
1540
|
-
publishChangeSet(changeSet(locator, replica, replica, { queue: true }));
|
|
1541
2349
|
moveTo({ ...status, ...attention() });
|
|
1542
2350
|
}
|
|
1543
2351
|
async function keepUpload(local, locator, identifier, upload) {
|
|
@@ -1546,7 +2354,8 @@ export function composeRuntime(options) {
|
|
|
1546
2354
|
await local.keepUpload(locator, identifier, upload);
|
|
1547
2355
|
if (fences.has(locator))
|
|
1548
2356
|
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1549
|
-
|
|
2357
|
+
const key = pendingKey(locator, identifier);
|
|
2358
|
+
pendings.set(key, { ...pendings.get(key), upload });
|
|
1550
2359
|
if (!continuing())
|
|
1551
2360
|
throw new Error('connection refused');
|
|
1552
2361
|
}
|
|
@@ -1579,7 +2388,7 @@ export function composeRuntime(options) {
|
|
|
1579
2388
|
if (fencedSince(locator, since))
|
|
1580
2389
|
return failure({ kind: 'room-not-readable', locator });
|
|
1581
2390
|
if (result.kind === 'scoped-refusal') {
|
|
1582
|
-
const room = replicas.get(result.locator)?.
|
|
2391
|
+
const room = replicas.get(result.locator)?.record;
|
|
1583
2392
|
if (room !== undefined)
|
|
1584
2393
|
await scoped(result, room, await openLocal());
|
|
1585
2394
|
}
|
|
@@ -1592,21 +2401,62 @@ export function composeRuntime(options) {
|
|
|
1592
2401
|
return UNCERTAIN;
|
|
1593
2402
|
}
|
|
1594
2403
|
}
|
|
1595
|
-
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) {
|
|
1596
2440
|
for (const batch of replicas.get(locator)?.queue ?? []) {
|
|
1597
2441
|
const change = batch.changes[queuedFile(batch, path, localId)];
|
|
1598
2442
|
if (change?.kind === 'binary')
|
|
1599
|
-
return change
|
|
2443
|
+
return change;
|
|
1600
2444
|
}
|
|
1601
|
-
|
|
2445
|
+
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2446
|
+
return drafted?.localId === localId ? drafted : null;
|
|
2447
|
+
}
|
|
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
|
+
};
|
|
1602
2453
|
}
|
|
1603
2454
|
function binaryRoom(request) {
|
|
1604
2455
|
if (request.kind === 'path')
|
|
1605
2456
|
return request.locator;
|
|
1606
|
-
|
|
1607
|
-
return scope?.kind === 'room' ? scope.locator : null;
|
|
2457
|
+
return options.adapter.referrerScope(request.referrer)?.locator ?? null;
|
|
1608
2458
|
}
|
|
1609
|
-
async function
|
|
2459
|
+
async function transferBinary(request, transfer, answer) {
|
|
1610
2460
|
if (closed || refusal !== null)
|
|
1611
2461
|
return UNCERTAIN;
|
|
1612
2462
|
try {
|
|
@@ -1622,69 +2472,116 @@ export function composeRuntime(options) {
|
|
|
1622
2472
|
? `transfer binary ${request.locator} ${request.path}`
|
|
1623
2473
|
: `transfer binary ${request.fileRef}`);
|
|
1624
2474
|
try {
|
|
1625
|
-
const result = await
|
|
1626
|
-
|
|
2475
|
+
const result = await transfer(authorization);
|
|
2476
|
+
const settled = result;
|
|
2477
|
+
journal.record(`outcome binary ${settled.kind}`);
|
|
1627
2478
|
const locator = binaryRoom(request);
|
|
1628
2479
|
if (locator !== null && fencedSince(locator, since))
|
|
1629
2480
|
return failure({ kind: 'room-not-readable', locator });
|
|
1630
|
-
if (
|
|
1631
|
-
const room = replicas.get(
|
|
2481
|
+
if (settled.kind === 'scoped-refusal') {
|
|
2482
|
+
const room = replicas.get(settled.locator)?.record;
|
|
1632
2483
|
if (room !== undefined)
|
|
1633
|
-
await scoped(
|
|
2484
|
+
await scoped(settled, room, await openLocal());
|
|
1634
2485
|
}
|
|
1635
|
-
if (
|
|
2486
|
+
if (settled.kind === 'revoked-authorization')
|
|
1636
2487
|
await refuse('revoked-authorization');
|
|
1637
|
-
return
|
|
2488
|
+
return answer(result);
|
|
1638
2489
|
}
|
|
1639
2490
|
catch {
|
|
1640
2491
|
journal.record('outcome binary transport-uncertainty');
|
|
1641
2492
|
return UNCERTAIN;
|
|
1642
2493
|
}
|
|
1643
2494
|
}
|
|
1644
|
-
async function
|
|
1645
|
-
|
|
2495
|
+
async function readBinary(request, name) {
|
|
2496
|
+
if (closed)
|
|
2497
|
+
return UNCERTAIN;
|
|
2498
|
+
const held = await keptFiles.find(name);
|
|
2499
|
+
if (held !== null)
|
|
2500
|
+
return held;
|
|
2501
|
+
return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
|
|
2502
|
+
}
|
|
2503
|
+
async function binaryPath(locator, path, revision) {
|
|
1646
2504
|
if (revision !== undefined)
|
|
1647
|
-
return
|
|
1648
|
-
const visible = replicas.get(locator)?.visible;
|
|
1649
|
-
const held = visible?.binaries[path];
|
|
1650
|
-
if (visible === undefined || held === undefined)
|
|
1651
|
-
return absentBinary(path);
|
|
1652
|
-
if (held.state === 'pending') {
|
|
1653
|
-
const blob = queuedBlob(locator, path, held.localId);
|
|
1654
|
-
return blob === null ? absentBinary(path) : { kind: 'binary', blob };
|
|
1655
|
-
}
|
|
1656
|
-
const record = binaryRecord(path, held, visible.revisions[path]);
|
|
1657
|
-
if (record.state !== 'accepted')
|
|
1658
|
-
return absentBinary(path);
|
|
2505
|
+
return { kind: 'path', locator, path, revision };
|
|
1659
2506
|
const replica = replicas.get(locator);
|
|
1660
|
-
|
|
2507
|
+
if (replica === undefined)
|
|
2508
|
+
return null;
|
|
2509
|
+
const local = await openLocal();
|
|
2510
|
+
const [held] = await local.entries(replica.record.key, 'visible', [path]);
|
|
2511
|
+
if (held?.binary === undefined || held.binary === null)
|
|
2512
|
+
return null;
|
|
2513
|
+
if (held.binary.state === 'pending')
|
|
2514
|
+
return { kind: 'pending', localId: held.binary.localId };
|
|
2515
|
+
const record = binaryRecord(path, held.binary, held.revision ?? undefined);
|
|
2516
|
+
if (record.state !== 'accepted')
|
|
2517
|
+
return null;
|
|
2518
|
+
const queued = replica.queue.flatMap((batch) => batch.changes);
|
|
2519
|
+
const sources = queued.flatMap((change) => (change.kind === 'move' ? [change.path] : []));
|
|
2520
|
+
const confirmedLayer = await stepped(slicing(await local.entries(replica.record.key, 'confirmed', [path, ...sources]), [
|
|
2521
|
+
path,
|
|
2522
|
+
...sources,
|
|
2523
|
+
]));
|
|
1661
2524
|
const holds = (at) => {
|
|
1662
|
-
const kept =
|
|
2525
|
+
const kept = confirmedLayer.binaries[at];
|
|
1663
2526
|
return (kept?.state === 'accepted' &&
|
|
1664
2527
|
kept.fileRef === record.fileRef &&
|
|
1665
|
-
|
|
2528
|
+
confirmedLayer.revisions[at] === record.revision);
|
|
1666
2529
|
};
|
|
1667
|
-
let
|
|
1668
|
-
for (let index = queued.length - 1; index >= 0 && !holds(
|
|
2530
|
+
let confirmedAt = path;
|
|
2531
|
+
for (let index = queued.length - 1; index >= 0 && !holds(confirmedAt); index -= 1) {
|
|
1669
2532
|
const change = queued[index];
|
|
1670
|
-
if (change.kind === 'move' && change.to ===
|
|
1671
|
-
|
|
2533
|
+
if (change.kind === 'move' && change.to === confirmedAt)
|
|
2534
|
+
confirmedAt = change.path;
|
|
1672
2535
|
}
|
|
1673
|
-
return
|
|
2536
|
+
return {
|
|
1674
2537
|
kind: 'path',
|
|
1675
2538
|
locator,
|
|
1676
|
-
path:
|
|
2539
|
+
path: confirmedAt,
|
|
1677
2540
|
revision: record.revision,
|
|
1678
2541
|
fileRef: record.fileRef,
|
|
1679
|
-
}
|
|
2542
|
+
};
|
|
2543
|
+
}
|
|
2544
|
+
async function readBinaryPath(locator, path, revision) {
|
|
2545
|
+
await ready;
|
|
2546
|
+
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2547
|
+
const asked = drafted !== undefined && drafted.localId === revision
|
|
2548
|
+
? { kind: 'pending', localId: revision }
|
|
2549
|
+
: await binaryPath(locator, path, revision);
|
|
2550
|
+
if (asked === null)
|
|
2551
|
+
return absentBinary(path);
|
|
2552
|
+
if (asked.kind === 'pending') {
|
|
2553
|
+
const facts = queuedFacts(locator, path, asked.localId);
|
|
2554
|
+
return facts === null
|
|
2555
|
+
? absentBinary(path)
|
|
2556
|
+
: {
|
|
2557
|
+
kind: 'binary',
|
|
2558
|
+
file: asked.localId,
|
|
2559
|
+
byteLength: facts.byteLength,
|
|
2560
|
+
mediaType: facts.mediaType,
|
|
2561
|
+
};
|
|
2562
|
+
}
|
|
2563
|
+
return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
|
|
2564
|
+
}
|
|
2565
|
+
async function linkBinaryPath(locator, path, revision) {
|
|
2566
|
+
await ready;
|
|
2567
|
+
const asked = await binaryPath(locator, path, revision);
|
|
2568
|
+
if (asked === null || asked.kind === 'pending')
|
|
2569
|
+
return absentBinary(path);
|
|
2570
|
+
return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
|
|
1680
2571
|
}
|
|
1681
2572
|
async function readBinaryReference(reference) {
|
|
1682
2573
|
await ready;
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
2574
|
+
const scope = options.adapter.referrerScope(reference.referrer);
|
|
2575
|
+
if (scope === null)
|
|
2576
|
+
return absentReference(reference.fileRef);
|
|
2577
|
+
return readBinary({ kind: 'reference', fileRef: reference.fileRef, referrer: reference.referrer }, { locator: scope.locator, path: scope.path ?? '', revision: reference.fileRef });
|
|
2578
|
+
}
|
|
2579
|
+
function readPart(file, part) {
|
|
2580
|
+
return options.database.ask({ kind: 'file.part', key: file, part });
|
|
2581
|
+
}
|
|
2582
|
+
async function cleanUp() {
|
|
2583
|
+
const freed = await options.database.ask({ kind: 'opened.clean' });
|
|
2584
|
+
return freed + ((await options.shell.cleanUp?.()) ?? 0);
|
|
1688
2585
|
}
|
|
1689
2586
|
function declaredAgents() {
|
|
1690
2587
|
for (const capability of capabilities) {
|
|
@@ -1702,7 +2599,14 @@ export function composeRuntime(options) {
|
|
|
1702
2599
|
}
|
|
1703
2600
|
return undefined;
|
|
1704
2601
|
}
|
|
1705
|
-
|
|
2602
|
+
function calledRooms(name, input) {
|
|
2603
|
+
const member = declaredAction(name)?.room;
|
|
2604
|
+
if (member === undefined || input === null || typeof input !== 'object')
|
|
2605
|
+
return [];
|
|
2606
|
+
const named = input[member];
|
|
2607
|
+
return (Array.isArray(named) ? named : [named]).filter((room) => typeof room === 'string');
|
|
2608
|
+
}
|
|
2609
|
+
async function invokeAction(name, input, dispatching) {
|
|
1706
2610
|
await ready;
|
|
1707
2611
|
if (closed || refusal !== null)
|
|
1708
2612
|
return UNCERTAIN;
|
|
@@ -1716,11 +2620,12 @@ export function composeRuntime(options) {
|
|
|
1716
2620
|
}
|
|
1717
2621
|
if (closed || authorization === undefined || refusal !== null)
|
|
1718
2622
|
return UNCERTAIN;
|
|
2623
|
+
await dispatching?.();
|
|
1719
2624
|
journal.record(`transfer action ${name}`);
|
|
1720
2625
|
try {
|
|
1721
2626
|
const result = await options.adapter.action(authorization, { name, input });
|
|
1722
2627
|
if (result.kind === 'scoped-refusal') {
|
|
1723
|
-
const room = replicas.get(result.locator)?.
|
|
2628
|
+
const room = replicas.get(result.locator)?.record;
|
|
1724
2629
|
if (room !== undefined)
|
|
1725
2630
|
await scoped(result, room, await openLocal());
|
|
1726
2631
|
}
|
|
@@ -1736,107 +2641,521 @@ export function composeRuntime(options) {
|
|
|
1736
2641
|
return UNCERTAIN;
|
|
1737
2642
|
}
|
|
1738
2643
|
}
|
|
2644
|
+
function unsettled(result) {
|
|
2645
|
+
return result.kind !== 'action' && result.retryable;
|
|
2646
|
+
}
|
|
2647
|
+
async function recoveredAction(name, input) {
|
|
2648
|
+
const operation = operationOf(input);
|
|
2649
|
+
if (operation === null)
|
|
2650
|
+
return invokeAction(name, input);
|
|
2651
|
+
const kept = async () => (await openLocal()).keepCall({ operation, name, input });
|
|
2652
|
+
calling.add(operation);
|
|
2653
|
+
try {
|
|
2654
|
+
let result = await invokeAction(name, input, kept);
|
|
2655
|
+
if (unsettled(result)) {
|
|
2656
|
+
await recovery.attempt(`action ${operation}`, async () => {
|
|
2657
|
+
if (closed || refusal !== null)
|
|
2658
|
+
return 'done';
|
|
2659
|
+
if (!socketOpen)
|
|
2660
|
+
return 'again';
|
|
2661
|
+
result = await invokeAction(name, input, kept);
|
|
2662
|
+
return unsettled(result) ? 'again' : 'done';
|
|
2663
|
+
}, { again: true });
|
|
2664
|
+
}
|
|
2665
|
+
if (!unsettled(result))
|
|
2666
|
+
await (await openLocal()).dropCall(operation);
|
|
2667
|
+
return result;
|
|
2668
|
+
}
|
|
2669
|
+
finally {
|
|
2670
|
+
calling.delete(operation);
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
async function resendCalls(signal) {
|
|
2674
|
+
if (!socketOpen || closed || stopping)
|
|
2675
|
+
return 'done';
|
|
2676
|
+
const local = await openLocal();
|
|
2677
|
+
for (const held of await local.calls()) {
|
|
2678
|
+
if (calling.has(held.operation))
|
|
2679
|
+
continue;
|
|
2680
|
+
if (closed || signal.aborted)
|
|
2681
|
+
return 'done';
|
|
2682
|
+
if (pastHorizon(held.operation)) {
|
|
2683
|
+
await local.dropCall(held.operation);
|
|
2684
|
+
continue;
|
|
2685
|
+
}
|
|
2686
|
+
calling.add(held.operation);
|
|
2687
|
+
try {
|
|
2688
|
+
if (unsettled(await invokeAction(held.name, held.input)))
|
|
2689
|
+
return 'again';
|
|
2690
|
+
await local.dropCall(held.operation);
|
|
2691
|
+
}
|
|
2692
|
+
finally {
|
|
2693
|
+
calling.delete(held.operation);
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
return 'done';
|
|
2697
|
+
}
|
|
1739
2698
|
function continuing() {
|
|
1740
2699
|
return !closed && !discoveryPaused;
|
|
1741
2700
|
}
|
|
1742
2701
|
function reachedBy(locator, roomVersion) {
|
|
1743
2702
|
const held = replicas.get(locator);
|
|
1744
|
-
return (held !== undefined &&
|
|
1745
|
-
|
|
2703
|
+
return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
|
|
2704
|
+
}
|
|
2705
|
+
async function writtenAfter(locator, roomVersion, path) {
|
|
2706
|
+
const held = replicas.get(locator);
|
|
2707
|
+
if (held === undefined)
|
|
2708
|
+
return false;
|
|
2709
|
+
const [entry] = await (await openLocal()).entries(held.record.key, 'confirmed', [path]);
|
|
2710
|
+
const written = entry?.written ?? undefined;
|
|
2711
|
+
return (written !== undefined &&
|
|
2712
|
+
options.adapter.reached(locator, written, roomVersion) &&
|
|
2713
|
+
!options.adapter.reached(locator, roomVersion, written));
|
|
2714
|
+
}
|
|
2715
|
+
function overriddenTexts(texts, overrides) {
|
|
2716
|
+
return async (paths) => {
|
|
2717
|
+
const known = new Map(await texts(paths.filter((path) => !overrides.has(path))));
|
|
2718
|
+
for (const path of paths) {
|
|
2719
|
+
const entry = overrides.get(path);
|
|
2720
|
+
if (entry === undefined)
|
|
2721
|
+
continue;
|
|
2722
|
+
if (entry === null)
|
|
2723
|
+
known.delete(path);
|
|
2724
|
+
else
|
|
2725
|
+
known.set(path, entry.binary !== null ? null : entry.content);
|
|
2726
|
+
}
|
|
2727
|
+
return known;
|
|
2728
|
+
};
|
|
2729
|
+
}
|
|
2730
|
+
async function unreadableRoom(texts, walk, paths) {
|
|
2731
|
+
if (paths !== null) {
|
|
2732
|
+
const text = {};
|
|
2733
|
+
for (const [path, content] of await texts(paths))
|
|
2734
|
+
if (content !== null)
|
|
2735
|
+
text[path] = content;
|
|
2736
|
+
return stepped(unreadableIn(text, Object.keys(text)));
|
|
2737
|
+
}
|
|
2738
|
+
for (let after = null;;) {
|
|
2739
|
+
const page = await walk(after);
|
|
2740
|
+
const text = {};
|
|
2741
|
+
for (const { path, content } of page.entries)
|
|
2742
|
+
if (content !== null)
|
|
2743
|
+
text[path] = content;
|
|
2744
|
+
const refused = await stepped(unreadableIn(text, Object.keys(text)));
|
|
2745
|
+
if (refused !== null)
|
|
2746
|
+
return refused;
|
|
2747
|
+
if (page.next === null)
|
|
2748
|
+
return null;
|
|
2749
|
+
after = page.next;
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
async function validatedRoom(local, record, view, previous, overrides = null) {
|
|
2753
|
+
const composition = compositionOf(record);
|
|
2754
|
+
const modeled = record.model.status !== 'unavailable' || record.selectionModel.status !== 'unavailable';
|
|
2755
|
+
const lookup = overrides === null
|
|
2756
|
+
? lookupOf(local, record.key, view)
|
|
2757
|
+
: overriddenLookup(lookupOf(local, record.key, view), overrides);
|
|
2758
|
+
const texts = overrides === null
|
|
2759
|
+
? textsOf(local, record.key, view)
|
|
2760
|
+
: overriddenTexts(textsOf(local, record.key, view), overrides);
|
|
2761
|
+
const walk = overrides === null
|
|
2762
|
+
? walkOf(local, record.key, view)
|
|
2763
|
+
: overriddenWalk(walkOf(local, record.key, view), overrides);
|
|
2764
|
+
let changes = null;
|
|
2765
|
+
let reshaped = false;
|
|
2766
|
+
if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
|
|
2767
|
+
const [was, now] = await Promise.all([
|
|
2768
|
+
local.entries(record.key, 'confirmed', previous.changed),
|
|
2769
|
+
overrides === null
|
|
2770
|
+
? local.entries(record.key, view, previous.changed)
|
|
2771
|
+
: Promise.resolve([]),
|
|
2772
|
+
]);
|
|
2773
|
+
const before = new Map(was.map((entry) => [entry.path, entry]));
|
|
2774
|
+
const after = overrides === null
|
|
2775
|
+
? new Map(now.map((entry) => [entry.path, entry]))
|
|
2776
|
+
: overrides;
|
|
2777
|
+
changes = previous.changed.flatMap((path) => {
|
|
2778
|
+
const one = before.get(path) ?? null;
|
|
2779
|
+
const other = after.get(path) ?? null;
|
|
2780
|
+
reshaped ||= textDiffers(one, other);
|
|
2781
|
+
const held = textOf(one);
|
|
2782
|
+
const next = textOf(other);
|
|
2783
|
+
return held === next ? [] : [{ path, was: held, now: next }];
|
|
2784
|
+
});
|
|
2785
|
+
}
|
|
2786
|
+
const named = changes === null ? null : changes.map(({ path }) => path);
|
|
2787
|
+
const textChanged = changes === null || changes.length > 0 || reshaped;
|
|
2788
|
+
const modelsChanged = previous === null || !sameComposition(compositionOf(previous.record), composition);
|
|
2789
|
+
if (modeled && (textChanged || modelsChanged)) {
|
|
2790
|
+
const clean = previous !== null &&
|
|
2791
|
+
changes !== null &&
|
|
2792
|
+
(await changedCleanly(changes, compositionOf(previous.record), composition, {
|
|
2793
|
+
lookup,
|
|
2794
|
+
texts,
|
|
2795
|
+
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2796
|
+
}));
|
|
2797
|
+
if (clean)
|
|
2798
|
+
return null;
|
|
2799
|
+
const verdict = await validatedCollection(walk, lookup, composition);
|
|
2800
|
+
if (verdict.verdict === 'accept')
|
|
2801
|
+
return null;
|
|
2802
|
+
if (verdict.rule !== 'model-unavailable')
|
|
2803
|
+
return { kind: 'refused', refusal: verdict };
|
|
2804
|
+
return unreadableRoom(texts, walk, named);
|
|
2805
|
+
}
|
|
2806
|
+
if (textChanged)
|
|
2807
|
+
return unreadableRoom(texts, walk, named);
|
|
2808
|
+
return null;
|
|
2809
|
+
}
|
|
2810
|
+
function refreshingOf(local, key, liveness, replaying = null) {
|
|
2811
|
+
const write = replaying === null ? exclusive : installWrite;
|
|
2812
|
+
return {
|
|
2813
|
+
async turn() {
|
|
2814
|
+
if (replaying === null)
|
|
2815
|
+
return;
|
|
2816
|
+
installing(replaying, 'replaying');
|
|
2817
|
+
await screened();
|
|
2818
|
+
},
|
|
2819
|
+
prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
|
|
2820
|
+
async stage(page) {
|
|
2821
|
+
for (let written = 0;;) {
|
|
2822
|
+
if (replaying !== null)
|
|
2823
|
+
await screened();
|
|
2824
|
+
const end = batchEnd(page.entries, written);
|
|
2825
|
+
const last = end === page.entries.length;
|
|
2826
|
+
const entries = page.entries.slice(written, end);
|
|
2827
|
+
const staged = await write(async () => {
|
|
2828
|
+
if (!liveness())
|
|
2829
|
+
return false;
|
|
2830
|
+
await timed(() => local.refresh(key, last
|
|
2831
|
+
? { ...page, entries }
|
|
2832
|
+
: { entries, removals: [], folders: [], folderRemovals: [] }));
|
|
2833
|
+
return true;
|
|
2834
|
+
});
|
|
2835
|
+
if (!staged)
|
|
2836
|
+
return false;
|
|
2837
|
+
if (last)
|
|
2838
|
+
return true;
|
|
2839
|
+
written = end;
|
|
2840
|
+
}
|
|
2841
|
+
},
|
|
2842
|
+
};
|
|
2843
|
+
}
|
|
2844
|
+
function batchEnd(entries, from) {
|
|
2845
|
+
let end = from;
|
|
2846
|
+
let spent = 0;
|
|
2847
|
+
while (end < entries.length) {
|
|
2848
|
+
const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
|
|
2849
|
+
if (spent + size > stageBytes && end > from)
|
|
2850
|
+
break;
|
|
2851
|
+
spent += size;
|
|
2852
|
+
end += 1;
|
|
2853
|
+
}
|
|
2854
|
+
return end;
|
|
2855
|
+
}
|
|
2856
|
+
async function timed(work) {
|
|
2857
|
+
const started = performance.now();
|
|
2858
|
+
const done = await work();
|
|
2859
|
+
const spent = performance.now() - started;
|
|
2860
|
+
if (spent < STAGE_AIM_MS / 2)
|
|
2861
|
+
stageBytes = Math.min(STAGE_BYTES, Math.round(stageBytes * 1.5));
|
|
2862
|
+
else if (spent > STAGE_AIM_MS * 2) {
|
|
2863
|
+
stageBytes = Math.max(STAGE_FLOOR, Math.round(stageBytes / 1.5));
|
|
2864
|
+
}
|
|
2865
|
+
return done;
|
|
2866
|
+
}
|
|
2867
|
+
function installWrite(work) {
|
|
2868
|
+
const next = installWrites.then(() => exclusive(work), () => exclusive(work));
|
|
2869
|
+
installWrites = next.then(() => undefined, () => undefined);
|
|
2870
|
+
return next;
|
|
2871
|
+
}
|
|
2872
|
+
function screened() {
|
|
2873
|
+
if (onScreen || closed)
|
|
2874
|
+
return Promise.resolve();
|
|
2875
|
+
queueReadiness();
|
|
2876
|
+
return new Promise((resolve) => shownWaiters.push(resolve));
|
|
2877
|
+
}
|
|
2878
|
+
async function heldPart() {
|
|
2879
|
+
while (partsFree === 0)
|
|
2880
|
+
await new Promise((admit) => partWaiters.push(admit));
|
|
2881
|
+
partsFree -= 1;
|
|
2882
|
+
return freed();
|
|
2883
|
+
}
|
|
2884
|
+
function freed() {
|
|
2885
|
+
let released = false;
|
|
2886
|
+
return () => {
|
|
2887
|
+
if (released)
|
|
2888
|
+
return;
|
|
2889
|
+
released = true;
|
|
2890
|
+
partsFree += 1;
|
|
2891
|
+
partWaiters.shift()?.();
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
function installing(locator, step, share = null) {
|
|
2895
|
+
installs.set(locator, { step, share });
|
|
2896
|
+
queueReadiness();
|
|
1746
2897
|
}
|
|
1747
|
-
function
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
options.adapter.reached(locator, written, roomVersion) &&
|
|
1751
|
-
!options.adapter.reached(locator, roomVersion, written));
|
|
2898
|
+
function installed(locator) {
|
|
2899
|
+
if (installs.delete(locator))
|
|
2900
|
+
queueReadiness();
|
|
1752
2901
|
}
|
|
1753
|
-
|
|
1754
|
-
if (
|
|
2902
|
+
function screen(value) {
|
|
2903
|
+
if (value === onScreen)
|
|
2904
|
+
return;
|
|
2905
|
+
onScreen = value;
|
|
2906
|
+
journal.record(`installation ${value ? 'shown' : 'hidden'}`);
|
|
2907
|
+
queueReadiness();
|
|
2908
|
+
if (!value)
|
|
2909
|
+
return;
|
|
2910
|
+
for (const resolve of shownWaiters.splice(0))
|
|
2911
|
+
resolve();
|
|
2912
|
+
}
|
|
2913
|
+
async function pullRefresh(authorized, held, liveness) {
|
|
2914
|
+
if (!liveness())
|
|
1755
2915
|
return unconfirmed;
|
|
2916
|
+
const turn = generation;
|
|
2917
|
+
const { record } = held;
|
|
2918
|
+
const base = held.confirmed;
|
|
2919
|
+
const local = await openLocal();
|
|
1756
2920
|
try {
|
|
1757
|
-
|
|
1758
|
-
const
|
|
1759
|
-
|
|
1760
|
-
|
|
2921
|
+
await exclusive(() => local.unrefresh(record.key));
|
|
2922
|
+
const result = await advancement(reads, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, liveness));
|
|
2923
|
+
if (result.kind !== 'advanced') {
|
|
2924
|
+
journal.record(`transfer refresh ${record.locator} ${result.kind}`);
|
|
2925
|
+
return result;
|
|
2926
|
+
}
|
|
2927
|
+
const refused = await validatedRoom(local, result.record, 'refreshed', {
|
|
2928
|
+
record,
|
|
2929
|
+
changed: [...result.changed],
|
|
2930
|
+
});
|
|
2931
|
+
if (refused !== null) {
|
|
2932
|
+
journal.record(`transfer refresh ${record.locator} refused`);
|
|
2933
|
+
return refused;
|
|
2934
|
+
}
|
|
2935
|
+
journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
|
|
2936
|
+
return { kind: 'advanced', record: result.record, base };
|
|
1761
2937
|
}
|
|
1762
2938
|
catch (error) {
|
|
1763
|
-
journal.record(`transfer refresh ${
|
|
2939
|
+
journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
|
|
1764
2940
|
throw error;
|
|
1765
2941
|
}
|
|
2942
|
+
finally {
|
|
2943
|
+
reads.finished(record.locator, turn);
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
const heldContents = new Map();
|
|
2947
|
+
function heldContentOf(local, key) {
|
|
2948
|
+
const known = heldContents.get(key);
|
|
2949
|
+
if (known !== undefined)
|
|
2950
|
+
return known;
|
|
2951
|
+
if (heldContents.size > ROOM_READS)
|
|
2952
|
+
heldContents.clear();
|
|
2953
|
+
const made = {
|
|
2954
|
+
files: (after, limit) => local.files(key, 'confirmed', after, limit),
|
|
2955
|
+
async contents(paths) {
|
|
2956
|
+
return new Map((await local.entries(key, 'confirmed', paths)).flatMap(({ path, content, revision }) => content === null || revision === null ? [] : [[path, { content, revision }]]));
|
|
2957
|
+
},
|
|
2958
|
+
};
|
|
2959
|
+
heldContents.set(key, made);
|
|
2960
|
+
return made;
|
|
1766
2961
|
}
|
|
1767
|
-
function staging(local,
|
|
2962
|
+
function staging(local, liveness) {
|
|
1768
2963
|
return {
|
|
1769
2964
|
held: (locator) => transfers.get(locator) ?? null,
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
await local.stage(record, page);
|
|
2965
|
+
async turn(locator, step) {
|
|
2966
|
+
installing(locator, step);
|
|
2967
|
+
for (;;) {
|
|
2968
|
+
await screened();
|
|
2969
|
+
const release = step === 'downloading' ? await heldPart() : () => { };
|
|
2970
|
+
if (onScreen || closed)
|
|
2971
|
+
return release;
|
|
2972
|
+
release();
|
|
1779
2973
|
}
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
2974
|
+
},
|
|
2975
|
+
spare() {
|
|
2976
|
+
if (!onScreen || closed || partsFree === 0)
|
|
2977
|
+
return null;
|
|
2978
|
+
partsFree -= 1;
|
|
2979
|
+
return freed();
|
|
2980
|
+
},
|
|
2981
|
+
arriving(locator) {
|
|
2982
|
+
let ended = false;
|
|
2983
|
+
return {
|
|
2984
|
+
at(token, bytes) {
|
|
2985
|
+
if (ended)
|
|
2986
|
+
return;
|
|
2987
|
+
const held = arrivals.get(locator) ?? new Map();
|
|
2988
|
+
arrivals.set(locator, held.set(token, bytes));
|
|
2989
|
+
const now = performance.now();
|
|
2990
|
+
if (now - arrivalReported < ARRIVAL_REPORT_MS)
|
|
2991
|
+
return;
|
|
2992
|
+
arrivalReported = now;
|
|
2993
|
+
queueReadiness();
|
|
2994
|
+
},
|
|
2995
|
+
end() {
|
|
2996
|
+
ended = true;
|
|
2997
|
+
if (arrivals.delete(locator))
|
|
2998
|
+
queueReadiness();
|
|
2999
|
+
},
|
|
3000
|
+
};
|
|
3001
|
+
},
|
|
3002
|
+
received: (locator, token) => local.transferPart(locator, token),
|
|
3003
|
+
keep: (locator, token, received) => installWrite(async () => {
|
|
3004
|
+
if (liveness())
|
|
3005
|
+
await local.keepPart(locator, token, received);
|
|
1789
3006
|
}),
|
|
3007
|
+
announced(locator) {
|
|
3008
|
+
const later = announced.get(locator);
|
|
3009
|
+
return later === undefined ? [] : [later];
|
|
3010
|
+
},
|
|
3011
|
+
async stage(record, page, composition, received) {
|
|
3012
|
+
const { locator } = record;
|
|
3013
|
+
const bytes = pageBytes(page);
|
|
3014
|
+
const whole = received?.byteLength ?? 0;
|
|
3015
|
+
const from = record.writing?.written ?? 0;
|
|
3016
|
+
const token = record.writing?.token ?? null;
|
|
3017
|
+
const entries = page.entries.length;
|
|
3018
|
+
let held = record;
|
|
3019
|
+
if (token !== null)
|
|
3020
|
+
arrivals.get(locator)?.delete(token);
|
|
3021
|
+
installing(locator, 'writing', { written: from, entries, bytes: whole });
|
|
3022
|
+
for (let written = from;;) {
|
|
3023
|
+
await screened();
|
|
3024
|
+
if (!liveness())
|
|
3025
|
+
return null;
|
|
3026
|
+
const end = batchEnd(page.entries, written);
|
|
3027
|
+
const last = end === entries;
|
|
3028
|
+
const next = last
|
|
3029
|
+
? {
|
|
3030
|
+
...held,
|
|
3031
|
+
writing: null,
|
|
3032
|
+
done: {
|
|
3033
|
+
parts: held.done.parts + (received === null ? 0 : 1),
|
|
3034
|
+
bytes: held.done.bytes + whole,
|
|
3035
|
+
},
|
|
3036
|
+
}
|
|
3037
|
+
: { ...held, writing: { token, written: end } };
|
|
3038
|
+
const key = await installWrite(async () => {
|
|
3039
|
+
if (!liveness())
|
|
3040
|
+
return null;
|
|
3041
|
+
if (written === from && !(await admitted(local, locator, bytes)))
|
|
3042
|
+
return null;
|
|
3043
|
+
try {
|
|
3044
|
+
return await timed(() => local.stage(next, composition, {
|
|
3045
|
+
entries: page.entries.slice(written, end),
|
|
3046
|
+
folders: last ? page.folders : [],
|
|
3047
|
+
}, last && received !== null && token !== null ? token : undefined));
|
|
3048
|
+
}
|
|
3049
|
+
catch (error) {
|
|
3050
|
+
if (!exceeded(error))
|
|
3051
|
+
throw error;
|
|
3052
|
+
await exceededOn(locator, bytes);
|
|
3053
|
+
return null;
|
|
3054
|
+
}
|
|
3055
|
+
});
|
|
3056
|
+
if (key === null)
|
|
3057
|
+
return null;
|
|
3058
|
+
const arrived = !transfers.has(locator);
|
|
3059
|
+
held = { ...next, key };
|
|
3060
|
+
transfers.set(locator, held);
|
|
3061
|
+
if (arrived)
|
|
3062
|
+
liveQueries.recover();
|
|
3063
|
+
if (last) {
|
|
3064
|
+
installing(locator, held.pageToken === null ? 'replaying' : 'downloading');
|
|
3065
|
+
liveQueries.recover();
|
|
3066
|
+
return held;
|
|
3067
|
+
}
|
|
3068
|
+
installing(locator, 'writing', { written: end, entries, bytes: whole });
|
|
3069
|
+
written = end;
|
|
3070
|
+
}
|
|
3071
|
+
},
|
|
3072
|
+
async refreshing(locator, key) {
|
|
3073
|
+
await exclusive(() => local.unrefresh(key));
|
|
3074
|
+
return refreshingOf(local, key, liveness, locator);
|
|
3075
|
+
},
|
|
1790
3076
|
abandon: (locator) => exclusive(async () => {
|
|
1791
|
-
if (!
|
|
3077
|
+
if (!liveness())
|
|
1792
3078
|
return;
|
|
1793
3079
|
await local.abandon(locator);
|
|
1794
3080
|
transfers.delete(locator);
|
|
1795
|
-
|
|
3081
|
+
liveQueries.recover();
|
|
3082
|
+
sweep();
|
|
3083
|
+
}),
|
|
3084
|
+
setAside: (locator) => exclusive(async () => {
|
|
3085
|
+
if (!liveness())
|
|
3086
|
+
return;
|
|
3087
|
+
await local.abandon(locator, true);
|
|
3088
|
+
transfers.delete(locator);
|
|
3089
|
+
liveQueries.recover();
|
|
1796
3090
|
}),
|
|
3091
|
+
release: (key) => exclusive(async () => {
|
|
3092
|
+
await local.release(key);
|
|
3093
|
+
sweep();
|
|
3094
|
+
}),
|
|
3095
|
+
content: (key) => heldContentOf(local, key),
|
|
3096
|
+
validate: (record, view) => validatedRoom(local, record, view, null),
|
|
1797
3097
|
};
|
|
1798
3098
|
}
|
|
1799
|
-
function reusable(room) {
|
|
1800
|
-
const record = replicas.get(room.locator)?.
|
|
3099
|
+
function reusable(room, local) {
|
|
3100
|
+
const record = replicas.get(room.locator)?.record;
|
|
1801
3101
|
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1802
3102
|
return null;
|
|
1803
|
-
return
|
|
3103
|
+
return heldContentOf(local, record.key);
|
|
1804
3104
|
}
|
|
1805
|
-
async function pullBootstrap(authorized, room, local,
|
|
1806
|
-
if (!
|
|
3105
|
+
async function pullBootstrap(authorized, room, local, liveness) {
|
|
3106
|
+
if (!liveness())
|
|
1807
3107
|
return unconfirmed;
|
|
3108
|
+
const turn = generation;
|
|
1808
3109
|
if (replicas.has(room.locator)) {
|
|
1809
3110
|
catching.add(room.locator);
|
|
1810
|
-
|
|
3111
|
+
liveQueries.recover();
|
|
1811
3112
|
}
|
|
1812
3113
|
try {
|
|
1813
|
-
const result = await bootstrapRoom(
|
|
3114
|
+
const result = await bootstrapRoom(reads, authorized, room, {
|
|
1814
3115
|
collection: modelOf(room.locator),
|
|
1815
3116
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
1816
|
-
}, (models, held) => (
|
|
1817
|
-
const detail = result.kind === 'staged' ? result.
|
|
3117
|
+
}, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
|
|
3118
|
+
const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
|
|
1818
3119
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1819
|
-
if (result.kind !== 'staged')
|
|
3120
|
+
if (result.kind !== 'staged') {
|
|
1820
3121
|
caughtUpTo(room.locator);
|
|
3122
|
+
installed(room.locator);
|
|
3123
|
+
}
|
|
1821
3124
|
return result;
|
|
1822
3125
|
}
|
|
1823
3126
|
catch (error) {
|
|
1824
3127
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
1825
3128
|
caughtUpTo(room.locator);
|
|
3129
|
+
installed(room.locator);
|
|
1826
3130
|
throw error;
|
|
1827
3131
|
}
|
|
3132
|
+
finally {
|
|
3133
|
+
reads.finished(room.locator, turn);
|
|
3134
|
+
}
|
|
1828
3135
|
}
|
|
1829
3136
|
function caughtUpTo(locator) {
|
|
1830
3137
|
if (catching.delete(locator))
|
|
1831
|
-
|
|
3138
|
+
liveQueries.recover();
|
|
3139
|
+
}
|
|
3140
|
+
function behind(room) {
|
|
3141
|
+
const held = replicas.get(room.locator)?.record.roomVersion;
|
|
3142
|
+
return (held !== undefined &&
|
|
3143
|
+
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
1832
3144
|
}
|
|
1833
3145
|
async function advance(room, local, authorized, live) {
|
|
1834
|
-
|
|
3146
|
+
const carried = reads.carried(room.locator);
|
|
3147
|
+
const withheld = await advanceOnce(room, local, authorized, live);
|
|
3148
|
+
if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
|
|
3149
|
+
return withheld;
|
|
3150
|
+
}
|
|
3151
|
+
return advanceOnce(room, local, authorized, live);
|
|
3152
|
+
}
|
|
3153
|
+
async function advanceOnce(room, local, authorized, live) {
|
|
1835
3154
|
if (!live() || paused.has(room.locator))
|
|
1836
3155
|
return unconfirmed;
|
|
1837
3156
|
const held = replicas.get(room.locator);
|
|
1838
3157
|
if (held !== undefined) {
|
|
1839
|
-
const refreshed = await pullRefresh(authorized, held
|
|
3158
|
+
const refreshed = await pullRefresh(authorized, held, live);
|
|
1840
3159
|
if (!live())
|
|
1841
3160
|
return unconfirmed;
|
|
1842
3161
|
if (refreshed.kind === 'still-current') {
|
|
@@ -1851,10 +3170,11 @@ export function composeRuntime(options) {
|
|
|
1851
3170
|
return null;
|
|
1852
3171
|
}
|
|
1853
3172
|
if (refreshed.kind === 'advanced') {
|
|
1854
|
-
|
|
1855
|
-
|
|
3173
|
+
const landing = { kind: 'refreshed', base: refreshed.base };
|
|
3174
|
+
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3175
|
+
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
1856
3176
|
const reached = now !== undefined &&
|
|
1857
|
-
options.adapter.reached(room.locator, now, refreshed.
|
|
3177
|
+
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
1858
3178
|
return reached ? null : advance(room, local, authorized, live);
|
|
1859
3179
|
}
|
|
1860
3180
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
@@ -1864,17 +3184,18 @@ export function composeRuntime(options) {
|
|
|
1864
3184
|
return refreshed;
|
|
1865
3185
|
if (opened !== null) {
|
|
1866
3186
|
owed.add(room.locator);
|
|
1867
|
-
|
|
3187
|
+
liveQueries.recover();
|
|
1868
3188
|
return unconfirmed;
|
|
1869
3189
|
}
|
|
3190
|
+
if (reloadsDue.get(room.locator) === generation)
|
|
3191
|
+
return unconfirmed;
|
|
1870
3192
|
}
|
|
1871
3193
|
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
1872
|
-
if (
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
return null;
|
|
3194
|
+
if (bootstrapped.kind === 'staged') {
|
|
3195
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
3196
|
+
return live() ? null : unconfirmed;
|
|
3197
|
+
}
|
|
3198
|
+
return live() ? bootstrapped : unconfirmed;
|
|
1878
3199
|
}
|
|
1879
3200
|
async function withhold(withheld, room, local, progress) {
|
|
1880
3201
|
if (withheld.kind === 'unconfirmed')
|
|
@@ -1946,57 +3267,254 @@ export function composeRuntime(options) {
|
|
|
1946
3267
|
(accepted.author === undefined ? '' : ` author ${accepted.author}`) +
|
|
1947
3268
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1948
3269
|
}
|
|
1949
|
-
|
|
1950
|
-
|
|
3270
|
+
function settling(acceptance) {
|
|
3271
|
+
return acceptance !== null && 'apply' in acceptance;
|
|
3272
|
+
}
|
|
3273
|
+
function refusalOf(acceptance) {
|
|
3274
|
+
return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
|
|
3275
|
+
}
|
|
3276
|
+
async function acceptance(room, local, submitted, accepted) {
|
|
1951
3277
|
if (!continuing())
|
|
1952
3278
|
return unconfirmed;
|
|
1953
3279
|
const current = replicas.get(room.locator);
|
|
1954
3280
|
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
1955
3281
|
return null;
|
|
1956
3282
|
}
|
|
1957
|
-
const held = current.
|
|
3283
|
+
const held = current.record.roomVersion;
|
|
1958
3284
|
logAccepted(room.locator, submitted, accepted);
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
3285
|
+
let record = current.record;
|
|
3286
|
+
let changes;
|
|
3287
|
+
if (!options.adapter.reached(room.locator, held, accepted.roomVersion)) {
|
|
3288
|
+
const paths = [...returnedPaths(submitted, accepted)];
|
|
3289
|
+
const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
|
|
3290
|
+
const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
|
|
3291
|
+
record = {
|
|
3292
|
+
...record,
|
|
3293
|
+
roomVersion: options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
3294
|
+
? accepted.roomVersion
|
|
3295
|
+
: held,
|
|
3296
|
+
};
|
|
3297
|
+
const overrides = new Map([
|
|
3298
|
+
...back.set.map((entry) => [entry.path, entry]),
|
|
3299
|
+
...back.removed.map((path) => [path, null]),
|
|
3300
|
+
]);
|
|
3301
|
+
const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
|
|
3302
|
+
if (refused !== null) {
|
|
3303
|
+
const key = pendingKey(room.locator, submitted.id);
|
|
3304
|
+
return {
|
|
3305
|
+
locator: room.locator,
|
|
3306
|
+
left: [],
|
|
3307
|
+
commit: {
|
|
3308
|
+
rooms: [],
|
|
3309
|
+
writes: [
|
|
3310
|
+
{
|
|
3311
|
+
locator: room.locator,
|
|
3312
|
+
accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
|
|
3313
|
+
},
|
|
3314
|
+
],
|
|
3315
|
+
},
|
|
3316
|
+
refused,
|
|
3317
|
+
apply() {
|
|
3318
|
+
const state = pendings.get(key);
|
|
3319
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
3320
|
+
pendings.set(key, { ...state, accepted: accepted.roomVersion });
|
|
3321
|
+
return changeSet(room.locator, { queue: true });
|
|
3322
|
+
},
|
|
3323
|
+
};
|
|
3324
|
+
}
|
|
3325
|
+
changes = back;
|
|
3326
|
+
}
|
|
3327
|
+
const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
|
|
3328
|
+
return {
|
|
3329
|
+
...settled,
|
|
3330
|
+
apply(answer) {
|
|
3331
|
+
const set = settled.apply(answer);
|
|
3332
|
+
pendings.delete(pendingKey(room.locator, submitted.id));
|
|
3333
|
+
unreadable.delete(room.locator);
|
|
3334
|
+
sourceRefusals.delete(room.locator);
|
|
3335
|
+
declarationRefusals.delete(room.locator);
|
|
3336
|
+
list(room, true);
|
|
3337
|
+
return set;
|
|
3338
|
+
},
|
|
3339
|
+
};
|
|
3340
|
+
}
|
|
3341
|
+
async function adoptedTogether(local, acceptances) {
|
|
3342
|
+
const settlements = acceptances.filter(settling);
|
|
3343
|
+
if (settlements.length > 0) {
|
|
3344
|
+
publishChangeSet(await together(local, settlements));
|
|
3345
|
+
moveTo({ ...status, ...attention() });
|
|
3346
|
+
}
|
|
3347
|
+
await unpair(local);
|
|
1982
3348
|
}
|
|
1983
3349
|
async function adopted(room, local, submitted, accepted) {
|
|
1984
|
-
const failed = await exclusive(() =>
|
|
3350
|
+
const failed = await exclusive(async () => {
|
|
3351
|
+
const adopting = await acceptance(room, local, submitted, accepted);
|
|
3352
|
+
await adoptedTogether(local, [adopting]);
|
|
3353
|
+
return refusalOf(adopting);
|
|
3354
|
+
});
|
|
3355
|
+
if (failed === null)
|
|
3356
|
+
return true;
|
|
3357
|
+
if (failed.kind === 'refused' && continuing())
|
|
3358
|
+
refusedSource(room, failed);
|
|
3359
|
+
return false;
|
|
3360
|
+
}
|
|
3361
|
+
async function adoptedMove(room, partner, local, submitted, accepted) {
|
|
3362
|
+
const other = discovered.get(partner);
|
|
3363
|
+
const failed = await exclusive(async () => {
|
|
3364
|
+
const left = await acceptance(room, local, submitted, accepted);
|
|
3365
|
+
const arriving = replicas.get(partner)?.queue.find((batch) => batch.id === submitted.id);
|
|
3366
|
+
const arrived = other === undefined || arriving === undefined || accepted.destination === undefined
|
|
3367
|
+
? null
|
|
3368
|
+
: await acceptance(other, local, arriving, {
|
|
3369
|
+
kind: 'accepted',
|
|
3370
|
+
...accepted.destination,
|
|
3371
|
+
});
|
|
3372
|
+
await adoptedTogether(local, [left, arrived]);
|
|
3373
|
+
const refusedArrival = refusalOf(arrived);
|
|
3374
|
+
if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
|
|
3375
|
+
refusedSource(other, refusedArrival);
|
|
3376
|
+
}
|
|
3377
|
+
return refusalOf(left);
|
|
3378
|
+
});
|
|
3379
|
+
submitLater(partner);
|
|
1985
3380
|
if (failed === null)
|
|
1986
3381
|
return true;
|
|
1987
3382
|
if (failed.kind === 'refused' && continuing())
|
|
1988
3383
|
refusedSource(room, failed);
|
|
1989
3384
|
return false;
|
|
1990
3385
|
}
|
|
3386
|
+
function queueIn(locator) {
|
|
3387
|
+
return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
3388
|
+
}
|
|
3389
|
+
function behindIn(locator, batch) {
|
|
3390
|
+
const held = replicas.get(locator)?.queue;
|
|
3391
|
+
if (held === undefined)
|
|
3392
|
+
return queuesWithoutReplica.get(locator) !== undefined;
|
|
3393
|
+
return held.findIndex((one) => one.id === batch.id) > 0;
|
|
3394
|
+
}
|
|
3395
|
+
function unpaired(skipped) {
|
|
3396
|
+
for (const locator of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
|
|
3397
|
+
for (const batch of queueIn(locator)) {
|
|
3398
|
+
const partner = partnerOf(batch, locator);
|
|
3399
|
+
if (partner === null || skipped.has(pendingKey(locator, batch.id)))
|
|
3400
|
+
continue;
|
|
3401
|
+
if (queueIn(partner).some((held) => held.id === batch.id))
|
|
3402
|
+
continue;
|
|
3403
|
+
if (unpairedIn(locator, batch))
|
|
3404
|
+
return { locator, batch };
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
return null;
|
|
3408
|
+
}
|
|
3409
|
+
async function unpairing(local, locator, batches) {
|
|
3410
|
+
const ids = new Set(batches.map(({ id }) => id));
|
|
3411
|
+
const leaving = batches.map((batch) => ({
|
|
3412
|
+
batch,
|
|
3413
|
+
returned: !localSaves && leavingIn(batch, locator) ? returnedBy('withdrawn') : null,
|
|
3414
|
+
}));
|
|
3415
|
+
const outcome = localSaves ? 'dropped' : 'unpaired';
|
|
3416
|
+
const current = replicas.get(locator);
|
|
3417
|
+
if (current !== undefined) {
|
|
3418
|
+
const settled = await settlement(local, locator, current, current.record, current.queue.filter((one) => !ids.has(one.id)), { leaving });
|
|
3419
|
+
return {
|
|
3420
|
+
...settled,
|
|
3421
|
+
apply(answer) {
|
|
3422
|
+
for (const batch of batches)
|
|
3423
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
3424
|
+
const set = settled.apply(answer);
|
|
3425
|
+
submitLater(locator);
|
|
3426
|
+
return set;
|
|
3427
|
+
},
|
|
3428
|
+
};
|
|
3429
|
+
}
|
|
3430
|
+
const queue = queueIn(locator).filter((one) => !ids.has(one.id));
|
|
3431
|
+
const written = new Map();
|
|
3432
|
+
for (const { batch, returned } of leaving) {
|
|
3433
|
+
if (returned === null)
|
|
3434
|
+
continue;
|
|
3435
|
+
for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
|
|
3436
|
+
written.set(saved.path, {
|
|
3437
|
+
locator,
|
|
3438
|
+
path: saved.path,
|
|
3439
|
+
revision: roomDraftRevision(),
|
|
3440
|
+
parts: returnedInto((written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path))?.parts, saved.parts),
|
|
3441
|
+
returned,
|
|
3442
|
+
});
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
const carried = carriedBack(locator, leaving.flatMap(({ batch, returned }) => (returned === null ? [] : [batch])), written);
|
|
3446
|
+
return {
|
|
3447
|
+
locator,
|
|
3448
|
+
left: batches,
|
|
3449
|
+
commit: {
|
|
3450
|
+
rooms: [],
|
|
3451
|
+
writes: [
|
|
3452
|
+
{
|
|
3453
|
+
locator,
|
|
3454
|
+
queue,
|
|
3455
|
+
roomDrafts: [...written.values()],
|
|
3456
|
+
consumedRoomDrafts: carried.taken,
|
|
3457
|
+
},
|
|
3458
|
+
...carried.writes,
|
|
3459
|
+
],
|
|
3460
|
+
},
|
|
3461
|
+
apply() {
|
|
3462
|
+
for (const batch of batches) {
|
|
3463
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
3464
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
3465
|
+
}
|
|
3466
|
+
if (queue.length > 0)
|
|
3467
|
+
queuesWithoutReplica.set(locator, queue);
|
|
3468
|
+
else
|
|
3469
|
+
queuesWithoutReplica.delete(locator);
|
|
3470
|
+
for (const record of written.values())
|
|
3471
|
+
rememberRoomDraft(record);
|
|
3472
|
+
carried.remember();
|
|
3473
|
+
submitLater(locator);
|
|
3474
|
+
return mergeChangeSets([
|
|
3475
|
+
changeSet(locator, {
|
|
3476
|
+
queue: true,
|
|
3477
|
+
drafts: [...written.keys(), ...carried.taken],
|
|
3478
|
+
}),
|
|
3479
|
+
...carried.changes,
|
|
3480
|
+
]);
|
|
3481
|
+
},
|
|
3482
|
+
};
|
|
3483
|
+
}
|
|
3484
|
+
async function unpair(local) {
|
|
3485
|
+
const handled = new Set();
|
|
3486
|
+
for (let held = unpaired(handled); held !== null && continuing(); held = unpaired(handled)) {
|
|
3487
|
+
const { locator, batch } = held;
|
|
3488
|
+
handled.add(pendingKey(locator, batch.id));
|
|
3489
|
+
publishChangeSet(await together(local, [await unpairing(local, locator, [batch])]));
|
|
3490
|
+
}
|
|
3491
|
+
if (handled.size === 0)
|
|
3492
|
+
return;
|
|
3493
|
+
moveTo({ ...status, ...attention() });
|
|
3494
|
+
}
|
|
3495
|
+
async function discardHeld(head, room, local, signal) {
|
|
3496
|
+
const partner = partnerOf(head, room.locator);
|
|
3497
|
+
const other = partner === null ? undefined : discovered.get(partner);
|
|
3498
|
+
await discardPending(other === undefined ? [room] : [room, other], local, signal);
|
|
3499
|
+
}
|
|
1991
3500
|
async function submitRoom(room, local, authorized, progress, signal) {
|
|
1992
3501
|
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
1993
3502
|
const replica = replicas.get(room.locator);
|
|
1994
3503
|
const head = replica?.queue[0];
|
|
1995
3504
|
if (replica === undefined || head === undefined)
|
|
1996
3505
|
return 'done';
|
|
1997
|
-
|
|
3506
|
+
const state = pendings.get(pendingKey(room.locator, head.id));
|
|
3507
|
+
if (state?.accepted !== undefined)
|
|
3508
|
+
return 'done';
|
|
3509
|
+
const partner = partnerOf(head, room.locator);
|
|
3510
|
+
if (partner !== null && !leavingIn(head, room.locator)) {
|
|
3511
|
+
submitLater(partner);
|
|
1998
3512
|
return 'done';
|
|
1999
|
-
|
|
3513
|
+
}
|
|
3514
|
+
if (partner !== null && behindIn(partner, head)) {
|
|
3515
|
+
progress.complete = false;
|
|
3516
|
+
return 'done';
|
|
3517
|
+
}
|
|
2000
3518
|
const { submission, lost: earlier } = state ?? {
|
|
2001
3519
|
submission: { state: 'waiting' },
|
|
2002
3520
|
lost: false,
|
|
@@ -2012,16 +3530,16 @@ export function composeRuntime(options) {
|
|
|
2012
3530
|
}
|
|
2013
3531
|
const lost = earlier || submission.state === 'submitted';
|
|
2014
3532
|
if (lost && pastHorizon(head.id)) {
|
|
2015
|
-
await
|
|
3533
|
+
await discardHeld(head, room, local, signal);
|
|
2016
3534
|
return 'done';
|
|
2017
3535
|
}
|
|
2018
3536
|
let result;
|
|
2019
3537
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
2020
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3538
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
|
|
2021
3539
|
if (discoveryPaused || fences.has(room.locator))
|
|
2022
3540
|
return 'done';
|
|
2023
3541
|
if (signal.aborted || writesHeld()) {
|
|
2024
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3542
|
+
await exclusive(() => mark(local, room.locator, head, submission, earlier));
|
|
2025
3543
|
if (signal.aborted)
|
|
2026
3544
|
return 'done';
|
|
2027
3545
|
progress.complete = false;
|
|
@@ -2029,11 +3547,17 @@ export function composeRuntime(options) {
|
|
|
2029
3547
|
return 'done';
|
|
2030
3548
|
}
|
|
2031
3549
|
const opensBefore = openSignals;
|
|
2032
|
-
const
|
|
3550
|
+
const answeredHere = awaitingAnswer(room.locator);
|
|
3551
|
+
const answeredThere = partner === null ? () => { } : awaitingAnswer(partner);
|
|
3552
|
+
const answered = () => {
|
|
3553
|
+
answeredHere();
|
|
3554
|
+
answeredThere();
|
|
3555
|
+
};
|
|
2033
3556
|
try {
|
|
2034
3557
|
result = await options.adapter.mutate(authorized, {
|
|
2035
3558
|
locator: room.locator,
|
|
2036
3559
|
changeBatch: head,
|
|
3560
|
+
file: batchFile(head),
|
|
2037
3561
|
...(state?.upload === undefined ? {} : { upload: state.upload }),
|
|
2038
3562
|
}, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
|
|
2039
3563
|
}
|
|
@@ -2058,9 +3582,10 @@ export function composeRuntime(options) {
|
|
|
2058
3582
|
return 'again';
|
|
2059
3583
|
}
|
|
2060
3584
|
if (result.kind === 'room-not-readable') {
|
|
3585
|
+
const unread = discovered.get(result.locator) ?? room;
|
|
2061
3586
|
progress.complete = false;
|
|
2062
|
-
unreadable.add(
|
|
2063
|
-
list(
|
|
3587
|
+
unreadable.add(unread.locator);
|
|
3588
|
+
list(unread, false);
|
|
2064
3589
|
emitRooms();
|
|
2065
3590
|
return 'done';
|
|
2066
3591
|
}
|
|
@@ -2075,24 +3600,30 @@ export function composeRuntime(options) {
|
|
|
2075
3600
|
}
|
|
2076
3601
|
if (result.kind === 'write-halted') {
|
|
2077
3602
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
2078
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3603
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
|
|
2079
3604
|
if (serviceMode === 'open' && openSignals !== opensBefore)
|
|
2080
3605
|
continue;
|
|
2081
3606
|
haltedRefusal = true;
|
|
2082
3607
|
progress.complete = false;
|
|
2083
3608
|
return 'done';
|
|
2084
3609
|
}
|
|
3610
|
+
const other = partner === null ? undefined : discovered.get(partner);
|
|
2085
3611
|
if (result.kind === 'conflicted' ||
|
|
2086
3612
|
result.kind === 'rejected' ||
|
|
2087
3613
|
result.kind === 'expired' ||
|
|
2088
3614
|
result.kind === 'retryable-refusal') {
|
|
2089
3615
|
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
2090
3616
|
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
2091
|
-
await
|
|
3617
|
+
await discardHeld(head, room, local, signal);
|
|
2092
3618
|
return 'done';
|
|
2093
3619
|
}
|
|
2094
3620
|
if (result.kind === 'expired') {
|
|
2095
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3621
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
|
|
3622
|
+
const there = other === undefined ? null : await advance(other, local, authorized, continuing);
|
|
3623
|
+
if (other !== undefined && there !== null) {
|
|
3624
|
+
await withhold(there, other, local, progress);
|
|
3625
|
+
return 'done';
|
|
3626
|
+
}
|
|
2096
3627
|
const withheld = await advance(room, local, authorized, continuing);
|
|
2097
3628
|
if (withheld !== null) {
|
|
2098
3629
|
await withhold(withheld, room, local, progress);
|
|
@@ -2109,11 +3640,25 @@ export function composeRuntime(options) {
|
|
|
2109
3640
|
if (result.kind === 'retryable-refusal') {
|
|
2110
3641
|
const reset = result.reset;
|
|
2111
3642
|
progress.complete = false;
|
|
2112
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3643
|
+
await exclusive(() => mark(local, room.locator, head, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
2113
3644
|
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
2114
3645
|
}
|
|
2115
3646
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
2116
3647
|
const left = leaving(head, result);
|
|
3648
|
+
const refreshed = async () => ({
|
|
3649
|
+
there: other === undefined ? null : await advance(other, local, authorized, continuing),
|
|
3650
|
+
here: await advance(room, local, authorized, continuing),
|
|
3651
|
+
});
|
|
3652
|
+
const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
|
|
3653
|
+
if (judged !== null && (judged.there !== null || judged.here !== null)) {
|
|
3654
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
|
|
3655
|
+
if (other !== undefined && judged.there !== null) {
|
|
3656
|
+
await withhold(judged.there, other, local, progress);
|
|
3657
|
+
}
|
|
3658
|
+
if (judged.here !== null)
|
|
3659
|
+
await withhold(judged.here, room, local, progress);
|
|
3660
|
+
return 'done';
|
|
3661
|
+
}
|
|
2117
3662
|
await exclusive(async () => {
|
|
2118
3663
|
if (!continuing())
|
|
2119
3664
|
return;
|
|
@@ -2121,25 +3666,29 @@ export function composeRuntime(options) {
|
|
|
2121
3666
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
2122
3667
|
return;
|
|
2123
3668
|
}
|
|
2124
|
-
const
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
});
|
|
2130
|
-
|
|
3669
|
+
const confirmedLayer = await sliceAt(current.record, 'confirmed', [head]);
|
|
3670
|
+
const held = (result.kind === 'expired' &&
|
|
3671
|
+
(await landedWhole(room.locator, head, confirmedLayer))) ||
|
|
3672
|
+
(result.kind === 'conflicted' &&
|
|
3673
|
+
(await landedWhole(room.locator, head, confirmedLayer, result.affected)));
|
|
3674
|
+
publishChangeSet(await settle(local, room.locator, current, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
|
|
3675
|
+
await unpair(local);
|
|
2131
3676
|
moveTo({ ...status, ...attention() });
|
|
2132
3677
|
});
|
|
2133
3678
|
if (result.kind === 'expired')
|
|
2134
3679
|
continue;
|
|
2135
|
-
const
|
|
2136
|
-
if (
|
|
2137
|
-
await withhold(
|
|
3680
|
+
const { there, here } = judged ?? (await refreshed());
|
|
3681
|
+
if (other !== undefined && there !== null)
|
|
3682
|
+
await withhold(there, other, local, progress);
|
|
3683
|
+
if (here !== null) {
|
|
3684
|
+
await withhold(here, room, local, progress);
|
|
2138
3685
|
return 'done';
|
|
2139
3686
|
}
|
|
2140
3687
|
continue;
|
|
2141
3688
|
}
|
|
2142
|
-
const kept = await
|
|
3689
|
+
const kept = await (partner === null
|
|
3690
|
+
? adopted(room, local, head, result)
|
|
3691
|
+
: adoptedMove(room, partner, local, head, result)).finally(answered);
|
|
2143
3692
|
if (!kept) {
|
|
2144
3693
|
progress.complete = false;
|
|
2145
3694
|
return 'done';
|
|
@@ -2150,44 +3699,67 @@ export function composeRuntime(options) {
|
|
|
2150
3699
|
function pastHorizon(identity) {
|
|
2151
3700
|
return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
|
|
2152
3701
|
}
|
|
2153
|
-
async function discardPending(
|
|
2154
|
-
const { locator } = room;
|
|
3702
|
+
async function discardPending(rooms, local, signal) {
|
|
2155
3703
|
const discarded = await exclusive(async () => {
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
3704
|
+
const held = rooms
|
|
3705
|
+
.filter(({ locator }) => !fences.has(locator))
|
|
3706
|
+
.map((room) => ({
|
|
3707
|
+
room,
|
|
3708
|
+
before: replicas.get(room.locator),
|
|
3709
|
+
queue: queueIn(room.locator),
|
|
3710
|
+
}));
|
|
3711
|
+
if (held.length === 0)
|
|
3712
|
+
return [];
|
|
3713
|
+
const answer = await local.commit({
|
|
3714
|
+
rooms: held.flatMap(({ room, before }) => before === undefined
|
|
3715
|
+
? []
|
|
3716
|
+
: [
|
|
3717
|
+
{
|
|
3718
|
+
locator: room.locator,
|
|
3719
|
+
key: before.record.key,
|
|
3720
|
+
roots: before.record.typedSelection?.roots ?? [],
|
|
3721
|
+
overlay: { set: [], cleared: [...before.overlaid] },
|
|
3722
|
+
},
|
|
3723
|
+
]),
|
|
3724
|
+
discards: held.map(({ room }) => room.locator),
|
|
3725
|
+
});
|
|
3726
|
+
for (const { room, before, queue } of held) {
|
|
3727
|
+
const { locator } = room;
|
|
3728
|
+
lastFenced.set(locator, ++fencings);
|
|
3729
|
+
for (const batch of queue)
|
|
3730
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
3731
|
+
queuesWithoutReplica.delete(locator);
|
|
3732
|
+
if (before !== undefined) {
|
|
3733
|
+
replicas.set(locator, {
|
|
3734
|
+
...before,
|
|
3735
|
+
queue: [],
|
|
3736
|
+
overlaid: new Set(),
|
|
3737
|
+
count: answer.counts[locator] ?? before.count,
|
|
3738
|
+
});
|
|
3739
|
+
}
|
|
3740
|
+
journal.record(`room discarded ${locator}`);
|
|
2167
3741
|
}
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
if (after !== null)
|
|
2173
|
-
replicas.set(locator, after);
|
|
2174
|
-
journal.record(`room discarded ${locator}`);
|
|
2175
|
-
publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
|
|
3742
|
+
publishChangeSet(mergeChangeSets(held.map(({ room }) => changeSet(room.locator, {
|
|
3743
|
+
documents: answer.changed[room.locator] ?? [],
|
|
3744
|
+
queue: true,
|
|
3745
|
+
}))));
|
|
2176
3746
|
moveTo({ ...status, ...attention() });
|
|
2177
|
-
return
|
|
3747
|
+
return held.map(({ room }) => room);
|
|
2178
3748
|
});
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
await
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
3749
|
+
for (const room of discarded) {
|
|
3750
|
+
if (signal.aborted)
|
|
3751
|
+
return;
|
|
3752
|
+
const progress = { complete: true, reachable: true };
|
|
3753
|
+
await roomTask(room.locator, async (live) => {
|
|
3754
|
+
await Promise.race([
|
|
3755
|
+
reload(room, local, progress, () => live() && !signal.aborted),
|
|
3756
|
+
abandonedOn(signal),
|
|
3757
|
+
]);
|
|
3758
|
+
});
|
|
3759
|
+
if (signal.aborted)
|
|
3760
|
+
return;
|
|
3761
|
+
await rest(progress);
|
|
3762
|
+
}
|
|
2191
3763
|
}
|
|
2192
3764
|
function awaitingAnswer(locator) {
|
|
2193
3765
|
let release = () => { };
|
|
@@ -2204,7 +3776,7 @@ export function composeRuntime(options) {
|
|
|
2204
3776
|
async function retainModel(local, room, attached, next) {
|
|
2205
3777
|
const key = modelKey(room.locator, attached);
|
|
2206
3778
|
const previous = modelOf(room.locator, attached);
|
|
2207
|
-
const heldBefore = replicas.get(room.locator)?.
|
|
3779
|
+
const heldBefore = replicas.get(room.locator)?.record;
|
|
2208
3780
|
if (JSON.stringify(previous) === JSON.stringify(next) &&
|
|
2209
3781
|
(heldBefore === undefined ||
|
|
2210
3782
|
(JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
|
|
@@ -2220,36 +3792,56 @@ export function composeRuntime(options) {
|
|
|
2220
3792
|
if (!continuing())
|
|
2221
3793
|
return;
|
|
2222
3794
|
const held = replicas.get(room.locator);
|
|
3795
|
+
let changed = null;
|
|
2223
3796
|
if (held !== undefined) {
|
|
2224
3797
|
const declared = {
|
|
2225
|
-
...held.
|
|
3798
|
+
...held.record,
|
|
2226
3799
|
operations: room.operations,
|
|
2227
3800
|
replica: room.replica,
|
|
2228
3801
|
typedSelection: room.typedSelection ?? null,
|
|
2229
3802
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
2230
3803
|
};
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
3804
|
+
let read = 'staged';
|
|
3805
|
+
if (!sameMembership(declared, held.record)) {
|
|
3806
|
+
const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), compositionOf(declared));
|
|
3807
|
+
read =
|
|
3808
|
+
verdict.verdict !== 'refuse'
|
|
3809
|
+
? 'staged'
|
|
3810
|
+
: verdict.rule === 'model-unavailable'
|
|
3811
|
+
? 'deferred'
|
|
3812
|
+
: { kind: 'refused', refusal: verdict };
|
|
3813
|
+
}
|
|
3814
|
+
const record = read === 'staged'
|
|
2235
3815
|
? declared
|
|
2236
|
-
: {
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
const
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
3816
|
+
: { ...declared, typedSelection: held.record.typedSelection };
|
|
3817
|
+
const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
|
|
3818
|
+
const replayedLayer = await replayed(record, confirmedLayer, held.queue);
|
|
3819
|
+
const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
|
|
3820
|
+
const answer = await local.commit({
|
|
3821
|
+
rooms: [
|
|
3822
|
+
{
|
|
3823
|
+
locator: room.locator,
|
|
3824
|
+
key: record.key,
|
|
3825
|
+
record: storedRecord(record),
|
|
3826
|
+
roots: record.typedSelection?.roots ?? [],
|
|
3827
|
+
composition: compositionOf(record),
|
|
3828
|
+
overlay: diff.overlay,
|
|
3829
|
+
},
|
|
3830
|
+
],
|
|
3831
|
+
});
|
|
3832
|
+
const { count, indexed } = committedRoom(room.locator, answer);
|
|
3833
|
+
replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
|
|
3834
|
+
changed = changeSet(room.locator, {
|
|
3835
|
+
documents: answer.changed[room.locator] ?? [],
|
|
3836
|
+
room: true,
|
|
3837
|
+
});
|
|
3838
|
+
if (read === 'staged' || read === 'deferred') {
|
|
2246
3839
|
if (declarationRefusals.delete(room.locator)) {
|
|
2247
3840
|
sourceRefusals.delete(room.locator);
|
|
2248
3841
|
unreadable.delete(room.locator);
|
|
2249
3842
|
}
|
|
2250
3843
|
}
|
|
2251
|
-
else
|
|
2252
|
-
replicas.set(room.locator, { ...held, confirmed: { ...held.confirmed, record } });
|
|
3844
|
+
else {
|
|
2253
3845
|
refusedSource(room, read);
|
|
2254
3846
|
declarationRefusals.add(room.locator);
|
|
2255
3847
|
}
|
|
@@ -2260,13 +3852,18 @@ export function composeRuntime(options) {
|
|
|
2260
3852
|
invalidModels.delete(key);
|
|
2261
3853
|
}
|
|
2262
3854
|
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
2263
|
-
if (
|
|
3855
|
+
if (changed === null)
|
|
2264
3856
|
emitRooms();
|
|
2265
3857
|
else
|
|
2266
|
-
publishChangeSet(
|
|
3858
|
+
publishChangeSet(changed);
|
|
2267
3859
|
});
|
|
2268
3860
|
}
|
|
2269
3861
|
async function scoped(result, room, local) {
|
|
3862
|
+
if (result.scope === 'content' && !result.retryable) {
|
|
3863
|
+
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
3864
|
+
await loseRooms([room.locator]);
|
|
3865
|
+
return;
|
|
3866
|
+
}
|
|
2270
3867
|
scopedRefusals.set(room.locator, {
|
|
2271
3868
|
code: result.code,
|
|
2272
3869
|
fields: result.fields,
|
|
@@ -2280,15 +3877,7 @@ export function composeRuntime(options) {
|
|
|
2280
3877
|
});
|
|
2281
3878
|
else if (result.scope !== 'operation') {
|
|
2282
3879
|
unreadable.add(room.locator);
|
|
2283
|
-
scopedAway = {
|
|
2284
|
-
...room,
|
|
2285
|
-
replica: { ...room.replica, admission: 'unavailable' },
|
|
2286
|
-
operations: {
|
|
2287
|
-
...room.operations,
|
|
2288
|
-
...(result.scope === 'content' ? { read: [] } : {}),
|
|
2289
|
-
...(result.scope === 'sync' ? { sync: [] } : {}),
|
|
2290
|
-
},
|
|
2291
|
-
};
|
|
3880
|
+
scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
|
|
2292
3881
|
await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
|
|
2293
3882
|
}
|
|
2294
3883
|
if (!continuing())
|
|
@@ -2299,30 +3888,22 @@ export function composeRuntime(options) {
|
|
|
2299
3888
|
}
|
|
2300
3889
|
function heldRooms(listed) {
|
|
2301
3890
|
return listed.flatMap((room) => {
|
|
2302
|
-
const record = replicas.get(room.locator)?.
|
|
3891
|
+
const record = replicas.get(room.locator)?.record;
|
|
2303
3892
|
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
2304
3893
|
return [];
|
|
2305
|
-
return [
|
|
2306
|
-
{
|
|
2307
|
-
locator: room.locator,
|
|
2308
|
-
roomVersion: record.roomVersion,
|
|
2309
|
-
documents: record.documents,
|
|
2310
|
-
revisions: record.revisions,
|
|
2311
|
-
},
|
|
2312
|
-
];
|
|
3894
|
+
return [{ locator: room.locator, roomVersion: record.roomVersion }];
|
|
2313
3895
|
});
|
|
2314
3896
|
}
|
|
2315
3897
|
async function synchronize(listed, local, progress, attempt, live) {
|
|
2316
|
-
const
|
|
2317
|
-
const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
|
|
3898
|
+
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
|
|
2318
3899
|
if (!live())
|
|
2319
3900
|
return;
|
|
2320
|
-
const ready =
|
|
3901
|
+
const ready = listed.filter((_, index) => {
|
|
2321
3902
|
const outcome = prepared[index];
|
|
2322
3903
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
2323
3904
|
});
|
|
2324
3905
|
if (attempt === 'opened') {
|
|
2325
|
-
options.adapter.roomStream?.hold(heldRooms(ready)
|
|
3906
|
+
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
2326
3907
|
handed = true;
|
|
2327
3908
|
}
|
|
2328
3909
|
discovering?.release();
|
|
@@ -2338,7 +3919,7 @@ export function composeRuntime(options) {
|
|
|
2338
3919
|
progress.complete = false;
|
|
2339
3920
|
return false;
|
|
2340
3921
|
}
|
|
2341
|
-
const prior = replicas.get(room.locator)?.
|
|
3922
|
+
const prior = replicas.get(room.locator)?.record;
|
|
2342
3923
|
if (prior !== undefined && prior.replica.selection !== room.replica.selection) {
|
|
2343
3924
|
list(prior, false);
|
|
2344
3925
|
emitRooms();
|
|
@@ -2350,19 +3931,10 @@ export function composeRuntime(options) {
|
|
|
2350
3931
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
2351
3932
|
if (discoveryPaused || authorization === undefined)
|
|
2352
3933
|
return false;
|
|
2353
|
-
|
|
2354
|
-
if (replicas.has(room.locator))
|
|
2355
|
-
progress.complete = false;
|
|
2356
|
-
return false;
|
|
2357
|
-
}
|
|
2358
|
-
return true;
|
|
3934
|
+
return room.replica.admission === 'admitted';
|
|
2359
3935
|
}
|
|
2360
3936
|
async function caughtUp(room, local, progress, live) {
|
|
2361
|
-
|
|
2362
|
-
if (held === undefined || authorization === undefined || !live())
|
|
2363
|
-
return;
|
|
2364
|
-
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
2365
|
-
if (!behind)
|
|
3937
|
+
if (authorization === undefined || !live() || !behind(room))
|
|
2366
3938
|
return;
|
|
2367
3939
|
const withheld = await advance(room, local, authorization, live);
|
|
2368
3940
|
if (withheld !== null)
|
|
@@ -2390,23 +3962,24 @@ export function composeRuntime(options) {
|
|
|
2390
3962
|
}
|
|
2391
3963
|
async function reload(room, local, progress, live) {
|
|
2392
3964
|
try {
|
|
3965
|
+
if (live())
|
|
3966
|
+
reloadsDue.delete(room.locator);
|
|
2393
3967
|
if (!live() || authorization === undefined)
|
|
2394
3968
|
return;
|
|
2395
3969
|
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
2396
|
-
if (!live())
|
|
2397
|
-
return;
|
|
2398
3970
|
if (bootstrapped.kind !== 'staged') {
|
|
2399
|
-
|
|
3971
|
+
if (live())
|
|
3972
|
+
await withhold(bootstrapped, room, local, progress);
|
|
2400
3973
|
return;
|
|
2401
3974
|
}
|
|
2402
|
-
await publish(local, room, bootstrapped.
|
|
3975
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
2403
3976
|
await caughtUp(room, local, progress, live);
|
|
2404
3977
|
}
|
|
2405
3978
|
finally {
|
|
2406
3979
|
caughtUpTo(room.locator);
|
|
2407
3980
|
}
|
|
2408
3981
|
}
|
|
2409
|
-
async function discoverRooms(local, progress, attempt, live) {
|
|
3982
|
+
async function discoverRooms(local, progress, attempt, live, scope) {
|
|
2410
3983
|
if (closed || authorization === undefined) {
|
|
2411
3984
|
notifyReady();
|
|
2412
3985
|
return;
|
|
@@ -2416,7 +3989,7 @@ export function composeRuntime(options) {
|
|
|
2416
3989
|
const started = discoveries;
|
|
2417
3990
|
let result;
|
|
2418
3991
|
try {
|
|
2419
|
-
result = await options.adapter.discover(authorization);
|
|
3992
|
+
result = await options.adapter.discover(authorization, scope);
|
|
2420
3993
|
}
|
|
2421
3994
|
catch {
|
|
2422
3995
|
result = { kind: 'transport-uncertainty' };
|
|
@@ -2456,12 +4029,13 @@ export function composeRuntime(options) {
|
|
|
2456
4029
|
}
|
|
2457
4030
|
for (const room of result.rooms) {
|
|
2458
4031
|
if (room.operations === undefined ||
|
|
2459
|
-
!['
|
|
4032
|
+
!['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
2460
4033
|
room.operations[operation].every((scope) => typeof scope === 'string')) ||
|
|
2461
4034
|
room.replica === undefined ||
|
|
2462
4035
|
typeof room.replica.selection !== 'string' ||
|
|
2463
4036
|
room.replica.selection.length === 0 ||
|
|
2464
|
-
!['admitted', 'unavailable'].includes(room.replica.admission)
|
|
4037
|
+
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
4038
|
+
!['locked', 'open'].includes(room.replica.offline)) {
|
|
2465
4039
|
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
2466
4040
|
await refuse('missing-required-guarantee');
|
|
2467
4041
|
notifyReady();
|
|
@@ -2485,15 +4059,16 @@ export function composeRuntime(options) {
|
|
|
2485
4059
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
2486
4060
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
2487
4061
|
limits = result.providerLimits;
|
|
2488
|
-
await local.
|
|
4062
|
+
await local.fact('providerLimits', limits);
|
|
4063
|
+
await local.fact('capabilities', capabilities);
|
|
2489
4064
|
}
|
|
2490
4065
|
const signedIn = result.member ?? null;
|
|
2491
4066
|
if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
|
|
2492
|
-
await local.
|
|
4067
|
+
await local.fact('member', signedIn);
|
|
2493
4068
|
member = signedIn;
|
|
2494
4069
|
emitMember();
|
|
2495
4070
|
}
|
|
2496
|
-
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
4071
|
+
await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
|
|
2497
4072
|
}
|
|
2498
4073
|
async function ensureAuthorization() {
|
|
2499
4074
|
if (authorization !== undefined)
|
|
@@ -2590,9 +4165,44 @@ export function composeRuntime(options) {
|
|
|
2590
4165
|
options.adapter.roomStream?.reopen();
|
|
2591
4166
|
});
|
|
2592
4167
|
}
|
|
2593
|
-
function
|
|
4168
|
+
function rediscover(scope) {
|
|
4169
|
+
return task(OPENING_LANE, async (live) => {
|
|
4170
|
+
if (!live() || !socketOpen)
|
|
4171
|
+
return;
|
|
4172
|
+
const progress = { complete: true, reachable: true };
|
|
4173
|
+
const gate = discover();
|
|
4174
|
+
moveTo(transferring(status));
|
|
4175
|
+
try {
|
|
4176
|
+
const local = await openLocal();
|
|
4177
|
+
if (!live())
|
|
4178
|
+
return;
|
|
4179
|
+
await ensureAuthorization();
|
|
4180
|
+
if (!live())
|
|
4181
|
+
return;
|
|
4182
|
+
await discoverRooms(local, progress, 'authorized', live, scope);
|
|
4183
|
+
}
|
|
4184
|
+
catch {
|
|
4185
|
+
progress.complete = false;
|
|
4186
|
+
}
|
|
4187
|
+
finally {
|
|
4188
|
+
gate.release();
|
|
4189
|
+
notifyReady();
|
|
4190
|
+
}
|
|
4191
|
+
if (!live())
|
|
4192
|
+
return;
|
|
4193
|
+
if (!progress.complete)
|
|
4194
|
+
complete = false;
|
|
4195
|
+
await rest(progress, live);
|
|
4196
|
+
});
|
|
4197
|
+
}
|
|
4198
|
+
function closedSocket(revoked, lost) {
|
|
2594
4199
|
const opened = socketOpen;
|
|
2595
4200
|
socketOpen = false;
|
|
4201
|
+
connectionLost();
|
|
4202
|
+
if (lost) {
|
|
4203
|
+
reads.lost();
|
|
4204
|
+
conversations.lost();
|
|
4205
|
+
}
|
|
2596
4206
|
if (revoked) {
|
|
2597
4207
|
void refuse('revoked-authorization');
|
|
2598
4208
|
return;
|
|
@@ -2609,9 +4219,10 @@ export function composeRuntime(options) {
|
|
|
2609
4219
|
function applyOpening(expired) {
|
|
2610
4220
|
const progress = underway?.generation === generation ? underway.progress : null;
|
|
2611
4221
|
const cycle = progress ?? { complete: true, reachable: true };
|
|
2612
|
-
const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
|
|
2613
4222
|
if (opened === generation)
|
|
2614
4223
|
opened = null;
|
|
4224
|
+
const reloading = progress === null ? [] : owedReloads(expired);
|
|
4225
|
+
const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
|
|
2615
4226
|
return task(OPENING_LANE, async (live) => {
|
|
2616
4227
|
underway = null;
|
|
2617
4228
|
if (!live())
|
|
@@ -2621,24 +4232,17 @@ export function composeRuntime(options) {
|
|
|
2621
4232
|
await rest(cycle, live);
|
|
2622
4233
|
return;
|
|
2623
4234
|
}
|
|
2624
|
-
await roomsCarried(expired);
|
|
4235
|
+
await roomsCarried([...expired, ...reloading]);
|
|
2625
4236
|
if (!live())
|
|
2626
4237
|
return;
|
|
2627
|
-
const replaced = new Set(owed);
|
|
2628
|
-
owed.clear();
|
|
2629
4238
|
try {
|
|
2630
4239
|
const local = await openLocal();
|
|
2631
|
-
const
|
|
2632
|
-
const reloading = held.filter((room) => !expired.includes(room.locator) &&
|
|
2633
|
-
(replaced.has(room.locator) || revoked(room.locator)));
|
|
2634
|
-
for (const room of reloading)
|
|
2635
|
-
catching.add(room.locator);
|
|
2636
|
-
readSide.recover();
|
|
2637
|
-
const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
|
|
2638
|
-
const reloaded = [...(await reloads), ...(await replacing)];
|
|
4240
|
+
const reloaded = await replacing;
|
|
2639
4241
|
if (progress.reachable) {
|
|
2640
|
-
for (const room of
|
|
2641
|
-
if (
|
|
4242
|
+
for (const room of discovered.values()) {
|
|
4243
|
+
if (replicas.has(room.locator) &&
|
|
4244
|
+
!expired.includes(room.locator) &&
|
|
4245
|
+
!reloading.includes(room.locator)) {
|
|
2642
4246
|
await confirmModels(room, local);
|
|
2643
4247
|
}
|
|
2644
4248
|
}
|
|
@@ -2655,14 +4259,29 @@ export function composeRuntime(options) {
|
|
|
2655
4259
|
await finishOpening(progress, live);
|
|
2656
4260
|
});
|
|
2657
4261
|
}
|
|
4262
|
+
function owedReloads(expired) {
|
|
4263
|
+
const reloading = [...discovered.values()]
|
|
4264
|
+
.filter((room) => replicas.has(room.locator) &&
|
|
4265
|
+
!expired.includes(room.locator) &&
|
|
4266
|
+
(owed.has(room.locator) || revoked(room.locator)))
|
|
4267
|
+
.map((room) => room.locator);
|
|
4268
|
+
owed.clear();
|
|
4269
|
+
for (const locator of reloading)
|
|
4270
|
+
catching.add(locator);
|
|
4271
|
+
liveQueries.recover();
|
|
4272
|
+
return reloading;
|
|
4273
|
+
}
|
|
2658
4274
|
async function reloadNow(replaced, progress) {
|
|
4275
|
+
const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
|
|
4276
|
+
for (const room of reloaded)
|
|
4277
|
+
reloadsDue.set(room.locator, generation);
|
|
2659
4278
|
try {
|
|
2660
4279
|
const local = await openLocal();
|
|
2661
|
-
return await Promise.allSettled(
|
|
2662
|
-
.filter((room) => replicas.has(room.locator) && replaced(room))
|
|
2663
|
-
.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
4280
|
+
return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
2664
4281
|
}
|
|
2665
4282
|
catch {
|
|
4283
|
+
for (const room of reloaded)
|
|
4284
|
+
reloadsDue.delete(room.locator);
|
|
2666
4285
|
progress.reachable = false;
|
|
2667
4286
|
return [];
|
|
2668
4287
|
}
|
|
@@ -2779,6 +4398,7 @@ export function composeRuntime(options) {
|
|
|
2779
4398
|
progress.complete = false;
|
|
2780
4399
|
return;
|
|
2781
4400
|
}
|
|
4401
|
+
reads.applied();
|
|
2782
4402
|
submitAll();
|
|
2783
4403
|
for (const locator of stale)
|
|
2784
4404
|
if (repairable.has(locator))
|
|
@@ -2787,10 +4407,11 @@ export function composeRuntime(options) {
|
|
|
2787
4407
|
if (status.providerUnreachable)
|
|
2788
4408
|
conversations.reconnected();
|
|
2789
4409
|
const reading = { complete: true, reachable: true };
|
|
2790
|
-
|
|
2791
|
-
await conversations.catchUp(reading);
|
|
4410
|
+
await conversations.catchUp(reading);
|
|
2792
4411
|
if (progress.reachable && reading.reachable)
|
|
2793
4412
|
await conversations.resend(reading);
|
|
4413
|
+
if (progress.reachable)
|
|
4414
|
+
void recovery.attempt('calls', resendCalls);
|
|
2794
4415
|
if (!reading.reachable) {
|
|
2795
4416
|
progress.reachable = false;
|
|
2796
4417
|
resendLater();
|
|
@@ -2814,7 +4435,7 @@ export function composeRuntime(options) {
|
|
|
2814
4435
|
if (synced && !recorded) {
|
|
2815
4436
|
const completedAt = new Date().toISOString();
|
|
2816
4437
|
try {
|
|
2817
|
-
await (await openLocal()).
|
|
4438
|
+
await (await openLocal()).fact('lastCompleted', completedAt);
|
|
2818
4439
|
if (!live())
|
|
2819
4440
|
return;
|
|
2820
4441
|
recorded = true;
|
|
@@ -2851,11 +4472,11 @@ export function composeRuntime(options) {
|
|
|
2851
4472
|
return;
|
|
2852
4473
|
const room = discovered.get(locator);
|
|
2853
4474
|
const held = replicas.get(locator);
|
|
2854
|
-
if (room === undefined || room.replica.admission !== 'admitted'
|
|
4475
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2855
4476
|
return;
|
|
2856
4477
|
if (held !== undefined &&
|
|
2857
|
-
(held.
|
|
2858
|
-
options.adapter.reached(locator, held.
|
|
4478
|
+
(held.record.replica.selection !== room.replica.selection ||
|
|
4479
|
+
options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
|
|
2859
4480
|
return;
|
|
2860
4481
|
}
|
|
2861
4482
|
const during = underway?.generation === generation ? underway.progress : null;
|
|
@@ -2902,18 +4523,29 @@ export function composeRuntime(options) {
|
|
|
2902
4523
|
return;
|
|
2903
4524
|
if (event.kind === 'opened') {
|
|
2904
4525
|
socketOpen = true;
|
|
4526
|
+
connectionLive();
|
|
2905
4527
|
generation += 1;
|
|
4528
|
+
reads.opened();
|
|
2906
4529
|
conversations.reopened();
|
|
2907
4530
|
recovery.resume();
|
|
2908
4531
|
void sync('opened');
|
|
2909
4532
|
}
|
|
2910
4533
|
else if (event.kind === 'closed')
|
|
2911
|
-
closedSocket(event.revoked);
|
|
4534
|
+
closedSocket(event.revoked, event.lost);
|
|
2912
4535
|
else if (event.kind === 'opening')
|
|
2913
4536
|
void applyOpening(event.expired);
|
|
4537
|
+
else if (event.kind === 'authority-changed')
|
|
4538
|
+
void rediscover(event.scope);
|
|
2914
4539
|
else
|
|
2915
4540
|
void applyRoom(event.locator, event.roomVersion);
|
|
2916
|
-
}, receipts);
|
|
4541
|
+
}, receipts, heldRevisions);
|
|
4542
|
+
}
|
|
4543
|
+
async function heldRevisions(locator, paths) {
|
|
4544
|
+
const held = replicas.get(locator);
|
|
4545
|
+
if (held === undefined || paths.length === 0)
|
|
4546
|
+
return new Map();
|
|
4547
|
+
const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
|
|
4548
|
+
return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
|
|
2917
4549
|
}
|
|
2918
4550
|
function writesHeld() {
|
|
2919
4551
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -2965,7 +4597,7 @@ export function composeRuntime(options) {
|
|
|
2965
4597
|
if (!live())
|
|
2966
4598
|
return;
|
|
2967
4599
|
await conversations.reconcile();
|
|
2968
|
-
if (conversations.current()
|
|
4600
|
+
if (conversations.current()) {
|
|
2969
4601
|
show();
|
|
2970
4602
|
return;
|
|
2971
4603
|
}
|
|
@@ -2983,10 +4615,12 @@ export function composeRuntime(options) {
|
|
|
2983
4615
|
if (closed)
|
|
2984
4616
|
return;
|
|
2985
4617
|
closed = true;
|
|
2986
|
-
if (narrowing !== null)
|
|
2987
|
-
clearTimeout(narrowing);
|
|
2988
4618
|
if (rechecking !== null)
|
|
2989
4619
|
clearInterval(rechecking);
|
|
4620
|
+
for (const resolve of shownWaiters.splice(0))
|
|
4621
|
+
resolve();
|
|
4622
|
+
connectionLive();
|
|
4623
|
+
lockListeners.clear();
|
|
2990
4624
|
readinessWatches.clear();
|
|
2991
4625
|
withdrawStream?.();
|
|
2992
4626
|
withdrawStream = undefined;
|
|
@@ -2995,17 +4629,13 @@ export function composeRuntime(options) {
|
|
|
2995
4629
|
withdrawServiceMode?.();
|
|
2996
4630
|
conversations.close();
|
|
2997
4631
|
void recovery.retire();
|
|
2998
|
-
store?.close();
|
|
2999
|
-
void opening?.then((opened) => {
|
|
3000
|
-
opened.close();
|
|
3001
|
-
}, () => { });
|
|
3002
4632
|
roomListeners.clear();
|
|
3003
4633
|
refusalListeners.clear();
|
|
3004
4634
|
storageLossListeners.clear();
|
|
3005
4635
|
statusListeners.clear();
|
|
3006
4636
|
serviceModeListeners.clear();
|
|
3007
4637
|
memberListeners.clear();
|
|
3008
|
-
|
|
4638
|
+
liveQueries.close();
|
|
3009
4639
|
queueSubscriptions.clear();
|
|
3010
4640
|
roomDraftListeners.clear();
|
|
3011
4641
|
presentationListeners.clear();
|
|
@@ -3032,71 +4662,98 @@ export function composeRuntime(options) {
|
|
|
3032
4662
|
if (options.adapter.roomStream === undefined)
|
|
3033
4663
|
void sync('unstreamed');
|
|
3034
4664
|
function planned(input, reach) {
|
|
3035
|
-
|
|
4665
|
+
const plan = planOf(input, roomFacts(reach), planReach(reach));
|
|
4666
|
+
const named = plan.paths !== null &&
|
|
4667
|
+
plan.rooms !== null &&
|
|
4668
|
+
plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
|
|
4669
|
+
return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
|
|
3036
4670
|
}
|
|
3037
4671
|
function watchPlan(plan, deliver) {
|
|
3038
|
-
|
|
4672
|
+
let seeded = false;
|
|
4673
|
+
let pending = true;
|
|
4674
|
+
const shown = () => {
|
|
4675
|
+
const seed = seeds.get(plan.key);
|
|
4676
|
+
if (seed === undefined || seeded || !pending)
|
|
4677
|
+
return;
|
|
4678
|
+
seeded = true;
|
|
4679
|
+
deliver(seededResult(seed));
|
|
4680
|
+
};
|
|
4681
|
+
const watchers = seedWatchers.get(plan.key) ?? new Set();
|
|
4682
|
+
seedWatchers.set(plan.key, watchers);
|
|
4683
|
+
watchers.add(shown);
|
|
4684
|
+
const stop = liveQueries.watch(plan, identityOf(plan), () => {
|
|
3039
4685
|
const cover = covered(plan);
|
|
3040
4686
|
awaitPreparation(cover);
|
|
3041
|
-
return cover;
|
|
3042
|
-
},
|
|
4687
|
+
return seedBehind(plan, cover);
|
|
4688
|
+
}, (message) => {
|
|
4689
|
+
const answered = message;
|
|
4690
|
+
pending = whole(answered) && 'pending' in answered.result;
|
|
4691
|
+
if (pending && seeds.has(plan.key)) {
|
|
4692
|
+
shown();
|
|
4693
|
+
return;
|
|
4694
|
+
}
|
|
4695
|
+
seeded = false;
|
|
4696
|
+
deliver(message);
|
|
4697
|
+
});
|
|
4698
|
+
return () => {
|
|
4699
|
+
watchers.delete(shown);
|
|
4700
|
+
if (watchers.size === 0)
|
|
4701
|
+
seedWatchers.delete(plan.key);
|
|
4702
|
+
stop();
|
|
4703
|
+
};
|
|
3043
4704
|
}
|
|
3044
|
-
async function searched(input, reach) {
|
|
3045
|
-
const
|
|
3046
|
-
const filter =
|
|
4705
|
+
async function searched(input, reach, locked) {
|
|
4706
|
+
const asked = searchPlanOf(input);
|
|
4707
|
+
const filter = {
|
|
4708
|
+
...filterOf(asked.documents ?? {}, roomFacts(reach), planReach(reach)),
|
|
4709
|
+
locked,
|
|
4710
|
+
};
|
|
4711
|
+
const unlocking = (filter.rooms ?? [])
|
|
4712
|
+
.filter((room) => locked.includes(room))
|
|
4713
|
+
.map((room) => ({ room, awaits: 'unlock' }));
|
|
3047
4714
|
for (;;) {
|
|
3048
4715
|
const cover = covered(filter);
|
|
3049
4716
|
awaitPreparation(cover);
|
|
3050
|
-
const pending = 'pending' in cover ?
|
|
3051
|
-
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
3052
|
-
const held = covered({
|
|
3053
|
-
...filter,
|
|
3054
|
-
rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
|
|
3055
|
-
});
|
|
3056
|
-
const covering = 'pending' in held ? [] : held.coverage.rooms;
|
|
3057
|
-
const unindexed = covering
|
|
3058
|
-
.filter(({ room }) => !searchIndex.rooms().has(room))
|
|
3059
|
-
.map(({ room }) => room);
|
|
3060
|
-
for (const room of unindexed)
|
|
3061
|
-
pending.push({ room, awaits: 'index' });
|
|
3062
|
-
const listed = conversations.searchable();
|
|
3063
|
-
const named = reach !== null && !reach.conversations
|
|
3064
|
-
? []
|
|
3065
|
-
: (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
|
|
3066
|
-
const waiting = named.filter((one) => !searchIndex.conversations().has(one));
|
|
3067
|
-
for (const conversation of waiting)
|
|
3068
|
-
pending.push({ conversation, awaits: 'index' });
|
|
3069
|
-
if (unindexed.length > 0 || waiting.length > 0)
|
|
3070
|
-
index(unindexed, waiting);
|
|
4717
|
+
const pending = [...unlocking, ...('pending' in cover ? cover.pending : [])];
|
|
3071
4718
|
const complete = pending.length === 0 && !('pending' in cover);
|
|
4719
|
+
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
3072
4720
|
const answering = complete
|
|
3073
4721
|
? filter
|
|
3074
|
-
: {
|
|
3075
|
-
...filter,
|
|
3076
|
-
rooms: covering
|
|
3077
|
-
.filter(({ room }) => searchIndex.rooms().has(room))
|
|
3078
|
-
.map(({ room }) => room),
|
|
3079
|
-
};
|
|
4722
|
+
: { ...filter, rooms: reachedRooms(filter).filter((room) => !awaited.has(room)) };
|
|
3080
4723
|
const answered = covered(answering);
|
|
3081
4724
|
if ('pending' in answered)
|
|
3082
4725
|
continue;
|
|
3083
|
-
const
|
|
3084
|
-
const
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
4726
|
+
const listed = conversations.searchable();
|
|
4727
|
+
const named = reach !== null && !reach.conversations
|
|
4728
|
+
? []
|
|
4729
|
+
: (asked.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
|
|
4730
|
+
const hits = await (await openLocal()).search({
|
|
4731
|
+
text: asked.text,
|
|
4732
|
+
limit: capped(asked.limit),
|
|
4733
|
+
after: asked.after,
|
|
4734
|
+
total: asked.total,
|
|
4735
|
+
documents: {
|
|
4736
|
+
plan: answering,
|
|
4737
|
+
rooms: answered.rooms,
|
|
4738
|
+
beneath: asked.collection
|
|
4739
|
+
? answered.rooms.map(({ locator, key }) => ({
|
|
4740
|
+
key,
|
|
4741
|
+
roots: replicas.get(locator)?.record.typedSelection?.roots ?? [],
|
|
4742
|
+
}))
|
|
4743
|
+
: [],
|
|
4744
|
+
},
|
|
4745
|
+
conversations: named,
|
|
4746
|
+
});
|
|
4747
|
+
if (JSON.stringify(covered(answering)) !== JSON.stringify(answered))
|
|
3091
4748
|
continue;
|
|
3092
|
-
const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, complete ? null : new Set(answering.rooms));
|
|
3093
4749
|
const coverage = {
|
|
3094
4750
|
rooms: answered.coverage.rooms,
|
|
3095
|
-
conversations:
|
|
3096
|
-
index: searchIndex.generation(),
|
|
4751
|
+
conversations: named,
|
|
3097
4752
|
overlay: answered.coverage.overlay,
|
|
3098
4753
|
};
|
|
3099
|
-
return complete
|
|
4754
|
+
return complete
|
|
4755
|
+
? { ...hits, coverage }
|
|
4756
|
+
: { pending, documents: hits.documents, messages: hits.messages, coverage };
|
|
3100
4757
|
}
|
|
3101
4758
|
}
|
|
3102
4759
|
const watch = (topic, input, deliver) => {
|
|
@@ -3105,13 +4762,17 @@ export function composeRuntime(options) {
|
|
|
3105
4762
|
roomDraftListeners.add(listener);
|
|
3106
4763
|
void exclusive(async () => {
|
|
3107
4764
|
await openLocal();
|
|
3108
|
-
if (closed || !roomDraftListeners.has(listener))
|
|
3109
|
-
return;
|
|
3110
|
-
listener.ready = true;
|
|
3111
4765
|
}).then(() => {
|
|
3112
|
-
if (closed || !
|
|
4766
|
+
if (closed || !roomDraftListeners.has(listener))
|
|
3113
4767
|
return;
|
|
3114
|
-
|
|
4768
|
+
const snapshot = snapshotRoomDrafts();
|
|
4769
|
+
draftsShown = draftsShown.then(async () => {
|
|
4770
|
+
const value = await snapshot.catch(() => null);
|
|
4771
|
+
if (value === null || closed || !roomDraftListeners.has(listener))
|
|
4772
|
+
return;
|
|
4773
|
+
listener.ready = true;
|
|
4774
|
+
listener.notify(value);
|
|
4775
|
+
});
|
|
3115
4776
|
}, () => { });
|
|
3116
4777
|
return () => {
|
|
3117
4778
|
roomDraftListeners.delete(listener);
|
|
@@ -3206,7 +4867,7 @@ export function composeRuntime(options) {
|
|
|
3206
4867
|
return listed();
|
|
3207
4868
|
},
|
|
3208
4869
|
query: ((input) => rowsQuery(planned(input, null))),
|
|
3209
|
-
search: (input) => searched(input, null),
|
|
4870
|
+
search: (input) => searched(input, null, []),
|
|
3210
4871
|
types(locator) {
|
|
3211
4872
|
return {
|
|
3212
4873
|
collection: modelTypes(modelOf(locator)),
|
|
@@ -3226,9 +4887,9 @@ export function composeRuntime(options) {
|
|
|
3226
4887
|
return listed;
|
|
3227
4888
|
});
|
|
3228
4889
|
},
|
|
3229
|
-
read(room, path) {
|
|
4890
|
+
async read(room, path) {
|
|
3230
4891
|
const record = roomDrafts.get(room)?.get(path);
|
|
3231
|
-
return record === undefined ? null :
|
|
4892
|
+
return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
|
|
3232
4893
|
},
|
|
3233
4894
|
write(room, path, revision, input) {
|
|
3234
4895
|
return exclusive(async () => {
|
|
@@ -3238,11 +4899,11 @@ export function composeRuntime(options) {
|
|
|
3238
4899
|
const local = await openLocal();
|
|
3239
4900
|
const current = roomDrafts.get(room)?.get(path);
|
|
3240
4901
|
if ((current?.revision ?? null) !== revision) {
|
|
3241
|
-
return staleWrite(current === undefined ? null :
|
|
4902
|
+
return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
|
|
3242
4903
|
}
|
|
3243
4904
|
const file = parts.file === undefined
|
|
3244
4905
|
? undefined
|
|
3245
|
-
: await heldFile(parts.file.blob, binaryBase(
|
|
4906
|
+
: await heldFile(parts.file.blob, binaryBase(await visibleOf(room, [path]), path));
|
|
3246
4907
|
const record = {
|
|
3247
4908
|
locator: room,
|
|
3248
4909
|
path,
|
|
@@ -3250,10 +4911,12 @@ export function composeRuntime(options) {
|
|
|
3250
4911
|
parts: held(current?.parts, parts, file),
|
|
3251
4912
|
returned: current?.returned ?? null,
|
|
3252
4913
|
};
|
|
3253
|
-
await local.keepRoomDraft(record
|
|
4914
|
+
await local.keepRoomDraft(record, parts.file === undefined || file === undefined
|
|
4915
|
+
? []
|
|
4916
|
+
: [{ key: file.localId, blob: parts.file.blob }]);
|
|
3254
4917
|
rememberRoomDraft(record);
|
|
3255
|
-
publishChangeSet(changeSet(room,
|
|
3256
|
-
return { outcome: 'written', roomDraft:
|
|
4918
|
+
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
4919
|
+
return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
|
|
3257
4920
|
});
|
|
3258
4921
|
},
|
|
3259
4922
|
discard(room, path, revision) {
|
|
@@ -3261,11 +4924,11 @@ export function composeRuntime(options) {
|
|
|
3261
4924
|
const local = await openLocal();
|
|
3262
4925
|
const current = roomDrafts.get(room)?.get(path);
|
|
3263
4926
|
if (current === undefined || current.revision !== revision) {
|
|
3264
|
-
return staleWrite(current === undefined ? null :
|
|
4927
|
+
return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
|
|
3265
4928
|
}
|
|
3266
4929
|
await local.discardRoomDraft(room, path);
|
|
3267
4930
|
forgetRoomDraft(room, path);
|
|
3268
|
-
publishChangeSet(changeSet(room,
|
|
4931
|
+
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
3269
4932
|
moveTo({ ...status, ...attention() });
|
|
3270
4933
|
return { outcome: 'written', roomDraft: null };
|
|
3271
4934
|
});
|
|
@@ -3281,29 +4944,39 @@ export function composeRuntime(options) {
|
|
|
3281
4944
|
});
|
|
3282
4945
|
},
|
|
3283
4946
|
write(app, value) {
|
|
3284
|
-
return
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
4947
|
+
return keepPresentation(app, () => structuredClone(value));
|
|
4948
|
+
},
|
|
4949
|
+
edit(app, members) {
|
|
4950
|
+
return keepPresentation(app, (held) => Object.fromEntries([
|
|
4951
|
+
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(members, member)),
|
|
4952
|
+
...Object.entries(structuredClone(members)).filter(([, value]) => value !== null),
|
|
4953
|
+
]));
|
|
3291
4954
|
},
|
|
3292
4955
|
},
|
|
3293
4956
|
save(room, named, writtenBy) {
|
|
3294
4957
|
return exclusive(() => saveRoomDrafts(room, named, writtenBy));
|
|
3295
4958
|
},
|
|
4959
|
+
saveParts(room, parts, writtenBy) {
|
|
4960
|
+
return exclusive(() => saveParts(room, parts, writtenBy));
|
|
4961
|
+
},
|
|
3296
4962
|
history(locator, path, query) {
|
|
3297
4963
|
return readHistory(locator, path, query);
|
|
3298
4964
|
},
|
|
4965
|
+
resolve(room, path, destination) {
|
|
4966
|
+
return resolveReference(room, path, destination);
|
|
4967
|
+
},
|
|
3299
4968
|
binaryFile: {
|
|
4969
|
+
part: readPart,
|
|
3300
4970
|
read: readBinaryPath,
|
|
4971
|
+
link: linkBinaryPath,
|
|
3301
4972
|
readReference: readBinaryReference,
|
|
3302
4973
|
},
|
|
3303
|
-
call(name, input) {
|
|
3304
|
-
|
|
4974
|
+
async call(name, input) {
|
|
4975
|
+
await ready;
|
|
4976
|
+
const room = calledRooms(name, input).find((one) => locked(one));
|
|
4977
|
+
return room === undefined ? recoveredAction(name, input) : lockedRoom(room);
|
|
3305
4978
|
},
|
|
3306
|
-
reached(locator, roomVersion, path) {
|
|
4979
|
+
async reached(locator, roomVersion, path) {
|
|
3307
4980
|
return path === undefined
|
|
3308
4981
|
? reachedBy(locator, roomVersion)
|
|
3309
4982
|
: writtenAfter(locator, roomVersion, path);
|
|
@@ -3311,12 +4984,13 @@ export function composeRuntime(options) {
|
|
|
3311
4984
|
log() {
|
|
3312
4985
|
return journal.export();
|
|
3313
4986
|
},
|
|
4987
|
+
cleanUp,
|
|
3314
4988
|
persistence() {
|
|
3315
4989
|
return persistence === 'granted';
|
|
3316
4990
|
},
|
|
3317
4991
|
async retry() {
|
|
3318
4992
|
journal.record('retry');
|
|
3319
|
-
|
|
4993
|
+
unpreparable.clear();
|
|
3320
4994
|
queueReadiness();
|
|
3321
4995
|
resume();
|
|
3322
4996
|
},
|
|
@@ -3346,13 +5020,17 @@ export function composeRuntime(options) {
|
|
|
3346
5020
|
watchPlan,
|
|
3347
5021
|
search: searched,
|
|
3348
5022
|
watchReadiness,
|
|
5023
|
+
locked,
|
|
5024
|
+
lockedRooms,
|
|
5025
|
+
watchLocks,
|
|
5026
|
+
located,
|
|
3349
5027
|
};
|
|
3350
5028
|
return {
|
|
3351
5029
|
md,
|
|
5030
|
+
seed,
|
|
3352
5031
|
stop,
|
|
3353
5032
|
lose: loseRooms,
|
|
3354
5033
|
watch,
|
|
3355
|
-
acquire,
|
|
3356
5034
|
persisted(answer) {
|
|
3357
5035
|
if (answer === persistence)
|
|
3358
5036
|
return;
|
|
@@ -3360,6 +5038,7 @@ export function composeRuntime(options) {
|
|
|
3360
5038
|
journal.record(`persistence ${answer}`);
|
|
3361
5039
|
queueReadiness();
|
|
3362
5040
|
},
|
|
5041
|
+
shown: screen,
|
|
3363
5042
|
within: (reach, port) => narrowed(reachable, reach, port),
|
|
3364
5043
|
};
|
|
3365
5044
|
}
|