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