@agentshouse/mdruntime 0.1.0-cbb8d8acc9d19 → 0.1.0-cc56db96f86ae
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 +285 -225
- package/README.md +21 -10
- package/dist/adapter.d.ts +114 -26
- 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 +147 -40
- package/dist/change-set.d.ts +7 -2
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +50 -16
- package/dist/change.d.ts +45 -0
- package/dist/change.d.ts.map +1 -1
- package/dist/change.js +34 -0
- package/dist/chat/acts.d.ts +33 -0
- package/dist/chat/acts.d.ts.map +1 -0
- package/dist/chat/acts.js +36 -0
- package/dist/chat/context.d.ts +3 -0
- package/dist/chat/context.d.ts.map +1 -0
- package/dist/chat/context.js +10 -0
- package/dist/chat/earlier.d.ts +10 -0
- package/dist/chat/earlier.d.ts.map +1 -0
- package/dist/chat/earlier.js +54 -0
- package/dist/chat/house.d.ts +25 -0
- package/dist/chat/house.d.ts.map +1 -0
- package/dist/chat/house.js +38 -0
- package/dist/chat/input.d.ts +11 -0
- package/dist/chat/input.d.ts.map +1 -0
- package/dist/chat/input.js +92 -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 +28 -0
- package/dist/chat/list.d.ts.map +1 -0
- package/dist/chat/list.js +103 -0
- package/dist/chat/markdown.d.ts +21 -0
- package/dist/chat/markdown.d.ts.map +1 -0
- package/dist/chat/markdown.js +246 -0
- package/dist/chat/message.d.ts +7 -0
- package/dist/chat/message.d.ts.map +1 -0
- package/dist/chat/message.js +237 -0
- package/dist/chat/messages.d.ts +29 -0
- package/dist/chat/messages.d.ts.map +1 -0
- package/dist/chat/messages.js +190 -0
- package/dist/chat/name.d.ts +7 -0
- package/dist/chat/name.d.ts.map +1 -0
- package/dist/chat/name.js +37 -0
- package/dist/chat/opened.d.ts +12 -0
- package/dist/chat/opened.d.ts.map +1 -0
- package/dist/chat/opened.js +19 -0
- package/dist/chat/overlay.d.ts +32 -0
- package/dist/chat/overlay.d.ts.map +1 -0
- package/dist/chat/overlay.js +103 -0
- package/dist/chat/parts.d.ts +58 -0
- package/dist/chat/parts.d.ts.map +1 -0
- package/dist/chat/parts.js +120 -0
- package/dist/chat/pieces.d.ts +62 -0
- package/dist/chat/pieces.d.ts.map +1 -0
- package/dist/chat/pieces.js +1110 -0
- package/dist/chat/process.d.ts +20 -0
- package/dist/chat/process.d.ts.map +1 -0
- package/dist/chat/process.js +32 -0
- package/dist/chat/provider.d.ts +68 -0
- package/dist/chat/provider.d.ts.map +1 -0
- package/dist/chat/provider.js +619 -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 +37 -0
- package/dist/chat/ready.d.ts.map +1 -0
- package/dist/chat/ready.js +93 -0
- package/dist/chat/room-file.d.ts +8 -0
- package/dist/chat/room-file.d.ts.map +1 -0
- package/dist/chat/room-file.js +67 -0
- package/dist/chat/steady.d.ts +3 -0
- package/dist/chat/steady.d.ts.map +1 -0
- package/dist/chat/steady.js +26 -0
- package/dist/chat/style.d.ts +4 -0
- package/dist/chat/style.d.ts.map +1 -0
- package/dist/chat/style.js +306 -0
- package/dist/chat.d.ts +15 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +8 -0
- package/dist/client.d.ts +33 -13
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +81 -16
- package/dist/collection.d.ts +23 -13
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +367 -38
- package/dist/connection.d.ts +54 -16
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +2582 -486
- package/dist/conversation.d.ts +39 -12
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +546 -171
- package/dist/digest.d.ts +2 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +100 -0
- package/dist/durability.d.ts +7 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/entry.d.ts +17 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +157 -0
- package/dist/expectation.d.ts +21 -0
- package/dist/expectation.d.ts.map +1 -0
- package/dist/expectation.js +70 -0
- package/dist/failure.d.ts +13 -0
- package/dist/failure.d.ts.map +1 -0
- package/dist/failure.js +19 -0
- package/dist/fence.d.ts +6 -0
- package/dist/fence.d.ts.map +1 -0
- package/dist/fence.js +10 -0
- package/dist/file.d.ts +2 -10
- package/dist/file.d.ts.map +1 -1
- package/dist/file.js +3 -120
- package/dist/host.d.ts +15 -3
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +107 -21
- 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 +20 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/memory.d.ts +11 -4
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +92 -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 +7 -2
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +29 -12
- package/dist/open.d.ts +2 -0
- package/dist/open.d.ts.map +1 -0
- package/dist/open.js +1 -0
- package/dist/outcome.d.ts +57 -3
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +67 -1
- package/dist/overlay.d.ts +18 -5
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +271 -165
- package/dist/query.d.ts +61 -4
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +40 -7
- package/dist/reach.d.ts +13 -4
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +201 -47
- package/dist/react.d.ts +31 -20
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +13 -21
- package/dist/read-side.d.ts +18 -9
- package/dist/read-side.d.ts.map +1 -1
- package/dist/read-side.js +903 -157
- 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 +198 -0
- package/dist/refresh.d.ts +13 -3
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +73 -43
- package/dist/replica.d.ts +16 -8
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +198 -91
- package/dist/room-draft.d.ts +40 -9
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +153 -37
- package/dist/rows.d.ts +10 -13
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +19 -14
- package/dist/save.d.ts +72 -4
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +812 -145
- package/dist/search.d.ts +52 -8
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +242 -46
- package/dist/seed.d.ts +16 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +75 -0
- package/dist/shared.d.ts +19 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +461 -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 +90 -23
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +675 -295
- package/dist/tables.d.ts +181 -0
- package/dist/tables.d.ts.map +1 -0
- package/dist/tables.js +309 -0
- package/dist/turn.d.ts +9 -0
- package/dist/turn.d.ts.map +1 -0
- package/dist/turn.js +67 -0
- package/dist/windows.d.ts +21 -0
- package/dist/windows.d.ts.map +1 -0
- package/dist/windows.js +84 -0
- package/dist/wire.d.ts +165 -33
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +67 -31
- package/package.json +30 -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,48 @@
|
|
|
1
|
+
import { roomTarget, target } from '@agentshouse/mdmodel';
|
|
1
2
|
import { attachedModel } from './attached-model.js';
|
|
2
3
|
import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
|
|
3
|
-
import { observing } from './observation.js';
|
|
4
|
-
import { filterOf, identityOf, planOf, } from './query.js';
|
|
4
|
+
import { observing, whole } from './observation.js';
|
|
5
|
+
import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
|
|
5
6
|
import { createReadSide } from './read-side.js';
|
|
7
|
+
import { seededResult } from './seed.js';
|
|
8
|
+
import { documentKey, markdownPath, roomOfKey } from './rows.js';
|
|
6
9
|
import { createSearchIndex, searchPlanOf, } from './search.js';
|
|
10
|
+
import { capped, MAXIMUM_WINDOW } from './windows.js';
|
|
7
11
|
import { bootstrapRoom } from './bootstrap.js';
|
|
8
|
-
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
12
|
+
import { changeSet, changing, entering, mergeChangeSets } from './change-set.js';
|
|
9
13
|
import { createConversationStore, } from './conversation.js';
|
|
10
|
-
import {
|
|
14
|
+
import { leaving as leavingIn, partnerOf, queuedFile, } from './change.js';
|
|
15
|
+
import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
|
|
16
|
+
import { runtimeError } from './failure.js';
|
|
11
17
|
import { createLog } from './log.js';
|
|
18
|
+
import { binaryRecord } from './binary.js';
|
|
19
|
+
import { ABSENT_TARGET, absentBinary, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
|
|
20
|
+
import { createRecovery } from './recovery.js';
|
|
21
|
+
import { doorReads } from './reads.js';
|
|
12
22
|
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 {
|
|
23
|
+
import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
|
|
24
|
+
import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicating, sameMembership, standing, } from './replica.js';
|
|
25
|
+
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
26
|
+
import { same, saving, STARTING, transferring, } from './status.js';
|
|
17
27
|
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
18
|
-
import {
|
|
28
|
+
import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
|
|
29
|
+
import { FencedMap } from './fence.js';
|
|
30
|
+
import { keysIn, tallying } from './shared.js';
|
|
31
|
+
import { finished, inChunks, ROW_WEIGHT, stepped, yielded } from './turn.js';
|
|
32
|
+
import { identityTime, roomDraftRevision } from './identity.js';
|
|
19
33
|
import { beneath, normalize } from './paths.js';
|
|
20
34
|
import { openRuntimeStore, } from './store.js';
|
|
21
35
|
import { unconfirmed } from './transfer.js';
|
|
22
36
|
import { governs, holds, narrowed, planReach, } from './reach.js';
|
|
37
|
+
function roomsAlone(plan) {
|
|
38
|
+
return (plan.reach === null &&
|
|
39
|
+
plan.root.types === null &&
|
|
40
|
+
plan.root.where === null &&
|
|
41
|
+
plan.root.pathPrefix === null &&
|
|
42
|
+
plan.paths === null &&
|
|
43
|
+
plan.hop === null &&
|
|
44
|
+
plan.backlinks === null);
|
|
45
|
+
}
|
|
23
46
|
function within(root, path) {
|
|
24
47
|
return root === '' || beneath(root, path);
|
|
25
48
|
}
|
|
@@ -33,22 +56,73 @@ function declaredSelection(room) {
|
|
|
33
56
|
root !== '' &&
|
|
34
57
|
selection.roots.every((other, at) => at === index || !within(other, root)));
|
|
35
58
|
}
|
|
59
|
+
const OPENING_LANE = 'opening';
|
|
60
|
+
const CONVERSATION_LANE = 'conversations';
|
|
61
|
+
const ROOM_READS = 256;
|
|
62
|
+
const ROOM_ATTEMPTS = 16;
|
|
63
|
+
// A Room that locks offline stays readable this long after the connection drops, so a laptop waking or changing network keeps working while a disconnected device locks soon after.
|
|
64
|
+
const LOCK_GRACE_MS = 120_000;
|
|
65
|
+
function operationOf(input) {
|
|
66
|
+
const operation = input?.operation_id;
|
|
67
|
+
return typeof operation === 'string' ? operation : null;
|
|
68
|
+
}
|
|
69
|
+
const outside = new Map();
|
|
70
|
+
function outsideOf(roots) {
|
|
71
|
+
const key = JSON.stringify(roots);
|
|
72
|
+
const held = outside.get(key);
|
|
73
|
+
if (held !== undefined)
|
|
74
|
+
return held;
|
|
75
|
+
const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
|
|
76
|
+
const made = {
|
|
77
|
+
documents: (path) => (markdownPath(path) ? counted(path) : 0),
|
|
78
|
+
binaries: counted,
|
|
79
|
+
};
|
|
80
|
+
outside.set(key, made);
|
|
81
|
+
return made;
|
|
82
|
+
}
|
|
83
|
+
function abandonedOn(signal) {
|
|
84
|
+
return new Promise((resolve) => {
|
|
85
|
+
if (signal.aborted)
|
|
86
|
+
resolve(null);
|
|
87
|
+
else
|
|
88
|
+
signal.addEventListener('abort', () => resolve(null), { once: true });
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function roomLane(locator) {
|
|
92
|
+
return `room ${locator}`;
|
|
93
|
+
}
|
|
94
|
+
function pendingKey(locator, id) {
|
|
95
|
+
return `${locator}\u0000${id}`;
|
|
96
|
+
}
|
|
36
97
|
export function composeRuntime(options) {
|
|
98
|
+
const localSaves = options.adapter.localSaves === true;
|
|
37
99
|
let authorization = options.authorization;
|
|
38
100
|
let opening;
|
|
39
101
|
let store;
|
|
40
|
-
const
|
|
102
|
+
const fences = new Map();
|
|
103
|
+
const isFenced = (locator) => fences.has(locator);
|
|
104
|
+
let fencings = 0;
|
|
105
|
+
const lastFenced = new Map();
|
|
106
|
+
const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
|
|
107
|
+
let discoveries = 0;
|
|
108
|
+
const replicas = new FencedMap(isFenced);
|
|
109
|
+
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
41
110
|
const pendings = new Map();
|
|
42
|
-
const
|
|
111
|
+
const seeds = new Map();
|
|
112
|
+
const seedWatchers = new Map();
|
|
113
|
+
const roomDrafts = new FencedMap(isFenced);
|
|
43
114
|
const presentations = new Map();
|
|
44
115
|
let limits = [];
|
|
45
116
|
let capabilities = [];
|
|
46
|
-
const rooms = new
|
|
117
|
+
const rooms = new FencedMap(isFenced);
|
|
118
|
+
const counted = new Map();
|
|
47
119
|
const unreadable = new Set();
|
|
48
|
-
const
|
|
49
|
-
const
|
|
120
|
+
const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
|
|
121
|
+
const models = new FencedMap(isModelFenced);
|
|
122
|
+
const liveModels = new FencedMap(isModelFenced);
|
|
50
123
|
const invalidModels = new Set();
|
|
51
124
|
const discovered = new Map();
|
|
125
|
+
const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
|
|
52
126
|
let roomsListed = false;
|
|
53
127
|
const scopedRefusals = new Map();
|
|
54
128
|
const sourceRefusals = new Map();
|
|
@@ -76,19 +150,30 @@ export function composeRuntime(options) {
|
|
|
76
150
|
let status = STARTING;
|
|
77
151
|
let shown = STARTING;
|
|
78
152
|
let discoveryPaused = false;
|
|
79
|
-
let resetTimer;
|
|
80
153
|
let closed = false;
|
|
81
154
|
let stopping = false;
|
|
82
155
|
let withdrawStream;
|
|
83
156
|
const announced = new Map();
|
|
84
157
|
let underway = null;
|
|
158
|
+
let streamed = null;
|
|
85
159
|
let handed = false;
|
|
86
|
-
const
|
|
160
|
+
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
161
|
+
const repairing = new Set();
|
|
162
|
+
const calling = new Set();
|
|
163
|
+
const repairable = new Map();
|
|
164
|
+
let outcomeRetention = null;
|
|
87
165
|
const answering = new Map();
|
|
88
166
|
let complete = false;
|
|
89
167
|
let recorded = false;
|
|
90
168
|
const stale = new Set();
|
|
91
|
-
let
|
|
169
|
+
let generation = 0;
|
|
170
|
+
let discovering = null;
|
|
171
|
+
let opened = null;
|
|
172
|
+
const owed = new Set();
|
|
173
|
+
const reloadsDue = new Map();
|
|
174
|
+
const lanes = new Map();
|
|
175
|
+
let roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
176
|
+
const working = new Set();
|
|
92
177
|
let mutations = Promise.resolve();
|
|
93
178
|
const roomListeners = new Set();
|
|
94
179
|
const refusalListeners = new Set();
|
|
@@ -99,27 +184,50 @@ export function composeRuntime(options) {
|
|
|
99
184
|
const presentationListeners = new Set();
|
|
100
185
|
const readSide = createReadSide();
|
|
101
186
|
let withdrawConversations;
|
|
102
|
-
let currencyQueued =
|
|
187
|
+
let currencyQueued = null;
|
|
103
188
|
let socketOpen = false;
|
|
189
|
+
const confirmed = new Set();
|
|
190
|
+
let lockGrace = null;
|
|
191
|
+
const lockListeners = new Set();
|
|
104
192
|
let serviceMode = 'open';
|
|
105
193
|
let haltedRefusal = false;
|
|
106
194
|
let openSignals = 0;
|
|
107
195
|
const serviceModeListeners = new Set();
|
|
196
|
+
let member = null;
|
|
197
|
+
const memberListeners = new Set();
|
|
198
|
+
const prepared = new Set();
|
|
199
|
+
const projecting = new Set();
|
|
200
|
+
const unpreparedDemand = new Set();
|
|
201
|
+
const unindexable = new Set();
|
|
202
|
+
let preparing = false;
|
|
203
|
+
let targets = {};
|
|
204
|
+
let persistence = options.shell.persistence;
|
|
205
|
+
let capacity = null;
|
|
206
|
+
const paused = new Set();
|
|
207
|
+
let rechecking = null;
|
|
208
|
+
const readinessWatches = new Set();
|
|
209
|
+
let readinessQueued = false;
|
|
210
|
+
const catching = new Set();
|
|
211
|
+
const transfers = new FencedMap(isFenced);
|
|
108
212
|
const conversations = createConversationStore({
|
|
109
213
|
agents: declaredAgents,
|
|
110
|
-
documents: (locator) =>
|
|
214
|
+
documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
|
|
111
215
|
call: (name, input) => invokeAction(name, input),
|
|
112
216
|
durably,
|
|
113
217
|
stream: () => options.adapter.conversations,
|
|
114
218
|
publish: publishChangeSet,
|
|
219
|
+
online: () => connectionOf() !== 'unreachable' && refusal === null,
|
|
115
220
|
closed: () => closed,
|
|
221
|
+
unanswered: resendLater,
|
|
222
|
+
expired: (operation) => pastHorizon(operation),
|
|
116
223
|
});
|
|
117
224
|
const searchIndex = createSearchIndex({
|
|
118
|
-
|
|
225
|
+
room: (room) => readSide.room(room),
|
|
119
226
|
document: (room, path) => readSide.document(room, path),
|
|
120
|
-
messages: () => conversations.messages(),
|
|
121
|
-
message: (conversation, identity) => conversations.message(conversation, identity),
|
|
122
227
|
});
|
|
228
|
+
const roomsToIndex = new Set();
|
|
229
|
+
const conversationsToIndex = new Set();
|
|
230
|
+
let indexing = false;
|
|
123
231
|
const journal = createLog();
|
|
124
232
|
let notifyReady = () => { };
|
|
125
233
|
const ready = new Promise((resolve) => {
|
|
@@ -174,6 +282,15 @@ export function composeRuntime(options) {
|
|
|
174
282
|
const value = presentations.get(app);
|
|
175
283
|
return value === undefined ? null : structuredClone(value);
|
|
176
284
|
}
|
|
285
|
+
function keepPresentation(app, next) {
|
|
286
|
+
return exclusive(async () => {
|
|
287
|
+
const local = await openLocal();
|
|
288
|
+
const stored = next(presentations.get(app));
|
|
289
|
+
await local.present({ app, value: stored });
|
|
290
|
+
presentations.set(app, stored);
|
|
291
|
+
publishChangeSet(changeSet('', null, null, { presentation: app }));
|
|
292
|
+
});
|
|
293
|
+
}
|
|
177
294
|
function emitPresentation(app) {
|
|
178
295
|
if (closed)
|
|
179
296
|
return;
|
|
@@ -183,7 +300,9 @@ export function composeRuntime(options) {
|
|
|
183
300
|
}
|
|
184
301
|
}
|
|
185
302
|
function pending() {
|
|
186
|
-
return
|
|
303
|
+
return (!localSaves &&
|
|
304
|
+
(queuesWithoutReplica.size > 0 ||
|
|
305
|
+
[...replicas.values()].some((replica) => replica.queue.length > 0)));
|
|
187
306
|
}
|
|
188
307
|
function attention() {
|
|
189
308
|
const attentionRequired = [];
|
|
@@ -196,9 +315,16 @@ export function composeRuntime(options) {
|
|
|
196
315
|
returned: structuredClone(record.returned),
|
|
197
316
|
});
|
|
198
317
|
}
|
|
318
|
+
for (const [room, refusal] of sourceRefusals)
|
|
319
|
+
attentionRequired.push({ room, refusal: structuredClone(refusal) });
|
|
199
320
|
attentionRequired.sort((left, right) => {
|
|
200
321
|
if (left.room !== right.room)
|
|
201
322
|
return left.room < right.room ? -1 : 1;
|
|
323
|
+
if ('refusal' in left || 'refusal' in right) {
|
|
324
|
+
if ('refusal' in left && 'refusal' in right)
|
|
325
|
+
return 0;
|
|
326
|
+
return 'refusal' in left ? -1 : 1;
|
|
327
|
+
}
|
|
202
328
|
if (left.path !== right.path)
|
|
203
329
|
return left.path < right.path ? -1 : 1;
|
|
204
330
|
return 0;
|
|
@@ -212,8 +338,75 @@ export function composeRuntime(options) {
|
|
|
212
338
|
}
|
|
213
339
|
return { attentionRequired, reset };
|
|
214
340
|
}
|
|
341
|
+
function documentsIn(locator) {
|
|
342
|
+
const replica = replicas.get(locator);
|
|
343
|
+
return replica === undefined ? 0 : finished(counting(replica));
|
|
344
|
+
}
|
|
345
|
+
function* counting(replica) {
|
|
346
|
+
const weighed = outsideOf(replica.confirmed.record.typedSelection?.roots ?? []);
|
|
347
|
+
return ((yield* tallying(replica.visible.documents, weighed.documents)) +
|
|
348
|
+
(yield* tallying(replica.visible.binaries, weighed.binaries)));
|
|
349
|
+
}
|
|
215
350
|
function listed() {
|
|
216
|
-
return structuredClone([...rooms.values()])
|
|
351
|
+
return structuredClone([...rooms.values()].map((room) => {
|
|
352
|
+
const documents = documentsIn(room.locator);
|
|
353
|
+
counted.set(room.locator, documents);
|
|
354
|
+
return locked(room.locator)
|
|
355
|
+
? {
|
|
356
|
+
...room,
|
|
357
|
+
model: modelTypes(NO_MODEL),
|
|
358
|
+
typedSelection: null,
|
|
359
|
+
documents: 0,
|
|
360
|
+
locked: true,
|
|
361
|
+
}
|
|
362
|
+
: { ...room, documents, locked: false };
|
|
363
|
+
}));
|
|
364
|
+
}
|
|
365
|
+
function locked(locator) {
|
|
366
|
+
return rooms.get(locator)?.replica.offline === 'locked' && !confirmed.has(locator);
|
|
367
|
+
}
|
|
368
|
+
function lockedRooms() {
|
|
369
|
+
return [...rooms.keys()].filter(locked).sort();
|
|
370
|
+
}
|
|
371
|
+
function relocked(before) {
|
|
372
|
+
const after = lockedRooms();
|
|
373
|
+
if (JSON.stringify(after) === JSON.stringify(before))
|
|
374
|
+
return;
|
|
375
|
+
journal.record(`locked ${after.length}`);
|
|
376
|
+
emitRooms();
|
|
377
|
+
for (const notify of [...lockListeners])
|
|
378
|
+
notify();
|
|
379
|
+
}
|
|
380
|
+
function confirmLocks(listed) {
|
|
381
|
+
if (!socketOpen)
|
|
382
|
+
return;
|
|
383
|
+
for (const room of listed) {
|
|
384
|
+
if (room.replica.admission === 'admitted')
|
|
385
|
+
confirmed.add(room.locator);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function connectionLost() {
|
|
389
|
+
if (lockGrace !== null || closed)
|
|
390
|
+
return;
|
|
391
|
+
lockGrace = setTimeout(() => {
|
|
392
|
+
lockGrace = null;
|
|
393
|
+
if (socketOpen || closed)
|
|
394
|
+
return;
|
|
395
|
+
const before = lockedRooms();
|
|
396
|
+
confirmed.clear();
|
|
397
|
+
relocked(before);
|
|
398
|
+
}, LOCK_GRACE_MS);
|
|
399
|
+
}
|
|
400
|
+
function connectionLive() {
|
|
401
|
+
if (lockGrace !== null)
|
|
402
|
+
clearTimeout(lockGrace);
|
|
403
|
+
lockGrace = null;
|
|
404
|
+
}
|
|
405
|
+
function watchLocks(notify) {
|
|
406
|
+
lockListeners.add(notify);
|
|
407
|
+
return () => {
|
|
408
|
+
lockListeners.delete(notify);
|
|
409
|
+
};
|
|
217
410
|
}
|
|
218
411
|
function roomFacts(reach = null) {
|
|
219
412
|
const held = [...rooms.values()]
|
|
@@ -233,18 +426,26 @@ export function composeRuntime(options) {
|
|
|
233
426
|
return { listed: roomsListed, rooms: held };
|
|
234
427
|
}
|
|
235
428
|
function emitRooms() {
|
|
429
|
+
readSide.recover();
|
|
430
|
+
queueReadiness();
|
|
236
431
|
const value = listed();
|
|
237
432
|
for (const listener of roomListeners)
|
|
238
433
|
listener(value);
|
|
239
434
|
}
|
|
240
435
|
function emitRefusal() {
|
|
436
|
+
queueReadiness();
|
|
241
437
|
for (const listener of refusalListeners)
|
|
242
438
|
listener(refusal);
|
|
243
439
|
}
|
|
244
440
|
function emitStorageLoss() {
|
|
441
|
+
queueReadiness();
|
|
245
442
|
for (const listener of storageLossListeners)
|
|
246
443
|
listener(storageLoss);
|
|
247
444
|
}
|
|
445
|
+
function emitMember() {
|
|
446
|
+
for (const listener of memberListeners)
|
|
447
|
+
listener(member);
|
|
448
|
+
}
|
|
248
449
|
function moveTo(next) {
|
|
249
450
|
if (closed)
|
|
250
451
|
return;
|
|
@@ -254,6 +455,7 @@ export function composeRuntime(options) {
|
|
|
254
455
|
function show() {
|
|
255
456
|
if (closed)
|
|
256
457
|
return;
|
|
458
|
+
queueReadiness();
|
|
257
459
|
const next = status.state === 'synced' && !conversations.current()
|
|
258
460
|
? { ...status, state: 'syncing' }
|
|
259
461
|
: status;
|
|
@@ -263,18 +465,17 @@ export function composeRuntime(options) {
|
|
|
263
465
|
for (const listener of statusListeners)
|
|
264
466
|
listener(shown);
|
|
265
467
|
}
|
|
266
|
-
function entryOf(batch) {
|
|
267
|
-
|
|
468
|
+
function entryOf(locator, batch) {
|
|
469
|
+
if (localSaves)
|
|
470
|
+
return Object.freeze({ batch, state: 'local' });
|
|
471
|
+
const state = pendings.get(pendingKey(locator, batch.id));
|
|
268
472
|
if (state?.accepted !== undefined) {
|
|
269
473
|
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
270
474
|
}
|
|
271
475
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
272
476
|
}
|
|
273
477
|
function entriesOf(locator) {
|
|
274
|
-
|
|
275
|
-
if (replica === undefined)
|
|
276
|
-
return [];
|
|
277
|
-
return replica.queue.map(entryOf);
|
|
478
|
+
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map((batch) => entryOf(locator, batch));
|
|
278
479
|
}
|
|
279
480
|
function emitQueue(locator) {
|
|
280
481
|
const entries = entriesOf(locator);
|
|
@@ -284,12 +485,467 @@ export function composeRuntime(options) {
|
|
|
284
485
|
}
|
|
285
486
|
}
|
|
286
487
|
function applyChangeSet(set) {
|
|
287
|
-
|
|
288
|
-
|
|
488
|
+
const held = {
|
|
489
|
+
...set,
|
|
490
|
+
documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
|
|
491
|
+
};
|
|
492
|
+
const applied = readSide.apply(held, () => searchIndex.changed(held));
|
|
493
|
+
if (applied instanceof Promise)
|
|
494
|
+
void applied.catch(unpublished);
|
|
495
|
+
}
|
|
496
|
+
function unpublished(error) {
|
|
497
|
+
journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
|
|
498
|
+
}
|
|
499
|
+
async function referenced(locator) {
|
|
500
|
+
for (;;) {
|
|
501
|
+
const record = replicas.get(locator)?.confirmed.record;
|
|
502
|
+
if (record === undefined || closed)
|
|
503
|
+
return;
|
|
504
|
+
const held = { text: record.documents, binaries: record.binaries };
|
|
505
|
+
const composition = compositionOf(record);
|
|
506
|
+
if (referencesIndexed(held, composition))
|
|
507
|
+
return;
|
|
508
|
+
await indexReferrers(held, composition);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
async function prepareReplica(locator) {
|
|
512
|
+
await referenced(locator);
|
|
513
|
+
const replica = replicas.get(locator);
|
|
514
|
+
if (replica === undefined || closed || prepared.has(locator))
|
|
515
|
+
return;
|
|
516
|
+
const visible = replica.visible;
|
|
517
|
+
const entered = [];
|
|
518
|
+
projecting.add(locator);
|
|
519
|
+
try {
|
|
520
|
+
await inChunks(await stepped(keysIn(visible.documents, visible.binaries)), (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
|
|
521
|
+
const current = replicas.get(locator);
|
|
522
|
+
if (current === undefined || closed)
|
|
523
|
+
return;
|
|
524
|
+
const set = entering(locator, current, chunk);
|
|
525
|
+
entered.push(set);
|
|
526
|
+
return readSide.apply(set);
|
|
527
|
+
});
|
|
528
|
+
if (!replicas.has(locator) || closed)
|
|
529
|
+
return;
|
|
530
|
+
prepared.add(locator);
|
|
531
|
+
}
|
|
532
|
+
finally {
|
|
533
|
+
projecting.delete(locator);
|
|
534
|
+
}
|
|
535
|
+
conversations.changed(mergeChangeSets(entered));
|
|
536
|
+
readSide.recover();
|
|
537
|
+
queueReadiness();
|
|
538
|
+
}
|
|
539
|
+
function unprepared() {
|
|
540
|
+
return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
|
|
541
|
+
}
|
|
542
|
+
function unbuilt(locator, error) {
|
|
543
|
+
unindexable.add(locator);
|
|
544
|
+
journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
|
|
545
|
+
queueReadiness();
|
|
546
|
+
}
|
|
547
|
+
function prepare(locators = []) {
|
|
548
|
+
for (const locator of locators)
|
|
549
|
+
unpreparedDemand.add(locator);
|
|
550
|
+
if (preparing)
|
|
551
|
+
return;
|
|
552
|
+
preparing = true;
|
|
553
|
+
void (async () => {
|
|
554
|
+
try {
|
|
555
|
+
for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
|
|
556
|
+
await yielded();
|
|
557
|
+
if (closed)
|
|
558
|
+
return;
|
|
559
|
+
unpreparedDemand.delete(waiting);
|
|
560
|
+
await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
finally {
|
|
564
|
+
preparing = false;
|
|
565
|
+
}
|
|
566
|
+
})();
|
|
567
|
+
}
|
|
568
|
+
function roomsOfReach(reach) {
|
|
569
|
+
return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
570
|
+
}
|
|
571
|
+
function reachedRooms(plan) {
|
|
572
|
+
const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
|
|
573
|
+
const named = plan.rooms ??
|
|
574
|
+
(reached === null
|
|
575
|
+
? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
|
|
576
|
+
: [...reached]);
|
|
577
|
+
return [...new Set(named)]
|
|
578
|
+
.filter((locator) => (reached === null || reached.has(locator)) && !plan.locked.includes(locator))
|
|
579
|
+
.sort();
|
|
580
|
+
}
|
|
581
|
+
function awaited(locator) {
|
|
582
|
+
return (!roomsListed ||
|
|
583
|
+
(rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
|
|
584
|
+
}
|
|
585
|
+
function covered(plan) {
|
|
586
|
+
const locators = reachedRooms(plan);
|
|
587
|
+
if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
|
|
588
|
+
return { pending: [] };
|
|
589
|
+
}
|
|
590
|
+
const pending = [];
|
|
591
|
+
const coverage = [];
|
|
592
|
+
const overlay = [];
|
|
593
|
+
for (const locator of locators) {
|
|
594
|
+
const replica = replicas.get(locator);
|
|
595
|
+
if (replica === undefined) {
|
|
596
|
+
if (awaited(locator))
|
|
597
|
+
pending.push({ room: locator, awaits: 'acquisition' });
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
if (!prepared.has(locator)) {
|
|
601
|
+
pending.push({ room: locator, awaits: 'preparation' });
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
coverage.push({
|
|
605
|
+
room: locator,
|
|
606
|
+
generation: replica.confirmed.record.transfer,
|
|
607
|
+
current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
|
|
608
|
+
});
|
|
609
|
+
for (const batch of replica.queue)
|
|
610
|
+
overlay.push(batch.id);
|
|
611
|
+
}
|
|
612
|
+
if (pending.length > 0)
|
|
613
|
+
return { pending };
|
|
614
|
+
return { coverage: { rooms: coverage, indexes: [], overlay } };
|
|
615
|
+
}
|
|
616
|
+
function awaitPreparation(cover) {
|
|
617
|
+
if (!('pending' in cover))
|
|
618
|
+
return;
|
|
619
|
+
const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
|
|
620
|
+
if (waiting.length > 0)
|
|
621
|
+
prepare(waiting.map(({ room }) => room));
|
|
622
|
+
}
|
|
623
|
+
function requiredRooms(given) {
|
|
624
|
+
const reach = planReach(given);
|
|
625
|
+
if (reach === null)
|
|
626
|
+
return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
|
|
627
|
+
const named = [...roomsOfReach(reach)].sort();
|
|
628
|
+
if (roomsListed)
|
|
629
|
+
return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
|
|
630
|
+
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
631
|
+
}
|
|
632
|
+
function roomState(locator, search) {
|
|
633
|
+
if (sourceRefusals.has(locator))
|
|
634
|
+
return 'refused';
|
|
635
|
+
const listedRoom = discovered.get(locator);
|
|
636
|
+
if (roomsListed &&
|
|
637
|
+
(listedRoom === undefined ||
|
|
638
|
+
listedRoom.replica.admission !== 'admitted' ||
|
|
639
|
+
unreadable.has(locator)))
|
|
640
|
+
return 'unadmitted';
|
|
641
|
+
if (!replicas.has(locator))
|
|
642
|
+
return 'acquiring';
|
|
643
|
+
if (!prepared.has(locator))
|
|
644
|
+
return 'preparing';
|
|
645
|
+
if (search && !searchIndex.rooms().has(locator))
|
|
646
|
+
return 'indexing';
|
|
647
|
+
return 'complete';
|
|
648
|
+
}
|
|
649
|
+
function conversationCoverage(reach, search) {
|
|
650
|
+
const declared = reach === null || reach.conversations;
|
|
651
|
+
if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
|
|
652
|
+
return 'undeclared';
|
|
653
|
+
if (!conversations.known() || !conversations.current())
|
|
654
|
+
return 'acquiring';
|
|
655
|
+
if (!search)
|
|
656
|
+
return 'complete';
|
|
657
|
+
const unindexed = conversations
|
|
658
|
+
.searchable()
|
|
659
|
+
.filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
|
|
660
|
+
if (unindexed.length === 0)
|
|
661
|
+
return 'complete';
|
|
662
|
+
index([], unindexed);
|
|
663
|
+
return 'indexing';
|
|
664
|
+
}
|
|
665
|
+
function connectionOf() {
|
|
666
|
+
if (socketOpen)
|
|
667
|
+
return 'connected';
|
|
668
|
+
if (status.providerUnreachable)
|
|
669
|
+
return 'unreachable';
|
|
670
|
+
return options.adapter.roomStream === undefined && status.lastCompleted !== null
|
|
671
|
+
? 'connected'
|
|
672
|
+
: 'connecting';
|
|
673
|
+
}
|
|
674
|
+
function refusedIn(locators) {
|
|
675
|
+
return locators.flatMap((room) => {
|
|
676
|
+
const held = sourceRefusals.get(room);
|
|
677
|
+
if (held === undefined)
|
|
678
|
+
return [];
|
|
679
|
+
const { file, location } = held.fields;
|
|
680
|
+
return [
|
|
681
|
+
{
|
|
682
|
+
room,
|
|
683
|
+
file: typeof file === 'string' ? file : '',
|
|
684
|
+
rule: held.code,
|
|
685
|
+
location: typeof location === 'string' ? location : null,
|
|
686
|
+
},
|
|
687
|
+
];
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
function readinessFor(port, reach) {
|
|
691
|
+
const required = requiredRooms(reach);
|
|
692
|
+
const search = port.search === 'required';
|
|
693
|
+
const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
|
|
694
|
+
const building = (state) => coverage
|
|
695
|
+
.filter((room) => room.state === state && !unindexable.has(room.room))
|
|
696
|
+
.map(({ room }) => room);
|
|
697
|
+
const preparing = building('preparing');
|
|
698
|
+
if (preparing.length > 0)
|
|
699
|
+
prepare(preparing);
|
|
700
|
+
const indexing = building('indexing');
|
|
701
|
+
if (indexing.length > 0)
|
|
702
|
+
index(indexing, []);
|
|
703
|
+
return readinessOf({
|
|
704
|
+
code: port.code,
|
|
705
|
+
coverage: {
|
|
706
|
+
listed: required.listed,
|
|
707
|
+
rooms: coverage,
|
|
708
|
+
conversations: conversationCoverage(reach, search),
|
|
709
|
+
search: search ? 'required' : 'unused',
|
|
710
|
+
},
|
|
711
|
+
persistence,
|
|
712
|
+
connection: connectionOf(),
|
|
713
|
+
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
714
|
+
capacity: capacity !== null &&
|
|
715
|
+
coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
|
|
716
|
+
? capacity
|
|
717
|
+
: port.capacity,
|
|
718
|
+
refused: refusedIn(required.rooms),
|
|
719
|
+
lost: storageLoss !== null,
|
|
720
|
+
established: targets[port.target]?.established === true,
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
function establish(name) {
|
|
724
|
+
targets = { ...targets, [name]: { established: true } };
|
|
725
|
+
queueReadiness();
|
|
726
|
+
void durably((local) => local.keepTargets(targets)).catch(() => {
|
|
727
|
+
journal.record(`target record failed ${name}`);
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
function deliverReadiness(watch) {
|
|
731
|
+
if (!readinessWatches.has(watch))
|
|
732
|
+
return;
|
|
733
|
+
const readiness = readinessFor(watch.port, watch.reach);
|
|
734
|
+
if (readiness.ready && targets[watch.port.target]?.established !== true) {
|
|
735
|
+
establish(watch.port.target);
|
|
736
|
+
}
|
|
737
|
+
const shown = JSON.stringify(readiness);
|
|
738
|
+
if (shown === watch.last)
|
|
739
|
+
return;
|
|
740
|
+
watch.last = shown;
|
|
741
|
+
watch.deliver(structuredClone(readiness));
|
|
742
|
+
}
|
|
743
|
+
function queueReadiness() {
|
|
744
|
+
if (readinessQueued || closed || readinessWatches.size === 0)
|
|
745
|
+
return;
|
|
746
|
+
readinessQueued = true;
|
|
747
|
+
queueMicrotask(() => {
|
|
748
|
+
readinessQueued = false;
|
|
749
|
+
if (closed)
|
|
750
|
+
return;
|
|
751
|
+
for (const watch of [...readinessWatches])
|
|
752
|
+
deliverReadiness(watch);
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
function watchReadiness(port, reach, deliver) {
|
|
756
|
+
const watch = { port, reach, deliver, last: null };
|
|
757
|
+
readinessWatches.add(watch);
|
|
758
|
+
void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
|
|
759
|
+
return () => {
|
|
760
|
+
readinessWatches.delete(watch);
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
function layerBytes(layer) {
|
|
764
|
+
let bytes = 0;
|
|
765
|
+
for (const [path, text] of Object.entries(layer.documents))
|
|
766
|
+
bytes += (path.length + text.length) * 2;
|
|
767
|
+
return bytes;
|
|
768
|
+
}
|
|
769
|
+
function grownBy(next, previous) {
|
|
770
|
+
if (previous === undefined)
|
|
771
|
+
return layerBytes(next);
|
|
772
|
+
const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
|
|
773
|
+
return layerBytes({ ...next, documents });
|
|
774
|
+
}
|
|
775
|
+
function exceeded(error) {
|
|
776
|
+
return (typeof error === 'object' &&
|
|
777
|
+
error !== null &&
|
|
778
|
+
error.name === 'QuotaExceededError');
|
|
779
|
+
}
|
|
780
|
+
async function estimated() {
|
|
781
|
+
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
782
|
+
return estimate === null ? null : Math.max(0, estimate.total - estimate.usage);
|
|
783
|
+
}
|
|
784
|
+
function pause(locator, cause) {
|
|
785
|
+
paused.add(locator);
|
|
786
|
+
capacity = cause;
|
|
787
|
+
journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
|
|
788
|
+
queueReadiness();
|
|
789
|
+
if (rechecking !== null)
|
|
790
|
+
return;
|
|
791
|
+
// A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
|
|
792
|
+
rechecking = setInterval(() => {
|
|
793
|
+
void recheck();
|
|
794
|
+
}, 60_000);
|
|
795
|
+
}
|
|
796
|
+
async function recheck() {
|
|
797
|
+
const held = capacity;
|
|
798
|
+
if (held === null || closed)
|
|
799
|
+
return;
|
|
800
|
+
const available = await estimated().catch(() => null);
|
|
801
|
+
if (available === null || available < (held.needed ?? 0))
|
|
802
|
+
return;
|
|
803
|
+
if (held.cause === 'storage-exceeded' &&
|
|
804
|
+
(held.available === null || available <= held.available))
|
|
805
|
+
return;
|
|
806
|
+
resume();
|
|
807
|
+
}
|
|
808
|
+
function resume() {
|
|
809
|
+
if (rechecking !== null)
|
|
810
|
+
clearInterval(rechecking);
|
|
811
|
+
rechecking = null;
|
|
812
|
+
if (paused.size === 0 && capacity === null)
|
|
813
|
+
return;
|
|
814
|
+
paused.clear();
|
|
815
|
+
capacity = null;
|
|
816
|
+
journal.record('capacity resumed');
|
|
817
|
+
queueReadiness();
|
|
818
|
+
if (!closed && authorization !== undefined)
|
|
819
|
+
void sync('unstreamed');
|
|
820
|
+
}
|
|
821
|
+
async function reclaimed(local) {
|
|
822
|
+
const unreferenced = [...transfers.keys()].filter((locator) => !discovered.has(locator));
|
|
823
|
+
for (const locator of unreferenced) {
|
|
824
|
+
await local.abandon(locator);
|
|
825
|
+
transfers.delete(locator);
|
|
826
|
+
}
|
|
827
|
+
if (unreferenced.length > 0)
|
|
828
|
+
journal.record(`capacity reclaimed ${unreferenced.length}`);
|
|
829
|
+
return unreferenced.length > 0;
|
|
830
|
+
}
|
|
831
|
+
async function admitted(local, locator, needed) {
|
|
832
|
+
if (paused.has(locator))
|
|
833
|
+
return false;
|
|
834
|
+
let available = await estimated().catch(() => null);
|
|
835
|
+
if (available === null || available >= needed)
|
|
836
|
+
return true;
|
|
837
|
+
if (await reclaimed(local))
|
|
838
|
+
available = await estimated().catch(() => null);
|
|
839
|
+
if (available === null || available >= needed)
|
|
840
|
+
return true;
|
|
841
|
+
pause(locator, { cause: 'insufficient-space', needed, available });
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
async function exceededOn(locator, bytes) {
|
|
845
|
+
const available = await estimated().catch(() => null);
|
|
846
|
+
pause(locator, { cause: 'storage-exceeded', needed: bytes, available });
|
|
847
|
+
}
|
|
848
|
+
function seedRoomHeld(room, version) {
|
|
849
|
+
return prepared.has(room) && reachedBy(room, version);
|
|
850
|
+
}
|
|
851
|
+
function seedHeld(at) {
|
|
852
|
+
return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
|
|
853
|
+
}
|
|
854
|
+
function seedApplies(plan) {
|
|
855
|
+
const seed = seeds.get(plan.key);
|
|
856
|
+
if (seed === undefined)
|
|
857
|
+
return null;
|
|
858
|
+
if (!seedHeld(seed.at))
|
|
859
|
+
return seed;
|
|
860
|
+
seeds.delete(plan.key);
|
|
861
|
+
return null;
|
|
862
|
+
}
|
|
863
|
+
function seedBehind(plan, cover) {
|
|
864
|
+
const seed = seedApplies(plan);
|
|
865
|
+
if (seed === null || 'pending' in cover)
|
|
866
|
+
return cover;
|
|
867
|
+
return {
|
|
868
|
+
pending: Object.entries(seed.at)
|
|
869
|
+
.filter(([room, version]) => !seedRoomHeld(room, version))
|
|
870
|
+
.map(([room]) => ({ room, awaits: 'acquisition' })),
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
function seed(entry) {
|
|
874
|
+
if (seedHeld(entry.at))
|
|
875
|
+
return;
|
|
876
|
+
for (const { query, rows } of entry.queries) {
|
|
877
|
+
const key = queryKey(query);
|
|
878
|
+
seeds.set(key, { rows, at: entry.at });
|
|
879
|
+
for (const shown of seedWatchers.get(key) ?? [])
|
|
880
|
+
shown();
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
async function rowsQuery(plan) {
|
|
884
|
+
const before = covered(plan);
|
|
885
|
+
awaitPreparation(before);
|
|
886
|
+
const seeded = seedApplies(plan);
|
|
887
|
+
if (seeded !== null)
|
|
888
|
+
return seededResult(seeded).result;
|
|
889
|
+
if ('pending' in before)
|
|
890
|
+
return before;
|
|
891
|
+
const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
|
|
892
|
+
awaitPreparation(cover);
|
|
893
|
+
if ('pending' in cover)
|
|
894
|
+
return cover;
|
|
895
|
+
return { ...rows, coverage: cover.coverage };
|
|
896
|
+
}
|
|
897
|
+
async function indexConversation(conversation) {
|
|
898
|
+
searchIndex.indexMessages(conversation, [], false);
|
|
899
|
+
let after = null;
|
|
900
|
+
for (;;) {
|
|
901
|
+
const batch = await durably((local) => local.messagesAfter(conversation, after));
|
|
902
|
+
if (closed)
|
|
903
|
+
return;
|
|
904
|
+
searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
|
|
905
|
+
if (batch.length === 0)
|
|
906
|
+
return;
|
|
907
|
+
after = batch[batch.length - 1].identity;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
function indexNext() {
|
|
911
|
+
const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
|
|
912
|
+
if (room !== undefined) {
|
|
913
|
+
roomsToIndex.delete(room);
|
|
914
|
+
return () => prepared.has(room)
|
|
915
|
+
? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
|
|
916
|
+
: undefined;
|
|
917
|
+
}
|
|
918
|
+
const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
|
|
919
|
+
if (conversation === undefined)
|
|
920
|
+
return null;
|
|
921
|
+
conversationsToIndex.delete(conversation);
|
|
922
|
+
return () => indexConversation(conversation);
|
|
923
|
+
}
|
|
924
|
+
function index(rooms, named) {
|
|
925
|
+
for (const room of rooms)
|
|
926
|
+
roomsToIndex.add(room);
|
|
927
|
+
for (const conversation of named)
|
|
928
|
+
conversationsToIndex.add(conversation);
|
|
929
|
+
if (indexing)
|
|
930
|
+
return;
|
|
931
|
+
indexing = true;
|
|
932
|
+
void (async () => {
|
|
933
|
+
try {
|
|
934
|
+
for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
|
|
935
|
+
await yielded();
|
|
936
|
+
await unit();
|
|
937
|
+
queueReadiness();
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
finally {
|
|
941
|
+
indexing = false;
|
|
942
|
+
}
|
|
943
|
+
})().catch(() => { });
|
|
289
944
|
}
|
|
290
945
|
function publishChangeSet(set) {
|
|
291
946
|
applyChangeSet(set);
|
|
292
|
-
if (set.rooms.length > 0
|
|
947
|
+
if (set.rooms.length > 0 ||
|
|
948
|
+
[...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
|
|
293
949
|
emitRooms();
|
|
294
950
|
for (const locator of set.queues)
|
|
295
951
|
emitQueue(locator);
|
|
@@ -301,34 +957,7 @@ export function composeRuntime(options) {
|
|
|
301
957
|
emitPresentation(app);
|
|
302
958
|
conversations.changed(set);
|
|
303
959
|
}
|
|
304
|
-
function stopTimer() {
|
|
305
|
-
if (resetTimer === undefined)
|
|
306
|
-
return;
|
|
307
|
-
clearTimeout(resetTimer);
|
|
308
|
-
resetTimer = undefined;
|
|
309
|
-
}
|
|
310
|
-
function armTimer() {
|
|
311
|
-
stopTimer();
|
|
312
|
-
if (closed || stopping)
|
|
313
|
-
return;
|
|
314
|
-
let earliest = null;
|
|
315
|
-
for (const { submission } of pendings.values()) {
|
|
316
|
-
if (submission.state !== 'reset')
|
|
317
|
-
continue;
|
|
318
|
-
const at = Date.parse(submission.reset);
|
|
319
|
-
if (earliest === null || at < earliest)
|
|
320
|
-
earliest = at;
|
|
321
|
-
}
|
|
322
|
-
if (earliest === null)
|
|
323
|
-
return;
|
|
324
|
-
// A Save House held back goes again only once the reset its answer named has passed.
|
|
325
|
-
resetTimer = setTimeout(() => {
|
|
326
|
-
resetTimer = undefined;
|
|
327
|
-
submitAll();
|
|
328
|
-
}, Math.max(0, earliest - Date.now()));
|
|
329
|
-
}
|
|
330
960
|
async function refuse(reason, provider = null) {
|
|
331
|
-
stopTimer();
|
|
332
961
|
discoveryPaused = true;
|
|
333
962
|
try {
|
|
334
963
|
if (reason === 'revoked-authorization') {
|
|
@@ -336,10 +965,7 @@ export function composeRuntime(options) {
|
|
|
336
965
|
const revoked = { status: 'unavailable', reason: 'revoked' };
|
|
337
966
|
try {
|
|
338
967
|
const local = await openLocal();
|
|
339
|
-
await local.revokeModels([...replicas.values()].map((replica) =>
|
|
340
|
-
record: replica.confirmed.record,
|
|
341
|
-
queue: replica.queue,
|
|
342
|
-
})));
|
|
968
|
+
await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
|
|
343
969
|
}
|
|
344
970
|
catch {
|
|
345
971
|
journal.record('model revocation persistence failed');
|
|
@@ -354,20 +980,22 @@ export function composeRuntime(options) {
|
|
|
354
980
|
models.set(key, revoked);
|
|
355
981
|
liveModels.delete(key);
|
|
356
982
|
}
|
|
357
|
-
const
|
|
358
|
-
for (const
|
|
983
|
+
const revocations = [];
|
|
984
|
+
for (const locator of [...rooms.keys()]) {
|
|
359
985
|
const before = replicas.get(locator) ?? null;
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
986
|
+
const after = before === null
|
|
987
|
+
? null
|
|
988
|
+
: await stepped(replicating(await confirmedOf({ ...before.confirmed.record, model: revoked, selectionModel: revoked }, before.confirmed), before.queue, before));
|
|
989
|
+
const set = await stepped(changing(locator, before, after, { room: true }));
|
|
990
|
+
revocations.push([locator, before, after, set]);
|
|
991
|
+
}
|
|
992
|
+
const changed = [];
|
|
993
|
+
for (const [locator, before, after, set] of revocations) {
|
|
994
|
+
const room = rooms.get(locator);
|
|
995
|
+
if (room === undefined || (replicas.get(locator) ?? null) !== before)
|
|
996
|
+
continue;
|
|
997
|
+
if (after !== null)
|
|
998
|
+
replicas.set(locator, after);
|
|
371
999
|
rooms.set(locator, {
|
|
372
1000
|
...room,
|
|
373
1001
|
model: modelTypes(revoked),
|
|
@@ -375,7 +1003,7 @@ export function composeRuntime(options) {
|
|
|
375
1003
|
? null
|
|
376
1004
|
: { ...room.typedSelection, model: modelTypes(revoked) },
|
|
377
1005
|
});
|
|
378
|
-
changed.push(
|
|
1006
|
+
changed.push(set);
|
|
379
1007
|
}
|
|
380
1008
|
publishChangeSet(mergeChangeSets(changed));
|
|
381
1009
|
});
|
|
@@ -383,7 +1011,8 @@ export function composeRuntime(options) {
|
|
|
383
1011
|
}
|
|
384
1012
|
finally {
|
|
385
1013
|
refusal = {
|
|
386
|
-
reason,
|
|
1014
|
+
code: reason,
|
|
1015
|
+
retryable: false,
|
|
387
1016
|
reauthorizationOffered: reason === 'revoked-authorization',
|
|
388
1017
|
refusal: provider,
|
|
389
1018
|
};
|
|
@@ -408,19 +1037,36 @@ export function composeRuntime(options) {
|
|
|
408
1037
|
},
|
|
409
1038
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
410
1039
|
roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
|
|
411
|
-
attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
|
|
412
1040
|
synchronizationAvailable,
|
|
413
1041
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
414
1042
|
});
|
|
415
1043
|
}
|
|
416
|
-
async function applyDiscovery(
|
|
1044
|
+
async function applyDiscovery(found, started, covered) {
|
|
1045
|
+
const lockedBefore = lockedRooms();
|
|
1046
|
+
for (const room of found) {
|
|
1047
|
+
const fence = fences.get(room.locator);
|
|
1048
|
+
if (fence !== undefined && fence.erased && fence.at < started)
|
|
1049
|
+
fences.delete(room.locator);
|
|
1050
|
+
}
|
|
1051
|
+
await loseRooms(found
|
|
1052
|
+
.filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
|
|
1053
|
+
.map((room) => room.locator));
|
|
1054
|
+
const listed = found.filter((room) => !fences.has(room.locator));
|
|
417
1055
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
418
1056
|
for (const locator of listedLocators)
|
|
419
1057
|
scopedRefusals.delete(locator);
|
|
420
|
-
|
|
1058
|
+
if (covered === undefined)
|
|
1059
|
+
discovered.clear();
|
|
1060
|
+
else
|
|
1061
|
+
for (const locator of covered)
|
|
1062
|
+
discovered.delete(locator);
|
|
421
1063
|
for (const room of listed)
|
|
422
1064
|
discovered.set(room.locator, room);
|
|
423
|
-
|
|
1065
|
+
const unlisted = unlistedHeld(new Set(found.map((room) => room.locator))).filter((locator) => covered === undefined || covered.includes(locator));
|
|
1066
|
+
if (options.adapter.exhaustiveDiscovery === true)
|
|
1067
|
+
await loseRooms(unlisted);
|
|
1068
|
+
else
|
|
1069
|
+
unlistedKept(unlisted);
|
|
424
1070
|
for (const room of listed) {
|
|
425
1071
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
426
1072
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -429,44 +1075,107 @@ export function composeRuntime(options) {
|
|
|
429
1075
|
}
|
|
430
1076
|
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
431
1077
|
}
|
|
1078
|
+
confirmLocks(listed);
|
|
432
1079
|
roomsListed = true;
|
|
433
1080
|
emitRooms();
|
|
1081
|
+
relocked(lockedBefore);
|
|
1082
|
+
return listed;
|
|
1083
|
+
}
|
|
1084
|
+
function heldLocally(locator) {
|
|
1085
|
+
return (replicas.has(locator) ||
|
|
1086
|
+
roomDrafts.has(locator) ||
|
|
1087
|
+
transfers.has(locator) ||
|
|
1088
|
+
queuesWithoutReplica.has(locator));
|
|
434
1089
|
}
|
|
435
1090
|
function unlistedHeld(listed) {
|
|
436
|
-
return [
|
|
1091
|
+
return [
|
|
1092
|
+
...new Set([
|
|
1093
|
+
...rooms.keys(),
|
|
1094
|
+
...roomDrafts.keys(),
|
|
1095
|
+
...transfers.keys(),
|
|
1096
|
+
...queuesWithoutReplica.keys(),
|
|
1097
|
+
]),
|
|
1098
|
+
].filter((locator) => !listed.has(locator));
|
|
437
1099
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
|
|
445
|
-
room: true,
|
|
446
|
-
queue: true,
|
|
447
|
-
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
448
|
-
}));
|
|
449
|
-
for (const locator of unlisted) {
|
|
450
|
-
for (const batch of replicas.get(locator)?.queue ?? [])
|
|
451
|
-
pendings.delete(batch.id);
|
|
452
|
-
replicas.delete(locator);
|
|
1100
|
+
function unlistedKept(unlisted) {
|
|
1101
|
+
for (const locator of unlisted) {
|
|
1102
|
+
const held = replicas.get(locator)?.confirmed.record;
|
|
1103
|
+
if (held !== undefined)
|
|
1104
|
+
list(held, false);
|
|
1105
|
+
else
|
|
453
1106
|
rooms.delete(locator);
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
function fence(locators) {
|
|
1110
|
+
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
1111
|
+
const removed = fresh.map((locator) => ({
|
|
1112
|
+
locator,
|
|
1113
|
+
shown: prepared.has(locator) || projecting.has(locator) ? (replicas.get(locator) ?? null) : null,
|
|
1114
|
+
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
1115
|
+
}));
|
|
1116
|
+
for (const locator of fresh) {
|
|
1117
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
1118
|
+
lastFenced.set(locator, ++fencings);
|
|
1119
|
+
confirmed.delete(locator);
|
|
1120
|
+
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
1121
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
1122
|
+
replicas.delete(locator);
|
|
1123
|
+
queuesWithoutReplica.delete(locator);
|
|
1124
|
+
rooms.delete(locator);
|
|
1125
|
+
roomDrafts.delete(locator);
|
|
1126
|
+
transfers.delete(locator);
|
|
1127
|
+
catching.delete(locator);
|
|
1128
|
+
discovered.delete(locator);
|
|
1129
|
+
announced.delete(locator);
|
|
1130
|
+
unreadable.delete(locator);
|
|
1131
|
+
stale.delete(locator);
|
|
1132
|
+
repairable.delete(locator);
|
|
1133
|
+
unindexable.delete(locator);
|
|
1134
|
+
paused.delete(locator);
|
|
1135
|
+
prepared.delete(locator);
|
|
1136
|
+
scopedRefusals.delete(locator);
|
|
1137
|
+
sourceRefusals.delete(locator);
|
|
1138
|
+
declarationRefusals.delete(locator);
|
|
1139
|
+
for (const attached of ['collection', 'typed-selection']) {
|
|
1140
|
+
const key = modelKey(locator, attached);
|
|
1141
|
+
models.delete(key);
|
|
1142
|
+
liveModels.delete(key);
|
|
1143
|
+
invalidModels.delete(key);
|
|
467
1144
|
}
|
|
468
|
-
|
|
469
|
-
|
|
1145
|
+
journal.record(`room lost ${locator}`);
|
|
1146
|
+
}
|
|
1147
|
+
if (fresh.length === 0)
|
|
1148
|
+
return fresh;
|
|
1149
|
+
searchIndex.forget(fresh);
|
|
1150
|
+
const withdrawn = readSide.apply(stepped(withdrawing(removed)), (set) => conversations.changed(set));
|
|
1151
|
+
if (withdrawn instanceof Promise)
|
|
1152
|
+
void withdrawn.catch(unpublished);
|
|
1153
|
+
publishChangeSet(mergeChangeSets(removed.map(({ locator, drafts }) => changeSet(locator, null, null, { room: true, queue: true, drafts }))));
|
|
1154
|
+
readSide.recover();
|
|
1155
|
+
moveTo({ ...status, ...attention() });
|
|
1156
|
+
return fresh;
|
|
1157
|
+
}
|
|
1158
|
+
function* withdrawing(removed) {
|
|
1159
|
+
const sets = [];
|
|
1160
|
+
for (const { locator, shown } of removed)
|
|
1161
|
+
sets.push(yield* changing(locator, shown, null));
|
|
1162
|
+
return mergeChangeSets(sets);
|
|
1163
|
+
}
|
|
1164
|
+
async function erased(local, locators) {
|
|
1165
|
+
await local.erase(locators);
|
|
1166
|
+
for (const locator of locators) {
|
|
1167
|
+
const held = fences.get(locator);
|
|
1168
|
+
if (held !== undefined)
|
|
1169
|
+
held.erased = true;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
async function loseRooms(locators) {
|
|
1173
|
+
const fresh = fence(locators);
|
|
1174
|
+
if (fresh.length === 0)
|
|
1175
|
+
return;
|
|
1176
|
+
await durably(async (local) => {
|
|
1177
|
+
await erased(local, fresh);
|
|
1178
|
+
await unpair(local);
|
|
470
1179
|
});
|
|
471
1180
|
}
|
|
472
1181
|
function restored(key, persisted) {
|
|
@@ -488,7 +1197,7 @@ export function composeRuntime(options) {
|
|
|
488
1197
|
return model;
|
|
489
1198
|
}
|
|
490
1199
|
async function hydrate(opened) {
|
|
491
|
-
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
|
|
1200
|
+
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
|
|
492
1201
|
opened.all(),
|
|
493
1202
|
opened.queues(),
|
|
494
1203
|
opened.roomDrafts(),
|
|
@@ -498,9 +1207,33 @@ export function composeRuntime(options) {
|
|
|
498
1207
|
opened.lastCompleted(),
|
|
499
1208
|
opened.providerLimits(),
|
|
500
1209
|
opened.capabilities(),
|
|
1210
|
+
opened.member(),
|
|
1211
|
+
opened.transfers(),
|
|
1212
|
+
opened.targets(),
|
|
501
1213
|
]);
|
|
1214
|
+
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1215
|
+
const previousReplicas = new Map(replicas);
|
|
1216
|
+
const read = new Map();
|
|
1217
|
+
for (const record of records) {
|
|
1218
|
+
const model = restored(modelKey(record.locator, 'collection'), record.model);
|
|
1219
|
+
const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
|
|
1220
|
+
const prior = previousReplicas.get(record.locator);
|
|
1221
|
+
const confirmed = await confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
|
|
1222
|
+
const replica = await stepped(replicating(confirmed, queuedBatches.get(record.locator) ?? [], prior));
|
|
1223
|
+
await stepped(counting(replica));
|
|
1224
|
+
read.set(record.locator, replica);
|
|
1225
|
+
}
|
|
1226
|
+
const sets = new Map();
|
|
1227
|
+
for (const locator of new Set([...previousReplicas.keys(), ...read.keys()])) {
|
|
1228
|
+
sets.set(locator, await stepped(changing(locator, previousReplicas.get(locator) ?? null, read.get(locator) ?? null)));
|
|
1229
|
+
}
|
|
1230
|
+
targets = heldTargets;
|
|
1231
|
+
transfers.clear();
|
|
1232
|
+
for (const record of staged)
|
|
1233
|
+
transfers.set(record.locator, record);
|
|
502
1234
|
limits = declared;
|
|
503
1235
|
capabilities = named;
|
|
1236
|
+
member = known;
|
|
504
1237
|
roomDrafts.clear();
|
|
505
1238
|
for (const record of kept)
|
|
506
1239
|
rememberRoomDraft(record);
|
|
@@ -508,27 +1241,24 @@ export function composeRuntime(options) {
|
|
|
508
1241
|
for (const record of presented)
|
|
509
1242
|
presentations.set(record.app, record.value);
|
|
510
1243
|
conversations.hydrate(held, chatDrafts);
|
|
511
|
-
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
512
1244
|
pendings.clear();
|
|
513
1245
|
for (const queue of queues) {
|
|
514
|
-
for (const { batch, state } of queue.pending)
|
|
515
|
-
pendings.set(batch.id, state);
|
|
1246
|
+
for (const { batch, state } of queue.pending) {
|
|
1247
|
+
pendings.set(pendingKey(queue.locator, batch.id), state);
|
|
1248
|
+
}
|
|
516
1249
|
}
|
|
517
|
-
const previousReplicas = new Map(replicas);
|
|
518
1250
|
replicas.clear();
|
|
1251
|
+
queuesWithoutReplica.clear();
|
|
1252
|
+
for (const [locator, queue] of queuedBatches) {
|
|
1253
|
+
if (!read.has(locator))
|
|
1254
|
+
queuesWithoutReplica.set(locator, queue);
|
|
1255
|
+
}
|
|
519
1256
|
for (const record of records) {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
const
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
const confirmed = read.kind === 'staged'
|
|
526
|
-
? read.confirmed
|
|
527
|
-
: {
|
|
528
|
-
record: retyped,
|
|
529
|
-
collection: parseCollection(retyped.documents, compositionOf(retyped)),
|
|
530
|
-
};
|
|
531
|
-
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
|
|
1257
|
+
if (fences.has(record.locator))
|
|
1258
|
+
continue;
|
|
1259
|
+
const replica = read.get(record.locator);
|
|
1260
|
+
const { model, selectionModel } = replica.confirmed.record;
|
|
1261
|
+
replicas.set(record.locator, replica);
|
|
532
1262
|
const existing = rooms.get(record.locator);
|
|
533
1263
|
if (existing === undefined)
|
|
534
1264
|
list(record, false);
|
|
@@ -536,7 +1266,6 @@ export function composeRuntime(options) {
|
|
|
536
1266
|
rooms.set(record.locator, {
|
|
537
1267
|
...existing,
|
|
538
1268
|
roomVersion: record.roomVersion,
|
|
539
|
-
attachments: record.attachments,
|
|
540
1269
|
operations: record.operations,
|
|
541
1270
|
replica: record.replica,
|
|
542
1271
|
model: modelTypes(model),
|
|
@@ -548,30 +1277,36 @@ export function composeRuntime(options) {
|
|
|
548
1277
|
},
|
|
549
1278
|
});
|
|
550
1279
|
}
|
|
551
|
-
if (read.kind === 'refused') {
|
|
552
|
-
refusedSource(record, read);
|
|
553
|
-
declarationRefusals.add(record.locator);
|
|
554
|
-
}
|
|
555
1280
|
}
|
|
556
1281
|
return {
|
|
557
1282
|
lastCompleted,
|
|
558
1283
|
locators: new Set([...queuedBatches.keys(), ...replicas.keys()]),
|
|
559
|
-
read: mergeChangeSets([...
|
|
1284
|
+
read: mergeChangeSets([...sets].flatMap(([locator, set]) => (fences.has(locator) ? [] : [set]))),
|
|
560
1285
|
};
|
|
561
1286
|
}
|
|
562
1287
|
async function readState(opened) {
|
|
563
1288
|
const { lastCompleted, locators, read } = await hydrate(opened);
|
|
564
1289
|
applyChangeSet(read);
|
|
1290
|
+
prepare();
|
|
565
1291
|
if (replicas.size > 0)
|
|
566
1292
|
emitRooms();
|
|
1293
|
+
if (member !== null)
|
|
1294
|
+
emitMember();
|
|
567
1295
|
for (const locator of locators)
|
|
568
1296
|
emitQueue(locator);
|
|
569
1297
|
publishRoomDrafts(false);
|
|
570
1298
|
return lastCompleted;
|
|
571
1299
|
}
|
|
572
1300
|
async function open() {
|
|
573
|
-
|
|
1301
|
+
if (closed)
|
|
1302
|
+
throw new Error('runtime closed');
|
|
1303
|
+
const opened = await openRuntimeStore(isFenced, options.database);
|
|
574
1304
|
store = opened;
|
|
1305
|
+
const interrupted = await opened.fences();
|
|
1306
|
+
for (const locator of interrupted) {
|
|
1307
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
1308
|
+
lastFenced.set(locator, ++fencings);
|
|
1309
|
+
}
|
|
575
1310
|
const kept = options.shell.generation;
|
|
576
1311
|
const storedGeneration = await opened.generation();
|
|
577
1312
|
const loss = detectStorageLoss(storedGeneration, kept);
|
|
@@ -585,7 +1320,12 @@ export function composeRuntime(options) {
|
|
|
585
1320
|
emitStorageLoss();
|
|
586
1321
|
}
|
|
587
1322
|
const lastCompleted = await readState(opened);
|
|
1323
|
+
if (interrupted.length > 0)
|
|
1324
|
+
await erased(opened, interrupted);
|
|
588
1325
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
1326
|
+
void exclusive(() => unpair(opened)).catch(() => {
|
|
1327
|
+
journal.record('unpair failed');
|
|
1328
|
+
});
|
|
589
1329
|
return opened;
|
|
590
1330
|
}
|
|
591
1331
|
function durably(work) {
|
|
@@ -606,26 +1346,92 @@ export function composeRuntime(options) {
|
|
|
606
1346
|
mutations = next.then(() => undefined, () => undefined);
|
|
607
1347
|
return next;
|
|
608
1348
|
}
|
|
609
|
-
function withdrawable(
|
|
610
|
-
|
|
611
|
-
|
|
1349
|
+
function withdrawable(locator) {
|
|
1350
|
+
return (batch) => {
|
|
1351
|
+
const state = pendings.get(pendingKey(locator, batch.id));
|
|
1352
|
+
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
function landedWhole(locator, batch, confirmed, current) {
|
|
1356
|
+
if (!landed(batch, confirmed, current))
|
|
1357
|
+
return false;
|
|
1358
|
+
const partner = partnerOf(batch, locator);
|
|
1359
|
+
if (partner === null)
|
|
1360
|
+
return true;
|
|
1361
|
+
const other = replicas.get(partner);
|
|
1362
|
+
const half = other?.queue.find((one) => one.id === batch.id);
|
|
1363
|
+
return other !== undefined && half !== undefined && landed(half, other.confirmed);
|
|
1364
|
+
}
|
|
1365
|
+
function carriedBack(locator, unlanded, written) {
|
|
1366
|
+
const back = new Map();
|
|
1367
|
+
const taken = [];
|
|
1368
|
+
for (const batch of unlanded) {
|
|
1369
|
+
const between = batch.between;
|
|
1370
|
+
if (between === undefined)
|
|
1371
|
+
continue;
|
|
1372
|
+
const source = between.source.room;
|
|
1373
|
+
const destination = between.destination.room;
|
|
1374
|
+
for (const { path: to } of pendings.get(pendingKey(destination, batch.id))?.drafts ?? []) {
|
|
1375
|
+
const record = roomDrafts.get(destination)?.get(to);
|
|
1376
|
+
const path = between.moves.find((move) => move.to === to)?.path;
|
|
1377
|
+
if (record === undefined || path === undefined)
|
|
1378
|
+
continue;
|
|
1379
|
+
const [into, key] = source === locator ? [written, path] : [back, documentKey(source, path)];
|
|
1380
|
+
const existing = into.get(key) ?? roomDrafts.get(source)?.get(path);
|
|
1381
|
+
taken.push({ locator: destination, path: to });
|
|
1382
|
+
into.set(key, {
|
|
1383
|
+
locator: source,
|
|
1384
|
+
path,
|
|
1385
|
+
revision: roomDraftRevision(),
|
|
1386
|
+
parts: returnedInto(existing?.parts, record.parts),
|
|
1387
|
+
returned: existing?.returned ?? null,
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
const records = [...back.values()];
|
|
1392
|
+
const elsewhere = taken.filter((one) => one.locator !== locator);
|
|
1393
|
+
const rooms = new Set([...records, ...elsewhere].map((one) => one.locator));
|
|
1394
|
+
return {
|
|
1395
|
+
taken: taken.filter((one) => one.locator === locator).map(({ path }) => path),
|
|
1396
|
+
writes: [...rooms].map((room) => ({
|
|
1397
|
+
locator: room,
|
|
1398
|
+
roomDrafts: records.filter((record) => record.locator === room),
|
|
1399
|
+
consumedRoomDrafts: elsewhere.filter((one) => one.locator === room).map(({ path }) => path),
|
|
1400
|
+
})),
|
|
1401
|
+
changes: [...rooms].map((room) => changeSet(room, null, null, {
|
|
1402
|
+
drafts: [...records, ...elsewhere]
|
|
1403
|
+
.filter((one) => one.locator === room)
|
|
1404
|
+
.map(({ path }) => path),
|
|
1405
|
+
})),
|
|
1406
|
+
remember() {
|
|
1407
|
+
for (const one of taken)
|
|
1408
|
+
forgetRoomDraft(one.locator, one.path);
|
|
1409
|
+
for (const record of records)
|
|
1410
|
+
rememberRoomDraft(record);
|
|
1411
|
+
},
|
|
1412
|
+
};
|
|
612
1413
|
}
|
|
613
1414
|
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
614
|
-
const
|
|
615
|
-
const kept =
|
|
1415
|
+
const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
1416
|
+
const { withdrawn, kept } = await stepped(standing(confirmed, queue, withdrawable(locator), localSaves));
|
|
1417
|
+
const founded = kept.filter((batch) => !queue.includes(batch));
|
|
1418
|
+
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
616
1419
|
const unqueued = withdrawn.map((batch) => ({
|
|
617
1420
|
batch,
|
|
618
|
-
|
|
1421
|
+
landed: landedWhole(locator, batch, confirmed),
|
|
619
1422
|
}));
|
|
620
1423
|
const returning = [
|
|
621
|
-
...[...unqueued].reverse(),
|
|
1424
|
+
...[...unqueued].reverse().map(({ batch, landed }) => ({
|
|
1425
|
+
batch,
|
|
1426
|
+
returned: localSaves || landed ? null : returnedBy('withdrawn'),
|
|
1427
|
+
})),
|
|
622
1428
|
...(settling.leaving === undefined ? [] : [settling.leaving]),
|
|
623
1429
|
];
|
|
624
1430
|
const written = new Map();
|
|
625
1431
|
for (const { batch, returned } of returning) {
|
|
626
|
-
if (returned === null)
|
|
1432
|
+
if (returned === null || batch.between?.destination.room === locator)
|
|
627
1433
|
continue;
|
|
628
|
-
for (const saved of pendings.get(batch.id)?.drafts ?? []) {
|
|
1434
|
+
for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
|
|
629
1435
|
const existing = written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path);
|
|
630
1436
|
written.set(saved.path, {
|
|
631
1437
|
locator,
|
|
@@ -636,28 +1442,68 @@ export function composeRuntime(options) {
|
|
|
636
1442
|
});
|
|
637
1443
|
}
|
|
638
1444
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
1445
|
+
const carried = carriedBack(locator, [
|
|
1446
|
+
...[...unqueued].reverse().flatMap(({ batch, landed }) => (landed ? [] : [batch])),
|
|
1447
|
+
...(settling.leaving === undefined || settling.leaving.returned === null
|
|
1448
|
+
? []
|
|
1449
|
+
: [settling.leaving.batch]),
|
|
1450
|
+
], written);
|
|
1451
|
+
await local.keepAuthored({
|
|
1452
|
+
locator,
|
|
642
1453
|
queue: kept,
|
|
643
|
-
|
|
1454
|
+
founded,
|
|
644
1455
|
roomDrafts: [...written.values()],
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
1456
|
+
consumedRoomDrafts: carried.taken,
|
|
1457
|
+
}, ...carried.writes);
|
|
1458
|
+
const staying = new Set(kept.map((batch) => batch.id));
|
|
1459
|
+
for (const batch of previousQueue) {
|
|
1460
|
+
if (!staying.has(batch.id))
|
|
1461
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
649
1462
|
}
|
|
650
1463
|
for (const record of written.values())
|
|
651
1464
|
rememberRoomDraft(record);
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
}
|
|
1465
|
+
carried.remember();
|
|
1466
|
+
const drafts = [...written.keys(), ...carried.taken];
|
|
1467
|
+
try {
|
|
1468
|
+
await local.write({
|
|
1469
|
+
next: confirmed.record,
|
|
1470
|
+
previous: before?.confirmed.record ?? null,
|
|
1471
|
+
...(settling.stored === undefined ? {} : { stored: settling.stored }),
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
catch (error) {
|
|
1475
|
+
const held = before === undefined ? null : await stepped(replicating(before.confirmed, kept, before));
|
|
1476
|
+
const set = await stepped(changing(locator, before ?? null, held, { queue: queueChanged, drafts }));
|
|
1477
|
+
if (held !== null)
|
|
1478
|
+
replicas.set(locator, held);
|
|
1479
|
+
else if (kept.length > 0)
|
|
1480
|
+
queuesWithoutReplica.set(locator, kept);
|
|
1481
|
+
else
|
|
1482
|
+
queuesWithoutReplica.delete(locator);
|
|
1483
|
+
publishChangeSet(mergeChangeSets([set, ...carried.changes]));
|
|
1484
|
+
moveTo({ ...status, ...attention() });
|
|
1485
|
+
throw error;
|
|
1486
|
+
}
|
|
1487
|
+
const changed = { room: settling.room, queue: queueChanged, drafts };
|
|
1488
|
+
const next = await stepped(replicating(confirmed, kept, before));
|
|
1489
|
+
await stepped(counting(next));
|
|
1490
|
+
const set = await stepped(changing(locator, before ?? null, next, changed));
|
|
1491
|
+
return () => {
|
|
1492
|
+
queuesWithoutReplica.delete(locator);
|
|
1493
|
+
for (const { batch, landed } of unqueued) {
|
|
1494
|
+
const outcome = localSaves ? 'dropped' : landed ? 'landed' : 'withdrawn';
|
|
1495
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
1496
|
+
}
|
|
1497
|
+
replicas.set(locator, next);
|
|
1498
|
+
return mergeChangeSets([
|
|
1499
|
+
heldChange(locator, before ?? null, next, set, changed),
|
|
1500
|
+
...carried.changes,
|
|
1501
|
+
]);
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
function heldChange(locator, before, next, set, changed) {
|
|
1505
|
+
const held = replicas.get(locator) ?? null;
|
|
1506
|
+
return held === next ? set : changeSet(locator, before, held, changed);
|
|
661
1507
|
}
|
|
662
1508
|
function adoptModels(locator, record) {
|
|
663
1509
|
for (const [attached, model] of [
|
|
@@ -690,10 +1536,16 @@ export function composeRuntime(options) {
|
|
|
690
1536
|
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
691
1537
|
};
|
|
692
1538
|
}
|
|
693
|
-
function publish(local, room, confirmed,
|
|
1539
|
+
function publish(local, room, confirmed, landing, live) {
|
|
1540
|
+
const { base, stored } = landing;
|
|
694
1541
|
return exclusive(async () => {
|
|
695
|
-
if (
|
|
1542
|
+
if (stored !== undefined) {
|
|
1543
|
+
await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
|
|
1544
|
+
}
|
|
1545
|
+
if (!live()) {
|
|
1546
|
+
caughtUpTo(room.locator);
|
|
696
1547
|
return true;
|
|
1548
|
+
}
|
|
697
1549
|
const prior = replicas.get(room.locator);
|
|
698
1550
|
if (base !== undefined &&
|
|
699
1551
|
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
@@ -701,47 +1553,261 @@ export function composeRuntime(options) {
|
|
|
701
1553
|
return false;
|
|
702
1554
|
}
|
|
703
1555
|
adoptModels(room.locator, confirmed.record);
|
|
704
|
-
const queue = prior?.queue ?? [];
|
|
1556
|
+
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
705
1557
|
const roomVersion = confirmed.record.roomVersion;
|
|
706
1558
|
const accepted = queue.filter((batch) => {
|
|
707
|
-
const accepted = pendings.get(batch.id)?.accepted;
|
|
1559
|
+
const accepted = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
|
|
708
1560
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
709
1561
|
});
|
|
710
|
-
|
|
1562
|
+
let settled;
|
|
1563
|
+
try {
|
|
1564
|
+
settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
|
|
1565
|
+
}
|
|
1566
|
+
catch (error) {
|
|
1567
|
+
if (!exceeded(error))
|
|
1568
|
+
throw error;
|
|
1569
|
+
await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
|
|
1570
|
+
caughtUpTo(room.locator);
|
|
1571
|
+
return true;
|
|
1572
|
+
}
|
|
1573
|
+
const set = settled();
|
|
1574
|
+
if (stored !== undefined) {
|
|
1575
|
+
transfers.delete(room.locator);
|
|
1576
|
+
catching.delete(room.locator);
|
|
1577
|
+
}
|
|
711
1578
|
for (const batch of accepted)
|
|
712
|
-
pendings.delete(batch.id);
|
|
1579
|
+
pendings.delete(pendingKey(room.locator, batch.id));
|
|
713
1580
|
stale.delete(room.locator);
|
|
1581
|
+
unindexable.delete(room.locator);
|
|
714
1582
|
if (unreadable.delete(room.locator))
|
|
715
1583
|
scopedRefusals.delete(room.locator);
|
|
716
1584
|
sourceRefusals.delete(room.locator);
|
|
717
1585
|
declarationRefusals.delete(room.locator);
|
|
718
1586
|
list(room, true);
|
|
1587
|
+
if (prior === undefined)
|
|
1588
|
+
prepared.add(room.locator);
|
|
719
1589
|
publishChangeSet(set);
|
|
1590
|
+
await unpair(local);
|
|
720
1591
|
moveTo({ ...status, ...attention() });
|
|
1592
|
+
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
1593
|
+
if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1594
|
+
submitLater(room.locator);
|
|
1595
|
+
}
|
|
721
1596
|
return true;
|
|
722
1597
|
});
|
|
723
1598
|
}
|
|
724
|
-
async function
|
|
1599
|
+
async function savedRoom(locator) {
|
|
725
1600
|
const local = await openLocal();
|
|
1601
|
+
await referenced(locator);
|
|
726
1602
|
const replica = replicas.get(locator);
|
|
727
1603
|
const room = rooms.get(locator);
|
|
728
1604
|
if (replica === undefined || room === undefined) {
|
|
729
|
-
throw
|
|
1605
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
730
1606
|
}
|
|
1607
|
+
return { local, replica, room };
|
|
1608
|
+
}
|
|
1609
|
+
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
1610
|
+
const target = await savedRoom(locator);
|
|
731
1611
|
const saved = [];
|
|
732
1612
|
for (const { path, revision } of named) {
|
|
733
1613
|
const record = roomDrafts.get(locator)?.get(path);
|
|
734
1614
|
if (record === undefined || record.revision !== revision) {
|
|
735
|
-
return { outcome: 'refused',
|
|
1615
|
+
return { outcome: 'refused', code: 'stale', retryable: false, file: path };
|
|
736
1616
|
}
|
|
737
1617
|
if (!saved.some((draft) => draft.path === path))
|
|
738
1618
|
saved.push({ path, parts: record.parts });
|
|
739
1619
|
}
|
|
1620
|
+
return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
|
|
1621
|
+
}
|
|
1622
|
+
async function saveParts(locator, named, writtenBy) {
|
|
1623
|
+
const read = named.map(({ path, parts }) => ({ path, parts: readParts(parts) }));
|
|
1624
|
+
if (read.some(({ path }, at) => path === '' ||
|
|
1625
|
+
normalize(path) !== path ||
|
|
1626
|
+
read.findIndex((one) => one.path === path) !== at)) {
|
|
1627
|
+
throw runtimeError('room-draft-malformed');
|
|
1628
|
+
}
|
|
1629
|
+
const target = await savedRoom(locator);
|
|
1630
|
+
const saved = [];
|
|
1631
|
+
const files = [];
|
|
1632
|
+
for (const { path, parts } of read) {
|
|
1633
|
+
const file = parts.file === undefined
|
|
1634
|
+
? undefined
|
|
1635
|
+
: await heldFile(parts.file.blob, binaryBase(target.replica.visible, path));
|
|
1636
|
+
if (parts.file !== undefined && file !== undefined) {
|
|
1637
|
+
files.push({ key: file.localId, blob: parts.file.blob });
|
|
1638
|
+
}
|
|
1639
|
+
saved.push({ path, parts: held(undefined, parts, file) });
|
|
1640
|
+
}
|
|
1641
|
+
return committed(locator, target, saved, new Set(), writtenBy, files);
|
|
1642
|
+
}
|
|
1643
|
+
function carriedDrafts(locator, saved, consumed) {
|
|
1644
|
+
const holding = roomDrafts.get(locator);
|
|
1645
|
+
return new Map(saved.flatMap(({ path, parts }) => {
|
|
1646
|
+
const record = holding?.get(path);
|
|
1647
|
+
if (parts.move === undefined || record === undefined || consumed.has(path))
|
|
1648
|
+
return [];
|
|
1649
|
+
const to = moveTarget(parts.move);
|
|
1650
|
+
if (to === path || holding?.has(to) === true)
|
|
1651
|
+
return [];
|
|
1652
|
+
return [[path, { ...record, path: to, revision: roomDraftRevision() }]];
|
|
1653
|
+
}));
|
|
1654
|
+
}
|
|
1655
|
+
function sideOf(locator, replica, room) {
|
|
1656
|
+
const record = replica.confirmed.record;
|
|
1657
|
+
return {
|
|
1658
|
+
room: locator,
|
|
1659
|
+
handle: room.handle,
|
|
1660
|
+
baseRoomVersion: record.roomVersion,
|
|
1661
|
+
composition: compositionFor(record),
|
|
1662
|
+
operations: room.operations,
|
|
1663
|
+
providerOwnedPaths: room.providerOwnedPaths,
|
|
1664
|
+
visible: replica.visible,
|
|
1665
|
+
queued: queuedDocuments(replica.queue),
|
|
1666
|
+
clean: {
|
|
1667
|
+
text: record.documents,
|
|
1668
|
+
binaries: record.binaries,
|
|
1669
|
+
composition: compositionOf(record),
|
|
1670
|
+
},
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
function movedInto(locator, saved) {
|
|
1674
|
+
const into = saved.find(({ parts }) => parts.move?.room !== undefined && parts.move.room !== locator);
|
|
1675
|
+
return into?.parts.move?.room ?? null;
|
|
1676
|
+
}
|
|
1677
|
+
async function committedMove(locator, destination, { local, replica, room }, saved, consumed, writtenBy, files) {
|
|
1678
|
+
const mixed = saved.find(({ parts }) => Object.keys(parts).length !== 1 || parts.move?.room !== destination);
|
|
1679
|
+
if (mixed !== undefined) {
|
|
1680
|
+
return { outcome: 'refused', code: 'mixed-room-move', retryable: false, file: mixed.path };
|
|
1681
|
+
}
|
|
740
1682
|
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
|
|
741
1683
|
if (changed.length > 0)
|
|
742
|
-
return { outcome: 'refused',
|
|
1684
|
+
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
1685
|
+
await referenced(destination);
|
|
1686
|
+
const target = replicas.get(destination);
|
|
1687
|
+
const listedRoom = rooms.get(destination);
|
|
1688
|
+
if (target === undefined || listedRoom === undefined) {
|
|
1689
|
+
throw runtimeError('room-not-held', `No Local Replica for ${destination}`);
|
|
1690
|
+
}
|
|
1691
|
+
const built = await buildMove({
|
|
1692
|
+
source: sideOf(locator, replica, room),
|
|
1693
|
+
destination: sideOf(destination, target, listedRoom),
|
|
1694
|
+
moves: saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) })),
|
|
1695
|
+
providerLimits: limits,
|
|
1696
|
+
local: localSaves,
|
|
1697
|
+
...(writtenBy === undefined ? {} : { writtenBy }),
|
|
1698
|
+
});
|
|
1699
|
+
if (built.kind === 'refused')
|
|
1700
|
+
return { outcome: 'refused', ...built.refusal };
|
|
1701
|
+
const id = built.leaving.id;
|
|
1702
|
+
const carrying = saved.flatMap(({ path, parts }) => {
|
|
1703
|
+
const record = roomDrafts.get(locator)?.get(path);
|
|
1704
|
+
const to = moveTarget(parts.move);
|
|
1705
|
+
if (record === undefined || consumed.has(path) || roomDrafts.get(destination)?.has(to)) {
|
|
1706
|
+
return [];
|
|
1707
|
+
}
|
|
1708
|
+
const moved = {
|
|
1709
|
+
...record,
|
|
1710
|
+
locator: destination,
|
|
1711
|
+
path: to,
|
|
1712
|
+
revision: roomDraftRevision(),
|
|
1713
|
+
};
|
|
1714
|
+
return [{ from: path, record: moved }];
|
|
1715
|
+
});
|
|
1716
|
+
const carried = carrying.map(({ record }) => record);
|
|
1717
|
+
const marked = carried.map(({ path }) => ({ path, parts: {} }));
|
|
1718
|
+
const left = [...consumed, ...carrying.map(({ from }) => from)];
|
|
1719
|
+
const leavingQueue = [...replica.queue, built.leaving];
|
|
1720
|
+
const arrivingQueue = [...target.queue, built.arriving];
|
|
1721
|
+
let fencedRoom;
|
|
1722
|
+
try {
|
|
1723
|
+
fencedRoom = await local.saveAuthored(files, {
|
|
1724
|
+
locator,
|
|
1725
|
+
queue: leavingQueue,
|
|
1726
|
+
saved: new Map([[id, saved]]),
|
|
1727
|
+
consumedRoomDrafts: left,
|
|
1728
|
+
}, {
|
|
1729
|
+
locator: destination,
|
|
1730
|
+
queue: arrivingQueue,
|
|
1731
|
+
saved: new Map([[id, marked]]),
|
|
1732
|
+
roomDrafts: carried,
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1735
|
+
catch {
|
|
1736
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
1737
|
+
}
|
|
1738
|
+
if (fencedRoom !== null) {
|
|
1739
|
+
throw runtimeError('room-not-held', `No Local Replica for ${fencedRoom}`);
|
|
1740
|
+
}
|
|
1741
|
+
const nextSource = await stepped(replicating(replica.confirmed, leavingQueue, replica));
|
|
1742
|
+
const nextDestination = await stepped(replicating(target.confirmed, arrivingQueue, target));
|
|
1743
|
+
const sets = [
|
|
1744
|
+
await stepped(changing(locator, replica, nextSource, { queue: true, drafts: left })),
|
|
1745
|
+
await stepped(changing(destination, target, nextDestination, {
|
|
1746
|
+
queue: true,
|
|
1747
|
+
drafts: carried.map(({ path }) => path),
|
|
1748
|
+
})),
|
|
1749
|
+
];
|
|
1750
|
+
if (fences.has(locator) || fences.has(destination)) {
|
|
1751
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1752
|
+
}
|
|
1753
|
+
pendings.set(pendingKey(locator, id), {
|
|
1754
|
+
drafts: saved,
|
|
1755
|
+
submission: { state: 'waiting' },
|
|
1756
|
+
lost: false,
|
|
1757
|
+
});
|
|
1758
|
+
pendings.set(pendingKey(destination, id), {
|
|
1759
|
+
drafts: marked,
|
|
1760
|
+
submission: { state: 'waiting' },
|
|
1761
|
+
lost: false,
|
|
1762
|
+
});
|
|
1763
|
+
for (const path of left)
|
|
1764
|
+
forgetRoomDraft(locator, path);
|
|
1765
|
+
for (const record of carried)
|
|
1766
|
+
rememberRoomDraft(record);
|
|
1767
|
+
replicas.set(locator, nextSource);
|
|
1768
|
+
replicas.set(destination, nextDestination);
|
|
1769
|
+
publishChangeSet(mergeChangeSets(sets));
|
|
1770
|
+
queuedSave(locator);
|
|
1771
|
+
return { outcome: 'saved', identifier: id };
|
|
1772
|
+
}
|
|
1773
|
+
function queuedSave(locator) {
|
|
1774
|
+
if (!localSaves)
|
|
1775
|
+
recorded = false;
|
|
1776
|
+
moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
|
|
1777
|
+
submitLater(locator);
|
|
1778
|
+
}
|
|
1779
|
+
async function committed(locator, target, saved, consumed, writtenBy, files = []) {
|
|
1780
|
+
const into = movedInto(locator, saved);
|
|
1781
|
+
if (into !== null) {
|
|
1782
|
+
return committedMove(locator, into, target, saved, consumed, writtenBy, files);
|
|
1783
|
+
}
|
|
1784
|
+
const { local, replica, room } = target;
|
|
1785
|
+
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
|
|
1786
|
+
if (changed.length > 0)
|
|
1787
|
+
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
743
1788
|
const parts = saved.map(({ path, parts }) => changesOf(path, parts));
|
|
744
|
-
const
|
|
1789
|
+
const changes = parts.flatMap(({ changes }) => changes);
|
|
1790
|
+
const visibleOver = function* (queue) {
|
|
1791
|
+
return (yield* replicating(replica.confirmed, queue, replica)).visible;
|
|
1792
|
+
};
|
|
1793
|
+
const requeue = await stepped(requeuing(replica.queue, changes, replica.visible, withdrawable(locator), visibleOver));
|
|
1794
|
+
if (requeue.kind === 'refused')
|
|
1795
|
+
return { outcome: 'refused', ...requeue.refusal };
|
|
1796
|
+
const foreign = [...requeue.withdrawn.keys()].some((origin) => {
|
|
1797
|
+
const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
|
|
1798
|
+
return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
|
|
1799
|
+
});
|
|
1800
|
+
if (foreign) {
|
|
1801
|
+
return {
|
|
1802
|
+
outcome: 'refused',
|
|
1803
|
+
code: 'unknown-revision',
|
|
1804
|
+
retryable: false,
|
|
1805
|
+
file: [...requeue.queued].at(-1),
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
const withdrawing = requeue.withdrawn.size > 0;
|
|
1809
|
+
const input = {
|
|
1810
|
+
local: localSaves,
|
|
745
1811
|
baseRoomVersion: replica.confirmed.record.roomVersion,
|
|
746
1812
|
composition: compositionFor(replica.confirmed.record),
|
|
747
1813
|
operations: room.operations,
|
|
@@ -749,74 +1815,166 @@ export function composeRuntime(options) {
|
|
|
749
1815
|
providerLimits: limits,
|
|
750
1816
|
handle: room.handle,
|
|
751
1817
|
visible: replica.visible,
|
|
752
|
-
changes
|
|
1818
|
+
changes,
|
|
753
1819
|
bases: parts.flatMap(({ bases }) => bases),
|
|
1820
|
+
queued: queuedDocuments(replica.queue),
|
|
754
1821
|
writtenBy,
|
|
1822
|
+
clean: {
|
|
1823
|
+
text: replica.confirmed.record.documents,
|
|
1824
|
+
binaries: replica.confirmed.record.binaries,
|
|
1825
|
+
composition: compositionOf(replica.confirmed.record),
|
|
1826
|
+
},
|
|
1827
|
+
};
|
|
1828
|
+
const checked = withdrawing
|
|
1829
|
+
? await buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
|
|
1830
|
+
: null;
|
|
1831
|
+
if (checked?.kind === 'refused')
|
|
1832
|
+
return { outcome: 'refused', ...checked.refusal };
|
|
1833
|
+
const owners = [...requeue.owners].flatMap((origin) => {
|
|
1834
|
+
const batch = requeue.withdrawn.get(origin);
|
|
1835
|
+
return batch === undefined || batch === null ? [] : [{ origin, batch }];
|
|
755
1836
|
});
|
|
756
|
-
if (
|
|
1837
|
+
if (owners.length > 1) {
|
|
1838
|
+
return {
|
|
1839
|
+
outcome: 'refused',
|
|
1840
|
+
code: 'unknown-revision',
|
|
1841
|
+
retryable: false,
|
|
1842
|
+
file: [...requeue.queued].at(-1),
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
const built = !withdrawing
|
|
1846
|
+
? await buildBatch(input)
|
|
1847
|
+
: requeue.changes.length === 0
|
|
1848
|
+
? null
|
|
1849
|
+
: await buildBatch({
|
|
1850
|
+
...input,
|
|
1851
|
+
visible: (await stepped(replicating(replica.confirmed, requeue.queue, replica)))
|
|
1852
|
+
.visible,
|
|
1853
|
+
changes: requeue.changes,
|
|
1854
|
+
queued: queuedDocuments(requeue.queue),
|
|
1855
|
+
});
|
|
1856
|
+
if (built?.kind === 'refused')
|
|
757
1857
|
return { outcome: 'refused', ...built.refusal };
|
|
758
|
-
const
|
|
759
|
-
const
|
|
1858
|
+
const [owner] = owners;
|
|
1859
|
+
const join = built === null || owner === undefined
|
|
1860
|
+
? null
|
|
1861
|
+
: joined(requeue.queue, owner.batch, built.batch);
|
|
1862
|
+
if (join?.kind === 'refused')
|
|
1863
|
+
return { outcome: 'refused', ...join.refusal };
|
|
1864
|
+
const withdrawn = new Map(requeue.withdrawn);
|
|
1865
|
+
if (join !== null && owner !== undefined)
|
|
1866
|
+
withdrawn.set(owner.origin, join.batch);
|
|
1867
|
+
const drafted = new Map();
|
|
1868
|
+
for (const [origin, replaced] of withdrawn) {
|
|
1869
|
+
if (replaced === null)
|
|
1870
|
+
continue;
|
|
1871
|
+
const carried = withdrawnDrafts(pendings.get(pendingKey(locator, origin))?.drafts ?? [], changes);
|
|
1872
|
+
drafted.set(replaced.id, replaced === join?.batch
|
|
1873
|
+
? [
|
|
1874
|
+
...carried.map((held) => joinedDraft(held, saved)),
|
|
1875
|
+
...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
|
|
1876
|
+
]
|
|
1877
|
+
: carried);
|
|
1878
|
+
}
|
|
1879
|
+
if (built !== null && join === null)
|
|
1880
|
+
drafted.set(built.batch.id, saved);
|
|
1881
|
+
const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
|
|
1882
|
+
for (const replaced of withdrawn.values()) {
|
|
1883
|
+
if (replaced === null || !batches.includes(replaced))
|
|
1884
|
+
continue;
|
|
1885
|
+
const limited = await stepped(limiting(batches, replaced, limits, visibleOver));
|
|
1886
|
+
if (limited !== null)
|
|
1887
|
+
return { outcome: 'refused', ...limited };
|
|
1888
|
+
}
|
|
1889
|
+
const carried = carriedDrafts(locator, saved, consumed);
|
|
1890
|
+
const left = [...consumed, ...carried.keys()];
|
|
1891
|
+
const arrived = [...carried.values()];
|
|
1892
|
+
let fencedRoom;
|
|
760
1893
|
try {
|
|
761
|
-
await local.
|
|
762
|
-
|
|
763
|
-
previous: replica.confirmed.record,
|
|
1894
|
+
fencedRoom = await local.saveAuthored(files, {
|
|
1895
|
+
locator,
|
|
764
1896
|
queue: batches,
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1897
|
+
saved: drafted,
|
|
1898
|
+
consumedRoomDrafts: left,
|
|
1899
|
+
roomDrafts: arrived,
|
|
768
1900
|
});
|
|
769
1901
|
}
|
|
770
1902
|
catch {
|
|
771
|
-
return { outcome: 'refused',
|
|
1903
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
772
1904
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
|
|
1905
|
+
if (fencedRoom !== null) {
|
|
1906
|
+
throw runtimeError('room-not-held', `No Local Replica for ${fencedRoom}`);
|
|
1907
|
+
}
|
|
1908
|
+
const next = await stepped(replicating(replica.confirmed, batches, replica));
|
|
1909
|
+
const set = await stepped(changing(locator, replica, next, {
|
|
779
1910
|
queue: true,
|
|
780
|
-
drafts:
|
|
1911
|
+
drafts: [...left, ...arrived.map(({ path }) => path)],
|
|
781
1912
|
}));
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
1913
|
+
if (fences.has(locator))
|
|
1914
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1915
|
+
for (const origin of withdrawn.keys())
|
|
1916
|
+
pendings.delete(pendingKey(locator, origin));
|
|
1917
|
+
for (const [id, drafts] of drafted) {
|
|
1918
|
+
pendings.set(pendingKey(locator, id), {
|
|
1919
|
+
drafts,
|
|
1920
|
+
submission: { state: 'waiting' },
|
|
1921
|
+
lost: false,
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1924
|
+
for (const path of left)
|
|
1925
|
+
forgetRoomDraft(locator, path);
|
|
1926
|
+
for (const record of arrived)
|
|
1927
|
+
rememberRoomDraft(record);
|
|
1928
|
+
replicas.set(locator, next);
|
|
1929
|
+
publishChangeSet(set);
|
|
1930
|
+
queuedSave(locator);
|
|
1931
|
+
const identifier = join?.batch.id ??
|
|
1932
|
+
built?.batch.id ??
|
|
1933
|
+
[...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
|
|
1934
|
+
return { outcome: 'saved', identifier };
|
|
785
1935
|
}
|
|
786
|
-
async function mark(local, locator,
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
state
|
|
790
|
-
(
|
|
791
|
-
|
|
1936
|
+
async function mark(local, locator, batch, submission, lost) {
|
|
1937
|
+
const partner = partnerOf(batch, locator);
|
|
1938
|
+
for (const held of partner === null ? [locator] : [locator, partner]) {
|
|
1939
|
+
const state = pendings.get(pendingKey(held, batch.id));
|
|
1940
|
+
if (!continuing() ||
|
|
1941
|
+
state === undefined ||
|
|
1942
|
+
(JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
|
|
1943
|
+
continue;
|
|
1944
|
+
}
|
|
1945
|
+
await local.submit(held, batch.id, submission, lost);
|
|
1946
|
+
if (fences.has(held))
|
|
1947
|
+
continue;
|
|
1948
|
+
pendings.set(pendingKey(held, batch.id), { ...state, submission, lost });
|
|
1949
|
+
const replica = replicas.get(held);
|
|
1950
|
+
if (replica !== undefined)
|
|
1951
|
+
publishChangeSet(changeSet(held, replica, replica, { queue: true }));
|
|
792
1952
|
}
|
|
793
|
-
await local.submit(locator, identifier, submission, lost);
|
|
794
|
-
pendings.set(identifier, { ...state, submission, lost });
|
|
795
|
-
const replica = replicas.get(locator);
|
|
796
|
-
if (replica !== undefined)
|
|
797
|
-
publishChangeSet(changeSet(locator, replica, replica, { queue: true }));
|
|
798
1953
|
moveTo({ ...status, ...attention() });
|
|
799
1954
|
}
|
|
800
1955
|
async function keepUpload(local, locator, identifier, upload) {
|
|
801
1956
|
if (!continuing())
|
|
802
1957
|
throw new Error('connection refused');
|
|
803
1958
|
await local.keepUpload(locator, identifier, upload);
|
|
804
|
-
|
|
1959
|
+
if (fences.has(locator))
|
|
1960
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1961
|
+
const key = pendingKey(locator, identifier);
|
|
1962
|
+
pendings.set(key, { ...pendings.get(key), upload });
|
|
805
1963
|
if (!continuing())
|
|
806
1964
|
throw new Error('connection refused');
|
|
807
1965
|
}
|
|
808
1966
|
async function readHistory(locator, path, query = {}) {
|
|
809
1967
|
if (closed || refusal !== null)
|
|
810
|
-
return
|
|
1968
|
+
return UNCERTAIN;
|
|
811
1969
|
try {
|
|
812
1970
|
await ensureAuthorization();
|
|
813
1971
|
}
|
|
814
1972
|
catch {
|
|
815
|
-
return
|
|
816
|
-
}
|
|
817
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
818
|
-
return { kind: 'transport-uncertainty' };
|
|
1973
|
+
return UNCERTAIN;
|
|
819
1974
|
}
|
|
1975
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1976
|
+
return UNCERTAIN;
|
|
1977
|
+
const since = fencings;
|
|
820
1978
|
const transfer = ['transfer', 'history', locator, path];
|
|
821
1979
|
if (query.revision !== undefined)
|
|
822
1980
|
transfer.push(query.revision);
|
|
@@ -831,6 +1989,8 @@ export function composeRuntime(options) {
|
|
|
831
1989
|
...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
|
|
832
1990
|
});
|
|
833
1991
|
journal.record(`outcome history ${result.kind}`);
|
|
1992
|
+
if (fencedSince(locator, since))
|
|
1993
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
834
1994
|
if (result.kind === 'scoped-refusal') {
|
|
835
1995
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
836
1996
|
if (room !== undefined)
|
|
@@ -838,12 +1998,170 @@ export function composeRuntime(options) {
|
|
|
838
1998
|
}
|
|
839
1999
|
if (result.kind === 'revoked-authorization')
|
|
840
2000
|
await refuse('revoked-authorization');
|
|
841
|
-
return result;
|
|
2001
|
+
return historyAnswer(result);
|
|
842
2002
|
}
|
|
843
2003
|
catch {
|
|
844
2004
|
journal.record('outcome history transport-uncertainty');
|
|
845
|
-
return
|
|
2005
|
+
return UNCERTAIN;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
function located(room, path, destination) {
|
|
2009
|
+
const crossing = roomTarget(destination);
|
|
2010
|
+
if (crossing === null) {
|
|
2011
|
+
const named = target(path, destination);
|
|
2012
|
+
return named === null || !rooms.has(room) ? null : { room, path: named };
|
|
2013
|
+
}
|
|
2014
|
+
const held = [...rooms.values()].find((listed) => listed.handle === crossing.handle);
|
|
2015
|
+
return held === undefined ? null : { room: held.locator, path: crossing.path };
|
|
2016
|
+
}
|
|
2017
|
+
async function resolveReference(room, path, destination) {
|
|
2018
|
+
if (roomTarget(destination) === null && target(path, destination) === null) {
|
|
2019
|
+
return ABSENT_TARGET;
|
|
2020
|
+
}
|
|
2021
|
+
const at = located(room, path, destination);
|
|
2022
|
+
if (at === null)
|
|
2023
|
+
return UNREADABLE;
|
|
2024
|
+
if (locked(at.room))
|
|
2025
|
+
return LOCKED_UNREADABLE;
|
|
2026
|
+
const visible = replicas.get(at.room)?.visible;
|
|
2027
|
+
if (visible?.documents[at.path] !== undefined || visible?.binaries[at.path] !== undefined) {
|
|
2028
|
+
return { kind: 'document', ...at };
|
|
2029
|
+
}
|
|
2030
|
+
const history = await readHistory(at.room, at.path);
|
|
2031
|
+
if (history.kind === 'no-history' || history.kind === 'revision')
|
|
2032
|
+
return ABSENT_TARGET;
|
|
2033
|
+
if (history.kind !== 'history')
|
|
2034
|
+
return history;
|
|
2035
|
+
const [last] = history.revisions;
|
|
2036
|
+
if (last === undefined)
|
|
2037
|
+
return ABSENT_TARGET;
|
|
2038
|
+
return history.deleted === true
|
|
2039
|
+
? { kind: 'deleted', ...at, revision: last }
|
|
2040
|
+
: { kind: 'document', ...at };
|
|
2041
|
+
}
|
|
2042
|
+
async function queuedBlob(locator, path, localId) {
|
|
2043
|
+
for (const batch of replicas.get(locator)?.queue ?? []) {
|
|
2044
|
+
const change = batch.changes[queuedFile(batch, path, localId)];
|
|
2045
|
+
if (change?.kind === 'binary')
|
|
2046
|
+
return (await openLocal()).file(localId, change.mediaType);
|
|
846
2047
|
}
|
|
2048
|
+
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2049
|
+
return drafted?.localId === localId
|
|
2050
|
+
? (await openLocal()).file(localId, drafted.mediaType)
|
|
2051
|
+
: null;
|
|
2052
|
+
}
|
|
2053
|
+
function batchFile(batch) {
|
|
2054
|
+
return async (localId) => {
|
|
2055
|
+
const change = batch.changes.find((held) => held.kind === 'binary' && held.localId === localId);
|
|
2056
|
+
return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
function binaryRoom(request) {
|
|
2060
|
+
if (request.kind === 'path')
|
|
2061
|
+
return request.locator;
|
|
2062
|
+
return options.adapter.referrerScope(request.referrer)?.locator ?? null;
|
|
2063
|
+
}
|
|
2064
|
+
async function transferBinary(request, transfer, answer) {
|
|
2065
|
+
if (closed || refusal !== null)
|
|
2066
|
+
return UNCERTAIN;
|
|
2067
|
+
try {
|
|
2068
|
+
await ensureAuthorization();
|
|
2069
|
+
}
|
|
2070
|
+
catch {
|
|
2071
|
+
return UNCERTAIN;
|
|
2072
|
+
}
|
|
2073
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
2074
|
+
return UNCERTAIN;
|
|
2075
|
+
const since = fencings;
|
|
2076
|
+
journal.record(request.kind === 'path'
|
|
2077
|
+
? `transfer binary ${request.locator} ${request.path}`
|
|
2078
|
+
: `transfer binary ${request.fileRef}`);
|
|
2079
|
+
try {
|
|
2080
|
+
const result = await transfer(authorization);
|
|
2081
|
+
const settled = result;
|
|
2082
|
+
journal.record(`outcome binary ${settled.kind}`);
|
|
2083
|
+
const locator = binaryRoom(request);
|
|
2084
|
+
if (locator !== null && fencedSince(locator, since))
|
|
2085
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
2086
|
+
if (settled.kind === 'scoped-refusal') {
|
|
2087
|
+
const room = replicas.get(settled.locator)?.confirmed.record;
|
|
2088
|
+
if (room !== undefined)
|
|
2089
|
+
await scoped(settled, room, await openLocal());
|
|
2090
|
+
}
|
|
2091
|
+
if (settled.kind === 'revoked-authorization')
|
|
2092
|
+
await refuse('revoked-authorization');
|
|
2093
|
+
return answer(result);
|
|
2094
|
+
}
|
|
2095
|
+
catch {
|
|
2096
|
+
journal.record('outcome binary transport-uncertainty');
|
|
2097
|
+
return UNCERTAIN;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
function readBinary(request) {
|
|
2101
|
+
return transferBinary(request, (held) => options.adapter.binary(held, request), binaryAnswer);
|
|
2102
|
+
}
|
|
2103
|
+
function binaryPath(locator, path, revision) {
|
|
2104
|
+
if (revision !== undefined)
|
|
2105
|
+
return { kind: 'path', locator, path, revision };
|
|
2106
|
+
const visible = replicas.get(locator)?.visible;
|
|
2107
|
+
const held = visible?.binaries[path];
|
|
2108
|
+
if (visible === undefined || held === undefined)
|
|
2109
|
+
return null;
|
|
2110
|
+
if (held.state === 'pending')
|
|
2111
|
+
return { kind: 'pending', localId: held.localId };
|
|
2112
|
+
const record = binaryRecord(path, held, visible.revisions[path]);
|
|
2113
|
+
if (record.state !== 'accepted')
|
|
2114
|
+
return null;
|
|
2115
|
+
const replica = replicas.get(locator);
|
|
2116
|
+
const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
|
|
2117
|
+
const holds = (at) => {
|
|
2118
|
+
const kept = replica?.confirmed.record.binaries[at];
|
|
2119
|
+
return (kept?.state === 'accepted' &&
|
|
2120
|
+
kept.fileRef === record.fileRef &&
|
|
2121
|
+
replica?.confirmed.record.revisions[at] === record.revision);
|
|
2122
|
+
};
|
|
2123
|
+
let confirmed = path;
|
|
2124
|
+
for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
|
|
2125
|
+
const change = queued[index];
|
|
2126
|
+
if (change.kind === 'move' && change.to === confirmed)
|
|
2127
|
+
confirmed = change.path;
|
|
2128
|
+
}
|
|
2129
|
+
return {
|
|
2130
|
+
kind: 'path',
|
|
2131
|
+
locator,
|
|
2132
|
+
path: confirmed,
|
|
2133
|
+
revision: record.revision,
|
|
2134
|
+
fileRef: record.fileRef,
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
async function readBinaryPath(locator, path, revision) {
|
|
2138
|
+
await ready;
|
|
2139
|
+
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2140
|
+
const asked = drafted !== undefined && drafted.localId === revision
|
|
2141
|
+
? { kind: 'pending', localId: revision }
|
|
2142
|
+
: binaryPath(locator, path, revision);
|
|
2143
|
+
if (asked === null)
|
|
2144
|
+
return absentBinary(path);
|
|
2145
|
+
if (asked.kind === 'pending') {
|
|
2146
|
+
const blob = await queuedBlob(locator, path, asked.localId);
|
|
2147
|
+
return blob === null ? absentBinary(path) : { kind: 'binary', blob };
|
|
2148
|
+
}
|
|
2149
|
+
return readBinary(asked);
|
|
2150
|
+
}
|
|
2151
|
+
async function linkBinaryPath(locator, path, revision) {
|
|
2152
|
+
await ready;
|
|
2153
|
+
const asked = binaryPath(locator, path, revision);
|
|
2154
|
+
if (asked === null || asked.kind === 'pending')
|
|
2155
|
+
return absentBinary(path);
|
|
2156
|
+
return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
|
|
2157
|
+
}
|
|
2158
|
+
async function readBinaryReference(reference) {
|
|
2159
|
+
await ready;
|
|
2160
|
+
return readBinary({
|
|
2161
|
+
kind: 'reference',
|
|
2162
|
+
fileRef: reference.fileRef,
|
|
2163
|
+
referrer: reference.referrer,
|
|
2164
|
+
});
|
|
847
2165
|
}
|
|
848
2166
|
function declaredAgents() {
|
|
849
2167
|
for (const capability of capabilities) {
|
|
@@ -861,21 +2179,28 @@ export function composeRuntime(options) {
|
|
|
861
2179
|
}
|
|
862
2180
|
return undefined;
|
|
863
2181
|
}
|
|
864
|
-
|
|
2182
|
+
function calledRooms(name, input) {
|
|
2183
|
+
const member = declaredAction(name)?.room;
|
|
2184
|
+
if (member === undefined || input === null || typeof input !== 'object')
|
|
2185
|
+
return [];
|
|
2186
|
+
const named = input[member];
|
|
2187
|
+
return (Array.isArray(named) ? named : [named]).filter((room) => typeof room === 'string');
|
|
2188
|
+
}
|
|
2189
|
+
async function invokeAction(name, input, dispatching) {
|
|
865
2190
|
await ready;
|
|
866
2191
|
if (closed || refusal !== null)
|
|
867
|
-
return
|
|
2192
|
+
return UNCERTAIN;
|
|
868
2193
|
if (declaredAction(name) === undefined)
|
|
869
|
-
return
|
|
2194
|
+
return undeclared(name);
|
|
870
2195
|
try {
|
|
871
2196
|
await ensureAuthorization();
|
|
872
2197
|
}
|
|
873
2198
|
catch {
|
|
874
|
-
return
|
|
875
|
-
}
|
|
876
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
877
|
-
return { kind: 'transport-uncertainty' };
|
|
2199
|
+
return UNCERTAIN;
|
|
878
2200
|
}
|
|
2201
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
2202
|
+
return UNCERTAIN;
|
|
2203
|
+
await dispatching?.();
|
|
879
2204
|
journal.record(`transfer action ${name}`);
|
|
880
2205
|
try {
|
|
881
2206
|
const result = await options.adapter.action(authorization, { name, input });
|
|
@@ -889,13 +2214,67 @@ export function composeRuntime(options) {
|
|
|
889
2214
|
journal.record(`outcome action ${result.kind}`);
|
|
890
2215
|
if (result.kind === 'rejected')
|
|
891
2216
|
journal.record(`refusal rejected ${result.code}`);
|
|
892
|
-
return result;
|
|
2217
|
+
return calledResult(result);
|
|
893
2218
|
}
|
|
894
2219
|
catch {
|
|
895
2220
|
journal.record('outcome action transport-uncertainty');
|
|
896
|
-
return
|
|
2221
|
+
return UNCERTAIN;
|
|
897
2222
|
}
|
|
898
2223
|
}
|
|
2224
|
+
function unsettled(result) {
|
|
2225
|
+
return result.kind !== 'action' && result.retryable;
|
|
2226
|
+
}
|
|
2227
|
+
async function recoveredAction(name, input) {
|
|
2228
|
+
const operation = operationOf(input);
|
|
2229
|
+
if (operation === null)
|
|
2230
|
+
return invokeAction(name, input);
|
|
2231
|
+
const kept = async () => (await openLocal()).keepCall({ operation, name, input });
|
|
2232
|
+
calling.add(operation);
|
|
2233
|
+
try {
|
|
2234
|
+
let result = await invokeAction(name, input, kept);
|
|
2235
|
+
if (unsettled(result)) {
|
|
2236
|
+
await recovery.attempt(`action ${operation}`, async () => {
|
|
2237
|
+
if (closed || refusal !== null)
|
|
2238
|
+
return 'done';
|
|
2239
|
+
if (!socketOpen)
|
|
2240
|
+
return 'again';
|
|
2241
|
+
result = await invokeAction(name, input, kept);
|
|
2242
|
+
return unsettled(result) ? 'again' : 'done';
|
|
2243
|
+
}, { again: true });
|
|
2244
|
+
}
|
|
2245
|
+
if (!unsettled(result))
|
|
2246
|
+
await (await openLocal()).dropCall(operation);
|
|
2247
|
+
return result;
|
|
2248
|
+
}
|
|
2249
|
+
finally {
|
|
2250
|
+
calling.delete(operation);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
async function resendCalls(signal) {
|
|
2254
|
+
if (!socketOpen || closed || stopping)
|
|
2255
|
+
return 'done';
|
|
2256
|
+
const local = await openLocal();
|
|
2257
|
+
for (const held of await local.calls()) {
|
|
2258
|
+
if (calling.has(held.operation))
|
|
2259
|
+
continue;
|
|
2260
|
+
if (closed || signal.aborted)
|
|
2261
|
+
return 'done';
|
|
2262
|
+
if (pastHorizon(held.operation)) {
|
|
2263
|
+
await local.dropCall(held.operation);
|
|
2264
|
+
continue;
|
|
2265
|
+
}
|
|
2266
|
+
calling.add(held.operation);
|
|
2267
|
+
try {
|
|
2268
|
+
if (unsettled(await invokeAction(held.name, held.input)))
|
|
2269
|
+
return 'again';
|
|
2270
|
+
await local.dropCall(held.operation);
|
|
2271
|
+
}
|
|
2272
|
+
finally {
|
|
2273
|
+
calling.delete(held.operation);
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
return 'done';
|
|
2277
|
+
}
|
|
899
2278
|
function continuing() {
|
|
900
2279
|
return !closed && !discoveryPaused;
|
|
901
2280
|
}
|
|
@@ -910,11 +2289,12 @@ export function composeRuntime(options) {
|
|
|
910
2289
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
911
2290
|
!options.adapter.reached(locator, roomVersion, written));
|
|
912
2291
|
}
|
|
913
|
-
async function pullRefresh(authorized, confirmed) {
|
|
914
|
-
if (!
|
|
2292
|
+
async function pullRefresh(authorized, confirmed, live) {
|
|
2293
|
+
if (!live())
|
|
915
2294
|
return unconfirmed;
|
|
2295
|
+
const turn = generation;
|
|
916
2296
|
try {
|
|
917
|
-
const result = await refreshRoom(
|
|
2297
|
+
const result = await refreshRoom(reads, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
|
|
918
2298
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
919
2299
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
920
2300
|
return result;
|
|
@@ -923,29 +2303,101 @@ export function composeRuntime(options) {
|
|
|
923
2303
|
journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
|
|
924
2304
|
throw error;
|
|
925
2305
|
}
|
|
2306
|
+
finally {
|
|
2307
|
+
reads.finished(confirmed.record.locator, turn);
|
|
2308
|
+
}
|
|
926
2309
|
}
|
|
927
|
-
|
|
928
|
-
|
|
2310
|
+
function staging(local, live) {
|
|
2311
|
+
return {
|
|
2312
|
+
held: (locator) => transfers.get(locator) ?? null,
|
|
2313
|
+
staged: (record) => local.staged(record),
|
|
2314
|
+
stage: (record, page) => exclusive(async () => {
|
|
2315
|
+
if (!live())
|
|
2316
|
+
return false;
|
|
2317
|
+
const bytes = layerBytes(page);
|
|
2318
|
+
if (!(await admitted(local, record.locator, bytes)))
|
|
2319
|
+
return false;
|
|
2320
|
+
try {
|
|
2321
|
+
await local.stage(record, page);
|
|
2322
|
+
}
|
|
2323
|
+
catch (error) {
|
|
2324
|
+
if (!exceeded(error))
|
|
2325
|
+
throw error;
|
|
2326
|
+
await exceededOn(record.locator, bytes);
|
|
2327
|
+
return false;
|
|
2328
|
+
}
|
|
2329
|
+
transfers.set(record.locator, record);
|
|
2330
|
+
readSide.recover();
|
|
2331
|
+
return true;
|
|
2332
|
+
}),
|
|
2333
|
+
abandon: (locator) => exclusive(async () => {
|
|
2334
|
+
if (!live())
|
|
2335
|
+
return;
|
|
2336
|
+
await local.abandon(locator);
|
|
2337
|
+
transfers.delete(locator);
|
|
2338
|
+
readSide.recover();
|
|
2339
|
+
}),
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
function reusable(room) {
|
|
2343
|
+
const record = replicas.get(room.locator)?.confirmed.record;
|
|
2344
|
+
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
2345
|
+
return null;
|
|
2346
|
+
return { documents: record.documents, revisions: record.revisions };
|
|
2347
|
+
}
|
|
2348
|
+
async function pullBootstrap(authorized, room, local, live) {
|
|
2349
|
+
if (!live())
|
|
929
2350
|
return unconfirmed;
|
|
2351
|
+
const turn = generation;
|
|
2352
|
+
if (replicas.has(room.locator)) {
|
|
2353
|
+
catching.add(room.locator);
|
|
2354
|
+
readSide.recover();
|
|
2355
|
+
}
|
|
930
2356
|
try {
|
|
931
|
-
const result = await bootstrapRoom(
|
|
2357
|
+
const result = await bootstrapRoom(reads, authorized, room, {
|
|
932
2358
|
collection: modelOf(room.locator),
|
|
933
2359
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
934
|
-
}, (models, held) => remodel(room.locator, models, held),
|
|
2360
|
+
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
935
2361
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
936
2362
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
2363
|
+
if (result.kind !== 'staged')
|
|
2364
|
+
caughtUpTo(room.locator);
|
|
937
2365
|
return result;
|
|
938
2366
|
}
|
|
939
2367
|
catch (error) {
|
|
940
2368
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
2369
|
+
caughtUpTo(room.locator);
|
|
941
2370
|
throw error;
|
|
942
2371
|
}
|
|
2372
|
+
finally {
|
|
2373
|
+
reads.finished(room.locator, turn);
|
|
2374
|
+
}
|
|
943
2375
|
}
|
|
944
|
-
|
|
2376
|
+
function caughtUpTo(locator) {
|
|
2377
|
+
if (catching.delete(locator))
|
|
2378
|
+
readSide.recover();
|
|
2379
|
+
}
|
|
2380
|
+
function behind(room) {
|
|
2381
|
+
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
2382
|
+
return (held !== undefined &&
|
|
2383
|
+
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
2384
|
+
}
|
|
2385
|
+
async function advance(room, local, authorized, live) {
|
|
2386
|
+
const carried = reads.carried(room.locator);
|
|
2387
|
+
const withheld = await advanceOnce(room, local, authorized, live);
|
|
2388
|
+
if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
|
|
2389
|
+
return withheld;
|
|
2390
|
+
}
|
|
2391
|
+
return advanceOnce(room, local, authorized, live);
|
|
2392
|
+
}
|
|
2393
|
+
async function advanceOnce(room, local, authorized, live) {
|
|
2394
|
+
await referenced(room.locator);
|
|
2395
|
+
if (!live() || paused.has(room.locator))
|
|
2396
|
+
return unconfirmed;
|
|
945
2397
|
const held = replicas.get(room.locator);
|
|
946
2398
|
if (held !== undefined) {
|
|
947
|
-
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
948
|
-
if (!
|
|
2399
|
+
const refreshed = await pullRefresh(authorized, held.confirmed, live);
|
|
2400
|
+
if (!live())
|
|
949
2401
|
return unconfirmed;
|
|
950
2402
|
if (refreshed.kind === 'still-current') {
|
|
951
2403
|
stale.delete(room.locator);
|
|
@@ -959,31 +2411,47 @@ export function composeRuntime(options) {
|
|
|
959
2411
|
return null;
|
|
960
2412
|
}
|
|
961
2413
|
if (refreshed.kind === 'advanced') {
|
|
962
|
-
if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
|
|
2414
|
+
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
|
|
963
2415
|
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
964
2416
|
const reached = now !== undefined &&
|
|
965
2417
|
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
966
|
-
return reached ? null : advance(room, local, authorized);
|
|
2418
|
+
return reached ? null : advance(room, local, authorized, live);
|
|
967
2419
|
}
|
|
968
2420
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
969
2421
|
return null;
|
|
970
2422
|
}
|
|
971
2423
|
if (refreshed.kind !== 'beyond-horizon')
|
|
972
2424
|
return refreshed;
|
|
2425
|
+
if (opened !== null) {
|
|
2426
|
+
owed.add(room.locator);
|
|
2427
|
+
readSide.recover();
|
|
2428
|
+
return unconfirmed;
|
|
2429
|
+
}
|
|
2430
|
+
if (reloadsDue.get(room.locator) === generation)
|
|
2431
|
+
return unconfirmed;
|
|
973
2432
|
}
|
|
974
|
-
const bootstrapped = await pullBootstrap(authorized, room);
|
|
975
|
-
if (!
|
|
2433
|
+
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
2434
|
+
if (!live())
|
|
976
2435
|
return unconfirmed;
|
|
977
2436
|
if (bootstrapped.kind !== 'staged')
|
|
978
2437
|
return bootstrapped;
|
|
979
|
-
await publish(local, room, bootstrapped.confirmed);
|
|
2438
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
980
2439
|
return null;
|
|
981
2440
|
}
|
|
982
2441
|
async function withhold(withheld, room, local, progress) {
|
|
2442
|
+
if (withheld.kind === 'unconfirmed')
|
|
2443
|
+
return;
|
|
983
2444
|
if (withheld.kind === 'refused')
|
|
984
2445
|
stale.delete(room.locator);
|
|
985
2446
|
else
|
|
986
2447
|
stale.add(room.locator);
|
|
2448
|
+
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
2449
|
+
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
2450
|
+
repairable.set(room.locator, reset);
|
|
2451
|
+
repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
|
|
2452
|
+
}
|
|
2453
|
+
else
|
|
2454
|
+
repairable.delete(room.locator);
|
|
987
2455
|
if (withheld.kind === 'scoped-refusal') {
|
|
988
2456
|
await scoped(withheld, room, local);
|
|
989
2457
|
}
|
|
@@ -1008,6 +2476,7 @@ export function composeRuntime(options) {
|
|
|
1008
2476
|
function refusedSource(room, { refusal }) {
|
|
1009
2477
|
sourceRefusals.set(room.locator, {
|
|
1010
2478
|
code: refusal.rule,
|
|
2479
|
+
retryable: false,
|
|
1011
2480
|
fields: {
|
|
1012
2481
|
file: refusal.file,
|
|
1013
2482
|
...(refusal.location === undefined ? {} : { location: refusal.location }),
|
|
@@ -1018,12 +2487,21 @@ export function composeRuntime(options) {
|
|
|
1018
2487
|
unreadable.add(room.locator);
|
|
1019
2488
|
list(room, false);
|
|
1020
2489
|
emitRooms();
|
|
2490
|
+
moveTo({ ...status, ...attention() });
|
|
1021
2491
|
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
1022
2492
|
}
|
|
1023
2493
|
function leaving(batch, result) {
|
|
1024
2494
|
if (result.kind === 'expired')
|
|
1025
|
-
return { batch, returned:
|
|
1026
|
-
return {
|
|
2495
|
+
return { batch, returned: returnedBy('expired') };
|
|
2496
|
+
return {
|
|
2497
|
+
batch,
|
|
2498
|
+
returned: {
|
|
2499
|
+
kind: result.kind,
|
|
2500
|
+
code: result.code,
|
|
2501
|
+
fields: result.fields,
|
|
2502
|
+
retryable: result.retryable,
|
|
2503
|
+
},
|
|
2504
|
+
};
|
|
1027
2505
|
}
|
|
1028
2506
|
function logAccepted(locator, submitted, accepted) {
|
|
1029
2507
|
journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
|
|
@@ -1031,6 +2509,7 @@ export function composeRuntime(options) {
|
|
|
1031
2509
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1032
2510
|
}
|
|
1033
2511
|
async function adoptAccepted(room, local, submitted, accepted) {
|
|
2512
|
+
await referenced(room.locator);
|
|
1034
2513
|
if (!continuing())
|
|
1035
2514
|
return unconfirmed;
|
|
1036
2515
|
const current = replicas.get(room.locator);
|
|
@@ -1041,75 +2520,217 @@ export function composeRuntime(options) {
|
|
|
1041
2520
|
logAccepted(room.locator, submitted, accepted);
|
|
1042
2521
|
const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
1043
2522
|
? { kind: 'staged', confirmed: current.confirmed }
|
|
1044
|
-
: applyReturned(current.confirmed,
|
|
2523
|
+
: await applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
1045
2524
|
? accepted.roomVersion
|
|
1046
2525
|
: held, accepted.roomVersion);
|
|
1047
2526
|
if (applied.kind !== 'staged') {
|
|
1048
|
-
const
|
|
2527
|
+
const key = pendingKey(room.locator, submitted.id);
|
|
2528
|
+
const state = pendings.get(key);
|
|
1049
2529
|
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1050
|
-
if (state !== undefined)
|
|
1051
|
-
pendings.set(
|
|
2530
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
2531
|
+
pendings.set(key, { ...state, accepted: accepted.roomVersion });
|
|
1052
2532
|
emitQueue(room.locator);
|
|
1053
2533
|
return applied;
|
|
1054
2534
|
}
|
|
1055
|
-
const
|
|
1056
|
-
pendings.delete(submitted.id);
|
|
2535
|
+
const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
2536
|
+
pendings.delete(pendingKey(room.locator, submitted.id));
|
|
1057
2537
|
unreadable.delete(room.locator);
|
|
1058
2538
|
sourceRefusals.delete(room.locator);
|
|
1059
2539
|
declarationRefusals.delete(room.locator);
|
|
2540
|
+
const set = settled();
|
|
1060
2541
|
list(room, true);
|
|
1061
2542
|
publishChangeSet(set);
|
|
1062
2543
|
moveTo({ ...status, ...attention() });
|
|
1063
2544
|
return null;
|
|
1064
2545
|
}
|
|
1065
2546
|
async function adopted(room, local, submitted, accepted) {
|
|
1066
|
-
const failed = await exclusive(() =>
|
|
2547
|
+
const failed = await exclusive(async () => {
|
|
2548
|
+
const adopting = await adoptAccepted(room, local, submitted, accepted);
|
|
2549
|
+
await unpair(local);
|
|
2550
|
+
return adopting;
|
|
2551
|
+
});
|
|
2552
|
+
if (failed === null)
|
|
2553
|
+
return true;
|
|
2554
|
+
if (failed.kind === 'refused' && continuing())
|
|
2555
|
+
refusedSource(room, failed);
|
|
2556
|
+
return false;
|
|
2557
|
+
}
|
|
2558
|
+
async function adoptedMove(room, partner, local, submitted, accepted) {
|
|
2559
|
+
const other = discovered.get(partner);
|
|
2560
|
+
const failed = await exclusive(async () => {
|
|
2561
|
+
const left = await adoptAccepted(room, local, submitted, accepted);
|
|
2562
|
+
const arriving = replicas.get(partner)?.queue.find((batch) => batch.id === submitted.id);
|
|
2563
|
+
const arrived = other === undefined || arriving === undefined || accepted.destination === undefined
|
|
2564
|
+
? null
|
|
2565
|
+
: await adoptAccepted(other, local, arriving, {
|
|
2566
|
+
kind: 'accepted',
|
|
2567
|
+
...accepted.destination,
|
|
2568
|
+
});
|
|
2569
|
+
if (other !== undefined && arrived?.kind === 'refused' && continuing()) {
|
|
2570
|
+
refusedSource(other, arrived);
|
|
2571
|
+
}
|
|
2572
|
+
await unpair(local);
|
|
2573
|
+
return left;
|
|
2574
|
+
});
|
|
2575
|
+
submitLater(partner);
|
|
1067
2576
|
if (failed === null)
|
|
1068
2577
|
return true;
|
|
1069
2578
|
if (failed.kind === 'refused' && continuing())
|
|
1070
2579
|
refusedSource(room, failed);
|
|
1071
2580
|
return false;
|
|
1072
2581
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
2582
|
+
function queueIn(locator) {
|
|
2583
|
+
return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
2584
|
+
}
|
|
2585
|
+
function behindIn(locator, batch) {
|
|
2586
|
+
const held = replicas.get(locator)?.queue;
|
|
2587
|
+
if (held === undefined)
|
|
2588
|
+
return queuesWithoutReplica.get(locator) !== undefined;
|
|
2589
|
+
return held.findIndex((one) => one.id === batch.id) > 0;
|
|
2590
|
+
}
|
|
2591
|
+
function unpaired(skipped) {
|
|
2592
|
+
for (const locator of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
|
|
2593
|
+
for (const batch of queueIn(locator)) {
|
|
2594
|
+
const partner = partnerOf(batch, locator);
|
|
2595
|
+
if (partner === null || skipped.has(pendingKey(locator, batch.id)))
|
|
2596
|
+
continue;
|
|
2597
|
+
if (queueIn(partner).some((held) => held.id === batch.id))
|
|
2598
|
+
continue;
|
|
2599
|
+
if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
|
|
2600
|
+
continue;
|
|
2601
|
+
if (leavingIn(batch, locator) && !withdrawable(locator)(batch))
|
|
2602
|
+
continue;
|
|
2603
|
+
return { locator, batch };
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
return null;
|
|
2607
|
+
}
|
|
2608
|
+
async function unpair(local) {
|
|
2609
|
+
const handled = new Set();
|
|
2610
|
+
for (let held = unpaired(handled); held !== null && continuing(); held = unpaired(handled)) {
|
|
2611
|
+
const { locator, batch } = held;
|
|
2612
|
+
handled.add(pendingKey(locator, batch.id));
|
|
2613
|
+
journal.record(`outcome ${locator} ${batch.id} ${localSaves ? 'dropped' : 'unpaired'}`);
|
|
2614
|
+
const leaving = {
|
|
2615
|
+
batch,
|
|
2616
|
+
returned: !localSaves && leavingIn(batch, locator) ? returnedBy('withdrawn') : null,
|
|
2617
|
+
};
|
|
2618
|
+
const current = replicas.get(locator);
|
|
2619
|
+
if (current !== undefined) {
|
|
2620
|
+
const settled = await settle(local, locator, current, current.confirmed, current.queue.filter((one) => one.id !== batch.id), { leaving });
|
|
2621
|
+
publishChangeSet(settled());
|
|
2622
|
+
continue;
|
|
2623
|
+
}
|
|
2624
|
+
const queue = queueIn(locator).filter((one) => one.id !== batch.id);
|
|
2625
|
+
const written = new Map((leaving.returned === null
|
|
2626
|
+
? []
|
|
2627
|
+
: (pendings.get(pendingKey(locator, batch.id))?.drafts ?? [])).map((saved) => [
|
|
2628
|
+
saved.path,
|
|
2629
|
+
{
|
|
2630
|
+
locator,
|
|
2631
|
+
path: saved.path,
|
|
2632
|
+
revision: roomDraftRevision(),
|
|
2633
|
+
parts: returnedInto(roomDrafts.get(locator)?.get(saved.path)?.parts, saved.parts),
|
|
2634
|
+
returned: leaving.returned,
|
|
2635
|
+
},
|
|
2636
|
+
]));
|
|
2637
|
+
const carried = carriedBack(locator, leaving.returned === null ? [] : [batch], written);
|
|
2638
|
+
await local.keepAuthored({
|
|
2639
|
+
locator,
|
|
2640
|
+
queue,
|
|
2641
|
+
roomDrafts: [...written.values()],
|
|
2642
|
+
consumedRoomDrafts: carried.taken,
|
|
2643
|
+
}, ...carried.writes);
|
|
2644
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
2645
|
+
if (queue.length > 0)
|
|
2646
|
+
queuesWithoutReplica.set(locator, queue);
|
|
2647
|
+
else
|
|
2648
|
+
queuesWithoutReplica.delete(locator);
|
|
2649
|
+
for (const record of written.values())
|
|
2650
|
+
rememberRoomDraft(record);
|
|
2651
|
+
carried.remember();
|
|
2652
|
+
publishChangeSet(mergeChangeSets([
|
|
2653
|
+
changeSet(locator, null, null, {
|
|
2654
|
+
queue: true,
|
|
2655
|
+
drafts: [...written.keys(), ...carried.taken],
|
|
2656
|
+
}),
|
|
2657
|
+
...carried.changes,
|
|
2658
|
+
]));
|
|
2659
|
+
}
|
|
2660
|
+
if (handled.size === 0)
|
|
2661
|
+
return;
|
|
2662
|
+
moveTo({ ...status, ...attention() });
|
|
2663
|
+
for (const key of handled)
|
|
2664
|
+
submitLater(key.slice(0, key.indexOf('\u0000')));
|
|
2665
|
+
}
|
|
2666
|
+
async function discardHeld(head, room, local, signal) {
|
|
2667
|
+
const partner = partnerOf(head, room.locator);
|
|
2668
|
+
const other = partner === null ? undefined : discovered.get(partner);
|
|
2669
|
+
await discardPending(room, local, signal);
|
|
2670
|
+
if (other !== undefined && !signal.aborted)
|
|
2671
|
+
await discardPending(other, local, signal);
|
|
2672
|
+
}
|
|
2673
|
+
async function submitRoom(room, local, authorized, progress, signal) {
|
|
2674
|
+
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
1075
2675
|
const replica = replicas.get(room.locator);
|
|
1076
2676
|
const head = replica?.queue[0];
|
|
1077
2677
|
if (replica === undefined || head === undefined)
|
|
1078
|
-
return;
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
2678
|
+
return 'done';
|
|
2679
|
+
const state = pendings.get(pendingKey(room.locator, head.id));
|
|
2680
|
+
if (state?.accepted !== undefined)
|
|
2681
|
+
return 'done';
|
|
2682
|
+
const partner = partnerOf(head, room.locator);
|
|
2683
|
+
if (partner !== null && !leavingIn(head, room.locator)) {
|
|
2684
|
+
submitLater(partner);
|
|
2685
|
+
return 'done';
|
|
2686
|
+
}
|
|
2687
|
+
if (partner !== null && behindIn(partner, head)) {
|
|
2688
|
+
progress.complete = false;
|
|
2689
|
+
return 'done';
|
|
2690
|
+
}
|
|
1082
2691
|
const { submission, lost: earlier } = state ?? {
|
|
1083
2692
|
submission: { state: 'waiting' },
|
|
1084
2693
|
lost: false,
|
|
1085
2694
|
};
|
|
1086
2695
|
if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
|
|
1087
2696
|
progress.complete = false;
|
|
1088
|
-
return;
|
|
2697
|
+
return { at: Date.parse(submission.reset) };
|
|
1089
2698
|
}
|
|
1090
2699
|
if (writesHeld()) {
|
|
1091
2700
|
progress.complete = false;
|
|
1092
2701
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1093
|
-
return;
|
|
2702
|
+
return 'done';
|
|
1094
2703
|
}
|
|
1095
2704
|
const lost = earlier || submission.state === 'submitted';
|
|
2705
|
+
if (lost && pastHorizon(head.id)) {
|
|
2706
|
+
await discardHeld(head, room, local, signal);
|
|
2707
|
+
return 'done';
|
|
2708
|
+
}
|
|
1096
2709
|
let result;
|
|
1097
2710
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
1098
|
-
await exclusive(() => mark(local, room.locator, head
|
|
1099
|
-
if (discoveryPaused)
|
|
1100
|
-
return;
|
|
1101
|
-
if (writesHeld()) {
|
|
1102
|
-
await exclusive(() => mark(local, room.locator, head
|
|
2711
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
|
|
2712
|
+
if (discoveryPaused || fences.has(room.locator))
|
|
2713
|
+
return 'done';
|
|
2714
|
+
if (signal.aborted || writesHeld()) {
|
|
2715
|
+
await exclusive(() => mark(local, room.locator, head, submission, earlier));
|
|
2716
|
+
if (signal.aborted)
|
|
2717
|
+
return 'done';
|
|
1103
2718
|
progress.complete = false;
|
|
1104
2719
|
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1105
|
-
return;
|
|
2720
|
+
return 'done';
|
|
1106
2721
|
}
|
|
1107
2722
|
const opensBefore = openSignals;
|
|
1108
|
-
const
|
|
2723
|
+
const answeredHere = awaitingAnswer(room.locator);
|
|
2724
|
+
const answeredThere = partner === null ? () => { } : awaitingAnswer(partner);
|
|
2725
|
+
const answered = () => {
|
|
2726
|
+
answeredHere();
|
|
2727
|
+
answeredThere();
|
|
2728
|
+
};
|
|
1109
2729
|
try {
|
|
1110
2730
|
result = await options.adapter.mutate(authorized, {
|
|
1111
2731
|
locator: room.locator,
|
|
1112
2732
|
changeBatch: head,
|
|
2733
|
+
file: batchFile(head),
|
|
1113
2734
|
...(state?.upload === undefined ? {} : { upload: state.upload }),
|
|
1114
2735
|
}, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
|
|
1115
2736
|
}
|
|
@@ -1118,50 +2739,69 @@ export function composeRuntime(options) {
|
|
|
1118
2739
|
progress.complete = false;
|
|
1119
2740
|
if (continuing())
|
|
1120
2741
|
progress.reachable = false;
|
|
1121
|
-
return;
|
|
2742
|
+
return 'again';
|
|
2743
|
+
}
|
|
2744
|
+
if (signal.aborted) {
|
|
2745
|
+
answered();
|
|
2746
|
+
return 'done';
|
|
1122
2747
|
}
|
|
1123
2748
|
if (result.kind !== 'accepted' || !continuing())
|
|
1124
2749
|
answered();
|
|
1125
2750
|
if (!continuing())
|
|
1126
|
-
return;
|
|
2751
|
+
return 'done';
|
|
1127
2752
|
if (result.kind === 'transport-uncertainty') {
|
|
1128
2753
|
progress.complete = false;
|
|
1129
2754
|
progress.reachable = false;
|
|
1130
|
-
return;
|
|
2755
|
+
return 'again';
|
|
1131
2756
|
}
|
|
1132
2757
|
if (result.kind === 'room-not-readable') {
|
|
2758
|
+
const unread = discovered.get(result.locator) ?? room;
|
|
1133
2759
|
progress.complete = false;
|
|
1134
|
-
unreadable.add(
|
|
1135
|
-
list(
|
|
2760
|
+
unreadable.add(unread.locator);
|
|
2761
|
+
list(unread, false);
|
|
1136
2762
|
emitRooms();
|
|
1137
|
-
return;
|
|
2763
|
+
return 'done';
|
|
1138
2764
|
}
|
|
1139
2765
|
if (result.kind === 'revoked-authorization') {
|
|
1140
2766
|
await refuse('revoked-authorization');
|
|
1141
|
-
return;
|
|
2767
|
+
return 'done';
|
|
1142
2768
|
}
|
|
1143
2769
|
if (result.kind === 'scoped-refusal') {
|
|
1144
2770
|
await scoped(result, room, local);
|
|
1145
2771
|
progress.complete = false;
|
|
1146
|
-
return;
|
|
2772
|
+
return 'done';
|
|
1147
2773
|
}
|
|
1148
2774
|
if (result.kind === 'write-halted') {
|
|
1149
2775
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
1150
|
-
await exclusive(() => mark(local, room.locator, head
|
|
2776
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
|
|
1151
2777
|
if (serviceMode === 'open' && openSignals !== opensBefore)
|
|
1152
2778
|
continue;
|
|
1153
2779
|
haltedRefusal = true;
|
|
1154
2780
|
progress.complete = false;
|
|
1155
|
-
return;
|
|
2781
|
+
return 'done';
|
|
1156
2782
|
}
|
|
2783
|
+
const other = partner === null ? undefined : discovered.get(partner);
|
|
1157
2784
|
if (result.kind === 'conflicted' ||
|
|
1158
2785
|
result.kind === 'rejected' ||
|
|
1159
2786
|
result.kind === 'expired' ||
|
|
1160
2787
|
result.kind === 'retryable-refusal') {
|
|
2788
|
+
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
2789
|
+
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
2790
|
+
await discardHeld(head, room, local, signal);
|
|
2791
|
+
return 'done';
|
|
2792
|
+
}
|
|
1161
2793
|
if (result.kind === 'expired') {
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
2794
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
|
|
2795
|
+
const there = other === undefined ? null : await advance(other, local, authorized, continuing);
|
|
2796
|
+
if (other !== undefined && there !== null) {
|
|
2797
|
+
await withhold(there, other, local, progress);
|
|
2798
|
+
return 'done';
|
|
2799
|
+
}
|
|
2800
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
2801
|
+
if (withheld !== null) {
|
|
2802
|
+
await withhold(withheld, room, local, progress);
|
|
2803
|
+
return 'done';
|
|
2804
|
+
}
|
|
1165
2805
|
}
|
|
1166
2806
|
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
|
|
1167
2807
|
(result.kind === 'conflicted' && result.roomVersion !== undefined
|
|
@@ -1173,12 +2813,25 @@ export function composeRuntime(options) {
|
|
|
1173
2813
|
if (result.kind === 'retryable-refusal') {
|
|
1174
2814
|
const reset = result.reset;
|
|
1175
2815
|
progress.complete = false;
|
|
1176
|
-
await exclusive(() => mark(local, room.locator, head
|
|
1177
|
-
|
|
1178
|
-
return;
|
|
2816
|
+
await exclusive(() => mark(local, room.locator, head, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
2817
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
1179
2818
|
}
|
|
1180
2819
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
1181
2820
|
const left = leaving(head, result);
|
|
2821
|
+
const refreshed = async () => ({
|
|
2822
|
+
there: other === undefined ? null : await advance(other, local, authorized, continuing),
|
|
2823
|
+
here: await advance(room, local, authorized, continuing),
|
|
2824
|
+
});
|
|
2825
|
+
const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
|
|
2826
|
+
if (judged !== null && (judged.there !== null || judged.here !== null)) {
|
|
2827
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
|
|
2828
|
+
if (other !== undefined && judged.there !== null) {
|
|
2829
|
+
await withhold(judged.there, other, local, progress);
|
|
2830
|
+
}
|
|
2831
|
+
if (judged.here !== null)
|
|
2832
|
+
await withhold(judged.here, room, local, progress);
|
|
2833
|
+
return 'done';
|
|
2834
|
+
}
|
|
1182
2835
|
await exclusive(async () => {
|
|
1183
2836
|
if (!continuing())
|
|
1184
2837
|
return;
|
|
@@ -1186,37 +2839,88 @@ export function composeRuntime(options) {
|
|
|
1186
2839
|
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
1187
2840
|
return;
|
|
1188
2841
|
}
|
|
1189
|
-
const
|
|
1190
|
-
leaving: (result.kind === 'expired' &&
|
|
1191
|
-
(result.kind === 'conflicted' &&
|
|
2842
|
+
const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
2843
|
+
leaving: (result.kind === 'expired' && landedWhole(room.locator, head, current.confirmed)) ||
|
|
2844
|
+
(result.kind === 'conflicted' &&
|
|
2845
|
+
landedWhole(room.locator, head, current.confirmed, result.affected))
|
|
1192
2846
|
? { ...left, returned: null }
|
|
1193
2847
|
: left,
|
|
1194
2848
|
});
|
|
1195
|
-
publishChangeSet(
|
|
2849
|
+
publishChangeSet(settled());
|
|
2850
|
+
await unpair(local);
|
|
1196
2851
|
moveTo({ ...status, ...attention() });
|
|
1197
2852
|
});
|
|
1198
2853
|
if (result.kind === 'expired')
|
|
1199
2854
|
continue;
|
|
1200
|
-
const
|
|
1201
|
-
if (
|
|
1202
|
-
|
|
2855
|
+
const { there, here } = judged ?? (await refreshed());
|
|
2856
|
+
if (other !== undefined && there !== null)
|
|
2857
|
+
await withhold(there, other, local, progress);
|
|
2858
|
+
if (here !== null) {
|
|
2859
|
+
await withhold(here, room, local, progress);
|
|
2860
|
+
return 'done';
|
|
2861
|
+
}
|
|
1203
2862
|
continue;
|
|
1204
2863
|
}
|
|
1205
|
-
const kept = await
|
|
2864
|
+
const kept = await (partner === null
|
|
2865
|
+
? adopted(room, local, head, result)
|
|
2866
|
+
: adoptedMove(room, partner, local, head, result)).finally(answered);
|
|
1206
2867
|
if (!kept) {
|
|
1207
2868
|
progress.complete = false;
|
|
1208
|
-
return;
|
|
2869
|
+
return 'done';
|
|
1209
2870
|
}
|
|
1210
2871
|
}
|
|
2872
|
+
return 'done';
|
|
2873
|
+
}
|
|
2874
|
+
function pastHorizon(identity) {
|
|
2875
|
+
return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
|
|
2876
|
+
}
|
|
2877
|
+
async function discardPending(room, local, signal) {
|
|
2878
|
+
const { locator } = room;
|
|
2879
|
+
const discarded = await exclusive(async () => {
|
|
2880
|
+
if (fences.has(locator))
|
|
2881
|
+
return false;
|
|
2882
|
+
const before = replicas.get(locator);
|
|
2883
|
+
const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
2884
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
2885
|
+
lastFenced.set(locator, ++fencings);
|
|
2886
|
+
try {
|
|
2887
|
+
await local.discard(locator);
|
|
2888
|
+
}
|
|
2889
|
+
finally {
|
|
2890
|
+
fences.delete(locator);
|
|
2891
|
+
}
|
|
2892
|
+
for (const batch of queue)
|
|
2893
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
2894
|
+
queuesWithoutReplica.delete(locator);
|
|
2895
|
+
const after = before === undefined ? null : await stepped(replicating(before.confirmed, [], before));
|
|
2896
|
+
if (after !== null)
|
|
2897
|
+
replicas.set(locator, after);
|
|
2898
|
+
journal.record(`room discarded ${locator}`);
|
|
2899
|
+
publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
|
|
2900
|
+
moveTo({ ...status, ...attention() });
|
|
2901
|
+
return true;
|
|
2902
|
+
});
|
|
2903
|
+
if (!discarded)
|
|
2904
|
+
return;
|
|
2905
|
+
const progress = { complete: true, reachable: true };
|
|
2906
|
+
await roomTask(locator, async (live) => {
|
|
2907
|
+
await Promise.race([
|
|
2908
|
+
reload(room, local, progress, () => live() && !signal.aborted),
|
|
2909
|
+
abandonedOn(signal),
|
|
2910
|
+
]);
|
|
2911
|
+
});
|
|
2912
|
+
if (signal.aborted)
|
|
2913
|
+
return;
|
|
2914
|
+
await rest(progress);
|
|
1211
2915
|
}
|
|
1212
2916
|
function awaitingAnswer(locator) {
|
|
1213
2917
|
let release = () => { };
|
|
1214
2918
|
const awaited = new Promise((resolve) => {
|
|
1215
2919
|
release = resolve;
|
|
1216
2920
|
});
|
|
1217
|
-
answering.set(locator, awaited);
|
|
2921
|
+
answering.set(locator, { generation, answered: awaited });
|
|
1218
2922
|
return () => {
|
|
1219
|
-
if (answering.get(locator) === awaited)
|
|
2923
|
+
if (answering.get(locator)?.answered === awaited)
|
|
1220
2924
|
answering.delete(locator);
|
|
1221
2925
|
release();
|
|
1222
2926
|
};
|
|
@@ -1240,6 +2944,7 @@ export function composeRuntime(options) {
|
|
|
1240
2944
|
if (!continuing())
|
|
1241
2945
|
return;
|
|
1242
2946
|
const held = replicas.get(room.locator);
|
|
2947
|
+
let changed = null;
|
|
1243
2948
|
if (held !== undefined) {
|
|
1244
2949
|
const declared = {
|
|
1245
2950
|
...held.confirmed.record,
|
|
@@ -1249,29 +2954,32 @@ export function composeRuntime(options) {
|
|
|
1249
2954
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
1250
2955
|
};
|
|
1251
2956
|
const reclassified = sameMembership(declared, held.confirmed.record)
|
|
1252
|
-
?
|
|
1253
|
-
: readReclassified(declared, held.confirmed);
|
|
2957
|
+
? { kind: 'staged', confirmed: await confirmedOf(declared, held.confirmed) }
|
|
2958
|
+
: await readReclassified(declared, held.confirmed);
|
|
1254
2959
|
const record = reclassified.kind === 'staged'
|
|
1255
2960
|
? declared
|
|
1256
2961
|
: {
|
|
1257
2962
|
...declared,
|
|
1258
2963
|
typedSelection: held.confirmed.record.typedSelection,
|
|
1259
2964
|
};
|
|
1260
|
-
const read = reclassified.kind === 'deferred'
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
2965
|
+
const read = reclassified.kind === 'deferred'
|
|
2966
|
+
? { kind: 'staged', confirmed: await confirmedOf(record, held.confirmed) }
|
|
2967
|
+
: reclassified;
|
|
2968
|
+
await local.write({ next: record, previous: held.confirmed.record });
|
|
2969
|
+
const replica = read.kind === 'staged'
|
|
2970
|
+
? await stepped(replicating(read.confirmed, held.queue, held))
|
|
2971
|
+
: { ...held, confirmed: { ...held.confirmed, record } };
|
|
2972
|
+
await stepped(counting(replica));
|
|
2973
|
+
const set = await stepped(changing(room.locator, held, replica, { room: true }));
|
|
2974
|
+
replicas.set(room.locator, replica);
|
|
2975
|
+
changed = heldChange(room.locator, held, replica, set, { room: true });
|
|
1266
2976
|
if (read.kind === 'staged') {
|
|
1267
|
-
replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
|
|
1268
2977
|
if (declarationRefusals.delete(room.locator)) {
|
|
1269
2978
|
sourceRefusals.delete(room.locator);
|
|
1270
2979
|
unreadable.delete(room.locator);
|
|
1271
2980
|
}
|
|
1272
2981
|
}
|
|
1273
|
-
else
|
|
1274
|
-
replicas.set(room.locator, { ...held, confirmed: { ...held.confirmed, record } });
|
|
2982
|
+
else {
|
|
1275
2983
|
refusedSource(room, read);
|
|
1276
2984
|
declarationRefusals.add(room.locator);
|
|
1277
2985
|
}
|
|
@@ -1282,14 +2990,23 @@ export function composeRuntime(options) {
|
|
|
1282
2990
|
invalidModels.delete(key);
|
|
1283
2991
|
}
|
|
1284
2992
|
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
1285
|
-
if (
|
|
2993
|
+
if (changed === null)
|
|
1286
2994
|
emitRooms();
|
|
1287
2995
|
else
|
|
1288
|
-
publishChangeSet(
|
|
2996
|
+
publishChangeSet(changed);
|
|
1289
2997
|
});
|
|
1290
2998
|
}
|
|
1291
2999
|
async function scoped(result, room, local) {
|
|
1292
|
-
|
|
3000
|
+
if (result.scope === 'content' && !result.retryable) {
|
|
3001
|
+
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
3002
|
+
await loseRooms([room.locator]);
|
|
3003
|
+
return;
|
|
3004
|
+
}
|
|
3005
|
+
scopedRefusals.set(room.locator, {
|
|
3006
|
+
code: result.code,
|
|
3007
|
+
fields: result.fields,
|
|
3008
|
+
retryable: result.retryable,
|
|
3009
|
+
});
|
|
1293
3010
|
let scopedAway = room;
|
|
1294
3011
|
if (result.scope === 'model')
|
|
1295
3012
|
await retainModel(local, room, 'collection', {
|
|
@@ -1298,15 +3015,7 @@ export function composeRuntime(options) {
|
|
|
1298
3015
|
});
|
|
1299
3016
|
else if (result.scope !== 'operation') {
|
|
1300
3017
|
unreadable.add(room.locator);
|
|
1301
|
-
scopedAway = {
|
|
1302
|
-
...room,
|
|
1303
|
-
replica: { ...room.replica, admission: 'unavailable' },
|
|
1304
|
-
operations: {
|
|
1305
|
-
...room.operations,
|
|
1306
|
-
...(result.scope === 'content' ? { read: [] } : {}),
|
|
1307
|
-
...(result.scope === 'sync' ? { sync: [] } : {}),
|
|
1308
|
-
},
|
|
1309
|
-
};
|
|
3018
|
+
scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
|
|
1310
3019
|
await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
|
|
1311
3020
|
}
|
|
1312
3021
|
if (!continuing())
|
|
@@ -1326,13 +3035,14 @@ export function composeRuntime(options) {
|
|
|
1326
3035
|
roomVersion: record.roomVersion,
|
|
1327
3036
|
documents: record.documents,
|
|
1328
3037
|
revisions: record.revisions,
|
|
1329
|
-
attachments: record.attachments,
|
|
1330
3038
|
},
|
|
1331
3039
|
];
|
|
1332
3040
|
});
|
|
1333
3041
|
}
|
|
1334
|
-
async function synchronize(listed, local, progress, attempt) {
|
|
3042
|
+
async function synchronize(listed, local, progress, attempt, live) {
|
|
1335
3043
|
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
|
|
3044
|
+
if (!live())
|
|
3045
|
+
return;
|
|
1336
3046
|
const ready = listed.filter((_, index) => {
|
|
1337
3047
|
const outcome = prepared[index];
|
|
1338
3048
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
@@ -1341,9 +3051,10 @@ export function composeRuntime(options) {
|
|
|
1341
3051
|
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
1342
3052
|
handed = true;
|
|
1343
3053
|
}
|
|
3054
|
+
discovering?.release();
|
|
1344
3055
|
const loaded = await Promise.allSettled(ready
|
|
1345
3056
|
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1346
|
-
.map((room) => loadRoom(room, local, progress)));
|
|
3057
|
+
.map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
|
|
1347
3058
|
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1348
3059
|
if (failed !== undefined)
|
|
1349
3060
|
throw failed.reason;
|
|
@@ -1365,21 +3076,12 @@ export function composeRuntime(options) {
|
|
|
1365
3076
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1366
3077
|
if (discoveryPaused || authorization === undefined)
|
|
1367
3078
|
return false;
|
|
1368
|
-
|
|
1369
|
-
if (replicas.has(room.locator))
|
|
1370
|
-
progress.complete = false;
|
|
1371
|
-
return false;
|
|
1372
|
-
}
|
|
1373
|
-
return true;
|
|
3079
|
+
return room.replica.admission === 'admitted';
|
|
1374
3080
|
}
|
|
1375
|
-
async function caughtUp(room, local, progress) {
|
|
1376
|
-
|
|
1377
|
-
if (held === undefined || authorization === undefined)
|
|
3081
|
+
async function caughtUp(room, local, progress, live) {
|
|
3082
|
+
if (authorization === undefined || !live() || !behind(room))
|
|
1378
3083
|
return;
|
|
1379
|
-
const
|
|
1380
|
-
if (!behind)
|
|
1381
|
-
return;
|
|
1382
|
-
const withheld = await advance(room, local, authorization);
|
|
3084
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1383
3085
|
if (withheld !== null)
|
|
1384
3086
|
await withhold(withheld, room, local, progress);
|
|
1385
3087
|
}
|
|
@@ -1389,47 +3091,57 @@ export function composeRuntime(options) {
|
|
|
1389
3091
|
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1390
3092
|
});
|
|
1391
3093
|
}
|
|
1392
|
-
async function loadRoom(room, local, progress) {
|
|
1393
|
-
if (
|
|
3094
|
+
async function loadRoom(room, local, progress, live) {
|
|
3095
|
+
if (!live() || authorization === undefined)
|
|
1394
3096
|
return;
|
|
1395
3097
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1396
|
-
await reload(room, local, progress);
|
|
3098
|
+
await reload(room, local, progress, live);
|
|
1397
3099
|
return;
|
|
1398
3100
|
}
|
|
1399
|
-
const withheld = await advance(room, local, authorization);
|
|
3101
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1400
3102
|
if (withheld !== null) {
|
|
1401
3103
|
await withhold(withheld, room, local, progress);
|
|
1402
3104
|
return;
|
|
1403
3105
|
}
|
|
1404
|
-
await caughtUp(room, local, progress);
|
|
3106
|
+
await caughtUp(room, local, progress, live);
|
|
1405
3107
|
}
|
|
1406
|
-
async function reload(room, local, progress) {
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
3108
|
+
async function reload(room, local, progress, live) {
|
|
3109
|
+
try {
|
|
3110
|
+
if (live())
|
|
3111
|
+
reloadsDue.delete(room.locator);
|
|
3112
|
+
if (!live() || authorization === undefined)
|
|
3113
|
+
return;
|
|
3114
|
+
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
3115
|
+
if (!live())
|
|
3116
|
+
return;
|
|
3117
|
+
if (bootstrapped.kind !== 'staged') {
|
|
3118
|
+
await withhold(bootstrapped, room, local, progress);
|
|
3119
|
+
return;
|
|
3120
|
+
}
|
|
3121
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
3122
|
+
await caughtUp(room, local, progress, live);
|
|
3123
|
+
}
|
|
3124
|
+
finally {
|
|
3125
|
+
caughtUpTo(room.locator);
|
|
1415
3126
|
}
|
|
1416
|
-
await publish(local, room, bootstrapped.confirmed);
|
|
1417
|
-
await caughtUp(room, local, progress);
|
|
1418
3127
|
}
|
|
1419
|
-
async function discoverRooms(local, progress, attempt) {
|
|
3128
|
+
async function discoverRooms(local, progress, attempt, live, scope) {
|
|
1420
3129
|
if (closed || authorization === undefined) {
|
|
1421
3130
|
notifyReady();
|
|
1422
3131
|
return;
|
|
1423
3132
|
}
|
|
1424
3133
|
moveTo(transferring(status));
|
|
3134
|
+
discoveries += 1;
|
|
3135
|
+
const started = discoveries;
|
|
1425
3136
|
let result;
|
|
1426
3137
|
try {
|
|
1427
|
-
result = await options.adapter.discover(authorization);
|
|
3138
|
+
result = await options.adapter.discover(authorization, scope);
|
|
1428
3139
|
}
|
|
1429
3140
|
catch {
|
|
1430
3141
|
result = { kind: 'transport-uncertainty' };
|
|
1431
3142
|
}
|
|
1432
|
-
if (
|
|
3143
|
+
if (!live()) {
|
|
3144
|
+
progress.complete = false;
|
|
1433
3145
|
notifyReady();
|
|
1434
3146
|
return;
|
|
1435
3147
|
}
|
|
@@ -1448,7 +3160,11 @@ export function composeRuntime(options) {
|
|
|
1448
3160
|
}
|
|
1449
3161
|
if (result.kind === 'contract-unavailable') {
|
|
1450
3162
|
journal.record(`connection contract unavailable ${result.code}`);
|
|
1451
|
-
await refuse('missing-required-guarantee', {
|
|
3163
|
+
await refuse('missing-required-guarantee', {
|
|
3164
|
+
code: result.code,
|
|
3165
|
+
fields: result.fields,
|
|
3166
|
+
retryable: result.retryable,
|
|
3167
|
+
});
|
|
1452
3168
|
notifyReady();
|
|
1453
3169
|
return;
|
|
1454
3170
|
}
|
|
@@ -1459,12 +3175,13 @@ export function composeRuntime(options) {
|
|
|
1459
3175
|
}
|
|
1460
3176
|
for (const room of result.rooms) {
|
|
1461
3177
|
if (room.operations === undefined ||
|
|
1462
|
-
!['
|
|
3178
|
+
!['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
1463
3179
|
room.operations[operation].every((scope) => typeof scope === 'string')) ||
|
|
1464
3180
|
room.replica === undefined ||
|
|
1465
3181
|
typeof room.replica.selection !== 'string' ||
|
|
1466
3182
|
room.replica.selection.length === 0 ||
|
|
1467
|
-
!['admitted', 'unavailable'].includes(room.replica.admission)
|
|
3183
|
+
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
3184
|
+
!['locked', 'open'].includes(room.replica.offline)) {
|
|
1468
3185
|
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
1469
3186
|
await refuse('missing-required-guarantee');
|
|
1470
3187
|
notifyReady();
|
|
@@ -1483,14 +3200,20 @@ export function composeRuntime(options) {
|
|
|
1483
3200
|
}
|
|
1484
3201
|
const previousCapabilities = capabilities;
|
|
1485
3202
|
capabilities = result.capabilities;
|
|
3203
|
+
outcomeRetention = result.retention.outcome;
|
|
1486
3204
|
notifyReady();
|
|
1487
3205
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1488
3206
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
1489
3207
|
limits = result.providerLimits;
|
|
1490
3208
|
await local.declare(limits, capabilities);
|
|
1491
3209
|
}
|
|
1492
|
-
|
|
1493
|
-
|
|
3210
|
+
const signedIn = result.member ?? null;
|
|
3211
|
+
if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
|
|
3212
|
+
await local.keepMember(signedIn);
|
|
3213
|
+
member = signedIn;
|
|
3214
|
+
emitMember();
|
|
3215
|
+
}
|
|
3216
|
+
await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
|
|
1494
3217
|
}
|
|
1495
3218
|
async function ensureAuthorization() {
|
|
1496
3219
|
if (authorization !== undefined)
|
|
@@ -1504,32 +3227,64 @@ export function composeRuntime(options) {
|
|
|
1504
3227
|
throw error;
|
|
1505
3228
|
}
|
|
1506
3229
|
}
|
|
1507
|
-
function
|
|
3230
|
+
async function admittedRead(work) {
|
|
3231
|
+
if (roomReads.generation !== generation) {
|
|
3232
|
+
roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
3233
|
+
}
|
|
3234
|
+
const reads = roomReads;
|
|
3235
|
+
while (reads.free === 0)
|
|
3236
|
+
await new Promise((admit) => reads.waiting.push(admit));
|
|
3237
|
+
reads.free -= 1;
|
|
3238
|
+
try {
|
|
3239
|
+
await work();
|
|
3240
|
+
}
|
|
3241
|
+
finally {
|
|
3242
|
+
reads.free += 1;
|
|
3243
|
+
reads.waiting.shift()?.();
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
function roomTask(locator, work) {
|
|
3247
|
+
return task(roomLane(locator), (live) => admittedRead(() => work(live)));
|
|
3248
|
+
}
|
|
3249
|
+
function task(lane, work) {
|
|
1508
3250
|
if (stopping)
|
|
1509
|
-
return
|
|
1510
|
-
|
|
1511
|
-
|
|
3251
|
+
return Promise.resolve();
|
|
3252
|
+
const held = lanes.get(lane);
|
|
3253
|
+
const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
|
|
3254
|
+
const turn = current.generation;
|
|
3255
|
+
const live = () => turn === generation && continuing();
|
|
3256
|
+
const run = current.chain.then(() => work(live)).catch(() => undefined);
|
|
3257
|
+
current.chain = run;
|
|
3258
|
+
lanes.set(lane, current);
|
|
3259
|
+
working.add(run);
|
|
3260
|
+
void run.finally(() => working.delete(run));
|
|
3261
|
+
return run;
|
|
1512
3262
|
}
|
|
1513
3263
|
function sync(attempt) {
|
|
1514
|
-
return task(async () => {
|
|
1515
|
-
if (
|
|
3264
|
+
return task(OPENING_LANE, async (live) => {
|
|
3265
|
+
if (!live()) {
|
|
1516
3266
|
notifyReady();
|
|
1517
3267
|
return;
|
|
1518
3268
|
}
|
|
1519
3269
|
const progress = { complete: true, reachable: true };
|
|
1520
|
-
|
|
3270
|
+
const gate = discover();
|
|
3271
|
+
if (attempt === 'opened')
|
|
3272
|
+
opened = generation;
|
|
3273
|
+
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
3274
|
+
if (attempt === 'opened')
|
|
3275
|
+
streamed = progress;
|
|
1521
3276
|
complete = false;
|
|
1522
3277
|
recorded = false;
|
|
1523
3278
|
moveTo(transferring(status));
|
|
1524
3279
|
handed = false;
|
|
1525
3280
|
try {
|
|
1526
3281
|
const local = await openLocal();
|
|
1527
|
-
if (
|
|
3282
|
+
if (!live())
|
|
1528
3283
|
return;
|
|
1529
3284
|
await ensureAuthorization();
|
|
1530
|
-
if (
|
|
3285
|
+
if (!live())
|
|
1531
3286
|
return;
|
|
1532
|
-
await discoverRooms(local, progress, attempt);
|
|
3287
|
+
await discoverRooms(local, progress, attempt, live);
|
|
1533
3288
|
}
|
|
1534
3289
|
catch {
|
|
1535
3290
|
progress.complete = false;
|
|
@@ -1537,50 +3292,105 @@ export function composeRuntime(options) {
|
|
|
1537
3292
|
progress.reachable = false;
|
|
1538
3293
|
}
|
|
1539
3294
|
finally {
|
|
3295
|
+
gate.release();
|
|
1540
3296
|
notifyReady();
|
|
1541
3297
|
}
|
|
3298
|
+
if (!live())
|
|
3299
|
+
return;
|
|
1542
3300
|
if (attempt !== 'opened') {
|
|
1543
|
-
await finishOpening(progress);
|
|
3301
|
+
await finishOpening(progress, live);
|
|
1544
3302
|
return;
|
|
1545
3303
|
}
|
|
1546
3304
|
if (handed || closed || progress.reachable)
|
|
1547
3305
|
return;
|
|
1548
3306
|
underway = null;
|
|
3307
|
+
opened = null;
|
|
1549
3308
|
journal.record('connection reopen');
|
|
1550
|
-
await rest(progress);
|
|
3309
|
+
await rest(progress, live);
|
|
1551
3310
|
options.adapter.roomStream?.reopen();
|
|
1552
3311
|
});
|
|
1553
3312
|
}
|
|
1554
|
-
function
|
|
3313
|
+
function rediscover(scope) {
|
|
3314
|
+
return task(OPENING_LANE, async (live) => {
|
|
3315
|
+
if (!live() || !socketOpen)
|
|
3316
|
+
return;
|
|
3317
|
+
const progress = { complete: true, reachable: true };
|
|
3318
|
+
const gate = discover();
|
|
3319
|
+
moveTo(transferring(status));
|
|
3320
|
+
try {
|
|
3321
|
+
const local = await openLocal();
|
|
3322
|
+
if (!live())
|
|
3323
|
+
return;
|
|
3324
|
+
await ensureAuthorization();
|
|
3325
|
+
if (!live())
|
|
3326
|
+
return;
|
|
3327
|
+
await discoverRooms(local, progress, 'authorized', live, scope);
|
|
3328
|
+
}
|
|
3329
|
+
catch {
|
|
3330
|
+
progress.complete = false;
|
|
3331
|
+
}
|
|
3332
|
+
finally {
|
|
3333
|
+
gate.release();
|
|
3334
|
+
notifyReady();
|
|
3335
|
+
}
|
|
3336
|
+
if (!live())
|
|
3337
|
+
return;
|
|
3338
|
+
if (!progress.complete)
|
|
3339
|
+
complete = false;
|
|
3340
|
+
await rest(progress, live);
|
|
3341
|
+
});
|
|
3342
|
+
}
|
|
3343
|
+
function closedSocket(revoked, lost) {
|
|
1555
3344
|
const opened = socketOpen;
|
|
1556
3345
|
socketOpen = false;
|
|
3346
|
+
connectionLost();
|
|
3347
|
+
if (lost) {
|
|
3348
|
+
reads.lost();
|
|
3349
|
+
conversations.lost();
|
|
3350
|
+
}
|
|
1557
3351
|
if (revoked) {
|
|
1558
3352
|
void refuse('revoked-authorization');
|
|
1559
3353
|
return;
|
|
1560
3354
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
3355
|
+
complete = false;
|
|
3356
|
+
if (streamed !== null)
|
|
3357
|
+
streamed.complete = false;
|
|
3358
|
+
moveTo({
|
|
3359
|
+
...status,
|
|
3360
|
+
state: status.state === 'synced' ? 'syncing' : status.state,
|
|
3361
|
+
providerUnreachable: status.providerUnreachable || !opened,
|
|
3362
|
+
});
|
|
1564
3363
|
}
|
|
1565
3364
|
function applyOpening(expired) {
|
|
1566
|
-
|
|
1567
|
-
|
|
3365
|
+
const progress = underway?.generation === generation ? underway.progress : null;
|
|
3366
|
+
const cycle = progress ?? { complete: true, reachable: true };
|
|
3367
|
+
if (opened === generation)
|
|
3368
|
+
opened = null;
|
|
3369
|
+
const reloading = progress === null ? [] : owedReloads(expired);
|
|
3370
|
+
const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
|
|
3371
|
+
return task(OPENING_LANE, async (live) => {
|
|
1568
3372
|
underway = null;
|
|
1569
|
-
if (
|
|
3373
|
+
if (!live())
|
|
1570
3374
|
return;
|
|
1571
3375
|
if (progress === null) {
|
|
1572
|
-
await
|
|
3376
|
+
await replacing;
|
|
3377
|
+
await rest(cycle, live);
|
|
1573
3378
|
return;
|
|
1574
3379
|
}
|
|
3380
|
+
await roomsCarried([...expired, ...reloading]);
|
|
3381
|
+
if (!live())
|
|
3382
|
+
return;
|
|
1575
3383
|
try {
|
|
1576
3384
|
const local = await openLocal();
|
|
1577
|
-
const
|
|
1578
|
-
const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
|
|
1579
|
-
const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
|
|
3385
|
+
const reloaded = await replacing;
|
|
1580
3386
|
if (progress.reachable) {
|
|
1581
|
-
for (const room of
|
|
1582
|
-
if (
|
|
3387
|
+
for (const room of discovered.values()) {
|
|
3388
|
+
if (replicas.has(room.locator) &&
|
|
3389
|
+
!expired.includes(room.locator) &&
|
|
3390
|
+
!reloading.includes(room.locator)) {
|
|
1583
3391
|
await confirmModels(room, local);
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
1584
3394
|
}
|
|
1585
3395
|
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1586
3396
|
if (failed !== undefined)
|
|
@@ -1590,21 +3400,36 @@ export function composeRuntime(options) {
|
|
|
1590
3400
|
progress.complete = false;
|
|
1591
3401
|
progress.reachable = false;
|
|
1592
3402
|
}
|
|
1593
|
-
|
|
3403
|
+
if (live())
|
|
3404
|
+
await finishOpening(progress, live);
|
|
1594
3405
|
});
|
|
1595
3406
|
}
|
|
1596
|
-
|
|
1597
|
-
const
|
|
3407
|
+
function owedReloads(expired) {
|
|
3408
|
+
const reloading = [...discovered.values()]
|
|
3409
|
+
.filter((room) => replicas.has(room.locator) &&
|
|
3410
|
+
!expired.includes(room.locator) &&
|
|
3411
|
+
(owed.has(room.locator) || revoked(room.locator)))
|
|
3412
|
+
.map((room) => room.locator);
|
|
3413
|
+
owed.clear();
|
|
3414
|
+
for (const locator of reloading)
|
|
3415
|
+
catching.add(locator);
|
|
3416
|
+
readSide.recover();
|
|
3417
|
+
return reloading;
|
|
3418
|
+
}
|
|
3419
|
+
async function reloadNow(replaced, progress) {
|
|
3420
|
+
const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
|
|
3421
|
+
for (const room of reloaded)
|
|
3422
|
+
reloadsDue.set(room.locator, generation);
|
|
1598
3423
|
try {
|
|
1599
3424
|
const local = await openLocal();
|
|
1600
|
-
await Promise.allSettled(
|
|
1601
|
-
.filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
|
|
1602
|
-
.map((room) => reload(room, local, progress)));
|
|
3425
|
+
return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
1603
3426
|
}
|
|
1604
3427
|
catch {
|
|
3428
|
+
for (const room of reloaded)
|
|
3429
|
+
reloadsDue.delete(room.locator);
|
|
1605
3430
|
progress.reachable = false;
|
|
3431
|
+
return [];
|
|
1606
3432
|
}
|
|
1607
|
-
await rest(progress);
|
|
1608
3433
|
}
|
|
1609
3434
|
async function confirmModels(room, local) {
|
|
1610
3435
|
for (const attached of ['collection', 'typed-selection']) {
|
|
@@ -1615,38 +3440,96 @@ export function composeRuntime(options) {
|
|
|
1615
3440
|
}
|
|
1616
3441
|
}
|
|
1617
3442
|
function submitLater(locator) {
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
3443
|
+
if (localSaves)
|
|
3444
|
+
return;
|
|
3445
|
+
void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
|
|
3446
|
+
}
|
|
3447
|
+
async function submitPass(locator, signal) {
|
|
3448
|
+
const room = discovered.get(locator);
|
|
3449
|
+
if (closed || stopping || discoveryPaused || room === undefined)
|
|
3450
|
+
return 'done';
|
|
3451
|
+
const progress = { complete: true, reachable: true };
|
|
3452
|
+
let next;
|
|
3453
|
+
try {
|
|
3454
|
+
await ensureAuthorization();
|
|
3455
|
+
if (authorization === undefined)
|
|
3456
|
+
return 'done';
|
|
3457
|
+
next = await submitRoom(room, await openLocal(), authorization, progress, signal);
|
|
3458
|
+
}
|
|
3459
|
+
catch {
|
|
3460
|
+
progress.reachable = false;
|
|
3461
|
+
next = 'again';
|
|
3462
|
+
}
|
|
3463
|
+
if (signal.aborted)
|
|
3464
|
+
return 'done';
|
|
3465
|
+
await rest(progress);
|
|
3466
|
+
return next === 'again' && !socketOpen ? 'done' : next;
|
|
3467
|
+
}
|
|
3468
|
+
function repairLater(locator, when) {
|
|
3469
|
+
if (!socketOpen || repairing.has(locator))
|
|
1621
3470
|
return;
|
|
3471
|
+
void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
|
|
3472
|
+
}
|
|
3473
|
+
async function repairPass(locator, signal) {
|
|
3474
|
+
const room = discovered.get(locator);
|
|
3475
|
+
if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
|
|
3476
|
+
return 'done';
|
|
3477
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
3478
|
+
return 'done';
|
|
3479
|
+
const progress = { complete: true, reachable: true };
|
|
3480
|
+
repairing.add(locator);
|
|
3481
|
+
try {
|
|
3482
|
+
const local = await openLocal();
|
|
3483
|
+
await roomTask(locator, async (live) => {
|
|
3484
|
+
const current = () => live() && !signal.aborted;
|
|
3485
|
+
if (!current() || authorization === undefined)
|
|
3486
|
+
return;
|
|
3487
|
+
const withheld = await Promise.race([
|
|
3488
|
+
advance(room, local, authorization, current),
|
|
3489
|
+
abandonedOn(signal),
|
|
3490
|
+
]);
|
|
3491
|
+
if (withheld !== null && current())
|
|
3492
|
+
await withhold(withheld, room, local, progress);
|
|
3493
|
+
});
|
|
1622
3494
|
}
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
3495
|
+
catch {
|
|
3496
|
+
stale.add(locator);
|
|
3497
|
+
progress.reachable = false;
|
|
3498
|
+
}
|
|
3499
|
+
finally {
|
|
3500
|
+
repairing.delete(locator);
|
|
3501
|
+
}
|
|
3502
|
+
if (signal.aborted)
|
|
3503
|
+
return 'done';
|
|
3504
|
+
await rest(progress);
|
|
3505
|
+
if (!stale.has(locator)) {
|
|
3506
|
+
if ((replicas.get(locator)?.queue.length ?? 0) > 0)
|
|
3507
|
+
submitLater(locator);
|
|
3508
|
+
return 'done';
|
|
3509
|
+
}
|
|
3510
|
+
if (!repairable.has(locator))
|
|
3511
|
+
return 'done';
|
|
3512
|
+
const reset = repairable.get(locator);
|
|
3513
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
3514
|
+
}
|
|
3515
|
+
function resendLater() {
|
|
3516
|
+
if (!socketOpen)
|
|
3517
|
+
return;
|
|
3518
|
+
void recovery.attempt('resend', resendPass, { again: true });
|
|
3519
|
+
}
|
|
3520
|
+
async function resendPass(signal) {
|
|
3521
|
+
if (!socketOpen || closed || stopping || discoveryPaused)
|
|
3522
|
+
return 'done';
|
|
3523
|
+
const reading = { complete: true, reachable: true };
|
|
3524
|
+
try {
|
|
3525
|
+
await conversations.resend(reading);
|
|
3526
|
+
}
|
|
3527
|
+
catch {
|
|
3528
|
+
reading.reachable = false;
|
|
3529
|
+
}
|
|
3530
|
+
if (signal.aborted)
|
|
3531
|
+
return 'done';
|
|
3532
|
+
return reading.reachable && reading.complete ? 'done' : 'again';
|
|
1650
3533
|
}
|
|
1651
3534
|
function submitAll() {
|
|
1652
3535
|
for (const [locator, replica] of replicas) {
|
|
@@ -1654,31 +3537,43 @@ export function composeRuntime(options) {
|
|
|
1654
3537
|
submitLater(locator);
|
|
1655
3538
|
}
|
|
1656
3539
|
}
|
|
1657
|
-
async function finishOpening(progress) {
|
|
3540
|
+
async function finishOpening(progress, live) {
|
|
1658
3541
|
try {
|
|
1659
|
-
if (
|
|
3542
|
+
if (!live()) {
|
|
1660
3543
|
progress.complete = false;
|
|
1661
3544
|
return;
|
|
1662
3545
|
}
|
|
3546
|
+
reads.applied();
|
|
1663
3547
|
submitAll();
|
|
3548
|
+
for (const locator of stale)
|
|
3549
|
+
if (repairable.has(locator))
|
|
3550
|
+
repairLater(locator, {});
|
|
1664
3551
|
await conversations.reconcile();
|
|
1665
3552
|
if (status.providerUnreachable)
|
|
1666
3553
|
conversations.reconnected();
|
|
1667
3554
|
const reading = { complete: true, reachable: true };
|
|
1668
3555
|
await conversations.catchUp(reading);
|
|
1669
|
-
if (
|
|
3556
|
+
if (progress.reachable && reading.reachable)
|
|
3557
|
+
await conversations.resend(reading);
|
|
3558
|
+
if (progress.reachable)
|
|
3559
|
+
void recovery.attempt('calls', resendCalls);
|
|
3560
|
+
if (!reading.reachable) {
|
|
1670
3561
|
progress.reachable = false;
|
|
3562
|
+
resendLater();
|
|
3563
|
+
}
|
|
1671
3564
|
}
|
|
1672
3565
|
catch {
|
|
1673
3566
|
progress.complete = false;
|
|
1674
3567
|
}
|
|
1675
3568
|
finally {
|
|
1676
|
-
|
|
1677
|
-
|
|
3569
|
+
if (live()) {
|
|
3570
|
+
complete = progress.complete;
|
|
3571
|
+
await rest(progress, live);
|
|
3572
|
+
}
|
|
1678
3573
|
}
|
|
1679
3574
|
}
|
|
1680
|
-
async function rest(progress) {
|
|
1681
|
-
if (
|
|
3575
|
+
async function rest(progress, live = () => !closed) {
|
|
3576
|
+
if (!live())
|
|
1682
3577
|
return;
|
|
1683
3578
|
let lastCompleted = status.lastCompleted;
|
|
1684
3579
|
let synced = complete && stale.size === 0 && !pending();
|
|
@@ -1686,6 +3581,8 @@ export function composeRuntime(options) {
|
|
|
1686
3581
|
const completedAt = new Date().toISOString();
|
|
1687
3582
|
try {
|
|
1688
3583
|
await (await openLocal()).complete(completedAt);
|
|
3584
|
+
if (!live())
|
|
3585
|
+
return;
|
|
1689
3586
|
recorded = true;
|
|
1690
3587
|
lastCompleted = completedAt;
|
|
1691
3588
|
}
|
|
@@ -1694,17 +3591,29 @@ export function composeRuntime(options) {
|
|
|
1694
3591
|
}
|
|
1695
3592
|
}
|
|
1696
3593
|
moveTo({
|
|
1697
|
-
state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
|
|
3594
|
+
state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
|
|
1698
3595
|
lastCompleted,
|
|
1699
3596
|
providerUnreachable: !progress.reachable,
|
|
1700
3597
|
...attention(),
|
|
1701
3598
|
});
|
|
1702
3599
|
}
|
|
3600
|
+
function discover() {
|
|
3601
|
+
let release = () => { };
|
|
3602
|
+
const done = new Promise((resolve) => {
|
|
3603
|
+
release = resolve;
|
|
3604
|
+
});
|
|
3605
|
+
discovering = { generation, done, release };
|
|
3606
|
+
return discovering;
|
|
3607
|
+
}
|
|
1703
3608
|
function applyRoom(locator, roomVersion) {
|
|
1704
3609
|
announced.set(locator, roomVersion);
|
|
1705
|
-
return task(async () => {
|
|
1706
|
-
|
|
1707
|
-
|
|
3610
|
+
return task(roomLane(locator), async (live) => {
|
|
3611
|
+
if (discovering?.generation === generation)
|
|
3612
|
+
await discovering.done;
|
|
3613
|
+
const saving = answering.get(locator);
|
|
3614
|
+
if (saving?.generation === generation)
|
|
3615
|
+
await saving.answered;
|
|
3616
|
+
if (!live() || authorization === undefined)
|
|
1708
3617
|
return;
|
|
1709
3618
|
const room = discovered.get(locator);
|
|
1710
3619
|
const held = replicas.get(locator);
|
|
@@ -1715,25 +3624,41 @@ export function composeRuntime(options) {
|
|
|
1715
3624
|
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
1716
3625
|
return;
|
|
1717
3626
|
}
|
|
1718
|
-
const during = underway;
|
|
3627
|
+
const during = underway?.generation === generation ? underway.progress : null;
|
|
1719
3628
|
const progress = during ?? { complete: true, reachable: true };
|
|
1720
3629
|
if (during === null)
|
|
1721
3630
|
moveTo(transferring(status));
|
|
1722
3631
|
try {
|
|
1723
3632
|
const local = await openLocal();
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
3633
|
+
await admittedRead(async () => {
|
|
3634
|
+
if (!live() || authorization === undefined)
|
|
3635
|
+
return;
|
|
3636
|
+
const withheld = await advance(room, local, authorization, live);
|
|
3637
|
+
if (withheld !== null)
|
|
3638
|
+
await withhold(withheld, room, local, progress);
|
|
3639
|
+
});
|
|
1727
3640
|
}
|
|
1728
3641
|
catch {
|
|
1729
3642
|
stale.add(locator);
|
|
1730
3643
|
}
|
|
1731
3644
|
finally {
|
|
1732
3645
|
if (during === null)
|
|
1733
|
-
await rest(progress);
|
|
3646
|
+
await rest(progress, live);
|
|
1734
3647
|
}
|
|
1735
3648
|
});
|
|
1736
3649
|
}
|
|
3650
|
+
function roomsCarried(replacing) {
|
|
3651
|
+
const replaced = new Set(replacing.map(roomLane));
|
|
3652
|
+
return Promise.all([...lanes]
|
|
3653
|
+
.filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
|
|
3654
|
+
.map(([, held]) => held.chain));
|
|
3655
|
+
}
|
|
3656
|
+
const receipts = {
|
|
3657
|
+
heads: async () => (await openLocal()).receivedHeads(),
|
|
3658
|
+
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
3659
|
+
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
3660
|
+
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
3661
|
+
};
|
|
1737
3662
|
function observeStream() {
|
|
1738
3663
|
const stream = options.adapter.roomStream;
|
|
1739
3664
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -1743,15 +3668,22 @@ export function composeRuntime(options) {
|
|
|
1743
3668
|
return;
|
|
1744
3669
|
if (event.kind === 'opened') {
|
|
1745
3670
|
socketOpen = true;
|
|
3671
|
+
connectionLive();
|
|
3672
|
+
generation += 1;
|
|
3673
|
+
reads.opened();
|
|
3674
|
+
conversations.reopened();
|
|
3675
|
+
recovery.resume();
|
|
1746
3676
|
void sync('opened');
|
|
1747
3677
|
}
|
|
1748
3678
|
else if (event.kind === 'closed')
|
|
1749
|
-
closedSocket(event.revoked);
|
|
3679
|
+
closedSocket(event.revoked, event.lost);
|
|
1750
3680
|
else if (event.kind === 'opening')
|
|
1751
3681
|
void applyOpening(event.expired);
|
|
3682
|
+
else if (event.kind === 'authority-changed')
|
|
3683
|
+
void rediscover(event.scope);
|
|
1752
3684
|
else
|
|
1753
3685
|
void applyRoom(event.locator, event.roomVersion);
|
|
1754
|
-
});
|
|
3686
|
+
}, receipts);
|
|
1755
3687
|
}
|
|
1756
3688
|
function writesHeld() {
|
|
1757
3689
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -1766,6 +3698,7 @@ export function composeRuntime(options) {
|
|
|
1766
3698
|
haltedRefusal = false;
|
|
1767
3699
|
if (mode !== serviceMode) {
|
|
1768
3700
|
serviceMode = mode;
|
|
3701
|
+
queueReadiness();
|
|
1769
3702
|
journal.record(`service mode ${mode}`);
|
|
1770
3703
|
for (const listener of serviceModeListeners)
|
|
1771
3704
|
listener(mode);
|
|
@@ -1778,6 +3711,8 @@ export function composeRuntime(options) {
|
|
|
1778
3711
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
1779
3712
|
return;
|
|
1780
3713
|
withdrawConversations = stream.observe((event) => {
|
|
3714
|
+
if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
|
|
3715
|
+
queueReadiness();
|
|
1781
3716
|
if (conversations.receive(event)) {
|
|
1782
3717
|
void ready.then(scheduleCurrency);
|
|
1783
3718
|
return;
|
|
@@ -1789,46 +3724,47 @@ export function composeRuntime(options) {
|
|
|
1789
3724
|
});
|
|
1790
3725
|
}
|
|
1791
3726
|
function scheduleCurrency() {
|
|
1792
|
-
if (currencyQueued || closed || stopping)
|
|
3727
|
+
if (currencyQueued === generation || closed || stopping)
|
|
1793
3728
|
return;
|
|
1794
|
-
currencyQueued =
|
|
1795
|
-
void task(runCurrency);
|
|
3729
|
+
currencyQueued = generation;
|
|
3730
|
+
void task(CONVERSATION_LANE, runCurrency);
|
|
1796
3731
|
}
|
|
1797
|
-
async function runCurrency() {
|
|
1798
|
-
currencyQueued
|
|
1799
|
-
|
|
3732
|
+
async function runCurrency(live) {
|
|
3733
|
+
if (currencyQueued === generation)
|
|
3734
|
+
currencyQueued = null;
|
|
3735
|
+
if (!live())
|
|
1800
3736
|
return;
|
|
1801
3737
|
await conversations.reconcile();
|
|
1802
3738
|
if (conversations.current()) {
|
|
1803
3739
|
show();
|
|
1804
3740
|
return;
|
|
1805
3741
|
}
|
|
1806
|
-
const
|
|
1807
|
-
|
|
1808
|
-
moveTo(transferring(resting));
|
|
3742
|
+
const progress = { complete: false, reachable: !status.providerUnreachable };
|
|
3743
|
+
moveTo(transferring(status));
|
|
1809
3744
|
try {
|
|
1810
3745
|
await conversations.catchUp(progress);
|
|
1811
3746
|
}
|
|
1812
3747
|
catch {
|
|
1813
3748
|
progress.reachable = false;
|
|
1814
3749
|
}
|
|
1815
|
-
|
|
1816
|
-
if (!closed) {
|
|
1817
|
-
moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
3750
|
+
await rest(progress, live);
|
|
1820
3751
|
}
|
|
1821
3752
|
function close() {
|
|
1822
3753
|
if (closed)
|
|
1823
3754
|
return;
|
|
1824
3755
|
closed = true;
|
|
3756
|
+
if (rechecking !== null)
|
|
3757
|
+
clearInterval(rechecking);
|
|
3758
|
+
connectionLive();
|
|
3759
|
+
lockListeners.clear();
|
|
3760
|
+
readinessWatches.clear();
|
|
1825
3761
|
withdrawStream?.();
|
|
1826
3762
|
withdrawStream = undefined;
|
|
1827
3763
|
withdrawConversations?.();
|
|
1828
3764
|
withdrawConversations = undefined;
|
|
1829
3765
|
withdrawServiceMode?.();
|
|
1830
3766
|
conversations.close();
|
|
1831
|
-
|
|
3767
|
+
void recovery.retire();
|
|
1832
3768
|
store?.close();
|
|
1833
3769
|
void opening?.then((opened) => {
|
|
1834
3770
|
opened.close();
|
|
@@ -1838,6 +3774,7 @@ export function composeRuntime(options) {
|
|
|
1838
3774
|
storageLossListeners.clear();
|
|
1839
3775
|
statusListeners.clear();
|
|
1840
3776
|
serviceModeListeners.clear();
|
|
3777
|
+
memberListeners.clear();
|
|
1841
3778
|
readSide.close();
|
|
1842
3779
|
queueSubscriptions.clear();
|
|
1843
3780
|
roomDraftListeners.clear();
|
|
@@ -1845,9 +3782,8 @@ export function composeRuntime(options) {
|
|
|
1845
3782
|
}
|
|
1846
3783
|
async function stop() {
|
|
1847
3784
|
stopping = true;
|
|
1848
|
-
|
|
1849
|
-
await
|
|
1850
|
-
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
3785
|
+
await recovery.retire();
|
|
3786
|
+
await Promise.all(working);
|
|
1851
3787
|
await mutations;
|
|
1852
3788
|
close();
|
|
1853
3789
|
}
|
|
@@ -1866,17 +3802,125 @@ export function composeRuntime(options) {
|
|
|
1866
3802
|
if (options.adapter.roomStream === undefined)
|
|
1867
3803
|
void sync('unstreamed');
|
|
1868
3804
|
function planned(input, reach) {
|
|
1869
|
-
|
|
3805
|
+
const plan = planOf(input, roomFacts(reach), planReach(reach));
|
|
3806
|
+
const named = plan.paths !== null &&
|
|
3807
|
+
plan.rooms !== null &&
|
|
3808
|
+
plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
|
|
3809
|
+
return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
|
|
1870
3810
|
}
|
|
1871
3811
|
function watchPlan(plan, deliver) {
|
|
1872
|
-
|
|
3812
|
+
let seeded = false;
|
|
3813
|
+
let pending = true;
|
|
3814
|
+
const shown = () => {
|
|
3815
|
+
const seed = seeds.get(plan.key);
|
|
3816
|
+
if (seed === undefined || seeded || !pending)
|
|
3817
|
+
return;
|
|
3818
|
+
seeded = true;
|
|
3819
|
+
deliver(seededResult(seed));
|
|
3820
|
+
};
|
|
3821
|
+
const watchers = seedWatchers.get(plan.key) ?? new Set();
|
|
3822
|
+
seedWatchers.set(plan.key, watchers);
|
|
3823
|
+
watchers.add(shown);
|
|
3824
|
+
const stop = readSide.watch(plan, identityOf(plan), () => {
|
|
3825
|
+
const cover = covered(plan);
|
|
3826
|
+
awaitPreparation(cover);
|
|
3827
|
+
return seedBehind(plan, cover);
|
|
3828
|
+
}, (message) => {
|
|
3829
|
+
const answered = message;
|
|
3830
|
+
pending = whole(answered) && 'pending' in answered.result;
|
|
3831
|
+
if (pending && seeds.has(plan.key)) {
|
|
3832
|
+
shown();
|
|
3833
|
+
return;
|
|
3834
|
+
}
|
|
3835
|
+
seeded = false;
|
|
3836
|
+
deliver(message);
|
|
3837
|
+
});
|
|
3838
|
+
return () => {
|
|
3839
|
+
watchers.delete(shown);
|
|
3840
|
+
if (watchers.size === 0)
|
|
3841
|
+
seedWatchers.delete(plan.key);
|
|
3842
|
+
stop();
|
|
3843
|
+
};
|
|
1873
3844
|
}
|
|
1874
|
-
async function searched(input, reach) {
|
|
1875
|
-
const
|
|
1876
|
-
const
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
3845
|
+
async function searched(input, reach, locked) {
|
|
3846
|
+
const asked = searchPlanOf(input);
|
|
3847
|
+
const plan = { ...asked, limit: capped(asked.limit) };
|
|
3848
|
+
const filter = {
|
|
3849
|
+
...filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)),
|
|
3850
|
+
locked,
|
|
3851
|
+
};
|
|
3852
|
+
const unlocking = (filter.rooms ?? [])
|
|
3853
|
+
.filter((room) => locked.includes(room))
|
|
3854
|
+
.map((room) => ({ room, awaits: 'unlock' }));
|
|
3855
|
+
for (;;) {
|
|
3856
|
+
const cover = covered(filter);
|
|
3857
|
+
awaitPreparation(cover);
|
|
3858
|
+
const pending = [
|
|
3859
|
+
...unlocking,
|
|
3860
|
+
...('pending' in cover ? cover.pending : []),
|
|
3861
|
+
];
|
|
3862
|
+
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
3863
|
+
const held = covered({
|
|
3864
|
+
...filter,
|
|
3865
|
+
rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
|
|
3866
|
+
});
|
|
3867
|
+
const covering = 'pending' in held ? [] : held.coverage.rooms;
|
|
3868
|
+
const unindexed = covering
|
|
3869
|
+
.filter(({ room }) => !searchIndex.rooms().has(room))
|
|
3870
|
+
.map(({ room }) => room);
|
|
3871
|
+
for (const room of unindexed)
|
|
3872
|
+
pending.push({ room, awaits: 'index' });
|
|
3873
|
+
const listed = conversations.searchable();
|
|
3874
|
+
const named = reach !== null && !reach.conversations
|
|
3875
|
+
? []
|
|
3876
|
+
: (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
|
|
3877
|
+
const waiting = named.filter((one) => !searchIndex.conversations().has(one));
|
|
3878
|
+
for (const conversation of waiting)
|
|
3879
|
+
pending.push({ conversation, awaits: 'index' });
|
|
3880
|
+
if (unindexed.length > 0 || waiting.length > 0)
|
|
3881
|
+
index(unindexed, waiting);
|
|
3882
|
+
const complete = pending.length === 0 && !('pending' in cover);
|
|
3883
|
+
const answering = complete
|
|
3884
|
+
? filter
|
|
3885
|
+
: {
|
|
3886
|
+
...filter,
|
|
3887
|
+
rooms: covering
|
|
3888
|
+
.filter(({ room }) => searchIndex.rooms().has(room))
|
|
3889
|
+
.map(({ room }) => room),
|
|
3890
|
+
};
|
|
3891
|
+
const answered = covered(answering);
|
|
3892
|
+
if ('pending' in answered)
|
|
3893
|
+
continue;
|
|
3894
|
+
const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
|
|
3895
|
+
await readSide.settled();
|
|
3896
|
+
const seen = searchIndex.changes();
|
|
3897
|
+
await searchIndex.caughtUp();
|
|
3898
|
+
const selected = roomsAlone(answering) ? null : await rowsQuery(answering);
|
|
3899
|
+
if (selected !== null && 'pending' in selected)
|
|
3900
|
+
continue;
|
|
3901
|
+
if (searchIndex.changes() !== seen ||
|
|
3902
|
+
JSON.stringify(covered(answering)) !== JSON.stringify(answered))
|
|
3903
|
+
continue;
|
|
3904
|
+
const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms !== null
|
|
3905
|
+
? new Set(answering.rooms)
|
|
3906
|
+
: locked.length === 0
|
|
3907
|
+
? null
|
|
3908
|
+
: new Set(reachedRooms(answering)), plan.collection
|
|
3909
|
+
? new Map(answered.coverage.rooms.map(({ room }) => [
|
|
3910
|
+
room,
|
|
3911
|
+
replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
|
|
3912
|
+
]))
|
|
3913
|
+
: null);
|
|
3914
|
+
const coverage = {
|
|
3915
|
+
rooms: answered.coverage.rooms,
|
|
3916
|
+
conversations: searchedConversations,
|
|
3917
|
+
index: searchIndex.generation(),
|
|
3918
|
+
overlay: answered.coverage.overlay,
|
|
3919
|
+
};
|
|
3920
|
+
return complete
|
|
3921
|
+
? { ...hits, coverage }
|
|
3922
|
+
: { pending, documents: hits.documents, messages: hits.messages, coverage };
|
|
3923
|
+
}
|
|
1880
3924
|
}
|
|
1881
3925
|
const watch = (topic, input, deliver) => {
|
|
1882
3926
|
if (topic === 'room-drafts') {
|
|
@@ -1901,6 +3945,8 @@ export function composeRuntime(options) {
|
|
|
1901
3945
|
}
|
|
1902
3946
|
if (topic === 'conversations')
|
|
1903
3947
|
return conversations.watchList(deliver);
|
|
3948
|
+
if (topic === 'agents')
|
|
3949
|
+
return conversations.watchAgents(deliver);
|
|
1904
3950
|
if (topic === 'presentation') {
|
|
1905
3951
|
const listener = {
|
|
1906
3952
|
app: input[0],
|
|
@@ -1920,6 +3966,9 @@ export function composeRuntime(options) {
|
|
|
1920
3966
|
presentationListeners.delete(listener);
|
|
1921
3967
|
};
|
|
1922
3968
|
}
|
|
3969
|
+
if (topic === 'readiness') {
|
|
3970
|
+
return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
|
|
3971
|
+
}
|
|
1923
3972
|
if (topic === 'rooms') {
|
|
1924
3973
|
roomListeners.add(deliver);
|
|
1925
3974
|
deliver(listed());
|
|
@@ -1955,6 +4004,13 @@ export function composeRuntime(options) {
|
|
|
1955
4004
|
serviceModeListeners.delete(deliver);
|
|
1956
4005
|
};
|
|
1957
4006
|
}
|
|
4007
|
+
if (topic === 'member') {
|
|
4008
|
+
memberListeners.add(deliver);
|
|
4009
|
+
deliver(member);
|
|
4010
|
+
return () => {
|
|
4011
|
+
memberListeners.delete(deliver);
|
|
4012
|
+
};
|
|
4013
|
+
}
|
|
1958
4014
|
if (topic === 'queue') {
|
|
1959
4015
|
const queue = { locator: input[0], notify: deliver };
|
|
1960
4016
|
queueSubscriptions.add(queue);
|
|
@@ -1972,8 +4028,8 @@ export function composeRuntime(options) {
|
|
|
1972
4028
|
rooms() {
|
|
1973
4029
|
return listed();
|
|
1974
4030
|
},
|
|
1975
|
-
query: ((input) =>
|
|
1976
|
-
search: (input) => searched(input, null),
|
|
4031
|
+
query: ((input) => rowsQuery(planned(input, null))),
|
|
4032
|
+
search: (input) => searched(input, null, []),
|
|
1977
4033
|
types(locator) {
|
|
1978
4034
|
return {
|
|
1979
4035
|
collection: modelTypes(modelOf(locator)),
|
|
@@ -2001,23 +4057,25 @@ export function composeRuntime(options) {
|
|
|
2001
4057
|
return exclusive(async () => {
|
|
2002
4058
|
const parts = readParts(input);
|
|
2003
4059
|
if (path === '' || normalize(path) !== path)
|
|
2004
|
-
throw
|
|
4060
|
+
throw runtimeError('room-draft-malformed');
|
|
2005
4061
|
const local = await openLocal();
|
|
2006
4062
|
const current = roomDrafts.get(room)?.get(path);
|
|
2007
4063
|
if ((current?.revision ?? null) !== revision) {
|
|
2008
|
-
return
|
|
2009
|
-
outcome: 'stale',
|
|
2010
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
2011
|
-
};
|
|
4064
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2012
4065
|
}
|
|
4066
|
+
const file = parts.file === undefined
|
|
4067
|
+
? undefined
|
|
4068
|
+
: await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
|
|
2013
4069
|
const record = {
|
|
2014
4070
|
locator: room,
|
|
2015
4071
|
path,
|
|
2016
4072
|
revision: roomDraftRevision(),
|
|
2017
|
-
parts: held(current?.parts, parts),
|
|
4073
|
+
parts: held(current?.parts, parts, file),
|
|
2018
4074
|
returned: current?.returned ?? null,
|
|
2019
4075
|
};
|
|
2020
|
-
await local.keepRoomDraft(record
|
|
4076
|
+
await local.keepRoomDraft(record, parts.file === undefined || file === undefined
|
|
4077
|
+
? []
|
|
4078
|
+
: [{ key: file.localId, blob: parts.file.blob }]);
|
|
2021
4079
|
rememberRoomDraft(record);
|
|
2022
4080
|
publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
|
|
2023
4081
|
return { outcome: 'written', roomDraft: roomDraftOf(record) };
|
|
@@ -2028,10 +4086,7 @@ export function composeRuntime(options) {
|
|
|
2028
4086
|
const local = await openLocal();
|
|
2029
4087
|
const current = roomDrafts.get(room)?.get(path);
|
|
2030
4088
|
if (current === undefined || current.revision !== revision) {
|
|
2031
|
-
return
|
|
2032
|
-
outcome: 'stale',
|
|
2033
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
2034
|
-
};
|
|
4089
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2035
4090
|
}
|
|
2036
4091
|
await local.discardRoomDraft(room, path);
|
|
2037
4092
|
forgetRoomDraft(room, path);
|
|
@@ -2051,23 +4106,36 @@ export function composeRuntime(options) {
|
|
|
2051
4106
|
});
|
|
2052
4107
|
},
|
|
2053
4108
|
write(app, value) {
|
|
2054
|
-
return
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
4109
|
+
return keepPresentation(app, () => structuredClone(value));
|
|
4110
|
+
},
|
|
4111
|
+
edit(app, members) {
|
|
4112
|
+
return keepPresentation(app, (held) => Object.fromEntries([
|
|
4113
|
+
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(members, member)),
|
|
4114
|
+
...Object.entries(structuredClone(members)).filter(([, value]) => value !== null),
|
|
4115
|
+
]));
|
|
2061
4116
|
},
|
|
2062
4117
|
},
|
|
2063
4118
|
save(room, named, writtenBy) {
|
|
2064
4119
|
return exclusive(() => saveRoomDrafts(room, named, writtenBy));
|
|
2065
4120
|
},
|
|
4121
|
+
saveParts(room, parts, writtenBy) {
|
|
4122
|
+
return exclusive(() => saveParts(room, parts, writtenBy));
|
|
4123
|
+
},
|
|
2066
4124
|
history(locator, path, query) {
|
|
2067
4125
|
return readHistory(locator, path, query);
|
|
2068
4126
|
},
|
|
2069
|
-
|
|
2070
|
-
return
|
|
4127
|
+
resolve(room, path, destination) {
|
|
4128
|
+
return resolveReference(room, path, destination);
|
|
4129
|
+
},
|
|
4130
|
+
binaryFile: {
|
|
4131
|
+
read: readBinaryPath,
|
|
4132
|
+
link: linkBinaryPath,
|
|
4133
|
+
readReference: readBinaryReference,
|
|
4134
|
+
},
|
|
4135
|
+
async call(name, input) {
|
|
4136
|
+
await ready;
|
|
4137
|
+
const room = calledRooms(name, input).find((one) => locked(one));
|
|
4138
|
+
return room === undefined ? recoveredAction(name, input) : lockedRoom(room);
|
|
2071
4139
|
},
|
|
2072
4140
|
reached(locator, roomVersion, path) {
|
|
2073
4141
|
return path === undefined
|
|
@@ -2078,30 +4146,58 @@ export function composeRuntime(options) {
|
|
|
2078
4146
|
return journal.export();
|
|
2079
4147
|
},
|
|
2080
4148
|
persistence() {
|
|
2081
|
-
return
|
|
4149
|
+
return persistence === 'granted';
|
|
4150
|
+
},
|
|
4151
|
+
async retry() {
|
|
4152
|
+
journal.record('retry');
|
|
4153
|
+
unindexable.clear();
|
|
4154
|
+
queueReadiness();
|
|
4155
|
+
resume();
|
|
2082
4156
|
},
|
|
2083
4157
|
observe: observing(watch),
|
|
2084
4158
|
async reauthorize() {
|
|
2085
4159
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2086
4160
|
return;
|
|
2087
4161
|
journal.record('connection reauthorize');
|
|
2088
|
-
await
|
|
4162
|
+
await Promise.all([...lanes.values()]
|
|
4163
|
+
.filter((held) => held.generation === generation)
|
|
4164
|
+
.map((held) => held.chain));
|
|
2089
4165
|
await mutations;
|
|
2090
4166
|
authorization = undefined;
|
|
2091
4167
|
discoveryPaused = false;
|
|
2092
4168
|
if (options.adapter.roomStream === undefined)
|
|
2093
4169
|
await sync('unstreamed');
|
|
2094
4170
|
else
|
|
2095
|
-
await task(ensureAuthorization);
|
|
4171
|
+
await task(OPENING_LANE, ensureAuthorization);
|
|
2096
4172
|
},
|
|
2097
4173
|
};
|
|
2098
4174
|
const reachable = {
|
|
2099
4175
|
md,
|
|
2100
4176
|
watch,
|
|
4177
|
+
referrerScope: (referrer) => options.adapter.referrerScope(referrer),
|
|
2101
4178
|
plan: planned,
|
|
2102
|
-
query: (plan) =>
|
|
4179
|
+
query: (plan) => rowsQuery(plan),
|
|
2103
4180
|
watchPlan,
|
|
2104
4181
|
search: searched,
|
|
4182
|
+
watchReadiness,
|
|
4183
|
+
locked,
|
|
4184
|
+
lockedRooms,
|
|
4185
|
+
watchLocks,
|
|
4186
|
+
located,
|
|
4187
|
+
};
|
|
4188
|
+
return {
|
|
4189
|
+
md,
|
|
4190
|
+
seed,
|
|
4191
|
+
stop,
|
|
4192
|
+
lose: loseRooms,
|
|
4193
|
+
watch,
|
|
4194
|
+
persisted(answer) {
|
|
4195
|
+
if (answer === persistence)
|
|
4196
|
+
return;
|
|
4197
|
+
persistence = answer;
|
|
4198
|
+
journal.record(`persistence ${answer}`);
|
|
4199
|
+
queueReadiness();
|
|
4200
|
+
},
|
|
4201
|
+
within: (reach, port) => narrowed(reachable, reach, port),
|
|
2105
4202
|
};
|
|
2106
|
-
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2107
4203
|
}
|