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