@agentshouse/mdruntime 0.1.0-c389df724fd2f → 0.1.0-c3b593308e82b
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 +271 -217
- package/README.md +13 -3
- package/dist/acquisition.d.ts +14 -0
- package/dist/acquisition.d.ts.map +1 -0
- package/dist/acquisition.js +24 -0
- package/dist/adapter.d.ts +78 -23
- package/dist/adapter.d.ts.map +1 -1
- package/dist/attempt.d.ts +16 -0
- package/dist/attempt.d.ts.map +1 -0
- package/dist/attempt.js +70 -0
- package/dist/binary.d.ts +23 -0
- package/dist/binary.d.ts.map +1 -0
- package/dist/binary.js +8 -0
- package/dist/bootstrap.d.ts +17 -5
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +113 -37
- package/dist/change-set.d.ts +2 -0
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +49 -16
- package/dist/change.d.ts +15 -0
- package/dist/change.d.ts.map +1 -1
- package/dist/change.js +26 -1
- package/dist/chat/acts.d.ts +33 -0
- package/dist/chat/acts.d.ts.map +1 -0
- package/dist/chat/acts.js +36 -0
- package/dist/chat/context.d.ts +3 -0
- package/dist/chat/context.d.ts.map +1 -0
- package/dist/chat/context.js +10 -0
- package/dist/chat/input.d.ts +9 -0
- package/dist/chat/input.d.ts.map +1 -0
- package/dist/chat/input.js +99 -0
- package/dist/chat/kit.d.ts +61 -0
- package/dist/chat/kit.d.ts.map +1 -0
- package/dist/chat/kit.js +123 -0
- package/dist/chat/lasting.d.ts +6 -0
- package/dist/chat/lasting.d.ts.map +1 -0
- package/dist/chat/lasting.js +19 -0
- package/dist/chat/list.d.ts +27 -0
- package/dist/chat/list.d.ts.map +1 -0
- package/dist/chat/list.js +63 -0
- package/dist/chat/markdown.d.ts +20 -0
- package/dist/chat/markdown.d.ts.map +1 -0
- package/dist/chat/markdown.js +188 -0
- package/dist/chat/message.d.ts +6 -0
- package/dist/chat/message.d.ts.map +1 -0
- package/dist/chat/message.js +192 -0
- package/dist/chat/messages.d.ts +22 -0
- package/dist/chat/messages.d.ts.map +1 -0
- package/dist/chat/messages.js +155 -0
- package/dist/chat/name.d.ts +6 -0
- package/dist/chat/name.d.ts.map +1 -0
- package/dist/chat/name.js +37 -0
- package/dist/chat/opened.d.ts +11 -0
- package/dist/chat/opened.d.ts.map +1 -0
- package/dist/chat/opened.js +17 -0
- package/dist/chat/overlay.d.ts +31 -0
- package/dist/chat/overlay.d.ts.map +1 -0
- package/dist/chat/overlay.js +97 -0
- package/dist/chat/parts.d.ts +57 -0
- package/dist/chat/parts.d.ts.map +1 -0
- package/dist/chat/parts.js +119 -0
- package/dist/chat/process.d.ts +20 -0
- package/dist/chat/process.d.ts.map +1 -0
- package/dist/chat/process.js +32 -0
- package/dist/chat/provider.d.ts +63 -0
- package/dist/chat/provider.d.ts.map +1 -0
- package/dist/chat/provider.js +629 -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 +34 -0
- package/dist/chat/ready.d.ts.map +1 -0
- package/dist/chat/ready.js +91 -0
- package/dist/chat/style.d.ts +4 -0
- package/dist/chat/style.d.ts.map +1 -0
- package/dist/chat/style.js +302 -0
- package/dist/chat.d.ts +14 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +8 -0
- package/dist/client.d.ts +23 -12
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +33 -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 +41 -15
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +1612 -378
- package/dist/conversation.d.ts +25 -10
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +374 -143
- package/dist/durability.d.ts +7 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/entry.d.ts +17 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +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 +14 -3
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +60 -18
- 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 +14 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/memory.d.ts +6 -3
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +76 -22
- package/dist/model.d.ts +7 -4
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +10 -1
- 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 +16 -3
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +118 -77
- package/dist/query.d.ts +36 -3
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +6 -1
- package/dist/reach.d.ts +8 -3
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +60 -24
- package/dist/react.d.ts +9 -5
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +3 -2
- package/dist/read-side.d.ts +13 -6
- package/dist/read-side.d.ts.map +1 -1
- package/dist/read-side.js +294 -85
- package/dist/readiness.d.ts +53 -0
- package/dist/readiness.d.ts.map +1 -0
- package/dist/readiness.js +33 -0
- package/dist/recovery.d.ts +25 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +197 -0
- package/dist/refresh.d.ts +10 -0
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +49 -38
- package/dist/replica.d.ts +8 -5
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +123 -67
- package/dist/room-draft.d.ts +26 -9
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +132 -23
- package/dist/rows.d.ts +9 -13
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +14 -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 +37 -8
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +116 -45
- package/dist/shared.d.ts +11 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +368 -0
- package/dist/status.d.ts +6 -2
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +0 -8
- package/dist/store.d.ts +73 -19
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +528 -200
- 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 +140 -33
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +60 -31
- package/package.json +26 -14
- package/dist/attachment.d.ts +0 -3
- package/dist/attachment.d.ts.map +0 -1
- package/dist/attachment.js +0 -8
package/dist/connection.js
CHANGED
|
@@ -3,19 +3,30 @@ 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 { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
|
|
25
|
+
import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
|
|
26
|
+
import { FencedMap } from './fence.js';
|
|
27
|
+
import { tally } from './shared.js';
|
|
28
|
+
import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
|
|
29
|
+
import { identityTime, roomDraftRevision } from './identity.js';
|
|
19
30
|
import { beneath, normalize } from './paths.js';
|
|
20
31
|
import { openRuntimeStore, } from './store.js';
|
|
21
32
|
import { unconfirmed } from './transfer.js';
|
|
@@ -33,20 +44,59 @@ function declaredSelection(room) {
|
|
|
33
44
|
root !== '' &&
|
|
34
45
|
selection.roots.every((other, at) => at === index || !within(other, root)));
|
|
35
46
|
}
|
|
47
|
+
const OPENING_LANE = 'opening';
|
|
48
|
+
const CONVERSATION_LANE = 'conversations';
|
|
49
|
+
const ROOM_READS = 256;
|
|
50
|
+
const ROOM_ATTEMPTS = 16;
|
|
51
|
+
const outside = new Map();
|
|
52
|
+
function outsideOf(roots) {
|
|
53
|
+
const key = JSON.stringify(roots);
|
|
54
|
+
const held = outside.get(key);
|
|
55
|
+
if (held !== undefined)
|
|
56
|
+
return held;
|
|
57
|
+
const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
|
|
58
|
+
const made = {
|
|
59
|
+
documents: (path) => (markdownPath(path) ? counted(path) : 0),
|
|
60
|
+
binaries: counted,
|
|
61
|
+
};
|
|
62
|
+
outside.set(key, made);
|
|
63
|
+
return made;
|
|
64
|
+
}
|
|
65
|
+
function abandonedOn(signal) {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
if (signal.aborted)
|
|
68
|
+
resolve(null);
|
|
69
|
+
else
|
|
70
|
+
signal.addEventListener('abort', () => resolve(null), { once: true });
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function roomLane(locator) {
|
|
74
|
+
return `room ${locator}`;
|
|
75
|
+
}
|
|
36
76
|
export function composeRuntime(options) {
|
|
77
|
+
const localSaves = options.adapter.localSaves === true;
|
|
37
78
|
let authorization = options.authorization;
|
|
38
79
|
let opening;
|
|
39
80
|
let store;
|
|
40
|
-
const
|
|
81
|
+
const fences = new Map();
|
|
82
|
+
const isFenced = (locator) => fences.has(locator);
|
|
83
|
+
let fencings = 0;
|
|
84
|
+
const lastFenced = new Map();
|
|
85
|
+
const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
|
|
86
|
+
let discoveries = 0;
|
|
87
|
+
const replicas = new FencedMap(isFenced);
|
|
88
|
+
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
41
89
|
const pendings = new Map();
|
|
42
|
-
const roomDrafts = new
|
|
90
|
+
const roomDrafts = new FencedMap(isFenced);
|
|
43
91
|
const presentations = new Map();
|
|
44
92
|
let limits = [];
|
|
45
93
|
let capabilities = [];
|
|
46
|
-
const rooms = new
|
|
94
|
+
const rooms = new FencedMap(isFenced);
|
|
95
|
+
const counted = new Map();
|
|
47
96
|
const unreadable = new Set();
|
|
48
|
-
const
|
|
49
|
-
const
|
|
97
|
+
const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
|
|
98
|
+
const models = new FencedMap(isModelFenced);
|
|
99
|
+
const liveModels = new FencedMap(isModelFenced);
|
|
50
100
|
const invalidModels = new Set();
|
|
51
101
|
const discovered = new Map();
|
|
52
102
|
let roomsListed = false;
|
|
@@ -76,19 +126,28 @@ export function composeRuntime(options) {
|
|
|
76
126
|
let status = STARTING;
|
|
77
127
|
let shown = STARTING;
|
|
78
128
|
let discoveryPaused = false;
|
|
79
|
-
let resetTimer;
|
|
80
129
|
let closed = false;
|
|
81
130
|
let stopping = false;
|
|
82
131
|
let withdrawStream;
|
|
83
132
|
const announced = new Map();
|
|
84
133
|
let underway = null;
|
|
134
|
+
let streamed = null;
|
|
85
135
|
let handed = false;
|
|
86
|
-
const
|
|
136
|
+
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
137
|
+
const repairing = new Set();
|
|
138
|
+
const repairable = new Map();
|
|
139
|
+
let outcomeRetention = null;
|
|
87
140
|
const answering = new Map();
|
|
88
141
|
let complete = false;
|
|
89
142
|
let recorded = false;
|
|
90
143
|
const stale = new Set();
|
|
91
|
-
let
|
|
144
|
+
let generation = 0;
|
|
145
|
+
let discovering = null;
|
|
146
|
+
let opened = null;
|
|
147
|
+
const owed = new Set();
|
|
148
|
+
const lanes = new Map();
|
|
149
|
+
let roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
150
|
+
const working = new Set();
|
|
92
151
|
let mutations = Promise.resolve();
|
|
93
152
|
const roomListeners = new Set();
|
|
94
153
|
const refusalListeners = new Set();
|
|
@@ -99,7 +158,7 @@ export function composeRuntime(options) {
|
|
|
99
158
|
const presentationListeners = new Set();
|
|
100
159
|
const readSide = createReadSide();
|
|
101
160
|
let withdrawConversations;
|
|
102
|
-
let currencyQueued =
|
|
161
|
+
let currencyQueued = null;
|
|
103
162
|
let socketOpen = false;
|
|
104
163
|
let serviceMode = 'open';
|
|
105
164
|
let haltedRefusal = false;
|
|
@@ -107,21 +166,41 @@ export function composeRuntime(options) {
|
|
|
107
166
|
const serviceModeListeners = new Set();
|
|
108
167
|
let member = null;
|
|
109
168
|
const memberListeners = new Set();
|
|
169
|
+
const prepared = new Set();
|
|
170
|
+
const projecting = new Set();
|
|
171
|
+
const unpreparedDemand = new Set();
|
|
172
|
+
const unindexable = new Set();
|
|
173
|
+
let preparing = false;
|
|
174
|
+
let demand = WHOLE_DEMAND;
|
|
175
|
+
let narrowing = null;
|
|
176
|
+
let targets = {};
|
|
177
|
+
let persistence = options.shell.persistence;
|
|
178
|
+
let capacity = null;
|
|
179
|
+
const paused = new Set();
|
|
180
|
+
let rechecking = null;
|
|
181
|
+
const readinessWatches = new Set();
|
|
182
|
+
let readinessQueued = false;
|
|
183
|
+
const catching = new Set();
|
|
184
|
+
const transfers = new FencedMap(isFenced);
|
|
110
185
|
const conversations = createConversationStore({
|
|
111
186
|
agents: declaredAgents,
|
|
112
|
-
documents: (locator) =>
|
|
187
|
+
documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
|
|
113
188
|
call: (name, input) => invokeAction(name, input),
|
|
114
189
|
durably,
|
|
115
190
|
stream: () => options.adapter.conversations,
|
|
116
191
|
publish: publishChangeSet,
|
|
192
|
+
online: () => !status.providerUnreachable && refusal === null,
|
|
117
193
|
closed: () => closed,
|
|
194
|
+
unanswered: resendLater,
|
|
195
|
+
expired: (operation) => pastHorizon(operation),
|
|
118
196
|
});
|
|
119
197
|
const searchIndex = createSearchIndex({
|
|
120
|
-
|
|
198
|
+
room: (room) => readSide.room(room),
|
|
121
199
|
document: (room, path) => readSide.document(room, path),
|
|
122
|
-
messages: () => conversations.messages(),
|
|
123
|
-
message: (conversation, identity) => conversations.message(conversation, identity),
|
|
124
200
|
});
|
|
201
|
+
const roomsToIndex = new Set();
|
|
202
|
+
const conversationsToIndex = new Set();
|
|
203
|
+
let indexing = false;
|
|
125
204
|
const journal = createLog();
|
|
126
205
|
let notifyReady = () => { };
|
|
127
206
|
const ready = new Promise((resolve) => {
|
|
@@ -185,7 +264,9 @@ export function composeRuntime(options) {
|
|
|
185
264
|
}
|
|
186
265
|
}
|
|
187
266
|
function pending() {
|
|
188
|
-
return
|
|
267
|
+
return (!localSaves &&
|
|
268
|
+
(queuesWithoutReplica.size > 0 ||
|
|
269
|
+
[...replicas.values()].some((replica) => replica.queue.length > 0)));
|
|
189
270
|
}
|
|
190
271
|
function attention() {
|
|
191
272
|
const attentionRequired = [];
|
|
@@ -198,9 +279,16 @@ export function composeRuntime(options) {
|
|
|
198
279
|
returned: structuredClone(record.returned),
|
|
199
280
|
});
|
|
200
281
|
}
|
|
282
|
+
for (const [room, refusal] of sourceRefusals)
|
|
283
|
+
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
201
284
|
attentionRequired.sort((left, right) => {
|
|
202
285
|
if (left.room !== right.room)
|
|
203
286
|
return left.room < right.room ? -1 : 1;
|
|
287
|
+
if ('refusal' in left || 'refusal' in right) {
|
|
288
|
+
if ('refusal' in left && 'refusal' in right)
|
|
289
|
+
return 0;
|
|
290
|
+
return 'refusal' in left ? -1 : 1;
|
|
291
|
+
}
|
|
204
292
|
if (left.path !== right.path)
|
|
205
293
|
return left.path < right.path ? -1 : 1;
|
|
206
294
|
return 0;
|
|
@@ -214,8 +302,21 @@ export function composeRuntime(options) {
|
|
|
214
302
|
}
|
|
215
303
|
return { attentionRequired, reset };
|
|
216
304
|
}
|
|
305
|
+
function documentsIn(locator) {
|
|
306
|
+
const replica = replicas.get(locator);
|
|
307
|
+
if (replica === undefined)
|
|
308
|
+
return 0;
|
|
309
|
+
const roots = replica.confirmed.record.typedSelection?.roots ?? [];
|
|
310
|
+
const weighed = outsideOf(roots);
|
|
311
|
+
return (tally(replica.visible.documents, weighed.documents) +
|
|
312
|
+
tally(replica.visible.binaries, weighed.binaries));
|
|
313
|
+
}
|
|
217
314
|
function listed() {
|
|
218
|
-
return structuredClone([...rooms.values()])
|
|
315
|
+
return structuredClone([...rooms.values()].map((room) => {
|
|
316
|
+
const documents = documentsIn(room.locator);
|
|
317
|
+
counted.set(room.locator, documents);
|
|
318
|
+
return { ...room, documents };
|
|
319
|
+
}));
|
|
219
320
|
}
|
|
220
321
|
function roomFacts(reach = null) {
|
|
221
322
|
const held = [...rooms.values()]
|
|
@@ -235,15 +336,19 @@ export function composeRuntime(options) {
|
|
|
235
336
|
return { listed: roomsListed, rooms: held };
|
|
236
337
|
}
|
|
237
338
|
function emitRooms() {
|
|
339
|
+
readSide.recover();
|
|
340
|
+
queueReadiness();
|
|
238
341
|
const value = listed();
|
|
239
342
|
for (const listener of roomListeners)
|
|
240
343
|
listener(value);
|
|
241
344
|
}
|
|
242
345
|
function emitRefusal() {
|
|
346
|
+
queueReadiness();
|
|
243
347
|
for (const listener of refusalListeners)
|
|
244
348
|
listener(refusal);
|
|
245
349
|
}
|
|
246
350
|
function emitStorageLoss() {
|
|
351
|
+
queueReadiness();
|
|
247
352
|
for (const listener of storageLossListeners)
|
|
248
353
|
listener(storageLoss);
|
|
249
354
|
}
|
|
@@ -260,7 +365,8 @@ export function composeRuntime(options) {
|
|
|
260
365
|
function show() {
|
|
261
366
|
if (closed)
|
|
262
367
|
return;
|
|
263
|
-
|
|
368
|
+
queueReadiness();
|
|
369
|
+
const next = status.state === 'synced' && demand.conversations && !conversations.current()
|
|
264
370
|
? { ...status, state: 'syncing' }
|
|
265
371
|
: status;
|
|
266
372
|
if (same(shown, next))
|
|
@@ -270,6 +376,8 @@ export function composeRuntime(options) {
|
|
|
270
376
|
listener(shown);
|
|
271
377
|
}
|
|
272
378
|
function entryOf(batch) {
|
|
379
|
+
if (localSaves)
|
|
380
|
+
return Object.freeze({ batch, state: 'local' });
|
|
273
381
|
const state = pendings.get(batch.id);
|
|
274
382
|
if (state?.accepted !== undefined) {
|
|
275
383
|
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
@@ -277,10 +385,7 @@ export function composeRuntime(options) {
|
|
|
277
385
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
278
386
|
}
|
|
279
387
|
function entriesOf(locator) {
|
|
280
|
-
|
|
281
|
-
if (replica === undefined)
|
|
282
|
-
return [];
|
|
283
|
-
return replica.queue.map(entryOf);
|
|
388
|
+
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
|
|
284
389
|
}
|
|
285
390
|
function emitQueue(locator) {
|
|
286
391
|
const entries = entriesOf(locator);
|
|
@@ -290,12 +395,477 @@ export function composeRuntime(options) {
|
|
|
290
395
|
}
|
|
291
396
|
}
|
|
292
397
|
function applyChangeSet(set) {
|
|
293
|
-
|
|
294
|
-
|
|
398
|
+
const held = {
|
|
399
|
+
...set,
|
|
400
|
+
documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
|
|
401
|
+
};
|
|
402
|
+
readSide.apply(held);
|
|
403
|
+
searchIndex.changed(held);
|
|
404
|
+
}
|
|
405
|
+
async function referenced(locator) {
|
|
406
|
+
for (;;) {
|
|
407
|
+
const record = replicas.get(locator)?.confirmed.record;
|
|
408
|
+
if (record === undefined || closed)
|
|
409
|
+
return;
|
|
410
|
+
const held = { text: record.documents, binaries: record.binaries };
|
|
411
|
+
const composition = compositionOf(record);
|
|
412
|
+
if (referencesIndexed(held, composition))
|
|
413
|
+
return;
|
|
414
|
+
await indexReferrers(held, composition);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
async function prepareReplica(locator) {
|
|
418
|
+
await referenced(locator);
|
|
419
|
+
const replica = replicas.get(locator);
|
|
420
|
+
if (replica === undefined || closed || prepared.has(locator))
|
|
421
|
+
return;
|
|
422
|
+
const visible = replica.visible;
|
|
423
|
+
const paths = [
|
|
424
|
+
...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
|
|
425
|
+
];
|
|
426
|
+
const entered = [];
|
|
427
|
+
projecting.add(locator);
|
|
428
|
+
try {
|
|
429
|
+
await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
|
|
430
|
+
const current = replicas.get(locator);
|
|
431
|
+
if (current === undefined || closed)
|
|
432
|
+
return;
|
|
433
|
+
const set = entering(locator, current, chunk);
|
|
434
|
+
readSide.apply(set);
|
|
435
|
+
entered.push(set);
|
|
436
|
+
});
|
|
437
|
+
if (!replicas.has(locator) || closed)
|
|
438
|
+
return;
|
|
439
|
+
prepared.add(locator);
|
|
440
|
+
}
|
|
441
|
+
finally {
|
|
442
|
+
projecting.delete(locator);
|
|
443
|
+
}
|
|
444
|
+
conversations.changed(mergeChangeSets(entered));
|
|
445
|
+
readSide.recover();
|
|
446
|
+
queueReadiness();
|
|
447
|
+
}
|
|
448
|
+
function unprepared() {
|
|
449
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
|
|
450
|
+
}
|
|
451
|
+
function unbuilt(locator, error) {
|
|
452
|
+
unindexable.add(locator);
|
|
453
|
+
journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
|
|
454
|
+
queueReadiness();
|
|
455
|
+
}
|
|
456
|
+
function prepare(locators = []) {
|
|
457
|
+
for (const locator of locators)
|
|
458
|
+
unpreparedDemand.add(locator);
|
|
459
|
+
if (preparing)
|
|
460
|
+
return;
|
|
461
|
+
preparing = true;
|
|
462
|
+
void (async () => {
|
|
463
|
+
try {
|
|
464
|
+
for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
|
|
465
|
+
await yielded();
|
|
466
|
+
if (closed)
|
|
467
|
+
return;
|
|
468
|
+
unpreparedDemand.delete(waiting);
|
|
469
|
+
await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
finally {
|
|
473
|
+
preparing = false;
|
|
474
|
+
}
|
|
475
|
+
})();
|
|
476
|
+
}
|
|
477
|
+
function reachedRooms(plan) {
|
|
478
|
+
const reach = plan.reach;
|
|
479
|
+
const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
480
|
+
const named = plan.rooms ??
|
|
481
|
+
(reached === null
|
|
482
|
+
? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
|
|
483
|
+
: [...reached]);
|
|
484
|
+
return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
|
|
485
|
+
}
|
|
486
|
+
function awaited(locator) {
|
|
487
|
+
return rooms.has(locator) || !roomsListed;
|
|
488
|
+
}
|
|
489
|
+
function covered(plan) {
|
|
490
|
+
const locators = reachedRooms(plan);
|
|
491
|
+
if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
|
|
492
|
+
return { pending: [] };
|
|
493
|
+
}
|
|
494
|
+
const pending = [];
|
|
495
|
+
const coverage = [];
|
|
496
|
+
const overlay = [];
|
|
497
|
+
for (const locator of locators) {
|
|
498
|
+
const replica = replicas.get(locator);
|
|
499
|
+
if (replica === undefined) {
|
|
500
|
+
if (awaited(locator))
|
|
501
|
+
pending.push({ room: locator, awaits: 'acquisition' });
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (!prepared.has(locator)) {
|
|
505
|
+
pending.push({ room: locator, awaits: 'preparation' });
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
coverage.push({
|
|
509
|
+
room: locator,
|
|
510
|
+
generation: replica.confirmed.record.transfer,
|
|
511
|
+
current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
|
|
512
|
+
});
|
|
513
|
+
for (const batch of replica.queue)
|
|
514
|
+
overlay.push(batch.id);
|
|
515
|
+
}
|
|
516
|
+
if (pending.length > 0)
|
|
517
|
+
return { pending };
|
|
518
|
+
return { coverage: { rooms: coverage, indexes: [], overlay } };
|
|
519
|
+
}
|
|
520
|
+
function awaitPreparation(cover) {
|
|
521
|
+
if (!('pending' in cover))
|
|
522
|
+
return;
|
|
523
|
+
const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
|
|
524
|
+
if (waiting.length > 0)
|
|
525
|
+
prepare(waiting.map(({ room }) => room));
|
|
526
|
+
}
|
|
527
|
+
function acquires(room) {
|
|
528
|
+
return (demanded(demand, room) ||
|
|
529
|
+
(replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
|
|
530
|
+
queuesWithoutReplica.has(room.locator));
|
|
531
|
+
}
|
|
532
|
+
function acquiredLocators() {
|
|
533
|
+
return [...discovered.values()]
|
|
534
|
+
.filter(acquires)
|
|
535
|
+
.map((room) => room.locator)
|
|
536
|
+
.sort();
|
|
537
|
+
}
|
|
538
|
+
function heldDemand(acquired) {
|
|
539
|
+
if (demand.whole)
|
|
540
|
+
return null;
|
|
541
|
+
return { rooms: acquired.map((room) => room.locator), conversations: demand.conversations };
|
|
542
|
+
}
|
|
543
|
+
function applyDemand(next) {
|
|
544
|
+
const before = acquiredLocators();
|
|
545
|
+
const conversed = demand.conversations;
|
|
546
|
+
demand = next;
|
|
547
|
+
const after = acquiredLocators();
|
|
548
|
+
queueReadiness();
|
|
549
|
+
if (!conversed && next.conversations)
|
|
550
|
+
scheduleCurrency();
|
|
551
|
+
if (JSON.stringify(before) === JSON.stringify(after) && conversed === next.conversations)
|
|
552
|
+
return;
|
|
553
|
+
journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
|
|
554
|
+
if (options.adapter.roomStream === undefined)
|
|
555
|
+
void sync('unstreamed');
|
|
556
|
+
else if (socketOpen)
|
|
557
|
+
options.adapter.roomStream.reopen();
|
|
558
|
+
}
|
|
559
|
+
function acquire(next) {
|
|
560
|
+
if (closed)
|
|
561
|
+
return;
|
|
562
|
+
if (narrowing !== null)
|
|
563
|
+
clearTimeout(narrowing);
|
|
564
|
+
narrowing = null;
|
|
565
|
+
const widening = wider(next, demand);
|
|
566
|
+
if (!widening && !narrower(next, demand))
|
|
567
|
+
return;
|
|
568
|
+
if (widening || !roomsListed) {
|
|
569
|
+
applyDemand(next);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
// A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
|
|
573
|
+
narrowing = setTimeout(() => {
|
|
574
|
+
narrowing = null;
|
|
575
|
+
applyDemand(next);
|
|
576
|
+
}, 5_000);
|
|
577
|
+
}
|
|
578
|
+
function requiredRooms(reach) {
|
|
579
|
+
if (reach === null)
|
|
580
|
+
return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
|
|
581
|
+
const named = [...new Set(reach.rooms)].sort();
|
|
582
|
+
if (roomsListed)
|
|
583
|
+
return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
|
|
584
|
+
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
585
|
+
}
|
|
586
|
+
function roomState(locator, search) {
|
|
587
|
+
if (sourceRefusals.has(locator))
|
|
588
|
+
return 'refused';
|
|
589
|
+
const listedRoom = discovered.get(locator);
|
|
590
|
+
if (roomsListed &&
|
|
591
|
+
(listedRoom === undefined ||
|
|
592
|
+
listedRoom.replica.admission !== 'admitted' ||
|
|
593
|
+
unreadable.has(locator)))
|
|
594
|
+
return 'unadmitted';
|
|
595
|
+
if (!replicas.has(locator))
|
|
596
|
+
return 'acquiring';
|
|
597
|
+
if (!prepared.has(locator))
|
|
598
|
+
return 'preparing';
|
|
599
|
+
if (search && !searchIndex.rooms().has(locator))
|
|
600
|
+
return 'indexing';
|
|
601
|
+
return 'complete';
|
|
602
|
+
}
|
|
603
|
+
function conversationCoverage(reach, search) {
|
|
604
|
+
const declared = reach === null || reach.conversations;
|
|
605
|
+
if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
|
|
606
|
+
return 'undeclared';
|
|
607
|
+
if (!conversations.known() || !conversations.current())
|
|
608
|
+
return 'acquiring';
|
|
609
|
+
if (!search)
|
|
610
|
+
return 'complete';
|
|
611
|
+
const unindexed = conversations
|
|
612
|
+
.searchable()
|
|
613
|
+
.filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
|
|
614
|
+
if (unindexed.length === 0)
|
|
615
|
+
return 'complete';
|
|
616
|
+
index([], unindexed);
|
|
617
|
+
return 'indexing';
|
|
618
|
+
}
|
|
619
|
+
function connectionOf() {
|
|
620
|
+
if (socketOpen)
|
|
621
|
+
return 'connected';
|
|
622
|
+
if (status.providerUnreachable)
|
|
623
|
+
return 'unreachable';
|
|
624
|
+
return options.adapter.roomStream === undefined && status.lastCompleted !== null
|
|
625
|
+
? 'connected'
|
|
626
|
+
: 'connecting';
|
|
627
|
+
}
|
|
628
|
+
function refusedIn(locators) {
|
|
629
|
+
return locators.flatMap((room) => {
|
|
630
|
+
const held = sourceRefusals.get(room);
|
|
631
|
+
if (held === undefined)
|
|
632
|
+
return [];
|
|
633
|
+
const { file, location } = held.fields;
|
|
634
|
+
return [
|
|
635
|
+
{
|
|
636
|
+
room,
|
|
637
|
+
file: typeof file === 'string' ? file : '',
|
|
638
|
+
rule: held.code,
|
|
639
|
+
location: typeof location === 'string' ? location : null,
|
|
640
|
+
},
|
|
641
|
+
];
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
function readinessFor(port, reach) {
|
|
645
|
+
const required = requiredRooms(reach);
|
|
646
|
+
const search = port.search === 'required';
|
|
647
|
+
const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
|
|
648
|
+
const building = (state) => coverage
|
|
649
|
+
.filter((room) => room.state === state && !unindexable.has(room.room))
|
|
650
|
+
.map(({ room }) => room);
|
|
651
|
+
const preparing = building('preparing');
|
|
652
|
+
if (preparing.length > 0)
|
|
653
|
+
prepare(preparing);
|
|
654
|
+
const indexing = building('indexing');
|
|
655
|
+
if (indexing.length > 0)
|
|
656
|
+
index(indexing, []);
|
|
657
|
+
return readinessOf({
|
|
658
|
+
code: port.code,
|
|
659
|
+
coverage: {
|
|
660
|
+
listed: required.listed,
|
|
661
|
+
rooms: coverage,
|
|
662
|
+
conversations: conversationCoverage(reach, search),
|
|
663
|
+
search: search ? 'required' : 'unused',
|
|
664
|
+
},
|
|
665
|
+
persistence,
|
|
666
|
+
connection: connectionOf(),
|
|
667
|
+
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
668
|
+
capacity: capacity !== null &&
|
|
669
|
+
coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
|
|
670
|
+
? capacity
|
|
671
|
+
: port.capacity,
|
|
672
|
+
refused: refusedIn(required.rooms),
|
|
673
|
+
lost: storageLoss !== null,
|
|
674
|
+
established: targets[port.target]?.established === true,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
function establish(name) {
|
|
678
|
+
targets = { ...targets, [name]: { established: true } };
|
|
679
|
+
queueReadiness();
|
|
680
|
+
void durably((local) => local.keepTargets(targets)).catch(() => {
|
|
681
|
+
journal.record(`target record failed ${name}`);
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
function deliverReadiness(watch) {
|
|
685
|
+
if (!readinessWatches.has(watch))
|
|
686
|
+
return;
|
|
687
|
+
const readiness = readinessFor(watch.port, watch.reach);
|
|
688
|
+
if (readiness.ready && targets[watch.port.target]?.established !== true) {
|
|
689
|
+
establish(watch.port.target);
|
|
690
|
+
}
|
|
691
|
+
const shown = JSON.stringify(readiness);
|
|
692
|
+
if (shown === watch.last)
|
|
693
|
+
return;
|
|
694
|
+
watch.last = shown;
|
|
695
|
+
watch.deliver(structuredClone(readiness));
|
|
696
|
+
}
|
|
697
|
+
function queueReadiness() {
|
|
698
|
+
if (readinessQueued || closed || readinessWatches.size === 0)
|
|
699
|
+
return;
|
|
700
|
+
readinessQueued = true;
|
|
701
|
+
queueMicrotask(() => {
|
|
702
|
+
readinessQueued = false;
|
|
703
|
+
if (closed)
|
|
704
|
+
return;
|
|
705
|
+
for (const watch of [...readinessWatches])
|
|
706
|
+
deliverReadiness(watch);
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
function watchReadiness(port, reach, deliver) {
|
|
710
|
+
const watch = { port, reach, deliver, last: null };
|
|
711
|
+
readinessWatches.add(watch);
|
|
712
|
+
void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
|
|
713
|
+
return () => {
|
|
714
|
+
readinessWatches.delete(watch);
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
function layerBytes(layer) {
|
|
718
|
+
let bytes = 0;
|
|
719
|
+
for (const [path, text] of Object.entries(layer.documents))
|
|
720
|
+
bytes += (path.length + text.length) * 2;
|
|
721
|
+
return bytes;
|
|
722
|
+
}
|
|
723
|
+
function grownBy(next, previous) {
|
|
724
|
+
if (previous === undefined)
|
|
725
|
+
return layerBytes(next);
|
|
726
|
+
const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
|
|
727
|
+
return layerBytes({ ...next, documents });
|
|
728
|
+
}
|
|
729
|
+
function exceeded(error) {
|
|
730
|
+
return (typeof error === 'object' &&
|
|
731
|
+
error !== null &&
|
|
732
|
+
error.name === 'QuotaExceededError');
|
|
733
|
+
}
|
|
734
|
+
async function estimated() {
|
|
735
|
+
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
736
|
+
return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
|
|
737
|
+
}
|
|
738
|
+
function pause(locator, cause) {
|
|
739
|
+
paused.add(locator);
|
|
740
|
+
capacity = cause;
|
|
741
|
+
journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
|
|
742
|
+
queueReadiness();
|
|
743
|
+
if (rechecking !== null)
|
|
744
|
+
return;
|
|
745
|
+
// A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
|
|
746
|
+
rechecking = setInterval(() => {
|
|
747
|
+
void recheck();
|
|
748
|
+
}, 60_000);
|
|
749
|
+
}
|
|
750
|
+
async function recheck() {
|
|
751
|
+
const held = capacity;
|
|
752
|
+
if (held === null || closed)
|
|
753
|
+
return;
|
|
754
|
+
const available = await estimated().catch(() => null);
|
|
755
|
+
if (available === null || available < (held.needed ?? 0))
|
|
756
|
+
return;
|
|
757
|
+
if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
|
|
758
|
+
return;
|
|
759
|
+
resume();
|
|
760
|
+
}
|
|
761
|
+
function resume() {
|
|
762
|
+
if (rechecking !== null)
|
|
763
|
+
clearInterval(rechecking);
|
|
764
|
+
rechecking = null;
|
|
765
|
+
if (paused.size === 0 && capacity === null)
|
|
766
|
+
return;
|
|
767
|
+
paused.clear();
|
|
768
|
+
capacity = null;
|
|
769
|
+
journal.record('capacity resumed');
|
|
770
|
+
queueReadiness();
|
|
771
|
+
if (!closed && authorization !== undefined)
|
|
772
|
+
void sync('unstreamed');
|
|
773
|
+
}
|
|
774
|
+
async function reclaimed(local) {
|
|
775
|
+
const unreferenced = [...transfers.keys()].filter((locator) => {
|
|
776
|
+
const room = discovered.get(locator);
|
|
777
|
+
return room === undefined || !acquires(room);
|
|
778
|
+
});
|
|
779
|
+
for (const locator of unreferenced) {
|
|
780
|
+
await local.abandon(locator);
|
|
781
|
+
transfers.delete(locator);
|
|
782
|
+
}
|
|
783
|
+
if (unreferenced.length > 0)
|
|
784
|
+
journal.record(`capacity reclaimed ${unreferenced.length}`);
|
|
785
|
+
return unreferenced.length > 0;
|
|
786
|
+
}
|
|
787
|
+
async function admitted(local, locator, needed) {
|
|
788
|
+
if (paused.has(locator))
|
|
789
|
+
return false;
|
|
790
|
+
let available = await estimated().catch(() => null);
|
|
791
|
+
if (available === null || available >= needed)
|
|
792
|
+
return true;
|
|
793
|
+
if (await reclaimed(local))
|
|
794
|
+
available = await estimated().catch(() => null);
|
|
795
|
+
if (available === null || available >= needed)
|
|
796
|
+
return true;
|
|
797
|
+
pause(locator, { cause: 'insufficient-space', needed, available });
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
async function exceededOn(locator, bytes) {
|
|
801
|
+
const available = await estimated().catch(() => null);
|
|
802
|
+
pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
|
|
803
|
+
}
|
|
804
|
+
async function rowsQuery(plan) {
|
|
805
|
+
const before = covered(plan);
|
|
806
|
+
awaitPreparation(before);
|
|
807
|
+
if ('pending' in before)
|
|
808
|
+
return before;
|
|
809
|
+
const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
|
|
810
|
+
awaitPreparation(cover);
|
|
811
|
+
if ('pending' in cover)
|
|
812
|
+
return cover;
|
|
813
|
+
return 'groups' in rows
|
|
814
|
+
? { groups: rows.groups, coverage: cover.coverage }
|
|
815
|
+
: { documents: rows.documents, coverage: cover.coverage };
|
|
816
|
+
}
|
|
817
|
+
async function indexConversation(conversation) {
|
|
818
|
+
searchIndex.indexMessages(conversation, [], false);
|
|
819
|
+
let after = null;
|
|
820
|
+
for (;;) {
|
|
821
|
+
const batch = await durably((local) => local.messagesAfter(conversation, after));
|
|
822
|
+
if (closed)
|
|
823
|
+
return;
|
|
824
|
+
searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
|
|
825
|
+
if (batch.length === 0)
|
|
826
|
+
return;
|
|
827
|
+
after = batch[batch.length - 1].identity;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
function indexNext() {
|
|
831
|
+
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
|
|
832
|
+
if (room !== undefined) {
|
|
833
|
+
roomsToIndex.delete(room);
|
|
834
|
+
return () => prepared.has(room)
|
|
835
|
+
? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
|
|
836
|
+
: undefined;
|
|
837
|
+
}
|
|
838
|
+
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
839
|
+
if (conversation === undefined)
|
|
840
|
+
return null;
|
|
841
|
+
conversationsToIndex.delete(conversation);
|
|
842
|
+
return () => indexConversation(conversation);
|
|
843
|
+
}
|
|
844
|
+
function index(rooms, named) {
|
|
845
|
+
for (const room of rooms)
|
|
846
|
+
roomsToIndex.add(room);
|
|
847
|
+
for (const conversation of named)
|
|
848
|
+
conversationsToIndex.add(conversation);
|
|
849
|
+
if (indexing)
|
|
850
|
+
return;
|
|
851
|
+
indexing = true;
|
|
852
|
+
void (async () => {
|
|
853
|
+
try {
|
|
854
|
+
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
855
|
+
await yielded();
|
|
856
|
+
await unit();
|
|
857
|
+
queueReadiness();
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
finally {
|
|
861
|
+
indexing = false;
|
|
862
|
+
}
|
|
863
|
+
})().catch(() => { });
|
|
295
864
|
}
|
|
296
865
|
function publishChangeSet(set) {
|
|
297
866
|
applyChangeSet(set);
|
|
298
|
-
if (set.rooms.length > 0
|
|
867
|
+
if (set.rooms.length > 0 ||
|
|
868
|
+
[...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
|
|
299
869
|
emitRooms();
|
|
300
870
|
for (const locator of set.queues)
|
|
301
871
|
emitQueue(locator);
|
|
@@ -307,34 +877,7 @@ export function composeRuntime(options) {
|
|
|
307
877
|
emitPresentation(app);
|
|
308
878
|
conversations.changed(set);
|
|
309
879
|
}
|
|
310
|
-
function stopTimer() {
|
|
311
|
-
if (resetTimer === undefined)
|
|
312
|
-
return;
|
|
313
|
-
clearTimeout(resetTimer);
|
|
314
|
-
resetTimer = undefined;
|
|
315
|
-
}
|
|
316
|
-
function armTimer() {
|
|
317
|
-
stopTimer();
|
|
318
|
-
if (closed || stopping)
|
|
319
|
-
return;
|
|
320
|
-
let earliest = null;
|
|
321
|
-
for (const { submission } of pendings.values()) {
|
|
322
|
-
if (submission.state !== 'reset')
|
|
323
|
-
continue;
|
|
324
|
-
const at = Date.parse(submission.reset);
|
|
325
|
-
if (earliest === null || at < earliest)
|
|
326
|
-
earliest = at;
|
|
327
|
-
}
|
|
328
|
-
if (earliest === null)
|
|
329
|
-
return;
|
|
330
|
-
// A Save House held back goes again only once the reset its answer named has passed.
|
|
331
|
-
resetTimer = setTimeout(() => {
|
|
332
|
-
resetTimer = undefined;
|
|
333
|
-
submitAll();
|
|
334
|
-
}, Math.max(0, earliest - Date.now()));
|
|
335
|
-
}
|
|
336
880
|
async function refuse(reason, provider = null) {
|
|
337
|
-
stopTimer();
|
|
338
881
|
discoveryPaused = true;
|
|
339
882
|
try {
|
|
340
883
|
if (reason === 'revoked-authorization') {
|
|
@@ -342,10 +885,7 @@ export function composeRuntime(options) {
|
|
|
342
885
|
const revoked = { status: 'unavailable', reason: 'revoked' };
|
|
343
886
|
try {
|
|
344
887
|
const local = await openLocal();
|
|
345
|
-
await local.revokeModels([...replicas.values()].map((replica) =>
|
|
346
|
-
record: replica.confirmed.record,
|
|
347
|
-
queue: replica.queue,
|
|
348
|
-
})));
|
|
888
|
+
await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
|
|
349
889
|
}
|
|
350
890
|
catch {
|
|
351
891
|
journal.record('model revocation persistence failed');
|
|
@@ -369,10 +909,7 @@ export function composeRuntime(options) {
|
|
|
369
909
|
model: revoked,
|
|
370
910
|
selectionModel: revoked,
|
|
371
911
|
};
|
|
372
|
-
|
|
373
|
-
replicas.set(locator, read.kind === 'staged'
|
|
374
|
-
? replicaOf(read.confirmed, before.queue, before)
|
|
375
|
-
: { ...before, confirmed: { ...before.confirmed, record } });
|
|
912
|
+
replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
|
|
376
913
|
}
|
|
377
914
|
rooms.set(locator, {
|
|
378
915
|
...room,
|
|
@@ -389,7 +926,8 @@ export function composeRuntime(options) {
|
|
|
389
926
|
}
|
|
390
927
|
finally {
|
|
391
928
|
refusal = {
|
|
392
|
-
reason,
|
|
929
|
+
code: reason,
|
|
930
|
+
retryable: false,
|
|
393
931
|
reauthorizationOffered: reason === 'revoked-authorization',
|
|
394
932
|
refusal: provider,
|
|
395
933
|
};
|
|
@@ -414,19 +952,28 @@ export function composeRuntime(options) {
|
|
|
414
952
|
},
|
|
415
953
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
416
954
|
roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
|
|
417
|
-
attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
|
|
418
955
|
synchronizationAvailable,
|
|
419
956
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
420
957
|
});
|
|
421
958
|
}
|
|
422
|
-
async function applyDiscovery(
|
|
959
|
+
async function applyDiscovery(found, started) {
|
|
960
|
+
for (const room of found) {
|
|
961
|
+
const fence = fences.get(room.locator);
|
|
962
|
+
if (fence !== undefined && fence.erased && fence.at < started)
|
|
963
|
+
fences.delete(room.locator);
|
|
964
|
+
}
|
|
965
|
+
const listed = found.filter((room) => !fences.has(room.locator));
|
|
423
966
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
424
967
|
for (const locator of listedLocators)
|
|
425
968
|
scopedRefusals.delete(locator);
|
|
426
969
|
discovered.clear();
|
|
427
970
|
for (const room of listed)
|
|
428
971
|
discovered.set(room.locator, room);
|
|
429
|
-
|
|
972
|
+
const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
|
|
973
|
+
if (options.adapter.exhaustiveDiscovery === true)
|
|
974
|
+
await loseRooms(unlisted);
|
|
975
|
+
else
|
|
976
|
+
unlistedKept(unlisted);
|
|
430
977
|
for (const room of listed) {
|
|
431
978
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
432
979
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -437,43 +984,85 @@ export function composeRuntime(options) {
|
|
|
437
984
|
}
|
|
438
985
|
roomsListed = true;
|
|
439
986
|
emitRooms();
|
|
987
|
+
return listed;
|
|
440
988
|
}
|
|
441
989
|
function unlistedHeld(listed) {
|
|
442
|
-
return [
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
pendings.delete(batch.id);
|
|
458
|
-
replicas.delete(locator);
|
|
990
|
+
return [
|
|
991
|
+
...new Set([
|
|
992
|
+
...rooms.keys(),
|
|
993
|
+
...roomDrafts.keys(),
|
|
994
|
+
...transfers.keys(),
|
|
995
|
+
...queuesWithoutReplica.keys(),
|
|
996
|
+
]),
|
|
997
|
+
].filter((locator) => !listed.has(locator));
|
|
998
|
+
}
|
|
999
|
+
function unlistedKept(unlisted) {
|
|
1000
|
+
for (const locator of unlisted) {
|
|
1001
|
+
const held = replicas.get(locator)?.confirmed.record;
|
|
1002
|
+
if (held !== undefined)
|
|
1003
|
+
list(held, false);
|
|
1004
|
+
else
|
|
459
1005
|
rooms.delete(locator);
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
function fence(locators) {
|
|
1009
|
+
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
1010
|
+
const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
|
|
1011
|
+
room: true,
|
|
1012
|
+
queue: true,
|
|
1013
|
+
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
1014
|
+
}));
|
|
1015
|
+
for (const locator of fresh) {
|
|
1016
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
1017
|
+
lastFenced.set(locator, ++fencings);
|
|
1018
|
+
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
1019
|
+
pendings.delete(batch.id);
|
|
1020
|
+
replicas.delete(locator);
|
|
1021
|
+
queuesWithoutReplica.delete(locator);
|
|
1022
|
+
rooms.delete(locator);
|
|
1023
|
+
roomDrafts.delete(locator);
|
|
1024
|
+
transfers.delete(locator);
|
|
1025
|
+
catching.delete(locator);
|
|
1026
|
+
discovered.delete(locator);
|
|
1027
|
+
announced.delete(locator);
|
|
1028
|
+
unreadable.delete(locator);
|
|
1029
|
+
stale.delete(locator);
|
|
1030
|
+
repairable.delete(locator);
|
|
1031
|
+
unindexable.delete(locator);
|
|
1032
|
+
paused.delete(locator);
|
|
1033
|
+
scopedRefusals.delete(locator);
|
|
1034
|
+
sourceRefusals.delete(locator);
|
|
1035
|
+
declarationRefusals.delete(locator);
|
|
1036
|
+
for (const attached of ['collection', 'typed-selection']) {
|
|
1037
|
+
const key = modelKey(locator, attached);
|
|
1038
|
+
models.delete(key);
|
|
1039
|
+
liveModels.delete(key);
|
|
1040
|
+
invalidModels.delete(key);
|
|
473
1041
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
1042
|
+
journal.record(`room lost ${locator}`);
|
|
1043
|
+
}
|
|
1044
|
+
if (fresh.length === 0)
|
|
1045
|
+
return fresh;
|
|
1046
|
+
publishChangeSet(mergeChangeSets(removed));
|
|
1047
|
+
for (const locator of fresh)
|
|
1048
|
+
prepared.delete(locator);
|
|
1049
|
+
searchIndex.forget(fresh);
|
|
1050
|
+
readSide.recover();
|
|
1051
|
+
moveTo({ ...status, ...attention() });
|
|
1052
|
+
return fresh;
|
|
1053
|
+
}
|
|
1054
|
+
async function erased(local, locators) {
|
|
1055
|
+
await local.erase(locators);
|
|
1056
|
+
for (const locator of locators) {
|
|
1057
|
+
const held = fences.get(locator);
|
|
1058
|
+
if (held !== undefined)
|
|
1059
|
+
held.erased = true;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
async function loseRooms(locators) {
|
|
1063
|
+
const fresh = fence(locators);
|
|
1064
|
+
if (fresh.length > 0)
|
|
1065
|
+
await durably((local) => erased(local, fresh));
|
|
477
1066
|
}
|
|
478
1067
|
function restored(key, persisted) {
|
|
479
1068
|
const prior = models.get(key);
|
|
@@ -494,7 +1083,7 @@ export function composeRuntime(options) {
|
|
|
494
1083
|
return model;
|
|
495
1084
|
}
|
|
496
1085
|
async function hydrate(opened) {
|
|
497
|
-
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known,] = await Promise.all([
|
|
1086
|
+
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
|
|
498
1087
|
opened.all(),
|
|
499
1088
|
opened.queues(),
|
|
500
1089
|
opened.roomDrafts(),
|
|
@@ -505,7 +1094,13 @@ export function composeRuntime(options) {
|
|
|
505
1094
|
opened.providerLimits(),
|
|
506
1095
|
opened.capabilities(),
|
|
507
1096
|
opened.member(),
|
|
1097
|
+
opened.transfers(),
|
|
1098
|
+
opened.targets(),
|
|
508
1099
|
]);
|
|
1100
|
+
targets = heldTargets;
|
|
1101
|
+
transfers.clear();
|
|
1102
|
+
for (const record of staged)
|
|
1103
|
+
transfers.set(record.locator, record);
|
|
509
1104
|
limits = declared;
|
|
510
1105
|
capabilities = named;
|
|
511
1106
|
member = known;
|
|
@@ -524,18 +1119,17 @@ export function composeRuntime(options) {
|
|
|
524
1119
|
}
|
|
525
1120
|
const previousReplicas = new Map(replicas);
|
|
526
1121
|
replicas.clear();
|
|
1122
|
+
queuesWithoutReplica.clear();
|
|
1123
|
+
const replicated = new Set(records.map((record) => record.locator));
|
|
1124
|
+
for (const [locator, queue] of queuedBatches) {
|
|
1125
|
+
if (!replicated.has(locator))
|
|
1126
|
+
queuesWithoutReplica.set(locator, queue);
|
|
1127
|
+
}
|
|
527
1128
|
for (const record of records) {
|
|
528
1129
|
const model = restored(modelKey(record.locator, 'collection'), record.model);
|
|
529
1130
|
const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
|
|
530
1131
|
const prior = previousReplicas.get(record.locator);
|
|
531
|
-
const
|
|
532
|
-
const read = readConfirmed(retyped, prior?.confirmed);
|
|
533
|
-
const confirmed = read.kind === 'staged'
|
|
534
|
-
? read.confirmed
|
|
535
|
-
: {
|
|
536
|
-
record: retyped,
|
|
537
|
-
collection: parseCollection(retyped.documents, compositionOf(retyped)),
|
|
538
|
-
};
|
|
1132
|
+
const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
|
|
539
1133
|
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
|
|
540
1134
|
const existing = rooms.get(record.locator);
|
|
541
1135
|
if (existing === undefined)
|
|
@@ -544,7 +1138,6 @@ export function composeRuntime(options) {
|
|
|
544
1138
|
rooms.set(record.locator, {
|
|
545
1139
|
...existing,
|
|
546
1140
|
roomVersion: record.roomVersion,
|
|
547
|
-
attachments: record.attachments,
|
|
548
1141
|
operations: record.operations,
|
|
549
1142
|
replica: record.replica,
|
|
550
1143
|
model: modelTypes(model),
|
|
@@ -556,10 +1149,6 @@ export function composeRuntime(options) {
|
|
|
556
1149
|
},
|
|
557
1150
|
});
|
|
558
1151
|
}
|
|
559
|
-
if (read.kind === 'refused') {
|
|
560
|
-
refusedSource(record, read);
|
|
561
|
-
declarationRefusals.add(record.locator);
|
|
562
|
-
}
|
|
563
1152
|
}
|
|
564
1153
|
return {
|
|
565
1154
|
lastCompleted,
|
|
@@ -570,6 +1159,7 @@ export function composeRuntime(options) {
|
|
|
570
1159
|
async function readState(opened) {
|
|
571
1160
|
const { lastCompleted, locators, read } = await hydrate(opened);
|
|
572
1161
|
applyChangeSet(read);
|
|
1162
|
+
prepare();
|
|
573
1163
|
if (replicas.size > 0)
|
|
574
1164
|
emitRooms();
|
|
575
1165
|
if (member !== null)
|
|
@@ -580,8 +1170,15 @@ export function composeRuntime(options) {
|
|
|
580
1170
|
return lastCompleted;
|
|
581
1171
|
}
|
|
582
1172
|
async function open() {
|
|
583
|
-
|
|
1173
|
+
if (closed)
|
|
1174
|
+
throw new Error('runtime closed');
|
|
1175
|
+
const opened = await openRuntimeStore(isFenced);
|
|
584
1176
|
store = opened;
|
|
1177
|
+
const interrupted = await opened.fences();
|
|
1178
|
+
for (const locator of interrupted) {
|
|
1179
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
1180
|
+
lastFenced.set(locator, ++fencings);
|
|
1181
|
+
}
|
|
585
1182
|
const kept = options.shell.generation;
|
|
586
1183
|
const storedGeneration = await opened.generation();
|
|
587
1184
|
const loss = detectStorageLoss(storedGeneration, kept);
|
|
@@ -595,6 +1192,8 @@ export function composeRuntime(options) {
|
|
|
595
1192
|
emitStorageLoss();
|
|
596
1193
|
}
|
|
597
1194
|
const lastCompleted = await readState(opened);
|
|
1195
|
+
if (interrupted.length > 0)
|
|
1196
|
+
await erased(opened, interrupted);
|
|
598
1197
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
599
1198
|
return opened;
|
|
600
1199
|
}
|
|
@@ -621,11 +1220,12 @@ export function composeRuntime(options) {
|
|
|
621
1220
|
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
622
1221
|
}
|
|
623
1222
|
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
624
|
-
const
|
|
1223
|
+
const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
1224
|
+
const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
|
|
625
1225
|
const kept = queue.filter((batch) => !withdrawn.includes(batch));
|
|
626
1226
|
const unqueued = withdrawn.map((batch) => ({
|
|
627
1227
|
batch,
|
|
628
|
-
returned: landed(batch, confirmed) ? null :
|
|
1228
|
+
returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
|
|
629
1229
|
}));
|
|
630
1230
|
const returning = [
|
|
631
1231
|
...[...unqueued].reverse(),
|
|
@@ -646,28 +1246,48 @@ export function composeRuntime(options) {
|
|
|
646
1246
|
});
|
|
647
1247
|
}
|
|
648
1248
|
}
|
|
649
|
-
await local.
|
|
650
|
-
|
|
651
|
-
previous: before?.confirmed.record ?? null,
|
|
652
|
-
queue: kept,
|
|
653
|
-
previousQueue: before?.queue ?? [],
|
|
654
|
-
roomDrafts: [...written.values()],
|
|
655
|
-
});
|
|
656
|
-
for (const batch of before?.queue ?? []) {
|
|
1249
|
+
await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
|
|
1250
|
+
for (const batch of previousQueue) {
|
|
657
1251
|
if (!kept.includes(batch))
|
|
658
1252
|
pendings.delete(batch.id);
|
|
659
1253
|
}
|
|
660
1254
|
for (const record of written.values())
|
|
661
1255
|
rememberRoomDraft(record);
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
1256
|
+
try {
|
|
1257
|
+
await local.write({
|
|
1258
|
+
next: confirmed.record,
|
|
1259
|
+
previous: before?.confirmed.record ?? null,
|
|
1260
|
+
...(settling.stored === undefined ? {} : { stored: settling.stored }),
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
catch (error) {
|
|
1264
|
+
const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
|
|
1265
|
+
if (held !== null)
|
|
1266
|
+
replicas.set(locator, held);
|
|
1267
|
+
else if (kept.length > 0)
|
|
1268
|
+
queuesWithoutReplica.set(locator, kept);
|
|
1269
|
+
else
|
|
1270
|
+
queuesWithoutReplica.delete(locator);
|
|
1271
|
+
publishChangeSet(changeSet(locator, before ?? null, held, {
|
|
1272
|
+
queue: kept.length !== previousQueue.length,
|
|
1273
|
+
drafts: [...written.keys()],
|
|
1274
|
+
}));
|
|
1275
|
+
moveTo({ ...status, ...attention() });
|
|
1276
|
+
throw error;
|
|
1277
|
+
}
|
|
1278
|
+
return () => {
|
|
1279
|
+
queuesWithoutReplica.delete(locator);
|
|
1280
|
+
for (const { batch, returned } of unqueued) {
|
|
1281
|
+
const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
|
|
1282
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
1283
|
+
}
|
|
1284
|
+
replicas.set(locator, replicaOf(confirmed, kept, before));
|
|
1285
|
+
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
1286
|
+
room: settling.room,
|
|
1287
|
+
queue: kept.length !== previousQueue.length,
|
|
1288
|
+
drafts: [...written.keys()],
|
|
1289
|
+
});
|
|
1290
|
+
};
|
|
671
1291
|
}
|
|
672
1292
|
function adoptModels(locator, record) {
|
|
673
1293
|
for (const [attached, model] of [
|
|
@@ -700,10 +1320,16 @@ export function composeRuntime(options) {
|
|
|
700
1320
|
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
701
1321
|
};
|
|
702
1322
|
}
|
|
703
|
-
function publish(local, room, confirmed,
|
|
1323
|
+
function publish(local, room, confirmed, landing, live) {
|
|
1324
|
+
const { base, stored } = landing;
|
|
704
1325
|
return exclusive(async () => {
|
|
705
|
-
if (
|
|
1326
|
+
if (stored !== undefined) {
|
|
1327
|
+
await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
|
|
1328
|
+
}
|
|
1329
|
+
if (!live()) {
|
|
1330
|
+
caughtUpTo(room.locator);
|
|
706
1331
|
return true;
|
|
1332
|
+
}
|
|
707
1333
|
const prior = replicas.get(room.locator);
|
|
708
1334
|
if (base !== undefined &&
|
|
709
1335
|
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
@@ -711,47 +1337,85 @@ export function composeRuntime(options) {
|
|
|
711
1337
|
return false;
|
|
712
1338
|
}
|
|
713
1339
|
adoptModels(room.locator, confirmed.record);
|
|
714
|
-
const queue = prior?.queue ?? [];
|
|
1340
|
+
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
715
1341
|
const roomVersion = confirmed.record.roomVersion;
|
|
716
1342
|
const accepted = queue.filter((batch) => {
|
|
717
1343
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
718
1344
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
719
1345
|
});
|
|
720
|
-
|
|
1346
|
+
let settled;
|
|
1347
|
+
try {
|
|
1348
|
+
settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
|
|
1349
|
+
}
|
|
1350
|
+
catch (error) {
|
|
1351
|
+
if (!exceeded(error))
|
|
1352
|
+
throw error;
|
|
1353
|
+
await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
|
|
1354
|
+
caughtUpTo(room.locator);
|
|
1355
|
+
return true;
|
|
1356
|
+
}
|
|
1357
|
+
const set = settled();
|
|
1358
|
+
if (stored !== undefined) {
|
|
1359
|
+
transfers.delete(room.locator);
|
|
1360
|
+
catching.delete(room.locator);
|
|
1361
|
+
}
|
|
721
1362
|
for (const batch of accepted)
|
|
722
1363
|
pendings.delete(batch.id);
|
|
723
1364
|
stale.delete(room.locator);
|
|
1365
|
+
unindexable.delete(room.locator);
|
|
724
1366
|
if (unreadable.delete(room.locator))
|
|
725
1367
|
scopedRefusals.delete(room.locator);
|
|
726
1368
|
sourceRefusals.delete(room.locator);
|
|
727
1369
|
declarationRefusals.delete(room.locator);
|
|
728
1370
|
list(room, true);
|
|
1371
|
+
if (prior === undefined)
|
|
1372
|
+
prepared.add(room.locator);
|
|
729
1373
|
publishChangeSet(set);
|
|
730
1374
|
moveTo({ ...status, ...attention() });
|
|
1375
|
+
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
731
1376
|
return true;
|
|
732
1377
|
});
|
|
733
1378
|
}
|
|
734
1379
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
735
1380
|
const local = await openLocal();
|
|
1381
|
+
await referenced(locator);
|
|
736
1382
|
const replica = replicas.get(locator);
|
|
737
1383
|
const room = rooms.get(locator);
|
|
738
1384
|
if (replica === undefined || room === undefined) {
|
|
739
|
-
throw
|
|
1385
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
740
1386
|
}
|
|
741
1387
|
const saved = [];
|
|
742
1388
|
for (const { path, revision } of named) {
|
|
743
1389
|
const record = roomDrafts.get(locator)?.get(path);
|
|
744
1390
|
if (record === undefined || record.revision !== revision) {
|
|
745
|
-
return { outcome: 'refused',
|
|
1391
|
+
return { outcome: 'refused', code: 'stale', retryable: false, file: path };
|
|
746
1392
|
}
|
|
747
1393
|
if (!saved.some((draft) => draft.path === path))
|
|
748
1394
|
saved.push({ path, parts: record.parts });
|
|
749
1395
|
}
|
|
750
1396
|
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
|
|
751
1397
|
if (changed.length > 0)
|
|
752
|
-
return { outcome: 'refused',
|
|
1398
|
+
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
753
1399
|
const parts = saved.map(({ path, parts }) => changesOf(path, parts));
|
|
754
|
-
const
|
|
1400
|
+
const changes = parts.flatMap(({ changes }) => changes);
|
|
1401
|
+
const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
|
|
1402
|
+
if (requeue.kind === 'refused')
|
|
1403
|
+
return { outcome: 'refused', ...requeue.refusal };
|
|
1404
|
+
const foreign = [...requeue.withdrawn.keys()].some((origin) => {
|
|
1405
|
+
const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
|
|
1406
|
+
return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
|
|
1407
|
+
});
|
|
1408
|
+
if (foreign) {
|
|
1409
|
+
return {
|
|
1410
|
+
outcome: 'refused',
|
|
1411
|
+
code: 'unknown-revision',
|
|
1412
|
+
retryable: false,
|
|
1413
|
+
file: [...requeue.queued].at(-1),
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
const withdrawing = requeue.withdrawn.size > 0;
|
|
1417
|
+
const input = {
|
|
1418
|
+
local: localSaves,
|
|
755
1419
|
baseRoomVersion: replica.confirmed.record.roomVersion,
|
|
756
1420
|
composition: compositionFor(replica.confirmed.record),
|
|
757
1421
|
operations: room.operations,
|
|
@@ -759,29 +1423,93 @@ export function composeRuntime(options) {
|
|
|
759
1423
|
providerLimits: limits,
|
|
760
1424
|
handle: room.handle,
|
|
761
1425
|
visible: replica.visible,
|
|
762
|
-
changes
|
|
1426
|
+
changes,
|
|
763
1427
|
bases: parts.flatMap(({ bases }) => bases),
|
|
764
1428
|
writtenBy,
|
|
1429
|
+
clean: {
|
|
1430
|
+
text: replica.confirmed.record.documents,
|
|
1431
|
+
binaries: replica.confirmed.record.binaries,
|
|
1432
|
+
composition: compositionOf(replica.confirmed.record),
|
|
1433
|
+
},
|
|
1434
|
+
};
|
|
1435
|
+
const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
|
|
1436
|
+
if (checked?.kind === 'refused')
|
|
1437
|
+
return { outcome: 'refused', ...checked.refusal };
|
|
1438
|
+
const owners = [...requeue.owners].flatMap((origin) => {
|
|
1439
|
+
const batch = requeue.withdrawn.get(origin);
|
|
1440
|
+
return batch === undefined || batch === null ? [] : [{ origin, batch }];
|
|
765
1441
|
});
|
|
766
|
-
if (
|
|
1442
|
+
if (owners.length > 1) {
|
|
1443
|
+
return {
|
|
1444
|
+
outcome: 'refused',
|
|
1445
|
+
code: 'unknown-revision',
|
|
1446
|
+
retryable: false,
|
|
1447
|
+
file: [...requeue.queued].at(-1),
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
const built = !withdrawing
|
|
1451
|
+
? buildBatch(input)
|
|
1452
|
+
: requeue.changes.length === 0
|
|
1453
|
+
? null
|
|
1454
|
+
: buildBatch({
|
|
1455
|
+
...input,
|
|
1456
|
+
visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
|
|
1457
|
+
changes: requeue.changes,
|
|
1458
|
+
});
|
|
1459
|
+
if (built?.kind === 'refused')
|
|
767
1460
|
return { outcome: 'refused', ...built.refusal };
|
|
768
|
-
const
|
|
1461
|
+
const [owner] = owners;
|
|
1462
|
+
const join = built === null || owner === undefined
|
|
1463
|
+
? null
|
|
1464
|
+
: joined(requeue.queue, owner.batch, built.batch);
|
|
1465
|
+
if (join?.kind === 'refused')
|
|
1466
|
+
return { outcome: 'refused', ...join.refusal };
|
|
1467
|
+
const withdrawn = new Map(requeue.withdrawn);
|
|
1468
|
+
if (join !== null && owner !== undefined)
|
|
1469
|
+
withdrawn.set(owner.origin, join.batch);
|
|
1470
|
+
const drafted = new Map();
|
|
1471
|
+
for (const [origin, replaced] of withdrawn) {
|
|
1472
|
+
if (replaced === null)
|
|
1473
|
+
continue;
|
|
1474
|
+
const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
|
|
1475
|
+
drafted.set(replaced.id, replaced === join?.batch
|
|
1476
|
+
? [
|
|
1477
|
+
...carried.map((held) => joinedDraft(held, saved)),
|
|
1478
|
+
...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
|
|
1479
|
+
]
|
|
1480
|
+
: carried);
|
|
1481
|
+
}
|
|
1482
|
+
if (built !== null && join === null)
|
|
1483
|
+
drafted.set(built.batch.id, saved);
|
|
1484
|
+
const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
|
|
1485
|
+
for (const replaced of withdrawn.values()) {
|
|
1486
|
+
if (replaced === null || !batches.includes(replaced))
|
|
1487
|
+
continue;
|
|
1488
|
+
const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
|
|
1489
|
+
if (limited !== null)
|
|
1490
|
+
return { outcome: 'refused', ...limited };
|
|
1491
|
+
}
|
|
769
1492
|
const consumed = saved.map(({ path }) => path);
|
|
770
1493
|
try {
|
|
771
|
-
await local.
|
|
772
|
-
|
|
773
|
-
previous: replica.confirmed.record,
|
|
1494
|
+
await local.keepAuthored({
|
|
1495
|
+
locator,
|
|
774
1496
|
queue: batches,
|
|
775
|
-
|
|
776
|
-
saved,
|
|
1497
|
+
saved: drafted,
|
|
777
1498
|
consumedRoomDrafts: consumed,
|
|
778
1499
|
});
|
|
779
1500
|
}
|
|
780
1501
|
catch {
|
|
781
|
-
return { outcome: 'refused',
|
|
1502
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
782
1503
|
}
|
|
783
|
-
|
|
784
|
-
|
|
1504
|
+
if (fences.has(locator))
|
|
1505
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1506
|
+
for (const origin of withdrawn.keys())
|
|
1507
|
+
pendings.delete(origin);
|
|
1508
|
+
for (const [id, drafts] of drafted) {
|
|
1509
|
+
pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
|
|
1510
|
+
}
|
|
1511
|
+
if (!localSaves)
|
|
1512
|
+
recorded = false;
|
|
785
1513
|
for (const path of consumed)
|
|
786
1514
|
forgetRoomDraft(locator, path);
|
|
787
1515
|
replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
|
|
@@ -789,9 +1517,12 @@ export function composeRuntime(options) {
|
|
|
789
1517
|
queue: true,
|
|
790
1518
|
drafts: consumed,
|
|
791
1519
|
}));
|
|
792
|
-
moveTo({ ...saving(status), ...attention() });
|
|
1520
|
+
moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
|
|
793
1521
|
submitLater(locator);
|
|
794
|
-
|
|
1522
|
+
const identifier = join?.batch.id ??
|
|
1523
|
+
built?.batch.id ??
|
|
1524
|
+
[...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
|
|
1525
|
+
return { outcome: 'saved', identifier };
|
|
795
1526
|
}
|
|
796
1527
|
async function mark(local, locator, identifier, submission, lost) {
|
|
797
1528
|
const state = pendings.get(identifier);
|
|
@@ -801,6 +1532,8 @@ export function composeRuntime(options) {
|
|
|
801
1532
|
return;
|
|
802
1533
|
}
|
|
803
1534
|
await local.submit(locator, identifier, submission, lost);
|
|
1535
|
+
if (fences.has(locator))
|
|
1536
|
+
return;
|
|
804
1537
|
pendings.set(identifier, { ...state, submission, lost });
|
|
805
1538
|
const replica = replicas.get(locator);
|
|
806
1539
|
if (replica !== undefined)
|
|
@@ -811,22 +1544,24 @@ export function composeRuntime(options) {
|
|
|
811
1544
|
if (!continuing())
|
|
812
1545
|
throw new Error('connection refused');
|
|
813
1546
|
await local.keepUpload(locator, identifier, upload);
|
|
1547
|
+
if (fences.has(locator))
|
|
1548
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
814
1549
|
pendings.set(identifier, { ...pendings.get(identifier), upload });
|
|
815
1550
|
if (!continuing())
|
|
816
1551
|
throw new Error('connection refused');
|
|
817
1552
|
}
|
|
818
1553
|
async function readHistory(locator, path, query = {}) {
|
|
819
1554
|
if (closed || refusal !== null)
|
|
820
|
-
return
|
|
1555
|
+
return UNCERTAIN;
|
|
821
1556
|
try {
|
|
822
1557
|
await ensureAuthorization();
|
|
823
1558
|
}
|
|
824
1559
|
catch {
|
|
825
|
-
return
|
|
826
|
-
}
|
|
827
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
828
|
-
return { kind: 'transport-uncertainty' };
|
|
1560
|
+
return UNCERTAIN;
|
|
829
1561
|
}
|
|
1562
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1563
|
+
return UNCERTAIN;
|
|
1564
|
+
const since = fencings;
|
|
830
1565
|
const transfer = ['transfer', 'history', locator, path];
|
|
831
1566
|
if (query.revision !== undefined)
|
|
832
1567
|
transfer.push(query.revision);
|
|
@@ -841,6 +1576,8 @@ export function composeRuntime(options) {
|
|
|
841
1576
|
...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
|
|
842
1577
|
});
|
|
843
1578
|
journal.record(`outcome history ${result.kind}`);
|
|
1579
|
+
if (fencedSince(locator, since))
|
|
1580
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
844
1581
|
if (result.kind === 'scoped-refusal') {
|
|
845
1582
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
846
1583
|
if (room !== undefined)
|
|
@@ -848,12 +1585,106 @@ export function composeRuntime(options) {
|
|
|
848
1585
|
}
|
|
849
1586
|
if (result.kind === 'revoked-authorization')
|
|
850
1587
|
await refuse('revoked-authorization');
|
|
851
|
-
return result;
|
|
1588
|
+
return historyAnswer(result);
|
|
852
1589
|
}
|
|
853
1590
|
catch {
|
|
854
1591
|
journal.record('outcome history transport-uncertainty');
|
|
855
|
-
return
|
|
1592
|
+
return UNCERTAIN;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
function queuedBlob(locator, path, localId) {
|
|
1596
|
+
for (const batch of replicas.get(locator)?.queue ?? []) {
|
|
1597
|
+
const change = batch.changes[queuedFile(batch, path, localId)];
|
|
1598
|
+
if (change?.kind === 'binary')
|
|
1599
|
+
return change.blob;
|
|
856
1600
|
}
|
|
1601
|
+
return null;
|
|
1602
|
+
}
|
|
1603
|
+
function binaryRoom(request) {
|
|
1604
|
+
if (request.kind === 'path')
|
|
1605
|
+
return request.locator;
|
|
1606
|
+
const scope = options.adapter.referrerScope(request.referrer);
|
|
1607
|
+
return scope?.kind === 'room' ? scope.locator : null;
|
|
1608
|
+
}
|
|
1609
|
+
async function readBinary(request) {
|
|
1610
|
+
if (closed || refusal !== null)
|
|
1611
|
+
return UNCERTAIN;
|
|
1612
|
+
try {
|
|
1613
|
+
await ensureAuthorization();
|
|
1614
|
+
}
|
|
1615
|
+
catch {
|
|
1616
|
+
return UNCERTAIN;
|
|
1617
|
+
}
|
|
1618
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1619
|
+
return UNCERTAIN;
|
|
1620
|
+
const since = fencings;
|
|
1621
|
+
journal.record(request.kind === 'path'
|
|
1622
|
+
? `transfer binary ${request.locator} ${request.path}`
|
|
1623
|
+
: `transfer binary ${request.fileRef}`);
|
|
1624
|
+
try {
|
|
1625
|
+
const result = await options.adapter.binary(authorization, request);
|
|
1626
|
+
journal.record(`outcome binary ${result.kind}`);
|
|
1627
|
+
const locator = binaryRoom(request);
|
|
1628
|
+
if (locator !== null && fencedSince(locator, since))
|
|
1629
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
1630
|
+
if (result.kind === 'scoped-refusal') {
|
|
1631
|
+
const room = replicas.get(result.locator)?.confirmed.record;
|
|
1632
|
+
if (room !== undefined)
|
|
1633
|
+
await scoped(result, room, await openLocal());
|
|
1634
|
+
}
|
|
1635
|
+
if (result.kind === 'revoked-authorization')
|
|
1636
|
+
await refuse('revoked-authorization');
|
|
1637
|
+
return binaryAnswer(result);
|
|
1638
|
+
}
|
|
1639
|
+
catch {
|
|
1640
|
+
journal.record('outcome binary transport-uncertainty');
|
|
1641
|
+
return UNCERTAIN;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
async function readBinaryPath(locator, path, revision) {
|
|
1645
|
+
await ready;
|
|
1646
|
+
if (revision !== undefined)
|
|
1647
|
+
return readBinary({ kind: 'path', locator, path, revision });
|
|
1648
|
+
const visible = replicas.get(locator)?.visible;
|
|
1649
|
+
const held = visible?.binaries[path];
|
|
1650
|
+
if (visible === undefined || held === undefined)
|
|
1651
|
+
return absentBinary(path);
|
|
1652
|
+
if (held.state === 'pending') {
|
|
1653
|
+
const blob = queuedBlob(locator, path, held.localId);
|
|
1654
|
+
return blob === null ? absentBinary(path) : { kind: 'binary', blob };
|
|
1655
|
+
}
|
|
1656
|
+
const record = binaryRecord(path, held, visible.revisions[path]);
|
|
1657
|
+
if (record.state !== 'accepted')
|
|
1658
|
+
return absentBinary(path);
|
|
1659
|
+
const replica = replicas.get(locator);
|
|
1660
|
+
const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
|
|
1661
|
+
const holds = (at) => {
|
|
1662
|
+
const kept = replica?.confirmed.record.binaries[at];
|
|
1663
|
+
return (kept?.state === 'accepted' &&
|
|
1664
|
+
kept.fileRef === record.fileRef &&
|
|
1665
|
+
replica?.confirmed.record.revisions[at] === record.revision);
|
|
1666
|
+
};
|
|
1667
|
+
let confirmed = path;
|
|
1668
|
+
for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
|
|
1669
|
+
const change = queued[index];
|
|
1670
|
+
if (change.kind === 'move' && change.to === confirmed)
|
|
1671
|
+
confirmed = change.path;
|
|
1672
|
+
}
|
|
1673
|
+
return readBinary({
|
|
1674
|
+
kind: 'path',
|
|
1675
|
+
locator,
|
|
1676
|
+
path: confirmed,
|
|
1677
|
+
revision: record.revision,
|
|
1678
|
+
fileRef: record.fileRef,
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
async function readBinaryReference(reference) {
|
|
1682
|
+
await ready;
|
|
1683
|
+
return readBinary({
|
|
1684
|
+
kind: 'reference',
|
|
1685
|
+
fileRef: reference.fileRef,
|
|
1686
|
+
referrer: reference.referrer,
|
|
1687
|
+
});
|
|
857
1688
|
}
|
|
858
1689
|
function declaredAgents() {
|
|
859
1690
|
for (const capability of capabilities) {
|
|
@@ -874,18 +1705,17 @@ export function composeRuntime(options) {
|
|
|
874
1705
|
async function invokeAction(name, input) {
|
|
875
1706
|
await ready;
|
|
876
1707
|
if (closed || refusal !== null)
|
|
877
|
-
return
|
|
1708
|
+
return UNCERTAIN;
|
|
878
1709
|
if (declaredAction(name) === undefined)
|
|
879
|
-
return
|
|
1710
|
+
return undeclared(name);
|
|
880
1711
|
try {
|
|
881
1712
|
await ensureAuthorization();
|
|
882
1713
|
}
|
|
883
1714
|
catch {
|
|
884
|
-
return
|
|
885
|
-
}
|
|
886
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
887
|
-
return { kind: 'transport-uncertainty' };
|
|
1715
|
+
return UNCERTAIN;
|
|
888
1716
|
}
|
|
1717
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1718
|
+
return UNCERTAIN;
|
|
889
1719
|
journal.record(`transfer action ${name}`);
|
|
890
1720
|
try {
|
|
891
1721
|
const result = await options.adapter.action(authorization, { name, input });
|
|
@@ -899,11 +1729,11 @@ export function composeRuntime(options) {
|
|
|
899
1729
|
journal.record(`outcome action ${result.kind}`);
|
|
900
1730
|
if (result.kind === 'rejected')
|
|
901
1731
|
journal.record(`refusal rejected ${result.code}`);
|
|
902
|
-
return result;
|
|
1732
|
+
return calledResult(result);
|
|
903
1733
|
}
|
|
904
1734
|
catch {
|
|
905
1735
|
journal.record('outcome action transport-uncertainty');
|
|
906
|
-
return
|
|
1736
|
+
return UNCERTAIN;
|
|
907
1737
|
}
|
|
908
1738
|
}
|
|
909
1739
|
function continuing() {
|
|
@@ -920,11 +1750,11 @@ export function composeRuntime(options) {
|
|
|
920
1750
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
921
1751
|
!options.adapter.reached(locator, roomVersion, written));
|
|
922
1752
|
}
|
|
923
|
-
async function pullRefresh(authorized, confirmed) {
|
|
924
|
-
if (!
|
|
1753
|
+
async function pullRefresh(authorized, confirmed, live) {
|
|
1754
|
+
if (!live())
|
|
925
1755
|
return unconfirmed;
|
|
926
1756
|
try {
|
|
927
|
-
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held),
|
|
1757
|
+
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
|
|
928
1758
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
929
1759
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
930
1760
|
return result;
|
|
@@ -934,28 +1764,80 @@ export function composeRuntime(options) {
|
|
|
934
1764
|
throw error;
|
|
935
1765
|
}
|
|
936
1766
|
}
|
|
937
|
-
|
|
938
|
-
|
|
1767
|
+
function staging(local, live) {
|
|
1768
|
+
return {
|
|
1769
|
+
held: (locator) => transfers.get(locator) ?? null,
|
|
1770
|
+
staged: (record) => local.staged(record),
|
|
1771
|
+
stage: (record, page) => exclusive(async () => {
|
|
1772
|
+
if (!live())
|
|
1773
|
+
return false;
|
|
1774
|
+
const bytes = layerBytes(page);
|
|
1775
|
+
if (!(await admitted(local, record.locator, bytes)))
|
|
1776
|
+
return false;
|
|
1777
|
+
try {
|
|
1778
|
+
await local.stage(record, page);
|
|
1779
|
+
}
|
|
1780
|
+
catch (error) {
|
|
1781
|
+
if (!exceeded(error))
|
|
1782
|
+
throw error;
|
|
1783
|
+
await exceededOn(record.locator, bytes);
|
|
1784
|
+
return false;
|
|
1785
|
+
}
|
|
1786
|
+
transfers.set(record.locator, record);
|
|
1787
|
+
readSide.recover();
|
|
1788
|
+
return true;
|
|
1789
|
+
}),
|
|
1790
|
+
abandon: (locator) => exclusive(async () => {
|
|
1791
|
+
if (!live())
|
|
1792
|
+
return;
|
|
1793
|
+
await local.abandon(locator);
|
|
1794
|
+
transfers.delete(locator);
|
|
1795
|
+
readSide.recover();
|
|
1796
|
+
}),
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
function reusable(room) {
|
|
1800
|
+
const record = replicas.get(room.locator)?.confirmed.record;
|
|
1801
|
+
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1802
|
+
return null;
|
|
1803
|
+
return { documents: record.documents, revisions: record.revisions };
|
|
1804
|
+
}
|
|
1805
|
+
async function pullBootstrap(authorized, room, local, live) {
|
|
1806
|
+
if (!live())
|
|
939
1807
|
return unconfirmed;
|
|
1808
|
+
if (replicas.has(room.locator)) {
|
|
1809
|
+
catching.add(room.locator);
|
|
1810
|
+
readSide.recover();
|
|
1811
|
+
}
|
|
940
1812
|
try {
|
|
941
1813
|
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
942
1814
|
collection: modelOf(room.locator),
|
|
943
1815
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
944
|
-
}, (models, held) => remodel(room.locator, models, held),
|
|
1816
|
+
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
945
1817
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
946
1818
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1819
|
+
if (result.kind !== 'staged')
|
|
1820
|
+
caughtUpTo(room.locator);
|
|
947
1821
|
return result;
|
|
948
1822
|
}
|
|
949
1823
|
catch (error) {
|
|
950
1824
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
1825
|
+
caughtUpTo(room.locator);
|
|
951
1826
|
throw error;
|
|
952
1827
|
}
|
|
953
1828
|
}
|
|
954
|
-
|
|
1829
|
+
function caughtUpTo(locator) {
|
|
1830
|
+
if (catching.delete(locator))
|
|
1831
|
+
readSide.recover();
|
|
1832
|
+
}
|
|
1833
|
+
async function advance(room, local, authorized, live) {
|
|
1834
|
+
await referenced(room.locator);
|
|
1835
|
+
if (!live() || paused.has(room.locator))
|
|
1836
|
+
return unconfirmed;
|
|
955
1837
|
const held = replicas.get(room.locator);
|
|
956
1838
|
if (held !== undefined) {
|
|
957
|
-
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
958
|
-
if (!
|
|
1839
|
+
const refreshed = await pullRefresh(authorized, held.confirmed, live);
|
|
1840
|
+
if (!live())
|
|
959
1841
|
return unconfirmed;
|
|
960
1842
|
if (refreshed.kind === 'still-current') {
|
|
961
1843
|
stale.delete(room.locator);
|
|
@@ -969,31 +1851,45 @@ export function composeRuntime(options) {
|
|
|
969
1851
|
return null;
|
|
970
1852
|
}
|
|
971
1853
|
if (refreshed.kind === 'advanced') {
|
|
972
|
-
if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
|
|
1854
|
+
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
|
|
973
1855
|
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
974
1856
|
const reached = now !== undefined &&
|
|
975
1857
|
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
976
|
-
return reached ? null : advance(room, local, authorized);
|
|
1858
|
+
return reached ? null : advance(room, local, authorized, live);
|
|
977
1859
|
}
|
|
978
1860
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
979
1861
|
return null;
|
|
980
1862
|
}
|
|
981
1863
|
if (refreshed.kind !== 'beyond-horizon')
|
|
982
1864
|
return refreshed;
|
|
1865
|
+
if (opened !== null) {
|
|
1866
|
+
owed.add(room.locator);
|
|
1867
|
+
readSide.recover();
|
|
1868
|
+
return unconfirmed;
|
|
1869
|
+
}
|
|
983
1870
|
}
|
|
984
|
-
const bootstrapped = await pullBootstrap(authorized, room);
|
|
985
|
-
if (!
|
|
1871
|
+
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
1872
|
+
if (!live())
|
|
986
1873
|
return unconfirmed;
|
|
987
1874
|
if (bootstrapped.kind !== 'staged')
|
|
988
1875
|
return bootstrapped;
|
|
989
|
-
await publish(local, room, bootstrapped.confirmed);
|
|
1876
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
990
1877
|
return null;
|
|
991
1878
|
}
|
|
992
1879
|
async function withhold(withheld, room, local, progress) {
|
|
1880
|
+
if (withheld.kind === 'unconfirmed')
|
|
1881
|
+
return;
|
|
993
1882
|
if (withheld.kind === 'refused')
|
|
994
1883
|
stale.delete(room.locator);
|
|
995
1884
|
else
|
|
996
1885
|
stale.add(room.locator);
|
|
1886
|
+
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
1887
|
+
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
1888
|
+
repairable.set(room.locator, reset);
|
|
1889
|
+
repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
|
|
1890
|
+
}
|
|
1891
|
+
else
|
|
1892
|
+
repairable.delete(room.locator);
|
|
997
1893
|
if (withheld.kind === 'scoped-refusal') {
|
|
998
1894
|
await scoped(withheld, room, local);
|
|
999
1895
|
}
|
|
@@ -1018,6 +1914,7 @@ export function composeRuntime(options) {
|
|
|
1018
1914
|
function refusedSource(room, { refusal }) {
|
|
1019
1915
|
sourceRefusals.set(room.locator, {
|
|
1020
1916
|
code: refusal.rule,
|
|
1917
|
+
retryable: false,
|
|
1021
1918
|
fields: {
|
|
1022
1919
|
file: refusal.file,
|
|
1023
1920
|
...(refusal.location === undefined ? {} : { location: refusal.location }),
|
|
@@ -1028,12 +1925,21 @@ export function composeRuntime(options) {
|
|
|
1028
1925
|
unreadable.add(room.locator);
|
|
1029
1926
|
list(room, false);
|
|
1030
1927
|
emitRooms();
|
|
1928
|
+
moveTo({ ...status, ...attention() });
|
|
1031
1929
|
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
1032
1930
|
}
|
|
1033
1931
|
function leaving(batch, result) {
|
|
1034
1932
|
if (result.kind === 'expired')
|
|
1035
|
-
return { batch, returned:
|
|
1036
|
-
return {
|
|
1933
|
+
return { batch, returned: returnedBy('expired') };
|
|
1934
|
+
return {
|
|
1935
|
+
batch,
|
|
1936
|
+
returned: {
|
|
1937
|
+
kind: result.kind,
|
|
1938
|
+
code: result.code,
|
|
1939
|
+
fields: result.fields,
|
|
1940
|
+
retryable: result.retryable,
|
|
1941
|
+
},
|
|
1942
|
+
};
|
|
1037
1943
|
}
|
|
1038
1944
|
function logAccepted(locator, submitted, accepted) {
|
|
1039
1945
|
journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
|
|
@@ -1041,6 +1947,7 @@ export function composeRuntime(options) {
|
|
|
1041
1947
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1042
1948
|
}
|
|
1043
1949
|
async function adoptAccepted(room, local, submitted, accepted) {
|
|
1950
|
+
await referenced(room.locator);
|
|
1044
1951
|
if (!continuing())
|
|
1045
1952
|
return unconfirmed;
|
|
1046
1953
|
const current = replicas.get(room.locator);
|
|
@@ -1051,22 +1958,23 @@ export function composeRuntime(options) {
|
|
|
1051
1958
|
logAccepted(room.locator, submitted, accepted);
|
|
1052
1959
|
const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
1053
1960
|
? { kind: 'staged', confirmed: current.confirmed }
|
|
1054
|
-
: applyReturned(current.confirmed,
|
|
1961
|
+
: applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
1055
1962
|
? accepted.roomVersion
|
|
1056
1963
|
: held, accepted.roomVersion);
|
|
1057
1964
|
if (applied.kind !== 'staged') {
|
|
1058
1965
|
const state = pendings.get(submitted.id);
|
|
1059
1966
|
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1060
|
-
if (state !== undefined)
|
|
1967
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
1061
1968
|
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1062
1969
|
emitQueue(room.locator);
|
|
1063
1970
|
return applied;
|
|
1064
1971
|
}
|
|
1065
|
-
const
|
|
1972
|
+
const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
1066
1973
|
pendings.delete(submitted.id);
|
|
1067
1974
|
unreadable.delete(room.locator);
|
|
1068
1975
|
sourceRefusals.delete(room.locator);
|
|
1069
1976
|
declarationRefusals.delete(room.locator);
|
|
1977
|
+
const set = settled();
|
|
1070
1978
|
list(room, true);
|
|
1071
1979
|
publishChangeSet(set);
|
|
1072
1980
|
moveTo({ ...status, ...attention() });
|
|
@@ -1080,14 +1988,14 @@ export function composeRuntime(options) {
|
|
|
1080
1988
|
refusedSource(room, failed);
|
|
1081
1989
|
return false;
|
|
1082
1990
|
}
|
|
1083
|
-
async function submitRoom(room, local, authorized, progress) {
|
|
1084
|
-
while (continuing() && authorization !== undefined) {
|
|
1991
|
+
async function submitRoom(room, local, authorized, progress, signal) {
|
|
1992
|
+
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
1085
1993
|
const replica = replicas.get(room.locator);
|
|
1086
1994
|
const head = replica?.queue[0];
|
|
1087
1995
|
if (replica === undefined || head === undefined)
|
|
1088
|
-
return;
|
|
1996
|
+
return 'done';
|
|
1089
1997
|
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1090
|
-
return;
|
|
1998
|
+
return 'done';
|
|
1091
1999
|
const state = pendings.get(head.id);
|
|
1092
2000
|
const { submission, lost: earlier } = state ?? {
|
|
1093
2001
|
submission: { state: 'waiting' },
|
|
@@ -1095,24 +2003,30 @@ export function composeRuntime(options) {
|
|
|
1095
2003
|
};
|
|
1096
2004
|
if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
|
|
1097
2005
|
progress.complete = false;
|
|
1098
|
-
return;
|
|
2006
|
+
return { at: Date.parse(submission.reset) };
|
|
1099
2007
|
}
|
|
1100
2008
|
if (writesHeld()) {
|
|
1101
2009
|
progress.complete = false;
|
|
1102
2010
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1103
|
-
return;
|
|
2011
|
+
return 'done';
|
|
1104
2012
|
}
|
|
1105
2013
|
const lost = earlier || submission.state === 'submitted';
|
|
2014
|
+
if (lost && pastHorizon(head.id)) {
|
|
2015
|
+
await discardPending(room, local, signal);
|
|
2016
|
+
return 'done';
|
|
2017
|
+
}
|
|
1106
2018
|
let result;
|
|
1107
2019
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
1108
2020
|
await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
|
|
1109
|
-
if (discoveryPaused)
|
|
1110
|
-
return;
|
|
1111
|
-
if (writesHeld()) {
|
|
2021
|
+
if (discoveryPaused || fences.has(room.locator))
|
|
2022
|
+
return 'done';
|
|
2023
|
+
if (signal.aborted || writesHeld()) {
|
|
1112
2024
|
await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
|
|
2025
|
+
if (signal.aborted)
|
|
2026
|
+
return 'done';
|
|
1113
2027
|
progress.complete = false;
|
|
1114
2028
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1115
|
-
return;
|
|
2029
|
+
return 'done';
|
|
1116
2030
|
}
|
|
1117
2031
|
const opensBefore = openSignals;
|
|
1118
2032
|
const answered = awaitingAnswer(room.locator);
|
|
@@ -1128,32 +2042,36 @@ export function composeRuntime(options) {
|
|
|
1128
2042
|
progress.complete = false;
|
|
1129
2043
|
if (continuing())
|
|
1130
2044
|
progress.reachable = false;
|
|
1131
|
-
return;
|
|
2045
|
+
return 'again';
|
|
2046
|
+
}
|
|
2047
|
+
if (signal.aborted) {
|
|
2048
|
+
answered();
|
|
2049
|
+
return 'done';
|
|
1132
2050
|
}
|
|
1133
2051
|
if (result.kind !== 'accepted' || !continuing())
|
|
1134
2052
|
answered();
|
|
1135
2053
|
if (!continuing())
|
|
1136
|
-
return;
|
|
2054
|
+
return 'done';
|
|
1137
2055
|
if (result.kind === 'transport-uncertainty') {
|
|
1138
2056
|
progress.complete = false;
|
|
1139
2057
|
progress.reachable = false;
|
|
1140
|
-
return;
|
|
2058
|
+
return 'again';
|
|
1141
2059
|
}
|
|
1142
2060
|
if (result.kind === 'room-not-readable') {
|
|
1143
2061
|
progress.complete = false;
|
|
1144
2062
|
unreadable.add(room.locator);
|
|
1145
2063
|
list(room, false);
|
|
1146
2064
|
emitRooms();
|
|
1147
|
-
return;
|
|
2065
|
+
return 'done';
|
|
1148
2066
|
}
|
|
1149
2067
|
if (result.kind === 'revoked-authorization') {
|
|
1150
2068
|
await refuse('revoked-authorization');
|
|
1151
|
-
return;
|
|
2069
|
+
return 'done';
|
|
1152
2070
|
}
|
|
1153
2071
|
if (result.kind === 'scoped-refusal') {
|
|
1154
2072
|
await scoped(result, room, local);
|
|
1155
2073
|
progress.complete = false;
|
|
1156
|
-
return;
|
|
2074
|
+
return 'done';
|
|
1157
2075
|
}
|
|
1158
2076
|
if (result.kind === 'write-halted') {
|
|
1159
2077
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
@@ -1162,16 +2080,24 @@ export function composeRuntime(options) {
|
|
|
1162
2080
|
continue;
|
|
1163
2081
|
haltedRefusal = true;
|
|
1164
2082
|
progress.complete = false;
|
|
1165
|
-
return;
|
|
2083
|
+
return 'done';
|
|
1166
2084
|
}
|
|
1167
2085
|
if (result.kind === 'conflicted' ||
|
|
1168
2086
|
result.kind === 'rejected' ||
|
|
1169
2087
|
result.kind === 'expired' ||
|
|
1170
2088
|
result.kind === 'retryable-refusal') {
|
|
2089
|
+
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
2090
|
+
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
2091
|
+
await discardPending(room, local, signal);
|
|
2092
|
+
return 'done';
|
|
2093
|
+
}
|
|
1171
2094
|
if (result.kind === 'expired') {
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
2095
|
+
await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
|
|
2096
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
2097
|
+
if (withheld !== null) {
|
|
2098
|
+
await withhold(withheld, room, local, progress);
|
|
2099
|
+
return 'done';
|
|
2100
|
+
}
|
|
1175
2101
|
}
|
|
1176
2102
|
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
|
|
1177
2103
|
(result.kind === 'conflicted' && result.roomVersion !== undefined
|
|
@@ -1184,8 +2110,7 @@ export function composeRuntime(options) {
|
|
|
1184
2110
|
const reset = result.reset;
|
|
1185
2111
|
progress.complete = false;
|
|
1186
2112
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1187
|
-
|
|
1188
|
-
return;
|
|
2113
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
1189
2114
|
}
|
|
1190
2115
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
1191
2116
|
const left = leaving(head, result);
|
|
@@ -1196,37 +2121,82 @@ export function composeRuntime(options) {
|
|
|
1196
2121
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
1197
2122
|
return;
|
|
1198
2123
|
}
|
|
1199
|
-
const
|
|
2124
|
+
const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
1200
2125
|
leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
|
|
1201
2126
|
(result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
|
|
1202
2127
|
? { ...left, returned: null }
|
|
1203
2128
|
: left,
|
|
1204
2129
|
});
|
|
1205
|
-
publishChangeSet(
|
|
2130
|
+
publishChangeSet(settled());
|
|
1206
2131
|
moveTo({ ...status, ...attention() });
|
|
1207
2132
|
});
|
|
1208
2133
|
if (result.kind === 'expired')
|
|
1209
2134
|
continue;
|
|
1210
|
-
const withheld = await advance(room, local, authorized);
|
|
1211
|
-
if (withheld !== null)
|
|
1212
|
-
|
|
2135
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
2136
|
+
if (withheld !== null) {
|
|
2137
|
+
await withhold(withheld, room, local, progress);
|
|
2138
|
+
return 'done';
|
|
2139
|
+
}
|
|
1213
2140
|
continue;
|
|
1214
2141
|
}
|
|
1215
2142
|
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1216
2143
|
if (!kept) {
|
|
1217
2144
|
progress.complete = false;
|
|
1218
|
-
return;
|
|
2145
|
+
return 'done';
|
|
1219
2146
|
}
|
|
1220
2147
|
}
|
|
2148
|
+
return 'done';
|
|
2149
|
+
}
|
|
2150
|
+
function pastHorizon(identity) {
|
|
2151
|
+
return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
|
|
2152
|
+
}
|
|
2153
|
+
async function discardPending(room, local, signal) {
|
|
2154
|
+
const { locator } = room;
|
|
2155
|
+
const discarded = await exclusive(async () => {
|
|
2156
|
+
if (fences.has(locator))
|
|
2157
|
+
return false;
|
|
2158
|
+
const before = replicas.get(locator);
|
|
2159
|
+
const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
2160
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
2161
|
+
lastFenced.set(locator, ++fencings);
|
|
2162
|
+
try {
|
|
2163
|
+
await local.discard(locator);
|
|
2164
|
+
}
|
|
2165
|
+
finally {
|
|
2166
|
+
fences.delete(locator);
|
|
2167
|
+
}
|
|
2168
|
+
for (const batch of queue)
|
|
2169
|
+
pendings.delete(batch.id);
|
|
2170
|
+
queuesWithoutReplica.delete(locator);
|
|
2171
|
+
const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
|
|
2172
|
+
if (after !== null)
|
|
2173
|
+
replicas.set(locator, after);
|
|
2174
|
+
journal.record(`room discarded ${locator}`);
|
|
2175
|
+
publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
|
|
2176
|
+
moveTo({ ...status, ...attention() });
|
|
2177
|
+
return true;
|
|
2178
|
+
});
|
|
2179
|
+
if (!discarded)
|
|
2180
|
+
return;
|
|
2181
|
+
const progress = { complete: true, reachable: true };
|
|
2182
|
+
await roomTask(locator, async (live) => {
|
|
2183
|
+
await Promise.race([
|
|
2184
|
+
reload(room, local, progress, () => live() && !signal.aborted),
|
|
2185
|
+
abandonedOn(signal),
|
|
2186
|
+
]);
|
|
2187
|
+
});
|
|
2188
|
+
if (signal.aborted)
|
|
2189
|
+
return;
|
|
2190
|
+
await rest(progress);
|
|
1221
2191
|
}
|
|
1222
2192
|
function awaitingAnswer(locator) {
|
|
1223
2193
|
let release = () => { };
|
|
1224
2194
|
const awaited = new Promise((resolve) => {
|
|
1225
2195
|
release = resolve;
|
|
1226
2196
|
});
|
|
1227
|
-
answering.set(locator, awaited);
|
|
2197
|
+
answering.set(locator, { generation, answered: awaited });
|
|
1228
2198
|
return () => {
|
|
1229
|
-
if (answering.get(locator) === awaited)
|
|
2199
|
+
if (answering.get(locator)?.answered === awaited)
|
|
1230
2200
|
answering.delete(locator);
|
|
1231
2201
|
release();
|
|
1232
2202
|
};
|
|
@@ -1259,7 +2229,7 @@ export function composeRuntime(options) {
|
|
|
1259
2229
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
1260
2230
|
};
|
|
1261
2231
|
const reclassified = sameMembership(declared, held.confirmed.record)
|
|
1262
|
-
?
|
|
2232
|
+
? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
|
|
1263
2233
|
: readReclassified(declared, held.confirmed);
|
|
1264
2234
|
const record = reclassified.kind === 'staged'
|
|
1265
2235
|
? declared
|
|
@@ -1267,12 +2237,10 @@ export function composeRuntime(options) {
|
|
|
1267
2237
|
...declared,
|
|
1268
2238
|
typedSelection: held.confirmed.record.typedSelection,
|
|
1269
2239
|
};
|
|
1270
|
-
const read = reclassified.kind === 'deferred'
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
queue: held.queue,
|
|
1275
|
-
});
|
|
2240
|
+
const read = reclassified.kind === 'deferred'
|
|
2241
|
+
? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
|
|
2242
|
+
: reclassified;
|
|
2243
|
+
await local.write({ next: record, previous: held.confirmed.record });
|
|
1276
2244
|
if (read.kind === 'staged') {
|
|
1277
2245
|
replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
|
|
1278
2246
|
if (declarationRefusals.delete(room.locator)) {
|
|
@@ -1299,7 +2267,11 @@ export function composeRuntime(options) {
|
|
|
1299
2267
|
});
|
|
1300
2268
|
}
|
|
1301
2269
|
async function scoped(result, room, local) {
|
|
1302
|
-
scopedRefusals.set(room.locator, {
|
|
2270
|
+
scopedRefusals.set(room.locator, {
|
|
2271
|
+
code: result.code,
|
|
2272
|
+
fields: result.fields,
|
|
2273
|
+
retryable: result.retryable,
|
|
2274
|
+
});
|
|
1303
2275
|
let scopedAway = room;
|
|
1304
2276
|
if (result.scope === 'model')
|
|
1305
2277
|
await retainModel(local, room, 'collection', {
|
|
@@ -1336,24 +2308,27 @@ export function composeRuntime(options) {
|
|
|
1336
2308
|
roomVersion: record.roomVersion,
|
|
1337
2309
|
documents: record.documents,
|
|
1338
2310
|
revisions: record.revisions,
|
|
1339
|
-
attachments: record.attachments,
|
|
1340
2311
|
},
|
|
1341
2312
|
];
|
|
1342
2313
|
});
|
|
1343
2314
|
}
|
|
1344
|
-
async function synchronize(listed, local, progress, attempt) {
|
|
1345
|
-
const
|
|
1346
|
-
const
|
|
2315
|
+
async function synchronize(listed, local, progress, attempt, live) {
|
|
2316
|
+
const acquired = listed.filter(acquires);
|
|
2317
|
+
const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
|
|
2318
|
+
if (!live())
|
|
2319
|
+
return;
|
|
2320
|
+
const ready = acquired.filter((_, index) => {
|
|
1347
2321
|
const outcome = prepared[index];
|
|
1348
2322
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1349
2323
|
});
|
|
1350
2324
|
if (attempt === 'opened') {
|
|
1351
|
-
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
2325
|
+
options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
|
|
1352
2326
|
handed = true;
|
|
1353
2327
|
}
|
|
2328
|
+
discovering?.release();
|
|
1354
2329
|
const loaded = await Promise.allSettled(ready
|
|
1355
2330
|
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1356
|
-
.map((room) => loadRoom(room, local, progress)));
|
|
2331
|
+
.map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
|
|
1357
2332
|
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1358
2333
|
if (failed !== undefined)
|
|
1359
2334
|
throw failed.reason;
|
|
@@ -1382,14 +2357,14 @@ export function composeRuntime(options) {
|
|
|
1382
2357
|
}
|
|
1383
2358
|
return true;
|
|
1384
2359
|
}
|
|
1385
|
-
async function caughtUp(room, local, progress) {
|
|
2360
|
+
async function caughtUp(room, local, progress, live) {
|
|
1386
2361
|
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1387
|
-
if (held === undefined || authorization === undefined)
|
|
2362
|
+
if (held === undefined || authorization === undefined || !live())
|
|
1388
2363
|
return;
|
|
1389
2364
|
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
1390
2365
|
if (!behind)
|
|
1391
2366
|
return;
|
|
1392
|
-
const withheld = await advance(room, local, authorization);
|
|
2367
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1393
2368
|
if (withheld !== null)
|
|
1394
2369
|
await withhold(withheld, room, local, progress);
|
|
1395
2370
|
}
|
|
@@ -1399,39 +2374,46 @@ export function composeRuntime(options) {
|
|
|
1399
2374
|
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1400
2375
|
});
|
|
1401
2376
|
}
|
|
1402
|
-
async function loadRoom(room, local, progress) {
|
|
1403
|
-
if (
|
|
2377
|
+
async function loadRoom(room, local, progress, live) {
|
|
2378
|
+
if (!live() || authorization === undefined)
|
|
1404
2379
|
return;
|
|
1405
2380
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1406
|
-
await reload(room, local, progress);
|
|
2381
|
+
await reload(room, local, progress, live);
|
|
1407
2382
|
return;
|
|
1408
2383
|
}
|
|
1409
|
-
const withheld = await advance(room, local, authorization);
|
|
2384
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1410
2385
|
if (withheld !== null) {
|
|
1411
2386
|
await withhold(withheld, room, local, progress);
|
|
1412
2387
|
return;
|
|
1413
2388
|
}
|
|
1414
|
-
await caughtUp(room, local, progress);
|
|
2389
|
+
await caughtUp(room, local, progress, live);
|
|
1415
2390
|
}
|
|
1416
|
-
async function reload(room, local, progress) {
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
2391
|
+
async function reload(room, local, progress, live) {
|
|
2392
|
+
try {
|
|
2393
|
+
if (!live() || authorization === undefined)
|
|
2394
|
+
return;
|
|
2395
|
+
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
2396
|
+
if (!live())
|
|
2397
|
+
return;
|
|
2398
|
+
if (bootstrapped.kind !== 'staged') {
|
|
2399
|
+
await withhold(bootstrapped, room, local, progress);
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
2403
|
+
await caughtUp(room, local, progress, live);
|
|
2404
|
+
}
|
|
2405
|
+
finally {
|
|
2406
|
+
caughtUpTo(room.locator);
|
|
1425
2407
|
}
|
|
1426
|
-
await publish(local, room, bootstrapped.confirmed);
|
|
1427
|
-
await caughtUp(room, local, progress);
|
|
1428
2408
|
}
|
|
1429
|
-
async function discoverRooms(local, progress, attempt) {
|
|
2409
|
+
async function discoverRooms(local, progress, attempt, live) {
|
|
1430
2410
|
if (closed || authorization === undefined) {
|
|
1431
2411
|
notifyReady();
|
|
1432
2412
|
return;
|
|
1433
2413
|
}
|
|
1434
2414
|
moveTo(transferring(status));
|
|
2415
|
+
discoveries += 1;
|
|
2416
|
+
const started = discoveries;
|
|
1435
2417
|
let result;
|
|
1436
2418
|
try {
|
|
1437
2419
|
result = await options.adapter.discover(authorization);
|
|
@@ -1439,7 +2421,8 @@ export function composeRuntime(options) {
|
|
|
1439
2421
|
catch {
|
|
1440
2422
|
result = { kind: 'transport-uncertainty' };
|
|
1441
2423
|
}
|
|
1442
|
-
if (
|
|
2424
|
+
if (!live()) {
|
|
2425
|
+
progress.complete = false;
|
|
1443
2426
|
notifyReady();
|
|
1444
2427
|
return;
|
|
1445
2428
|
}
|
|
@@ -1458,7 +2441,11 @@ export function composeRuntime(options) {
|
|
|
1458
2441
|
}
|
|
1459
2442
|
if (result.kind === 'contract-unavailable') {
|
|
1460
2443
|
journal.record(`connection contract unavailable ${result.code}`);
|
|
1461
|
-
await refuse('missing-required-guarantee', {
|
|
2444
|
+
await refuse('missing-required-guarantee', {
|
|
2445
|
+
code: result.code,
|
|
2446
|
+
fields: result.fields,
|
|
2447
|
+
retryable: result.retryable,
|
|
2448
|
+
});
|
|
1462
2449
|
notifyReady();
|
|
1463
2450
|
return;
|
|
1464
2451
|
}
|
|
@@ -1493,6 +2480,7 @@ export function composeRuntime(options) {
|
|
|
1493
2480
|
}
|
|
1494
2481
|
const previousCapabilities = capabilities;
|
|
1495
2482
|
capabilities = result.capabilities;
|
|
2483
|
+
outcomeRetention = result.retention.outcome;
|
|
1496
2484
|
notifyReady();
|
|
1497
2485
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1498
2486
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
@@ -1505,8 +2493,7 @@ export function composeRuntime(options) {
|
|
|
1505
2493
|
member = signedIn;
|
|
1506
2494
|
emitMember();
|
|
1507
2495
|
}
|
|
1508
|
-
await applyDiscovery(result.rooms, local);
|
|
1509
|
-
await synchronize(result.rooms, local, progress, attempt);
|
|
2496
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
1510
2497
|
}
|
|
1511
2498
|
async function ensureAuthorization() {
|
|
1512
2499
|
if (authorization !== undefined)
|
|
@@ -1520,32 +2507,64 @@ export function composeRuntime(options) {
|
|
|
1520
2507
|
throw error;
|
|
1521
2508
|
}
|
|
1522
2509
|
}
|
|
1523
|
-
function
|
|
2510
|
+
async function admittedRead(work) {
|
|
2511
|
+
if (roomReads.generation !== generation) {
|
|
2512
|
+
roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
2513
|
+
}
|
|
2514
|
+
const reads = roomReads;
|
|
2515
|
+
while (reads.free === 0)
|
|
2516
|
+
await new Promise((admit) => reads.waiting.push(admit));
|
|
2517
|
+
reads.free -= 1;
|
|
2518
|
+
try {
|
|
2519
|
+
await work();
|
|
2520
|
+
}
|
|
2521
|
+
finally {
|
|
2522
|
+
reads.free += 1;
|
|
2523
|
+
reads.waiting.shift()?.();
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
function roomTask(locator, work) {
|
|
2527
|
+
return task(roomLane(locator), (live) => admittedRead(() => work(live)));
|
|
2528
|
+
}
|
|
2529
|
+
function task(lane, work) {
|
|
1524
2530
|
if (stopping)
|
|
1525
|
-
return
|
|
1526
|
-
|
|
1527
|
-
|
|
2531
|
+
return Promise.resolve();
|
|
2532
|
+
const held = lanes.get(lane);
|
|
2533
|
+
const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
|
|
2534
|
+
const turn = current.generation;
|
|
2535
|
+
const live = () => turn === generation && continuing();
|
|
2536
|
+
const run = current.chain.then(() => work(live)).catch(() => undefined);
|
|
2537
|
+
current.chain = run;
|
|
2538
|
+
lanes.set(lane, current);
|
|
2539
|
+
working.add(run);
|
|
2540
|
+
void run.finally(() => working.delete(run));
|
|
2541
|
+
return run;
|
|
1528
2542
|
}
|
|
1529
2543
|
function sync(attempt) {
|
|
1530
|
-
return task(async () => {
|
|
1531
|
-
if (
|
|
2544
|
+
return task(OPENING_LANE, async (live) => {
|
|
2545
|
+
if (!live()) {
|
|
1532
2546
|
notifyReady();
|
|
1533
2547
|
return;
|
|
1534
2548
|
}
|
|
1535
2549
|
const progress = { complete: true, reachable: true };
|
|
1536
|
-
|
|
2550
|
+
const gate = discover();
|
|
2551
|
+
if (attempt === 'opened')
|
|
2552
|
+
opened = generation;
|
|
2553
|
+
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
2554
|
+
if (attempt === 'opened')
|
|
2555
|
+
streamed = progress;
|
|
1537
2556
|
complete = false;
|
|
1538
2557
|
recorded = false;
|
|
1539
2558
|
moveTo(transferring(status));
|
|
1540
2559
|
handed = false;
|
|
1541
2560
|
try {
|
|
1542
2561
|
const local = await openLocal();
|
|
1543
|
-
if (
|
|
2562
|
+
if (!live())
|
|
1544
2563
|
return;
|
|
1545
2564
|
await ensureAuthorization();
|
|
1546
|
-
if (
|
|
2565
|
+
if (!live())
|
|
1547
2566
|
return;
|
|
1548
|
-
await discoverRooms(local, progress, attempt);
|
|
2567
|
+
await discoverRooms(local, progress, attempt, live);
|
|
1549
2568
|
}
|
|
1550
2569
|
catch {
|
|
1551
2570
|
progress.complete = false;
|
|
@@ -1553,17 +2572,21 @@ export function composeRuntime(options) {
|
|
|
1553
2572
|
progress.reachable = false;
|
|
1554
2573
|
}
|
|
1555
2574
|
finally {
|
|
2575
|
+
gate.release();
|
|
1556
2576
|
notifyReady();
|
|
1557
2577
|
}
|
|
2578
|
+
if (!live())
|
|
2579
|
+
return;
|
|
1558
2580
|
if (attempt !== 'opened') {
|
|
1559
|
-
await finishOpening(progress);
|
|
2581
|
+
await finishOpening(progress, live);
|
|
1560
2582
|
return;
|
|
1561
2583
|
}
|
|
1562
2584
|
if (handed || closed || progress.reachable)
|
|
1563
2585
|
return;
|
|
1564
2586
|
underway = null;
|
|
2587
|
+
opened = null;
|
|
1565
2588
|
journal.record('connection reopen');
|
|
1566
|
-
await rest(progress);
|
|
2589
|
+
await rest(progress, live);
|
|
1567
2590
|
options.adapter.roomStream?.reopen();
|
|
1568
2591
|
});
|
|
1569
2592
|
}
|
|
@@ -1574,29 +2597,51 @@ export function composeRuntime(options) {
|
|
|
1574
2597
|
void refuse('revoked-authorization');
|
|
1575
2598
|
return;
|
|
1576
2599
|
}
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
2600
|
+
complete = false;
|
|
2601
|
+
if (streamed !== null)
|
|
2602
|
+
streamed.complete = false;
|
|
2603
|
+
moveTo({
|
|
2604
|
+
...status,
|
|
2605
|
+
state: status.state === 'synced' ? 'syncing' : status.state,
|
|
2606
|
+
providerUnreachable: status.providerUnreachable || !opened,
|
|
2607
|
+
});
|
|
1580
2608
|
}
|
|
1581
2609
|
function applyOpening(expired) {
|
|
1582
|
-
|
|
1583
|
-
|
|
2610
|
+
const progress = underway?.generation === generation ? underway.progress : null;
|
|
2611
|
+
const cycle = progress ?? { complete: true, reachable: true };
|
|
2612
|
+
const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
|
|
2613
|
+
if (opened === generation)
|
|
2614
|
+
opened = null;
|
|
2615
|
+
return task(OPENING_LANE, async (live) => {
|
|
1584
2616
|
underway = null;
|
|
1585
|
-
if (
|
|
2617
|
+
if (!live())
|
|
1586
2618
|
return;
|
|
1587
2619
|
if (progress === null) {
|
|
1588
|
-
await
|
|
2620
|
+
await replacing;
|
|
2621
|
+
await rest(cycle, live);
|
|
1589
2622
|
return;
|
|
1590
2623
|
}
|
|
2624
|
+
await roomsCarried(expired);
|
|
2625
|
+
if (!live())
|
|
2626
|
+
return;
|
|
2627
|
+
const replaced = new Set(owed);
|
|
2628
|
+
owed.clear();
|
|
1591
2629
|
try {
|
|
1592
2630
|
const local = await openLocal();
|
|
1593
2631
|
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
1594
|
-
const reloading = held.filter((room) => expired.includes(room.locator)
|
|
1595
|
-
|
|
2632
|
+
const reloading = held.filter((room) => !expired.includes(room.locator) &&
|
|
2633
|
+
(replaced.has(room.locator) || revoked(room.locator)));
|
|
2634
|
+
for (const room of reloading)
|
|
2635
|
+
catching.add(room.locator);
|
|
2636
|
+
readSide.recover();
|
|
2637
|
+
const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
|
|
2638
|
+
const reloaded = [...(await reloads), ...(await replacing)];
|
|
1596
2639
|
if (progress.reachable) {
|
|
1597
|
-
for (const room of held)
|
|
1598
|
-
if (!reloading.includes(room))
|
|
2640
|
+
for (const room of held) {
|
|
2641
|
+
if (!expired.includes(room.locator) && !reloading.includes(room)) {
|
|
1599
2642
|
await confirmModels(room, local);
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
1600
2645
|
}
|
|
1601
2646
|
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1602
2647
|
if (failed !== undefined)
|
|
@@ -1606,21 +2651,21 @@ export function composeRuntime(options) {
|
|
|
1606
2651
|
progress.complete = false;
|
|
1607
2652
|
progress.reachable = false;
|
|
1608
2653
|
}
|
|
1609
|
-
|
|
2654
|
+
if (live())
|
|
2655
|
+
await finishOpening(progress, live);
|
|
1610
2656
|
});
|
|
1611
2657
|
}
|
|
1612
|
-
async function
|
|
1613
|
-
const progress = { complete: true, reachable: true };
|
|
2658
|
+
async function reloadNow(replaced, progress) {
|
|
1614
2659
|
try {
|
|
1615
2660
|
const local = await openLocal();
|
|
1616
|
-
await Promise.allSettled([...discovered.values()]
|
|
1617
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
1618
|
-
.map((room) => reload(room, local, progress)));
|
|
2661
|
+
return await Promise.allSettled([...discovered.values()]
|
|
2662
|
+
.filter((room) => replicas.has(room.locator) && replaced(room))
|
|
2663
|
+
.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
1619
2664
|
}
|
|
1620
2665
|
catch {
|
|
1621
2666
|
progress.reachable = false;
|
|
2667
|
+
return [];
|
|
1622
2668
|
}
|
|
1623
|
-
await rest(progress);
|
|
1624
2669
|
}
|
|
1625
2670
|
async function confirmModels(room, local) {
|
|
1626
2671
|
for (const attached of ['collection', 'typed-selection']) {
|
|
@@ -1631,38 +2676,96 @@ export function composeRuntime(options) {
|
|
|
1631
2676
|
}
|
|
1632
2677
|
}
|
|
1633
2678
|
function submitLater(locator) {
|
|
1634
|
-
|
|
1635
|
-
if (running !== undefined) {
|
|
1636
|
-
running.again = true;
|
|
2679
|
+
if (localSaves)
|
|
1637
2680
|
return;
|
|
2681
|
+
void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
|
|
2682
|
+
}
|
|
2683
|
+
async function submitPass(locator, signal) {
|
|
2684
|
+
const room = discovered.get(locator);
|
|
2685
|
+
if (closed || stopping || discoveryPaused || room === undefined)
|
|
2686
|
+
return 'done';
|
|
2687
|
+
const progress = { complete: true, reachable: true };
|
|
2688
|
+
let next;
|
|
2689
|
+
try {
|
|
2690
|
+
await ensureAuthorization();
|
|
2691
|
+
if (authorization === undefined)
|
|
2692
|
+
return 'done';
|
|
2693
|
+
next = await submitRoom(room, await openLocal(), authorization, progress, signal);
|
|
1638
2694
|
}
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
2695
|
+
catch {
|
|
2696
|
+
progress.reachable = false;
|
|
2697
|
+
next = 'again';
|
|
2698
|
+
}
|
|
2699
|
+
if (signal.aborted)
|
|
2700
|
+
return 'done';
|
|
2701
|
+
await rest(progress);
|
|
2702
|
+
return next === 'again' && !socketOpen ? 'done' : next;
|
|
2703
|
+
}
|
|
2704
|
+
function repairLater(locator, when) {
|
|
2705
|
+
if (!socketOpen || repairing.has(locator))
|
|
2706
|
+
return;
|
|
2707
|
+
void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
|
|
2708
|
+
}
|
|
2709
|
+
async function repairPass(locator, signal) {
|
|
2710
|
+
const room = discovered.get(locator);
|
|
2711
|
+
if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
|
|
2712
|
+
return 'done';
|
|
2713
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2714
|
+
return 'done';
|
|
2715
|
+
const progress = { complete: true, reachable: true };
|
|
2716
|
+
repairing.add(locator);
|
|
2717
|
+
try {
|
|
2718
|
+
const local = await openLocal();
|
|
2719
|
+
await roomTask(locator, async (live) => {
|
|
2720
|
+
const current = () => live() && !signal.aborted;
|
|
2721
|
+
if (!current() || authorization === undefined)
|
|
2722
|
+
return;
|
|
2723
|
+
const withheld = await Promise.race([
|
|
2724
|
+
advance(room, local, authorization, current),
|
|
2725
|
+
abandonedOn(signal),
|
|
2726
|
+
]);
|
|
2727
|
+
if (withheld !== null && current())
|
|
2728
|
+
await withhold(withheld, room, local, progress);
|
|
2729
|
+
});
|
|
2730
|
+
}
|
|
2731
|
+
catch {
|
|
2732
|
+
stale.add(locator);
|
|
2733
|
+
progress.reachable = false;
|
|
2734
|
+
}
|
|
2735
|
+
finally {
|
|
2736
|
+
repairing.delete(locator);
|
|
2737
|
+
}
|
|
2738
|
+
if (signal.aborted)
|
|
2739
|
+
return 'done';
|
|
2740
|
+
await rest(progress);
|
|
2741
|
+
if (!stale.has(locator)) {
|
|
2742
|
+
if ((replicas.get(locator)?.queue.length ?? 0) > 0)
|
|
2743
|
+
submitLater(locator);
|
|
2744
|
+
return 'done';
|
|
2745
|
+
}
|
|
2746
|
+
if (!repairable.has(locator))
|
|
2747
|
+
return 'done';
|
|
2748
|
+
const reset = repairable.get(locator);
|
|
2749
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
2750
|
+
}
|
|
2751
|
+
function resendLater() {
|
|
2752
|
+
if (!socketOpen)
|
|
2753
|
+
return;
|
|
2754
|
+
void recovery.attempt('resend', resendPass, { again: true });
|
|
2755
|
+
}
|
|
2756
|
+
async function resendPass(signal) {
|
|
2757
|
+
if (!socketOpen || closed || stopping || discoveryPaused)
|
|
2758
|
+
return 'done';
|
|
2759
|
+
const reading = { complete: true, reachable: true };
|
|
2760
|
+
try {
|
|
2761
|
+
await conversations.resend(reading);
|
|
2762
|
+
}
|
|
2763
|
+
catch {
|
|
2764
|
+
reading.reachable = false;
|
|
2765
|
+
}
|
|
2766
|
+
if (signal.aborted)
|
|
2767
|
+
return 'done';
|
|
2768
|
+
return reading.reachable && reading.complete ? 'done' : 'again';
|
|
1666
2769
|
}
|
|
1667
2770
|
function submitAll() {
|
|
1668
2771
|
for (const [locator, replica] of replicas) {
|
|
@@ -1670,31 +2773,41 @@ export function composeRuntime(options) {
|
|
|
1670
2773
|
submitLater(locator);
|
|
1671
2774
|
}
|
|
1672
2775
|
}
|
|
1673
|
-
async function finishOpening(progress) {
|
|
2776
|
+
async function finishOpening(progress, live) {
|
|
1674
2777
|
try {
|
|
1675
|
-
if (
|
|
2778
|
+
if (!live()) {
|
|
1676
2779
|
progress.complete = false;
|
|
1677
2780
|
return;
|
|
1678
2781
|
}
|
|
1679
2782
|
submitAll();
|
|
2783
|
+
for (const locator of stale)
|
|
2784
|
+
if (repairable.has(locator))
|
|
2785
|
+
repairLater(locator, {});
|
|
1680
2786
|
await conversations.reconcile();
|
|
1681
2787
|
if (status.providerUnreachable)
|
|
1682
2788
|
conversations.reconnected();
|
|
1683
2789
|
const reading = { complete: true, reachable: true };
|
|
1684
|
-
|
|
1685
|
-
|
|
2790
|
+
if (demand.conversations)
|
|
2791
|
+
await conversations.catchUp(reading);
|
|
2792
|
+
if (progress.reachable && reading.reachable)
|
|
2793
|
+
await conversations.resend(reading);
|
|
2794
|
+
if (!reading.reachable) {
|
|
1686
2795
|
progress.reachable = false;
|
|
2796
|
+
resendLater();
|
|
2797
|
+
}
|
|
1687
2798
|
}
|
|
1688
2799
|
catch {
|
|
1689
2800
|
progress.complete = false;
|
|
1690
2801
|
}
|
|
1691
2802
|
finally {
|
|
1692
|
-
|
|
1693
|
-
|
|
2803
|
+
if (live()) {
|
|
2804
|
+
complete = progress.complete;
|
|
2805
|
+
await rest(progress, live);
|
|
2806
|
+
}
|
|
1694
2807
|
}
|
|
1695
2808
|
}
|
|
1696
|
-
async function rest(progress) {
|
|
1697
|
-
if (
|
|
2809
|
+
async function rest(progress, live = () => !closed) {
|
|
2810
|
+
if (!live())
|
|
1698
2811
|
return;
|
|
1699
2812
|
let lastCompleted = status.lastCompleted;
|
|
1700
2813
|
let synced = complete && stale.size === 0 && !pending();
|
|
@@ -1702,6 +2815,8 @@ export function composeRuntime(options) {
|
|
|
1702
2815
|
const completedAt = new Date().toISOString();
|
|
1703
2816
|
try {
|
|
1704
2817
|
await (await openLocal()).complete(completedAt);
|
|
2818
|
+
if (!live())
|
|
2819
|
+
return;
|
|
1705
2820
|
recorded = true;
|
|
1706
2821
|
lastCompleted = completedAt;
|
|
1707
2822
|
}
|
|
@@ -1710,46 +2825,74 @@ export function composeRuntime(options) {
|
|
|
1710
2825
|
}
|
|
1711
2826
|
}
|
|
1712
2827
|
moveTo({
|
|
1713
|
-
state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
|
|
2828
|
+
state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
|
|
1714
2829
|
lastCompleted,
|
|
1715
2830
|
providerUnreachable: !progress.reachable,
|
|
1716
2831
|
...attention(),
|
|
1717
2832
|
});
|
|
1718
2833
|
}
|
|
2834
|
+
function discover() {
|
|
2835
|
+
let release = () => { };
|
|
2836
|
+
const done = new Promise((resolve) => {
|
|
2837
|
+
release = resolve;
|
|
2838
|
+
});
|
|
2839
|
+
discovering = { generation, done, release };
|
|
2840
|
+
return discovering;
|
|
2841
|
+
}
|
|
1719
2842
|
function applyRoom(locator, roomVersion) {
|
|
1720
2843
|
announced.set(locator, roomVersion);
|
|
1721
|
-
return task(async () => {
|
|
1722
|
-
|
|
1723
|
-
|
|
2844
|
+
return task(roomLane(locator), async (live) => {
|
|
2845
|
+
if (discovering?.generation === generation)
|
|
2846
|
+
await discovering.done;
|
|
2847
|
+
const saving = answering.get(locator);
|
|
2848
|
+
if (saving?.generation === generation)
|
|
2849
|
+
await saving.answered;
|
|
2850
|
+
if (!live() || authorization === undefined)
|
|
1724
2851
|
return;
|
|
1725
2852
|
const room = discovered.get(locator);
|
|
1726
2853
|
const held = replicas.get(locator);
|
|
1727
|
-
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2854
|
+
if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
|
|
1728
2855
|
return;
|
|
1729
2856
|
if (held !== undefined &&
|
|
1730
2857
|
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
1731
2858
|
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
1732
2859
|
return;
|
|
1733
2860
|
}
|
|
1734
|
-
const during = underway;
|
|
2861
|
+
const during = underway?.generation === generation ? underway.progress : null;
|
|
1735
2862
|
const progress = during ?? { complete: true, reachable: true };
|
|
1736
2863
|
if (during === null)
|
|
1737
2864
|
moveTo(transferring(status));
|
|
1738
2865
|
try {
|
|
1739
2866
|
const local = await openLocal();
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
2867
|
+
await admittedRead(async () => {
|
|
2868
|
+
if (!live() || authorization === undefined)
|
|
2869
|
+
return;
|
|
2870
|
+
const withheld = await advance(room, local, authorization, live);
|
|
2871
|
+
if (withheld !== null)
|
|
2872
|
+
await withhold(withheld, room, local, progress);
|
|
2873
|
+
});
|
|
1743
2874
|
}
|
|
1744
2875
|
catch {
|
|
1745
2876
|
stale.add(locator);
|
|
1746
2877
|
}
|
|
1747
2878
|
finally {
|
|
1748
2879
|
if (during === null)
|
|
1749
|
-
await rest(progress);
|
|
2880
|
+
await rest(progress, live);
|
|
1750
2881
|
}
|
|
1751
2882
|
});
|
|
1752
2883
|
}
|
|
2884
|
+
function roomsCarried(replacing) {
|
|
2885
|
+
const replaced = new Set(replacing.map(roomLane));
|
|
2886
|
+
return Promise.all([...lanes]
|
|
2887
|
+
.filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
|
|
2888
|
+
.map(([, held]) => held.chain));
|
|
2889
|
+
}
|
|
2890
|
+
const receipts = {
|
|
2891
|
+
heads: async () => (await openLocal()).receivedHeads(),
|
|
2892
|
+
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
2893
|
+
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
2894
|
+
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
2895
|
+
};
|
|
1753
2896
|
function observeStream() {
|
|
1754
2897
|
const stream = options.adapter.roomStream;
|
|
1755
2898
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -1759,6 +2902,9 @@ export function composeRuntime(options) {
|
|
|
1759
2902
|
return;
|
|
1760
2903
|
if (event.kind === 'opened') {
|
|
1761
2904
|
socketOpen = true;
|
|
2905
|
+
generation += 1;
|
|
2906
|
+
conversations.reopened();
|
|
2907
|
+
recovery.resume();
|
|
1762
2908
|
void sync('opened');
|
|
1763
2909
|
}
|
|
1764
2910
|
else if (event.kind === 'closed')
|
|
@@ -1767,7 +2913,7 @@ export function composeRuntime(options) {
|
|
|
1767
2913
|
void applyOpening(event.expired);
|
|
1768
2914
|
else
|
|
1769
2915
|
void applyRoom(event.locator, event.roomVersion);
|
|
1770
|
-
});
|
|
2916
|
+
}, receipts);
|
|
1771
2917
|
}
|
|
1772
2918
|
function writesHeld() {
|
|
1773
2919
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -1782,6 +2928,7 @@ export function composeRuntime(options) {
|
|
|
1782
2928
|
haltedRefusal = false;
|
|
1783
2929
|
if (mode !== serviceMode) {
|
|
1784
2930
|
serviceMode = mode;
|
|
2931
|
+
queueReadiness();
|
|
1785
2932
|
journal.record(`service mode ${mode}`);
|
|
1786
2933
|
for (const listener of serviceModeListeners)
|
|
1787
2934
|
listener(mode);
|
|
@@ -1794,6 +2941,8 @@ export function composeRuntime(options) {
|
|
|
1794
2941
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
1795
2942
|
return;
|
|
1796
2943
|
withdrawConversations = stream.observe((event) => {
|
|
2944
|
+
if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
|
|
2945
|
+
queueReadiness();
|
|
1797
2946
|
if (conversations.receive(event)) {
|
|
1798
2947
|
void ready.then(scheduleCurrency);
|
|
1799
2948
|
return;
|
|
@@ -1805,46 +2954,47 @@ export function composeRuntime(options) {
|
|
|
1805
2954
|
});
|
|
1806
2955
|
}
|
|
1807
2956
|
function scheduleCurrency() {
|
|
1808
|
-
if (currencyQueued || closed || stopping)
|
|
2957
|
+
if (currencyQueued === generation || closed || stopping)
|
|
1809
2958
|
return;
|
|
1810
|
-
currencyQueued =
|
|
1811
|
-
void task(runCurrency);
|
|
2959
|
+
currencyQueued = generation;
|
|
2960
|
+
void task(CONVERSATION_LANE, runCurrency);
|
|
1812
2961
|
}
|
|
1813
|
-
async function runCurrency() {
|
|
1814
|
-
currencyQueued
|
|
1815
|
-
|
|
2962
|
+
async function runCurrency(live) {
|
|
2963
|
+
if (currencyQueued === generation)
|
|
2964
|
+
currencyQueued = null;
|
|
2965
|
+
if (!live())
|
|
1816
2966
|
return;
|
|
1817
2967
|
await conversations.reconcile();
|
|
1818
|
-
if (conversations.current()) {
|
|
2968
|
+
if (conversations.current() || !demand.conversations) {
|
|
1819
2969
|
show();
|
|
1820
2970
|
return;
|
|
1821
2971
|
}
|
|
1822
|
-
const
|
|
1823
|
-
|
|
1824
|
-
moveTo(transferring(resting));
|
|
2972
|
+
const progress = { complete: false, reachable: !status.providerUnreachable };
|
|
2973
|
+
moveTo(transferring(status));
|
|
1825
2974
|
try {
|
|
1826
2975
|
await conversations.catchUp(progress);
|
|
1827
2976
|
}
|
|
1828
2977
|
catch {
|
|
1829
2978
|
progress.reachable = false;
|
|
1830
2979
|
}
|
|
1831
|
-
|
|
1832
|
-
if (!closed) {
|
|
1833
|
-
moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
|
|
1834
|
-
}
|
|
1835
|
-
}
|
|
2980
|
+
await rest(progress, live);
|
|
1836
2981
|
}
|
|
1837
2982
|
function close() {
|
|
1838
2983
|
if (closed)
|
|
1839
2984
|
return;
|
|
1840
2985
|
closed = true;
|
|
2986
|
+
if (narrowing !== null)
|
|
2987
|
+
clearTimeout(narrowing);
|
|
2988
|
+
if (rechecking !== null)
|
|
2989
|
+
clearInterval(rechecking);
|
|
2990
|
+
readinessWatches.clear();
|
|
1841
2991
|
withdrawStream?.();
|
|
1842
2992
|
withdrawStream = undefined;
|
|
1843
2993
|
withdrawConversations?.();
|
|
1844
2994
|
withdrawConversations = undefined;
|
|
1845
2995
|
withdrawServiceMode?.();
|
|
1846
2996
|
conversations.close();
|
|
1847
|
-
|
|
2997
|
+
void recovery.retire();
|
|
1848
2998
|
store?.close();
|
|
1849
2999
|
void opening?.then((opened) => {
|
|
1850
3000
|
opened.close();
|
|
@@ -1862,9 +3012,8 @@ export function composeRuntime(options) {
|
|
|
1862
3012
|
}
|
|
1863
3013
|
async function stop() {
|
|
1864
3014
|
stopping = true;
|
|
1865
|
-
|
|
1866
|
-
await
|
|
1867
|
-
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
3015
|
+
await recovery.retire();
|
|
3016
|
+
await Promise.all(working);
|
|
1868
3017
|
await mutations;
|
|
1869
3018
|
close();
|
|
1870
3019
|
}
|
|
@@ -1886,14 +3035,69 @@ export function composeRuntime(options) {
|
|
|
1886
3035
|
return planOf(input, roomFacts(reach), planReach(reach));
|
|
1887
3036
|
}
|
|
1888
3037
|
function watchPlan(plan, deliver) {
|
|
1889
|
-
return readSide.watch(plan, identityOf(plan),
|
|
3038
|
+
return readSide.watch(plan, identityOf(plan), () => {
|
|
3039
|
+
const cover = covered(plan);
|
|
3040
|
+
awaitPreparation(cover);
|
|
3041
|
+
return cover;
|
|
3042
|
+
}, deliver);
|
|
1890
3043
|
}
|
|
1891
3044
|
async function searched(input, reach) {
|
|
1892
3045
|
const plan = searchPlanOf(input);
|
|
1893
|
-
const
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
3046
|
+
const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
|
|
3047
|
+
for (;;) {
|
|
3048
|
+
const cover = covered(filter);
|
|
3049
|
+
awaitPreparation(cover);
|
|
3050
|
+
const pending = 'pending' in cover ? [...cover.pending] : [];
|
|
3051
|
+
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
3052
|
+
const held = covered({
|
|
3053
|
+
...filter,
|
|
3054
|
+
rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
|
|
3055
|
+
});
|
|
3056
|
+
const covering = 'pending' in held ? [] : held.coverage.rooms;
|
|
3057
|
+
const unindexed = covering
|
|
3058
|
+
.filter(({ room }) => !searchIndex.rooms().has(room))
|
|
3059
|
+
.map(({ room }) => room);
|
|
3060
|
+
for (const room of unindexed)
|
|
3061
|
+
pending.push({ room, awaits: 'index' });
|
|
3062
|
+
const listed = conversations.searchable();
|
|
3063
|
+
const named = reach !== null && !reach.conversations
|
|
3064
|
+
? []
|
|
3065
|
+
: (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
|
|
3066
|
+
const waiting = named.filter((one) => !searchIndex.conversations().has(one));
|
|
3067
|
+
for (const conversation of waiting)
|
|
3068
|
+
pending.push({ conversation, awaits: 'index' });
|
|
3069
|
+
if (unindexed.length > 0 || waiting.length > 0)
|
|
3070
|
+
index(unindexed, waiting);
|
|
3071
|
+
const complete = pending.length === 0 && !('pending' in cover);
|
|
3072
|
+
const answering = complete
|
|
3073
|
+
? filter
|
|
3074
|
+
: {
|
|
3075
|
+
...filter,
|
|
3076
|
+
rooms: covering
|
|
3077
|
+
.filter(({ room }) => searchIndex.rooms().has(room))
|
|
3078
|
+
.map(({ room }) => room),
|
|
3079
|
+
};
|
|
3080
|
+
const answered = covered(answering);
|
|
3081
|
+
if ('pending' in answered)
|
|
3082
|
+
continue;
|
|
3083
|
+
const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
|
|
3084
|
+
const seen = searchIndex.changes();
|
|
3085
|
+
await searchIndex.caughtUp();
|
|
3086
|
+
const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(answering);
|
|
3087
|
+
if (selected !== null && 'pending' in selected)
|
|
3088
|
+
continue;
|
|
3089
|
+
if (searchIndex.changes() !== seen ||
|
|
3090
|
+
JSON.stringify(covered(answering)) !== JSON.stringify(answered))
|
|
3091
|
+
continue;
|
|
3092
|
+
const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, complete ? null : new Set(answering.rooms));
|
|
3093
|
+
const coverage = {
|
|
3094
|
+
rooms: answered.coverage.rooms,
|
|
3095
|
+
conversations: searchedConversations,
|
|
3096
|
+
index: searchIndex.generation(),
|
|
3097
|
+
overlay: answered.coverage.overlay,
|
|
3098
|
+
};
|
|
3099
|
+
return complete ? { ...hits, coverage } : { pending, ...hits, coverage };
|
|
3100
|
+
}
|
|
1897
3101
|
}
|
|
1898
3102
|
const watch = (topic, input, deliver) => {
|
|
1899
3103
|
if (topic === 'room-drafts') {
|
|
@@ -1918,6 +3122,8 @@ export function composeRuntime(options) {
|
|
|
1918
3122
|
}
|
|
1919
3123
|
if (topic === 'conversations')
|
|
1920
3124
|
return conversations.watchList(deliver);
|
|
3125
|
+
if (topic === 'agents')
|
|
3126
|
+
return conversations.watchAgents(deliver);
|
|
1921
3127
|
if (topic === 'presentation') {
|
|
1922
3128
|
const listener = {
|
|
1923
3129
|
app: input[0],
|
|
@@ -1937,6 +3143,9 @@ export function composeRuntime(options) {
|
|
|
1937
3143
|
presentationListeners.delete(listener);
|
|
1938
3144
|
};
|
|
1939
3145
|
}
|
|
3146
|
+
if (topic === 'readiness') {
|
|
3147
|
+
return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
|
|
3148
|
+
}
|
|
1940
3149
|
if (topic === 'rooms') {
|
|
1941
3150
|
roomListeners.add(deliver);
|
|
1942
3151
|
deliver(listed());
|
|
@@ -1996,7 +3205,7 @@ export function composeRuntime(options) {
|
|
|
1996
3205
|
rooms() {
|
|
1997
3206
|
return listed();
|
|
1998
3207
|
},
|
|
1999
|
-
query: ((input) =>
|
|
3208
|
+
query: ((input) => rowsQuery(planned(input, null))),
|
|
2000
3209
|
search: (input) => searched(input, null),
|
|
2001
3210
|
types(locator) {
|
|
2002
3211
|
return {
|
|
@@ -2025,20 +3234,20 @@ export function composeRuntime(options) {
|
|
|
2025
3234
|
return exclusive(async () => {
|
|
2026
3235
|
const parts = readParts(input);
|
|
2027
3236
|
if (path === '' || normalize(path) !== path)
|
|
2028
|
-
throw
|
|
3237
|
+
throw runtimeError('room-draft-malformed');
|
|
2029
3238
|
const local = await openLocal();
|
|
2030
3239
|
const current = roomDrafts.get(room)?.get(path);
|
|
2031
3240
|
if ((current?.revision ?? null) !== revision) {
|
|
2032
|
-
return
|
|
2033
|
-
outcome: 'stale',
|
|
2034
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
2035
|
-
};
|
|
3241
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2036
3242
|
}
|
|
3243
|
+
const file = parts.file === undefined
|
|
3244
|
+
? undefined
|
|
3245
|
+
: await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
|
|
2037
3246
|
const record = {
|
|
2038
3247
|
locator: room,
|
|
2039
3248
|
path,
|
|
2040
3249
|
revision: roomDraftRevision(),
|
|
2041
|
-
parts: held(current?.parts, parts),
|
|
3250
|
+
parts: held(current?.parts, parts, file),
|
|
2042
3251
|
returned: current?.returned ?? null,
|
|
2043
3252
|
};
|
|
2044
3253
|
await local.keepRoomDraft(record);
|
|
@@ -2052,10 +3261,7 @@ export function composeRuntime(options) {
|
|
|
2052
3261
|
const local = await openLocal();
|
|
2053
3262
|
const current = roomDrafts.get(room)?.get(path);
|
|
2054
3263
|
if (current === undefined || current.revision !== revision) {
|
|
2055
|
-
return
|
|
2056
|
-
outcome: 'stale',
|
|
2057
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
2058
|
-
};
|
|
3264
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2059
3265
|
}
|
|
2060
3266
|
await local.discardRoomDraft(room, path);
|
|
2061
3267
|
forgetRoomDraft(room, path);
|
|
@@ -2090,6 +3296,10 @@ export function composeRuntime(options) {
|
|
|
2090
3296
|
history(locator, path, query) {
|
|
2091
3297
|
return readHistory(locator, path, query);
|
|
2092
3298
|
},
|
|
3299
|
+
binaryFile: {
|
|
3300
|
+
read: readBinaryPath,
|
|
3301
|
+
readReference: readBinaryReference,
|
|
3302
|
+
},
|
|
2093
3303
|
call(name, input) {
|
|
2094
3304
|
return invokeAction(name, input);
|
|
2095
3305
|
},
|
|
@@ -2102,30 +3312,54 @@ export function composeRuntime(options) {
|
|
|
2102
3312
|
return journal.export();
|
|
2103
3313
|
},
|
|
2104
3314
|
persistence() {
|
|
2105
|
-
return
|
|
3315
|
+
return persistence === 'granted';
|
|
3316
|
+
},
|
|
3317
|
+
async retry() {
|
|
3318
|
+
journal.record('retry');
|
|
3319
|
+
unindexable.clear();
|
|
3320
|
+
queueReadiness();
|
|
3321
|
+
resume();
|
|
2106
3322
|
},
|
|
2107
3323
|
observe: observing(watch),
|
|
2108
3324
|
async reauthorize() {
|
|
2109
3325
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2110
3326
|
return;
|
|
2111
3327
|
journal.record('connection reauthorize');
|
|
2112
|
-
await
|
|
3328
|
+
await Promise.all([...lanes.values()]
|
|
3329
|
+
.filter((held) => held.generation === generation)
|
|
3330
|
+
.map((held) => held.chain));
|
|
2113
3331
|
await mutations;
|
|
2114
3332
|
authorization = undefined;
|
|
2115
3333
|
discoveryPaused = false;
|
|
2116
3334
|
if (options.adapter.roomStream === undefined)
|
|
2117
3335
|
await sync('unstreamed');
|
|
2118
3336
|
else
|
|
2119
|
-
await task(ensureAuthorization);
|
|
3337
|
+
await task(OPENING_LANE, ensureAuthorization);
|
|
2120
3338
|
},
|
|
2121
3339
|
};
|
|
2122
3340
|
const reachable = {
|
|
2123
3341
|
md,
|
|
2124
3342
|
watch,
|
|
3343
|
+
referrerScope: (referrer) => options.adapter.referrerScope(referrer),
|
|
2125
3344
|
plan: planned,
|
|
2126
|
-
query: (plan) =>
|
|
3345
|
+
query: (plan) => rowsQuery(plan),
|
|
2127
3346
|
watchPlan,
|
|
2128
3347
|
search: searched,
|
|
3348
|
+
watchReadiness,
|
|
3349
|
+
};
|
|
3350
|
+
return {
|
|
3351
|
+
md,
|
|
3352
|
+
stop,
|
|
3353
|
+
lose: loseRooms,
|
|
3354
|
+
watch,
|
|
3355
|
+
acquire,
|
|
3356
|
+
persisted(answer) {
|
|
3357
|
+
if (answer === persistence)
|
|
3358
|
+
return;
|
|
3359
|
+
persistence = answer;
|
|
3360
|
+
journal.record(`persistence ${answer}`);
|
|
3361
|
+
queueReadiness();
|
|
3362
|
+
},
|
|
3363
|
+
within: (reach, port) => narrowed(reachable, reach, port),
|
|
2129
3364
|
};
|
|
2130
|
-
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2131
3365
|
}
|