@agentshouse/mdruntime 0.1.0-c2aaa9c0e3ca4 → 0.1.0-c2c52158b7381
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 +285 -276
- package/README.md +9 -8
- package/dist/adapter.d.ts +80 -21
- 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/attempt.d.ts.map +1 -1
- package/dist/attempt.js +14 -1
- package/dist/bootstrap.d.ts +3 -2
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +83 -26
- package/dist/change-set.d.ts +7 -2
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +37 -10
- package/dist/change.d.ts +31 -1
- package/dist/change.d.ts.map +1 -1
- package/dist/change.js +10 -1
- package/dist/chat/acts.d.ts +3 -4
- package/dist/chat/acts.d.ts.map +1 -1
- package/dist/chat/acts.js +10 -10
- 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.d.ts +2 -1
- package/dist/chat/kit.d.ts.map +1 -1
- package/dist/chat/kit.js +3 -3
- package/dist/chat/list.d.ts +9 -1
- package/dist/chat/list.d.ts.map +1 -1
- package/dist/chat/list.js +53 -9
- package/dist/chat/markdown.d.ts +3 -1
- package/dist/chat/markdown.d.ts.map +1 -1
- package/dist/chat/markdown.js +85 -26
- package/dist/chat/message.d.ts +1 -0
- package/dist/chat/message.d.ts.map +1 -1
- package/dist/chat/message.js +57 -12
- package/dist/chat/messages.d.ts +9 -1
- package/dist/chat/messages.d.ts.map +1 -1
- package/dist/chat/messages.js +67 -28
- 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 +7 -2
- package/dist/chat/provider.d.ts.map +1 -1
- package/dist/chat/provider.js +129 -155
- 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 -1
- package/dist/chat/ready.d.ts.map +1 -1
- package/dist/chat/ready.js +9 -7
- 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 +8 -4
- 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 +21 -9
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +47 -8
- package/dist/collection.d.ts +21 -6
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +332 -40
- package/dist/connection.d.ts +39 -11
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +2294 -492
- package/dist/conversation.d.ts +28 -9
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +500 -165
- 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 +8 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/entry.d.ts +18 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +157 -0
- package/dist/fence.d.ts +6 -0
- package/dist/fence.d.ts.map +1 -0
- package/dist/fence.js +10 -0
- 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 +15 -3
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +103 -13
- package/dist/identity.d.ts +2 -0
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +6 -0
- package/dist/index.d.ts +15 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/memory.d.ts +8 -2
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +43 -12
- package/dist/model.d.ts +4 -2
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +10 -1
- 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 +7 -2
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +29 -12
- 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 +39 -4
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +29 -0
- package/dist/overlay.d.ts +6 -4
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +229 -161
- package/dist/query.d.ts +57 -4
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +37 -7
- package/dist/reach.d.ts +11 -4
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +192 -41
- package/dist/react.d.ts +8 -6
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +7 -17
- package/dist/read-side.d.ts +18 -9
- package/dist/read-side.d.ts.map +1 -1
- package/dist/read-side.js +894 -155
- package/dist/readiness.d.ts +53 -0
- package/dist/readiness.d.ts.map +1 -0
- package/dist/readiness.js +33 -0
- 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 +25 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +198 -0
- package/dist/refresh.d.ts +4 -4
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +46 -20
- package/dist/replica.d.ts +12 -7
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +151 -64
- package/dist/room-draft.d.ts +16 -2
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +32 -25
- package/dist/rows.d.ts +6 -13
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +10 -13
- package/dist/save.d.ts +52 -10
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +519 -159
- package/dist/search.d.ts +49 -6
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +242 -46
- 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 +19 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +461 -0
- package/dist/status.d.ts +4 -0
- package/dist/status.d.ts.map +1 -1
- package/dist/store.d.ts +61 -17
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +581 -310
- package/dist/tables.d.ts +242 -0
- package/dist/tables.d.ts.map +1 -0
- package/dist/tables.js +406 -0
- package/dist/turn.d.ts +9 -0
- package/dist/turn.d.ts.map +1 -0
- package/dist/turn.js +67 -0
- package/dist/windows.d.ts +21 -0
- package/dist/windows.d.ts.map +1 -0
- package/dist/windows.js +84 -0
- package/dist/wire.d.ts +168 -32
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +56 -31
- package/package.json +10 -2
package/dist/connection.js
CHANGED
|
@@ -1,30 +1,49 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { 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';
|
|
4
|
+
import { observing, whole } from './observation.js';
|
|
5
|
+
import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
|
|
5
6
|
import { createReadSide } from './read-side.js';
|
|
6
|
-
import {
|
|
7
|
+
import { seededResult } from './seed.js';
|
|
8
|
+
import { documentKey, markdownPath, 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, mergeChangeSets } from './change-set.js';
|
|
12
|
+
import { changeSet, changing, entering, mergeChangeSets } from './change-set.js';
|
|
10
13
|
import { createConversationStore, } from './conversation.js';
|
|
11
|
-
import { queuedFile } from './change.js';
|
|
12
|
-
import { validRoot } from './collection.js';
|
|
14
|
+
import { leaving as leavingIn, partnerOf, queuedFile, } from './change.js';
|
|
15
|
+
import { indexReferrers, referencesIndexed, 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';
|
|
21
|
+
import { createRecovery } from './recovery.js';
|
|
22
|
+
import { doorReads } from './reads.js';
|
|
17
23
|
import { refreshRoom } from './refresh.js';
|
|
18
|
-
import { buildBatch, joined,
|
|
19
|
-
import { applyReturned, compositionOf, confirmedOf, landed, readReclassified,
|
|
20
|
-
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
24
|
+
import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
|
|
25
|
+
import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicating, sameMembership, standing, } from './replica.js';
|
|
26
|
+
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
21
27
|
import { same, saving, STARTING, transferring, } from './status.js';
|
|
22
28
|
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
23
|
-
import {
|
|
29
|
+
import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
|
|
30
|
+
import { FencedMap } from './fence.js';
|
|
31
|
+
import { keysIn, tallying } from './shared.js';
|
|
32
|
+
import { finished, inChunks, ROW_WEIGHT, stepped, yielded } from './turn.js';
|
|
33
|
+
import { identityTime, roomDraftRevision } from './identity.js';
|
|
24
34
|
import { beneath, normalize } from './paths.js';
|
|
25
35
|
import { openRuntimeStore, } from './store.js';
|
|
26
36
|
import { unconfirmed } from './transfer.js';
|
|
27
37
|
import { governs, holds, narrowed, planReach, } from './reach.js';
|
|
38
|
+
function roomsAlone(plan) {
|
|
39
|
+
return (plan.reach === null &&
|
|
40
|
+
plan.root.types === null &&
|
|
41
|
+
plan.root.where === null &&
|
|
42
|
+
plan.root.pathPrefix === null &&
|
|
43
|
+
plan.paths === null &&
|
|
44
|
+
plan.hop === null &&
|
|
45
|
+
plan.backlinks === null);
|
|
46
|
+
}
|
|
28
47
|
function within(root, path) {
|
|
29
48
|
return root === '' || beneath(root, path);
|
|
30
49
|
}
|
|
@@ -38,25 +57,74 @@ function declaredSelection(room) {
|
|
|
38
57
|
root !== '' &&
|
|
39
58
|
selection.roots.every((other, at) => at === index || !within(other, root)));
|
|
40
59
|
}
|
|
60
|
+
const OPENING_LANE = 'opening';
|
|
61
|
+
const CONVERSATION_LANE = 'conversations';
|
|
62
|
+
const ROOM_READS = 256;
|
|
63
|
+
const ROOM_ATTEMPTS = 16;
|
|
64
|
+
// 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.
|
|
65
|
+
const LOCK_GRACE_MS = 120_000;
|
|
66
|
+
function operationOf(input) {
|
|
67
|
+
const operation = input?.operation_id;
|
|
68
|
+
return typeof operation === 'string' ? operation : null;
|
|
69
|
+
}
|
|
70
|
+
const outside = new Map();
|
|
71
|
+
function outsideOf(roots) {
|
|
72
|
+
const key = JSON.stringify(roots);
|
|
73
|
+
const held = outside.get(key);
|
|
74
|
+
if (held !== undefined)
|
|
75
|
+
return held;
|
|
76
|
+
const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
|
|
77
|
+
const made = {
|
|
78
|
+
documents: (path) => (markdownPath(path) ? counted(path) : 0),
|
|
79
|
+
binaries: counted,
|
|
80
|
+
};
|
|
81
|
+
outside.set(key, made);
|
|
82
|
+
return made;
|
|
83
|
+
}
|
|
84
|
+
function abandonedOn(signal) {
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
if (signal.aborted)
|
|
87
|
+
resolve(null);
|
|
88
|
+
else
|
|
89
|
+
signal.addEventListener('abort', () => resolve(null), { once: true });
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function roomLane(locator) {
|
|
93
|
+
return `room ${locator}`;
|
|
94
|
+
}
|
|
95
|
+
function pendingKey(locator, id) {
|
|
96
|
+
return `${locator}\u0000${id}`;
|
|
97
|
+
}
|
|
41
98
|
export function composeRuntime(options) {
|
|
42
99
|
const localSaves = options.adapter.localSaves === true;
|
|
100
|
+
const roomTypes = roomTypesOf(options.adapter.roomTypes);
|
|
43
101
|
let authorization = options.authorization;
|
|
44
102
|
let opening;
|
|
45
103
|
let store;
|
|
46
|
-
const
|
|
47
|
-
const
|
|
104
|
+
const fences = new Map();
|
|
105
|
+
const isFenced = (locator) => fences.has(locator);
|
|
106
|
+
let fencings = 0;
|
|
107
|
+
const lastFenced = new Map();
|
|
108
|
+
const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
|
|
109
|
+
let discoveries = 0;
|
|
110
|
+
const replicas = new FencedMap(isFenced);
|
|
111
|
+
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
48
112
|
const pendings = new Map();
|
|
49
|
-
const
|
|
113
|
+
const seeds = new Map();
|
|
114
|
+
const seedWatchers = new Map();
|
|
115
|
+
const roomDrafts = new FencedMap(isFenced);
|
|
50
116
|
const presentations = new Map();
|
|
51
117
|
let limits = [];
|
|
52
118
|
let capabilities = [];
|
|
53
|
-
const rooms = new
|
|
119
|
+
const rooms = new FencedMap(isFenced);
|
|
54
120
|
const counted = new Map();
|
|
55
121
|
const unreadable = new Set();
|
|
56
|
-
const
|
|
57
|
-
const
|
|
122
|
+
const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
|
|
123
|
+
const models = new FencedMap(isModelFenced);
|
|
124
|
+
const liveModels = new FencedMap(isModelFenced);
|
|
58
125
|
const invalidModels = new Set();
|
|
59
126
|
const discovered = new Map();
|
|
127
|
+
const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
|
|
60
128
|
let roomsListed = false;
|
|
61
129
|
const scopedRefusals = new Map();
|
|
62
130
|
const sourceRefusals = new Map();
|
|
@@ -84,7 +152,6 @@ export function composeRuntime(options) {
|
|
|
84
152
|
let status = STARTING;
|
|
85
153
|
let shown = STARTING;
|
|
86
154
|
let discoveryPaused = false;
|
|
87
|
-
let resetTimer;
|
|
88
155
|
let closed = false;
|
|
89
156
|
let stopping = false;
|
|
90
157
|
let withdrawStream;
|
|
@@ -92,12 +159,23 @@ export function composeRuntime(options) {
|
|
|
92
159
|
let underway = null;
|
|
93
160
|
let streamed = null;
|
|
94
161
|
let handed = false;
|
|
95
|
-
const
|
|
162
|
+
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
163
|
+
const repairing = new Set();
|
|
164
|
+
const calling = new Set();
|
|
165
|
+
const repairable = new Map();
|
|
166
|
+
let outcomeRetention = null;
|
|
96
167
|
const answering = new Map();
|
|
97
168
|
let complete = false;
|
|
98
169
|
let recorded = false;
|
|
99
170
|
const stale = new Set();
|
|
100
|
-
let
|
|
171
|
+
let generation = 0;
|
|
172
|
+
let discovering = null;
|
|
173
|
+
let opened = null;
|
|
174
|
+
const owed = new Set();
|
|
175
|
+
const reloadsDue = new Map();
|
|
176
|
+
const lanes = new Map();
|
|
177
|
+
let roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
178
|
+
const working = new Set();
|
|
101
179
|
let mutations = Promise.resolve();
|
|
102
180
|
const roomListeners = new Set();
|
|
103
181
|
const refusalListeners = new Set();
|
|
@@ -108,33 +186,52 @@ export function composeRuntime(options) {
|
|
|
108
186
|
const presentationListeners = new Set();
|
|
109
187
|
const readSide = createReadSide();
|
|
110
188
|
let withdrawConversations;
|
|
111
|
-
let currencyQueued =
|
|
189
|
+
let currencyQueued = null;
|
|
112
190
|
let socketOpen = false;
|
|
191
|
+
const confirmed = new Set();
|
|
192
|
+
let lockGrace = null;
|
|
193
|
+
const lockListeners = new Set();
|
|
113
194
|
let serviceMode = 'open';
|
|
114
195
|
let haltedRefusal = false;
|
|
115
196
|
let openSignals = 0;
|
|
116
197
|
const serviceModeListeners = new Set();
|
|
117
198
|
let member = null;
|
|
118
199
|
const memberListeners = new Set();
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
const
|
|
200
|
+
const prepared = new Set();
|
|
201
|
+
const projecting = new Set();
|
|
202
|
+
const unpreparedDemand = new Set();
|
|
203
|
+
const unindexable = new Set();
|
|
204
|
+
let preparing = false;
|
|
205
|
+
let targets = {};
|
|
206
|
+
let persistence = options.shell.persistence;
|
|
207
|
+
let capacity = null;
|
|
208
|
+
const paused = new Set();
|
|
209
|
+
let rechecking = null;
|
|
210
|
+
const readinessWatches = new Set();
|
|
211
|
+
let readinessQueued = false;
|
|
212
|
+
const catching = new Set();
|
|
213
|
+
const transfers = new FencedMap(isFenced);
|
|
122
214
|
const conversations = createConversationStore({
|
|
123
215
|
agents: declaredAgents,
|
|
124
|
-
documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
|
|
216
|
+
documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
|
|
125
217
|
call: (name, input) => invokeAction(name, input),
|
|
126
218
|
durably,
|
|
127
219
|
stream: () => options.adapter.conversations,
|
|
128
220
|
publish: publishChangeSet,
|
|
221
|
+
online: () => connectionOf() !== 'unreachable' && refusal === null,
|
|
129
222
|
closed: () => closed,
|
|
223
|
+
unanswered: resendLater,
|
|
224
|
+
expired: (operation) => pastHorizon(operation),
|
|
130
225
|
});
|
|
131
226
|
const searchIndex = createSearchIndex({
|
|
132
|
-
|
|
227
|
+
room: (room) => readSide.room(room),
|
|
133
228
|
document: (room, path) => readSide.document(room, path),
|
|
134
|
-
messages: () => conversations.messages(),
|
|
135
|
-
message: (conversation, identity) => conversations.message(conversation, identity),
|
|
136
229
|
});
|
|
230
|
+
const roomsToIndex = new Set();
|
|
231
|
+
const conversationsToIndex = new Set();
|
|
232
|
+
let indexing = false;
|
|
137
233
|
const journal = createLog();
|
|
234
|
+
const keptFiles = openedFiles(options.database, isFenced);
|
|
138
235
|
let notifyReady = () => { };
|
|
139
236
|
const ready = new Promise((resolve) => {
|
|
140
237
|
notifyReady = resolve;
|
|
@@ -188,6 +285,15 @@ export function composeRuntime(options) {
|
|
|
188
285
|
const value = presentations.get(app);
|
|
189
286
|
return value === undefined ? null : structuredClone(value);
|
|
190
287
|
}
|
|
288
|
+
function keepPresentation(app, next) {
|
|
289
|
+
return exclusive(async () => {
|
|
290
|
+
const local = await openLocal();
|
|
291
|
+
const stored = next(presentations.get(app));
|
|
292
|
+
await local.present({ app, value: stored });
|
|
293
|
+
presentations.set(app, stored);
|
|
294
|
+
publishChangeSet(changeSet('', null, null, { presentation: app }));
|
|
295
|
+
});
|
|
296
|
+
}
|
|
191
297
|
function emitPresentation(app) {
|
|
192
298
|
if (closed)
|
|
193
299
|
return;
|
|
@@ -212,9 +318,16 @@ export function composeRuntime(options) {
|
|
|
212
318
|
returned: structuredClone(record.returned),
|
|
213
319
|
});
|
|
214
320
|
}
|
|
321
|
+
for (const [room, refusal] of sourceRefusals)
|
|
322
|
+
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
215
323
|
attentionRequired.sort((left, right) => {
|
|
216
324
|
if (left.room !== right.room)
|
|
217
325
|
return left.room < right.room ? -1 : 1;
|
|
326
|
+
if ('refusal' in left || 'refusal' in right) {
|
|
327
|
+
if ('refusal' in left && 'refusal' in right)
|
|
328
|
+
return 0;
|
|
329
|
+
return 'refusal' in left ? -1 : 1;
|
|
330
|
+
}
|
|
218
331
|
if (left.path !== right.path)
|
|
219
332
|
return left.path < right.path ? -1 : 1;
|
|
220
333
|
return 0;
|
|
@@ -230,23 +343,74 @@ export function composeRuntime(options) {
|
|
|
230
343
|
}
|
|
231
344
|
function documentsIn(locator) {
|
|
232
345
|
const replica = replicas.get(locator);
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
count += 1;
|
|
240
|
-
}
|
|
241
|
-
return count;
|
|
346
|
+
return replica === undefined ? 0 : finished(counting(replica));
|
|
347
|
+
}
|
|
348
|
+
function* counting(replica) {
|
|
349
|
+
const weighed = outsideOf(replica.confirmed.record.typedSelection?.roots ?? []);
|
|
350
|
+
return ((yield* tallying(replica.visible.documents, weighed.documents)) +
|
|
351
|
+
(yield* tallying(replica.visible.binaries, weighed.binaries)));
|
|
242
352
|
}
|
|
243
353
|
function listed() {
|
|
244
354
|
return structuredClone([...rooms.values()].map((room) => {
|
|
245
355
|
const documents = documentsIn(room.locator);
|
|
246
356
|
counted.set(room.locator, documents);
|
|
247
|
-
return
|
|
357
|
+
return locked(room.locator)
|
|
358
|
+
? {
|
|
359
|
+
...room,
|
|
360
|
+
model: modelTypes(NO_MODEL),
|
|
361
|
+
typedSelection: null,
|
|
362
|
+
documents: 0,
|
|
363
|
+
locked: true,
|
|
364
|
+
}
|
|
365
|
+
: { ...room, documents, locked: false };
|
|
248
366
|
}));
|
|
249
367
|
}
|
|
368
|
+
function locked(locator) {
|
|
369
|
+
return rooms.get(locator)?.replica.offline === 'locked' && !confirmed.has(locator);
|
|
370
|
+
}
|
|
371
|
+
function lockedRooms() {
|
|
372
|
+
return [...rooms.keys()].filter(locked).sort();
|
|
373
|
+
}
|
|
374
|
+
function relocked(before) {
|
|
375
|
+
const after = lockedRooms();
|
|
376
|
+
if (JSON.stringify(after) === JSON.stringify(before))
|
|
377
|
+
return;
|
|
378
|
+
journal.record(`locked ${after.length}`);
|
|
379
|
+
emitRooms();
|
|
380
|
+
for (const notify of [...lockListeners])
|
|
381
|
+
notify();
|
|
382
|
+
}
|
|
383
|
+
function confirmLocks(listed) {
|
|
384
|
+
if (!socketOpen)
|
|
385
|
+
return;
|
|
386
|
+
for (const room of listed) {
|
|
387
|
+
if (room.replica.admission === 'admitted')
|
|
388
|
+
confirmed.add(room.locator);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
function connectionLost() {
|
|
392
|
+
if (lockGrace !== null || closed)
|
|
393
|
+
return;
|
|
394
|
+
lockGrace = setTimeout(() => {
|
|
395
|
+
lockGrace = null;
|
|
396
|
+
if (socketOpen || closed)
|
|
397
|
+
return;
|
|
398
|
+
const before = lockedRooms();
|
|
399
|
+
confirmed.clear();
|
|
400
|
+
relocked(before);
|
|
401
|
+
}, LOCK_GRACE_MS);
|
|
402
|
+
}
|
|
403
|
+
function connectionLive() {
|
|
404
|
+
if (lockGrace !== null)
|
|
405
|
+
clearTimeout(lockGrace);
|
|
406
|
+
lockGrace = null;
|
|
407
|
+
}
|
|
408
|
+
function watchLocks(notify) {
|
|
409
|
+
lockListeners.add(notify);
|
|
410
|
+
return () => {
|
|
411
|
+
lockListeners.delete(notify);
|
|
412
|
+
};
|
|
413
|
+
}
|
|
250
414
|
function roomFacts(reach = null) {
|
|
251
415
|
const held = [...rooms.values()]
|
|
252
416
|
.filter((room) => holds(reach, room.locator))
|
|
@@ -265,15 +429,19 @@ export function composeRuntime(options) {
|
|
|
265
429
|
return { listed: roomsListed, rooms: held };
|
|
266
430
|
}
|
|
267
431
|
function emitRooms() {
|
|
432
|
+
readSide.recover();
|
|
433
|
+
queueReadiness();
|
|
268
434
|
const value = listed();
|
|
269
435
|
for (const listener of roomListeners)
|
|
270
436
|
listener(value);
|
|
271
437
|
}
|
|
272
438
|
function emitRefusal() {
|
|
439
|
+
queueReadiness();
|
|
273
440
|
for (const listener of refusalListeners)
|
|
274
441
|
listener(refusal);
|
|
275
442
|
}
|
|
276
443
|
function emitStorageLoss() {
|
|
444
|
+
queueReadiness();
|
|
277
445
|
for (const listener of storageLossListeners)
|
|
278
446
|
listener(storageLoss);
|
|
279
447
|
}
|
|
@@ -290,6 +458,7 @@ export function composeRuntime(options) {
|
|
|
290
458
|
function show() {
|
|
291
459
|
if (closed)
|
|
292
460
|
return;
|
|
461
|
+
queueReadiness();
|
|
293
462
|
const next = status.state === 'synced' && !conversations.current()
|
|
294
463
|
? { ...status, state: 'syncing' }
|
|
295
464
|
: status;
|
|
@@ -299,17 +468,17 @@ export function composeRuntime(options) {
|
|
|
299
468
|
for (const listener of statusListeners)
|
|
300
469
|
listener(shown);
|
|
301
470
|
}
|
|
302
|
-
function entryOf(batch) {
|
|
471
|
+
function entryOf(locator, batch) {
|
|
303
472
|
if (localSaves)
|
|
304
473
|
return Object.freeze({ batch, state: 'local' });
|
|
305
|
-
const state = pendings.get(batch.id);
|
|
474
|
+
const state = pendings.get(pendingKey(locator, batch.id));
|
|
306
475
|
if (state?.accepted !== undefined) {
|
|
307
476
|
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
308
477
|
}
|
|
309
478
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
310
479
|
}
|
|
311
480
|
function entriesOf(locator) {
|
|
312
|
-
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
|
|
481
|
+
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map((batch) => entryOf(locator, batch));
|
|
313
482
|
}
|
|
314
483
|
function emitQueue(locator) {
|
|
315
484
|
const entries = entriesOf(locator);
|
|
@@ -319,42 +488,462 @@ export function composeRuntime(options) {
|
|
|
319
488
|
}
|
|
320
489
|
}
|
|
321
490
|
function applyChangeSet(set) {
|
|
322
|
-
const held =
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
searchIndex.changed(held);
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
491
|
+
const held = {
|
|
492
|
+
...set,
|
|
493
|
+
documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
|
|
494
|
+
};
|
|
495
|
+
const applied = readSide.apply(held, () => searchIndex.changed(held));
|
|
496
|
+
if (applied instanceof Promise)
|
|
497
|
+
void applied.catch(unpublished);
|
|
498
|
+
}
|
|
499
|
+
function unpublished(error) {
|
|
500
|
+
journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
|
|
501
|
+
}
|
|
502
|
+
async function referenced(locator) {
|
|
503
|
+
for (;;) {
|
|
504
|
+
const record = replicas.get(locator)?.confirmed.record;
|
|
505
|
+
if (record === undefined || closed)
|
|
506
|
+
return;
|
|
507
|
+
const held = { text: record.documents, binaries: record.binaries };
|
|
508
|
+
const composition = compositionOf(record);
|
|
509
|
+
if (referencesIndexed(held, composition))
|
|
510
|
+
return;
|
|
511
|
+
await indexReferrers(held, composition);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
async function prepareReplica(locator) {
|
|
515
|
+
await referenced(locator);
|
|
516
|
+
const replica = replicas.get(locator);
|
|
517
|
+
if (replica === undefined || closed || prepared.has(locator))
|
|
330
518
|
return;
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
519
|
+
const visible = replica.visible;
|
|
520
|
+
const entered = [];
|
|
521
|
+
projecting.add(locator);
|
|
522
|
+
try {
|
|
523
|
+
await inChunks(await stepped(keysIn(visible.documents, visible.binaries)), (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
|
|
524
|
+
const current = replicas.get(locator);
|
|
525
|
+
if (current === undefined || closed)
|
|
526
|
+
return;
|
|
527
|
+
const set = entering(locator, current, chunk);
|
|
528
|
+
entered.push(set);
|
|
529
|
+
return readSide.apply(set);
|
|
530
|
+
});
|
|
531
|
+
if (!replicas.has(locator) || closed)
|
|
532
|
+
return;
|
|
533
|
+
prepared.add(locator);
|
|
341
534
|
}
|
|
342
|
-
|
|
535
|
+
finally {
|
|
536
|
+
projecting.delete(locator);
|
|
537
|
+
}
|
|
538
|
+
conversations.changed(mergeChangeSets(entered));
|
|
539
|
+
readSide.recover();
|
|
540
|
+
queueReadiness();
|
|
541
|
+
}
|
|
542
|
+
function unprepared() {
|
|
543
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
|
|
544
|
+
}
|
|
545
|
+
function unbuilt(locator, error) {
|
|
546
|
+
unindexable.add(locator);
|
|
547
|
+
journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
|
|
548
|
+
queueReadiness();
|
|
549
|
+
}
|
|
550
|
+
function prepare(locators = []) {
|
|
551
|
+
for (const locator of locators)
|
|
552
|
+
unpreparedDemand.add(locator);
|
|
553
|
+
if (preparing)
|
|
343
554
|
return;
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
555
|
+
preparing = true;
|
|
556
|
+
void (async () => {
|
|
557
|
+
try {
|
|
558
|
+
for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
|
|
559
|
+
await yielded();
|
|
560
|
+
if (closed)
|
|
561
|
+
return;
|
|
562
|
+
unpreparedDemand.delete(waiting);
|
|
563
|
+
await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
finally {
|
|
567
|
+
preparing = false;
|
|
568
|
+
}
|
|
569
|
+
})();
|
|
570
|
+
}
|
|
571
|
+
function roomsOfReach(reach) {
|
|
572
|
+
return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
347
573
|
}
|
|
348
574
|
function reachedRooms(plan) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
575
|
+
const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
|
|
576
|
+
const named = plan.rooms ??
|
|
577
|
+
(reached === null
|
|
578
|
+
? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
|
|
579
|
+
: [...reached]);
|
|
580
|
+
return [...new Set(named)]
|
|
581
|
+
.filter((locator) => (reached === null || reached.has(locator)) && !plan.locked.includes(locator))
|
|
582
|
+
.sort();
|
|
583
|
+
}
|
|
584
|
+
function awaited(locator) {
|
|
585
|
+
return (!roomsListed ||
|
|
586
|
+
(rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
|
|
587
|
+
}
|
|
588
|
+
function covered(plan) {
|
|
589
|
+
const locators = reachedRooms(plan);
|
|
590
|
+
if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
|
|
591
|
+
return { pending: [] };
|
|
592
|
+
}
|
|
593
|
+
const pending = [];
|
|
594
|
+
const coverage = [];
|
|
595
|
+
const overlay = [];
|
|
596
|
+
for (const locator of locators) {
|
|
597
|
+
const replica = replicas.get(locator);
|
|
598
|
+
if (replica === undefined) {
|
|
599
|
+
if (awaited(locator))
|
|
600
|
+
pending.push({ room: locator, awaits: 'acquisition' });
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
if (!prepared.has(locator)) {
|
|
604
|
+
pending.push({ room: locator, awaits: 'preparation' });
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
coverage.push({
|
|
608
|
+
room: locator,
|
|
609
|
+
generation: replica.confirmed.record.transfer,
|
|
610
|
+
current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
|
|
611
|
+
});
|
|
612
|
+
for (const batch of replica.queue)
|
|
613
|
+
overlay.push(batch.id);
|
|
614
|
+
}
|
|
615
|
+
if (pending.length > 0)
|
|
616
|
+
return { pending };
|
|
617
|
+
return { coverage: { rooms: coverage, indexes: [], overlay } };
|
|
618
|
+
}
|
|
619
|
+
function awaitPreparation(cover) {
|
|
620
|
+
if (!('pending' in cover))
|
|
621
|
+
return;
|
|
622
|
+
const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
|
|
623
|
+
if (waiting.length > 0)
|
|
624
|
+
prepare(waiting.map(({ room }) => room));
|
|
625
|
+
}
|
|
626
|
+
function requiredRooms(given) {
|
|
627
|
+
const reach = planReach(given);
|
|
628
|
+
if (reach === null)
|
|
629
|
+
return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
|
|
630
|
+
const named = [...roomsOfReach(reach)].sort();
|
|
631
|
+
if (roomsListed)
|
|
632
|
+
return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
|
|
633
|
+
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
634
|
+
}
|
|
635
|
+
function roomState(locator, search) {
|
|
636
|
+
if (sourceRefusals.has(locator))
|
|
637
|
+
return 'refused';
|
|
638
|
+
const listedRoom = discovered.get(locator);
|
|
639
|
+
if (roomsListed &&
|
|
640
|
+
(listedRoom === undefined ||
|
|
641
|
+
listedRoom.replica.admission !== 'admitted' ||
|
|
642
|
+
unreadable.has(locator)))
|
|
643
|
+
return 'unadmitted';
|
|
644
|
+
if (!replicas.has(locator))
|
|
645
|
+
return 'acquiring';
|
|
646
|
+
if (!prepared.has(locator))
|
|
647
|
+
return 'preparing';
|
|
648
|
+
if (search && !searchIndex.rooms().has(locator))
|
|
649
|
+
return 'indexing';
|
|
650
|
+
return 'complete';
|
|
651
|
+
}
|
|
652
|
+
function conversationCoverage(reach, search) {
|
|
653
|
+
const declared = reach === null || reach.conversations;
|
|
654
|
+
if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
|
|
655
|
+
return 'undeclared';
|
|
656
|
+
if (!conversations.known() || !conversations.current())
|
|
657
|
+
return 'acquiring';
|
|
658
|
+
if (!search)
|
|
659
|
+
return 'complete';
|
|
660
|
+
const unindexed = conversations
|
|
661
|
+
.searchable()
|
|
662
|
+
.filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
|
|
663
|
+
if (unindexed.length === 0)
|
|
664
|
+
return 'complete';
|
|
665
|
+
index([], unindexed);
|
|
666
|
+
return 'indexing';
|
|
667
|
+
}
|
|
668
|
+
function connectionOf() {
|
|
669
|
+
if (socketOpen)
|
|
670
|
+
return 'connected';
|
|
671
|
+
if (status.providerUnreachable)
|
|
672
|
+
return 'unreachable';
|
|
673
|
+
return options.adapter.roomStream === undefined && status.lastCompleted !== null
|
|
674
|
+
? 'connected'
|
|
675
|
+
: 'connecting';
|
|
676
|
+
}
|
|
677
|
+
function refusedIn(locators) {
|
|
678
|
+
return locators.flatMap((room) => {
|
|
679
|
+
const held = sourceRefusals.get(room);
|
|
680
|
+
if (held === undefined)
|
|
681
|
+
return [];
|
|
682
|
+
const { file, location } = held.fields;
|
|
683
|
+
return [
|
|
684
|
+
{
|
|
685
|
+
room,
|
|
686
|
+
file: typeof file === 'string' ? file : '',
|
|
687
|
+
rule: held.code,
|
|
688
|
+
location: typeof location === 'string' ? location : null,
|
|
689
|
+
},
|
|
690
|
+
];
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
function readinessFor(port, reach) {
|
|
694
|
+
const required = requiredRooms(reach);
|
|
695
|
+
const search = port.search === 'required';
|
|
696
|
+
const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
|
|
697
|
+
const building = (state) => coverage
|
|
698
|
+
.filter((room) => room.state === state && !unindexable.has(room.room))
|
|
699
|
+
.map(({ room }) => room);
|
|
700
|
+
const preparing = building('preparing');
|
|
701
|
+
if (preparing.length > 0)
|
|
702
|
+
prepare(preparing);
|
|
703
|
+
const indexing = building('indexing');
|
|
704
|
+
if (indexing.length > 0)
|
|
705
|
+
index(indexing, []);
|
|
706
|
+
return readinessOf({
|
|
707
|
+
code: port.code,
|
|
708
|
+
coverage: {
|
|
709
|
+
listed: required.listed,
|
|
710
|
+
rooms: coverage,
|
|
711
|
+
conversations: conversationCoverage(reach, search),
|
|
712
|
+
search: search ? 'required' : 'unused',
|
|
713
|
+
},
|
|
714
|
+
persistence,
|
|
715
|
+
connection: connectionOf(),
|
|
716
|
+
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
717
|
+
capacity: capacity !== null &&
|
|
718
|
+
coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
|
|
719
|
+
? capacity
|
|
720
|
+
: port.capacity,
|
|
721
|
+
refused: refusedIn(required.rooms),
|
|
722
|
+
lost: storageLoss !== null,
|
|
723
|
+
established: targets[port.target]?.established === true,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
function establish(name) {
|
|
727
|
+
targets = { ...targets, [name]: { established: true } };
|
|
728
|
+
queueReadiness();
|
|
729
|
+
void durably((local) => local.keepTargets(targets)).catch(() => {
|
|
730
|
+
journal.record(`target record failed ${name}`);
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
function deliverReadiness(watch) {
|
|
734
|
+
if (!readinessWatches.has(watch))
|
|
735
|
+
return;
|
|
736
|
+
const readiness = readinessFor(watch.port, watch.reach);
|
|
737
|
+
if (readiness.ready && targets[watch.port.target]?.established !== true) {
|
|
738
|
+
establish(watch.port.target);
|
|
739
|
+
}
|
|
740
|
+
const shown = JSON.stringify(readiness);
|
|
741
|
+
if (shown === watch.last)
|
|
742
|
+
return;
|
|
743
|
+
watch.last = shown;
|
|
744
|
+
watch.deliver(structuredClone(readiness));
|
|
745
|
+
}
|
|
746
|
+
function queueReadiness() {
|
|
747
|
+
if (readinessQueued || closed || readinessWatches.size === 0)
|
|
748
|
+
return;
|
|
749
|
+
readinessQueued = true;
|
|
750
|
+
queueMicrotask(() => {
|
|
751
|
+
readinessQueued = false;
|
|
752
|
+
if (closed)
|
|
753
|
+
return;
|
|
754
|
+
for (const watch of [...readinessWatches])
|
|
755
|
+
deliverReadiness(watch);
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
function watchReadiness(port, reach, deliver) {
|
|
759
|
+
const watch = { port, reach, deliver, last: null };
|
|
760
|
+
readinessWatches.add(watch);
|
|
761
|
+
void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
|
|
762
|
+
return () => {
|
|
763
|
+
readinessWatches.delete(watch);
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
function layerBytes(layer) {
|
|
767
|
+
let bytes = 0;
|
|
768
|
+
for (const [path, text] of Object.entries(layer.documents))
|
|
769
|
+
bytes += (path.length + text.length) * 2;
|
|
770
|
+
return bytes;
|
|
771
|
+
}
|
|
772
|
+
function grownBy(next, previous) {
|
|
773
|
+
if (previous === undefined)
|
|
774
|
+
return layerBytes(next);
|
|
775
|
+
const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
|
|
776
|
+
return layerBytes({ ...next, documents });
|
|
777
|
+
}
|
|
778
|
+
function exceeded(error) {
|
|
779
|
+
return (typeof error === 'object' &&
|
|
780
|
+
error !== null &&
|
|
781
|
+
error.name === 'QuotaExceededError');
|
|
782
|
+
}
|
|
783
|
+
async function estimated() {
|
|
784
|
+
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
785
|
+
return estimate === null ? null : Math.max(0, estimate.total - estimate.usage);
|
|
786
|
+
}
|
|
787
|
+
function pause(locator, cause) {
|
|
788
|
+
paused.add(locator);
|
|
789
|
+
capacity = cause;
|
|
790
|
+
journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
|
|
791
|
+
queueReadiness();
|
|
792
|
+
if (rechecking !== null)
|
|
793
|
+
return;
|
|
794
|
+
// A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
|
|
795
|
+
rechecking = setInterval(() => {
|
|
796
|
+
void recheck();
|
|
797
|
+
}, 60_000);
|
|
798
|
+
}
|
|
799
|
+
async function recheck() {
|
|
800
|
+
const held = capacity;
|
|
801
|
+
if (held === null || closed)
|
|
802
|
+
return;
|
|
803
|
+
const available = await estimated().catch(() => null);
|
|
804
|
+
if (available === null || available < (held.needed ?? 0))
|
|
805
|
+
return;
|
|
806
|
+
if (held.cause === 'storage-exceeded' &&
|
|
807
|
+
(held.available === null || available <= held.available))
|
|
808
|
+
return;
|
|
809
|
+
resume();
|
|
810
|
+
}
|
|
811
|
+
function resume() {
|
|
812
|
+
if (rechecking !== null)
|
|
813
|
+
clearInterval(rechecking);
|
|
814
|
+
rechecking = null;
|
|
815
|
+
if (paused.size === 0 && capacity === null)
|
|
816
|
+
return;
|
|
817
|
+
paused.clear();
|
|
818
|
+
capacity = null;
|
|
819
|
+
journal.record('capacity resumed');
|
|
820
|
+
queueReadiness();
|
|
821
|
+
if (!closed && authorization !== undefined)
|
|
822
|
+
void sync('unstreamed');
|
|
823
|
+
}
|
|
824
|
+
async function reclaimed(local) {
|
|
825
|
+
const unreferenced = [...transfers.keys()].filter((locator) => !discovered.has(locator));
|
|
826
|
+
for (const locator of unreferenced) {
|
|
827
|
+
await local.abandon(locator);
|
|
828
|
+
transfers.delete(locator);
|
|
829
|
+
}
|
|
830
|
+
if (unreferenced.length > 0)
|
|
831
|
+
journal.record(`capacity reclaimed ${unreferenced.length}`);
|
|
832
|
+
return unreferenced.length > 0;
|
|
833
|
+
}
|
|
834
|
+
async function admitted(local, locator, needed) {
|
|
835
|
+
if (paused.has(locator))
|
|
836
|
+
return false;
|
|
837
|
+
let available = await estimated().catch(() => null);
|
|
838
|
+
if (available === null || available >= needed)
|
|
839
|
+
return true;
|
|
840
|
+
if (await reclaimed(local))
|
|
841
|
+
available = await estimated().catch(() => null);
|
|
842
|
+
if (available === null || available >= needed)
|
|
843
|
+
return true;
|
|
844
|
+
pause(locator, { cause: 'insufficient-space', needed, available });
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
async function exceededOn(locator, bytes) {
|
|
848
|
+
const available = await estimated().catch(() => null);
|
|
849
|
+
pause(locator, { cause: 'storage-exceeded', needed: bytes, available });
|
|
850
|
+
}
|
|
851
|
+
function seedRoomHeld(room, version) {
|
|
852
|
+
return prepared.has(room) && reachedBy(room, version);
|
|
853
|
+
}
|
|
854
|
+
function seedHeld(at) {
|
|
855
|
+
return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
|
|
856
|
+
}
|
|
857
|
+
function seedApplies(plan) {
|
|
858
|
+
const seed = seeds.get(plan.key);
|
|
859
|
+
if (seed === undefined)
|
|
860
|
+
return null;
|
|
861
|
+
if (!seedHeld(seed.at))
|
|
862
|
+
return seed;
|
|
863
|
+
seeds.delete(plan.key);
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
866
|
+
function seedBehind(plan, cover) {
|
|
867
|
+
const seed = seedApplies(plan);
|
|
868
|
+
if (seed === null || 'pending' in cover)
|
|
869
|
+
return cover;
|
|
870
|
+
return {
|
|
871
|
+
pending: Object.entries(seed.at)
|
|
872
|
+
.filter(([room, version]) => !seedRoomHeld(room, version))
|
|
873
|
+
.map(([room]) => ({ room, awaits: 'acquisition' })),
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
function seed(entry) {
|
|
877
|
+
if (seedHeld(entry.at))
|
|
878
|
+
return;
|
|
879
|
+
for (const { query, rows } of entry.queries) {
|
|
880
|
+
const key = queryKey(query);
|
|
881
|
+
seeds.set(key, { rows, at: entry.at });
|
|
882
|
+
for (const shown of seedWatchers.get(key) ?? [])
|
|
883
|
+
shown();
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
async function rowsQuery(plan) {
|
|
887
|
+
const before = covered(plan);
|
|
888
|
+
awaitPreparation(before);
|
|
889
|
+
const seeded = seedApplies(plan);
|
|
890
|
+
if (seeded !== null)
|
|
891
|
+
return seededResult(seeded).result;
|
|
892
|
+
if ('pending' in before)
|
|
893
|
+
return before;
|
|
894
|
+
const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
|
|
895
|
+
awaitPreparation(cover);
|
|
896
|
+
if ('pending' in cover)
|
|
897
|
+
return cover;
|
|
898
|
+
return { ...rows, coverage: cover.coverage };
|
|
899
|
+
}
|
|
900
|
+
async function indexConversation(conversation) {
|
|
901
|
+
searchIndex.indexMessages(conversation, [], false);
|
|
902
|
+
let after = null;
|
|
903
|
+
for (;;) {
|
|
904
|
+
const batch = await durably((local) => local.messagesAfter(conversation, after));
|
|
905
|
+
if (closed)
|
|
906
|
+
return;
|
|
907
|
+
searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
|
|
908
|
+
if (batch.length === 0)
|
|
909
|
+
return;
|
|
910
|
+
after = batch[batch.length - 1].identity;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
function indexNext() {
|
|
914
|
+
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
|
|
915
|
+
if (room !== undefined) {
|
|
916
|
+
roomsToIndex.delete(room);
|
|
917
|
+
return () => prepared.has(room)
|
|
918
|
+
? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
|
|
919
|
+
: undefined;
|
|
920
|
+
}
|
|
921
|
+
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
922
|
+
if (conversation === undefined)
|
|
352
923
|
return null;
|
|
353
|
-
|
|
924
|
+
conversationsToIndex.delete(conversation);
|
|
925
|
+
return () => indexConversation(conversation);
|
|
354
926
|
}
|
|
355
|
-
function
|
|
356
|
-
|
|
357
|
-
|
|
927
|
+
function index(rooms, named) {
|
|
928
|
+
for (const room of rooms)
|
|
929
|
+
roomsToIndex.add(room);
|
|
930
|
+
for (const conversation of named)
|
|
931
|
+
conversationsToIndex.add(conversation);
|
|
932
|
+
if (indexing)
|
|
933
|
+
return;
|
|
934
|
+
indexing = true;
|
|
935
|
+
void (async () => {
|
|
936
|
+
try {
|
|
937
|
+
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
938
|
+
await yielded();
|
|
939
|
+
await unit();
|
|
940
|
+
queueReadiness();
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
finally {
|
|
944
|
+
indexing = false;
|
|
945
|
+
}
|
|
946
|
+
})().catch(() => { });
|
|
358
947
|
}
|
|
359
948
|
function publishChangeSet(set) {
|
|
360
949
|
applyChangeSet(set);
|
|
@@ -371,34 +960,7 @@ export function composeRuntime(options) {
|
|
|
371
960
|
emitPresentation(app);
|
|
372
961
|
conversations.changed(set);
|
|
373
962
|
}
|
|
374
|
-
function stopTimer() {
|
|
375
|
-
if (resetTimer === undefined)
|
|
376
|
-
return;
|
|
377
|
-
clearTimeout(resetTimer);
|
|
378
|
-
resetTimer = undefined;
|
|
379
|
-
}
|
|
380
|
-
function armTimer() {
|
|
381
|
-
stopTimer();
|
|
382
|
-
if (closed || stopping)
|
|
383
|
-
return;
|
|
384
|
-
let earliest = null;
|
|
385
|
-
for (const { submission } of pendings.values()) {
|
|
386
|
-
if (submission.state !== 'reset')
|
|
387
|
-
continue;
|
|
388
|
-
const at = Date.parse(submission.reset);
|
|
389
|
-
if (earliest === null || at < earliest)
|
|
390
|
-
earliest = at;
|
|
391
|
-
}
|
|
392
|
-
if (earliest === null)
|
|
393
|
-
return;
|
|
394
|
-
// A Save House held back goes again only once the reset its answer named has passed.
|
|
395
|
-
resetTimer = setTimeout(() => {
|
|
396
|
-
resetTimer = undefined;
|
|
397
|
-
submitAll();
|
|
398
|
-
}, Math.max(0, earliest - Date.now()));
|
|
399
|
-
}
|
|
400
963
|
async function refuse(reason, provider = null) {
|
|
401
|
-
stopTimer();
|
|
402
964
|
discoveryPaused = true;
|
|
403
965
|
try {
|
|
404
966
|
if (reason === 'revoked-authorization') {
|
|
@@ -421,17 +983,22 @@ export function composeRuntime(options) {
|
|
|
421
983
|
models.set(key, revoked);
|
|
422
984
|
liveModels.delete(key);
|
|
423
985
|
}
|
|
424
|
-
const
|
|
425
|
-
for (const
|
|
986
|
+
const revocations = [];
|
|
987
|
+
for (const locator of [...rooms.keys()]) {
|
|
426
988
|
const before = replicas.get(locator) ?? null;
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
989
|
+
const after = before === null
|
|
990
|
+
? null
|
|
991
|
+
: await stepped(replicating(await confirmedOf({ ...before.confirmed.record, model: revoked, selectionModel: revoked }, before.confirmed), before.queue, before));
|
|
992
|
+
const set = await stepped(changing(locator, before, after, { room: true }));
|
|
993
|
+
revocations.push([locator, before, after, set]);
|
|
994
|
+
}
|
|
995
|
+
const changed = [];
|
|
996
|
+
for (const [locator, before, after, set] of revocations) {
|
|
997
|
+
const room = rooms.get(locator);
|
|
998
|
+
if (room === undefined || (replicas.get(locator) ?? null) !== before)
|
|
999
|
+
continue;
|
|
1000
|
+
if (after !== null)
|
|
1001
|
+
replicas.set(locator, after);
|
|
435
1002
|
rooms.set(locator, {
|
|
436
1003
|
...room,
|
|
437
1004
|
model: modelTypes(revoked),
|
|
@@ -439,7 +1006,7 @@ export function composeRuntime(options) {
|
|
|
439
1006
|
? null
|
|
440
1007
|
: { ...room.typedSelection, model: modelTypes(revoked) },
|
|
441
1008
|
});
|
|
442
|
-
changed.push(
|
|
1009
|
+
changed.push(set);
|
|
443
1010
|
}
|
|
444
1011
|
publishChangeSet(mergeChangeSets(changed));
|
|
445
1012
|
});
|
|
@@ -477,14 +1044,32 @@ export function composeRuntime(options) {
|
|
|
477
1044
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
478
1045
|
});
|
|
479
1046
|
}
|
|
480
|
-
async function applyDiscovery(
|
|
1047
|
+
async function applyDiscovery(found, started, covered) {
|
|
1048
|
+
const lockedBefore = lockedRooms();
|
|
1049
|
+
for (const room of found) {
|
|
1050
|
+
const fence = fences.get(room.locator);
|
|
1051
|
+
if (fence !== undefined && fence.erased && fence.at < started)
|
|
1052
|
+
fences.delete(room.locator);
|
|
1053
|
+
}
|
|
1054
|
+
await loseRooms(found
|
|
1055
|
+
.filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
|
|
1056
|
+
.map((room) => room.locator));
|
|
1057
|
+
const listed = found.filter((room) => !fences.has(room.locator));
|
|
481
1058
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
482
1059
|
for (const locator of listedLocators)
|
|
483
1060
|
scopedRefusals.delete(locator);
|
|
484
|
-
|
|
1061
|
+
if (covered === undefined)
|
|
1062
|
+
discovered.clear();
|
|
1063
|
+
else
|
|
1064
|
+
for (const locator of covered)
|
|
1065
|
+
discovered.delete(locator);
|
|
485
1066
|
for (const room of listed)
|
|
486
1067
|
discovered.set(room.locator, room);
|
|
487
|
-
|
|
1068
|
+
const unlisted = unlistedHeld(new Set(found.map((room) => room.locator))).filter((locator) => covered === undefined || covered.includes(locator));
|
|
1069
|
+
if (options.adapter.exhaustiveDiscovery === true)
|
|
1070
|
+
await loseRooms(unlisted);
|
|
1071
|
+
else
|
|
1072
|
+
unlistedKept(unlisted);
|
|
488
1073
|
for (const room of listed) {
|
|
489
1074
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
490
1075
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -493,8 +1078,17 @@ export function composeRuntime(options) {
|
|
|
493
1078
|
}
|
|
494
1079
|
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
495
1080
|
}
|
|
1081
|
+
confirmLocks(listed);
|
|
496
1082
|
roomsListed = true;
|
|
497
1083
|
emitRooms();
|
|
1084
|
+
relocked(lockedBefore);
|
|
1085
|
+
return listed;
|
|
1086
|
+
}
|
|
1087
|
+
function heldLocally(locator) {
|
|
1088
|
+
return (replicas.has(locator) ||
|
|
1089
|
+
roomDrafts.has(locator) ||
|
|
1090
|
+
transfers.has(locator) ||
|
|
1091
|
+
queuesWithoutReplica.has(locator));
|
|
498
1092
|
}
|
|
499
1093
|
function unlistedHeld(listed) {
|
|
500
1094
|
return [
|
|
@@ -506,40 +1100,85 @@ export function composeRuntime(options) {
|
|
|
506
1100
|
]),
|
|
507
1101
|
].filter((locator) => !listed.has(locator));
|
|
508
1102
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
|
|
516
|
-
room: true,
|
|
517
|
-
queue: true,
|
|
518
|
-
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
519
|
-
}));
|
|
520
|
-
for (const locator of unlisted) {
|
|
521
|
-
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
522
|
-
pendings.delete(batch.id);
|
|
523
|
-
replicas.delete(locator);
|
|
524
|
-
queuesWithoutReplica.delete(locator);
|
|
1103
|
+
function unlistedKept(unlisted) {
|
|
1104
|
+
for (const locator of unlisted) {
|
|
1105
|
+
const held = replicas.get(locator)?.confirmed.record;
|
|
1106
|
+
if (held !== undefined)
|
|
1107
|
+
list(held, false);
|
|
1108
|
+
else
|
|
525
1109
|
rooms.delete(locator);
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
function fence(locators) {
|
|
1113
|
+
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
1114
|
+
const removed = fresh.map((locator) => ({
|
|
1115
|
+
locator,
|
|
1116
|
+
shown: prepared.has(locator) || projecting.has(locator) ? (replicas.get(locator) ?? null) : null,
|
|
1117
|
+
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
1118
|
+
}));
|
|
1119
|
+
for (const locator of fresh) {
|
|
1120
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
1121
|
+
lastFenced.set(locator, ++fencings);
|
|
1122
|
+
confirmed.delete(locator);
|
|
1123
|
+
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
1124
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
1125
|
+
replicas.delete(locator);
|
|
1126
|
+
queuesWithoutReplica.delete(locator);
|
|
1127
|
+
rooms.delete(locator);
|
|
1128
|
+
roomDrafts.delete(locator);
|
|
1129
|
+
transfers.delete(locator);
|
|
1130
|
+
catching.delete(locator);
|
|
1131
|
+
discovered.delete(locator);
|
|
1132
|
+
announced.delete(locator);
|
|
1133
|
+
unreadable.delete(locator);
|
|
1134
|
+
stale.delete(locator);
|
|
1135
|
+
repairable.delete(locator);
|
|
1136
|
+
unindexable.delete(locator);
|
|
1137
|
+
paused.delete(locator);
|
|
1138
|
+
prepared.delete(locator);
|
|
1139
|
+
scopedRefusals.delete(locator);
|
|
1140
|
+
sourceRefusals.delete(locator);
|
|
1141
|
+
declarationRefusals.delete(locator);
|
|
1142
|
+
for (const attached of ['collection', 'typed-selection']) {
|
|
1143
|
+
const key = modelKey(locator, attached);
|
|
1144
|
+
models.delete(key);
|
|
1145
|
+
liveModels.delete(key);
|
|
1146
|
+
invalidModels.delete(key);
|
|
540
1147
|
}
|
|
541
|
-
|
|
542
|
-
|
|
1148
|
+
journal.record(`room lost ${locator}`);
|
|
1149
|
+
}
|
|
1150
|
+
if (fresh.length === 0)
|
|
1151
|
+
return fresh;
|
|
1152
|
+
searchIndex.forget(fresh);
|
|
1153
|
+
const withdrawn = readSide.apply(stepped(withdrawing(removed)), (set) => conversations.changed(set));
|
|
1154
|
+
if (withdrawn instanceof Promise)
|
|
1155
|
+
void withdrawn.catch(unpublished);
|
|
1156
|
+
publishChangeSet(mergeChangeSets(removed.map(({ locator, drafts }) => changeSet(locator, null, null, { room: true, queue: true, drafts }))));
|
|
1157
|
+
readSide.recover();
|
|
1158
|
+
moveTo({ ...status, ...attention() });
|
|
1159
|
+
return fresh;
|
|
1160
|
+
}
|
|
1161
|
+
function* withdrawing(removed) {
|
|
1162
|
+
const sets = [];
|
|
1163
|
+
for (const { locator, shown } of removed)
|
|
1164
|
+
sets.push(yield* changing(locator, shown, null));
|
|
1165
|
+
return mergeChangeSets(sets);
|
|
1166
|
+
}
|
|
1167
|
+
async function erased(local, locators) {
|
|
1168
|
+
await local.erase(locators);
|
|
1169
|
+
for (const locator of locators) {
|
|
1170
|
+
const held = fences.get(locator);
|
|
1171
|
+
if (held !== undefined)
|
|
1172
|
+
held.erased = true;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
async function loseRooms(locators) {
|
|
1176
|
+
const fresh = fence(locators);
|
|
1177
|
+
if (fresh.length === 0)
|
|
1178
|
+
return;
|
|
1179
|
+
await durably(async (local) => {
|
|
1180
|
+
await erased(local, fresh);
|
|
1181
|
+
await unpair(local);
|
|
543
1182
|
});
|
|
544
1183
|
}
|
|
545
1184
|
function restored(key, persisted) {
|
|
@@ -561,7 +1200,7 @@ export function composeRuntime(options) {
|
|
|
561
1200
|
return model;
|
|
562
1201
|
}
|
|
563
1202
|
async function hydrate(opened) {
|
|
564
|
-
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
|
|
1203
|
+
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
|
|
565
1204
|
opened.all(),
|
|
566
1205
|
opened.queues(),
|
|
567
1206
|
opened.roomDrafts(),
|
|
@@ -573,7 +1212,25 @@ export function composeRuntime(options) {
|
|
|
573
1212
|
opened.capabilities(),
|
|
574
1213
|
opened.member(),
|
|
575
1214
|
opened.transfers(),
|
|
1215
|
+
opened.targets(),
|
|
576
1216
|
]);
|
|
1217
|
+
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1218
|
+
const previousReplicas = new Map(replicas);
|
|
1219
|
+
const read = new Map();
|
|
1220
|
+
for (const record of records) {
|
|
1221
|
+
const model = restored(modelKey(record.locator, 'collection'), record.model);
|
|
1222
|
+
const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
|
|
1223
|
+
const prior = previousReplicas.get(record.locator);
|
|
1224
|
+
const confirmed = await confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
|
|
1225
|
+
const replica = await stepped(replicating(confirmed, queuedBatches.get(record.locator) ?? [], prior));
|
|
1226
|
+
await stepped(counting(replica));
|
|
1227
|
+
read.set(record.locator, replica);
|
|
1228
|
+
}
|
|
1229
|
+
const sets = new Map();
|
|
1230
|
+
for (const locator of new Set([...previousReplicas.keys(), ...read.keys()])) {
|
|
1231
|
+
sets.set(locator, await stepped(changing(locator, previousReplicas.get(locator) ?? null, read.get(locator) ?? null)));
|
|
1232
|
+
}
|
|
1233
|
+
targets = heldTargets;
|
|
577
1234
|
transfers.clear();
|
|
578
1235
|
for (const record of staged)
|
|
579
1236
|
transfers.set(record.locator, record);
|
|
@@ -587,26 +1244,24 @@ export function composeRuntime(options) {
|
|
|
587
1244
|
for (const record of presented)
|
|
588
1245
|
presentations.set(record.app, record.value);
|
|
589
1246
|
conversations.hydrate(held, chatDrafts);
|
|
590
|
-
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
591
1247
|
pendings.clear();
|
|
592
1248
|
for (const queue of queues) {
|
|
593
|
-
for (const { batch, state } of queue.pending)
|
|
594
|
-
pendings.set(batch.id, state);
|
|
1249
|
+
for (const { batch, state } of queue.pending) {
|
|
1250
|
+
pendings.set(pendingKey(queue.locator, batch.id), state);
|
|
1251
|
+
}
|
|
595
1252
|
}
|
|
596
|
-
const previousReplicas = new Map(replicas);
|
|
597
1253
|
replicas.clear();
|
|
598
1254
|
queuesWithoutReplica.clear();
|
|
599
|
-
const replicated = new Set(records.map((record) => record.locator));
|
|
600
1255
|
for (const [locator, queue] of queuedBatches) {
|
|
601
|
-
if (!
|
|
1256
|
+
if (!read.has(locator))
|
|
602
1257
|
queuesWithoutReplica.set(locator, queue);
|
|
603
1258
|
}
|
|
604
1259
|
for (const record of records) {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
const
|
|
608
|
-
const
|
|
609
|
-
replicas.set(record.locator,
|
|
1260
|
+
if (fences.has(record.locator))
|
|
1261
|
+
continue;
|
|
1262
|
+
const replica = read.get(record.locator);
|
|
1263
|
+
const { model, selectionModel } = replica.confirmed.record;
|
|
1264
|
+
replicas.set(record.locator, replica);
|
|
610
1265
|
const existing = rooms.get(record.locator);
|
|
611
1266
|
if (existing === undefined)
|
|
612
1267
|
list(record, false);
|
|
@@ -629,12 +1284,13 @@ export function composeRuntime(options) {
|
|
|
629
1284
|
return {
|
|
630
1285
|
lastCompleted,
|
|
631
1286
|
locators: new Set([...queuedBatches.keys(), ...replicas.keys()]),
|
|
632
|
-
read: mergeChangeSets([...
|
|
1287
|
+
read: mergeChangeSets([...sets].flatMap(([locator, set]) => (fences.has(locator) ? [] : [set]))),
|
|
633
1288
|
};
|
|
634
1289
|
}
|
|
635
1290
|
async function readState(opened) {
|
|
636
1291
|
const { lastCompleted, locators, read } = await hydrate(opened);
|
|
637
1292
|
applyChangeSet(read);
|
|
1293
|
+
prepare();
|
|
638
1294
|
if (replicas.size > 0)
|
|
639
1295
|
emitRooms();
|
|
640
1296
|
if (member !== null)
|
|
@@ -645,8 +1301,15 @@ export function composeRuntime(options) {
|
|
|
645
1301
|
return lastCompleted;
|
|
646
1302
|
}
|
|
647
1303
|
async function open() {
|
|
648
|
-
|
|
1304
|
+
if (closed)
|
|
1305
|
+
throw new Error('runtime closed');
|
|
1306
|
+
const opened = await openRuntimeStore(isFenced, options.database);
|
|
649
1307
|
store = opened;
|
|
1308
|
+
const interrupted = await opened.fences();
|
|
1309
|
+
for (const locator of interrupted) {
|
|
1310
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
1311
|
+
lastFenced.set(locator, ++fencings);
|
|
1312
|
+
}
|
|
650
1313
|
const kept = options.shell.generation;
|
|
651
1314
|
const storedGeneration = await opened.generation();
|
|
652
1315
|
const loss = detectStorageLoss(storedGeneration, kept);
|
|
@@ -660,7 +1323,12 @@ export function composeRuntime(options) {
|
|
|
660
1323
|
emitStorageLoss();
|
|
661
1324
|
}
|
|
662
1325
|
const lastCompleted = await readState(opened);
|
|
1326
|
+
if (interrupted.length > 0)
|
|
1327
|
+
await erased(opened, interrupted);
|
|
663
1328
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
1329
|
+
void exclusive(() => unpair(opened)).catch(() => {
|
|
1330
|
+
journal.record('unpair failed');
|
|
1331
|
+
});
|
|
664
1332
|
return opened;
|
|
665
1333
|
}
|
|
666
1334
|
function durably(work) {
|
|
@@ -681,27 +1349,92 @@ export function composeRuntime(options) {
|
|
|
681
1349
|
mutations = next.then(() => undefined, () => undefined);
|
|
682
1350
|
return next;
|
|
683
1351
|
}
|
|
684
|
-
function withdrawable(
|
|
685
|
-
|
|
686
|
-
|
|
1352
|
+
function withdrawable(locator) {
|
|
1353
|
+
return (batch) => {
|
|
1354
|
+
const state = pendings.get(pendingKey(locator, batch.id));
|
|
1355
|
+
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
function landedWhole(locator, batch, confirmed, current) {
|
|
1359
|
+
if (!landed(batch, confirmed, current))
|
|
1360
|
+
return false;
|
|
1361
|
+
const partner = partnerOf(batch, locator);
|
|
1362
|
+
if (partner === null)
|
|
1363
|
+
return true;
|
|
1364
|
+
const other = replicas.get(partner);
|
|
1365
|
+
const half = other?.queue.find((one) => one.id === batch.id);
|
|
1366
|
+
return other !== undefined && half !== undefined && landed(half, other.confirmed);
|
|
1367
|
+
}
|
|
1368
|
+
function carriedBack(locator, unlanded, written) {
|
|
1369
|
+
const back = new Map();
|
|
1370
|
+
const taken = [];
|
|
1371
|
+
for (const batch of unlanded) {
|
|
1372
|
+
const between = batch.between;
|
|
1373
|
+
if (between === undefined)
|
|
1374
|
+
continue;
|
|
1375
|
+
const source = between.source.room;
|
|
1376
|
+
const destination = between.destination.room;
|
|
1377
|
+
for (const { path: to } of pendings.get(pendingKey(destination, batch.id))?.drafts ?? []) {
|
|
1378
|
+
const record = roomDrafts.get(destination)?.get(to);
|
|
1379
|
+
const path = between.moves.find((move) => move.to === to)?.path;
|
|
1380
|
+
if (record === undefined || path === undefined)
|
|
1381
|
+
continue;
|
|
1382
|
+
const [into, key] = source === locator ? [written, path] : [back, documentKey(source, path)];
|
|
1383
|
+
const existing = into.get(key) ?? roomDrafts.get(source)?.get(path);
|
|
1384
|
+
taken.push({ locator: destination, path: to });
|
|
1385
|
+
into.set(key, {
|
|
1386
|
+
locator: source,
|
|
1387
|
+
path,
|
|
1388
|
+
revision: roomDraftRevision(),
|
|
1389
|
+
parts: returnedInto(existing?.parts, record.parts),
|
|
1390
|
+
returned: existing?.returned ?? null,
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
const records = [...back.values()];
|
|
1395
|
+
const elsewhere = taken.filter((one) => one.locator !== locator);
|
|
1396
|
+
const rooms = new Set([...records, ...elsewhere].map((one) => one.locator));
|
|
1397
|
+
return {
|
|
1398
|
+
taken: taken.filter((one) => one.locator === locator).map(({ path }) => path),
|
|
1399
|
+
writes: [...rooms].map((room) => ({
|
|
1400
|
+
locator: room,
|
|
1401
|
+
roomDrafts: records.filter((record) => record.locator === room),
|
|
1402
|
+
consumedRoomDrafts: elsewhere.filter((one) => one.locator === room).map(({ path }) => path),
|
|
1403
|
+
})),
|
|
1404
|
+
changes: [...rooms].map((room) => changeSet(room, null, null, {
|
|
1405
|
+
drafts: [...records, ...elsewhere]
|
|
1406
|
+
.filter((one) => one.locator === room)
|
|
1407
|
+
.map(({ path }) => path),
|
|
1408
|
+
})),
|
|
1409
|
+
remember() {
|
|
1410
|
+
for (const one of taken)
|
|
1411
|
+
forgetRoomDraft(one.locator, one.path);
|
|
1412
|
+
for (const record of records)
|
|
1413
|
+
rememberRoomDraft(record);
|
|
1414
|
+
},
|
|
1415
|
+
};
|
|
687
1416
|
}
|
|
688
1417
|
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
689
1418
|
const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
690
|
-
const withdrawn =
|
|
691
|
-
const
|
|
1419
|
+
const { withdrawn, kept } = await stepped(standing(confirmed, queue, withdrawable(locator), localSaves));
|
|
1420
|
+
const founded = kept.filter((batch) => !queue.includes(batch));
|
|
1421
|
+
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
692
1422
|
const unqueued = withdrawn.map((batch) => ({
|
|
693
1423
|
batch,
|
|
694
|
-
|
|
1424
|
+
landed: landedWhole(locator, batch, confirmed),
|
|
695
1425
|
}));
|
|
696
1426
|
const returning = [
|
|
697
|
-
...[...unqueued].reverse(),
|
|
1427
|
+
...[...unqueued].reverse().map(({ batch, landed }) => ({
|
|
1428
|
+
batch,
|
|
1429
|
+
returned: localSaves || landed ? null : returnedBy('withdrawn'),
|
|
1430
|
+
})),
|
|
698
1431
|
...(settling.leaving === undefined ? [] : [settling.leaving]),
|
|
699
1432
|
];
|
|
700
1433
|
const written = new Map();
|
|
701
1434
|
for (const { batch, returned } of returning) {
|
|
702
|
-
if (returned === null)
|
|
1435
|
+
if (returned === null || batch.between?.destination.room === locator)
|
|
703
1436
|
continue;
|
|
704
|
-
for (const saved of pendings.get(batch.id)?.drafts ?? []) {
|
|
1437
|
+
for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
|
|
705
1438
|
const existing = written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path);
|
|
706
1439
|
written.set(saved.path, {
|
|
707
1440
|
locator,
|
|
@@ -712,13 +1445,28 @@ export function composeRuntime(options) {
|
|
|
712
1445
|
});
|
|
713
1446
|
}
|
|
714
1447
|
}
|
|
715
|
-
|
|
1448
|
+
const carried = carriedBack(locator, [
|
|
1449
|
+
...[...unqueued].reverse().flatMap(({ batch, landed }) => (landed ? [] : [batch])),
|
|
1450
|
+
...(settling.leaving === undefined || settling.leaving.returned === null
|
|
1451
|
+
? []
|
|
1452
|
+
: [settling.leaving.batch]),
|
|
1453
|
+
], written);
|
|
1454
|
+
await local.keepAuthored({
|
|
1455
|
+
locator,
|
|
1456
|
+
queue: kept,
|
|
1457
|
+
founded,
|
|
1458
|
+
roomDrafts: [...written.values()],
|
|
1459
|
+
consumedRoomDrafts: carried.taken,
|
|
1460
|
+
}, ...carried.writes);
|
|
1461
|
+
const staying = new Set(kept.map((batch) => batch.id));
|
|
716
1462
|
for (const batch of previousQueue) {
|
|
717
|
-
if (!
|
|
718
|
-
pendings.delete(batch.id);
|
|
1463
|
+
if (!staying.has(batch.id))
|
|
1464
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
719
1465
|
}
|
|
720
1466
|
for (const record of written.values())
|
|
721
1467
|
rememberRoomDraft(record);
|
|
1468
|
+
carried.remember();
|
|
1469
|
+
const drafts = [...written.keys(), ...carried.taken];
|
|
722
1470
|
try {
|
|
723
1471
|
await local.write({
|
|
724
1472
|
next: confirmed.record,
|
|
@@ -727,31 +1475,38 @@ export function composeRuntime(options) {
|
|
|
727
1475
|
});
|
|
728
1476
|
}
|
|
729
1477
|
catch (error) {
|
|
730
|
-
const held = before === undefined ? null :
|
|
1478
|
+
const held = before === undefined ? null : await stepped(replicating(before.confirmed, kept, before));
|
|
1479
|
+
const set = await stepped(changing(locator, before ?? null, held, { queue: queueChanged, drafts }));
|
|
731
1480
|
if (held !== null)
|
|
732
1481
|
replicas.set(locator, held);
|
|
733
1482
|
else if (kept.length > 0)
|
|
734
1483
|
queuesWithoutReplica.set(locator, kept);
|
|
735
1484
|
else
|
|
736
1485
|
queuesWithoutReplica.delete(locator);
|
|
737
|
-
publishChangeSet(
|
|
738
|
-
queue: kept.length !== previousQueue.length,
|
|
739
|
-
drafts: [...written.keys()],
|
|
740
|
-
}));
|
|
1486
|
+
publishChangeSet(mergeChangeSets([set, ...carried.changes]));
|
|
741
1487
|
moveTo({ ...status, ...attention() });
|
|
742
1488
|
throw error;
|
|
743
1489
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
1490
|
+
const changed = { room: settling.room, queue: queueChanged, drafts };
|
|
1491
|
+
const next = await stepped(replicating(confirmed, kept, before));
|
|
1492
|
+
await stepped(counting(next));
|
|
1493
|
+
const set = await stepped(changing(locator, before ?? null, next, changed));
|
|
1494
|
+
return () => {
|
|
1495
|
+
queuesWithoutReplica.delete(locator);
|
|
1496
|
+
for (const { batch, landed } of unqueued) {
|
|
1497
|
+
const outcome = localSaves ? 'dropped' : landed ? 'landed' : 'withdrawn';
|
|
1498
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
1499
|
+
}
|
|
1500
|
+
replicas.set(locator, next);
|
|
1501
|
+
return mergeChangeSets([
|
|
1502
|
+
heldChange(locator, before ?? null, next, set, changed),
|
|
1503
|
+
...carried.changes,
|
|
1504
|
+
]);
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
function heldChange(locator, before, next, set, changed) {
|
|
1508
|
+
const held = replicas.get(locator) ?? null;
|
|
1509
|
+
return held === next ? set : changeSet(locator, before, held, changed);
|
|
755
1510
|
}
|
|
756
1511
|
function adoptModels(locator, record) {
|
|
757
1512
|
for (const [attached, model] of [
|
|
@@ -771,7 +1526,7 @@ export function composeRuntime(options) {
|
|
|
771
1526
|
const taken = (answer, previous, attached) => {
|
|
772
1527
|
if (answer === undefined)
|
|
773
1528
|
return previous;
|
|
774
|
-
const { state, refused } = attachedModel(answer, previous);
|
|
1529
|
+
const { state, refused } = attachedModel(answer, previous, attached === 'collection' ? roomTypes : NO_ROOM_TYPES);
|
|
775
1530
|
if (refused !== null) {
|
|
776
1531
|
const where = refused.location === undefined ? '' : ` ${refused.location}`;
|
|
777
1532
|
journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
|
|
@@ -784,11 +1539,16 @@ export function composeRuntime(options) {
|
|
|
784
1539
|
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
785
1540
|
};
|
|
786
1541
|
}
|
|
787
|
-
function publish(local, room, confirmed, landing
|
|
1542
|
+
function publish(local, room, confirmed, landing, live) {
|
|
788
1543
|
const { base, stored } = landing;
|
|
789
1544
|
return exclusive(async () => {
|
|
790
|
-
if (
|
|
1545
|
+
if (stored !== undefined) {
|
|
1546
|
+
await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
|
|
1547
|
+
}
|
|
1548
|
+
if (!live()) {
|
|
1549
|
+
caughtUpTo(room.locator);
|
|
791
1550
|
return true;
|
|
1551
|
+
}
|
|
792
1552
|
const prior = replicas.get(room.locator);
|
|
793
1553
|
if (base !== undefined &&
|
|
794
1554
|
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
@@ -799,32 +1559,58 @@ export function composeRuntime(options) {
|
|
|
799
1559
|
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
800
1560
|
const roomVersion = confirmed.record.roomVersion;
|
|
801
1561
|
const accepted = queue.filter((batch) => {
|
|
802
|
-
const accepted = pendings.get(batch.id)?.accepted;
|
|
1562
|
+
const accepted = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
|
|
803
1563
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
804
1564
|
});
|
|
805
|
-
|
|
806
|
-
|
|
1565
|
+
let settled;
|
|
1566
|
+
try {
|
|
1567
|
+
settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
|
|
1568
|
+
}
|
|
1569
|
+
catch (error) {
|
|
1570
|
+
if (!exceeded(error))
|
|
1571
|
+
throw error;
|
|
1572
|
+
await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
|
|
1573
|
+
caughtUpTo(room.locator);
|
|
1574
|
+
return true;
|
|
1575
|
+
}
|
|
1576
|
+
const set = settled();
|
|
1577
|
+
if (stored !== undefined) {
|
|
807
1578
|
transfers.delete(room.locator);
|
|
1579
|
+
catching.delete(room.locator);
|
|
1580
|
+
}
|
|
808
1581
|
for (const batch of accepted)
|
|
809
|
-
pendings.delete(batch.id);
|
|
1582
|
+
pendings.delete(pendingKey(room.locator, batch.id));
|
|
810
1583
|
stale.delete(room.locator);
|
|
1584
|
+
unindexable.delete(room.locator);
|
|
811
1585
|
if (unreadable.delete(room.locator))
|
|
812
1586
|
scopedRefusals.delete(room.locator);
|
|
813
1587
|
sourceRefusals.delete(room.locator);
|
|
814
1588
|
declarationRefusals.delete(room.locator);
|
|
815
1589
|
list(room, true);
|
|
1590
|
+
if (prior === undefined)
|
|
1591
|
+
prepared.add(room.locator);
|
|
816
1592
|
publishChangeSet(set);
|
|
1593
|
+
await unpair(local);
|
|
817
1594
|
moveTo({ ...status, ...attention() });
|
|
1595
|
+
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
1596
|
+
if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1597
|
+
submitLater(room.locator);
|
|
1598
|
+
}
|
|
818
1599
|
return true;
|
|
819
1600
|
});
|
|
820
1601
|
}
|
|
821
|
-
async function
|
|
1602
|
+
async function savedRoom(locator) {
|
|
822
1603
|
const local = await openLocal();
|
|
1604
|
+
await referenced(locator);
|
|
823
1605
|
const replica = replicas.get(locator);
|
|
824
1606
|
const room = rooms.get(locator);
|
|
825
1607
|
if (replica === undefined || room === undefined) {
|
|
826
1608
|
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
827
1609
|
}
|
|
1610
|
+
return { local, replica, room };
|
|
1611
|
+
}
|
|
1612
|
+
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
1613
|
+
const target = await savedRoom(locator);
|
|
828
1614
|
const saved = [];
|
|
829
1615
|
for (const { path, revision } of named) {
|
|
830
1616
|
const record = roomDrafts.get(locator)?.get(path);
|
|
@@ -834,12 +1620,180 @@ export function composeRuntime(options) {
|
|
|
834
1620
|
if (!saved.some((draft) => draft.path === path))
|
|
835
1621
|
saved.push({ path, parts: record.parts });
|
|
836
1622
|
}
|
|
1623
|
+
return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
|
|
1624
|
+
}
|
|
1625
|
+
async function saveParts(locator, named, writtenBy) {
|
|
1626
|
+
const read = named.map(({ path, parts }) => ({ path, parts: readParts(parts) }));
|
|
1627
|
+
if (read.some(({ path }, at) => path === '' ||
|
|
1628
|
+
normalize(path) !== path ||
|
|
1629
|
+
read.findIndex((one) => one.path === path) !== at)) {
|
|
1630
|
+
throw runtimeError('room-draft-malformed');
|
|
1631
|
+
}
|
|
1632
|
+
const target = await savedRoom(locator);
|
|
1633
|
+
const saved = [];
|
|
1634
|
+
const files = [];
|
|
1635
|
+
for (const { path, parts } of read) {
|
|
1636
|
+
const file = parts.file === undefined
|
|
1637
|
+
? undefined
|
|
1638
|
+
: await heldFile(parts.file.blob, binaryBase(target.replica.visible, path));
|
|
1639
|
+
if (parts.file !== undefined && file !== undefined) {
|
|
1640
|
+
files.push({ key: file.localId, blob: parts.file.blob });
|
|
1641
|
+
}
|
|
1642
|
+
saved.push({ path, parts: held(undefined, parts, file) });
|
|
1643
|
+
}
|
|
1644
|
+
return committed(locator, target, saved, new Set(), writtenBy, files);
|
|
1645
|
+
}
|
|
1646
|
+
function carriedDrafts(locator, saved, consumed) {
|
|
1647
|
+
const holding = roomDrafts.get(locator);
|
|
1648
|
+
return new Map(saved.flatMap(({ path, parts }) => {
|
|
1649
|
+
const record = holding?.get(path);
|
|
1650
|
+
if (parts.move === undefined || record === undefined || consumed.has(path))
|
|
1651
|
+
return [];
|
|
1652
|
+
const to = moveTarget(parts.move);
|
|
1653
|
+
if (to === path || holding?.has(to) === true)
|
|
1654
|
+
return [];
|
|
1655
|
+
return [[path, { ...record, path: to, revision: roomDraftRevision() }]];
|
|
1656
|
+
}));
|
|
1657
|
+
}
|
|
1658
|
+
function sideOf(locator, replica, room) {
|
|
1659
|
+
const record = replica.confirmed.record;
|
|
1660
|
+
return {
|
|
1661
|
+
room: locator,
|
|
1662
|
+
handle: room.handle,
|
|
1663
|
+
baseRoomVersion: record.roomVersion,
|
|
1664
|
+
composition: compositionFor(record),
|
|
1665
|
+
operations: room.operations,
|
|
1666
|
+
providerOwnedPaths: room.providerOwnedPaths,
|
|
1667
|
+
visible: replica.visible,
|
|
1668
|
+
queued: queuedDocuments(replica.queue),
|
|
1669
|
+
clean: {
|
|
1670
|
+
text: record.documents,
|
|
1671
|
+
binaries: record.binaries,
|
|
1672
|
+
composition: compositionOf(record),
|
|
1673
|
+
},
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
function movedInto(locator, saved) {
|
|
1677
|
+
const into = saved.find(({ parts }) => parts.move?.room !== undefined && parts.move.room !== locator);
|
|
1678
|
+
return into?.parts.move?.room ?? null;
|
|
1679
|
+
}
|
|
1680
|
+
async function committedMove(locator, destination, { local, replica, room }, saved, consumed, writtenBy, files) {
|
|
1681
|
+
const mixed = saved.find(({ parts }) => Object.keys(parts).length !== 1 || parts.move?.room !== destination);
|
|
1682
|
+
if (mixed !== undefined) {
|
|
1683
|
+
return { outcome: 'refused', code: 'mixed-room-move', retryable: false, file: mixed.path };
|
|
1684
|
+
}
|
|
1685
|
+
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
|
|
1686
|
+
if (changed.length > 0)
|
|
1687
|
+
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
1688
|
+
await referenced(destination);
|
|
1689
|
+
const target = replicas.get(destination);
|
|
1690
|
+
const listedRoom = rooms.get(destination);
|
|
1691
|
+
if (target === undefined || listedRoom === undefined) {
|
|
1692
|
+
throw runtimeError('room-not-held', `No Local Replica for ${destination}`);
|
|
1693
|
+
}
|
|
1694
|
+
const built = await buildMove({
|
|
1695
|
+
source: sideOf(locator, replica, room),
|
|
1696
|
+
destination: sideOf(destination, target, listedRoom),
|
|
1697
|
+
moves: saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) })),
|
|
1698
|
+
providerLimits: limits,
|
|
1699
|
+
local: localSaves,
|
|
1700
|
+
...(writtenBy === undefined ? {} : { writtenBy }),
|
|
1701
|
+
});
|
|
1702
|
+
if (built.kind === 'refused')
|
|
1703
|
+
return { outcome: 'refused', ...built.refusal };
|
|
1704
|
+
const id = built.leaving.id;
|
|
1705
|
+
const carrying = saved.flatMap(({ path, parts }) => {
|
|
1706
|
+
const record = roomDrafts.get(locator)?.get(path);
|
|
1707
|
+
const to = moveTarget(parts.move);
|
|
1708
|
+
if (record === undefined || consumed.has(path) || roomDrafts.get(destination)?.has(to)) {
|
|
1709
|
+
return [];
|
|
1710
|
+
}
|
|
1711
|
+
const moved = {
|
|
1712
|
+
...record,
|
|
1713
|
+
locator: destination,
|
|
1714
|
+
path: to,
|
|
1715
|
+
revision: roomDraftRevision(),
|
|
1716
|
+
};
|
|
1717
|
+
return [{ from: path, record: moved }];
|
|
1718
|
+
});
|
|
1719
|
+
const carried = carrying.map(({ record }) => record);
|
|
1720
|
+
const marked = carried.map(({ path }) => ({ path, parts: {} }));
|
|
1721
|
+
const left = [...consumed, ...carrying.map(({ from }) => from)];
|
|
1722
|
+
const leavingQueue = [...replica.queue, built.leaving];
|
|
1723
|
+
const arrivingQueue = [...target.queue, built.arriving];
|
|
1724
|
+
let fencedRoom;
|
|
1725
|
+
try {
|
|
1726
|
+
fencedRoom = await local.saveAuthored(files, {
|
|
1727
|
+
locator,
|
|
1728
|
+
queue: leavingQueue,
|
|
1729
|
+
saved: new Map([[id, saved]]),
|
|
1730
|
+
consumedRoomDrafts: left,
|
|
1731
|
+
}, {
|
|
1732
|
+
locator: destination,
|
|
1733
|
+
queue: arrivingQueue,
|
|
1734
|
+
saved: new Map([[id, marked]]),
|
|
1735
|
+
roomDrafts: carried,
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
catch {
|
|
1739
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
1740
|
+
}
|
|
1741
|
+
if (fencedRoom !== null) {
|
|
1742
|
+
throw runtimeError('room-not-held', `No Local Replica for ${fencedRoom}`);
|
|
1743
|
+
}
|
|
1744
|
+
const nextSource = await stepped(replicating(replica.confirmed, leavingQueue, replica));
|
|
1745
|
+
const nextDestination = await stepped(replicating(target.confirmed, arrivingQueue, target));
|
|
1746
|
+
const sets = [
|
|
1747
|
+
await stepped(changing(locator, replica, nextSource, { queue: true, drafts: left })),
|
|
1748
|
+
await stepped(changing(destination, target, nextDestination, {
|
|
1749
|
+
queue: true,
|
|
1750
|
+
drafts: carried.map(({ path }) => path),
|
|
1751
|
+
})),
|
|
1752
|
+
];
|
|
1753
|
+
if (fences.has(locator) || fences.has(destination)) {
|
|
1754
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1755
|
+
}
|
|
1756
|
+
pendings.set(pendingKey(locator, id), {
|
|
1757
|
+
drafts: saved,
|
|
1758
|
+
submission: { state: 'waiting' },
|
|
1759
|
+
lost: false,
|
|
1760
|
+
});
|
|
1761
|
+
pendings.set(pendingKey(destination, id), {
|
|
1762
|
+
drafts: marked,
|
|
1763
|
+
submission: { state: 'waiting' },
|
|
1764
|
+
lost: false,
|
|
1765
|
+
});
|
|
1766
|
+
for (const path of left)
|
|
1767
|
+
forgetRoomDraft(locator, path);
|
|
1768
|
+
for (const record of carried)
|
|
1769
|
+
rememberRoomDraft(record);
|
|
1770
|
+
replicas.set(locator, nextSource);
|
|
1771
|
+
replicas.set(destination, nextDestination);
|
|
1772
|
+
publishChangeSet(mergeChangeSets(sets));
|
|
1773
|
+
queuedSave(locator);
|
|
1774
|
+
return { outcome: 'saved', identifier: id };
|
|
1775
|
+
}
|
|
1776
|
+
function queuedSave(locator) {
|
|
1777
|
+
if (!localSaves)
|
|
1778
|
+
recorded = false;
|
|
1779
|
+
moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
|
|
1780
|
+
submitLater(locator);
|
|
1781
|
+
}
|
|
1782
|
+
async function committed(locator, target, saved, consumed, writtenBy, files = []) {
|
|
1783
|
+
const into = movedInto(locator, saved);
|
|
1784
|
+
if (into !== null) {
|
|
1785
|
+
return committedMove(locator, into, target, saved, consumed, writtenBy, files);
|
|
1786
|
+
}
|
|
1787
|
+
const { local, replica, room } = target;
|
|
837
1788
|
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
|
|
838
1789
|
if (changed.length > 0)
|
|
839
1790
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
840
1791
|
const parts = saved.map(({ path, parts }) => changesOf(path, parts));
|
|
841
1792
|
const changes = parts.flatMap(({ changes }) => changes);
|
|
842
|
-
const
|
|
1793
|
+
const visibleOver = function* (queue) {
|
|
1794
|
+
return (yield* replicating(replica.confirmed, queue, replica)).visible;
|
|
1795
|
+
};
|
|
1796
|
+
const requeue = await stepped(requeuing(replica.queue, changes, replica.visible, withdrawable(locator), visibleOver));
|
|
843
1797
|
if (requeue.kind === 'refused')
|
|
844
1798
|
return { outcome: 'refused', ...requeue.refusal };
|
|
845
1799
|
const foreign = [...requeue.withdrawn.keys()].some((origin) => {
|
|
@@ -866,9 +1820,17 @@ export function composeRuntime(options) {
|
|
|
866
1820
|
visible: replica.visible,
|
|
867
1821
|
changes,
|
|
868
1822
|
bases: parts.flatMap(({ bases }) => bases),
|
|
1823
|
+
queued: queuedDocuments(replica.queue),
|
|
869
1824
|
writtenBy,
|
|
1825
|
+
clean: {
|
|
1826
|
+
text: replica.confirmed.record.documents,
|
|
1827
|
+
binaries: replica.confirmed.record.binaries,
|
|
1828
|
+
composition: compositionOf(replica.confirmed.record),
|
|
1829
|
+
},
|
|
870
1830
|
};
|
|
871
|
-
const checked = withdrawing
|
|
1831
|
+
const checked = withdrawing
|
|
1832
|
+
? await buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
|
|
1833
|
+
: null;
|
|
872
1834
|
if (checked?.kind === 'refused')
|
|
873
1835
|
return { outcome: 'refused', ...checked.refusal };
|
|
874
1836
|
const owners = [...requeue.owners].flatMap((origin) => {
|
|
@@ -884,13 +1846,15 @@ export function composeRuntime(options) {
|
|
|
884
1846
|
};
|
|
885
1847
|
}
|
|
886
1848
|
const built = !withdrawing
|
|
887
|
-
? buildBatch(input)
|
|
1849
|
+
? await buildBatch(input)
|
|
888
1850
|
: requeue.changes.length === 0
|
|
889
1851
|
? null
|
|
890
|
-
: buildBatch({
|
|
1852
|
+
: await buildBatch({
|
|
891
1853
|
...input,
|
|
892
|
-
visible:
|
|
1854
|
+
visible: (await stepped(replicating(replica.confirmed, requeue.queue, replica)))
|
|
1855
|
+
.visible,
|
|
893
1856
|
changes: requeue.changes,
|
|
1857
|
+
queued: queuedDocuments(requeue.queue),
|
|
894
1858
|
});
|
|
895
1859
|
if (built?.kind === 'refused')
|
|
896
1860
|
return { outcome: 'refused', ...built.refusal };
|
|
@@ -907,7 +1871,7 @@ export function composeRuntime(options) {
|
|
|
907
1871
|
for (const [origin, replaced] of withdrawn) {
|
|
908
1872
|
if (replaced === null)
|
|
909
1873
|
continue;
|
|
910
|
-
const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
|
|
1874
|
+
const carried = withdrawnDrafts(pendings.get(pendingKey(locator, origin))?.drafts ?? [], changes);
|
|
911
1875
|
drafted.set(replaced.id, replaced === join?.batch
|
|
912
1876
|
? [
|
|
913
1877
|
...carried.map((held) => joinedDraft(held, saved)),
|
|
@@ -921,62 +1885,84 @@ export function composeRuntime(options) {
|
|
|
921
1885
|
for (const replaced of withdrawn.values()) {
|
|
922
1886
|
if (replaced === null || !batches.includes(replaced))
|
|
923
1887
|
continue;
|
|
924
|
-
const limited =
|
|
1888
|
+
const limited = await stepped(limiting(batches, replaced, limits, visibleOver));
|
|
925
1889
|
if (limited !== null)
|
|
926
1890
|
return { outcome: 'refused', ...limited };
|
|
927
1891
|
}
|
|
928
|
-
const
|
|
1892
|
+
const carried = carriedDrafts(locator, saved, consumed);
|
|
1893
|
+
const left = [...consumed, ...carried.keys()];
|
|
1894
|
+
const arrived = [...carried.values()];
|
|
1895
|
+
let fencedRoom;
|
|
929
1896
|
try {
|
|
930
|
-
await local.
|
|
1897
|
+
fencedRoom = await local.saveAuthored(files, {
|
|
931
1898
|
locator,
|
|
932
1899
|
queue: batches,
|
|
933
1900
|
saved: drafted,
|
|
934
|
-
consumedRoomDrafts:
|
|
1901
|
+
consumedRoomDrafts: left,
|
|
1902
|
+
roomDrafts: arrived,
|
|
935
1903
|
});
|
|
936
1904
|
}
|
|
937
1905
|
catch {
|
|
938
1906
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
939
1907
|
}
|
|
1908
|
+
if (fencedRoom !== null) {
|
|
1909
|
+
throw runtimeError('room-not-held', `No Local Replica for ${fencedRoom}`);
|
|
1910
|
+
}
|
|
1911
|
+
const next = await stepped(replicating(replica.confirmed, batches, replica));
|
|
1912
|
+
const set = await stepped(changing(locator, replica, next, {
|
|
1913
|
+
queue: true,
|
|
1914
|
+
drafts: [...left, ...arrived.map(({ path }) => path)],
|
|
1915
|
+
}));
|
|
1916
|
+
if (fences.has(locator))
|
|
1917
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
940
1918
|
for (const origin of withdrawn.keys())
|
|
941
|
-
pendings.delete(origin);
|
|
1919
|
+
pendings.delete(pendingKey(locator, origin));
|
|
942
1920
|
for (const [id, drafts] of drafted) {
|
|
943
|
-
pendings.set(
|
|
1921
|
+
pendings.set(pendingKey(locator, id), {
|
|
1922
|
+
drafts,
|
|
1923
|
+
submission: { state: 'waiting' },
|
|
1924
|
+
lost: false,
|
|
1925
|
+
});
|
|
944
1926
|
}
|
|
945
|
-
|
|
946
|
-
recorded = false;
|
|
947
|
-
for (const path of consumed)
|
|
1927
|
+
for (const path of left)
|
|
948
1928
|
forgetRoomDraft(locator, path);
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
|
|
955
|
-
submitLater(locator);
|
|
1929
|
+
for (const record of arrived)
|
|
1930
|
+
rememberRoomDraft(record);
|
|
1931
|
+
replicas.set(locator, next);
|
|
1932
|
+
publishChangeSet(set);
|
|
1933
|
+
queuedSave(locator);
|
|
956
1934
|
const identifier = join?.batch.id ??
|
|
957
1935
|
built?.batch.id ??
|
|
958
1936
|
[...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
|
|
959
1937
|
return { outcome: 'saved', identifier };
|
|
960
1938
|
}
|
|
961
|
-
async function mark(local, locator,
|
|
962
|
-
const
|
|
963
|
-
|
|
964
|
-
state
|
|
965
|
-
(
|
|
966
|
-
|
|
1939
|
+
async function mark(local, locator, batch, submission, lost) {
|
|
1940
|
+
const partner = partnerOf(batch, locator);
|
|
1941
|
+
for (const held of partner === null ? [locator] : [locator, partner]) {
|
|
1942
|
+
const state = pendings.get(pendingKey(held, batch.id));
|
|
1943
|
+
if (!continuing() ||
|
|
1944
|
+
state === undefined ||
|
|
1945
|
+
(JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
|
|
1946
|
+
continue;
|
|
1947
|
+
}
|
|
1948
|
+
await local.submit(held, batch.id, submission, lost);
|
|
1949
|
+
if (fences.has(held))
|
|
1950
|
+
continue;
|
|
1951
|
+
pendings.set(pendingKey(held, batch.id), { ...state, submission, lost });
|
|
1952
|
+
const replica = replicas.get(held);
|
|
1953
|
+
if (replica !== undefined)
|
|
1954
|
+
publishChangeSet(changeSet(held, replica, replica, { queue: true }));
|
|
967
1955
|
}
|
|
968
|
-
await local.submit(locator, identifier, submission, lost);
|
|
969
|
-
pendings.set(identifier, { ...state, submission, lost });
|
|
970
|
-
const replica = replicas.get(locator);
|
|
971
|
-
if (replica !== undefined)
|
|
972
|
-
publishChangeSet(changeSet(locator, replica, replica, { queue: true }));
|
|
973
1956
|
moveTo({ ...status, ...attention() });
|
|
974
1957
|
}
|
|
975
1958
|
async function keepUpload(local, locator, identifier, upload) {
|
|
976
1959
|
if (!continuing())
|
|
977
1960
|
throw new Error('connection refused');
|
|
978
1961
|
await local.keepUpload(locator, identifier, upload);
|
|
979
|
-
|
|
1962
|
+
if (fences.has(locator))
|
|
1963
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1964
|
+
const key = pendingKey(locator, identifier);
|
|
1965
|
+
pendings.set(key, { ...pendings.get(key), upload });
|
|
980
1966
|
if (!continuing())
|
|
981
1967
|
throw new Error('connection refused');
|
|
982
1968
|
}
|
|
@@ -991,6 +1977,7 @@ export function composeRuntime(options) {
|
|
|
991
1977
|
}
|
|
992
1978
|
if (closed || authorization === undefined || refusal !== null)
|
|
993
1979
|
return UNCERTAIN;
|
|
1980
|
+
const since = fencings;
|
|
994
1981
|
const transfer = ['transfer', 'history', locator, path];
|
|
995
1982
|
if (query.revision !== undefined)
|
|
996
1983
|
transfer.push(query.revision);
|
|
@@ -1005,6 +1992,8 @@ export function composeRuntime(options) {
|
|
|
1005
1992
|
...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
|
|
1006
1993
|
});
|
|
1007
1994
|
journal.record(`outcome history ${result.kind}`);
|
|
1995
|
+
if (fencedSince(locator, since))
|
|
1996
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
1008
1997
|
if (result.kind === 'scoped-refusal') {
|
|
1009
1998
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
1010
1999
|
if (room !== undefined)
|
|
@@ -1019,15 +2008,61 @@ export function composeRuntime(options) {
|
|
|
1019
2008
|
return UNCERTAIN;
|
|
1020
2009
|
}
|
|
1021
2010
|
}
|
|
1022
|
-
function
|
|
2011
|
+
function located(room, path, destination) {
|
|
2012
|
+
const crossing = roomTarget(destination);
|
|
2013
|
+
if (crossing === null) {
|
|
2014
|
+
const named = target(path, destination);
|
|
2015
|
+
return named === null || !rooms.has(room) ? null : { room, path: named };
|
|
2016
|
+
}
|
|
2017
|
+
const held = [...rooms.values()].find((listed) => listed.handle === crossing.handle);
|
|
2018
|
+
return held === undefined ? null : { room: held.locator, path: crossing.path };
|
|
2019
|
+
}
|
|
2020
|
+
async function resolveReference(room, path, destination) {
|
|
2021
|
+
if (roomTarget(destination) === null && target(path, destination) === null) {
|
|
2022
|
+
return ABSENT_TARGET;
|
|
2023
|
+
}
|
|
2024
|
+
const at = located(room, path, destination);
|
|
2025
|
+
if (at === null)
|
|
2026
|
+
return UNREADABLE;
|
|
2027
|
+
if (locked(at.room))
|
|
2028
|
+
return LOCKED_UNREADABLE;
|
|
2029
|
+
const visible = replicas.get(at.room)?.visible;
|
|
2030
|
+
if (visible?.documents[at.path] !== undefined || visible?.binaries[at.path] !== undefined) {
|
|
2031
|
+
return { kind: 'document', ...at };
|
|
2032
|
+
}
|
|
2033
|
+
const history = await readHistory(at.room, at.path);
|
|
2034
|
+
if (history.kind === 'no-history' || history.kind === 'revision')
|
|
2035
|
+
return ABSENT_TARGET;
|
|
2036
|
+
if (history.kind !== 'history')
|
|
2037
|
+
return history;
|
|
2038
|
+
const [last] = history.revisions;
|
|
2039
|
+
if (last === undefined)
|
|
2040
|
+
return ABSENT_TARGET;
|
|
2041
|
+
return history.deleted === true
|
|
2042
|
+
? { kind: 'deleted', ...at, revision: last }
|
|
2043
|
+
: { kind: 'document', ...at };
|
|
2044
|
+
}
|
|
2045
|
+
function queuedFacts(locator, path, localId) {
|
|
1023
2046
|
for (const batch of replicas.get(locator)?.queue ?? []) {
|
|
1024
2047
|
const change = batch.changes[queuedFile(batch, path, localId)];
|
|
1025
2048
|
if (change?.kind === 'binary')
|
|
1026
|
-
return change
|
|
2049
|
+
return change;
|
|
1027
2050
|
}
|
|
1028
|
-
|
|
2051
|
+
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2052
|
+
return drafted?.localId === localId ? drafted : null;
|
|
2053
|
+
}
|
|
2054
|
+
function batchFile(batch) {
|
|
2055
|
+
return async (localId) => {
|
|
2056
|
+
const change = batch.changes.find((held) => held.kind === 'binary' && held.localId === localId);
|
|
2057
|
+
return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
|
|
2058
|
+
};
|
|
1029
2059
|
}
|
|
1030
|
-
|
|
2060
|
+
function binaryRoom(request) {
|
|
2061
|
+
if (request.kind === 'path')
|
|
2062
|
+
return request.locator;
|
|
2063
|
+
return options.adapter.referrerScope(request.referrer)?.locator ?? null;
|
|
2064
|
+
}
|
|
2065
|
+
async function transferBinary(request, transfer, answer) {
|
|
1031
2066
|
if (closed || refusal !== null)
|
|
1032
2067
|
return UNCERTAIN;
|
|
1033
2068
|
try {
|
|
@@ -1038,41 +2073,51 @@ export function composeRuntime(options) {
|
|
|
1038
2073
|
}
|
|
1039
2074
|
if (closed || authorization === undefined || refusal !== null)
|
|
1040
2075
|
return UNCERTAIN;
|
|
2076
|
+
const since = fencings;
|
|
1041
2077
|
journal.record(request.kind === 'path'
|
|
1042
2078
|
? `transfer binary ${request.locator} ${request.path}`
|
|
1043
2079
|
: `transfer binary ${request.fileRef}`);
|
|
1044
2080
|
try {
|
|
1045
|
-
const result = await
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
2081
|
+
const result = await transfer(authorization);
|
|
2082
|
+
const settled = result;
|
|
2083
|
+
journal.record(`outcome binary ${settled.kind}`);
|
|
2084
|
+
const locator = binaryRoom(request);
|
|
2085
|
+
if (locator !== null && fencedSince(locator, since))
|
|
2086
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
2087
|
+
if (settled.kind === 'scoped-refusal') {
|
|
2088
|
+
const room = replicas.get(settled.locator)?.confirmed.record;
|
|
1049
2089
|
if (room !== undefined)
|
|
1050
|
-
await scoped(
|
|
2090
|
+
await scoped(settled, room, await openLocal());
|
|
1051
2091
|
}
|
|
1052
|
-
if (
|
|
2092
|
+
if (settled.kind === 'revoked-authorization')
|
|
1053
2093
|
await refuse('revoked-authorization');
|
|
1054
|
-
return
|
|
2094
|
+
return answer(result);
|
|
1055
2095
|
}
|
|
1056
2096
|
catch {
|
|
1057
2097
|
journal.record('outcome binary transport-uncertainty');
|
|
1058
2098
|
return UNCERTAIN;
|
|
1059
2099
|
}
|
|
1060
2100
|
}
|
|
1061
|
-
async function
|
|
1062
|
-
|
|
2101
|
+
async function readBinary(request, name) {
|
|
2102
|
+
if (closed)
|
|
2103
|
+
return UNCERTAIN;
|
|
2104
|
+
const held = await keptFiles.find(name);
|
|
2105
|
+
if (held !== null)
|
|
2106
|
+
return held;
|
|
2107
|
+
return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
|
|
2108
|
+
}
|
|
2109
|
+
function binaryPath(locator, path, revision) {
|
|
1063
2110
|
if (revision !== undefined)
|
|
1064
|
-
return
|
|
2111
|
+
return { kind: 'path', locator, path, revision };
|
|
1065
2112
|
const visible = replicas.get(locator)?.visible;
|
|
1066
2113
|
const held = visible?.binaries[path];
|
|
1067
2114
|
if (visible === undefined || held === undefined)
|
|
1068
|
-
return
|
|
1069
|
-
if (held.state === 'pending')
|
|
1070
|
-
|
|
1071
|
-
return blob === null ? absentBinary(path) : { kind: 'binary', blob };
|
|
1072
|
-
}
|
|
2115
|
+
return null;
|
|
2116
|
+
if (held.state === 'pending')
|
|
2117
|
+
return { kind: 'pending', localId: held.localId };
|
|
1073
2118
|
const record = binaryRecord(path, held, visible.revisions[path]);
|
|
1074
2119
|
if (record.state !== 'accepted')
|
|
1075
|
-
return
|
|
2120
|
+
return null;
|
|
1076
2121
|
const replica = replicas.get(locator);
|
|
1077
2122
|
const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
|
|
1078
2123
|
const holds = (at) => {
|
|
@@ -1087,21 +2132,55 @@ export function composeRuntime(options) {
|
|
|
1087
2132
|
if (change.kind === 'move' && change.to === confirmed)
|
|
1088
2133
|
confirmed = change.path;
|
|
1089
2134
|
}
|
|
1090
|
-
return
|
|
2135
|
+
return {
|
|
1091
2136
|
kind: 'path',
|
|
1092
2137
|
locator,
|
|
1093
2138
|
path: confirmed,
|
|
1094
2139
|
revision: record.revision,
|
|
1095
2140
|
fileRef: record.fileRef,
|
|
1096
|
-
}
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
async function readBinaryPath(locator, path, revision) {
|
|
2144
|
+
await ready;
|
|
2145
|
+
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2146
|
+
const asked = drafted !== undefined && drafted.localId === revision
|
|
2147
|
+
? { kind: 'pending', localId: revision }
|
|
2148
|
+
: binaryPath(locator, path, revision);
|
|
2149
|
+
if (asked === null)
|
|
2150
|
+
return absentBinary(path);
|
|
2151
|
+
if (asked.kind === 'pending') {
|
|
2152
|
+
const facts = queuedFacts(locator, path, asked.localId);
|
|
2153
|
+
return facts === null
|
|
2154
|
+
? absentBinary(path)
|
|
2155
|
+
: {
|
|
2156
|
+
kind: 'binary',
|
|
2157
|
+
file: asked.localId,
|
|
2158
|
+
byteLength: facts.byteLength,
|
|
2159
|
+
mediaType: facts.mediaType,
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
2162
|
+
return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
|
|
2163
|
+
}
|
|
2164
|
+
async function linkBinaryPath(locator, path, revision) {
|
|
2165
|
+
await ready;
|
|
2166
|
+
const asked = binaryPath(locator, path, revision);
|
|
2167
|
+
if (asked === null || asked.kind === 'pending')
|
|
2168
|
+
return absentBinary(path);
|
|
2169
|
+
return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
|
|
1097
2170
|
}
|
|
1098
2171
|
async function readBinaryReference(reference) {
|
|
1099
2172
|
await ready;
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
2173
|
+
const scope = options.adapter.referrerScope(reference.referrer);
|
|
2174
|
+
if (scope === null)
|
|
2175
|
+
return absentReference(reference.fileRef);
|
|
2176
|
+
return readBinary({ kind: 'reference', fileRef: reference.fileRef, referrer: reference.referrer }, { locator: scope.locator, path: scope.path ?? '', revision: reference.fileRef });
|
|
2177
|
+
}
|
|
2178
|
+
function readPart(file, part) {
|
|
2179
|
+
return options.database.ask({ kind: 'file.part', key: file, part });
|
|
2180
|
+
}
|
|
2181
|
+
async function cleanUp() {
|
|
2182
|
+
const freed = await options.database.ask({ kind: 'opened.clean' });
|
|
2183
|
+
return freed + ((await options.shell.cleanUp?.()) ?? 0);
|
|
1105
2184
|
}
|
|
1106
2185
|
function declaredAgents() {
|
|
1107
2186
|
for (const capability of capabilities) {
|
|
@@ -1119,7 +2198,14 @@ export function composeRuntime(options) {
|
|
|
1119
2198
|
}
|
|
1120
2199
|
return undefined;
|
|
1121
2200
|
}
|
|
1122
|
-
|
|
2201
|
+
function calledRooms(name, input) {
|
|
2202
|
+
const member = declaredAction(name)?.room;
|
|
2203
|
+
if (member === undefined || input === null || typeof input !== 'object')
|
|
2204
|
+
return [];
|
|
2205
|
+
const named = input[member];
|
|
2206
|
+
return (Array.isArray(named) ? named : [named]).filter((room) => typeof room === 'string');
|
|
2207
|
+
}
|
|
2208
|
+
async function invokeAction(name, input, dispatching) {
|
|
1123
2209
|
await ready;
|
|
1124
2210
|
if (closed || refusal !== null)
|
|
1125
2211
|
return UNCERTAIN;
|
|
@@ -1133,6 +2219,7 @@ export function composeRuntime(options) {
|
|
|
1133
2219
|
}
|
|
1134
2220
|
if (closed || authorization === undefined || refusal !== null)
|
|
1135
2221
|
return UNCERTAIN;
|
|
2222
|
+
await dispatching?.();
|
|
1136
2223
|
journal.record(`transfer action ${name}`);
|
|
1137
2224
|
try {
|
|
1138
2225
|
const result = await options.adapter.action(authorization, { name, input });
|
|
@@ -1153,6 +2240,60 @@ export function composeRuntime(options) {
|
|
|
1153
2240
|
return UNCERTAIN;
|
|
1154
2241
|
}
|
|
1155
2242
|
}
|
|
2243
|
+
function unsettled(result) {
|
|
2244
|
+
return result.kind !== 'action' && result.retryable;
|
|
2245
|
+
}
|
|
2246
|
+
async function recoveredAction(name, input) {
|
|
2247
|
+
const operation = operationOf(input);
|
|
2248
|
+
if (operation === null)
|
|
2249
|
+
return invokeAction(name, input);
|
|
2250
|
+
const kept = async () => (await openLocal()).keepCall({ operation, name, input });
|
|
2251
|
+
calling.add(operation);
|
|
2252
|
+
try {
|
|
2253
|
+
let result = await invokeAction(name, input, kept);
|
|
2254
|
+
if (unsettled(result)) {
|
|
2255
|
+
await recovery.attempt(`action ${operation}`, async () => {
|
|
2256
|
+
if (closed || refusal !== null)
|
|
2257
|
+
return 'done';
|
|
2258
|
+
if (!socketOpen)
|
|
2259
|
+
return 'again';
|
|
2260
|
+
result = await invokeAction(name, input, kept);
|
|
2261
|
+
return unsettled(result) ? 'again' : 'done';
|
|
2262
|
+
}, { again: true });
|
|
2263
|
+
}
|
|
2264
|
+
if (!unsettled(result))
|
|
2265
|
+
await (await openLocal()).dropCall(operation);
|
|
2266
|
+
return result;
|
|
2267
|
+
}
|
|
2268
|
+
finally {
|
|
2269
|
+
calling.delete(operation);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
async function resendCalls(signal) {
|
|
2273
|
+
if (!socketOpen || closed || stopping)
|
|
2274
|
+
return 'done';
|
|
2275
|
+
const local = await openLocal();
|
|
2276
|
+
for (const held of await local.calls()) {
|
|
2277
|
+
if (calling.has(held.operation))
|
|
2278
|
+
continue;
|
|
2279
|
+
if (closed || signal.aborted)
|
|
2280
|
+
return 'done';
|
|
2281
|
+
if (pastHorizon(held.operation)) {
|
|
2282
|
+
await local.dropCall(held.operation);
|
|
2283
|
+
continue;
|
|
2284
|
+
}
|
|
2285
|
+
calling.add(held.operation);
|
|
2286
|
+
try {
|
|
2287
|
+
if (unsettled(await invokeAction(held.name, held.input)))
|
|
2288
|
+
return 'again';
|
|
2289
|
+
await local.dropCall(held.operation);
|
|
2290
|
+
}
|
|
2291
|
+
finally {
|
|
2292
|
+
calling.delete(held.operation);
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
return 'done';
|
|
2296
|
+
}
|
|
1156
2297
|
function continuing() {
|
|
1157
2298
|
return !closed && !discoveryPaused;
|
|
1158
2299
|
}
|
|
@@ -1167,11 +2308,12 @@ export function composeRuntime(options) {
|
|
|
1167
2308
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
1168
2309
|
!options.adapter.reached(locator, roomVersion, written));
|
|
1169
2310
|
}
|
|
1170
|
-
async function pullRefresh(authorized, confirmed) {
|
|
1171
|
-
if (!
|
|
2311
|
+
async function pullRefresh(authorized, confirmed, live) {
|
|
2312
|
+
if (!live())
|
|
1172
2313
|
return unconfirmed;
|
|
2314
|
+
const turn = generation;
|
|
1173
2315
|
try {
|
|
1174
|
-
const result = await refreshRoom(
|
|
2316
|
+
const result = await refreshRoom(reads, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
|
|
1175
2317
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
1176
2318
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
1177
2319
|
return result;
|
|
@@ -1180,48 +2322,101 @@ export function composeRuntime(options) {
|
|
|
1180
2322
|
journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
|
|
1181
2323
|
throw error;
|
|
1182
2324
|
}
|
|
2325
|
+
finally {
|
|
2326
|
+
reads.finished(confirmed.record.locator, turn);
|
|
2327
|
+
}
|
|
1183
2328
|
}
|
|
1184
|
-
function staging(local) {
|
|
2329
|
+
function staging(local, live) {
|
|
1185
2330
|
return {
|
|
1186
2331
|
held: (locator) => transfers.get(locator) ?? null,
|
|
1187
2332
|
staged: (record) => local.staged(record),
|
|
1188
2333
|
stage: (record, page) => exclusive(async () => {
|
|
1189
|
-
if (!
|
|
2334
|
+
if (!live())
|
|
2335
|
+
return false;
|
|
2336
|
+
const bytes = layerBytes(page);
|
|
2337
|
+
if (!(await admitted(local, record.locator, bytes)))
|
|
1190
2338
|
return false;
|
|
1191
|
-
|
|
2339
|
+
try {
|
|
2340
|
+
await local.stage(record, page);
|
|
2341
|
+
}
|
|
2342
|
+
catch (error) {
|
|
2343
|
+
if (!exceeded(error))
|
|
2344
|
+
throw error;
|
|
2345
|
+
await exceededOn(record.locator, bytes);
|
|
2346
|
+
return false;
|
|
2347
|
+
}
|
|
1192
2348
|
transfers.set(record.locator, record);
|
|
2349
|
+
readSide.recover();
|
|
1193
2350
|
return true;
|
|
1194
2351
|
}),
|
|
1195
2352
|
abandon: (locator) => exclusive(async () => {
|
|
1196
|
-
if (!
|
|
2353
|
+
if (!live())
|
|
1197
2354
|
return;
|
|
1198
2355
|
await local.abandon(locator);
|
|
1199
2356
|
transfers.delete(locator);
|
|
2357
|
+
readSide.recover();
|
|
1200
2358
|
}),
|
|
1201
2359
|
};
|
|
1202
2360
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
2361
|
+
function reusable(room) {
|
|
2362
|
+
const record = replicas.get(room.locator)?.confirmed.record;
|
|
2363
|
+
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
2364
|
+
return null;
|
|
2365
|
+
return { documents: record.documents, revisions: record.revisions };
|
|
2366
|
+
}
|
|
2367
|
+
async function pullBootstrap(authorized, room, local, live) {
|
|
2368
|
+
if (!live())
|
|
1205
2369
|
return unconfirmed;
|
|
2370
|
+
const turn = generation;
|
|
2371
|
+
if (replicas.has(room.locator)) {
|
|
2372
|
+
catching.add(room.locator);
|
|
2373
|
+
readSide.recover();
|
|
2374
|
+
}
|
|
1206
2375
|
try {
|
|
1207
|
-
const result = await bootstrapRoom(
|
|
2376
|
+
const result = await bootstrapRoom(reads, authorized, room, {
|
|
1208
2377
|
collection: modelOf(room.locator),
|
|
1209
2378
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
1210
|
-
}, (models, held) => remodel(room.locator, models, held),
|
|
2379
|
+
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
1211
2380
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
1212
2381
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
2382
|
+
if (result.kind !== 'staged')
|
|
2383
|
+
caughtUpTo(room.locator);
|
|
1213
2384
|
return result;
|
|
1214
2385
|
}
|
|
1215
2386
|
catch (error) {
|
|
1216
2387
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
2388
|
+
caughtUpTo(room.locator);
|
|
1217
2389
|
throw error;
|
|
1218
2390
|
}
|
|
2391
|
+
finally {
|
|
2392
|
+
reads.finished(room.locator, turn);
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
function caughtUpTo(locator) {
|
|
2396
|
+
if (catching.delete(locator))
|
|
2397
|
+
readSide.recover();
|
|
2398
|
+
}
|
|
2399
|
+
function behind(room) {
|
|
2400
|
+
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
2401
|
+
return (held !== undefined &&
|
|
2402
|
+
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
2403
|
+
}
|
|
2404
|
+
async function advance(room, local, authorized, live) {
|
|
2405
|
+
const carried = reads.carried(room.locator);
|
|
2406
|
+
const withheld = await advanceOnce(room, local, authorized, live);
|
|
2407
|
+
if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
|
|
2408
|
+
return withheld;
|
|
2409
|
+
}
|
|
2410
|
+
return advanceOnce(room, local, authorized, live);
|
|
1219
2411
|
}
|
|
1220
|
-
async function
|
|
2412
|
+
async function advanceOnce(room, local, authorized, live) {
|
|
2413
|
+
await referenced(room.locator);
|
|
2414
|
+
if (!live() || paused.has(room.locator))
|
|
2415
|
+
return unconfirmed;
|
|
1221
2416
|
const held = replicas.get(room.locator);
|
|
1222
2417
|
if (held !== undefined) {
|
|
1223
|
-
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
1224
|
-
if (!
|
|
2418
|
+
const refreshed = await pullRefresh(authorized, held.confirmed, live);
|
|
2419
|
+
if (!live())
|
|
1225
2420
|
return unconfirmed;
|
|
1226
2421
|
if (refreshed.kind === 'still-current') {
|
|
1227
2422
|
stale.delete(room.locator);
|
|
@@ -1235,31 +2430,47 @@ export function composeRuntime(options) {
|
|
|
1235
2430
|
return null;
|
|
1236
2431
|
}
|
|
1237
2432
|
if (refreshed.kind === 'advanced') {
|
|
1238
|
-
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
|
|
2433
|
+
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
|
|
1239
2434
|
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1240
2435
|
const reached = now !== undefined &&
|
|
1241
2436
|
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
1242
|
-
return reached ? null : advance(room, local, authorized);
|
|
2437
|
+
return reached ? null : advance(room, local, authorized, live);
|
|
1243
2438
|
}
|
|
1244
2439
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1245
2440
|
return null;
|
|
1246
2441
|
}
|
|
1247
2442
|
if (refreshed.kind !== 'beyond-horizon')
|
|
1248
2443
|
return refreshed;
|
|
2444
|
+
if (opened !== null) {
|
|
2445
|
+
owed.add(room.locator);
|
|
2446
|
+
readSide.recover();
|
|
2447
|
+
return unconfirmed;
|
|
2448
|
+
}
|
|
2449
|
+
if (reloadsDue.get(room.locator) === generation)
|
|
2450
|
+
return unconfirmed;
|
|
1249
2451
|
}
|
|
1250
|
-
const bootstrapped = await pullBootstrap(authorized, room, local);
|
|
1251
|
-
if (!
|
|
2452
|
+
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
2453
|
+
if (!live())
|
|
1252
2454
|
return unconfirmed;
|
|
1253
2455
|
if (bootstrapped.kind !== 'staged')
|
|
1254
2456
|
return bootstrapped;
|
|
1255
|
-
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
2457
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
1256
2458
|
return null;
|
|
1257
2459
|
}
|
|
1258
2460
|
async function withhold(withheld, room, local, progress) {
|
|
2461
|
+
if (withheld.kind === 'unconfirmed')
|
|
2462
|
+
return;
|
|
1259
2463
|
if (withheld.kind === 'refused')
|
|
1260
2464
|
stale.delete(room.locator);
|
|
1261
2465
|
else
|
|
1262
2466
|
stale.add(room.locator);
|
|
2467
|
+
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
2468
|
+
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
2469
|
+
repairable.set(room.locator, reset);
|
|
2470
|
+
repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
|
|
2471
|
+
}
|
|
2472
|
+
else
|
|
2473
|
+
repairable.delete(room.locator);
|
|
1263
2474
|
if (withheld.kind === 'scoped-refusal') {
|
|
1264
2475
|
await scoped(withheld, room, local);
|
|
1265
2476
|
}
|
|
@@ -1295,6 +2506,7 @@ export function composeRuntime(options) {
|
|
|
1295
2506
|
unreadable.add(room.locator);
|
|
1296
2507
|
list(room, false);
|
|
1297
2508
|
emitRooms();
|
|
2509
|
+
moveTo({ ...status, ...attention() });
|
|
1298
2510
|
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
1299
2511
|
}
|
|
1300
2512
|
function leaving(batch, result) {
|
|
@@ -1316,6 +2528,7 @@ export function composeRuntime(options) {
|
|
|
1316
2528
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1317
2529
|
}
|
|
1318
2530
|
async function adoptAccepted(room, local, submitted, accepted) {
|
|
2531
|
+
await referenced(room.locator);
|
|
1319
2532
|
if (!continuing())
|
|
1320
2533
|
return unconfirmed;
|
|
1321
2534
|
const current = replicas.get(room.locator);
|
|
@@ -1326,75 +2539,217 @@ export function composeRuntime(options) {
|
|
|
1326
2539
|
logAccepted(room.locator, submitted, accepted);
|
|
1327
2540
|
const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
1328
2541
|
? { kind: 'staged', confirmed: current.confirmed }
|
|
1329
|
-
: applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
2542
|
+
: await applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
1330
2543
|
? accepted.roomVersion
|
|
1331
2544
|
: held, accepted.roomVersion);
|
|
1332
2545
|
if (applied.kind !== 'staged') {
|
|
1333
|
-
const
|
|
2546
|
+
const key = pendingKey(room.locator, submitted.id);
|
|
2547
|
+
const state = pendings.get(key);
|
|
1334
2548
|
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1335
|
-
if (state !== undefined)
|
|
1336
|
-
pendings.set(
|
|
2549
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
2550
|
+
pendings.set(key, { ...state, accepted: accepted.roomVersion });
|
|
1337
2551
|
emitQueue(room.locator);
|
|
1338
2552
|
return applied;
|
|
1339
2553
|
}
|
|
1340
|
-
const
|
|
1341
|
-
pendings.delete(submitted.id);
|
|
2554
|
+
const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
2555
|
+
pendings.delete(pendingKey(room.locator, submitted.id));
|
|
1342
2556
|
unreadable.delete(room.locator);
|
|
1343
2557
|
sourceRefusals.delete(room.locator);
|
|
1344
2558
|
declarationRefusals.delete(room.locator);
|
|
2559
|
+
const set = settled();
|
|
1345
2560
|
list(room, true);
|
|
1346
2561
|
publishChangeSet(set);
|
|
1347
2562
|
moveTo({ ...status, ...attention() });
|
|
1348
2563
|
return null;
|
|
1349
2564
|
}
|
|
1350
2565
|
async function adopted(room, local, submitted, accepted) {
|
|
1351
|
-
const failed = await exclusive(() =>
|
|
2566
|
+
const failed = await exclusive(async () => {
|
|
2567
|
+
const adopting = await adoptAccepted(room, local, submitted, accepted);
|
|
2568
|
+
await unpair(local);
|
|
2569
|
+
return adopting;
|
|
2570
|
+
});
|
|
2571
|
+
if (failed === null)
|
|
2572
|
+
return true;
|
|
2573
|
+
if (failed.kind === 'refused' && continuing())
|
|
2574
|
+
refusedSource(room, failed);
|
|
2575
|
+
return false;
|
|
2576
|
+
}
|
|
2577
|
+
async function adoptedMove(room, partner, local, submitted, accepted) {
|
|
2578
|
+
const other = discovered.get(partner);
|
|
2579
|
+
const failed = await exclusive(async () => {
|
|
2580
|
+
const left = await adoptAccepted(room, local, submitted, accepted);
|
|
2581
|
+
const arriving = replicas.get(partner)?.queue.find((batch) => batch.id === submitted.id);
|
|
2582
|
+
const arrived = other === undefined || arriving === undefined || accepted.destination === undefined
|
|
2583
|
+
? null
|
|
2584
|
+
: await adoptAccepted(other, local, arriving, {
|
|
2585
|
+
kind: 'accepted',
|
|
2586
|
+
...accepted.destination,
|
|
2587
|
+
});
|
|
2588
|
+
if (other !== undefined && arrived?.kind === 'refused' && continuing()) {
|
|
2589
|
+
refusedSource(other, arrived);
|
|
2590
|
+
}
|
|
2591
|
+
await unpair(local);
|
|
2592
|
+
return left;
|
|
2593
|
+
});
|
|
2594
|
+
submitLater(partner);
|
|
1352
2595
|
if (failed === null)
|
|
1353
2596
|
return true;
|
|
1354
2597
|
if (failed.kind === 'refused' && continuing())
|
|
1355
2598
|
refusedSource(room, failed);
|
|
1356
2599
|
return false;
|
|
1357
2600
|
}
|
|
1358
|
-
|
|
1359
|
-
|
|
2601
|
+
function queueIn(locator) {
|
|
2602
|
+
return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
2603
|
+
}
|
|
2604
|
+
function behindIn(locator, batch) {
|
|
2605
|
+
const held = replicas.get(locator)?.queue;
|
|
2606
|
+
if (held === undefined)
|
|
2607
|
+
return queuesWithoutReplica.get(locator) !== undefined;
|
|
2608
|
+
return held.findIndex((one) => one.id === batch.id) > 0;
|
|
2609
|
+
}
|
|
2610
|
+
function unpaired(skipped) {
|
|
2611
|
+
for (const locator of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
|
|
2612
|
+
for (const batch of queueIn(locator)) {
|
|
2613
|
+
const partner = partnerOf(batch, locator);
|
|
2614
|
+
if (partner === null || skipped.has(pendingKey(locator, batch.id)))
|
|
2615
|
+
continue;
|
|
2616
|
+
if (queueIn(partner).some((held) => held.id === batch.id))
|
|
2617
|
+
continue;
|
|
2618
|
+
if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
|
|
2619
|
+
continue;
|
|
2620
|
+
if (leavingIn(batch, locator) && !withdrawable(locator)(batch))
|
|
2621
|
+
continue;
|
|
2622
|
+
return { locator, batch };
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
return null;
|
|
2626
|
+
}
|
|
2627
|
+
async function unpair(local) {
|
|
2628
|
+
const handled = new Set();
|
|
2629
|
+
for (let held = unpaired(handled); held !== null && continuing(); held = unpaired(handled)) {
|
|
2630
|
+
const { locator, batch } = held;
|
|
2631
|
+
handled.add(pendingKey(locator, batch.id));
|
|
2632
|
+
journal.record(`outcome ${locator} ${batch.id} ${localSaves ? 'dropped' : 'unpaired'}`);
|
|
2633
|
+
const leaving = {
|
|
2634
|
+
batch,
|
|
2635
|
+
returned: !localSaves && leavingIn(batch, locator) ? returnedBy('withdrawn') : null,
|
|
2636
|
+
};
|
|
2637
|
+
const current = replicas.get(locator);
|
|
2638
|
+
if (current !== undefined) {
|
|
2639
|
+
const settled = await settle(local, locator, current, current.confirmed, current.queue.filter((one) => one.id !== batch.id), { leaving });
|
|
2640
|
+
publishChangeSet(settled());
|
|
2641
|
+
continue;
|
|
2642
|
+
}
|
|
2643
|
+
const queue = queueIn(locator).filter((one) => one.id !== batch.id);
|
|
2644
|
+
const written = new Map((leaving.returned === null
|
|
2645
|
+
? []
|
|
2646
|
+
: (pendings.get(pendingKey(locator, batch.id))?.drafts ?? [])).map((saved) => [
|
|
2647
|
+
saved.path,
|
|
2648
|
+
{
|
|
2649
|
+
locator,
|
|
2650
|
+
path: saved.path,
|
|
2651
|
+
revision: roomDraftRevision(),
|
|
2652
|
+
parts: returnedInto(roomDrafts.get(locator)?.get(saved.path)?.parts, saved.parts),
|
|
2653
|
+
returned: leaving.returned,
|
|
2654
|
+
},
|
|
2655
|
+
]));
|
|
2656
|
+
const carried = carriedBack(locator, leaving.returned === null ? [] : [batch], written);
|
|
2657
|
+
await local.keepAuthored({
|
|
2658
|
+
locator,
|
|
2659
|
+
queue,
|
|
2660
|
+
roomDrafts: [...written.values()],
|
|
2661
|
+
consumedRoomDrafts: carried.taken,
|
|
2662
|
+
}, ...carried.writes);
|
|
2663
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
2664
|
+
if (queue.length > 0)
|
|
2665
|
+
queuesWithoutReplica.set(locator, queue);
|
|
2666
|
+
else
|
|
2667
|
+
queuesWithoutReplica.delete(locator);
|
|
2668
|
+
for (const record of written.values())
|
|
2669
|
+
rememberRoomDraft(record);
|
|
2670
|
+
carried.remember();
|
|
2671
|
+
publishChangeSet(mergeChangeSets([
|
|
2672
|
+
changeSet(locator, null, null, {
|
|
2673
|
+
queue: true,
|
|
2674
|
+
drafts: [...written.keys(), ...carried.taken],
|
|
2675
|
+
}),
|
|
2676
|
+
...carried.changes,
|
|
2677
|
+
]));
|
|
2678
|
+
}
|
|
2679
|
+
if (handled.size === 0)
|
|
2680
|
+
return;
|
|
2681
|
+
moveTo({ ...status, ...attention() });
|
|
2682
|
+
for (const key of handled)
|
|
2683
|
+
submitLater(key.slice(0, key.indexOf('\u0000')));
|
|
2684
|
+
}
|
|
2685
|
+
async function discardHeld(head, room, local, signal) {
|
|
2686
|
+
const partner = partnerOf(head, room.locator);
|
|
2687
|
+
const other = partner === null ? undefined : discovered.get(partner);
|
|
2688
|
+
await discardPending(room, local, signal);
|
|
2689
|
+
if (other !== undefined && !signal.aborted)
|
|
2690
|
+
await discardPending(other, local, signal);
|
|
2691
|
+
}
|
|
2692
|
+
async function submitRoom(room, local, authorized, progress, signal) {
|
|
2693
|
+
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
1360
2694
|
const replica = replicas.get(room.locator);
|
|
1361
2695
|
const head = replica?.queue[0];
|
|
1362
2696
|
if (replica === undefined || head === undefined)
|
|
1363
|
-
return;
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
2697
|
+
return 'done';
|
|
2698
|
+
const state = pendings.get(pendingKey(room.locator, head.id));
|
|
2699
|
+
if (state?.accepted !== undefined)
|
|
2700
|
+
return 'done';
|
|
2701
|
+
const partner = partnerOf(head, room.locator);
|
|
2702
|
+
if (partner !== null && !leavingIn(head, room.locator)) {
|
|
2703
|
+
submitLater(partner);
|
|
2704
|
+
return 'done';
|
|
2705
|
+
}
|
|
2706
|
+
if (partner !== null && behindIn(partner, head)) {
|
|
2707
|
+
progress.complete = false;
|
|
2708
|
+
return 'done';
|
|
2709
|
+
}
|
|
1367
2710
|
const { submission, lost: earlier } = state ?? {
|
|
1368
2711
|
submission: { state: 'waiting' },
|
|
1369
2712
|
lost: false,
|
|
1370
2713
|
};
|
|
1371
2714
|
if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
|
|
1372
2715
|
progress.complete = false;
|
|
1373
|
-
return;
|
|
2716
|
+
return { at: Date.parse(submission.reset) };
|
|
1374
2717
|
}
|
|
1375
2718
|
if (writesHeld()) {
|
|
1376
2719
|
progress.complete = false;
|
|
1377
2720
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1378
|
-
return;
|
|
2721
|
+
return 'done';
|
|
1379
2722
|
}
|
|
1380
2723
|
const lost = earlier || submission.state === 'submitted';
|
|
2724
|
+
if (lost && pastHorizon(head.id)) {
|
|
2725
|
+
await discardHeld(head, room, local, signal);
|
|
2726
|
+
return 'done';
|
|
2727
|
+
}
|
|
1381
2728
|
let result;
|
|
1382
2729
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
1383
|
-
await exclusive(() => mark(local, room.locator, head
|
|
1384
|
-
if (discoveryPaused)
|
|
1385
|
-
return;
|
|
1386
|
-
if (writesHeld()) {
|
|
1387
|
-
await exclusive(() => mark(local, room.locator, head
|
|
2730
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
|
|
2731
|
+
if (discoveryPaused || fences.has(room.locator))
|
|
2732
|
+
return 'done';
|
|
2733
|
+
if (signal.aborted || writesHeld()) {
|
|
2734
|
+
await exclusive(() => mark(local, room.locator, head, submission, earlier));
|
|
2735
|
+
if (signal.aborted)
|
|
2736
|
+
return 'done';
|
|
1388
2737
|
progress.complete = false;
|
|
1389
2738
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1390
|
-
return;
|
|
2739
|
+
return 'done';
|
|
1391
2740
|
}
|
|
1392
2741
|
const opensBefore = openSignals;
|
|
1393
|
-
const
|
|
2742
|
+
const answeredHere = awaitingAnswer(room.locator);
|
|
2743
|
+
const answeredThere = partner === null ? () => { } : awaitingAnswer(partner);
|
|
2744
|
+
const answered = () => {
|
|
2745
|
+
answeredHere();
|
|
2746
|
+
answeredThere();
|
|
2747
|
+
};
|
|
1394
2748
|
try {
|
|
1395
2749
|
result = await options.adapter.mutate(authorized, {
|
|
1396
2750
|
locator: room.locator,
|
|
1397
2751
|
changeBatch: head,
|
|
2752
|
+
file: batchFile(head),
|
|
1398
2753
|
...(state?.upload === undefined ? {} : { upload: state.upload }),
|
|
1399
2754
|
}, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
|
|
1400
2755
|
}
|
|
@@ -1403,50 +2758,69 @@ export function composeRuntime(options) {
|
|
|
1403
2758
|
progress.complete = false;
|
|
1404
2759
|
if (continuing())
|
|
1405
2760
|
progress.reachable = false;
|
|
1406
|
-
return;
|
|
2761
|
+
return 'again';
|
|
2762
|
+
}
|
|
2763
|
+
if (signal.aborted) {
|
|
2764
|
+
answered();
|
|
2765
|
+
return 'done';
|
|
1407
2766
|
}
|
|
1408
2767
|
if (result.kind !== 'accepted' || !continuing())
|
|
1409
2768
|
answered();
|
|
1410
2769
|
if (!continuing())
|
|
1411
|
-
return;
|
|
2770
|
+
return 'done';
|
|
1412
2771
|
if (result.kind === 'transport-uncertainty') {
|
|
1413
2772
|
progress.complete = false;
|
|
1414
2773
|
progress.reachable = false;
|
|
1415
|
-
return;
|
|
2774
|
+
return 'again';
|
|
1416
2775
|
}
|
|
1417
2776
|
if (result.kind === 'room-not-readable') {
|
|
2777
|
+
const unread = discovered.get(result.locator) ?? room;
|
|
1418
2778
|
progress.complete = false;
|
|
1419
|
-
unreadable.add(
|
|
1420
|
-
list(
|
|
2779
|
+
unreadable.add(unread.locator);
|
|
2780
|
+
list(unread, false);
|
|
1421
2781
|
emitRooms();
|
|
1422
|
-
return;
|
|
2782
|
+
return 'done';
|
|
1423
2783
|
}
|
|
1424
2784
|
if (result.kind === 'revoked-authorization') {
|
|
1425
2785
|
await refuse('revoked-authorization');
|
|
1426
|
-
return;
|
|
2786
|
+
return 'done';
|
|
1427
2787
|
}
|
|
1428
2788
|
if (result.kind === 'scoped-refusal') {
|
|
1429
2789
|
await scoped(result, room, local);
|
|
1430
2790
|
progress.complete = false;
|
|
1431
|
-
return;
|
|
2791
|
+
return 'done';
|
|
1432
2792
|
}
|
|
1433
2793
|
if (result.kind === 'write-halted') {
|
|
1434
2794
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
1435
|
-
await exclusive(() => mark(local, room.locator, head
|
|
2795
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
|
|
1436
2796
|
if (serviceMode === 'open' && openSignals !== opensBefore)
|
|
1437
2797
|
continue;
|
|
1438
2798
|
haltedRefusal = true;
|
|
1439
2799
|
progress.complete = false;
|
|
1440
|
-
return;
|
|
2800
|
+
return 'done';
|
|
1441
2801
|
}
|
|
2802
|
+
const other = partner === null ? undefined : discovered.get(partner);
|
|
1442
2803
|
if (result.kind === 'conflicted' ||
|
|
1443
2804
|
result.kind === 'rejected' ||
|
|
1444
2805
|
result.kind === 'expired' ||
|
|
1445
2806
|
result.kind === 'retryable-refusal') {
|
|
2807
|
+
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
2808
|
+
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
2809
|
+
await discardHeld(head, room, local, signal);
|
|
2810
|
+
return 'done';
|
|
2811
|
+
}
|
|
1446
2812
|
if (result.kind === 'expired') {
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
2813
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
|
|
2814
|
+
const there = other === undefined ? null : await advance(other, local, authorized, continuing);
|
|
2815
|
+
if (other !== undefined && there !== null) {
|
|
2816
|
+
await withhold(there, other, local, progress);
|
|
2817
|
+
return 'done';
|
|
2818
|
+
}
|
|
2819
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
2820
|
+
if (withheld !== null) {
|
|
2821
|
+
await withhold(withheld, room, local, progress);
|
|
2822
|
+
return 'done';
|
|
2823
|
+
}
|
|
1450
2824
|
}
|
|
1451
2825
|
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
|
|
1452
2826
|
(result.kind === 'conflicted' && result.roomVersion !== undefined
|
|
@@ -1458,12 +2832,25 @@ export function composeRuntime(options) {
|
|
|
1458
2832
|
if (result.kind === 'retryable-refusal') {
|
|
1459
2833
|
const reset = result.reset;
|
|
1460
2834
|
progress.complete = false;
|
|
1461
|
-
await exclusive(() => mark(local, room.locator, head
|
|
1462
|
-
|
|
1463
|
-
return;
|
|
2835
|
+
await exclusive(() => mark(local, room.locator, head, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
2836
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
1464
2837
|
}
|
|
1465
2838
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
1466
2839
|
const left = leaving(head, result);
|
|
2840
|
+
const refreshed = async () => ({
|
|
2841
|
+
there: other === undefined ? null : await advance(other, local, authorized, continuing),
|
|
2842
|
+
here: await advance(room, local, authorized, continuing),
|
|
2843
|
+
});
|
|
2844
|
+
const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
|
|
2845
|
+
if (judged !== null && (judged.there !== null || judged.here !== null)) {
|
|
2846
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
|
|
2847
|
+
if (other !== undefined && judged.there !== null) {
|
|
2848
|
+
await withhold(judged.there, other, local, progress);
|
|
2849
|
+
}
|
|
2850
|
+
if (judged.here !== null)
|
|
2851
|
+
await withhold(judged.here, room, local, progress);
|
|
2852
|
+
return 'done';
|
|
2853
|
+
}
|
|
1467
2854
|
await exclusive(async () => {
|
|
1468
2855
|
if (!continuing())
|
|
1469
2856
|
return;
|
|
@@ -1471,37 +2858,88 @@ export function composeRuntime(options) {
|
|
|
1471
2858
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
1472
2859
|
return;
|
|
1473
2860
|
}
|
|
1474
|
-
const
|
|
1475
|
-
leaving: (result.kind === 'expired' &&
|
|
1476
|
-
(result.kind === 'conflicted' &&
|
|
2861
|
+
const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
2862
|
+
leaving: (result.kind === 'expired' && landedWhole(room.locator, head, current.confirmed)) ||
|
|
2863
|
+
(result.kind === 'conflicted' &&
|
|
2864
|
+
landedWhole(room.locator, head, current.confirmed, result.affected))
|
|
1477
2865
|
? { ...left, returned: null }
|
|
1478
2866
|
: left,
|
|
1479
2867
|
});
|
|
1480
|
-
publishChangeSet(
|
|
2868
|
+
publishChangeSet(settled());
|
|
2869
|
+
await unpair(local);
|
|
1481
2870
|
moveTo({ ...status, ...attention() });
|
|
1482
2871
|
});
|
|
1483
2872
|
if (result.kind === 'expired')
|
|
1484
2873
|
continue;
|
|
1485
|
-
const
|
|
1486
|
-
if (
|
|
1487
|
-
|
|
2874
|
+
const { there, here } = judged ?? (await refreshed());
|
|
2875
|
+
if (other !== undefined && there !== null)
|
|
2876
|
+
await withhold(there, other, local, progress);
|
|
2877
|
+
if (here !== null) {
|
|
2878
|
+
await withhold(here, room, local, progress);
|
|
2879
|
+
return 'done';
|
|
2880
|
+
}
|
|
1488
2881
|
continue;
|
|
1489
2882
|
}
|
|
1490
|
-
const kept = await
|
|
2883
|
+
const kept = await (partner === null
|
|
2884
|
+
? adopted(room, local, head, result)
|
|
2885
|
+
: adoptedMove(room, partner, local, head, result)).finally(answered);
|
|
1491
2886
|
if (!kept) {
|
|
1492
2887
|
progress.complete = false;
|
|
1493
|
-
return;
|
|
2888
|
+
return 'done';
|
|
1494
2889
|
}
|
|
1495
2890
|
}
|
|
2891
|
+
return 'done';
|
|
2892
|
+
}
|
|
2893
|
+
function pastHorizon(identity) {
|
|
2894
|
+
return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
|
|
2895
|
+
}
|
|
2896
|
+
async function discardPending(room, local, signal) {
|
|
2897
|
+
const { locator } = room;
|
|
2898
|
+
const discarded = await exclusive(async () => {
|
|
2899
|
+
if (fences.has(locator))
|
|
2900
|
+
return false;
|
|
2901
|
+
const before = replicas.get(locator);
|
|
2902
|
+
const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
2903
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
2904
|
+
lastFenced.set(locator, ++fencings);
|
|
2905
|
+
try {
|
|
2906
|
+
await local.discard(locator);
|
|
2907
|
+
}
|
|
2908
|
+
finally {
|
|
2909
|
+
fences.delete(locator);
|
|
2910
|
+
}
|
|
2911
|
+
for (const batch of queue)
|
|
2912
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
2913
|
+
queuesWithoutReplica.delete(locator);
|
|
2914
|
+
const after = before === undefined ? null : await stepped(replicating(before.confirmed, [], before));
|
|
2915
|
+
if (after !== null)
|
|
2916
|
+
replicas.set(locator, after);
|
|
2917
|
+
journal.record(`room discarded ${locator}`);
|
|
2918
|
+
publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
|
|
2919
|
+
moveTo({ ...status, ...attention() });
|
|
2920
|
+
return true;
|
|
2921
|
+
});
|
|
2922
|
+
if (!discarded)
|
|
2923
|
+
return;
|
|
2924
|
+
const progress = { complete: true, reachable: true };
|
|
2925
|
+
await roomTask(locator, async (live) => {
|
|
2926
|
+
await Promise.race([
|
|
2927
|
+
reload(room, local, progress, () => live() && !signal.aborted),
|
|
2928
|
+
abandonedOn(signal),
|
|
2929
|
+
]);
|
|
2930
|
+
});
|
|
2931
|
+
if (signal.aborted)
|
|
2932
|
+
return;
|
|
2933
|
+
await rest(progress);
|
|
1496
2934
|
}
|
|
1497
2935
|
function awaitingAnswer(locator) {
|
|
1498
2936
|
let release = () => { };
|
|
1499
2937
|
const awaited = new Promise((resolve) => {
|
|
1500
2938
|
release = resolve;
|
|
1501
2939
|
});
|
|
1502
|
-
answering.set(locator, awaited);
|
|
2940
|
+
answering.set(locator, { generation, answered: awaited });
|
|
1503
2941
|
return () => {
|
|
1504
|
-
if (answering.get(locator) === awaited)
|
|
2942
|
+
if (answering.get(locator)?.answered === awaited)
|
|
1505
2943
|
answering.delete(locator);
|
|
1506
2944
|
release();
|
|
1507
2945
|
};
|
|
@@ -1525,6 +2963,7 @@ export function composeRuntime(options) {
|
|
|
1525
2963
|
if (!continuing())
|
|
1526
2964
|
return;
|
|
1527
2965
|
const held = replicas.get(room.locator);
|
|
2966
|
+
let changed = null;
|
|
1528
2967
|
if (held !== undefined) {
|
|
1529
2968
|
const declared = {
|
|
1530
2969
|
...held.confirmed.record,
|
|
@@ -1534,8 +2973,8 @@ export function composeRuntime(options) {
|
|
|
1534
2973
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
1535
2974
|
};
|
|
1536
2975
|
const reclassified = sameMembership(declared, held.confirmed.record)
|
|
1537
|
-
? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
|
|
1538
|
-
: readReclassified(declared, held.confirmed);
|
|
2976
|
+
? { kind: 'staged', confirmed: await confirmedOf(declared, held.confirmed) }
|
|
2977
|
+
: await readReclassified(declared, held.confirmed);
|
|
1539
2978
|
const record = reclassified.kind === 'staged'
|
|
1540
2979
|
? declared
|
|
1541
2980
|
: {
|
|
@@ -1543,18 +2982,23 @@ export function composeRuntime(options) {
|
|
|
1543
2982
|
typedSelection: held.confirmed.record.typedSelection,
|
|
1544
2983
|
};
|
|
1545
2984
|
const read = reclassified.kind === 'deferred'
|
|
1546
|
-
? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
|
|
2985
|
+
? { kind: 'staged', confirmed: await confirmedOf(record, held.confirmed) }
|
|
1547
2986
|
: reclassified;
|
|
1548
2987
|
await local.write({ next: record, previous: held.confirmed.record });
|
|
2988
|
+
const replica = read.kind === 'staged'
|
|
2989
|
+
? await stepped(replicating(read.confirmed, held.queue, held))
|
|
2990
|
+
: { ...held, confirmed: { ...held.confirmed, record } };
|
|
2991
|
+
await stepped(counting(replica));
|
|
2992
|
+
const set = await stepped(changing(room.locator, held, replica, { room: true }));
|
|
2993
|
+
replicas.set(room.locator, replica);
|
|
2994
|
+
changed = heldChange(room.locator, held, replica, set, { room: true });
|
|
1549
2995
|
if (read.kind === 'staged') {
|
|
1550
|
-
replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
|
|
1551
2996
|
if (declarationRefusals.delete(room.locator)) {
|
|
1552
2997
|
sourceRefusals.delete(room.locator);
|
|
1553
2998
|
unreadable.delete(room.locator);
|
|
1554
2999
|
}
|
|
1555
3000
|
}
|
|
1556
|
-
else
|
|
1557
|
-
replicas.set(room.locator, { ...held, confirmed: { ...held.confirmed, record } });
|
|
3001
|
+
else {
|
|
1558
3002
|
refusedSource(room, read);
|
|
1559
3003
|
declarationRefusals.add(room.locator);
|
|
1560
3004
|
}
|
|
@@ -1565,13 +3009,18 @@ export function composeRuntime(options) {
|
|
|
1565
3009
|
invalidModels.delete(key);
|
|
1566
3010
|
}
|
|
1567
3011
|
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
1568
|
-
if (
|
|
3012
|
+
if (changed === null)
|
|
1569
3013
|
emitRooms();
|
|
1570
3014
|
else
|
|
1571
|
-
publishChangeSet(
|
|
3015
|
+
publishChangeSet(changed);
|
|
1572
3016
|
});
|
|
1573
3017
|
}
|
|
1574
3018
|
async function scoped(result, room, local) {
|
|
3019
|
+
if (result.scope === 'content' && !result.retryable) {
|
|
3020
|
+
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
3021
|
+
await loseRooms([room.locator]);
|
|
3022
|
+
return;
|
|
3023
|
+
}
|
|
1575
3024
|
scopedRefusals.set(room.locator, {
|
|
1576
3025
|
code: result.code,
|
|
1577
3026
|
fields: result.fields,
|
|
@@ -1585,15 +3034,7 @@ export function composeRuntime(options) {
|
|
|
1585
3034
|
});
|
|
1586
3035
|
else if (result.scope !== 'operation') {
|
|
1587
3036
|
unreadable.add(room.locator);
|
|
1588
|
-
scopedAway = {
|
|
1589
|
-
...room,
|
|
1590
|
-
replica: { ...room.replica, admission: 'unavailable' },
|
|
1591
|
-
operations: {
|
|
1592
|
-
...room.operations,
|
|
1593
|
-
...(result.scope === 'content' ? { read: [] } : {}),
|
|
1594
|
-
...(result.scope === 'sync' ? { sync: [] } : {}),
|
|
1595
|
-
},
|
|
1596
|
-
};
|
|
3037
|
+
scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
|
|
1597
3038
|
await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
|
|
1598
3039
|
}
|
|
1599
3040
|
if (!continuing())
|
|
@@ -1617,8 +3058,10 @@ export function composeRuntime(options) {
|
|
|
1617
3058
|
];
|
|
1618
3059
|
});
|
|
1619
3060
|
}
|
|
1620
|
-
async function synchronize(listed, local, progress, attempt) {
|
|
3061
|
+
async function synchronize(listed, local, progress, attempt, live) {
|
|
1621
3062
|
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
|
|
3063
|
+
if (!live())
|
|
3064
|
+
return;
|
|
1622
3065
|
const ready = listed.filter((_, index) => {
|
|
1623
3066
|
const outcome = prepared[index];
|
|
1624
3067
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
@@ -1627,9 +3070,10 @@ export function composeRuntime(options) {
|
|
|
1627
3070
|
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
1628
3071
|
handed = true;
|
|
1629
3072
|
}
|
|
3073
|
+
discovering?.release();
|
|
1630
3074
|
const loaded = await Promise.allSettled(ready
|
|
1631
3075
|
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1632
|
-
.map((room) => loadRoom(room, local, progress)));
|
|
3076
|
+
.map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
|
|
1633
3077
|
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1634
3078
|
if (failed !== undefined)
|
|
1635
3079
|
throw failed.reason;
|
|
@@ -1651,21 +3095,12 @@ export function composeRuntime(options) {
|
|
|
1651
3095
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1652
3096
|
if (discoveryPaused || authorization === undefined)
|
|
1653
3097
|
return false;
|
|
1654
|
-
|
|
1655
|
-
if (replicas.has(room.locator))
|
|
1656
|
-
progress.complete = false;
|
|
1657
|
-
return false;
|
|
1658
|
-
}
|
|
1659
|
-
return true;
|
|
3098
|
+
return room.replica.admission === 'admitted';
|
|
1660
3099
|
}
|
|
1661
|
-
async function caughtUp(room, local, progress) {
|
|
1662
|
-
|
|
1663
|
-
if (held === undefined || authorization === undefined)
|
|
1664
|
-
return;
|
|
1665
|
-
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
1666
|
-
if (!behind)
|
|
3100
|
+
async function caughtUp(room, local, progress, live) {
|
|
3101
|
+
if (authorization === undefined || !live() || !behind(room))
|
|
1667
3102
|
return;
|
|
1668
|
-
const withheld = await advance(room, local, authorization);
|
|
3103
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1669
3104
|
if (withheld !== null)
|
|
1670
3105
|
await withhold(withheld, room, local, progress);
|
|
1671
3106
|
}
|
|
@@ -1675,47 +3110,57 @@ export function composeRuntime(options) {
|
|
|
1675
3110
|
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1676
3111
|
});
|
|
1677
3112
|
}
|
|
1678
|
-
async function loadRoom(room, local, progress) {
|
|
1679
|
-
if (
|
|
3113
|
+
async function loadRoom(room, local, progress, live) {
|
|
3114
|
+
if (!live() || authorization === undefined)
|
|
1680
3115
|
return;
|
|
1681
3116
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1682
|
-
await reload(room, local, progress);
|
|
3117
|
+
await reload(room, local, progress, live);
|
|
1683
3118
|
return;
|
|
1684
3119
|
}
|
|
1685
|
-
const withheld = await advance(room, local, authorization);
|
|
3120
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1686
3121
|
if (withheld !== null) {
|
|
1687
3122
|
await withhold(withheld, room, local, progress);
|
|
1688
3123
|
return;
|
|
1689
3124
|
}
|
|
1690
|
-
await caughtUp(room, local, progress);
|
|
3125
|
+
await caughtUp(room, local, progress, live);
|
|
1691
3126
|
}
|
|
1692
|
-
async function reload(room, local, progress) {
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
3127
|
+
async function reload(room, local, progress, live) {
|
|
3128
|
+
try {
|
|
3129
|
+
if (live())
|
|
3130
|
+
reloadsDue.delete(room.locator);
|
|
3131
|
+
if (!live() || authorization === undefined)
|
|
3132
|
+
return;
|
|
3133
|
+
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
3134
|
+
if (!live())
|
|
3135
|
+
return;
|
|
3136
|
+
if (bootstrapped.kind !== 'staged') {
|
|
3137
|
+
await withhold(bootstrapped, room, local, progress);
|
|
3138
|
+
return;
|
|
3139
|
+
}
|
|
3140
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
3141
|
+
await caughtUp(room, local, progress, live);
|
|
3142
|
+
}
|
|
3143
|
+
finally {
|
|
3144
|
+
caughtUpTo(room.locator);
|
|
1701
3145
|
}
|
|
1702
|
-
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
1703
|
-
await caughtUp(room, local, progress);
|
|
1704
3146
|
}
|
|
1705
|
-
async function discoverRooms(local, progress, attempt) {
|
|
3147
|
+
async function discoverRooms(local, progress, attempt, live, scope) {
|
|
1706
3148
|
if (closed || authorization === undefined) {
|
|
1707
3149
|
notifyReady();
|
|
1708
3150
|
return;
|
|
1709
3151
|
}
|
|
1710
3152
|
moveTo(transferring(status));
|
|
3153
|
+
discoveries += 1;
|
|
3154
|
+
const started = discoveries;
|
|
1711
3155
|
let result;
|
|
1712
3156
|
try {
|
|
1713
|
-
result = await options.adapter.discover(authorization);
|
|
3157
|
+
result = await options.adapter.discover(authorization, scope);
|
|
1714
3158
|
}
|
|
1715
3159
|
catch {
|
|
1716
3160
|
result = { kind: 'transport-uncertainty' };
|
|
1717
3161
|
}
|
|
1718
|
-
if (
|
|
3162
|
+
if (!live()) {
|
|
3163
|
+
progress.complete = false;
|
|
1719
3164
|
notifyReady();
|
|
1720
3165
|
return;
|
|
1721
3166
|
}
|
|
@@ -1749,12 +3194,13 @@ export function composeRuntime(options) {
|
|
|
1749
3194
|
}
|
|
1750
3195
|
for (const room of result.rooms) {
|
|
1751
3196
|
if (room.operations === undefined ||
|
|
1752
|
-
!['
|
|
3197
|
+
!['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
1753
3198
|
room.operations[operation].every((scope) => typeof scope === 'string')) ||
|
|
1754
3199
|
room.replica === undefined ||
|
|
1755
3200
|
typeof room.replica.selection !== 'string' ||
|
|
1756
3201
|
room.replica.selection.length === 0 ||
|
|
1757
|
-
!['admitted', 'unavailable'].includes(room.replica.admission)
|
|
3202
|
+
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
3203
|
+
!['locked', 'open'].includes(room.replica.offline)) {
|
|
1758
3204
|
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
1759
3205
|
await refuse('missing-required-guarantee');
|
|
1760
3206
|
notifyReady();
|
|
@@ -1773,6 +3219,7 @@ export function composeRuntime(options) {
|
|
|
1773
3219
|
}
|
|
1774
3220
|
const previousCapabilities = capabilities;
|
|
1775
3221
|
capabilities = result.capabilities;
|
|
3222
|
+
outcomeRetention = result.retention.outcome;
|
|
1776
3223
|
notifyReady();
|
|
1777
3224
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1778
3225
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
@@ -1785,8 +3232,7 @@ export function composeRuntime(options) {
|
|
|
1785
3232
|
member = signedIn;
|
|
1786
3233
|
emitMember();
|
|
1787
3234
|
}
|
|
1788
|
-
await applyDiscovery(result.rooms, local);
|
|
1789
|
-
await synchronize(result.rooms, local, progress, attempt);
|
|
3235
|
+
await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
|
|
1790
3236
|
}
|
|
1791
3237
|
async function ensureAuthorization() {
|
|
1792
3238
|
if (authorization !== undefined)
|
|
@@ -1800,20 +3246,50 @@ export function composeRuntime(options) {
|
|
|
1800
3246
|
throw error;
|
|
1801
3247
|
}
|
|
1802
3248
|
}
|
|
1803
|
-
function
|
|
3249
|
+
async function admittedRead(work) {
|
|
3250
|
+
if (roomReads.generation !== generation) {
|
|
3251
|
+
roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
3252
|
+
}
|
|
3253
|
+
const reads = roomReads;
|
|
3254
|
+
while (reads.free === 0)
|
|
3255
|
+
await new Promise((admit) => reads.waiting.push(admit));
|
|
3256
|
+
reads.free -= 1;
|
|
3257
|
+
try {
|
|
3258
|
+
await work();
|
|
3259
|
+
}
|
|
3260
|
+
finally {
|
|
3261
|
+
reads.free += 1;
|
|
3262
|
+
reads.waiting.shift()?.();
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
function roomTask(locator, work) {
|
|
3266
|
+
return task(roomLane(locator), (live) => admittedRead(() => work(live)));
|
|
3267
|
+
}
|
|
3268
|
+
function task(lane, work) {
|
|
1804
3269
|
if (stopping)
|
|
1805
|
-
return
|
|
1806
|
-
|
|
1807
|
-
|
|
3270
|
+
return Promise.resolve();
|
|
3271
|
+
const held = lanes.get(lane);
|
|
3272
|
+
const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
|
|
3273
|
+
const turn = current.generation;
|
|
3274
|
+
const live = () => turn === generation && continuing();
|
|
3275
|
+
const run = current.chain.then(() => work(live)).catch(() => undefined);
|
|
3276
|
+
current.chain = run;
|
|
3277
|
+
lanes.set(lane, current);
|
|
3278
|
+
working.add(run);
|
|
3279
|
+
void run.finally(() => working.delete(run));
|
|
3280
|
+
return run;
|
|
1808
3281
|
}
|
|
1809
3282
|
function sync(attempt) {
|
|
1810
|
-
return task(async () => {
|
|
1811
|
-
if (
|
|
3283
|
+
return task(OPENING_LANE, async (live) => {
|
|
3284
|
+
if (!live()) {
|
|
1812
3285
|
notifyReady();
|
|
1813
3286
|
return;
|
|
1814
3287
|
}
|
|
1815
3288
|
const progress = { complete: true, reachable: true };
|
|
1816
|
-
|
|
3289
|
+
const gate = discover();
|
|
3290
|
+
if (attempt === 'opened')
|
|
3291
|
+
opened = generation;
|
|
3292
|
+
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
1817
3293
|
if (attempt === 'opened')
|
|
1818
3294
|
streamed = progress;
|
|
1819
3295
|
complete = false;
|
|
@@ -1822,12 +3298,12 @@ export function composeRuntime(options) {
|
|
|
1822
3298
|
handed = false;
|
|
1823
3299
|
try {
|
|
1824
3300
|
const local = await openLocal();
|
|
1825
|
-
if (
|
|
3301
|
+
if (!live())
|
|
1826
3302
|
return;
|
|
1827
3303
|
await ensureAuthorization();
|
|
1828
|
-
if (
|
|
3304
|
+
if (!live())
|
|
1829
3305
|
return;
|
|
1830
|
-
await discoverRooms(local, progress, attempt);
|
|
3306
|
+
await discoverRooms(local, progress, attempt, live);
|
|
1831
3307
|
}
|
|
1832
3308
|
catch {
|
|
1833
3309
|
progress.complete = false;
|
|
@@ -1835,23 +3311,62 @@ export function composeRuntime(options) {
|
|
|
1835
3311
|
progress.reachable = false;
|
|
1836
3312
|
}
|
|
1837
3313
|
finally {
|
|
3314
|
+
gate.release();
|
|
1838
3315
|
notifyReady();
|
|
1839
3316
|
}
|
|
3317
|
+
if (!live())
|
|
3318
|
+
return;
|
|
1840
3319
|
if (attempt !== 'opened') {
|
|
1841
|
-
await finishOpening(progress);
|
|
3320
|
+
await finishOpening(progress, live);
|
|
1842
3321
|
return;
|
|
1843
3322
|
}
|
|
1844
3323
|
if (handed || closed || progress.reachable)
|
|
1845
3324
|
return;
|
|
1846
3325
|
underway = null;
|
|
3326
|
+
opened = null;
|
|
1847
3327
|
journal.record('connection reopen');
|
|
1848
|
-
await rest(progress);
|
|
3328
|
+
await rest(progress, live);
|
|
1849
3329
|
options.adapter.roomStream?.reopen();
|
|
1850
3330
|
});
|
|
1851
3331
|
}
|
|
1852
|
-
function
|
|
3332
|
+
function rediscover(scope) {
|
|
3333
|
+
return task(OPENING_LANE, async (live) => {
|
|
3334
|
+
if (!live() || !socketOpen)
|
|
3335
|
+
return;
|
|
3336
|
+
const progress = { complete: true, reachable: true };
|
|
3337
|
+
const gate = discover();
|
|
3338
|
+
moveTo(transferring(status));
|
|
3339
|
+
try {
|
|
3340
|
+
const local = await openLocal();
|
|
3341
|
+
if (!live())
|
|
3342
|
+
return;
|
|
3343
|
+
await ensureAuthorization();
|
|
3344
|
+
if (!live())
|
|
3345
|
+
return;
|
|
3346
|
+
await discoverRooms(local, progress, 'authorized', live, scope);
|
|
3347
|
+
}
|
|
3348
|
+
catch {
|
|
3349
|
+
progress.complete = false;
|
|
3350
|
+
}
|
|
3351
|
+
finally {
|
|
3352
|
+
gate.release();
|
|
3353
|
+
notifyReady();
|
|
3354
|
+
}
|
|
3355
|
+
if (!live())
|
|
3356
|
+
return;
|
|
3357
|
+
if (!progress.complete)
|
|
3358
|
+
complete = false;
|
|
3359
|
+
await rest(progress, live);
|
|
3360
|
+
});
|
|
3361
|
+
}
|
|
3362
|
+
function closedSocket(revoked, lost) {
|
|
1853
3363
|
const opened = socketOpen;
|
|
1854
3364
|
socketOpen = false;
|
|
3365
|
+
connectionLost();
|
|
3366
|
+
if (lost) {
|
|
3367
|
+
reads.lost();
|
|
3368
|
+
conversations.lost();
|
|
3369
|
+
}
|
|
1855
3370
|
if (revoked) {
|
|
1856
3371
|
void refuse('revoked-authorization');
|
|
1857
3372
|
return;
|
|
@@ -1866,24 +3381,35 @@ export function composeRuntime(options) {
|
|
|
1866
3381
|
});
|
|
1867
3382
|
}
|
|
1868
3383
|
function applyOpening(expired) {
|
|
1869
|
-
|
|
1870
|
-
|
|
3384
|
+
const progress = underway?.generation === generation ? underway.progress : null;
|
|
3385
|
+
const cycle = progress ?? { complete: true, reachable: true };
|
|
3386
|
+
if (opened === generation)
|
|
3387
|
+
opened = null;
|
|
3388
|
+
const reloading = progress === null ? [] : owedReloads(expired);
|
|
3389
|
+
const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
|
|
3390
|
+
return task(OPENING_LANE, async (live) => {
|
|
1871
3391
|
underway = null;
|
|
1872
|
-
if (
|
|
3392
|
+
if (!live())
|
|
1873
3393
|
return;
|
|
1874
3394
|
if (progress === null) {
|
|
1875
|
-
await
|
|
3395
|
+
await replacing;
|
|
3396
|
+
await rest(cycle, live);
|
|
1876
3397
|
return;
|
|
1877
3398
|
}
|
|
3399
|
+
await roomsCarried([...expired, ...reloading]);
|
|
3400
|
+
if (!live())
|
|
3401
|
+
return;
|
|
1878
3402
|
try {
|
|
1879
3403
|
const local = await openLocal();
|
|
1880
|
-
const
|
|
1881
|
-
const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
|
|
1882
|
-
const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
|
|
3404
|
+
const reloaded = await replacing;
|
|
1883
3405
|
if (progress.reachable) {
|
|
1884
|
-
for (const room of
|
|
1885
|
-
if (
|
|
3406
|
+
for (const room of discovered.values()) {
|
|
3407
|
+
if (replicas.has(room.locator) &&
|
|
3408
|
+
!expired.includes(room.locator) &&
|
|
3409
|
+
!reloading.includes(room.locator)) {
|
|
1886
3410
|
await confirmModels(room, local);
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
1887
3413
|
}
|
|
1888
3414
|
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1889
3415
|
if (failed !== undefined)
|
|
@@ -1893,21 +3419,36 @@ export function composeRuntime(options) {
|
|
|
1893
3419
|
progress.complete = false;
|
|
1894
3420
|
progress.reachable = false;
|
|
1895
3421
|
}
|
|
1896
|
-
|
|
3422
|
+
if (live())
|
|
3423
|
+
await finishOpening(progress, live);
|
|
1897
3424
|
});
|
|
1898
3425
|
}
|
|
1899
|
-
|
|
1900
|
-
const
|
|
3426
|
+
function owedReloads(expired) {
|
|
3427
|
+
const reloading = [...discovered.values()]
|
|
3428
|
+
.filter((room) => replicas.has(room.locator) &&
|
|
3429
|
+
!expired.includes(room.locator) &&
|
|
3430
|
+
(owed.has(room.locator) || revoked(room.locator)))
|
|
3431
|
+
.map((room) => room.locator);
|
|
3432
|
+
owed.clear();
|
|
3433
|
+
for (const locator of reloading)
|
|
3434
|
+
catching.add(locator);
|
|
3435
|
+
readSide.recover();
|
|
3436
|
+
return reloading;
|
|
3437
|
+
}
|
|
3438
|
+
async function reloadNow(replaced, progress) {
|
|
3439
|
+
const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
|
|
3440
|
+
for (const room of reloaded)
|
|
3441
|
+
reloadsDue.set(room.locator, generation);
|
|
1901
3442
|
try {
|
|
1902
3443
|
const local = await openLocal();
|
|
1903
|
-
await Promise.allSettled(
|
|
1904
|
-
.filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
|
|
1905
|
-
.map((room) => reload(room, local, progress)));
|
|
3444
|
+
return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
1906
3445
|
}
|
|
1907
3446
|
catch {
|
|
3447
|
+
for (const room of reloaded)
|
|
3448
|
+
reloadsDue.delete(room.locator);
|
|
1908
3449
|
progress.reachable = false;
|
|
3450
|
+
return [];
|
|
1909
3451
|
}
|
|
1910
|
-
await rest(progress);
|
|
1911
3452
|
}
|
|
1912
3453
|
async function confirmModels(room, local) {
|
|
1913
3454
|
for (const attached of ['collection', 'typed-selection']) {
|
|
@@ -1920,38 +3461,94 @@ export function composeRuntime(options) {
|
|
|
1920
3461
|
function submitLater(locator) {
|
|
1921
3462
|
if (localSaves)
|
|
1922
3463
|
return;
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
3464
|
+
void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
|
|
3465
|
+
}
|
|
3466
|
+
async function submitPass(locator, signal) {
|
|
3467
|
+
const room = discovered.get(locator);
|
|
3468
|
+
if (closed || stopping || discoveryPaused || room === undefined)
|
|
3469
|
+
return 'done';
|
|
3470
|
+
const progress = { complete: true, reachable: true };
|
|
3471
|
+
let next;
|
|
3472
|
+
try {
|
|
3473
|
+
await ensureAuthorization();
|
|
3474
|
+
if (authorization === undefined)
|
|
3475
|
+
return 'done';
|
|
3476
|
+
next = await submitRoom(room, await openLocal(), authorization, progress, signal);
|
|
3477
|
+
}
|
|
3478
|
+
catch {
|
|
3479
|
+
progress.reachable = false;
|
|
3480
|
+
next = 'again';
|
|
3481
|
+
}
|
|
3482
|
+
if (signal.aborted)
|
|
3483
|
+
return 'done';
|
|
3484
|
+
await rest(progress);
|
|
3485
|
+
return next === 'again' && !socketOpen ? 'done' : next;
|
|
3486
|
+
}
|
|
3487
|
+
function repairLater(locator, when) {
|
|
3488
|
+
if (!socketOpen || repairing.has(locator))
|
|
1926
3489
|
return;
|
|
3490
|
+
void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
|
|
3491
|
+
}
|
|
3492
|
+
async function repairPass(locator, signal) {
|
|
3493
|
+
const room = discovered.get(locator);
|
|
3494
|
+
if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
|
|
3495
|
+
return 'done';
|
|
3496
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
3497
|
+
return 'done';
|
|
3498
|
+
const progress = { complete: true, reachable: true };
|
|
3499
|
+
repairing.add(locator);
|
|
3500
|
+
try {
|
|
3501
|
+
const local = await openLocal();
|
|
3502
|
+
await roomTask(locator, async (live) => {
|
|
3503
|
+
const current = () => live() && !signal.aborted;
|
|
3504
|
+
if (!current() || authorization === undefined)
|
|
3505
|
+
return;
|
|
3506
|
+
const withheld = await Promise.race([
|
|
3507
|
+
advance(room, local, authorization, current),
|
|
3508
|
+
abandonedOn(signal),
|
|
3509
|
+
]);
|
|
3510
|
+
if (withheld !== null && current())
|
|
3511
|
+
await withhold(withheld, room, local, progress);
|
|
3512
|
+
});
|
|
1927
3513
|
}
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
3514
|
+
catch {
|
|
3515
|
+
stale.add(locator);
|
|
3516
|
+
progress.reachable = false;
|
|
3517
|
+
}
|
|
3518
|
+
finally {
|
|
3519
|
+
repairing.delete(locator);
|
|
3520
|
+
}
|
|
3521
|
+
if (signal.aborted)
|
|
3522
|
+
return 'done';
|
|
3523
|
+
await rest(progress);
|
|
3524
|
+
if (!stale.has(locator)) {
|
|
3525
|
+
if ((replicas.get(locator)?.queue.length ?? 0) > 0)
|
|
3526
|
+
submitLater(locator);
|
|
3527
|
+
return 'done';
|
|
3528
|
+
}
|
|
3529
|
+
if (!repairable.has(locator))
|
|
3530
|
+
return 'done';
|
|
3531
|
+
const reset = repairable.get(locator);
|
|
3532
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
3533
|
+
}
|
|
3534
|
+
function resendLater() {
|
|
3535
|
+
if (!socketOpen)
|
|
3536
|
+
return;
|
|
3537
|
+
void recovery.attempt('resend', resendPass, { again: true });
|
|
3538
|
+
}
|
|
3539
|
+
async function resendPass(signal) {
|
|
3540
|
+
if (!socketOpen || closed || stopping || discoveryPaused)
|
|
3541
|
+
return 'done';
|
|
3542
|
+
const reading = { complete: true, reachable: true };
|
|
3543
|
+
try {
|
|
3544
|
+
await conversations.resend(reading);
|
|
3545
|
+
}
|
|
3546
|
+
catch {
|
|
3547
|
+
reading.reachable = false;
|
|
3548
|
+
}
|
|
3549
|
+
if (signal.aborted)
|
|
3550
|
+
return 'done';
|
|
3551
|
+
return reading.reachable && reading.complete ? 'done' : 'again';
|
|
1955
3552
|
}
|
|
1956
3553
|
function submitAll() {
|
|
1957
3554
|
for (const [locator, replica] of replicas) {
|
|
@@ -1959,31 +3556,43 @@ export function composeRuntime(options) {
|
|
|
1959
3556
|
submitLater(locator);
|
|
1960
3557
|
}
|
|
1961
3558
|
}
|
|
1962
|
-
async function finishOpening(progress) {
|
|
3559
|
+
async function finishOpening(progress, live) {
|
|
1963
3560
|
try {
|
|
1964
|
-
if (
|
|
3561
|
+
if (!live()) {
|
|
1965
3562
|
progress.complete = false;
|
|
1966
3563
|
return;
|
|
1967
3564
|
}
|
|
3565
|
+
reads.applied();
|
|
1968
3566
|
submitAll();
|
|
3567
|
+
for (const locator of stale)
|
|
3568
|
+
if (repairable.has(locator))
|
|
3569
|
+
repairLater(locator, {});
|
|
1969
3570
|
await conversations.reconcile();
|
|
1970
3571
|
if (status.providerUnreachable)
|
|
1971
3572
|
conversations.reconnected();
|
|
1972
3573
|
const reading = { complete: true, reachable: true };
|
|
1973
3574
|
await conversations.catchUp(reading);
|
|
1974
|
-
if (
|
|
3575
|
+
if (progress.reachable && reading.reachable)
|
|
3576
|
+
await conversations.resend(reading);
|
|
3577
|
+
if (progress.reachable)
|
|
3578
|
+
void recovery.attempt('calls', resendCalls);
|
|
3579
|
+
if (!reading.reachable) {
|
|
1975
3580
|
progress.reachable = false;
|
|
3581
|
+
resendLater();
|
|
3582
|
+
}
|
|
1976
3583
|
}
|
|
1977
3584
|
catch {
|
|
1978
3585
|
progress.complete = false;
|
|
1979
3586
|
}
|
|
1980
3587
|
finally {
|
|
1981
|
-
|
|
1982
|
-
|
|
3588
|
+
if (live()) {
|
|
3589
|
+
complete = progress.complete;
|
|
3590
|
+
await rest(progress, live);
|
|
3591
|
+
}
|
|
1983
3592
|
}
|
|
1984
3593
|
}
|
|
1985
|
-
async function rest(progress) {
|
|
1986
|
-
if (
|
|
3594
|
+
async function rest(progress, live = () => !closed) {
|
|
3595
|
+
if (!live())
|
|
1987
3596
|
return;
|
|
1988
3597
|
let lastCompleted = status.lastCompleted;
|
|
1989
3598
|
let synced = complete && stale.size === 0 && !pending();
|
|
@@ -1991,6 +3600,8 @@ export function composeRuntime(options) {
|
|
|
1991
3600
|
const completedAt = new Date().toISOString();
|
|
1992
3601
|
try {
|
|
1993
3602
|
await (await openLocal()).complete(completedAt);
|
|
3603
|
+
if (!live())
|
|
3604
|
+
return;
|
|
1994
3605
|
recorded = true;
|
|
1995
3606
|
lastCompleted = completedAt;
|
|
1996
3607
|
}
|
|
@@ -2005,11 +3616,23 @@ export function composeRuntime(options) {
|
|
|
2005
3616
|
...attention(),
|
|
2006
3617
|
});
|
|
2007
3618
|
}
|
|
3619
|
+
function discover() {
|
|
3620
|
+
let release = () => { };
|
|
3621
|
+
const done = new Promise((resolve) => {
|
|
3622
|
+
release = resolve;
|
|
3623
|
+
});
|
|
3624
|
+
discovering = { generation, done, release };
|
|
3625
|
+
return discovering;
|
|
3626
|
+
}
|
|
2008
3627
|
function applyRoom(locator, roomVersion) {
|
|
2009
3628
|
announced.set(locator, roomVersion);
|
|
2010
|
-
return task(async () => {
|
|
2011
|
-
|
|
2012
|
-
|
|
3629
|
+
return task(roomLane(locator), async (live) => {
|
|
3630
|
+
if (discovering?.generation === generation)
|
|
3631
|
+
await discovering.done;
|
|
3632
|
+
const saving = answering.get(locator);
|
|
3633
|
+
if (saving?.generation === generation)
|
|
3634
|
+
await saving.answered;
|
|
3635
|
+
if (!live() || authorization === undefined)
|
|
2013
3636
|
return;
|
|
2014
3637
|
const room = discovered.get(locator);
|
|
2015
3638
|
const held = replicas.get(locator);
|
|
@@ -2020,25 +3643,41 @@ export function composeRuntime(options) {
|
|
|
2020
3643
|
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
2021
3644
|
return;
|
|
2022
3645
|
}
|
|
2023
|
-
const during = underway;
|
|
3646
|
+
const during = underway?.generation === generation ? underway.progress : null;
|
|
2024
3647
|
const progress = during ?? { complete: true, reachable: true };
|
|
2025
3648
|
if (during === null)
|
|
2026
3649
|
moveTo(transferring(status));
|
|
2027
3650
|
try {
|
|
2028
3651
|
const local = await openLocal();
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
3652
|
+
await admittedRead(async () => {
|
|
3653
|
+
if (!live() || authorization === undefined)
|
|
3654
|
+
return;
|
|
3655
|
+
const withheld = await advance(room, local, authorization, live);
|
|
3656
|
+
if (withheld !== null)
|
|
3657
|
+
await withhold(withheld, room, local, progress);
|
|
3658
|
+
});
|
|
2032
3659
|
}
|
|
2033
3660
|
catch {
|
|
2034
3661
|
stale.add(locator);
|
|
2035
3662
|
}
|
|
2036
3663
|
finally {
|
|
2037
3664
|
if (during === null)
|
|
2038
|
-
await rest(progress);
|
|
3665
|
+
await rest(progress, live);
|
|
2039
3666
|
}
|
|
2040
3667
|
});
|
|
2041
3668
|
}
|
|
3669
|
+
function roomsCarried(replacing) {
|
|
3670
|
+
const replaced = new Set(replacing.map(roomLane));
|
|
3671
|
+
return Promise.all([...lanes]
|
|
3672
|
+
.filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
|
|
3673
|
+
.map(([, held]) => held.chain));
|
|
3674
|
+
}
|
|
3675
|
+
const receipts = {
|
|
3676
|
+
heads: async () => (await openLocal()).receivedHeads(),
|
|
3677
|
+
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
3678
|
+
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
3679
|
+
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
3680
|
+
};
|
|
2042
3681
|
function observeStream() {
|
|
2043
3682
|
const stream = options.adapter.roomStream;
|
|
2044
3683
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -2048,15 +3687,22 @@ export function composeRuntime(options) {
|
|
|
2048
3687
|
return;
|
|
2049
3688
|
if (event.kind === 'opened') {
|
|
2050
3689
|
socketOpen = true;
|
|
3690
|
+
connectionLive();
|
|
3691
|
+
generation += 1;
|
|
3692
|
+
reads.opened();
|
|
3693
|
+
conversations.reopened();
|
|
3694
|
+
recovery.resume();
|
|
2051
3695
|
void sync('opened');
|
|
2052
3696
|
}
|
|
2053
3697
|
else if (event.kind === 'closed')
|
|
2054
|
-
closedSocket(event.revoked);
|
|
3698
|
+
closedSocket(event.revoked, event.lost);
|
|
2055
3699
|
else if (event.kind === 'opening')
|
|
2056
3700
|
void applyOpening(event.expired);
|
|
3701
|
+
else if (event.kind === 'authority-changed')
|
|
3702
|
+
void rediscover(event.scope);
|
|
2057
3703
|
else
|
|
2058
3704
|
void applyRoom(event.locator, event.roomVersion);
|
|
2059
|
-
});
|
|
3705
|
+
}, receipts);
|
|
2060
3706
|
}
|
|
2061
3707
|
function writesHeld() {
|
|
2062
3708
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -2071,6 +3717,7 @@ export function composeRuntime(options) {
|
|
|
2071
3717
|
haltedRefusal = false;
|
|
2072
3718
|
if (mode !== serviceMode) {
|
|
2073
3719
|
serviceMode = mode;
|
|
3720
|
+
queueReadiness();
|
|
2074
3721
|
journal.record(`service mode ${mode}`);
|
|
2075
3722
|
for (const listener of serviceModeListeners)
|
|
2076
3723
|
listener(mode);
|
|
@@ -2083,6 +3730,8 @@ export function composeRuntime(options) {
|
|
|
2083
3730
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
2084
3731
|
return;
|
|
2085
3732
|
withdrawConversations = stream.observe((event) => {
|
|
3733
|
+
if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
|
|
3734
|
+
queueReadiness();
|
|
2086
3735
|
if (conversations.receive(event)) {
|
|
2087
3736
|
void ready.then(scheduleCurrency);
|
|
2088
3737
|
return;
|
|
@@ -2094,14 +3743,15 @@ export function composeRuntime(options) {
|
|
|
2094
3743
|
});
|
|
2095
3744
|
}
|
|
2096
3745
|
function scheduleCurrency() {
|
|
2097
|
-
if (currencyQueued || closed || stopping)
|
|
3746
|
+
if (currencyQueued === generation || closed || stopping)
|
|
2098
3747
|
return;
|
|
2099
|
-
currencyQueued =
|
|
2100
|
-
void task(runCurrency);
|
|
3748
|
+
currencyQueued = generation;
|
|
3749
|
+
void task(CONVERSATION_LANE, runCurrency);
|
|
2101
3750
|
}
|
|
2102
|
-
async function runCurrency() {
|
|
2103
|
-
currencyQueued
|
|
2104
|
-
|
|
3751
|
+
async function runCurrency(live) {
|
|
3752
|
+
if (currencyQueued === generation)
|
|
3753
|
+
currencyQueued = null;
|
|
3754
|
+
if (!live())
|
|
2105
3755
|
return;
|
|
2106
3756
|
await conversations.reconcile();
|
|
2107
3757
|
if (conversations.current()) {
|
|
@@ -2116,19 +3766,24 @@ export function composeRuntime(options) {
|
|
|
2116
3766
|
catch {
|
|
2117
3767
|
progress.reachable = false;
|
|
2118
3768
|
}
|
|
2119
|
-
await rest(progress);
|
|
3769
|
+
await rest(progress, live);
|
|
2120
3770
|
}
|
|
2121
3771
|
function close() {
|
|
2122
3772
|
if (closed)
|
|
2123
3773
|
return;
|
|
2124
3774
|
closed = true;
|
|
3775
|
+
if (rechecking !== null)
|
|
3776
|
+
clearInterval(rechecking);
|
|
3777
|
+
connectionLive();
|
|
3778
|
+
lockListeners.clear();
|
|
3779
|
+
readinessWatches.clear();
|
|
2125
3780
|
withdrawStream?.();
|
|
2126
3781
|
withdrawStream = undefined;
|
|
2127
3782
|
withdrawConversations?.();
|
|
2128
3783
|
withdrawConversations = undefined;
|
|
2129
3784
|
withdrawServiceMode?.();
|
|
2130
3785
|
conversations.close();
|
|
2131
|
-
|
|
3786
|
+
void recovery.retire();
|
|
2132
3787
|
store?.close();
|
|
2133
3788
|
void opening?.then((opened) => {
|
|
2134
3789
|
opened.close();
|
|
@@ -2146,9 +3801,8 @@ export function composeRuntime(options) {
|
|
|
2146
3801
|
}
|
|
2147
3802
|
async function stop() {
|
|
2148
3803
|
stopping = true;
|
|
2149
|
-
|
|
2150
|
-
await
|
|
2151
|
-
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
3804
|
+
await recovery.retire();
|
|
3805
|
+
await Promise.all(working);
|
|
2152
3806
|
await mutations;
|
|
2153
3807
|
close();
|
|
2154
3808
|
}
|
|
@@ -2167,20 +3821,125 @@ export function composeRuntime(options) {
|
|
|
2167
3821
|
if (options.adapter.roomStream === undefined)
|
|
2168
3822
|
void sync('unstreamed');
|
|
2169
3823
|
function planned(input, reach) {
|
|
2170
|
-
|
|
3824
|
+
const plan = planOf(input, roomFacts(reach), planReach(reach));
|
|
3825
|
+
const named = plan.paths !== null &&
|
|
3826
|
+
plan.rooms !== null &&
|
|
3827
|
+
plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
|
|
3828
|
+
return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
|
|
2171
3829
|
}
|
|
2172
3830
|
function watchPlan(plan, deliver) {
|
|
2173
|
-
|
|
2174
|
-
|
|
3831
|
+
let seeded = false;
|
|
3832
|
+
let pending = true;
|
|
3833
|
+
const shown = () => {
|
|
3834
|
+
const seed = seeds.get(plan.key);
|
|
3835
|
+
if (seed === undefined || seeded || !pending)
|
|
3836
|
+
return;
|
|
3837
|
+
seeded = true;
|
|
3838
|
+
deliver(seededResult(seed));
|
|
3839
|
+
};
|
|
3840
|
+
const watchers = seedWatchers.get(plan.key) ?? new Set();
|
|
3841
|
+
seedWatchers.set(plan.key, watchers);
|
|
3842
|
+
watchers.add(shown);
|
|
3843
|
+
const stop = readSide.watch(plan, identityOf(plan), () => {
|
|
3844
|
+
const cover = covered(plan);
|
|
3845
|
+
awaitPreparation(cover);
|
|
3846
|
+
return seedBehind(plan, cover);
|
|
3847
|
+
}, (message) => {
|
|
3848
|
+
const answered = message;
|
|
3849
|
+
pending = whole(answered) && 'pending' in answered.result;
|
|
3850
|
+
if (pending && seeds.has(plan.key)) {
|
|
3851
|
+
shown();
|
|
3852
|
+
return;
|
|
3853
|
+
}
|
|
3854
|
+
seeded = false;
|
|
3855
|
+
deliver(message);
|
|
3856
|
+
});
|
|
3857
|
+
return () => {
|
|
3858
|
+
watchers.delete(shown);
|
|
3859
|
+
if (watchers.size === 0)
|
|
3860
|
+
seedWatchers.delete(plan.key);
|
|
3861
|
+
stop();
|
|
3862
|
+
};
|
|
2175
3863
|
}
|
|
2176
|
-
async function searched(input, reach) {
|
|
2177
|
-
const
|
|
2178
|
-
const
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
3864
|
+
async function searched(input, reach, locked) {
|
|
3865
|
+
const asked = searchPlanOf(input);
|
|
3866
|
+
const plan = { ...asked, limit: capped(asked.limit) };
|
|
3867
|
+
const filter = {
|
|
3868
|
+
...filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)),
|
|
3869
|
+
locked,
|
|
3870
|
+
};
|
|
3871
|
+
const unlocking = (filter.rooms ?? [])
|
|
3872
|
+
.filter((room) => locked.includes(room))
|
|
3873
|
+
.map((room) => ({ room, awaits: 'unlock' }));
|
|
3874
|
+
for (;;) {
|
|
3875
|
+
const cover = covered(filter);
|
|
3876
|
+
awaitPreparation(cover);
|
|
3877
|
+
const pending = [
|
|
3878
|
+
...unlocking,
|
|
3879
|
+
...('pending' in cover ? cover.pending : []),
|
|
3880
|
+
];
|
|
3881
|
+
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
3882
|
+
const held = covered({
|
|
3883
|
+
...filter,
|
|
3884
|
+
rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
|
|
3885
|
+
});
|
|
3886
|
+
const covering = 'pending' in held ? [] : held.coverage.rooms;
|
|
3887
|
+
const unindexed = covering
|
|
3888
|
+
.filter(({ room }) => !searchIndex.rooms().has(room))
|
|
3889
|
+
.map(({ room }) => room);
|
|
3890
|
+
for (const room of unindexed)
|
|
3891
|
+
pending.push({ room, awaits: 'index' });
|
|
3892
|
+
const listed = conversations.searchable();
|
|
3893
|
+
const named = reach !== null && !reach.conversations
|
|
3894
|
+
? []
|
|
3895
|
+
: (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
|
|
3896
|
+
const waiting = named.filter((one) => !searchIndex.conversations().has(one));
|
|
3897
|
+
for (const conversation of waiting)
|
|
3898
|
+
pending.push({ conversation, awaits: 'index' });
|
|
3899
|
+
if (unindexed.length > 0 || waiting.length > 0)
|
|
3900
|
+
index(unindexed, waiting);
|
|
3901
|
+
const complete = pending.length === 0 && !('pending' in cover);
|
|
3902
|
+
const answering = complete
|
|
3903
|
+
? filter
|
|
3904
|
+
: {
|
|
3905
|
+
...filter,
|
|
3906
|
+
rooms: covering
|
|
3907
|
+
.filter(({ room }) => searchIndex.rooms().has(room))
|
|
3908
|
+
.map(({ room }) => room),
|
|
3909
|
+
};
|
|
3910
|
+
const answered = covered(answering);
|
|
3911
|
+
if ('pending' in answered)
|
|
3912
|
+
continue;
|
|
3913
|
+
const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
|
|
3914
|
+
await readSide.settled();
|
|
3915
|
+
const seen = searchIndex.changes();
|
|
3916
|
+
await searchIndex.caughtUp();
|
|
3917
|
+
const selected = roomsAlone(answering) ? null : await rowsQuery(answering);
|
|
3918
|
+
if (selected !== null && 'pending' in selected)
|
|
3919
|
+
continue;
|
|
3920
|
+
if (searchIndex.changes() !== seen ||
|
|
3921
|
+
JSON.stringify(covered(answering)) !== JSON.stringify(answered))
|
|
3922
|
+
continue;
|
|
3923
|
+
const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms !== null
|
|
3924
|
+
? new Set(answering.rooms)
|
|
3925
|
+
: locked.length === 0
|
|
3926
|
+
? null
|
|
3927
|
+
: new Set(reachedRooms(answering)), plan.collection
|
|
3928
|
+
? new Map(answered.coverage.rooms.map(({ room }) => [
|
|
3929
|
+
room,
|
|
3930
|
+
replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
|
|
3931
|
+
]))
|
|
3932
|
+
: null);
|
|
3933
|
+
const coverage = {
|
|
3934
|
+
rooms: answered.coverage.rooms,
|
|
3935
|
+
conversations: searchedConversations,
|
|
3936
|
+
index: searchIndex.generation(),
|
|
3937
|
+
overlay: answered.coverage.overlay,
|
|
3938
|
+
};
|
|
3939
|
+
return complete
|
|
3940
|
+
? { ...hits, coverage }
|
|
3941
|
+
: { pending, documents: hits.documents, messages: hits.messages, coverage };
|
|
3942
|
+
}
|
|
2184
3943
|
}
|
|
2185
3944
|
const watch = (topic, input, deliver) => {
|
|
2186
3945
|
if (topic === 'room-drafts') {
|
|
@@ -2226,6 +3985,9 @@ export function composeRuntime(options) {
|
|
|
2226
3985
|
presentationListeners.delete(listener);
|
|
2227
3986
|
};
|
|
2228
3987
|
}
|
|
3988
|
+
if (topic === 'readiness') {
|
|
3989
|
+
return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
|
|
3990
|
+
}
|
|
2229
3991
|
if (topic === 'rooms') {
|
|
2230
3992
|
roomListeners.add(deliver);
|
|
2231
3993
|
deliver(listed());
|
|
@@ -2286,7 +4048,7 @@ export function composeRuntime(options) {
|
|
|
2286
4048
|
return listed();
|
|
2287
4049
|
},
|
|
2288
4050
|
query: ((input) => rowsQuery(planned(input, null))),
|
|
2289
|
-
search: (input) => searched(input, null),
|
|
4051
|
+
search: (input) => searched(input, null, []),
|
|
2290
4052
|
types(locator) {
|
|
2291
4053
|
return {
|
|
2292
4054
|
collection: modelTypes(modelOf(locator)),
|
|
@@ -2330,7 +4092,9 @@ export function composeRuntime(options) {
|
|
|
2330
4092
|
parts: held(current?.parts, parts, file),
|
|
2331
4093
|
returned: current?.returned ?? null,
|
|
2332
4094
|
};
|
|
2333
|
-
await local.keepRoomDraft(record
|
|
4095
|
+
await local.keepRoomDraft(record, parts.file === undefined || file === undefined
|
|
4096
|
+
? []
|
|
4097
|
+
: [{ key: file.localId, blob: parts.file.blob }]);
|
|
2334
4098
|
rememberRoomDraft(record);
|
|
2335
4099
|
publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
|
|
2336
4100
|
return { outcome: 'written', roomDraft: roomDraftOf(record) };
|
|
@@ -2361,27 +4125,37 @@ export function composeRuntime(options) {
|
|
|
2361
4125
|
});
|
|
2362
4126
|
},
|
|
2363
4127
|
write(app, value) {
|
|
2364
|
-
return
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
4128
|
+
return keepPresentation(app, () => structuredClone(value));
|
|
4129
|
+
},
|
|
4130
|
+
edit(app, members) {
|
|
4131
|
+
return keepPresentation(app, (held) => Object.fromEntries([
|
|
4132
|
+
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(members, member)),
|
|
4133
|
+
...Object.entries(structuredClone(members)).filter(([, value]) => value !== null),
|
|
4134
|
+
]));
|
|
2371
4135
|
},
|
|
2372
4136
|
},
|
|
2373
4137
|
save(room, named, writtenBy) {
|
|
2374
4138
|
return exclusive(() => saveRoomDrafts(room, named, writtenBy));
|
|
2375
4139
|
},
|
|
4140
|
+
saveParts(room, parts, writtenBy) {
|
|
4141
|
+
return exclusive(() => saveParts(room, parts, writtenBy));
|
|
4142
|
+
},
|
|
2376
4143
|
history(locator, path, query) {
|
|
2377
4144
|
return readHistory(locator, path, query);
|
|
2378
4145
|
},
|
|
4146
|
+
resolve(room, path, destination) {
|
|
4147
|
+
return resolveReference(room, path, destination);
|
|
4148
|
+
},
|
|
2379
4149
|
binaryFile: {
|
|
4150
|
+
part: readPart,
|
|
2380
4151
|
read: readBinaryPath,
|
|
4152
|
+
link: linkBinaryPath,
|
|
2381
4153
|
readReference: readBinaryReference,
|
|
2382
4154
|
},
|
|
2383
|
-
call(name, input) {
|
|
2384
|
-
|
|
4155
|
+
async call(name, input) {
|
|
4156
|
+
await ready;
|
|
4157
|
+
const room = calledRooms(name, input).find((one) => locked(one));
|
|
4158
|
+
return room === undefined ? recoveredAction(name, input) : lockedRoom(room);
|
|
2385
4159
|
},
|
|
2386
4160
|
reached(locator, roomVersion, path) {
|
|
2387
4161
|
return path === undefined
|
|
@@ -2391,22 +4165,31 @@ export function composeRuntime(options) {
|
|
|
2391
4165
|
log() {
|
|
2392
4166
|
return journal.export();
|
|
2393
4167
|
},
|
|
4168
|
+
cleanUp,
|
|
2394
4169
|
persistence() {
|
|
2395
|
-
return
|
|
4170
|
+
return persistence === 'granted';
|
|
4171
|
+
},
|
|
4172
|
+
async retry() {
|
|
4173
|
+
journal.record('retry');
|
|
4174
|
+
unindexable.clear();
|
|
4175
|
+
queueReadiness();
|
|
4176
|
+
resume();
|
|
2396
4177
|
},
|
|
2397
4178
|
observe: observing(watch),
|
|
2398
4179
|
async reauthorize() {
|
|
2399
4180
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2400
4181
|
return;
|
|
2401
4182
|
journal.record('connection reauthorize');
|
|
2402
|
-
await
|
|
4183
|
+
await Promise.all([...lanes.values()]
|
|
4184
|
+
.filter((held) => held.generation === generation)
|
|
4185
|
+
.map((held) => held.chain));
|
|
2403
4186
|
await mutations;
|
|
2404
4187
|
authorization = undefined;
|
|
2405
4188
|
discoveryPaused = false;
|
|
2406
4189
|
if (options.adapter.roomStream === undefined)
|
|
2407
4190
|
await sync('unstreamed');
|
|
2408
4191
|
else
|
|
2409
|
-
await task(ensureAuthorization);
|
|
4192
|
+
await task(OPENING_LANE, ensureAuthorization);
|
|
2410
4193
|
},
|
|
2411
4194
|
};
|
|
2412
4195
|
const reachable = {
|
|
@@ -2417,6 +4200,25 @@ export function composeRuntime(options) {
|
|
|
2417
4200
|
query: (plan) => rowsQuery(plan),
|
|
2418
4201
|
watchPlan,
|
|
2419
4202
|
search: searched,
|
|
4203
|
+
watchReadiness,
|
|
4204
|
+
locked,
|
|
4205
|
+
lockedRooms,
|
|
4206
|
+
watchLocks,
|
|
4207
|
+
located,
|
|
4208
|
+
};
|
|
4209
|
+
return {
|
|
4210
|
+
md,
|
|
4211
|
+
seed,
|
|
4212
|
+
stop,
|
|
4213
|
+
lose: loseRooms,
|
|
4214
|
+
watch,
|
|
4215
|
+
persisted(answer) {
|
|
4216
|
+
if (answer === persistence)
|
|
4217
|
+
return;
|
|
4218
|
+
persistence = answer;
|
|
4219
|
+
journal.record(`persistence ${answer}`);
|
|
4220
|
+
queueReadiness();
|
|
4221
|
+
},
|
|
4222
|
+
within: (reach, port) => narrowed(reachable, reach, port),
|
|
2420
4223
|
};
|
|
2421
|
-
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2422
4224
|
}
|