@agentshouse/mdruntime 0.1.0-c5b50992a4193 → 0.1.0-c5d3fa6fb67bc
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 +265 -210
- 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 +80 -20
- 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 +25 -12
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +68 -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 +42 -15
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +1636 -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 -2
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +60 -17
- 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 +7 -3
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +77 -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 +28 -17
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +8 -6
- package/dist/read-side.d.ts +13 -6
- package/dist/read-side.d.ts.map +1 -1
- package/dist/read-side.js +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 +124 -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 +75 -18
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +539 -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 +141 -33
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +61 -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,27 +158,49 @@ 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;
|
|
106
165
|
let openSignals = 0;
|
|
107
166
|
const serviceModeListeners = new Set();
|
|
167
|
+
let member = null;
|
|
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);
|
|
108
185
|
const conversations = createConversationStore({
|
|
109
186
|
agents: declaredAgents,
|
|
110
|
-
documents: (locator) =>
|
|
187
|
+
documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
|
|
111
188
|
call: (name, input) => invokeAction(name, input),
|
|
112
189
|
durably,
|
|
113
190
|
stream: () => options.adapter.conversations,
|
|
114
191
|
publish: publishChangeSet,
|
|
192
|
+
online: () => !status.providerUnreachable && refusal === null,
|
|
115
193
|
closed: () => closed,
|
|
194
|
+
unanswered: resendLater,
|
|
195
|
+
expired: (operation) => pastHorizon(operation),
|
|
116
196
|
});
|
|
117
197
|
const searchIndex = createSearchIndex({
|
|
118
|
-
|
|
198
|
+
room: (room) => readSide.room(room),
|
|
119
199
|
document: (room, path) => readSide.document(room, path),
|
|
120
|
-
messages: () => conversations.messages(),
|
|
121
|
-
message: (conversation, identity) => conversations.message(conversation, identity),
|
|
122
200
|
});
|
|
201
|
+
const roomsToIndex = new Set();
|
|
202
|
+
const conversationsToIndex = new Set();
|
|
203
|
+
let indexing = false;
|
|
123
204
|
const journal = createLog();
|
|
124
205
|
let notifyReady = () => { };
|
|
125
206
|
const ready = new Promise((resolve) => {
|
|
@@ -183,7 +264,9 @@ export function composeRuntime(options) {
|
|
|
183
264
|
}
|
|
184
265
|
}
|
|
185
266
|
function pending() {
|
|
186
|
-
return
|
|
267
|
+
return (!localSaves &&
|
|
268
|
+
(queuesWithoutReplica.size > 0 ||
|
|
269
|
+
[...replicas.values()].some((replica) => replica.queue.length > 0)));
|
|
187
270
|
}
|
|
188
271
|
function attention() {
|
|
189
272
|
const attentionRequired = [];
|
|
@@ -196,9 +279,16 @@ export function composeRuntime(options) {
|
|
|
196
279
|
returned: structuredClone(record.returned),
|
|
197
280
|
});
|
|
198
281
|
}
|
|
282
|
+
for (const [room, refusal] of sourceRefusals)
|
|
283
|
+
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
199
284
|
attentionRequired.sort((left, right) => {
|
|
200
285
|
if (left.room !== right.room)
|
|
201
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
|
+
}
|
|
202
292
|
if (left.path !== right.path)
|
|
203
293
|
return left.path < right.path ? -1 : 1;
|
|
204
294
|
return 0;
|
|
@@ -212,8 +302,21 @@ export function composeRuntime(options) {
|
|
|
212
302
|
}
|
|
213
303
|
return { attentionRequired, reset };
|
|
214
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
|
+
}
|
|
215
314
|
function listed() {
|
|
216
|
-
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
|
+
}));
|
|
217
320
|
}
|
|
218
321
|
function roomFacts(reach = null) {
|
|
219
322
|
const held = [...rooms.values()]
|
|
@@ -233,18 +336,26 @@ export function composeRuntime(options) {
|
|
|
233
336
|
return { listed: roomsListed, rooms: held };
|
|
234
337
|
}
|
|
235
338
|
function emitRooms() {
|
|
339
|
+
readSide.recover();
|
|
340
|
+
queueReadiness();
|
|
236
341
|
const value = listed();
|
|
237
342
|
for (const listener of roomListeners)
|
|
238
343
|
listener(value);
|
|
239
344
|
}
|
|
240
345
|
function emitRefusal() {
|
|
346
|
+
queueReadiness();
|
|
241
347
|
for (const listener of refusalListeners)
|
|
242
348
|
listener(refusal);
|
|
243
349
|
}
|
|
244
350
|
function emitStorageLoss() {
|
|
351
|
+
queueReadiness();
|
|
245
352
|
for (const listener of storageLossListeners)
|
|
246
353
|
listener(storageLoss);
|
|
247
354
|
}
|
|
355
|
+
function emitMember() {
|
|
356
|
+
for (const listener of memberListeners)
|
|
357
|
+
listener(member);
|
|
358
|
+
}
|
|
248
359
|
function moveTo(next) {
|
|
249
360
|
if (closed)
|
|
250
361
|
return;
|
|
@@ -254,7 +365,8 @@ export function composeRuntime(options) {
|
|
|
254
365
|
function show() {
|
|
255
366
|
if (closed)
|
|
256
367
|
return;
|
|
257
|
-
|
|
368
|
+
queueReadiness();
|
|
369
|
+
const next = status.state === 'synced' && demand.conversations && !conversations.current()
|
|
258
370
|
? { ...status, state: 'syncing' }
|
|
259
371
|
: status;
|
|
260
372
|
if (same(shown, next))
|
|
@@ -264,6 +376,8 @@ export function composeRuntime(options) {
|
|
|
264
376
|
listener(shown);
|
|
265
377
|
}
|
|
266
378
|
function entryOf(batch) {
|
|
379
|
+
if (localSaves)
|
|
380
|
+
return Object.freeze({ batch, state: 'local' });
|
|
267
381
|
const state = pendings.get(batch.id);
|
|
268
382
|
if (state?.accepted !== undefined) {
|
|
269
383
|
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
@@ -271,10 +385,7 @@ export function composeRuntime(options) {
|
|
|
271
385
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
272
386
|
}
|
|
273
387
|
function entriesOf(locator) {
|
|
274
|
-
|
|
275
|
-
if (replica === undefined)
|
|
276
|
-
return [];
|
|
277
|
-
return replica.queue.map(entryOf);
|
|
388
|
+
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
|
|
278
389
|
}
|
|
279
390
|
function emitQueue(locator) {
|
|
280
391
|
const entries = entriesOf(locator);
|
|
@@ -284,12 +395,477 @@ export function composeRuntime(options) {
|
|
|
284
395
|
}
|
|
285
396
|
}
|
|
286
397
|
function applyChangeSet(set) {
|
|
287
|
-
|
|
288
|
-
|
|
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(() => { });
|
|
289
864
|
}
|
|
290
865
|
function publishChangeSet(set) {
|
|
291
866
|
applyChangeSet(set);
|
|
292
|
-
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)))
|
|
293
869
|
emitRooms();
|
|
294
870
|
for (const locator of set.queues)
|
|
295
871
|
emitQueue(locator);
|
|
@@ -301,34 +877,7 @@ export function composeRuntime(options) {
|
|
|
301
877
|
emitPresentation(app);
|
|
302
878
|
conversations.changed(set);
|
|
303
879
|
}
|
|
304
|
-
function stopTimer() {
|
|
305
|
-
if (resetTimer === undefined)
|
|
306
|
-
return;
|
|
307
|
-
clearTimeout(resetTimer);
|
|
308
|
-
resetTimer = undefined;
|
|
309
|
-
}
|
|
310
|
-
function armTimer() {
|
|
311
|
-
stopTimer();
|
|
312
|
-
if (closed || stopping)
|
|
313
|
-
return;
|
|
314
|
-
let earliest = null;
|
|
315
|
-
for (const { submission } of pendings.values()) {
|
|
316
|
-
if (submission.state !== 'reset')
|
|
317
|
-
continue;
|
|
318
|
-
const at = Date.parse(submission.reset);
|
|
319
|
-
if (earliest === null || at < earliest)
|
|
320
|
-
earliest = at;
|
|
321
|
-
}
|
|
322
|
-
if (earliest === null)
|
|
323
|
-
return;
|
|
324
|
-
// A Save House held back goes again only once the reset its answer named has passed.
|
|
325
|
-
resetTimer = setTimeout(() => {
|
|
326
|
-
resetTimer = undefined;
|
|
327
|
-
submitAll();
|
|
328
|
-
}, Math.max(0, earliest - Date.now()));
|
|
329
|
-
}
|
|
330
880
|
async function refuse(reason, provider = null) {
|
|
331
|
-
stopTimer();
|
|
332
881
|
discoveryPaused = true;
|
|
333
882
|
try {
|
|
334
883
|
if (reason === 'revoked-authorization') {
|
|
@@ -336,10 +885,7 @@ export function composeRuntime(options) {
|
|
|
336
885
|
const revoked = { status: 'unavailable', reason: 'revoked' };
|
|
337
886
|
try {
|
|
338
887
|
const local = await openLocal();
|
|
339
|
-
await local.revokeModels([...replicas.values()].map((replica) =>
|
|
340
|
-
record: replica.confirmed.record,
|
|
341
|
-
queue: replica.queue,
|
|
342
|
-
})));
|
|
888
|
+
await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
|
|
343
889
|
}
|
|
344
890
|
catch {
|
|
345
891
|
journal.record('model revocation persistence failed');
|
|
@@ -363,10 +909,7 @@ export function composeRuntime(options) {
|
|
|
363
909
|
model: revoked,
|
|
364
910
|
selectionModel: revoked,
|
|
365
911
|
};
|
|
366
|
-
|
|
367
|
-
replicas.set(locator, read.kind === 'staged'
|
|
368
|
-
? replicaOf(read.confirmed, before.queue, before)
|
|
369
|
-
: { ...before, confirmed: { ...before.confirmed, record } });
|
|
912
|
+
replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
|
|
370
913
|
}
|
|
371
914
|
rooms.set(locator, {
|
|
372
915
|
...room,
|
|
@@ -383,7 +926,8 @@ export function composeRuntime(options) {
|
|
|
383
926
|
}
|
|
384
927
|
finally {
|
|
385
928
|
refusal = {
|
|
386
|
-
reason,
|
|
929
|
+
code: reason,
|
|
930
|
+
retryable: false,
|
|
387
931
|
reauthorizationOffered: reason === 'revoked-authorization',
|
|
388
932
|
refusal: provider,
|
|
389
933
|
};
|
|
@@ -408,19 +952,28 @@ export function composeRuntime(options) {
|
|
|
408
952
|
},
|
|
409
953
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
410
954
|
roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
|
|
411
|
-
attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
|
|
412
955
|
synchronizationAvailable,
|
|
413
956
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
414
957
|
});
|
|
415
958
|
}
|
|
416
|
-
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));
|
|
417
966
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
418
967
|
for (const locator of listedLocators)
|
|
419
968
|
scopedRefusals.delete(locator);
|
|
420
969
|
discovered.clear();
|
|
421
970
|
for (const room of listed)
|
|
422
971
|
discovered.set(room.locator, room);
|
|
423
|
-
|
|
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);
|
|
424
977
|
for (const room of listed) {
|
|
425
978
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
426
979
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -431,43 +984,85 @@ export function composeRuntime(options) {
|
|
|
431
984
|
}
|
|
432
985
|
roomsListed = true;
|
|
433
986
|
emitRooms();
|
|
987
|
+
return listed;
|
|
434
988
|
}
|
|
435
989
|
function unlistedHeld(listed) {
|
|
436
|
-
return [
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
pendings.delete(batch.id);
|
|
452
|
-
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
|
|
453
1005
|
rooms.delete(locator);
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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);
|
|
467
1041
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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));
|
|
471
1066
|
}
|
|
472
1067
|
function restored(key, persisted) {
|
|
473
1068
|
const prior = models.get(key);
|
|
@@ -488,7 +1083,7 @@ export function composeRuntime(options) {
|
|
|
488
1083
|
return model;
|
|
489
1084
|
}
|
|
490
1085
|
async function hydrate(opened) {
|
|
491
|
-
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
|
|
1086
|
+
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
|
|
492
1087
|
opened.all(),
|
|
493
1088
|
opened.queues(),
|
|
494
1089
|
opened.roomDrafts(),
|
|
@@ -498,9 +1093,17 @@ export function composeRuntime(options) {
|
|
|
498
1093
|
opened.lastCompleted(),
|
|
499
1094
|
opened.providerLimits(),
|
|
500
1095
|
opened.capabilities(),
|
|
1096
|
+
opened.member(),
|
|
1097
|
+
opened.transfers(),
|
|
1098
|
+
opened.targets(),
|
|
501
1099
|
]);
|
|
1100
|
+
targets = heldTargets;
|
|
1101
|
+
transfers.clear();
|
|
1102
|
+
for (const record of staged)
|
|
1103
|
+
transfers.set(record.locator, record);
|
|
502
1104
|
limits = declared;
|
|
503
1105
|
capabilities = named;
|
|
1106
|
+
member = known;
|
|
504
1107
|
roomDrafts.clear();
|
|
505
1108
|
for (const record of kept)
|
|
506
1109
|
rememberRoomDraft(record);
|
|
@@ -516,18 +1119,17 @@ export function composeRuntime(options) {
|
|
|
516
1119
|
}
|
|
517
1120
|
const previousReplicas = new Map(replicas);
|
|
518
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
|
+
}
|
|
519
1128
|
for (const record of records) {
|
|
520
1129
|
const model = restored(modelKey(record.locator, 'collection'), record.model);
|
|
521
1130
|
const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
|
|
522
1131
|
const prior = previousReplicas.get(record.locator);
|
|
523
|
-
const
|
|
524
|
-
const read = readConfirmed(retyped, prior?.confirmed);
|
|
525
|
-
const confirmed = read.kind === 'staged'
|
|
526
|
-
? read.confirmed
|
|
527
|
-
: {
|
|
528
|
-
record: retyped,
|
|
529
|
-
collection: parseCollection(retyped.documents, compositionOf(retyped)),
|
|
530
|
-
};
|
|
1132
|
+
const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
|
|
531
1133
|
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
|
|
532
1134
|
const existing = rooms.get(record.locator);
|
|
533
1135
|
if (existing === undefined)
|
|
@@ -536,7 +1138,6 @@ export function composeRuntime(options) {
|
|
|
536
1138
|
rooms.set(record.locator, {
|
|
537
1139
|
...existing,
|
|
538
1140
|
roomVersion: record.roomVersion,
|
|
539
|
-
attachments: record.attachments,
|
|
540
1141
|
operations: record.operations,
|
|
541
1142
|
replica: record.replica,
|
|
542
1143
|
model: modelTypes(model),
|
|
@@ -548,10 +1149,6 @@ export function composeRuntime(options) {
|
|
|
548
1149
|
},
|
|
549
1150
|
});
|
|
550
1151
|
}
|
|
551
|
-
if (read.kind === 'refused') {
|
|
552
|
-
refusedSource(record, read);
|
|
553
|
-
declarationRefusals.add(record.locator);
|
|
554
|
-
}
|
|
555
1152
|
}
|
|
556
1153
|
return {
|
|
557
1154
|
lastCompleted,
|
|
@@ -562,16 +1159,26 @@ export function composeRuntime(options) {
|
|
|
562
1159
|
async function readState(opened) {
|
|
563
1160
|
const { lastCompleted, locators, read } = await hydrate(opened);
|
|
564
1161
|
applyChangeSet(read);
|
|
1162
|
+
prepare();
|
|
565
1163
|
if (replicas.size > 0)
|
|
566
1164
|
emitRooms();
|
|
1165
|
+
if (member !== null)
|
|
1166
|
+
emitMember();
|
|
567
1167
|
for (const locator of locators)
|
|
568
1168
|
emitQueue(locator);
|
|
569
1169
|
publishRoomDrafts(false);
|
|
570
1170
|
return lastCompleted;
|
|
571
1171
|
}
|
|
572
1172
|
async function open() {
|
|
573
|
-
|
|
1173
|
+
if (closed)
|
|
1174
|
+
throw new Error('runtime closed');
|
|
1175
|
+
const opened = await openRuntimeStore(isFenced);
|
|
574
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
|
+
}
|
|
575
1182
|
const kept = options.shell.generation;
|
|
576
1183
|
const storedGeneration = await opened.generation();
|
|
577
1184
|
const loss = detectStorageLoss(storedGeneration, kept);
|
|
@@ -585,6 +1192,8 @@ export function composeRuntime(options) {
|
|
|
585
1192
|
emitStorageLoss();
|
|
586
1193
|
}
|
|
587
1194
|
const lastCompleted = await readState(opened);
|
|
1195
|
+
if (interrupted.length > 0)
|
|
1196
|
+
await erased(opened, interrupted);
|
|
588
1197
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
589
1198
|
return opened;
|
|
590
1199
|
}
|
|
@@ -611,11 +1220,12 @@ export function composeRuntime(options) {
|
|
|
611
1220
|
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
612
1221
|
}
|
|
613
1222
|
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
614
|
-
const
|
|
1223
|
+
const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
1224
|
+
const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
|
|
615
1225
|
const kept = queue.filter((batch) => !withdrawn.includes(batch));
|
|
616
1226
|
const unqueued = withdrawn.map((batch) => ({
|
|
617
1227
|
batch,
|
|
618
|
-
returned: landed(batch, confirmed) ? null :
|
|
1228
|
+
returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
|
|
619
1229
|
}));
|
|
620
1230
|
const returning = [
|
|
621
1231
|
...[...unqueued].reverse(),
|
|
@@ -636,28 +1246,48 @@ export function composeRuntime(options) {
|
|
|
636
1246
|
});
|
|
637
1247
|
}
|
|
638
1248
|
}
|
|
639
|
-
await local.
|
|
640
|
-
|
|
641
|
-
previous: before?.confirmed.record ?? null,
|
|
642
|
-
queue: kept,
|
|
643
|
-
previousQueue: before?.queue ?? [],
|
|
644
|
-
roomDrafts: [...written.values()],
|
|
645
|
-
});
|
|
646
|
-
for (const batch of before?.queue ?? []) {
|
|
1249
|
+
await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
|
|
1250
|
+
for (const batch of previousQueue) {
|
|
647
1251
|
if (!kept.includes(batch))
|
|
648
1252
|
pendings.delete(batch.id);
|
|
649
1253
|
}
|
|
650
1254
|
for (const record of written.values())
|
|
651
1255
|
rememberRoomDraft(record);
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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
|
+
};
|
|
661
1291
|
}
|
|
662
1292
|
function adoptModels(locator, record) {
|
|
663
1293
|
for (const [attached, model] of [
|
|
@@ -690,10 +1320,16 @@ export function composeRuntime(options) {
|
|
|
690
1320
|
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
691
1321
|
};
|
|
692
1322
|
}
|
|
693
|
-
function publish(local, room, confirmed,
|
|
1323
|
+
function publish(local, room, confirmed, landing, live) {
|
|
1324
|
+
const { base, stored } = landing;
|
|
694
1325
|
return exclusive(async () => {
|
|
695
|
-
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);
|
|
696
1331
|
return true;
|
|
1332
|
+
}
|
|
697
1333
|
const prior = replicas.get(room.locator);
|
|
698
1334
|
if (base !== undefined &&
|
|
699
1335
|
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
@@ -701,47 +1337,85 @@ export function composeRuntime(options) {
|
|
|
701
1337
|
return false;
|
|
702
1338
|
}
|
|
703
1339
|
adoptModels(room.locator, confirmed.record);
|
|
704
|
-
const queue = prior?.queue ?? [];
|
|
1340
|
+
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
705
1341
|
const roomVersion = confirmed.record.roomVersion;
|
|
706
1342
|
const accepted = queue.filter((batch) => {
|
|
707
1343
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
708
1344
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
709
1345
|
});
|
|
710
|
-
|
|
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
|
+
}
|
|
711
1362
|
for (const batch of accepted)
|
|
712
1363
|
pendings.delete(batch.id);
|
|
713
1364
|
stale.delete(room.locator);
|
|
1365
|
+
unindexable.delete(room.locator);
|
|
714
1366
|
if (unreadable.delete(room.locator))
|
|
715
1367
|
scopedRefusals.delete(room.locator);
|
|
716
1368
|
sourceRefusals.delete(room.locator);
|
|
717
1369
|
declarationRefusals.delete(room.locator);
|
|
718
1370
|
list(room, true);
|
|
1371
|
+
if (prior === undefined)
|
|
1372
|
+
prepared.add(room.locator);
|
|
719
1373
|
publishChangeSet(set);
|
|
720
1374
|
moveTo({ ...status, ...attention() });
|
|
1375
|
+
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
721
1376
|
return true;
|
|
722
1377
|
});
|
|
723
1378
|
}
|
|
724
1379
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
725
1380
|
const local = await openLocal();
|
|
1381
|
+
await referenced(locator);
|
|
726
1382
|
const replica = replicas.get(locator);
|
|
727
1383
|
const room = rooms.get(locator);
|
|
728
1384
|
if (replica === undefined || room === undefined) {
|
|
729
|
-
throw
|
|
1385
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
730
1386
|
}
|
|
731
1387
|
const saved = [];
|
|
732
1388
|
for (const { path, revision } of named) {
|
|
733
1389
|
const record = roomDrafts.get(locator)?.get(path);
|
|
734
1390
|
if (record === undefined || record.revision !== revision) {
|
|
735
|
-
return { outcome: 'refused',
|
|
1391
|
+
return { outcome: 'refused', code: 'stale', retryable: false, file: path };
|
|
736
1392
|
}
|
|
737
1393
|
if (!saved.some((draft) => draft.path === path))
|
|
738
1394
|
saved.push({ path, parts: record.parts });
|
|
739
1395
|
}
|
|
740
1396
|
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
|
|
741
1397
|
if (changed.length > 0)
|
|
742
|
-
return { outcome: 'refused',
|
|
1398
|
+
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
743
1399
|
const parts = saved.map(({ path, parts }) => changesOf(path, parts));
|
|
744
|
-
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,
|
|
745
1419
|
baseRoomVersion: replica.confirmed.record.roomVersion,
|
|
746
1420
|
composition: compositionFor(replica.confirmed.record),
|
|
747
1421
|
operations: room.operations,
|
|
@@ -749,29 +1423,93 @@ export function composeRuntime(options) {
|
|
|
749
1423
|
providerLimits: limits,
|
|
750
1424
|
handle: room.handle,
|
|
751
1425
|
visible: replica.visible,
|
|
752
|
-
changes
|
|
1426
|
+
changes,
|
|
753
1427
|
bases: parts.flatMap(({ bases }) => bases),
|
|
754
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 }];
|
|
755
1441
|
});
|
|
756
|
-
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')
|
|
757
1460
|
return { outcome: 'refused', ...built.refusal };
|
|
758
|
-
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
|
+
}
|
|
759
1492
|
const consumed = saved.map(({ path }) => path);
|
|
760
1493
|
try {
|
|
761
|
-
await local.
|
|
762
|
-
|
|
763
|
-
previous: replica.confirmed.record,
|
|
1494
|
+
await local.keepAuthored({
|
|
1495
|
+
locator,
|
|
764
1496
|
queue: batches,
|
|
765
|
-
|
|
766
|
-
saved,
|
|
1497
|
+
saved: drafted,
|
|
767
1498
|
consumedRoomDrafts: consumed,
|
|
768
1499
|
});
|
|
769
1500
|
}
|
|
770
1501
|
catch {
|
|
771
|
-
return { outcome: 'refused',
|
|
1502
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
1503
|
+
}
|
|
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 });
|
|
772
1510
|
}
|
|
773
|
-
|
|
774
|
-
|
|
1511
|
+
if (!localSaves)
|
|
1512
|
+
recorded = false;
|
|
775
1513
|
for (const path of consumed)
|
|
776
1514
|
forgetRoomDraft(locator, path);
|
|
777
1515
|
replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
|
|
@@ -779,9 +1517,12 @@ export function composeRuntime(options) {
|
|
|
779
1517
|
queue: true,
|
|
780
1518
|
drafts: consumed,
|
|
781
1519
|
}));
|
|
782
|
-
moveTo({ ...saving(status), ...attention() });
|
|
1520
|
+
moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
|
|
783
1521
|
submitLater(locator);
|
|
784
|
-
|
|
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 };
|
|
785
1526
|
}
|
|
786
1527
|
async function mark(local, locator, identifier, submission, lost) {
|
|
787
1528
|
const state = pendings.get(identifier);
|
|
@@ -791,6 +1532,8 @@ export function composeRuntime(options) {
|
|
|
791
1532
|
return;
|
|
792
1533
|
}
|
|
793
1534
|
await local.submit(locator, identifier, submission, lost);
|
|
1535
|
+
if (fences.has(locator))
|
|
1536
|
+
return;
|
|
794
1537
|
pendings.set(identifier, { ...state, submission, lost });
|
|
795
1538
|
const replica = replicas.get(locator);
|
|
796
1539
|
if (replica !== undefined)
|
|
@@ -801,22 +1544,24 @@ export function composeRuntime(options) {
|
|
|
801
1544
|
if (!continuing())
|
|
802
1545
|
throw new Error('connection refused');
|
|
803
1546
|
await local.keepUpload(locator, identifier, upload);
|
|
1547
|
+
if (fences.has(locator))
|
|
1548
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
804
1549
|
pendings.set(identifier, { ...pendings.get(identifier), upload });
|
|
805
1550
|
if (!continuing())
|
|
806
1551
|
throw new Error('connection refused');
|
|
807
1552
|
}
|
|
808
1553
|
async function readHistory(locator, path, query = {}) {
|
|
809
1554
|
if (closed || refusal !== null)
|
|
810
|
-
return
|
|
1555
|
+
return UNCERTAIN;
|
|
811
1556
|
try {
|
|
812
1557
|
await ensureAuthorization();
|
|
813
1558
|
}
|
|
814
1559
|
catch {
|
|
815
|
-
return
|
|
816
|
-
}
|
|
817
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
818
|
-
return { kind: 'transport-uncertainty' };
|
|
1560
|
+
return UNCERTAIN;
|
|
819
1561
|
}
|
|
1562
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1563
|
+
return UNCERTAIN;
|
|
1564
|
+
const since = fencings;
|
|
820
1565
|
const transfer = ['transfer', 'history', locator, path];
|
|
821
1566
|
if (query.revision !== undefined)
|
|
822
1567
|
transfer.push(query.revision);
|
|
@@ -831,6 +1576,8 @@ export function composeRuntime(options) {
|
|
|
831
1576
|
...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
|
|
832
1577
|
});
|
|
833
1578
|
journal.record(`outcome history ${result.kind}`);
|
|
1579
|
+
if (fencedSince(locator, since))
|
|
1580
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
834
1581
|
if (result.kind === 'scoped-refusal') {
|
|
835
1582
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
836
1583
|
if (room !== undefined)
|
|
@@ -838,13 +1585,107 @@ export function composeRuntime(options) {
|
|
|
838
1585
|
}
|
|
839
1586
|
if (result.kind === 'revoked-authorization')
|
|
840
1587
|
await refuse('revoked-authorization');
|
|
841
|
-
return result;
|
|
1588
|
+
return historyAnswer(result);
|
|
842
1589
|
}
|
|
843
1590
|
catch {
|
|
844
1591
|
journal.record('outcome history transport-uncertainty');
|
|
845
|
-
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;
|
|
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;
|
|
846
1642
|
}
|
|
847
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
|
+
});
|
|
1688
|
+
}
|
|
848
1689
|
function declaredAgents() {
|
|
849
1690
|
for (const capability of capabilities) {
|
|
850
1691
|
if (capability.conversationAgents !== undefined)
|
|
@@ -864,18 +1705,17 @@ export function composeRuntime(options) {
|
|
|
864
1705
|
async function invokeAction(name, input) {
|
|
865
1706
|
await ready;
|
|
866
1707
|
if (closed || refusal !== null)
|
|
867
|
-
return
|
|
1708
|
+
return UNCERTAIN;
|
|
868
1709
|
if (declaredAction(name) === undefined)
|
|
869
|
-
return
|
|
1710
|
+
return undeclared(name);
|
|
870
1711
|
try {
|
|
871
1712
|
await ensureAuthorization();
|
|
872
1713
|
}
|
|
873
1714
|
catch {
|
|
874
|
-
return
|
|
875
|
-
}
|
|
876
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
877
|
-
return { kind: 'transport-uncertainty' };
|
|
1715
|
+
return UNCERTAIN;
|
|
878
1716
|
}
|
|
1717
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1718
|
+
return UNCERTAIN;
|
|
879
1719
|
journal.record(`transfer action ${name}`);
|
|
880
1720
|
try {
|
|
881
1721
|
const result = await options.adapter.action(authorization, { name, input });
|
|
@@ -889,11 +1729,11 @@ export function composeRuntime(options) {
|
|
|
889
1729
|
journal.record(`outcome action ${result.kind}`);
|
|
890
1730
|
if (result.kind === 'rejected')
|
|
891
1731
|
journal.record(`refusal rejected ${result.code}`);
|
|
892
|
-
return result;
|
|
1732
|
+
return calledResult(result);
|
|
893
1733
|
}
|
|
894
1734
|
catch {
|
|
895
1735
|
journal.record('outcome action transport-uncertainty');
|
|
896
|
-
return
|
|
1736
|
+
return UNCERTAIN;
|
|
897
1737
|
}
|
|
898
1738
|
}
|
|
899
1739
|
function continuing() {
|
|
@@ -910,11 +1750,11 @@ export function composeRuntime(options) {
|
|
|
910
1750
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
911
1751
|
!options.adapter.reached(locator, roomVersion, written));
|
|
912
1752
|
}
|
|
913
|
-
async function pullRefresh(authorized, confirmed) {
|
|
914
|
-
if (!
|
|
1753
|
+
async function pullRefresh(authorized, confirmed, live) {
|
|
1754
|
+
if (!live())
|
|
915
1755
|
return unconfirmed;
|
|
916
1756
|
try {
|
|
917
|
-
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);
|
|
918
1758
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
919
1759
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
920
1760
|
return result;
|
|
@@ -924,28 +1764,80 @@ export function composeRuntime(options) {
|
|
|
924
1764
|
throw error;
|
|
925
1765
|
}
|
|
926
1766
|
}
|
|
927
|
-
|
|
928
|
-
|
|
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())
|
|
929
1807
|
return unconfirmed;
|
|
1808
|
+
if (replicas.has(room.locator)) {
|
|
1809
|
+
catching.add(room.locator);
|
|
1810
|
+
readSide.recover();
|
|
1811
|
+
}
|
|
930
1812
|
try {
|
|
931
1813
|
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
932
1814
|
collection: modelOf(room.locator),
|
|
933
1815
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
934
|
-
}, (models, held) => remodel(room.locator, models, held),
|
|
1816
|
+
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
935
1817
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
936
1818
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1819
|
+
if (result.kind !== 'staged')
|
|
1820
|
+
caughtUpTo(room.locator);
|
|
937
1821
|
return result;
|
|
938
1822
|
}
|
|
939
1823
|
catch (error) {
|
|
940
1824
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
1825
|
+
caughtUpTo(room.locator);
|
|
941
1826
|
throw error;
|
|
942
1827
|
}
|
|
943
1828
|
}
|
|
944
|
-
|
|
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;
|
|
945
1837
|
const held = replicas.get(room.locator);
|
|
946
1838
|
if (held !== undefined) {
|
|
947
|
-
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
948
|
-
if (!
|
|
1839
|
+
const refreshed = await pullRefresh(authorized, held.confirmed, live);
|
|
1840
|
+
if (!live())
|
|
949
1841
|
return unconfirmed;
|
|
950
1842
|
if (refreshed.kind === 'still-current') {
|
|
951
1843
|
stale.delete(room.locator);
|
|
@@ -959,31 +1851,45 @@ export function composeRuntime(options) {
|
|
|
959
1851
|
return null;
|
|
960
1852
|
}
|
|
961
1853
|
if (refreshed.kind === 'advanced') {
|
|
962
|
-
if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
|
|
1854
|
+
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
|
|
963
1855
|
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
964
1856
|
const reached = now !== undefined &&
|
|
965
1857
|
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
966
|
-
return reached ? null : advance(room, local, authorized);
|
|
1858
|
+
return reached ? null : advance(room, local, authorized, live);
|
|
967
1859
|
}
|
|
968
1860
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
969
1861
|
return null;
|
|
970
1862
|
}
|
|
971
1863
|
if (refreshed.kind !== 'beyond-horizon')
|
|
972
1864
|
return refreshed;
|
|
1865
|
+
if (opened !== null) {
|
|
1866
|
+
owed.add(room.locator);
|
|
1867
|
+
readSide.recover();
|
|
1868
|
+
return unconfirmed;
|
|
1869
|
+
}
|
|
973
1870
|
}
|
|
974
|
-
const bootstrapped = await pullBootstrap(authorized, room);
|
|
975
|
-
if (!
|
|
1871
|
+
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
1872
|
+
if (!live())
|
|
976
1873
|
return unconfirmed;
|
|
977
1874
|
if (bootstrapped.kind !== 'staged')
|
|
978
1875
|
return bootstrapped;
|
|
979
|
-
await publish(local, room, bootstrapped.confirmed);
|
|
1876
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
980
1877
|
return null;
|
|
981
1878
|
}
|
|
982
1879
|
async function withhold(withheld, room, local, progress) {
|
|
1880
|
+
if (withheld.kind === 'unconfirmed')
|
|
1881
|
+
return;
|
|
983
1882
|
if (withheld.kind === 'refused')
|
|
984
1883
|
stale.delete(room.locator);
|
|
985
1884
|
else
|
|
986
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);
|
|
987
1893
|
if (withheld.kind === 'scoped-refusal') {
|
|
988
1894
|
await scoped(withheld, room, local);
|
|
989
1895
|
}
|
|
@@ -1008,6 +1914,7 @@ export function composeRuntime(options) {
|
|
|
1008
1914
|
function refusedSource(room, { refusal }) {
|
|
1009
1915
|
sourceRefusals.set(room.locator, {
|
|
1010
1916
|
code: refusal.rule,
|
|
1917
|
+
retryable: false,
|
|
1011
1918
|
fields: {
|
|
1012
1919
|
file: refusal.file,
|
|
1013
1920
|
...(refusal.location === undefined ? {} : { location: refusal.location }),
|
|
@@ -1018,12 +1925,21 @@ export function composeRuntime(options) {
|
|
|
1018
1925
|
unreadable.add(room.locator);
|
|
1019
1926
|
list(room, false);
|
|
1020
1927
|
emitRooms();
|
|
1928
|
+
moveTo({ ...status, ...attention() });
|
|
1021
1929
|
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
1022
1930
|
}
|
|
1023
1931
|
function leaving(batch, result) {
|
|
1024
1932
|
if (result.kind === 'expired')
|
|
1025
|
-
return { batch, returned:
|
|
1026
|
-
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
|
+
};
|
|
1027
1943
|
}
|
|
1028
1944
|
function logAccepted(locator, submitted, accepted) {
|
|
1029
1945
|
journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
|
|
@@ -1031,6 +1947,7 @@ export function composeRuntime(options) {
|
|
|
1031
1947
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1032
1948
|
}
|
|
1033
1949
|
async function adoptAccepted(room, local, submitted, accepted) {
|
|
1950
|
+
await referenced(room.locator);
|
|
1034
1951
|
if (!continuing())
|
|
1035
1952
|
return unconfirmed;
|
|
1036
1953
|
const current = replicas.get(room.locator);
|
|
@@ -1041,22 +1958,23 @@ export function composeRuntime(options) {
|
|
|
1041
1958
|
logAccepted(room.locator, submitted, accepted);
|
|
1042
1959
|
const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
1043
1960
|
? { kind: 'staged', confirmed: current.confirmed }
|
|
1044
|
-
: applyReturned(current.confirmed,
|
|
1961
|
+
: applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
1045
1962
|
? accepted.roomVersion
|
|
1046
1963
|
: held, accepted.roomVersion);
|
|
1047
1964
|
if (applied.kind !== 'staged') {
|
|
1048
1965
|
const state = pendings.get(submitted.id);
|
|
1049
1966
|
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1050
|
-
if (state !== undefined)
|
|
1967
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
1051
1968
|
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1052
1969
|
emitQueue(room.locator);
|
|
1053
1970
|
return applied;
|
|
1054
1971
|
}
|
|
1055
|
-
const
|
|
1972
|
+
const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
1056
1973
|
pendings.delete(submitted.id);
|
|
1057
1974
|
unreadable.delete(room.locator);
|
|
1058
1975
|
sourceRefusals.delete(room.locator);
|
|
1059
1976
|
declarationRefusals.delete(room.locator);
|
|
1977
|
+
const set = settled();
|
|
1060
1978
|
list(room, true);
|
|
1061
1979
|
publishChangeSet(set);
|
|
1062
1980
|
moveTo({ ...status, ...attention() });
|
|
@@ -1070,14 +1988,14 @@ export function composeRuntime(options) {
|
|
|
1070
1988
|
refusedSource(room, failed);
|
|
1071
1989
|
return false;
|
|
1072
1990
|
}
|
|
1073
|
-
async function submitRoom(room, local, authorized, progress) {
|
|
1074
|
-
while (continuing() && authorization !== undefined) {
|
|
1991
|
+
async function submitRoom(room, local, authorized, progress, signal) {
|
|
1992
|
+
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
1075
1993
|
const replica = replicas.get(room.locator);
|
|
1076
1994
|
const head = replica?.queue[0];
|
|
1077
1995
|
if (replica === undefined || head === undefined)
|
|
1078
|
-
return;
|
|
1996
|
+
return 'done';
|
|
1079
1997
|
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1080
|
-
return;
|
|
1998
|
+
return 'done';
|
|
1081
1999
|
const state = pendings.get(head.id);
|
|
1082
2000
|
const { submission, lost: earlier } = state ?? {
|
|
1083
2001
|
submission: { state: 'waiting' },
|
|
@@ -1085,24 +2003,30 @@ export function composeRuntime(options) {
|
|
|
1085
2003
|
};
|
|
1086
2004
|
if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
|
|
1087
2005
|
progress.complete = false;
|
|
1088
|
-
return;
|
|
2006
|
+
return { at: Date.parse(submission.reset) };
|
|
1089
2007
|
}
|
|
1090
2008
|
if (writesHeld()) {
|
|
1091
2009
|
progress.complete = false;
|
|
1092
2010
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1093
|
-
return;
|
|
2011
|
+
return 'done';
|
|
1094
2012
|
}
|
|
1095
2013
|
const lost = earlier || submission.state === 'submitted';
|
|
2014
|
+
if (lost && pastHorizon(head.id)) {
|
|
2015
|
+
await discardPending(room, local, signal);
|
|
2016
|
+
return 'done';
|
|
2017
|
+
}
|
|
1096
2018
|
let result;
|
|
1097
2019
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
1098
2020
|
await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
|
|
1099
|
-
if (discoveryPaused)
|
|
1100
|
-
return;
|
|
1101
|
-
if (writesHeld()) {
|
|
2021
|
+
if (discoveryPaused || fences.has(room.locator))
|
|
2022
|
+
return 'done';
|
|
2023
|
+
if (signal.aborted || writesHeld()) {
|
|
1102
2024
|
await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
|
|
2025
|
+
if (signal.aborted)
|
|
2026
|
+
return 'done';
|
|
1103
2027
|
progress.complete = false;
|
|
1104
2028
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1105
|
-
return;
|
|
2029
|
+
return 'done';
|
|
1106
2030
|
}
|
|
1107
2031
|
const opensBefore = openSignals;
|
|
1108
2032
|
const answered = awaitingAnswer(room.locator);
|
|
@@ -1118,32 +2042,36 @@ export function composeRuntime(options) {
|
|
|
1118
2042
|
progress.complete = false;
|
|
1119
2043
|
if (continuing())
|
|
1120
2044
|
progress.reachable = false;
|
|
1121
|
-
return;
|
|
2045
|
+
return 'again';
|
|
2046
|
+
}
|
|
2047
|
+
if (signal.aborted) {
|
|
2048
|
+
answered();
|
|
2049
|
+
return 'done';
|
|
1122
2050
|
}
|
|
1123
2051
|
if (result.kind !== 'accepted' || !continuing())
|
|
1124
2052
|
answered();
|
|
1125
2053
|
if (!continuing())
|
|
1126
|
-
return;
|
|
2054
|
+
return 'done';
|
|
1127
2055
|
if (result.kind === 'transport-uncertainty') {
|
|
1128
2056
|
progress.complete = false;
|
|
1129
2057
|
progress.reachable = false;
|
|
1130
|
-
return;
|
|
2058
|
+
return 'again';
|
|
1131
2059
|
}
|
|
1132
2060
|
if (result.kind === 'room-not-readable') {
|
|
1133
2061
|
progress.complete = false;
|
|
1134
2062
|
unreadable.add(room.locator);
|
|
1135
2063
|
list(room, false);
|
|
1136
2064
|
emitRooms();
|
|
1137
|
-
return;
|
|
2065
|
+
return 'done';
|
|
1138
2066
|
}
|
|
1139
2067
|
if (result.kind === 'revoked-authorization') {
|
|
1140
2068
|
await refuse('revoked-authorization');
|
|
1141
|
-
return;
|
|
2069
|
+
return 'done';
|
|
1142
2070
|
}
|
|
1143
2071
|
if (result.kind === 'scoped-refusal') {
|
|
1144
2072
|
await scoped(result, room, local);
|
|
1145
2073
|
progress.complete = false;
|
|
1146
|
-
return;
|
|
2074
|
+
return 'done';
|
|
1147
2075
|
}
|
|
1148
2076
|
if (result.kind === 'write-halted') {
|
|
1149
2077
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
@@ -1152,16 +2080,24 @@ export function composeRuntime(options) {
|
|
|
1152
2080
|
continue;
|
|
1153
2081
|
haltedRefusal = true;
|
|
1154
2082
|
progress.complete = false;
|
|
1155
|
-
return;
|
|
2083
|
+
return 'done';
|
|
1156
2084
|
}
|
|
1157
2085
|
if (result.kind === 'conflicted' ||
|
|
1158
2086
|
result.kind === 'rejected' ||
|
|
1159
2087
|
result.kind === 'expired' ||
|
|
1160
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
|
+
}
|
|
1161
2094
|
if (result.kind === 'expired') {
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
+
}
|
|
1165
2101
|
}
|
|
1166
2102
|
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
|
|
1167
2103
|
(result.kind === 'conflicted' && result.roomVersion !== undefined
|
|
@@ -1174,8 +2110,7 @@ export function composeRuntime(options) {
|
|
|
1174
2110
|
const reset = result.reset;
|
|
1175
2111
|
progress.complete = false;
|
|
1176
2112
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1177
|
-
|
|
1178
|
-
return;
|
|
2113
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
1179
2114
|
}
|
|
1180
2115
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
1181
2116
|
const left = leaving(head, result);
|
|
@@ -1186,37 +2121,82 @@ export function composeRuntime(options) {
|
|
|
1186
2121
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
1187
2122
|
return;
|
|
1188
2123
|
}
|
|
1189
|
-
const
|
|
2124
|
+
const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
1190
2125
|
leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
|
|
1191
2126
|
(result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
|
|
1192
2127
|
? { ...left, returned: null }
|
|
1193
2128
|
: left,
|
|
1194
2129
|
});
|
|
1195
|
-
publishChangeSet(
|
|
2130
|
+
publishChangeSet(settled());
|
|
1196
2131
|
moveTo({ ...status, ...attention() });
|
|
1197
2132
|
});
|
|
1198
2133
|
if (result.kind === 'expired')
|
|
1199
2134
|
continue;
|
|
1200
|
-
const withheld = await advance(room, local, authorized);
|
|
1201
|
-
if (withheld !== null)
|
|
1202
|
-
|
|
2135
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
2136
|
+
if (withheld !== null) {
|
|
2137
|
+
await withhold(withheld, room, local, progress);
|
|
2138
|
+
return 'done';
|
|
2139
|
+
}
|
|
1203
2140
|
continue;
|
|
1204
2141
|
}
|
|
1205
2142
|
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1206
2143
|
if (!kept) {
|
|
1207
2144
|
progress.complete = false;
|
|
1208
|
-
return;
|
|
2145
|
+
return 'done';
|
|
1209
2146
|
}
|
|
1210
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);
|
|
1211
2191
|
}
|
|
1212
2192
|
function awaitingAnswer(locator) {
|
|
1213
2193
|
let release = () => { };
|
|
1214
2194
|
const awaited = new Promise((resolve) => {
|
|
1215
2195
|
release = resolve;
|
|
1216
2196
|
});
|
|
1217
|
-
answering.set(locator, awaited);
|
|
2197
|
+
answering.set(locator, { generation, answered: awaited });
|
|
1218
2198
|
return () => {
|
|
1219
|
-
if (answering.get(locator) === awaited)
|
|
2199
|
+
if (answering.get(locator)?.answered === awaited)
|
|
1220
2200
|
answering.delete(locator);
|
|
1221
2201
|
release();
|
|
1222
2202
|
};
|
|
@@ -1249,7 +2229,7 @@ export function composeRuntime(options) {
|
|
|
1249
2229
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
1250
2230
|
};
|
|
1251
2231
|
const reclassified = sameMembership(declared, held.confirmed.record)
|
|
1252
|
-
?
|
|
2232
|
+
? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
|
|
1253
2233
|
: readReclassified(declared, held.confirmed);
|
|
1254
2234
|
const record = reclassified.kind === 'staged'
|
|
1255
2235
|
? declared
|
|
@@ -1257,12 +2237,10 @@ export function composeRuntime(options) {
|
|
|
1257
2237
|
...declared,
|
|
1258
2238
|
typedSelection: held.confirmed.record.typedSelection,
|
|
1259
2239
|
};
|
|
1260
|
-
const read = reclassified.kind === 'deferred'
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
queue: held.queue,
|
|
1265
|
-
});
|
|
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 });
|
|
1266
2244
|
if (read.kind === 'staged') {
|
|
1267
2245
|
replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
|
|
1268
2246
|
if (declarationRefusals.delete(room.locator)) {
|
|
@@ -1289,7 +2267,11 @@ export function composeRuntime(options) {
|
|
|
1289
2267
|
});
|
|
1290
2268
|
}
|
|
1291
2269
|
async function scoped(result, room, local) {
|
|
1292
|
-
scopedRefusals.set(room.locator, {
|
|
2270
|
+
scopedRefusals.set(room.locator, {
|
|
2271
|
+
code: result.code,
|
|
2272
|
+
fields: result.fields,
|
|
2273
|
+
retryable: result.retryable,
|
|
2274
|
+
});
|
|
1293
2275
|
let scopedAway = room;
|
|
1294
2276
|
if (result.scope === 'model')
|
|
1295
2277
|
await retainModel(local, room, 'collection', {
|
|
@@ -1326,24 +2308,27 @@ export function composeRuntime(options) {
|
|
|
1326
2308
|
roomVersion: record.roomVersion,
|
|
1327
2309
|
documents: record.documents,
|
|
1328
2310
|
revisions: record.revisions,
|
|
1329
|
-
attachments: record.attachments,
|
|
1330
2311
|
},
|
|
1331
2312
|
];
|
|
1332
2313
|
});
|
|
1333
2314
|
}
|
|
1334
|
-
async function synchronize(listed, local, progress, attempt) {
|
|
1335
|
-
const
|
|
1336
|
-
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) => {
|
|
1337
2321
|
const outcome = prepared[index];
|
|
1338
2322
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1339
2323
|
});
|
|
1340
2324
|
if (attempt === 'opened') {
|
|
1341
|
-
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
2325
|
+
options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
|
|
1342
2326
|
handed = true;
|
|
1343
2327
|
}
|
|
2328
|
+
discovering?.release();
|
|
1344
2329
|
const loaded = await Promise.allSettled(ready
|
|
1345
2330
|
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1346
|
-
.map((room) => loadRoom(room, local, progress)));
|
|
2331
|
+
.map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
|
|
1347
2332
|
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1348
2333
|
if (failed !== undefined)
|
|
1349
2334
|
throw failed.reason;
|
|
@@ -1372,14 +2357,14 @@ export function composeRuntime(options) {
|
|
|
1372
2357
|
}
|
|
1373
2358
|
return true;
|
|
1374
2359
|
}
|
|
1375
|
-
async function caughtUp(room, local, progress) {
|
|
2360
|
+
async function caughtUp(room, local, progress, live) {
|
|
1376
2361
|
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1377
|
-
if (held === undefined || authorization === undefined)
|
|
2362
|
+
if (held === undefined || authorization === undefined || !live())
|
|
1378
2363
|
return;
|
|
1379
2364
|
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
1380
2365
|
if (!behind)
|
|
1381
2366
|
return;
|
|
1382
|
-
const withheld = await advance(room, local, authorization);
|
|
2367
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1383
2368
|
if (withheld !== null)
|
|
1384
2369
|
await withhold(withheld, room, local, progress);
|
|
1385
2370
|
}
|
|
@@ -1389,39 +2374,46 @@ export function composeRuntime(options) {
|
|
|
1389
2374
|
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1390
2375
|
});
|
|
1391
2376
|
}
|
|
1392
|
-
async function loadRoom(room, local, progress) {
|
|
1393
|
-
if (
|
|
2377
|
+
async function loadRoom(room, local, progress, live) {
|
|
2378
|
+
if (!live() || authorization === undefined)
|
|
1394
2379
|
return;
|
|
1395
2380
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1396
|
-
await reload(room, local, progress);
|
|
2381
|
+
await reload(room, local, progress, live);
|
|
1397
2382
|
return;
|
|
1398
2383
|
}
|
|
1399
|
-
const withheld = await advance(room, local, authorization);
|
|
2384
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1400
2385
|
if (withheld !== null) {
|
|
1401
2386
|
await withhold(withheld, room, local, progress);
|
|
1402
2387
|
return;
|
|
1403
2388
|
}
|
|
1404
|
-
await caughtUp(room, local, progress);
|
|
2389
|
+
await caughtUp(room, local, progress, live);
|
|
1405
2390
|
}
|
|
1406
|
-
async function reload(room, local, progress) {
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
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);
|
|
1415
2407
|
}
|
|
1416
|
-
await publish(local, room, bootstrapped.confirmed);
|
|
1417
|
-
await caughtUp(room, local, progress);
|
|
1418
2408
|
}
|
|
1419
|
-
async function discoverRooms(local, progress, attempt) {
|
|
2409
|
+
async function discoverRooms(local, progress, attempt, live) {
|
|
1420
2410
|
if (closed || authorization === undefined) {
|
|
1421
2411
|
notifyReady();
|
|
1422
2412
|
return;
|
|
1423
2413
|
}
|
|
1424
2414
|
moveTo(transferring(status));
|
|
2415
|
+
discoveries += 1;
|
|
2416
|
+
const started = discoveries;
|
|
1425
2417
|
let result;
|
|
1426
2418
|
try {
|
|
1427
2419
|
result = await options.adapter.discover(authorization);
|
|
@@ -1429,7 +2421,8 @@ export function composeRuntime(options) {
|
|
|
1429
2421
|
catch {
|
|
1430
2422
|
result = { kind: 'transport-uncertainty' };
|
|
1431
2423
|
}
|
|
1432
|
-
if (
|
|
2424
|
+
if (!live()) {
|
|
2425
|
+
progress.complete = false;
|
|
1433
2426
|
notifyReady();
|
|
1434
2427
|
return;
|
|
1435
2428
|
}
|
|
@@ -1448,7 +2441,11 @@ export function composeRuntime(options) {
|
|
|
1448
2441
|
}
|
|
1449
2442
|
if (result.kind === 'contract-unavailable') {
|
|
1450
2443
|
journal.record(`connection contract unavailable ${result.code}`);
|
|
1451
|
-
await refuse('missing-required-guarantee', {
|
|
2444
|
+
await refuse('missing-required-guarantee', {
|
|
2445
|
+
code: result.code,
|
|
2446
|
+
fields: result.fields,
|
|
2447
|
+
retryable: result.retryable,
|
|
2448
|
+
});
|
|
1452
2449
|
notifyReady();
|
|
1453
2450
|
return;
|
|
1454
2451
|
}
|
|
@@ -1483,14 +2480,20 @@ export function composeRuntime(options) {
|
|
|
1483
2480
|
}
|
|
1484
2481
|
const previousCapabilities = capabilities;
|
|
1485
2482
|
capabilities = result.capabilities;
|
|
2483
|
+
outcomeRetention = result.retention.outcome;
|
|
1486
2484
|
notifyReady();
|
|
1487
2485
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1488
2486
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
1489
2487
|
limits = result.providerLimits;
|
|
1490
2488
|
await local.declare(limits, capabilities);
|
|
1491
2489
|
}
|
|
1492
|
-
|
|
1493
|
-
|
|
2490
|
+
const signedIn = result.member ?? null;
|
|
2491
|
+
if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
|
|
2492
|
+
await local.keepMember(signedIn);
|
|
2493
|
+
member = signedIn;
|
|
2494
|
+
emitMember();
|
|
2495
|
+
}
|
|
2496
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
1494
2497
|
}
|
|
1495
2498
|
async function ensureAuthorization() {
|
|
1496
2499
|
if (authorization !== undefined)
|
|
@@ -1504,32 +2507,64 @@ export function composeRuntime(options) {
|
|
|
1504
2507
|
throw error;
|
|
1505
2508
|
}
|
|
1506
2509
|
}
|
|
1507
|
-
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) {
|
|
1508
2530
|
if (stopping)
|
|
1509
|
-
return
|
|
1510
|
-
|
|
1511
|
-
|
|
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;
|
|
1512
2542
|
}
|
|
1513
2543
|
function sync(attempt) {
|
|
1514
|
-
return task(async () => {
|
|
1515
|
-
if (
|
|
2544
|
+
return task(OPENING_LANE, async (live) => {
|
|
2545
|
+
if (!live()) {
|
|
1516
2546
|
notifyReady();
|
|
1517
2547
|
return;
|
|
1518
2548
|
}
|
|
1519
2549
|
const progress = { complete: true, reachable: true };
|
|
1520
|
-
|
|
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;
|
|
1521
2556
|
complete = false;
|
|
1522
2557
|
recorded = false;
|
|
1523
2558
|
moveTo(transferring(status));
|
|
1524
2559
|
handed = false;
|
|
1525
2560
|
try {
|
|
1526
2561
|
const local = await openLocal();
|
|
1527
|
-
if (
|
|
2562
|
+
if (!live())
|
|
1528
2563
|
return;
|
|
1529
2564
|
await ensureAuthorization();
|
|
1530
|
-
if (
|
|
2565
|
+
if (!live())
|
|
1531
2566
|
return;
|
|
1532
|
-
await discoverRooms(local, progress, attempt);
|
|
2567
|
+
await discoverRooms(local, progress, attempt, live);
|
|
1533
2568
|
}
|
|
1534
2569
|
catch {
|
|
1535
2570
|
progress.complete = false;
|
|
@@ -1537,17 +2572,21 @@ export function composeRuntime(options) {
|
|
|
1537
2572
|
progress.reachable = false;
|
|
1538
2573
|
}
|
|
1539
2574
|
finally {
|
|
2575
|
+
gate.release();
|
|
1540
2576
|
notifyReady();
|
|
1541
2577
|
}
|
|
2578
|
+
if (!live())
|
|
2579
|
+
return;
|
|
1542
2580
|
if (attempt !== 'opened') {
|
|
1543
|
-
await finishOpening(progress);
|
|
2581
|
+
await finishOpening(progress, live);
|
|
1544
2582
|
return;
|
|
1545
2583
|
}
|
|
1546
2584
|
if (handed || closed || progress.reachable)
|
|
1547
2585
|
return;
|
|
1548
2586
|
underway = null;
|
|
2587
|
+
opened = null;
|
|
1549
2588
|
journal.record('connection reopen');
|
|
1550
|
-
await rest(progress);
|
|
2589
|
+
await rest(progress, live);
|
|
1551
2590
|
options.adapter.roomStream?.reopen();
|
|
1552
2591
|
});
|
|
1553
2592
|
}
|
|
@@ -1558,29 +2597,51 @@ export function composeRuntime(options) {
|
|
|
1558
2597
|
void refuse('revoked-authorization');
|
|
1559
2598
|
return;
|
|
1560
2599
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
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
|
+
});
|
|
1564
2608
|
}
|
|
1565
2609
|
function applyOpening(expired) {
|
|
1566
|
-
|
|
1567
|
-
|
|
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) => {
|
|
1568
2616
|
underway = null;
|
|
1569
|
-
if (
|
|
2617
|
+
if (!live())
|
|
1570
2618
|
return;
|
|
1571
2619
|
if (progress === null) {
|
|
1572
|
-
await
|
|
2620
|
+
await replacing;
|
|
2621
|
+
await rest(cycle, live);
|
|
1573
2622
|
return;
|
|
1574
2623
|
}
|
|
2624
|
+
await roomsCarried(expired);
|
|
2625
|
+
if (!live())
|
|
2626
|
+
return;
|
|
2627
|
+
const replaced = new Set(owed);
|
|
2628
|
+
owed.clear();
|
|
1575
2629
|
try {
|
|
1576
2630
|
const local = await openLocal();
|
|
1577
2631
|
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
1578
|
-
const reloading = held.filter((room) => expired.includes(room.locator)
|
|
1579
|
-
|
|
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)];
|
|
1580
2639
|
if (progress.reachable) {
|
|
1581
|
-
for (const room of held)
|
|
1582
|
-
if (!reloading.includes(room))
|
|
2640
|
+
for (const room of held) {
|
|
2641
|
+
if (!expired.includes(room.locator) && !reloading.includes(room)) {
|
|
1583
2642
|
await confirmModels(room, local);
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
1584
2645
|
}
|
|
1585
2646
|
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1586
2647
|
if (failed !== undefined)
|
|
@@ -1590,21 +2651,21 @@ export function composeRuntime(options) {
|
|
|
1590
2651
|
progress.complete = false;
|
|
1591
2652
|
progress.reachable = false;
|
|
1592
2653
|
}
|
|
1593
|
-
|
|
2654
|
+
if (live())
|
|
2655
|
+
await finishOpening(progress, live);
|
|
1594
2656
|
});
|
|
1595
2657
|
}
|
|
1596
|
-
async function
|
|
1597
|
-
const progress = { complete: true, reachable: true };
|
|
2658
|
+
async function reloadNow(replaced, progress) {
|
|
1598
2659
|
try {
|
|
1599
2660
|
const local = await openLocal();
|
|
1600
|
-
await Promise.allSettled([...discovered.values()]
|
|
1601
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
1602
|
-
.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))));
|
|
1603
2664
|
}
|
|
1604
2665
|
catch {
|
|
1605
2666
|
progress.reachable = false;
|
|
2667
|
+
return [];
|
|
1606
2668
|
}
|
|
1607
|
-
await rest(progress);
|
|
1608
2669
|
}
|
|
1609
2670
|
async function confirmModels(room, local) {
|
|
1610
2671
|
for (const attached of ['collection', 'typed-selection']) {
|
|
@@ -1615,38 +2676,96 @@ export function composeRuntime(options) {
|
|
|
1615
2676
|
}
|
|
1616
2677
|
}
|
|
1617
2678
|
function submitLater(locator) {
|
|
1618
|
-
|
|
1619
|
-
if (running !== undefined) {
|
|
1620
|
-
running.again = true;
|
|
2679
|
+
if (localSaves)
|
|
1621
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);
|
|
1622
2694
|
}
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
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';
|
|
1650
2769
|
}
|
|
1651
2770
|
function submitAll() {
|
|
1652
2771
|
for (const [locator, replica] of replicas) {
|
|
@@ -1654,31 +2773,41 @@ export function composeRuntime(options) {
|
|
|
1654
2773
|
submitLater(locator);
|
|
1655
2774
|
}
|
|
1656
2775
|
}
|
|
1657
|
-
async function finishOpening(progress) {
|
|
2776
|
+
async function finishOpening(progress, live) {
|
|
1658
2777
|
try {
|
|
1659
|
-
if (
|
|
2778
|
+
if (!live()) {
|
|
1660
2779
|
progress.complete = false;
|
|
1661
2780
|
return;
|
|
1662
2781
|
}
|
|
1663
2782
|
submitAll();
|
|
2783
|
+
for (const locator of stale)
|
|
2784
|
+
if (repairable.has(locator))
|
|
2785
|
+
repairLater(locator, {});
|
|
1664
2786
|
await conversations.reconcile();
|
|
1665
2787
|
if (status.providerUnreachable)
|
|
1666
2788
|
conversations.reconnected();
|
|
1667
2789
|
const reading = { complete: true, reachable: true };
|
|
1668
|
-
|
|
1669
|
-
|
|
2790
|
+
if (demand.conversations)
|
|
2791
|
+
await conversations.catchUp(reading);
|
|
2792
|
+
if (progress.reachable && reading.reachable)
|
|
2793
|
+
await conversations.resend(reading);
|
|
2794
|
+
if (!reading.reachable) {
|
|
1670
2795
|
progress.reachable = false;
|
|
2796
|
+
resendLater();
|
|
2797
|
+
}
|
|
1671
2798
|
}
|
|
1672
2799
|
catch {
|
|
1673
2800
|
progress.complete = false;
|
|
1674
2801
|
}
|
|
1675
2802
|
finally {
|
|
1676
|
-
|
|
1677
|
-
|
|
2803
|
+
if (live()) {
|
|
2804
|
+
complete = progress.complete;
|
|
2805
|
+
await rest(progress, live);
|
|
2806
|
+
}
|
|
1678
2807
|
}
|
|
1679
2808
|
}
|
|
1680
|
-
async function rest(progress) {
|
|
1681
|
-
if (
|
|
2809
|
+
async function rest(progress, live = () => !closed) {
|
|
2810
|
+
if (!live())
|
|
1682
2811
|
return;
|
|
1683
2812
|
let lastCompleted = status.lastCompleted;
|
|
1684
2813
|
let synced = complete && stale.size === 0 && !pending();
|
|
@@ -1686,6 +2815,8 @@ export function composeRuntime(options) {
|
|
|
1686
2815
|
const completedAt = new Date().toISOString();
|
|
1687
2816
|
try {
|
|
1688
2817
|
await (await openLocal()).complete(completedAt);
|
|
2818
|
+
if (!live())
|
|
2819
|
+
return;
|
|
1689
2820
|
recorded = true;
|
|
1690
2821
|
lastCompleted = completedAt;
|
|
1691
2822
|
}
|
|
@@ -1694,46 +2825,74 @@ export function composeRuntime(options) {
|
|
|
1694
2825
|
}
|
|
1695
2826
|
}
|
|
1696
2827
|
moveTo({
|
|
1697
|
-
state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
|
|
2828
|
+
state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
|
|
1698
2829
|
lastCompleted,
|
|
1699
2830
|
providerUnreachable: !progress.reachable,
|
|
1700
2831
|
...attention(),
|
|
1701
2832
|
});
|
|
1702
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
|
+
}
|
|
1703
2842
|
function applyRoom(locator, roomVersion) {
|
|
1704
2843
|
announced.set(locator, roomVersion);
|
|
1705
|
-
return task(async () => {
|
|
1706
|
-
|
|
1707
|
-
|
|
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)
|
|
1708
2851
|
return;
|
|
1709
2852
|
const room = discovered.get(locator);
|
|
1710
2853
|
const held = replicas.get(locator);
|
|
1711
|
-
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2854
|
+
if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
|
|
1712
2855
|
return;
|
|
1713
2856
|
if (held !== undefined &&
|
|
1714
2857
|
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
1715
2858
|
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
1716
2859
|
return;
|
|
1717
2860
|
}
|
|
1718
|
-
const during = underway;
|
|
2861
|
+
const during = underway?.generation === generation ? underway.progress : null;
|
|
1719
2862
|
const progress = during ?? { complete: true, reachable: true };
|
|
1720
2863
|
if (during === null)
|
|
1721
2864
|
moveTo(transferring(status));
|
|
1722
2865
|
try {
|
|
1723
2866
|
const local = await openLocal();
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
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
|
+
});
|
|
1727
2874
|
}
|
|
1728
2875
|
catch {
|
|
1729
2876
|
stale.add(locator);
|
|
1730
2877
|
}
|
|
1731
2878
|
finally {
|
|
1732
2879
|
if (during === null)
|
|
1733
|
-
await rest(progress);
|
|
2880
|
+
await rest(progress, live);
|
|
1734
2881
|
}
|
|
1735
2882
|
});
|
|
1736
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
|
+
};
|
|
1737
2896
|
function observeStream() {
|
|
1738
2897
|
const stream = options.adapter.roomStream;
|
|
1739
2898
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -1743,6 +2902,9 @@ export function composeRuntime(options) {
|
|
|
1743
2902
|
return;
|
|
1744
2903
|
if (event.kind === 'opened') {
|
|
1745
2904
|
socketOpen = true;
|
|
2905
|
+
generation += 1;
|
|
2906
|
+
conversations.reopened();
|
|
2907
|
+
recovery.resume();
|
|
1746
2908
|
void sync('opened');
|
|
1747
2909
|
}
|
|
1748
2910
|
else if (event.kind === 'closed')
|
|
@@ -1751,7 +2913,7 @@ export function composeRuntime(options) {
|
|
|
1751
2913
|
void applyOpening(event.expired);
|
|
1752
2914
|
else
|
|
1753
2915
|
void applyRoom(event.locator, event.roomVersion);
|
|
1754
|
-
});
|
|
2916
|
+
}, receipts);
|
|
1755
2917
|
}
|
|
1756
2918
|
function writesHeld() {
|
|
1757
2919
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -1766,6 +2928,7 @@ export function composeRuntime(options) {
|
|
|
1766
2928
|
haltedRefusal = false;
|
|
1767
2929
|
if (mode !== serviceMode) {
|
|
1768
2930
|
serviceMode = mode;
|
|
2931
|
+
queueReadiness();
|
|
1769
2932
|
journal.record(`service mode ${mode}`);
|
|
1770
2933
|
for (const listener of serviceModeListeners)
|
|
1771
2934
|
listener(mode);
|
|
@@ -1778,6 +2941,8 @@ export function composeRuntime(options) {
|
|
|
1778
2941
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
1779
2942
|
return;
|
|
1780
2943
|
withdrawConversations = stream.observe((event) => {
|
|
2944
|
+
if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
|
|
2945
|
+
queueReadiness();
|
|
1781
2946
|
if (conversations.receive(event)) {
|
|
1782
2947
|
void ready.then(scheduleCurrency);
|
|
1783
2948
|
return;
|
|
@@ -1789,46 +2954,47 @@ export function composeRuntime(options) {
|
|
|
1789
2954
|
});
|
|
1790
2955
|
}
|
|
1791
2956
|
function scheduleCurrency() {
|
|
1792
|
-
if (currencyQueued || closed || stopping)
|
|
2957
|
+
if (currencyQueued === generation || closed || stopping)
|
|
1793
2958
|
return;
|
|
1794
|
-
currencyQueued =
|
|
1795
|
-
void task(runCurrency);
|
|
2959
|
+
currencyQueued = generation;
|
|
2960
|
+
void task(CONVERSATION_LANE, runCurrency);
|
|
1796
2961
|
}
|
|
1797
|
-
async function runCurrency() {
|
|
1798
|
-
currencyQueued
|
|
1799
|
-
|
|
2962
|
+
async function runCurrency(live) {
|
|
2963
|
+
if (currencyQueued === generation)
|
|
2964
|
+
currencyQueued = null;
|
|
2965
|
+
if (!live())
|
|
1800
2966
|
return;
|
|
1801
2967
|
await conversations.reconcile();
|
|
1802
|
-
if (conversations.current()) {
|
|
2968
|
+
if (conversations.current() || !demand.conversations) {
|
|
1803
2969
|
show();
|
|
1804
2970
|
return;
|
|
1805
2971
|
}
|
|
1806
|
-
const
|
|
1807
|
-
|
|
1808
|
-
moveTo(transferring(resting));
|
|
2972
|
+
const progress = { complete: false, reachable: !status.providerUnreachable };
|
|
2973
|
+
moveTo(transferring(status));
|
|
1809
2974
|
try {
|
|
1810
2975
|
await conversations.catchUp(progress);
|
|
1811
2976
|
}
|
|
1812
2977
|
catch {
|
|
1813
2978
|
progress.reachable = false;
|
|
1814
2979
|
}
|
|
1815
|
-
|
|
1816
|
-
if (!closed) {
|
|
1817
|
-
moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
2980
|
+
await rest(progress, live);
|
|
1820
2981
|
}
|
|
1821
2982
|
function close() {
|
|
1822
2983
|
if (closed)
|
|
1823
2984
|
return;
|
|
1824
2985
|
closed = true;
|
|
2986
|
+
if (narrowing !== null)
|
|
2987
|
+
clearTimeout(narrowing);
|
|
2988
|
+
if (rechecking !== null)
|
|
2989
|
+
clearInterval(rechecking);
|
|
2990
|
+
readinessWatches.clear();
|
|
1825
2991
|
withdrawStream?.();
|
|
1826
2992
|
withdrawStream = undefined;
|
|
1827
2993
|
withdrawConversations?.();
|
|
1828
2994
|
withdrawConversations = undefined;
|
|
1829
2995
|
withdrawServiceMode?.();
|
|
1830
2996
|
conversations.close();
|
|
1831
|
-
|
|
2997
|
+
void recovery.retire();
|
|
1832
2998
|
store?.close();
|
|
1833
2999
|
void opening?.then((opened) => {
|
|
1834
3000
|
opened.close();
|
|
@@ -1838,6 +3004,7 @@ export function composeRuntime(options) {
|
|
|
1838
3004
|
storageLossListeners.clear();
|
|
1839
3005
|
statusListeners.clear();
|
|
1840
3006
|
serviceModeListeners.clear();
|
|
3007
|
+
memberListeners.clear();
|
|
1841
3008
|
readSide.close();
|
|
1842
3009
|
queueSubscriptions.clear();
|
|
1843
3010
|
roomDraftListeners.clear();
|
|
@@ -1845,9 +3012,8 @@ export function composeRuntime(options) {
|
|
|
1845
3012
|
}
|
|
1846
3013
|
async function stop() {
|
|
1847
3014
|
stopping = true;
|
|
1848
|
-
|
|
1849
|
-
await
|
|
1850
|
-
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
3015
|
+
await recovery.retire();
|
|
3016
|
+
await Promise.all(working);
|
|
1851
3017
|
await mutations;
|
|
1852
3018
|
close();
|
|
1853
3019
|
}
|
|
@@ -1869,14 +3035,69 @@ export function composeRuntime(options) {
|
|
|
1869
3035
|
return planOf(input, roomFacts(reach), planReach(reach));
|
|
1870
3036
|
}
|
|
1871
3037
|
function watchPlan(plan, deliver) {
|
|
1872
|
-
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);
|
|
1873
3043
|
}
|
|
1874
3044
|
async function searched(input, reach) {
|
|
1875
3045
|
const plan = searchPlanOf(input);
|
|
1876
|
-
const
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
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
|
+
}
|
|
1880
3101
|
}
|
|
1881
3102
|
const watch = (topic, input, deliver) => {
|
|
1882
3103
|
if (topic === 'room-drafts') {
|
|
@@ -1901,6 +3122,8 @@ export function composeRuntime(options) {
|
|
|
1901
3122
|
}
|
|
1902
3123
|
if (topic === 'conversations')
|
|
1903
3124
|
return conversations.watchList(deliver);
|
|
3125
|
+
if (topic === 'agents')
|
|
3126
|
+
return conversations.watchAgents(deliver);
|
|
1904
3127
|
if (topic === 'presentation') {
|
|
1905
3128
|
const listener = {
|
|
1906
3129
|
app: input[0],
|
|
@@ -1920,6 +3143,9 @@ export function composeRuntime(options) {
|
|
|
1920
3143
|
presentationListeners.delete(listener);
|
|
1921
3144
|
};
|
|
1922
3145
|
}
|
|
3146
|
+
if (topic === 'readiness') {
|
|
3147
|
+
return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
|
|
3148
|
+
}
|
|
1923
3149
|
if (topic === 'rooms') {
|
|
1924
3150
|
roomListeners.add(deliver);
|
|
1925
3151
|
deliver(listed());
|
|
@@ -1955,6 +3181,13 @@ export function composeRuntime(options) {
|
|
|
1955
3181
|
serviceModeListeners.delete(deliver);
|
|
1956
3182
|
};
|
|
1957
3183
|
}
|
|
3184
|
+
if (topic === 'member') {
|
|
3185
|
+
memberListeners.add(deliver);
|
|
3186
|
+
deliver(member);
|
|
3187
|
+
return () => {
|
|
3188
|
+
memberListeners.delete(deliver);
|
|
3189
|
+
};
|
|
3190
|
+
}
|
|
1958
3191
|
if (topic === 'queue') {
|
|
1959
3192
|
const queue = { locator: input[0], notify: deliver };
|
|
1960
3193
|
queueSubscriptions.add(queue);
|
|
@@ -1972,7 +3205,7 @@ export function composeRuntime(options) {
|
|
|
1972
3205
|
rooms() {
|
|
1973
3206
|
return listed();
|
|
1974
3207
|
},
|
|
1975
|
-
query: ((input) =>
|
|
3208
|
+
query: ((input) => rowsQuery(planned(input, null))),
|
|
1976
3209
|
search: (input) => searched(input, null),
|
|
1977
3210
|
types(locator) {
|
|
1978
3211
|
return {
|
|
@@ -2001,20 +3234,20 @@ export function composeRuntime(options) {
|
|
|
2001
3234
|
return exclusive(async () => {
|
|
2002
3235
|
const parts = readParts(input);
|
|
2003
3236
|
if (path === '' || normalize(path) !== path)
|
|
2004
|
-
throw
|
|
3237
|
+
throw runtimeError('room-draft-malformed');
|
|
2005
3238
|
const local = await openLocal();
|
|
2006
3239
|
const current = roomDrafts.get(room)?.get(path);
|
|
2007
3240
|
if ((current?.revision ?? null) !== revision) {
|
|
2008
|
-
return
|
|
2009
|
-
outcome: 'stale',
|
|
2010
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
2011
|
-
};
|
|
3241
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2012
3242
|
}
|
|
3243
|
+
const file = parts.file === undefined
|
|
3244
|
+
? undefined
|
|
3245
|
+
: await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
|
|
2013
3246
|
const record = {
|
|
2014
3247
|
locator: room,
|
|
2015
3248
|
path,
|
|
2016
3249
|
revision: roomDraftRevision(),
|
|
2017
|
-
parts: held(current?.parts, parts),
|
|
3250
|
+
parts: held(current?.parts, parts, file),
|
|
2018
3251
|
returned: current?.returned ?? null,
|
|
2019
3252
|
};
|
|
2020
3253
|
await local.keepRoomDraft(record);
|
|
@@ -2028,10 +3261,7 @@ export function composeRuntime(options) {
|
|
|
2028
3261
|
const local = await openLocal();
|
|
2029
3262
|
const current = roomDrafts.get(room)?.get(path);
|
|
2030
3263
|
if (current === undefined || current.revision !== revision) {
|
|
2031
|
-
return
|
|
2032
|
-
outcome: 'stale',
|
|
2033
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
2034
|
-
};
|
|
3264
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2035
3265
|
}
|
|
2036
3266
|
await local.discardRoomDraft(room, path);
|
|
2037
3267
|
forgetRoomDraft(room, path);
|
|
@@ -2066,6 +3296,10 @@ export function composeRuntime(options) {
|
|
|
2066
3296
|
history(locator, path, query) {
|
|
2067
3297
|
return readHistory(locator, path, query);
|
|
2068
3298
|
},
|
|
3299
|
+
binaryFile: {
|
|
3300
|
+
read: readBinaryPath,
|
|
3301
|
+
readReference: readBinaryReference,
|
|
3302
|
+
},
|
|
2069
3303
|
call(name, input) {
|
|
2070
3304
|
return invokeAction(name, input);
|
|
2071
3305
|
},
|
|
@@ -2078,30 +3312,54 @@ export function composeRuntime(options) {
|
|
|
2078
3312
|
return journal.export();
|
|
2079
3313
|
},
|
|
2080
3314
|
persistence() {
|
|
2081
|
-
return
|
|
3315
|
+
return persistence === 'granted';
|
|
3316
|
+
},
|
|
3317
|
+
async retry() {
|
|
3318
|
+
journal.record('retry');
|
|
3319
|
+
unindexable.clear();
|
|
3320
|
+
queueReadiness();
|
|
3321
|
+
resume();
|
|
2082
3322
|
},
|
|
2083
3323
|
observe: observing(watch),
|
|
2084
3324
|
async reauthorize() {
|
|
2085
3325
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2086
3326
|
return;
|
|
2087
3327
|
journal.record('connection reauthorize');
|
|
2088
|
-
await
|
|
3328
|
+
await Promise.all([...lanes.values()]
|
|
3329
|
+
.filter((held) => held.generation === generation)
|
|
3330
|
+
.map((held) => held.chain));
|
|
2089
3331
|
await mutations;
|
|
2090
3332
|
authorization = undefined;
|
|
2091
3333
|
discoveryPaused = false;
|
|
2092
3334
|
if (options.adapter.roomStream === undefined)
|
|
2093
3335
|
await sync('unstreamed');
|
|
2094
3336
|
else
|
|
2095
|
-
await task(ensureAuthorization);
|
|
3337
|
+
await task(OPENING_LANE, ensureAuthorization);
|
|
2096
3338
|
},
|
|
2097
3339
|
};
|
|
2098
3340
|
const reachable = {
|
|
2099
3341
|
md,
|
|
2100
3342
|
watch,
|
|
3343
|
+
referrerScope: (referrer) => options.adapter.referrerScope(referrer),
|
|
2101
3344
|
plan: planned,
|
|
2102
|
-
query: (plan) =>
|
|
3345
|
+
query: (plan) => rowsQuery(plan),
|
|
2103
3346
|
watchPlan,
|
|
2104
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),
|
|
2105
3364
|
};
|
|
2106
|
-
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2107
3365
|
}
|