@agentshouse/mdruntime 0.1.0-ca1eff92bbe73 → 0.1.0-ca4dc698bd0c4
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 -209
- 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 +76 -17
- package/dist/adapter.d.ts.map +1 -1
- package/dist/attempt.d.ts +16 -0
- package/dist/attempt.d.ts.map +1 -0
- package/dist/attempt.js +70 -0
- package/dist/binary.d.ts +23 -0
- package/dist/binary.d.ts.map +1 -0
- package/dist/binary.js +8 -0
- package/dist/bootstrap.d.ts +17 -5
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +109 -37
- package/dist/change-set.d.ts +2 -0
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +48 -16
- package/dist/change.d.ts +15 -0
- package/dist/change.d.ts.map +1 -1
- package/dist/change.js +26 -1
- package/dist/chat/acts.d.ts +33 -0
- package/dist/chat/acts.d.ts.map +1 -0
- package/dist/chat/acts.js +36 -0
- package/dist/chat/context.d.ts +3 -0
- package/dist/chat/context.d.ts.map +1 -0
- package/dist/chat/context.js +10 -0
- package/dist/chat/input.d.ts +9 -0
- package/dist/chat/input.d.ts.map +1 -0
- package/dist/chat/input.js +99 -0
- package/dist/chat/kit.d.ts +60 -0
- package/dist/chat/kit.d.ts.map +1 -0
- package/dist/chat/kit.js +123 -0
- package/dist/chat/lasting.d.ts +6 -0
- package/dist/chat/lasting.d.ts.map +1 -0
- package/dist/chat/lasting.js +19 -0
- package/dist/chat/list.d.ts +26 -0
- package/dist/chat/list.d.ts.map +1 -0
- package/dist/chat/list.js +62 -0
- package/dist/chat/markdown.d.ts +20 -0
- package/dist/chat/markdown.d.ts.map +1 -0
- package/dist/chat/markdown.js +188 -0
- package/dist/chat/message.d.ts +6 -0
- package/dist/chat/message.d.ts.map +1 -0
- package/dist/chat/message.js +192 -0
- package/dist/chat/messages.d.ts +21 -0
- package/dist/chat/messages.d.ts.map +1 -0
- package/dist/chat/messages.js +151 -0
- package/dist/chat/name.d.ts +6 -0
- package/dist/chat/name.d.ts.map +1 -0
- package/dist/chat/name.js +37 -0
- package/dist/chat/opened.d.ts +11 -0
- package/dist/chat/opened.d.ts.map +1 -0
- package/dist/chat/opened.js +17 -0
- package/dist/chat/overlay.d.ts +31 -0
- package/dist/chat/overlay.d.ts.map +1 -0
- package/dist/chat/overlay.js +97 -0
- package/dist/chat/parts.d.ts +57 -0
- package/dist/chat/parts.d.ts.map +1 -0
- package/dist/chat/parts.js +119 -0
- package/dist/chat/process.d.ts +20 -0
- package/dist/chat/process.d.ts.map +1 -0
- package/dist/chat/process.js +32 -0
- package/dist/chat/provider.d.ts +63 -0
- package/dist/chat/provider.d.ts.map +1 -0
- package/dist/chat/provider.js +620 -0
- package/dist/chat/question.d.ts +9 -0
- package/dist/chat/question.d.ts.map +1 -0
- package/dist/chat/question.js +80 -0
- package/dist/chat/questions.d.ts +31 -0
- package/dist/chat/questions.d.ts.map +1 -0
- package/dist/chat/questions.js +111 -0
- package/dist/chat/read.d.ts +5 -0
- package/dist/chat/read.d.ts.map +1 -0
- package/dist/chat/read.js +13 -0
- package/dist/chat/ready.d.ts +33 -0
- package/dist/chat/ready.d.ts.map +1 -0
- package/dist/chat/ready.js +91 -0
- package/dist/chat/style.d.ts +4 -0
- package/dist/chat/style.d.ts.map +1 -0
- package/dist/chat/style.js +302 -0
- package/dist/chat.d.ts +14 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +8 -0
- package/dist/client.d.ts +23 -11
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +67 -16
- package/dist/collection.d.ts +21 -12
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +271 -23
- package/dist/connection.d.ts +41 -15
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +1631 -378
- package/dist/conversation.d.ts +24 -10
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +307 -136
- 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 +13 -2
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +48 -14
- package/dist/identity.d.ts +5 -0
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +19 -1
- package/dist/index.d.ts +14 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/memory.d.ts +6 -3
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +70 -22
- package/dist/model.d.ts +4 -2
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +9 -0
- package/dist/observation.d.ts +5 -2
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +22 -11
- package/dist/open.d.ts +2 -0
- package/dist/open.d.ts.map +1 -0
- package/dist/open.js +1 -0
- package/dist/outcome.d.ts +32 -3
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +42 -1
- package/dist/overlay.d.ts +15 -3
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +94 -77
- package/dist/query.d.ts +32 -3
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +5 -31
- package/dist/reach.d.ts +8 -2
- 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 +291 -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 +43 -38
- package/dist/replica.d.ts +8 -5
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +115 -67
- package/dist/room-draft.d.ts +26 -9
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +132 -23
- package/dist/rows.d.ts +8 -13
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +12 -14
- package/dist/save.d.ts +31 -1
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +376 -45
- package/dist/search.d.ts +31 -8
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +108 -45
- package/dist/shared.d.ts +11 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +368 -0
- package/dist/status.d.ts +6 -2
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +0 -8
- package/dist/store.d.ts +73 -17
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +531 -198
- 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 +9 -3
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +17 -2
- package/package.json +26 -14
- package/dist/attachment.d.ts +0 -3
- package/dist/attachment.d.ts.map +0 -1
- package/dist/attachment.js +0 -8
package/dist/connection.js
CHANGED
|
@@ -3,19 +3,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,486 @@ 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 target = targets[port.target];
|
|
647
|
+
const search = target?.search === true;
|
|
648
|
+
const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
|
|
649
|
+
const building = (state) => coverage
|
|
650
|
+
.filter((room) => room.state === state && !unindexable.has(room.room))
|
|
651
|
+
.map(({ room }) => room);
|
|
652
|
+
const preparing = building('preparing');
|
|
653
|
+
if (preparing.length > 0)
|
|
654
|
+
prepare(preparing);
|
|
655
|
+
const indexing = building('indexing');
|
|
656
|
+
if (indexing.length > 0)
|
|
657
|
+
index(indexing, []);
|
|
658
|
+
return readinessOf({
|
|
659
|
+
code: port.code,
|
|
660
|
+
coverage: {
|
|
661
|
+
listed: required.listed,
|
|
662
|
+
rooms: coverage,
|
|
663
|
+
conversations: conversationCoverage(reach, search),
|
|
664
|
+
search: search ? 'required' : 'unused',
|
|
665
|
+
},
|
|
666
|
+
persistence,
|
|
667
|
+
connection: connectionOf(),
|
|
668
|
+
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
669
|
+
capacity: capacity !== null &&
|
|
670
|
+
coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
|
|
671
|
+
? capacity
|
|
672
|
+
: port.capacity,
|
|
673
|
+
refused: refusedIn(required.rooms),
|
|
674
|
+
lost: storageLoss !== null,
|
|
675
|
+
established: target?.established === true,
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
function keepTarget(name, change) {
|
|
679
|
+
const held = targets[name] ?? { established: false, search: false };
|
|
680
|
+
const next = { ...held, ...change };
|
|
681
|
+
if (next.established === held.established && next.search === held.search)
|
|
682
|
+
return;
|
|
683
|
+
targets = { ...targets, [name]: next };
|
|
684
|
+
queueReadiness();
|
|
685
|
+
void durably((local) => local.keepTargets(targets)).catch(() => {
|
|
686
|
+
journal.record(`target record failed ${name}`);
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
function deliverReadiness(watch) {
|
|
690
|
+
if (!readinessWatches.has(watch))
|
|
691
|
+
return;
|
|
692
|
+
const readiness = readinessFor(watch.port, watch.reach);
|
|
693
|
+
if (readiness.ready && targets[watch.port.target]?.established !== true) {
|
|
694
|
+
keepTarget(watch.port.target, { established: true });
|
|
695
|
+
}
|
|
696
|
+
const shown = JSON.stringify(readiness);
|
|
697
|
+
if (shown === watch.last)
|
|
698
|
+
return;
|
|
699
|
+
watch.last = shown;
|
|
700
|
+
watch.deliver(structuredClone(readiness));
|
|
701
|
+
}
|
|
702
|
+
function queueReadiness() {
|
|
703
|
+
if (readinessQueued || closed || readinessWatches.size === 0)
|
|
704
|
+
return;
|
|
705
|
+
readinessQueued = true;
|
|
706
|
+
queueMicrotask(() => {
|
|
707
|
+
readinessQueued = false;
|
|
708
|
+
if (closed)
|
|
709
|
+
return;
|
|
710
|
+
for (const watch of [...readinessWatches])
|
|
711
|
+
deliverReadiness(watch);
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
function watchReadiness(port, reach, deliver) {
|
|
715
|
+
const watch = { port, reach, deliver, last: null };
|
|
716
|
+
readinessWatches.add(watch);
|
|
717
|
+
void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
|
|
718
|
+
return () => {
|
|
719
|
+
readinessWatches.delete(watch);
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
function searching(target) {
|
|
723
|
+
if (targets[target]?.search !== true)
|
|
724
|
+
keepTarget(target, { search: true });
|
|
725
|
+
}
|
|
726
|
+
function layerBytes(layer) {
|
|
727
|
+
let bytes = 0;
|
|
728
|
+
for (const [path, text] of Object.entries(layer.documents))
|
|
729
|
+
bytes += (path.length + text.length) * 2;
|
|
730
|
+
return bytes;
|
|
731
|
+
}
|
|
732
|
+
function grownBy(next, previous) {
|
|
733
|
+
if (previous === undefined)
|
|
734
|
+
return layerBytes(next);
|
|
735
|
+
const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
|
|
736
|
+
return layerBytes({ ...next, documents });
|
|
737
|
+
}
|
|
738
|
+
function exceeded(error) {
|
|
739
|
+
return (typeof error === 'object' &&
|
|
740
|
+
error !== null &&
|
|
741
|
+
error.name === 'QuotaExceededError');
|
|
742
|
+
}
|
|
743
|
+
async function estimated() {
|
|
744
|
+
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
745
|
+
return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
|
|
746
|
+
}
|
|
747
|
+
function pause(locator, cause) {
|
|
748
|
+
paused.add(locator);
|
|
749
|
+
capacity = cause;
|
|
750
|
+
journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
|
|
751
|
+
queueReadiness();
|
|
752
|
+
if (rechecking !== null)
|
|
753
|
+
return;
|
|
754
|
+
// A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
|
|
755
|
+
rechecking = setInterval(() => {
|
|
756
|
+
void recheck();
|
|
757
|
+
}, 60_000);
|
|
758
|
+
}
|
|
759
|
+
async function recheck() {
|
|
760
|
+
const held = capacity;
|
|
761
|
+
if (held === null || closed)
|
|
762
|
+
return;
|
|
763
|
+
const available = await estimated().catch(() => null);
|
|
764
|
+
if (available === null || available < (held.needed ?? 0))
|
|
765
|
+
return;
|
|
766
|
+
if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
|
|
767
|
+
return;
|
|
768
|
+
resume();
|
|
769
|
+
}
|
|
770
|
+
function resume() {
|
|
771
|
+
if (rechecking !== null)
|
|
772
|
+
clearInterval(rechecking);
|
|
773
|
+
rechecking = null;
|
|
774
|
+
if (paused.size === 0 && capacity === null)
|
|
775
|
+
return;
|
|
776
|
+
paused.clear();
|
|
777
|
+
capacity = null;
|
|
778
|
+
journal.record('capacity resumed');
|
|
779
|
+
queueReadiness();
|
|
780
|
+
if (!closed && authorization !== undefined)
|
|
781
|
+
void sync('unstreamed');
|
|
782
|
+
}
|
|
783
|
+
async function reclaimed(local) {
|
|
784
|
+
const unreferenced = [...transfers.keys()].filter((locator) => {
|
|
785
|
+
const room = discovered.get(locator);
|
|
786
|
+
return room === undefined || !acquires(room);
|
|
787
|
+
});
|
|
788
|
+
for (const locator of unreferenced) {
|
|
789
|
+
await local.abandon(locator);
|
|
790
|
+
transfers.delete(locator);
|
|
791
|
+
}
|
|
792
|
+
if (unreferenced.length > 0)
|
|
793
|
+
journal.record(`capacity reclaimed ${unreferenced.length}`);
|
|
794
|
+
return unreferenced.length > 0;
|
|
795
|
+
}
|
|
796
|
+
async function admitted(local, locator, needed) {
|
|
797
|
+
if (paused.has(locator))
|
|
798
|
+
return false;
|
|
799
|
+
let available = await estimated().catch(() => null);
|
|
800
|
+
if (available === null || available >= needed)
|
|
801
|
+
return true;
|
|
802
|
+
if (await reclaimed(local))
|
|
803
|
+
available = await estimated().catch(() => null);
|
|
804
|
+
if (available === null || available >= needed)
|
|
805
|
+
return true;
|
|
806
|
+
pause(locator, { cause: 'insufficient-space', needed, available });
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
async function exceededOn(locator, bytes) {
|
|
810
|
+
const available = await estimated().catch(() => null);
|
|
811
|
+
pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
|
|
812
|
+
}
|
|
813
|
+
async function rowsQuery(plan) {
|
|
814
|
+
const before = covered(plan);
|
|
815
|
+
awaitPreparation(before);
|
|
816
|
+
if ('pending' in before)
|
|
817
|
+
return before;
|
|
818
|
+
const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
|
|
819
|
+
awaitPreparation(cover);
|
|
820
|
+
if ('pending' in cover)
|
|
821
|
+
return cover;
|
|
822
|
+
return 'groups' in rows
|
|
823
|
+
? { groups: rows.groups, coverage: cover.coverage }
|
|
824
|
+
: { documents: rows.documents, coverage: cover.coverage };
|
|
825
|
+
}
|
|
826
|
+
async function indexConversation(conversation) {
|
|
827
|
+
searchIndex.indexMessages(conversation, [], false);
|
|
828
|
+
let after = null;
|
|
829
|
+
for (;;) {
|
|
830
|
+
const batch = await durably((local) => local.messagesAfter(conversation, after));
|
|
831
|
+
if (closed)
|
|
832
|
+
return;
|
|
833
|
+
searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
|
|
834
|
+
if (batch.length === 0)
|
|
835
|
+
return;
|
|
836
|
+
after = batch[batch.length - 1].identity;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
function indexNext() {
|
|
840
|
+
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
|
|
841
|
+
if (room !== undefined) {
|
|
842
|
+
roomsToIndex.delete(room);
|
|
843
|
+
return () => prepared.has(room)
|
|
844
|
+
? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
|
|
845
|
+
: undefined;
|
|
846
|
+
}
|
|
847
|
+
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
848
|
+
if (conversation === undefined)
|
|
849
|
+
return null;
|
|
850
|
+
conversationsToIndex.delete(conversation);
|
|
851
|
+
return () => indexConversation(conversation);
|
|
852
|
+
}
|
|
853
|
+
function index(rooms, named) {
|
|
854
|
+
for (const room of rooms)
|
|
855
|
+
roomsToIndex.add(room);
|
|
856
|
+
for (const conversation of named)
|
|
857
|
+
conversationsToIndex.add(conversation);
|
|
858
|
+
if (indexing)
|
|
859
|
+
return;
|
|
860
|
+
indexing = true;
|
|
861
|
+
void (async () => {
|
|
862
|
+
try {
|
|
863
|
+
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
864
|
+
await yielded();
|
|
865
|
+
await unit();
|
|
866
|
+
queueReadiness();
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
finally {
|
|
870
|
+
indexing = false;
|
|
871
|
+
}
|
|
872
|
+
})().catch(() => { });
|
|
289
873
|
}
|
|
290
874
|
function publishChangeSet(set) {
|
|
291
875
|
applyChangeSet(set);
|
|
292
|
-
if (set.rooms.length > 0
|
|
876
|
+
if (set.rooms.length > 0 ||
|
|
877
|
+
[...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
|
|
293
878
|
emitRooms();
|
|
294
879
|
for (const locator of set.queues)
|
|
295
880
|
emitQueue(locator);
|
|
@@ -301,34 +886,7 @@ export function composeRuntime(options) {
|
|
|
301
886
|
emitPresentation(app);
|
|
302
887
|
conversations.changed(set);
|
|
303
888
|
}
|
|
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
889
|
async function refuse(reason, provider = null) {
|
|
331
|
-
stopTimer();
|
|
332
890
|
discoveryPaused = true;
|
|
333
891
|
try {
|
|
334
892
|
if (reason === 'revoked-authorization') {
|
|
@@ -336,10 +894,7 @@ export function composeRuntime(options) {
|
|
|
336
894
|
const revoked = { status: 'unavailable', reason: 'revoked' };
|
|
337
895
|
try {
|
|
338
896
|
const local = await openLocal();
|
|
339
|
-
await local.revokeModels([...replicas.values()].map((replica) =>
|
|
340
|
-
record: replica.confirmed.record,
|
|
341
|
-
queue: replica.queue,
|
|
342
|
-
})));
|
|
897
|
+
await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
|
|
343
898
|
}
|
|
344
899
|
catch {
|
|
345
900
|
journal.record('model revocation persistence failed');
|
|
@@ -363,10 +918,7 @@ export function composeRuntime(options) {
|
|
|
363
918
|
model: revoked,
|
|
364
919
|
selectionModel: revoked,
|
|
365
920
|
};
|
|
366
|
-
|
|
367
|
-
replicas.set(locator, read.kind === 'staged'
|
|
368
|
-
? replicaOf(read.confirmed, before.queue, before)
|
|
369
|
-
: { ...before, confirmed: { ...before.confirmed, record } });
|
|
921
|
+
replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
|
|
370
922
|
}
|
|
371
923
|
rooms.set(locator, {
|
|
372
924
|
...room,
|
|
@@ -383,7 +935,8 @@ export function composeRuntime(options) {
|
|
|
383
935
|
}
|
|
384
936
|
finally {
|
|
385
937
|
refusal = {
|
|
386
|
-
reason,
|
|
938
|
+
code: reason,
|
|
939
|
+
retryable: false,
|
|
387
940
|
reauthorizationOffered: reason === 'revoked-authorization',
|
|
388
941
|
refusal: provider,
|
|
389
942
|
};
|
|
@@ -408,19 +961,28 @@ export function composeRuntime(options) {
|
|
|
408
961
|
},
|
|
409
962
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
410
963
|
roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
|
|
411
|
-
attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
|
|
412
964
|
synchronizationAvailable,
|
|
413
965
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
414
966
|
});
|
|
415
967
|
}
|
|
416
|
-
async function applyDiscovery(
|
|
968
|
+
async function applyDiscovery(found, started) {
|
|
969
|
+
for (const room of found) {
|
|
970
|
+
const fence = fences.get(room.locator);
|
|
971
|
+
if (fence !== undefined && fence.erased && fence.at < started)
|
|
972
|
+
fences.delete(room.locator);
|
|
973
|
+
}
|
|
974
|
+
const listed = found.filter((room) => !fences.has(room.locator));
|
|
417
975
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
418
976
|
for (const locator of listedLocators)
|
|
419
977
|
scopedRefusals.delete(locator);
|
|
420
978
|
discovered.clear();
|
|
421
979
|
for (const room of listed)
|
|
422
980
|
discovered.set(room.locator, room);
|
|
423
|
-
|
|
981
|
+
const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
|
|
982
|
+
if (options.adapter.exhaustiveDiscovery === true)
|
|
983
|
+
await loseRooms(unlisted);
|
|
984
|
+
else
|
|
985
|
+
unlistedKept(unlisted);
|
|
424
986
|
for (const room of listed) {
|
|
425
987
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
426
988
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -431,43 +993,85 @@ export function composeRuntime(options) {
|
|
|
431
993
|
}
|
|
432
994
|
roomsListed = true;
|
|
433
995
|
emitRooms();
|
|
996
|
+
return listed;
|
|
434
997
|
}
|
|
435
998
|
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);
|
|
999
|
+
return [
|
|
1000
|
+
...new Set([
|
|
1001
|
+
...rooms.keys(),
|
|
1002
|
+
...roomDrafts.keys(),
|
|
1003
|
+
...transfers.keys(),
|
|
1004
|
+
...queuesWithoutReplica.keys(),
|
|
1005
|
+
]),
|
|
1006
|
+
].filter((locator) => !listed.has(locator));
|
|
1007
|
+
}
|
|
1008
|
+
function unlistedKept(unlisted) {
|
|
1009
|
+
for (const locator of unlisted) {
|
|
1010
|
+
const held = replicas.get(locator)?.confirmed.record;
|
|
1011
|
+
if (held !== undefined)
|
|
1012
|
+
list(held, false);
|
|
1013
|
+
else
|
|
453
1014
|
rooms.delete(locator);
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
function fence(locators) {
|
|
1018
|
+
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
1019
|
+
const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
|
|
1020
|
+
room: true,
|
|
1021
|
+
queue: true,
|
|
1022
|
+
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
1023
|
+
}));
|
|
1024
|
+
for (const locator of fresh) {
|
|
1025
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
1026
|
+
lastFenced.set(locator, ++fencings);
|
|
1027
|
+
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
1028
|
+
pendings.delete(batch.id);
|
|
1029
|
+
replicas.delete(locator);
|
|
1030
|
+
queuesWithoutReplica.delete(locator);
|
|
1031
|
+
rooms.delete(locator);
|
|
1032
|
+
roomDrafts.delete(locator);
|
|
1033
|
+
transfers.delete(locator);
|
|
1034
|
+
catching.delete(locator);
|
|
1035
|
+
discovered.delete(locator);
|
|
1036
|
+
announced.delete(locator);
|
|
1037
|
+
unreadable.delete(locator);
|
|
1038
|
+
stale.delete(locator);
|
|
1039
|
+
repairable.delete(locator);
|
|
1040
|
+
unindexable.delete(locator);
|
|
1041
|
+
paused.delete(locator);
|
|
1042
|
+
scopedRefusals.delete(locator);
|
|
1043
|
+
sourceRefusals.delete(locator);
|
|
1044
|
+
declarationRefusals.delete(locator);
|
|
1045
|
+
for (const attached of ['collection', 'typed-selection']) {
|
|
1046
|
+
const key = modelKey(locator, attached);
|
|
1047
|
+
models.delete(key);
|
|
1048
|
+
liveModels.delete(key);
|
|
1049
|
+
invalidModels.delete(key);
|
|
467
1050
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
1051
|
+
journal.record(`room lost ${locator}`);
|
|
1052
|
+
}
|
|
1053
|
+
if (fresh.length === 0)
|
|
1054
|
+
return fresh;
|
|
1055
|
+
publishChangeSet(mergeChangeSets(removed));
|
|
1056
|
+
for (const locator of fresh)
|
|
1057
|
+
prepared.delete(locator);
|
|
1058
|
+
searchIndex.forget(fresh);
|
|
1059
|
+
readSide.recover();
|
|
1060
|
+
moveTo({ ...status, ...attention() });
|
|
1061
|
+
return fresh;
|
|
1062
|
+
}
|
|
1063
|
+
async function erased(local, locators) {
|
|
1064
|
+
await local.erase(locators);
|
|
1065
|
+
for (const locator of locators) {
|
|
1066
|
+
const held = fences.get(locator);
|
|
1067
|
+
if (held !== undefined)
|
|
1068
|
+
held.erased = true;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
async function loseRooms(locators) {
|
|
1072
|
+
const fresh = fence(locators);
|
|
1073
|
+
if (fresh.length > 0)
|
|
1074
|
+
await durably((local) => erased(local, fresh));
|
|
471
1075
|
}
|
|
472
1076
|
function restored(key, persisted) {
|
|
473
1077
|
const prior = models.get(key);
|
|
@@ -488,7 +1092,7 @@ export function composeRuntime(options) {
|
|
|
488
1092
|
return model;
|
|
489
1093
|
}
|
|
490
1094
|
async function hydrate(opened) {
|
|
491
|
-
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
|
|
1095
|
+
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
|
|
492
1096
|
opened.all(),
|
|
493
1097
|
opened.queues(),
|
|
494
1098
|
opened.roomDrafts(),
|
|
@@ -498,9 +1102,17 @@ export function composeRuntime(options) {
|
|
|
498
1102
|
opened.lastCompleted(),
|
|
499
1103
|
opened.providerLimits(),
|
|
500
1104
|
opened.capabilities(),
|
|
1105
|
+
opened.member(),
|
|
1106
|
+
opened.transfers(),
|
|
1107
|
+
opened.targets(),
|
|
501
1108
|
]);
|
|
1109
|
+
targets = heldTargets;
|
|
1110
|
+
transfers.clear();
|
|
1111
|
+
for (const record of staged)
|
|
1112
|
+
transfers.set(record.locator, record);
|
|
502
1113
|
limits = declared;
|
|
503
1114
|
capabilities = named;
|
|
1115
|
+
member = known;
|
|
504
1116
|
roomDrafts.clear();
|
|
505
1117
|
for (const record of kept)
|
|
506
1118
|
rememberRoomDraft(record);
|
|
@@ -516,18 +1128,17 @@ export function composeRuntime(options) {
|
|
|
516
1128
|
}
|
|
517
1129
|
const previousReplicas = new Map(replicas);
|
|
518
1130
|
replicas.clear();
|
|
1131
|
+
queuesWithoutReplica.clear();
|
|
1132
|
+
const replicated = new Set(records.map((record) => record.locator));
|
|
1133
|
+
for (const [locator, queue] of queuedBatches) {
|
|
1134
|
+
if (!replicated.has(locator))
|
|
1135
|
+
queuesWithoutReplica.set(locator, queue);
|
|
1136
|
+
}
|
|
519
1137
|
for (const record of records) {
|
|
520
1138
|
const model = restored(modelKey(record.locator, 'collection'), record.model);
|
|
521
1139
|
const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
|
|
522
1140
|
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
|
-
};
|
|
1141
|
+
const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
|
|
531
1142
|
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
|
|
532
1143
|
const existing = rooms.get(record.locator);
|
|
533
1144
|
if (existing === undefined)
|
|
@@ -536,7 +1147,6 @@ export function composeRuntime(options) {
|
|
|
536
1147
|
rooms.set(record.locator, {
|
|
537
1148
|
...existing,
|
|
538
1149
|
roomVersion: record.roomVersion,
|
|
539
|
-
attachments: record.attachments,
|
|
540
1150
|
operations: record.operations,
|
|
541
1151
|
replica: record.replica,
|
|
542
1152
|
model: modelTypes(model),
|
|
@@ -548,10 +1158,6 @@ export function composeRuntime(options) {
|
|
|
548
1158
|
},
|
|
549
1159
|
});
|
|
550
1160
|
}
|
|
551
|
-
if (read.kind === 'refused') {
|
|
552
|
-
refusedSource(record, read);
|
|
553
|
-
declarationRefusals.add(record.locator);
|
|
554
|
-
}
|
|
555
1161
|
}
|
|
556
1162
|
return {
|
|
557
1163
|
lastCompleted,
|
|
@@ -562,16 +1168,26 @@ export function composeRuntime(options) {
|
|
|
562
1168
|
async function readState(opened) {
|
|
563
1169
|
const { lastCompleted, locators, read } = await hydrate(opened);
|
|
564
1170
|
applyChangeSet(read);
|
|
1171
|
+
prepare();
|
|
565
1172
|
if (replicas.size > 0)
|
|
566
1173
|
emitRooms();
|
|
1174
|
+
if (member !== null)
|
|
1175
|
+
emitMember();
|
|
567
1176
|
for (const locator of locators)
|
|
568
1177
|
emitQueue(locator);
|
|
569
1178
|
publishRoomDrafts(false);
|
|
570
1179
|
return lastCompleted;
|
|
571
1180
|
}
|
|
572
1181
|
async function open() {
|
|
573
|
-
|
|
1182
|
+
if (closed)
|
|
1183
|
+
throw new Error('runtime closed');
|
|
1184
|
+
const opened = await openRuntimeStore(isFenced);
|
|
574
1185
|
store = opened;
|
|
1186
|
+
const interrupted = await opened.fences();
|
|
1187
|
+
for (const locator of interrupted) {
|
|
1188
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
1189
|
+
lastFenced.set(locator, ++fencings);
|
|
1190
|
+
}
|
|
575
1191
|
const kept = options.shell.generation;
|
|
576
1192
|
const storedGeneration = await opened.generation();
|
|
577
1193
|
const loss = detectStorageLoss(storedGeneration, kept);
|
|
@@ -585,6 +1201,8 @@ export function composeRuntime(options) {
|
|
|
585
1201
|
emitStorageLoss();
|
|
586
1202
|
}
|
|
587
1203
|
const lastCompleted = await readState(opened);
|
|
1204
|
+
if (interrupted.length > 0)
|
|
1205
|
+
await erased(opened, interrupted);
|
|
588
1206
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
589
1207
|
return opened;
|
|
590
1208
|
}
|
|
@@ -611,11 +1229,12 @@ export function composeRuntime(options) {
|
|
|
611
1229
|
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
612
1230
|
}
|
|
613
1231
|
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
614
|
-
const
|
|
1232
|
+
const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
1233
|
+
const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
|
|
615
1234
|
const kept = queue.filter((batch) => !withdrawn.includes(batch));
|
|
616
1235
|
const unqueued = withdrawn.map((batch) => ({
|
|
617
1236
|
batch,
|
|
618
|
-
returned: landed(batch, confirmed) ? null :
|
|
1237
|
+
returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
|
|
619
1238
|
}));
|
|
620
1239
|
const returning = [
|
|
621
1240
|
...[...unqueued].reverse(),
|
|
@@ -636,28 +1255,48 @@ export function composeRuntime(options) {
|
|
|
636
1255
|
});
|
|
637
1256
|
}
|
|
638
1257
|
}
|
|
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 ?? []) {
|
|
1258
|
+
await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
|
|
1259
|
+
for (const batch of previousQueue) {
|
|
647
1260
|
if (!kept.includes(batch))
|
|
648
1261
|
pendings.delete(batch.id);
|
|
649
1262
|
}
|
|
650
1263
|
for (const record of written.values())
|
|
651
1264
|
rememberRoomDraft(record);
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
1265
|
+
try {
|
|
1266
|
+
await local.write({
|
|
1267
|
+
next: confirmed.record,
|
|
1268
|
+
previous: before?.confirmed.record ?? null,
|
|
1269
|
+
...(settling.stored === undefined ? {} : { stored: settling.stored }),
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
catch (error) {
|
|
1273
|
+
const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
|
|
1274
|
+
if (held !== null)
|
|
1275
|
+
replicas.set(locator, held);
|
|
1276
|
+
else if (kept.length > 0)
|
|
1277
|
+
queuesWithoutReplica.set(locator, kept);
|
|
1278
|
+
else
|
|
1279
|
+
queuesWithoutReplica.delete(locator);
|
|
1280
|
+
publishChangeSet(changeSet(locator, before ?? null, held, {
|
|
1281
|
+
queue: kept.length !== previousQueue.length,
|
|
1282
|
+
drafts: [...written.keys()],
|
|
1283
|
+
}));
|
|
1284
|
+
moveTo({ ...status, ...attention() });
|
|
1285
|
+
throw error;
|
|
1286
|
+
}
|
|
1287
|
+
return () => {
|
|
1288
|
+
queuesWithoutReplica.delete(locator);
|
|
1289
|
+
for (const { batch, returned } of unqueued) {
|
|
1290
|
+
const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
|
|
1291
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
1292
|
+
}
|
|
1293
|
+
replicas.set(locator, replicaOf(confirmed, kept, before));
|
|
1294
|
+
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
1295
|
+
room: settling.room,
|
|
1296
|
+
queue: kept.length !== previousQueue.length,
|
|
1297
|
+
drafts: [...written.keys()],
|
|
1298
|
+
});
|
|
1299
|
+
};
|
|
661
1300
|
}
|
|
662
1301
|
function adoptModels(locator, record) {
|
|
663
1302
|
for (const [attached, model] of [
|
|
@@ -690,10 +1329,16 @@ export function composeRuntime(options) {
|
|
|
690
1329
|
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
691
1330
|
};
|
|
692
1331
|
}
|
|
693
|
-
function publish(local, room, confirmed,
|
|
1332
|
+
function publish(local, room, confirmed, landing, live) {
|
|
1333
|
+
const { base, stored } = landing;
|
|
694
1334
|
return exclusive(async () => {
|
|
695
|
-
if (
|
|
1335
|
+
if (stored !== undefined) {
|
|
1336
|
+
await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
|
|
1337
|
+
}
|
|
1338
|
+
if (!live()) {
|
|
1339
|
+
caughtUpTo(room.locator);
|
|
696
1340
|
return true;
|
|
1341
|
+
}
|
|
697
1342
|
const prior = replicas.get(room.locator);
|
|
698
1343
|
if (base !== undefined &&
|
|
699
1344
|
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
@@ -701,47 +1346,85 @@ export function composeRuntime(options) {
|
|
|
701
1346
|
return false;
|
|
702
1347
|
}
|
|
703
1348
|
adoptModels(room.locator, confirmed.record);
|
|
704
|
-
const queue = prior?.queue ?? [];
|
|
1349
|
+
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
705
1350
|
const roomVersion = confirmed.record.roomVersion;
|
|
706
1351
|
const accepted = queue.filter((batch) => {
|
|
707
1352
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
708
1353
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
709
1354
|
});
|
|
710
|
-
|
|
1355
|
+
let settled;
|
|
1356
|
+
try {
|
|
1357
|
+
settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
|
|
1358
|
+
}
|
|
1359
|
+
catch (error) {
|
|
1360
|
+
if (!exceeded(error))
|
|
1361
|
+
throw error;
|
|
1362
|
+
await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
|
|
1363
|
+
caughtUpTo(room.locator);
|
|
1364
|
+
return true;
|
|
1365
|
+
}
|
|
1366
|
+
const set = settled();
|
|
1367
|
+
if (stored !== undefined) {
|
|
1368
|
+
transfers.delete(room.locator);
|
|
1369
|
+
catching.delete(room.locator);
|
|
1370
|
+
}
|
|
711
1371
|
for (const batch of accepted)
|
|
712
1372
|
pendings.delete(batch.id);
|
|
713
1373
|
stale.delete(room.locator);
|
|
1374
|
+
unindexable.delete(room.locator);
|
|
714
1375
|
if (unreadable.delete(room.locator))
|
|
715
1376
|
scopedRefusals.delete(room.locator);
|
|
716
1377
|
sourceRefusals.delete(room.locator);
|
|
717
1378
|
declarationRefusals.delete(room.locator);
|
|
718
1379
|
list(room, true);
|
|
1380
|
+
if (prior === undefined)
|
|
1381
|
+
prepared.add(room.locator);
|
|
719
1382
|
publishChangeSet(set);
|
|
720
1383
|
moveTo({ ...status, ...attention() });
|
|
1384
|
+
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
721
1385
|
return true;
|
|
722
1386
|
});
|
|
723
1387
|
}
|
|
724
1388
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
725
1389
|
const local = await openLocal();
|
|
1390
|
+
await referenced(locator);
|
|
726
1391
|
const replica = replicas.get(locator);
|
|
727
1392
|
const room = rooms.get(locator);
|
|
728
1393
|
if (replica === undefined || room === undefined) {
|
|
729
|
-
throw
|
|
1394
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
730
1395
|
}
|
|
731
1396
|
const saved = [];
|
|
732
1397
|
for (const { path, revision } of named) {
|
|
733
1398
|
const record = roomDrafts.get(locator)?.get(path);
|
|
734
1399
|
if (record === undefined || record.revision !== revision) {
|
|
735
|
-
return { outcome: 'refused',
|
|
1400
|
+
return { outcome: 'refused', code: 'stale', retryable: false, file: path };
|
|
736
1401
|
}
|
|
737
1402
|
if (!saved.some((draft) => draft.path === path))
|
|
738
1403
|
saved.push({ path, parts: record.parts });
|
|
739
1404
|
}
|
|
740
1405
|
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
|
|
741
1406
|
if (changed.length > 0)
|
|
742
|
-
return { outcome: 'refused',
|
|
1407
|
+
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
743
1408
|
const parts = saved.map(({ path, parts }) => changesOf(path, parts));
|
|
744
|
-
const
|
|
1409
|
+
const changes = parts.flatMap(({ changes }) => changes);
|
|
1410
|
+
const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
|
|
1411
|
+
if (requeue.kind === 'refused')
|
|
1412
|
+
return { outcome: 'refused', ...requeue.refusal };
|
|
1413
|
+
const foreign = [...requeue.withdrawn.keys()].some((origin) => {
|
|
1414
|
+
const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
|
|
1415
|
+
return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
|
|
1416
|
+
});
|
|
1417
|
+
if (foreign) {
|
|
1418
|
+
return {
|
|
1419
|
+
outcome: 'refused',
|
|
1420
|
+
code: 'unknown-revision',
|
|
1421
|
+
retryable: false,
|
|
1422
|
+
file: [...requeue.queued].at(-1),
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
const withdrawing = requeue.withdrawn.size > 0;
|
|
1426
|
+
const input = {
|
|
1427
|
+
local: localSaves,
|
|
745
1428
|
baseRoomVersion: replica.confirmed.record.roomVersion,
|
|
746
1429
|
composition: compositionFor(replica.confirmed.record),
|
|
747
1430
|
operations: room.operations,
|
|
@@ -749,29 +1432,93 @@ export function composeRuntime(options) {
|
|
|
749
1432
|
providerLimits: limits,
|
|
750
1433
|
handle: room.handle,
|
|
751
1434
|
visible: replica.visible,
|
|
752
|
-
changes
|
|
1435
|
+
changes,
|
|
753
1436
|
bases: parts.flatMap(({ bases }) => bases),
|
|
754
1437
|
writtenBy,
|
|
1438
|
+
clean: {
|
|
1439
|
+
text: replica.confirmed.record.documents,
|
|
1440
|
+
binaries: replica.confirmed.record.binaries,
|
|
1441
|
+
composition: compositionOf(replica.confirmed.record),
|
|
1442
|
+
},
|
|
1443
|
+
};
|
|
1444
|
+
const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
|
|
1445
|
+
if (checked?.kind === 'refused')
|
|
1446
|
+
return { outcome: 'refused', ...checked.refusal };
|
|
1447
|
+
const owners = [...requeue.owners].flatMap((origin) => {
|
|
1448
|
+
const batch = requeue.withdrawn.get(origin);
|
|
1449
|
+
return batch === undefined || batch === null ? [] : [{ origin, batch }];
|
|
755
1450
|
});
|
|
756
|
-
if (
|
|
1451
|
+
if (owners.length > 1) {
|
|
1452
|
+
return {
|
|
1453
|
+
outcome: 'refused',
|
|
1454
|
+
code: 'unknown-revision',
|
|
1455
|
+
retryable: false,
|
|
1456
|
+
file: [...requeue.queued].at(-1),
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
const built = !withdrawing
|
|
1460
|
+
? buildBatch(input)
|
|
1461
|
+
: requeue.changes.length === 0
|
|
1462
|
+
? null
|
|
1463
|
+
: buildBatch({
|
|
1464
|
+
...input,
|
|
1465
|
+
visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
|
|
1466
|
+
changes: requeue.changes,
|
|
1467
|
+
});
|
|
1468
|
+
if (built?.kind === 'refused')
|
|
757
1469
|
return { outcome: 'refused', ...built.refusal };
|
|
758
|
-
const
|
|
1470
|
+
const [owner] = owners;
|
|
1471
|
+
const join = built === null || owner === undefined
|
|
1472
|
+
? null
|
|
1473
|
+
: joined(requeue.queue, owner.batch, built.batch);
|
|
1474
|
+
if (join?.kind === 'refused')
|
|
1475
|
+
return { outcome: 'refused', ...join.refusal };
|
|
1476
|
+
const withdrawn = new Map(requeue.withdrawn);
|
|
1477
|
+
if (join !== null && owner !== undefined)
|
|
1478
|
+
withdrawn.set(owner.origin, join.batch);
|
|
1479
|
+
const drafted = new Map();
|
|
1480
|
+
for (const [origin, replaced] of withdrawn) {
|
|
1481
|
+
if (replaced === null)
|
|
1482
|
+
continue;
|
|
1483
|
+
const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
|
|
1484
|
+
drafted.set(replaced.id, replaced === join?.batch
|
|
1485
|
+
? [
|
|
1486
|
+
...carried.map((held) => joinedDraft(held, saved)),
|
|
1487
|
+
...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
|
|
1488
|
+
]
|
|
1489
|
+
: carried);
|
|
1490
|
+
}
|
|
1491
|
+
if (built !== null && join === null)
|
|
1492
|
+
drafted.set(built.batch.id, saved);
|
|
1493
|
+
const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
|
|
1494
|
+
for (const replaced of withdrawn.values()) {
|
|
1495
|
+
if (replaced === null || !batches.includes(replaced))
|
|
1496
|
+
continue;
|
|
1497
|
+
const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
|
|
1498
|
+
if (limited !== null)
|
|
1499
|
+
return { outcome: 'refused', ...limited };
|
|
1500
|
+
}
|
|
759
1501
|
const consumed = saved.map(({ path }) => path);
|
|
760
1502
|
try {
|
|
761
|
-
await local.
|
|
762
|
-
|
|
763
|
-
previous: replica.confirmed.record,
|
|
1503
|
+
await local.keepAuthored({
|
|
1504
|
+
locator,
|
|
764
1505
|
queue: batches,
|
|
765
|
-
|
|
766
|
-
saved,
|
|
1506
|
+
saved: drafted,
|
|
767
1507
|
consumedRoomDrafts: consumed,
|
|
768
1508
|
});
|
|
769
1509
|
}
|
|
770
1510
|
catch {
|
|
771
|
-
return { outcome: 'refused',
|
|
1511
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
1512
|
+
}
|
|
1513
|
+
if (fences.has(locator))
|
|
1514
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1515
|
+
for (const origin of withdrawn.keys())
|
|
1516
|
+
pendings.delete(origin);
|
|
1517
|
+
for (const [id, drafts] of drafted) {
|
|
1518
|
+
pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
|
|
772
1519
|
}
|
|
773
|
-
|
|
774
|
-
|
|
1520
|
+
if (!localSaves)
|
|
1521
|
+
recorded = false;
|
|
775
1522
|
for (const path of consumed)
|
|
776
1523
|
forgetRoomDraft(locator, path);
|
|
777
1524
|
replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
|
|
@@ -779,9 +1526,12 @@ export function composeRuntime(options) {
|
|
|
779
1526
|
queue: true,
|
|
780
1527
|
drafts: consumed,
|
|
781
1528
|
}));
|
|
782
|
-
moveTo({ ...saving(status), ...attention() });
|
|
1529
|
+
moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
|
|
783
1530
|
submitLater(locator);
|
|
784
|
-
|
|
1531
|
+
const identifier = join?.batch.id ??
|
|
1532
|
+
built?.batch.id ??
|
|
1533
|
+
[...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
|
|
1534
|
+
return { outcome: 'saved', identifier };
|
|
785
1535
|
}
|
|
786
1536
|
async function mark(local, locator, identifier, submission, lost) {
|
|
787
1537
|
const state = pendings.get(identifier);
|
|
@@ -791,6 +1541,8 @@ export function composeRuntime(options) {
|
|
|
791
1541
|
return;
|
|
792
1542
|
}
|
|
793
1543
|
await local.submit(locator, identifier, submission, lost);
|
|
1544
|
+
if (fences.has(locator))
|
|
1545
|
+
return;
|
|
794
1546
|
pendings.set(identifier, { ...state, submission, lost });
|
|
795
1547
|
const replica = replicas.get(locator);
|
|
796
1548
|
if (replica !== undefined)
|
|
@@ -801,22 +1553,24 @@ export function composeRuntime(options) {
|
|
|
801
1553
|
if (!continuing())
|
|
802
1554
|
throw new Error('connection refused');
|
|
803
1555
|
await local.keepUpload(locator, identifier, upload);
|
|
1556
|
+
if (fences.has(locator))
|
|
1557
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
804
1558
|
pendings.set(identifier, { ...pendings.get(identifier), upload });
|
|
805
1559
|
if (!continuing())
|
|
806
1560
|
throw new Error('connection refused');
|
|
807
1561
|
}
|
|
808
1562
|
async function readHistory(locator, path, query = {}) {
|
|
809
1563
|
if (closed || refusal !== null)
|
|
810
|
-
return
|
|
1564
|
+
return UNCERTAIN;
|
|
811
1565
|
try {
|
|
812
1566
|
await ensureAuthorization();
|
|
813
1567
|
}
|
|
814
1568
|
catch {
|
|
815
|
-
return
|
|
816
|
-
}
|
|
817
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
818
|
-
return { kind: 'transport-uncertainty' };
|
|
1569
|
+
return UNCERTAIN;
|
|
819
1570
|
}
|
|
1571
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1572
|
+
return UNCERTAIN;
|
|
1573
|
+
const since = fencings;
|
|
820
1574
|
const transfer = ['transfer', 'history', locator, path];
|
|
821
1575
|
if (query.revision !== undefined)
|
|
822
1576
|
transfer.push(query.revision);
|
|
@@ -831,6 +1585,8 @@ export function composeRuntime(options) {
|
|
|
831
1585
|
...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
|
|
832
1586
|
});
|
|
833
1587
|
journal.record(`outcome history ${result.kind}`);
|
|
1588
|
+
if (fencedSince(locator, since))
|
|
1589
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
834
1590
|
if (result.kind === 'scoped-refusal') {
|
|
835
1591
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
836
1592
|
if (room !== undefined)
|
|
@@ -838,13 +1594,107 @@ export function composeRuntime(options) {
|
|
|
838
1594
|
}
|
|
839
1595
|
if (result.kind === 'revoked-authorization')
|
|
840
1596
|
await refuse('revoked-authorization');
|
|
841
|
-
return result;
|
|
1597
|
+
return historyAnswer(result);
|
|
842
1598
|
}
|
|
843
1599
|
catch {
|
|
844
1600
|
journal.record('outcome history transport-uncertainty');
|
|
845
|
-
return
|
|
1601
|
+
return UNCERTAIN;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
function queuedBlob(locator, path, localId) {
|
|
1605
|
+
for (const batch of replicas.get(locator)?.queue ?? []) {
|
|
1606
|
+
const change = batch.changes[queuedFile(batch, path, localId)];
|
|
1607
|
+
if (change?.kind === 'binary')
|
|
1608
|
+
return change.blob;
|
|
1609
|
+
}
|
|
1610
|
+
return null;
|
|
1611
|
+
}
|
|
1612
|
+
function binaryRoom(request) {
|
|
1613
|
+
if (request.kind === 'path')
|
|
1614
|
+
return request.locator;
|
|
1615
|
+
const scope = options.adapter.referrerScope(request.referrer);
|
|
1616
|
+
return scope?.kind === 'room' ? scope.locator : null;
|
|
1617
|
+
}
|
|
1618
|
+
async function readBinary(request) {
|
|
1619
|
+
if (closed || refusal !== null)
|
|
1620
|
+
return UNCERTAIN;
|
|
1621
|
+
try {
|
|
1622
|
+
await ensureAuthorization();
|
|
1623
|
+
}
|
|
1624
|
+
catch {
|
|
1625
|
+
return UNCERTAIN;
|
|
1626
|
+
}
|
|
1627
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1628
|
+
return UNCERTAIN;
|
|
1629
|
+
const since = fencings;
|
|
1630
|
+
journal.record(request.kind === 'path'
|
|
1631
|
+
? `transfer binary ${request.locator} ${request.path}`
|
|
1632
|
+
: `transfer binary ${request.fileRef}`);
|
|
1633
|
+
try {
|
|
1634
|
+
const result = await options.adapter.binary(authorization, request);
|
|
1635
|
+
journal.record(`outcome binary ${result.kind}`);
|
|
1636
|
+
const locator = binaryRoom(request);
|
|
1637
|
+
if (locator !== null && fencedSince(locator, since))
|
|
1638
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
1639
|
+
if (result.kind === 'scoped-refusal') {
|
|
1640
|
+
const room = replicas.get(result.locator)?.confirmed.record;
|
|
1641
|
+
if (room !== undefined)
|
|
1642
|
+
await scoped(result, room, await openLocal());
|
|
1643
|
+
}
|
|
1644
|
+
if (result.kind === 'revoked-authorization')
|
|
1645
|
+
await refuse('revoked-authorization');
|
|
1646
|
+
return binaryAnswer(result);
|
|
1647
|
+
}
|
|
1648
|
+
catch {
|
|
1649
|
+
journal.record('outcome binary transport-uncertainty');
|
|
1650
|
+
return UNCERTAIN;
|
|
846
1651
|
}
|
|
847
1652
|
}
|
|
1653
|
+
async function readBinaryPath(locator, path, revision) {
|
|
1654
|
+
await ready;
|
|
1655
|
+
if (revision !== undefined)
|
|
1656
|
+
return readBinary({ kind: 'path', locator, path, revision });
|
|
1657
|
+
const visible = replicas.get(locator)?.visible;
|
|
1658
|
+
const held = visible?.binaries[path];
|
|
1659
|
+
if (visible === undefined || held === undefined)
|
|
1660
|
+
return absentBinary(path);
|
|
1661
|
+
if (held.state === 'pending') {
|
|
1662
|
+
const blob = queuedBlob(locator, path, held.localId);
|
|
1663
|
+
return blob === null ? absentBinary(path) : { kind: 'binary', blob };
|
|
1664
|
+
}
|
|
1665
|
+
const record = binaryRecord(path, held, visible.revisions[path]);
|
|
1666
|
+
if (record.state !== 'accepted')
|
|
1667
|
+
return absentBinary(path);
|
|
1668
|
+
const replica = replicas.get(locator);
|
|
1669
|
+
const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
|
|
1670
|
+
const holds = (at) => {
|
|
1671
|
+
const kept = replica?.confirmed.record.binaries[at];
|
|
1672
|
+
return (kept?.state === 'accepted' &&
|
|
1673
|
+
kept.fileRef === record.fileRef &&
|
|
1674
|
+
replica?.confirmed.record.revisions[at] === record.revision);
|
|
1675
|
+
};
|
|
1676
|
+
let confirmed = path;
|
|
1677
|
+
for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
|
|
1678
|
+
const change = queued[index];
|
|
1679
|
+
if (change.kind === 'move' && change.to === confirmed)
|
|
1680
|
+
confirmed = change.path;
|
|
1681
|
+
}
|
|
1682
|
+
return readBinary({
|
|
1683
|
+
kind: 'path',
|
|
1684
|
+
locator,
|
|
1685
|
+
path: confirmed,
|
|
1686
|
+
revision: record.revision,
|
|
1687
|
+
fileRef: record.fileRef,
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
async function readBinaryReference(reference) {
|
|
1691
|
+
await ready;
|
|
1692
|
+
return readBinary({
|
|
1693
|
+
kind: 'reference',
|
|
1694
|
+
fileRef: reference.fileRef,
|
|
1695
|
+
referrer: reference.referrer,
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
848
1698
|
function declaredAgents() {
|
|
849
1699
|
for (const capability of capabilities) {
|
|
850
1700
|
if (capability.conversationAgents !== undefined)
|
|
@@ -864,18 +1714,17 @@ export function composeRuntime(options) {
|
|
|
864
1714
|
async function invokeAction(name, input) {
|
|
865
1715
|
await ready;
|
|
866
1716
|
if (closed || refusal !== null)
|
|
867
|
-
return
|
|
1717
|
+
return UNCERTAIN;
|
|
868
1718
|
if (declaredAction(name) === undefined)
|
|
869
|
-
return
|
|
1719
|
+
return undeclared(name);
|
|
870
1720
|
try {
|
|
871
1721
|
await ensureAuthorization();
|
|
872
1722
|
}
|
|
873
1723
|
catch {
|
|
874
|
-
return
|
|
875
|
-
}
|
|
876
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
877
|
-
return { kind: 'transport-uncertainty' };
|
|
1724
|
+
return UNCERTAIN;
|
|
878
1725
|
}
|
|
1726
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1727
|
+
return UNCERTAIN;
|
|
879
1728
|
journal.record(`transfer action ${name}`);
|
|
880
1729
|
try {
|
|
881
1730
|
const result = await options.adapter.action(authorization, { name, input });
|
|
@@ -889,11 +1738,11 @@ export function composeRuntime(options) {
|
|
|
889
1738
|
journal.record(`outcome action ${result.kind}`);
|
|
890
1739
|
if (result.kind === 'rejected')
|
|
891
1740
|
journal.record(`refusal rejected ${result.code}`);
|
|
892
|
-
return result;
|
|
1741
|
+
return calledResult(result);
|
|
893
1742
|
}
|
|
894
1743
|
catch {
|
|
895
1744
|
journal.record('outcome action transport-uncertainty');
|
|
896
|
-
return
|
|
1745
|
+
return UNCERTAIN;
|
|
897
1746
|
}
|
|
898
1747
|
}
|
|
899
1748
|
function continuing() {
|
|
@@ -910,11 +1759,11 @@ export function composeRuntime(options) {
|
|
|
910
1759
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
911
1760
|
!options.adapter.reached(locator, roomVersion, written));
|
|
912
1761
|
}
|
|
913
|
-
async function pullRefresh(authorized, confirmed) {
|
|
914
|
-
if (!
|
|
1762
|
+
async function pullRefresh(authorized, confirmed, live) {
|
|
1763
|
+
if (!live())
|
|
915
1764
|
return unconfirmed;
|
|
916
1765
|
try {
|
|
917
|
-
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held),
|
|
1766
|
+
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
|
|
918
1767
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
919
1768
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
920
1769
|
return result;
|
|
@@ -924,28 +1773,80 @@ export function composeRuntime(options) {
|
|
|
924
1773
|
throw error;
|
|
925
1774
|
}
|
|
926
1775
|
}
|
|
927
|
-
|
|
928
|
-
|
|
1776
|
+
function staging(local, live) {
|
|
1777
|
+
return {
|
|
1778
|
+
held: (locator) => transfers.get(locator) ?? null,
|
|
1779
|
+
staged: (record) => local.staged(record),
|
|
1780
|
+
stage: (record, page) => exclusive(async () => {
|
|
1781
|
+
if (!live())
|
|
1782
|
+
return false;
|
|
1783
|
+
const bytes = layerBytes(page);
|
|
1784
|
+
if (!(await admitted(local, record.locator, bytes)))
|
|
1785
|
+
return false;
|
|
1786
|
+
try {
|
|
1787
|
+
await local.stage(record, page);
|
|
1788
|
+
}
|
|
1789
|
+
catch (error) {
|
|
1790
|
+
if (!exceeded(error))
|
|
1791
|
+
throw error;
|
|
1792
|
+
await exceededOn(record.locator, bytes);
|
|
1793
|
+
return false;
|
|
1794
|
+
}
|
|
1795
|
+
transfers.set(record.locator, record);
|
|
1796
|
+
readSide.recover();
|
|
1797
|
+
return true;
|
|
1798
|
+
}),
|
|
1799
|
+
abandon: (locator) => exclusive(async () => {
|
|
1800
|
+
if (!live())
|
|
1801
|
+
return;
|
|
1802
|
+
await local.abandon(locator);
|
|
1803
|
+
transfers.delete(locator);
|
|
1804
|
+
readSide.recover();
|
|
1805
|
+
}),
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
function reusable(room) {
|
|
1809
|
+
const record = replicas.get(room.locator)?.confirmed.record;
|
|
1810
|
+
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1811
|
+
return null;
|
|
1812
|
+
return { documents: record.documents, revisions: record.revisions };
|
|
1813
|
+
}
|
|
1814
|
+
async function pullBootstrap(authorized, room, local, live) {
|
|
1815
|
+
if (!live())
|
|
929
1816
|
return unconfirmed;
|
|
1817
|
+
if (replicas.has(room.locator)) {
|
|
1818
|
+
catching.add(room.locator);
|
|
1819
|
+
readSide.recover();
|
|
1820
|
+
}
|
|
930
1821
|
try {
|
|
931
1822
|
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
932
1823
|
collection: modelOf(room.locator),
|
|
933
1824
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
934
|
-
}, (models, held) => remodel(room.locator, models, held),
|
|
1825
|
+
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
935
1826
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
936
1827
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1828
|
+
if (result.kind !== 'staged')
|
|
1829
|
+
caughtUpTo(room.locator);
|
|
937
1830
|
return result;
|
|
938
1831
|
}
|
|
939
1832
|
catch (error) {
|
|
940
1833
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
1834
|
+
caughtUpTo(room.locator);
|
|
941
1835
|
throw error;
|
|
942
1836
|
}
|
|
943
1837
|
}
|
|
944
|
-
|
|
1838
|
+
function caughtUpTo(locator) {
|
|
1839
|
+
if (catching.delete(locator))
|
|
1840
|
+
readSide.recover();
|
|
1841
|
+
}
|
|
1842
|
+
async function advance(room, local, authorized, live) {
|
|
1843
|
+
await referenced(room.locator);
|
|
1844
|
+
if (!live() || paused.has(room.locator))
|
|
1845
|
+
return unconfirmed;
|
|
945
1846
|
const held = replicas.get(room.locator);
|
|
946
1847
|
if (held !== undefined) {
|
|
947
|
-
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
948
|
-
if (!
|
|
1848
|
+
const refreshed = await pullRefresh(authorized, held.confirmed, live);
|
|
1849
|
+
if (!live())
|
|
949
1850
|
return unconfirmed;
|
|
950
1851
|
if (refreshed.kind === 'still-current') {
|
|
951
1852
|
stale.delete(room.locator);
|
|
@@ -959,31 +1860,45 @@ export function composeRuntime(options) {
|
|
|
959
1860
|
return null;
|
|
960
1861
|
}
|
|
961
1862
|
if (refreshed.kind === 'advanced') {
|
|
962
|
-
if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
|
|
1863
|
+
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
|
|
963
1864
|
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
964
1865
|
const reached = now !== undefined &&
|
|
965
1866
|
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
966
|
-
return reached ? null : advance(room, local, authorized);
|
|
1867
|
+
return reached ? null : advance(room, local, authorized, live);
|
|
967
1868
|
}
|
|
968
1869
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
969
1870
|
return null;
|
|
970
1871
|
}
|
|
971
1872
|
if (refreshed.kind !== 'beyond-horizon')
|
|
972
1873
|
return refreshed;
|
|
1874
|
+
if (opened !== null) {
|
|
1875
|
+
owed.add(room.locator);
|
|
1876
|
+
readSide.recover();
|
|
1877
|
+
return unconfirmed;
|
|
1878
|
+
}
|
|
973
1879
|
}
|
|
974
|
-
const bootstrapped = await pullBootstrap(authorized, room);
|
|
975
|
-
if (!
|
|
1880
|
+
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
1881
|
+
if (!live())
|
|
976
1882
|
return unconfirmed;
|
|
977
1883
|
if (bootstrapped.kind !== 'staged')
|
|
978
1884
|
return bootstrapped;
|
|
979
|
-
await publish(local, room, bootstrapped.confirmed);
|
|
1885
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
980
1886
|
return null;
|
|
981
1887
|
}
|
|
982
1888
|
async function withhold(withheld, room, local, progress) {
|
|
1889
|
+
if (withheld.kind === 'unconfirmed')
|
|
1890
|
+
return;
|
|
983
1891
|
if (withheld.kind === 'refused')
|
|
984
1892
|
stale.delete(room.locator);
|
|
985
1893
|
else
|
|
986
1894
|
stale.add(room.locator);
|
|
1895
|
+
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
1896
|
+
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
1897
|
+
repairable.set(room.locator, reset);
|
|
1898
|
+
repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
|
|
1899
|
+
}
|
|
1900
|
+
else
|
|
1901
|
+
repairable.delete(room.locator);
|
|
987
1902
|
if (withheld.kind === 'scoped-refusal') {
|
|
988
1903
|
await scoped(withheld, room, local);
|
|
989
1904
|
}
|
|
@@ -1008,6 +1923,7 @@ export function composeRuntime(options) {
|
|
|
1008
1923
|
function refusedSource(room, { refusal }) {
|
|
1009
1924
|
sourceRefusals.set(room.locator, {
|
|
1010
1925
|
code: refusal.rule,
|
|
1926
|
+
retryable: false,
|
|
1011
1927
|
fields: {
|
|
1012
1928
|
file: refusal.file,
|
|
1013
1929
|
...(refusal.location === undefined ? {} : { location: refusal.location }),
|
|
@@ -1018,12 +1934,21 @@ export function composeRuntime(options) {
|
|
|
1018
1934
|
unreadable.add(room.locator);
|
|
1019
1935
|
list(room, false);
|
|
1020
1936
|
emitRooms();
|
|
1937
|
+
moveTo({ ...status, ...attention() });
|
|
1021
1938
|
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
1022
1939
|
}
|
|
1023
1940
|
function leaving(batch, result) {
|
|
1024
1941
|
if (result.kind === 'expired')
|
|
1025
|
-
return { batch, returned:
|
|
1026
|
-
return {
|
|
1942
|
+
return { batch, returned: returnedBy('expired') };
|
|
1943
|
+
return {
|
|
1944
|
+
batch,
|
|
1945
|
+
returned: {
|
|
1946
|
+
kind: result.kind,
|
|
1947
|
+
code: result.code,
|
|
1948
|
+
fields: result.fields,
|
|
1949
|
+
retryable: result.retryable,
|
|
1950
|
+
},
|
|
1951
|
+
};
|
|
1027
1952
|
}
|
|
1028
1953
|
function logAccepted(locator, submitted, accepted) {
|
|
1029
1954
|
journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
|
|
@@ -1031,6 +1956,7 @@ export function composeRuntime(options) {
|
|
|
1031
1956
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1032
1957
|
}
|
|
1033
1958
|
async function adoptAccepted(room, local, submitted, accepted) {
|
|
1959
|
+
await referenced(room.locator);
|
|
1034
1960
|
if (!continuing())
|
|
1035
1961
|
return unconfirmed;
|
|
1036
1962
|
const current = replicas.get(room.locator);
|
|
@@ -1041,22 +1967,23 @@ export function composeRuntime(options) {
|
|
|
1041
1967
|
logAccepted(room.locator, submitted, accepted);
|
|
1042
1968
|
const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
1043
1969
|
? { kind: 'staged', confirmed: current.confirmed }
|
|
1044
|
-
: applyReturned(current.confirmed,
|
|
1970
|
+
: applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
1045
1971
|
? accepted.roomVersion
|
|
1046
1972
|
: held, accepted.roomVersion);
|
|
1047
1973
|
if (applied.kind !== 'staged') {
|
|
1048
1974
|
const state = pendings.get(submitted.id);
|
|
1049
1975
|
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1050
|
-
if (state !== undefined)
|
|
1976
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
1051
1977
|
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1052
1978
|
emitQueue(room.locator);
|
|
1053
1979
|
return applied;
|
|
1054
1980
|
}
|
|
1055
|
-
const
|
|
1981
|
+
const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
1056
1982
|
pendings.delete(submitted.id);
|
|
1057
1983
|
unreadable.delete(room.locator);
|
|
1058
1984
|
sourceRefusals.delete(room.locator);
|
|
1059
1985
|
declarationRefusals.delete(room.locator);
|
|
1986
|
+
const set = settled();
|
|
1060
1987
|
list(room, true);
|
|
1061
1988
|
publishChangeSet(set);
|
|
1062
1989
|
moveTo({ ...status, ...attention() });
|
|
@@ -1070,14 +1997,14 @@ export function composeRuntime(options) {
|
|
|
1070
1997
|
refusedSource(room, failed);
|
|
1071
1998
|
return false;
|
|
1072
1999
|
}
|
|
1073
|
-
async function submitRoom(room, local, authorized, progress) {
|
|
1074
|
-
while (continuing() && authorization !== undefined) {
|
|
2000
|
+
async function submitRoom(room, local, authorized, progress, signal) {
|
|
2001
|
+
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
1075
2002
|
const replica = replicas.get(room.locator);
|
|
1076
2003
|
const head = replica?.queue[0];
|
|
1077
2004
|
if (replica === undefined || head === undefined)
|
|
1078
|
-
return;
|
|
2005
|
+
return 'done';
|
|
1079
2006
|
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1080
|
-
return;
|
|
2007
|
+
return 'done';
|
|
1081
2008
|
const state = pendings.get(head.id);
|
|
1082
2009
|
const { submission, lost: earlier } = state ?? {
|
|
1083
2010
|
submission: { state: 'waiting' },
|
|
@@ -1085,24 +2012,30 @@ export function composeRuntime(options) {
|
|
|
1085
2012
|
};
|
|
1086
2013
|
if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
|
|
1087
2014
|
progress.complete = false;
|
|
1088
|
-
return;
|
|
2015
|
+
return { at: Date.parse(submission.reset) };
|
|
1089
2016
|
}
|
|
1090
2017
|
if (writesHeld()) {
|
|
1091
2018
|
progress.complete = false;
|
|
1092
2019
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1093
|
-
return;
|
|
2020
|
+
return 'done';
|
|
1094
2021
|
}
|
|
1095
2022
|
const lost = earlier || submission.state === 'submitted';
|
|
2023
|
+
if (lost && pastHorizon(head.id)) {
|
|
2024
|
+
await discardPending(room, local, signal);
|
|
2025
|
+
return 'done';
|
|
2026
|
+
}
|
|
1096
2027
|
let result;
|
|
1097
2028
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
1098
2029
|
await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
|
|
1099
|
-
if (discoveryPaused)
|
|
1100
|
-
return;
|
|
1101
|
-
if (writesHeld()) {
|
|
2030
|
+
if (discoveryPaused || fences.has(room.locator))
|
|
2031
|
+
return 'done';
|
|
2032
|
+
if (signal.aborted || writesHeld()) {
|
|
1102
2033
|
await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
|
|
2034
|
+
if (signal.aborted)
|
|
2035
|
+
return 'done';
|
|
1103
2036
|
progress.complete = false;
|
|
1104
2037
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1105
|
-
return;
|
|
2038
|
+
return 'done';
|
|
1106
2039
|
}
|
|
1107
2040
|
const opensBefore = openSignals;
|
|
1108
2041
|
const answered = awaitingAnswer(room.locator);
|
|
@@ -1118,32 +2051,36 @@ export function composeRuntime(options) {
|
|
|
1118
2051
|
progress.complete = false;
|
|
1119
2052
|
if (continuing())
|
|
1120
2053
|
progress.reachable = false;
|
|
1121
|
-
return;
|
|
2054
|
+
return 'again';
|
|
2055
|
+
}
|
|
2056
|
+
if (signal.aborted) {
|
|
2057
|
+
answered();
|
|
2058
|
+
return 'done';
|
|
1122
2059
|
}
|
|
1123
2060
|
if (result.kind !== 'accepted' || !continuing())
|
|
1124
2061
|
answered();
|
|
1125
2062
|
if (!continuing())
|
|
1126
|
-
return;
|
|
2063
|
+
return 'done';
|
|
1127
2064
|
if (result.kind === 'transport-uncertainty') {
|
|
1128
2065
|
progress.complete = false;
|
|
1129
2066
|
progress.reachable = false;
|
|
1130
|
-
return;
|
|
2067
|
+
return 'again';
|
|
1131
2068
|
}
|
|
1132
2069
|
if (result.kind === 'room-not-readable') {
|
|
1133
2070
|
progress.complete = false;
|
|
1134
2071
|
unreadable.add(room.locator);
|
|
1135
2072
|
list(room, false);
|
|
1136
2073
|
emitRooms();
|
|
1137
|
-
return;
|
|
2074
|
+
return 'done';
|
|
1138
2075
|
}
|
|
1139
2076
|
if (result.kind === 'revoked-authorization') {
|
|
1140
2077
|
await refuse('revoked-authorization');
|
|
1141
|
-
return;
|
|
2078
|
+
return 'done';
|
|
1142
2079
|
}
|
|
1143
2080
|
if (result.kind === 'scoped-refusal') {
|
|
1144
2081
|
await scoped(result, room, local);
|
|
1145
2082
|
progress.complete = false;
|
|
1146
|
-
return;
|
|
2083
|
+
return 'done';
|
|
1147
2084
|
}
|
|
1148
2085
|
if (result.kind === 'write-halted') {
|
|
1149
2086
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
@@ -1152,16 +2089,24 @@ export function composeRuntime(options) {
|
|
|
1152
2089
|
continue;
|
|
1153
2090
|
haltedRefusal = true;
|
|
1154
2091
|
progress.complete = false;
|
|
1155
|
-
return;
|
|
2092
|
+
return 'done';
|
|
1156
2093
|
}
|
|
1157
2094
|
if (result.kind === 'conflicted' ||
|
|
1158
2095
|
result.kind === 'rejected' ||
|
|
1159
2096
|
result.kind === 'expired' ||
|
|
1160
2097
|
result.kind === 'retryable-refusal') {
|
|
2098
|
+
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
2099
|
+
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
2100
|
+
await discardPending(room, local, signal);
|
|
2101
|
+
return 'done';
|
|
2102
|
+
}
|
|
1161
2103
|
if (result.kind === 'expired') {
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
2104
|
+
await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
|
|
2105
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
2106
|
+
if (withheld !== null) {
|
|
2107
|
+
await withhold(withheld, room, local, progress);
|
|
2108
|
+
return 'done';
|
|
2109
|
+
}
|
|
1165
2110
|
}
|
|
1166
2111
|
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
|
|
1167
2112
|
(result.kind === 'conflicted' && result.roomVersion !== undefined
|
|
@@ -1174,8 +2119,7 @@ export function composeRuntime(options) {
|
|
|
1174
2119
|
const reset = result.reset;
|
|
1175
2120
|
progress.complete = false;
|
|
1176
2121
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1177
|
-
|
|
1178
|
-
return;
|
|
2122
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
1179
2123
|
}
|
|
1180
2124
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
1181
2125
|
const left = leaving(head, result);
|
|
@@ -1186,37 +2130,82 @@ export function composeRuntime(options) {
|
|
|
1186
2130
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
1187
2131
|
return;
|
|
1188
2132
|
}
|
|
1189
|
-
const
|
|
2133
|
+
const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
1190
2134
|
leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
|
|
1191
2135
|
(result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
|
|
1192
2136
|
? { ...left, returned: null }
|
|
1193
2137
|
: left,
|
|
1194
2138
|
});
|
|
1195
|
-
publishChangeSet(
|
|
2139
|
+
publishChangeSet(settled());
|
|
1196
2140
|
moveTo({ ...status, ...attention() });
|
|
1197
2141
|
});
|
|
1198
2142
|
if (result.kind === 'expired')
|
|
1199
2143
|
continue;
|
|
1200
|
-
const withheld = await advance(room, local, authorized);
|
|
1201
|
-
if (withheld !== null)
|
|
1202
|
-
|
|
2144
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
2145
|
+
if (withheld !== null) {
|
|
2146
|
+
await withhold(withheld, room, local, progress);
|
|
2147
|
+
return 'done';
|
|
2148
|
+
}
|
|
1203
2149
|
continue;
|
|
1204
2150
|
}
|
|
1205
2151
|
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1206
2152
|
if (!kept) {
|
|
1207
2153
|
progress.complete = false;
|
|
1208
|
-
return;
|
|
2154
|
+
return 'done';
|
|
1209
2155
|
}
|
|
1210
2156
|
}
|
|
2157
|
+
return 'done';
|
|
2158
|
+
}
|
|
2159
|
+
function pastHorizon(identity) {
|
|
2160
|
+
return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
|
|
2161
|
+
}
|
|
2162
|
+
async function discardPending(room, local, signal) {
|
|
2163
|
+
const { locator } = room;
|
|
2164
|
+
const discarded = await exclusive(async () => {
|
|
2165
|
+
if (fences.has(locator))
|
|
2166
|
+
return false;
|
|
2167
|
+
const before = replicas.get(locator);
|
|
2168
|
+
const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
2169
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
2170
|
+
lastFenced.set(locator, ++fencings);
|
|
2171
|
+
try {
|
|
2172
|
+
await local.discard(locator);
|
|
2173
|
+
}
|
|
2174
|
+
finally {
|
|
2175
|
+
fences.delete(locator);
|
|
2176
|
+
}
|
|
2177
|
+
for (const batch of queue)
|
|
2178
|
+
pendings.delete(batch.id);
|
|
2179
|
+
queuesWithoutReplica.delete(locator);
|
|
2180
|
+
const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
|
|
2181
|
+
if (after !== null)
|
|
2182
|
+
replicas.set(locator, after);
|
|
2183
|
+
journal.record(`room discarded ${locator}`);
|
|
2184
|
+
publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
|
|
2185
|
+
moveTo({ ...status, ...attention() });
|
|
2186
|
+
return true;
|
|
2187
|
+
});
|
|
2188
|
+
if (!discarded)
|
|
2189
|
+
return;
|
|
2190
|
+
const progress = { complete: true, reachable: true };
|
|
2191
|
+
await roomTask(locator, async (live) => {
|
|
2192
|
+
await Promise.race([
|
|
2193
|
+
reload(room, local, progress, () => live() && !signal.aborted),
|
|
2194
|
+
abandonedOn(signal),
|
|
2195
|
+
]);
|
|
2196
|
+
});
|
|
2197
|
+
if (signal.aborted)
|
|
2198
|
+
return;
|
|
2199
|
+
await rest(progress);
|
|
1211
2200
|
}
|
|
1212
2201
|
function awaitingAnswer(locator) {
|
|
1213
2202
|
let release = () => { };
|
|
1214
2203
|
const awaited = new Promise((resolve) => {
|
|
1215
2204
|
release = resolve;
|
|
1216
2205
|
});
|
|
1217
|
-
answering.set(locator, awaited);
|
|
2206
|
+
answering.set(locator, { generation, answered: awaited });
|
|
1218
2207
|
return () => {
|
|
1219
|
-
if (answering.get(locator) === awaited)
|
|
2208
|
+
if (answering.get(locator)?.answered === awaited)
|
|
1220
2209
|
answering.delete(locator);
|
|
1221
2210
|
release();
|
|
1222
2211
|
};
|
|
@@ -1249,7 +2238,7 @@ export function composeRuntime(options) {
|
|
|
1249
2238
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
1250
2239
|
};
|
|
1251
2240
|
const reclassified = sameMembership(declared, held.confirmed.record)
|
|
1252
|
-
?
|
|
2241
|
+
? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
|
|
1253
2242
|
: readReclassified(declared, held.confirmed);
|
|
1254
2243
|
const record = reclassified.kind === 'staged'
|
|
1255
2244
|
? declared
|
|
@@ -1257,12 +2246,10 @@ export function composeRuntime(options) {
|
|
|
1257
2246
|
...declared,
|
|
1258
2247
|
typedSelection: held.confirmed.record.typedSelection,
|
|
1259
2248
|
};
|
|
1260
|
-
const read = reclassified.kind === 'deferred'
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
queue: held.queue,
|
|
1265
|
-
});
|
|
2249
|
+
const read = reclassified.kind === 'deferred'
|
|
2250
|
+
? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
|
|
2251
|
+
: reclassified;
|
|
2252
|
+
await local.write({ next: record, previous: held.confirmed.record });
|
|
1266
2253
|
if (read.kind === 'staged') {
|
|
1267
2254
|
replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
|
|
1268
2255
|
if (declarationRefusals.delete(room.locator)) {
|
|
@@ -1289,7 +2276,11 @@ export function composeRuntime(options) {
|
|
|
1289
2276
|
});
|
|
1290
2277
|
}
|
|
1291
2278
|
async function scoped(result, room, local) {
|
|
1292
|
-
scopedRefusals.set(room.locator, {
|
|
2279
|
+
scopedRefusals.set(room.locator, {
|
|
2280
|
+
code: result.code,
|
|
2281
|
+
fields: result.fields,
|
|
2282
|
+
retryable: result.retryable,
|
|
2283
|
+
});
|
|
1293
2284
|
let scopedAway = room;
|
|
1294
2285
|
if (result.scope === 'model')
|
|
1295
2286
|
await retainModel(local, room, 'collection', {
|
|
@@ -1326,24 +2317,27 @@ export function composeRuntime(options) {
|
|
|
1326
2317
|
roomVersion: record.roomVersion,
|
|
1327
2318
|
documents: record.documents,
|
|
1328
2319
|
revisions: record.revisions,
|
|
1329
|
-
attachments: record.attachments,
|
|
1330
2320
|
},
|
|
1331
2321
|
];
|
|
1332
2322
|
});
|
|
1333
2323
|
}
|
|
1334
|
-
async function synchronize(listed, local, progress, attempt) {
|
|
1335
|
-
const
|
|
1336
|
-
const
|
|
2324
|
+
async function synchronize(listed, local, progress, attempt, live) {
|
|
2325
|
+
const acquired = listed.filter(acquires);
|
|
2326
|
+
const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
|
|
2327
|
+
if (!live())
|
|
2328
|
+
return;
|
|
2329
|
+
const ready = acquired.filter((_, index) => {
|
|
1337
2330
|
const outcome = prepared[index];
|
|
1338
2331
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1339
2332
|
});
|
|
1340
2333
|
if (attempt === 'opened') {
|
|
1341
|
-
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
2334
|
+
options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
|
|
1342
2335
|
handed = true;
|
|
1343
2336
|
}
|
|
2337
|
+
discovering?.release();
|
|
1344
2338
|
const loaded = await Promise.allSettled(ready
|
|
1345
2339
|
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1346
|
-
.map((room) => loadRoom(room, local, progress)));
|
|
2340
|
+
.map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
|
|
1347
2341
|
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1348
2342
|
if (failed !== undefined)
|
|
1349
2343
|
throw failed.reason;
|
|
@@ -1372,14 +2366,14 @@ export function composeRuntime(options) {
|
|
|
1372
2366
|
}
|
|
1373
2367
|
return true;
|
|
1374
2368
|
}
|
|
1375
|
-
async function caughtUp(room, local, progress) {
|
|
2369
|
+
async function caughtUp(room, local, progress, live) {
|
|
1376
2370
|
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1377
|
-
if (held === undefined || authorization === undefined)
|
|
2371
|
+
if (held === undefined || authorization === undefined || !live())
|
|
1378
2372
|
return;
|
|
1379
2373
|
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
1380
2374
|
if (!behind)
|
|
1381
2375
|
return;
|
|
1382
|
-
const withheld = await advance(room, local, authorization);
|
|
2376
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1383
2377
|
if (withheld !== null)
|
|
1384
2378
|
await withhold(withheld, room, local, progress);
|
|
1385
2379
|
}
|
|
@@ -1389,39 +2383,46 @@ export function composeRuntime(options) {
|
|
|
1389
2383
|
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1390
2384
|
});
|
|
1391
2385
|
}
|
|
1392
|
-
async function loadRoom(room, local, progress) {
|
|
1393
|
-
if (
|
|
2386
|
+
async function loadRoom(room, local, progress, live) {
|
|
2387
|
+
if (!live() || authorization === undefined)
|
|
1394
2388
|
return;
|
|
1395
2389
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1396
|
-
await reload(room, local, progress);
|
|
2390
|
+
await reload(room, local, progress, live);
|
|
1397
2391
|
return;
|
|
1398
2392
|
}
|
|
1399
|
-
const withheld = await advance(room, local, authorization);
|
|
2393
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1400
2394
|
if (withheld !== null) {
|
|
1401
2395
|
await withhold(withheld, room, local, progress);
|
|
1402
2396
|
return;
|
|
1403
2397
|
}
|
|
1404
|
-
await caughtUp(room, local, progress);
|
|
2398
|
+
await caughtUp(room, local, progress, live);
|
|
1405
2399
|
}
|
|
1406
|
-
async function reload(room, local, progress) {
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
2400
|
+
async function reload(room, local, progress, live) {
|
|
2401
|
+
try {
|
|
2402
|
+
if (!live() || authorization === undefined)
|
|
2403
|
+
return;
|
|
2404
|
+
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
2405
|
+
if (!live())
|
|
2406
|
+
return;
|
|
2407
|
+
if (bootstrapped.kind !== 'staged') {
|
|
2408
|
+
await withhold(bootstrapped, room, local, progress);
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
2412
|
+
await caughtUp(room, local, progress, live);
|
|
2413
|
+
}
|
|
2414
|
+
finally {
|
|
2415
|
+
caughtUpTo(room.locator);
|
|
1415
2416
|
}
|
|
1416
|
-
await publish(local, room, bootstrapped.confirmed);
|
|
1417
|
-
await caughtUp(room, local, progress);
|
|
1418
2417
|
}
|
|
1419
|
-
async function discoverRooms(local, progress, attempt) {
|
|
2418
|
+
async function discoverRooms(local, progress, attempt, live) {
|
|
1420
2419
|
if (closed || authorization === undefined) {
|
|
1421
2420
|
notifyReady();
|
|
1422
2421
|
return;
|
|
1423
2422
|
}
|
|
1424
2423
|
moveTo(transferring(status));
|
|
2424
|
+
discoveries += 1;
|
|
2425
|
+
const started = discoveries;
|
|
1425
2426
|
let result;
|
|
1426
2427
|
try {
|
|
1427
2428
|
result = await options.adapter.discover(authorization);
|
|
@@ -1429,7 +2430,8 @@ export function composeRuntime(options) {
|
|
|
1429
2430
|
catch {
|
|
1430
2431
|
result = { kind: 'transport-uncertainty' };
|
|
1431
2432
|
}
|
|
1432
|
-
if (
|
|
2433
|
+
if (!live()) {
|
|
2434
|
+
progress.complete = false;
|
|
1433
2435
|
notifyReady();
|
|
1434
2436
|
return;
|
|
1435
2437
|
}
|
|
@@ -1448,7 +2450,11 @@ export function composeRuntime(options) {
|
|
|
1448
2450
|
}
|
|
1449
2451
|
if (result.kind === 'contract-unavailable') {
|
|
1450
2452
|
journal.record(`connection contract unavailable ${result.code}`);
|
|
1451
|
-
await refuse('missing-required-guarantee', {
|
|
2453
|
+
await refuse('missing-required-guarantee', {
|
|
2454
|
+
code: result.code,
|
|
2455
|
+
fields: result.fields,
|
|
2456
|
+
retryable: result.retryable,
|
|
2457
|
+
});
|
|
1452
2458
|
notifyReady();
|
|
1453
2459
|
return;
|
|
1454
2460
|
}
|
|
@@ -1483,14 +2489,20 @@ export function composeRuntime(options) {
|
|
|
1483
2489
|
}
|
|
1484
2490
|
const previousCapabilities = capabilities;
|
|
1485
2491
|
capabilities = result.capabilities;
|
|
2492
|
+
outcomeRetention = result.retention.outcome;
|
|
1486
2493
|
notifyReady();
|
|
1487
2494
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1488
2495
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
1489
2496
|
limits = result.providerLimits;
|
|
1490
2497
|
await local.declare(limits, capabilities);
|
|
1491
2498
|
}
|
|
1492
|
-
|
|
1493
|
-
|
|
2499
|
+
const signedIn = result.member ?? null;
|
|
2500
|
+
if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
|
|
2501
|
+
await local.keepMember(signedIn);
|
|
2502
|
+
member = signedIn;
|
|
2503
|
+
emitMember();
|
|
2504
|
+
}
|
|
2505
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
1494
2506
|
}
|
|
1495
2507
|
async function ensureAuthorization() {
|
|
1496
2508
|
if (authorization !== undefined)
|
|
@@ -1504,32 +2516,64 @@ export function composeRuntime(options) {
|
|
|
1504
2516
|
throw error;
|
|
1505
2517
|
}
|
|
1506
2518
|
}
|
|
1507
|
-
function
|
|
2519
|
+
async function admittedRead(work) {
|
|
2520
|
+
if (roomReads.generation !== generation) {
|
|
2521
|
+
roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
2522
|
+
}
|
|
2523
|
+
const reads = roomReads;
|
|
2524
|
+
while (reads.free === 0)
|
|
2525
|
+
await new Promise((admit) => reads.waiting.push(admit));
|
|
2526
|
+
reads.free -= 1;
|
|
2527
|
+
try {
|
|
2528
|
+
await work();
|
|
2529
|
+
}
|
|
2530
|
+
finally {
|
|
2531
|
+
reads.free += 1;
|
|
2532
|
+
reads.waiting.shift()?.();
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
function roomTask(locator, work) {
|
|
2536
|
+
return task(roomLane(locator), (live) => admittedRead(() => work(live)));
|
|
2537
|
+
}
|
|
2538
|
+
function task(lane, work) {
|
|
1508
2539
|
if (stopping)
|
|
1509
|
-
return
|
|
1510
|
-
|
|
1511
|
-
|
|
2540
|
+
return Promise.resolve();
|
|
2541
|
+
const held = lanes.get(lane);
|
|
2542
|
+
const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
|
|
2543
|
+
const turn = current.generation;
|
|
2544
|
+
const live = () => turn === generation && continuing();
|
|
2545
|
+
const run = current.chain.then(() => work(live)).catch(() => undefined);
|
|
2546
|
+
current.chain = run;
|
|
2547
|
+
lanes.set(lane, current);
|
|
2548
|
+
working.add(run);
|
|
2549
|
+
void run.finally(() => working.delete(run));
|
|
2550
|
+
return run;
|
|
1512
2551
|
}
|
|
1513
2552
|
function sync(attempt) {
|
|
1514
|
-
return task(async () => {
|
|
1515
|
-
if (
|
|
2553
|
+
return task(OPENING_LANE, async (live) => {
|
|
2554
|
+
if (!live()) {
|
|
1516
2555
|
notifyReady();
|
|
1517
2556
|
return;
|
|
1518
2557
|
}
|
|
1519
2558
|
const progress = { complete: true, reachable: true };
|
|
1520
|
-
|
|
2559
|
+
const gate = discover();
|
|
2560
|
+
if (attempt === 'opened')
|
|
2561
|
+
opened = generation;
|
|
2562
|
+
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
2563
|
+
if (attempt === 'opened')
|
|
2564
|
+
streamed = progress;
|
|
1521
2565
|
complete = false;
|
|
1522
2566
|
recorded = false;
|
|
1523
2567
|
moveTo(transferring(status));
|
|
1524
2568
|
handed = false;
|
|
1525
2569
|
try {
|
|
1526
2570
|
const local = await openLocal();
|
|
1527
|
-
if (
|
|
2571
|
+
if (!live())
|
|
1528
2572
|
return;
|
|
1529
2573
|
await ensureAuthorization();
|
|
1530
|
-
if (
|
|
2574
|
+
if (!live())
|
|
1531
2575
|
return;
|
|
1532
|
-
await discoverRooms(local, progress, attempt);
|
|
2576
|
+
await discoverRooms(local, progress, attempt, live);
|
|
1533
2577
|
}
|
|
1534
2578
|
catch {
|
|
1535
2579
|
progress.complete = false;
|
|
@@ -1537,17 +2581,21 @@ export function composeRuntime(options) {
|
|
|
1537
2581
|
progress.reachable = false;
|
|
1538
2582
|
}
|
|
1539
2583
|
finally {
|
|
2584
|
+
gate.release();
|
|
1540
2585
|
notifyReady();
|
|
1541
2586
|
}
|
|
2587
|
+
if (!live())
|
|
2588
|
+
return;
|
|
1542
2589
|
if (attempt !== 'opened') {
|
|
1543
|
-
await finishOpening(progress);
|
|
2590
|
+
await finishOpening(progress, live);
|
|
1544
2591
|
return;
|
|
1545
2592
|
}
|
|
1546
2593
|
if (handed || closed || progress.reachable)
|
|
1547
2594
|
return;
|
|
1548
2595
|
underway = null;
|
|
2596
|
+
opened = null;
|
|
1549
2597
|
journal.record('connection reopen');
|
|
1550
|
-
await rest(progress);
|
|
2598
|
+
await rest(progress, live);
|
|
1551
2599
|
options.adapter.roomStream?.reopen();
|
|
1552
2600
|
});
|
|
1553
2601
|
}
|
|
@@ -1558,29 +2606,51 @@ export function composeRuntime(options) {
|
|
|
1558
2606
|
void refuse('revoked-authorization');
|
|
1559
2607
|
return;
|
|
1560
2608
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
2609
|
+
complete = false;
|
|
2610
|
+
if (streamed !== null)
|
|
2611
|
+
streamed.complete = false;
|
|
2612
|
+
moveTo({
|
|
2613
|
+
...status,
|
|
2614
|
+
state: status.state === 'synced' ? 'syncing' : status.state,
|
|
2615
|
+
providerUnreachable: status.providerUnreachable || !opened,
|
|
2616
|
+
});
|
|
1564
2617
|
}
|
|
1565
2618
|
function applyOpening(expired) {
|
|
1566
|
-
|
|
1567
|
-
|
|
2619
|
+
const progress = underway?.generation === generation ? underway.progress : null;
|
|
2620
|
+
const cycle = progress ?? { complete: true, reachable: true };
|
|
2621
|
+
const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
|
|
2622
|
+
if (opened === generation)
|
|
2623
|
+
opened = null;
|
|
2624
|
+
return task(OPENING_LANE, async (live) => {
|
|
1568
2625
|
underway = null;
|
|
1569
|
-
if (
|
|
2626
|
+
if (!live())
|
|
1570
2627
|
return;
|
|
1571
2628
|
if (progress === null) {
|
|
1572
|
-
await
|
|
2629
|
+
await replacing;
|
|
2630
|
+
await rest(cycle, live);
|
|
1573
2631
|
return;
|
|
1574
2632
|
}
|
|
2633
|
+
await roomsCarried(expired);
|
|
2634
|
+
if (!live())
|
|
2635
|
+
return;
|
|
2636
|
+
const replaced = new Set(owed);
|
|
2637
|
+
owed.clear();
|
|
1575
2638
|
try {
|
|
1576
2639
|
const local = await openLocal();
|
|
1577
2640
|
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
1578
|
-
const reloading = held.filter((room) => expired.includes(room.locator)
|
|
1579
|
-
|
|
2641
|
+
const reloading = held.filter((room) => !expired.includes(room.locator) &&
|
|
2642
|
+
(replaced.has(room.locator) || revoked(room.locator)));
|
|
2643
|
+
for (const room of reloading)
|
|
2644
|
+
catching.add(room.locator);
|
|
2645
|
+
readSide.recover();
|
|
2646
|
+
const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
|
|
2647
|
+
const reloaded = [...(await reloads), ...(await replacing)];
|
|
1580
2648
|
if (progress.reachable) {
|
|
1581
|
-
for (const room of held)
|
|
1582
|
-
if (!reloading.includes(room))
|
|
2649
|
+
for (const room of held) {
|
|
2650
|
+
if (!expired.includes(room.locator) && !reloading.includes(room)) {
|
|
1583
2651
|
await confirmModels(room, local);
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
1584
2654
|
}
|
|
1585
2655
|
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1586
2656
|
if (failed !== undefined)
|
|
@@ -1590,21 +2660,21 @@ export function composeRuntime(options) {
|
|
|
1590
2660
|
progress.complete = false;
|
|
1591
2661
|
progress.reachable = false;
|
|
1592
2662
|
}
|
|
1593
|
-
|
|
2663
|
+
if (live())
|
|
2664
|
+
await finishOpening(progress, live);
|
|
1594
2665
|
});
|
|
1595
2666
|
}
|
|
1596
|
-
async function
|
|
1597
|
-
const progress = { complete: true, reachable: true };
|
|
2667
|
+
async function reloadNow(replaced, progress) {
|
|
1598
2668
|
try {
|
|
1599
2669
|
const local = await openLocal();
|
|
1600
|
-
await Promise.allSettled([...discovered.values()]
|
|
1601
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
1602
|
-
.map((room) => reload(room, local, progress)));
|
|
2670
|
+
return await Promise.allSettled([...discovered.values()]
|
|
2671
|
+
.filter((room) => replicas.has(room.locator) && replaced(room))
|
|
2672
|
+
.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
1603
2673
|
}
|
|
1604
2674
|
catch {
|
|
1605
2675
|
progress.reachable = false;
|
|
2676
|
+
return [];
|
|
1606
2677
|
}
|
|
1607
|
-
await rest(progress);
|
|
1608
2678
|
}
|
|
1609
2679
|
async function confirmModels(room, local) {
|
|
1610
2680
|
for (const attached of ['collection', 'typed-selection']) {
|
|
@@ -1615,38 +2685,96 @@ export function composeRuntime(options) {
|
|
|
1615
2685
|
}
|
|
1616
2686
|
}
|
|
1617
2687
|
function submitLater(locator) {
|
|
1618
|
-
|
|
1619
|
-
if (running !== undefined) {
|
|
1620
|
-
running.again = true;
|
|
2688
|
+
if (localSaves)
|
|
1621
2689
|
return;
|
|
2690
|
+
void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
|
|
2691
|
+
}
|
|
2692
|
+
async function submitPass(locator, signal) {
|
|
2693
|
+
const room = discovered.get(locator);
|
|
2694
|
+
if (closed || stopping || discoveryPaused || room === undefined)
|
|
2695
|
+
return 'done';
|
|
2696
|
+
const progress = { complete: true, reachable: true };
|
|
2697
|
+
let next;
|
|
2698
|
+
try {
|
|
2699
|
+
await ensureAuthorization();
|
|
2700
|
+
if (authorization === undefined)
|
|
2701
|
+
return 'done';
|
|
2702
|
+
next = await submitRoom(room, await openLocal(), authorization, progress, signal);
|
|
1622
2703
|
}
|
|
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
|
-
|
|
2704
|
+
catch {
|
|
2705
|
+
progress.reachable = false;
|
|
2706
|
+
next = 'again';
|
|
2707
|
+
}
|
|
2708
|
+
if (signal.aborted)
|
|
2709
|
+
return 'done';
|
|
2710
|
+
await rest(progress);
|
|
2711
|
+
return next === 'again' && !socketOpen ? 'done' : next;
|
|
2712
|
+
}
|
|
2713
|
+
function repairLater(locator, when) {
|
|
2714
|
+
if (!socketOpen || repairing.has(locator))
|
|
2715
|
+
return;
|
|
2716
|
+
void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
|
|
2717
|
+
}
|
|
2718
|
+
async function repairPass(locator, signal) {
|
|
2719
|
+
const room = discovered.get(locator);
|
|
2720
|
+
if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
|
|
2721
|
+
return 'done';
|
|
2722
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2723
|
+
return 'done';
|
|
2724
|
+
const progress = { complete: true, reachable: true };
|
|
2725
|
+
repairing.add(locator);
|
|
2726
|
+
try {
|
|
2727
|
+
const local = await openLocal();
|
|
2728
|
+
await roomTask(locator, async (live) => {
|
|
2729
|
+
const current = () => live() && !signal.aborted;
|
|
2730
|
+
if (!current() || authorization === undefined)
|
|
2731
|
+
return;
|
|
2732
|
+
const withheld = await Promise.race([
|
|
2733
|
+
advance(room, local, authorization, current),
|
|
2734
|
+
abandonedOn(signal),
|
|
2735
|
+
]);
|
|
2736
|
+
if (withheld !== null && current())
|
|
2737
|
+
await withhold(withheld, room, local, progress);
|
|
2738
|
+
});
|
|
2739
|
+
}
|
|
2740
|
+
catch {
|
|
2741
|
+
stale.add(locator);
|
|
2742
|
+
progress.reachable = false;
|
|
2743
|
+
}
|
|
2744
|
+
finally {
|
|
2745
|
+
repairing.delete(locator);
|
|
2746
|
+
}
|
|
2747
|
+
if (signal.aborted)
|
|
2748
|
+
return 'done';
|
|
2749
|
+
await rest(progress);
|
|
2750
|
+
if (!stale.has(locator)) {
|
|
2751
|
+
if ((replicas.get(locator)?.queue.length ?? 0) > 0)
|
|
2752
|
+
submitLater(locator);
|
|
2753
|
+
return 'done';
|
|
2754
|
+
}
|
|
2755
|
+
if (!repairable.has(locator))
|
|
2756
|
+
return 'done';
|
|
2757
|
+
const reset = repairable.get(locator);
|
|
2758
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
2759
|
+
}
|
|
2760
|
+
function resendLater() {
|
|
2761
|
+
if (!socketOpen)
|
|
2762
|
+
return;
|
|
2763
|
+
void recovery.attempt('resend', resendPass, { again: true });
|
|
2764
|
+
}
|
|
2765
|
+
async function resendPass(signal) {
|
|
2766
|
+
if (!socketOpen || closed || stopping || discoveryPaused)
|
|
2767
|
+
return 'done';
|
|
2768
|
+
const reading = { complete: true, reachable: true };
|
|
2769
|
+
try {
|
|
2770
|
+
await conversations.resend(reading);
|
|
2771
|
+
}
|
|
2772
|
+
catch {
|
|
2773
|
+
reading.reachable = false;
|
|
2774
|
+
}
|
|
2775
|
+
if (signal.aborted)
|
|
2776
|
+
return 'done';
|
|
2777
|
+
return reading.reachable && reading.complete ? 'done' : 'again';
|
|
1650
2778
|
}
|
|
1651
2779
|
function submitAll() {
|
|
1652
2780
|
for (const [locator, replica] of replicas) {
|
|
@@ -1654,31 +2782,41 @@ export function composeRuntime(options) {
|
|
|
1654
2782
|
submitLater(locator);
|
|
1655
2783
|
}
|
|
1656
2784
|
}
|
|
1657
|
-
async function finishOpening(progress) {
|
|
2785
|
+
async function finishOpening(progress, live) {
|
|
1658
2786
|
try {
|
|
1659
|
-
if (
|
|
2787
|
+
if (!live()) {
|
|
1660
2788
|
progress.complete = false;
|
|
1661
2789
|
return;
|
|
1662
2790
|
}
|
|
1663
2791
|
submitAll();
|
|
2792
|
+
for (const locator of stale)
|
|
2793
|
+
if (repairable.has(locator))
|
|
2794
|
+
repairLater(locator, {});
|
|
1664
2795
|
await conversations.reconcile();
|
|
1665
2796
|
if (status.providerUnreachable)
|
|
1666
2797
|
conversations.reconnected();
|
|
1667
2798
|
const reading = { complete: true, reachable: true };
|
|
1668
|
-
|
|
1669
|
-
|
|
2799
|
+
if (demand.conversations)
|
|
2800
|
+
await conversations.catchUp(reading);
|
|
2801
|
+
if (progress.reachable && reading.reachable)
|
|
2802
|
+
await conversations.resend(reading);
|
|
2803
|
+
if (!reading.reachable) {
|
|
1670
2804
|
progress.reachable = false;
|
|
2805
|
+
resendLater();
|
|
2806
|
+
}
|
|
1671
2807
|
}
|
|
1672
2808
|
catch {
|
|
1673
2809
|
progress.complete = false;
|
|
1674
2810
|
}
|
|
1675
2811
|
finally {
|
|
1676
|
-
|
|
1677
|
-
|
|
2812
|
+
if (live()) {
|
|
2813
|
+
complete = progress.complete;
|
|
2814
|
+
await rest(progress, live);
|
|
2815
|
+
}
|
|
1678
2816
|
}
|
|
1679
2817
|
}
|
|
1680
|
-
async function rest(progress) {
|
|
1681
|
-
if (
|
|
2818
|
+
async function rest(progress, live = () => !closed) {
|
|
2819
|
+
if (!live())
|
|
1682
2820
|
return;
|
|
1683
2821
|
let lastCompleted = status.lastCompleted;
|
|
1684
2822
|
let synced = complete && stale.size === 0 && !pending();
|
|
@@ -1686,6 +2824,8 @@ export function composeRuntime(options) {
|
|
|
1686
2824
|
const completedAt = new Date().toISOString();
|
|
1687
2825
|
try {
|
|
1688
2826
|
await (await openLocal()).complete(completedAt);
|
|
2827
|
+
if (!live())
|
|
2828
|
+
return;
|
|
1689
2829
|
recorded = true;
|
|
1690
2830
|
lastCompleted = completedAt;
|
|
1691
2831
|
}
|
|
@@ -1694,46 +2834,74 @@ export function composeRuntime(options) {
|
|
|
1694
2834
|
}
|
|
1695
2835
|
}
|
|
1696
2836
|
moveTo({
|
|
1697
|
-
state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
|
|
2837
|
+
state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
|
|
1698
2838
|
lastCompleted,
|
|
1699
2839
|
providerUnreachable: !progress.reachable,
|
|
1700
2840
|
...attention(),
|
|
1701
2841
|
});
|
|
1702
2842
|
}
|
|
2843
|
+
function discover() {
|
|
2844
|
+
let release = () => { };
|
|
2845
|
+
const done = new Promise((resolve) => {
|
|
2846
|
+
release = resolve;
|
|
2847
|
+
});
|
|
2848
|
+
discovering = { generation, done, release };
|
|
2849
|
+
return discovering;
|
|
2850
|
+
}
|
|
1703
2851
|
function applyRoom(locator, roomVersion) {
|
|
1704
2852
|
announced.set(locator, roomVersion);
|
|
1705
|
-
return task(async () => {
|
|
1706
|
-
|
|
1707
|
-
|
|
2853
|
+
return task(roomLane(locator), async (live) => {
|
|
2854
|
+
if (discovering?.generation === generation)
|
|
2855
|
+
await discovering.done;
|
|
2856
|
+
const saving = answering.get(locator);
|
|
2857
|
+
if (saving?.generation === generation)
|
|
2858
|
+
await saving.answered;
|
|
2859
|
+
if (!live() || authorization === undefined)
|
|
1708
2860
|
return;
|
|
1709
2861
|
const room = discovered.get(locator);
|
|
1710
2862
|
const held = replicas.get(locator);
|
|
1711
|
-
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2863
|
+
if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
|
|
1712
2864
|
return;
|
|
1713
2865
|
if (held !== undefined &&
|
|
1714
2866
|
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
1715
2867
|
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
1716
2868
|
return;
|
|
1717
2869
|
}
|
|
1718
|
-
const during = underway;
|
|
2870
|
+
const during = underway?.generation === generation ? underway.progress : null;
|
|
1719
2871
|
const progress = during ?? { complete: true, reachable: true };
|
|
1720
2872
|
if (during === null)
|
|
1721
2873
|
moveTo(transferring(status));
|
|
1722
2874
|
try {
|
|
1723
2875
|
const local = await openLocal();
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
2876
|
+
await admittedRead(async () => {
|
|
2877
|
+
if (!live() || authorization === undefined)
|
|
2878
|
+
return;
|
|
2879
|
+
const withheld = await advance(room, local, authorization, live);
|
|
2880
|
+
if (withheld !== null)
|
|
2881
|
+
await withhold(withheld, room, local, progress);
|
|
2882
|
+
});
|
|
1727
2883
|
}
|
|
1728
2884
|
catch {
|
|
1729
2885
|
stale.add(locator);
|
|
1730
2886
|
}
|
|
1731
2887
|
finally {
|
|
1732
2888
|
if (during === null)
|
|
1733
|
-
await rest(progress);
|
|
2889
|
+
await rest(progress, live);
|
|
1734
2890
|
}
|
|
1735
2891
|
});
|
|
1736
2892
|
}
|
|
2893
|
+
function roomsCarried(replacing) {
|
|
2894
|
+
const replaced = new Set(replacing.map(roomLane));
|
|
2895
|
+
return Promise.all([...lanes]
|
|
2896
|
+
.filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
|
|
2897
|
+
.map(([, held]) => held.chain));
|
|
2898
|
+
}
|
|
2899
|
+
const receipts = {
|
|
2900
|
+
heads: async () => (await openLocal()).receivedHeads(),
|
|
2901
|
+
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
2902
|
+
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
2903
|
+
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
2904
|
+
};
|
|
1737
2905
|
function observeStream() {
|
|
1738
2906
|
const stream = options.adapter.roomStream;
|
|
1739
2907
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -1743,6 +2911,9 @@ export function composeRuntime(options) {
|
|
|
1743
2911
|
return;
|
|
1744
2912
|
if (event.kind === 'opened') {
|
|
1745
2913
|
socketOpen = true;
|
|
2914
|
+
generation += 1;
|
|
2915
|
+
conversations.reopened();
|
|
2916
|
+
recovery.resume();
|
|
1746
2917
|
void sync('opened');
|
|
1747
2918
|
}
|
|
1748
2919
|
else if (event.kind === 'closed')
|
|
@@ -1751,7 +2922,7 @@ export function composeRuntime(options) {
|
|
|
1751
2922
|
void applyOpening(event.expired);
|
|
1752
2923
|
else
|
|
1753
2924
|
void applyRoom(event.locator, event.roomVersion);
|
|
1754
|
-
});
|
|
2925
|
+
}, receipts);
|
|
1755
2926
|
}
|
|
1756
2927
|
function writesHeld() {
|
|
1757
2928
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -1766,6 +2937,7 @@ export function composeRuntime(options) {
|
|
|
1766
2937
|
haltedRefusal = false;
|
|
1767
2938
|
if (mode !== serviceMode) {
|
|
1768
2939
|
serviceMode = mode;
|
|
2940
|
+
queueReadiness();
|
|
1769
2941
|
journal.record(`service mode ${mode}`);
|
|
1770
2942
|
for (const listener of serviceModeListeners)
|
|
1771
2943
|
listener(mode);
|
|
@@ -1778,6 +2950,8 @@ export function composeRuntime(options) {
|
|
|
1778
2950
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
1779
2951
|
return;
|
|
1780
2952
|
withdrawConversations = stream.observe((event) => {
|
|
2953
|
+
if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
|
|
2954
|
+
queueReadiness();
|
|
1781
2955
|
if (conversations.receive(event)) {
|
|
1782
2956
|
void ready.then(scheduleCurrency);
|
|
1783
2957
|
return;
|
|
@@ -1789,46 +2963,47 @@ export function composeRuntime(options) {
|
|
|
1789
2963
|
});
|
|
1790
2964
|
}
|
|
1791
2965
|
function scheduleCurrency() {
|
|
1792
|
-
if (currencyQueued || closed || stopping)
|
|
2966
|
+
if (currencyQueued === generation || closed || stopping)
|
|
1793
2967
|
return;
|
|
1794
|
-
currencyQueued =
|
|
1795
|
-
void task(runCurrency);
|
|
2968
|
+
currencyQueued = generation;
|
|
2969
|
+
void task(CONVERSATION_LANE, runCurrency);
|
|
1796
2970
|
}
|
|
1797
|
-
async function runCurrency() {
|
|
1798
|
-
currencyQueued
|
|
1799
|
-
|
|
2971
|
+
async function runCurrency(live) {
|
|
2972
|
+
if (currencyQueued === generation)
|
|
2973
|
+
currencyQueued = null;
|
|
2974
|
+
if (!live())
|
|
1800
2975
|
return;
|
|
1801
2976
|
await conversations.reconcile();
|
|
1802
|
-
if (conversations.current()) {
|
|
2977
|
+
if (conversations.current() || !demand.conversations) {
|
|
1803
2978
|
show();
|
|
1804
2979
|
return;
|
|
1805
2980
|
}
|
|
1806
|
-
const
|
|
1807
|
-
|
|
1808
|
-
moveTo(transferring(resting));
|
|
2981
|
+
const progress = { complete: false, reachable: !status.providerUnreachable };
|
|
2982
|
+
moveTo(transferring(status));
|
|
1809
2983
|
try {
|
|
1810
2984
|
await conversations.catchUp(progress);
|
|
1811
2985
|
}
|
|
1812
2986
|
catch {
|
|
1813
2987
|
progress.reachable = false;
|
|
1814
2988
|
}
|
|
1815
|
-
|
|
1816
|
-
if (!closed) {
|
|
1817
|
-
moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
2989
|
+
await rest(progress, live);
|
|
1820
2990
|
}
|
|
1821
2991
|
function close() {
|
|
1822
2992
|
if (closed)
|
|
1823
2993
|
return;
|
|
1824
2994
|
closed = true;
|
|
2995
|
+
if (narrowing !== null)
|
|
2996
|
+
clearTimeout(narrowing);
|
|
2997
|
+
if (rechecking !== null)
|
|
2998
|
+
clearInterval(rechecking);
|
|
2999
|
+
readinessWatches.clear();
|
|
1825
3000
|
withdrawStream?.();
|
|
1826
3001
|
withdrawStream = undefined;
|
|
1827
3002
|
withdrawConversations?.();
|
|
1828
3003
|
withdrawConversations = undefined;
|
|
1829
3004
|
withdrawServiceMode?.();
|
|
1830
3005
|
conversations.close();
|
|
1831
|
-
|
|
3006
|
+
void recovery.retire();
|
|
1832
3007
|
store?.close();
|
|
1833
3008
|
void opening?.then((opened) => {
|
|
1834
3009
|
opened.close();
|
|
@@ -1838,6 +3013,7 @@ export function composeRuntime(options) {
|
|
|
1838
3013
|
storageLossListeners.clear();
|
|
1839
3014
|
statusListeners.clear();
|
|
1840
3015
|
serviceModeListeners.clear();
|
|
3016
|
+
memberListeners.clear();
|
|
1841
3017
|
readSide.close();
|
|
1842
3018
|
queueSubscriptions.clear();
|
|
1843
3019
|
roomDraftListeners.clear();
|
|
@@ -1845,9 +3021,8 @@ export function composeRuntime(options) {
|
|
|
1845
3021
|
}
|
|
1846
3022
|
async function stop() {
|
|
1847
3023
|
stopping = true;
|
|
1848
|
-
|
|
1849
|
-
await
|
|
1850
|
-
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
3024
|
+
await recovery.retire();
|
|
3025
|
+
await Promise.all(working);
|
|
1851
3026
|
await mutations;
|
|
1852
3027
|
close();
|
|
1853
3028
|
}
|
|
@@ -1869,14 +3044,54 @@ export function composeRuntime(options) {
|
|
|
1869
3044
|
return planOf(input, roomFacts(reach), planReach(reach));
|
|
1870
3045
|
}
|
|
1871
3046
|
function watchPlan(plan, deliver) {
|
|
1872
|
-
return readSide.watch(plan, identityOf(plan),
|
|
3047
|
+
return readSide.watch(plan, identityOf(plan), () => {
|
|
3048
|
+
const cover = covered(plan);
|
|
3049
|
+
awaitPreparation(cover);
|
|
3050
|
+
return cover;
|
|
3051
|
+
}, deliver);
|
|
1873
3052
|
}
|
|
1874
3053
|
async function searched(input, reach) {
|
|
1875
3054
|
const plan = searchPlanOf(input);
|
|
1876
|
-
const
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
3055
|
+
const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
|
|
3056
|
+
for (;;) {
|
|
3057
|
+
const cover = covered(filter);
|
|
3058
|
+
awaitPreparation(cover);
|
|
3059
|
+
const pending = 'pending' in cover ? [...cover.pending] : [];
|
|
3060
|
+
const covering = 'pending' in cover ? [] : cover.coverage.rooms;
|
|
3061
|
+
const unindexed = covering
|
|
3062
|
+
.filter(({ room }) => !searchIndex.rooms().has(room))
|
|
3063
|
+
.map(({ room }) => room);
|
|
3064
|
+
for (const room of unindexed)
|
|
3065
|
+
pending.push({ room, awaits: 'index' });
|
|
3066
|
+
const held = conversations.searchable();
|
|
3067
|
+
const named = reach !== null && !reach.conversations
|
|
3068
|
+
? []
|
|
3069
|
+
: (plan.conversations ?? held).filter((one) => held.includes(one) && conversations.acquired(one));
|
|
3070
|
+
const waiting = named.filter((one) => !searchIndex.conversations().has(one));
|
|
3071
|
+
for (const conversation of waiting)
|
|
3072
|
+
pending.push({ conversation, awaits: 'index' });
|
|
3073
|
+
if (unindexed.length > 0 || waiting.length > 0)
|
|
3074
|
+
index(unindexed, waiting);
|
|
3075
|
+
if (pending.length > 0 || 'pending' in cover)
|
|
3076
|
+
return { pending };
|
|
3077
|
+
const seen = searchIndex.changes();
|
|
3078
|
+
await searchIndex.caughtUp();
|
|
3079
|
+
const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(filter);
|
|
3080
|
+
if (selected !== null && 'pending' in selected)
|
|
3081
|
+
return selected;
|
|
3082
|
+
if (searchIndex.changes() !== seen ||
|
|
3083
|
+
JSON.stringify(covered(filter)) !== JSON.stringify(cover))
|
|
3084
|
+
continue;
|
|
3085
|
+
return {
|
|
3086
|
+
...searchIndex.search(plan, selected === null ? null : selected.documents, named),
|
|
3087
|
+
coverage: {
|
|
3088
|
+
rooms: covering,
|
|
3089
|
+
conversations: named,
|
|
3090
|
+
index: searchIndex.generation(),
|
|
3091
|
+
overlay: cover.coverage.overlay,
|
|
3092
|
+
},
|
|
3093
|
+
};
|
|
3094
|
+
}
|
|
1880
3095
|
}
|
|
1881
3096
|
const watch = (topic, input, deliver) => {
|
|
1882
3097
|
if (topic === 'room-drafts') {
|
|
@@ -1901,6 +3116,8 @@ export function composeRuntime(options) {
|
|
|
1901
3116
|
}
|
|
1902
3117
|
if (topic === 'conversations')
|
|
1903
3118
|
return conversations.watchList(deliver);
|
|
3119
|
+
if (topic === 'agents')
|
|
3120
|
+
return conversations.watchAgents(deliver);
|
|
1904
3121
|
if (topic === 'presentation') {
|
|
1905
3122
|
const listener = {
|
|
1906
3123
|
app: input[0],
|
|
@@ -1920,6 +3137,9 @@ export function composeRuntime(options) {
|
|
|
1920
3137
|
presentationListeners.delete(listener);
|
|
1921
3138
|
};
|
|
1922
3139
|
}
|
|
3140
|
+
if (topic === 'readiness') {
|
|
3141
|
+
return watchReadiness({ target: '', code: 'stored', capacity: null }, null, deliver);
|
|
3142
|
+
}
|
|
1923
3143
|
if (topic === 'rooms') {
|
|
1924
3144
|
roomListeners.add(deliver);
|
|
1925
3145
|
deliver(listed());
|
|
@@ -1955,6 +3175,13 @@ export function composeRuntime(options) {
|
|
|
1955
3175
|
serviceModeListeners.delete(deliver);
|
|
1956
3176
|
};
|
|
1957
3177
|
}
|
|
3178
|
+
if (topic === 'member') {
|
|
3179
|
+
memberListeners.add(deliver);
|
|
3180
|
+
deliver(member);
|
|
3181
|
+
return () => {
|
|
3182
|
+
memberListeners.delete(deliver);
|
|
3183
|
+
};
|
|
3184
|
+
}
|
|
1958
3185
|
if (topic === 'queue') {
|
|
1959
3186
|
const queue = { locator: input[0], notify: deliver };
|
|
1960
3187
|
queueSubscriptions.add(queue);
|
|
@@ -1972,7 +3199,7 @@ export function composeRuntime(options) {
|
|
|
1972
3199
|
rooms() {
|
|
1973
3200
|
return listed();
|
|
1974
3201
|
},
|
|
1975
|
-
query: ((input) =>
|
|
3202
|
+
query: ((input) => rowsQuery(planned(input, null))),
|
|
1976
3203
|
search: (input) => searched(input, null),
|
|
1977
3204
|
types(locator) {
|
|
1978
3205
|
return {
|
|
@@ -2001,20 +3228,20 @@ export function composeRuntime(options) {
|
|
|
2001
3228
|
return exclusive(async () => {
|
|
2002
3229
|
const parts = readParts(input);
|
|
2003
3230
|
if (path === '' || normalize(path) !== path)
|
|
2004
|
-
throw
|
|
3231
|
+
throw runtimeError('room-draft-malformed');
|
|
2005
3232
|
const local = await openLocal();
|
|
2006
3233
|
const current = roomDrafts.get(room)?.get(path);
|
|
2007
3234
|
if ((current?.revision ?? null) !== revision) {
|
|
2008
|
-
return
|
|
2009
|
-
outcome: 'stale',
|
|
2010
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
2011
|
-
};
|
|
3235
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2012
3236
|
}
|
|
3237
|
+
const file = parts.file === undefined
|
|
3238
|
+
? undefined
|
|
3239
|
+
: await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
|
|
2013
3240
|
const record = {
|
|
2014
3241
|
locator: room,
|
|
2015
3242
|
path,
|
|
2016
3243
|
revision: roomDraftRevision(),
|
|
2017
|
-
parts: held(current?.parts, parts),
|
|
3244
|
+
parts: held(current?.parts, parts, file),
|
|
2018
3245
|
returned: current?.returned ?? null,
|
|
2019
3246
|
};
|
|
2020
3247
|
await local.keepRoomDraft(record);
|
|
@@ -2028,10 +3255,7 @@ export function composeRuntime(options) {
|
|
|
2028
3255
|
const local = await openLocal();
|
|
2029
3256
|
const current = roomDrafts.get(room)?.get(path);
|
|
2030
3257
|
if (current === undefined || current.revision !== revision) {
|
|
2031
|
-
return
|
|
2032
|
-
outcome: 'stale',
|
|
2033
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
2034
|
-
};
|
|
3258
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2035
3259
|
}
|
|
2036
3260
|
await local.discardRoomDraft(room, path);
|
|
2037
3261
|
forgetRoomDraft(room, path);
|
|
@@ -2066,6 +3290,10 @@ export function composeRuntime(options) {
|
|
|
2066
3290
|
history(locator, path, query) {
|
|
2067
3291
|
return readHistory(locator, path, query);
|
|
2068
3292
|
},
|
|
3293
|
+
binaryFile: {
|
|
3294
|
+
read: readBinaryPath,
|
|
3295
|
+
readReference: readBinaryReference,
|
|
3296
|
+
},
|
|
2069
3297
|
call(name, input) {
|
|
2070
3298
|
return invokeAction(name, input);
|
|
2071
3299
|
},
|
|
@@ -2078,30 +3306,55 @@ export function composeRuntime(options) {
|
|
|
2078
3306
|
return journal.export();
|
|
2079
3307
|
},
|
|
2080
3308
|
persistence() {
|
|
2081
|
-
return
|
|
3309
|
+
return persistence === 'granted';
|
|
3310
|
+
},
|
|
3311
|
+
async retry() {
|
|
3312
|
+
journal.record('retry');
|
|
3313
|
+
unindexable.clear();
|
|
3314
|
+
queueReadiness();
|
|
3315
|
+
resume();
|
|
2082
3316
|
},
|
|
2083
3317
|
observe: observing(watch),
|
|
2084
3318
|
async reauthorize() {
|
|
2085
3319
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2086
3320
|
return;
|
|
2087
3321
|
journal.record('connection reauthorize');
|
|
2088
|
-
await
|
|
3322
|
+
await Promise.all([...lanes.values()]
|
|
3323
|
+
.filter((held) => held.generation === generation)
|
|
3324
|
+
.map((held) => held.chain));
|
|
2089
3325
|
await mutations;
|
|
2090
3326
|
authorization = undefined;
|
|
2091
3327
|
discoveryPaused = false;
|
|
2092
3328
|
if (options.adapter.roomStream === undefined)
|
|
2093
3329
|
await sync('unstreamed');
|
|
2094
3330
|
else
|
|
2095
|
-
await task(ensureAuthorization);
|
|
3331
|
+
await task(OPENING_LANE, ensureAuthorization);
|
|
2096
3332
|
},
|
|
2097
3333
|
};
|
|
2098
3334
|
const reachable = {
|
|
2099
3335
|
md,
|
|
2100
3336
|
watch,
|
|
3337
|
+
referrerScope: (referrer) => options.adapter.referrerScope(referrer),
|
|
2101
3338
|
plan: planned,
|
|
2102
|
-
query: (plan) =>
|
|
3339
|
+
query: (plan) => rowsQuery(plan),
|
|
2103
3340
|
watchPlan,
|
|
2104
3341
|
search: searched,
|
|
3342
|
+
searching,
|
|
3343
|
+
watchReadiness,
|
|
3344
|
+
};
|
|
3345
|
+
return {
|
|
3346
|
+
md,
|
|
3347
|
+
stop,
|
|
3348
|
+
lose: loseRooms,
|
|
3349
|
+
watch,
|
|
3350
|
+
acquire,
|
|
3351
|
+
persisted(answer) {
|
|
3352
|
+
if (answer === persistence)
|
|
3353
|
+
return;
|
|
3354
|
+
persistence = answer;
|
|
3355
|
+
journal.record(`persistence ${answer}`);
|
|
3356
|
+
queueReadiness();
|
|
3357
|
+
},
|
|
3358
|
+
within: (reach, port) => narrowed(reachable, reach, port),
|
|
2105
3359
|
};
|
|
2106
|
-
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2107
3360
|
}
|