@agentshouse/mdruntime 0.1.0-c7bd7ca3faee4 → 0.1.0-c89fa07b05d4b
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 +259 -216
- package/README.md +11 -1
- package/dist/adapter.d.ts +66 -16
- package/dist/adapter.d.ts.map +1 -1
- 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 +17 -5
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +109 -37
- package/dist/change-set.d.ts +2 -0
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +48 -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/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 +60 -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 +26 -0
- package/dist/chat/list.d.ts.map +1 -0
- package/dist/chat/list.js +62 -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 +21 -0
- package/dist/chat/messages.d.ts.map +1 -0
- package/dist/chat/messages.js +151 -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 +620 -0
- package/dist/chat/question.d.ts +9 -0
- package/dist/chat/question.d.ts.map +1 -0
- package/dist/chat/question.js +80 -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 +33 -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 +18 -11
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +31 -16
- package/dist/collection.d.ts +21 -12
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +271 -23
- package/dist/connection.d.ts +28 -14
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +1218 -370
- package/dist/conversation.d.ts +23 -10
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +304 -136
- package/dist/entry.d.ts +17 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +156 -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 +4 -2
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +17 -13
- 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 +12 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/memory.d.ts +4 -3
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +63 -21
- package/dist/model.d.ts +4 -2
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +9 -0
- package/dist/observation.d.ts +5 -2
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +22 -11
- 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 +32 -3
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +42 -1
- package/dist/overlay.d.ts +15 -3
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +94 -77
- package/dist/query.d.ts +32 -3
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +3 -0
- package/dist/reach.d.ts +2 -0
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +45 -21
- package/dist/react.d.ts +7 -5
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +3 -2
- package/dist/read-side.d.ts +13 -6
- package/dist/read-side.d.ts.map +1 -1
- package/dist/read-side.js +291 -85
- 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 +10 -0
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +43 -38
- package/dist/replica.d.ts +8 -5
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +114 -67
- package/dist/room-draft.d.ts +26 -9
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +132 -23
- package/dist/rows.d.ts +8 -13
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +12 -14
- package/dist/save.d.ts +31 -1
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +376 -45
- package/dist/search.d.ts +31 -8
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +109 -44
- 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 +2 -2
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +0 -8
- package/dist/store.d.ts +64 -18
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +494 -195
- package/dist/turn.d.ts +7 -0
- package/dist/turn.d.ts.map +1 -0
- package/dist/turn.js +45 -0
- package/dist/wire.d.ts +6 -3
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +14 -2
- 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
|
@@ -3,19 +3,28 @@ import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
|
|
|
3
3
|
import { observing } from './observation.js';
|
|
4
4
|
import { filterOf, identityOf, planOf, } from './query.js';
|
|
5
5
|
import { createReadSide } from './read-side.js';
|
|
6
|
+
import { markdownPath, roomOfKey } from './rows.js';
|
|
6
7
|
import { createSearchIndex, searchPlanOf, } from './search.js';
|
|
7
8
|
import { bootstrapRoom } from './bootstrap.js';
|
|
8
|
-
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
9
|
+
import { changeSet, entering, mergeChangeSets } from './change-set.js';
|
|
9
10
|
import { createConversationStore, } from './conversation.js';
|
|
10
|
-
import {
|
|
11
|
+
import { queuedFile } from './change.js';
|
|
12
|
+
import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
|
|
13
|
+
import { runtimeError } from './failure.js';
|
|
11
14
|
import { createLog } from './log.js';
|
|
15
|
+
import { binaryRecord } from './binary.js';
|
|
16
|
+
import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
|
|
17
|
+
import { createRecovery } from './recovery.js';
|
|
12
18
|
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 {
|
|
19
|
+
import { buildBatch, joined, limitedIn, requeued } from './save.js';
|
|
20
|
+
import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
|
|
21
|
+
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
22
|
+
import { same, saving, STARTING, transferring, } from './status.js';
|
|
17
23
|
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
18
|
-
import {
|
|
24
|
+
import { FencedMap } from './fence.js';
|
|
25
|
+
import { tally } from './shared.js';
|
|
26
|
+
import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
|
|
27
|
+
import { identityTime, roomDraftRevision } from './identity.js';
|
|
19
28
|
import { beneath, normalize } from './paths.js';
|
|
20
29
|
import { openRuntimeStore, } from './store.js';
|
|
21
30
|
import { unconfirmed } from './transfer.js';
|
|
@@ -33,20 +42,59 @@ function declaredSelection(room) {
|
|
|
33
42
|
root !== '' &&
|
|
34
43
|
selection.roots.every((other, at) => at === index || !within(other, root)));
|
|
35
44
|
}
|
|
45
|
+
const OPENING_LANE = 'opening';
|
|
46
|
+
const CONVERSATION_LANE = 'conversations';
|
|
47
|
+
const ROOM_READS = 256;
|
|
48
|
+
const ROOM_ATTEMPTS = 16;
|
|
49
|
+
const outside = new Map();
|
|
50
|
+
function outsideOf(roots) {
|
|
51
|
+
const key = JSON.stringify(roots);
|
|
52
|
+
const held = outside.get(key);
|
|
53
|
+
if (held !== undefined)
|
|
54
|
+
return held;
|
|
55
|
+
const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
|
|
56
|
+
const made = {
|
|
57
|
+
documents: (path) => (markdownPath(path) ? counted(path) : 0),
|
|
58
|
+
binaries: counted,
|
|
59
|
+
};
|
|
60
|
+
outside.set(key, made);
|
|
61
|
+
return made;
|
|
62
|
+
}
|
|
63
|
+
function abandonedOn(signal) {
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
if (signal.aborted)
|
|
66
|
+
resolve(null);
|
|
67
|
+
else
|
|
68
|
+
signal.addEventListener('abort', () => resolve(null), { once: true });
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function roomLane(locator) {
|
|
72
|
+
return `room ${locator}`;
|
|
73
|
+
}
|
|
36
74
|
export function composeRuntime(options) {
|
|
75
|
+
const localSaves = options.adapter.localSaves === true;
|
|
37
76
|
let authorization = options.authorization;
|
|
38
77
|
let opening;
|
|
39
78
|
let store;
|
|
40
|
-
const
|
|
79
|
+
const fences = new Map();
|
|
80
|
+
const isFenced = (locator) => fences.has(locator);
|
|
81
|
+
let fencings = 0;
|
|
82
|
+
const lastFenced = new Map();
|
|
83
|
+
const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
|
|
84
|
+
let discoveries = 0;
|
|
85
|
+
const replicas = new FencedMap(isFenced);
|
|
86
|
+
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
41
87
|
const pendings = new Map();
|
|
42
|
-
const roomDrafts = new
|
|
88
|
+
const roomDrafts = new FencedMap(isFenced);
|
|
43
89
|
const presentations = new Map();
|
|
44
90
|
let limits = [];
|
|
45
91
|
let capabilities = [];
|
|
46
|
-
const rooms = new
|
|
92
|
+
const rooms = new FencedMap(isFenced);
|
|
93
|
+
const counted = new Map();
|
|
47
94
|
const unreadable = new Set();
|
|
48
|
-
const
|
|
49
|
-
const
|
|
95
|
+
const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
|
|
96
|
+
const models = new FencedMap(isModelFenced);
|
|
97
|
+
const liveModels = new FencedMap(isModelFenced);
|
|
50
98
|
const invalidModels = new Set();
|
|
51
99
|
const discovered = new Map();
|
|
52
100
|
let roomsListed = false;
|
|
@@ -76,19 +124,28 @@ export function composeRuntime(options) {
|
|
|
76
124
|
let status = STARTING;
|
|
77
125
|
let shown = STARTING;
|
|
78
126
|
let discoveryPaused = false;
|
|
79
|
-
let resetTimer;
|
|
80
127
|
let closed = false;
|
|
81
128
|
let stopping = false;
|
|
82
129
|
let withdrawStream;
|
|
83
130
|
const announced = new Map();
|
|
84
131
|
let underway = null;
|
|
132
|
+
let streamed = null;
|
|
85
133
|
let handed = false;
|
|
86
|
-
const
|
|
134
|
+
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
135
|
+
const repairing = new Set();
|
|
136
|
+
const repairable = new Map();
|
|
137
|
+
let outcomeRetention = null;
|
|
87
138
|
const answering = new Map();
|
|
88
139
|
let complete = false;
|
|
89
140
|
let recorded = false;
|
|
90
141
|
const stale = new Set();
|
|
91
|
-
let
|
|
142
|
+
let generation = 0;
|
|
143
|
+
let discovering = null;
|
|
144
|
+
let opened = null;
|
|
145
|
+
const owed = new Set();
|
|
146
|
+
const lanes = new Map();
|
|
147
|
+
let roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
148
|
+
const working = new Set();
|
|
92
149
|
let mutations = Promise.resolve();
|
|
93
150
|
const roomListeners = new Set();
|
|
94
151
|
const refusalListeners = new Set();
|
|
@@ -99,7 +156,7 @@ export function composeRuntime(options) {
|
|
|
99
156
|
const presentationListeners = new Set();
|
|
100
157
|
const readSide = createReadSide();
|
|
101
158
|
let withdrawConversations;
|
|
102
|
-
let currencyQueued =
|
|
159
|
+
let currencyQueued = null;
|
|
103
160
|
let socketOpen = false;
|
|
104
161
|
let serviceMode = 'open';
|
|
105
162
|
let haltedRefusal = false;
|
|
@@ -107,21 +164,31 @@ export function composeRuntime(options) {
|
|
|
107
164
|
const serviceModeListeners = new Set();
|
|
108
165
|
let member = null;
|
|
109
166
|
const memberListeners = new Set();
|
|
167
|
+
const prepared = new Set();
|
|
168
|
+
const projecting = new Set();
|
|
169
|
+
const demanded = new Set();
|
|
170
|
+
let preparing = false;
|
|
171
|
+
const catching = new Set();
|
|
172
|
+
const transfers = new FencedMap(isFenced);
|
|
110
173
|
const conversations = createConversationStore({
|
|
111
174
|
agents: declaredAgents,
|
|
112
|
-
documents: (locator) =>
|
|
175
|
+
documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
|
|
113
176
|
call: (name, input) => invokeAction(name, input),
|
|
114
177
|
durably,
|
|
115
178
|
stream: () => options.adapter.conversations,
|
|
116
179
|
publish: publishChangeSet,
|
|
180
|
+
online: () => !status.providerUnreachable && refusal === null,
|
|
117
181
|
closed: () => closed,
|
|
182
|
+
unanswered: resendLater,
|
|
183
|
+
expired: (operation) => pastHorizon(operation),
|
|
118
184
|
});
|
|
119
185
|
const searchIndex = createSearchIndex({
|
|
120
|
-
|
|
186
|
+
room: (room) => readSide.room(room),
|
|
121
187
|
document: (room, path) => readSide.document(room, path),
|
|
122
|
-
messages: () => conversations.messages(),
|
|
123
|
-
message: (conversation, identity) => conversations.message(conversation, identity),
|
|
124
188
|
});
|
|
189
|
+
const roomsToIndex = new Set();
|
|
190
|
+
const conversationsToIndex = new Set();
|
|
191
|
+
let indexing = false;
|
|
125
192
|
const journal = createLog();
|
|
126
193
|
let notifyReady = () => { };
|
|
127
194
|
const ready = new Promise((resolve) => {
|
|
@@ -185,7 +252,9 @@ export function composeRuntime(options) {
|
|
|
185
252
|
}
|
|
186
253
|
}
|
|
187
254
|
function pending() {
|
|
188
|
-
return
|
|
255
|
+
return (!localSaves &&
|
|
256
|
+
(queuesWithoutReplica.size > 0 ||
|
|
257
|
+
[...replicas.values()].some((replica) => replica.queue.length > 0)));
|
|
189
258
|
}
|
|
190
259
|
function attention() {
|
|
191
260
|
const attentionRequired = [];
|
|
@@ -214,8 +283,21 @@ export function composeRuntime(options) {
|
|
|
214
283
|
}
|
|
215
284
|
return { attentionRequired, reset };
|
|
216
285
|
}
|
|
286
|
+
function documentsIn(locator) {
|
|
287
|
+
const replica = replicas.get(locator);
|
|
288
|
+
if (replica === undefined)
|
|
289
|
+
return 0;
|
|
290
|
+
const roots = replica.confirmed.record.typedSelection?.roots ?? [];
|
|
291
|
+
const weighed = outsideOf(roots);
|
|
292
|
+
return (tally(replica.visible.documents, weighed.documents) +
|
|
293
|
+
tally(replica.visible.binaries, weighed.binaries));
|
|
294
|
+
}
|
|
217
295
|
function listed() {
|
|
218
|
-
return structuredClone([...rooms.values()])
|
|
296
|
+
return structuredClone([...rooms.values()].map((room) => {
|
|
297
|
+
const documents = documentsIn(room.locator);
|
|
298
|
+
counted.set(room.locator, documents);
|
|
299
|
+
return { ...room, documents };
|
|
300
|
+
}));
|
|
219
301
|
}
|
|
220
302
|
function roomFacts(reach = null) {
|
|
221
303
|
const held = [...rooms.values()]
|
|
@@ -235,6 +317,7 @@ export function composeRuntime(options) {
|
|
|
235
317
|
return { listed: roomsListed, rooms: held };
|
|
236
318
|
}
|
|
237
319
|
function emitRooms() {
|
|
320
|
+
readSide.recover();
|
|
238
321
|
const value = listed();
|
|
239
322
|
for (const listener of roomListeners)
|
|
240
323
|
listener(value);
|
|
@@ -270,6 +353,8 @@ export function composeRuntime(options) {
|
|
|
270
353
|
listener(shown);
|
|
271
354
|
}
|
|
272
355
|
function entryOf(batch) {
|
|
356
|
+
if (localSaves)
|
|
357
|
+
return Object.freeze({ batch, state: 'local' });
|
|
273
358
|
const state = pendings.get(batch.id);
|
|
274
359
|
if (state?.accepted !== undefined) {
|
|
275
360
|
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
@@ -277,10 +362,7 @@ export function composeRuntime(options) {
|
|
|
277
362
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
278
363
|
}
|
|
279
364
|
function entriesOf(locator) {
|
|
280
|
-
|
|
281
|
-
if (replica === undefined)
|
|
282
|
-
return [];
|
|
283
|
-
return replica.queue.map(entryOf);
|
|
365
|
+
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
|
|
284
366
|
}
|
|
285
367
|
function emitQueue(locator) {
|
|
286
368
|
const entries = entriesOf(locator);
|
|
@@ -290,12 +372,191 @@ export function composeRuntime(options) {
|
|
|
290
372
|
}
|
|
291
373
|
}
|
|
292
374
|
function applyChangeSet(set) {
|
|
293
|
-
|
|
294
|
-
|
|
375
|
+
const held = {
|
|
376
|
+
...set,
|
|
377
|
+
documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
|
|
378
|
+
};
|
|
379
|
+
readSide.apply(held);
|
|
380
|
+
searchIndex.changed(held);
|
|
381
|
+
}
|
|
382
|
+
async function referenced(locator) {
|
|
383
|
+
for (;;) {
|
|
384
|
+
const record = replicas.get(locator)?.confirmed.record;
|
|
385
|
+
if (record === undefined || closed)
|
|
386
|
+
return;
|
|
387
|
+
const held = { text: record.documents, binaries: record.binaries };
|
|
388
|
+
const composition = compositionOf(record);
|
|
389
|
+
if (referencesIndexed(held, composition))
|
|
390
|
+
return;
|
|
391
|
+
await indexReferrers(held, composition);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
async function prepareReplica(locator) {
|
|
395
|
+
await referenced(locator);
|
|
396
|
+
const replica = replicas.get(locator);
|
|
397
|
+
if (replica === undefined || closed || prepared.has(locator))
|
|
398
|
+
return;
|
|
399
|
+
const visible = replica.visible;
|
|
400
|
+
const paths = [
|
|
401
|
+
...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
|
|
402
|
+
];
|
|
403
|
+
const entered = [];
|
|
404
|
+
projecting.add(locator);
|
|
405
|
+
try {
|
|
406
|
+
await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
|
|
407
|
+
const current = replicas.get(locator);
|
|
408
|
+
if (current === undefined || closed)
|
|
409
|
+
return;
|
|
410
|
+
const set = entering(locator, current, chunk);
|
|
411
|
+
readSide.apply(set);
|
|
412
|
+
entered.push(set);
|
|
413
|
+
});
|
|
414
|
+
if (!replicas.has(locator) || closed)
|
|
415
|
+
return;
|
|
416
|
+
prepared.add(locator);
|
|
417
|
+
}
|
|
418
|
+
finally {
|
|
419
|
+
projecting.delete(locator);
|
|
420
|
+
}
|
|
421
|
+
conversations.changed(mergeChangeSets(entered));
|
|
422
|
+
readSide.recover();
|
|
423
|
+
}
|
|
424
|
+
function unprepared() {
|
|
425
|
+
return [...demanded, ...replicas.keys()].find((locator) => !prepared.has(locator));
|
|
426
|
+
}
|
|
427
|
+
function prepare(locators = []) {
|
|
428
|
+
for (const locator of locators)
|
|
429
|
+
demanded.add(locator);
|
|
430
|
+
if (preparing)
|
|
431
|
+
return;
|
|
432
|
+
preparing = true;
|
|
433
|
+
void (async () => {
|
|
434
|
+
try {
|
|
435
|
+
for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
|
|
436
|
+
await yielded();
|
|
437
|
+
if (closed)
|
|
438
|
+
return;
|
|
439
|
+
demanded.delete(waiting);
|
|
440
|
+
await prepareReplica(waiting);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
finally {
|
|
444
|
+
preparing = false;
|
|
445
|
+
}
|
|
446
|
+
})();
|
|
447
|
+
}
|
|
448
|
+
function reachedRooms(plan) {
|
|
449
|
+
const reach = plan.reach;
|
|
450
|
+
const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
451
|
+
const named = plan.rooms ??
|
|
452
|
+
(reached === null
|
|
453
|
+
? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
|
|
454
|
+
: [...reached]);
|
|
455
|
+
return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
|
|
456
|
+
}
|
|
457
|
+
function awaited(locator) {
|
|
458
|
+
return rooms.has(locator) || !roomsListed;
|
|
459
|
+
}
|
|
460
|
+
function covered(plan) {
|
|
461
|
+
const locators = reachedRooms(plan);
|
|
462
|
+
if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
|
|
463
|
+
return { pending: [] };
|
|
464
|
+
}
|
|
465
|
+
const pending = [];
|
|
466
|
+
const coverage = [];
|
|
467
|
+
const overlay = [];
|
|
468
|
+
for (const locator of locators) {
|
|
469
|
+
const replica = replicas.get(locator);
|
|
470
|
+
if (replica === undefined) {
|
|
471
|
+
if (awaited(locator))
|
|
472
|
+
pending.push({ room: locator, awaits: 'acquisition' });
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
if (!prepared.has(locator)) {
|
|
476
|
+
pending.push({ room: locator, awaits: 'preparation' });
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
coverage.push({
|
|
480
|
+
room: locator,
|
|
481
|
+
generation: replica.confirmed.record.transfer,
|
|
482
|
+
current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
|
|
483
|
+
});
|
|
484
|
+
for (const batch of replica.queue)
|
|
485
|
+
overlay.push(batch.id);
|
|
486
|
+
}
|
|
487
|
+
if (pending.length > 0)
|
|
488
|
+
return { pending };
|
|
489
|
+
return { coverage: { rooms: coverage, indexes: [], overlay } };
|
|
490
|
+
}
|
|
491
|
+
function awaitPreparation(cover) {
|
|
492
|
+
if (!('pending' in cover))
|
|
493
|
+
return;
|
|
494
|
+
const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
|
|
495
|
+
if (waiting.length > 0)
|
|
496
|
+
prepare(waiting.map(({ room }) => room));
|
|
497
|
+
}
|
|
498
|
+
async function rowsQuery(plan) {
|
|
499
|
+
const before = covered(plan);
|
|
500
|
+
awaitPreparation(before);
|
|
501
|
+
if ('pending' in before)
|
|
502
|
+
return before;
|
|
503
|
+
const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
|
|
504
|
+
awaitPreparation(cover);
|
|
505
|
+
if ('pending' in cover)
|
|
506
|
+
return cover;
|
|
507
|
+
return 'groups' in rows
|
|
508
|
+
? { groups: rows.groups, coverage: cover.coverage }
|
|
509
|
+
: { documents: rows.documents, coverage: cover.coverage };
|
|
510
|
+
}
|
|
511
|
+
async function indexConversation(conversation) {
|
|
512
|
+
searchIndex.indexMessages(conversation, [], false);
|
|
513
|
+
let after = null;
|
|
514
|
+
for (;;) {
|
|
515
|
+
const batch = await durably((local) => local.messagesAfter(conversation, after));
|
|
516
|
+
if (closed)
|
|
517
|
+
return;
|
|
518
|
+
searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
|
|
519
|
+
if (batch.length === 0)
|
|
520
|
+
return;
|
|
521
|
+
after = batch[batch.length - 1].identity;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
function indexNext() {
|
|
525
|
+
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator));
|
|
526
|
+
if (room !== undefined) {
|
|
527
|
+
roomsToIndex.delete(room);
|
|
528
|
+
return () => (prepared.has(room) ? searchIndex.indexRoom(room) : undefined);
|
|
529
|
+
}
|
|
530
|
+
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
531
|
+
if (conversation === undefined)
|
|
532
|
+
return null;
|
|
533
|
+
conversationsToIndex.delete(conversation);
|
|
534
|
+
return () => indexConversation(conversation);
|
|
535
|
+
}
|
|
536
|
+
function index(rooms, named) {
|
|
537
|
+
for (const room of rooms)
|
|
538
|
+
roomsToIndex.add(room);
|
|
539
|
+
for (const conversation of named)
|
|
540
|
+
conversationsToIndex.add(conversation);
|
|
541
|
+
if (indexing)
|
|
542
|
+
return;
|
|
543
|
+
indexing = true;
|
|
544
|
+
void (async () => {
|
|
545
|
+
try {
|
|
546
|
+
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
547
|
+
await yielded();
|
|
548
|
+
await unit();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
finally {
|
|
552
|
+
indexing = false;
|
|
553
|
+
}
|
|
554
|
+
})().catch(() => { });
|
|
295
555
|
}
|
|
296
556
|
function publishChangeSet(set) {
|
|
297
557
|
applyChangeSet(set);
|
|
298
|
-
if (set.rooms.length > 0
|
|
558
|
+
if (set.rooms.length > 0 ||
|
|
559
|
+
[...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
|
|
299
560
|
emitRooms();
|
|
300
561
|
for (const locator of set.queues)
|
|
301
562
|
emitQueue(locator);
|
|
@@ -307,34 +568,7 @@ export function composeRuntime(options) {
|
|
|
307
568
|
emitPresentation(app);
|
|
308
569
|
conversations.changed(set);
|
|
309
570
|
}
|
|
310
|
-
function stopTimer() {
|
|
311
|
-
if (resetTimer === undefined)
|
|
312
|
-
return;
|
|
313
|
-
clearTimeout(resetTimer);
|
|
314
|
-
resetTimer = undefined;
|
|
315
|
-
}
|
|
316
|
-
function armTimer() {
|
|
317
|
-
stopTimer();
|
|
318
|
-
if (closed || stopping)
|
|
319
|
-
return;
|
|
320
|
-
let earliest = null;
|
|
321
|
-
for (const { submission } of pendings.values()) {
|
|
322
|
-
if (submission.state !== 'reset')
|
|
323
|
-
continue;
|
|
324
|
-
const at = Date.parse(submission.reset);
|
|
325
|
-
if (earliest === null || at < earliest)
|
|
326
|
-
earliest = at;
|
|
327
|
-
}
|
|
328
|
-
if (earliest === null)
|
|
329
|
-
return;
|
|
330
|
-
// A Save House held back goes again only once the reset its answer named has passed.
|
|
331
|
-
resetTimer = setTimeout(() => {
|
|
332
|
-
resetTimer = undefined;
|
|
333
|
-
submitAll();
|
|
334
|
-
}, Math.max(0, earliest - Date.now()));
|
|
335
|
-
}
|
|
336
571
|
async function refuse(reason, provider = null) {
|
|
337
|
-
stopTimer();
|
|
338
572
|
discoveryPaused = true;
|
|
339
573
|
try {
|
|
340
574
|
if (reason === 'revoked-authorization') {
|
|
@@ -342,10 +576,7 @@ export function composeRuntime(options) {
|
|
|
342
576
|
const revoked = { status: 'unavailable', reason: 'revoked' };
|
|
343
577
|
try {
|
|
344
578
|
const local = await openLocal();
|
|
345
|
-
await local.revokeModels([...replicas.values()].map((replica) =>
|
|
346
|
-
record: replica.confirmed.record,
|
|
347
|
-
queue: replica.queue,
|
|
348
|
-
})));
|
|
579
|
+
await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
|
|
349
580
|
}
|
|
350
581
|
catch {
|
|
351
582
|
journal.record('model revocation persistence failed');
|
|
@@ -369,10 +600,7 @@ export function composeRuntime(options) {
|
|
|
369
600
|
model: revoked,
|
|
370
601
|
selectionModel: revoked,
|
|
371
602
|
};
|
|
372
|
-
|
|
373
|
-
replicas.set(locator, read.kind === 'staged'
|
|
374
|
-
? replicaOf(read.confirmed, before.queue, before)
|
|
375
|
-
: { ...before, confirmed: { ...before.confirmed, record } });
|
|
603
|
+
replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
|
|
376
604
|
}
|
|
377
605
|
rooms.set(locator, {
|
|
378
606
|
...room,
|
|
@@ -389,7 +617,8 @@ export function composeRuntime(options) {
|
|
|
389
617
|
}
|
|
390
618
|
finally {
|
|
391
619
|
refusal = {
|
|
392
|
-
reason,
|
|
620
|
+
code: reason,
|
|
621
|
+
retryable: false,
|
|
393
622
|
reauthorizationOffered: reason === 'revoked-authorization',
|
|
394
623
|
refusal: provider,
|
|
395
624
|
};
|
|
@@ -414,19 +643,28 @@ export function composeRuntime(options) {
|
|
|
414
643
|
},
|
|
415
644
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
416
645
|
roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
|
|
417
|
-
attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
|
|
418
646
|
synchronizationAvailable,
|
|
419
647
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
420
648
|
});
|
|
421
649
|
}
|
|
422
|
-
async function applyDiscovery(
|
|
650
|
+
async function applyDiscovery(found, started) {
|
|
651
|
+
for (const room of found) {
|
|
652
|
+
const fence = fences.get(room.locator);
|
|
653
|
+
if (fence !== undefined && fence.erased && fence.at < started)
|
|
654
|
+
fences.delete(room.locator);
|
|
655
|
+
}
|
|
656
|
+
const listed = found.filter((room) => !fences.has(room.locator));
|
|
423
657
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
424
658
|
for (const locator of listedLocators)
|
|
425
659
|
scopedRefusals.delete(locator);
|
|
426
660
|
discovered.clear();
|
|
427
661
|
for (const room of listed)
|
|
428
662
|
discovered.set(room.locator, room);
|
|
429
|
-
|
|
663
|
+
const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
|
|
664
|
+
if (options.adapter.exhaustiveDiscovery === true)
|
|
665
|
+
await loseRooms(unlisted);
|
|
666
|
+
else
|
|
667
|
+
unlistedKept(unlisted);
|
|
430
668
|
for (const room of listed) {
|
|
431
669
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
432
670
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -437,43 +675,83 @@ export function composeRuntime(options) {
|
|
|
437
675
|
}
|
|
438
676
|
roomsListed = true;
|
|
439
677
|
emitRooms();
|
|
678
|
+
return listed;
|
|
440
679
|
}
|
|
441
680
|
function unlistedHeld(listed) {
|
|
442
|
-
return [
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
pendings.delete(batch.id);
|
|
458
|
-
replicas.delete(locator);
|
|
681
|
+
return [
|
|
682
|
+
...new Set([
|
|
683
|
+
...rooms.keys(),
|
|
684
|
+
...roomDrafts.keys(),
|
|
685
|
+
...transfers.keys(),
|
|
686
|
+
...queuesWithoutReplica.keys(),
|
|
687
|
+
]),
|
|
688
|
+
].filter((locator) => !listed.has(locator));
|
|
689
|
+
}
|
|
690
|
+
function unlistedKept(unlisted) {
|
|
691
|
+
for (const locator of unlisted) {
|
|
692
|
+
const held = replicas.get(locator)?.confirmed.record;
|
|
693
|
+
if (held !== undefined)
|
|
694
|
+
list(held, false);
|
|
695
|
+
else
|
|
459
696
|
rooms.delete(locator);
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
function fence(locators) {
|
|
700
|
+
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
701
|
+
const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
|
|
702
|
+
room: true,
|
|
703
|
+
queue: true,
|
|
704
|
+
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
705
|
+
}));
|
|
706
|
+
for (const locator of fresh) {
|
|
707
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
708
|
+
lastFenced.set(locator, ++fencings);
|
|
709
|
+
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
710
|
+
pendings.delete(batch.id);
|
|
711
|
+
replicas.delete(locator);
|
|
712
|
+
queuesWithoutReplica.delete(locator);
|
|
713
|
+
rooms.delete(locator);
|
|
714
|
+
roomDrafts.delete(locator);
|
|
715
|
+
transfers.delete(locator);
|
|
716
|
+
catching.delete(locator);
|
|
717
|
+
discovered.delete(locator);
|
|
718
|
+
announced.delete(locator);
|
|
719
|
+
unreadable.delete(locator);
|
|
720
|
+
stale.delete(locator);
|
|
721
|
+
repairable.delete(locator);
|
|
722
|
+
scopedRefusals.delete(locator);
|
|
723
|
+
sourceRefusals.delete(locator);
|
|
724
|
+
declarationRefusals.delete(locator);
|
|
725
|
+
for (const attached of ['collection', 'typed-selection']) {
|
|
726
|
+
const key = modelKey(locator, attached);
|
|
727
|
+
models.delete(key);
|
|
728
|
+
liveModels.delete(key);
|
|
729
|
+
invalidModels.delete(key);
|
|
473
730
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
731
|
+
journal.record(`room lost ${locator}`);
|
|
732
|
+
}
|
|
733
|
+
if (fresh.length === 0)
|
|
734
|
+
return fresh;
|
|
735
|
+
publishChangeSet(mergeChangeSets(removed));
|
|
736
|
+
for (const locator of fresh)
|
|
737
|
+
prepared.delete(locator);
|
|
738
|
+
searchIndex.forget(fresh);
|
|
739
|
+
readSide.recover();
|
|
740
|
+
moveTo({ ...status, ...attention() });
|
|
741
|
+
return fresh;
|
|
742
|
+
}
|
|
743
|
+
async function erased(local, locators) {
|
|
744
|
+
await local.erase(locators);
|
|
745
|
+
for (const locator of locators) {
|
|
746
|
+
const held = fences.get(locator);
|
|
747
|
+
if (held !== undefined)
|
|
748
|
+
held.erased = true;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
async function loseRooms(locators) {
|
|
752
|
+
const fresh = fence(locators);
|
|
753
|
+
if (fresh.length > 0)
|
|
754
|
+
await durably((local) => erased(local, fresh));
|
|
477
755
|
}
|
|
478
756
|
function restored(key, persisted) {
|
|
479
757
|
const prior = models.get(key);
|
|
@@ -494,7 +772,7 @@ export function composeRuntime(options) {
|
|
|
494
772
|
return model;
|
|
495
773
|
}
|
|
496
774
|
async function hydrate(opened) {
|
|
497
|
-
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known,] = await Promise.all([
|
|
775
|
+
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
|
|
498
776
|
opened.all(),
|
|
499
777
|
opened.queues(),
|
|
500
778
|
opened.roomDrafts(),
|
|
@@ -505,7 +783,11 @@ export function composeRuntime(options) {
|
|
|
505
783
|
opened.providerLimits(),
|
|
506
784
|
opened.capabilities(),
|
|
507
785
|
opened.member(),
|
|
786
|
+
opened.transfers(),
|
|
508
787
|
]);
|
|
788
|
+
transfers.clear();
|
|
789
|
+
for (const record of staged)
|
|
790
|
+
transfers.set(record.locator, record);
|
|
509
791
|
limits = declared;
|
|
510
792
|
capabilities = named;
|
|
511
793
|
member = known;
|
|
@@ -524,18 +806,17 @@ export function composeRuntime(options) {
|
|
|
524
806
|
}
|
|
525
807
|
const previousReplicas = new Map(replicas);
|
|
526
808
|
replicas.clear();
|
|
809
|
+
queuesWithoutReplica.clear();
|
|
810
|
+
const replicated = new Set(records.map((record) => record.locator));
|
|
811
|
+
for (const [locator, queue] of queuedBatches) {
|
|
812
|
+
if (!replicated.has(locator))
|
|
813
|
+
queuesWithoutReplica.set(locator, queue);
|
|
814
|
+
}
|
|
527
815
|
for (const record of records) {
|
|
528
816
|
const model = restored(modelKey(record.locator, 'collection'), record.model);
|
|
529
817
|
const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
|
|
530
818
|
const prior = previousReplicas.get(record.locator);
|
|
531
|
-
const
|
|
532
|
-
const read = readConfirmed(retyped, prior?.confirmed);
|
|
533
|
-
const confirmed = read.kind === 'staged'
|
|
534
|
-
? read.confirmed
|
|
535
|
-
: {
|
|
536
|
-
record: retyped,
|
|
537
|
-
collection: parseCollection(retyped.documents, compositionOf(retyped)),
|
|
538
|
-
};
|
|
819
|
+
const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
|
|
539
820
|
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
|
|
540
821
|
const existing = rooms.get(record.locator);
|
|
541
822
|
if (existing === undefined)
|
|
@@ -544,7 +825,6 @@ export function composeRuntime(options) {
|
|
|
544
825
|
rooms.set(record.locator, {
|
|
545
826
|
...existing,
|
|
546
827
|
roomVersion: record.roomVersion,
|
|
547
|
-
attachments: record.attachments,
|
|
548
828
|
operations: record.operations,
|
|
549
829
|
replica: record.replica,
|
|
550
830
|
model: modelTypes(model),
|
|
@@ -556,10 +836,6 @@ export function composeRuntime(options) {
|
|
|
556
836
|
},
|
|
557
837
|
});
|
|
558
838
|
}
|
|
559
|
-
if (read.kind === 'refused') {
|
|
560
|
-
refusedSource(record, read);
|
|
561
|
-
declarationRefusals.add(record.locator);
|
|
562
|
-
}
|
|
563
839
|
}
|
|
564
840
|
return {
|
|
565
841
|
lastCompleted,
|
|
@@ -570,6 +846,7 @@ export function composeRuntime(options) {
|
|
|
570
846
|
async function readState(opened) {
|
|
571
847
|
const { lastCompleted, locators, read } = await hydrate(opened);
|
|
572
848
|
applyChangeSet(read);
|
|
849
|
+
prepare();
|
|
573
850
|
if (replicas.size > 0)
|
|
574
851
|
emitRooms();
|
|
575
852
|
if (member !== null)
|
|
@@ -580,8 +857,15 @@ export function composeRuntime(options) {
|
|
|
580
857
|
return lastCompleted;
|
|
581
858
|
}
|
|
582
859
|
async function open() {
|
|
583
|
-
|
|
860
|
+
if (closed)
|
|
861
|
+
throw new Error('runtime closed');
|
|
862
|
+
const opened = await openRuntimeStore(isFenced);
|
|
584
863
|
store = opened;
|
|
864
|
+
const interrupted = await opened.fences();
|
|
865
|
+
for (const locator of interrupted) {
|
|
866
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
867
|
+
lastFenced.set(locator, ++fencings);
|
|
868
|
+
}
|
|
585
869
|
const kept = options.shell.generation;
|
|
586
870
|
const storedGeneration = await opened.generation();
|
|
587
871
|
const loss = detectStorageLoss(storedGeneration, kept);
|
|
@@ -595,6 +879,8 @@ export function composeRuntime(options) {
|
|
|
595
879
|
emitStorageLoss();
|
|
596
880
|
}
|
|
597
881
|
const lastCompleted = await readState(opened);
|
|
882
|
+
if (interrupted.length > 0)
|
|
883
|
+
await erased(opened, interrupted);
|
|
598
884
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
599
885
|
return opened;
|
|
600
886
|
}
|
|
@@ -621,11 +907,12 @@ export function composeRuntime(options) {
|
|
|
621
907
|
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
622
908
|
}
|
|
623
909
|
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
624
|
-
const
|
|
910
|
+
const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
911
|
+
const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
|
|
625
912
|
const kept = queue.filter((batch) => !withdrawn.includes(batch));
|
|
626
913
|
const unqueued = withdrawn.map((batch) => ({
|
|
627
914
|
batch,
|
|
628
|
-
returned: landed(batch, confirmed) ? null :
|
|
915
|
+
returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
|
|
629
916
|
}));
|
|
630
917
|
const returning = [
|
|
631
918
|
...[...unqueued].reverse(),
|
|
@@ -646,28 +933,48 @@ export function composeRuntime(options) {
|
|
|
646
933
|
});
|
|
647
934
|
}
|
|
648
935
|
}
|
|
649
|
-
await local.
|
|
650
|
-
|
|
651
|
-
previous: before?.confirmed.record ?? null,
|
|
652
|
-
queue: kept,
|
|
653
|
-
previousQueue: before?.queue ?? [],
|
|
654
|
-
roomDrafts: [...written.values()],
|
|
655
|
-
});
|
|
656
|
-
for (const batch of before?.queue ?? []) {
|
|
936
|
+
await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
|
|
937
|
+
for (const batch of previousQueue) {
|
|
657
938
|
if (!kept.includes(batch))
|
|
658
939
|
pendings.delete(batch.id);
|
|
659
940
|
}
|
|
660
941
|
for (const record of written.values())
|
|
661
942
|
rememberRoomDraft(record);
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
943
|
+
try {
|
|
944
|
+
await local.write({
|
|
945
|
+
next: confirmed.record,
|
|
946
|
+
previous: before?.confirmed.record ?? null,
|
|
947
|
+
...(settling.stored === undefined ? {} : { stored: settling.stored }),
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
catch (error) {
|
|
951
|
+
const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
|
|
952
|
+
if (held !== null)
|
|
953
|
+
replicas.set(locator, held);
|
|
954
|
+
else if (kept.length > 0)
|
|
955
|
+
queuesWithoutReplica.set(locator, kept);
|
|
956
|
+
else
|
|
957
|
+
queuesWithoutReplica.delete(locator);
|
|
958
|
+
publishChangeSet(changeSet(locator, before ?? null, held, {
|
|
959
|
+
queue: kept.length !== previousQueue.length,
|
|
960
|
+
drafts: [...written.keys()],
|
|
961
|
+
}));
|
|
962
|
+
moveTo({ ...status, ...attention() });
|
|
963
|
+
throw error;
|
|
964
|
+
}
|
|
965
|
+
return () => {
|
|
966
|
+
queuesWithoutReplica.delete(locator);
|
|
967
|
+
for (const { batch, returned } of unqueued) {
|
|
968
|
+
const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
|
|
969
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
970
|
+
}
|
|
971
|
+
replicas.set(locator, replicaOf(confirmed, kept, before));
|
|
972
|
+
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
973
|
+
room: settling.room,
|
|
974
|
+
queue: kept.length !== previousQueue.length,
|
|
975
|
+
drafts: [...written.keys()],
|
|
976
|
+
});
|
|
977
|
+
};
|
|
671
978
|
}
|
|
672
979
|
function adoptModels(locator, record) {
|
|
673
980
|
for (const [attached, model] of [
|
|
@@ -700,10 +1007,16 @@ export function composeRuntime(options) {
|
|
|
700
1007
|
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
701
1008
|
};
|
|
702
1009
|
}
|
|
703
|
-
function publish(local, room, confirmed,
|
|
1010
|
+
function publish(local, room, confirmed, landing, live) {
|
|
1011
|
+
const { base, stored } = landing;
|
|
704
1012
|
return exclusive(async () => {
|
|
705
|
-
if (
|
|
1013
|
+
if (stored !== undefined) {
|
|
1014
|
+
await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
|
|
1015
|
+
}
|
|
1016
|
+
if (!live()) {
|
|
1017
|
+
caughtUpTo(room.locator);
|
|
706
1018
|
return true;
|
|
1019
|
+
}
|
|
707
1020
|
const prior = replicas.get(room.locator);
|
|
708
1021
|
if (base !== undefined &&
|
|
709
1022
|
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
@@ -711,13 +1024,17 @@ export function composeRuntime(options) {
|
|
|
711
1024
|
return false;
|
|
712
1025
|
}
|
|
713
1026
|
adoptModels(room.locator, confirmed.record);
|
|
714
|
-
const queue = prior?.queue ?? [];
|
|
1027
|
+
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
715
1028
|
const roomVersion = confirmed.record.roomVersion;
|
|
716
1029
|
const accepted = queue.filter((batch) => {
|
|
717
1030
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
718
1031
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
719
1032
|
});
|
|
720
|
-
const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
|
|
1033
|
+
const set = (await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) }))();
|
|
1034
|
+
if (stored !== undefined) {
|
|
1035
|
+
transfers.delete(room.locator);
|
|
1036
|
+
catching.delete(room.locator);
|
|
1037
|
+
}
|
|
721
1038
|
for (const batch of accepted)
|
|
722
1039
|
pendings.delete(batch.id);
|
|
723
1040
|
stale.delete(room.locator);
|
|
@@ -726,32 +1043,54 @@ export function composeRuntime(options) {
|
|
|
726
1043
|
sourceRefusals.delete(room.locator);
|
|
727
1044
|
declarationRefusals.delete(room.locator);
|
|
728
1045
|
list(room, true);
|
|
1046
|
+
if (prior === undefined)
|
|
1047
|
+
prepared.add(room.locator);
|
|
729
1048
|
publishChangeSet(set);
|
|
730
1049
|
moveTo({ ...status, ...attention() });
|
|
1050
|
+
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
731
1051
|
return true;
|
|
732
1052
|
});
|
|
733
1053
|
}
|
|
734
1054
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
735
1055
|
const local = await openLocal();
|
|
1056
|
+
await referenced(locator);
|
|
736
1057
|
const replica = replicas.get(locator);
|
|
737
1058
|
const room = rooms.get(locator);
|
|
738
1059
|
if (replica === undefined || room === undefined) {
|
|
739
|
-
throw
|
|
1060
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
740
1061
|
}
|
|
741
1062
|
const saved = [];
|
|
742
1063
|
for (const { path, revision } of named) {
|
|
743
1064
|
const record = roomDrafts.get(locator)?.get(path);
|
|
744
1065
|
if (record === undefined || record.revision !== revision) {
|
|
745
|
-
return { outcome: 'refused',
|
|
1066
|
+
return { outcome: 'refused', code: 'stale', retryable: false, file: path };
|
|
746
1067
|
}
|
|
747
1068
|
if (!saved.some((draft) => draft.path === path))
|
|
748
1069
|
saved.push({ path, parts: record.parts });
|
|
749
1070
|
}
|
|
750
1071
|
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
|
|
751
1072
|
if (changed.length > 0)
|
|
752
|
-
return { outcome: 'refused',
|
|
1073
|
+
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
753
1074
|
const parts = saved.map(({ path, parts }) => changesOf(path, parts));
|
|
754
|
-
const
|
|
1075
|
+
const changes = parts.flatMap(({ changes }) => changes);
|
|
1076
|
+
const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
|
|
1077
|
+
if (requeue.kind === 'refused')
|
|
1078
|
+
return { outcome: 'refused', ...requeue.refusal };
|
|
1079
|
+
const foreign = [...requeue.withdrawn.keys()].some((origin) => {
|
|
1080
|
+
const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
|
|
1081
|
+
return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
|
|
1082
|
+
});
|
|
1083
|
+
if (foreign) {
|
|
1084
|
+
return {
|
|
1085
|
+
outcome: 'refused',
|
|
1086
|
+
code: 'unknown-revision',
|
|
1087
|
+
retryable: false,
|
|
1088
|
+
file: [...requeue.queued].at(-1),
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
const withdrawing = requeue.withdrawn.size > 0;
|
|
1092
|
+
const input = {
|
|
1093
|
+
local: localSaves,
|
|
755
1094
|
baseRoomVersion: replica.confirmed.record.roomVersion,
|
|
756
1095
|
composition: compositionFor(replica.confirmed.record),
|
|
757
1096
|
operations: room.operations,
|
|
@@ -759,29 +1098,93 @@ export function composeRuntime(options) {
|
|
|
759
1098
|
providerLimits: limits,
|
|
760
1099
|
handle: room.handle,
|
|
761
1100
|
visible: replica.visible,
|
|
762
|
-
changes
|
|
1101
|
+
changes,
|
|
763
1102
|
bases: parts.flatMap(({ bases }) => bases),
|
|
764
1103
|
writtenBy,
|
|
1104
|
+
clean: {
|
|
1105
|
+
text: replica.confirmed.record.documents,
|
|
1106
|
+
binaries: replica.confirmed.record.binaries,
|
|
1107
|
+
composition: compositionOf(replica.confirmed.record),
|
|
1108
|
+
},
|
|
1109
|
+
};
|
|
1110
|
+
const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
|
|
1111
|
+
if (checked?.kind === 'refused')
|
|
1112
|
+
return { outcome: 'refused', ...checked.refusal };
|
|
1113
|
+
const owners = [...requeue.owners].flatMap((origin) => {
|
|
1114
|
+
const batch = requeue.withdrawn.get(origin);
|
|
1115
|
+
return batch === undefined || batch === null ? [] : [{ origin, batch }];
|
|
765
1116
|
});
|
|
766
|
-
if (
|
|
1117
|
+
if (owners.length > 1) {
|
|
1118
|
+
return {
|
|
1119
|
+
outcome: 'refused',
|
|
1120
|
+
code: 'unknown-revision',
|
|
1121
|
+
retryable: false,
|
|
1122
|
+
file: [...requeue.queued].at(-1),
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
const built = !withdrawing
|
|
1126
|
+
? buildBatch(input)
|
|
1127
|
+
: requeue.changes.length === 0
|
|
1128
|
+
? null
|
|
1129
|
+
: buildBatch({
|
|
1130
|
+
...input,
|
|
1131
|
+
visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
|
|
1132
|
+
changes: requeue.changes,
|
|
1133
|
+
});
|
|
1134
|
+
if (built?.kind === 'refused')
|
|
767
1135
|
return { outcome: 'refused', ...built.refusal };
|
|
768
|
-
const
|
|
1136
|
+
const [owner] = owners;
|
|
1137
|
+
const join = built === null || owner === undefined
|
|
1138
|
+
? null
|
|
1139
|
+
: joined(requeue.queue, owner.batch, built.batch);
|
|
1140
|
+
if (join?.kind === 'refused')
|
|
1141
|
+
return { outcome: 'refused', ...join.refusal };
|
|
1142
|
+
const withdrawn = new Map(requeue.withdrawn);
|
|
1143
|
+
if (join !== null && owner !== undefined)
|
|
1144
|
+
withdrawn.set(owner.origin, join.batch);
|
|
1145
|
+
const drafted = new Map();
|
|
1146
|
+
for (const [origin, replaced] of withdrawn) {
|
|
1147
|
+
if (replaced === null)
|
|
1148
|
+
continue;
|
|
1149
|
+
const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
|
|
1150
|
+
drafted.set(replaced.id, replaced === join?.batch
|
|
1151
|
+
? [
|
|
1152
|
+
...carried.map((held) => joinedDraft(held, saved)),
|
|
1153
|
+
...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
|
|
1154
|
+
]
|
|
1155
|
+
: carried);
|
|
1156
|
+
}
|
|
1157
|
+
if (built !== null && join === null)
|
|
1158
|
+
drafted.set(built.batch.id, saved);
|
|
1159
|
+
const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
|
|
1160
|
+
for (const replaced of withdrawn.values()) {
|
|
1161
|
+
if (replaced === null || !batches.includes(replaced))
|
|
1162
|
+
continue;
|
|
1163
|
+
const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
|
|
1164
|
+
if (limited !== null)
|
|
1165
|
+
return { outcome: 'refused', ...limited };
|
|
1166
|
+
}
|
|
769
1167
|
const consumed = saved.map(({ path }) => path);
|
|
770
1168
|
try {
|
|
771
|
-
await local.
|
|
772
|
-
|
|
773
|
-
previous: replica.confirmed.record,
|
|
1169
|
+
await local.keepAuthored({
|
|
1170
|
+
locator,
|
|
774
1171
|
queue: batches,
|
|
775
|
-
|
|
776
|
-
saved,
|
|
1172
|
+
saved: drafted,
|
|
777
1173
|
consumedRoomDrafts: consumed,
|
|
778
1174
|
});
|
|
779
1175
|
}
|
|
780
1176
|
catch {
|
|
781
|
-
return { outcome: 'refused',
|
|
1177
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
1178
|
+
}
|
|
1179
|
+
if (fences.has(locator))
|
|
1180
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1181
|
+
for (const origin of withdrawn.keys())
|
|
1182
|
+
pendings.delete(origin);
|
|
1183
|
+
for (const [id, drafts] of drafted) {
|
|
1184
|
+
pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
|
|
782
1185
|
}
|
|
783
|
-
|
|
784
|
-
|
|
1186
|
+
if (!localSaves)
|
|
1187
|
+
recorded = false;
|
|
785
1188
|
for (const path of consumed)
|
|
786
1189
|
forgetRoomDraft(locator, path);
|
|
787
1190
|
replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
|
|
@@ -789,9 +1192,12 @@ export function composeRuntime(options) {
|
|
|
789
1192
|
queue: true,
|
|
790
1193
|
drafts: consumed,
|
|
791
1194
|
}));
|
|
792
|
-
moveTo({ ...saving(status), ...attention() });
|
|
1195
|
+
moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
|
|
793
1196
|
submitLater(locator);
|
|
794
|
-
|
|
1197
|
+
const identifier = join?.batch.id ??
|
|
1198
|
+
built?.batch.id ??
|
|
1199
|
+
[...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
|
|
1200
|
+
return { outcome: 'saved', identifier };
|
|
795
1201
|
}
|
|
796
1202
|
async function mark(local, locator, identifier, submission, lost) {
|
|
797
1203
|
const state = pendings.get(identifier);
|
|
@@ -801,6 +1207,8 @@ export function composeRuntime(options) {
|
|
|
801
1207
|
return;
|
|
802
1208
|
}
|
|
803
1209
|
await local.submit(locator, identifier, submission, lost);
|
|
1210
|
+
if (fences.has(locator))
|
|
1211
|
+
return;
|
|
804
1212
|
pendings.set(identifier, { ...state, submission, lost });
|
|
805
1213
|
const replica = replicas.get(locator);
|
|
806
1214
|
if (replica !== undefined)
|
|
@@ -811,22 +1219,24 @@ export function composeRuntime(options) {
|
|
|
811
1219
|
if (!continuing())
|
|
812
1220
|
throw new Error('connection refused');
|
|
813
1221
|
await local.keepUpload(locator, identifier, upload);
|
|
1222
|
+
if (fences.has(locator))
|
|
1223
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
814
1224
|
pendings.set(identifier, { ...pendings.get(identifier), upload });
|
|
815
1225
|
if (!continuing())
|
|
816
1226
|
throw new Error('connection refused');
|
|
817
1227
|
}
|
|
818
1228
|
async function readHistory(locator, path, query = {}) {
|
|
819
1229
|
if (closed || refusal !== null)
|
|
820
|
-
return
|
|
1230
|
+
return UNCERTAIN;
|
|
821
1231
|
try {
|
|
822
1232
|
await ensureAuthorization();
|
|
823
1233
|
}
|
|
824
1234
|
catch {
|
|
825
|
-
return
|
|
826
|
-
}
|
|
827
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
828
|
-
return { kind: 'transport-uncertainty' };
|
|
1235
|
+
return UNCERTAIN;
|
|
829
1236
|
}
|
|
1237
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1238
|
+
return UNCERTAIN;
|
|
1239
|
+
const since = fencings;
|
|
830
1240
|
const transfer = ['transfer', 'history', locator, path];
|
|
831
1241
|
if (query.revision !== undefined)
|
|
832
1242
|
transfer.push(query.revision);
|
|
@@ -841,6 +1251,8 @@ export function composeRuntime(options) {
|
|
|
841
1251
|
...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
|
|
842
1252
|
});
|
|
843
1253
|
journal.record(`outcome history ${result.kind}`);
|
|
1254
|
+
if (fencedSince(locator, since))
|
|
1255
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
844
1256
|
if (result.kind === 'scoped-refusal') {
|
|
845
1257
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
846
1258
|
if (room !== undefined)
|
|
@@ -848,12 +1260,106 @@ export function composeRuntime(options) {
|
|
|
848
1260
|
}
|
|
849
1261
|
if (result.kind === 'revoked-authorization')
|
|
850
1262
|
await refuse('revoked-authorization');
|
|
851
|
-
return result;
|
|
1263
|
+
return historyAnswer(result);
|
|
852
1264
|
}
|
|
853
1265
|
catch {
|
|
854
1266
|
journal.record('outcome history transport-uncertainty');
|
|
855
|
-
return
|
|
1267
|
+
return UNCERTAIN;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
function queuedBlob(locator, path, localId) {
|
|
1271
|
+
for (const batch of replicas.get(locator)?.queue ?? []) {
|
|
1272
|
+
const change = batch.changes[queuedFile(batch, path, localId)];
|
|
1273
|
+
if (change?.kind === 'binary')
|
|
1274
|
+
return change.blob;
|
|
1275
|
+
}
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
function binaryRoom(request) {
|
|
1279
|
+
if (request.kind === 'path')
|
|
1280
|
+
return request.locator;
|
|
1281
|
+
const scope = options.adapter.referrerScope(request.referrer);
|
|
1282
|
+
return scope?.kind === 'room' ? scope.locator : null;
|
|
1283
|
+
}
|
|
1284
|
+
async function readBinary(request) {
|
|
1285
|
+
if (closed || refusal !== null)
|
|
1286
|
+
return UNCERTAIN;
|
|
1287
|
+
try {
|
|
1288
|
+
await ensureAuthorization();
|
|
856
1289
|
}
|
|
1290
|
+
catch {
|
|
1291
|
+
return UNCERTAIN;
|
|
1292
|
+
}
|
|
1293
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1294
|
+
return UNCERTAIN;
|
|
1295
|
+
const since = fencings;
|
|
1296
|
+
journal.record(request.kind === 'path'
|
|
1297
|
+
? `transfer binary ${request.locator} ${request.path}`
|
|
1298
|
+
: `transfer binary ${request.fileRef}`);
|
|
1299
|
+
try {
|
|
1300
|
+
const result = await options.adapter.binary(authorization, request);
|
|
1301
|
+
journal.record(`outcome binary ${result.kind}`);
|
|
1302
|
+
const locator = binaryRoom(request);
|
|
1303
|
+
if (locator !== null && fencedSince(locator, since))
|
|
1304
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
1305
|
+
if (result.kind === 'scoped-refusal') {
|
|
1306
|
+
const room = replicas.get(result.locator)?.confirmed.record;
|
|
1307
|
+
if (room !== undefined)
|
|
1308
|
+
await scoped(result, room, await openLocal());
|
|
1309
|
+
}
|
|
1310
|
+
if (result.kind === 'revoked-authorization')
|
|
1311
|
+
await refuse('revoked-authorization');
|
|
1312
|
+
return binaryAnswer(result);
|
|
1313
|
+
}
|
|
1314
|
+
catch {
|
|
1315
|
+
journal.record('outcome binary transport-uncertainty');
|
|
1316
|
+
return UNCERTAIN;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
async function readBinaryPath(locator, path, revision) {
|
|
1320
|
+
await ready;
|
|
1321
|
+
if (revision !== undefined)
|
|
1322
|
+
return readBinary({ kind: 'path', locator, path, revision });
|
|
1323
|
+
const visible = replicas.get(locator)?.visible;
|
|
1324
|
+
const held = visible?.binaries[path];
|
|
1325
|
+
if (visible === undefined || held === undefined)
|
|
1326
|
+
return absentBinary(path);
|
|
1327
|
+
if (held.state === 'pending') {
|
|
1328
|
+
const blob = queuedBlob(locator, path, held.localId);
|
|
1329
|
+
return blob === null ? absentBinary(path) : { kind: 'binary', blob };
|
|
1330
|
+
}
|
|
1331
|
+
const record = binaryRecord(path, held, visible.revisions[path]);
|
|
1332
|
+
if (record.state !== 'accepted')
|
|
1333
|
+
return absentBinary(path);
|
|
1334
|
+
const replica = replicas.get(locator);
|
|
1335
|
+
const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
|
|
1336
|
+
const holds = (at) => {
|
|
1337
|
+
const kept = replica?.confirmed.record.binaries[at];
|
|
1338
|
+
return (kept?.state === 'accepted' &&
|
|
1339
|
+
kept.fileRef === record.fileRef &&
|
|
1340
|
+
replica?.confirmed.record.revisions[at] === record.revision);
|
|
1341
|
+
};
|
|
1342
|
+
let confirmed = path;
|
|
1343
|
+
for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
|
|
1344
|
+
const change = queued[index];
|
|
1345
|
+
if (change.kind === 'move' && change.to === confirmed)
|
|
1346
|
+
confirmed = change.path;
|
|
1347
|
+
}
|
|
1348
|
+
return readBinary({
|
|
1349
|
+
kind: 'path',
|
|
1350
|
+
locator,
|
|
1351
|
+
path: confirmed,
|
|
1352
|
+
revision: record.revision,
|
|
1353
|
+
fileRef: record.fileRef,
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
async function readBinaryReference(reference) {
|
|
1357
|
+
await ready;
|
|
1358
|
+
return readBinary({
|
|
1359
|
+
kind: 'reference',
|
|
1360
|
+
fileRef: reference.fileRef,
|
|
1361
|
+
referrer: reference.referrer,
|
|
1362
|
+
});
|
|
857
1363
|
}
|
|
858
1364
|
function declaredAgents() {
|
|
859
1365
|
for (const capability of capabilities) {
|
|
@@ -874,18 +1380,17 @@ export function composeRuntime(options) {
|
|
|
874
1380
|
async function invokeAction(name, input) {
|
|
875
1381
|
await ready;
|
|
876
1382
|
if (closed || refusal !== null)
|
|
877
|
-
return
|
|
1383
|
+
return UNCERTAIN;
|
|
878
1384
|
if (declaredAction(name) === undefined)
|
|
879
|
-
return
|
|
1385
|
+
return undeclared(name);
|
|
880
1386
|
try {
|
|
881
1387
|
await ensureAuthorization();
|
|
882
1388
|
}
|
|
883
1389
|
catch {
|
|
884
|
-
return
|
|
885
|
-
}
|
|
886
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
887
|
-
return { kind: 'transport-uncertainty' };
|
|
1390
|
+
return UNCERTAIN;
|
|
888
1391
|
}
|
|
1392
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1393
|
+
return UNCERTAIN;
|
|
889
1394
|
journal.record(`transfer action ${name}`);
|
|
890
1395
|
try {
|
|
891
1396
|
const result = await options.adapter.action(authorization, { name, input });
|
|
@@ -899,11 +1404,11 @@ export function composeRuntime(options) {
|
|
|
899
1404
|
journal.record(`outcome action ${result.kind}`);
|
|
900
1405
|
if (result.kind === 'rejected')
|
|
901
1406
|
journal.record(`refusal rejected ${result.code}`);
|
|
902
|
-
return result;
|
|
1407
|
+
return calledResult(result);
|
|
903
1408
|
}
|
|
904
1409
|
catch {
|
|
905
1410
|
journal.record('outcome action transport-uncertainty');
|
|
906
|
-
return
|
|
1411
|
+
return UNCERTAIN;
|
|
907
1412
|
}
|
|
908
1413
|
}
|
|
909
1414
|
function continuing() {
|
|
@@ -920,11 +1425,11 @@ export function composeRuntime(options) {
|
|
|
920
1425
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
921
1426
|
!options.adapter.reached(locator, roomVersion, written));
|
|
922
1427
|
}
|
|
923
|
-
async function pullRefresh(authorized, confirmed) {
|
|
924
|
-
if (!
|
|
1428
|
+
async function pullRefresh(authorized, confirmed, live) {
|
|
1429
|
+
if (!live())
|
|
925
1430
|
return unconfirmed;
|
|
926
1431
|
try {
|
|
927
|
-
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held),
|
|
1432
|
+
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
|
|
928
1433
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
929
1434
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
930
1435
|
return result;
|
|
@@ -934,28 +1439,69 @@ export function composeRuntime(options) {
|
|
|
934
1439
|
throw error;
|
|
935
1440
|
}
|
|
936
1441
|
}
|
|
937
|
-
|
|
938
|
-
|
|
1442
|
+
function staging(local, live) {
|
|
1443
|
+
return {
|
|
1444
|
+
held: (locator) => transfers.get(locator) ?? null,
|
|
1445
|
+
staged: (record) => local.staged(record),
|
|
1446
|
+
stage: (record, page) => exclusive(async () => {
|
|
1447
|
+
if (!live())
|
|
1448
|
+
return false;
|
|
1449
|
+
await local.stage(record, page);
|
|
1450
|
+
transfers.set(record.locator, record);
|
|
1451
|
+
readSide.recover();
|
|
1452
|
+
return true;
|
|
1453
|
+
}),
|
|
1454
|
+
abandon: (locator) => exclusive(async () => {
|
|
1455
|
+
if (!live())
|
|
1456
|
+
return;
|
|
1457
|
+
await local.abandon(locator);
|
|
1458
|
+
transfers.delete(locator);
|
|
1459
|
+
readSide.recover();
|
|
1460
|
+
}),
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
function reusable(room) {
|
|
1464
|
+
const record = replicas.get(room.locator)?.confirmed.record;
|
|
1465
|
+
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1466
|
+
return null;
|
|
1467
|
+
return { documents: record.documents, revisions: record.revisions };
|
|
1468
|
+
}
|
|
1469
|
+
async function pullBootstrap(authorized, room, local, live) {
|
|
1470
|
+
if (!live())
|
|
939
1471
|
return unconfirmed;
|
|
1472
|
+
if (replicas.has(room.locator)) {
|
|
1473
|
+
catching.add(room.locator);
|
|
1474
|
+
readSide.recover();
|
|
1475
|
+
}
|
|
940
1476
|
try {
|
|
941
1477
|
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
942
1478
|
collection: modelOf(room.locator),
|
|
943
1479
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
944
|
-
}, (models, held) => remodel(room.locator, models, held),
|
|
1480
|
+
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
945
1481
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
946
1482
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1483
|
+
if (result.kind !== 'staged')
|
|
1484
|
+
caughtUpTo(room.locator);
|
|
947
1485
|
return result;
|
|
948
1486
|
}
|
|
949
1487
|
catch (error) {
|
|
950
1488
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
1489
|
+
caughtUpTo(room.locator);
|
|
951
1490
|
throw error;
|
|
952
1491
|
}
|
|
953
1492
|
}
|
|
954
|
-
|
|
1493
|
+
function caughtUpTo(locator) {
|
|
1494
|
+
if (catching.delete(locator))
|
|
1495
|
+
readSide.recover();
|
|
1496
|
+
}
|
|
1497
|
+
async function advance(room, local, authorized, live) {
|
|
1498
|
+
await referenced(room.locator);
|
|
1499
|
+
if (!live())
|
|
1500
|
+
return unconfirmed;
|
|
955
1501
|
const held = replicas.get(room.locator);
|
|
956
1502
|
if (held !== undefined) {
|
|
957
|
-
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
958
|
-
if (!
|
|
1503
|
+
const refreshed = await pullRefresh(authorized, held.confirmed, live);
|
|
1504
|
+
if (!live())
|
|
959
1505
|
return unconfirmed;
|
|
960
1506
|
if (refreshed.kind === 'still-current') {
|
|
961
1507
|
stale.delete(room.locator);
|
|
@@ -969,31 +1515,45 @@ export function composeRuntime(options) {
|
|
|
969
1515
|
return null;
|
|
970
1516
|
}
|
|
971
1517
|
if (refreshed.kind === 'advanced') {
|
|
972
|
-
if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
|
|
1518
|
+
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
|
|
973
1519
|
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
974
1520
|
const reached = now !== undefined &&
|
|
975
1521
|
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
976
|
-
return reached ? null : advance(room, local, authorized);
|
|
1522
|
+
return reached ? null : advance(room, local, authorized, live);
|
|
977
1523
|
}
|
|
978
1524
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
979
1525
|
return null;
|
|
980
1526
|
}
|
|
981
1527
|
if (refreshed.kind !== 'beyond-horizon')
|
|
982
1528
|
return refreshed;
|
|
1529
|
+
if (opened !== null) {
|
|
1530
|
+
owed.add(room.locator);
|
|
1531
|
+
readSide.recover();
|
|
1532
|
+
return unconfirmed;
|
|
1533
|
+
}
|
|
983
1534
|
}
|
|
984
|
-
const bootstrapped = await pullBootstrap(authorized, room);
|
|
985
|
-
if (!
|
|
1535
|
+
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
1536
|
+
if (!live())
|
|
986
1537
|
return unconfirmed;
|
|
987
1538
|
if (bootstrapped.kind !== 'staged')
|
|
988
1539
|
return bootstrapped;
|
|
989
|
-
await publish(local, room, bootstrapped.confirmed);
|
|
1540
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
990
1541
|
return null;
|
|
991
1542
|
}
|
|
992
1543
|
async function withhold(withheld, room, local, progress) {
|
|
1544
|
+
if (withheld.kind === 'unconfirmed')
|
|
1545
|
+
return;
|
|
993
1546
|
if (withheld.kind === 'refused')
|
|
994
1547
|
stale.delete(room.locator);
|
|
995
1548
|
else
|
|
996
1549
|
stale.add(room.locator);
|
|
1550
|
+
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
1551
|
+
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
1552
|
+
repairable.set(room.locator, reset);
|
|
1553
|
+
repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
|
|
1554
|
+
}
|
|
1555
|
+
else
|
|
1556
|
+
repairable.delete(room.locator);
|
|
997
1557
|
if (withheld.kind === 'scoped-refusal') {
|
|
998
1558
|
await scoped(withheld, room, local);
|
|
999
1559
|
}
|
|
@@ -1018,6 +1578,7 @@ export function composeRuntime(options) {
|
|
|
1018
1578
|
function refusedSource(room, { refusal }) {
|
|
1019
1579
|
sourceRefusals.set(room.locator, {
|
|
1020
1580
|
code: refusal.rule,
|
|
1581
|
+
retryable: false,
|
|
1021
1582
|
fields: {
|
|
1022
1583
|
file: refusal.file,
|
|
1023
1584
|
...(refusal.location === undefined ? {} : { location: refusal.location }),
|
|
@@ -1032,8 +1593,16 @@ export function composeRuntime(options) {
|
|
|
1032
1593
|
}
|
|
1033
1594
|
function leaving(batch, result) {
|
|
1034
1595
|
if (result.kind === 'expired')
|
|
1035
|
-
return { batch, returned:
|
|
1036
|
-
return {
|
|
1596
|
+
return { batch, returned: returnedBy('expired') };
|
|
1597
|
+
return {
|
|
1598
|
+
batch,
|
|
1599
|
+
returned: {
|
|
1600
|
+
kind: result.kind,
|
|
1601
|
+
code: result.code,
|
|
1602
|
+
fields: result.fields,
|
|
1603
|
+
retryable: result.retryable,
|
|
1604
|
+
},
|
|
1605
|
+
};
|
|
1037
1606
|
}
|
|
1038
1607
|
function logAccepted(locator, submitted, accepted) {
|
|
1039
1608
|
journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
|
|
@@ -1041,6 +1610,7 @@ export function composeRuntime(options) {
|
|
|
1041
1610
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1042
1611
|
}
|
|
1043
1612
|
async function adoptAccepted(room, local, submitted, accepted) {
|
|
1613
|
+
await referenced(room.locator);
|
|
1044
1614
|
if (!continuing())
|
|
1045
1615
|
return unconfirmed;
|
|
1046
1616
|
const current = replicas.get(room.locator);
|
|
@@ -1051,22 +1621,23 @@ export function composeRuntime(options) {
|
|
|
1051
1621
|
logAccepted(room.locator, submitted, accepted);
|
|
1052
1622
|
const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
1053
1623
|
? { kind: 'staged', confirmed: current.confirmed }
|
|
1054
|
-
: applyReturned(current.confirmed,
|
|
1624
|
+
: applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
1055
1625
|
? accepted.roomVersion
|
|
1056
1626
|
: held, accepted.roomVersion);
|
|
1057
1627
|
if (applied.kind !== 'staged') {
|
|
1058
1628
|
const state = pendings.get(submitted.id);
|
|
1059
1629
|
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1060
|
-
if (state !== undefined)
|
|
1630
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
1061
1631
|
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1062
1632
|
emitQueue(room.locator);
|
|
1063
1633
|
return applied;
|
|
1064
1634
|
}
|
|
1065
|
-
const
|
|
1635
|
+
const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
1066
1636
|
pendings.delete(submitted.id);
|
|
1067
1637
|
unreadable.delete(room.locator);
|
|
1068
1638
|
sourceRefusals.delete(room.locator);
|
|
1069
1639
|
declarationRefusals.delete(room.locator);
|
|
1640
|
+
const set = settled();
|
|
1070
1641
|
list(room, true);
|
|
1071
1642
|
publishChangeSet(set);
|
|
1072
1643
|
moveTo({ ...status, ...attention() });
|
|
@@ -1080,14 +1651,14 @@ export function composeRuntime(options) {
|
|
|
1080
1651
|
refusedSource(room, failed);
|
|
1081
1652
|
return false;
|
|
1082
1653
|
}
|
|
1083
|
-
async function submitRoom(room, local, authorized, progress) {
|
|
1084
|
-
while (continuing() && authorization !== undefined) {
|
|
1654
|
+
async function submitRoom(room, local, authorized, progress, signal) {
|
|
1655
|
+
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
1085
1656
|
const replica = replicas.get(room.locator);
|
|
1086
1657
|
const head = replica?.queue[0];
|
|
1087
1658
|
if (replica === undefined || head === undefined)
|
|
1088
|
-
return;
|
|
1659
|
+
return 'done';
|
|
1089
1660
|
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1090
|
-
return;
|
|
1661
|
+
return 'done';
|
|
1091
1662
|
const state = pendings.get(head.id);
|
|
1092
1663
|
const { submission, lost: earlier } = state ?? {
|
|
1093
1664
|
submission: { state: 'waiting' },
|
|
@@ -1095,24 +1666,30 @@ export function composeRuntime(options) {
|
|
|
1095
1666
|
};
|
|
1096
1667
|
if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
|
|
1097
1668
|
progress.complete = false;
|
|
1098
|
-
return;
|
|
1669
|
+
return { at: Date.parse(submission.reset) };
|
|
1099
1670
|
}
|
|
1100
1671
|
if (writesHeld()) {
|
|
1101
1672
|
progress.complete = false;
|
|
1102
1673
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1103
|
-
return;
|
|
1674
|
+
return 'done';
|
|
1104
1675
|
}
|
|
1105
1676
|
const lost = earlier || submission.state === 'submitted';
|
|
1677
|
+
if (lost && pastHorizon(head.id)) {
|
|
1678
|
+
await discardPending(room, local, signal);
|
|
1679
|
+
return 'done';
|
|
1680
|
+
}
|
|
1106
1681
|
let result;
|
|
1107
1682
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
1108
1683
|
await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
|
|
1109
|
-
if (discoveryPaused)
|
|
1110
|
-
return;
|
|
1111
|
-
if (writesHeld()) {
|
|
1684
|
+
if (discoveryPaused || fences.has(room.locator))
|
|
1685
|
+
return 'done';
|
|
1686
|
+
if (signal.aborted || writesHeld()) {
|
|
1112
1687
|
await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
|
|
1688
|
+
if (signal.aborted)
|
|
1689
|
+
return 'done';
|
|
1113
1690
|
progress.complete = false;
|
|
1114
1691
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1115
|
-
return;
|
|
1692
|
+
return 'done';
|
|
1116
1693
|
}
|
|
1117
1694
|
const opensBefore = openSignals;
|
|
1118
1695
|
const answered = awaitingAnswer(room.locator);
|
|
@@ -1128,32 +1705,36 @@ export function composeRuntime(options) {
|
|
|
1128
1705
|
progress.complete = false;
|
|
1129
1706
|
if (continuing())
|
|
1130
1707
|
progress.reachable = false;
|
|
1131
|
-
return;
|
|
1708
|
+
return 'again';
|
|
1709
|
+
}
|
|
1710
|
+
if (signal.aborted) {
|
|
1711
|
+
answered();
|
|
1712
|
+
return 'done';
|
|
1132
1713
|
}
|
|
1133
1714
|
if (result.kind !== 'accepted' || !continuing())
|
|
1134
1715
|
answered();
|
|
1135
1716
|
if (!continuing())
|
|
1136
|
-
return;
|
|
1717
|
+
return 'done';
|
|
1137
1718
|
if (result.kind === 'transport-uncertainty') {
|
|
1138
1719
|
progress.complete = false;
|
|
1139
1720
|
progress.reachable = false;
|
|
1140
|
-
return;
|
|
1721
|
+
return 'again';
|
|
1141
1722
|
}
|
|
1142
1723
|
if (result.kind === 'room-not-readable') {
|
|
1143
1724
|
progress.complete = false;
|
|
1144
1725
|
unreadable.add(room.locator);
|
|
1145
1726
|
list(room, false);
|
|
1146
1727
|
emitRooms();
|
|
1147
|
-
return;
|
|
1728
|
+
return 'done';
|
|
1148
1729
|
}
|
|
1149
1730
|
if (result.kind === 'revoked-authorization') {
|
|
1150
1731
|
await refuse('revoked-authorization');
|
|
1151
|
-
return;
|
|
1732
|
+
return 'done';
|
|
1152
1733
|
}
|
|
1153
1734
|
if (result.kind === 'scoped-refusal') {
|
|
1154
1735
|
await scoped(result, room, local);
|
|
1155
1736
|
progress.complete = false;
|
|
1156
|
-
return;
|
|
1737
|
+
return 'done';
|
|
1157
1738
|
}
|
|
1158
1739
|
if (result.kind === 'write-halted') {
|
|
1159
1740
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
@@ -1162,16 +1743,24 @@ export function composeRuntime(options) {
|
|
|
1162
1743
|
continue;
|
|
1163
1744
|
haltedRefusal = true;
|
|
1164
1745
|
progress.complete = false;
|
|
1165
|
-
return;
|
|
1746
|
+
return 'done';
|
|
1166
1747
|
}
|
|
1167
1748
|
if (result.kind === 'conflicted' ||
|
|
1168
1749
|
result.kind === 'rejected' ||
|
|
1169
1750
|
result.kind === 'expired' ||
|
|
1170
1751
|
result.kind === 'retryable-refusal') {
|
|
1752
|
+
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
1753
|
+
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
1754
|
+
await discardPending(room, local, signal);
|
|
1755
|
+
return 'done';
|
|
1756
|
+
}
|
|
1171
1757
|
if (result.kind === 'expired') {
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1758
|
+
await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
|
|
1759
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
1760
|
+
if (withheld !== null) {
|
|
1761
|
+
await withhold(withheld, room, local, progress);
|
|
1762
|
+
return 'done';
|
|
1763
|
+
}
|
|
1175
1764
|
}
|
|
1176
1765
|
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
|
|
1177
1766
|
(result.kind === 'conflicted' && result.roomVersion !== undefined
|
|
@@ -1184,8 +1773,7 @@ export function composeRuntime(options) {
|
|
|
1184
1773
|
const reset = result.reset;
|
|
1185
1774
|
progress.complete = false;
|
|
1186
1775
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1187
|
-
|
|
1188
|
-
return;
|
|
1776
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
1189
1777
|
}
|
|
1190
1778
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
1191
1779
|
const left = leaving(head, result);
|
|
@@ -1196,37 +1784,82 @@ export function composeRuntime(options) {
|
|
|
1196
1784
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
1197
1785
|
return;
|
|
1198
1786
|
}
|
|
1199
|
-
const
|
|
1787
|
+
const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
1200
1788
|
leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
|
|
1201
1789
|
(result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
|
|
1202
1790
|
? { ...left, returned: null }
|
|
1203
1791
|
: left,
|
|
1204
1792
|
});
|
|
1205
|
-
publishChangeSet(
|
|
1793
|
+
publishChangeSet(settled());
|
|
1206
1794
|
moveTo({ ...status, ...attention() });
|
|
1207
1795
|
});
|
|
1208
1796
|
if (result.kind === 'expired')
|
|
1209
1797
|
continue;
|
|
1210
|
-
const withheld = await advance(room, local, authorized);
|
|
1211
|
-
if (withheld !== null)
|
|
1212
|
-
|
|
1798
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
1799
|
+
if (withheld !== null) {
|
|
1800
|
+
await withhold(withheld, room, local, progress);
|
|
1801
|
+
return 'done';
|
|
1802
|
+
}
|
|
1213
1803
|
continue;
|
|
1214
1804
|
}
|
|
1215
1805
|
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1216
1806
|
if (!kept) {
|
|
1217
1807
|
progress.complete = false;
|
|
1218
|
-
return;
|
|
1808
|
+
return 'done';
|
|
1219
1809
|
}
|
|
1220
1810
|
}
|
|
1811
|
+
return 'done';
|
|
1812
|
+
}
|
|
1813
|
+
function pastHorizon(identity) {
|
|
1814
|
+
return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
|
|
1815
|
+
}
|
|
1816
|
+
async function discardPending(room, local, signal) {
|
|
1817
|
+
const { locator } = room;
|
|
1818
|
+
const discarded = await exclusive(async () => {
|
|
1819
|
+
if (fences.has(locator))
|
|
1820
|
+
return false;
|
|
1821
|
+
const before = replicas.get(locator);
|
|
1822
|
+
const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
1823
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
1824
|
+
lastFenced.set(locator, ++fencings);
|
|
1825
|
+
try {
|
|
1826
|
+
await local.discard(locator);
|
|
1827
|
+
}
|
|
1828
|
+
finally {
|
|
1829
|
+
fences.delete(locator);
|
|
1830
|
+
}
|
|
1831
|
+
for (const batch of queue)
|
|
1832
|
+
pendings.delete(batch.id);
|
|
1833
|
+
queuesWithoutReplica.delete(locator);
|
|
1834
|
+
const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
|
|
1835
|
+
if (after !== null)
|
|
1836
|
+
replicas.set(locator, after);
|
|
1837
|
+
journal.record(`room discarded ${locator}`);
|
|
1838
|
+
publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
|
|
1839
|
+
moveTo({ ...status, ...attention() });
|
|
1840
|
+
return true;
|
|
1841
|
+
});
|
|
1842
|
+
if (!discarded)
|
|
1843
|
+
return;
|
|
1844
|
+
const progress = { complete: true, reachable: true };
|
|
1845
|
+
await roomTask(locator, async (live) => {
|
|
1846
|
+
await Promise.race([
|
|
1847
|
+
reload(room, local, progress, () => live() && !signal.aborted),
|
|
1848
|
+
abandonedOn(signal),
|
|
1849
|
+
]);
|
|
1850
|
+
});
|
|
1851
|
+
if (signal.aborted)
|
|
1852
|
+
return;
|
|
1853
|
+
await rest(progress);
|
|
1221
1854
|
}
|
|
1222
1855
|
function awaitingAnswer(locator) {
|
|
1223
1856
|
let release = () => { };
|
|
1224
1857
|
const awaited = new Promise((resolve) => {
|
|
1225
1858
|
release = resolve;
|
|
1226
1859
|
});
|
|
1227
|
-
answering.set(locator, awaited);
|
|
1860
|
+
answering.set(locator, { generation, answered: awaited });
|
|
1228
1861
|
return () => {
|
|
1229
|
-
if (answering.get(locator) === awaited)
|
|
1862
|
+
if (answering.get(locator)?.answered === awaited)
|
|
1230
1863
|
answering.delete(locator);
|
|
1231
1864
|
release();
|
|
1232
1865
|
};
|
|
@@ -1259,7 +1892,7 @@ export function composeRuntime(options) {
|
|
|
1259
1892
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
1260
1893
|
};
|
|
1261
1894
|
const reclassified = sameMembership(declared, held.confirmed.record)
|
|
1262
|
-
?
|
|
1895
|
+
? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
|
|
1263
1896
|
: readReclassified(declared, held.confirmed);
|
|
1264
1897
|
const record = reclassified.kind === 'staged'
|
|
1265
1898
|
? declared
|
|
@@ -1267,12 +1900,10 @@ export function composeRuntime(options) {
|
|
|
1267
1900
|
...declared,
|
|
1268
1901
|
typedSelection: held.confirmed.record.typedSelection,
|
|
1269
1902
|
};
|
|
1270
|
-
const read = reclassified.kind === 'deferred'
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
queue: held.queue,
|
|
1275
|
-
});
|
|
1903
|
+
const read = reclassified.kind === 'deferred'
|
|
1904
|
+
? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
|
|
1905
|
+
: reclassified;
|
|
1906
|
+
await local.write({ next: record, previous: held.confirmed.record });
|
|
1276
1907
|
if (read.kind === 'staged') {
|
|
1277
1908
|
replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
|
|
1278
1909
|
if (declarationRefusals.delete(room.locator)) {
|
|
@@ -1299,7 +1930,11 @@ export function composeRuntime(options) {
|
|
|
1299
1930
|
});
|
|
1300
1931
|
}
|
|
1301
1932
|
async function scoped(result, room, local) {
|
|
1302
|
-
scopedRefusals.set(room.locator, {
|
|
1933
|
+
scopedRefusals.set(room.locator, {
|
|
1934
|
+
code: result.code,
|
|
1935
|
+
fields: result.fields,
|
|
1936
|
+
retryable: result.retryable,
|
|
1937
|
+
});
|
|
1303
1938
|
let scopedAway = room;
|
|
1304
1939
|
if (result.scope === 'model')
|
|
1305
1940
|
await retainModel(local, room, 'collection', {
|
|
@@ -1336,13 +1971,14 @@ export function composeRuntime(options) {
|
|
|
1336
1971
|
roomVersion: record.roomVersion,
|
|
1337
1972
|
documents: record.documents,
|
|
1338
1973
|
revisions: record.revisions,
|
|
1339
|
-
attachments: record.attachments,
|
|
1340
1974
|
},
|
|
1341
1975
|
];
|
|
1342
1976
|
});
|
|
1343
1977
|
}
|
|
1344
|
-
async function synchronize(listed, local, progress, attempt) {
|
|
1978
|
+
async function synchronize(listed, local, progress, attempt, live) {
|
|
1345
1979
|
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
|
|
1980
|
+
if (!live())
|
|
1981
|
+
return;
|
|
1346
1982
|
const ready = listed.filter((_, index) => {
|
|
1347
1983
|
const outcome = prepared[index];
|
|
1348
1984
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
@@ -1351,9 +1987,10 @@ export function composeRuntime(options) {
|
|
|
1351
1987
|
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
1352
1988
|
handed = true;
|
|
1353
1989
|
}
|
|
1990
|
+
discovering?.release();
|
|
1354
1991
|
const loaded = await Promise.allSettled(ready
|
|
1355
1992
|
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1356
|
-
.map((room) => loadRoom(room, local, progress)));
|
|
1993
|
+
.map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
|
|
1357
1994
|
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1358
1995
|
if (failed !== undefined)
|
|
1359
1996
|
throw failed.reason;
|
|
@@ -1382,14 +2019,14 @@ export function composeRuntime(options) {
|
|
|
1382
2019
|
}
|
|
1383
2020
|
return true;
|
|
1384
2021
|
}
|
|
1385
|
-
async function caughtUp(room, local, progress) {
|
|
2022
|
+
async function caughtUp(room, local, progress, live) {
|
|
1386
2023
|
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1387
|
-
if (held === undefined || authorization === undefined)
|
|
2024
|
+
if (held === undefined || authorization === undefined || !live())
|
|
1388
2025
|
return;
|
|
1389
2026
|
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
1390
2027
|
if (!behind)
|
|
1391
2028
|
return;
|
|
1392
|
-
const withheld = await advance(room, local, authorization);
|
|
2029
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1393
2030
|
if (withheld !== null)
|
|
1394
2031
|
await withhold(withheld, room, local, progress);
|
|
1395
2032
|
}
|
|
@@ -1399,39 +2036,46 @@ export function composeRuntime(options) {
|
|
|
1399
2036
|
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1400
2037
|
});
|
|
1401
2038
|
}
|
|
1402
|
-
async function loadRoom(room, local, progress) {
|
|
1403
|
-
if (
|
|
2039
|
+
async function loadRoom(room, local, progress, live) {
|
|
2040
|
+
if (!live() || authorization === undefined)
|
|
1404
2041
|
return;
|
|
1405
2042
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1406
|
-
await reload(room, local, progress);
|
|
2043
|
+
await reload(room, local, progress, live);
|
|
1407
2044
|
return;
|
|
1408
2045
|
}
|
|
1409
|
-
const withheld = await advance(room, local, authorization);
|
|
2046
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1410
2047
|
if (withheld !== null) {
|
|
1411
2048
|
await withhold(withheld, room, local, progress);
|
|
1412
2049
|
return;
|
|
1413
2050
|
}
|
|
1414
|
-
await caughtUp(room, local, progress);
|
|
2051
|
+
await caughtUp(room, local, progress, live);
|
|
1415
2052
|
}
|
|
1416
|
-
async function reload(room, local, progress) {
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
2053
|
+
async function reload(room, local, progress, live) {
|
|
2054
|
+
try {
|
|
2055
|
+
if (!live() || authorization === undefined)
|
|
2056
|
+
return;
|
|
2057
|
+
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
2058
|
+
if (!live())
|
|
2059
|
+
return;
|
|
2060
|
+
if (bootstrapped.kind !== 'staged') {
|
|
2061
|
+
await withhold(bootstrapped, room, local, progress);
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
2065
|
+
await caughtUp(room, local, progress, live);
|
|
2066
|
+
}
|
|
2067
|
+
finally {
|
|
2068
|
+
caughtUpTo(room.locator);
|
|
1425
2069
|
}
|
|
1426
|
-
await publish(local, room, bootstrapped.confirmed);
|
|
1427
|
-
await caughtUp(room, local, progress);
|
|
1428
2070
|
}
|
|
1429
|
-
async function discoverRooms(local, progress, attempt) {
|
|
2071
|
+
async function discoverRooms(local, progress, attempt, live) {
|
|
1430
2072
|
if (closed || authorization === undefined) {
|
|
1431
2073
|
notifyReady();
|
|
1432
2074
|
return;
|
|
1433
2075
|
}
|
|
1434
2076
|
moveTo(transferring(status));
|
|
2077
|
+
discoveries += 1;
|
|
2078
|
+
const started = discoveries;
|
|
1435
2079
|
let result;
|
|
1436
2080
|
try {
|
|
1437
2081
|
result = await options.adapter.discover(authorization);
|
|
@@ -1439,7 +2083,8 @@ export function composeRuntime(options) {
|
|
|
1439
2083
|
catch {
|
|
1440
2084
|
result = { kind: 'transport-uncertainty' };
|
|
1441
2085
|
}
|
|
1442
|
-
if (
|
|
2086
|
+
if (!live()) {
|
|
2087
|
+
progress.complete = false;
|
|
1443
2088
|
notifyReady();
|
|
1444
2089
|
return;
|
|
1445
2090
|
}
|
|
@@ -1458,7 +2103,11 @@ export function composeRuntime(options) {
|
|
|
1458
2103
|
}
|
|
1459
2104
|
if (result.kind === 'contract-unavailable') {
|
|
1460
2105
|
journal.record(`connection contract unavailable ${result.code}`);
|
|
1461
|
-
await refuse('missing-required-guarantee', {
|
|
2106
|
+
await refuse('missing-required-guarantee', {
|
|
2107
|
+
code: result.code,
|
|
2108
|
+
fields: result.fields,
|
|
2109
|
+
retryable: result.retryable,
|
|
2110
|
+
});
|
|
1462
2111
|
notifyReady();
|
|
1463
2112
|
return;
|
|
1464
2113
|
}
|
|
@@ -1493,6 +2142,7 @@ export function composeRuntime(options) {
|
|
|
1493
2142
|
}
|
|
1494
2143
|
const previousCapabilities = capabilities;
|
|
1495
2144
|
capabilities = result.capabilities;
|
|
2145
|
+
outcomeRetention = result.retention.outcome;
|
|
1496
2146
|
notifyReady();
|
|
1497
2147
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1498
2148
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
@@ -1505,8 +2155,7 @@ export function composeRuntime(options) {
|
|
|
1505
2155
|
member = signedIn;
|
|
1506
2156
|
emitMember();
|
|
1507
2157
|
}
|
|
1508
|
-
await applyDiscovery(result.rooms, local);
|
|
1509
|
-
await synchronize(result.rooms, local, progress, attempt);
|
|
2158
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
1510
2159
|
}
|
|
1511
2160
|
async function ensureAuthorization() {
|
|
1512
2161
|
if (authorization !== undefined)
|
|
@@ -1520,32 +2169,64 @@ export function composeRuntime(options) {
|
|
|
1520
2169
|
throw error;
|
|
1521
2170
|
}
|
|
1522
2171
|
}
|
|
1523
|
-
function
|
|
2172
|
+
async function admittedRead(work) {
|
|
2173
|
+
if (roomReads.generation !== generation) {
|
|
2174
|
+
roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
2175
|
+
}
|
|
2176
|
+
const reads = roomReads;
|
|
2177
|
+
while (reads.free === 0)
|
|
2178
|
+
await new Promise((admit) => reads.waiting.push(admit));
|
|
2179
|
+
reads.free -= 1;
|
|
2180
|
+
try {
|
|
2181
|
+
await work();
|
|
2182
|
+
}
|
|
2183
|
+
finally {
|
|
2184
|
+
reads.free += 1;
|
|
2185
|
+
reads.waiting.shift()?.();
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
function roomTask(locator, work) {
|
|
2189
|
+
return task(roomLane(locator), (live) => admittedRead(() => work(live)));
|
|
2190
|
+
}
|
|
2191
|
+
function task(lane, work) {
|
|
1524
2192
|
if (stopping)
|
|
1525
|
-
return
|
|
1526
|
-
|
|
1527
|
-
|
|
2193
|
+
return Promise.resolve();
|
|
2194
|
+
const held = lanes.get(lane);
|
|
2195
|
+
const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
|
|
2196
|
+
const turn = current.generation;
|
|
2197
|
+
const live = () => turn === generation && continuing();
|
|
2198
|
+
const run = current.chain.then(() => work(live)).catch(() => undefined);
|
|
2199
|
+
current.chain = run;
|
|
2200
|
+
lanes.set(lane, current);
|
|
2201
|
+
working.add(run);
|
|
2202
|
+
void run.finally(() => working.delete(run));
|
|
2203
|
+
return run;
|
|
1528
2204
|
}
|
|
1529
2205
|
function sync(attempt) {
|
|
1530
|
-
return task(async () => {
|
|
1531
|
-
if (
|
|
2206
|
+
return task(OPENING_LANE, async (live) => {
|
|
2207
|
+
if (!live()) {
|
|
1532
2208
|
notifyReady();
|
|
1533
2209
|
return;
|
|
1534
2210
|
}
|
|
1535
2211
|
const progress = { complete: true, reachable: true };
|
|
1536
|
-
|
|
2212
|
+
const gate = discover();
|
|
2213
|
+
if (attempt === 'opened')
|
|
2214
|
+
opened = generation;
|
|
2215
|
+
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
2216
|
+
if (attempt === 'opened')
|
|
2217
|
+
streamed = progress;
|
|
1537
2218
|
complete = false;
|
|
1538
2219
|
recorded = false;
|
|
1539
2220
|
moveTo(transferring(status));
|
|
1540
2221
|
handed = false;
|
|
1541
2222
|
try {
|
|
1542
2223
|
const local = await openLocal();
|
|
1543
|
-
if (
|
|
2224
|
+
if (!live())
|
|
1544
2225
|
return;
|
|
1545
2226
|
await ensureAuthorization();
|
|
1546
|
-
if (
|
|
2227
|
+
if (!live())
|
|
1547
2228
|
return;
|
|
1548
|
-
await discoverRooms(local, progress, attempt);
|
|
2229
|
+
await discoverRooms(local, progress, attempt, live);
|
|
1549
2230
|
}
|
|
1550
2231
|
catch {
|
|
1551
2232
|
progress.complete = false;
|
|
@@ -1553,17 +2234,21 @@ export function composeRuntime(options) {
|
|
|
1553
2234
|
progress.reachable = false;
|
|
1554
2235
|
}
|
|
1555
2236
|
finally {
|
|
2237
|
+
gate.release();
|
|
1556
2238
|
notifyReady();
|
|
1557
2239
|
}
|
|
2240
|
+
if (!live())
|
|
2241
|
+
return;
|
|
1558
2242
|
if (attempt !== 'opened') {
|
|
1559
|
-
await finishOpening(progress);
|
|
2243
|
+
await finishOpening(progress, live);
|
|
1560
2244
|
return;
|
|
1561
2245
|
}
|
|
1562
2246
|
if (handed || closed || progress.reachable)
|
|
1563
2247
|
return;
|
|
1564
2248
|
underway = null;
|
|
2249
|
+
opened = null;
|
|
1565
2250
|
journal.record('connection reopen');
|
|
1566
|
-
await rest(progress);
|
|
2251
|
+
await rest(progress, live);
|
|
1567
2252
|
options.adapter.roomStream?.reopen();
|
|
1568
2253
|
});
|
|
1569
2254
|
}
|
|
@@ -1574,29 +2259,51 @@ export function composeRuntime(options) {
|
|
|
1574
2259
|
void refuse('revoked-authorization');
|
|
1575
2260
|
return;
|
|
1576
2261
|
}
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
2262
|
+
complete = false;
|
|
2263
|
+
if (streamed !== null)
|
|
2264
|
+
streamed.complete = false;
|
|
2265
|
+
moveTo({
|
|
2266
|
+
...status,
|
|
2267
|
+
state: status.state === 'synced' ? 'syncing' : status.state,
|
|
2268
|
+
providerUnreachable: status.providerUnreachable || !opened,
|
|
2269
|
+
});
|
|
1580
2270
|
}
|
|
1581
2271
|
function applyOpening(expired) {
|
|
1582
|
-
|
|
1583
|
-
|
|
2272
|
+
const progress = underway?.generation === generation ? underway.progress : null;
|
|
2273
|
+
const cycle = progress ?? { complete: true, reachable: true };
|
|
2274
|
+
const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
|
|
2275
|
+
if (opened === generation)
|
|
2276
|
+
opened = null;
|
|
2277
|
+
return task(OPENING_LANE, async (live) => {
|
|
1584
2278
|
underway = null;
|
|
1585
|
-
if (
|
|
2279
|
+
if (!live())
|
|
1586
2280
|
return;
|
|
1587
2281
|
if (progress === null) {
|
|
1588
|
-
await
|
|
2282
|
+
await replacing;
|
|
2283
|
+
await rest(cycle, live);
|
|
1589
2284
|
return;
|
|
1590
2285
|
}
|
|
2286
|
+
await roomsCarried(expired);
|
|
2287
|
+
if (!live())
|
|
2288
|
+
return;
|
|
2289
|
+
const replaced = new Set(owed);
|
|
2290
|
+
owed.clear();
|
|
1591
2291
|
try {
|
|
1592
2292
|
const local = await openLocal();
|
|
1593
2293
|
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
1594
|
-
const reloading = held.filter((room) => expired.includes(room.locator)
|
|
1595
|
-
|
|
2294
|
+
const reloading = held.filter((room) => !expired.includes(room.locator) &&
|
|
2295
|
+
(replaced.has(room.locator) || revoked(room.locator)));
|
|
2296
|
+
for (const room of reloading)
|
|
2297
|
+
catching.add(room.locator);
|
|
2298
|
+
readSide.recover();
|
|
2299
|
+
const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
|
|
2300
|
+
const reloaded = [...(await reloads), ...(await replacing)];
|
|
1596
2301
|
if (progress.reachable) {
|
|
1597
|
-
for (const room of held)
|
|
1598
|
-
if (!reloading.includes(room))
|
|
2302
|
+
for (const room of held) {
|
|
2303
|
+
if (!expired.includes(room.locator) && !reloading.includes(room)) {
|
|
1599
2304
|
await confirmModels(room, local);
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
1600
2307
|
}
|
|
1601
2308
|
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1602
2309
|
if (failed !== undefined)
|
|
@@ -1606,21 +2313,21 @@ export function composeRuntime(options) {
|
|
|
1606
2313
|
progress.complete = false;
|
|
1607
2314
|
progress.reachable = false;
|
|
1608
2315
|
}
|
|
1609
|
-
|
|
2316
|
+
if (live())
|
|
2317
|
+
await finishOpening(progress, live);
|
|
1610
2318
|
});
|
|
1611
2319
|
}
|
|
1612
|
-
async function
|
|
1613
|
-
const progress = { complete: true, reachable: true };
|
|
2320
|
+
async function reloadNow(replaced, progress) {
|
|
1614
2321
|
try {
|
|
1615
2322
|
const local = await openLocal();
|
|
1616
|
-
await Promise.allSettled([...discovered.values()]
|
|
1617
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
1618
|
-
.map((room) => reload(room, local, progress)));
|
|
2323
|
+
return await Promise.allSettled([...discovered.values()]
|
|
2324
|
+
.filter((room) => replicas.has(room.locator) && replaced(room))
|
|
2325
|
+
.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
1619
2326
|
}
|
|
1620
2327
|
catch {
|
|
1621
2328
|
progress.reachable = false;
|
|
2329
|
+
return [];
|
|
1622
2330
|
}
|
|
1623
|
-
await rest(progress);
|
|
1624
2331
|
}
|
|
1625
2332
|
async function confirmModels(room, local) {
|
|
1626
2333
|
for (const attached of ['collection', 'typed-selection']) {
|
|
@@ -1631,38 +2338,96 @@ export function composeRuntime(options) {
|
|
|
1631
2338
|
}
|
|
1632
2339
|
}
|
|
1633
2340
|
function submitLater(locator) {
|
|
1634
|
-
|
|
1635
|
-
if (running !== undefined) {
|
|
1636
|
-
running.again = true;
|
|
2341
|
+
if (localSaves)
|
|
1637
2342
|
return;
|
|
2343
|
+
void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
|
|
2344
|
+
}
|
|
2345
|
+
async function submitPass(locator, signal) {
|
|
2346
|
+
const room = discovered.get(locator);
|
|
2347
|
+
if (closed || stopping || discoveryPaused || room === undefined)
|
|
2348
|
+
return 'done';
|
|
2349
|
+
const progress = { complete: true, reachable: true };
|
|
2350
|
+
let next;
|
|
2351
|
+
try {
|
|
2352
|
+
await ensureAuthorization();
|
|
2353
|
+
if (authorization === undefined)
|
|
2354
|
+
return 'done';
|
|
2355
|
+
next = await submitRoom(room, await openLocal(), authorization, progress, signal);
|
|
1638
2356
|
}
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
2357
|
+
catch {
|
|
2358
|
+
progress.reachable = false;
|
|
2359
|
+
next = 'again';
|
|
2360
|
+
}
|
|
2361
|
+
if (signal.aborted)
|
|
2362
|
+
return 'done';
|
|
2363
|
+
await rest(progress);
|
|
2364
|
+
return next === 'again' && !socketOpen ? 'done' : next;
|
|
2365
|
+
}
|
|
2366
|
+
function repairLater(locator, when) {
|
|
2367
|
+
if (!socketOpen || repairing.has(locator))
|
|
2368
|
+
return;
|
|
2369
|
+
void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
|
|
2370
|
+
}
|
|
2371
|
+
async function repairPass(locator, signal) {
|
|
2372
|
+
const room = discovered.get(locator);
|
|
2373
|
+
if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
|
|
2374
|
+
return 'done';
|
|
2375
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2376
|
+
return 'done';
|
|
2377
|
+
const progress = { complete: true, reachable: true };
|
|
2378
|
+
repairing.add(locator);
|
|
2379
|
+
try {
|
|
2380
|
+
const local = await openLocal();
|
|
2381
|
+
await roomTask(locator, async (live) => {
|
|
2382
|
+
const current = () => live() && !signal.aborted;
|
|
2383
|
+
if (!current() || authorization === undefined)
|
|
2384
|
+
return;
|
|
2385
|
+
const withheld = await Promise.race([
|
|
2386
|
+
advance(room, local, authorization, current),
|
|
2387
|
+
abandonedOn(signal),
|
|
2388
|
+
]);
|
|
2389
|
+
if (withheld !== null && current())
|
|
2390
|
+
await withhold(withheld, room, local, progress);
|
|
2391
|
+
});
|
|
2392
|
+
}
|
|
2393
|
+
catch {
|
|
2394
|
+
stale.add(locator);
|
|
2395
|
+
progress.reachable = false;
|
|
2396
|
+
}
|
|
2397
|
+
finally {
|
|
2398
|
+
repairing.delete(locator);
|
|
2399
|
+
}
|
|
2400
|
+
if (signal.aborted)
|
|
2401
|
+
return 'done';
|
|
2402
|
+
await rest(progress);
|
|
2403
|
+
if (!stale.has(locator)) {
|
|
2404
|
+
if ((replicas.get(locator)?.queue.length ?? 0) > 0)
|
|
2405
|
+
submitLater(locator);
|
|
2406
|
+
return 'done';
|
|
2407
|
+
}
|
|
2408
|
+
if (!repairable.has(locator))
|
|
2409
|
+
return 'done';
|
|
2410
|
+
const reset = repairable.get(locator);
|
|
2411
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
2412
|
+
}
|
|
2413
|
+
function resendLater() {
|
|
2414
|
+
if (!socketOpen)
|
|
2415
|
+
return;
|
|
2416
|
+
void recovery.attempt('resend', resendPass, { again: true });
|
|
2417
|
+
}
|
|
2418
|
+
async function resendPass(signal) {
|
|
2419
|
+
if (!socketOpen || closed || stopping || discoveryPaused)
|
|
2420
|
+
return 'done';
|
|
2421
|
+
const reading = { complete: true, reachable: true };
|
|
2422
|
+
try {
|
|
2423
|
+
await conversations.resend(reading);
|
|
2424
|
+
}
|
|
2425
|
+
catch {
|
|
2426
|
+
reading.reachable = false;
|
|
2427
|
+
}
|
|
2428
|
+
if (signal.aborted)
|
|
2429
|
+
return 'done';
|
|
2430
|
+
return reading.reachable && reading.complete ? 'done' : 'again';
|
|
1666
2431
|
}
|
|
1667
2432
|
function submitAll() {
|
|
1668
2433
|
for (const [locator, replica] of replicas) {
|
|
@@ -1670,31 +2435,40 @@ export function composeRuntime(options) {
|
|
|
1670
2435
|
submitLater(locator);
|
|
1671
2436
|
}
|
|
1672
2437
|
}
|
|
1673
|
-
async function finishOpening(progress) {
|
|
2438
|
+
async function finishOpening(progress, live) {
|
|
1674
2439
|
try {
|
|
1675
|
-
if (
|
|
2440
|
+
if (!live()) {
|
|
1676
2441
|
progress.complete = false;
|
|
1677
2442
|
return;
|
|
1678
2443
|
}
|
|
1679
2444
|
submitAll();
|
|
2445
|
+
for (const locator of stale)
|
|
2446
|
+
if (repairable.has(locator))
|
|
2447
|
+
repairLater(locator, {});
|
|
1680
2448
|
await conversations.reconcile();
|
|
1681
2449
|
if (status.providerUnreachable)
|
|
1682
2450
|
conversations.reconnected();
|
|
1683
2451
|
const reading = { complete: true, reachable: true };
|
|
1684
2452
|
await conversations.catchUp(reading);
|
|
1685
|
-
if (
|
|
2453
|
+
if (progress.reachable && reading.reachable)
|
|
2454
|
+
await conversations.resend(reading);
|
|
2455
|
+
if (!reading.reachable) {
|
|
1686
2456
|
progress.reachable = false;
|
|
2457
|
+
resendLater();
|
|
2458
|
+
}
|
|
1687
2459
|
}
|
|
1688
2460
|
catch {
|
|
1689
2461
|
progress.complete = false;
|
|
1690
2462
|
}
|
|
1691
2463
|
finally {
|
|
1692
|
-
|
|
1693
|
-
|
|
2464
|
+
if (live()) {
|
|
2465
|
+
complete = progress.complete;
|
|
2466
|
+
await rest(progress, live);
|
|
2467
|
+
}
|
|
1694
2468
|
}
|
|
1695
2469
|
}
|
|
1696
|
-
async function rest(progress) {
|
|
1697
|
-
if (
|
|
2470
|
+
async function rest(progress, live = () => !closed) {
|
|
2471
|
+
if (!live())
|
|
1698
2472
|
return;
|
|
1699
2473
|
let lastCompleted = status.lastCompleted;
|
|
1700
2474
|
let synced = complete && stale.size === 0 && !pending();
|
|
@@ -1702,6 +2476,8 @@ export function composeRuntime(options) {
|
|
|
1702
2476
|
const completedAt = new Date().toISOString();
|
|
1703
2477
|
try {
|
|
1704
2478
|
await (await openLocal()).complete(completedAt);
|
|
2479
|
+
if (!live())
|
|
2480
|
+
return;
|
|
1705
2481
|
recorded = true;
|
|
1706
2482
|
lastCompleted = completedAt;
|
|
1707
2483
|
}
|
|
@@ -1710,17 +2486,29 @@ export function composeRuntime(options) {
|
|
|
1710
2486
|
}
|
|
1711
2487
|
}
|
|
1712
2488
|
moveTo({
|
|
1713
|
-
state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
|
|
2489
|
+
state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
|
|
1714
2490
|
lastCompleted,
|
|
1715
2491
|
providerUnreachable: !progress.reachable,
|
|
1716
2492
|
...attention(),
|
|
1717
2493
|
});
|
|
1718
2494
|
}
|
|
2495
|
+
function discover() {
|
|
2496
|
+
let release = () => { };
|
|
2497
|
+
const done = new Promise((resolve) => {
|
|
2498
|
+
release = resolve;
|
|
2499
|
+
});
|
|
2500
|
+
discovering = { generation, done, release };
|
|
2501
|
+
return discovering;
|
|
2502
|
+
}
|
|
1719
2503
|
function applyRoom(locator, roomVersion) {
|
|
1720
2504
|
announced.set(locator, roomVersion);
|
|
1721
|
-
return task(async () => {
|
|
1722
|
-
|
|
1723
|
-
|
|
2505
|
+
return task(roomLane(locator), async (live) => {
|
|
2506
|
+
if (discovering?.generation === generation)
|
|
2507
|
+
await discovering.done;
|
|
2508
|
+
const saving = answering.get(locator);
|
|
2509
|
+
if (saving?.generation === generation)
|
|
2510
|
+
await saving.answered;
|
|
2511
|
+
if (!live() || authorization === undefined)
|
|
1724
2512
|
return;
|
|
1725
2513
|
const room = discovered.get(locator);
|
|
1726
2514
|
const held = replicas.get(locator);
|
|
@@ -1731,25 +2519,41 @@ export function composeRuntime(options) {
|
|
|
1731
2519
|
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
1732
2520
|
return;
|
|
1733
2521
|
}
|
|
1734
|
-
const during = underway;
|
|
2522
|
+
const during = underway?.generation === generation ? underway.progress : null;
|
|
1735
2523
|
const progress = during ?? { complete: true, reachable: true };
|
|
1736
2524
|
if (during === null)
|
|
1737
2525
|
moveTo(transferring(status));
|
|
1738
2526
|
try {
|
|
1739
2527
|
const local = await openLocal();
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
2528
|
+
await admittedRead(async () => {
|
|
2529
|
+
if (!live() || authorization === undefined)
|
|
2530
|
+
return;
|
|
2531
|
+
const withheld = await advance(room, local, authorization, live);
|
|
2532
|
+
if (withheld !== null)
|
|
2533
|
+
await withhold(withheld, room, local, progress);
|
|
2534
|
+
});
|
|
1743
2535
|
}
|
|
1744
2536
|
catch {
|
|
1745
2537
|
stale.add(locator);
|
|
1746
2538
|
}
|
|
1747
2539
|
finally {
|
|
1748
2540
|
if (during === null)
|
|
1749
|
-
await rest(progress);
|
|
2541
|
+
await rest(progress, live);
|
|
1750
2542
|
}
|
|
1751
2543
|
});
|
|
1752
2544
|
}
|
|
2545
|
+
function roomsCarried(replacing) {
|
|
2546
|
+
const replaced = new Set(replacing.map(roomLane));
|
|
2547
|
+
return Promise.all([...lanes]
|
|
2548
|
+
.filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
|
|
2549
|
+
.map(([, held]) => held.chain));
|
|
2550
|
+
}
|
|
2551
|
+
const receipts = {
|
|
2552
|
+
heads: async () => (await openLocal()).receivedHeads(),
|
|
2553
|
+
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
2554
|
+
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
2555
|
+
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
2556
|
+
};
|
|
1753
2557
|
function observeStream() {
|
|
1754
2558
|
const stream = options.adapter.roomStream;
|
|
1755
2559
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -1759,6 +2563,9 @@ export function composeRuntime(options) {
|
|
|
1759
2563
|
return;
|
|
1760
2564
|
if (event.kind === 'opened') {
|
|
1761
2565
|
socketOpen = true;
|
|
2566
|
+
generation += 1;
|
|
2567
|
+
conversations.reopened();
|
|
2568
|
+
recovery.resume();
|
|
1762
2569
|
void sync('opened');
|
|
1763
2570
|
}
|
|
1764
2571
|
else if (event.kind === 'closed')
|
|
@@ -1767,7 +2574,7 @@ export function composeRuntime(options) {
|
|
|
1767
2574
|
void applyOpening(event.expired);
|
|
1768
2575
|
else
|
|
1769
2576
|
void applyRoom(event.locator, event.roomVersion);
|
|
1770
|
-
});
|
|
2577
|
+
}, receipts);
|
|
1771
2578
|
}
|
|
1772
2579
|
function writesHeld() {
|
|
1773
2580
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -1805,34 +2612,30 @@ export function composeRuntime(options) {
|
|
|
1805
2612
|
});
|
|
1806
2613
|
}
|
|
1807
2614
|
function scheduleCurrency() {
|
|
1808
|
-
if (currencyQueued || closed || stopping)
|
|
2615
|
+
if (currencyQueued === generation || closed || stopping)
|
|
1809
2616
|
return;
|
|
1810
|
-
currencyQueued =
|
|
1811
|
-
void task(runCurrency);
|
|
2617
|
+
currencyQueued = generation;
|
|
2618
|
+
void task(CONVERSATION_LANE, runCurrency);
|
|
1812
2619
|
}
|
|
1813
|
-
async function runCurrency() {
|
|
1814
|
-
currencyQueued
|
|
1815
|
-
|
|
2620
|
+
async function runCurrency(live) {
|
|
2621
|
+
if (currencyQueued === generation)
|
|
2622
|
+
currencyQueued = null;
|
|
2623
|
+
if (!live())
|
|
1816
2624
|
return;
|
|
1817
2625
|
await conversations.reconcile();
|
|
1818
2626
|
if (conversations.current()) {
|
|
1819
2627
|
show();
|
|
1820
2628
|
return;
|
|
1821
2629
|
}
|
|
1822
|
-
const
|
|
1823
|
-
|
|
1824
|
-
moveTo(transferring(resting));
|
|
2630
|
+
const progress = { complete: false, reachable: !status.providerUnreachable };
|
|
2631
|
+
moveTo(transferring(status));
|
|
1825
2632
|
try {
|
|
1826
2633
|
await conversations.catchUp(progress);
|
|
1827
2634
|
}
|
|
1828
2635
|
catch {
|
|
1829
2636
|
progress.reachable = false;
|
|
1830
2637
|
}
|
|
1831
|
-
|
|
1832
|
-
if (!closed) {
|
|
1833
|
-
moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
|
|
1834
|
-
}
|
|
1835
|
-
}
|
|
2638
|
+
await rest(progress, live);
|
|
1836
2639
|
}
|
|
1837
2640
|
function close() {
|
|
1838
2641
|
if (closed)
|
|
@@ -1844,7 +2647,7 @@ export function composeRuntime(options) {
|
|
|
1844
2647
|
withdrawConversations = undefined;
|
|
1845
2648
|
withdrawServiceMode?.();
|
|
1846
2649
|
conversations.close();
|
|
1847
|
-
|
|
2650
|
+
void recovery.retire();
|
|
1848
2651
|
store?.close();
|
|
1849
2652
|
void opening?.then((opened) => {
|
|
1850
2653
|
opened.close();
|
|
@@ -1862,9 +2665,8 @@ export function composeRuntime(options) {
|
|
|
1862
2665
|
}
|
|
1863
2666
|
async function stop() {
|
|
1864
2667
|
stopping = true;
|
|
1865
|
-
|
|
1866
|
-
await
|
|
1867
|
-
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
2668
|
+
await recovery.retire();
|
|
2669
|
+
await Promise.all(working);
|
|
1868
2670
|
await mutations;
|
|
1869
2671
|
close();
|
|
1870
2672
|
}
|
|
@@ -1886,14 +2688,54 @@ export function composeRuntime(options) {
|
|
|
1886
2688
|
return planOf(input, roomFacts(reach), planReach(reach));
|
|
1887
2689
|
}
|
|
1888
2690
|
function watchPlan(plan, deliver) {
|
|
1889
|
-
return readSide.watch(plan, identityOf(plan),
|
|
2691
|
+
return readSide.watch(plan, identityOf(plan), () => {
|
|
2692
|
+
const cover = covered(plan);
|
|
2693
|
+
awaitPreparation(cover);
|
|
2694
|
+
return cover;
|
|
2695
|
+
}, deliver);
|
|
1890
2696
|
}
|
|
1891
2697
|
async function searched(input, reach) {
|
|
1892
2698
|
const plan = searchPlanOf(input);
|
|
1893
|
-
const
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
2699
|
+
const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
|
|
2700
|
+
for (;;) {
|
|
2701
|
+
const cover = covered(filter);
|
|
2702
|
+
awaitPreparation(cover);
|
|
2703
|
+
const pending = 'pending' in cover ? [...cover.pending] : [];
|
|
2704
|
+
const covering = 'pending' in cover ? [] : cover.coverage.rooms;
|
|
2705
|
+
const unindexed = covering
|
|
2706
|
+
.filter(({ room }) => !searchIndex.rooms().has(room))
|
|
2707
|
+
.map(({ room }) => room);
|
|
2708
|
+
for (const room of unindexed)
|
|
2709
|
+
pending.push({ room, awaits: 'index' });
|
|
2710
|
+
const held = conversations.searchable();
|
|
2711
|
+
const named = reach !== null && !reach.conversations
|
|
2712
|
+
? []
|
|
2713
|
+
: (plan.conversations ?? held).filter((one) => held.includes(one) && conversations.acquired(one));
|
|
2714
|
+
const waiting = named.filter((one) => !searchIndex.conversations().has(one));
|
|
2715
|
+
for (const conversation of waiting)
|
|
2716
|
+
pending.push({ conversation, awaits: 'index' });
|
|
2717
|
+
if (unindexed.length > 0 || waiting.length > 0)
|
|
2718
|
+
index(unindexed, waiting);
|
|
2719
|
+
if (pending.length > 0 || 'pending' in cover)
|
|
2720
|
+
return { pending };
|
|
2721
|
+
const seen = searchIndex.changes();
|
|
2722
|
+
await searchIndex.caughtUp();
|
|
2723
|
+
const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(filter);
|
|
2724
|
+
if (selected !== null && 'pending' in selected)
|
|
2725
|
+
return selected;
|
|
2726
|
+
if (searchIndex.changes() !== seen ||
|
|
2727
|
+
JSON.stringify(covered(filter)) !== JSON.stringify(cover))
|
|
2728
|
+
continue;
|
|
2729
|
+
return {
|
|
2730
|
+
...searchIndex.search(plan, selected === null ? null : selected.documents, named),
|
|
2731
|
+
coverage: {
|
|
2732
|
+
rooms: covering,
|
|
2733
|
+
conversations: named,
|
|
2734
|
+
index: searchIndex.generation(),
|
|
2735
|
+
overlay: cover.coverage.overlay,
|
|
2736
|
+
},
|
|
2737
|
+
};
|
|
2738
|
+
}
|
|
1897
2739
|
}
|
|
1898
2740
|
const watch = (topic, input, deliver) => {
|
|
1899
2741
|
if (topic === 'room-drafts') {
|
|
@@ -1918,6 +2760,8 @@ export function composeRuntime(options) {
|
|
|
1918
2760
|
}
|
|
1919
2761
|
if (topic === 'conversations')
|
|
1920
2762
|
return conversations.watchList(deliver);
|
|
2763
|
+
if (topic === 'agents')
|
|
2764
|
+
return conversations.watchAgents(deliver);
|
|
1921
2765
|
if (topic === 'presentation') {
|
|
1922
2766
|
const listener = {
|
|
1923
2767
|
app: input[0],
|
|
@@ -1996,7 +2840,7 @@ export function composeRuntime(options) {
|
|
|
1996
2840
|
rooms() {
|
|
1997
2841
|
return listed();
|
|
1998
2842
|
},
|
|
1999
|
-
query: ((input) =>
|
|
2843
|
+
query: ((input) => rowsQuery(planned(input, null))),
|
|
2000
2844
|
search: (input) => searched(input, null),
|
|
2001
2845
|
types(locator) {
|
|
2002
2846
|
return {
|
|
@@ -2025,20 +2869,20 @@ export function composeRuntime(options) {
|
|
|
2025
2869
|
return exclusive(async () => {
|
|
2026
2870
|
const parts = readParts(input);
|
|
2027
2871
|
if (path === '' || normalize(path) !== path)
|
|
2028
|
-
throw
|
|
2872
|
+
throw runtimeError('room-draft-malformed');
|
|
2029
2873
|
const local = await openLocal();
|
|
2030
2874
|
const current = roomDrafts.get(room)?.get(path);
|
|
2031
2875
|
if ((current?.revision ?? null) !== revision) {
|
|
2032
|
-
return
|
|
2033
|
-
outcome: 'stale',
|
|
2034
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
2035
|
-
};
|
|
2876
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2036
2877
|
}
|
|
2878
|
+
const file = parts.file === undefined
|
|
2879
|
+
? undefined
|
|
2880
|
+
: await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
|
|
2037
2881
|
const record = {
|
|
2038
2882
|
locator: room,
|
|
2039
2883
|
path,
|
|
2040
2884
|
revision: roomDraftRevision(),
|
|
2041
|
-
parts: held(current?.parts, parts),
|
|
2885
|
+
parts: held(current?.parts, parts, file),
|
|
2042
2886
|
returned: current?.returned ?? null,
|
|
2043
2887
|
};
|
|
2044
2888
|
await local.keepRoomDraft(record);
|
|
@@ -2052,10 +2896,7 @@ export function composeRuntime(options) {
|
|
|
2052
2896
|
const local = await openLocal();
|
|
2053
2897
|
const current = roomDrafts.get(room)?.get(path);
|
|
2054
2898
|
if (current === undefined || current.revision !== revision) {
|
|
2055
|
-
return
|
|
2056
|
-
outcome: 'stale',
|
|
2057
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
2058
|
-
};
|
|
2899
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2059
2900
|
}
|
|
2060
2901
|
await local.discardRoomDraft(room, path);
|
|
2061
2902
|
forgetRoomDraft(room, path);
|
|
@@ -2090,6 +2931,10 @@ export function composeRuntime(options) {
|
|
|
2090
2931
|
history(locator, path, query) {
|
|
2091
2932
|
return readHistory(locator, path, query);
|
|
2092
2933
|
},
|
|
2934
|
+
binaryFile: {
|
|
2935
|
+
read: readBinaryPath,
|
|
2936
|
+
readReference: readBinaryReference,
|
|
2937
|
+
},
|
|
2093
2938
|
call(name, input) {
|
|
2094
2939
|
return invokeAction(name, input);
|
|
2095
2940
|
},
|
|
@@ -2109,23 +2954,26 @@ export function composeRuntime(options) {
|
|
|
2109
2954
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2110
2955
|
return;
|
|
2111
2956
|
journal.record('connection reauthorize');
|
|
2112
|
-
await
|
|
2957
|
+
await Promise.all([...lanes.values()]
|
|
2958
|
+
.filter((held) => held.generation === generation)
|
|
2959
|
+
.map((held) => held.chain));
|
|
2113
2960
|
await mutations;
|
|
2114
2961
|
authorization = undefined;
|
|
2115
2962
|
discoveryPaused = false;
|
|
2116
2963
|
if (options.adapter.roomStream === undefined)
|
|
2117
2964
|
await sync('unstreamed');
|
|
2118
2965
|
else
|
|
2119
|
-
await task(ensureAuthorization);
|
|
2966
|
+
await task(OPENING_LANE, ensureAuthorization);
|
|
2120
2967
|
},
|
|
2121
2968
|
};
|
|
2122
2969
|
const reachable = {
|
|
2123
2970
|
md,
|
|
2124
2971
|
watch,
|
|
2972
|
+
referrerScope: (referrer) => options.adapter.referrerScope(referrer),
|
|
2125
2973
|
plan: planned,
|
|
2126
|
-
query: (plan) =>
|
|
2974
|
+
query: (plan) => rowsQuery(plan),
|
|
2127
2975
|
watchPlan,
|
|
2128
2976
|
search: searched,
|
|
2129
2977
|
};
|
|
2130
|
-
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2978
|
+
return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2131
2979
|
}
|