@agentshouse/mdruntime 0.1.0-c5d3fa6fb67bc → 0.1.0-c5f333dfa0583
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 +225 -212
- package/README.md +9 -8
- package/dist/adapter.d.ts +64 -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 +15 -7
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +90 -61
- package/dist/chain.d.ts +11 -0
- package/dist/chain.d.ts.map +1 -0
- package/dist/chain.js +64 -0
- package/dist/change-set.d.ts +8 -9
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +10 -85
- 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 +11 -0
- package/dist/compile.d.ts.map +1 -0
- package/dist/compile.js +712 -0
- package/dist/connection.d.ts +21 -8
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +2319 -688
- package/dist/conversation.d.ts +15 -3
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +233 -80
- package/dist/digest.d.ts +7 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +113 -0
- package/dist/durability.d.ts +2 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/entry.d.ts +3 -1
- package/dist/entry.d.ts.map +1 -1
- package/dist/entry.js +18 -23
- package/dist/file.d.ts +2 -10
- package/dist/file.d.ts.map +1 -1
- package/dist/file.js +3 -120
- package/dist/host.d.ts +5 -4
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +55 -7
- package/dist/index.d.ts +10 -6
- 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/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 +26 -2
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +34 -6
- 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 +1 -1
- package/dist/readiness.d.ts.map +1 -1
- package/dist/reads.d.ts +11 -0
- package/dist/reads.d.ts.map +1 -0
- package/dist/reads.js +98 -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 +12 -15
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +74 -51
- package/dist/replica.d.ts +55 -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 +19 -4
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +192 -35
- 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 +99 -71
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +461 -586
- package/dist/stored.d.ts +143 -0
- package/dist/stored.d.ts.map +1 -0
- package/dist/stored.js +561 -0
- package/dist/tables.d.ts +320 -0
- package/dist/tables.d.ts.map +1 -0
- package/dist/tables.js +537 -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/package.json +10 -3
- 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,36 +1,51 @@
|
|
|
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 {
|
|
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';
|
|
7
9
|
import { createSearchIndex, 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 { fingerprint } from './stored.js';
|
|
32
|
+
import { sameBinary } from './overlay.js';
|
|
33
|
+
import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
|
|
29
34
|
import { identityTime, roomDraftRevision } from './identity.js';
|
|
30
35
|
import { beneath, normalize } from './paths.js';
|
|
31
36
|
import { openRuntimeStore, } from './store.js';
|
|
37
|
+
import { changedCleanly, sameComposition, validatedCollection, } from './validation.js';
|
|
32
38
|
import { unconfirmed } from './transfer.js';
|
|
33
39
|
import { governs, holds, narrowed, planReach, } from './reach.js';
|
|
40
|
+
function roomsAlone(plan) {
|
|
41
|
+
return (plan.reach === null &&
|
|
42
|
+
plan.root.types === null &&
|
|
43
|
+
plan.root.where === null &&
|
|
44
|
+
plan.root.pathPrefix === null &&
|
|
45
|
+
plan.paths === null &&
|
|
46
|
+
plan.hop === null &&
|
|
47
|
+
plan.backlinks === null);
|
|
48
|
+
}
|
|
34
49
|
function within(root, path) {
|
|
35
50
|
return root === '' || beneath(root, path);
|
|
36
51
|
}
|
|
@@ -48,19 +63,18 @@ const OPENING_LANE = 'opening';
|
|
|
48
63
|
const CONVERSATION_LANE = 'conversations';
|
|
49
64
|
const ROOM_READS = 256;
|
|
50
65
|
const ROOM_ATTEMPTS = 16;
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return made;
|
|
66
|
+
const SEARCH_BATCH = 256;
|
|
67
|
+
const ERASE_BATCH = 512;
|
|
68
|
+
const REDERIVE_BATCH = 256;
|
|
69
|
+
const WALK_BYTES = 1 << 20;
|
|
70
|
+
const QUERY_BYTES = 1 << 20;
|
|
71
|
+
const STAGE_BYTES = 1 << 20;
|
|
72
|
+
const CLEAN_CHANGES = 2_000;
|
|
73
|
+
// 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.
|
|
74
|
+
const LOCK_GRACE_MS = 120_000;
|
|
75
|
+
function operationOf(input) {
|
|
76
|
+
const operation = input?.operation_id;
|
|
77
|
+
return typeof operation === 'string' ? operation : null;
|
|
64
78
|
}
|
|
65
79
|
function abandonedOn(signal) {
|
|
66
80
|
return new Promise((resolve) => {
|
|
@@ -73,8 +87,12 @@ function abandonedOn(signal) {
|
|
|
73
87
|
function roomLane(locator) {
|
|
74
88
|
return `room ${locator}`;
|
|
75
89
|
}
|
|
90
|
+
function pendingKey(locator, id) {
|
|
91
|
+
return `${locator}\u0000${id}`;
|
|
92
|
+
}
|
|
76
93
|
export function composeRuntime(options) {
|
|
77
94
|
const localSaves = options.adapter.localSaves === true;
|
|
95
|
+
const roomTypes = roomTypesOf(options.adapter.roomTypes);
|
|
78
96
|
let authorization = options.authorization;
|
|
79
97
|
let opening;
|
|
80
98
|
let store;
|
|
@@ -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,10 +179,13 @@ 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;
|
|
@@ -167,12 +193,9 @@ export function composeRuntime(options) {
|
|
|
167
193
|
let member = null;
|
|
168
194
|
const memberListeners = new Set();
|
|
169
195
|
const prepared = new Set();
|
|
170
|
-
const projecting = new Set();
|
|
171
196
|
const unpreparedDemand = new Set();
|
|
172
197
|
const unindexable = new Set();
|
|
173
198
|
let preparing = false;
|
|
174
|
-
let demand = WHOLE_DEMAND;
|
|
175
|
-
let narrowing = null;
|
|
176
199
|
let targets = {};
|
|
177
200
|
let persistence = options.shell.persistence;
|
|
178
201
|
let capacity = null;
|
|
@@ -184,24 +207,54 @@ export function composeRuntime(options) {
|
|
|
184
207
|
const transfers = new FencedMap(isFenced);
|
|
185
208
|
const conversations = createConversationStore({
|
|
186
209
|
agents: declaredAgents,
|
|
187
|
-
documents: (
|
|
210
|
+
documents: (declaration, after) => rowsQuery(planOf({
|
|
211
|
+
rooms: [declaration.locator],
|
|
212
|
+
types: [declaration.type],
|
|
213
|
+
select: {
|
|
214
|
+
fields: [
|
|
215
|
+
declaration.identity,
|
|
216
|
+
declaration.name,
|
|
217
|
+
...(declaration.retired === undefined ? [] : [declaration.retired]),
|
|
218
|
+
].map((field) => `/${escapePointer(field)}`),
|
|
219
|
+
},
|
|
220
|
+
limit: MAXIMUM_WINDOW,
|
|
221
|
+
...(after === undefined ? {} : { after }),
|
|
222
|
+
}, roomFacts())),
|
|
188
223
|
call: (name, input) => invokeAction(name, input),
|
|
189
224
|
durably,
|
|
190
225
|
stream: () => options.adapter.conversations,
|
|
191
226
|
publish: publishChangeSet,
|
|
192
|
-
online: () =>
|
|
227
|
+
online: () => connectionOf() !== 'unreachable' && refusal === null,
|
|
193
228
|
closed: () => closed,
|
|
194
229
|
unanswered: resendLater,
|
|
195
230
|
expired: (operation) => pastHorizon(operation),
|
|
196
231
|
});
|
|
197
232
|
const searchIndex = createSearchIndex({
|
|
198
|
-
room
|
|
199
|
-
|
|
233
|
+
async *room(room) {
|
|
234
|
+
const local = await openLocal();
|
|
235
|
+
for (let after = null;;) {
|
|
236
|
+
const key = replicas.get(room)?.record.key;
|
|
237
|
+
if (key === undefined)
|
|
238
|
+
return;
|
|
239
|
+
const page = await local.searched(key, room, after, SEARCH_BATCH, null);
|
|
240
|
+
yield page.documents;
|
|
241
|
+
if (page.next === null)
|
|
242
|
+
return;
|
|
243
|
+
after = page.next;
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
async documents(room, paths) {
|
|
247
|
+
const key = replicas.get(room)?.record.key;
|
|
248
|
+
if (key === undefined || paths.length === 0)
|
|
249
|
+
return [];
|
|
250
|
+
return (await (await openLocal()).searched(key, room, null, paths.length, paths)).documents;
|
|
251
|
+
},
|
|
200
252
|
});
|
|
201
253
|
const roomsToIndex = new Set();
|
|
202
254
|
const conversationsToIndex = new Set();
|
|
203
255
|
let indexing = false;
|
|
204
256
|
const journal = createLog();
|
|
257
|
+
const keptFiles = openedFiles(options.database, isFenced);
|
|
205
258
|
let notifyReady = () => { };
|
|
206
259
|
const ready = new Promise((resolve) => {
|
|
207
260
|
notifyReady = resolve;
|
|
@@ -219,16 +272,36 @@ export function composeRuntime(options) {
|
|
|
219
272
|
if (room.size === 0)
|
|
220
273
|
roomDrafts.delete(locator);
|
|
221
274
|
}
|
|
222
|
-
function
|
|
275
|
+
async function visibleOf(locator, paths) {
|
|
276
|
+
const held = replicas.get(locator);
|
|
277
|
+
if (held === undefined)
|
|
278
|
+
return undefined;
|
|
279
|
+
const local = await openLocal();
|
|
280
|
+
const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
|
|
281
|
+
return { ...slice, collection: collectionOf(slice.documents, compositionOf(held.record)) };
|
|
282
|
+
}
|
|
283
|
+
function roomDraftOf(record, visible) {
|
|
223
284
|
return structuredClone({
|
|
224
285
|
room: record.locator,
|
|
225
286
|
path: record.path,
|
|
226
287
|
revision: record.revision,
|
|
227
288
|
parts: record.parts,
|
|
228
|
-
changedUnderneath: underneath(record.parts, record.path,
|
|
289
|
+
changedUnderneath: underneath(record.parts, record.path, visible),
|
|
229
290
|
returned: record.returned,
|
|
230
291
|
});
|
|
231
292
|
}
|
|
293
|
+
async function roomDraftsOf(records) {
|
|
294
|
+
const rooms = new Map();
|
|
295
|
+
for (const record of records) {
|
|
296
|
+
const paths = rooms.get(record.locator) ?? [];
|
|
297
|
+
paths.push(record.path);
|
|
298
|
+
rooms.set(record.locator, paths);
|
|
299
|
+
}
|
|
300
|
+
const layers = new Map();
|
|
301
|
+
for (const [locator, paths] of rooms)
|
|
302
|
+
layers.set(locator, await visibleOf(locator, paths));
|
|
303
|
+
return records.map((record) => roomDraftOf(record, layers.get(record.locator)));
|
|
304
|
+
}
|
|
232
305
|
function snapshotRoomDrafts() {
|
|
233
306
|
const records = [...roomDrafts.values()].flatMap((room) => [...room.values()]);
|
|
234
307
|
records.sort((left, right) => {
|
|
@@ -238,23 +311,38 @@ export function composeRuntime(options) {
|
|
|
238
311
|
return left.path < right.path ? -1 : 1;
|
|
239
312
|
return 0;
|
|
240
313
|
});
|
|
241
|
-
return records
|
|
314
|
+
return roomDraftsOf(records);
|
|
242
315
|
}
|
|
316
|
+
let draftsShown = Promise.resolve();
|
|
243
317
|
function publishRoomDrafts(waitingOnly) {
|
|
244
318
|
if (closed)
|
|
245
319
|
return;
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
listener
|
|
252
|
-
|
|
320
|
+
const snapshot = snapshotRoomDrafts();
|
|
321
|
+
draftsShown = draftsShown.then(async () => {
|
|
322
|
+
const value = await snapshot.catch(() => null);
|
|
323
|
+
if (value === null || closed)
|
|
324
|
+
return;
|
|
325
|
+
for (const listener of roomDraftListeners) {
|
|
326
|
+
if (waitingOnly && listener.ready)
|
|
327
|
+
continue;
|
|
328
|
+
listener.ready = true;
|
|
329
|
+
listener.notify(value);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
253
332
|
}
|
|
254
333
|
function presentationOf(app) {
|
|
255
334
|
const value = presentations.get(app);
|
|
256
335
|
return value === undefined ? null : structuredClone(value);
|
|
257
336
|
}
|
|
337
|
+
function keepPresentation(app, next) {
|
|
338
|
+
return exclusive(async () => {
|
|
339
|
+
const local = await openLocal();
|
|
340
|
+
const stored = next(presentations.get(app));
|
|
341
|
+
await local.present({ app, value: stored });
|
|
342
|
+
presentations.set(app, stored);
|
|
343
|
+
publishChangeSet(changeSet('', { presentation: app }));
|
|
344
|
+
});
|
|
345
|
+
}
|
|
258
346
|
function emitPresentation(app) {
|
|
259
347
|
if (closed)
|
|
260
348
|
return;
|
|
@@ -303,21 +391,69 @@ export function composeRuntime(options) {
|
|
|
303
391
|
return { attentionRequired, reset };
|
|
304
392
|
}
|
|
305
393
|
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));
|
|
394
|
+
return replicas.get(locator)?.count ?? 0;
|
|
313
395
|
}
|
|
314
396
|
function listed() {
|
|
315
397
|
return structuredClone([...rooms.values()].map((room) => {
|
|
316
398
|
const documents = documentsIn(room.locator);
|
|
317
399
|
counted.set(room.locator, documents);
|
|
318
|
-
return
|
|
400
|
+
return locked(room.locator)
|
|
401
|
+
? {
|
|
402
|
+
...room,
|
|
403
|
+
model: modelTypes(NO_MODEL),
|
|
404
|
+
typedSelection: null,
|
|
405
|
+
documents: 0,
|
|
406
|
+
locked: true,
|
|
407
|
+
}
|
|
408
|
+
: { ...room, documents, locked: false };
|
|
319
409
|
}));
|
|
320
410
|
}
|
|
411
|
+
function locked(locator) {
|
|
412
|
+
return rooms.get(locator)?.replica.offline === 'locked' && !confirmed.has(locator);
|
|
413
|
+
}
|
|
414
|
+
function lockedRooms() {
|
|
415
|
+
return [...rooms.keys()].filter(locked).sort();
|
|
416
|
+
}
|
|
417
|
+
function relocked(before) {
|
|
418
|
+
const after = lockedRooms();
|
|
419
|
+
if (JSON.stringify(after) === JSON.stringify(before))
|
|
420
|
+
return;
|
|
421
|
+
journal.record(`locked ${after.length}`);
|
|
422
|
+
emitRooms();
|
|
423
|
+
for (const notify of [...lockListeners])
|
|
424
|
+
notify();
|
|
425
|
+
}
|
|
426
|
+
function confirmLocks(listed) {
|
|
427
|
+
if (!socketOpen)
|
|
428
|
+
return;
|
|
429
|
+
for (const room of listed) {
|
|
430
|
+
if (room.replica.admission === 'admitted')
|
|
431
|
+
confirmed.add(room.locator);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function connectionLost() {
|
|
435
|
+
if (lockGrace !== null || closed)
|
|
436
|
+
return;
|
|
437
|
+
lockGrace = setTimeout(() => {
|
|
438
|
+
lockGrace = null;
|
|
439
|
+
if (socketOpen || closed)
|
|
440
|
+
return;
|
|
441
|
+
const before = lockedRooms();
|
|
442
|
+
confirmed.clear();
|
|
443
|
+
relocked(before);
|
|
444
|
+
}, LOCK_GRACE_MS);
|
|
445
|
+
}
|
|
446
|
+
function connectionLive() {
|
|
447
|
+
if (lockGrace !== null)
|
|
448
|
+
clearTimeout(lockGrace);
|
|
449
|
+
lockGrace = null;
|
|
450
|
+
}
|
|
451
|
+
function watchLocks(notify) {
|
|
452
|
+
lockListeners.add(notify);
|
|
453
|
+
return () => {
|
|
454
|
+
lockListeners.delete(notify);
|
|
455
|
+
};
|
|
456
|
+
}
|
|
321
457
|
function roomFacts(reach = null) {
|
|
322
458
|
const held = [...rooms.values()]
|
|
323
459
|
.filter((room) => holds(reach, room.locator))
|
|
@@ -336,7 +472,7 @@ export function composeRuntime(options) {
|
|
|
336
472
|
return { listed: roomsListed, rooms: held };
|
|
337
473
|
}
|
|
338
474
|
function emitRooms() {
|
|
339
|
-
|
|
475
|
+
liveQueries.recover();
|
|
340
476
|
queueReadiness();
|
|
341
477
|
const value = listed();
|
|
342
478
|
for (const listener of roomListeners)
|
|
@@ -366,7 +502,7 @@ export function composeRuntime(options) {
|
|
|
366
502
|
if (closed)
|
|
367
503
|
return;
|
|
368
504
|
queueReadiness();
|
|
369
|
-
const next = status.state === 'synced' &&
|
|
505
|
+
const next = status.state === 'synced' && !conversations.current()
|
|
370
506
|
? { ...status, state: 'syncing' }
|
|
371
507
|
: status;
|
|
372
508
|
if (same(shown, next))
|
|
@@ -375,17 +511,17 @@ export function composeRuntime(options) {
|
|
|
375
511
|
for (const listener of statusListeners)
|
|
376
512
|
listener(shown);
|
|
377
513
|
}
|
|
378
|
-
function entryOf(batch) {
|
|
514
|
+
function entryOf(locator, batch) {
|
|
379
515
|
if (localSaves)
|
|
380
516
|
return Object.freeze({ batch, state: 'local' });
|
|
381
|
-
const state = pendings.get(batch.id);
|
|
517
|
+
const state = pendings.get(pendingKey(locator, batch.id));
|
|
382
518
|
if (state?.accepted !== undefined) {
|
|
383
519
|
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
384
520
|
}
|
|
385
521
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
386
522
|
}
|
|
387
523
|
function entriesOf(locator) {
|
|
388
|
-
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
|
|
524
|
+
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map((batch) => entryOf(locator, batch));
|
|
389
525
|
}
|
|
390
526
|
function emitQueue(locator) {
|
|
391
527
|
const entries = entriesOf(locator);
|
|
@@ -395,55 +531,29 @@ export function composeRuntime(options) {
|
|
|
395
531
|
}
|
|
396
532
|
}
|
|
397
533
|
function applyChangeSet(set) {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
|
|
401
|
-
};
|
|
402
|
-
readSide.apply(held);
|
|
403
|
-
searchIndex.changed(held);
|
|
534
|
+
liveQueries.touched(set);
|
|
535
|
+
searchIndex.changed(set);
|
|
404
536
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const record = replicas.get(locator)?.confirmed.record;
|
|
408
|
-
if (record === undefined || closed)
|
|
409
|
-
return;
|
|
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
|
-
}
|
|
537
|
+
function unpublished(error) {
|
|
538
|
+
journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
|
|
416
539
|
}
|
|
417
540
|
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)
|
|
541
|
+
for (;;) {
|
|
542
|
+
const held = replicas.get(locator);
|
|
543
|
+
if (held === undefined || closed || prepared.has(locator))
|
|
438
544
|
return;
|
|
545
|
+
const key = held.record.key;
|
|
546
|
+
const done = await exclusive(async () => (await openLocal()).rederive(key, REDERIVE_BATCH));
|
|
547
|
+
if (replicas.get(locator)?.record.key !== key)
|
|
548
|
+
continue;
|
|
549
|
+
if (!done)
|
|
550
|
+
continue;
|
|
439
551
|
prepared.add(locator);
|
|
552
|
+
publishChangeSet(changeSet(locator, { documents: null }));
|
|
553
|
+
liveQueries.recover();
|
|
554
|
+
queueReadiness();
|
|
555
|
+
return;
|
|
440
556
|
}
|
|
441
|
-
finally {
|
|
442
|
-
projecting.delete(locator);
|
|
443
|
-
}
|
|
444
|
-
conversations.changed(mergeChangeSets(entered));
|
|
445
|
-
readSide.recover();
|
|
446
|
-
queueReadiness();
|
|
447
557
|
}
|
|
448
558
|
function unprepared() {
|
|
449
559
|
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
|
|
@@ -474,17 +584,22 @@ export function composeRuntime(options) {
|
|
|
474
584
|
}
|
|
475
585
|
})();
|
|
476
586
|
}
|
|
587
|
+
function roomsOfReach(reach) {
|
|
588
|
+
return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
589
|
+
}
|
|
477
590
|
function reachedRooms(plan) {
|
|
478
|
-
const
|
|
479
|
-
const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
591
|
+
const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
|
|
480
592
|
const named = plan.rooms ??
|
|
481
593
|
(reached === null
|
|
482
594
|
? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
|
|
483
595
|
: [...reached]);
|
|
484
|
-
return [...new Set(named)]
|
|
596
|
+
return [...new Set(named)]
|
|
597
|
+
.filter((locator) => (reached === null || reached.has(locator)) && !plan.locked.includes(locator))
|
|
598
|
+
.sort();
|
|
485
599
|
}
|
|
486
600
|
function awaited(locator) {
|
|
487
|
-
return
|
|
601
|
+
return (!roomsListed ||
|
|
602
|
+
(rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
|
|
488
603
|
}
|
|
489
604
|
function covered(plan) {
|
|
490
605
|
const locators = reachedRooms(plan);
|
|
@@ -494,6 +609,7 @@ export function composeRuntime(options) {
|
|
|
494
609
|
const pending = [];
|
|
495
610
|
const coverage = [];
|
|
496
611
|
const overlay = [];
|
|
612
|
+
const read = [];
|
|
497
613
|
for (const locator of locators) {
|
|
498
614
|
const replica = replicas.get(locator);
|
|
499
615
|
if (replica === undefined) {
|
|
@@ -507,15 +623,16 @@ export function composeRuntime(options) {
|
|
|
507
623
|
}
|
|
508
624
|
coverage.push({
|
|
509
625
|
room: locator,
|
|
510
|
-
generation: replica.
|
|
626
|
+
generation: replica.record.transfer,
|
|
511
627
|
current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
|
|
512
628
|
});
|
|
629
|
+
read.push({ locator, key: replica.record.key });
|
|
513
630
|
for (const batch of replica.queue)
|
|
514
631
|
overlay.push(batch.id);
|
|
515
632
|
}
|
|
516
633
|
if (pending.length > 0)
|
|
517
634
|
return { pending };
|
|
518
|
-
return { coverage: { rooms: coverage, indexes: [], overlay } };
|
|
635
|
+
return { coverage: { rooms: coverage, indexes: [], overlay }, rooms: read };
|
|
519
636
|
}
|
|
520
637
|
function awaitPreparation(cover) {
|
|
521
638
|
if (!('pending' in cover))
|
|
@@ -524,61 +641,11 @@ export function composeRuntime(options) {
|
|
|
524
641
|
if (waiting.length > 0)
|
|
525
642
|
prepare(waiting.map(({ room }) => room));
|
|
526
643
|
}
|
|
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) {
|
|
644
|
+
function requiredRooms(given) {
|
|
645
|
+
const reach = planReach(given);
|
|
579
646
|
if (reach === null)
|
|
580
647
|
return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
|
|
581
|
-
const named = [...
|
|
648
|
+
const named = [...roomsOfReach(reach)].sort();
|
|
582
649
|
if (roomsListed)
|
|
583
650
|
return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
|
|
584
651
|
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
@@ -677,7 +744,7 @@ export function composeRuntime(options) {
|
|
|
677
744
|
function establish(name) {
|
|
678
745
|
targets = { ...targets, [name]: { established: true } };
|
|
679
746
|
queueReadiness();
|
|
680
|
-
void durably((local) => local.
|
|
747
|
+
void durably((local) => local.fact('targets', targets)).catch(() => {
|
|
681
748
|
journal.record(`target record failed ${name}`);
|
|
682
749
|
});
|
|
683
750
|
}
|
|
@@ -714,18 +781,12 @@ export function composeRuntime(options) {
|
|
|
714
781
|
readinessWatches.delete(watch);
|
|
715
782
|
};
|
|
716
783
|
}
|
|
717
|
-
function
|
|
784
|
+
function pageBytes(page) {
|
|
718
785
|
let bytes = 0;
|
|
719
|
-
for (const
|
|
720
|
-
bytes += (path.length +
|
|
786
|
+
for (const { path, content } of page.entries)
|
|
787
|
+
bytes += (path.length + (content?.length ?? 0)) * 2;
|
|
721
788
|
return bytes;
|
|
722
789
|
}
|
|
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
790
|
function exceeded(error) {
|
|
730
791
|
return (typeof error === 'object' &&
|
|
731
792
|
error !== null &&
|
|
@@ -733,7 +794,7 @@ export function composeRuntime(options) {
|
|
|
733
794
|
}
|
|
734
795
|
async function estimated() {
|
|
735
796
|
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
736
|
-
return estimate === null ? null : Math.max(0, estimate.
|
|
797
|
+
return estimate === null ? null : Math.max(0, estimate.total - estimate.usage);
|
|
737
798
|
}
|
|
738
799
|
function pause(locator, cause) {
|
|
739
800
|
paused.add(locator);
|
|
@@ -754,7 +815,8 @@ export function composeRuntime(options) {
|
|
|
754
815
|
const available = await estimated().catch(() => null);
|
|
755
816
|
if (available === null || available < (held.needed ?? 0))
|
|
756
817
|
return;
|
|
757
|
-
if (held.cause === '
|
|
818
|
+
if (held.cause === 'storage-exceeded' &&
|
|
819
|
+
(held.available === null || available <= held.available))
|
|
758
820
|
return;
|
|
759
821
|
resume();
|
|
760
822
|
}
|
|
@@ -772,10 +834,7 @@ export function composeRuntime(options) {
|
|
|
772
834
|
void sync('unstreamed');
|
|
773
835
|
}
|
|
774
836
|
async function reclaimed(local) {
|
|
775
|
-
const unreferenced = [...transfers.keys()].filter((locator) =>
|
|
776
|
-
const room = discovered.get(locator);
|
|
777
|
-
return room === undefined || !acquires(room);
|
|
778
|
-
});
|
|
837
|
+
const unreferenced = [...transfers.keys()].filter((locator) => !discovered.has(locator));
|
|
779
838
|
for (const locator of unreferenced) {
|
|
780
839
|
await local.abandon(locator);
|
|
781
840
|
transfers.delete(locator);
|
|
@@ -799,20 +858,156 @@ export function composeRuntime(options) {
|
|
|
799
858
|
}
|
|
800
859
|
async function exceededOn(locator, bytes) {
|
|
801
860
|
const available = await estimated().catch(() => null);
|
|
802
|
-
pause(locator, { cause: '
|
|
861
|
+
pause(locator, { cause: 'storage-exceeded', needed: bytes, available });
|
|
862
|
+
}
|
|
863
|
+
function seedRoomHeld(room, version) {
|
|
864
|
+
return prepared.has(room) && reachedBy(room, version);
|
|
865
|
+
}
|
|
866
|
+
function seedHeld(at) {
|
|
867
|
+
return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
|
|
868
|
+
}
|
|
869
|
+
function seedApplies(plan) {
|
|
870
|
+
const seed = seeds.get(plan.key);
|
|
871
|
+
if (seed === undefined)
|
|
872
|
+
return null;
|
|
873
|
+
if (!seedHeld(seed.at))
|
|
874
|
+
return seed;
|
|
875
|
+
seeds.delete(plan.key);
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
function seedBehind(plan, cover) {
|
|
879
|
+
const seed = seedApplies(plan);
|
|
880
|
+
if (seed === null || 'pending' in cover)
|
|
881
|
+
return cover;
|
|
882
|
+
return {
|
|
883
|
+
pending: Object.entries(seed.at)
|
|
884
|
+
.filter(([room, version]) => !seedRoomHeld(room, version))
|
|
885
|
+
.map(([room]) => ({ room, awaits: 'acquisition' })),
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
function seed(entry) {
|
|
889
|
+
if (seedHeld(entry.at))
|
|
890
|
+
return;
|
|
891
|
+
for (const { query, rows } of entry.queries) {
|
|
892
|
+
const key = queryKey(query);
|
|
893
|
+
seeds.set(key, { rows, at: entry.at });
|
|
894
|
+
for (const shown of seedWatchers.get(key) ?? [])
|
|
895
|
+
shown();
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
function sameCover(left, right) {
|
|
899
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
900
|
+
}
|
|
901
|
+
async function coveredRows(plan, cover) {
|
|
902
|
+
for (;;) {
|
|
903
|
+
const before = cover();
|
|
904
|
+
if ('pending' in before)
|
|
905
|
+
return { pending: before };
|
|
906
|
+
const local = await openLocal();
|
|
907
|
+
const keys = before.rooms.map(({ key }) => key);
|
|
908
|
+
const changes = local.changes(keys);
|
|
909
|
+
const current = () => local.changes(keys) === changes;
|
|
910
|
+
const rows = await windowRows(local, plan, before.rooms, current);
|
|
911
|
+
const after = cover();
|
|
912
|
+
if (rows !== null && sameCover(before, after))
|
|
913
|
+
return { rows, cover: before, current };
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
async function windowRows(local, plan, rooms, unchanged) {
|
|
917
|
+
const first = await local.query(plan, rooms, QUERY_BYTES);
|
|
918
|
+
if (first.more === undefined || !('documents' in first)) {
|
|
919
|
+
const { more: _more, ...rows } = first;
|
|
920
|
+
return rows;
|
|
921
|
+
}
|
|
922
|
+
const documents = [...first.documents];
|
|
923
|
+
for (let more = first.more; more !== undefined;) {
|
|
924
|
+
if (!unchanged())
|
|
925
|
+
return null;
|
|
926
|
+
const page = await local.query({
|
|
927
|
+
...plan,
|
|
928
|
+
after: { text: more, values: JSON.parse(more) },
|
|
929
|
+
limit: plan.limit === null ? null : plan.limit - documents.length,
|
|
930
|
+
total: false,
|
|
931
|
+
}, rooms, QUERY_BYTES);
|
|
932
|
+
if (!('documents' in page))
|
|
933
|
+
return null;
|
|
934
|
+
documents.push(...page.documents);
|
|
935
|
+
more = page.more;
|
|
936
|
+
if (more === undefined) {
|
|
937
|
+
return {
|
|
938
|
+
documents,
|
|
939
|
+
...(page.next === undefined ? {} : { next: page.next }),
|
|
940
|
+
...(first.total === undefined ? {} : { total: first.total }),
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return null;
|
|
945
|
+
}
|
|
946
|
+
function keyedOf(rows, coverage) {
|
|
947
|
+
if ('groups' in rows) {
|
|
948
|
+
return {
|
|
949
|
+
result: { ...rows, coverage },
|
|
950
|
+
keys: rows.groups.map((group) => JSON.stringify(group.key)),
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
return {
|
|
954
|
+
result: { ...rows, coverage },
|
|
955
|
+
keys: rows.documents.map((document) => documentKey(document.room, document.path)),
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
async function answeredPlan(plan, cover) {
|
|
959
|
+
const answered = await coveredRows(plan, cover);
|
|
960
|
+
if ('pending' in answered) {
|
|
961
|
+
const pending = answered.pending;
|
|
962
|
+
return { result: pending, keys: [], current: () => true };
|
|
963
|
+
}
|
|
964
|
+
const held = answered.cover;
|
|
965
|
+
return { ...keyedOf(answered.rows, held.coverage), current: answered.current };
|
|
803
966
|
}
|
|
804
967
|
async function rowsQuery(plan) {
|
|
805
968
|
const before = covered(plan);
|
|
806
969
|
awaitPreparation(before);
|
|
970
|
+
const seeded = seedApplies(plan);
|
|
971
|
+
if (seeded !== null)
|
|
972
|
+
return seededResult(seeded).result;
|
|
807
973
|
if ('pending' in before)
|
|
808
974
|
return before;
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
|
|
975
|
+
const answered = await coveredRows(plan, () => {
|
|
976
|
+
const cover = covered(plan);
|
|
977
|
+
awaitPreparation(cover);
|
|
812
978
|
return cover;
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
979
|
+
});
|
|
980
|
+
if ('pending' in answered)
|
|
981
|
+
return answered.pending;
|
|
982
|
+
const held = answered.cover;
|
|
983
|
+
return { ...answered.rows, coverage: held.coverage };
|
|
984
|
+
}
|
|
985
|
+
async function everyRow(plan) {
|
|
986
|
+
const window = { ...plan, limit: MAXIMUM_WINDOW, after: null, total: false };
|
|
987
|
+
const first = await rowsQuery(window);
|
|
988
|
+
if ('pending' in first || first.next === undefined)
|
|
989
|
+
return first;
|
|
990
|
+
const documents = 'documents' in first ? [...first.documents] : [];
|
|
991
|
+
const groups = 'groups' in first ? [...first.groups] : [];
|
|
992
|
+
for (let next = first.next; next !== undefined;) {
|
|
993
|
+
const values = JSON.parse(next);
|
|
994
|
+
const page = await rowsQuery({
|
|
995
|
+
...window,
|
|
996
|
+
after: { text: next, values: values.length === 0 ? null : values },
|
|
997
|
+
});
|
|
998
|
+
if ('pending' in page)
|
|
999
|
+
return page;
|
|
1000
|
+
if ('documents' in page)
|
|
1001
|
+
documents.push(...page.documents);
|
|
1002
|
+
else
|
|
1003
|
+
groups.push(...page.groups);
|
|
1004
|
+
if (JSON.stringify(page.coverage) !== JSON.stringify(first.coverage))
|
|
1005
|
+
return everyRow(plan);
|
|
1006
|
+
next = page.next;
|
|
1007
|
+
}
|
|
1008
|
+
return 'documents' in first
|
|
1009
|
+
? { documents, coverage: first.coverage }
|
|
1010
|
+
: { groups, coverage: first.coverage };
|
|
816
1011
|
}
|
|
817
1012
|
async function indexConversation(conversation) {
|
|
818
1013
|
searchIndex.indexMessages(conversation, [], false);
|
|
@@ -870,7 +1065,10 @@ export function composeRuntime(options) {
|
|
|
870
1065
|
for (const locator of set.queues)
|
|
871
1066
|
emitQueue(locator);
|
|
872
1067
|
if (set.drafts.length > 0 ||
|
|
873
|
-
set.documents.some((change) =>
|
|
1068
|
+
set.documents.some((change) => {
|
|
1069
|
+
const held = roomDrafts.get(change.locator);
|
|
1070
|
+
return held !== undefined && (change.path === null || held.has(change.path));
|
|
1071
|
+
})) {
|
|
874
1072
|
publishRoomDrafts(false);
|
|
875
1073
|
}
|
|
876
1074
|
for (const app of set.presentations)
|
|
@@ -883,9 +1081,21 @@ export function composeRuntime(options) {
|
|
|
883
1081
|
if (reason === 'revoked-authorization') {
|
|
884
1082
|
await exclusive(async () => {
|
|
885
1083
|
const revoked = { status: 'unavailable', reason: 'revoked' };
|
|
1084
|
+
const records = [...replicas].map(([locator, held]) => ({
|
|
1085
|
+
locator,
|
|
1086
|
+
held,
|
|
1087
|
+
record: { ...held.record, model: revoked, selectionModel: revoked },
|
|
1088
|
+
}));
|
|
886
1089
|
try {
|
|
887
1090
|
const local = await openLocal();
|
|
888
|
-
await local.
|
|
1091
|
+
await local.commit({
|
|
1092
|
+
rooms: records.map(({ locator, record }) => ({
|
|
1093
|
+
locator,
|
|
1094
|
+
key: record.key,
|
|
1095
|
+
record: storedRecord(record),
|
|
1096
|
+
composition: compositionOf(record),
|
|
1097
|
+
})),
|
|
1098
|
+
});
|
|
889
1099
|
}
|
|
890
1100
|
catch {
|
|
891
1101
|
journal.record('model revocation persistence failed');
|
|
@@ -901,16 +1111,16 @@ export function composeRuntime(options) {
|
|
|
901
1111
|
liveModels.delete(key);
|
|
902
1112
|
}
|
|
903
1113
|
const changed = [];
|
|
904
|
-
for (const
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1114
|
+
for (const { locator, held, record } of records) {
|
|
1115
|
+
if (replicas.get(locator) !== held)
|
|
1116
|
+
continue;
|
|
1117
|
+
recomposed(locator, held.record, record);
|
|
1118
|
+
replicas.set(locator, { ...held, record });
|
|
1119
|
+
}
|
|
1120
|
+
for (const locator of [...rooms.keys()]) {
|
|
1121
|
+
const room = rooms.get(locator);
|
|
1122
|
+
if (room === undefined)
|
|
1123
|
+
continue;
|
|
914
1124
|
rooms.set(locator, {
|
|
915
1125
|
...room,
|
|
916
1126
|
model: modelTypes(revoked),
|
|
@@ -918,9 +1128,10 @@ export function composeRuntime(options) {
|
|
|
918
1128
|
? null
|
|
919
1129
|
: { ...room.typedSelection, model: modelTypes(revoked) },
|
|
920
1130
|
});
|
|
921
|
-
changed.push(changeSet(locator,
|
|
1131
|
+
changed.push(changeSet(locator, { documents: null, room: true }));
|
|
922
1132
|
}
|
|
923
1133
|
publishChangeSet(mergeChangeSets(changed));
|
|
1134
|
+
prepare();
|
|
924
1135
|
});
|
|
925
1136
|
}
|
|
926
1137
|
}
|
|
@@ -951,41 +1162,57 @@ export function composeRuntime(options) {
|
|
|
951
1162
|
model: modelTypes(modelOf(room.locator, 'typed-selection')),
|
|
952
1163
|
},
|
|
953
1164
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
954
|
-
roomVersion: replicas.get(room.locator)?.
|
|
1165
|
+
roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
|
|
955
1166
|
synchronizationAvailable,
|
|
956
1167
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
957
1168
|
});
|
|
958
1169
|
}
|
|
959
|
-
async function applyDiscovery(found, started) {
|
|
1170
|
+
async function applyDiscovery(found, started, covered) {
|
|
1171
|
+
const lockedBefore = lockedRooms();
|
|
960
1172
|
for (const room of found) {
|
|
961
1173
|
const fence = fences.get(room.locator);
|
|
962
1174
|
if (fence !== undefined && fence.erased && fence.at < started)
|
|
963
1175
|
fences.delete(room.locator);
|
|
964
1176
|
}
|
|
1177
|
+
await loseRooms(found
|
|
1178
|
+
.filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
|
|
1179
|
+
.map((room) => room.locator));
|
|
965
1180
|
const listed = found.filter((room) => !fences.has(room.locator));
|
|
966
1181
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
967
1182
|
for (const locator of listedLocators)
|
|
968
1183
|
scopedRefusals.delete(locator);
|
|
969
|
-
|
|
1184
|
+
if (covered === undefined)
|
|
1185
|
+
discovered.clear();
|
|
1186
|
+
else
|
|
1187
|
+
for (const locator of covered)
|
|
1188
|
+
discovered.delete(locator);
|
|
970
1189
|
for (const room of listed)
|
|
971
1190
|
discovered.set(room.locator, room);
|
|
972
|
-
const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
|
|
1191
|
+
const unlisted = unlistedHeld(new Set(found.map((room) => room.locator))).filter((locator) => covered === undefined || covered.includes(locator));
|
|
973
1192
|
if (options.adapter.exhaustiveDiscovery === true)
|
|
974
1193
|
await loseRooms(unlisted);
|
|
975
1194
|
else
|
|
976
1195
|
unlistedKept(unlisted);
|
|
977
1196
|
for (const room of listed) {
|
|
978
|
-
const held = replicas.get(room.locator)?.
|
|
1197
|
+
const held = replicas.get(room.locator)?.record;
|
|
979
1198
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
980
1199
|
list(held, false);
|
|
981
1200
|
continue;
|
|
982
1201
|
}
|
|
983
1202
|
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
984
1203
|
}
|
|
1204
|
+
confirmLocks(listed);
|
|
985
1205
|
roomsListed = true;
|
|
986
1206
|
emitRooms();
|
|
1207
|
+
relocked(lockedBefore);
|
|
987
1208
|
return listed;
|
|
988
1209
|
}
|
|
1210
|
+
function heldLocally(locator) {
|
|
1211
|
+
return (replicas.has(locator) ||
|
|
1212
|
+
roomDrafts.has(locator) ||
|
|
1213
|
+
transfers.has(locator) ||
|
|
1214
|
+
queuesWithoutReplica.has(locator));
|
|
1215
|
+
}
|
|
989
1216
|
function unlistedHeld(listed) {
|
|
990
1217
|
return [
|
|
991
1218
|
...new Set([
|
|
@@ -998,7 +1225,7 @@ export function composeRuntime(options) {
|
|
|
998
1225
|
}
|
|
999
1226
|
function unlistedKept(unlisted) {
|
|
1000
1227
|
for (const locator of unlisted) {
|
|
1001
|
-
const held = replicas.get(locator)?.
|
|
1228
|
+
const held = replicas.get(locator)?.record;
|
|
1002
1229
|
if (held !== undefined)
|
|
1003
1230
|
list(held, false);
|
|
1004
1231
|
else
|
|
@@ -1007,16 +1234,16 @@ export function composeRuntime(options) {
|
|
|
1007
1234
|
}
|
|
1008
1235
|
function fence(locators) {
|
|
1009
1236
|
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
1010
|
-
const removed = fresh.map((locator) =>
|
|
1011
|
-
|
|
1012
|
-
queue: true,
|
|
1237
|
+
const removed = fresh.map((locator) => ({
|
|
1238
|
+
locator,
|
|
1013
1239
|
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
1014
1240
|
}));
|
|
1015
1241
|
for (const locator of fresh) {
|
|
1016
1242
|
fences.set(locator, { at: discoveries, erased: false });
|
|
1017
1243
|
lastFenced.set(locator, ++fencings);
|
|
1244
|
+
confirmed.delete(locator);
|
|
1018
1245
|
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
1019
|
-
pendings.delete(batch.id);
|
|
1246
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
1020
1247
|
replicas.delete(locator);
|
|
1021
1248
|
queuesWithoutReplica.delete(locator);
|
|
1022
1249
|
rooms.delete(locator);
|
|
@@ -1030,6 +1257,7 @@ export function composeRuntime(options) {
|
|
|
1030
1257
|
repairable.delete(locator);
|
|
1031
1258
|
unindexable.delete(locator);
|
|
1032
1259
|
paused.delete(locator);
|
|
1260
|
+
prepared.delete(locator);
|
|
1033
1261
|
scopedRefusals.delete(locator);
|
|
1034
1262
|
sourceRefusals.delete(locator);
|
|
1035
1263
|
declarationRefusals.delete(locator);
|
|
@@ -1043,26 +1271,49 @@ export function composeRuntime(options) {
|
|
|
1043
1271
|
}
|
|
1044
1272
|
if (fresh.length === 0)
|
|
1045
1273
|
return fresh;
|
|
1046
|
-
publishChangeSet(mergeChangeSets(removed));
|
|
1047
|
-
for (const locator of fresh)
|
|
1048
|
-
prepared.delete(locator);
|
|
1049
1274
|
searchIndex.forget(fresh);
|
|
1050
|
-
|
|
1275
|
+
publishChangeSet(mergeChangeSets(removed.map(({ locator, drafts }) => changeSet(locator, { documents: null, room: true, queue: true, drafts }))));
|
|
1276
|
+
liveQueries.recover();
|
|
1051
1277
|
moveTo({ ...status, ...attention() });
|
|
1052
1278
|
return fresh;
|
|
1053
1279
|
}
|
|
1054
|
-
async function
|
|
1055
|
-
await local.erase(locators);
|
|
1280
|
+
async function eraseFenced(locators) {
|
|
1056
1281
|
for (const locator of locators) {
|
|
1282
|
+
while (await durably((local) => local.erase(locator, ERASE_BATCH)))
|
|
1283
|
+
await yielded();
|
|
1057
1284
|
const held = fences.get(locator);
|
|
1058
1285
|
if (held !== undefined)
|
|
1059
1286
|
held.erased = true;
|
|
1060
1287
|
}
|
|
1061
1288
|
}
|
|
1289
|
+
let sweeping = false;
|
|
1290
|
+
function sweep() {
|
|
1291
|
+
if (sweeping || closed)
|
|
1292
|
+
return;
|
|
1293
|
+
sweeping = true;
|
|
1294
|
+
void (async () => {
|
|
1295
|
+
try {
|
|
1296
|
+
while (!closed && (await durably((local) => local.erase(null, ERASE_BATCH)))) {
|
|
1297
|
+
await yielded();
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
catch {
|
|
1301
|
+
journal.record('erasure failed');
|
|
1302
|
+
}
|
|
1303
|
+
finally {
|
|
1304
|
+
sweeping = false;
|
|
1305
|
+
}
|
|
1306
|
+
})();
|
|
1307
|
+
}
|
|
1062
1308
|
async function loseRooms(locators) {
|
|
1063
1309
|
const fresh = fence(locators);
|
|
1064
|
-
if (fresh.length
|
|
1065
|
-
|
|
1310
|
+
if (fresh.length === 0)
|
|
1311
|
+
return;
|
|
1312
|
+
await durably(async (local) => {
|
|
1313
|
+
await local.fence(fresh);
|
|
1314
|
+
await unpair(local);
|
|
1315
|
+
});
|
|
1316
|
+
await eraseFenced(fresh);
|
|
1066
1317
|
}
|
|
1067
1318
|
function restored(key, persisted) {
|
|
1068
1319
|
const prior = models.get(key);
|
|
@@ -1083,54 +1334,58 @@ export function composeRuntime(options) {
|
|
|
1083
1334
|
return model;
|
|
1084
1335
|
}
|
|
1085
1336
|
async function hydrate(opened) {
|
|
1086
|
-
const [
|
|
1087
|
-
opened.
|
|
1337
|
+
const [held, queues, kept, heldConversations, chatDrafts] = await Promise.all([
|
|
1338
|
+
opened.opened(),
|
|
1088
1339
|
opened.queues(),
|
|
1089
1340
|
opened.roomDrafts(),
|
|
1090
|
-
opened.presentation(),
|
|
1091
1341
|
opened.conversations(),
|
|
1092
1342
|
opened.chatDrafts(),
|
|
1093
|
-
opened.lastCompleted(),
|
|
1094
|
-
opened.providerLimits(),
|
|
1095
|
-
opened.capabilities(),
|
|
1096
|
-
opened.member(),
|
|
1097
|
-
opened.transfers(),
|
|
1098
|
-
opened.targets(),
|
|
1099
1343
|
]);
|
|
1100
|
-
|
|
1344
|
+
const records = [...held.rooms.values()];
|
|
1345
|
+
const counts = await opened.counts(records.map(({ key, typedSelection }) => ({ key, roots: typedSelection?.roots ?? [] })));
|
|
1346
|
+
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1347
|
+
targets = held.targets;
|
|
1101
1348
|
transfers.clear();
|
|
1102
|
-
for (const record of
|
|
1103
|
-
transfers.set(
|
|
1104
|
-
limits =
|
|
1105
|
-
capabilities =
|
|
1106
|
-
member =
|
|
1349
|
+
for (const [locator, record] of held.transfers)
|
|
1350
|
+
transfers.set(locator, record);
|
|
1351
|
+
limits = held.providerLimits;
|
|
1352
|
+
capabilities = held.capabilities;
|
|
1353
|
+
member = held.member;
|
|
1107
1354
|
roomDrafts.clear();
|
|
1108
1355
|
for (const record of kept)
|
|
1109
1356
|
rememberRoomDraft(record);
|
|
1110
1357
|
presentations.clear();
|
|
1111
|
-
for (const record of
|
|
1358
|
+
for (const record of held.presentation)
|
|
1112
1359
|
presentations.set(record.app, record.value);
|
|
1113
|
-
conversations.hydrate(
|
|
1114
|
-
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1360
|
+
conversations.hydrate(heldConversations, chatDrafts);
|
|
1115
1361
|
pendings.clear();
|
|
1116
1362
|
for (const queue of queues) {
|
|
1117
|
-
for (const { batch, state } of queue.pending)
|
|
1118
|
-
pendings.set(batch.id, state);
|
|
1363
|
+
for (const { batch, state } of queue.pending) {
|
|
1364
|
+
pendings.set(pendingKey(queue.locator, batch.id), state);
|
|
1365
|
+
}
|
|
1119
1366
|
}
|
|
1120
|
-
const previousReplicas = new Map(replicas);
|
|
1121
1367
|
replicas.clear();
|
|
1368
|
+
prepared.clear();
|
|
1122
1369
|
queuesWithoutReplica.clear();
|
|
1123
|
-
const replicated = new Set(records.map((record) => record.locator));
|
|
1124
1370
|
for (const [locator, queue] of queuedBatches) {
|
|
1125
|
-
if (!
|
|
1371
|
+
if (!held.rooms.has(locator))
|
|
1126
1372
|
queuesWithoutReplica.set(locator, queue);
|
|
1127
1373
|
}
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
const
|
|
1132
|
-
const
|
|
1133
|
-
|
|
1374
|
+
records.forEach((stored, index) => {
|
|
1375
|
+
if (fences.has(stored.locator))
|
|
1376
|
+
return;
|
|
1377
|
+
const model = restored(modelKey(stored.locator, 'collection'), stored.model);
|
|
1378
|
+
const selectionModel = restored(modelKey(stored.locator, 'typed-selection'), stored.selectionModel);
|
|
1379
|
+
const record = { ...stored, model, selectionModel };
|
|
1380
|
+
replicas.set(record.locator, {
|
|
1381
|
+
record,
|
|
1382
|
+
queue: queuedBatches.get(record.locator) ?? [],
|
|
1383
|
+
overlaid: held.overlaid.get(record.key) ?? new Set(),
|
|
1384
|
+
count: counts[index] ?? 0,
|
|
1385
|
+
confirmed: 0,
|
|
1386
|
+
});
|
|
1387
|
+
if (!held.unprepared.has(record.key))
|
|
1388
|
+
prepared.add(record.locator);
|
|
1134
1389
|
const existing = rooms.get(record.locator);
|
|
1135
1390
|
if (existing === undefined)
|
|
1136
1391
|
list(record, false);
|
|
@@ -1149,11 +1404,15 @@ export function composeRuntime(options) {
|
|
|
1149
1404
|
},
|
|
1150
1405
|
});
|
|
1151
1406
|
}
|
|
1152
|
-
}
|
|
1407
|
+
});
|
|
1408
|
+
for (const key of held.orphans)
|
|
1409
|
+
await opened.release(key);
|
|
1410
|
+
if (held.erasing || held.orphans.length > 0)
|
|
1411
|
+
queueMicrotask(sweep);
|
|
1153
1412
|
return {
|
|
1154
|
-
lastCompleted,
|
|
1413
|
+
lastCompleted: held.lastCompleted,
|
|
1155
1414
|
locators: new Set([...queuedBatches.keys(), ...replicas.keys()]),
|
|
1156
|
-
read: mergeChangeSets([...
|
|
1415
|
+
read: mergeChangeSets([...replicas.keys()].map((locator) => changeSet(locator, { documents: null }))),
|
|
1157
1416
|
};
|
|
1158
1417
|
}
|
|
1159
1418
|
async function readState(opened) {
|
|
@@ -1172,7 +1431,7 @@ export function composeRuntime(options) {
|
|
|
1172
1431
|
async function open() {
|
|
1173
1432
|
if (closed)
|
|
1174
1433
|
throw new Error('runtime closed');
|
|
1175
|
-
const opened = await openRuntimeStore(isFenced);
|
|
1434
|
+
const opened = await openRuntimeStore(isFenced, options.database);
|
|
1176
1435
|
store = opened;
|
|
1177
1436
|
const interrupted = await opened.fences();
|
|
1178
1437
|
for (const locator of interrupted) {
|
|
@@ -1192,9 +1451,17 @@ export function composeRuntime(options) {
|
|
|
1192
1451
|
emitStorageLoss();
|
|
1193
1452
|
}
|
|
1194
1453
|
const lastCompleted = await readState(opened);
|
|
1195
|
-
if (interrupted.length > 0)
|
|
1196
|
-
|
|
1454
|
+
if (interrupted.length > 0) {
|
|
1455
|
+
queueMicrotask(() => {
|
|
1456
|
+
void eraseFenced(interrupted).catch(() => {
|
|
1457
|
+
journal.record('erasure failed');
|
|
1458
|
+
});
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1197
1461
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
1462
|
+
void exclusive(() => unpair(opened)).catch(() => {
|
|
1463
|
+
journal.record('unpair failed');
|
|
1464
|
+
});
|
|
1198
1465
|
return opened;
|
|
1199
1466
|
}
|
|
1200
1467
|
function durably(work) {
|
|
@@ -1215,78 +1482,272 @@ export function composeRuntime(options) {
|
|
|
1215
1482
|
mutations = next.then(() => undefined, () => undefined);
|
|
1216
1483
|
return next;
|
|
1217
1484
|
}
|
|
1218
|
-
function withdrawable(
|
|
1219
|
-
|
|
1220
|
-
|
|
1485
|
+
function withdrawable(locator) {
|
|
1486
|
+
return (batch) => {
|
|
1487
|
+
const state = pendings.get(pendingKey(locator, batch.id));
|
|
1488
|
+
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
const REWRITE_LINKS = ['field', 'body'];
|
|
1492
|
+
const CHECKED_LINKS = ['field', 'section'];
|
|
1493
|
+
async function sliceAt(record, view, queue, extra = [], moved = []) {
|
|
1494
|
+
const local = await openLocal();
|
|
1495
|
+
const needed = new Set([...touchedBy(queue), ...extra]);
|
|
1496
|
+
const movedPaths = [...new Set([...movedBy(queue, record.locator), ...moved])];
|
|
1497
|
+
if (movedPaths.length > 0) {
|
|
1498
|
+
for (const { path } of await local.referrers(record.key, view, movedPaths, REWRITE_LINKS)) {
|
|
1499
|
+
needed.add(path);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
const paths = [...needed];
|
|
1503
|
+
return stepped(slicing(await local.entries(record.key, view, paths), paths));
|
|
1221
1504
|
}
|
|
1222
|
-
async function
|
|
1505
|
+
async function landedWhole(locator, batch, confirmedLayer, current) {
|
|
1506
|
+
if (!landed(batch, confirmedLayer, current))
|
|
1507
|
+
return false;
|
|
1508
|
+
const partner = partnerOf(batch, locator);
|
|
1509
|
+
if (partner === null)
|
|
1510
|
+
return true;
|
|
1511
|
+
const other = replicas.get(partner);
|
|
1512
|
+
const half = other?.queue.find((one) => one.id === batch.id);
|
|
1513
|
+
if (other === undefined || half === undefined)
|
|
1514
|
+
return false;
|
|
1515
|
+
return landed(half, await sliceAt(other.record, 'confirmed', [half]));
|
|
1516
|
+
}
|
|
1517
|
+
function carriedBack(locator, unlanded, written) {
|
|
1518
|
+
const back = new Map();
|
|
1519
|
+
const taken = [];
|
|
1520
|
+
for (const batch of unlanded) {
|
|
1521
|
+
const between = batch.between;
|
|
1522
|
+
if (between === undefined)
|
|
1523
|
+
continue;
|
|
1524
|
+
const source = between.source.room;
|
|
1525
|
+
const destination = between.destination.room;
|
|
1526
|
+
for (const { path: to } of pendings.get(pendingKey(destination, batch.id))?.drafts ?? []) {
|
|
1527
|
+
const record = roomDrafts.get(destination)?.get(to);
|
|
1528
|
+
const path = between.moves.find((move) => move.to === to)?.path;
|
|
1529
|
+
if (record === undefined || path === undefined)
|
|
1530
|
+
continue;
|
|
1531
|
+
const [into, key] = source === locator ? [written, path] : [back, documentKey(source, path)];
|
|
1532
|
+
const existing = into.get(key) ?? roomDrafts.get(source)?.get(path);
|
|
1533
|
+
taken.push({ locator: destination, path: to });
|
|
1534
|
+
into.set(key, {
|
|
1535
|
+
locator: source,
|
|
1536
|
+
path,
|
|
1537
|
+
revision: roomDraftRevision(),
|
|
1538
|
+
parts: returnedInto(existing?.parts, record.parts),
|
|
1539
|
+
returned: existing?.returned ?? null,
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
const records = [...back.values()];
|
|
1544
|
+
const elsewhere = taken.filter((one) => one.locator !== locator);
|
|
1545
|
+
const rooms = new Set([...records, ...elsewhere].map((one) => one.locator));
|
|
1546
|
+
return {
|
|
1547
|
+
taken: taken.filter((one) => one.locator === locator).map(({ path }) => path),
|
|
1548
|
+
writes: [...rooms].map((room) => ({
|
|
1549
|
+
locator: room,
|
|
1550
|
+
roomDrafts: records.filter((record) => record.locator === room),
|
|
1551
|
+
consumedRoomDrafts: elsewhere.filter((one) => one.locator === room).map(({ path }) => path),
|
|
1552
|
+
})),
|
|
1553
|
+
changes: [...rooms].map((room) => changeSet(room, {
|
|
1554
|
+
drafts: [...records, ...elsewhere]
|
|
1555
|
+
.filter((one) => one.locator === room)
|
|
1556
|
+
.map(({ path }) => path),
|
|
1557
|
+
})),
|
|
1558
|
+
remember() {
|
|
1559
|
+
for (const one of taken)
|
|
1560
|
+
forgetRoomDraft(one.locator, one.path);
|
|
1561
|
+
for (const record of records)
|
|
1562
|
+
rememberRoomDraft(record);
|
|
1563
|
+
},
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
function recomposed(locator, before, after) {
|
|
1567
|
+
if (before !== undefined &&
|
|
1568
|
+
before.key === after.key &&
|
|
1569
|
+
fingerprint(compositionOf(before)) === fingerprint(compositionOf(after))) {
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
if (before !== undefined && before.key === after.key)
|
|
1573
|
+
prepared.delete(locator);
|
|
1574
|
+
}
|
|
1575
|
+
function committedRoom(locator, record, answer) {
|
|
1576
|
+
if (answer.prepared[locator] === false)
|
|
1577
|
+
prepared.delete(locator);
|
|
1578
|
+
else if (answer.prepared[locator] === true)
|
|
1579
|
+
prepared.add(locator);
|
|
1580
|
+
if (!prepared.has(locator))
|
|
1581
|
+
prepare([locator]);
|
|
1582
|
+
return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0 };
|
|
1583
|
+
}
|
|
1584
|
+
function replayed(record, confirmedLayer, queue) {
|
|
1585
|
+
return stepped(replaying(confirmedLayer, queue, record.locator, compositionOf(record)));
|
|
1586
|
+
}
|
|
1587
|
+
async function overlayDiff(record, before, visible, touched) {
|
|
1588
|
+
const kept = before !== undefined && before.record.key === record.key
|
|
1589
|
+
? before.overlaid
|
|
1590
|
+
: new Set();
|
|
1591
|
+
return {
|
|
1592
|
+
overlay: {
|
|
1593
|
+
set: await stepped(overlayOf(visible, touched)),
|
|
1594
|
+
cleared: [...kept].filter((path) => !touched.has(path)),
|
|
1595
|
+
},
|
|
1596
|
+
touched,
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
async function settle(local, locator, before, record, queue, settling = {}) {
|
|
1600
|
+
return together(local, [await settlement(local, locator, before, record, queue, settling)]);
|
|
1601
|
+
}
|
|
1602
|
+
function unpairedIn(locator, batch) {
|
|
1603
|
+
if (!queueIn(locator).some((held) => held.id === batch.id))
|
|
1604
|
+
return false;
|
|
1605
|
+
if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
|
|
1606
|
+
return false;
|
|
1607
|
+
return !leavingIn(batch, locator) || withdrawable(locator)(batch);
|
|
1608
|
+
}
|
|
1609
|
+
async function together(local, settlements) {
|
|
1610
|
+
const settled = new Set(settlements.map(({ locator }) => locator));
|
|
1611
|
+
const all = [...settlements];
|
|
1612
|
+
for (let wave = settlements; wave.length > 0;) {
|
|
1613
|
+
const partnered = new Map();
|
|
1614
|
+
for (const { locator, left } of wave) {
|
|
1615
|
+
for (const batch of left) {
|
|
1616
|
+
const partner = partnerOf(batch, locator);
|
|
1617
|
+
if (partner === null || settled.has(partner) || !unpairedIn(partner, batch))
|
|
1618
|
+
continue;
|
|
1619
|
+
partnered.set(partner, [...(partnered.get(partner) ?? []), batch]);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
const partners = [];
|
|
1623
|
+
for (const [partner, batches] of partnered) {
|
|
1624
|
+
settled.add(partner);
|
|
1625
|
+
partners.push(await unpairing(local, partner, batches));
|
|
1626
|
+
}
|
|
1627
|
+
all.push(...partners);
|
|
1628
|
+
wave = partners;
|
|
1629
|
+
}
|
|
1630
|
+
const answer = await local.commit({
|
|
1631
|
+
rooms: all.flatMap(({ commit }) => commit.rooms),
|
|
1632
|
+
writes: all.flatMap(({ commit }) => commit.writes),
|
|
1633
|
+
});
|
|
1634
|
+
return mergeChangeSets(all.map((one) => one.apply(answer)));
|
|
1635
|
+
}
|
|
1636
|
+
async function settlement(local, locator, before, record, queue, settling = {}) {
|
|
1637
|
+
const view = settling.view ?? 'confirmed';
|
|
1223
1638
|
const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
1224
|
-
const
|
|
1225
|
-
const
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1639
|
+
const overrides = settling.changes;
|
|
1640
|
+
const read = await sliceAt(record, view, queue, [
|
|
1641
|
+
...(before !== undefined && before.record.key === record.key ? before.overlaid : []),
|
|
1642
|
+
...(overrides?.set.map(({ path }) => path) ?? []),
|
|
1643
|
+
...(overrides?.removed ?? []),
|
|
1644
|
+
]);
|
|
1645
|
+
const confirmedLayer = overrides === undefined
|
|
1646
|
+
? read
|
|
1647
|
+
: await stepped(slicingWith(read, overrides.set, overrides.removed));
|
|
1648
|
+
const composition = compositionOf(record);
|
|
1649
|
+
const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
|
|
1650
|
+
const founded = kept.filter((batch) => !queue.includes(batch));
|
|
1651
|
+
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
1652
|
+
const unqueued = [];
|
|
1653
|
+
for (const batch of withdrawn) {
|
|
1654
|
+
unqueued.push({ batch, landed: await landedWhole(locator, batch, confirmedLayer) });
|
|
1655
|
+
}
|
|
1230
1656
|
const returning = [
|
|
1231
|
-
...[...unqueued].reverse(),
|
|
1232
|
-
|
|
1657
|
+
...[...unqueued].reverse().map(({ batch, landed: held }) => ({
|
|
1658
|
+
batch,
|
|
1659
|
+
returned: localSaves || held ? null : returnedBy('withdrawn'),
|
|
1660
|
+
})),
|
|
1661
|
+
...(settling.leaving ?? []),
|
|
1233
1662
|
];
|
|
1234
1663
|
const written = new Map();
|
|
1235
|
-
for (const { batch, returned } of returning) {
|
|
1236
|
-
if (
|
|
1664
|
+
for (const { batch, returned: reason } of returning) {
|
|
1665
|
+
if (reason === null || batch.between?.destination.room === locator)
|
|
1237
1666
|
continue;
|
|
1238
|
-
for (const saved of pendings.get(batch.id)?.drafts ?? []) {
|
|
1667
|
+
for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
|
|
1239
1668
|
const existing = written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path);
|
|
1240
1669
|
written.set(saved.path, {
|
|
1241
1670
|
locator,
|
|
1242
1671
|
path: saved.path,
|
|
1243
1672
|
revision: roomDraftRevision(),
|
|
1244
1673
|
parts: returnedInto(existing?.parts, saved.parts),
|
|
1245
|
-
returned,
|
|
1674
|
+
returned: reason,
|
|
1246
1675
|
});
|
|
1247
1676
|
}
|
|
1248
1677
|
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1678
|
+
const carried = carriedBack(locator, [
|
|
1679
|
+
...[...unqueued].reverse().flatMap(({ batch, landed: held }) => (held ? [] : [batch])),
|
|
1680
|
+
...(settling.leaving ?? []).flatMap(({ batch, returned }) => returned === null ? [] : [batch]),
|
|
1681
|
+
], written);
|
|
1682
|
+
const replayedLayer = await replayed(record, confirmedLayer, kept);
|
|
1683
|
+
const { overlay, touched } = await overlayDiff(record, before, replayedLayer.visible, replayedLayer.touched);
|
|
1684
|
+
const commit = {
|
|
1685
|
+
rooms: [
|
|
1686
|
+
{
|
|
1687
|
+
locator,
|
|
1688
|
+
key: record.key,
|
|
1689
|
+
record: storedRecord(record),
|
|
1690
|
+
roots: record.typedSelection?.roots ?? [],
|
|
1691
|
+
...settling.commit,
|
|
1692
|
+
...(overrides === undefined
|
|
1693
|
+
? {}
|
|
1694
|
+
: { confirmed: { set: overrides.set, removed: overrides.removed } }),
|
|
1695
|
+
overlay,
|
|
1696
|
+
},
|
|
1697
|
+
],
|
|
1698
|
+
writes: [
|
|
1699
|
+
{
|
|
1700
|
+
locator,
|
|
1701
|
+
queue: kept,
|
|
1702
|
+
founded,
|
|
1703
|
+
roomDrafts: [...written.values()],
|
|
1704
|
+
consumedRoomDrafts: carried.taken,
|
|
1705
|
+
},
|
|
1706
|
+
...carried.writes,
|
|
1707
|
+
],
|
|
1708
|
+
};
|
|
1709
|
+
const staying = new Set(kept.map((batch) => batch.id));
|
|
1710
|
+
return {
|
|
1711
|
+
locator,
|
|
1712
|
+
left: previousQueue.filter((batch) => !staying.has(batch.id)),
|
|
1713
|
+
commit,
|
|
1714
|
+
apply: (answer) => {
|
|
1715
|
+
for (const batch of previousQueue) {
|
|
1716
|
+
if (!staying.has(batch.id))
|
|
1717
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
1718
|
+
}
|
|
1719
|
+
for (const draft of written.values())
|
|
1720
|
+
rememberRoomDraft(draft);
|
|
1721
|
+
carried.remember();
|
|
1722
|
+
const drafts = [...written.keys(), ...carried.taken];
|
|
1270
1723
|
queuesWithoutReplica.delete(locator);
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1724
|
+
for (const { batch, landed: held } of unqueued) {
|
|
1725
|
+
const outcome = localSaves ? 'dropped' : held ? 'landed' : 'withdrawn';
|
|
1726
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
1727
|
+
}
|
|
1728
|
+
const changed = answer.changed[locator];
|
|
1729
|
+
const confirmedChanged = settling.commit?.published !== undefined ||
|
|
1730
|
+
settling.commit?.refreshed === true ||
|
|
1731
|
+
overrides !== undefined;
|
|
1732
|
+
recomposed(locator, before?.record, record);
|
|
1733
|
+
const { count } = committedRoom(locator, record, answer);
|
|
1734
|
+
replicas.set(locator, {
|
|
1735
|
+
record,
|
|
1736
|
+
queue: kept,
|
|
1737
|
+
overlaid: touched,
|
|
1738
|
+
count,
|
|
1739
|
+
confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
|
|
1740
|
+
});
|
|
1741
|
+
return mergeChangeSets([
|
|
1742
|
+
changeSet(locator, {
|
|
1743
|
+
documents: changed === undefined ? [] : changed,
|
|
1744
|
+
room: settling.room,
|
|
1745
|
+
queue: queueChanged,
|
|
1746
|
+
drafts,
|
|
1747
|
+
}),
|
|
1748
|
+
...carried.changes,
|
|
1749
|
+
]);
|
|
1750
|
+
},
|
|
1290
1751
|
};
|
|
1291
1752
|
}
|
|
1292
1753
|
function adoptModels(locator, record) {
|
|
@@ -1307,7 +1768,7 @@ export function composeRuntime(options) {
|
|
|
1307
1768
|
const taken = (answer, previous, attached) => {
|
|
1308
1769
|
if (answer === undefined)
|
|
1309
1770
|
return previous;
|
|
1310
|
-
const { state, refused } = attachedModel(answer, previous);
|
|
1771
|
+
const { state, refused } = attachedModel(answer, previous, attached === 'collection' ? roomTypes : NO_ROOM_TYPES);
|
|
1311
1772
|
if (refused !== null) {
|
|
1312
1773
|
const where = refused.location === undefined ? '' : ` ${refused.location}`;
|
|
1313
1774
|
journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
|
|
@@ -1320,47 +1781,57 @@ export function composeRuntime(options) {
|
|
|
1320
1781
|
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
1321
1782
|
};
|
|
1322
1783
|
}
|
|
1323
|
-
function publish(local, room,
|
|
1324
|
-
const { base, stored } = landing;
|
|
1784
|
+
function publish(local, room, record, landing, live) {
|
|
1325
1785
|
return exclusive(async () => {
|
|
1326
|
-
if (stored !== undefined) {
|
|
1327
|
-
await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
|
|
1328
|
-
}
|
|
1329
1786
|
if (!live()) {
|
|
1330
1787
|
caughtUpTo(room.locator);
|
|
1331
1788
|
return true;
|
|
1332
1789
|
}
|
|
1333
1790
|
const prior = replicas.get(room.locator);
|
|
1334
|
-
if (
|
|
1335
|
-
(prior
|
|
1336
|
-
|
|
1791
|
+
if (landing.kind === 'refreshed' &&
|
|
1792
|
+
(prior === undefined || prior.record.key !== record.key || prior.confirmed !== landing.base)) {
|
|
1793
|
+
await local.unrefresh(record.key);
|
|
1337
1794
|
return false;
|
|
1338
1795
|
}
|
|
1339
|
-
adoptModels(room.locator,
|
|
1796
|
+
adoptModels(room.locator, record);
|
|
1340
1797
|
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
1341
|
-
const roomVersion =
|
|
1798
|
+
const roomVersion = record.roomVersion;
|
|
1342
1799
|
const accepted = queue.filter((batch) => {
|
|
1343
|
-
const
|
|
1344
|
-
return
|
|
1800
|
+
const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
|
|
1801
|
+
return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
|
|
1345
1802
|
});
|
|
1346
|
-
let
|
|
1803
|
+
let set;
|
|
1347
1804
|
try {
|
|
1348
|
-
|
|
1805
|
+
set = await settle(local, room.locator, prior, record, queue.filter((batch) => !accepted.includes(batch)), landing.kind === 'refreshed'
|
|
1806
|
+
? {
|
|
1807
|
+
room: true,
|
|
1808
|
+
view: 'refreshed',
|
|
1809
|
+
commit: { refreshed: true, composition: compositionOf(record) },
|
|
1810
|
+
}
|
|
1811
|
+
: {
|
|
1812
|
+
room: true,
|
|
1813
|
+
view: landing.refreshed ? 'refreshed' : 'confirmed',
|
|
1814
|
+
commit: {
|
|
1815
|
+
published: { from: prior?.record.key ?? null },
|
|
1816
|
+
composition: compositionOf(record),
|
|
1817
|
+
...(landing.refreshed ? { refreshed: true } : {}),
|
|
1818
|
+
},
|
|
1819
|
+
});
|
|
1349
1820
|
}
|
|
1350
1821
|
catch (error) {
|
|
1351
1822
|
if (!exceeded(error))
|
|
1352
1823
|
throw error;
|
|
1353
|
-
await exceededOn(room.locator,
|
|
1824
|
+
await exceededOn(room.locator, null);
|
|
1354
1825
|
caughtUpTo(room.locator);
|
|
1355
1826
|
return true;
|
|
1356
1827
|
}
|
|
1357
|
-
|
|
1358
|
-
if (stored !== undefined) {
|
|
1828
|
+
if (landing.kind === 'bootstrapped') {
|
|
1359
1829
|
transfers.delete(room.locator);
|
|
1360
1830
|
catching.delete(room.locator);
|
|
1831
|
+
sweep();
|
|
1361
1832
|
}
|
|
1362
1833
|
for (const batch of accepted)
|
|
1363
|
-
pendings.delete(batch.id);
|
|
1834
|
+
pendings.delete(pendingKey(room.locator, batch.id));
|
|
1364
1835
|
stale.delete(room.locator);
|
|
1365
1836
|
unindexable.delete(room.locator);
|
|
1366
1837
|
if (unreadable.delete(room.locator))
|
|
@@ -1368,22 +1839,27 @@ export function composeRuntime(options) {
|
|
|
1368
1839
|
sourceRefusals.delete(room.locator);
|
|
1369
1840
|
declarationRefusals.delete(room.locator);
|
|
1370
1841
|
list(room, true);
|
|
1371
|
-
if (prior === undefined)
|
|
1372
|
-
prepared.add(room.locator);
|
|
1373
1842
|
publishChangeSet(set);
|
|
1843
|
+
await unpair(local);
|
|
1374
1844
|
moveTo({ ...status, ...attention() });
|
|
1375
1845
|
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
1846
|
+
if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1847
|
+
submitLater(room.locator);
|
|
1848
|
+
}
|
|
1376
1849
|
return true;
|
|
1377
1850
|
});
|
|
1378
1851
|
}
|
|
1379
|
-
async function
|
|
1852
|
+
async function savedRoom(locator) {
|
|
1380
1853
|
const local = await openLocal();
|
|
1381
|
-
await referenced(locator);
|
|
1382
1854
|
const replica = replicas.get(locator);
|
|
1383
1855
|
const room = rooms.get(locator);
|
|
1384
1856
|
if (replica === undefined || room === undefined) {
|
|
1385
1857
|
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1386
1858
|
}
|
|
1859
|
+
return { local, replica, room };
|
|
1860
|
+
}
|
|
1861
|
+
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
1862
|
+
const target = await savedRoom(locator);
|
|
1387
1863
|
const saved = [];
|
|
1388
1864
|
for (const { path, revision } of named) {
|
|
1389
1865
|
const record = roomDrafts.get(locator)?.get(path);
|
|
@@ -1393,12 +1869,396 @@ export function composeRuntime(options) {
|
|
|
1393
1869
|
if (!saved.some((draft) => draft.path === path))
|
|
1394
1870
|
saved.push({ path, parts: record.parts });
|
|
1395
1871
|
}
|
|
1396
|
-
|
|
1872
|
+
return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
|
|
1873
|
+
}
|
|
1874
|
+
async function saveParts(locator, named, writtenBy) {
|
|
1875
|
+
const read = named.map(({ path, parts }) => ({ path, parts: readParts(parts) }));
|
|
1876
|
+
if (read.some(({ path }, at) => path === '' ||
|
|
1877
|
+
normalize(path) !== path ||
|
|
1878
|
+
read.findIndex((one) => one.path === path) !== at)) {
|
|
1879
|
+
throw runtimeError('room-draft-malformed');
|
|
1880
|
+
}
|
|
1881
|
+
const target = await savedRoom(locator);
|
|
1882
|
+
const saved = [];
|
|
1883
|
+
const files = [];
|
|
1884
|
+
const filed = read.filter(({ parts }) => parts.file !== undefined).map(({ path }) => path);
|
|
1885
|
+
const visible = filed.length === 0 ? undefined : await visibleOf(locator, filed);
|
|
1886
|
+
for (const { path, parts } of read) {
|
|
1887
|
+
const file = parts.file === undefined
|
|
1888
|
+
? undefined
|
|
1889
|
+
: await heldFile(parts.file.blob, binaryBase(visible, path));
|
|
1890
|
+
if (parts.file !== undefined && file !== undefined) {
|
|
1891
|
+
files.push({ key: file.localId, blob: parts.file.blob });
|
|
1892
|
+
}
|
|
1893
|
+
saved.push({ path, parts: held(undefined, parts, file) });
|
|
1894
|
+
}
|
|
1895
|
+
return committed(locator, target, saved, new Set(), writtenBy, files);
|
|
1896
|
+
}
|
|
1897
|
+
function carriedDrafts(locator, saved, consumed) {
|
|
1898
|
+
const holding = roomDrafts.get(locator);
|
|
1899
|
+
return new Map(saved.flatMap(({ path, parts }) => {
|
|
1900
|
+
const record = holding?.get(path);
|
|
1901
|
+
if (parts.move === undefined || record === undefined || consumed.has(path))
|
|
1902
|
+
return [];
|
|
1903
|
+
const to = moveTarget(parts.move);
|
|
1904
|
+
if (to === path || holding?.has(to) === true)
|
|
1905
|
+
return [];
|
|
1906
|
+
return [[path, { ...record, path: to, revision: roomDraftRevision() }]];
|
|
1907
|
+
}));
|
|
1908
|
+
}
|
|
1909
|
+
async function savingIn(replica, changes, extra, leaving = []) {
|
|
1910
|
+
const record = replica.record;
|
|
1911
|
+
const local = await openLocal();
|
|
1912
|
+
const paths = [
|
|
1913
|
+
...extra,
|
|
1914
|
+
...changes.flatMap((change) => change.kind === 'move' ? [change.path, change.to] : [change.path]),
|
|
1915
|
+
];
|
|
1916
|
+
const moved = [
|
|
1917
|
+
...leaving,
|
|
1918
|
+
...changes.flatMap((change) => (change.kind === 'move' ? [change.path] : [])),
|
|
1919
|
+
];
|
|
1920
|
+
const removed = changes.flatMap((change) => (change.kind === 'remove' ? [change.path] : []));
|
|
1921
|
+
const referring = removed.length === 0
|
|
1922
|
+
? []
|
|
1923
|
+
: (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
|
|
1924
|
+
const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
|
|
1925
|
+
const { visible } = await replayed(record, confirmedLayer, replica.queue);
|
|
1926
|
+
const composition = compositionOf(record);
|
|
1927
|
+
return {
|
|
1928
|
+
record,
|
|
1929
|
+
confirmed: confirmedLayer,
|
|
1930
|
+
visible,
|
|
1931
|
+
*visibleOver(queue) {
|
|
1932
|
+
return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
|
|
1933
|
+
},
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
async function unmodeledIn(record) {
|
|
1937
|
+
const composition = compositionFor(record);
|
|
1938
|
+
if (available(composition))
|
|
1939
|
+
return false;
|
|
1940
|
+
const local = await openLocal();
|
|
1941
|
+
const roots = record.typedSelection?.roots ?? [];
|
|
1942
|
+
if (composition.model.status === 'unavailable' &&
|
|
1943
|
+
(await local.typedWithin(record.key, 'visible', roots, false))) {
|
|
1944
|
+
return true;
|
|
1945
|
+
}
|
|
1946
|
+
return (composition.selection !== null &&
|
|
1947
|
+
composition.selection.model.status === 'unavailable' &&
|
|
1948
|
+
(await local.typedWithin(record.key, 'visible', roots, true)));
|
|
1949
|
+
}
|
|
1950
|
+
function textDiffers(left, right) {
|
|
1951
|
+
return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
|
|
1952
|
+
!sameBinary(left?.binary ?? undefined, right?.binary ?? undefined));
|
|
1953
|
+
}
|
|
1954
|
+
function textOf(entry) {
|
|
1955
|
+
if (entry === null)
|
|
1956
|
+
return undefined;
|
|
1957
|
+
return entry.binary !== null ? null : entry.content;
|
|
1958
|
+
}
|
|
1959
|
+
function walkOf(local, key, view) {
|
|
1960
|
+
return async (after) => {
|
|
1961
|
+
const page = await local.walk(key, view, after, WALK_BYTES);
|
|
1962
|
+
return {
|
|
1963
|
+
entries: page.entries.map(({ path, content, binary }) => ({
|
|
1964
|
+
path,
|
|
1965
|
+
content: binary !== null ? null : content,
|
|
1966
|
+
})),
|
|
1967
|
+
next: page.next,
|
|
1968
|
+
};
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
function lookupOf(local, key, view) {
|
|
1972
|
+
return async (paths) => {
|
|
1973
|
+
if (paths.length === 0)
|
|
1974
|
+
return new Map();
|
|
1975
|
+
return new Map((await local.typed(key, view, paths)).map(({ path, type, binary }) => [
|
|
1976
|
+
path,
|
|
1977
|
+
{ type: type ?? undefined, binary },
|
|
1978
|
+
]));
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
function declaredOf(entry) {
|
|
1982
|
+
if (entry.binary !== null)
|
|
1983
|
+
return { type: undefined, binary: true };
|
|
1984
|
+
const read = parseFrontmatter(entry.content ?? '');
|
|
1985
|
+
return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
|
|
1986
|
+
}
|
|
1987
|
+
function overriddenWalk(walk, overrides) {
|
|
1988
|
+
const sorted = [...overrides.keys()].sort();
|
|
1989
|
+
const beyond = (path) => {
|
|
1990
|
+
if (path === null)
|
|
1991
|
+
return 0;
|
|
1992
|
+
let low = 0;
|
|
1993
|
+
let high = sorted.length;
|
|
1994
|
+
while (low < high) {
|
|
1995
|
+
const middle = (low + high) >>> 1;
|
|
1996
|
+
if (sorted[middle] <= path)
|
|
1997
|
+
low = middle + 1;
|
|
1998
|
+
else
|
|
1999
|
+
high = middle;
|
|
2000
|
+
}
|
|
2001
|
+
return low;
|
|
2002
|
+
};
|
|
2003
|
+
return async (after) => {
|
|
2004
|
+
const page = await walk(after);
|
|
2005
|
+
const end = page.next;
|
|
2006
|
+
const added = sorted
|
|
2007
|
+
.slice(beyond(after), end === null ? sorted.length : beyond(end))
|
|
2008
|
+
.flatMap((path) => {
|
|
2009
|
+
const entry = overrides.get(path);
|
|
2010
|
+
return entry === null || entry === undefined
|
|
2011
|
+
? []
|
|
2012
|
+
: [{ path, content: entry.binary !== null ? null : entry.content }];
|
|
2013
|
+
});
|
|
2014
|
+
const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
|
|
2015
|
+
return { entries, next: end };
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
function overriddenLookup(lookup, overrides) {
|
|
2019
|
+
return async (paths) => {
|
|
2020
|
+
const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
|
|
2021
|
+
for (const path of paths) {
|
|
2022
|
+
const entry = overrides.get(path);
|
|
2023
|
+
if (entry === undefined)
|
|
2024
|
+
continue;
|
|
2025
|
+
if (entry === null)
|
|
2026
|
+
known.delete(path);
|
|
2027
|
+
else
|
|
2028
|
+
known.set(path, declaredOf(entry));
|
|
2029
|
+
}
|
|
2030
|
+
return known;
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
function validatorOf(record, confirmedLayer) {
|
|
2034
|
+
return async (resulting, touched) => {
|
|
2035
|
+
const composition = compositionFor(record);
|
|
2036
|
+
const local = await openLocal();
|
|
2037
|
+
const changed = [];
|
|
2038
|
+
const overrides = new Map();
|
|
2039
|
+
const pace = paced();
|
|
2040
|
+
for (const path of new Set([...confirmedLayer.held, ...touched])) {
|
|
2041
|
+
await pace(ROW_WEIGHT);
|
|
2042
|
+
const was = entryAt(confirmedLayer, path);
|
|
2043
|
+
const now = entryAt(resulting, path);
|
|
2044
|
+
if (!textDiffers(was, now))
|
|
2045
|
+
continue;
|
|
2046
|
+
overrides.set(path, now);
|
|
2047
|
+
if (textOf(was) !== textOf(now))
|
|
2048
|
+
changed.push({ path, was: textOf(was), now: textOf(now) });
|
|
2049
|
+
}
|
|
2050
|
+
const lookup = overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides);
|
|
2051
|
+
const clean = available(composition) &&
|
|
2052
|
+
changed.length <= CLEAN_CHANGES &&
|
|
2053
|
+
(await changedCleanly(changed, compositionOf(record), composition, {
|
|
2054
|
+
lookup,
|
|
2055
|
+
texts: textsOf(local, record.key, 'confirmed'),
|
|
2056
|
+
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2057
|
+
}));
|
|
2058
|
+
if (clean)
|
|
2059
|
+
return { verdict: 'accept' };
|
|
2060
|
+
return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
function textsOf(local, key, view) {
|
|
2064
|
+
return async (paths) => new Map((await local.entries(key, view, paths)).map(({ path, content, binary }) => [
|
|
2065
|
+
path,
|
|
2066
|
+
binary !== null ? null : content,
|
|
2067
|
+
]));
|
|
2068
|
+
}
|
|
2069
|
+
function referrersOf(local, key, view) {
|
|
2070
|
+
return async (targets) => [
|
|
2071
|
+
...new Set((await local.referrers(key, view, targets, CHECKED_LINKS)).map(({ path }) => path)),
|
|
2072
|
+
];
|
|
2073
|
+
}
|
|
2074
|
+
async function sideOf(locator, replica, room, saving) {
|
|
2075
|
+
const record = replica.record;
|
|
2076
|
+
return {
|
|
2077
|
+
room: locator,
|
|
2078
|
+
handle: room.handle,
|
|
2079
|
+
baseRoomVersion: record.roomVersion,
|
|
2080
|
+
composition: compositionFor(record),
|
|
2081
|
+
operations: room.operations,
|
|
2082
|
+
providerOwnedPaths: room.providerOwnedPaths,
|
|
2083
|
+
visible: saving.visible,
|
|
2084
|
+
queued: queuedDocuments(replica.queue),
|
|
2085
|
+
unmodeled: await unmodeledIn(record),
|
|
2086
|
+
validate: validatorOf(record, saving.confirmed),
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
2089
|
+
function movedInto(locator, saved) {
|
|
2090
|
+
const into = saved.find(({ parts }) => parts.move?.room !== undefined && parts.move.room !== locator);
|
|
2091
|
+
return into?.parts.move?.room ?? null;
|
|
2092
|
+
}
|
|
2093
|
+
async function saveCommit(local, files, rooms) {
|
|
2094
|
+
const touched = new Map();
|
|
2095
|
+
const commits = [];
|
|
2096
|
+
for (const { locator, saving, before, queue } of rooms) {
|
|
2097
|
+
const replayedLayer = await replayed(saving.record, saving.confirmed, queue);
|
|
2098
|
+
const diff = await overlayDiff(saving.record, before, replayedLayer.visible, replayedLayer.touched);
|
|
2099
|
+
touched.set(locator, diff.touched);
|
|
2100
|
+
commits.push({
|
|
2101
|
+
locator,
|
|
2102
|
+
key: saving.record.key,
|
|
2103
|
+
roots: saving.record.typedSelection?.roots ?? [],
|
|
2104
|
+
overlay: diff.overlay,
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
try {
|
|
2108
|
+
const answer = await local.commit({
|
|
2109
|
+
rooms: commits,
|
|
2110
|
+
writes: rooms.map(({ write }) => write),
|
|
2111
|
+
files,
|
|
2112
|
+
whole: true,
|
|
2113
|
+
});
|
|
2114
|
+
return { answer, touched };
|
|
2115
|
+
}
|
|
2116
|
+
catch {
|
|
2117
|
+
return null;
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
function savedReplica(locator, before, queue, touched, answer) {
|
|
2121
|
+
replicas.set(locator, {
|
|
2122
|
+
...before,
|
|
2123
|
+
queue,
|
|
2124
|
+
overlaid: touched,
|
|
2125
|
+
count: answer.counts[locator] ?? before.count,
|
|
2126
|
+
});
|
|
2127
|
+
}
|
|
2128
|
+
async function committedMove(locator, destination, { local, replica, room }, saved, consumed, writtenBy, files) {
|
|
2129
|
+
const mixed = saved.find(({ parts }) => Object.keys(parts).length !== 1 || parts.move?.room !== destination);
|
|
2130
|
+
if (mixed !== undefined) {
|
|
2131
|
+
return { outcome: 'refused', code: 'mixed-room-move', retryable: false, file: mixed.path };
|
|
2132
|
+
}
|
|
2133
|
+
const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
|
|
2134
|
+
const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
|
|
2135
|
+
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, sourceSaving.visible)));
|
|
1397
2136
|
if (changed.length > 0)
|
|
1398
2137
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
1399
|
-
const
|
|
1400
|
-
const
|
|
1401
|
-
|
|
2138
|
+
const target = replicas.get(destination);
|
|
2139
|
+
const listedRoom = rooms.get(destination);
|
|
2140
|
+
if (target === undefined || listedRoom === undefined) {
|
|
2141
|
+
throw runtimeError('room-not-held', `No Local Replica for ${destination}`);
|
|
2142
|
+
}
|
|
2143
|
+
const arriving = await savingIn(target, [], moves.map(({ to }) => to));
|
|
2144
|
+
const built = await buildMove({
|
|
2145
|
+
source: await sideOf(locator, replica, room, sourceSaving),
|
|
2146
|
+
destination: await sideOf(destination, target, listedRoom, arriving),
|
|
2147
|
+
moves,
|
|
2148
|
+
providerLimits: limits,
|
|
2149
|
+
local: localSaves,
|
|
2150
|
+
...(writtenBy === undefined ? {} : { writtenBy }),
|
|
2151
|
+
});
|
|
2152
|
+
if (built.kind === 'refused')
|
|
2153
|
+
return { outcome: 'refused', ...built.refusal };
|
|
2154
|
+
const id = built.leaving.id;
|
|
2155
|
+
const carrying = saved.flatMap(({ path, parts }) => {
|
|
2156
|
+
const record = roomDrafts.get(locator)?.get(path);
|
|
2157
|
+
const to = moveTarget(parts.move);
|
|
2158
|
+
if (record === undefined || consumed.has(path) || roomDrafts.get(destination)?.has(to)) {
|
|
2159
|
+
return [];
|
|
2160
|
+
}
|
|
2161
|
+
const moved = {
|
|
2162
|
+
...record,
|
|
2163
|
+
locator: destination,
|
|
2164
|
+
path: to,
|
|
2165
|
+
revision: roomDraftRevision(),
|
|
2166
|
+
};
|
|
2167
|
+
return [{ from: path, record: moved }];
|
|
2168
|
+
});
|
|
2169
|
+
const carried = carrying.map(({ record }) => record);
|
|
2170
|
+
const marked = carried.map(({ path }) => ({ path, parts: {} }));
|
|
2171
|
+
const left = [...consumed, ...carrying.map(({ from }) => from)];
|
|
2172
|
+
const leavingQueue = [...replica.queue, built.leaving];
|
|
2173
|
+
const arrivingQueue = [...target.queue, built.arriving];
|
|
2174
|
+
const committedBoth = await saveCommit(local, files, [
|
|
2175
|
+
{
|
|
2176
|
+
locator,
|
|
2177
|
+
saving: sourceSaving,
|
|
2178
|
+
before: replica,
|
|
2179
|
+
queue: leavingQueue,
|
|
2180
|
+
write: {
|
|
2181
|
+
locator,
|
|
2182
|
+
queue: leavingQueue,
|
|
2183
|
+
saved: new Map([[id, saved]]),
|
|
2184
|
+
consumedRoomDrafts: left,
|
|
2185
|
+
},
|
|
2186
|
+
},
|
|
2187
|
+
{
|
|
2188
|
+
locator: destination,
|
|
2189
|
+
saving: arriving,
|
|
2190
|
+
before: target,
|
|
2191
|
+
queue: arrivingQueue,
|
|
2192
|
+
write: {
|
|
2193
|
+
locator: destination,
|
|
2194
|
+
queue: arrivingQueue,
|
|
2195
|
+
saved: new Map([[id, marked]]),
|
|
2196
|
+
roomDrafts: carried,
|
|
2197
|
+
},
|
|
2198
|
+
},
|
|
2199
|
+
]);
|
|
2200
|
+
if (committedBoth === null)
|
|
2201
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2202
|
+
const { answer, touched } = committedBoth;
|
|
2203
|
+
if (answer.fenced !== null) {
|
|
2204
|
+
throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
|
|
2205
|
+
}
|
|
2206
|
+
if (fences.has(locator) || fences.has(destination)) {
|
|
2207
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
2208
|
+
}
|
|
2209
|
+
pendings.set(pendingKey(locator, id), {
|
|
2210
|
+
drafts: saved,
|
|
2211
|
+
submission: { state: 'waiting' },
|
|
2212
|
+
lost: false,
|
|
2213
|
+
});
|
|
2214
|
+
pendings.set(pendingKey(destination, id), {
|
|
2215
|
+
drafts: marked,
|
|
2216
|
+
submission: { state: 'waiting' },
|
|
2217
|
+
lost: false,
|
|
2218
|
+
});
|
|
2219
|
+
for (const path of left)
|
|
2220
|
+
forgetRoomDraft(locator, path);
|
|
2221
|
+
for (const record of carried)
|
|
2222
|
+
rememberRoomDraft(record);
|
|
2223
|
+
savedReplica(locator, replica, leavingQueue, touched.get(locator), answer);
|
|
2224
|
+
savedReplica(destination, target, arrivingQueue, touched.get(destination), answer);
|
|
2225
|
+
publishChangeSet(mergeChangeSets([
|
|
2226
|
+
changeSet(locator, {
|
|
2227
|
+
documents: answer.changed[locator] ?? [],
|
|
2228
|
+
queue: true,
|
|
2229
|
+
drafts: left,
|
|
2230
|
+
}),
|
|
2231
|
+
changeSet(destination, {
|
|
2232
|
+
documents: answer.changed[destination] ?? [],
|
|
2233
|
+
queue: true,
|
|
2234
|
+
drafts: carried.map(({ path }) => path),
|
|
2235
|
+
}),
|
|
2236
|
+
]));
|
|
2237
|
+
queuedSave(locator);
|
|
2238
|
+
return { outcome: 'saved', identifier: id };
|
|
2239
|
+
}
|
|
2240
|
+
function queuedSave(locator) {
|
|
2241
|
+
if (!localSaves)
|
|
2242
|
+
recorded = false;
|
|
2243
|
+
moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
|
|
2244
|
+
submitLater(locator);
|
|
2245
|
+
}
|
|
2246
|
+
async function committed(locator, target, saved, consumed, writtenBy, files = []) {
|
|
2247
|
+
const into = movedInto(locator, saved);
|
|
2248
|
+
if (into !== null) {
|
|
2249
|
+
return committedMove(locator, into, target, saved, consumed, writtenBy, files);
|
|
2250
|
+
}
|
|
2251
|
+
const { local, replica, room } = target;
|
|
2252
|
+
const parts = saved.map(({ path, parts: held }) => changesOf(path, held));
|
|
2253
|
+
const changes = parts.flatMap(({ changes: made }) => made);
|
|
2254
|
+
const saving = await savingIn(replica, changes, [
|
|
2255
|
+
...saved.map(({ path }) => path),
|
|
2256
|
+
...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
|
|
2257
|
+
]);
|
|
2258
|
+
const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, underneath(held, path, saving.visible)));
|
|
2259
|
+
if (changed.length > 0)
|
|
2260
|
+
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
2261
|
+
const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
|
|
1402
2262
|
if (requeue.kind === 'refused')
|
|
1403
2263
|
return { outcome: 'refused', ...requeue.refusal };
|
|
1404
2264
|
const foreign = [...requeue.withdrawn.keys()].some((origin) => {
|
|
@@ -1416,23 +2276,23 @@ export function composeRuntime(options) {
|
|
|
1416
2276
|
const withdrawing = requeue.withdrawn.size > 0;
|
|
1417
2277
|
const input = {
|
|
1418
2278
|
local: localSaves,
|
|
1419
|
-
baseRoomVersion: replica.
|
|
1420
|
-
composition: compositionFor(replica.
|
|
2279
|
+
baseRoomVersion: replica.record.roomVersion,
|
|
2280
|
+
composition: compositionFor(replica.record),
|
|
1421
2281
|
operations: room.operations,
|
|
1422
2282
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
1423
2283
|
providerLimits: limits,
|
|
1424
2284
|
handle: room.handle,
|
|
1425
|
-
visible:
|
|
2285
|
+
visible: saving.visible,
|
|
1426
2286
|
changes,
|
|
1427
2287
|
bases: parts.flatMap(({ bases }) => bases),
|
|
2288
|
+
queued: queuedDocuments(replica.queue),
|
|
1428
2289
|
writtenBy,
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
binaries: replica.confirmed.record.binaries,
|
|
1432
|
-
composition: compositionOf(replica.confirmed.record),
|
|
1433
|
-
},
|
|
2290
|
+
unmodeled: await unmodeledIn(replica.record),
|
|
2291
|
+
validate: validatorOf(replica.record, saving.confirmed),
|
|
1434
2292
|
};
|
|
1435
|
-
const checked = withdrawing
|
|
2293
|
+
const checked = withdrawing
|
|
2294
|
+
? await buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
|
|
2295
|
+
: null;
|
|
1436
2296
|
if (checked?.kind === 'refused')
|
|
1437
2297
|
return { outcome: 'refused', ...checked.refusal };
|
|
1438
2298
|
const owners = [...requeue.owners].flatMap((origin) => {
|
|
@@ -1448,13 +2308,14 @@ export function composeRuntime(options) {
|
|
|
1448
2308
|
};
|
|
1449
2309
|
}
|
|
1450
2310
|
const built = !withdrawing
|
|
1451
|
-
? buildBatch(input)
|
|
2311
|
+
? await buildBatch(input)
|
|
1452
2312
|
: requeue.changes.length === 0
|
|
1453
2313
|
? null
|
|
1454
|
-
: buildBatch({
|
|
2314
|
+
: await buildBatch({
|
|
1455
2315
|
...input,
|
|
1456
|
-
visible:
|
|
2316
|
+
visible: await stepped(saving.visibleOver(requeue.queue)),
|
|
1457
2317
|
changes: requeue.changes,
|
|
2318
|
+
queued: queuedDocuments(requeue.queue),
|
|
1458
2319
|
});
|
|
1459
2320
|
if (built?.kind === 'refused')
|
|
1460
2321
|
return { outcome: 'refused', ...built.refusal };
|
|
@@ -1471,7 +2332,7 @@ export function composeRuntime(options) {
|
|
|
1471
2332
|
for (const [origin, replaced] of withdrawn) {
|
|
1472
2333
|
if (replaced === null)
|
|
1473
2334
|
continue;
|
|
1474
|
-
const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
|
|
2335
|
+
const carried = withdrawnDrafts(pendings.get(pendingKey(locator, origin))?.drafts ?? [], changes);
|
|
1475
2336
|
drafted.set(replaced.id, replaced === join?.batch
|
|
1476
2337
|
? [
|
|
1477
2338
|
...carried.map((held) => joinedDraft(held, saved)),
|
|
@@ -1485,59 +2346,77 @@ export function composeRuntime(options) {
|
|
|
1485
2346
|
for (const replaced of withdrawn.values()) {
|
|
1486
2347
|
if (replaced === null || !batches.includes(replaced))
|
|
1487
2348
|
continue;
|
|
1488
|
-
const limited =
|
|
2349
|
+
const limited = await stepped(limiting(batches, replaced, limits, saving.visibleOver));
|
|
1489
2350
|
if (limited !== null)
|
|
1490
2351
|
return { outcome: 'refused', ...limited };
|
|
1491
2352
|
}
|
|
1492
|
-
const
|
|
1493
|
-
|
|
1494
|
-
|
|
2353
|
+
const carried = carriedDrafts(locator, saved, consumed);
|
|
2354
|
+
const left = [...consumed, ...carried.keys()];
|
|
2355
|
+
const arrived = [...carried.values()];
|
|
2356
|
+
const done = await saveCommit(local, files, [
|
|
2357
|
+
{
|
|
1495
2358
|
locator,
|
|
2359
|
+
saving,
|
|
2360
|
+
before: replica,
|
|
1496
2361
|
queue: batches,
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
2362
|
+
write: {
|
|
2363
|
+
locator,
|
|
2364
|
+
queue: batches,
|
|
2365
|
+
saved: drafted,
|
|
2366
|
+
consumedRoomDrafts: left,
|
|
2367
|
+
roomDrafts: arrived,
|
|
2368
|
+
},
|
|
2369
|
+
},
|
|
2370
|
+
]);
|
|
2371
|
+
if (done === null)
|
|
1502
2372
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
2373
|
+
const { answer, touched } = done;
|
|
2374
|
+
if (answer.fenced !== null) {
|
|
2375
|
+
throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
|
|
1503
2376
|
}
|
|
1504
2377
|
if (fences.has(locator))
|
|
1505
2378
|
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1506
2379
|
for (const origin of withdrawn.keys())
|
|
1507
|
-
pendings.delete(origin);
|
|
2380
|
+
pendings.delete(pendingKey(locator, origin));
|
|
1508
2381
|
for (const [id, drafts] of drafted) {
|
|
1509
|
-
pendings.set(
|
|
2382
|
+
pendings.set(pendingKey(locator, id), {
|
|
2383
|
+
drafts,
|
|
2384
|
+
submission: { state: 'waiting' },
|
|
2385
|
+
lost: false,
|
|
2386
|
+
});
|
|
1510
2387
|
}
|
|
1511
|
-
|
|
1512
|
-
recorded = false;
|
|
1513
|
-
for (const path of consumed)
|
|
2388
|
+
for (const path of left)
|
|
1514
2389
|
forgetRoomDraft(locator, path);
|
|
1515
|
-
|
|
1516
|
-
|
|
2390
|
+
for (const record of arrived)
|
|
2391
|
+
rememberRoomDraft(record);
|
|
2392
|
+
savedReplica(locator, replica, batches, touched.get(locator), answer);
|
|
2393
|
+
publishChangeSet(changeSet(locator, {
|
|
2394
|
+
documents: answer.changed[locator] ?? [],
|
|
1517
2395
|
queue: true,
|
|
1518
|
-
drafts:
|
|
2396
|
+
drafts: [...left, ...arrived.map(({ path }) => path)],
|
|
1519
2397
|
}));
|
|
1520
|
-
|
|
1521
|
-
submitLater(locator);
|
|
2398
|
+
queuedSave(locator);
|
|
1522
2399
|
const identifier = join?.batch.id ??
|
|
1523
2400
|
built?.batch.id ??
|
|
1524
2401
|
[...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
|
|
1525
2402
|
return { outcome: 'saved', identifier };
|
|
1526
2403
|
}
|
|
1527
|
-
async function mark(local, locator,
|
|
1528
|
-
const
|
|
1529
|
-
|
|
1530
|
-
state
|
|
1531
|
-
(
|
|
1532
|
-
|
|
2404
|
+
async function mark(local, locator, batch, submission, lost) {
|
|
2405
|
+
const partner = partnerOf(batch, locator);
|
|
2406
|
+
for (const held of partner === null ? [locator] : [locator, partner]) {
|
|
2407
|
+
const state = pendings.get(pendingKey(held, batch.id));
|
|
2408
|
+
if (!continuing() ||
|
|
2409
|
+
state === undefined ||
|
|
2410
|
+
(JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
|
|
2411
|
+
continue;
|
|
2412
|
+
}
|
|
2413
|
+
await local.submit(held, batch.id, submission, lost);
|
|
2414
|
+
if (fences.has(held))
|
|
2415
|
+
continue;
|
|
2416
|
+
pendings.set(pendingKey(held, batch.id), { ...state, submission, lost });
|
|
2417
|
+
if (replicas.has(held))
|
|
2418
|
+
publishChangeSet(changeSet(held, { queue: true }));
|
|
1533
2419
|
}
|
|
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
2420
|
moveTo({ ...status, ...attention() });
|
|
1542
2421
|
}
|
|
1543
2422
|
async function keepUpload(local, locator, identifier, upload) {
|
|
@@ -1546,7 +2425,8 @@ export function composeRuntime(options) {
|
|
|
1546
2425
|
await local.keepUpload(locator, identifier, upload);
|
|
1547
2426
|
if (fences.has(locator))
|
|
1548
2427
|
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1549
|
-
|
|
2428
|
+
const key = pendingKey(locator, identifier);
|
|
2429
|
+
pendings.set(key, { ...pendings.get(key), upload });
|
|
1550
2430
|
if (!continuing())
|
|
1551
2431
|
throw new Error('connection refused');
|
|
1552
2432
|
}
|
|
@@ -1579,7 +2459,7 @@ export function composeRuntime(options) {
|
|
|
1579
2459
|
if (fencedSince(locator, since))
|
|
1580
2460
|
return failure({ kind: 'room-not-readable', locator });
|
|
1581
2461
|
if (result.kind === 'scoped-refusal') {
|
|
1582
|
-
const room = replicas.get(result.locator)?.
|
|
2462
|
+
const room = replicas.get(result.locator)?.record;
|
|
1583
2463
|
if (room !== undefined)
|
|
1584
2464
|
await scoped(result, room, await openLocal());
|
|
1585
2465
|
}
|
|
@@ -1592,21 +2472,62 @@ export function composeRuntime(options) {
|
|
|
1592
2472
|
return UNCERTAIN;
|
|
1593
2473
|
}
|
|
1594
2474
|
}
|
|
1595
|
-
function
|
|
2475
|
+
function located(room, path, destination) {
|
|
2476
|
+
const crossing = roomTarget(destination);
|
|
2477
|
+
if (crossing === null) {
|
|
2478
|
+
const named = target(path, destination);
|
|
2479
|
+
return named === null || !rooms.has(room) ? null : { room, path: named };
|
|
2480
|
+
}
|
|
2481
|
+
const held = [...rooms.values()].find((listed) => listed.handle === crossing.handle);
|
|
2482
|
+
return held === undefined ? null : { room: held.locator, path: crossing.path };
|
|
2483
|
+
}
|
|
2484
|
+
async function resolveReference(room, path, destination) {
|
|
2485
|
+
if (roomTarget(destination) === null && target(path, destination) === null) {
|
|
2486
|
+
return ABSENT_TARGET;
|
|
2487
|
+
}
|
|
2488
|
+
const at = located(room, path, destination);
|
|
2489
|
+
if (at === null)
|
|
2490
|
+
return UNREADABLE;
|
|
2491
|
+
if (locked(at.room))
|
|
2492
|
+
return LOCKED_UNREADABLE;
|
|
2493
|
+
const held = replicas.get(at.room);
|
|
2494
|
+
if (held !== undefined &&
|
|
2495
|
+
(await (await openLocal()).entries(held.record.key, 'visible', [at.path])).length > 0) {
|
|
2496
|
+
return { kind: 'document', ...at };
|
|
2497
|
+
}
|
|
2498
|
+
const history = await readHistory(at.room, at.path);
|
|
2499
|
+
if (history.kind === 'no-history' || history.kind === 'revision')
|
|
2500
|
+
return ABSENT_TARGET;
|
|
2501
|
+
if (history.kind !== 'history')
|
|
2502
|
+
return history;
|
|
2503
|
+
const [last] = history.revisions;
|
|
2504
|
+
if (last === undefined)
|
|
2505
|
+
return ABSENT_TARGET;
|
|
2506
|
+
return history.deleted === true
|
|
2507
|
+
? { kind: 'deleted', ...at, revision: last }
|
|
2508
|
+
: { kind: 'document', ...at };
|
|
2509
|
+
}
|
|
2510
|
+
function queuedFacts(locator, path, localId) {
|
|
1596
2511
|
for (const batch of replicas.get(locator)?.queue ?? []) {
|
|
1597
2512
|
const change = batch.changes[queuedFile(batch, path, localId)];
|
|
1598
2513
|
if (change?.kind === 'binary')
|
|
1599
|
-
return change
|
|
2514
|
+
return change;
|
|
1600
2515
|
}
|
|
1601
|
-
|
|
2516
|
+
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2517
|
+
return drafted?.localId === localId ? drafted : null;
|
|
2518
|
+
}
|
|
2519
|
+
function batchFile(batch) {
|
|
2520
|
+
return async (localId) => {
|
|
2521
|
+
const change = batch.changes.find((held) => held.kind === 'binary' && held.localId === localId);
|
|
2522
|
+
return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
|
|
2523
|
+
};
|
|
1602
2524
|
}
|
|
1603
2525
|
function binaryRoom(request) {
|
|
1604
2526
|
if (request.kind === 'path')
|
|
1605
2527
|
return request.locator;
|
|
1606
|
-
|
|
1607
|
-
return scope?.kind === 'room' ? scope.locator : null;
|
|
2528
|
+
return options.adapter.referrerScope(request.referrer)?.locator ?? null;
|
|
1608
2529
|
}
|
|
1609
|
-
async function
|
|
2530
|
+
async function transferBinary(request, transfer, answer) {
|
|
1610
2531
|
if (closed || refusal !== null)
|
|
1611
2532
|
return UNCERTAIN;
|
|
1612
2533
|
try {
|
|
@@ -1622,69 +2543,116 @@ export function composeRuntime(options) {
|
|
|
1622
2543
|
? `transfer binary ${request.locator} ${request.path}`
|
|
1623
2544
|
: `transfer binary ${request.fileRef}`);
|
|
1624
2545
|
try {
|
|
1625
|
-
const result = await
|
|
1626
|
-
|
|
2546
|
+
const result = await transfer(authorization);
|
|
2547
|
+
const settled = result;
|
|
2548
|
+
journal.record(`outcome binary ${settled.kind}`);
|
|
1627
2549
|
const locator = binaryRoom(request);
|
|
1628
2550
|
if (locator !== null && fencedSince(locator, since))
|
|
1629
2551
|
return failure({ kind: 'room-not-readable', locator });
|
|
1630
|
-
if (
|
|
1631
|
-
const room = replicas.get(
|
|
2552
|
+
if (settled.kind === 'scoped-refusal') {
|
|
2553
|
+
const room = replicas.get(settled.locator)?.record;
|
|
1632
2554
|
if (room !== undefined)
|
|
1633
|
-
await scoped(
|
|
2555
|
+
await scoped(settled, room, await openLocal());
|
|
1634
2556
|
}
|
|
1635
|
-
if (
|
|
2557
|
+
if (settled.kind === 'revoked-authorization')
|
|
1636
2558
|
await refuse('revoked-authorization');
|
|
1637
|
-
return
|
|
2559
|
+
return answer(result);
|
|
1638
2560
|
}
|
|
1639
2561
|
catch {
|
|
1640
2562
|
journal.record('outcome binary transport-uncertainty');
|
|
1641
2563
|
return UNCERTAIN;
|
|
1642
2564
|
}
|
|
1643
2565
|
}
|
|
1644
|
-
async function
|
|
1645
|
-
|
|
2566
|
+
async function readBinary(request, name) {
|
|
2567
|
+
if (closed)
|
|
2568
|
+
return UNCERTAIN;
|
|
2569
|
+
const held = await keptFiles.find(name);
|
|
2570
|
+
if (held !== null)
|
|
2571
|
+
return held;
|
|
2572
|
+
return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
|
|
2573
|
+
}
|
|
2574
|
+
async function binaryPath(locator, path, revision) {
|
|
1646
2575
|
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);
|
|
2576
|
+
return { kind: 'path', locator, path, revision };
|
|
1659
2577
|
const replica = replicas.get(locator);
|
|
1660
|
-
|
|
2578
|
+
if (replica === undefined)
|
|
2579
|
+
return null;
|
|
2580
|
+
const local = await openLocal();
|
|
2581
|
+
const [held] = await local.entries(replica.record.key, 'visible', [path]);
|
|
2582
|
+
if (held?.binary === undefined || held.binary === null)
|
|
2583
|
+
return null;
|
|
2584
|
+
if (held.binary.state === 'pending')
|
|
2585
|
+
return { kind: 'pending', localId: held.binary.localId };
|
|
2586
|
+
const record = binaryRecord(path, held.binary, held.revision ?? undefined);
|
|
2587
|
+
if (record.state !== 'accepted')
|
|
2588
|
+
return null;
|
|
2589
|
+
const queued = replica.queue.flatMap((batch) => batch.changes);
|
|
2590
|
+
const sources = queued.flatMap((change) => (change.kind === 'move' ? [change.path] : []));
|
|
2591
|
+
const confirmedLayer = await stepped(slicing(await local.entries(replica.record.key, 'confirmed', [path, ...sources]), [
|
|
2592
|
+
path,
|
|
2593
|
+
...sources,
|
|
2594
|
+
]));
|
|
1661
2595
|
const holds = (at) => {
|
|
1662
|
-
const kept =
|
|
2596
|
+
const kept = confirmedLayer.binaries[at];
|
|
1663
2597
|
return (kept?.state === 'accepted' &&
|
|
1664
2598
|
kept.fileRef === record.fileRef &&
|
|
1665
|
-
|
|
2599
|
+
confirmedLayer.revisions[at] === record.revision);
|
|
1666
2600
|
};
|
|
1667
|
-
let
|
|
1668
|
-
for (let index = queued.length - 1; index >= 0 && !holds(
|
|
2601
|
+
let confirmedAt = path;
|
|
2602
|
+
for (let index = queued.length - 1; index >= 0 && !holds(confirmedAt); index -= 1) {
|
|
1669
2603
|
const change = queued[index];
|
|
1670
|
-
if (change.kind === 'move' && change.to ===
|
|
1671
|
-
|
|
2604
|
+
if (change.kind === 'move' && change.to === confirmedAt)
|
|
2605
|
+
confirmedAt = change.path;
|
|
1672
2606
|
}
|
|
1673
|
-
return
|
|
2607
|
+
return {
|
|
1674
2608
|
kind: 'path',
|
|
1675
2609
|
locator,
|
|
1676
|
-
path:
|
|
2610
|
+
path: confirmedAt,
|
|
1677
2611
|
revision: record.revision,
|
|
1678
2612
|
fileRef: record.fileRef,
|
|
1679
|
-
}
|
|
2613
|
+
};
|
|
2614
|
+
}
|
|
2615
|
+
async function readBinaryPath(locator, path, revision) {
|
|
2616
|
+
await ready;
|
|
2617
|
+
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2618
|
+
const asked = drafted !== undefined && drafted.localId === revision
|
|
2619
|
+
? { kind: 'pending', localId: revision }
|
|
2620
|
+
: await binaryPath(locator, path, revision);
|
|
2621
|
+
if (asked === null)
|
|
2622
|
+
return absentBinary(path);
|
|
2623
|
+
if (asked.kind === 'pending') {
|
|
2624
|
+
const facts = queuedFacts(locator, path, asked.localId);
|
|
2625
|
+
return facts === null
|
|
2626
|
+
? absentBinary(path)
|
|
2627
|
+
: {
|
|
2628
|
+
kind: 'binary',
|
|
2629
|
+
file: asked.localId,
|
|
2630
|
+
byteLength: facts.byteLength,
|
|
2631
|
+
mediaType: facts.mediaType,
|
|
2632
|
+
};
|
|
2633
|
+
}
|
|
2634
|
+
return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
|
|
2635
|
+
}
|
|
2636
|
+
async function linkBinaryPath(locator, path, revision) {
|
|
2637
|
+
await ready;
|
|
2638
|
+
const asked = await binaryPath(locator, path, revision);
|
|
2639
|
+
if (asked === null || asked.kind === 'pending')
|
|
2640
|
+
return absentBinary(path);
|
|
2641
|
+
return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
|
|
1680
2642
|
}
|
|
1681
2643
|
async function readBinaryReference(reference) {
|
|
1682
2644
|
await ready;
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
2645
|
+
const scope = options.adapter.referrerScope(reference.referrer);
|
|
2646
|
+
if (scope === null)
|
|
2647
|
+
return absentReference(reference.fileRef);
|
|
2648
|
+
return readBinary({ kind: 'reference', fileRef: reference.fileRef, referrer: reference.referrer }, { locator: scope.locator, path: scope.path ?? '', revision: reference.fileRef });
|
|
2649
|
+
}
|
|
2650
|
+
function readPart(file, part) {
|
|
2651
|
+
return options.database.ask({ kind: 'file.part', key: file, part });
|
|
2652
|
+
}
|
|
2653
|
+
async function cleanUp() {
|
|
2654
|
+
const freed = await options.database.ask({ kind: 'opened.clean' });
|
|
2655
|
+
return freed + ((await options.shell.cleanUp?.()) ?? 0);
|
|
1688
2656
|
}
|
|
1689
2657
|
function declaredAgents() {
|
|
1690
2658
|
for (const capability of capabilities) {
|
|
@@ -1702,7 +2670,14 @@ export function composeRuntime(options) {
|
|
|
1702
2670
|
}
|
|
1703
2671
|
return undefined;
|
|
1704
2672
|
}
|
|
1705
|
-
|
|
2673
|
+
function calledRooms(name, input) {
|
|
2674
|
+
const member = declaredAction(name)?.room;
|
|
2675
|
+
if (member === undefined || input === null || typeof input !== 'object')
|
|
2676
|
+
return [];
|
|
2677
|
+
const named = input[member];
|
|
2678
|
+
return (Array.isArray(named) ? named : [named]).filter((room) => typeof room === 'string');
|
|
2679
|
+
}
|
|
2680
|
+
async function invokeAction(name, input, dispatching) {
|
|
1706
2681
|
await ready;
|
|
1707
2682
|
if (closed || refusal !== null)
|
|
1708
2683
|
return UNCERTAIN;
|
|
@@ -1716,11 +2691,12 @@ export function composeRuntime(options) {
|
|
|
1716
2691
|
}
|
|
1717
2692
|
if (closed || authorization === undefined || refusal !== null)
|
|
1718
2693
|
return UNCERTAIN;
|
|
2694
|
+
await dispatching?.();
|
|
1719
2695
|
journal.record(`transfer action ${name}`);
|
|
1720
2696
|
try {
|
|
1721
2697
|
const result = await options.adapter.action(authorization, { name, input });
|
|
1722
2698
|
if (result.kind === 'scoped-refusal') {
|
|
1723
|
-
const room = replicas.get(result.locator)?.
|
|
2699
|
+
const room = replicas.get(result.locator)?.record;
|
|
1724
2700
|
if (room !== undefined)
|
|
1725
2701
|
await scoped(result, room, await openLocal());
|
|
1726
2702
|
}
|
|
@@ -1736,85 +2712,336 @@ export function composeRuntime(options) {
|
|
|
1736
2712
|
return UNCERTAIN;
|
|
1737
2713
|
}
|
|
1738
2714
|
}
|
|
2715
|
+
function unsettled(result) {
|
|
2716
|
+
return result.kind !== 'action' && result.retryable;
|
|
2717
|
+
}
|
|
2718
|
+
async function recoveredAction(name, input) {
|
|
2719
|
+
const operation = operationOf(input);
|
|
2720
|
+
if (operation === null)
|
|
2721
|
+
return invokeAction(name, input);
|
|
2722
|
+
const kept = async () => (await openLocal()).keepCall({ operation, name, input });
|
|
2723
|
+
calling.add(operation);
|
|
2724
|
+
try {
|
|
2725
|
+
let result = await invokeAction(name, input, kept);
|
|
2726
|
+
if (unsettled(result)) {
|
|
2727
|
+
await recovery.attempt(`action ${operation}`, async () => {
|
|
2728
|
+
if (closed || refusal !== null)
|
|
2729
|
+
return 'done';
|
|
2730
|
+
if (!socketOpen)
|
|
2731
|
+
return 'again';
|
|
2732
|
+
result = await invokeAction(name, input, kept);
|
|
2733
|
+
return unsettled(result) ? 'again' : 'done';
|
|
2734
|
+
}, { again: true });
|
|
2735
|
+
}
|
|
2736
|
+
if (!unsettled(result))
|
|
2737
|
+
await (await openLocal()).dropCall(operation);
|
|
2738
|
+
return result;
|
|
2739
|
+
}
|
|
2740
|
+
finally {
|
|
2741
|
+
calling.delete(operation);
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
async function resendCalls(signal) {
|
|
2745
|
+
if (!socketOpen || closed || stopping)
|
|
2746
|
+
return 'done';
|
|
2747
|
+
const local = await openLocal();
|
|
2748
|
+
for (const held of await local.calls()) {
|
|
2749
|
+
if (calling.has(held.operation))
|
|
2750
|
+
continue;
|
|
2751
|
+
if (closed || signal.aborted)
|
|
2752
|
+
return 'done';
|
|
2753
|
+
if (pastHorizon(held.operation)) {
|
|
2754
|
+
await local.dropCall(held.operation);
|
|
2755
|
+
continue;
|
|
2756
|
+
}
|
|
2757
|
+
calling.add(held.operation);
|
|
2758
|
+
try {
|
|
2759
|
+
if (unsettled(await invokeAction(held.name, held.input)))
|
|
2760
|
+
return 'again';
|
|
2761
|
+
await local.dropCall(held.operation);
|
|
2762
|
+
}
|
|
2763
|
+
finally {
|
|
2764
|
+
calling.delete(held.operation);
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
return 'done';
|
|
2768
|
+
}
|
|
1739
2769
|
function continuing() {
|
|
1740
2770
|
return !closed && !discoveryPaused;
|
|
1741
2771
|
}
|
|
1742
2772
|
function reachedBy(locator, roomVersion) {
|
|
1743
2773
|
const held = replicas.get(locator);
|
|
1744
|
-
return (held !== undefined &&
|
|
1745
|
-
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion));
|
|
2774
|
+
return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
|
|
1746
2775
|
}
|
|
1747
|
-
function writtenAfter(locator, roomVersion, path) {
|
|
1748
|
-
const
|
|
2776
|
+
async function writtenAfter(locator, roomVersion, path) {
|
|
2777
|
+
const held = replicas.get(locator);
|
|
2778
|
+
if (held === undefined)
|
|
2779
|
+
return false;
|
|
2780
|
+
const [entry] = await (await openLocal()).entries(held.record.key, 'confirmed', [path]);
|
|
2781
|
+
const written = entry?.written ?? undefined;
|
|
1749
2782
|
return (written !== undefined &&
|
|
1750
2783
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
1751
2784
|
!options.adapter.reached(locator, roomVersion, written));
|
|
1752
2785
|
}
|
|
1753
|
-
|
|
1754
|
-
|
|
2786
|
+
function overriddenTexts(texts, overrides) {
|
|
2787
|
+
return async (paths) => {
|
|
2788
|
+
const known = new Map(await texts(paths.filter((path) => !overrides.has(path))));
|
|
2789
|
+
for (const path of paths) {
|
|
2790
|
+
const entry = overrides.get(path);
|
|
2791
|
+
if (entry === undefined)
|
|
2792
|
+
continue;
|
|
2793
|
+
if (entry === null)
|
|
2794
|
+
known.delete(path);
|
|
2795
|
+
else
|
|
2796
|
+
known.set(path, entry.binary !== null ? null : entry.content);
|
|
2797
|
+
}
|
|
2798
|
+
return known;
|
|
2799
|
+
};
|
|
2800
|
+
}
|
|
2801
|
+
async function unreadableRoom(texts, walk, paths) {
|
|
2802
|
+
if (paths !== null) {
|
|
2803
|
+
const text = {};
|
|
2804
|
+
for (const [path, content] of await texts(paths))
|
|
2805
|
+
if (content !== null)
|
|
2806
|
+
text[path] = content;
|
|
2807
|
+
return stepped(unreadableIn(text, Object.keys(text)));
|
|
2808
|
+
}
|
|
2809
|
+
for (let after = null;;) {
|
|
2810
|
+
const page = await walk(after);
|
|
2811
|
+
const text = {};
|
|
2812
|
+
for (const { path, content } of page.entries)
|
|
2813
|
+
if (content !== null)
|
|
2814
|
+
text[path] = content;
|
|
2815
|
+
const refused = await stepped(unreadableIn(text, Object.keys(text)));
|
|
2816
|
+
if (refused !== null)
|
|
2817
|
+
return refused;
|
|
2818
|
+
if (page.next === null)
|
|
2819
|
+
return null;
|
|
2820
|
+
after = page.next;
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
async function validatedRoom(local, record, view, previous, overrides = null) {
|
|
2824
|
+
const composition = compositionOf(record);
|
|
2825
|
+
const modeled = record.model.status !== 'unavailable' || record.selectionModel.status !== 'unavailable';
|
|
2826
|
+
const lookup = overrides === null
|
|
2827
|
+
? lookupOf(local, record.key, view)
|
|
2828
|
+
: overriddenLookup(lookupOf(local, record.key, view), overrides);
|
|
2829
|
+
const texts = overrides === null
|
|
2830
|
+
? textsOf(local, record.key, view)
|
|
2831
|
+
: overriddenTexts(textsOf(local, record.key, view), overrides);
|
|
2832
|
+
const walk = overrides === null
|
|
2833
|
+
? walkOf(local, record.key, view)
|
|
2834
|
+
: overriddenWalk(walkOf(local, record.key, view), overrides);
|
|
2835
|
+
let changes = null;
|
|
2836
|
+
let reshaped = false;
|
|
2837
|
+
if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
|
|
2838
|
+
const [was, now] = await Promise.all([
|
|
2839
|
+
local.entries(record.key, 'confirmed', previous.changed),
|
|
2840
|
+
overrides === null
|
|
2841
|
+
? local.entries(record.key, view, previous.changed)
|
|
2842
|
+
: Promise.resolve([]),
|
|
2843
|
+
]);
|
|
2844
|
+
const before = new Map(was.map((entry) => [entry.path, entry]));
|
|
2845
|
+
const after = overrides === null
|
|
2846
|
+
? new Map(now.map((entry) => [entry.path, entry]))
|
|
2847
|
+
: overrides;
|
|
2848
|
+
changes = previous.changed.flatMap((path) => {
|
|
2849
|
+
const one = before.get(path) ?? null;
|
|
2850
|
+
const other = after.get(path) ?? null;
|
|
2851
|
+
reshaped ||= textDiffers(one, other);
|
|
2852
|
+
const held = textOf(one);
|
|
2853
|
+
const next = textOf(other);
|
|
2854
|
+
return held === next ? [] : [{ path, was: held, now: next }];
|
|
2855
|
+
});
|
|
2856
|
+
}
|
|
2857
|
+
const named = changes === null ? null : changes.map(({ path }) => path);
|
|
2858
|
+
const textChanged = changes === null || changes.length > 0 || reshaped;
|
|
2859
|
+
const modelsChanged = previous === null || !sameComposition(compositionOf(previous.record), composition);
|
|
2860
|
+
if (modeled && (textChanged || modelsChanged)) {
|
|
2861
|
+
const clean = previous !== null &&
|
|
2862
|
+
changes !== null &&
|
|
2863
|
+
(await changedCleanly(changes, compositionOf(previous.record), composition, {
|
|
2864
|
+
lookup,
|
|
2865
|
+
texts,
|
|
2866
|
+
referrers: referrersOf(local, record.key, 'confirmed'),
|
|
2867
|
+
}));
|
|
2868
|
+
if (clean)
|
|
2869
|
+
return null;
|
|
2870
|
+
const verdict = await validatedCollection(walk, lookup, composition);
|
|
2871
|
+
if (verdict.verdict === 'accept')
|
|
2872
|
+
return null;
|
|
2873
|
+
if (verdict.rule !== 'model-unavailable')
|
|
2874
|
+
return { kind: 'refused', refusal: verdict };
|
|
2875
|
+
return unreadableRoom(texts, walk, named);
|
|
2876
|
+
}
|
|
2877
|
+
if (textChanged)
|
|
2878
|
+
return unreadableRoom(texts, walk, named);
|
|
2879
|
+
return null;
|
|
2880
|
+
}
|
|
2881
|
+
function refreshingOf(local, key, liveness) {
|
|
2882
|
+
return {
|
|
2883
|
+
prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
|
|
2884
|
+
async stage(page) {
|
|
2885
|
+
const batches = batched(page.entries);
|
|
2886
|
+
for (const [index, entries] of batches.entries()) {
|
|
2887
|
+
const last = index === batches.length - 1;
|
|
2888
|
+
const staged = await exclusive(async () => {
|
|
2889
|
+
if (!liveness())
|
|
2890
|
+
return false;
|
|
2891
|
+
await local.refresh(key, last
|
|
2892
|
+
? { ...page, entries }
|
|
2893
|
+
: { entries, removals: [], folders: [], folderRemovals: [] });
|
|
2894
|
+
return true;
|
|
2895
|
+
});
|
|
2896
|
+
if (!staged)
|
|
2897
|
+
return false;
|
|
2898
|
+
}
|
|
2899
|
+
return true;
|
|
2900
|
+
},
|
|
2901
|
+
};
|
|
2902
|
+
}
|
|
2903
|
+
function batched(entries) {
|
|
2904
|
+
const batches = [[]];
|
|
2905
|
+
let spent = 0;
|
|
2906
|
+
for (const entry of entries) {
|
|
2907
|
+
const size = (entry.content?.length ?? 0) + ROW_WEIGHT;
|
|
2908
|
+
if (spent + size > STAGE_BYTES && batches.at(-1).length > 0) {
|
|
2909
|
+
batches.push([]);
|
|
2910
|
+
spent = 0;
|
|
2911
|
+
}
|
|
2912
|
+
batches.at(-1).push(entry);
|
|
2913
|
+
spent += size;
|
|
2914
|
+
}
|
|
2915
|
+
return batches;
|
|
2916
|
+
}
|
|
2917
|
+
async function pullRefresh(authorized, held, liveness) {
|
|
2918
|
+
if (!liveness())
|
|
1755
2919
|
return unconfirmed;
|
|
2920
|
+
const turn = generation;
|
|
2921
|
+
const { record } = held;
|
|
2922
|
+
const base = held.confirmed;
|
|
2923
|
+
const local = await openLocal();
|
|
1756
2924
|
try {
|
|
1757
|
-
|
|
1758
|
-
const
|
|
1759
|
-
|
|
1760
|
-
|
|
2925
|
+
await exclusive(() => local.unrefresh(record.key));
|
|
2926
|
+
const result = await advancement(reads, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, liveness));
|
|
2927
|
+
if (result.kind !== 'advanced') {
|
|
2928
|
+
journal.record(`transfer refresh ${record.locator} ${result.kind}`);
|
|
2929
|
+
return result;
|
|
2930
|
+
}
|
|
2931
|
+
const refused = await validatedRoom(local, result.record, 'refreshed', {
|
|
2932
|
+
record,
|
|
2933
|
+
changed: [...result.changed],
|
|
2934
|
+
});
|
|
2935
|
+
if (refused !== null) {
|
|
2936
|
+
journal.record(`transfer refresh ${record.locator} refused`);
|
|
2937
|
+
return refused;
|
|
2938
|
+
}
|
|
2939
|
+
journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
|
|
2940
|
+
return { kind: 'advanced', record: result.record, base };
|
|
1761
2941
|
}
|
|
1762
2942
|
catch (error) {
|
|
1763
|
-
journal.record(`transfer refresh ${
|
|
2943
|
+
journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
|
|
1764
2944
|
throw error;
|
|
1765
2945
|
}
|
|
2946
|
+
finally {
|
|
2947
|
+
reads.finished(record.locator, turn);
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
const heldContents = new Map();
|
|
2951
|
+
function heldContentOf(local, key) {
|
|
2952
|
+
const known = heldContents.get(key);
|
|
2953
|
+
if (known !== undefined)
|
|
2954
|
+
return known;
|
|
2955
|
+
if (heldContents.size > ROOM_READS)
|
|
2956
|
+
heldContents.clear();
|
|
2957
|
+
const made = {
|
|
2958
|
+
files: (after, limit) => local.files(key, 'confirmed', after, limit),
|
|
2959
|
+
async contents(paths) {
|
|
2960
|
+
return new Map((await local.entries(key, 'confirmed', paths)).flatMap(({ path, content, revision }) => content === null || revision === null ? [] : [[path, { content, revision }]]));
|
|
2961
|
+
},
|
|
2962
|
+
};
|
|
2963
|
+
heldContents.set(key, made);
|
|
2964
|
+
return made;
|
|
1766
2965
|
}
|
|
1767
|
-
function staging(local,
|
|
2966
|
+
function staging(local, liveness) {
|
|
1768
2967
|
return {
|
|
1769
2968
|
held: (locator) => transfers.get(locator) ?? null,
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
const
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
2969
|
+
async stage(record, page, composition) {
|
|
2970
|
+
const bytes = pageBytes(page);
|
|
2971
|
+
const batches = batched(page.entries);
|
|
2972
|
+
let key = record.key;
|
|
2973
|
+
for (const [index, entries] of batches.entries()) {
|
|
2974
|
+
const last = index === batches.length - 1;
|
|
2975
|
+
const staged = await exclusive(async () => {
|
|
2976
|
+
if (!liveness())
|
|
2977
|
+
return null;
|
|
2978
|
+
if (index === 0 && !(await admitted(local, record.locator, bytes)))
|
|
2979
|
+
return null;
|
|
2980
|
+
try {
|
|
2981
|
+
return await local.stage(record, composition, { entries, folders: last ? page.folders : [] }, key, last);
|
|
2982
|
+
}
|
|
2983
|
+
catch (error) {
|
|
2984
|
+
if (!exceeded(error))
|
|
2985
|
+
throw error;
|
|
2986
|
+
await exceededOn(record.locator, bytes);
|
|
2987
|
+
return null;
|
|
2988
|
+
}
|
|
2989
|
+
});
|
|
2990
|
+
if (staged === null)
|
|
2991
|
+
return null;
|
|
2992
|
+
key = staged;
|
|
1785
2993
|
}
|
|
1786
|
-
transfers.set(record.locator, record);
|
|
1787
|
-
|
|
1788
|
-
return
|
|
1789
|
-
}
|
|
2994
|
+
transfers.set(record.locator, { ...record, key });
|
|
2995
|
+
liveQueries.recover();
|
|
2996
|
+
return key;
|
|
2997
|
+
},
|
|
2998
|
+
async refreshing(key) {
|
|
2999
|
+
await exclusive(() => local.unrefresh(key));
|
|
3000
|
+
return refreshingOf(local, key, liveness);
|
|
3001
|
+
},
|
|
1790
3002
|
abandon: (locator) => exclusive(async () => {
|
|
1791
|
-
if (!
|
|
3003
|
+
if (!liveness())
|
|
1792
3004
|
return;
|
|
1793
3005
|
await local.abandon(locator);
|
|
1794
3006
|
transfers.delete(locator);
|
|
1795
|
-
|
|
3007
|
+
liveQueries.recover();
|
|
3008
|
+
sweep();
|
|
3009
|
+
}),
|
|
3010
|
+
setAside: (locator) => exclusive(async () => {
|
|
3011
|
+
if (!liveness())
|
|
3012
|
+
return;
|
|
3013
|
+
await local.abandon(locator, true);
|
|
3014
|
+
transfers.delete(locator);
|
|
3015
|
+
liveQueries.recover();
|
|
1796
3016
|
}),
|
|
3017
|
+
release: (key) => exclusive(async () => {
|
|
3018
|
+
await local.release(key);
|
|
3019
|
+
sweep();
|
|
3020
|
+
}),
|
|
3021
|
+
content: (key) => heldContentOf(local, key),
|
|
3022
|
+
validate: (record, view) => validatedRoom(local, record, view, null),
|
|
1797
3023
|
};
|
|
1798
3024
|
}
|
|
1799
|
-
function reusable(room) {
|
|
1800
|
-
const record = replicas.get(room.locator)?.
|
|
3025
|
+
function reusable(room, local) {
|
|
3026
|
+
const record = replicas.get(room.locator)?.record;
|
|
1801
3027
|
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1802
3028
|
return null;
|
|
1803
|
-
return
|
|
3029
|
+
return heldContentOf(local, record.key);
|
|
1804
3030
|
}
|
|
1805
|
-
async function pullBootstrap(authorized, room, local,
|
|
1806
|
-
if (!
|
|
3031
|
+
async function pullBootstrap(authorized, room, local, liveness) {
|
|
3032
|
+
if (!liveness())
|
|
1807
3033
|
return unconfirmed;
|
|
3034
|
+
const turn = generation;
|
|
1808
3035
|
if (replicas.has(room.locator)) {
|
|
1809
3036
|
catching.add(room.locator);
|
|
1810
|
-
|
|
3037
|
+
liveQueries.recover();
|
|
1811
3038
|
}
|
|
1812
3039
|
try {
|
|
1813
|
-
const result = await bootstrapRoom(
|
|
3040
|
+
const result = await bootstrapRoom(reads, authorized, room, {
|
|
1814
3041
|
collection: modelOf(room.locator),
|
|
1815
3042
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
1816
|
-
}, (models, held) => (
|
|
1817
|
-
const detail = result.kind === 'staged' ? result.
|
|
3043
|
+
}, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
|
|
3044
|
+
const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
|
|
1818
3045
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1819
3046
|
if (result.kind !== 'staged')
|
|
1820
3047
|
caughtUpTo(room.locator);
|
|
@@ -1825,18 +3052,33 @@ export function composeRuntime(options) {
|
|
|
1825
3052
|
caughtUpTo(room.locator);
|
|
1826
3053
|
throw error;
|
|
1827
3054
|
}
|
|
3055
|
+
finally {
|
|
3056
|
+
reads.finished(room.locator, turn);
|
|
3057
|
+
}
|
|
1828
3058
|
}
|
|
1829
3059
|
function caughtUpTo(locator) {
|
|
1830
3060
|
if (catching.delete(locator))
|
|
1831
|
-
|
|
3061
|
+
liveQueries.recover();
|
|
3062
|
+
}
|
|
3063
|
+
function behind(room) {
|
|
3064
|
+
const held = replicas.get(room.locator)?.record.roomVersion;
|
|
3065
|
+
return (held !== undefined &&
|
|
3066
|
+
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
1832
3067
|
}
|
|
1833
3068
|
async function advance(room, local, authorized, live) {
|
|
1834
|
-
|
|
3069
|
+
const carried = reads.carried(room.locator);
|
|
3070
|
+
const withheld = await advanceOnce(room, local, authorized, live);
|
|
3071
|
+
if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
|
|
3072
|
+
return withheld;
|
|
3073
|
+
}
|
|
3074
|
+
return advanceOnce(room, local, authorized, live);
|
|
3075
|
+
}
|
|
3076
|
+
async function advanceOnce(room, local, authorized, live) {
|
|
1835
3077
|
if (!live() || paused.has(room.locator))
|
|
1836
3078
|
return unconfirmed;
|
|
1837
3079
|
const held = replicas.get(room.locator);
|
|
1838
3080
|
if (held !== undefined) {
|
|
1839
|
-
const refreshed = await pullRefresh(authorized, held
|
|
3081
|
+
const refreshed = await pullRefresh(authorized, held, live);
|
|
1840
3082
|
if (!live())
|
|
1841
3083
|
return unconfirmed;
|
|
1842
3084
|
if (refreshed.kind === 'still-current') {
|
|
@@ -1851,10 +3093,11 @@ export function composeRuntime(options) {
|
|
|
1851
3093
|
return null;
|
|
1852
3094
|
}
|
|
1853
3095
|
if (refreshed.kind === 'advanced') {
|
|
1854
|
-
|
|
1855
|
-
|
|
3096
|
+
const landing = { kind: 'refreshed', base: refreshed.base };
|
|
3097
|
+
if (!(await publish(local, room, refreshed.record, landing, live))) {
|
|
3098
|
+
const now = replicas.get(room.locator)?.record.roomVersion;
|
|
1856
3099
|
const reached = now !== undefined &&
|
|
1857
|
-
options.adapter.reached(room.locator, now, refreshed.
|
|
3100
|
+
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
1858
3101
|
return reached ? null : advance(room, local, authorized, live);
|
|
1859
3102
|
}
|
|
1860
3103
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
@@ -1864,16 +3107,18 @@ export function composeRuntime(options) {
|
|
|
1864
3107
|
return refreshed;
|
|
1865
3108
|
if (opened !== null) {
|
|
1866
3109
|
owed.add(room.locator);
|
|
1867
|
-
|
|
3110
|
+
liveQueries.recover();
|
|
1868
3111
|
return unconfirmed;
|
|
1869
3112
|
}
|
|
3113
|
+
if (reloadsDue.get(room.locator) === generation)
|
|
3114
|
+
return unconfirmed;
|
|
1870
3115
|
}
|
|
1871
3116
|
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
1872
3117
|
if (!live())
|
|
1873
3118
|
return unconfirmed;
|
|
1874
3119
|
if (bootstrapped.kind !== 'staged')
|
|
1875
3120
|
return bootstrapped;
|
|
1876
|
-
await publish(local, room, bootstrapped.
|
|
3121
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
1877
3122
|
return null;
|
|
1878
3123
|
}
|
|
1879
3124
|
async function withhold(withheld, room, local, progress) {
|
|
@@ -1946,57 +3191,254 @@ export function composeRuntime(options) {
|
|
|
1946
3191
|
(accepted.author === undefined ? '' : ` author ${accepted.author}`) +
|
|
1947
3192
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1948
3193
|
}
|
|
1949
|
-
|
|
1950
|
-
|
|
3194
|
+
function settling(acceptance) {
|
|
3195
|
+
return acceptance !== null && 'apply' in acceptance;
|
|
3196
|
+
}
|
|
3197
|
+
function refusalOf(acceptance) {
|
|
3198
|
+
return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
|
|
3199
|
+
}
|
|
3200
|
+
async function acceptance(room, local, submitted, accepted) {
|
|
1951
3201
|
if (!continuing())
|
|
1952
3202
|
return unconfirmed;
|
|
1953
3203
|
const current = replicas.get(room.locator);
|
|
1954
3204
|
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
1955
3205
|
return null;
|
|
1956
3206
|
}
|
|
1957
|
-
const held = current.
|
|
3207
|
+
const held = current.record.roomVersion;
|
|
1958
3208
|
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
|
-
|
|
3209
|
+
let record = current.record;
|
|
3210
|
+
let changes;
|
|
3211
|
+
if (!options.adapter.reached(room.locator, held, accepted.roomVersion)) {
|
|
3212
|
+
const paths = [...returnedPaths(submitted, accepted)];
|
|
3213
|
+
const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
|
|
3214
|
+
const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
|
|
3215
|
+
record = {
|
|
3216
|
+
...record,
|
|
3217
|
+
roomVersion: options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
3218
|
+
? accepted.roomVersion
|
|
3219
|
+
: held,
|
|
3220
|
+
};
|
|
3221
|
+
const overrides = new Map([
|
|
3222
|
+
...back.set.map((entry) => [entry.path, entry]),
|
|
3223
|
+
...back.removed.map((path) => [path, null]),
|
|
3224
|
+
]);
|
|
3225
|
+
const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
|
|
3226
|
+
if (refused !== null) {
|
|
3227
|
+
const key = pendingKey(room.locator, submitted.id);
|
|
3228
|
+
return {
|
|
3229
|
+
locator: room.locator,
|
|
3230
|
+
left: [],
|
|
3231
|
+
commit: {
|
|
3232
|
+
rooms: [],
|
|
3233
|
+
writes: [
|
|
3234
|
+
{
|
|
3235
|
+
locator: room.locator,
|
|
3236
|
+
accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
|
|
3237
|
+
},
|
|
3238
|
+
],
|
|
3239
|
+
},
|
|
3240
|
+
refused,
|
|
3241
|
+
apply() {
|
|
3242
|
+
const state = pendings.get(key);
|
|
3243
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
3244
|
+
pendings.set(key, { ...state, accepted: accepted.roomVersion });
|
|
3245
|
+
return changeSet(room.locator, { queue: true });
|
|
3246
|
+
},
|
|
3247
|
+
};
|
|
3248
|
+
}
|
|
3249
|
+
changes = back;
|
|
3250
|
+
}
|
|
3251
|
+
const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
|
|
3252
|
+
return {
|
|
3253
|
+
...settled,
|
|
3254
|
+
apply(answer) {
|
|
3255
|
+
const set = settled.apply(answer);
|
|
3256
|
+
pendings.delete(pendingKey(room.locator, submitted.id));
|
|
3257
|
+
unreadable.delete(room.locator);
|
|
3258
|
+
sourceRefusals.delete(room.locator);
|
|
3259
|
+
declarationRefusals.delete(room.locator);
|
|
3260
|
+
list(room, true);
|
|
3261
|
+
return set;
|
|
3262
|
+
},
|
|
3263
|
+
};
|
|
3264
|
+
}
|
|
3265
|
+
async function adoptedTogether(local, acceptances) {
|
|
3266
|
+
const settlements = acceptances.filter(settling);
|
|
3267
|
+
if (settlements.length > 0) {
|
|
3268
|
+
publishChangeSet(await together(local, settlements));
|
|
3269
|
+
moveTo({ ...status, ...attention() });
|
|
3270
|
+
}
|
|
3271
|
+
await unpair(local);
|
|
1982
3272
|
}
|
|
1983
3273
|
async function adopted(room, local, submitted, accepted) {
|
|
1984
|
-
const failed = await exclusive(() =>
|
|
3274
|
+
const failed = await exclusive(async () => {
|
|
3275
|
+
const adopting = await acceptance(room, local, submitted, accepted);
|
|
3276
|
+
await adoptedTogether(local, [adopting]);
|
|
3277
|
+
return refusalOf(adopting);
|
|
3278
|
+
});
|
|
3279
|
+
if (failed === null)
|
|
3280
|
+
return true;
|
|
3281
|
+
if (failed.kind === 'refused' && continuing())
|
|
3282
|
+
refusedSource(room, failed);
|
|
3283
|
+
return false;
|
|
3284
|
+
}
|
|
3285
|
+
async function adoptedMove(room, partner, local, submitted, accepted) {
|
|
3286
|
+
const other = discovered.get(partner);
|
|
3287
|
+
const failed = await exclusive(async () => {
|
|
3288
|
+
const left = await acceptance(room, local, submitted, accepted);
|
|
3289
|
+
const arriving = replicas.get(partner)?.queue.find((batch) => batch.id === submitted.id);
|
|
3290
|
+
const arrived = other === undefined || arriving === undefined || accepted.destination === undefined
|
|
3291
|
+
? null
|
|
3292
|
+
: await acceptance(other, local, arriving, {
|
|
3293
|
+
kind: 'accepted',
|
|
3294
|
+
...accepted.destination,
|
|
3295
|
+
});
|
|
3296
|
+
await adoptedTogether(local, [left, arrived]);
|
|
3297
|
+
const refusedArrival = refusalOf(arrived);
|
|
3298
|
+
if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
|
|
3299
|
+
refusedSource(other, refusedArrival);
|
|
3300
|
+
}
|
|
3301
|
+
return refusalOf(left);
|
|
3302
|
+
});
|
|
3303
|
+
submitLater(partner);
|
|
1985
3304
|
if (failed === null)
|
|
1986
3305
|
return true;
|
|
1987
3306
|
if (failed.kind === 'refused' && continuing())
|
|
1988
3307
|
refusedSource(room, failed);
|
|
1989
3308
|
return false;
|
|
1990
3309
|
}
|
|
3310
|
+
function queueIn(locator) {
|
|
3311
|
+
return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
3312
|
+
}
|
|
3313
|
+
function behindIn(locator, batch) {
|
|
3314
|
+
const held = replicas.get(locator)?.queue;
|
|
3315
|
+
if (held === undefined)
|
|
3316
|
+
return queuesWithoutReplica.get(locator) !== undefined;
|
|
3317
|
+
return held.findIndex((one) => one.id === batch.id) > 0;
|
|
3318
|
+
}
|
|
3319
|
+
function unpaired(skipped) {
|
|
3320
|
+
for (const locator of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
|
|
3321
|
+
for (const batch of queueIn(locator)) {
|
|
3322
|
+
const partner = partnerOf(batch, locator);
|
|
3323
|
+
if (partner === null || skipped.has(pendingKey(locator, batch.id)))
|
|
3324
|
+
continue;
|
|
3325
|
+
if (queueIn(partner).some((held) => held.id === batch.id))
|
|
3326
|
+
continue;
|
|
3327
|
+
if (unpairedIn(locator, batch))
|
|
3328
|
+
return { locator, batch };
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
return null;
|
|
3332
|
+
}
|
|
3333
|
+
async function unpairing(local, locator, batches) {
|
|
3334
|
+
const ids = new Set(batches.map(({ id }) => id));
|
|
3335
|
+
const leaving = batches.map((batch) => ({
|
|
3336
|
+
batch,
|
|
3337
|
+
returned: !localSaves && leavingIn(batch, locator) ? returnedBy('withdrawn') : null,
|
|
3338
|
+
}));
|
|
3339
|
+
const outcome = localSaves ? 'dropped' : 'unpaired';
|
|
3340
|
+
const current = replicas.get(locator);
|
|
3341
|
+
if (current !== undefined) {
|
|
3342
|
+
const settled = await settlement(local, locator, current, current.record, current.queue.filter((one) => !ids.has(one.id)), { leaving });
|
|
3343
|
+
return {
|
|
3344
|
+
...settled,
|
|
3345
|
+
apply(answer) {
|
|
3346
|
+
for (const batch of batches)
|
|
3347
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
3348
|
+
const set = settled.apply(answer);
|
|
3349
|
+
submitLater(locator);
|
|
3350
|
+
return set;
|
|
3351
|
+
},
|
|
3352
|
+
};
|
|
3353
|
+
}
|
|
3354
|
+
const queue = queueIn(locator).filter((one) => !ids.has(one.id));
|
|
3355
|
+
const written = new Map();
|
|
3356
|
+
for (const { batch, returned } of leaving) {
|
|
3357
|
+
if (returned === null)
|
|
3358
|
+
continue;
|
|
3359
|
+
for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
|
|
3360
|
+
written.set(saved.path, {
|
|
3361
|
+
locator,
|
|
3362
|
+
path: saved.path,
|
|
3363
|
+
revision: roomDraftRevision(),
|
|
3364
|
+
parts: returnedInto((written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path))?.parts, saved.parts),
|
|
3365
|
+
returned,
|
|
3366
|
+
});
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
const carried = carriedBack(locator, leaving.flatMap(({ batch, returned }) => (returned === null ? [] : [batch])), written);
|
|
3370
|
+
return {
|
|
3371
|
+
locator,
|
|
3372
|
+
left: batches,
|
|
3373
|
+
commit: {
|
|
3374
|
+
rooms: [],
|
|
3375
|
+
writes: [
|
|
3376
|
+
{
|
|
3377
|
+
locator,
|
|
3378
|
+
queue,
|
|
3379
|
+
roomDrafts: [...written.values()],
|
|
3380
|
+
consumedRoomDrafts: carried.taken,
|
|
3381
|
+
},
|
|
3382
|
+
...carried.writes,
|
|
3383
|
+
],
|
|
3384
|
+
},
|
|
3385
|
+
apply() {
|
|
3386
|
+
for (const batch of batches) {
|
|
3387
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
3388
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
3389
|
+
}
|
|
3390
|
+
if (queue.length > 0)
|
|
3391
|
+
queuesWithoutReplica.set(locator, queue);
|
|
3392
|
+
else
|
|
3393
|
+
queuesWithoutReplica.delete(locator);
|
|
3394
|
+
for (const record of written.values())
|
|
3395
|
+
rememberRoomDraft(record);
|
|
3396
|
+
carried.remember();
|
|
3397
|
+
submitLater(locator);
|
|
3398
|
+
return mergeChangeSets([
|
|
3399
|
+
changeSet(locator, {
|
|
3400
|
+
queue: true,
|
|
3401
|
+
drafts: [...written.keys(), ...carried.taken],
|
|
3402
|
+
}),
|
|
3403
|
+
...carried.changes,
|
|
3404
|
+
]);
|
|
3405
|
+
},
|
|
3406
|
+
};
|
|
3407
|
+
}
|
|
3408
|
+
async function unpair(local) {
|
|
3409
|
+
const handled = new Set();
|
|
3410
|
+
for (let held = unpaired(handled); held !== null && continuing(); held = unpaired(handled)) {
|
|
3411
|
+
const { locator, batch } = held;
|
|
3412
|
+
handled.add(pendingKey(locator, batch.id));
|
|
3413
|
+
publishChangeSet(await together(local, [await unpairing(local, locator, [batch])]));
|
|
3414
|
+
}
|
|
3415
|
+
if (handled.size === 0)
|
|
3416
|
+
return;
|
|
3417
|
+
moveTo({ ...status, ...attention() });
|
|
3418
|
+
}
|
|
3419
|
+
async function discardHeld(head, room, local, signal) {
|
|
3420
|
+
const partner = partnerOf(head, room.locator);
|
|
3421
|
+
const other = partner === null ? undefined : discovered.get(partner);
|
|
3422
|
+
await discardPending(other === undefined ? [room] : [room, other], local, signal);
|
|
3423
|
+
}
|
|
1991
3424
|
async function submitRoom(room, local, authorized, progress, signal) {
|
|
1992
3425
|
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
1993
3426
|
const replica = replicas.get(room.locator);
|
|
1994
3427
|
const head = replica?.queue[0];
|
|
1995
3428
|
if (replica === undefined || head === undefined)
|
|
1996
3429
|
return 'done';
|
|
1997
|
-
|
|
3430
|
+
const state = pendings.get(pendingKey(room.locator, head.id));
|
|
3431
|
+
if (state?.accepted !== undefined)
|
|
1998
3432
|
return 'done';
|
|
1999
|
-
const
|
|
3433
|
+
const partner = partnerOf(head, room.locator);
|
|
3434
|
+
if (partner !== null && !leavingIn(head, room.locator)) {
|
|
3435
|
+
submitLater(partner);
|
|
3436
|
+
return 'done';
|
|
3437
|
+
}
|
|
3438
|
+
if (partner !== null && behindIn(partner, head)) {
|
|
3439
|
+
progress.complete = false;
|
|
3440
|
+
return 'done';
|
|
3441
|
+
}
|
|
2000
3442
|
const { submission, lost: earlier } = state ?? {
|
|
2001
3443
|
submission: { state: 'waiting' },
|
|
2002
3444
|
lost: false,
|
|
@@ -2012,16 +3454,16 @@ export function composeRuntime(options) {
|
|
|
2012
3454
|
}
|
|
2013
3455
|
const lost = earlier || submission.state === 'submitted';
|
|
2014
3456
|
if (lost && pastHorizon(head.id)) {
|
|
2015
|
-
await
|
|
3457
|
+
await discardHeld(head, room, local, signal);
|
|
2016
3458
|
return 'done';
|
|
2017
3459
|
}
|
|
2018
3460
|
let result;
|
|
2019
3461
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
2020
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3462
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
|
|
2021
3463
|
if (discoveryPaused || fences.has(room.locator))
|
|
2022
3464
|
return 'done';
|
|
2023
3465
|
if (signal.aborted || writesHeld()) {
|
|
2024
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3466
|
+
await exclusive(() => mark(local, room.locator, head, submission, earlier));
|
|
2025
3467
|
if (signal.aborted)
|
|
2026
3468
|
return 'done';
|
|
2027
3469
|
progress.complete = false;
|
|
@@ -2029,11 +3471,17 @@ export function composeRuntime(options) {
|
|
|
2029
3471
|
return 'done';
|
|
2030
3472
|
}
|
|
2031
3473
|
const opensBefore = openSignals;
|
|
2032
|
-
const
|
|
3474
|
+
const answeredHere = awaitingAnswer(room.locator);
|
|
3475
|
+
const answeredThere = partner === null ? () => { } : awaitingAnswer(partner);
|
|
3476
|
+
const answered = () => {
|
|
3477
|
+
answeredHere();
|
|
3478
|
+
answeredThere();
|
|
3479
|
+
};
|
|
2033
3480
|
try {
|
|
2034
3481
|
result = await options.adapter.mutate(authorized, {
|
|
2035
3482
|
locator: room.locator,
|
|
2036
3483
|
changeBatch: head,
|
|
3484
|
+
file: batchFile(head),
|
|
2037
3485
|
...(state?.upload === undefined ? {} : { upload: state.upload }),
|
|
2038
3486
|
}, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
|
|
2039
3487
|
}
|
|
@@ -2058,9 +3506,10 @@ export function composeRuntime(options) {
|
|
|
2058
3506
|
return 'again';
|
|
2059
3507
|
}
|
|
2060
3508
|
if (result.kind === 'room-not-readable') {
|
|
3509
|
+
const unread = discovered.get(result.locator) ?? room;
|
|
2061
3510
|
progress.complete = false;
|
|
2062
|
-
unreadable.add(
|
|
2063
|
-
list(
|
|
3511
|
+
unreadable.add(unread.locator);
|
|
3512
|
+
list(unread, false);
|
|
2064
3513
|
emitRooms();
|
|
2065
3514
|
return 'done';
|
|
2066
3515
|
}
|
|
@@ -2075,24 +3524,30 @@ export function composeRuntime(options) {
|
|
|
2075
3524
|
}
|
|
2076
3525
|
if (result.kind === 'write-halted') {
|
|
2077
3526
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
2078
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3527
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
|
|
2079
3528
|
if (serviceMode === 'open' && openSignals !== opensBefore)
|
|
2080
3529
|
continue;
|
|
2081
3530
|
haltedRefusal = true;
|
|
2082
3531
|
progress.complete = false;
|
|
2083
3532
|
return 'done';
|
|
2084
3533
|
}
|
|
3534
|
+
const other = partner === null ? undefined : discovered.get(partner);
|
|
2085
3535
|
if (result.kind === 'conflicted' ||
|
|
2086
3536
|
result.kind === 'rejected' ||
|
|
2087
3537
|
result.kind === 'expired' ||
|
|
2088
3538
|
result.kind === 'retryable-refusal') {
|
|
2089
3539
|
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
2090
3540
|
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
2091
|
-
await
|
|
3541
|
+
await discardHeld(head, room, local, signal);
|
|
2092
3542
|
return 'done';
|
|
2093
3543
|
}
|
|
2094
3544
|
if (result.kind === 'expired') {
|
|
2095
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3545
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
|
|
3546
|
+
const there = other === undefined ? null : await advance(other, local, authorized, continuing);
|
|
3547
|
+
if (other !== undefined && there !== null) {
|
|
3548
|
+
await withhold(there, other, local, progress);
|
|
3549
|
+
return 'done';
|
|
3550
|
+
}
|
|
2096
3551
|
const withheld = await advance(room, local, authorized, continuing);
|
|
2097
3552
|
if (withheld !== null) {
|
|
2098
3553
|
await withhold(withheld, room, local, progress);
|
|
@@ -2109,11 +3564,25 @@ export function composeRuntime(options) {
|
|
|
2109
3564
|
if (result.kind === 'retryable-refusal') {
|
|
2110
3565
|
const reset = result.reset;
|
|
2111
3566
|
progress.complete = false;
|
|
2112
|
-
await exclusive(() => mark(local, room.locator, head
|
|
3567
|
+
await exclusive(() => mark(local, room.locator, head, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
2113
3568
|
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
2114
3569
|
}
|
|
2115
3570
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
2116
3571
|
const left = leaving(head, result);
|
|
3572
|
+
const refreshed = async () => ({
|
|
3573
|
+
there: other === undefined ? null : await advance(other, local, authorized, continuing),
|
|
3574
|
+
here: await advance(room, local, authorized, continuing),
|
|
3575
|
+
});
|
|
3576
|
+
const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
|
|
3577
|
+
if (judged !== null && (judged.there !== null || judged.here !== null)) {
|
|
3578
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
|
|
3579
|
+
if (other !== undefined && judged.there !== null) {
|
|
3580
|
+
await withhold(judged.there, other, local, progress);
|
|
3581
|
+
}
|
|
3582
|
+
if (judged.here !== null)
|
|
3583
|
+
await withhold(judged.here, room, local, progress);
|
|
3584
|
+
return 'done';
|
|
3585
|
+
}
|
|
2117
3586
|
await exclusive(async () => {
|
|
2118
3587
|
if (!continuing())
|
|
2119
3588
|
return;
|
|
@@ -2121,25 +3590,29 @@ export function composeRuntime(options) {
|
|
|
2121
3590
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
2122
3591
|
return;
|
|
2123
3592
|
}
|
|
2124
|
-
const
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
});
|
|
2130
|
-
|
|
3593
|
+
const confirmedLayer = await sliceAt(current.record, 'confirmed', [head]);
|
|
3594
|
+
const held = (result.kind === 'expired' &&
|
|
3595
|
+
(await landedWhole(room.locator, head, confirmedLayer))) ||
|
|
3596
|
+
(result.kind === 'conflicted' &&
|
|
3597
|
+
(await landedWhole(room.locator, head, confirmedLayer, result.affected)));
|
|
3598
|
+
publishChangeSet(await settle(local, room.locator, current, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
|
|
3599
|
+
await unpair(local);
|
|
2131
3600
|
moveTo({ ...status, ...attention() });
|
|
2132
3601
|
});
|
|
2133
3602
|
if (result.kind === 'expired')
|
|
2134
3603
|
continue;
|
|
2135
|
-
const
|
|
2136
|
-
if (
|
|
2137
|
-
await withhold(
|
|
3604
|
+
const { there, here } = judged ?? (await refreshed());
|
|
3605
|
+
if (other !== undefined && there !== null)
|
|
3606
|
+
await withhold(there, other, local, progress);
|
|
3607
|
+
if (here !== null) {
|
|
3608
|
+
await withhold(here, room, local, progress);
|
|
2138
3609
|
return 'done';
|
|
2139
3610
|
}
|
|
2140
3611
|
continue;
|
|
2141
3612
|
}
|
|
2142
|
-
const kept = await
|
|
3613
|
+
const kept = await (partner === null
|
|
3614
|
+
? adopted(room, local, head, result)
|
|
3615
|
+
: adoptedMove(room, partner, local, head, result)).finally(answered);
|
|
2143
3616
|
if (!kept) {
|
|
2144
3617
|
progress.complete = false;
|
|
2145
3618
|
return 'done';
|
|
@@ -2150,44 +3623,67 @@ export function composeRuntime(options) {
|
|
|
2150
3623
|
function pastHorizon(identity) {
|
|
2151
3624
|
return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
|
|
2152
3625
|
}
|
|
2153
|
-
async function discardPending(
|
|
2154
|
-
const { locator } = room;
|
|
3626
|
+
async function discardPending(rooms, local, signal) {
|
|
2155
3627
|
const discarded = await exclusive(async () => {
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
3628
|
+
const held = rooms
|
|
3629
|
+
.filter(({ locator }) => !fences.has(locator))
|
|
3630
|
+
.map((room) => ({
|
|
3631
|
+
room,
|
|
3632
|
+
before: replicas.get(room.locator),
|
|
3633
|
+
queue: queueIn(room.locator),
|
|
3634
|
+
}));
|
|
3635
|
+
if (held.length === 0)
|
|
3636
|
+
return [];
|
|
3637
|
+
const answer = await local.commit({
|
|
3638
|
+
rooms: held.flatMap(({ room, before }) => before === undefined
|
|
3639
|
+
? []
|
|
3640
|
+
: [
|
|
3641
|
+
{
|
|
3642
|
+
locator: room.locator,
|
|
3643
|
+
key: before.record.key,
|
|
3644
|
+
roots: before.record.typedSelection?.roots ?? [],
|
|
3645
|
+
overlay: { set: [], cleared: [...before.overlaid] },
|
|
3646
|
+
},
|
|
3647
|
+
]),
|
|
3648
|
+
discards: held.map(({ room }) => room.locator),
|
|
3649
|
+
});
|
|
3650
|
+
for (const { room, before, queue } of held) {
|
|
3651
|
+
const { locator } = room;
|
|
3652
|
+
lastFenced.set(locator, ++fencings);
|
|
3653
|
+
for (const batch of queue)
|
|
3654
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
3655
|
+
queuesWithoutReplica.delete(locator);
|
|
3656
|
+
if (before !== undefined) {
|
|
3657
|
+
replicas.set(locator, {
|
|
3658
|
+
...before,
|
|
3659
|
+
queue: [],
|
|
3660
|
+
overlaid: new Set(),
|
|
3661
|
+
count: answer.counts[locator] ?? before.count,
|
|
3662
|
+
});
|
|
3663
|
+
}
|
|
3664
|
+
journal.record(`room discarded ${locator}`);
|
|
2167
3665
|
}
|
|
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 }));
|
|
3666
|
+
publishChangeSet(mergeChangeSets(held.map(({ room }) => changeSet(room.locator, {
|
|
3667
|
+
documents: answer.changed[room.locator] ?? [],
|
|
3668
|
+
queue: true,
|
|
3669
|
+
}))));
|
|
2176
3670
|
moveTo({ ...status, ...attention() });
|
|
2177
|
-
return
|
|
2178
|
-
});
|
|
2179
|
-
if (!discarded)
|
|
2180
|
-
return;
|
|
2181
|
-
const progress = { complete: true, reachable: true };
|
|
2182
|
-
await roomTask(locator, async (live) => {
|
|
2183
|
-
await Promise.race([
|
|
2184
|
-
reload(room, local, progress, () => live() && !signal.aborted),
|
|
2185
|
-
abandonedOn(signal),
|
|
2186
|
-
]);
|
|
3671
|
+
return held.map(({ room }) => room);
|
|
2187
3672
|
});
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
3673
|
+
for (const room of discarded) {
|
|
3674
|
+
if (signal.aborted)
|
|
3675
|
+
return;
|
|
3676
|
+
const progress = { complete: true, reachable: true };
|
|
3677
|
+
await roomTask(room.locator, async (live) => {
|
|
3678
|
+
await Promise.race([
|
|
3679
|
+
reload(room, local, progress, () => live() && !signal.aborted),
|
|
3680
|
+
abandonedOn(signal),
|
|
3681
|
+
]);
|
|
3682
|
+
});
|
|
3683
|
+
if (signal.aborted)
|
|
3684
|
+
return;
|
|
3685
|
+
await rest(progress);
|
|
3686
|
+
}
|
|
2191
3687
|
}
|
|
2192
3688
|
function awaitingAnswer(locator) {
|
|
2193
3689
|
let release = () => { };
|
|
@@ -2204,7 +3700,7 @@ export function composeRuntime(options) {
|
|
|
2204
3700
|
async function retainModel(local, room, attached, next) {
|
|
2205
3701
|
const key = modelKey(room.locator, attached);
|
|
2206
3702
|
const previous = modelOf(room.locator, attached);
|
|
2207
|
-
const heldBefore = replicas.get(room.locator)?.
|
|
3703
|
+
const heldBefore = replicas.get(room.locator)?.record;
|
|
2208
3704
|
if (JSON.stringify(previous) === JSON.stringify(next) &&
|
|
2209
3705
|
(heldBefore === undefined ||
|
|
2210
3706
|
(JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
|
|
@@ -2220,36 +3716,57 @@ export function composeRuntime(options) {
|
|
|
2220
3716
|
if (!continuing())
|
|
2221
3717
|
return;
|
|
2222
3718
|
const held = replicas.get(room.locator);
|
|
3719
|
+
let changed = null;
|
|
2223
3720
|
if (held !== undefined) {
|
|
2224
3721
|
const declared = {
|
|
2225
|
-
...held.
|
|
3722
|
+
...held.record,
|
|
2226
3723
|
operations: room.operations,
|
|
2227
3724
|
replica: room.replica,
|
|
2228
3725
|
typedSelection: room.typedSelection ?? null,
|
|
2229
3726
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
2230
3727
|
};
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
3728
|
+
let read = 'staged';
|
|
3729
|
+
if (!sameMembership(declared, held.record)) {
|
|
3730
|
+
const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), compositionOf(declared));
|
|
3731
|
+
read =
|
|
3732
|
+
verdict.verdict !== 'refuse'
|
|
3733
|
+
? 'staged'
|
|
3734
|
+
: verdict.rule === 'model-unavailable'
|
|
3735
|
+
? 'deferred'
|
|
3736
|
+
: { kind: 'refused', refusal: verdict };
|
|
3737
|
+
}
|
|
3738
|
+
const record = read === 'staged'
|
|
2235
3739
|
? declared
|
|
2236
|
-
: {
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
const
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
3740
|
+
: { ...declared, typedSelection: held.record.typedSelection };
|
|
3741
|
+
const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
|
|
3742
|
+
const replayedLayer = await replayed(record, confirmedLayer, held.queue);
|
|
3743
|
+
const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
|
|
3744
|
+
const answer = await local.commit({
|
|
3745
|
+
rooms: [
|
|
3746
|
+
{
|
|
3747
|
+
locator: room.locator,
|
|
3748
|
+
key: record.key,
|
|
3749
|
+
record: storedRecord(record),
|
|
3750
|
+
roots: record.typedSelection?.roots ?? [],
|
|
3751
|
+
composition: compositionOf(record),
|
|
3752
|
+
overlay: diff.overlay,
|
|
3753
|
+
},
|
|
3754
|
+
],
|
|
3755
|
+
});
|
|
3756
|
+
recomposed(room.locator, held.record, record);
|
|
3757
|
+
const { count } = committedRoom(room.locator, record, answer);
|
|
3758
|
+
replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count });
|
|
3759
|
+
changed = changeSet(room.locator, {
|
|
3760
|
+
documents: answer.changed[room.locator] ?? [],
|
|
3761
|
+
room: true,
|
|
3762
|
+
});
|
|
3763
|
+
if (read === 'staged' || read === 'deferred') {
|
|
2246
3764
|
if (declarationRefusals.delete(room.locator)) {
|
|
2247
3765
|
sourceRefusals.delete(room.locator);
|
|
2248
3766
|
unreadable.delete(room.locator);
|
|
2249
3767
|
}
|
|
2250
3768
|
}
|
|
2251
|
-
else
|
|
2252
|
-
replicas.set(room.locator, { ...held, confirmed: { ...held.confirmed, record } });
|
|
3769
|
+
else {
|
|
2253
3770
|
refusedSource(room, read);
|
|
2254
3771
|
declarationRefusals.add(room.locator);
|
|
2255
3772
|
}
|
|
@@ -2260,13 +3777,18 @@ export function composeRuntime(options) {
|
|
|
2260
3777
|
invalidModels.delete(key);
|
|
2261
3778
|
}
|
|
2262
3779
|
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
2263
|
-
if (
|
|
3780
|
+
if (changed === null)
|
|
2264
3781
|
emitRooms();
|
|
2265
3782
|
else
|
|
2266
|
-
publishChangeSet(
|
|
3783
|
+
publishChangeSet(changed);
|
|
2267
3784
|
});
|
|
2268
3785
|
}
|
|
2269
3786
|
async function scoped(result, room, local) {
|
|
3787
|
+
if (result.scope === 'content' && !result.retryable) {
|
|
3788
|
+
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
3789
|
+
await loseRooms([room.locator]);
|
|
3790
|
+
return;
|
|
3791
|
+
}
|
|
2270
3792
|
scopedRefusals.set(room.locator, {
|
|
2271
3793
|
code: result.code,
|
|
2272
3794
|
fields: result.fields,
|
|
@@ -2280,15 +3802,7 @@ export function composeRuntime(options) {
|
|
|
2280
3802
|
});
|
|
2281
3803
|
else if (result.scope !== 'operation') {
|
|
2282
3804
|
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
|
-
};
|
|
3805
|
+
scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
|
|
2292
3806
|
await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
|
|
2293
3807
|
}
|
|
2294
3808
|
if (!continuing())
|
|
@@ -2299,30 +3813,22 @@ export function composeRuntime(options) {
|
|
|
2299
3813
|
}
|
|
2300
3814
|
function heldRooms(listed) {
|
|
2301
3815
|
return listed.flatMap((room) => {
|
|
2302
|
-
const record = replicas.get(room.locator)?.
|
|
3816
|
+
const record = replicas.get(room.locator)?.record;
|
|
2303
3817
|
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
2304
3818
|
return [];
|
|
2305
|
-
return [
|
|
2306
|
-
{
|
|
2307
|
-
locator: room.locator,
|
|
2308
|
-
roomVersion: record.roomVersion,
|
|
2309
|
-
documents: record.documents,
|
|
2310
|
-
revisions: record.revisions,
|
|
2311
|
-
},
|
|
2312
|
-
];
|
|
3819
|
+
return [{ locator: room.locator, roomVersion: record.roomVersion }];
|
|
2313
3820
|
});
|
|
2314
3821
|
}
|
|
2315
3822
|
async function synchronize(listed, local, progress, attempt, live) {
|
|
2316
|
-
const
|
|
2317
|
-
const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
|
|
3823
|
+
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
|
|
2318
3824
|
if (!live())
|
|
2319
3825
|
return;
|
|
2320
|
-
const ready =
|
|
3826
|
+
const ready = listed.filter((_, index) => {
|
|
2321
3827
|
const outcome = prepared[index];
|
|
2322
3828
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
2323
3829
|
});
|
|
2324
3830
|
if (attempt === 'opened') {
|
|
2325
|
-
options.adapter.roomStream?.hold(heldRooms(ready)
|
|
3831
|
+
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
2326
3832
|
handed = true;
|
|
2327
3833
|
}
|
|
2328
3834
|
discovering?.release();
|
|
@@ -2338,7 +3844,7 @@ export function composeRuntime(options) {
|
|
|
2338
3844
|
progress.complete = false;
|
|
2339
3845
|
return false;
|
|
2340
3846
|
}
|
|
2341
|
-
const prior = replicas.get(room.locator)?.
|
|
3847
|
+
const prior = replicas.get(room.locator)?.record;
|
|
2342
3848
|
if (prior !== undefined && prior.replica.selection !== room.replica.selection) {
|
|
2343
3849
|
list(prior, false);
|
|
2344
3850
|
emitRooms();
|
|
@@ -2350,19 +3856,10 @@ export function composeRuntime(options) {
|
|
|
2350
3856
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
2351
3857
|
if (discoveryPaused || authorization === undefined)
|
|
2352
3858
|
return false;
|
|
2353
|
-
|
|
2354
|
-
if (replicas.has(room.locator))
|
|
2355
|
-
progress.complete = false;
|
|
2356
|
-
return false;
|
|
2357
|
-
}
|
|
2358
|
-
return true;
|
|
3859
|
+
return room.replica.admission === 'admitted';
|
|
2359
3860
|
}
|
|
2360
3861
|
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)
|
|
3862
|
+
if (authorization === undefined || !live() || !behind(room))
|
|
2366
3863
|
return;
|
|
2367
3864
|
const withheld = await advance(room, local, authorization, live);
|
|
2368
3865
|
if (withheld !== null)
|
|
@@ -2390,6 +3887,8 @@ export function composeRuntime(options) {
|
|
|
2390
3887
|
}
|
|
2391
3888
|
async function reload(room, local, progress, live) {
|
|
2392
3889
|
try {
|
|
3890
|
+
if (live())
|
|
3891
|
+
reloadsDue.delete(room.locator);
|
|
2393
3892
|
if (!live() || authorization === undefined)
|
|
2394
3893
|
return;
|
|
2395
3894
|
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
@@ -2399,14 +3898,14 @@ export function composeRuntime(options) {
|
|
|
2399
3898
|
await withhold(bootstrapped, room, local, progress);
|
|
2400
3899
|
return;
|
|
2401
3900
|
}
|
|
2402
|
-
await publish(local, room, bootstrapped.
|
|
3901
|
+
await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
|
|
2403
3902
|
await caughtUp(room, local, progress, live);
|
|
2404
3903
|
}
|
|
2405
3904
|
finally {
|
|
2406
3905
|
caughtUpTo(room.locator);
|
|
2407
3906
|
}
|
|
2408
3907
|
}
|
|
2409
|
-
async function discoverRooms(local, progress, attempt, live) {
|
|
3908
|
+
async function discoverRooms(local, progress, attempt, live, scope) {
|
|
2410
3909
|
if (closed || authorization === undefined) {
|
|
2411
3910
|
notifyReady();
|
|
2412
3911
|
return;
|
|
@@ -2416,7 +3915,7 @@ export function composeRuntime(options) {
|
|
|
2416
3915
|
const started = discoveries;
|
|
2417
3916
|
let result;
|
|
2418
3917
|
try {
|
|
2419
|
-
result = await options.adapter.discover(authorization);
|
|
3918
|
+
result = await options.adapter.discover(authorization, scope);
|
|
2420
3919
|
}
|
|
2421
3920
|
catch {
|
|
2422
3921
|
result = { kind: 'transport-uncertainty' };
|
|
@@ -2456,12 +3955,13 @@ export function composeRuntime(options) {
|
|
|
2456
3955
|
}
|
|
2457
3956
|
for (const room of result.rooms) {
|
|
2458
3957
|
if (room.operations === undefined ||
|
|
2459
|
-
!['
|
|
3958
|
+
!['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
2460
3959
|
room.operations[operation].every((scope) => typeof scope === 'string')) ||
|
|
2461
3960
|
room.replica === undefined ||
|
|
2462
3961
|
typeof room.replica.selection !== 'string' ||
|
|
2463
3962
|
room.replica.selection.length === 0 ||
|
|
2464
|
-
!['admitted', 'unavailable'].includes(room.replica.admission)
|
|
3963
|
+
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
3964
|
+
!['locked', 'open'].includes(room.replica.offline)) {
|
|
2465
3965
|
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
2466
3966
|
await refuse('missing-required-guarantee');
|
|
2467
3967
|
notifyReady();
|
|
@@ -2485,15 +3985,16 @@ export function composeRuntime(options) {
|
|
|
2485
3985
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
2486
3986
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
2487
3987
|
limits = result.providerLimits;
|
|
2488
|
-
await local.
|
|
3988
|
+
await local.fact('providerLimits', limits);
|
|
3989
|
+
await local.fact('capabilities', capabilities);
|
|
2489
3990
|
}
|
|
2490
3991
|
const signedIn = result.member ?? null;
|
|
2491
3992
|
if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
|
|
2492
|
-
await local.
|
|
3993
|
+
await local.fact('member', signedIn);
|
|
2493
3994
|
member = signedIn;
|
|
2494
3995
|
emitMember();
|
|
2495
3996
|
}
|
|
2496
|
-
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
3997
|
+
await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
|
|
2497
3998
|
}
|
|
2498
3999
|
async function ensureAuthorization() {
|
|
2499
4000
|
if (authorization !== undefined)
|
|
@@ -2590,9 +4091,44 @@ export function composeRuntime(options) {
|
|
|
2590
4091
|
options.adapter.roomStream?.reopen();
|
|
2591
4092
|
});
|
|
2592
4093
|
}
|
|
2593
|
-
function
|
|
4094
|
+
function rediscover(scope) {
|
|
4095
|
+
return task(OPENING_LANE, async (live) => {
|
|
4096
|
+
if (!live() || !socketOpen)
|
|
4097
|
+
return;
|
|
4098
|
+
const progress = { complete: true, reachable: true };
|
|
4099
|
+
const gate = discover();
|
|
4100
|
+
moveTo(transferring(status));
|
|
4101
|
+
try {
|
|
4102
|
+
const local = await openLocal();
|
|
4103
|
+
if (!live())
|
|
4104
|
+
return;
|
|
4105
|
+
await ensureAuthorization();
|
|
4106
|
+
if (!live())
|
|
4107
|
+
return;
|
|
4108
|
+
await discoverRooms(local, progress, 'authorized', live, scope);
|
|
4109
|
+
}
|
|
4110
|
+
catch {
|
|
4111
|
+
progress.complete = false;
|
|
4112
|
+
}
|
|
4113
|
+
finally {
|
|
4114
|
+
gate.release();
|
|
4115
|
+
notifyReady();
|
|
4116
|
+
}
|
|
4117
|
+
if (!live())
|
|
4118
|
+
return;
|
|
4119
|
+
if (!progress.complete)
|
|
4120
|
+
complete = false;
|
|
4121
|
+
await rest(progress, live);
|
|
4122
|
+
});
|
|
4123
|
+
}
|
|
4124
|
+
function closedSocket(revoked, lost) {
|
|
2594
4125
|
const opened = socketOpen;
|
|
2595
4126
|
socketOpen = false;
|
|
4127
|
+
connectionLost();
|
|
4128
|
+
if (lost) {
|
|
4129
|
+
reads.lost();
|
|
4130
|
+
conversations.lost();
|
|
4131
|
+
}
|
|
2596
4132
|
if (revoked) {
|
|
2597
4133
|
void refuse('revoked-authorization');
|
|
2598
4134
|
return;
|
|
@@ -2609,9 +4145,10 @@ export function composeRuntime(options) {
|
|
|
2609
4145
|
function applyOpening(expired) {
|
|
2610
4146
|
const progress = underway?.generation === generation ? underway.progress : null;
|
|
2611
4147
|
const cycle = progress ?? { complete: true, reachable: true };
|
|
2612
|
-
const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
|
|
2613
4148
|
if (opened === generation)
|
|
2614
4149
|
opened = null;
|
|
4150
|
+
const reloading = progress === null ? [] : owedReloads(expired);
|
|
4151
|
+
const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
|
|
2615
4152
|
return task(OPENING_LANE, async (live) => {
|
|
2616
4153
|
underway = null;
|
|
2617
4154
|
if (!live())
|
|
@@ -2621,24 +4158,17 @@ export function composeRuntime(options) {
|
|
|
2621
4158
|
await rest(cycle, live);
|
|
2622
4159
|
return;
|
|
2623
4160
|
}
|
|
2624
|
-
await roomsCarried(expired);
|
|
4161
|
+
await roomsCarried([...expired, ...reloading]);
|
|
2625
4162
|
if (!live())
|
|
2626
4163
|
return;
|
|
2627
|
-
const replaced = new Set(owed);
|
|
2628
|
-
owed.clear();
|
|
2629
4164
|
try {
|
|
2630
4165
|
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)];
|
|
4166
|
+
const reloaded = await replacing;
|
|
2639
4167
|
if (progress.reachable) {
|
|
2640
|
-
for (const room of
|
|
2641
|
-
if (
|
|
4168
|
+
for (const room of discovered.values()) {
|
|
4169
|
+
if (replicas.has(room.locator) &&
|
|
4170
|
+
!expired.includes(room.locator) &&
|
|
4171
|
+
!reloading.includes(room.locator)) {
|
|
2642
4172
|
await confirmModels(room, local);
|
|
2643
4173
|
}
|
|
2644
4174
|
}
|
|
@@ -2655,14 +4185,29 @@ export function composeRuntime(options) {
|
|
|
2655
4185
|
await finishOpening(progress, live);
|
|
2656
4186
|
});
|
|
2657
4187
|
}
|
|
4188
|
+
function owedReloads(expired) {
|
|
4189
|
+
const reloading = [...discovered.values()]
|
|
4190
|
+
.filter((room) => replicas.has(room.locator) &&
|
|
4191
|
+
!expired.includes(room.locator) &&
|
|
4192
|
+
(owed.has(room.locator) || revoked(room.locator)))
|
|
4193
|
+
.map((room) => room.locator);
|
|
4194
|
+
owed.clear();
|
|
4195
|
+
for (const locator of reloading)
|
|
4196
|
+
catching.add(locator);
|
|
4197
|
+
liveQueries.recover();
|
|
4198
|
+
return reloading;
|
|
4199
|
+
}
|
|
2658
4200
|
async function reloadNow(replaced, progress) {
|
|
4201
|
+
const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
|
|
4202
|
+
for (const room of reloaded)
|
|
4203
|
+
reloadsDue.set(room.locator, generation);
|
|
2659
4204
|
try {
|
|
2660
4205
|
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))));
|
|
4206
|
+
return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
2664
4207
|
}
|
|
2665
4208
|
catch {
|
|
4209
|
+
for (const room of reloaded)
|
|
4210
|
+
reloadsDue.delete(room.locator);
|
|
2666
4211
|
progress.reachable = false;
|
|
2667
4212
|
return [];
|
|
2668
4213
|
}
|
|
@@ -2779,6 +4324,7 @@ export function composeRuntime(options) {
|
|
|
2779
4324
|
progress.complete = false;
|
|
2780
4325
|
return;
|
|
2781
4326
|
}
|
|
4327
|
+
reads.applied();
|
|
2782
4328
|
submitAll();
|
|
2783
4329
|
for (const locator of stale)
|
|
2784
4330
|
if (repairable.has(locator))
|
|
@@ -2787,10 +4333,11 @@ export function composeRuntime(options) {
|
|
|
2787
4333
|
if (status.providerUnreachable)
|
|
2788
4334
|
conversations.reconnected();
|
|
2789
4335
|
const reading = { complete: true, reachable: true };
|
|
2790
|
-
|
|
2791
|
-
await conversations.catchUp(reading);
|
|
4336
|
+
await conversations.catchUp(reading);
|
|
2792
4337
|
if (progress.reachable && reading.reachable)
|
|
2793
4338
|
await conversations.resend(reading);
|
|
4339
|
+
if (progress.reachable)
|
|
4340
|
+
void recovery.attempt('calls', resendCalls);
|
|
2794
4341
|
if (!reading.reachable) {
|
|
2795
4342
|
progress.reachable = false;
|
|
2796
4343
|
resendLater();
|
|
@@ -2814,7 +4361,7 @@ export function composeRuntime(options) {
|
|
|
2814
4361
|
if (synced && !recorded) {
|
|
2815
4362
|
const completedAt = new Date().toISOString();
|
|
2816
4363
|
try {
|
|
2817
|
-
await (await openLocal()).
|
|
4364
|
+
await (await openLocal()).fact('lastCompleted', completedAt);
|
|
2818
4365
|
if (!live())
|
|
2819
4366
|
return;
|
|
2820
4367
|
recorded = true;
|
|
@@ -2851,11 +4398,11 @@ export function composeRuntime(options) {
|
|
|
2851
4398
|
return;
|
|
2852
4399
|
const room = discovered.get(locator);
|
|
2853
4400
|
const held = replicas.get(locator);
|
|
2854
|
-
if (room === undefined || room.replica.admission !== 'admitted'
|
|
4401
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2855
4402
|
return;
|
|
2856
4403
|
if (held !== undefined &&
|
|
2857
|
-
(held.
|
|
2858
|
-
options.adapter.reached(locator, held.
|
|
4404
|
+
(held.record.replica.selection !== room.replica.selection ||
|
|
4405
|
+
options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
|
|
2859
4406
|
return;
|
|
2860
4407
|
}
|
|
2861
4408
|
const during = underway?.generation === generation ? underway.progress : null;
|
|
@@ -2902,18 +4449,29 @@ export function composeRuntime(options) {
|
|
|
2902
4449
|
return;
|
|
2903
4450
|
if (event.kind === 'opened') {
|
|
2904
4451
|
socketOpen = true;
|
|
4452
|
+
connectionLive();
|
|
2905
4453
|
generation += 1;
|
|
4454
|
+
reads.opened();
|
|
2906
4455
|
conversations.reopened();
|
|
2907
4456
|
recovery.resume();
|
|
2908
4457
|
void sync('opened');
|
|
2909
4458
|
}
|
|
2910
4459
|
else if (event.kind === 'closed')
|
|
2911
|
-
closedSocket(event.revoked);
|
|
4460
|
+
closedSocket(event.revoked, event.lost);
|
|
2912
4461
|
else if (event.kind === 'opening')
|
|
2913
4462
|
void applyOpening(event.expired);
|
|
4463
|
+
else if (event.kind === 'authority-changed')
|
|
4464
|
+
void rediscover(event.scope);
|
|
2914
4465
|
else
|
|
2915
4466
|
void applyRoom(event.locator, event.roomVersion);
|
|
2916
|
-
}, receipts);
|
|
4467
|
+
}, receipts, heldRevisions);
|
|
4468
|
+
}
|
|
4469
|
+
async function heldRevisions(locator, paths) {
|
|
4470
|
+
const held = replicas.get(locator);
|
|
4471
|
+
if (held === undefined || paths.length === 0)
|
|
4472
|
+
return new Map();
|
|
4473
|
+
const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
|
|
4474
|
+
return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
|
|
2917
4475
|
}
|
|
2918
4476
|
function writesHeld() {
|
|
2919
4477
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -2965,7 +4523,7 @@ export function composeRuntime(options) {
|
|
|
2965
4523
|
if (!live())
|
|
2966
4524
|
return;
|
|
2967
4525
|
await conversations.reconcile();
|
|
2968
|
-
if (conversations.current()
|
|
4526
|
+
if (conversations.current()) {
|
|
2969
4527
|
show();
|
|
2970
4528
|
return;
|
|
2971
4529
|
}
|
|
@@ -2983,10 +4541,10 @@ export function composeRuntime(options) {
|
|
|
2983
4541
|
if (closed)
|
|
2984
4542
|
return;
|
|
2985
4543
|
closed = true;
|
|
2986
|
-
if (narrowing !== null)
|
|
2987
|
-
clearTimeout(narrowing);
|
|
2988
4544
|
if (rechecking !== null)
|
|
2989
4545
|
clearInterval(rechecking);
|
|
4546
|
+
connectionLive();
|
|
4547
|
+
lockListeners.clear();
|
|
2990
4548
|
readinessWatches.clear();
|
|
2991
4549
|
withdrawStream?.();
|
|
2992
4550
|
withdrawStream = undefined;
|
|
@@ -3005,7 +4563,7 @@ export function composeRuntime(options) {
|
|
|
3005
4563
|
statusListeners.clear();
|
|
3006
4564
|
serviceModeListeners.clear();
|
|
3007
4565
|
memberListeners.clear();
|
|
3008
|
-
|
|
4566
|
+
liveQueries.close();
|
|
3009
4567
|
queueSubscriptions.clear();
|
|
3010
4568
|
roomDraftListeners.clear();
|
|
3011
4569
|
presentationListeners.clear();
|
|
@@ -3032,22 +4590,63 @@ export function composeRuntime(options) {
|
|
|
3032
4590
|
if (options.adapter.roomStream === undefined)
|
|
3033
4591
|
void sync('unstreamed');
|
|
3034
4592
|
function planned(input, reach) {
|
|
3035
|
-
|
|
4593
|
+
const plan = planOf(input, roomFacts(reach), planReach(reach));
|
|
4594
|
+
const named = plan.paths !== null &&
|
|
4595
|
+
plan.rooms !== null &&
|
|
4596
|
+
plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
|
|
4597
|
+
return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
|
|
3036
4598
|
}
|
|
3037
4599
|
function watchPlan(plan, deliver) {
|
|
3038
|
-
|
|
4600
|
+
let seeded = false;
|
|
4601
|
+
let pending = true;
|
|
4602
|
+
const shown = () => {
|
|
4603
|
+
const seed = seeds.get(plan.key);
|
|
4604
|
+
if (seed === undefined || seeded || !pending)
|
|
4605
|
+
return;
|
|
4606
|
+
seeded = true;
|
|
4607
|
+
deliver(seededResult(seed));
|
|
4608
|
+
};
|
|
4609
|
+
const watchers = seedWatchers.get(plan.key) ?? new Set();
|
|
4610
|
+
seedWatchers.set(plan.key, watchers);
|
|
4611
|
+
watchers.add(shown);
|
|
4612
|
+
const stop = liveQueries.watch(plan, identityOf(plan), () => {
|
|
3039
4613
|
const cover = covered(plan);
|
|
3040
4614
|
awaitPreparation(cover);
|
|
3041
|
-
return cover;
|
|
3042
|
-
},
|
|
4615
|
+
return seedBehind(plan, cover);
|
|
4616
|
+
}, (message) => {
|
|
4617
|
+
const answered = message;
|
|
4618
|
+
pending = whole(answered) && 'pending' in answered.result;
|
|
4619
|
+
if (pending && seeds.has(plan.key)) {
|
|
4620
|
+
shown();
|
|
4621
|
+
return;
|
|
4622
|
+
}
|
|
4623
|
+
seeded = false;
|
|
4624
|
+
deliver(message);
|
|
4625
|
+
});
|
|
4626
|
+
return () => {
|
|
4627
|
+
watchers.delete(shown);
|
|
4628
|
+
if (watchers.size === 0)
|
|
4629
|
+
seedWatchers.delete(plan.key);
|
|
4630
|
+
stop();
|
|
4631
|
+
};
|
|
3043
4632
|
}
|
|
3044
|
-
async function searched(input, reach) {
|
|
3045
|
-
const
|
|
3046
|
-
const
|
|
4633
|
+
async function searched(input, reach, locked) {
|
|
4634
|
+
const asked = searchPlanOf(input);
|
|
4635
|
+
const plan = { ...asked, limit: capped(asked.limit) };
|
|
4636
|
+
const filter = {
|
|
4637
|
+
...filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)),
|
|
4638
|
+
locked,
|
|
4639
|
+
};
|
|
4640
|
+
const unlocking = (filter.rooms ?? [])
|
|
4641
|
+
.filter((room) => locked.includes(room))
|
|
4642
|
+
.map((room) => ({ room, awaits: 'unlock' }));
|
|
3047
4643
|
for (;;) {
|
|
3048
4644
|
const cover = covered(filter);
|
|
3049
4645
|
awaitPreparation(cover);
|
|
3050
|
-
const pending =
|
|
4646
|
+
const pending = [
|
|
4647
|
+
...unlocking,
|
|
4648
|
+
...('pending' in cover ? cover.pending : []),
|
|
4649
|
+
];
|
|
3051
4650
|
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
3052
4651
|
const held = covered({
|
|
3053
4652
|
...filter,
|
|
@@ -3083,20 +4682,31 @@ export function composeRuntime(options) {
|
|
|
3083
4682
|
const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
|
|
3084
4683
|
const seen = searchIndex.changes();
|
|
3085
4684
|
await searchIndex.caughtUp();
|
|
3086
|
-
const selected =
|
|
4685
|
+
const selected = roomsAlone(answering) ? null : await everyRow(answering);
|
|
3087
4686
|
if (selected !== null && 'pending' in selected)
|
|
3088
4687
|
continue;
|
|
3089
4688
|
if (searchIndex.changes() !== seen ||
|
|
3090
4689
|
JSON.stringify(covered(answering)) !== JSON.stringify(answered))
|
|
3091
4690
|
continue;
|
|
3092
|
-
const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations,
|
|
4691
|
+
const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms !== null
|
|
4692
|
+
? new Set(answering.rooms)
|
|
4693
|
+
: locked.length === 0
|
|
4694
|
+
? null
|
|
4695
|
+
: new Set(reachedRooms(answering)), plan.collection
|
|
4696
|
+
? new Map(answered.coverage.rooms.map(({ room }) => [
|
|
4697
|
+
room,
|
|
4698
|
+
replicas.get(room)?.record.typedSelection?.roots ?? [],
|
|
4699
|
+
]))
|
|
4700
|
+
: null);
|
|
3093
4701
|
const coverage = {
|
|
3094
4702
|
rooms: answered.coverage.rooms,
|
|
3095
4703
|
conversations: searchedConversations,
|
|
3096
4704
|
index: searchIndex.generation(),
|
|
3097
4705
|
overlay: answered.coverage.overlay,
|
|
3098
4706
|
};
|
|
3099
|
-
return complete
|
|
4707
|
+
return complete
|
|
4708
|
+
? { ...hits, coverage }
|
|
4709
|
+
: { pending, documents: hits.documents, messages: hits.messages, coverage };
|
|
3100
4710
|
}
|
|
3101
4711
|
}
|
|
3102
4712
|
const watch = (topic, input, deliver) => {
|
|
@@ -3105,13 +4715,17 @@ export function composeRuntime(options) {
|
|
|
3105
4715
|
roomDraftListeners.add(listener);
|
|
3106
4716
|
void exclusive(async () => {
|
|
3107
4717
|
await openLocal();
|
|
3108
|
-
if (closed || !roomDraftListeners.has(listener))
|
|
3109
|
-
return;
|
|
3110
|
-
listener.ready = true;
|
|
3111
4718
|
}).then(() => {
|
|
3112
|
-
if (closed || !
|
|
4719
|
+
if (closed || !roomDraftListeners.has(listener))
|
|
3113
4720
|
return;
|
|
3114
|
-
|
|
4721
|
+
const snapshot = snapshotRoomDrafts();
|
|
4722
|
+
draftsShown = draftsShown.then(async () => {
|
|
4723
|
+
const value = await snapshot.catch(() => null);
|
|
4724
|
+
if (value === null || closed || !roomDraftListeners.has(listener))
|
|
4725
|
+
return;
|
|
4726
|
+
listener.ready = true;
|
|
4727
|
+
listener.notify(value);
|
|
4728
|
+
});
|
|
3115
4729
|
}, () => { });
|
|
3116
4730
|
return () => {
|
|
3117
4731
|
roomDraftListeners.delete(listener);
|
|
@@ -3206,7 +4820,7 @@ export function composeRuntime(options) {
|
|
|
3206
4820
|
return listed();
|
|
3207
4821
|
},
|
|
3208
4822
|
query: ((input) => rowsQuery(planned(input, null))),
|
|
3209
|
-
search: (input) => searched(input, null),
|
|
4823
|
+
search: (input) => searched(input, null, []),
|
|
3210
4824
|
types(locator) {
|
|
3211
4825
|
return {
|
|
3212
4826
|
collection: modelTypes(modelOf(locator)),
|
|
@@ -3226,9 +4840,9 @@ export function composeRuntime(options) {
|
|
|
3226
4840
|
return listed;
|
|
3227
4841
|
});
|
|
3228
4842
|
},
|
|
3229
|
-
read(room, path) {
|
|
4843
|
+
async read(room, path) {
|
|
3230
4844
|
const record = roomDrafts.get(room)?.get(path);
|
|
3231
|
-
return record === undefined ? null :
|
|
4845
|
+
return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
|
|
3232
4846
|
},
|
|
3233
4847
|
write(room, path, revision, input) {
|
|
3234
4848
|
return exclusive(async () => {
|
|
@@ -3238,11 +4852,11 @@ export function composeRuntime(options) {
|
|
|
3238
4852
|
const local = await openLocal();
|
|
3239
4853
|
const current = roomDrafts.get(room)?.get(path);
|
|
3240
4854
|
if ((current?.revision ?? null) !== revision) {
|
|
3241
|
-
return staleWrite(current === undefined ? null :
|
|
4855
|
+
return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
|
|
3242
4856
|
}
|
|
3243
4857
|
const file = parts.file === undefined
|
|
3244
4858
|
? undefined
|
|
3245
|
-
: await heldFile(parts.file.blob, binaryBase(
|
|
4859
|
+
: await heldFile(parts.file.blob, binaryBase(await visibleOf(room, [path]), path));
|
|
3246
4860
|
const record = {
|
|
3247
4861
|
locator: room,
|
|
3248
4862
|
path,
|
|
@@ -3250,10 +4864,12 @@ export function composeRuntime(options) {
|
|
|
3250
4864
|
parts: held(current?.parts, parts, file),
|
|
3251
4865
|
returned: current?.returned ?? null,
|
|
3252
4866
|
};
|
|
3253
|
-
await local.keepRoomDraft(record
|
|
4867
|
+
await local.keepRoomDraft(record, parts.file === undefined || file === undefined
|
|
4868
|
+
? []
|
|
4869
|
+
: [{ key: file.localId, blob: parts.file.blob }]);
|
|
3254
4870
|
rememberRoomDraft(record);
|
|
3255
|
-
publishChangeSet(changeSet(room,
|
|
3256
|
-
return { outcome: 'written', roomDraft:
|
|
4871
|
+
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
4872
|
+
return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
|
|
3257
4873
|
});
|
|
3258
4874
|
},
|
|
3259
4875
|
discard(room, path, revision) {
|
|
@@ -3261,11 +4877,11 @@ export function composeRuntime(options) {
|
|
|
3261
4877
|
const local = await openLocal();
|
|
3262
4878
|
const current = roomDrafts.get(room)?.get(path);
|
|
3263
4879
|
if (current === undefined || current.revision !== revision) {
|
|
3264
|
-
return staleWrite(current === undefined ? null :
|
|
4880
|
+
return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
|
|
3265
4881
|
}
|
|
3266
4882
|
await local.discardRoomDraft(room, path);
|
|
3267
4883
|
forgetRoomDraft(room, path);
|
|
3268
|
-
publishChangeSet(changeSet(room,
|
|
4884
|
+
publishChangeSet(changeSet(room, { drafts: [path] }));
|
|
3269
4885
|
moveTo({ ...status, ...attention() });
|
|
3270
4886
|
return { outcome: 'written', roomDraft: null };
|
|
3271
4887
|
});
|
|
@@ -3281,29 +4897,39 @@ export function composeRuntime(options) {
|
|
|
3281
4897
|
});
|
|
3282
4898
|
},
|
|
3283
4899
|
write(app, value) {
|
|
3284
|
-
return
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
4900
|
+
return keepPresentation(app, () => structuredClone(value));
|
|
4901
|
+
},
|
|
4902
|
+
edit(app, members) {
|
|
4903
|
+
return keepPresentation(app, (held) => Object.fromEntries([
|
|
4904
|
+
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(members, member)),
|
|
4905
|
+
...Object.entries(structuredClone(members)).filter(([, value]) => value !== null),
|
|
4906
|
+
]));
|
|
3291
4907
|
},
|
|
3292
4908
|
},
|
|
3293
4909
|
save(room, named, writtenBy) {
|
|
3294
4910
|
return exclusive(() => saveRoomDrafts(room, named, writtenBy));
|
|
3295
4911
|
},
|
|
4912
|
+
saveParts(room, parts, writtenBy) {
|
|
4913
|
+
return exclusive(() => saveParts(room, parts, writtenBy));
|
|
4914
|
+
},
|
|
3296
4915
|
history(locator, path, query) {
|
|
3297
4916
|
return readHistory(locator, path, query);
|
|
3298
4917
|
},
|
|
4918
|
+
resolve(room, path, destination) {
|
|
4919
|
+
return resolveReference(room, path, destination);
|
|
4920
|
+
},
|
|
3299
4921
|
binaryFile: {
|
|
4922
|
+
part: readPart,
|
|
3300
4923
|
read: readBinaryPath,
|
|
4924
|
+
link: linkBinaryPath,
|
|
3301
4925
|
readReference: readBinaryReference,
|
|
3302
4926
|
},
|
|
3303
|
-
call(name, input) {
|
|
3304
|
-
|
|
4927
|
+
async call(name, input) {
|
|
4928
|
+
await ready;
|
|
4929
|
+
const room = calledRooms(name, input).find((one) => locked(one));
|
|
4930
|
+
return room === undefined ? recoveredAction(name, input) : lockedRoom(room);
|
|
3305
4931
|
},
|
|
3306
|
-
reached(locator, roomVersion, path) {
|
|
4932
|
+
async reached(locator, roomVersion, path) {
|
|
3307
4933
|
return path === undefined
|
|
3308
4934
|
? reachedBy(locator, roomVersion)
|
|
3309
4935
|
: writtenAfter(locator, roomVersion, path);
|
|
@@ -3311,6 +4937,7 @@ export function composeRuntime(options) {
|
|
|
3311
4937
|
log() {
|
|
3312
4938
|
return journal.export();
|
|
3313
4939
|
},
|
|
4940
|
+
cleanUp,
|
|
3314
4941
|
persistence() {
|
|
3315
4942
|
return persistence === 'granted';
|
|
3316
4943
|
},
|
|
@@ -3346,13 +4973,17 @@ export function composeRuntime(options) {
|
|
|
3346
4973
|
watchPlan,
|
|
3347
4974
|
search: searched,
|
|
3348
4975
|
watchReadiness,
|
|
4976
|
+
locked,
|
|
4977
|
+
lockedRooms,
|
|
4978
|
+
watchLocks,
|
|
4979
|
+
located,
|
|
3349
4980
|
};
|
|
3350
4981
|
return {
|
|
3351
4982
|
md,
|
|
4983
|
+
seed,
|
|
3352
4984
|
stop,
|
|
3353
4985
|
lose: loseRooms,
|
|
3354
4986
|
watch,
|
|
3355
|
-
acquire,
|
|
3356
4987
|
persisted(answer) {
|
|
3357
4988
|
if (answer === persistence)
|
|
3358
4989
|
return;
|