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