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