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