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