@agentshouse/mdruntime 0.1.0-cebcd51f70ed1 → 0.1.0-cedb03eaa48b6
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 +283 -222
- package/README.md +20 -10
- package/dist/adapter.d.ts +95 -20
- package/dist/adapter.d.ts.map +1 -1
- package/dist/attached-model.d.ts.map +1 -1
- package/dist/attached-model.js +15 -2
- package/dist/attempt.d.ts +16 -0
- package/dist/attempt.d.ts.map +1 -0
- package/dist/attempt.js +70 -0
- package/dist/binary.d.ts +23 -0
- package/dist/binary.d.ts.map +1 -0
- package/dist/binary.js +8 -0
- package/dist/bootstrap.d.ts +18 -5
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +113 -37
- package/dist/change-set.d.ts +2 -0
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +49 -16
- package/dist/change.d.ts +15 -0
- package/dist/change.d.ts.map +1 -1
- package/dist/change.js +26 -1
- package/dist/chat/acts.d.ts +33 -0
- package/dist/chat/acts.d.ts.map +1 -0
- package/dist/chat/acts.js +36 -0
- package/dist/chat/context.d.ts +3 -0
- package/dist/chat/context.d.ts.map +1 -0
- package/dist/chat/context.js +10 -0
- 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/input.d.ts +9 -0
- package/dist/chat/input.d.ts.map +1 -0
- package/dist/chat/input.js +99 -0
- package/dist/chat/kit.d.ts +61 -0
- package/dist/chat/kit.d.ts.map +1 -0
- package/dist/chat/kit.js +123 -0
- package/dist/chat/lasting.d.ts +6 -0
- package/dist/chat/lasting.d.ts.map +1 -0
- package/dist/chat/lasting.js +19 -0
- package/dist/chat/list.d.ts +27 -0
- package/dist/chat/list.d.ts.map +1 -0
- package/dist/chat/list.js +99 -0
- package/dist/chat/markdown.d.ts +20 -0
- package/dist/chat/markdown.d.ts.map +1 -0
- package/dist/chat/markdown.js +188 -0
- package/dist/chat/message.d.ts +6 -0
- package/dist/chat/message.d.ts.map +1 -0
- package/dist/chat/message.js +192 -0
- package/dist/chat/messages.d.ts +22 -0
- package/dist/chat/messages.d.ts.map +1 -0
- package/dist/chat/messages.js +155 -0
- package/dist/chat/name.d.ts +6 -0
- package/dist/chat/name.d.ts.map +1 -0
- package/dist/chat/name.js +37 -0
- package/dist/chat/opened.d.ts +11 -0
- package/dist/chat/opened.d.ts.map +1 -0
- package/dist/chat/opened.js +17 -0
- package/dist/chat/overlay.d.ts +31 -0
- package/dist/chat/overlay.d.ts.map +1 -0
- package/dist/chat/overlay.js +97 -0
- package/dist/chat/parts.d.ts +57 -0
- package/dist/chat/parts.d.ts.map +1 -0
- package/dist/chat/parts.js +119 -0
- package/dist/chat/process.d.ts +20 -0
- package/dist/chat/process.d.ts.map +1 -0
- package/dist/chat/process.js +32 -0
- package/dist/chat/provider.d.ts +63 -0
- package/dist/chat/provider.d.ts.map +1 -0
- package/dist/chat/provider.js +628 -0
- package/dist/chat/question.d.ts +9 -0
- package/dist/chat/question.d.ts.map +1 -0
- package/dist/chat/question.js +69 -0
- package/dist/chat/questions.d.ts +31 -0
- package/dist/chat/questions.d.ts.map +1 -0
- package/dist/chat/questions.js +111 -0
- package/dist/chat/read.d.ts +5 -0
- package/dist/chat/read.d.ts.map +1 -0
- package/dist/chat/read.js +13 -0
- package/dist/chat/ready.d.ts +34 -0
- package/dist/chat/ready.d.ts.map +1 -0
- package/dist/chat/ready.js +91 -0
- package/dist/chat/style.d.ts +4 -0
- package/dist/chat/style.d.ts.map +1 -0
- package/dist/chat/style.js +302 -0
- package/dist/chat.d.ts +14 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +8 -0
- package/dist/client.d.ts +30 -13
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +79 -16
- package/dist/collection.d.ts +21 -12
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +301 -23
- package/dist/connection.d.ts +50 -16
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +1936 -415
- package/dist/conversation.d.ts +31 -10
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +521 -170
- package/dist/durability.d.ts +7 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/entry.d.ts +17 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +157 -0
- package/dist/expectation.d.ts +21 -0
- package/dist/expectation.d.ts.map +1 -0
- package/dist/expectation.js +70 -0
- package/dist/failure.d.ts +13 -0
- package/dist/failure.d.ts.map +1 -0
- package/dist/failure.js +19 -0
- package/dist/fence.d.ts +6 -0
- package/dist/fence.d.ts.map +1 -0
- package/dist/fence.js +10 -0
- package/dist/host.d.ts +15 -2
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +102 -19
- package/dist/identity.d.ts +5 -0
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +19 -1
- package/dist/index.d.ts +16 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/memory.d.ts +11 -4
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +91 -24
- package/dist/model.d.ts +14 -4
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +12 -1
- package/dist/observation.d.ts +7 -2
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +29 -12
- package/dist/open.d.ts +2 -0
- package/dist/open.d.ts.map +1 -0
- package/dist/open.js +1 -0
- package/dist/outcome.d.ts +38 -3
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +45 -1
- package/dist/overlay.d.ts +16 -3
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +118 -77
- package/dist/query.d.ts +60 -4
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +41 -38
- package/dist/reach.d.ts +8 -3
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +73 -24
- package/dist/react.d.ts +31 -20
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +13 -21
- package/dist/read-side.d.ts +16 -7
- package/dist/read-side.d.ts.map +1 -1
- package/dist/read-side.js +493 -102
- 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 +197 -0
- package/dist/refresh.d.ts +13 -3
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +49 -38
- package/dist/replica.d.ts +13 -6
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +143 -70
- package/dist/room-draft.d.ts +38 -9
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +134 -25
- package/dist/rows.d.ts +10 -13
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +19 -14
- package/dist/save.d.ts +32 -1
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +390 -45
- package/dist/search.d.ts +52 -8
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +192 -47
- 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 +11 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +368 -0
- package/dist/status.d.ts +6 -2
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +0 -8
- package/dist/store.d.ts +89 -18
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +573 -200
- package/dist/turn.d.ts +7 -0
- package/dist/turn.d.ts.map +1 -0
- package/dist/turn.js +51 -0
- package/dist/windows.d.ts +26 -0
- package/dist/windows.d.ts.map +1 -0
- package/dist/windows.js +93 -0
- package/dist/wire.d.ts +157 -33
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +65 -31
- package/package.json +26 -14
- package/dist/attachment.d.ts +0 -3
- package/dist/attachment.d.ts.map +0 -1
- package/dist/attachment.js +0 -8
package/dist/connection.js
CHANGED
|
@@ -1,25 +1,47 @@
|
|
|
1
1
|
import { attachedModel } from './attached-model.js';
|
|
2
2
|
import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
|
|
3
|
-
import { observing } from './observation.js';
|
|
4
|
-
import { filterOf, identityOf, planOf, } from './query.js';
|
|
3
|
+
import { observing, whole } from './observation.js';
|
|
4
|
+
import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
|
|
5
5
|
import { createReadSide } from './read-side.js';
|
|
6
|
+
import { seededResult } from './seed.js';
|
|
7
|
+
import { markdownPath, roomOfKey } from './rows.js';
|
|
6
8
|
import { createSearchIndex, searchPlanOf, } from './search.js';
|
|
9
|
+
import { capped, MAXIMUM_WINDOW } from './windows.js';
|
|
7
10
|
import { bootstrapRoom } from './bootstrap.js';
|
|
8
|
-
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
11
|
+
import { changeSet, entering, mergeChangeSets } from './change-set.js';
|
|
9
12
|
import { createConversationStore, } from './conversation.js';
|
|
10
|
-
import {
|
|
13
|
+
import { queuedFile } from './change.js';
|
|
14
|
+
import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
|
|
15
|
+
import { runtimeError } from './failure.js';
|
|
11
16
|
import { createLog } from './log.js';
|
|
17
|
+
import { binaryRecord } from './binary.js';
|
|
18
|
+
import { absentBinary, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
|
|
19
|
+
import { createRecovery } from './recovery.js';
|
|
20
|
+
import { doorReads } from './reads.js';
|
|
12
21
|
import { refreshRoom } from './refresh.js';
|
|
13
|
-
import { buildBatch } from './save.js';
|
|
14
|
-
import { applyReturned, compositionOf,
|
|
15
|
-
import { changedParts, changesOf, held, readParts, returnedInto, underneath, } from './room-draft.js';
|
|
16
|
-
import {
|
|
22
|
+
import { buildBatch, joined, limitedIn, queuedDocuments, requeued, } from './save.js';
|
|
23
|
+
import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, standing, } from './replica.js';
|
|
24
|
+
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
25
|
+
import { same, saving, STARTING, transferring, } from './status.js';
|
|
17
26
|
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
18
|
-
import {
|
|
27
|
+
import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
|
|
28
|
+
import { FencedMap } from './fence.js';
|
|
29
|
+
import { tally } from './shared.js';
|
|
30
|
+
import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
|
|
31
|
+
import { identityTime, roomDraftRevision } from './identity.js';
|
|
19
32
|
import { beneath, normalize } from './paths.js';
|
|
20
33
|
import { openRuntimeStore, } from './store.js';
|
|
21
34
|
import { unconfirmed } from './transfer.js';
|
|
22
35
|
import { governs, holds, narrowed, planReach, } from './reach.js';
|
|
36
|
+
function roomsAlone(plan) {
|
|
37
|
+
return (plan.reach === null &&
|
|
38
|
+
plan.root.types === null &&
|
|
39
|
+
plan.root.where === null &&
|
|
40
|
+
plan.root.pathPrefix === null &&
|
|
41
|
+
plan.paths === null &&
|
|
42
|
+
plan.hop === null &&
|
|
43
|
+
plan.backlinks === null);
|
|
44
|
+
}
|
|
23
45
|
function within(root, path) {
|
|
24
46
|
return root === '' || beneath(root, path);
|
|
25
47
|
}
|
|
@@ -33,22 +55,68 @@ function declaredSelection(room) {
|
|
|
33
55
|
root !== '' &&
|
|
34
56
|
selection.roots.every((other, at) => at === index || !within(other, root)));
|
|
35
57
|
}
|
|
58
|
+
const OPENING_LANE = 'opening';
|
|
59
|
+
const CONVERSATION_LANE = 'conversations';
|
|
60
|
+
const ROOM_READS = 256;
|
|
61
|
+
const ROOM_ATTEMPTS = 16;
|
|
62
|
+
function operationOf(input) {
|
|
63
|
+
const operation = input?.operation_id;
|
|
64
|
+
return typeof operation === 'string' ? operation : null;
|
|
65
|
+
}
|
|
66
|
+
const outside = new Map();
|
|
67
|
+
function outsideOf(roots) {
|
|
68
|
+
const key = JSON.stringify(roots);
|
|
69
|
+
const held = outside.get(key);
|
|
70
|
+
if (held !== undefined)
|
|
71
|
+
return held;
|
|
72
|
+
const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
|
|
73
|
+
const made = {
|
|
74
|
+
documents: (path) => (markdownPath(path) ? counted(path) : 0),
|
|
75
|
+
binaries: counted,
|
|
76
|
+
};
|
|
77
|
+
outside.set(key, made);
|
|
78
|
+
return made;
|
|
79
|
+
}
|
|
80
|
+
function abandonedOn(signal) {
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
if (signal.aborted)
|
|
83
|
+
resolve(null);
|
|
84
|
+
else
|
|
85
|
+
signal.addEventListener('abort', () => resolve(null), { once: true });
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function roomLane(locator) {
|
|
89
|
+
return `room ${locator}`;
|
|
90
|
+
}
|
|
36
91
|
export function composeRuntime(options) {
|
|
92
|
+
const localSaves = options.adapter.localSaves === true;
|
|
37
93
|
let authorization = options.authorization;
|
|
38
94
|
let opening;
|
|
39
95
|
let store;
|
|
40
|
-
const
|
|
96
|
+
const fences = new Map();
|
|
97
|
+
const isFenced = (locator) => fences.has(locator);
|
|
98
|
+
let fencings = 0;
|
|
99
|
+
const lastFenced = new Map();
|
|
100
|
+
const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
|
|
101
|
+
let discoveries = 0;
|
|
102
|
+
const replicas = new FencedMap(isFenced);
|
|
103
|
+
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
41
104
|
const pendings = new Map();
|
|
42
|
-
const
|
|
105
|
+
const seeds = new Map();
|
|
106
|
+
const seedWatchers = new Map();
|
|
107
|
+
const roomDrafts = new FencedMap(isFenced);
|
|
43
108
|
const presentations = new Map();
|
|
44
109
|
let limits = [];
|
|
45
110
|
let capabilities = [];
|
|
46
|
-
const rooms = new
|
|
111
|
+
const rooms = new FencedMap(isFenced);
|
|
112
|
+
const counted = new Map();
|
|
47
113
|
const unreadable = new Set();
|
|
48
|
-
const
|
|
49
|
-
const
|
|
114
|
+
const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
|
|
115
|
+
const models = new FencedMap(isModelFenced);
|
|
116
|
+
const liveModels = new FencedMap(isModelFenced);
|
|
50
117
|
const invalidModels = new Set();
|
|
51
118
|
const discovered = new Map();
|
|
119
|
+
const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
|
|
52
120
|
let roomsListed = false;
|
|
53
121
|
const scopedRefusals = new Map();
|
|
54
122
|
const sourceRefusals = new Map();
|
|
@@ -76,18 +144,29 @@ export function composeRuntime(options) {
|
|
|
76
144
|
let status = STARTING;
|
|
77
145
|
let shown = STARTING;
|
|
78
146
|
let discoveryPaused = false;
|
|
79
|
-
let resetTimer;
|
|
80
147
|
let closed = false;
|
|
81
148
|
let stopping = false;
|
|
82
149
|
let withdrawStream;
|
|
83
150
|
const announced = new Map();
|
|
84
151
|
let underway = null;
|
|
85
|
-
|
|
152
|
+
let streamed = null;
|
|
153
|
+
let handed = false;
|
|
154
|
+
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
155
|
+
const repairing = new Set();
|
|
156
|
+
const calling = new Set();
|
|
157
|
+
const repairable = new Map();
|
|
158
|
+
let outcomeRetention = null;
|
|
86
159
|
const answering = new Map();
|
|
87
160
|
let complete = false;
|
|
88
161
|
let recorded = false;
|
|
89
162
|
const stale = new Set();
|
|
90
|
-
let
|
|
163
|
+
let generation = 0;
|
|
164
|
+
let discovering = null;
|
|
165
|
+
let opened = null;
|
|
166
|
+
const owed = new Set();
|
|
167
|
+
const lanes = new Map();
|
|
168
|
+
let roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
169
|
+
const working = new Set();
|
|
91
170
|
let mutations = Promise.resolve();
|
|
92
171
|
const roomListeners = new Set();
|
|
93
172
|
const refusalListeners = new Set();
|
|
@@ -98,27 +177,47 @@ export function composeRuntime(options) {
|
|
|
98
177
|
const presentationListeners = new Set();
|
|
99
178
|
const readSide = createReadSide();
|
|
100
179
|
let withdrawConversations;
|
|
101
|
-
let currencyQueued =
|
|
180
|
+
let currencyQueued = null;
|
|
102
181
|
let socketOpen = false;
|
|
103
182
|
let serviceMode = 'open';
|
|
104
183
|
let haltedRefusal = false;
|
|
105
184
|
let openSignals = 0;
|
|
106
185
|
const serviceModeListeners = new Set();
|
|
186
|
+
let member = null;
|
|
187
|
+
const memberListeners = new Set();
|
|
188
|
+
const prepared = new Set();
|
|
189
|
+
const projecting = new Set();
|
|
190
|
+
const unpreparedDemand = new Set();
|
|
191
|
+
const unindexable = new Set();
|
|
192
|
+
let preparing = false;
|
|
193
|
+
let targets = {};
|
|
194
|
+
let persistence = options.shell.persistence;
|
|
195
|
+
let capacity = null;
|
|
196
|
+
const paused = new Set();
|
|
197
|
+
let rechecking = null;
|
|
198
|
+
const readinessWatches = new Set();
|
|
199
|
+
let readinessQueued = false;
|
|
200
|
+
const catching = new Set();
|
|
201
|
+
const transfers = new FencedMap(isFenced);
|
|
107
202
|
const conversations = createConversationStore({
|
|
108
203
|
agents: declaredAgents,
|
|
109
|
-
documents: (locator) =>
|
|
204
|
+
documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
|
|
110
205
|
call: (name, input) => invokeAction(name, input),
|
|
111
206
|
durably,
|
|
112
207
|
stream: () => options.adapter.conversations,
|
|
113
208
|
publish: publishChangeSet,
|
|
209
|
+
online: () => connectionOf() !== 'unreachable' && refusal === null,
|
|
114
210
|
closed: () => closed,
|
|
211
|
+
unanswered: resendLater,
|
|
212
|
+
expired: (operation) => pastHorizon(operation),
|
|
115
213
|
});
|
|
116
214
|
const searchIndex = createSearchIndex({
|
|
117
|
-
|
|
215
|
+
room: (room) => readSide.room(room),
|
|
118
216
|
document: (room, path) => readSide.document(room, path),
|
|
119
|
-
messages: () => conversations.messages(),
|
|
120
|
-
message: (conversation, identity) => conversations.message(conversation, identity),
|
|
121
217
|
});
|
|
218
|
+
const roomsToIndex = new Set();
|
|
219
|
+
const conversationsToIndex = new Set();
|
|
220
|
+
let indexing = false;
|
|
122
221
|
const journal = createLog();
|
|
123
222
|
let notifyReady = () => { };
|
|
124
223
|
const ready = new Promise((resolve) => {
|
|
@@ -173,6 +272,15 @@ export function composeRuntime(options) {
|
|
|
173
272
|
const value = presentations.get(app);
|
|
174
273
|
return value === undefined ? null : structuredClone(value);
|
|
175
274
|
}
|
|
275
|
+
function keepPresentation(app, next) {
|
|
276
|
+
return exclusive(async () => {
|
|
277
|
+
const local = await openLocal();
|
|
278
|
+
const stored = next(presentations.get(app));
|
|
279
|
+
await local.present({ app, value: stored });
|
|
280
|
+
presentations.set(app, stored);
|
|
281
|
+
publishChangeSet(changeSet('', null, null, { presentation: app }));
|
|
282
|
+
});
|
|
283
|
+
}
|
|
176
284
|
function emitPresentation(app) {
|
|
177
285
|
if (closed)
|
|
178
286
|
return;
|
|
@@ -182,7 +290,9 @@ export function composeRuntime(options) {
|
|
|
182
290
|
}
|
|
183
291
|
}
|
|
184
292
|
function pending() {
|
|
185
|
-
return
|
|
293
|
+
return (!localSaves &&
|
|
294
|
+
(queuesWithoutReplica.size > 0 ||
|
|
295
|
+
[...replicas.values()].some((replica) => replica.queue.length > 0)));
|
|
186
296
|
}
|
|
187
297
|
function attention() {
|
|
188
298
|
const attentionRequired = [];
|
|
@@ -195,9 +305,16 @@ export function composeRuntime(options) {
|
|
|
195
305
|
returned: structuredClone(record.returned),
|
|
196
306
|
});
|
|
197
307
|
}
|
|
308
|
+
for (const [room, refusal] of sourceRefusals)
|
|
309
|
+
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
198
310
|
attentionRequired.sort((left, right) => {
|
|
199
311
|
if (left.room !== right.room)
|
|
200
312
|
return left.room < right.room ? -1 : 1;
|
|
313
|
+
if ('refusal' in left || 'refusal' in right) {
|
|
314
|
+
if ('refusal' in left && 'refusal' in right)
|
|
315
|
+
return 0;
|
|
316
|
+
return 'refusal' in left ? -1 : 1;
|
|
317
|
+
}
|
|
201
318
|
if (left.path !== right.path)
|
|
202
319
|
return left.path < right.path ? -1 : 1;
|
|
203
320
|
return 0;
|
|
@@ -211,8 +328,21 @@ export function composeRuntime(options) {
|
|
|
211
328
|
}
|
|
212
329
|
return { attentionRequired, reset };
|
|
213
330
|
}
|
|
331
|
+
function documentsIn(locator) {
|
|
332
|
+
const replica = replicas.get(locator);
|
|
333
|
+
if (replica === undefined)
|
|
334
|
+
return 0;
|
|
335
|
+
const roots = replica.confirmed.record.typedSelection?.roots ?? [];
|
|
336
|
+
const weighed = outsideOf(roots);
|
|
337
|
+
return (tally(replica.visible.documents, weighed.documents) +
|
|
338
|
+
tally(replica.visible.binaries, weighed.binaries));
|
|
339
|
+
}
|
|
214
340
|
function listed() {
|
|
215
|
-
return structuredClone([...rooms.values()])
|
|
341
|
+
return structuredClone([...rooms.values()].map((room) => {
|
|
342
|
+
const documents = documentsIn(room.locator);
|
|
343
|
+
counted.set(room.locator, documents);
|
|
344
|
+
return { ...room, documents };
|
|
345
|
+
}));
|
|
216
346
|
}
|
|
217
347
|
function roomFacts(reach = null) {
|
|
218
348
|
const held = [...rooms.values()]
|
|
@@ -232,18 +362,26 @@ export function composeRuntime(options) {
|
|
|
232
362
|
return { listed: roomsListed, rooms: held };
|
|
233
363
|
}
|
|
234
364
|
function emitRooms() {
|
|
365
|
+
readSide.recover();
|
|
366
|
+
queueReadiness();
|
|
235
367
|
const value = listed();
|
|
236
368
|
for (const listener of roomListeners)
|
|
237
369
|
listener(value);
|
|
238
370
|
}
|
|
239
371
|
function emitRefusal() {
|
|
372
|
+
queueReadiness();
|
|
240
373
|
for (const listener of refusalListeners)
|
|
241
374
|
listener(refusal);
|
|
242
375
|
}
|
|
243
376
|
function emitStorageLoss() {
|
|
377
|
+
queueReadiness();
|
|
244
378
|
for (const listener of storageLossListeners)
|
|
245
379
|
listener(storageLoss);
|
|
246
380
|
}
|
|
381
|
+
function emitMember() {
|
|
382
|
+
for (const listener of memberListeners)
|
|
383
|
+
listener(member);
|
|
384
|
+
}
|
|
247
385
|
function moveTo(next) {
|
|
248
386
|
if (closed)
|
|
249
387
|
return;
|
|
@@ -253,6 +391,7 @@ export function composeRuntime(options) {
|
|
|
253
391
|
function show() {
|
|
254
392
|
if (closed)
|
|
255
393
|
return;
|
|
394
|
+
queueReadiness();
|
|
256
395
|
const next = status.state === 'synced' && !conversations.current()
|
|
257
396
|
? { ...status, state: 'syncing' }
|
|
258
397
|
: status;
|
|
@@ -263,6 +402,8 @@ export function composeRuntime(options) {
|
|
|
263
402
|
listener(shown);
|
|
264
403
|
}
|
|
265
404
|
function entryOf(batch) {
|
|
405
|
+
if (localSaves)
|
|
406
|
+
return Object.freeze({ batch, state: 'local' });
|
|
266
407
|
const state = pendings.get(batch.id);
|
|
267
408
|
if (state?.accepted !== undefined) {
|
|
268
409
|
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
@@ -270,10 +411,7 @@ export function composeRuntime(options) {
|
|
|
270
411
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
271
412
|
}
|
|
272
413
|
function entriesOf(locator) {
|
|
273
|
-
|
|
274
|
-
if (replica === undefined)
|
|
275
|
-
return [];
|
|
276
|
-
return replica.queue.map(entryOf);
|
|
414
|
+
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
|
|
277
415
|
}
|
|
278
416
|
function emitQueue(locator) {
|
|
279
417
|
const entries = entriesOf(locator);
|
|
@@ -283,12 +421,464 @@ export function composeRuntime(options) {
|
|
|
283
421
|
}
|
|
284
422
|
}
|
|
285
423
|
function applyChangeSet(set) {
|
|
286
|
-
|
|
287
|
-
|
|
424
|
+
const held = {
|
|
425
|
+
...set,
|
|
426
|
+
documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
|
|
427
|
+
};
|
|
428
|
+
const applied = readSide.apply(held, () => searchIndex.changed(held));
|
|
429
|
+
if (applied instanceof Promise)
|
|
430
|
+
void applied.catch(unpublished);
|
|
431
|
+
}
|
|
432
|
+
function unpublished(error) {
|
|
433
|
+
journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
|
|
434
|
+
}
|
|
435
|
+
async function referenced(locator) {
|
|
436
|
+
for (;;) {
|
|
437
|
+
const record = replicas.get(locator)?.confirmed.record;
|
|
438
|
+
if (record === undefined || closed)
|
|
439
|
+
return;
|
|
440
|
+
const held = { text: record.documents, binaries: record.binaries };
|
|
441
|
+
const composition = compositionOf(record);
|
|
442
|
+
if (referencesIndexed(held, composition))
|
|
443
|
+
return;
|
|
444
|
+
await indexReferrers(held, composition);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
async function prepareReplica(locator) {
|
|
448
|
+
await referenced(locator);
|
|
449
|
+
const replica = replicas.get(locator);
|
|
450
|
+
if (replica === undefined || closed || prepared.has(locator))
|
|
451
|
+
return;
|
|
452
|
+
const visible = replica.visible;
|
|
453
|
+
const paths = [
|
|
454
|
+
...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
|
|
455
|
+
];
|
|
456
|
+
const entered = [];
|
|
457
|
+
projecting.add(locator);
|
|
458
|
+
try {
|
|
459
|
+
await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
|
|
460
|
+
const current = replicas.get(locator);
|
|
461
|
+
if (current === undefined || closed)
|
|
462
|
+
return;
|
|
463
|
+
const set = entering(locator, current, chunk);
|
|
464
|
+
entered.push(set);
|
|
465
|
+
return readSide.apply(set);
|
|
466
|
+
});
|
|
467
|
+
if (!replicas.has(locator) || closed)
|
|
468
|
+
return;
|
|
469
|
+
prepared.add(locator);
|
|
470
|
+
}
|
|
471
|
+
finally {
|
|
472
|
+
projecting.delete(locator);
|
|
473
|
+
}
|
|
474
|
+
conversations.changed(mergeChangeSets(entered));
|
|
475
|
+
readSide.recover();
|
|
476
|
+
queueReadiness();
|
|
477
|
+
}
|
|
478
|
+
function unprepared() {
|
|
479
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
|
|
480
|
+
}
|
|
481
|
+
function unbuilt(locator, error) {
|
|
482
|
+
unindexable.add(locator);
|
|
483
|
+
journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
|
|
484
|
+
queueReadiness();
|
|
485
|
+
}
|
|
486
|
+
function prepare(locators = []) {
|
|
487
|
+
for (const locator of locators)
|
|
488
|
+
unpreparedDemand.add(locator);
|
|
489
|
+
if (preparing)
|
|
490
|
+
return;
|
|
491
|
+
preparing = true;
|
|
492
|
+
void (async () => {
|
|
493
|
+
try {
|
|
494
|
+
for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
|
|
495
|
+
await yielded();
|
|
496
|
+
if (closed)
|
|
497
|
+
return;
|
|
498
|
+
unpreparedDemand.delete(waiting);
|
|
499
|
+
await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
finally {
|
|
503
|
+
preparing = false;
|
|
504
|
+
}
|
|
505
|
+
})();
|
|
506
|
+
}
|
|
507
|
+
function reachedRooms(plan) {
|
|
508
|
+
const reach = plan.reach;
|
|
509
|
+
const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
510
|
+
const named = plan.rooms ??
|
|
511
|
+
(reached === null
|
|
512
|
+
? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
|
|
513
|
+
: [...reached]);
|
|
514
|
+
return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
|
|
515
|
+
}
|
|
516
|
+
function awaited(locator) {
|
|
517
|
+
return (!roomsListed ||
|
|
518
|
+
(rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
|
|
519
|
+
}
|
|
520
|
+
function covered(plan) {
|
|
521
|
+
const locators = reachedRooms(plan);
|
|
522
|
+
if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
|
|
523
|
+
return { pending: [] };
|
|
524
|
+
}
|
|
525
|
+
const pending = [];
|
|
526
|
+
const coverage = [];
|
|
527
|
+
const overlay = [];
|
|
528
|
+
for (const locator of locators) {
|
|
529
|
+
const replica = replicas.get(locator);
|
|
530
|
+
if (replica === undefined) {
|
|
531
|
+
if (awaited(locator))
|
|
532
|
+
pending.push({ room: locator, awaits: 'acquisition' });
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (!prepared.has(locator)) {
|
|
536
|
+
pending.push({ room: locator, awaits: 'preparation' });
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
coverage.push({
|
|
540
|
+
room: locator,
|
|
541
|
+
generation: replica.confirmed.record.transfer,
|
|
542
|
+
current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
|
|
543
|
+
});
|
|
544
|
+
for (const batch of replica.queue)
|
|
545
|
+
overlay.push(batch.id);
|
|
546
|
+
}
|
|
547
|
+
if (pending.length > 0)
|
|
548
|
+
return { pending };
|
|
549
|
+
return { coverage: { rooms: coverage, indexes: [], overlay } };
|
|
550
|
+
}
|
|
551
|
+
function awaitPreparation(cover) {
|
|
552
|
+
if (!('pending' in cover))
|
|
553
|
+
return;
|
|
554
|
+
const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
|
|
555
|
+
if (waiting.length > 0)
|
|
556
|
+
prepare(waiting.map(({ room }) => room));
|
|
557
|
+
}
|
|
558
|
+
function requiredRooms(reach) {
|
|
559
|
+
if (reach === null)
|
|
560
|
+
return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
|
|
561
|
+
const named = [...new Set(reach.rooms)].sort();
|
|
562
|
+
if (roomsListed)
|
|
563
|
+
return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
|
|
564
|
+
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
565
|
+
}
|
|
566
|
+
function roomState(locator, search) {
|
|
567
|
+
if (sourceRefusals.has(locator))
|
|
568
|
+
return 'refused';
|
|
569
|
+
const listedRoom = discovered.get(locator);
|
|
570
|
+
if (roomsListed &&
|
|
571
|
+
(listedRoom === undefined ||
|
|
572
|
+
listedRoom.replica.admission !== 'admitted' ||
|
|
573
|
+
unreadable.has(locator)))
|
|
574
|
+
return 'unadmitted';
|
|
575
|
+
if (!replicas.has(locator))
|
|
576
|
+
return 'acquiring';
|
|
577
|
+
if (!prepared.has(locator))
|
|
578
|
+
return 'preparing';
|
|
579
|
+
if (search && !searchIndex.rooms().has(locator))
|
|
580
|
+
return 'indexing';
|
|
581
|
+
return 'complete';
|
|
582
|
+
}
|
|
583
|
+
function conversationCoverage(reach, search) {
|
|
584
|
+
const declared = reach === null || reach.conversations;
|
|
585
|
+
if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
|
|
586
|
+
return 'undeclared';
|
|
587
|
+
if (!conversations.known() || !conversations.current())
|
|
588
|
+
return 'acquiring';
|
|
589
|
+
if (!search)
|
|
590
|
+
return 'complete';
|
|
591
|
+
const unindexed = conversations
|
|
592
|
+
.searchable()
|
|
593
|
+
.filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
|
|
594
|
+
if (unindexed.length === 0)
|
|
595
|
+
return 'complete';
|
|
596
|
+
index([], unindexed);
|
|
597
|
+
return 'indexing';
|
|
598
|
+
}
|
|
599
|
+
function connectionOf() {
|
|
600
|
+
if (socketOpen)
|
|
601
|
+
return 'connected';
|
|
602
|
+
if (status.providerUnreachable)
|
|
603
|
+
return 'unreachable';
|
|
604
|
+
return options.adapter.roomStream === undefined && status.lastCompleted !== null
|
|
605
|
+
? 'connected'
|
|
606
|
+
: 'connecting';
|
|
607
|
+
}
|
|
608
|
+
function refusedIn(locators) {
|
|
609
|
+
return locators.flatMap((room) => {
|
|
610
|
+
const held = sourceRefusals.get(room);
|
|
611
|
+
if (held === undefined)
|
|
612
|
+
return [];
|
|
613
|
+
const { file, location } = held.fields;
|
|
614
|
+
return [
|
|
615
|
+
{
|
|
616
|
+
room,
|
|
617
|
+
file: typeof file === 'string' ? file : '',
|
|
618
|
+
rule: held.code,
|
|
619
|
+
location: typeof location === 'string' ? location : null,
|
|
620
|
+
},
|
|
621
|
+
];
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
function readinessFor(port, reach) {
|
|
625
|
+
const required = requiredRooms(reach);
|
|
626
|
+
const search = port.search === 'required';
|
|
627
|
+
const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
|
|
628
|
+
const building = (state) => coverage
|
|
629
|
+
.filter((room) => room.state === state && !unindexable.has(room.room))
|
|
630
|
+
.map(({ room }) => room);
|
|
631
|
+
const preparing = building('preparing');
|
|
632
|
+
if (preparing.length > 0)
|
|
633
|
+
prepare(preparing);
|
|
634
|
+
const indexing = building('indexing');
|
|
635
|
+
if (indexing.length > 0)
|
|
636
|
+
index(indexing, []);
|
|
637
|
+
return readinessOf({
|
|
638
|
+
code: port.code,
|
|
639
|
+
coverage: {
|
|
640
|
+
listed: required.listed,
|
|
641
|
+
rooms: coverage,
|
|
642
|
+
conversations: conversationCoverage(reach, search),
|
|
643
|
+
search: search ? 'required' : 'unused',
|
|
644
|
+
},
|
|
645
|
+
persistence,
|
|
646
|
+
connection: connectionOf(),
|
|
647
|
+
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
648
|
+
capacity: capacity !== null &&
|
|
649
|
+
coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
|
|
650
|
+
? capacity
|
|
651
|
+
: port.capacity,
|
|
652
|
+
refused: refusedIn(required.rooms),
|
|
653
|
+
lost: storageLoss !== null,
|
|
654
|
+
established: targets[port.target]?.established === true,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
function establish(name) {
|
|
658
|
+
targets = { ...targets, [name]: { established: true } };
|
|
659
|
+
queueReadiness();
|
|
660
|
+
void durably((local) => local.keepTargets(targets)).catch(() => {
|
|
661
|
+
journal.record(`target record failed ${name}`);
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
function deliverReadiness(watch) {
|
|
665
|
+
if (!readinessWatches.has(watch))
|
|
666
|
+
return;
|
|
667
|
+
const readiness = readinessFor(watch.port, watch.reach);
|
|
668
|
+
if (readiness.ready && targets[watch.port.target]?.established !== true) {
|
|
669
|
+
establish(watch.port.target);
|
|
670
|
+
}
|
|
671
|
+
const shown = JSON.stringify(readiness);
|
|
672
|
+
if (shown === watch.last)
|
|
673
|
+
return;
|
|
674
|
+
watch.last = shown;
|
|
675
|
+
watch.deliver(structuredClone(readiness));
|
|
676
|
+
}
|
|
677
|
+
function queueReadiness() {
|
|
678
|
+
if (readinessQueued || closed || readinessWatches.size === 0)
|
|
679
|
+
return;
|
|
680
|
+
readinessQueued = true;
|
|
681
|
+
queueMicrotask(() => {
|
|
682
|
+
readinessQueued = false;
|
|
683
|
+
if (closed)
|
|
684
|
+
return;
|
|
685
|
+
for (const watch of [...readinessWatches])
|
|
686
|
+
deliverReadiness(watch);
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
function watchReadiness(port, reach, deliver) {
|
|
690
|
+
const watch = { port, reach, deliver, last: null };
|
|
691
|
+
readinessWatches.add(watch);
|
|
692
|
+
void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
|
|
693
|
+
return () => {
|
|
694
|
+
readinessWatches.delete(watch);
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
function layerBytes(layer) {
|
|
698
|
+
let bytes = 0;
|
|
699
|
+
for (const [path, text] of Object.entries(layer.documents))
|
|
700
|
+
bytes += (path.length + text.length) * 2;
|
|
701
|
+
return bytes;
|
|
702
|
+
}
|
|
703
|
+
function grownBy(next, previous) {
|
|
704
|
+
if (previous === undefined)
|
|
705
|
+
return layerBytes(next);
|
|
706
|
+
const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
|
|
707
|
+
return layerBytes({ ...next, documents });
|
|
708
|
+
}
|
|
709
|
+
function exceeded(error) {
|
|
710
|
+
return (typeof error === 'object' &&
|
|
711
|
+
error !== null &&
|
|
712
|
+
error.name === 'QuotaExceededError');
|
|
713
|
+
}
|
|
714
|
+
async function estimated() {
|
|
715
|
+
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
716
|
+
return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
|
|
717
|
+
}
|
|
718
|
+
function pause(locator, cause) {
|
|
719
|
+
paused.add(locator);
|
|
720
|
+
capacity = cause;
|
|
721
|
+
journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
|
|
722
|
+
queueReadiness();
|
|
723
|
+
if (rechecking !== null)
|
|
724
|
+
return;
|
|
725
|
+
// A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
|
|
726
|
+
rechecking = setInterval(() => {
|
|
727
|
+
void recheck();
|
|
728
|
+
}, 60_000);
|
|
729
|
+
}
|
|
730
|
+
async function recheck() {
|
|
731
|
+
const held = capacity;
|
|
732
|
+
if (held === null || closed)
|
|
733
|
+
return;
|
|
734
|
+
const available = await estimated().catch(() => null);
|
|
735
|
+
if (available === null || available < (held.needed ?? 0))
|
|
736
|
+
return;
|
|
737
|
+
if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
|
|
738
|
+
return;
|
|
739
|
+
resume();
|
|
740
|
+
}
|
|
741
|
+
function resume() {
|
|
742
|
+
if (rechecking !== null)
|
|
743
|
+
clearInterval(rechecking);
|
|
744
|
+
rechecking = null;
|
|
745
|
+
if (paused.size === 0 && capacity === null)
|
|
746
|
+
return;
|
|
747
|
+
paused.clear();
|
|
748
|
+
capacity = null;
|
|
749
|
+
journal.record('capacity resumed');
|
|
750
|
+
queueReadiness();
|
|
751
|
+
if (!closed && authorization !== undefined)
|
|
752
|
+
void sync('unstreamed');
|
|
753
|
+
}
|
|
754
|
+
async function reclaimed(local) {
|
|
755
|
+
const unreferenced = [...transfers.keys()].filter((locator) => !discovered.has(locator));
|
|
756
|
+
for (const locator of unreferenced) {
|
|
757
|
+
await local.abandon(locator);
|
|
758
|
+
transfers.delete(locator);
|
|
759
|
+
}
|
|
760
|
+
if (unreferenced.length > 0)
|
|
761
|
+
journal.record(`capacity reclaimed ${unreferenced.length}`);
|
|
762
|
+
return unreferenced.length > 0;
|
|
763
|
+
}
|
|
764
|
+
async function admitted(local, locator, needed) {
|
|
765
|
+
if (paused.has(locator))
|
|
766
|
+
return false;
|
|
767
|
+
let available = await estimated().catch(() => null);
|
|
768
|
+
if (available === null || available >= needed)
|
|
769
|
+
return true;
|
|
770
|
+
if (await reclaimed(local))
|
|
771
|
+
available = await estimated().catch(() => null);
|
|
772
|
+
if (available === null || available >= needed)
|
|
773
|
+
return true;
|
|
774
|
+
pause(locator, { cause: 'insufficient-space', needed, available });
|
|
775
|
+
return false;
|
|
776
|
+
}
|
|
777
|
+
async function exceededOn(locator, bytes) {
|
|
778
|
+
const available = await estimated().catch(() => null);
|
|
779
|
+
pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
|
|
780
|
+
}
|
|
781
|
+
function seedRoomHeld(room, version) {
|
|
782
|
+
return prepared.has(room) && reachedBy(room, version);
|
|
783
|
+
}
|
|
784
|
+
function seedHeld(at) {
|
|
785
|
+
return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
|
|
786
|
+
}
|
|
787
|
+
function seedApplies(plan) {
|
|
788
|
+
const seed = seeds.get(plan.key);
|
|
789
|
+
if (seed === undefined)
|
|
790
|
+
return null;
|
|
791
|
+
if (!seedHeld(seed.at))
|
|
792
|
+
return seed;
|
|
793
|
+
seeds.delete(plan.key);
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
function seedBehind(plan, cover) {
|
|
797
|
+
const seed = seedApplies(plan);
|
|
798
|
+
if (seed === null || 'pending' in cover)
|
|
799
|
+
return cover;
|
|
800
|
+
return {
|
|
801
|
+
pending: Object.entries(seed.at)
|
|
802
|
+
.filter(([room, version]) => !seedRoomHeld(room, version))
|
|
803
|
+
.map(([room]) => ({ room, awaits: 'acquisition' })),
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
function seed(entry) {
|
|
807
|
+
if (seedHeld(entry.at))
|
|
808
|
+
return;
|
|
809
|
+
for (const { query, rows } of entry.queries) {
|
|
810
|
+
const key = queryKey(query);
|
|
811
|
+
seeds.set(key, { rows, at: entry.at });
|
|
812
|
+
for (const shown of seedWatchers.get(key) ?? [])
|
|
813
|
+
shown();
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
async function rowsQuery(plan) {
|
|
817
|
+
const before = covered(plan);
|
|
818
|
+
awaitPreparation(before);
|
|
819
|
+
const seeded = seedApplies(plan);
|
|
820
|
+
if (seeded !== null)
|
|
821
|
+
return seededResult(seeded).result;
|
|
822
|
+
if ('pending' in before)
|
|
823
|
+
return before;
|
|
824
|
+
const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
|
|
825
|
+
awaitPreparation(cover);
|
|
826
|
+
if ('pending' in cover)
|
|
827
|
+
return cover;
|
|
828
|
+
return { ...rows, coverage: cover.coverage };
|
|
829
|
+
}
|
|
830
|
+
async function indexConversation(conversation) {
|
|
831
|
+
searchIndex.indexMessages(conversation, [], false);
|
|
832
|
+
let after = null;
|
|
833
|
+
for (;;) {
|
|
834
|
+
const batch = await durably((local) => local.messagesAfter(conversation, after));
|
|
835
|
+
if (closed)
|
|
836
|
+
return;
|
|
837
|
+
searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
|
|
838
|
+
if (batch.length === 0)
|
|
839
|
+
return;
|
|
840
|
+
after = batch[batch.length - 1].identity;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
function indexNext() {
|
|
844
|
+
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
|
|
845
|
+
if (room !== undefined) {
|
|
846
|
+
roomsToIndex.delete(room);
|
|
847
|
+
return () => prepared.has(room)
|
|
848
|
+
? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
|
|
849
|
+
: undefined;
|
|
850
|
+
}
|
|
851
|
+
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
852
|
+
if (conversation === undefined)
|
|
853
|
+
return null;
|
|
854
|
+
conversationsToIndex.delete(conversation);
|
|
855
|
+
return () => indexConversation(conversation);
|
|
856
|
+
}
|
|
857
|
+
function index(rooms, named) {
|
|
858
|
+
for (const room of rooms)
|
|
859
|
+
roomsToIndex.add(room);
|
|
860
|
+
for (const conversation of named)
|
|
861
|
+
conversationsToIndex.add(conversation);
|
|
862
|
+
if (indexing)
|
|
863
|
+
return;
|
|
864
|
+
indexing = true;
|
|
865
|
+
void (async () => {
|
|
866
|
+
try {
|
|
867
|
+
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
868
|
+
await yielded();
|
|
869
|
+
await unit();
|
|
870
|
+
queueReadiness();
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
finally {
|
|
874
|
+
indexing = false;
|
|
875
|
+
}
|
|
876
|
+
})().catch(() => { });
|
|
288
877
|
}
|
|
289
878
|
function publishChangeSet(set) {
|
|
290
879
|
applyChangeSet(set);
|
|
291
|
-
if (set.rooms.length > 0
|
|
880
|
+
if (set.rooms.length > 0 ||
|
|
881
|
+
[...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
|
|
292
882
|
emitRooms();
|
|
293
883
|
for (const locator of set.queues)
|
|
294
884
|
emitQueue(locator);
|
|
@@ -300,34 +890,7 @@ export function composeRuntime(options) {
|
|
|
300
890
|
emitPresentation(app);
|
|
301
891
|
conversations.changed(set);
|
|
302
892
|
}
|
|
303
|
-
function stopTimer() {
|
|
304
|
-
if (resetTimer === undefined)
|
|
305
|
-
return;
|
|
306
|
-
clearTimeout(resetTimer);
|
|
307
|
-
resetTimer = undefined;
|
|
308
|
-
}
|
|
309
|
-
function armTimer() {
|
|
310
|
-
stopTimer();
|
|
311
|
-
if (closed || stopping)
|
|
312
|
-
return;
|
|
313
|
-
let earliest = null;
|
|
314
|
-
for (const { submission } of pendings.values()) {
|
|
315
|
-
if (submission.state !== 'reset')
|
|
316
|
-
continue;
|
|
317
|
-
const at = Date.parse(submission.reset);
|
|
318
|
-
if (earliest === null || at < earliest)
|
|
319
|
-
earliest = at;
|
|
320
|
-
}
|
|
321
|
-
if (earliest === null)
|
|
322
|
-
return;
|
|
323
|
-
// A Save House held back goes again only once the reset its answer named has passed.
|
|
324
|
-
resetTimer = setTimeout(() => {
|
|
325
|
-
resetTimer = undefined;
|
|
326
|
-
submitAll();
|
|
327
|
-
}, Math.max(0, earliest - Date.now()));
|
|
328
|
-
}
|
|
329
893
|
async function refuse(reason, provider = null) {
|
|
330
|
-
stopTimer();
|
|
331
894
|
discoveryPaused = true;
|
|
332
895
|
try {
|
|
333
896
|
if (reason === 'revoked-authorization') {
|
|
@@ -335,10 +898,7 @@ export function composeRuntime(options) {
|
|
|
335
898
|
const revoked = { status: 'unavailable', reason: 'revoked' };
|
|
336
899
|
try {
|
|
337
900
|
const local = await openLocal();
|
|
338
|
-
await local.revokeModels([...replicas.values()].map((replica) =>
|
|
339
|
-
record: replica.confirmed.record,
|
|
340
|
-
queue: replica.queue,
|
|
341
|
-
})));
|
|
901
|
+
await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
|
|
342
902
|
}
|
|
343
903
|
catch {
|
|
344
904
|
journal.record('model revocation persistence failed');
|
|
@@ -362,10 +922,7 @@ export function composeRuntime(options) {
|
|
|
362
922
|
model: revoked,
|
|
363
923
|
selectionModel: revoked,
|
|
364
924
|
};
|
|
365
|
-
|
|
366
|
-
replicas.set(locator, read.kind === 'staged'
|
|
367
|
-
? replicaOf(read.confirmed, before.queue, before)
|
|
368
|
-
: { ...before, confirmed: { ...before.confirmed, record } });
|
|
925
|
+
replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
|
|
369
926
|
}
|
|
370
927
|
rooms.set(locator, {
|
|
371
928
|
...room,
|
|
@@ -382,7 +939,8 @@ export function composeRuntime(options) {
|
|
|
382
939
|
}
|
|
383
940
|
finally {
|
|
384
941
|
refusal = {
|
|
385
|
-
reason,
|
|
942
|
+
code: reason,
|
|
943
|
+
retryable: false,
|
|
386
944
|
reauthorizationOffered: reason === 'revoked-authorization',
|
|
387
945
|
refusal: provider,
|
|
388
946
|
};
|
|
@@ -407,19 +965,35 @@ export function composeRuntime(options) {
|
|
|
407
965
|
},
|
|
408
966
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
409
967
|
roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
|
|
410
|
-
attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
|
|
411
968
|
synchronizationAvailable,
|
|
412
969
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
413
970
|
});
|
|
414
971
|
}
|
|
415
|
-
async function applyDiscovery(
|
|
972
|
+
async function applyDiscovery(found, started, covered) {
|
|
973
|
+
for (const room of found) {
|
|
974
|
+
const fence = fences.get(room.locator);
|
|
975
|
+
if (fence !== undefined && fence.erased && fence.at < started)
|
|
976
|
+
fences.delete(room.locator);
|
|
977
|
+
}
|
|
978
|
+
await loseRooms(found
|
|
979
|
+
.filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
|
|
980
|
+
.map((room) => room.locator));
|
|
981
|
+
const listed = found.filter((room) => !fences.has(room.locator));
|
|
416
982
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
417
983
|
for (const locator of listedLocators)
|
|
418
984
|
scopedRefusals.delete(locator);
|
|
419
|
-
|
|
985
|
+
if (covered === undefined)
|
|
986
|
+
discovered.clear();
|
|
987
|
+
else
|
|
988
|
+
for (const locator of covered)
|
|
989
|
+
discovered.delete(locator);
|
|
420
990
|
for (const room of listed)
|
|
421
991
|
discovered.set(room.locator, room);
|
|
422
|
-
|
|
992
|
+
const unlisted = unlistedHeld(new Set(found.map((room) => room.locator))).filter((locator) => covered === undefined || covered.includes(locator));
|
|
993
|
+
if (options.adapter.exhaustiveDiscovery === true)
|
|
994
|
+
await loseRooms(unlisted);
|
|
995
|
+
else
|
|
996
|
+
unlistedKept(unlisted);
|
|
423
997
|
for (const room of listed) {
|
|
424
998
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
425
999
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -430,43 +1004,91 @@ export function composeRuntime(options) {
|
|
|
430
1004
|
}
|
|
431
1005
|
roomsListed = true;
|
|
432
1006
|
emitRooms();
|
|
1007
|
+
return listed;
|
|
433
1008
|
}
|
|
434
|
-
function
|
|
435
|
-
return
|
|
1009
|
+
function heldLocally(locator) {
|
|
1010
|
+
return (replicas.has(locator) ||
|
|
1011
|
+
roomDrafts.has(locator) ||
|
|
1012
|
+
transfers.has(locator) ||
|
|
1013
|
+
queuesWithoutReplica.has(locator));
|
|
436
1014
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
1015
|
+
function unlistedHeld(listed) {
|
|
1016
|
+
return [
|
|
1017
|
+
...new Set([
|
|
1018
|
+
...rooms.keys(),
|
|
1019
|
+
...roomDrafts.keys(),
|
|
1020
|
+
...transfers.keys(),
|
|
1021
|
+
...queuesWithoutReplica.keys(),
|
|
1022
|
+
]),
|
|
1023
|
+
].filter((locator) => !listed.has(locator));
|
|
1024
|
+
}
|
|
1025
|
+
function unlistedKept(unlisted) {
|
|
1026
|
+
for (const locator of unlisted) {
|
|
1027
|
+
const held = replicas.get(locator)?.confirmed.record;
|
|
1028
|
+
if (held !== undefined)
|
|
1029
|
+
list(held, false);
|
|
1030
|
+
else
|
|
452
1031
|
rooms.delete(locator);
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
function fence(locators) {
|
|
1035
|
+
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
1036
|
+
const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
|
|
1037
|
+
room: true,
|
|
1038
|
+
queue: true,
|
|
1039
|
+
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
1040
|
+
}));
|
|
1041
|
+
for (const locator of fresh) {
|
|
1042
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
1043
|
+
lastFenced.set(locator, ++fencings);
|
|
1044
|
+
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
1045
|
+
pendings.delete(batch.id);
|
|
1046
|
+
replicas.delete(locator);
|
|
1047
|
+
queuesWithoutReplica.delete(locator);
|
|
1048
|
+
rooms.delete(locator);
|
|
1049
|
+
roomDrafts.delete(locator);
|
|
1050
|
+
transfers.delete(locator);
|
|
1051
|
+
catching.delete(locator);
|
|
1052
|
+
discovered.delete(locator);
|
|
1053
|
+
announced.delete(locator);
|
|
1054
|
+
unreadable.delete(locator);
|
|
1055
|
+
stale.delete(locator);
|
|
1056
|
+
repairable.delete(locator);
|
|
1057
|
+
unindexable.delete(locator);
|
|
1058
|
+
paused.delete(locator);
|
|
1059
|
+
scopedRefusals.delete(locator);
|
|
1060
|
+
sourceRefusals.delete(locator);
|
|
1061
|
+
declarationRefusals.delete(locator);
|
|
1062
|
+
for (const attached of ['collection', 'typed-selection']) {
|
|
1063
|
+
const key = modelKey(locator, attached);
|
|
1064
|
+
models.delete(key);
|
|
1065
|
+
liveModels.delete(key);
|
|
1066
|
+
invalidModels.delete(key);
|
|
466
1067
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
1068
|
+
journal.record(`room lost ${locator}`);
|
|
1069
|
+
}
|
|
1070
|
+
if (fresh.length === 0)
|
|
1071
|
+
return fresh;
|
|
1072
|
+
publishChangeSet(mergeChangeSets(removed));
|
|
1073
|
+
for (const locator of fresh)
|
|
1074
|
+
prepared.delete(locator);
|
|
1075
|
+
searchIndex.forget(fresh);
|
|
1076
|
+
readSide.recover();
|
|
1077
|
+
moveTo({ ...status, ...attention() });
|
|
1078
|
+
return fresh;
|
|
1079
|
+
}
|
|
1080
|
+
async function erased(local, locators) {
|
|
1081
|
+
await local.erase(locators);
|
|
1082
|
+
for (const locator of locators) {
|
|
1083
|
+
const held = fences.get(locator);
|
|
1084
|
+
if (held !== undefined)
|
|
1085
|
+
held.erased = true;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
async function loseRooms(locators) {
|
|
1089
|
+
const fresh = fence(locators);
|
|
1090
|
+
if (fresh.length > 0)
|
|
1091
|
+
await durably((local) => erased(local, fresh));
|
|
470
1092
|
}
|
|
471
1093
|
function restored(key, persisted) {
|
|
472
1094
|
const prior = models.get(key);
|
|
@@ -487,7 +1109,7 @@ export function composeRuntime(options) {
|
|
|
487
1109
|
return model;
|
|
488
1110
|
}
|
|
489
1111
|
async function hydrate(opened) {
|
|
490
|
-
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
|
|
1112
|
+
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
|
|
491
1113
|
opened.all(),
|
|
492
1114
|
opened.queues(),
|
|
493
1115
|
opened.roomDrafts(),
|
|
@@ -497,9 +1119,17 @@ export function composeRuntime(options) {
|
|
|
497
1119
|
opened.lastCompleted(),
|
|
498
1120
|
opened.providerLimits(),
|
|
499
1121
|
opened.capabilities(),
|
|
1122
|
+
opened.member(),
|
|
1123
|
+
opened.transfers(),
|
|
1124
|
+
opened.targets(),
|
|
500
1125
|
]);
|
|
1126
|
+
targets = heldTargets;
|
|
1127
|
+
transfers.clear();
|
|
1128
|
+
for (const record of staged)
|
|
1129
|
+
transfers.set(record.locator, record);
|
|
501
1130
|
limits = declared;
|
|
502
1131
|
capabilities = named;
|
|
1132
|
+
member = known;
|
|
503
1133
|
roomDrafts.clear();
|
|
504
1134
|
for (const record of kept)
|
|
505
1135
|
rememberRoomDraft(record);
|
|
@@ -515,18 +1145,17 @@ export function composeRuntime(options) {
|
|
|
515
1145
|
}
|
|
516
1146
|
const previousReplicas = new Map(replicas);
|
|
517
1147
|
replicas.clear();
|
|
1148
|
+
queuesWithoutReplica.clear();
|
|
1149
|
+
const replicated = new Set(records.map((record) => record.locator));
|
|
1150
|
+
for (const [locator, queue] of queuedBatches) {
|
|
1151
|
+
if (!replicated.has(locator))
|
|
1152
|
+
queuesWithoutReplica.set(locator, queue);
|
|
1153
|
+
}
|
|
518
1154
|
for (const record of records) {
|
|
519
1155
|
const model = restored(modelKey(record.locator, 'collection'), record.model);
|
|
520
1156
|
const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
|
|
521
1157
|
const prior = previousReplicas.get(record.locator);
|
|
522
|
-
const
|
|
523
|
-
const read = readConfirmed(retyped, prior?.confirmed);
|
|
524
|
-
const confirmed = read.kind === 'staged'
|
|
525
|
-
? read.confirmed
|
|
526
|
-
: {
|
|
527
|
-
record: retyped,
|
|
528
|
-
collection: parseCollection(retyped.documents, compositionOf(retyped)),
|
|
529
|
-
};
|
|
1158
|
+
const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
|
|
530
1159
|
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
|
|
531
1160
|
const existing = rooms.get(record.locator);
|
|
532
1161
|
if (existing === undefined)
|
|
@@ -535,7 +1164,6 @@ export function composeRuntime(options) {
|
|
|
535
1164
|
rooms.set(record.locator, {
|
|
536
1165
|
...existing,
|
|
537
1166
|
roomVersion: record.roomVersion,
|
|
538
|
-
attachments: record.attachments,
|
|
539
1167
|
operations: record.operations,
|
|
540
1168
|
replica: record.replica,
|
|
541
1169
|
model: modelTypes(model),
|
|
@@ -547,10 +1175,6 @@ export function composeRuntime(options) {
|
|
|
547
1175
|
},
|
|
548
1176
|
});
|
|
549
1177
|
}
|
|
550
|
-
if (read.kind === 'refused') {
|
|
551
|
-
refusedSource(record, read);
|
|
552
|
-
declarationRefusals.add(record.locator);
|
|
553
|
-
}
|
|
554
1178
|
}
|
|
555
1179
|
return {
|
|
556
1180
|
lastCompleted,
|
|
@@ -561,16 +1185,26 @@ export function composeRuntime(options) {
|
|
|
561
1185
|
async function readState(opened) {
|
|
562
1186
|
const { lastCompleted, locators, read } = await hydrate(opened);
|
|
563
1187
|
applyChangeSet(read);
|
|
1188
|
+
prepare();
|
|
564
1189
|
if (replicas.size > 0)
|
|
565
1190
|
emitRooms();
|
|
1191
|
+
if (member !== null)
|
|
1192
|
+
emitMember();
|
|
566
1193
|
for (const locator of locators)
|
|
567
1194
|
emitQueue(locator);
|
|
568
1195
|
publishRoomDrafts(false);
|
|
569
1196
|
return lastCompleted;
|
|
570
1197
|
}
|
|
571
1198
|
async function open() {
|
|
572
|
-
|
|
1199
|
+
if (closed)
|
|
1200
|
+
throw new Error('runtime closed');
|
|
1201
|
+
const opened = await openRuntimeStore(isFenced);
|
|
573
1202
|
store = opened;
|
|
1203
|
+
const interrupted = await opened.fences();
|
|
1204
|
+
for (const locator of interrupted) {
|
|
1205
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
1206
|
+
lastFenced.set(locator, ++fencings);
|
|
1207
|
+
}
|
|
574
1208
|
const kept = options.shell.generation;
|
|
575
1209
|
const storedGeneration = await opened.generation();
|
|
576
1210
|
const loss = detectStorageLoss(storedGeneration, kept);
|
|
@@ -584,6 +1218,8 @@ export function composeRuntime(options) {
|
|
|
584
1218
|
emitStorageLoss();
|
|
585
1219
|
}
|
|
586
1220
|
const lastCompleted = await readState(opened);
|
|
1221
|
+
if (interrupted.length > 0)
|
|
1222
|
+
await erased(opened, interrupted);
|
|
587
1223
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
588
1224
|
return opened;
|
|
589
1225
|
}
|
|
@@ -610,10 +1246,16 @@ export function composeRuntime(options) {
|
|
|
610
1246
|
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
611
1247
|
}
|
|
612
1248
|
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
613
|
-
const
|
|
614
|
-
const kept =
|
|
1249
|
+
const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
1250
|
+
const { withdrawn, kept } = standing(confirmed, queue, withdrawable, localSaves);
|
|
1251
|
+
const founded = kept.filter((batch) => !queue.includes(batch));
|
|
1252
|
+
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
1253
|
+
const unqueued = withdrawn.map((batch) => ({
|
|
1254
|
+
batch,
|
|
1255
|
+
returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
|
|
1256
|
+
}));
|
|
615
1257
|
const returning = [
|
|
616
|
-
...
|
|
1258
|
+
...[...unqueued].reverse(),
|
|
617
1259
|
...(settling.leaving === undefined ? [] : [settling.leaving]),
|
|
618
1260
|
];
|
|
619
1261
|
const written = new Map();
|
|
@@ -631,27 +1273,49 @@ export function composeRuntime(options) {
|
|
|
631
1273
|
});
|
|
632
1274
|
}
|
|
633
1275
|
}
|
|
634
|
-
await local.
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
previousQueue: before?.queue ?? [],
|
|
639
|
-
roomDrafts: [...written.values()],
|
|
640
|
-
});
|
|
641
|
-
for (const batch of before?.queue ?? []) {
|
|
642
|
-
if (!kept.includes(batch))
|
|
1276
|
+
await local.keepAuthored({ locator, queue: kept, founded, roomDrafts: [...written.values()] });
|
|
1277
|
+
const staying = new Set(kept.map((batch) => batch.id));
|
|
1278
|
+
for (const batch of previousQueue) {
|
|
1279
|
+
if (!staying.has(batch.id))
|
|
643
1280
|
pendings.delete(batch.id);
|
|
644
1281
|
}
|
|
645
1282
|
for (const record of written.values())
|
|
646
1283
|
rememberRoomDraft(record);
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
1284
|
+
try {
|
|
1285
|
+
await local.write({
|
|
1286
|
+
next: confirmed.record,
|
|
1287
|
+
previous: before?.confirmed.record ?? null,
|
|
1288
|
+
...(settling.stored === undefined ? {} : { stored: settling.stored }),
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
catch (error) {
|
|
1292
|
+
const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
|
|
1293
|
+
if (held !== null)
|
|
1294
|
+
replicas.set(locator, held);
|
|
1295
|
+
else if (kept.length > 0)
|
|
1296
|
+
queuesWithoutReplica.set(locator, kept);
|
|
1297
|
+
else
|
|
1298
|
+
queuesWithoutReplica.delete(locator);
|
|
1299
|
+
publishChangeSet(changeSet(locator, before ?? null, held, {
|
|
1300
|
+
queue: queueChanged,
|
|
1301
|
+
drafts: [...written.keys()],
|
|
1302
|
+
}));
|
|
1303
|
+
moveTo({ ...status, ...attention() });
|
|
1304
|
+
throw error;
|
|
1305
|
+
}
|
|
1306
|
+
return () => {
|
|
1307
|
+
queuesWithoutReplica.delete(locator);
|
|
1308
|
+
for (const { batch, returned } of unqueued) {
|
|
1309
|
+
const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
|
|
1310
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
1311
|
+
}
|
|
1312
|
+
replicas.set(locator, replicaOf(confirmed, kept, before));
|
|
1313
|
+
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
1314
|
+
room: settling.room,
|
|
1315
|
+
queue: queueChanged,
|
|
1316
|
+
drafts: [...written.keys()],
|
|
1317
|
+
});
|
|
1318
|
+
};
|
|
655
1319
|
}
|
|
656
1320
|
function adoptModels(locator, record) {
|
|
657
1321
|
for (const [attached, model] of [
|
|
@@ -684,10 +1348,16 @@ export function composeRuntime(options) {
|
|
|
684
1348
|
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
685
1349
|
};
|
|
686
1350
|
}
|
|
687
|
-
function publish(local, room, confirmed,
|
|
1351
|
+
function publish(local, room, confirmed, landing, live) {
|
|
1352
|
+
const { base, stored } = landing;
|
|
688
1353
|
return exclusive(async () => {
|
|
689
|
-
if (
|
|
1354
|
+
if (stored !== undefined) {
|
|
1355
|
+
await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
|
|
1356
|
+
}
|
|
1357
|
+
if (!live()) {
|
|
1358
|
+
caughtUpTo(room.locator);
|
|
690
1359
|
return true;
|
|
1360
|
+
}
|
|
691
1361
|
const prior = replicas.get(room.locator);
|
|
692
1362
|
if (base !== undefined &&
|
|
693
1363
|
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
@@ -695,47 +1365,121 @@ export function composeRuntime(options) {
|
|
|
695
1365
|
return false;
|
|
696
1366
|
}
|
|
697
1367
|
adoptModels(room.locator, confirmed.record);
|
|
698
|
-
const queue = prior?.queue ?? [];
|
|
1368
|
+
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
699
1369
|
const roomVersion = confirmed.record.roomVersion;
|
|
700
1370
|
const accepted = queue.filter((batch) => {
|
|
701
1371
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
702
1372
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
703
1373
|
});
|
|
704
|
-
|
|
1374
|
+
let settled;
|
|
1375
|
+
try {
|
|
1376
|
+
settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
|
|
1377
|
+
}
|
|
1378
|
+
catch (error) {
|
|
1379
|
+
if (!exceeded(error))
|
|
1380
|
+
throw error;
|
|
1381
|
+
await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
|
|
1382
|
+
caughtUpTo(room.locator);
|
|
1383
|
+
return true;
|
|
1384
|
+
}
|
|
1385
|
+
const set = settled();
|
|
1386
|
+
if (stored !== undefined) {
|
|
1387
|
+
transfers.delete(room.locator);
|
|
1388
|
+
catching.delete(room.locator);
|
|
1389
|
+
}
|
|
705
1390
|
for (const batch of accepted)
|
|
706
1391
|
pendings.delete(batch.id);
|
|
707
1392
|
stale.delete(room.locator);
|
|
1393
|
+
unindexable.delete(room.locator);
|
|
708
1394
|
if (unreadable.delete(room.locator))
|
|
709
1395
|
scopedRefusals.delete(room.locator);
|
|
710
1396
|
sourceRefusals.delete(room.locator);
|
|
711
1397
|
declarationRefusals.delete(room.locator);
|
|
712
1398
|
list(room, true);
|
|
1399
|
+
if (prior === undefined)
|
|
1400
|
+
prepared.add(room.locator);
|
|
713
1401
|
publishChangeSet(set);
|
|
714
1402
|
moveTo({ ...status, ...attention() });
|
|
1403
|
+
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
715
1404
|
return true;
|
|
716
1405
|
});
|
|
717
1406
|
}
|
|
718
|
-
async function
|
|
1407
|
+
async function savedRoom(locator) {
|
|
719
1408
|
const local = await openLocal();
|
|
1409
|
+
await referenced(locator);
|
|
720
1410
|
const replica = replicas.get(locator);
|
|
721
1411
|
const room = rooms.get(locator);
|
|
722
1412
|
if (replica === undefined || room === undefined) {
|
|
723
|
-
throw
|
|
1413
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
724
1414
|
}
|
|
1415
|
+
return { local, replica, room };
|
|
1416
|
+
}
|
|
1417
|
+
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
1418
|
+
const target = await savedRoom(locator);
|
|
725
1419
|
const saved = [];
|
|
726
1420
|
for (const { path, revision } of named) {
|
|
727
1421
|
const record = roomDrafts.get(locator)?.get(path);
|
|
728
1422
|
if (record === undefined || record.revision !== revision) {
|
|
729
|
-
return { outcome: 'refused',
|
|
1423
|
+
return { outcome: 'refused', code: 'stale', retryable: false, file: path };
|
|
730
1424
|
}
|
|
731
1425
|
if (!saved.some((draft) => draft.path === path))
|
|
732
1426
|
saved.push({ path, parts: record.parts });
|
|
733
1427
|
}
|
|
1428
|
+
return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
|
|
1429
|
+
}
|
|
1430
|
+
async function saveParts(locator, named, writtenBy) {
|
|
1431
|
+
const read = named.map(({ path, parts }) => ({ path, parts: readParts(parts) }));
|
|
1432
|
+
if (read.some(({ path }, at) => path === '' ||
|
|
1433
|
+
normalize(path) !== path ||
|
|
1434
|
+
read.findIndex((one) => one.path === path) !== at)) {
|
|
1435
|
+
throw runtimeError('room-draft-malformed');
|
|
1436
|
+
}
|
|
1437
|
+
const target = await savedRoom(locator);
|
|
1438
|
+
const saved = [];
|
|
1439
|
+
for (const { path, parts } of read) {
|
|
1440
|
+
const file = parts.file === undefined
|
|
1441
|
+
? undefined
|
|
1442
|
+
: await heldFile(parts.file.blob, binaryBase(target.replica.visible, path));
|
|
1443
|
+
saved.push({ path, parts: held(undefined, parts, file) });
|
|
1444
|
+
}
|
|
1445
|
+
return committed(locator, target, saved, new Set(), writtenBy);
|
|
1446
|
+
}
|
|
1447
|
+
function carriedDrafts(locator, saved, consumed) {
|
|
1448
|
+
const holding = roomDrafts.get(locator);
|
|
1449
|
+
return new Map(saved.flatMap(({ path, parts }) => {
|
|
1450
|
+
const record = holding?.get(path);
|
|
1451
|
+
if (parts.move === undefined || record === undefined || consumed.has(path))
|
|
1452
|
+
return [];
|
|
1453
|
+
const to = moveTarget(parts.move);
|
|
1454
|
+
if (to === path || holding?.has(to) === true)
|
|
1455
|
+
return [];
|
|
1456
|
+
return [[path, { ...record, path: to, revision: roomDraftRevision() }]];
|
|
1457
|
+
}));
|
|
1458
|
+
}
|
|
1459
|
+
async function committed(locator, { local, replica, room }, saved, consumed, writtenBy) {
|
|
734
1460
|
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
|
|
735
1461
|
if (changed.length > 0)
|
|
736
|
-
return { outcome: 'refused',
|
|
1462
|
+
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
737
1463
|
const parts = saved.map(({ path, parts }) => changesOf(path, parts));
|
|
738
|
-
const
|
|
1464
|
+
const changes = parts.flatMap(({ changes }) => changes);
|
|
1465
|
+
const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
|
|
1466
|
+
if (requeue.kind === 'refused')
|
|
1467
|
+
return { outcome: 'refused', ...requeue.refusal };
|
|
1468
|
+
const foreign = [...requeue.withdrawn.keys()].some((origin) => {
|
|
1469
|
+
const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
|
|
1470
|
+
return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
|
|
1471
|
+
});
|
|
1472
|
+
if (foreign) {
|
|
1473
|
+
return {
|
|
1474
|
+
outcome: 'refused',
|
|
1475
|
+
code: 'unknown-revision',
|
|
1476
|
+
retryable: false,
|
|
1477
|
+
file: [...requeue.queued].at(-1),
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
const withdrawing = requeue.withdrawn.size > 0;
|
|
1481
|
+
const input = {
|
|
1482
|
+
local: localSaves,
|
|
739
1483
|
baseRoomVersion: replica.confirmed.record.roomVersion,
|
|
740
1484
|
composition: compositionFor(replica.confirmed.record),
|
|
741
1485
|
operations: room.operations,
|
|
@@ -743,39 +1487,115 @@ export function composeRuntime(options) {
|
|
|
743
1487
|
providerLimits: limits,
|
|
744
1488
|
handle: room.handle,
|
|
745
1489
|
visible: replica.visible,
|
|
746
|
-
changes
|
|
1490
|
+
changes,
|
|
747
1491
|
bases: parts.flatMap(({ bases }) => bases),
|
|
1492
|
+
queued: queuedDocuments(replica.queue),
|
|
748
1493
|
writtenBy,
|
|
1494
|
+
clean: {
|
|
1495
|
+
text: replica.confirmed.record.documents,
|
|
1496
|
+
binaries: replica.confirmed.record.binaries,
|
|
1497
|
+
composition: compositionOf(replica.confirmed.record),
|
|
1498
|
+
},
|
|
1499
|
+
};
|
|
1500
|
+
const checked = withdrawing
|
|
1501
|
+
? buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
|
|
1502
|
+
: null;
|
|
1503
|
+
if (checked?.kind === 'refused')
|
|
1504
|
+
return { outcome: 'refused', ...checked.refusal };
|
|
1505
|
+
const owners = [...requeue.owners].flatMap((origin) => {
|
|
1506
|
+
const batch = requeue.withdrawn.get(origin);
|
|
1507
|
+
return batch === undefined || batch === null ? [] : [{ origin, batch }];
|
|
749
1508
|
});
|
|
750
|
-
if (
|
|
1509
|
+
if (owners.length > 1) {
|
|
1510
|
+
return {
|
|
1511
|
+
outcome: 'refused',
|
|
1512
|
+
code: 'unknown-revision',
|
|
1513
|
+
retryable: false,
|
|
1514
|
+
file: [...requeue.queued].at(-1),
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
const built = !withdrawing
|
|
1518
|
+
? buildBatch(input)
|
|
1519
|
+
: requeue.changes.length === 0
|
|
1520
|
+
? null
|
|
1521
|
+
: buildBatch({
|
|
1522
|
+
...input,
|
|
1523
|
+
visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
|
|
1524
|
+
changes: requeue.changes,
|
|
1525
|
+
queued: queuedDocuments(requeue.queue),
|
|
1526
|
+
});
|
|
1527
|
+
if (built?.kind === 'refused')
|
|
751
1528
|
return { outcome: 'refused', ...built.refusal };
|
|
752
|
-
const
|
|
753
|
-
const
|
|
1529
|
+
const [owner] = owners;
|
|
1530
|
+
const join = built === null || owner === undefined
|
|
1531
|
+
? null
|
|
1532
|
+
: joined(requeue.queue, owner.batch, built.batch);
|
|
1533
|
+
if (join?.kind === 'refused')
|
|
1534
|
+
return { outcome: 'refused', ...join.refusal };
|
|
1535
|
+
const withdrawn = new Map(requeue.withdrawn);
|
|
1536
|
+
if (join !== null && owner !== undefined)
|
|
1537
|
+
withdrawn.set(owner.origin, join.batch);
|
|
1538
|
+
const drafted = new Map();
|
|
1539
|
+
for (const [origin, replaced] of withdrawn) {
|
|
1540
|
+
if (replaced === null)
|
|
1541
|
+
continue;
|
|
1542
|
+
const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
|
|
1543
|
+
drafted.set(replaced.id, replaced === join?.batch
|
|
1544
|
+
? [
|
|
1545
|
+
...carried.map((held) => joinedDraft(held, saved)),
|
|
1546
|
+
...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
|
|
1547
|
+
]
|
|
1548
|
+
: carried);
|
|
1549
|
+
}
|
|
1550
|
+
if (built !== null && join === null)
|
|
1551
|
+
drafted.set(built.batch.id, saved);
|
|
1552
|
+
const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
|
|
1553
|
+
for (const replaced of withdrawn.values()) {
|
|
1554
|
+
if (replaced === null || !batches.includes(replaced))
|
|
1555
|
+
continue;
|
|
1556
|
+
const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
|
|
1557
|
+
if (limited !== null)
|
|
1558
|
+
return { outcome: 'refused', ...limited };
|
|
1559
|
+
}
|
|
1560
|
+
const carried = carriedDrafts(locator, saved, consumed);
|
|
1561
|
+
const left = [...consumed, ...carried.keys()];
|
|
1562
|
+
const arrived = [...carried.values()];
|
|
754
1563
|
try {
|
|
755
|
-
await local.
|
|
756
|
-
|
|
757
|
-
previous: replica.confirmed.record,
|
|
1564
|
+
await local.keepAuthored({
|
|
1565
|
+
locator,
|
|
758
1566
|
queue: batches,
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1567
|
+
saved: drafted,
|
|
1568
|
+
consumedRoomDrafts: left,
|
|
1569
|
+
roomDrafts: arrived,
|
|
762
1570
|
});
|
|
763
1571
|
}
|
|
764
1572
|
catch {
|
|
765
|
-
return { outcome: 'refused',
|
|
1573
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
1574
|
+
}
|
|
1575
|
+
if (fences.has(locator))
|
|
1576
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1577
|
+
for (const origin of withdrawn.keys())
|
|
1578
|
+
pendings.delete(origin);
|
|
1579
|
+
for (const [id, drafts] of drafted) {
|
|
1580
|
+
pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
|
|
766
1581
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
for (const path of
|
|
1582
|
+
if (!localSaves)
|
|
1583
|
+
recorded = false;
|
|
1584
|
+
for (const path of left)
|
|
770
1585
|
forgetRoomDraft(locator, path);
|
|
1586
|
+
for (const record of arrived)
|
|
1587
|
+
rememberRoomDraft(record);
|
|
771
1588
|
replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
|
|
772
1589
|
publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
|
|
773
1590
|
queue: true,
|
|
774
|
-
drafts:
|
|
1591
|
+
drafts: [...left, ...arrived.map(({ path }) => path)],
|
|
775
1592
|
}));
|
|
776
|
-
moveTo({ ...saving(status), ...attention() });
|
|
1593
|
+
moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
|
|
777
1594
|
submitLater(locator);
|
|
778
|
-
|
|
1595
|
+
const identifier = join?.batch.id ??
|
|
1596
|
+
built?.batch.id ??
|
|
1597
|
+
[...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
|
|
1598
|
+
return { outcome: 'saved', identifier };
|
|
779
1599
|
}
|
|
780
1600
|
async function mark(local, locator, identifier, submission, lost) {
|
|
781
1601
|
const state = pendings.get(identifier);
|
|
@@ -785,6 +1605,8 @@ export function composeRuntime(options) {
|
|
|
785
1605
|
return;
|
|
786
1606
|
}
|
|
787
1607
|
await local.submit(locator, identifier, submission, lost);
|
|
1608
|
+
if (fences.has(locator))
|
|
1609
|
+
return;
|
|
788
1610
|
pendings.set(identifier, { ...state, submission, lost });
|
|
789
1611
|
const replica = replicas.get(locator);
|
|
790
1612
|
if (replica !== undefined)
|
|
@@ -795,22 +1617,24 @@ export function composeRuntime(options) {
|
|
|
795
1617
|
if (!continuing())
|
|
796
1618
|
throw new Error('connection refused');
|
|
797
1619
|
await local.keepUpload(locator, identifier, upload);
|
|
1620
|
+
if (fences.has(locator))
|
|
1621
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
798
1622
|
pendings.set(identifier, { ...pendings.get(identifier), upload });
|
|
799
1623
|
if (!continuing())
|
|
800
1624
|
throw new Error('connection refused');
|
|
801
1625
|
}
|
|
802
1626
|
async function readHistory(locator, path, query = {}) {
|
|
803
1627
|
if (closed || refusal !== null)
|
|
804
|
-
return
|
|
1628
|
+
return UNCERTAIN;
|
|
805
1629
|
try {
|
|
806
1630
|
await ensureAuthorization();
|
|
807
1631
|
}
|
|
808
1632
|
catch {
|
|
809
|
-
return
|
|
810
|
-
}
|
|
811
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
812
|
-
return { kind: 'transport-uncertainty' };
|
|
1633
|
+
return UNCERTAIN;
|
|
813
1634
|
}
|
|
1635
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1636
|
+
return UNCERTAIN;
|
|
1637
|
+
const since = fencings;
|
|
814
1638
|
const transfer = ['transfer', 'history', locator, path];
|
|
815
1639
|
if (query.revision !== undefined)
|
|
816
1640
|
transfer.push(query.revision);
|
|
@@ -825,6 +1649,8 @@ export function composeRuntime(options) {
|
|
|
825
1649
|
...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
|
|
826
1650
|
});
|
|
827
1651
|
journal.record(`outcome history ${result.kind}`);
|
|
1652
|
+
if (fencedSince(locator, since))
|
|
1653
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
828
1654
|
if (result.kind === 'scoped-refusal') {
|
|
829
1655
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
830
1656
|
if (room !== undefined)
|
|
@@ -832,12 +1658,125 @@ export function composeRuntime(options) {
|
|
|
832
1658
|
}
|
|
833
1659
|
if (result.kind === 'revoked-authorization')
|
|
834
1660
|
await refuse('revoked-authorization');
|
|
835
|
-
return result;
|
|
1661
|
+
return historyAnswer(result);
|
|
836
1662
|
}
|
|
837
1663
|
catch {
|
|
838
1664
|
journal.record('outcome history transport-uncertainty');
|
|
839
|
-
return
|
|
1665
|
+
return UNCERTAIN;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
function queuedBlob(locator, path, localId) {
|
|
1669
|
+
for (const batch of replicas.get(locator)?.queue ?? []) {
|
|
1670
|
+
const change = batch.changes[queuedFile(batch, path, localId)];
|
|
1671
|
+
if (change?.kind === 'binary')
|
|
1672
|
+
return change.blob;
|
|
840
1673
|
}
|
|
1674
|
+
return null;
|
|
1675
|
+
}
|
|
1676
|
+
function binaryRoom(request) {
|
|
1677
|
+
if (request.kind === 'path')
|
|
1678
|
+
return request.locator;
|
|
1679
|
+
const scope = options.adapter.referrerScope(request.referrer);
|
|
1680
|
+
return scope?.kind === 'room' ? scope.locator : null;
|
|
1681
|
+
}
|
|
1682
|
+
async function transferBinary(request, transfer, answer) {
|
|
1683
|
+
if (closed || refusal !== null)
|
|
1684
|
+
return UNCERTAIN;
|
|
1685
|
+
try {
|
|
1686
|
+
await ensureAuthorization();
|
|
1687
|
+
}
|
|
1688
|
+
catch {
|
|
1689
|
+
return UNCERTAIN;
|
|
1690
|
+
}
|
|
1691
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1692
|
+
return UNCERTAIN;
|
|
1693
|
+
const since = fencings;
|
|
1694
|
+
journal.record(request.kind === 'path'
|
|
1695
|
+
? `transfer binary ${request.locator} ${request.path}`
|
|
1696
|
+
: `transfer binary ${request.fileRef}`);
|
|
1697
|
+
try {
|
|
1698
|
+
const result = await transfer(authorization);
|
|
1699
|
+
const settled = result;
|
|
1700
|
+
journal.record(`outcome binary ${settled.kind}`);
|
|
1701
|
+
const locator = binaryRoom(request);
|
|
1702
|
+
if (locator !== null && fencedSince(locator, since))
|
|
1703
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
1704
|
+
if (settled.kind === 'scoped-refusal') {
|
|
1705
|
+
const room = replicas.get(settled.locator)?.confirmed.record;
|
|
1706
|
+
if (room !== undefined)
|
|
1707
|
+
await scoped(settled, room, await openLocal());
|
|
1708
|
+
}
|
|
1709
|
+
if (settled.kind === 'revoked-authorization')
|
|
1710
|
+
await refuse('revoked-authorization');
|
|
1711
|
+
return answer(result);
|
|
1712
|
+
}
|
|
1713
|
+
catch {
|
|
1714
|
+
journal.record('outcome binary transport-uncertainty');
|
|
1715
|
+
return UNCERTAIN;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
function readBinary(request) {
|
|
1719
|
+
return transferBinary(request, (held) => options.adapter.binary(held, request), binaryAnswer);
|
|
1720
|
+
}
|
|
1721
|
+
function binaryPath(locator, path, revision) {
|
|
1722
|
+
if (revision !== undefined)
|
|
1723
|
+
return { kind: 'path', locator, path, revision };
|
|
1724
|
+
const visible = replicas.get(locator)?.visible;
|
|
1725
|
+
const held = visible?.binaries[path];
|
|
1726
|
+
if (visible === undefined || held === undefined)
|
|
1727
|
+
return null;
|
|
1728
|
+
if (held.state === 'pending')
|
|
1729
|
+
return { kind: 'pending', localId: held.localId };
|
|
1730
|
+
const record = binaryRecord(path, held, visible.revisions[path]);
|
|
1731
|
+
if (record.state !== 'accepted')
|
|
1732
|
+
return null;
|
|
1733
|
+
const replica = replicas.get(locator);
|
|
1734
|
+
const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
|
|
1735
|
+
const holds = (at) => {
|
|
1736
|
+
const kept = replica?.confirmed.record.binaries[at];
|
|
1737
|
+
return (kept?.state === 'accepted' &&
|
|
1738
|
+
kept.fileRef === record.fileRef &&
|
|
1739
|
+
replica?.confirmed.record.revisions[at] === record.revision);
|
|
1740
|
+
};
|
|
1741
|
+
let confirmed = path;
|
|
1742
|
+
for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
|
|
1743
|
+
const change = queued[index];
|
|
1744
|
+
if (change.kind === 'move' && change.to === confirmed)
|
|
1745
|
+
confirmed = change.path;
|
|
1746
|
+
}
|
|
1747
|
+
return {
|
|
1748
|
+
kind: 'path',
|
|
1749
|
+
locator,
|
|
1750
|
+
path: confirmed,
|
|
1751
|
+
revision: record.revision,
|
|
1752
|
+
fileRef: record.fileRef,
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
async function readBinaryPath(locator, path, revision) {
|
|
1756
|
+
await ready;
|
|
1757
|
+
const asked = binaryPath(locator, path, revision);
|
|
1758
|
+
if (asked === null)
|
|
1759
|
+
return absentBinary(path);
|
|
1760
|
+
if (asked.kind === 'pending') {
|
|
1761
|
+
const blob = queuedBlob(locator, path, asked.localId);
|
|
1762
|
+
return blob === null ? absentBinary(path) : { kind: 'binary', blob };
|
|
1763
|
+
}
|
|
1764
|
+
return readBinary(asked);
|
|
1765
|
+
}
|
|
1766
|
+
async function linkBinaryPath(locator, path, revision) {
|
|
1767
|
+
await ready;
|
|
1768
|
+
const asked = binaryPath(locator, path, revision);
|
|
1769
|
+
if (asked === null || asked.kind === 'pending')
|
|
1770
|
+
return absentBinary(path);
|
|
1771
|
+
return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
|
|
1772
|
+
}
|
|
1773
|
+
async function readBinaryReference(reference) {
|
|
1774
|
+
await ready;
|
|
1775
|
+
return readBinary({
|
|
1776
|
+
kind: 'reference',
|
|
1777
|
+
fileRef: reference.fileRef,
|
|
1778
|
+
referrer: reference.referrer,
|
|
1779
|
+
});
|
|
841
1780
|
}
|
|
842
1781
|
function declaredAgents() {
|
|
843
1782
|
for (const capability of capabilities) {
|
|
@@ -855,21 +1794,21 @@ export function composeRuntime(options) {
|
|
|
855
1794
|
}
|
|
856
1795
|
return undefined;
|
|
857
1796
|
}
|
|
858
|
-
async function invokeAction(name, input) {
|
|
1797
|
+
async function invokeAction(name, input, dispatching) {
|
|
859
1798
|
await ready;
|
|
860
1799
|
if (closed || refusal !== null)
|
|
861
|
-
return
|
|
1800
|
+
return UNCERTAIN;
|
|
862
1801
|
if (declaredAction(name) === undefined)
|
|
863
|
-
return
|
|
1802
|
+
return undeclared(name);
|
|
864
1803
|
try {
|
|
865
1804
|
await ensureAuthorization();
|
|
866
1805
|
}
|
|
867
1806
|
catch {
|
|
868
|
-
return
|
|
869
|
-
}
|
|
870
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
871
|
-
return { kind: 'transport-uncertainty' };
|
|
1807
|
+
return UNCERTAIN;
|
|
872
1808
|
}
|
|
1809
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1810
|
+
return UNCERTAIN;
|
|
1811
|
+
await dispatching?.();
|
|
873
1812
|
journal.record(`transfer action ${name}`);
|
|
874
1813
|
try {
|
|
875
1814
|
const result = await options.adapter.action(authorization, { name, input });
|
|
@@ -883,12 +1822,66 @@ export function composeRuntime(options) {
|
|
|
883
1822
|
journal.record(`outcome action ${result.kind}`);
|
|
884
1823
|
if (result.kind === 'rejected')
|
|
885
1824
|
journal.record(`refusal rejected ${result.code}`);
|
|
1825
|
+
return calledResult(result);
|
|
1826
|
+
}
|
|
1827
|
+
catch {
|
|
1828
|
+
journal.record('outcome action transport-uncertainty');
|
|
1829
|
+
return UNCERTAIN;
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
function unsettled(result) {
|
|
1833
|
+
return result.kind !== 'action' && result.retryable;
|
|
1834
|
+
}
|
|
1835
|
+
async function recoveredAction(name, input) {
|
|
1836
|
+
const operation = operationOf(input);
|
|
1837
|
+
if (operation === null)
|
|
1838
|
+
return invokeAction(name, input);
|
|
1839
|
+
const kept = async () => (await openLocal()).keepCall({ operation, name, input });
|
|
1840
|
+
calling.add(operation);
|
|
1841
|
+
try {
|
|
1842
|
+
let result = await invokeAction(name, input, kept);
|
|
1843
|
+
if (unsettled(result)) {
|
|
1844
|
+
await recovery.attempt(`action ${operation}`, async () => {
|
|
1845
|
+
if (closed || refusal !== null)
|
|
1846
|
+
return 'done';
|
|
1847
|
+
if (!socketOpen)
|
|
1848
|
+
return 'again';
|
|
1849
|
+
result = await invokeAction(name, input, kept);
|
|
1850
|
+
return unsettled(result) ? 'again' : 'done';
|
|
1851
|
+
}, { again: true });
|
|
1852
|
+
}
|
|
1853
|
+
if (!unsettled(result))
|
|
1854
|
+
await (await openLocal()).dropCall(operation);
|
|
886
1855
|
return result;
|
|
887
1856
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1857
|
+
finally {
|
|
1858
|
+
calling.delete(operation);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
async function resendCalls(signal) {
|
|
1862
|
+
if (!socketOpen || closed || stopping)
|
|
1863
|
+
return 'done';
|
|
1864
|
+
const local = await openLocal();
|
|
1865
|
+
for (const held of await local.calls()) {
|
|
1866
|
+
if (calling.has(held.operation))
|
|
1867
|
+
continue;
|
|
1868
|
+
if (closed || signal.aborted)
|
|
1869
|
+
return 'done';
|
|
1870
|
+
if (pastHorizon(held.operation)) {
|
|
1871
|
+
await local.dropCall(held.operation);
|
|
1872
|
+
continue;
|
|
1873
|
+
}
|
|
1874
|
+
calling.add(held.operation);
|
|
1875
|
+
try {
|
|
1876
|
+
if (unsettled(await invokeAction(held.name, held.input)))
|
|
1877
|
+
return 'again';
|
|
1878
|
+
await local.dropCall(held.operation);
|
|
1879
|
+
}
|
|
1880
|
+
finally {
|
|
1881
|
+
calling.delete(held.operation);
|
|
1882
|
+
}
|
|
891
1883
|
}
|
|
1884
|
+
return 'done';
|
|
892
1885
|
}
|
|
893
1886
|
function continuing() {
|
|
894
1887
|
return !closed && !discoveryPaused;
|
|
@@ -904,11 +1897,12 @@ export function composeRuntime(options) {
|
|
|
904
1897
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
905
1898
|
!options.adapter.reached(locator, roomVersion, written));
|
|
906
1899
|
}
|
|
907
|
-
async function pullRefresh(authorized, confirmed) {
|
|
908
|
-
if (!
|
|
1900
|
+
async function pullRefresh(authorized, confirmed, live) {
|
|
1901
|
+
if (!live())
|
|
909
1902
|
return unconfirmed;
|
|
1903
|
+
const turn = generation;
|
|
910
1904
|
try {
|
|
911
|
-
const result = await refreshRoom(
|
|
1905
|
+
const result = await refreshRoom(reads, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
|
|
912
1906
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
913
1907
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
914
1908
|
return result;
|
|
@@ -917,29 +1911,101 @@ export function composeRuntime(options) {
|
|
|
917
1911
|
journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
|
|
918
1912
|
throw error;
|
|
919
1913
|
}
|
|
1914
|
+
finally {
|
|
1915
|
+
reads.finished(confirmed.record.locator, turn);
|
|
1916
|
+
}
|
|
920
1917
|
}
|
|
921
|
-
|
|
922
|
-
|
|
1918
|
+
function staging(local, live) {
|
|
1919
|
+
return {
|
|
1920
|
+
held: (locator) => transfers.get(locator) ?? null,
|
|
1921
|
+
staged: (record) => local.staged(record),
|
|
1922
|
+
stage: (record, page) => exclusive(async () => {
|
|
1923
|
+
if (!live())
|
|
1924
|
+
return false;
|
|
1925
|
+
const bytes = layerBytes(page);
|
|
1926
|
+
if (!(await admitted(local, record.locator, bytes)))
|
|
1927
|
+
return false;
|
|
1928
|
+
try {
|
|
1929
|
+
await local.stage(record, page);
|
|
1930
|
+
}
|
|
1931
|
+
catch (error) {
|
|
1932
|
+
if (!exceeded(error))
|
|
1933
|
+
throw error;
|
|
1934
|
+
await exceededOn(record.locator, bytes);
|
|
1935
|
+
return false;
|
|
1936
|
+
}
|
|
1937
|
+
transfers.set(record.locator, record);
|
|
1938
|
+
readSide.recover();
|
|
1939
|
+
return true;
|
|
1940
|
+
}),
|
|
1941
|
+
abandon: (locator) => exclusive(async () => {
|
|
1942
|
+
if (!live())
|
|
1943
|
+
return;
|
|
1944
|
+
await local.abandon(locator);
|
|
1945
|
+
transfers.delete(locator);
|
|
1946
|
+
readSide.recover();
|
|
1947
|
+
}),
|
|
1948
|
+
};
|
|
1949
|
+
}
|
|
1950
|
+
function reusable(room) {
|
|
1951
|
+
const record = replicas.get(room.locator)?.confirmed.record;
|
|
1952
|
+
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1953
|
+
return null;
|
|
1954
|
+
return { documents: record.documents, revisions: record.revisions };
|
|
1955
|
+
}
|
|
1956
|
+
async function pullBootstrap(authorized, room, local, live) {
|
|
1957
|
+
if (!live())
|
|
923
1958
|
return unconfirmed;
|
|
1959
|
+
const turn = generation;
|
|
1960
|
+
if (replicas.has(room.locator)) {
|
|
1961
|
+
catching.add(room.locator);
|
|
1962
|
+
readSide.recover();
|
|
1963
|
+
}
|
|
924
1964
|
try {
|
|
925
|
-
const result = await bootstrapRoom(
|
|
1965
|
+
const result = await bootstrapRoom(reads, authorized, room, {
|
|
926
1966
|
collection: modelOf(room.locator),
|
|
927
1967
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
928
|
-
}, (models, held) => remodel(room.locator, models, held),
|
|
1968
|
+
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
929
1969
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
930
1970
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1971
|
+
if (result.kind !== 'staged')
|
|
1972
|
+
caughtUpTo(room.locator);
|
|
931
1973
|
return result;
|
|
932
1974
|
}
|
|
933
1975
|
catch (error) {
|
|
934
1976
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
1977
|
+
caughtUpTo(room.locator);
|
|
935
1978
|
throw error;
|
|
936
1979
|
}
|
|
1980
|
+
finally {
|
|
1981
|
+
reads.finished(room.locator, turn);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
function caughtUpTo(locator) {
|
|
1985
|
+
if (catching.delete(locator))
|
|
1986
|
+
readSide.recover();
|
|
1987
|
+
}
|
|
1988
|
+
function behind(room) {
|
|
1989
|
+
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1990
|
+
return (held !== undefined &&
|
|
1991
|
+
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
1992
|
+
}
|
|
1993
|
+
async function advance(room, local, authorized, live) {
|
|
1994
|
+
const carried = reads.carried(room.locator);
|
|
1995
|
+
const withheld = await advanceOnce(room, local, authorized, live);
|
|
1996
|
+
if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
|
|
1997
|
+
return withheld;
|
|
1998
|
+
}
|
|
1999
|
+
return advanceOnce(room, local, authorized, live);
|
|
937
2000
|
}
|
|
938
|
-
async function
|
|
2001
|
+
async function advanceOnce(room, local, authorized, live) {
|
|
2002
|
+
await referenced(room.locator);
|
|
2003
|
+
if (!live() || paused.has(room.locator))
|
|
2004
|
+
return unconfirmed;
|
|
939
2005
|
const held = replicas.get(room.locator);
|
|
940
2006
|
if (held !== undefined) {
|
|
941
|
-
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
942
|
-
if (!
|
|
2007
|
+
const refreshed = await pullRefresh(authorized, held.confirmed, live);
|
|
2008
|
+
if (!live())
|
|
943
2009
|
return unconfirmed;
|
|
944
2010
|
if (refreshed.kind === 'still-current') {
|
|
945
2011
|
stale.delete(room.locator);
|
|
@@ -953,31 +2019,45 @@ export function composeRuntime(options) {
|
|
|
953
2019
|
return null;
|
|
954
2020
|
}
|
|
955
2021
|
if (refreshed.kind === 'advanced') {
|
|
956
|
-
if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
|
|
2022
|
+
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
|
|
957
2023
|
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
958
2024
|
const reached = now !== undefined &&
|
|
959
2025
|
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
960
|
-
return reached ? null : advance(room, local, authorized);
|
|
2026
|
+
return reached ? null : advance(room, local, authorized, live);
|
|
961
2027
|
}
|
|
962
2028
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
963
2029
|
return null;
|
|
964
2030
|
}
|
|
965
2031
|
if (refreshed.kind !== 'beyond-horizon')
|
|
966
2032
|
return refreshed;
|
|
2033
|
+
if (opened !== null) {
|
|
2034
|
+
owed.add(room.locator);
|
|
2035
|
+
readSide.recover();
|
|
2036
|
+
return unconfirmed;
|
|
2037
|
+
}
|
|
967
2038
|
}
|
|
968
|
-
const bootstrapped = await pullBootstrap(authorized, room);
|
|
969
|
-
if (!
|
|
2039
|
+
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
2040
|
+
if (!live())
|
|
970
2041
|
return unconfirmed;
|
|
971
2042
|
if (bootstrapped.kind !== 'staged')
|
|
972
2043
|
return bootstrapped;
|
|
973
|
-
await publish(local, room, bootstrapped.confirmed);
|
|
2044
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
974
2045
|
return null;
|
|
975
2046
|
}
|
|
976
2047
|
async function withhold(withheld, room, local, progress) {
|
|
2048
|
+
if (withheld.kind === 'unconfirmed')
|
|
2049
|
+
return;
|
|
977
2050
|
if (withheld.kind === 'refused')
|
|
978
2051
|
stale.delete(room.locator);
|
|
979
2052
|
else
|
|
980
2053
|
stale.add(room.locator);
|
|
2054
|
+
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
2055
|
+
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
2056
|
+
repairable.set(room.locator, reset);
|
|
2057
|
+
repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
|
|
2058
|
+
}
|
|
2059
|
+
else
|
|
2060
|
+
repairable.delete(room.locator);
|
|
981
2061
|
if (withheld.kind === 'scoped-refusal') {
|
|
982
2062
|
await scoped(withheld, room, local);
|
|
983
2063
|
}
|
|
@@ -1002,6 +2082,7 @@ export function composeRuntime(options) {
|
|
|
1002
2082
|
function refusedSource(room, { refusal }) {
|
|
1003
2083
|
sourceRefusals.set(room.locator, {
|
|
1004
2084
|
code: refusal.rule,
|
|
2085
|
+
retryable: false,
|
|
1005
2086
|
fields: {
|
|
1006
2087
|
file: refusal.file,
|
|
1007
2088
|
...(refusal.location === undefined ? {} : { location: refusal.location }),
|
|
@@ -1012,12 +2093,21 @@ export function composeRuntime(options) {
|
|
|
1012
2093
|
unreadable.add(room.locator);
|
|
1013
2094
|
list(room, false);
|
|
1014
2095
|
emitRooms();
|
|
2096
|
+
moveTo({ ...status, ...attention() });
|
|
1015
2097
|
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
1016
2098
|
}
|
|
1017
2099
|
function leaving(batch, result) {
|
|
1018
2100
|
if (result.kind === 'expired')
|
|
1019
|
-
return { batch, returned:
|
|
1020
|
-
return {
|
|
2101
|
+
return { batch, returned: returnedBy('expired') };
|
|
2102
|
+
return {
|
|
2103
|
+
batch,
|
|
2104
|
+
returned: {
|
|
2105
|
+
kind: result.kind,
|
|
2106
|
+
code: result.code,
|
|
2107
|
+
fields: result.fields,
|
|
2108
|
+
retryable: result.retryable,
|
|
2109
|
+
},
|
|
2110
|
+
};
|
|
1021
2111
|
}
|
|
1022
2112
|
function logAccepted(locator, submitted, accepted) {
|
|
1023
2113
|
journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
|
|
@@ -1025,6 +2115,7 @@ export function composeRuntime(options) {
|
|
|
1025
2115
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1026
2116
|
}
|
|
1027
2117
|
async function adoptAccepted(room, local, submitted, accepted) {
|
|
2118
|
+
await referenced(room.locator);
|
|
1028
2119
|
if (!continuing())
|
|
1029
2120
|
return unconfirmed;
|
|
1030
2121
|
const current = replicas.get(room.locator);
|
|
@@ -1035,22 +2126,23 @@ export function composeRuntime(options) {
|
|
|
1035
2126
|
logAccepted(room.locator, submitted, accepted);
|
|
1036
2127
|
const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
1037
2128
|
? { kind: 'staged', confirmed: current.confirmed }
|
|
1038
|
-
: applyReturned(current.confirmed,
|
|
2129
|
+
: applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
1039
2130
|
? accepted.roomVersion
|
|
1040
2131
|
: held, accepted.roomVersion);
|
|
1041
2132
|
if (applied.kind !== 'staged') {
|
|
1042
2133
|
const state = pendings.get(submitted.id);
|
|
1043
2134
|
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1044
|
-
if (state !== undefined)
|
|
2135
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
1045
2136
|
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1046
2137
|
emitQueue(room.locator);
|
|
1047
2138
|
return applied;
|
|
1048
2139
|
}
|
|
1049
|
-
const
|
|
2140
|
+
const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
1050
2141
|
pendings.delete(submitted.id);
|
|
1051
2142
|
unreadable.delete(room.locator);
|
|
1052
2143
|
sourceRefusals.delete(room.locator);
|
|
1053
2144
|
declarationRefusals.delete(room.locator);
|
|
2145
|
+
const set = settled();
|
|
1054
2146
|
list(room, true);
|
|
1055
2147
|
publishChangeSet(set);
|
|
1056
2148
|
moveTo({ ...status, ...attention() });
|
|
@@ -1064,14 +2156,14 @@ export function composeRuntime(options) {
|
|
|
1064
2156
|
refusedSource(room, failed);
|
|
1065
2157
|
return false;
|
|
1066
2158
|
}
|
|
1067
|
-
async function submitRoom(room, local, authorized, progress) {
|
|
1068
|
-
while (continuing() && authorization !== undefined) {
|
|
2159
|
+
async function submitRoom(room, local, authorized, progress, signal) {
|
|
2160
|
+
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
1069
2161
|
const replica = replicas.get(room.locator);
|
|
1070
2162
|
const head = replica?.queue[0];
|
|
1071
2163
|
if (replica === undefined || head === undefined)
|
|
1072
|
-
return;
|
|
2164
|
+
return 'done';
|
|
1073
2165
|
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1074
|
-
return;
|
|
2166
|
+
return 'done';
|
|
1075
2167
|
const state = pendings.get(head.id);
|
|
1076
2168
|
const { submission, lost: earlier } = state ?? {
|
|
1077
2169
|
submission: { state: 'waiting' },
|
|
@@ -1079,24 +2171,30 @@ export function composeRuntime(options) {
|
|
|
1079
2171
|
};
|
|
1080
2172
|
if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
|
|
1081
2173
|
progress.complete = false;
|
|
1082
|
-
return;
|
|
2174
|
+
return { at: Date.parse(submission.reset) };
|
|
1083
2175
|
}
|
|
1084
2176
|
if (writesHeld()) {
|
|
1085
2177
|
progress.complete = false;
|
|
1086
2178
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1087
|
-
return;
|
|
2179
|
+
return 'done';
|
|
1088
2180
|
}
|
|
1089
2181
|
const lost = earlier || submission.state === 'submitted';
|
|
2182
|
+
if (lost && pastHorizon(head.id)) {
|
|
2183
|
+
await discardPending(room, local, signal);
|
|
2184
|
+
return 'done';
|
|
2185
|
+
}
|
|
1090
2186
|
let result;
|
|
1091
2187
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
1092
2188
|
await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
|
|
1093
|
-
if (discoveryPaused)
|
|
1094
|
-
return;
|
|
1095
|
-
if (writesHeld()) {
|
|
2189
|
+
if (discoveryPaused || fences.has(room.locator))
|
|
2190
|
+
return 'done';
|
|
2191
|
+
if (signal.aborted || writesHeld()) {
|
|
1096
2192
|
await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
|
|
2193
|
+
if (signal.aborted)
|
|
2194
|
+
return 'done';
|
|
1097
2195
|
progress.complete = false;
|
|
1098
2196
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1099
|
-
return;
|
|
2197
|
+
return 'done';
|
|
1100
2198
|
}
|
|
1101
2199
|
const opensBefore = openSignals;
|
|
1102
2200
|
const answered = awaitingAnswer(room.locator);
|
|
@@ -1112,32 +2210,36 @@ export function composeRuntime(options) {
|
|
|
1112
2210
|
progress.complete = false;
|
|
1113
2211
|
if (continuing())
|
|
1114
2212
|
progress.reachable = false;
|
|
1115
|
-
return;
|
|
2213
|
+
return 'again';
|
|
2214
|
+
}
|
|
2215
|
+
if (signal.aborted) {
|
|
2216
|
+
answered();
|
|
2217
|
+
return 'done';
|
|
1116
2218
|
}
|
|
1117
2219
|
if (result.kind !== 'accepted' || !continuing())
|
|
1118
2220
|
answered();
|
|
1119
2221
|
if (!continuing())
|
|
1120
|
-
return;
|
|
2222
|
+
return 'done';
|
|
1121
2223
|
if (result.kind === 'transport-uncertainty') {
|
|
1122
2224
|
progress.complete = false;
|
|
1123
2225
|
progress.reachable = false;
|
|
1124
|
-
return;
|
|
2226
|
+
return 'again';
|
|
1125
2227
|
}
|
|
1126
2228
|
if (result.kind === 'room-not-readable') {
|
|
1127
2229
|
progress.complete = false;
|
|
1128
2230
|
unreadable.add(room.locator);
|
|
1129
2231
|
list(room, false);
|
|
1130
2232
|
emitRooms();
|
|
1131
|
-
return;
|
|
2233
|
+
return 'done';
|
|
1132
2234
|
}
|
|
1133
2235
|
if (result.kind === 'revoked-authorization') {
|
|
1134
2236
|
await refuse('revoked-authorization');
|
|
1135
|
-
return;
|
|
2237
|
+
return 'done';
|
|
1136
2238
|
}
|
|
1137
2239
|
if (result.kind === 'scoped-refusal') {
|
|
1138
2240
|
await scoped(result, room, local);
|
|
1139
2241
|
progress.complete = false;
|
|
1140
|
-
return;
|
|
2242
|
+
return 'done';
|
|
1141
2243
|
}
|
|
1142
2244
|
if (result.kind === 'write-halted') {
|
|
1143
2245
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
@@ -1146,16 +2248,24 @@ export function composeRuntime(options) {
|
|
|
1146
2248
|
continue;
|
|
1147
2249
|
haltedRefusal = true;
|
|
1148
2250
|
progress.complete = false;
|
|
1149
|
-
return;
|
|
2251
|
+
return 'done';
|
|
1150
2252
|
}
|
|
1151
2253
|
if (result.kind === 'conflicted' ||
|
|
1152
2254
|
result.kind === 'rejected' ||
|
|
1153
2255
|
result.kind === 'expired' ||
|
|
1154
2256
|
result.kind === 'retryable-refusal') {
|
|
2257
|
+
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
2258
|
+
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
2259
|
+
await discardPending(room, local, signal);
|
|
2260
|
+
return 'done';
|
|
2261
|
+
}
|
|
1155
2262
|
if (result.kind === 'expired') {
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
2263
|
+
await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
|
|
2264
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
2265
|
+
if (withheld !== null) {
|
|
2266
|
+
await withhold(withheld, room, local, progress);
|
|
2267
|
+
return 'done';
|
|
2268
|
+
}
|
|
1159
2269
|
}
|
|
1160
2270
|
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
|
|
1161
2271
|
(result.kind === 'conflicted' && result.roomVersion !== undefined
|
|
@@ -1168,8 +2278,7 @@ export function composeRuntime(options) {
|
|
|
1168
2278
|
const reset = result.reset;
|
|
1169
2279
|
progress.complete = false;
|
|
1170
2280
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1171
|
-
|
|
1172
|
-
return;
|
|
2281
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
1173
2282
|
}
|
|
1174
2283
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
1175
2284
|
const left = leaving(head, result);
|
|
@@ -1180,36 +2289,82 @@ export function composeRuntime(options) {
|
|
|
1180
2289
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
1181
2290
|
return;
|
|
1182
2291
|
}
|
|
1183
|
-
const
|
|
1184
|
-
leaving: result.kind === 'expired' && landed(head, current.confirmed)
|
|
2292
|
+
const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
2293
|
+
leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
|
|
2294
|
+
(result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
|
|
1185
2295
|
? { ...left, returned: null }
|
|
1186
2296
|
: left,
|
|
1187
2297
|
});
|
|
1188
|
-
publishChangeSet(
|
|
2298
|
+
publishChangeSet(settled());
|
|
1189
2299
|
moveTo({ ...status, ...attention() });
|
|
1190
2300
|
});
|
|
1191
2301
|
if (result.kind === 'expired')
|
|
1192
2302
|
continue;
|
|
1193
|
-
const withheld = await advance(room, local, authorized);
|
|
1194
|
-
if (withheld !== null)
|
|
1195
|
-
|
|
2303
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
2304
|
+
if (withheld !== null) {
|
|
2305
|
+
await withhold(withheld, room, local, progress);
|
|
2306
|
+
return 'done';
|
|
2307
|
+
}
|
|
1196
2308
|
continue;
|
|
1197
2309
|
}
|
|
1198
2310
|
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1199
2311
|
if (!kept) {
|
|
1200
2312
|
progress.complete = false;
|
|
1201
|
-
return;
|
|
2313
|
+
return 'done';
|
|
1202
2314
|
}
|
|
1203
2315
|
}
|
|
2316
|
+
return 'done';
|
|
2317
|
+
}
|
|
2318
|
+
function pastHorizon(identity) {
|
|
2319
|
+
return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
|
|
2320
|
+
}
|
|
2321
|
+
async function discardPending(room, local, signal) {
|
|
2322
|
+
const { locator } = room;
|
|
2323
|
+
const discarded = await exclusive(async () => {
|
|
2324
|
+
if (fences.has(locator))
|
|
2325
|
+
return false;
|
|
2326
|
+
const before = replicas.get(locator);
|
|
2327
|
+
const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
2328
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
2329
|
+
lastFenced.set(locator, ++fencings);
|
|
2330
|
+
try {
|
|
2331
|
+
await local.discard(locator);
|
|
2332
|
+
}
|
|
2333
|
+
finally {
|
|
2334
|
+
fences.delete(locator);
|
|
2335
|
+
}
|
|
2336
|
+
for (const batch of queue)
|
|
2337
|
+
pendings.delete(batch.id);
|
|
2338
|
+
queuesWithoutReplica.delete(locator);
|
|
2339
|
+
const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
|
|
2340
|
+
if (after !== null)
|
|
2341
|
+
replicas.set(locator, after);
|
|
2342
|
+
journal.record(`room discarded ${locator}`);
|
|
2343
|
+
publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
|
|
2344
|
+
moveTo({ ...status, ...attention() });
|
|
2345
|
+
return true;
|
|
2346
|
+
});
|
|
2347
|
+
if (!discarded)
|
|
2348
|
+
return;
|
|
2349
|
+
const progress = { complete: true, reachable: true };
|
|
2350
|
+
await roomTask(locator, async (live) => {
|
|
2351
|
+
await Promise.race([
|
|
2352
|
+
reload(room, local, progress, () => live() && !signal.aborted),
|
|
2353
|
+
abandonedOn(signal),
|
|
2354
|
+
]);
|
|
2355
|
+
});
|
|
2356
|
+
if (signal.aborted)
|
|
2357
|
+
return;
|
|
2358
|
+
await rest(progress);
|
|
1204
2359
|
}
|
|
1205
2360
|
function awaitingAnswer(locator) {
|
|
1206
2361
|
let release = () => { };
|
|
1207
2362
|
const awaited = new Promise((resolve) => {
|
|
1208
2363
|
release = resolve;
|
|
1209
2364
|
});
|
|
1210
|
-
answering.set(locator, awaited);
|
|
2365
|
+
answering.set(locator, { generation, answered: awaited });
|
|
1211
2366
|
return () => {
|
|
1212
|
-
if (answering.get(locator) === awaited)
|
|
2367
|
+
if (answering.get(locator)?.answered === awaited)
|
|
1213
2368
|
answering.delete(locator);
|
|
1214
2369
|
release();
|
|
1215
2370
|
};
|
|
@@ -1242,7 +2397,7 @@ export function composeRuntime(options) {
|
|
|
1242
2397
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
1243
2398
|
};
|
|
1244
2399
|
const reclassified = sameMembership(declared, held.confirmed.record)
|
|
1245
|
-
?
|
|
2400
|
+
? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
|
|
1246
2401
|
: readReclassified(declared, held.confirmed);
|
|
1247
2402
|
const record = reclassified.kind === 'staged'
|
|
1248
2403
|
? declared
|
|
@@ -1250,12 +2405,10 @@ export function composeRuntime(options) {
|
|
|
1250
2405
|
...declared,
|
|
1251
2406
|
typedSelection: held.confirmed.record.typedSelection,
|
|
1252
2407
|
};
|
|
1253
|
-
const read = reclassified.kind === 'deferred'
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
queue: held.queue,
|
|
1258
|
-
});
|
|
2408
|
+
const read = reclassified.kind === 'deferred'
|
|
2409
|
+
? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
|
|
2410
|
+
: reclassified;
|
|
2411
|
+
await local.write({ next: record, previous: held.confirmed.record });
|
|
1259
2412
|
if (read.kind === 'staged') {
|
|
1260
2413
|
replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
|
|
1261
2414
|
if (declarationRefusals.delete(room.locator)) {
|
|
@@ -1282,7 +2435,16 @@ export function composeRuntime(options) {
|
|
|
1282
2435
|
});
|
|
1283
2436
|
}
|
|
1284
2437
|
async function scoped(result, room, local) {
|
|
1285
|
-
|
|
2438
|
+
if ((result.scope === 'content' || result.scope === 'sync') && !result.retryable) {
|
|
2439
|
+
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
2440
|
+
await loseRooms([room.locator]);
|
|
2441
|
+
return;
|
|
2442
|
+
}
|
|
2443
|
+
scopedRefusals.set(room.locator, {
|
|
2444
|
+
code: result.code,
|
|
2445
|
+
fields: result.fields,
|
|
2446
|
+
retryable: result.retryable,
|
|
2447
|
+
});
|
|
1286
2448
|
let scopedAway = room;
|
|
1287
2449
|
if (result.scope === 'model')
|
|
1288
2450
|
await retainModel(local, room, 'collection', {
|
|
@@ -1319,22 +2481,26 @@ export function composeRuntime(options) {
|
|
|
1319
2481
|
roomVersion: record.roomVersion,
|
|
1320
2482
|
documents: record.documents,
|
|
1321
2483
|
revisions: record.revisions,
|
|
1322
|
-
attachments: record.attachments,
|
|
1323
2484
|
},
|
|
1324
2485
|
];
|
|
1325
2486
|
});
|
|
1326
2487
|
}
|
|
1327
|
-
async function synchronize(listed, local, progress, attempt) {
|
|
2488
|
+
async function synchronize(listed, local, progress, attempt, live) {
|
|
1328
2489
|
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
|
|
2490
|
+
if (!live())
|
|
2491
|
+
return;
|
|
1329
2492
|
const ready = listed.filter((_, index) => {
|
|
1330
2493
|
const outcome = prepared[index];
|
|
1331
2494
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1332
2495
|
});
|
|
1333
|
-
if (attempt === 'opened')
|
|
2496
|
+
if (attempt === 'opened') {
|
|
1334
2497
|
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
2498
|
+
handed = true;
|
|
2499
|
+
}
|
|
2500
|
+
discovering?.release();
|
|
1335
2501
|
const loaded = await Promise.allSettled(ready
|
|
1336
2502
|
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1337
|
-
.map((room) => loadRoom(room, local, progress)));
|
|
2503
|
+
.map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
|
|
1338
2504
|
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1339
2505
|
if (failed !== undefined)
|
|
1340
2506
|
throw failed.reason;
|
|
@@ -1356,21 +2522,12 @@ export function composeRuntime(options) {
|
|
|
1356
2522
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1357
2523
|
if (discoveryPaused || authorization === undefined)
|
|
1358
2524
|
return false;
|
|
1359
|
-
|
|
1360
|
-
if (replicas.has(room.locator))
|
|
1361
|
-
progress.complete = false;
|
|
1362
|
-
return false;
|
|
1363
|
-
}
|
|
1364
|
-
return true;
|
|
2525
|
+
return room.replica.admission === 'admitted';
|
|
1365
2526
|
}
|
|
1366
|
-
async function caughtUp(room, local, progress) {
|
|
1367
|
-
|
|
1368
|
-
if (held === undefined || authorization === undefined)
|
|
2527
|
+
async function caughtUp(room, local, progress, live) {
|
|
2528
|
+
if (authorization === undefined || !live() || !behind(room))
|
|
1369
2529
|
return;
|
|
1370
|
-
const
|
|
1371
|
-
if (!behind)
|
|
1372
|
-
return;
|
|
1373
|
-
const withheld = await advance(room, local, authorization);
|
|
2530
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1374
2531
|
if (withheld !== null)
|
|
1375
2532
|
await withhold(withheld, room, local, progress);
|
|
1376
2533
|
}
|
|
@@ -1380,47 +2537,55 @@ export function composeRuntime(options) {
|
|
|
1380
2537
|
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1381
2538
|
});
|
|
1382
2539
|
}
|
|
1383
|
-
async function loadRoom(room, local, progress) {
|
|
1384
|
-
if (
|
|
2540
|
+
async function loadRoom(room, local, progress, live) {
|
|
2541
|
+
if (!live() || authorization === undefined)
|
|
1385
2542
|
return;
|
|
1386
2543
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1387
|
-
await reload(room, local, progress);
|
|
2544
|
+
await reload(room, local, progress, live);
|
|
1388
2545
|
return;
|
|
1389
2546
|
}
|
|
1390
|
-
const withheld = await advance(room, local, authorization);
|
|
2547
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1391
2548
|
if (withheld !== null) {
|
|
1392
2549
|
await withhold(withheld, room, local, progress);
|
|
1393
2550
|
return;
|
|
1394
2551
|
}
|
|
1395
|
-
await caughtUp(room, local, progress);
|
|
2552
|
+
await caughtUp(room, local, progress, live);
|
|
1396
2553
|
}
|
|
1397
|
-
async function reload(room, local, progress) {
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
2554
|
+
async function reload(room, local, progress, live) {
|
|
2555
|
+
try {
|
|
2556
|
+
if (!live() || authorization === undefined)
|
|
2557
|
+
return;
|
|
2558
|
+
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
2559
|
+
if (!live())
|
|
2560
|
+
return;
|
|
2561
|
+
if (bootstrapped.kind !== 'staged') {
|
|
2562
|
+
await withhold(bootstrapped, room, local, progress);
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2565
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
2566
|
+
await caughtUp(room, local, progress, live);
|
|
2567
|
+
}
|
|
2568
|
+
finally {
|
|
2569
|
+
caughtUpTo(room.locator);
|
|
1406
2570
|
}
|
|
1407
|
-
await publish(local, room, bootstrapped.confirmed);
|
|
1408
|
-
await caughtUp(room, local, progress);
|
|
1409
2571
|
}
|
|
1410
|
-
async function discoverRooms(local, progress, attempt) {
|
|
2572
|
+
async function discoverRooms(local, progress, attempt, live, scope) {
|
|
1411
2573
|
if (closed || authorization === undefined) {
|
|
1412
2574
|
notifyReady();
|
|
1413
2575
|
return;
|
|
1414
2576
|
}
|
|
1415
2577
|
moveTo(transferring(status));
|
|
2578
|
+
discoveries += 1;
|
|
2579
|
+
const started = discoveries;
|
|
1416
2580
|
let result;
|
|
1417
2581
|
try {
|
|
1418
|
-
result = await options.adapter.discover(authorization);
|
|
2582
|
+
result = await options.adapter.discover(authorization, scope);
|
|
1419
2583
|
}
|
|
1420
2584
|
catch {
|
|
1421
2585
|
result = { kind: 'transport-uncertainty' };
|
|
1422
2586
|
}
|
|
1423
|
-
if (
|
|
2587
|
+
if (!live()) {
|
|
2588
|
+
progress.complete = false;
|
|
1424
2589
|
notifyReady();
|
|
1425
2590
|
return;
|
|
1426
2591
|
}
|
|
@@ -1434,15 +2599,16 @@ export function composeRuntime(options) {
|
|
|
1434
2599
|
}
|
|
1435
2600
|
progress.reachable = false;
|
|
1436
2601
|
progress.complete = false;
|
|
1437
|
-
if (attempt === 'opened') {
|
|
1438
|
-
options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
|
|
1439
|
-
}
|
|
1440
2602
|
notifyReady();
|
|
1441
2603
|
return;
|
|
1442
2604
|
}
|
|
1443
2605
|
if (result.kind === 'contract-unavailable') {
|
|
1444
2606
|
journal.record(`connection contract unavailable ${result.code}`);
|
|
1445
|
-
await refuse('missing-required-guarantee', {
|
|
2607
|
+
await refuse('missing-required-guarantee', {
|
|
2608
|
+
code: result.code,
|
|
2609
|
+
fields: result.fields,
|
|
2610
|
+
retryable: result.retryable,
|
|
2611
|
+
});
|
|
1446
2612
|
notifyReady();
|
|
1447
2613
|
return;
|
|
1448
2614
|
}
|
|
@@ -1477,14 +2643,20 @@ export function composeRuntime(options) {
|
|
|
1477
2643
|
}
|
|
1478
2644
|
const previousCapabilities = capabilities;
|
|
1479
2645
|
capabilities = result.capabilities;
|
|
2646
|
+
outcomeRetention = result.retention.outcome;
|
|
1480
2647
|
notifyReady();
|
|
1481
2648
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1482
2649
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
1483
2650
|
limits = result.providerLimits;
|
|
1484
2651
|
await local.declare(limits, capabilities);
|
|
1485
2652
|
}
|
|
1486
|
-
|
|
1487
|
-
|
|
2653
|
+
const signedIn = result.member ?? null;
|
|
2654
|
+
if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
|
|
2655
|
+
await local.keepMember(signedIn);
|
|
2656
|
+
member = signedIn;
|
|
2657
|
+
emitMember();
|
|
2658
|
+
}
|
|
2659
|
+
await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
|
|
1488
2660
|
}
|
|
1489
2661
|
async function ensureAuthorization() {
|
|
1490
2662
|
if (authorization !== undefined)
|
|
@@ -1498,76 +2670,169 @@ export function composeRuntime(options) {
|
|
|
1498
2670
|
throw error;
|
|
1499
2671
|
}
|
|
1500
2672
|
}
|
|
1501
|
-
function
|
|
2673
|
+
async function admittedRead(work) {
|
|
2674
|
+
if (roomReads.generation !== generation) {
|
|
2675
|
+
roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
2676
|
+
}
|
|
2677
|
+
const reads = roomReads;
|
|
2678
|
+
while (reads.free === 0)
|
|
2679
|
+
await new Promise((admit) => reads.waiting.push(admit));
|
|
2680
|
+
reads.free -= 1;
|
|
2681
|
+
try {
|
|
2682
|
+
await work();
|
|
2683
|
+
}
|
|
2684
|
+
finally {
|
|
2685
|
+
reads.free += 1;
|
|
2686
|
+
reads.waiting.shift()?.();
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
function roomTask(locator, work) {
|
|
2690
|
+
return task(roomLane(locator), (live) => admittedRead(() => work(live)));
|
|
2691
|
+
}
|
|
2692
|
+
function task(lane, work) {
|
|
1502
2693
|
if (stopping)
|
|
1503
|
-
return
|
|
1504
|
-
|
|
1505
|
-
|
|
2694
|
+
return Promise.resolve();
|
|
2695
|
+
const held = lanes.get(lane);
|
|
2696
|
+
const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
|
|
2697
|
+
const turn = current.generation;
|
|
2698
|
+
const live = () => turn === generation && continuing();
|
|
2699
|
+
const run = current.chain.then(() => work(live)).catch(() => undefined);
|
|
2700
|
+
current.chain = run;
|
|
2701
|
+
lanes.set(lane, current);
|
|
2702
|
+
working.add(run);
|
|
2703
|
+
void run.finally(() => working.delete(run));
|
|
2704
|
+
return run;
|
|
1506
2705
|
}
|
|
1507
2706
|
function sync(attempt) {
|
|
1508
|
-
return task(async () => {
|
|
1509
|
-
if (
|
|
2707
|
+
return task(OPENING_LANE, async (live) => {
|
|
2708
|
+
if (!live()) {
|
|
1510
2709
|
notifyReady();
|
|
1511
2710
|
return;
|
|
1512
2711
|
}
|
|
1513
2712
|
const progress = { complete: true, reachable: true };
|
|
1514
|
-
|
|
2713
|
+
const gate = discover();
|
|
2714
|
+
if (attempt === 'opened')
|
|
2715
|
+
opened = generation;
|
|
2716
|
+
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
2717
|
+
if (attempt === 'opened')
|
|
2718
|
+
streamed = progress;
|
|
1515
2719
|
complete = false;
|
|
1516
2720
|
recorded = false;
|
|
1517
2721
|
moveTo(transferring(status));
|
|
2722
|
+
handed = false;
|
|
1518
2723
|
try {
|
|
1519
2724
|
const local = await openLocal();
|
|
1520
|
-
if (
|
|
2725
|
+
if (!live())
|
|
1521
2726
|
return;
|
|
1522
2727
|
await ensureAuthorization();
|
|
1523
|
-
if (
|
|
2728
|
+
if (!live())
|
|
1524
2729
|
return;
|
|
1525
|
-
await discoverRooms(local, progress, attempt);
|
|
2730
|
+
await discoverRooms(local, progress, attempt, live);
|
|
1526
2731
|
}
|
|
1527
2732
|
catch {
|
|
1528
2733
|
progress.complete = false;
|
|
1529
|
-
if (attempt === 'opened')
|
|
1530
|
-
|
|
1531
|
-
}
|
|
2734
|
+
if (attempt === 'opened' && !handed)
|
|
2735
|
+
progress.reachable = false;
|
|
1532
2736
|
}
|
|
1533
2737
|
finally {
|
|
2738
|
+
gate.release();
|
|
1534
2739
|
notifyReady();
|
|
1535
2740
|
}
|
|
1536
|
-
if (
|
|
2741
|
+
if (!live())
|
|
2742
|
+
return;
|
|
2743
|
+
if (attempt !== 'opened') {
|
|
2744
|
+
await finishOpening(progress, live);
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2747
|
+
if (handed || closed || progress.reachable)
|
|
2748
|
+
return;
|
|
2749
|
+
underway = null;
|
|
2750
|
+
opened = null;
|
|
2751
|
+
journal.record('connection reopen');
|
|
2752
|
+
await rest(progress, live);
|
|
2753
|
+
options.adapter.roomStream?.reopen();
|
|
2754
|
+
});
|
|
2755
|
+
}
|
|
2756
|
+
function rediscover(scope) {
|
|
2757
|
+
return task(OPENING_LANE, async (live) => {
|
|
2758
|
+
if (!live() || !socketOpen)
|
|
2759
|
+
return;
|
|
2760
|
+
const progress = { complete: true, reachable: true };
|
|
2761
|
+
const gate = discover();
|
|
2762
|
+
moveTo(transferring(status));
|
|
2763
|
+
try {
|
|
2764
|
+
const local = await openLocal();
|
|
2765
|
+
if (!live())
|
|
2766
|
+
return;
|
|
2767
|
+
await ensureAuthorization();
|
|
2768
|
+
if (!live())
|
|
2769
|
+
return;
|
|
2770
|
+
await discoverRooms(local, progress, 'authorized', live, scope);
|
|
2771
|
+
}
|
|
2772
|
+
catch {
|
|
2773
|
+
progress.complete = false;
|
|
2774
|
+
}
|
|
2775
|
+
finally {
|
|
2776
|
+
gate.release();
|
|
2777
|
+
notifyReady();
|
|
2778
|
+
}
|
|
2779
|
+
if (!live())
|
|
1537
2780
|
return;
|
|
1538
|
-
|
|
2781
|
+
if (!progress.complete)
|
|
2782
|
+
complete = false;
|
|
2783
|
+
await rest(progress, live);
|
|
1539
2784
|
});
|
|
1540
2785
|
}
|
|
1541
|
-
function closedSocket(revoked) {
|
|
2786
|
+
function closedSocket(revoked, lost) {
|
|
1542
2787
|
const opened = socketOpen;
|
|
1543
2788
|
socketOpen = false;
|
|
2789
|
+
if (lost) {
|
|
2790
|
+
reads.lost();
|
|
2791
|
+
conversations.lost();
|
|
2792
|
+
}
|
|
1544
2793
|
if (revoked) {
|
|
1545
2794
|
void refuse('revoked-authorization');
|
|
1546
2795
|
return;
|
|
1547
2796
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
2797
|
+
complete = false;
|
|
2798
|
+
if (streamed !== null)
|
|
2799
|
+
streamed.complete = false;
|
|
2800
|
+
moveTo({
|
|
2801
|
+
...status,
|
|
2802
|
+
state: status.state === 'synced' ? 'syncing' : status.state,
|
|
2803
|
+
providerUnreachable: status.providerUnreachable || !opened,
|
|
2804
|
+
});
|
|
1551
2805
|
}
|
|
1552
2806
|
function applyOpening(expired) {
|
|
1553
|
-
|
|
1554
|
-
|
|
2807
|
+
const progress = underway?.generation === generation ? underway.progress : null;
|
|
2808
|
+
const cycle = progress ?? { complete: true, reachable: true };
|
|
2809
|
+
if (opened === generation)
|
|
2810
|
+
opened = null;
|
|
2811
|
+
const reloading = progress === null ? [] : owedReloads(expired);
|
|
2812
|
+
const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
|
|
2813
|
+
return task(OPENING_LANE, async (live) => {
|
|
1555
2814
|
underway = null;
|
|
1556
|
-
if (
|
|
2815
|
+
if (!live())
|
|
1557
2816
|
return;
|
|
1558
2817
|
if (progress === null) {
|
|
1559
|
-
await
|
|
2818
|
+
await replacing;
|
|
2819
|
+
await rest(cycle, live);
|
|
1560
2820
|
return;
|
|
1561
2821
|
}
|
|
2822
|
+
await roomsCarried([...expired, ...reloading]);
|
|
2823
|
+
if (!live())
|
|
2824
|
+
return;
|
|
1562
2825
|
try {
|
|
1563
2826
|
const local = await openLocal();
|
|
1564
|
-
const
|
|
1565
|
-
const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
|
|
1566
|
-
const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
|
|
2827
|
+
const reloaded = await replacing;
|
|
1567
2828
|
if (progress.reachable) {
|
|
1568
|
-
for (const room of
|
|
1569
|
-
if (
|
|
2829
|
+
for (const room of discovered.values()) {
|
|
2830
|
+
if (replicas.has(room.locator) &&
|
|
2831
|
+
!expired.includes(room.locator) &&
|
|
2832
|
+
!reloading.includes(room.locator)) {
|
|
1570
2833
|
await confirmModels(room, local);
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
1571
2836
|
}
|
|
1572
2837
|
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1573
2838
|
if (failed !== undefined)
|
|
@@ -1577,21 +2842,33 @@ export function composeRuntime(options) {
|
|
|
1577
2842
|
progress.complete = false;
|
|
1578
2843
|
progress.reachable = false;
|
|
1579
2844
|
}
|
|
1580
|
-
|
|
2845
|
+
if (live())
|
|
2846
|
+
await finishOpening(progress, live);
|
|
1581
2847
|
});
|
|
1582
2848
|
}
|
|
1583
|
-
|
|
1584
|
-
const
|
|
2849
|
+
function owedReloads(expired) {
|
|
2850
|
+
const reloading = [...discovered.values()]
|
|
2851
|
+
.filter((room) => replicas.has(room.locator) &&
|
|
2852
|
+
!expired.includes(room.locator) &&
|
|
2853
|
+
(owed.has(room.locator) || revoked(room.locator)))
|
|
2854
|
+
.map((room) => room.locator);
|
|
2855
|
+
owed.clear();
|
|
2856
|
+
for (const locator of reloading)
|
|
2857
|
+
catching.add(locator);
|
|
2858
|
+
readSide.recover();
|
|
2859
|
+
return reloading;
|
|
2860
|
+
}
|
|
2861
|
+
async function reloadNow(replaced, progress) {
|
|
1585
2862
|
try {
|
|
1586
2863
|
const local = await openLocal();
|
|
1587
|
-
await Promise.allSettled([...discovered.values()]
|
|
1588
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
1589
|
-
.map((room) => reload(room, local, progress)));
|
|
2864
|
+
return await Promise.allSettled([...discovered.values()]
|
|
2865
|
+
.filter((room) => replicas.has(room.locator) && replaced(room))
|
|
2866
|
+
.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
1590
2867
|
}
|
|
1591
2868
|
catch {
|
|
1592
2869
|
progress.reachable = false;
|
|
2870
|
+
return [];
|
|
1593
2871
|
}
|
|
1594
|
-
await rest(progress);
|
|
1595
2872
|
}
|
|
1596
2873
|
async function confirmModels(room, local) {
|
|
1597
2874
|
for (const attached of ['collection', 'typed-selection']) {
|
|
@@ -1602,38 +2879,96 @@ export function composeRuntime(options) {
|
|
|
1602
2879
|
}
|
|
1603
2880
|
}
|
|
1604
2881
|
function submitLater(locator) {
|
|
1605
|
-
|
|
1606
|
-
if (running !== undefined) {
|
|
1607
|
-
running.again = true;
|
|
2882
|
+
if (localSaves)
|
|
1608
2883
|
return;
|
|
2884
|
+
void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
|
|
2885
|
+
}
|
|
2886
|
+
async function submitPass(locator, signal) {
|
|
2887
|
+
const room = discovered.get(locator);
|
|
2888
|
+
if (closed || stopping || discoveryPaused || room === undefined)
|
|
2889
|
+
return 'done';
|
|
2890
|
+
const progress = { complete: true, reachable: true };
|
|
2891
|
+
let next;
|
|
2892
|
+
try {
|
|
2893
|
+
await ensureAuthorization();
|
|
2894
|
+
if (authorization === undefined)
|
|
2895
|
+
return 'done';
|
|
2896
|
+
next = await submitRoom(room, await openLocal(), authorization, progress, signal);
|
|
1609
2897
|
}
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
2898
|
+
catch {
|
|
2899
|
+
progress.reachable = false;
|
|
2900
|
+
next = 'again';
|
|
2901
|
+
}
|
|
2902
|
+
if (signal.aborted)
|
|
2903
|
+
return 'done';
|
|
2904
|
+
await rest(progress);
|
|
2905
|
+
return next === 'again' && !socketOpen ? 'done' : next;
|
|
2906
|
+
}
|
|
2907
|
+
function repairLater(locator, when) {
|
|
2908
|
+
if (!socketOpen || repairing.has(locator))
|
|
2909
|
+
return;
|
|
2910
|
+
void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
|
|
2911
|
+
}
|
|
2912
|
+
async function repairPass(locator, signal) {
|
|
2913
|
+
const room = discovered.get(locator);
|
|
2914
|
+
if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
|
|
2915
|
+
return 'done';
|
|
2916
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2917
|
+
return 'done';
|
|
2918
|
+
const progress = { complete: true, reachable: true };
|
|
2919
|
+
repairing.add(locator);
|
|
2920
|
+
try {
|
|
2921
|
+
const local = await openLocal();
|
|
2922
|
+
await roomTask(locator, async (live) => {
|
|
2923
|
+
const current = () => live() && !signal.aborted;
|
|
2924
|
+
if (!current() || authorization === undefined)
|
|
2925
|
+
return;
|
|
2926
|
+
const withheld = await Promise.race([
|
|
2927
|
+
advance(room, local, authorization, current),
|
|
2928
|
+
abandonedOn(signal),
|
|
2929
|
+
]);
|
|
2930
|
+
if (withheld !== null && current())
|
|
2931
|
+
await withhold(withheld, room, local, progress);
|
|
2932
|
+
});
|
|
2933
|
+
}
|
|
2934
|
+
catch {
|
|
2935
|
+
stale.add(locator);
|
|
2936
|
+
progress.reachable = false;
|
|
2937
|
+
}
|
|
2938
|
+
finally {
|
|
2939
|
+
repairing.delete(locator);
|
|
2940
|
+
}
|
|
2941
|
+
if (signal.aborted)
|
|
2942
|
+
return 'done';
|
|
2943
|
+
await rest(progress);
|
|
2944
|
+
if (!stale.has(locator)) {
|
|
2945
|
+
if ((replicas.get(locator)?.queue.length ?? 0) > 0)
|
|
2946
|
+
submitLater(locator);
|
|
2947
|
+
return 'done';
|
|
2948
|
+
}
|
|
2949
|
+
if (!repairable.has(locator))
|
|
2950
|
+
return 'done';
|
|
2951
|
+
const reset = repairable.get(locator);
|
|
2952
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
2953
|
+
}
|
|
2954
|
+
function resendLater() {
|
|
2955
|
+
if (!socketOpen)
|
|
2956
|
+
return;
|
|
2957
|
+
void recovery.attempt('resend', resendPass, { again: true });
|
|
2958
|
+
}
|
|
2959
|
+
async function resendPass(signal) {
|
|
2960
|
+
if (!socketOpen || closed || stopping || discoveryPaused)
|
|
2961
|
+
return 'done';
|
|
2962
|
+
const reading = { complete: true, reachable: true };
|
|
2963
|
+
try {
|
|
2964
|
+
await conversations.resend(reading);
|
|
2965
|
+
}
|
|
2966
|
+
catch {
|
|
2967
|
+
reading.reachable = false;
|
|
2968
|
+
}
|
|
2969
|
+
if (signal.aborted)
|
|
2970
|
+
return 'done';
|
|
2971
|
+
return reading.reachable && reading.complete ? 'done' : 'again';
|
|
1637
2972
|
}
|
|
1638
2973
|
function submitAll() {
|
|
1639
2974
|
for (const [locator, replica] of replicas) {
|
|
@@ -1641,31 +2976,43 @@ export function composeRuntime(options) {
|
|
|
1641
2976
|
submitLater(locator);
|
|
1642
2977
|
}
|
|
1643
2978
|
}
|
|
1644
|
-
async function finishOpening(progress) {
|
|
2979
|
+
async function finishOpening(progress, live) {
|
|
1645
2980
|
try {
|
|
1646
|
-
if (
|
|
2981
|
+
if (!live()) {
|
|
1647
2982
|
progress.complete = false;
|
|
1648
2983
|
return;
|
|
1649
2984
|
}
|
|
2985
|
+
reads.applied();
|
|
1650
2986
|
submitAll();
|
|
2987
|
+
for (const locator of stale)
|
|
2988
|
+
if (repairable.has(locator))
|
|
2989
|
+
repairLater(locator, {});
|
|
1651
2990
|
await conversations.reconcile();
|
|
1652
2991
|
if (status.providerUnreachable)
|
|
1653
2992
|
conversations.reconnected();
|
|
1654
2993
|
const reading = { complete: true, reachable: true };
|
|
1655
2994
|
await conversations.catchUp(reading);
|
|
1656
|
-
if (
|
|
2995
|
+
if (progress.reachable && reading.reachable)
|
|
2996
|
+
await conversations.resend(reading);
|
|
2997
|
+
if (progress.reachable)
|
|
2998
|
+
void recovery.attempt('calls', resendCalls);
|
|
2999
|
+
if (!reading.reachable) {
|
|
1657
3000
|
progress.reachable = false;
|
|
3001
|
+
resendLater();
|
|
3002
|
+
}
|
|
1658
3003
|
}
|
|
1659
3004
|
catch {
|
|
1660
3005
|
progress.complete = false;
|
|
1661
3006
|
}
|
|
1662
3007
|
finally {
|
|
1663
|
-
|
|
1664
|
-
|
|
3008
|
+
if (live()) {
|
|
3009
|
+
complete = progress.complete;
|
|
3010
|
+
await rest(progress, live);
|
|
3011
|
+
}
|
|
1665
3012
|
}
|
|
1666
3013
|
}
|
|
1667
|
-
async function rest(progress) {
|
|
1668
|
-
if (
|
|
3014
|
+
async function rest(progress, live = () => !closed) {
|
|
3015
|
+
if (!live())
|
|
1669
3016
|
return;
|
|
1670
3017
|
let lastCompleted = status.lastCompleted;
|
|
1671
3018
|
let synced = complete && stale.size === 0 && !pending();
|
|
@@ -1673,6 +3020,8 @@ export function composeRuntime(options) {
|
|
|
1673
3020
|
const completedAt = new Date().toISOString();
|
|
1674
3021
|
try {
|
|
1675
3022
|
await (await openLocal()).complete(completedAt);
|
|
3023
|
+
if (!live())
|
|
3024
|
+
return;
|
|
1676
3025
|
recorded = true;
|
|
1677
3026
|
lastCompleted = completedAt;
|
|
1678
3027
|
}
|
|
@@ -1681,17 +3030,29 @@ export function composeRuntime(options) {
|
|
|
1681
3030
|
}
|
|
1682
3031
|
}
|
|
1683
3032
|
moveTo({
|
|
1684
|
-
state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
|
|
3033
|
+
state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
|
|
1685
3034
|
lastCompleted,
|
|
1686
3035
|
providerUnreachable: !progress.reachable,
|
|
1687
3036
|
...attention(),
|
|
1688
3037
|
});
|
|
1689
3038
|
}
|
|
3039
|
+
function discover() {
|
|
3040
|
+
let release = () => { };
|
|
3041
|
+
const done = new Promise((resolve) => {
|
|
3042
|
+
release = resolve;
|
|
3043
|
+
});
|
|
3044
|
+
discovering = { generation, done, release };
|
|
3045
|
+
return discovering;
|
|
3046
|
+
}
|
|
1690
3047
|
function applyRoom(locator, roomVersion) {
|
|
1691
3048
|
announced.set(locator, roomVersion);
|
|
1692
|
-
return task(async () => {
|
|
1693
|
-
|
|
1694
|
-
|
|
3049
|
+
return task(roomLane(locator), async (live) => {
|
|
3050
|
+
if (discovering?.generation === generation)
|
|
3051
|
+
await discovering.done;
|
|
3052
|
+
const saving = answering.get(locator);
|
|
3053
|
+
if (saving?.generation === generation)
|
|
3054
|
+
await saving.answered;
|
|
3055
|
+
if (!live() || authorization === undefined)
|
|
1695
3056
|
return;
|
|
1696
3057
|
const room = discovered.get(locator);
|
|
1697
3058
|
const held = replicas.get(locator);
|
|
@@ -1702,25 +3063,41 @@ export function composeRuntime(options) {
|
|
|
1702
3063
|
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
1703
3064
|
return;
|
|
1704
3065
|
}
|
|
1705
|
-
const during = underway;
|
|
3066
|
+
const during = underway?.generation === generation ? underway.progress : null;
|
|
1706
3067
|
const progress = during ?? { complete: true, reachable: true };
|
|
1707
3068
|
if (during === null)
|
|
1708
3069
|
moveTo(transferring(status));
|
|
1709
3070
|
try {
|
|
1710
3071
|
const local = await openLocal();
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
3072
|
+
await admittedRead(async () => {
|
|
3073
|
+
if (!live() || authorization === undefined)
|
|
3074
|
+
return;
|
|
3075
|
+
const withheld = await advance(room, local, authorization, live);
|
|
3076
|
+
if (withheld !== null)
|
|
3077
|
+
await withhold(withheld, room, local, progress);
|
|
3078
|
+
});
|
|
1714
3079
|
}
|
|
1715
3080
|
catch {
|
|
1716
3081
|
stale.add(locator);
|
|
1717
3082
|
}
|
|
1718
3083
|
finally {
|
|
1719
3084
|
if (during === null)
|
|
1720
|
-
await rest(progress);
|
|
3085
|
+
await rest(progress, live);
|
|
1721
3086
|
}
|
|
1722
3087
|
});
|
|
1723
3088
|
}
|
|
3089
|
+
function roomsCarried(replacing) {
|
|
3090
|
+
const replaced = new Set(replacing.map(roomLane));
|
|
3091
|
+
return Promise.all([...lanes]
|
|
3092
|
+
.filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
|
|
3093
|
+
.map(([, held]) => held.chain));
|
|
3094
|
+
}
|
|
3095
|
+
const receipts = {
|
|
3096
|
+
heads: async () => (await openLocal()).receivedHeads(),
|
|
3097
|
+
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
3098
|
+
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
3099
|
+
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
3100
|
+
};
|
|
1724
3101
|
function observeStream() {
|
|
1725
3102
|
const stream = options.adapter.roomStream;
|
|
1726
3103
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -1730,15 +3107,21 @@ export function composeRuntime(options) {
|
|
|
1730
3107
|
return;
|
|
1731
3108
|
if (event.kind === 'opened') {
|
|
1732
3109
|
socketOpen = true;
|
|
3110
|
+
generation += 1;
|
|
3111
|
+
reads.opened();
|
|
3112
|
+
conversations.reopened();
|
|
3113
|
+
recovery.resume();
|
|
1733
3114
|
void sync('opened');
|
|
1734
3115
|
}
|
|
1735
3116
|
else if (event.kind === 'closed')
|
|
1736
|
-
closedSocket(event.revoked);
|
|
3117
|
+
closedSocket(event.revoked, event.lost);
|
|
1737
3118
|
else if (event.kind === 'opening')
|
|
1738
3119
|
void applyOpening(event.expired);
|
|
3120
|
+
else if (event.kind === 'authority-changed')
|
|
3121
|
+
void rediscover(event.scope);
|
|
1739
3122
|
else
|
|
1740
3123
|
void applyRoom(event.locator, event.roomVersion);
|
|
1741
|
-
});
|
|
3124
|
+
}, receipts);
|
|
1742
3125
|
}
|
|
1743
3126
|
function writesHeld() {
|
|
1744
3127
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -1753,6 +3136,7 @@ export function composeRuntime(options) {
|
|
|
1753
3136
|
haltedRefusal = false;
|
|
1754
3137
|
if (mode !== serviceMode) {
|
|
1755
3138
|
serviceMode = mode;
|
|
3139
|
+
queueReadiness();
|
|
1756
3140
|
journal.record(`service mode ${mode}`);
|
|
1757
3141
|
for (const listener of serviceModeListeners)
|
|
1758
3142
|
listener(mode);
|
|
@@ -1765,6 +3149,8 @@ export function composeRuntime(options) {
|
|
|
1765
3149
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
1766
3150
|
return;
|
|
1767
3151
|
withdrawConversations = stream.observe((event) => {
|
|
3152
|
+
if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
|
|
3153
|
+
queueReadiness();
|
|
1768
3154
|
if (conversations.receive(event)) {
|
|
1769
3155
|
void ready.then(scheduleCurrency);
|
|
1770
3156
|
return;
|
|
@@ -1776,46 +3162,45 @@ export function composeRuntime(options) {
|
|
|
1776
3162
|
});
|
|
1777
3163
|
}
|
|
1778
3164
|
function scheduleCurrency() {
|
|
1779
|
-
if (currencyQueued || closed || stopping)
|
|
3165
|
+
if (currencyQueued === generation || closed || stopping)
|
|
1780
3166
|
return;
|
|
1781
|
-
currencyQueued =
|
|
1782
|
-
void task(runCurrency);
|
|
3167
|
+
currencyQueued = generation;
|
|
3168
|
+
void task(CONVERSATION_LANE, runCurrency);
|
|
1783
3169
|
}
|
|
1784
|
-
async function runCurrency() {
|
|
1785
|
-
currencyQueued
|
|
1786
|
-
|
|
3170
|
+
async function runCurrency(live) {
|
|
3171
|
+
if (currencyQueued === generation)
|
|
3172
|
+
currencyQueued = null;
|
|
3173
|
+
if (!live())
|
|
1787
3174
|
return;
|
|
1788
3175
|
await conversations.reconcile();
|
|
1789
3176
|
if (conversations.current()) {
|
|
1790
3177
|
show();
|
|
1791
3178
|
return;
|
|
1792
3179
|
}
|
|
1793
|
-
const
|
|
1794
|
-
|
|
1795
|
-
moveTo(transferring(resting));
|
|
3180
|
+
const progress = { complete: false, reachable: !status.providerUnreachable };
|
|
3181
|
+
moveTo(transferring(status));
|
|
1796
3182
|
try {
|
|
1797
3183
|
await conversations.catchUp(progress);
|
|
1798
3184
|
}
|
|
1799
3185
|
catch {
|
|
1800
3186
|
progress.reachable = false;
|
|
1801
3187
|
}
|
|
1802
|
-
|
|
1803
|
-
if (!closed) {
|
|
1804
|
-
moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
3188
|
+
await rest(progress, live);
|
|
1807
3189
|
}
|
|
1808
3190
|
function close() {
|
|
1809
3191
|
if (closed)
|
|
1810
3192
|
return;
|
|
1811
3193
|
closed = true;
|
|
3194
|
+
if (rechecking !== null)
|
|
3195
|
+
clearInterval(rechecking);
|
|
3196
|
+
readinessWatches.clear();
|
|
1812
3197
|
withdrawStream?.();
|
|
1813
3198
|
withdrawStream = undefined;
|
|
1814
3199
|
withdrawConversations?.();
|
|
1815
3200
|
withdrawConversations = undefined;
|
|
1816
3201
|
withdrawServiceMode?.();
|
|
1817
3202
|
conversations.close();
|
|
1818
|
-
|
|
3203
|
+
void recovery.retire();
|
|
1819
3204
|
store?.close();
|
|
1820
3205
|
void opening?.then((opened) => {
|
|
1821
3206
|
opened.close();
|
|
@@ -1825,6 +3210,7 @@ export function composeRuntime(options) {
|
|
|
1825
3210
|
storageLossListeners.clear();
|
|
1826
3211
|
statusListeners.clear();
|
|
1827
3212
|
serviceModeListeners.clear();
|
|
3213
|
+
memberListeners.clear();
|
|
1828
3214
|
readSide.close();
|
|
1829
3215
|
queueSubscriptions.clear();
|
|
1830
3216
|
roomDraftListeners.clear();
|
|
@@ -1832,9 +3218,8 @@ export function composeRuntime(options) {
|
|
|
1832
3218
|
}
|
|
1833
3219
|
async function stop() {
|
|
1834
3220
|
stopping = true;
|
|
1835
|
-
|
|
1836
|
-
await
|
|
1837
|
-
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
3221
|
+
await recovery.retire();
|
|
3222
|
+
await Promise.all(working);
|
|
1838
3223
|
await mutations;
|
|
1839
3224
|
close();
|
|
1840
3225
|
}
|
|
@@ -1853,17 +3238,112 @@ export function composeRuntime(options) {
|
|
|
1853
3238
|
if (options.adapter.roomStream === undefined)
|
|
1854
3239
|
void sync('unstreamed');
|
|
1855
3240
|
function planned(input, reach) {
|
|
1856
|
-
|
|
3241
|
+
const plan = planOf(input, roomFacts(reach), planReach(reach));
|
|
3242
|
+
const named = plan.paths !== null &&
|
|
3243
|
+
plan.rooms !== null &&
|
|
3244
|
+
plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
|
|
3245
|
+
return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
|
|
1857
3246
|
}
|
|
1858
3247
|
function watchPlan(plan, deliver) {
|
|
1859
|
-
|
|
3248
|
+
let seeded = false;
|
|
3249
|
+
let pending = true;
|
|
3250
|
+
const shown = () => {
|
|
3251
|
+
const seed = seeds.get(plan.key);
|
|
3252
|
+
if (seed === undefined || seeded || !pending)
|
|
3253
|
+
return;
|
|
3254
|
+
seeded = true;
|
|
3255
|
+
deliver(seededResult(seed));
|
|
3256
|
+
};
|
|
3257
|
+
const watchers = seedWatchers.get(plan.key) ?? new Set();
|
|
3258
|
+
seedWatchers.set(plan.key, watchers);
|
|
3259
|
+
watchers.add(shown);
|
|
3260
|
+
const stop = readSide.watch(plan, identityOf(plan), () => {
|
|
3261
|
+
const cover = covered(plan);
|
|
3262
|
+
awaitPreparation(cover);
|
|
3263
|
+
return seedBehind(plan, cover);
|
|
3264
|
+
}, (message) => {
|
|
3265
|
+
const answered = message;
|
|
3266
|
+
pending = whole(answered) && 'pending' in answered.result;
|
|
3267
|
+
if (pending && seeds.has(plan.key)) {
|
|
3268
|
+
shown();
|
|
3269
|
+
return;
|
|
3270
|
+
}
|
|
3271
|
+
seeded = false;
|
|
3272
|
+
deliver(message);
|
|
3273
|
+
});
|
|
3274
|
+
return () => {
|
|
3275
|
+
watchers.delete(shown);
|
|
3276
|
+
if (watchers.size === 0)
|
|
3277
|
+
seedWatchers.delete(plan.key);
|
|
3278
|
+
stop();
|
|
3279
|
+
};
|
|
1860
3280
|
}
|
|
1861
3281
|
async function searched(input, reach) {
|
|
1862
|
-
const
|
|
1863
|
-
const
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
3282
|
+
const asked = searchPlanOf(input);
|
|
3283
|
+
const plan = { ...asked, limit: capped(asked.limit) };
|
|
3284
|
+
const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
|
|
3285
|
+
for (;;) {
|
|
3286
|
+
const cover = covered(filter);
|
|
3287
|
+
awaitPreparation(cover);
|
|
3288
|
+
const pending = 'pending' in cover ? [...cover.pending] : [];
|
|
3289
|
+
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
3290
|
+
const held = covered({
|
|
3291
|
+
...filter,
|
|
3292
|
+
rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
|
|
3293
|
+
});
|
|
3294
|
+
const covering = 'pending' in held ? [] : held.coverage.rooms;
|
|
3295
|
+
const unindexed = covering
|
|
3296
|
+
.filter(({ room }) => !searchIndex.rooms().has(room))
|
|
3297
|
+
.map(({ room }) => room);
|
|
3298
|
+
for (const room of unindexed)
|
|
3299
|
+
pending.push({ room, awaits: 'index' });
|
|
3300
|
+
const listed = conversations.searchable();
|
|
3301
|
+
const named = reach !== null && !reach.conversations
|
|
3302
|
+
? []
|
|
3303
|
+
: (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
|
|
3304
|
+
const waiting = named.filter((one) => !searchIndex.conversations().has(one));
|
|
3305
|
+
for (const conversation of waiting)
|
|
3306
|
+
pending.push({ conversation, awaits: 'index' });
|
|
3307
|
+
if (unindexed.length > 0 || waiting.length > 0)
|
|
3308
|
+
index(unindexed, waiting);
|
|
3309
|
+
const complete = pending.length === 0 && !('pending' in cover);
|
|
3310
|
+
const answering = complete
|
|
3311
|
+
? filter
|
|
3312
|
+
: {
|
|
3313
|
+
...filter,
|
|
3314
|
+
rooms: covering
|
|
3315
|
+
.filter(({ room }) => searchIndex.rooms().has(room))
|
|
3316
|
+
.map(({ room }) => room),
|
|
3317
|
+
};
|
|
3318
|
+
const answered = covered(answering);
|
|
3319
|
+
if ('pending' in answered)
|
|
3320
|
+
continue;
|
|
3321
|
+
const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
|
|
3322
|
+
await readSide.settled();
|
|
3323
|
+
const seen = searchIndex.changes();
|
|
3324
|
+
await searchIndex.caughtUp();
|
|
3325
|
+
const selected = roomsAlone(answering) ? null : await rowsQuery(answering);
|
|
3326
|
+
if (selected !== null && 'pending' in selected)
|
|
3327
|
+
continue;
|
|
3328
|
+
if (searchIndex.changes() !== seen ||
|
|
3329
|
+
JSON.stringify(covered(answering)) !== JSON.stringify(answered))
|
|
3330
|
+
continue;
|
|
3331
|
+
const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms === null ? null : new Set(answering.rooms), plan.collection
|
|
3332
|
+
? new Map(answered.coverage.rooms.map(({ room }) => [
|
|
3333
|
+
room,
|
|
3334
|
+
replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
|
|
3335
|
+
]))
|
|
3336
|
+
: null);
|
|
3337
|
+
const coverage = {
|
|
3338
|
+
rooms: answered.coverage.rooms,
|
|
3339
|
+
conversations: searchedConversations,
|
|
3340
|
+
index: searchIndex.generation(),
|
|
3341
|
+
overlay: answered.coverage.overlay,
|
|
3342
|
+
};
|
|
3343
|
+
return complete
|
|
3344
|
+
? { ...hits, coverage }
|
|
3345
|
+
: { pending, documents: hits.documents, messages: hits.messages, coverage };
|
|
3346
|
+
}
|
|
1867
3347
|
}
|
|
1868
3348
|
const watch = (topic, input, deliver) => {
|
|
1869
3349
|
if (topic === 'room-drafts') {
|
|
@@ -1888,6 +3368,8 @@ export function composeRuntime(options) {
|
|
|
1888
3368
|
}
|
|
1889
3369
|
if (topic === 'conversations')
|
|
1890
3370
|
return conversations.watchList(deliver);
|
|
3371
|
+
if (topic === 'agents')
|
|
3372
|
+
return conversations.watchAgents(deliver);
|
|
1891
3373
|
if (topic === 'presentation') {
|
|
1892
3374
|
const listener = {
|
|
1893
3375
|
app: input[0],
|
|
@@ -1907,6 +3389,9 @@ export function composeRuntime(options) {
|
|
|
1907
3389
|
presentationListeners.delete(listener);
|
|
1908
3390
|
};
|
|
1909
3391
|
}
|
|
3392
|
+
if (topic === 'readiness') {
|
|
3393
|
+
return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
|
|
3394
|
+
}
|
|
1910
3395
|
if (topic === 'rooms') {
|
|
1911
3396
|
roomListeners.add(deliver);
|
|
1912
3397
|
deliver(listed());
|
|
@@ -1942,6 +3427,13 @@ export function composeRuntime(options) {
|
|
|
1942
3427
|
serviceModeListeners.delete(deliver);
|
|
1943
3428
|
};
|
|
1944
3429
|
}
|
|
3430
|
+
if (topic === 'member') {
|
|
3431
|
+
memberListeners.add(deliver);
|
|
3432
|
+
deliver(member);
|
|
3433
|
+
return () => {
|
|
3434
|
+
memberListeners.delete(deliver);
|
|
3435
|
+
};
|
|
3436
|
+
}
|
|
1945
3437
|
if (topic === 'queue') {
|
|
1946
3438
|
const queue = { locator: input[0], notify: deliver };
|
|
1947
3439
|
queueSubscriptions.add(queue);
|
|
@@ -1959,7 +3451,7 @@ export function composeRuntime(options) {
|
|
|
1959
3451
|
rooms() {
|
|
1960
3452
|
return listed();
|
|
1961
3453
|
},
|
|
1962
|
-
query: ((input) =>
|
|
3454
|
+
query: ((input) => rowsQuery(planned(input, null))),
|
|
1963
3455
|
search: (input) => searched(input, null),
|
|
1964
3456
|
types(locator) {
|
|
1965
3457
|
return {
|
|
@@ -1988,20 +3480,20 @@ export function composeRuntime(options) {
|
|
|
1988
3480
|
return exclusive(async () => {
|
|
1989
3481
|
const parts = readParts(input);
|
|
1990
3482
|
if (path === '' || normalize(path) !== path)
|
|
1991
|
-
throw
|
|
3483
|
+
throw runtimeError('room-draft-malformed');
|
|
1992
3484
|
const local = await openLocal();
|
|
1993
3485
|
const current = roomDrafts.get(room)?.get(path);
|
|
1994
3486
|
if ((current?.revision ?? null) !== revision) {
|
|
1995
|
-
return
|
|
1996
|
-
outcome: 'stale',
|
|
1997
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
1998
|
-
};
|
|
3487
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
1999
3488
|
}
|
|
3489
|
+
const file = parts.file === undefined
|
|
3490
|
+
? undefined
|
|
3491
|
+
: await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
|
|
2000
3492
|
const record = {
|
|
2001
3493
|
locator: room,
|
|
2002
3494
|
path,
|
|
2003
3495
|
revision: roomDraftRevision(),
|
|
2004
|
-
parts: held(current?.parts, parts),
|
|
3496
|
+
parts: held(current?.parts, parts, file),
|
|
2005
3497
|
returned: current?.returned ?? null,
|
|
2006
3498
|
};
|
|
2007
3499
|
await local.keepRoomDraft(record);
|
|
@@ -2015,10 +3507,7 @@ export function composeRuntime(options) {
|
|
|
2015
3507
|
const local = await openLocal();
|
|
2016
3508
|
const current = roomDrafts.get(room)?.get(path);
|
|
2017
3509
|
if (current === undefined || current.revision !== revision) {
|
|
2018
|
-
return
|
|
2019
|
-
outcome: 'stale',
|
|
2020
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
2021
|
-
};
|
|
3510
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2022
3511
|
}
|
|
2023
3512
|
await local.discardRoomDraft(room, path);
|
|
2024
3513
|
forgetRoomDraft(room, path);
|
|
@@ -2038,23 +3527,31 @@ export function composeRuntime(options) {
|
|
|
2038
3527
|
});
|
|
2039
3528
|
},
|
|
2040
3529
|
write(app, value) {
|
|
2041
|
-
return
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
3530
|
+
return keepPresentation(app, () => structuredClone(value));
|
|
3531
|
+
},
|
|
3532
|
+
edit(app, members) {
|
|
3533
|
+
return keepPresentation(app, (held) => Object.fromEntries([
|
|
3534
|
+
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(members, member)),
|
|
3535
|
+
...Object.entries(structuredClone(members)).filter(([, value]) => value !== null),
|
|
3536
|
+
]));
|
|
2048
3537
|
},
|
|
2049
3538
|
},
|
|
2050
3539
|
save(room, named, writtenBy) {
|
|
2051
3540
|
return exclusive(() => saveRoomDrafts(room, named, writtenBy));
|
|
2052
3541
|
},
|
|
3542
|
+
saveParts(room, parts, writtenBy) {
|
|
3543
|
+
return exclusive(() => saveParts(room, parts, writtenBy));
|
|
3544
|
+
},
|
|
2053
3545
|
history(locator, path, query) {
|
|
2054
3546
|
return readHistory(locator, path, query);
|
|
2055
3547
|
},
|
|
3548
|
+
binaryFile: {
|
|
3549
|
+
read: readBinaryPath,
|
|
3550
|
+
link: linkBinaryPath,
|
|
3551
|
+
readReference: readBinaryReference,
|
|
3552
|
+
},
|
|
2056
3553
|
call(name, input) {
|
|
2057
|
-
return
|
|
3554
|
+
return recoveredAction(name, input);
|
|
2058
3555
|
},
|
|
2059
3556
|
reached(locator, roomVersion, path) {
|
|
2060
3557
|
return path === undefined
|
|
@@ -2065,30 +3562,54 @@ export function composeRuntime(options) {
|
|
|
2065
3562
|
return journal.export();
|
|
2066
3563
|
},
|
|
2067
3564
|
persistence() {
|
|
2068
|
-
return
|
|
3565
|
+
return persistence === 'granted';
|
|
3566
|
+
},
|
|
3567
|
+
async retry() {
|
|
3568
|
+
journal.record('retry');
|
|
3569
|
+
unindexable.clear();
|
|
3570
|
+
queueReadiness();
|
|
3571
|
+
resume();
|
|
2069
3572
|
},
|
|
2070
3573
|
observe: observing(watch),
|
|
2071
3574
|
async reauthorize() {
|
|
2072
3575
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2073
3576
|
return;
|
|
2074
3577
|
journal.record('connection reauthorize');
|
|
2075
|
-
await
|
|
3578
|
+
await Promise.all([...lanes.values()]
|
|
3579
|
+
.filter((held) => held.generation === generation)
|
|
3580
|
+
.map((held) => held.chain));
|
|
2076
3581
|
await mutations;
|
|
2077
3582
|
authorization = undefined;
|
|
2078
3583
|
discoveryPaused = false;
|
|
2079
3584
|
if (options.adapter.roomStream === undefined)
|
|
2080
3585
|
await sync('unstreamed');
|
|
2081
3586
|
else
|
|
2082
|
-
await task(ensureAuthorization);
|
|
3587
|
+
await task(OPENING_LANE, ensureAuthorization);
|
|
2083
3588
|
},
|
|
2084
3589
|
};
|
|
2085
3590
|
const reachable = {
|
|
2086
3591
|
md,
|
|
2087
3592
|
watch,
|
|
3593
|
+
referrerScope: (referrer) => options.adapter.referrerScope(referrer),
|
|
2088
3594
|
plan: planned,
|
|
2089
|
-
query: (plan) =>
|
|
3595
|
+
query: (plan) => rowsQuery(plan),
|
|
2090
3596
|
watchPlan,
|
|
2091
3597
|
search: searched,
|
|
3598
|
+
watchReadiness,
|
|
3599
|
+
};
|
|
3600
|
+
return {
|
|
3601
|
+
md,
|
|
3602
|
+
seed,
|
|
3603
|
+
stop,
|
|
3604
|
+
lose: loseRooms,
|
|
3605
|
+
watch,
|
|
3606
|
+
persisted(answer) {
|
|
3607
|
+
if (answer === persistence)
|
|
3608
|
+
return;
|
|
3609
|
+
persistence = answer;
|
|
3610
|
+
journal.record(`persistence ${answer}`);
|
|
3611
|
+
queueReadiness();
|
|
3612
|
+
},
|
|
3613
|
+
within: (reach, port) => narrowed(reachable, reach, port),
|
|
2092
3614
|
};
|
|
2093
|
-
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2094
3615
|
}
|