@agentshouse/mdruntime 0.1.0-cacb4c3f6734e → 0.1.0-cb0eb0d87ff49
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 +164 -99
- package/README.md +13 -3
- package/dist/adapter.d.ts +72 -16
- package/dist/adapter.d.ts.map +1 -1
- package/dist/attached-model.d.ts.map +1 -1
- package/dist/attached-model.js +15 -2
- package/dist/attempt.d.ts +16 -0
- package/dist/attempt.d.ts.map +1 -0
- package/dist/attempt.js +70 -0
- package/dist/binary.d.ts +23 -0
- package/dist/binary.d.ts.map +1 -0
- package/dist/binary.js +8 -0
- package/dist/bootstrap.d.ts +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 +20 -11
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +66 -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 +29 -14
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +1262 -377
- 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 -1
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +17 -12
- 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 +5 -3
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +70 -21
- package/dist/model.d.ts +11 -2
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +11 -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 +5 -31
- 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 +26 -17
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +8 -6
- 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 +9 -6
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +116 -68
- 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 +66 -17
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +506 -196
- 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 +7 -3
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +15 -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,18 +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;
|
|
85
|
-
|
|
132
|
+
let streamed = null;
|
|
133
|
+
let handed = false;
|
|
134
|
+
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
135
|
+
const repairing = new Set();
|
|
136
|
+
const repairable = new Map();
|
|
137
|
+
let outcomeRetention = null;
|
|
86
138
|
const answering = new Map();
|
|
87
139
|
let complete = false;
|
|
88
140
|
let recorded = false;
|
|
89
141
|
const stale = new Set();
|
|
90
|
-
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();
|
|
91
149
|
let mutations = Promise.resolve();
|
|
92
150
|
const roomListeners = new Set();
|
|
93
151
|
const refusalListeners = new Set();
|
|
@@ -98,27 +156,39 @@ export function composeRuntime(options) {
|
|
|
98
156
|
const presentationListeners = new Set();
|
|
99
157
|
const readSide = createReadSide();
|
|
100
158
|
let withdrawConversations;
|
|
101
|
-
let currencyQueued =
|
|
159
|
+
let currencyQueued = null;
|
|
102
160
|
let socketOpen = false;
|
|
103
161
|
let serviceMode = 'open';
|
|
104
162
|
let haltedRefusal = false;
|
|
105
163
|
let openSignals = 0;
|
|
106
164
|
const serviceModeListeners = new Set();
|
|
165
|
+
let member = null;
|
|
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);
|
|
107
173
|
const conversations = createConversationStore({
|
|
108
174
|
agents: declaredAgents,
|
|
109
|
-
documents: (locator) =>
|
|
175
|
+
documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
|
|
110
176
|
call: (name, input) => invokeAction(name, input),
|
|
111
177
|
durably,
|
|
112
178
|
stream: () => options.adapter.conversations,
|
|
113
179
|
publish: publishChangeSet,
|
|
180
|
+
online: () => !status.providerUnreachable && refusal === null,
|
|
114
181
|
closed: () => closed,
|
|
182
|
+
unanswered: resendLater,
|
|
183
|
+
expired: (operation) => pastHorizon(operation),
|
|
115
184
|
});
|
|
116
185
|
const searchIndex = createSearchIndex({
|
|
117
|
-
|
|
186
|
+
room: (room) => readSide.room(room),
|
|
118
187
|
document: (room, path) => readSide.document(room, path),
|
|
119
|
-
messages: () => conversations.messages(),
|
|
120
|
-
message: (conversation, identity) => conversations.message(conversation, identity),
|
|
121
188
|
});
|
|
189
|
+
const roomsToIndex = new Set();
|
|
190
|
+
const conversationsToIndex = new Set();
|
|
191
|
+
let indexing = false;
|
|
122
192
|
const journal = createLog();
|
|
123
193
|
let notifyReady = () => { };
|
|
124
194
|
const ready = new Promise((resolve) => {
|
|
@@ -182,7 +252,9 @@ export function composeRuntime(options) {
|
|
|
182
252
|
}
|
|
183
253
|
}
|
|
184
254
|
function pending() {
|
|
185
|
-
return
|
|
255
|
+
return (!localSaves &&
|
|
256
|
+
(queuesWithoutReplica.size > 0 ||
|
|
257
|
+
[...replicas.values()].some((replica) => replica.queue.length > 0)));
|
|
186
258
|
}
|
|
187
259
|
function attention() {
|
|
188
260
|
const attentionRequired = [];
|
|
@@ -211,8 +283,21 @@ export function composeRuntime(options) {
|
|
|
211
283
|
}
|
|
212
284
|
return { attentionRequired, reset };
|
|
213
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
|
+
}
|
|
214
295
|
function listed() {
|
|
215
|
-
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
|
+
}));
|
|
216
301
|
}
|
|
217
302
|
function roomFacts(reach = null) {
|
|
218
303
|
const held = [...rooms.values()]
|
|
@@ -232,6 +317,7 @@ export function composeRuntime(options) {
|
|
|
232
317
|
return { listed: roomsListed, rooms: held };
|
|
233
318
|
}
|
|
234
319
|
function emitRooms() {
|
|
320
|
+
readSide.recover();
|
|
235
321
|
const value = listed();
|
|
236
322
|
for (const listener of roomListeners)
|
|
237
323
|
listener(value);
|
|
@@ -244,6 +330,10 @@ export function composeRuntime(options) {
|
|
|
244
330
|
for (const listener of storageLossListeners)
|
|
245
331
|
listener(storageLoss);
|
|
246
332
|
}
|
|
333
|
+
function emitMember() {
|
|
334
|
+
for (const listener of memberListeners)
|
|
335
|
+
listener(member);
|
|
336
|
+
}
|
|
247
337
|
function moveTo(next) {
|
|
248
338
|
if (closed)
|
|
249
339
|
return;
|
|
@@ -263,6 +353,8 @@ export function composeRuntime(options) {
|
|
|
263
353
|
listener(shown);
|
|
264
354
|
}
|
|
265
355
|
function entryOf(batch) {
|
|
356
|
+
if (localSaves)
|
|
357
|
+
return Object.freeze({ batch, state: 'local' });
|
|
266
358
|
const state = pendings.get(batch.id);
|
|
267
359
|
if (state?.accepted !== undefined) {
|
|
268
360
|
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
@@ -270,10 +362,7 @@ export function composeRuntime(options) {
|
|
|
270
362
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
271
363
|
}
|
|
272
364
|
function entriesOf(locator) {
|
|
273
|
-
|
|
274
|
-
if (replica === undefined)
|
|
275
|
-
return [];
|
|
276
|
-
return replica.queue.map(entryOf);
|
|
365
|
+
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
|
|
277
366
|
}
|
|
278
367
|
function emitQueue(locator) {
|
|
279
368
|
const entries = entriesOf(locator);
|
|
@@ -283,12 +372,191 @@ export function composeRuntime(options) {
|
|
|
283
372
|
}
|
|
284
373
|
}
|
|
285
374
|
function applyChangeSet(set) {
|
|
286
|
-
|
|
287
|
-
|
|
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(() => { });
|
|
288
555
|
}
|
|
289
556
|
function publishChangeSet(set) {
|
|
290
557
|
applyChangeSet(set);
|
|
291
|
-
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)))
|
|
292
560
|
emitRooms();
|
|
293
561
|
for (const locator of set.queues)
|
|
294
562
|
emitQueue(locator);
|
|
@@ -300,34 +568,7 @@ export function composeRuntime(options) {
|
|
|
300
568
|
emitPresentation(app);
|
|
301
569
|
conversations.changed(set);
|
|
302
570
|
}
|
|
303
|
-
function stopTimer() {
|
|
304
|
-
if (resetTimer === undefined)
|
|
305
|
-
return;
|
|
306
|
-
clearTimeout(resetTimer);
|
|
307
|
-
resetTimer = undefined;
|
|
308
|
-
}
|
|
309
|
-
function armTimer() {
|
|
310
|
-
stopTimer();
|
|
311
|
-
if (closed || stopping)
|
|
312
|
-
return;
|
|
313
|
-
let earliest = null;
|
|
314
|
-
for (const { submission } of pendings.values()) {
|
|
315
|
-
if (submission.state !== 'reset')
|
|
316
|
-
continue;
|
|
317
|
-
const at = Date.parse(submission.reset);
|
|
318
|
-
if (earliest === null || at < earliest)
|
|
319
|
-
earliest = at;
|
|
320
|
-
}
|
|
321
|
-
if (earliest === null)
|
|
322
|
-
return;
|
|
323
|
-
// A Save House held back goes again only once the reset its answer named has passed.
|
|
324
|
-
resetTimer = setTimeout(() => {
|
|
325
|
-
resetTimer = undefined;
|
|
326
|
-
submitAll();
|
|
327
|
-
}, Math.max(0, earliest - Date.now()));
|
|
328
|
-
}
|
|
329
571
|
async function refuse(reason, provider = null) {
|
|
330
|
-
stopTimer();
|
|
331
572
|
discoveryPaused = true;
|
|
332
573
|
try {
|
|
333
574
|
if (reason === 'revoked-authorization') {
|
|
@@ -335,10 +576,7 @@ export function composeRuntime(options) {
|
|
|
335
576
|
const revoked = { status: 'unavailable', reason: 'revoked' };
|
|
336
577
|
try {
|
|
337
578
|
const local = await openLocal();
|
|
338
|
-
await local.revokeModels([...replicas.values()].map((replica) =>
|
|
339
|
-
record: replica.confirmed.record,
|
|
340
|
-
queue: replica.queue,
|
|
341
|
-
})));
|
|
579
|
+
await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
|
|
342
580
|
}
|
|
343
581
|
catch {
|
|
344
582
|
journal.record('model revocation persistence failed');
|
|
@@ -362,10 +600,7 @@ export function composeRuntime(options) {
|
|
|
362
600
|
model: revoked,
|
|
363
601
|
selectionModel: revoked,
|
|
364
602
|
};
|
|
365
|
-
|
|
366
|
-
replicas.set(locator, read.kind === 'staged'
|
|
367
|
-
? replicaOf(read.confirmed, before.queue, before)
|
|
368
|
-
: { ...before, confirmed: { ...before.confirmed, record } });
|
|
603
|
+
replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
|
|
369
604
|
}
|
|
370
605
|
rooms.set(locator, {
|
|
371
606
|
...room,
|
|
@@ -382,7 +617,8 @@ export function composeRuntime(options) {
|
|
|
382
617
|
}
|
|
383
618
|
finally {
|
|
384
619
|
refusal = {
|
|
385
|
-
reason,
|
|
620
|
+
code: reason,
|
|
621
|
+
retryable: false,
|
|
386
622
|
reauthorizationOffered: reason === 'revoked-authorization',
|
|
387
623
|
refusal: provider,
|
|
388
624
|
};
|
|
@@ -407,19 +643,28 @@ export function composeRuntime(options) {
|
|
|
407
643
|
},
|
|
408
644
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
409
645
|
roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
|
|
410
|
-
attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
|
|
411
646
|
synchronizationAvailable,
|
|
412
647
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
413
648
|
});
|
|
414
649
|
}
|
|
415
|
-
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));
|
|
416
657
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
417
658
|
for (const locator of listedLocators)
|
|
418
659
|
scopedRefusals.delete(locator);
|
|
419
660
|
discovered.clear();
|
|
420
661
|
for (const room of listed)
|
|
421
662
|
discovered.set(room.locator, room);
|
|
422
|
-
|
|
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);
|
|
423
668
|
for (const room of listed) {
|
|
424
669
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
425
670
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -430,43 +675,83 @@ export function composeRuntime(options) {
|
|
|
430
675
|
}
|
|
431
676
|
roomsListed = true;
|
|
432
677
|
emitRooms();
|
|
678
|
+
return listed;
|
|
433
679
|
}
|
|
434
680
|
function unlistedHeld(listed) {
|
|
435
|
-
return [
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
pendings.delete(batch.id);
|
|
451
|
-
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
|
|
452
696
|
rooms.delete(locator);
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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);
|
|
466
730
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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));
|
|
470
755
|
}
|
|
471
756
|
function restored(key, persisted) {
|
|
472
757
|
const prior = models.get(key);
|
|
@@ -487,7 +772,7 @@ export function composeRuntime(options) {
|
|
|
487
772
|
return model;
|
|
488
773
|
}
|
|
489
774
|
async function hydrate(opened) {
|
|
490
|
-
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
|
|
775
|
+
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
|
|
491
776
|
opened.all(),
|
|
492
777
|
opened.queues(),
|
|
493
778
|
opened.roomDrafts(),
|
|
@@ -497,9 +782,15 @@ export function composeRuntime(options) {
|
|
|
497
782
|
opened.lastCompleted(),
|
|
498
783
|
opened.providerLimits(),
|
|
499
784
|
opened.capabilities(),
|
|
785
|
+
opened.member(),
|
|
786
|
+
opened.transfers(),
|
|
500
787
|
]);
|
|
788
|
+
transfers.clear();
|
|
789
|
+
for (const record of staged)
|
|
790
|
+
transfers.set(record.locator, record);
|
|
501
791
|
limits = declared;
|
|
502
792
|
capabilities = named;
|
|
793
|
+
member = known;
|
|
503
794
|
roomDrafts.clear();
|
|
504
795
|
for (const record of kept)
|
|
505
796
|
rememberRoomDraft(record);
|
|
@@ -515,18 +806,17 @@ export function composeRuntime(options) {
|
|
|
515
806
|
}
|
|
516
807
|
const previousReplicas = new Map(replicas);
|
|
517
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
|
+
}
|
|
518
815
|
for (const record of records) {
|
|
519
816
|
const model = restored(modelKey(record.locator, 'collection'), record.model);
|
|
520
817
|
const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
|
|
521
818
|
const prior = previousReplicas.get(record.locator);
|
|
522
|
-
const
|
|
523
|
-
const read = readConfirmed(retyped, prior?.confirmed);
|
|
524
|
-
const confirmed = read.kind === 'staged'
|
|
525
|
-
? read.confirmed
|
|
526
|
-
: {
|
|
527
|
-
record: retyped,
|
|
528
|
-
collection: parseCollection(retyped.documents, compositionOf(retyped)),
|
|
529
|
-
};
|
|
819
|
+
const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
|
|
530
820
|
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
|
|
531
821
|
const existing = rooms.get(record.locator);
|
|
532
822
|
if (existing === undefined)
|
|
@@ -535,7 +825,6 @@ export function composeRuntime(options) {
|
|
|
535
825
|
rooms.set(record.locator, {
|
|
536
826
|
...existing,
|
|
537
827
|
roomVersion: record.roomVersion,
|
|
538
|
-
attachments: record.attachments,
|
|
539
828
|
operations: record.operations,
|
|
540
829
|
replica: record.replica,
|
|
541
830
|
model: modelTypes(model),
|
|
@@ -547,10 +836,6 @@ export function composeRuntime(options) {
|
|
|
547
836
|
},
|
|
548
837
|
});
|
|
549
838
|
}
|
|
550
|
-
if (read.kind === 'refused') {
|
|
551
|
-
refusedSource(record, read);
|
|
552
|
-
declarationRefusals.add(record.locator);
|
|
553
|
-
}
|
|
554
839
|
}
|
|
555
840
|
return {
|
|
556
841
|
lastCompleted,
|
|
@@ -561,16 +846,26 @@ export function composeRuntime(options) {
|
|
|
561
846
|
async function readState(opened) {
|
|
562
847
|
const { lastCompleted, locators, read } = await hydrate(opened);
|
|
563
848
|
applyChangeSet(read);
|
|
849
|
+
prepare();
|
|
564
850
|
if (replicas.size > 0)
|
|
565
851
|
emitRooms();
|
|
852
|
+
if (member !== null)
|
|
853
|
+
emitMember();
|
|
566
854
|
for (const locator of locators)
|
|
567
855
|
emitQueue(locator);
|
|
568
856
|
publishRoomDrafts(false);
|
|
569
857
|
return lastCompleted;
|
|
570
858
|
}
|
|
571
859
|
async function open() {
|
|
572
|
-
|
|
860
|
+
if (closed)
|
|
861
|
+
throw new Error('runtime closed');
|
|
862
|
+
const opened = await openRuntimeStore(isFenced);
|
|
573
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
|
+
}
|
|
574
869
|
const kept = options.shell.generation;
|
|
575
870
|
const storedGeneration = await opened.generation();
|
|
576
871
|
const loss = detectStorageLoss(storedGeneration, kept);
|
|
@@ -584,6 +879,8 @@ export function composeRuntime(options) {
|
|
|
584
879
|
emitStorageLoss();
|
|
585
880
|
}
|
|
586
881
|
const lastCompleted = await readState(opened);
|
|
882
|
+
if (interrupted.length > 0)
|
|
883
|
+
await erased(opened, interrupted);
|
|
587
884
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
588
885
|
return opened;
|
|
589
886
|
}
|
|
@@ -610,10 +907,15 @@ export function composeRuntime(options) {
|
|
|
610
907
|
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
611
908
|
}
|
|
612
909
|
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
613
|
-
const
|
|
910
|
+
const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
911
|
+
const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
|
|
614
912
|
const kept = queue.filter((batch) => !withdrawn.includes(batch));
|
|
913
|
+
const unqueued = withdrawn.map((batch) => ({
|
|
914
|
+
batch,
|
|
915
|
+
returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
|
|
916
|
+
}));
|
|
615
917
|
const returning = [
|
|
616
|
-
...
|
|
918
|
+
...[...unqueued].reverse(),
|
|
617
919
|
...(settling.leaving === undefined ? [] : [settling.leaving]),
|
|
618
920
|
];
|
|
619
921
|
const written = new Map();
|
|
@@ -631,27 +933,48 @@ export function composeRuntime(options) {
|
|
|
631
933
|
});
|
|
632
934
|
}
|
|
633
935
|
}
|
|
634
|
-
await local.
|
|
635
|
-
|
|
636
|
-
previous: before?.confirmed.record ?? null,
|
|
637
|
-
queue: kept,
|
|
638
|
-
previousQueue: before?.queue ?? [],
|
|
639
|
-
roomDrafts: [...written.values()],
|
|
640
|
-
});
|
|
641
|
-
for (const batch of before?.queue ?? []) {
|
|
936
|
+
await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
|
|
937
|
+
for (const batch of previousQueue) {
|
|
642
938
|
if (!kept.includes(batch))
|
|
643
939
|
pendings.delete(batch.id);
|
|
644
940
|
}
|
|
645
941
|
for (const record of written.values())
|
|
646
942
|
rememberRoomDraft(record);
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
+
};
|
|
655
978
|
}
|
|
656
979
|
function adoptModels(locator, record) {
|
|
657
980
|
for (const [attached, model] of [
|
|
@@ -684,10 +1007,16 @@ export function composeRuntime(options) {
|
|
|
684
1007
|
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
685
1008
|
};
|
|
686
1009
|
}
|
|
687
|
-
function publish(local, room, confirmed,
|
|
1010
|
+
function publish(local, room, confirmed, landing, live) {
|
|
1011
|
+
const { base, stored } = landing;
|
|
688
1012
|
return exclusive(async () => {
|
|
689
|
-
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);
|
|
690
1018
|
return true;
|
|
1019
|
+
}
|
|
691
1020
|
const prior = replicas.get(room.locator);
|
|
692
1021
|
if (base !== undefined &&
|
|
693
1022
|
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
@@ -695,13 +1024,17 @@ export function composeRuntime(options) {
|
|
|
695
1024
|
return false;
|
|
696
1025
|
}
|
|
697
1026
|
adoptModels(room.locator, confirmed.record);
|
|
698
|
-
const queue = prior?.queue ?? [];
|
|
1027
|
+
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
699
1028
|
const roomVersion = confirmed.record.roomVersion;
|
|
700
1029
|
const accepted = queue.filter((batch) => {
|
|
701
1030
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
702
1031
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
703
1032
|
});
|
|
704
|
-
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
|
+
}
|
|
705
1038
|
for (const batch of accepted)
|
|
706
1039
|
pendings.delete(batch.id);
|
|
707
1040
|
stale.delete(room.locator);
|
|
@@ -710,32 +1043,54 @@ export function composeRuntime(options) {
|
|
|
710
1043
|
sourceRefusals.delete(room.locator);
|
|
711
1044
|
declarationRefusals.delete(room.locator);
|
|
712
1045
|
list(room, true);
|
|
1046
|
+
if (prior === undefined)
|
|
1047
|
+
prepared.add(room.locator);
|
|
713
1048
|
publishChangeSet(set);
|
|
714
1049
|
moveTo({ ...status, ...attention() });
|
|
1050
|
+
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
715
1051
|
return true;
|
|
716
1052
|
});
|
|
717
1053
|
}
|
|
718
1054
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
719
1055
|
const local = await openLocal();
|
|
1056
|
+
await referenced(locator);
|
|
720
1057
|
const replica = replicas.get(locator);
|
|
721
1058
|
const room = rooms.get(locator);
|
|
722
1059
|
if (replica === undefined || room === undefined) {
|
|
723
|
-
throw
|
|
1060
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
724
1061
|
}
|
|
725
1062
|
const saved = [];
|
|
726
1063
|
for (const { path, revision } of named) {
|
|
727
1064
|
const record = roomDrafts.get(locator)?.get(path);
|
|
728
1065
|
if (record === undefined || record.revision !== revision) {
|
|
729
|
-
return { outcome: 'refused',
|
|
1066
|
+
return { outcome: 'refused', code: 'stale', retryable: false, file: path };
|
|
730
1067
|
}
|
|
731
1068
|
if (!saved.some((draft) => draft.path === path))
|
|
732
1069
|
saved.push({ path, parts: record.parts });
|
|
733
1070
|
}
|
|
734
1071
|
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
|
|
735
1072
|
if (changed.length > 0)
|
|
736
|
-
return { outcome: 'refused',
|
|
1073
|
+
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
737
1074
|
const parts = saved.map(({ path, parts }) => changesOf(path, parts));
|
|
738
|
-
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,
|
|
739
1094
|
baseRoomVersion: replica.confirmed.record.roomVersion,
|
|
740
1095
|
composition: compositionFor(replica.confirmed.record),
|
|
741
1096
|
operations: room.operations,
|
|
@@ -743,29 +1098,93 @@ export function composeRuntime(options) {
|
|
|
743
1098
|
providerLimits: limits,
|
|
744
1099
|
handle: room.handle,
|
|
745
1100
|
visible: replica.visible,
|
|
746
|
-
changes
|
|
1101
|
+
changes,
|
|
747
1102
|
bases: parts.flatMap(({ bases }) => bases),
|
|
748
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 }];
|
|
749
1116
|
});
|
|
750
|
-
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')
|
|
751
1135
|
return { outcome: 'refused', ...built.refusal };
|
|
752
|
-
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
|
+
}
|
|
753
1167
|
const consumed = saved.map(({ path }) => path);
|
|
754
1168
|
try {
|
|
755
|
-
await local.
|
|
756
|
-
|
|
757
|
-
previous: replica.confirmed.record,
|
|
1169
|
+
await local.keepAuthored({
|
|
1170
|
+
locator,
|
|
758
1171
|
queue: batches,
|
|
759
|
-
|
|
760
|
-
saved,
|
|
1172
|
+
saved: drafted,
|
|
761
1173
|
consumedRoomDrafts: consumed,
|
|
762
1174
|
});
|
|
763
1175
|
}
|
|
764
1176
|
catch {
|
|
765
|
-
return { outcome: 'refused',
|
|
1177
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
766
1178
|
}
|
|
767
|
-
|
|
768
|
-
|
|
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 });
|
|
1185
|
+
}
|
|
1186
|
+
if (!localSaves)
|
|
1187
|
+
recorded = false;
|
|
769
1188
|
for (const path of consumed)
|
|
770
1189
|
forgetRoomDraft(locator, path);
|
|
771
1190
|
replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
|
|
@@ -773,9 +1192,12 @@ export function composeRuntime(options) {
|
|
|
773
1192
|
queue: true,
|
|
774
1193
|
drafts: consumed,
|
|
775
1194
|
}));
|
|
776
|
-
moveTo({ ...saving(status), ...attention() });
|
|
1195
|
+
moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
|
|
777
1196
|
submitLater(locator);
|
|
778
|
-
|
|
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 };
|
|
779
1201
|
}
|
|
780
1202
|
async function mark(local, locator, identifier, submission, lost) {
|
|
781
1203
|
const state = pendings.get(identifier);
|
|
@@ -785,6 +1207,8 @@ export function composeRuntime(options) {
|
|
|
785
1207
|
return;
|
|
786
1208
|
}
|
|
787
1209
|
await local.submit(locator, identifier, submission, lost);
|
|
1210
|
+
if (fences.has(locator))
|
|
1211
|
+
return;
|
|
788
1212
|
pendings.set(identifier, { ...state, submission, lost });
|
|
789
1213
|
const replica = replicas.get(locator);
|
|
790
1214
|
if (replica !== undefined)
|
|
@@ -795,22 +1219,24 @@ export function composeRuntime(options) {
|
|
|
795
1219
|
if (!continuing())
|
|
796
1220
|
throw new Error('connection refused');
|
|
797
1221
|
await local.keepUpload(locator, identifier, upload);
|
|
1222
|
+
if (fences.has(locator))
|
|
1223
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
798
1224
|
pendings.set(identifier, { ...pendings.get(identifier), upload });
|
|
799
1225
|
if (!continuing())
|
|
800
1226
|
throw new Error('connection refused');
|
|
801
1227
|
}
|
|
802
1228
|
async function readHistory(locator, path, query = {}) {
|
|
803
1229
|
if (closed || refusal !== null)
|
|
804
|
-
return
|
|
1230
|
+
return UNCERTAIN;
|
|
805
1231
|
try {
|
|
806
1232
|
await ensureAuthorization();
|
|
807
1233
|
}
|
|
808
1234
|
catch {
|
|
809
|
-
return
|
|
810
|
-
}
|
|
811
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
812
|
-
return { kind: 'transport-uncertainty' };
|
|
1235
|
+
return UNCERTAIN;
|
|
813
1236
|
}
|
|
1237
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1238
|
+
return UNCERTAIN;
|
|
1239
|
+
const since = fencings;
|
|
814
1240
|
const transfer = ['transfer', 'history', locator, path];
|
|
815
1241
|
if (query.revision !== undefined)
|
|
816
1242
|
transfer.push(query.revision);
|
|
@@ -825,6 +1251,8 @@ export function composeRuntime(options) {
|
|
|
825
1251
|
...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
|
|
826
1252
|
});
|
|
827
1253
|
journal.record(`outcome history ${result.kind}`);
|
|
1254
|
+
if (fencedSince(locator, since))
|
|
1255
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
828
1256
|
if (result.kind === 'scoped-refusal') {
|
|
829
1257
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
830
1258
|
if (room !== undefined)
|
|
@@ -832,12 +1260,106 @@ export function composeRuntime(options) {
|
|
|
832
1260
|
}
|
|
833
1261
|
if (result.kind === 'revoked-authorization')
|
|
834
1262
|
await refuse('revoked-authorization');
|
|
835
|
-
return result;
|
|
1263
|
+
return historyAnswer(result);
|
|
836
1264
|
}
|
|
837
1265
|
catch {
|
|
838
1266
|
journal.record('outcome history transport-uncertainty');
|
|
839
|
-
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();
|
|
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);
|
|
840
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
|
+
});
|
|
841
1363
|
}
|
|
842
1364
|
function declaredAgents() {
|
|
843
1365
|
for (const capability of capabilities) {
|
|
@@ -858,18 +1380,17 @@ export function composeRuntime(options) {
|
|
|
858
1380
|
async function invokeAction(name, input) {
|
|
859
1381
|
await ready;
|
|
860
1382
|
if (closed || refusal !== null)
|
|
861
|
-
return
|
|
1383
|
+
return UNCERTAIN;
|
|
862
1384
|
if (declaredAction(name) === undefined)
|
|
863
|
-
return
|
|
1385
|
+
return undeclared(name);
|
|
864
1386
|
try {
|
|
865
1387
|
await ensureAuthorization();
|
|
866
1388
|
}
|
|
867
1389
|
catch {
|
|
868
|
-
return
|
|
869
|
-
}
|
|
870
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
871
|
-
return { kind: 'transport-uncertainty' };
|
|
1390
|
+
return UNCERTAIN;
|
|
872
1391
|
}
|
|
1392
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1393
|
+
return UNCERTAIN;
|
|
873
1394
|
journal.record(`transfer action ${name}`);
|
|
874
1395
|
try {
|
|
875
1396
|
const result = await options.adapter.action(authorization, { name, input });
|
|
@@ -883,11 +1404,11 @@ export function composeRuntime(options) {
|
|
|
883
1404
|
journal.record(`outcome action ${result.kind}`);
|
|
884
1405
|
if (result.kind === 'rejected')
|
|
885
1406
|
journal.record(`refusal rejected ${result.code}`);
|
|
886
|
-
return result;
|
|
1407
|
+
return calledResult(result);
|
|
887
1408
|
}
|
|
888
1409
|
catch {
|
|
889
1410
|
journal.record('outcome action transport-uncertainty');
|
|
890
|
-
return
|
|
1411
|
+
return UNCERTAIN;
|
|
891
1412
|
}
|
|
892
1413
|
}
|
|
893
1414
|
function continuing() {
|
|
@@ -904,11 +1425,11 @@ export function composeRuntime(options) {
|
|
|
904
1425
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
905
1426
|
!options.adapter.reached(locator, roomVersion, written));
|
|
906
1427
|
}
|
|
907
|
-
async function pullRefresh(authorized, confirmed) {
|
|
908
|
-
if (!
|
|
1428
|
+
async function pullRefresh(authorized, confirmed, live) {
|
|
1429
|
+
if (!live())
|
|
909
1430
|
return unconfirmed;
|
|
910
1431
|
try {
|
|
911
|
-
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);
|
|
912
1433
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
913
1434
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
914
1435
|
return result;
|
|
@@ -918,28 +1439,69 @@ export function composeRuntime(options) {
|
|
|
918
1439
|
throw error;
|
|
919
1440
|
}
|
|
920
1441
|
}
|
|
921
|
-
|
|
922
|
-
|
|
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())
|
|
923
1471
|
return unconfirmed;
|
|
1472
|
+
if (replicas.has(room.locator)) {
|
|
1473
|
+
catching.add(room.locator);
|
|
1474
|
+
readSide.recover();
|
|
1475
|
+
}
|
|
924
1476
|
try {
|
|
925
1477
|
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
926
1478
|
collection: modelOf(room.locator),
|
|
927
1479
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
928
|
-
}, (models, held) => remodel(room.locator, models, held),
|
|
1480
|
+
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
929
1481
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
930
1482
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1483
|
+
if (result.kind !== 'staged')
|
|
1484
|
+
caughtUpTo(room.locator);
|
|
931
1485
|
return result;
|
|
932
1486
|
}
|
|
933
1487
|
catch (error) {
|
|
934
1488
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
1489
|
+
caughtUpTo(room.locator);
|
|
935
1490
|
throw error;
|
|
936
1491
|
}
|
|
937
1492
|
}
|
|
938
|
-
|
|
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;
|
|
939
1501
|
const held = replicas.get(room.locator);
|
|
940
1502
|
if (held !== undefined) {
|
|
941
|
-
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
942
|
-
if (!
|
|
1503
|
+
const refreshed = await pullRefresh(authorized, held.confirmed, live);
|
|
1504
|
+
if (!live())
|
|
943
1505
|
return unconfirmed;
|
|
944
1506
|
if (refreshed.kind === 'still-current') {
|
|
945
1507
|
stale.delete(room.locator);
|
|
@@ -953,31 +1515,45 @@ export function composeRuntime(options) {
|
|
|
953
1515
|
return null;
|
|
954
1516
|
}
|
|
955
1517
|
if (refreshed.kind === 'advanced') {
|
|
956
|
-
if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
|
|
1518
|
+
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
|
|
957
1519
|
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
958
1520
|
const reached = now !== undefined &&
|
|
959
1521
|
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
960
|
-
return reached ? null : advance(room, local, authorized);
|
|
1522
|
+
return reached ? null : advance(room, local, authorized, live);
|
|
961
1523
|
}
|
|
962
1524
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
963
1525
|
return null;
|
|
964
1526
|
}
|
|
965
1527
|
if (refreshed.kind !== 'beyond-horizon')
|
|
966
1528
|
return refreshed;
|
|
1529
|
+
if (opened !== null) {
|
|
1530
|
+
owed.add(room.locator);
|
|
1531
|
+
readSide.recover();
|
|
1532
|
+
return unconfirmed;
|
|
1533
|
+
}
|
|
967
1534
|
}
|
|
968
|
-
const bootstrapped = await pullBootstrap(authorized, room);
|
|
969
|
-
if (!
|
|
1535
|
+
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
1536
|
+
if (!live())
|
|
970
1537
|
return unconfirmed;
|
|
971
1538
|
if (bootstrapped.kind !== 'staged')
|
|
972
1539
|
return bootstrapped;
|
|
973
|
-
await publish(local, room, bootstrapped.confirmed);
|
|
1540
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
974
1541
|
return null;
|
|
975
1542
|
}
|
|
976
1543
|
async function withhold(withheld, room, local, progress) {
|
|
1544
|
+
if (withheld.kind === 'unconfirmed')
|
|
1545
|
+
return;
|
|
977
1546
|
if (withheld.kind === 'refused')
|
|
978
1547
|
stale.delete(room.locator);
|
|
979
1548
|
else
|
|
980
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);
|
|
981
1557
|
if (withheld.kind === 'scoped-refusal') {
|
|
982
1558
|
await scoped(withheld, room, local);
|
|
983
1559
|
}
|
|
@@ -1002,6 +1578,7 @@ export function composeRuntime(options) {
|
|
|
1002
1578
|
function refusedSource(room, { refusal }) {
|
|
1003
1579
|
sourceRefusals.set(room.locator, {
|
|
1004
1580
|
code: refusal.rule,
|
|
1581
|
+
retryable: false,
|
|
1005
1582
|
fields: {
|
|
1006
1583
|
file: refusal.file,
|
|
1007
1584
|
...(refusal.location === undefined ? {} : { location: refusal.location }),
|
|
@@ -1016,8 +1593,16 @@ export function composeRuntime(options) {
|
|
|
1016
1593
|
}
|
|
1017
1594
|
function leaving(batch, result) {
|
|
1018
1595
|
if (result.kind === 'expired')
|
|
1019
|
-
return { batch, returned:
|
|
1020
|
-
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
|
+
};
|
|
1021
1606
|
}
|
|
1022
1607
|
function logAccepted(locator, submitted, accepted) {
|
|
1023
1608
|
journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
|
|
@@ -1025,6 +1610,7 @@ export function composeRuntime(options) {
|
|
|
1025
1610
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1026
1611
|
}
|
|
1027
1612
|
async function adoptAccepted(room, local, submitted, accepted) {
|
|
1613
|
+
await referenced(room.locator);
|
|
1028
1614
|
if (!continuing())
|
|
1029
1615
|
return unconfirmed;
|
|
1030
1616
|
const current = replicas.get(room.locator);
|
|
@@ -1035,22 +1621,23 @@ export function composeRuntime(options) {
|
|
|
1035
1621
|
logAccepted(room.locator, submitted, accepted);
|
|
1036
1622
|
const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
1037
1623
|
? { kind: 'staged', confirmed: current.confirmed }
|
|
1038
|
-
: applyReturned(current.confirmed,
|
|
1624
|
+
: applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
1039
1625
|
? accepted.roomVersion
|
|
1040
1626
|
: held, accepted.roomVersion);
|
|
1041
1627
|
if (applied.kind !== 'staged') {
|
|
1042
1628
|
const state = pendings.get(submitted.id);
|
|
1043
1629
|
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1044
|
-
if (state !== undefined)
|
|
1630
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
1045
1631
|
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1046
1632
|
emitQueue(room.locator);
|
|
1047
1633
|
return applied;
|
|
1048
1634
|
}
|
|
1049
|
-
const
|
|
1635
|
+
const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
1050
1636
|
pendings.delete(submitted.id);
|
|
1051
1637
|
unreadable.delete(room.locator);
|
|
1052
1638
|
sourceRefusals.delete(room.locator);
|
|
1053
1639
|
declarationRefusals.delete(room.locator);
|
|
1640
|
+
const set = settled();
|
|
1054
1641
|
list(room, true);
|
|
1055
1642
|
publishChangeSet(set);
|
|
1056
1643
|
moveTo({ ...status, ...attention() });
|
|
@@ -1064,14 +1651,14 @@ export function composeRuntime(options) {
|
|
|
1064
1651
|
refusedSource(room, failed);
|
|
1065
1652
|
return false;
|
|
1066
1653
|
}
|
|
1067
|
-
async function submitRoom(room, local, authorized, progress) {
|
|
1068
|
-
while (continuing() && authorization !== undefined) {
|
|
1654
|
+
async function submitRoom(room, local, authorized, progress, signal) {
|
|
1655
|
+
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
1069
1656
|
const replica = replicas.get(room.locator);
|
|
1070
1657
|
const head = replica?.queue[0];
|
|
1071
1658
|
if (replica === undefined || head === undefined)
|
|
1072
|
-
return;
|
|
1659
|
+
return 'done';
|
|
1073
1660
|
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1074
|
-
return;
|
|
1661
|
+
return 'done';
|
|
1075
1662
|
const state = pendings.get(head.id);
|
|
1076
1663
|
const { submission, lost: earlier } = state ?? {
|
|
1077
1664
|
submission: { state: 'waiting' },
|
|
@@ -1079,24 +1666,30 @@ export function composeRuntime(options) {
|
|
|
1079
1666
|
};
|
|
1080
1667
|
if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
|
|
1081
1668
|
progress.complete = false;
|
|
1082
|
-
return;
|
|
1669
|
+
return { at: Date.parse(submission.reset) };
|
|
1083
1670
|
}
|
|
1084
1671
|
if (writesHeld()) {
|
|
1085
1672
|
progress.complete = false;
|
|
1086
1673
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1087
|
-
return;
|
|
1674
|
+
return 'done';
|
|
1088
1675
|
}
|
|
1089
1676
|
const lost = earlier || submission.state === 'submitted';
|
|
1677
|
+
if (lost && pastHorizon(head.id)) {
|
|
1678
|
+
await discardPending(room, local, signal);
|
|
1679
|
+
return 'done';
|
|
1680
|
+
}
|
|
1090
1681
|
let result;
|
|
1091
1682
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
1092
1683
|
await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
|
|
1093
|
-
if (discoveryPaused)
|
|
1094
|
-
return;
|
|
1095
|
-
if (writesHeld()) {
|
|
1684
|
+
if (discoveryPaused || fences.has(room.locator))
|
|
1685
|
+
return 'done';
|
|
1686
|
+
if (signal.aborted || writesHeld()) {
|
|
1096
1687
|
await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
|
|
1688
|
+
if (signal.aborted)
|
|
1689
|
+
return 'done';
|
|
1097
1690
|
progress.complete = false;
|
|
1098
1691
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1099
|
-
return;
|
|
1692
|
+
return 'done';
|
|
1100
1693
|
}
|
|
1101
1694
|
const opensBefore = openSignals;
|
|
1102
1695
|
const answered = awaitingAnswer(room.locator);
|
|
@@ -1112,32 +1705,36 @@ export function composeRuntime(options) {
|
|
|
1112
1705
|
progress.complete = false;
|
|
1113
1706
|
if (continuing())
|
|
1114
1707
|
progress.reachable = false;
|
|
1115
|
-
return;
|
|
1708
|
+
return 'again';
|
|
1709
|
+
}
|
|
1710
|
+
if (signal.aborted) {
|
|
1711
|
+
answered();
|
|
1712
|
+
return 'done';
|
|
1116
1713
|
}
|
|
1117
1714
|
if (result.kind !== 'accepted' || !continuing())
|
|
1118
1715
|
answered();
|
|
1119
1716
|
if (!continuing())
|
|
1120
|
-
return;
|
|
1717
|
+
return 'done';
|
|
1121
1718
|
if (result.kind === 'transport-uncertainty') {
|
|
1122
1719
|
progress.complete = false;
|
|
1123
1720
|
progress.reachable = false;
|
|
1124
|
-
return;
|
|
1721
|
+
return 'again';
|
|
1125
1722
|
}
|
|
1126
1723
|
if (result.kind === 'room-not-readable') {
|
|
1127
1724
|
progress.complete = false;
|
|
1128
1725
|
unreadable.add(room.locator);
|
|
1129
1726
|
list(room, false);
|
|
1130
1727
|
emitRooms();
|
|
1131
|
-
return;
|
|
1728
|
+
return 'done';
|
|
1132
1729
|
}
|
|
1133
1730
|
if (result.kind === 'revoked-authorization') {
|
|
1134
1731
|
await refuse('revoked-authorization');
|
|
1135
|
-
return;
|
|
1732
|
+
return 'done';
|
|
1136
1733
|
}
|
|
1137
1734
|
if (result.kind === 'scoped-refusal') {
|
|
1138
1735
|
await scoped(result, room, local);
|
|
1139
1736
|
progress.complete = false;
|
|
1140
|
-
return;
|
|
1737
|
+
return 'done';
|
|
1141
1738
|
}
|
|
1142
1739
|
if (result.kind === 'write-halted') {
|
|
1143
1740
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
@@ -1146,16 +1743,24 @@ export function composeRuntime(options) {
|
|
|
1146
1743
|
continue;
|
|
1147
1744
|
haltedRefusal = true;
|
|
1148
1745
|
progress.complete = false;
|
|
1149
|
-
return;
|
|
1746
|
+
return 'done';
|
|
1150
1747
|
}
|
|
1151
1748
|
if (result.kind === 'conflicted' ||
|
|
1152
1749
|
result.kind === 'rejected' ||
|
|
1153
1750
|
result.kind === 'expired' ||
|
|
1154
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
|
+
}
|
|
1155
1757
|
if (result.kind === 'expired') {
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
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
|
+
}
|
|
1159
1764
|
}
|
|
1160
1765
|
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
|
|
1161
1766
|
(result.kind === 'conflicted' && result.roomVersion !== undefined
|
|
@@ -1168,8 +1773,7 @@ export function composeRuntime(options) {
|
|
|
1168
1773
|
const reset = result.reset;
|
|
1169
1774
|
progress.complete = false;
|
|
1170
1775
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1171
|
-
|
|
1172
|
-
return;
|
|
1776
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
1173
1777
|
}
|
|
1174
1778
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
1175
1779
|
const left = leaving(head, result);
|
|
@@ -1180,36 +1784,82 @@ export function composeRuntime(options) {
|
|
|
1180
1784
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
1181
1785
|
return;
|
|
1182
1786
|
}
|
|
1183
|
-
const
|
|
1184
|
-
leaving: result.kind === 'expired' && landed(head, current.confirmed)
|
|
1787
|
+
const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
1788
|
+
leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
|
|
1789
|
+
(result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
|
|
1185
1790
|
? { ...left, returned: null }
|
|
1186
1791
|
: left,
|
|
1187
1792
|
});
|
|
1188
|
-
publishChangeSet(
|
|
1793
|
+
publishChangeSet(settled());
|
|
1189
1794
|
moveTo({ ...status, ...attention() });
|
|
1190
1795
|
});
|
|
1191
1796
|
if (result.kind === 'expired')
|
|
1192
1797
|
continue;
|
|
1193
|
-
const withheld = await advance(room, local, authorized);
|
|
1194
|
-
if (withheld !== null)
|
|
1195
|
-
|
|
1798
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
1799
|
+
if (withheld !== null) {
|
|
1800
|
+
await withhold(withheld, room, local, progress);
|
|
1801
|
+
return 'done';
|
|
1802
|
+
}
|
|
1196
1803
|
continue;
|
|
1197
1804
|
}
|
|
1198
1805
|
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1199
1806
|
if (!kept) {
|
|
1200
1807
|
progress.complete = false;
|
|
1201
|
-
return;
|
|
1808
|
+
return 'done';
|
|
1202
1809
|
}
|
|
1203
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);
|
|
1204
1854
|
}
|
|
1205
1855
|
function awaitingAnswer(locator) {
|
|
1206
1856
|
let release = () => { };
|
|
1207
1857
|
const awaited = new Promise((resolve) => {
|
|
1208
1858
|
release = resolve;
|
|
1209
1859
|
});
|
|
1210
|
-
answering.set(locator, awaited);
|
|
1860
|
+
answering.set(locator, { generation, answered: awaited });
|
|
1211
1861
|
return () => {
|
|
1212
|
-
if (answering.get(locator) === awaited)
|
|
1862
|
+
if (answering.get(locator)?.answered === awaited)
|
|
1213
1863
|
answering.delete(locator);
|
|
1214
1864
|
release();
|
|
1215
1865
|
};
|
|
@@ -1242,7 +1892,7 @@ export function composeRuntime(options) {
|
|
|
1242
1892
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
1243
1893
|
};
|
|
1244
1894
|
const reclassified = sameMembership(declared, held.confirmed.record)
|
|
1245
|
-
?
|
|
1895
|
+
? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
|
|
1246
1896
|
: readReclassified(declared, held.confirmed);
|
|
1247
1897
|
const record = reclassified.kind === 'staged'
|
|
1248
1898
|
? declared
|
|
@@ -1250,12 +1900,10 @@ export function composeRuntime(options) {
|
|
|
1250
1900
|
...declared,
|
|
1251
1901
|
typedSelection: held.confirmed.record.typedSelection,
|
|
1252
1902
|
};
|
|
1253
|
-
const read = reclassified.kind === 'deferred'
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
queue: held.queue,
|
|
1258
|
-
});
|
|
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 });
|
|
1259
1907
|
if (read.kind === 'staged') {
|
|
1260
1908
|
replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
|
|
1261
1909
|
if (declarationRefusals.delete(room.locator)) {
|
|
@@ -1282,7 +1930,11 @@ export function composeRuntime(options) {
|
|
|
1282
1930
|
});
|
|
1283
1931
|
}
|
|
1284
1932
|
async function scoped(result, room, local) {
|
|
1285
|
-
scopedRefusals.set(room.locator, {
|
|
1933
|
+
scopedRefusals.set(room.locator, {
|
|
1934
|
+
code: result.code,
|
|
1935
|
+
fields: result.fields,
|
|
1936
|
+
retryable: result.retryable,
|
|
1937
|
+
});
|
|
1286
1938
|
let scopedAway = room;
|
|
1287
1939
|
if (result.scope === 'model')
|
|
1288
1940
|
await retainModel(local, room, 'collection', {
|
|
@@ -1319,22 +1971,26 @@ export function composeRuntime(options) {
|
|
|
1319
1971
|
roomVersion: record.roomVersion,
|
|
1320
1972
|
documents: record.documents,
|
|
1321
1973
|
revisions: record.revisions,
|
|
1322
|
-
attachments: record.attachments,
|
|
1323
1974
|
},
|
|
1324
1975
|
];
|
|
1325
1976
|
});
|
|
1326
1977
|
}
|
|
1327
|
-
async function synchronize(listed, local, progress, attempt) {
|
|
1978
|
+
async function synchronize(listed, local, progress, attempt, live) {
|
|
1328
1979
|
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
|
|
1980
|
+
if (!live())
|
|
1981
|
+
return;
|
|
1329
1982
|
const ready = listed.filter((_, index) => {
|
|
1330
1983
|
const outcome = prepared[index];
|
|
1331
1984
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1332
1985
|
});
|
|
1333
|
-
if (attempt === 'opened')
|
|
1986
|
+
if (attempt === 'opened') {
|
|
1334
1987
|
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
1988
|
+
handed = true;
|
|
1989
|
+
}
|
|
1990
|
+
discovering?.release();
|
|
1335
1991
|
const loaded = await Promise.allSettled(ready
|
|
1336
1992
|
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1337
|
-
.map((room) => loadRoom(room, local, progress)));
|
|
1993
|
+
.map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
|
|
1338
1994
|
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1339
1995
|
if (failed !== undefined)
|
|
1340
1996
|
throw failed.reason;
|
|
@@ -1363,14 +2019,14 @@ export function composeRuntime(options) {
|
|
|
1363
2019
|
}
|
|
1364
2020
|
return true;
|
|
1365
2021
|
}
|
|
1366
|
-
async function caughtUp(room, local, progress) {
|
|
2022
|
+
async function caughtUp(room, local, progress, live) {
|
|
1367
2023
|
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1368
|
-
if (held === undefined || authorization === undefined)
|
|
2024
|
+
if (held === undefined || authorization === undefined || !live())
|
|
1369
2025
|
return;
|
|
1370
2026
|
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
1371
2027
|
if (!behind)
|
|
1372
2028
|
return;
|
|
1373
|
-
const withheld = await advance(room, local, authorization);
|
|
2029
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1374
2030
|
if (withheld !== null)
|
|
1375
2031
|
await withhold(withheld, room, local, progress);
|
|
1376
2032
|
}
|
|
@@ -1380,39 +2036,46 @@ export function composeRuntime(options) {
|
|
|
1380
2036
|
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1381
2037
|
});
|
|
1382
2038
|
}
|
|
1383
|
-
async function loadRoom(room, local, progress) {
|
|
1384
|
-
if (
|
|
2039
|
+
async function loadRoom(room, local, progress, live) {
|
|
2040
|
+
if (!live() || authorization === undefined)
|
|
1385
2041
|
return;
|
|
1386
2042
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1387
|
-
await reload(room, local, progress);
|
|
2043
|
+
await reload(room, local, progress, live);
|
|
1388
2044
|
return;
|
|
1389
2045
|
}
|
|
1390
|
-
const withheld = await advance(room, local, authorization);
|
|
2046
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1391
2047
|
if (withheld !== null) {
|
|
1392
2048
|
await withhold(withheld, room, local, progress);
|
|
1393
2049
|
return;
|
|
1394
2050
|
}
|
|
1395
|
-
await caughtUp(room, local, progress);
|
|
2051
|
+
await caughtUp(room, local, progress, live);
|
|
1396
2052
|
}
|
|
1397
|
-
async function reload(room, local, progress) {
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
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);
|
|
1406
2069
|
}
|
|
1407
|
-
await publish(local, room, bootstrapped.confirmed);
|
|
1408
|
-
await caughtUp(room, local, progress);
|
|
1409
2070
|
}
|
|
1410
|
-
async function discoverRooms(local, progress, attempt) {
|
|
2071
|
+
async function discoverRooms(local, progress, attempt, live) {
|
|
1411
2072
|
if (closed || authorization === undefined) {
|
|
1412
2073
|
notifyReady();
|
|
1413
2074
|
return;
|
|
1414
2075
|
}
|
|
1415
2076
|
moveTo(transferring(status));
|
|
2077
|
+
discoveries += 1;
|
|
2078
|
+
const started = discoveries;
|
|
1416
2079
|
let result;
|
|
1417
2080
|
try {
|
|
1418
2081
|
result = await options.adapter.discover(authorization);
|
|
@@ -1420,7 +2083,8 @@ export function composeRuntime(options) {
|
|
|
1420
2083
|
catch {
|
|
1421
2084
|
result = { kind: 'transport-uncertainty' };
|
|
1422
2085
|
}
|
|
1423
|
-
if (
|
|
2086
|
+
if (!live()) {
|
|
2087
|
+
progress.complete = false;
|
|
1424
2088
|
notifyReady();
|
|
1425
2089
|
return;
|
|
1426
2090
|
}
|
|
@@ -1434,15 +2098,16 @@ export function composeRuntime(options) {
|
|
|
1434
2098
|
}
|
|
1435
2099
|
progress.reachable = false;
|
|
1436
2100
|
progress.complete = false;
|
|
1437
|
-
if (attempt === 'opened') {
|
|
1438
|
-
options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
|
|
1439
|
-
}
|
|
1440
2101
|
notifyReady();
|
|
1441
2102
|
return;
|
|
1442
2103
|
}
|
|
1443
2104
|
if (result.kind === 'contract-unavailable') {
|
|
1444
2105
|
journal.record(`connection contract unavailable ${result.code}`);
|
|
1445
|
-
await refuse('missing-required-guarantee', {
|
|
2106
|
+
await refuse('missing-required-guarantee', {
|
|
2107
|
+
code: result.code,
|
|
2108
|
+
fields: result.fields,
|
|
2109
|
+
retryable: result.retryable,
|
|
2110
|
+
});
|
|
1446
2111
|
notifyReady();
|
|
1447
2112
|
return;
|
|
1448
2113
|
}
|
|
@@ -1477,14 +2142,20 @@ export function composeRuntime(options) {
|
|
|
1477
2142
|
}
|
|
1478
2143
|
const previousCapabilities = capabilities;
|
|
1479
2144
|
capabilities = result.capabilities;
|
|
2145
|
+
outcomeRetention = result.retention.outcome;
|
|
1480
2146
|
notifyReady();
|
|
1481
2147
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1482
2148
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
1483
2149
|
limits = result.providerLimits;
|
|
1484
2150
|
await local.declare(limits, capabilities);
|
|
1485
2151
|
}
|
|
1486
|
-
|
|
1487
|
-
|
|
2152
|
+
const signedIn = result.member ?? null;
|
|
2153
|
+
if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
|
|
2154
|
+
await local.keepMember(signedIn);
|
|
2155
|
+
member = signedIn;
|
|
2156
|
+
emitMember();
|
|
2157
|
+
}
|
|
2158
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
1488
2159
|
}
|
|
1489
2160
|
async function ensureAuthorization() {
|
|
1490
2161
|
if (authorization !== undefined)
|
|
@@ -1498,44 +2169,87 @@ export function composeRuntime(options) {
|
|
|
1498
2169
|
throw error;
|
|
1499
2170
|
}
|
|
1500
2171
|
}
|
|
1501
|
-
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) {
|
|
1502
2192
|
if (stopping)
|
|
1503
|
-
return
|
|
1504
|
-
|
|
1505
|
-
|
|
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;
|
|
1506
2204
|
}
|
|
1507
2205
|
function sync(attempt) {
|
|
1508
|
-
return task(async () => {
|
|
1509
|
-
if (
|
|
2206
|
+
return task(OPENING_LANE, async (live) => {
|
|
2207
|
+
if (!live()) {
|
|
1510
2208
|
notifyReady();
|
|
1511
2209
|
return;
|
|
1512
2210
|
}
|
|
1513
2211
|
const progress = { complete: true, reachable: true };
|
|
1514
|
-
|
|
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;
|
|
1515
2218
|
complete = false;
|
|
1516
2219
|
recorded = false;
|
|
1517
2220
|
moveTo(transferring(status));
|
|
2221
|
+
handed = false;
|
|
1518
2222
|
try {
|
|
1519
2223
|
const local = await openLocal();
|
|
1520
|
-
if (
|
|
2224
|
+
if (!live())
|
|
1521
2225
|
return;
|
|
1522
2226
|
await ensureAuthorization();
|
|
1523
|
-
if (
|
|
2227
|
+
if (!live())
|
|
1524
2228
|
return;
|
|
1525
|
-
await discoverRooms(local, progress, attempt);
|
|
2229
|
+
await discoverRooms(local, progress, attempt, live);
|
|
1526
2230
|
}
|
|
1527
2231
|
catch {
|
|
1528
2232
|
progress.complete = false;
|
|
1529
|
-
if (attempt === 'opened')
|
|
1530
|
-
|
|
1531
|
-
}
|
|
2233
|
+
if (attempt === 'opened' && !handed)
|
|
2234
|
+
progress.reachable = false;
|
|
1532
2235
|
}
|
|
1533
2236
|
finally {
|
|
2237
|
+
gate.release();
|
|
1534
2238
|
notifyReady();
|
|
1535
2239
|
}
|
|
1536
|
-
if (
|
|
2240
|
+
if (!live())
|
|
2241
|
+
return;
|
|
2242
|
+
if (attempt !== 'opened') {
|
|
2243
|
+
await finishOpening(progress, live);
|
|
1537
2244
|
return;
|
|
1538
|
-
|
|
2245
|
+
}
|
|
2246
|
+
if (handed || closed || progress.reachable)
|
|
2247
|
+
return;
|
|
2248
|
+
underway = null;
|
|
2249
|
+
opened = null;
|
|
2250
|
+
journal.record('connection reopen');
|
|
2251
|
+
await rest(progress, live);
|
|
2252
|
+
options.adapter.roomStream?.reopen();
|
|
1539
2253
|
});
|
|
1540
2254
|
}
|
|
1541
2255
|
function closedSocket(revoked) {
|
|
@@ -1545,29 +2259,51 @@ export function composeRuntime(options) {
|
|
|
1545
2259
|
void refuse('revoked-authorization');
|
|
1546
2260
|
return;
|
|
1547
2261
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
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
|
+
});
|
|
1551
2270
|
}
|
|
1552
2271
|
function applyOpening(expired) {
|
|
1553
|
-
|
|
1554
|
-
|
|
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
|
+
return task(OPENING_LANE, async (live) => {
|
|
1555
2276
|
underway = null;
|
|
1556
|
-
if (
|
|
2277
|
+
if (!live())
|
|
1557
2278
|
return;
|
|
1558
2279
|
if (progress === null) {
|
|
1559
|
-
await
|
|
2280
|
+
await replacing;
|
|
2281
|
+
await rest(cycle, live);
|
|
1560
2282
|
return;
|
|
1561
2283
|
}
|
|
2284
|
+
await roomsCarried(expired);
|
|
2285
|
+
if (!live())
|
|
2286
|
+
return;
|
|
2287
|
+
if (opened === generation)
|
|
2288
|
+
opened = null;
|
|
2289
|
+
const replaced = new Set(owed);
|
|
2290
|
+
owed.clear();
|
|
1562
2291
|
try {
|
|
1563
2292
|
const local = await openLocal();
|
|
1564
2293
|
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
1565
|
-
const reloading = held.filter((room) => expired.includes(room.locator)
|
|
1566
|
-
|
|
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)];
|
|
1567
2301
|
if (progress.reachable) {
|
|
1568
|
-
for (const room of held)
|
|
1569
|
-
if (!reloading.includes(room))
|
|
2302
|
+
for (const room of held) {
|
|
2303
|
+
if (!expired.includes(room.locator) && !reloading.includes(room)) {
|
|
1570
2304
|
await confirmModels(room, local);
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
1571
2307
|
}
|
|
1572
2308
|
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1573
2309
|
if (failed !== undefined)
|
|
@@ -1577,21 +2313,21 @@ export function composeRuntime(options) {
|
|
|
1577
2313
|
progress.complete = false;
|
|
1578
2314
|
progress.reachable = false;
|
|
1579
2315
|
}
|
|
1580
|
-
|
|
2316
|
+
if (live())
|
|
2317
|
+
await finishOpening(progress, live);
|
|
1581
2318
|
});
|
|
1582
2319
|
}
|
|
1583
|
-
async function
|
|
1584
|
-
const progress = { complete: true, reachable: true };
|
|
2320
|
+
async function reloadNow(replaced, progress) {
|
|
1585
2321
|
try {
|
|
1586
2322
|
const local = await openLocal();
|
|
1587
|
-
await Promise.allSettled([...discovered.values()]
|
|
1588
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
1589
|
-
.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))));
|
|
1590
2326
|
}
|
|
1591
2327
|
catch {
|
|
1592
2328
|
progress.reachable = false;
|
|
2329
|
+
return [];
|
|
1593
2330
|
}
|
|
1594
|
-
await rest(progress);
|
|
1595
2331
|
}
|
|
1596
2332
|
async function confirmModels(room, local) {
|
|
1597
2333
|
for (const attached of ['collection', 'typed-selection']) {
|
|
@@ -1602,38 +2338,96 @@ export function composeRuntime(options) {
|
|
|
1602
2338
|
}
|
|
1603
2339
|
}
|
|
1604
2340
|
function submitLater(locator) {
|
|
1605
|
-
|
|
1606
|
-
if (running !== undefined) {
|
|
1607
|
-
running.again = true;
|
|
2341
|
+
if (localSaves)
|
|
1608
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);
|
|
1609
2356
|
}
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
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';
|
|
1637
2431
|
}
|
|
1638
2432
|
function submitAll() {
|
|
1639
2433
|
for (const [locator, replica] of replicas) {
|
|
@@ -1641,31 +2435,40 @@ export function composeRuntime(options) {
|
|
|
1641
2435
|
submitLater(locator);
|
|
1642
2436
|
}
|
|
1643
2437
|
}
|
|
1644
|
-
async function finishOpening(progress) {
|
|
2438
|
+
async function finishOpening(progress, live) {
|
|
1645
2439
|
try {
|
|
1646
|
-
if (
|
|
2440
|
+
if (!live()) {
|
|
1647
2441
|
progress.complete = false;
|
|
1648
2442
|
return;
|
|
1649
2443
|
}
|
|
1650
2444
|
submitAll();
|
|
2445
|
+
for (const locator of stale)
|
|
2446
|
+
if (repairable.has(locator))
|
|
2447
|
+
repairLater(locator, {});
|
|
1651
2448
|
await conversations.reconcile();
|
|
1652
2449
|
if (status.providerUnreachable)
|
|
1653
2450
|
conversations.reconnected();
|
|
1654
2451
|
const reading = { complete: true, reachable: true };
|
|
1655
2452
|
await conversations.catchUp(reading);
|
|
1656
|
-
if (
|
|
2453
|
+
if (progress.reachable && reading.reachable)
|
|
2454
|
+
await conversations.resend(reading);
|
|
2455
|
+
if (!reading.reachable) {
|
|
1657
2456
|
progress.reachable = false;
|
|
2457
|
+
resendLater();
|
|
2458
|
+
}
|
|
1658
2459
|
}
|
|
1659
2460
|
catch {
|
|
1660
2461
|
progress.complete = false;
|
|
1661
2462
|
}
|
|
1662
2463
|
finally {
|
|
1663
|
-
|
|
1664
|
-
|
|
2464
|
+
if (live()) {
|
|
2465
|
+
complete = progress.complete;
|
|
2466
|
+
await rest(progress, live);
|
|
2467
|
+
}
|
|
1665
2468
|
}
|
|
1666
2469
|
}
|
|
1667
|
-
async function rest(progress) {
|
|
1668
|
-
if (
|
|
2470
|
+
async function rest(progress, live = () => !closed) {
|
|
2471
|
+
if (!live())
|
|
1669
2472
|
return;
|
|
1670
2473
|
let lastCompleted = status.lastCompleted;
|
|
1671
2474
|
let synced = complete && stale.size === 0 && !pending();
|
|
@@ -1673,6 +2476,8 @@ export function composeRuntime(options) {
|
|
|
1673
2476
|
const completedAt = new Date().toISOString();
|
|
1674
2477
|
try {
|
|
1675
2478
|
await (await openLocal()).complete(completedAt);
|
|
2479
|
+
if (!live())
|
|
2480
|
+
return;
|
|
1676
2481
|
recorded = true;
|
|
1677
2482
|
lastCompleted = completedAt;
|
|
1678
2483
|
}
|
|
@@ -1681,17 +2486,29 @@ export function composeRuntime(options) {
|
|
|
1681
2486
|
}
|
|
1682
2487
|
}
|
|
1683
2488
|
moveTo({
|
|
1684
|
-
state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
|
|
2489
|
+
state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
|
|
1685
2490
|
lastCompleted,
|
|
1686
2491
|
providerUnreachable: !progress.reachable,
|
|
1687
2492
|
...attention(),
|
|
1688
2493
|
});
|
|
1689
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
|
+
}
|
|
1690
2503
|
function applyRoom(locator, roomVersion) {
|
|
1691
2504
|
announced.set(locator, roomVersion);
|
|
1692
|
-
return task(async () => {
|
|
1693
|
-
|
|
1694
|
-
|
|
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)
|
|
1695
2512
|
return;
|
|
1696
2513
|
const room = discovered.get(locator);
|
|
1697
2514
|
const held = replicas.get(locator);
|
|
@@ -1702,25 +2519,41 @@ export function composeRuntime(options) {
|
|
|
1702
2519
|
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
1703
2520
|
return;
|
|
1704
2521
|
}
|
|
1705
|
-
const during = underway;
|
|
2522
|
+
const during = underway?.generation === generation ? underway.progress : null;
|
|
1706
2523
|
const progress = during ?? { complete: true, reachable: true };
|
|
1707
2524
|
if (during === null)
|
|
1708
2525
|
moveTo(transferring(status));
|
|
1709
2526
|
try {
|
|
1710
2527
|
const local = await openLocal();
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
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
|
+
});
|
|
1714
2535
|
}
|
|
1715
2536
|
catch {
|
|
1716
2537
|
stale.add(locator);
|
|
1717
2538
|
}
|
|
1718
2539
|
finally {
|
|
1719
2540
|
if (during === null)
|
|
1720
|
-
await rest(progress);
|
|
2541
|
+
await rest(progress, live);
|
|
1721
2542
|
}
|
|
1722
2543
|
});
|
|
1723
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
|
+
};
|
|
1724
2557
|
function observeStream() {
|
|
1725
2558
|
const stream = options.adapter.roomStream;
|
|
1726
2559
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -1730,6 +2563,9 @@ export function composeRuntime(options) {
|
|
|
1730
2563
|
return;
|
|
1731
2564
|
if (event.kind === 'opened') {
|
|
1732
2565
|
socketOpen = true;
|
|
2566
|
+
generation += 1;
|
|
2567
|
+
conversations.reopened();
|
|
2568
|
+
recovery.resume();
|
|
1733
2569
|
void sync('opened');
|
|
1734
2570
|
}
|
|
1735
2571
|
else if (event.kind === 'closed')
|
|
@@ -1738,7 +2574,7 @@ export function composeRuntime(options) {
|
|
|
1738
2574
|
void applyOpening(event.expired);
|
|
1739
2575
|
else
|
|
1740
2576
|
void applyRoom(event.locator, event.roomVersion);
|
|
1741
|
-
});
|
|
2577
|
+
}, receipts);
|
|
1742
2578
|
}
|
|
1743
2579
|
function writesHeld() {
|
|
1744
2580
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -1776,34 +2612,30 @@ export function composeRuntime(options) {
|
|
|
1776
2612
|
});
|
|
1777
2613
|
}
|
|
1778
2614
|
function scheduleCurrency() {
|
|
1779
|
-
if (currencyQueued || closed || stopping)
|
|
2615
|
+
if (currencyQueued === generation || closed || stopping)
|
|
1780
2616
|
return;
|
|
1781
|
-
currencyQueued =
|
|
1782
|
-
void task(runCurrency);
|
|
2617
|
+
currencyQueued = generation;
|
|
2618
|
+
void task(CONVERSATION_LANE, runCurrency);
|
|
1783
2619
|
}
|
|
1784
|
-
async function runCurrency() {
|
|
1785
|
-
currencyQueued
|
|
1786
|
-
|
|
2620
|
+
async function runCurrency(live) {
|
|
2621
|
+
if (currencyQueued === generation)
|
|
2622
|
+
currencyQueued = null;
|
|
2623
|
+
if (!live())
|
|
1787
2624
|
return;
|
|
1788
2625
|
await conversations.reconcile();
|
|
1789
2626
|
if (conversations.current()) {
|
|
1790
2627
|
show();
|
|
1791
2628
|
return;
|
|
1792
2629
|
}
|
|
1793
|
-
const
|
|
1794
|
-
|
|
1795
|
-
moveTo(transferring(resting));
|
|
2630
|
+
const progress = { complete: false, reachable: !status.providerUnreachable };
|
|
2631
|
+
moveTo(transferring(status));
|
|
1796
2632
|
try {
|
|
1797
2633
|
await conversations.catchUp(progress);
|
|
1798
2634
|
}
|
|
1799
2635
|
catch {
|
|
1800
2636
|
progress.reachable = false;
|
|
1801
2637
|
}
|
|
1802
|
-
|
|
1803
|
-
if (!closed) {
|
|
1804
|
-
moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
2638
|
+
await rest(progress, live);
|
|
1807
2639
|
}
|
|
1808
2640
|
function close() {
|
|
1809
2641
|
if (closed)
|
|
@@ -1815,7 +2647,7 @@ export function composeRuntime(options) {
|
|
|
1815
2647
|
withdrawConversations = undefined;
|
|
1816
2648
|
withdrawServiceMode?.();
|
|
1817
2649
|
conversations.close();
|
|
1818
|
-
|
|
2650
|
+
void recovery.retire();
|
|
1819
2651
|
store?.close();
|
|
1820
2652
|
void opening?.then((opened) => {
|
|
1821
2653
|
opened.close();
|
|
@@ -1825,6 +2657,7 @@ export function composeRuntime(options) {
|
|
|
1825
2657
|
storageLossListeners.clear();
|
|
1826
2658
|
statusListeners.clear();
|
|
1827
2659
|
serviceModeListeners.clear();
|
|
2660
|
+
memberListeners.clear();
|
|
1828
2661
|
readSide.close();
|
|
1829
2662
|
queueSubscriptions.clear();
|
|
1830
2663
|
roomDraftListeners.clear();
|
|
@@ -1832,9 +2665,8 @@ export function composeRuntime(options) {
|
|
|
1832
2665
|
}
|
|
1833
2666
|
async function stop() {
|
|
1834
2667
|
stopping = true;
|
|
1835
|
-
|
|
1836
|
-
await
|
|
1837
|
-
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
2668
|
+
await recovery.retire();
|
|
2669
|
+
await Promise.all(working);
|
|
1838
2670
|
await mutations;
|
|
1839
2671
|
close();
|
|
1840
2672
|
}
|
|
@@ -1856,14 +2688,54 @@ export function composeRuntime(options) {
|
|
|
1856
2688
|
return planOf(input, roomFacts(reach), planReach(reach));
|
|
1857
2689
|
}
|
|
1858
2690
|
function watchPlan(plan, deliver) {
|
|
1859
|
-
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);
|
|
1860
2696
|
}
|
|
1861
2697
|
async function searched(input, reach) {
|
|
1862
2698
|
const plan = searchPlanOf(input);
|
|
1863
|
-
const
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
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
|
+
}
|
|
1867
2739
|
}
|
|
1868
2740
|
const watch = (topic, input, deliver) => {
|
|
1869
2741
|
if (topic === 'room-drafts') {
|
|
@@ -1888,6 +2760,8 @@ export function composeRuntime(options) {
|
|
|
1888
2760
|
}
|
|
1889
2761
|
if (topic === 'conversations')
|
|
1890
2762
|
return conversations.watchList(deliver);
|
|
2763
|
+
if (topic === 'agents')
|
|
2764
|
+
return conversations.watchAgents(deliver);
|
|
1891
2765
|
if (topic === 'presentation') {
|
|
1892
2766
|
const listener = {
|
|
1893
2767
|
app: input[0],
|
|
@@ -1942,6 +2816,13 @@ export function composeRuntime(options) {
|
|
|
1942
2816
|
serviceModeListeners.delete(deliver);
|
|
1943
2817
|
};
|
|
1944
2818
|
}
|
|
2819
|
+
if (topic === 'member') {
|
|
2820
|
+
memberListeners.add(deliver);
|
|
2821
|
+
deliver(member);
|
|
2822
|
+
return () => {
|
|
2823
|
+
memberListeners.delete(deliver);
|
|
2824
|
+
};
|
|
2825
|
+
}
|
|
1945
2826
|
if (topic === 'queue') {
|
|
1946
2827
|
const queue = { locator: input[0], notify: deliver };
|
|
1947
2828
|
queueSubscriptions.add(queue);
|
|
@@ -1959,7 +2840,7 @@ export function composeRuntime(options) {
|
|
|
1959
2840
|
rooms() {
|
|
1960
2841
|
return listed();
|
|
1961
2842
|
},
|
|
1962
|
-
query: ((input) =>
|
|
2843
|
+
query: ((input) => rowsQuery(planned(input, null))),
|
|
1963
2844
|
search: (input) => searched(input, null),
|
|
1964
2845
|
types(locator) {
|
|
1965
2846
|
return {
|
|
@@ -1988,20 +2869,20 @@ export function composeRuntime(options) {
|
|
|
1988
2869
|
return exclusive(async () => {
|
|
1989
2870
|
const parts = readParts(input);
|
|
1990
2871
|
if (path === '' || normalize(path) !== path)
|
|
1991
|
-
throw
|
|
2872
|
+
throw runtimeError('room-draft-malformed');
|
|
1992
2873
|
const local = await openLocal();
|
|
1993
2874
|
const current = roomDrafts.get(room)?.get(path);
|
|
1994
2875
|
if ((current?.revision ?? null) !== revision) {
|
|
1995
|
-
return
|
|
1996
|
-
outcome: 'stale',
|
|
1997
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
1998
|
-
};
|
|
2876
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
1999
2877
|
}
|
|
2878
|
+
const file = parts.file === undefined
|
|
2879
|
+
? undefined
|
|
2880
|
+
: await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
|
|
2000
2881
|
const record = {
|
|
2001
2882
|
locator: room,
|
|
2002
2883
|
path,
|
|
2003
2884
|
revision: roomDraftRevision(),
|
|
2004
|
-
parts: held(current?.parts, parts),
|
|
2885
|
+
parts: held(current?.parts, parts, file),
|
|
2005
2886
|
returned: current?.returned ?? null,
|
|
2006
2887
|
};
|
|
2007
2888
|
await local.keepRoomDraft(record);
|
|
@@ -2015,10 +2896,7 @@ export function composeRuntime(options) {
|
|
|
2015
2896
|
const local = await openLocal();
|
|
2016
2897
|
const current = roomDrafts.get(room)?.get(path);
|
|
2017
2898
|
if (current === undefined || current.revision !== revision) {
|
|
2018
|
-
return
|
|
2019
|
-
outcome: 'stale',
|
|
2020
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
2021
|
-
};
|
|
2899
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2022
2900
|
}
|
|
2023
2901
|
await local.discardRoomDraft(room, path);
|
|
2024
2902
|
forgetRoomDraft(room, path);
|
|
@@ -2053,6 +2931,10 @@ export function composeRuntime(options) {
|
|
|
2053
2931
|
history(locator, path, query) {
|
|
2054
2932
|
return readHistory(locator, path, query);
|
|
2055
2933
|
},
|
|
2934
|
+
binaryFile: {
|
|
2935
|
+
read: readBinaryPath,
|
|
2936
|
+
readReference: readBinaryReference,
|
|
2937
|
+
},
|
|
2056
2938
|
call(name, input) {
|
|
2057
2939
|
return invokeAction(name, input);
|
|
2058
2940
|
},
|
|
@@ -2072,23 +2954,26 @@ export function composeRuntime(options) {
|
|
|
2072
2954
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2073
2955
|
return;
|
|
2074
2956
|
journal.record('connection reauthorize');
|
|
2075
|
-
await
|
|
2957
|
+
await Promise.all([...lanes.values()]
|
|
2958
|
+
.filter((held) => held.generation === generation)
|
|
2959
|
+
.map((held) => held.chain));
|
|
2076
2960
|
await mutations;
|
|
2077
2961
|
authorization = undefined;
|
|
2078
2962
|
discoveryPaused = false;
|
|
2079
2963
|
if (options.adapter.roomStream === undefined)
|
|
2080
2964
|
await sync('unstreamed');
|
|
2081
2965
|
else
|
|
2082
|
-
await task(ensureAuthorization);
|
|
2966
|
+
await task(OPENING_LANE, ensureAuthorization);
|
|
2083
2967
|
},
|
|
2084
2968
|
};
|
|
2085
2969
|
const reachable = {
|
|
2086
2970
|
md,
|
|
2087
2971
|
watch,
|
|
2972
|
+
referrerScope: (referrer) => options.adapter.referrerScope(referrer),
|
|
2088
2973
|
plan: planned,
|
|
2089
|
-
query: (plan) =>
|
|
2974
|
+
query: (plan) => rowsQuery(plan),
|
|
2090
2975
|
watchPlan,
|
|
2091
2976
|
search: searched,
|
|
2092
2977
|
};
|
|
2093
|
-
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2978
|
+
return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2094
2979
|
}
|