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