@agentshouse/mdruntime 0.1.0-alpha.8 → 0.1.0-c009335e08279
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 +208 -0
- package/README.md +44 -0
- package/dist/adapter.d.ts +215 -25
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +2 -3
- package/dist/attachment.d.ts +3 -0
- package/dist/attachment.d.ts.map +1 -0
- package/dist/attachment.js +8 -0
- package/dist/bootstrap.d.ts +6 -7
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +31 -28
- package/dist/change-set.d.ts +33 -0
- package/dist/change-set.d.ts.map +1 -0
- package/dist/change-set.js +83 -0
- package/dist/change.d.ts +9 -0
- package/dist/change.d.ts.map +1 -1
- package/dist/change.js +6 -5
- package/dist/client.d.ts +69 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +165 -0
- package/dist/collection.d.ts +22 -15
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +60 -78
- package/dist/connection.d.ts +68 -40
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +1597 -961
- package/dist/conversation.d.ts +79 -0
- package/dist/conversation.d.ts.map +1 -0
- package/dist/conversation.js +557 -0
- package/dist/durability.d.ts +8 -3
- package/dist/durability.d.ts.map +1 -1
- package/dist/durability.js +7 -61
- package/dist/file.d.ts +20 -0
- package/dist/file.d.ts.map +1 -0
- package/dist/file.js +126 -0
- package/dist/host.d.ts +13 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +175 -0
- package/dist/identity.d.ts +1 -0
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +10 -9
- package/dist/index.d.ts +14 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -3
- package/dist/log.js +5 -6
- package/dist/memory.d.ts +17 -2
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +268 -113
- package/dist/model.d.ts +10 -25
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +18 -21
- package/dist/observation.d.ts +25 -0
- package/dist/observation.d.ts.map +1 -0
- package/dist/observation.js +85 -0
- package/dist/outcome.d.ts +12 -24
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +1 -13
- package/dist/overlay.d.ts +9 -2
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +187 -43
- package/dist/paths.d.ts +0 -1
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +12 -19
- package/dist/query.d.ts +210 -9
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +420 -15
- package/dist/read-side.d.ts +14 -0
- package/dist/read-side.d.ts.map +1 -0
- package/dist/read-side.js +371 -0
- package/dist/refresh.d.ts +4 -5
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +45 -30
- package/dist/replica.d.ts +27 -6
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +233 -30
- package/dist/room-draft.d.ts +124 -0
- package/dist/room-draft.d.ts.map +1 -0
- package/dist/room-draft.js +327 -0
- package/dist/rows.d.ts +41 -0
- package/dist/rows.d.ts.map +1 -0
- package/dist/rows.js +100 -0
- package/dist/save.d.ts +10 -6
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +266 -333
- package/dist/search.d.ts +61 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +125 -0
- package/dist/status.d.ts +9 -2
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +11 -14
- package/dist/store.d.ts +75 -18
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +358 -97
- package/dist/transfer.d.ts +5 -0
- package/dist/transfer.d.ts.map +1 -1
- package/dist/transfer.js +1 -5
- package/dist/wire.d.ts +64 -0
- package/dist/wire.d.ts.map +1 -0
- package/dist/wire.js +118 -0
- package/package.json +15 -13
- package/dist/adapter.js.map +0 -1
- package/dist/bootstrap.js.map +0 -1
- package/dist/change.js.map +0 -1
- package/dist/collection.js.map +0 -1
- package/dist/connection.js.map +0 -1
- package/dist/durability.js.map +0 -1
- package/dist/identity.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/log.js.map +0 -1
- package/dist/memory.js.map +0 -1
- package/dist/model.js.map +0 -1
- package/dist/outcome.js.map +0 -1
- package/dist/overlay.js.map +0 -1
- package/dist/paths.js.map +0 -1
- package/dist/query.js.map +0 -1
- package/dist/refresh.js.map +0 -1
- package/dist/replica.js.map +0 -1
- package/dist/save.js.map +0 -1
- package/dist/status.js.map +0 -1
- package/dist/store.js.map +0 -1
- package/dist/transfer.js.map +0 -1
package/dist/connection.js
CHANGED
|
@@ -1,45 +1,111 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { cached, modelTypes, NO_MODEL, sameModel } from './model.js';
|
|
3
|
-
import {
|
|
1
|
+
import { verify } from '@agentshouse/mdmodel';
|
|
2
|
+
import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
|
|
3
|
+
import { opened } from './observation.js';
|
|
4
|
+
import { filterOf, identityOf, planOf, } from './query.js';
|
|
5
|
+
import { createReadSide } from './read-side.js';
|
|
6
|
+
import { createSearchIndex, searchPlanOf, } from './search.js';
|
|
7
|
+
import { ROOM_CONTRACT_VERSION, VERSION_1_CAPABILITIES, } from './adapter.js';
|
|
4
8
|
import { bootstrapRoom } from './bootstrap.js';
|
|
5
|
-
import {
|
|
9
|
+
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
10
|
+
import { createConversationStore, } from './conversation.js';
|
|
11
|
+
import { MD_MODEL, parseCollection, validRoot } from './collection.js';
|
|
6
12
|
import { createLog } from './log.js';
|
|
7
|
-
import { queuedOutcome, refused } from './outcome.js';
|
|
8
13
|
import { refreshRoom } from './refresh.js';
|
|
9
14
|
import { buildBatch } from './save.js';
|
|
10
|
-
import { applyReturned, readConfirmed, replicaOf } from './replica.js';
|
|
11
|
-
import {
|
|
15
|
+
import { applyReturned, compositionOf, landed, readConfirmed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
|
|
16
|
+
import { changedParts, changesOf, held, readParts, returnedInto, underneath, } from './room-draft.js';
|
|
17
|
+
import { settled, same, saving, STARTING, transferring, } from './status.js';
|
|
12
18
|
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
13
|
-
import {
|
|
19
|
+
import { roomDraftRevision } from './identity.js';
|
|
20
|
+
import { beneath, normalize } from './paths.js';
|
|
21
|
+
import { openRuntimeStore, } from './store.js';
|
|
14
22
|
import { unconfirmed } from './transfer.js';
|
|
23
|
+
// Discovery repeats each minute so a Room the stream stopped serving stays unrefreshed no longer.
|
|
15
24
|
const DEFAULT_DISCOVERY_INTERVAL_MS = 60_000;
|
|
25
|
+
const EMPTY_SCHEMA = { verdict: 'accept', model: { types: {} } };
|
|
16
26
|
const knownCapabilities = new Set(VERSION_1_CAPABILITIES);
|
|
17
|
-
|
|
27
|
+
function within(root, path) {
|
|
28
|
+
return root === '' || beneath(root, path);
|
|
29
|
+
}
|
|
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
|
+
function declaredSelection(room) {
|
|
41
|
+
const selection = room.typedSelection;
|
|
42
|
+
if (selection === undefined)
|
|
43
|
+
return true;
|
|
44
|
+
if (!Array.isArray(selection.roots) || selection.roots.length === 0)
|
|
45
|
+
return false;
|
|
46
|
+
if (!declaredIdentity(selection.attachedModel))
|
|
47
|
+
return false;
|
|
48
|
+
return selection.roots.every((root, index) => validRoot(root) &&
|
|
49
|
+
root !== '' &&
|
|
50
|
+
selection.roots.every((other, at) => at === index || !within(other, root)));
|
|
51
|
+
}
|
|
52
|
+
export function composeRuntime(options) {
|
|
18
53
|
const period = options.discoveryIntervalMs ?? DEFAULT_DISCOVERY_INTERVAL_MS;
|
|
19
54
|
let authorization = options.authorization;
|
|
20
55
|
let opening;
|
|
21
56
|
let store;
|
|
22
57
|
const replicas = new Map();
|
|
23
|
-
const
|
|
24
|
-
const
|
|
58
|
+
const pendings = new Map();
|
|
59
|
+
const roomDrafts = new Map();
|
|
60
|
+
const presentations = new Map();
|
|
61
|
+
const wanted = new Map();
|
|
25
62
|
let limits = [];
|
|
26
63
|
let capabilities = [];
|
|
64
|
+
function rewriting() {
|
|
65
|
+
return capabilities.some((capability) => capability.name === 'move-reference-rewrites')
|
|
66
|
+
? 'provider'
|
|
67
|
+
: 'runtime';
|
|
68
|
+
}
|
|
27
69
|
const rooms = new Map();
|
|
28
70
|
const unreadable = new Set();
|
|
29
71
|
const models = new Map();
|
|
30
72
|
const liveModels = new Map();
|
|
31
73
|
const invalidModels = new Set();
|
|
32
74
|
const modelEpoch = new Map();
|
|
33
|
-
|
|
34
|
-
|
|
75
|
+
const discovered = new Map();
|
|
76
|
+
let roomsListed = false;
|
|
77
|
+
const scopedRefusals = new Map();
|
|
78
|
+
const sourceRefusals = new Map();
|
|
79
|
+
const declarationRefusals = new Set();
|
|
80
|
+
function modelKey(locator, attached) {
|
|
81
|
+
return `${attached} ${locator}`;
|
|
82
|
+
}
|
|
83
|
+
function modelOf(locator, attached = 'collection') {
|
|
84
|
+
return models.get(modelKey(locator, attached)) ?? NO_MODEL;
|
|
85
|
+
}
|
|
86
|
+
function savedModel(locator, attached) {
|
|
87
|
+
return invalidModels.has(modelKey(locator, attached))
|
|
88
|
+
? { status: 'unavailable', reason: 'revoked' }
|
|
89
|
+
: modelOf(locator, attached);
|
|
90
|
+
}
|
|
91
|
+
function compositionFor(record) {
|
|
92
|
+
return compositionOf({
|
|
93
|
+
...record,
|
|
94
|
+
model: savedModel(record.locator, 'collection'),
|
|
95
|
+
selectionModel: savedModel(record.locator, 'typed-selection'),
|
|
96
|
+
});
|
|
35
97
|
}
|
|
36
|
-
const mismatchedRoots = new Set();
|
|
37
98
|
let refusal = null;
|
|
38
99
|
let storageLoss = null;
|
|
39
100
|
let status = STARTING;
|
|
101
|
+
let shown = STARTING;
|
|
40
102
|
let discoveryPaused = false;
|
|
41
103
|
let timer;
|
|
42
104
|
let closed = false;
|
|
105
|
+
let stopping = false;
|
|
106
|
+
let withdrawNotifications;
|
|
107
|
+
let handed;
|
|
108
|
+
const notices = new Map();
|
|
43
109
|
let chain = Promise.resolve();
|
|
44
110
|
let mutations = Promise.resolve();
|
|
45
111
|
let inflight = false;
|
|
@@ -48,264 +114,326 @@ export function createMdRuntime(options) {
|
|
|
48
114
|
const refusalListeners = new Set();
|
|
49
115
|
const storageLossListeners = new Set();
|
|
50
116
|
const statusListeners = new Set();
|
|
51
|
-
const querySubscriptions = new Set();
|
|
52
117
|
const queueSubscriptions = new Set();
|
|
53
|
-
const
|
|
118
|
+
const roomDraftListeners = new Set();
|
|
119
|
+
const presentationListeners = new Set();
|
|
120
|
+
const readSide = createReadSide();
|
|
121
|
+
let withdrawConversations;
|
|
122
|
+
let currencyQueued = false;
|
|
123
|
+
let serviceMode = 'open';
|
|
124
|
+
let haltedRefusal = false;
|
|
125
|
+
let openSignals = 0;
|
|
126
|
+
const serviceModeListeners = new Set();
|
|
127
|
+
const conversations = createConversationStore({
|
|
128
|
+
cards: declaredCards,
|
|
129
|
+
documents: (locator) => readSide.query(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
|
|
130
|
+
call: (name, input) => invokeAction(name, input),
|
|
131
|
+
durably,
|
|
132
|
+
stream: () => options.adapter.conversations,
|
|
133
|
+
publish: publishChangeSet,
|
|
134
|
+
closed: () => closed,
|
|
135
|
+
});
|
|
136
|
+
const searchIndex = createSearchIndex({
|
|
137
|
+
documents: () => readSide.documents(),
|
|
138
|
+
document: (room, path) => readSide.document(room, path),
|
|
139
|
+
messages: () => conversations.messages(),
|
|
140
|
+
message: (conversation, identity) => conversations.message(conversation, identity),
|
|
141
|
+
});
|
|
54
142
|
const journal = createLog();
|
|
55
|
-
let notifyReady = ()=>{};
|
|
56
|
-
const ready = new Promise((resolve)=>{
|
|
143
|
+
let notifyReady = () => { };
|
|
144
|
+
const ready = new Promise((resolve) => {
|
|
57
145
|
notifyReady = resolve;
|
|
58
146
|
});
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
room.set(path, content);
|
|
69
|
-
drafts.set(locator, room);
|
|
70
|
-
}
|
|
71
|
-
function forgetDraft(locator, path) {
|
|
72
|
-
const room = drafts.get(locator);
|
|
73
|
-
if (room === undefined) return;
|
|
147
|
+
function rememberRoomDraft(record) {
|
|
148
|
+
const room = roomDrafts.get(record.locator) ?? new Map();
|
|
149
|
+
room.set(record.path, record);
|
|
150
|
+
roomDrafts.set(record.locator, room);
|
|
151
|
+
}
|
|
152
|
+
function forgetRoomDraft(locator, path) {
|
|
153
|
+
const room = roomDrafts.get(locator);
|
|
154
|
+
if (room === undefined)
|
|
155
|
+
return;
|
|
74
156
|
room.delete(path);
|
|
75
|
-
if (room.size === 0)
|
|
157
|
+
if (room.size === 0)
|
|
158
|
+
roomDrafts.delete(locator);
|
|
76
159
|
}
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
entries.sort((left, right)=>{
|
|
89
|
-
if (left.locator !== right.locator) return left.locator < right.locator ? -1 : 1;
|
|
90
|
-
if (left.path !== right.path) return left.path < right.path ? -1 : 1;
|
|
91
|
-
return 0;
|
|
160
|
+
function roomDraftOf(record) {
|
|
161
|
+
return structuredClone({
|
|
162
|
+
room: record.locator,
|
|
163
|
+
path: record.path,
|
|
164
|
+
revision: record.revision,
|
|
165
|
+
parts: record.parts,
|
|
166
|
+
changedUnderneath: underneath(record.parts, record.path, replicas.get(record.locator)?.visible),
|
|
167
|
+
returned: record.returned,
|
|
92
168
|
});
|
|
93
|
-
return entries;
|
|
94
169
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
170
|
+
function snapshotRoomDrafts() {
|
|
171
|
+
const records = [...roomDrafts.values()].flatMap((room) => [...room.values()]);
|
|
172
|
+
records.sort((left, right) => {
|
|
173
|
+
if (left.locator !== right.locator)
|
|
174
|
+
return left.locator < right.locator ? -1 : 1;
|
|
175
|
+
if (left.path !== right.path)
|
|
176
|
+
return left.path < right.path ? -1 : 1;
|
|
177
|
+
return 0;
|
|
98
178
|
});
|
|
99
|
-
return
|
|
179
|
+
return records.map(roomDraftOf);
|
|
100
180
|
}
|
|
101
|
-
function
|
|
102
|
-
if (closed)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
181
|
+
function publishRoomDrafts(waitingOnly) {
|
|
182
|
+
if (closed)
|
|
183
|
+
return;
|
|
184
|
+
const value = snapshotRoomDrafts();
|
|
185
|
+
for (const listener of roomDraftListeners) {
|
|
186
|
+
if (waitingOnly && listener.ready)
|
|
187
|
+
continue;
|
|
106
188
|
listener.ready = true;
|
|
107
189
|
listener.notify(value);
|
|
108
190
|
}
|
|
109
191
|
}
|
|
110
|
-
function
|
|
111
|
-
|
|
192
|
+
function presentationOf(app) {
|
|
193
|
+
const value = presentations.get(app);
|
|
194
|
+
return value === undefined ? null : structuredClone(value);
|
|
112
195
|
}
|
|
113
|
-
function
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
await hydrate(local);
|
|
121
|
-
await work(local);
|
|
122
|
-
});
|
|
123
|
-
}).then(()=>{
|
|
124
|
-
emitDrafts();
|
|
125
|
-
announce();
|
|
126
|
-
});
|
|
196
|
+
function emitPresentation(app) {
|
|
197
|
+
if (closed)
|
|
198
|
+
return;
|
|
199
|
+
for (const listener of presentationListeners) {
|
|
200
|
+
if (listener.app === app && listener.ready)
|
|
201
|
+
listener.notify(presentationOf(app));
|
|
202
|
+
}
|
|
127
203
|
}
|
|
128
204
|
function pending() {
|
|
129
|
-
return [
|
|
130
|
-
...replicas.values()
|
|
131
|
-
].some((replica)=>replica.queue.length > 0);
|
|
205
|
+
return [...replicas.values()].some((replica) => replica.queue.length > 0);
|
|
132
206
|
}
|
|
133
207
|
function attention() {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
for (const batch of batches){
|
|
144
|
-
const outcome = queuedOutcome(outcomes.get(batch.id));
|
|
145
|
-
if (outcome.kind !== 'pending') stored[batch.id] = outcome;
|
|
208
|
+
const attentionRequired = [];
|
|
209
|
+
for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
|
|
210
|
+
if (record.returned === null)
|
|
211
|
+
continue;
|
|
212
|
+
attentionRequired.push({
|
|
213
|
+
room: record.locator,
|
|
214
|
+
path: record.path,
|
|
215
|
+
returned: structuredClone(record.returned),
|
|
216
|
+
});
|
|
146
217
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
outcomes: storedOutcomes(batches)
|
|
218
|
+
attentionRequired.sort((left, right) => {
|
|
219
|
+
if (left.room !== right.room)
|
|
220
|
+
return left.room < right.room ? -1 : 1;
|
|
221
|
+
if (left.path !== right.path)
|
|
222
|
+
return left.path < right.path ? -1 : 1;
|
|
223
|
+
return 0;
|
|
154
224
|
});
|
|
155
|
-
|
|
225
|
+
let reset = null;
|
|
226
|
+
for (const { submission } of pendings.values()) {
|
|
227
|
+
if (submission.state !== 'reset')
|
|
228
|
+
continue;
|
|
229
|
+
if (reset === null || submission.reset < reset)
|
|
230
|
+
reset = submission.reset;
|
|
231
|
+
}
|
|
232
|
+
return { attentionRequired, reset };
|
|
156
233
|
}
|
|
157
234
|
function listed() {
|
|
158
|
-
return structuredClone([
|
|
159
|
-
...rooms.values()
|
|
160
|
-
]);
|
|
235
|
+
return structuredClone([...rooms.values()]);
|
|
161
236
|
}
|
|
162
|
-
function
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
room:
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
237
|
+
function roomFacts() {
|
|
238
|
+
const held = [...rooms.values()].map((room) => {
|
|
239
|
+
const attached = [
|
|
240
|
+
modelOf(room.locator),
|
|
241
|
+
...(room.typedSelection === null ? [] : [modelOf(room.locator, 'typed-selection')]),
|
|
242
|
+
];
|
|
243
|
+
return {
|
|
244
|
+
locator: room.locator,
|
|
245
|
+
available: attached.every((model) => model.status !== 'unavailable'),
|
|
246
|
+
types: attached.flatMap((model) => model.status === 'unavailable' ? [] : Object.values(model.types)),
|
|
247
|
+
};
|
|
248
|
+
});
|
|
249
|
+
return { listed: roomsListed, rooms: held };
|
|
171
250
|
}
|
|
172
251
|
function emitRooms() {
|
|
173
252
|
const value = listed();
|
|
174
|
-
for (const listener of roomListeners)
|
|
253
|
+
for (const listener of roomListeners)
|
|
254
|
+
listener(value);
|
|
175
255
|
}
|
|
176
256
|
function emitRefusal() {
|
|
177
|
-
for (const listener of refusalListeners)
|
|
257
|
+
for (const listener of refusalListeners)
|
|
258
|
+
listener(refusal);
|
|
178
259
|
}
|
|
179
260
|
function emitStorageLoss() {
|
|
180
|
-
for (const listener of storageLossListeners)
|
|
261
|
+
for (const listener of storageLossListeners)
|
|
262
|
+
listener(storageLoss);
|
|
181
263
|
}
|
|
182
|
-
function
|
|
183
|
-
if (
|
|
184
|
-
if (type === 'hello') {
|
|
185
|
-
channel.postMessage({
|
|
186
|
-
type: 'hello'
|
|
187
|
-
});
|
|
264
|
+
function moveTo(next) {
|
|
265
|
+
if (closed)
|
|
188
266
|
return;
|
|
189
|
-
}
|
|
190
|
-
channel.postMessage({
|
|
191
|
-
type,
|
|
192
|
-
status
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
function adopt(next) {
|
|
196
|
-
if (closed || same(status, next)) return;
|
|
197
267
|
status = next;
|
|
198
|
-
|
|
268
|
+
show();
|
|
199
269
|
}
|
|
200
|
-
function
|
|
201
|
-
if (closed
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if (
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
...request,
|
|
212
|
-
selector: {
|
|
213
|
-
$and: [
|
|
214
|
-
PRESENTED,
|
|
215
|
-
request.selector ?? {}
|
|
216
|
-
]
|
|
217
|
-
}
|
|
218
|
-
});
|
|
270
|
+
function show() {
|
|
271
|
+
if (closed)
|
|
272
|
+
return;
|
|
273
|
+
const next = status.state === 'synced' && !conversations.current()
|
|
274
|
+
? { ...status, state: 'syncing' }
|
|
275
|
+
: status;
|
|
276
|
+
if (same(shown, next))
|
|
277
|
+
return;
|
|
278
|
+
shown = next;
|
|
279
|
+
for (const listener of statusListeners)
|
|
280
|
+
listener(shown);
|
|
219
281
|
}
|
|
220
|
-
function
|
|
221
|
-
|
|
222
|
-
|
|
282
|
+
function entryOf(batch) {
|
|
283
|
+
const state = pendings.get(batch.id);
|
|
284
|
+
if (state?.accepted !== undefined) {
|
|
285
|
+
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
223
286
|
}
|
|
287
|
+
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
224
288
|
}
|
|
225
289
|
function entriesOf(locator) {
|
|
226
290
|
const replica = replicas.get(locator);
|
|
227
|
-
if (replica === undefined)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
outcome: queuedOutcome(outcomes.get(batch.id))
|
|
231
|
-
}));
|
|
291
|
+
if (replica === undefined)
|
|
292
|
+
return [];
|
|
293
|
+
return replica.queue.map(entryOf);
|
|
232
294
|
}
|
|
233
295
|
function emitQueue(locator) {
|
|
234
296
|
const entries = entriesOf(locator);
|
|
235
|
-
for (const subscription of queueSubscriptions){
|
|
236
|
-
if (subscription.locator === locator)
|
|
297
|
+
for (const subscription of queueSubscriptions) {
|
|
298
|
+
if (subscription.locator === locator)
|
|
299
|
+
subscription.notify(entries);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function applyChangeSet(set) {
|
|
303
|
+
readSide.apply(set);
|
|
304
|
+
searchIndex.changed(set);
|
|
305
|
+
}
|
|
306
|
+
function publishChangeSet(set) {
|
|
307
|
+
applyChangeSet(set);
|
|
308
|
+
if (set.rooms.length > 0)
|
|
309
|
+
emitRooms();
|
|
310
|
+
for (const locator of set.queues)
|
|
311
|
+
emitQueue(locator);
|
|
312
|
+
if (set.drafts.length > 0 ||
|
|
313
|
+
set.documents.some((change) => roomDrafts.get(change.locator)?.has(change.path) === true)) {
|
|
314
|
+
publishRoomDrafts(false);
|
|
315
|
+
}
|
|
316
|
+
for (const app of set.presentations)
|
|
317
|
+
emitPresentation(app);
|
|
318
|
+
conversations.changed(set);
|
|
319
|
+
const cards = declaredCards();
|
|
320
|
+
if (cards !== undefined && set.documents.some((change) => change.locator === cards.locator)) {
|
|
321
|
+
scheduleCurrency();
|
|
237
322
|
}
|
|
238
323
|
}
|
|
239
324
|
function stopTimer() {
|
|
240
|
-
if (timer === undefined)
|
|
325
|
+
if (timer === undefined)
|
|
326
|
+
return;
|
|
241
327
|
clearInterval(timer);
|
|
242
328
|
timer = undefined;
|
|
243
329
|
}
|
|
244
330
|
function startTimer() {
|
|
245
|
-
if (timer !== undefined)
|
|
246
|
-
|
|
331
|
+
if (timer !== undefined)
|
|
332
|
+
return;
|
|
333
|
+
timer = setInterval(() => {
|
|
247
334
|
void enqueue();
|
|
248
335
|
}, period);
|
|
249
336
|
}
|
|
250
|
-
function refuse(reason) {
|
|
251
|
-
if (reason === 'revoked-authorization') {
|
|
252
|
-
const revoked = {
|
|
253
|
-
status: 'unavailable',
|
|
254
|
-
reason: 'revoked'
|
|
255
|
-
};
|
|
256
|
-
for (const locator of new Set([
|
|
257
|
-
...models.keys(),
|
|
258
|
-
...rooms.keys()
|
|
259
|
-
])){
|
|
260
|
-
invalidModels.add(locator);
|
|
261
|
-
modelEpoch.set(locator, (modelEpoch.get(locator) ?? 0) + 1);
|
|
262
|
-
models.set(locator, revoked);
|
|
263
|
-
liveModels.delete(locator);
|
|
264
|
-
}
|
|
265
|
-
for (const [locator, room] of rooms)rooms.set(locator, {
|
|
266
|
-
...room,
|
|
267
|
-
model: modelTypes(revoked)
|
|
268
|
-
});
|
|
269
|
-
emitRooms();
|
|
270
|
-
void exclusive(async ()=>{
|
|
271
|
-
const local = await openLocal();
|
|
272
|
-
await acrossTabs('mdruntime-replica', async ()=>{
|
|
273
|
-
for (const record of (await local.all()))await local.publish({
|
|
274
|
-
...record,
|
|
275
|
-
model: revoked
|
|
276
|
-
});
|
|
277
|
-
});
|
|
278
|
-
announce();
|
|
279
|
-
}).catch(()=>{
|
|
280
|
-
journal.record('model invalidation durable-write failure');
|
|
281
|
-
});
|
|
282
|
-
}
|
|
337
|
+
async function refuse(reason, provider = null) {
|
|
283
338
|
stopTimer();
|
|
284
339
|
discoveryPaused = true;
|
|
285
|
-
|
|
286
|
-
reason
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
340
|
+
try {
|
|
341
|
+
if (reason === 'revoked-authorization') {
|
|
342
|
+
await exclusive(async () => {
|
|
343
|
+
const revoked = { status: 'unavailable', reason: 'revoked' };
|
|
344
|
+
try {
|
|
345
|
+
const local = await openLocal();
|
|
346
|
+
await local.revokeModels([...replicas.values()].map((replica) => ({
|
|
347
|
+
record: replica.confirmed.record,
|
|
348
|
+
queue: replica.queue,
|
|
349
|
+
})));
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
journal.record('model revocation persistence failed');
|
|
353
|
+
}
|
|
354
|
+
const keys = new Set(models.keys());
|
|
355
|
+
for (const locator of rooms.keys()) {
|
|
356
|
+
keys.add(modelKey(locator, 'collection'));
|
|
357
|
+
keys.add(modelKey(locator, 'typed-selection'));
|
|
358
|
+
}
|
|
359
|
+
for (const key of keys) {
|
|
360
|
+
invalidModels.add(key);
|
|
361
|
+
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
362
|
+
models.set(key, revoked);
|
|
363
|
+
liveModels.delete(key);
|
|
364
|
+
}
|
|
365
|
+
const changed = [];
|
|
366
|
+
for (const [locator, room] of rooms) {
|
|
367
|
+
const before = replicas.get(locator) ?? null;
|
|
368
|
+
if (before !== null) {
|
|
369
|
+
const record = {
|
|
370
|
+
...before.confirmed.record,
|
|
371
|
+
model: revoked,
|
|
372
|
+
selectionModel: revoked,
|
|
373
|
+
};
|
|
374
|
+
const read = readConfirmed(record, before.confirmed);
|
|
375
|
+
replicas.set(locator, read.kind === 'staged'
|
|
376
|
+
? replicaOf(read.confirmed, before.queue, rewriting(), before)
|
|
377
|
+
: { ...before, confirmed: { ...before.confirmed, record } });
|
|
378
|
+
}
|
|
379
|
+
rooms.set(locator, {
|
|
380
|
+
...room,
|
|
381
|
+
model: modelTypes(revoked),
|
|
382
|
+
typedSelection: room.typedSelection === null
|
|
383
|
+
? null
|
|
384
|
+
: { ...room.typedSelection, model: modelTypes(revoked) },
|
|
385
|
+
});
|
|
386
|
+
changed.push(changeSet(locator, before, replicas.get(locator) ?? null, { room: true }));
|
|
387
|
+
}
|
|
388
|
+
publishChangeSet(mergeChangeSets(changed));
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
finally {
|
|
393
|
+
refusal = {
|
|
394
|
+
reason,
|
|
395
|
+
reauthorizationOffered: reason === 'revoked-authorization',
|
|
396
|
+
refusal: provider,
|
|
397
|
+
};
|
|
398
|
+
journal.record(`connection refused ${reason}`);
|
|
399
|
+
emitRefusal();
|
|
400
|
+
}
|
|
291
401
|
}
|
|
292
402
|
function list(room, synchronizationAvailable) {
|
|
403
|
+
const selection = room.typedSelection ?? null;
|
|
293
404
|
rooms.set(room.locator, {
|
|
294
405
|
name: room.name,
|
|
295
|
-
collectionRoot: room.collectionRoot,
|
|
296
406
|
locator: room.locator,
|
|
297
407
|
operations: room.operations,
|
|
298
408
|
replica: room.replica,
|
|
299
409
|
attachedModel: room.attachedModel,
|
|
300
410
|
model: modelTypes(modelOf(room.locator)),
|
|
411
|
+
typedSelection: selection === null
|
|
412
|
+
? null
|
|
413
|
+
: {
|
|
414
|
+
roots: selection.roots,
|
|
415
|
+
attachedModel: selection.attachedModel,
|
|
416
|
+
model: modelTypes(modelOf(room.locator, 'typed-selection')),
|
|
417
|
+
},
|
|
301
418
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
302
419
|
roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
|
|
303
|
-
|
|
420
|
+
attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
|
|
421
|
+
synchronizationAvailable,
|
|
422
|
+
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
304
423
|
});
|
|
305
424
|
}
|
|
306
|
-
function applyDiscovery(
|
|
307
|
-
const listedLocators = new Set(
|
|
308
|
-
for (const
|
|
425
|
+
async function applyDiscovery(listed, local) {
|
|
426
|
+
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
427
|
+
for (const locator of listedLocators)
|
|
428
|
+
scopedRefusals.delete(locator);
|
|
429
|
+
discovered.clear();
|
|
430
|
+
for (const room of listed)
|
|
431
|
+
discovered.set(room.locator, room);
|
|
432
|
+
for (const locator of wanted.keys())
|
|
433
|
+
if (!listedLocators.has(locator))
|
|
434
|
+
wanted.delete(locator);
|
|
435
|
+
await dropUnlisted(listedLocators, local);
|
|
436
|
+
for (const room of listed) {
|
|
309
437
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
310
438
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
311
439
|
list(held, false);
|
|
@@ -313,136 +441,176 @@ export function createMdRuntime(options) {
|
|
|
313
441
|
}
|
|
314
442
|
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
315
443
|
}
|
|
316
|
-
|
|
317
|
-
if (listedLocators.has(locator)) continue;
|
|
318
|
-
rooms.set(locator, {
|
|
319
|
-
...room,
|
|
320
|
-
synchronizationAvailable: false
|
|
321
|
-
});
|
|
322
|
-
}
|
|
444
|
+
roomsListed = true;
|
|
323
445
|
emitRooms();
|
|
324
|
-
|
|
446
|
+
}
|
|
447
|
+
function unlistedHeld(listed) {
|
|
448
|
+
return [...new Set([...rooms.keys(), ...roomDrafts.keys()])].filter((locator) => !listed.has(locator));
|
|
449
|
+
}
|
|
450
|
+
async function dropUnlisted(listed, local) {
|
|
451
|
+
if (unlistedHeld(listed).length === 0)
|
|
452
|
+
return;
|
|
453
|
+
await exclusive(async () => {
|
|
454
|
+
const unlisted = unlistedHeld(listed);
|
|
455
|
+
await local.dropRooms(unlisted);
|
|
456
|
+
const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
|
|
457
|
+
room: true,
|
|
458
|
+
queue: true,
|
|
459
|
+
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
460
|
+
}));
|
|
461
|
+
for (const locator of unlisted) {
|
|
462
|
+
for (const batch of replicas.get(locator)?.queue ?? [])
|
|
463
|
+
pendings.delete(batch.id);
|
|
464
|
+
replicas.delete(locator);
|
|
465
|
+
rooms.delete(locator);
|
|
466
|
+
roomDrafts.delete(locator);
|
|
467
|
+
unreadable.delete(locator);
|
|
468
|
+
scopedRefusals.delete(locator);
|
|
469
|
+
sourceRefusals.delete(locator);
|
|
470
|
+
declarationRefusals.delete(locator);
|
|
471
|
+
for (const attached of ['collection', 'typed-selection']) {
|
|
472
|
+
const key = modelKey(locator, attached);
|
|
473
|
+
models.delete(key);
|
|
474
|
+
liveModels.delete(key);
|
|
475
|
+
invalidModels.delete(key);
|
|
476
|
+
modelEpoch.delete(key);
|
|
477
|
+
}
|
|
478
|
+
journal.record(`room unlisted ${locator}`);
|
|
479
|
+
}
|
|
480
|
+
publishChangeSet(mergeChangeSets(removed));
|
|
481
|
+
moveTo({ ...status, ...attention() });
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
function restored(key, persisted) {
|
|
485
|
+
const prior = models.get(key);
|
|
486
|
+
if (prior !== undefined &&
|
|
487
|
+
prior.status !== 'unavailable' &&
|
|
488
|
+
(persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
|
|
489
|
+
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
490
|
+
invalidModels.add(key);
|
|
491
|
+
}
|
|
492
|
+
const model = persisted.status === 'unavailable' ||
|
|
493
|
+
liveModels.get(key) === JSON.stringify(persisted.snapshot)
|
|
494
|
+
? persisted
|
|
495
|
+
: cached(persisted);
|
|
496
|
+
models.set(key, model);
|
|
497
|
+
if (model.status === 'unavailable') {
|
|
498
|
+
liveModels.delete(key);
|
|
499
|
+
invalidModels.delete(key);
|
|
500
|
+
}
|
|
501
|
+
return model;
|
|
325
502
|
}
|
|
326
503
|
async function hydrate(opened) {
|
|
327
|
-
const [records, queues, kept, lastCompleted, declared] = await Promise.all([
|
|
504
|
+
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
|
|
328
505
|
opened.all(),
|
|
329
506
|
opened.queues(),
|
|
330
|
-
opened.
|
|
507
|
+
opened.roomDrafts(),
|
|
508
|
+
opened.presentation(),
|
|
509
|
+
opened.conversations(),
|
|
510
|
+
opened.chatDrafts(),
|
|
331
511
|
opened.lastCompleted(),
|
|
332
|
-
opened.providerLimits()
|
|
512
|
+
opened.providerLimits(),
|
|
513
|
+
opened.capabilities(),
|
|
333
514
|
]);
|
|
334
515
|
limits = declared;
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
516
|
+
capabilities = named;
|
|
517
|
+
roomDrafts.clear();
|
|
518
|
+
for (const record of kept)
|
|
519
|
+
rememberRoomDraft(record);
|
|
520
|
+
presentations.clear();
|
|
521
|
+
for (const record of presented)
|
|
522
|
+
presentations.set(record.app, record.value);
|
|
523
|
+
conversations.hydrate(held, chatDrafts);
|
|
524
|
+
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
525
|
+
pendings.clear();
|
|
526
|
+
for (const queue of queues) {
|
|
527
|
+
for (const { batch, state } of queue.pending)
|
|
528
|
+
pendings.set(batch.id, state);
|
|
346
529
|
}
|
|
530
|
+
const previousReplicas = new Map(replicas);
|
|
347
531
|
replicas.clear();
|
|
348
|
-
for (const record of records){
|
|
349
|
-
const
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const confirmed = readConfirmed({
|
|
362
|
-
...record,
|
|
363
|
-
model
|
|
364
|
-
});
|
|
365
|
-
if (confirmed === null) continue;
|
|
366
|
-
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? []));
|
|
532
|
+
for (const record of records) {
|
|
533
|
+
const model = restored(modelKey(record.locator, 'collection'), record.model);
|
|
534
|
+
const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
|
|
535
|
+
const prior = previousReplicas.get(record.locator);
|
|
536
|
+
const retyped = { ...record, model, selectionModel };
|
|
537
|
+
const read = readConfirmed(retyped, prior?.confirmed);
|
|
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));
|
|
367
545
|
const existing = rooms.get(record.locator);
|
|
368
|
-
if (existing === undefined)
|
|
546
|
+
if (existing === undefined)
|
|
547
|
+
list(record, false);
|
|
369
548
|
else {
|
|
370
549
|
rooms.set(record.locator, {
|
|
371
550
|
...existing,
|
|
372
551
|
roomVersion: record.roomVersion,
|
|
552
|
+
attachments: record.attachments,
|
|
373
553
|
operations: record.operations,
|
|
374
554
|
replica: record.replica,
|
|
375
555
|
attachedModel: record.attachedModel,
|
|
376
|
-
model: modelTypes(model)
|
|
556
|
+
model: modelTypes(model),
|
|
557
|
+
typedSelection: record.typedSelection === null
|
|
558
|
+
? null
|
|
559
|
+
: {
|
|
560
|
+
roots: record.typedSelection.roots,
|
|
561
|
+
attachedModel: record.typedSelection.attachedModel,
|
|
562
|
+
model: modelTypes(selectionModel),
|
|
563
|
+
},
|
|
377
564
|
});
|
|
378
565
|
}
|
|
566
|
+
if (read.kind === 'refused') {
|
|
567
|
+
refusedSource(record, read);
|
|
568
|
+
declarationRefusals.add(record.locator);
|
|
569
|
+
}
|
|
379
570
|
}
|
|
380
571
|
return {
|
|
381
572
|
lastCompleted,
|
|
382
|
-
locators: new Set([
|
|
383
|
-
|
|
384
|
-
...replicas.keys()
|
|
385
|
-
])
|
|
573
|
+
locators: new Set([...queuedBatches.keys(), ...replicas.keys()]),
|
|
574
|
+
read: mergeChangeSets([...new Set([...previousReplicas.keys(), ...replicas.keys()])].map((locator) => changeSet(locator, previousReplicas.get(locator) ?? null, replicas.get(locator) ?? null))),
|
|
386
575
|
};
|
|
387
576
|
}
|
|
388
577
|
async function readState(opened) {
|
|
389
|
-
const { lastCompleted, locators } = await hydrate(opened);
|
|
390
|
-
|
|
578
|
+
const { lastCompleted, locators, read } = await hydrate(opened);
|
|
579
|
+
applyChangeSet(read);
|
|
580
|
+
if (replicas.size > 0)
|
|
391
581
|
emitRooms();
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
emitDrafts();
|
|
582
|
+
for (const locator of locators)
|
|
583
|
+
emitQueue(locator);
|
|
584
|
+
publishRoomDrafts(false);
|
|
396
585
|
return lastCompleted;
|
|
397
586
|
}
|
|
398
|
-
async function applyRemote(message) {
|
|
399
|
-
if (closed) return;
|
|
400
|
-
if (message.type === 'hello') {
|
|
401
|
-
if (leader) announce();
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
applyingRemote = true;
|
|
405
|
-
try {
|
|
406
|
-
const local = await openLocal();
|
|
407
|
-
const lastCompleted = await readState(local);
|
|
408
|
-
if (message.status !== undefined) adopt(message.status);
|
|
409
|
-
else {
|
|
410
|
-
adopt({
|
|
411
|
-
...status,
|
|
412
|
-
lastCompleted,
|
|
413
|
-
attentionRequired: attention(),
|
|
414
|
-
state: pending() ? 'pending' : status.state
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
} finally{
|
|
418
|
-
applyingRemote = false;
|
|
419
|
-
}
|
|
420
|
-
if (message.type === 'enqueue' && leader) void enqueue();
|
|
421
|
-
}
|
|
422
587
|
async function open() {
|
|
423
|
-
const opened = await
|
|
588
|
+
const opened = await openRuntimeStore();
|
|
424
589
|
store = opened;
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
590
|
+
const kept = options.shell.generation;
|
|
591
|
+
const storedGeneration = await opened.generation();
|
|
592
|
+
const loss = detectStorageLoss(storedGeneration, kept);
|
|
593
|
+
const generation = nextGeneration(storedGeneration, kept);
|
|
594
|
+
if (generation !== storedGeneration)
|
|
595
|
+
await opened.rememberGeneration(generation);
|
|
596
|
+
if (generation !== kept)
|
|
597
|
+
await options.shell.keepGeneration(generation);
|
|
598
|
+
if (loss !== null) {
|
|
599
|
+
storageLoss = loss;
|
|
600
|
+
emitStorageLoss();
|
|
601
|
+
}
|
|
435
602
|
const lastCompleted = await readState(opened);
|
|
436
|
-
|
|
437
|
-
...status,
|
|
438
|
-
lastCompleted,
|
|
439
|
-
attentionRequired: attention()
|
|
440
|
-
});
|
|
441
|
-
if (!leader) announce('hello');
|
|
603
|
+
moveTo({ ...status, lastCompleted, ...attention() });
|
|
442
604
|
return opened;
|
|
443
605
|
}
|
|
606
|
+
function durably(work) {
|
|
607
|
+
return exclusive(async () => {
|
|
608
|
+
const local = await openLocal();
|
|
609
|
+
return work(local);
|
|
610
|
+
});
|
|
611
|
+
}
|
|
444
612
|
function openLocal() {
|
|
445
|
-
opening ??= open().catch((error)=>{
|
|
613
|
+
opening ??= open().catch((error) => {
|
|
446
614
|
opening = undefined;
|
|
447
615
|
throw error;
|
|
448
616
|
});
|
|
@@ -450,253 +618,326 @@ export function createMdRuntime(options) {
|
|
|
450
618
|
}
|
|
451
619
|
function exclusive(work) {
|
|
452
620
|
const next = mutations.then(work, work);
|
|
453
|
-
mutations = next.then(()=>undefined, ()=>undefined);
|
|
621
|
+
mutations = next.then(() => undefined, () => undefined);
|
|
454
622
|
return next;
|
|
455
623
|
}
|
|
456
|
-
function
|
|
457
|
-
|
|
458
|
-
return
|
|
624
|
+
function withdrawable(batch) {
|
|
625
|
+
const state = pendings.get(batch.id);
|
|
626
|
+
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
459
627
|
}
|
|
460
|
-
function
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
628
|
+
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
629
|
+
const withdrawn = withdrawals(confirmed, queue, withdrawable, rewriting());
|
|
630
|
+
const kept = queue.filter((batch) => !withdrawn.includes(batch));
|
|
631
|
+
const returning = [
|
|
632
|
+
...withdrawn.map((batch) => ({ batch, returned: { kind: 'withdrawn' } })).reverse(),
|
|
633
|
+
...(settling.leaving === undefined ? [] : [settling.leaving]),
|
|
634
|
+
];
|
|
635
|
+
const written = new Map();
|
|
636
|
+
for (const { batch, returned } of returning) {
|
|
637
|
+
if (returned === null)
|
|
638
|
+
continue;
|
|
639
|
+
for (const saved of pendings.get(batch.id)?.drafts ?? []) {
|
|
640
|
+
const existing = written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path);
|
|
641
|
+
written.set(saved.path, {
|
|
642
|
+
locator,
|
|
643
|
+
path: saved.path,
|
|
644
|
+
revision: roomDraftRevision(),
|
|
645
|
+
parts: returnedInto(existing?.parts, saved.parts),
|
|
646
|
+
returned,
|
|
467
647
|
});
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
await local.write({
|
|
651
|
+
next: confirmed.record,
|
|
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 ?? []) {
|
|
658
|
+
if (!kept.includes(batch))
|
|
659
|
+
pendings.delete(batch.id);
|
|
660
|
+
}
|
|
661
|
+
for (const record of written.values())
|
|
662
|
+
rememberRoomDraft(record);
|
|
663
|
+
for (const batch of withdrawn)
|
|
664
|
+
journal.record(`outcome ${locator} ${batch.id} withdrawn`);
|
|
665
|
+
replicas.set(locator, replicaOf(confirmed, kept, rewriting(), before));
|
|
666
|
+
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
667
|
+
room: settling.room,
|
|
668
|
+
queue: kept.length !== (before?.queue.length ?? 0),
|
|
669
|
+
drafts: [...written.keys()],
|
|
477
670
|
});
|
|
478
671
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
model: invalidModels.has(locator) ? {
|
|
497
|
-
status: 'unavailable',
|
|
498
|
-
reason: 'revoked'
|
|
499
|
-
} : modelOf(locator),
|
|
500
|
-
binding: options.modelBinding,
|
|
501
|
-
providerOwnedPaths: room.providerOwnedPaths,
|
|
502
|
-
providerLimits: limits,
|
|
503
|
-
visible: replica.visible,
|
|
504
|
-
changes
|
|
672
|
+
function publish(local, room, confirmed) {
|
|
673
|
+
return exclusive(async () => {
|
|
674
|
+
if (!continuing())
|
|
675
|
+
return;
|
|
676
|
+
const prior = replicas.get(room.locator);
|
|
677
|
+
const retained = readConfirmed({
|
|
678
|
+
...confirmed.record,
|
|
679
|
+
model: modelOf(room.locator),
|
|
680
|
+
selectionModel: modelOf(room.locator, 'typed-selection'),
|
|
681
|
+
}, prior?.confirmed);
|
|
682
|
+
if (retained.kind !== 'staged')
|
|
683
|
+
throw new Error(`Cannot retype staged Room ${room.locator}`);
|
|
684
|
+
const queue = prior?.queue ?? [];
|
|
685
|
+
const roomVersion = retained.confirmed.record.roomVersion;
|
|
686
|
+
const accepted = queue.filter((batch) => {
|
|
687
|
+
const accepted = pendings.get(batch.id)?.accepted;
|
|
688
|
+
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
505
689
|
});
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
const head = replica.queue[0];
|
|
511
|
-
if (replacing !== undefined) {
|
|
512
|
-
if (head === undefined || head.id !== replacing || !refused(outcomes.get(head.id))) {
|
|
513
|
-
return {
|
|
514
|
-
outcome: 'refused',
|
|
515
|
-
rule: 'no-refused-head'
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
const batches = replacing === undefined ? [
|
|
520
|
-
...replica.queue,
|
|
521
|
-
built.batch
|
|
522
|
-
] : [
|
|
523
|
-
built.batch,
|
|
524
|
-
...replica.queue.slice(1)
|
|
525
|
-
];
|
|
526
|
-
const consumed = [
|
|
527
|
-
...new Set(changes.map((change)=>change.path))
|
|
528
|
-
];
|
|
529
|
-
try {
|
|
530
|
-
await local.append({
|
|
531
|
-
locator,
|
|
532
|
-
batches,
|
|
533
|
-
outcomes: storedOutcomes(batches)
|
|
534
|
-
}, consumed);
|
|
535
|
-
} catch {
|
|
536
|
-
return {
|
|
537
|
-
outcome: 'refused',
|
|
538
|
-
rule: 'durable-write'
|
|
539
|
-
};
|
|
690
|
+
const set = await settle(local, room.locator, prior, retained.confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
|
|
691
|
+
const target = wanted.get(room.locator);
|
|
692
|
+
if (target !== undefined && options.adapter.reached(room.locator, roomVersion, target)) {
|
|
693
|
+
wanted.delete(room.locator);
|
|
540
694
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
moveTo({
|
|
548
|
-
...saving(status),
|
|
549
|
-
attentionRequired: attention()
|
|
550
|
-
});
|
|
551
|
-
return {
|
|
552
|
-
outcome: 'saved',
|
|
553
|
-
id: built.batch.id
|
|
554
|
-
};
|
|
695
|
+
unreadable.delete(room.locator);
|
|
696
|
+
sourceRefusals.delete(room.locator);
|
|
697
|
+
declarationRefusals.delete(room.locator);
|
|
698
|
+
list(room, true);
|
|
699
|
+
publishChangeSet(set);
|
|
700
|
+
moveTo({ ...status, ...attention() });
|
|
555
701
|
});
|
|
556
|
-
if (result.outcome === 'saved') void enqueue();
|
|
557
|
-
return result;
|
|
558
702
|
}
|
|
559
|
-
async function
|
|
703
|
+
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
560
704
|
const local = await openLocal();
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
705
|
+
const replica = replicas.get(locator);
|
|
706
|
+
const room = rooms.get(locator);
|
|
707
|
+
if (replica === undefined || room === undefined) {
|
|
708
|
+
throw new Error(`No Local Replica for ${locator}`);
|
|
709
|
+
}
|
|
710
|
+
const saved = [];
|
|
711
|
+
for (const { path, revision } of named) {
|
|
712
|
+
const record = roomDrafts.get(locator)?.get(path);
|
|
713
|
+
if (record === undefined || record.revision !== revision) {
|
|
714
|
+
return { outcome: 'refused', rule: 'stale', file: path };
|
|
567
715
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
716
|
+
if (!saved.some((draft) => draft.path === path))
|
|
717
|
+
saved.push({ path, parts: record.parts });
|
|
718
|
+
}
|
|
719
|
+
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
|
|
720
|
+
if (changed.length > 0)
|
|
721
|
+
return { outcome: 'refused', rule: 'changed-underneath', parts: changed };
|
|
722
|
+
const parts = saved.map(({ path, parts }) => changesOf(path, parts));
|
|
723
|
+
const built = buildBatch({
|
|
724
|
+
baseRoomVersion: replica.confirmed.record.roomVersion,
|
|
725
|
+
composition: compositionFor(replica.confirmed.record),
|
|
726
|
+
operations: room.operations,
|
|
727
|
+
providerOwnedPaths: room.providerOwnedPaths,
|
|
728
|
+
providerLimits: limits,
|
|
729
|
+
rewriting: rewriting(),
|
|
730
|
+
visible: replica.visible,
|
|
731
|
+
changes: parts.flatMap(({ changes }) => changes),
|
|
732
|
+
bases: parts.flatMap(({ bases }) => bases),
|
|
733
|
+
writtenBy,
|
|
580
734
|
});
|
|
581
|
-
if (
|
|
735
|
+
if (built.kind === 'refused')
|
|
736
|
+
return { outcome: 'refused', ...built.refusal };
|
|
737
|
+
const batches = [...replica.queue, built.batch];
|
|
738
|
+
const consumed = saved.map(({ path }) => path);
|
|
739
|
+
try {
|
|
740
|
+
await local.write({
|
|
741
|
+
next: replica.confirmed.record,
|
|
742
|
+
previous: replica.confirmed.record,
|
|
743
|
+
queue: batches,
|
|
744
|
+
previousQueue: replica.queue,
|
|
745
|
+
saved,
|
|
746
|
+
consumedRoomDrafts: consumed,
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
catch {
|
|
750
|
+
return { outcome: 'refused', rule: 'durable-write' };
|
|
751
|
+
}
|
|
752
|
+
pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
|
|
753
|
+
for (const path of consumed)
|
|
754
|
+
forgetRoomDraft(locator, path);
|
|
755
|
+
replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting(), replica));
|
|
756
|
+
publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
|
|
757
|
+
queue: true,
|
|
758
|
+
drafts: consumed,
|
|
759
|
+
}));
|
|
760
|
+
moveTo({ ...saving(status), ...attention() });
|
|
761
|
+
void enqueue();
|
|
762
|
+
return { outcome: 'saved', identifier: built.batch.id };
|
|
763
|
+
}
|
|
764
|
+
async function mark(local, locator, identifier, submission, lost) {
|
|
765
|
+
const state = pendings.get(identifier);
|
|
766
|
+
if (!continuing() ||
|
|
767
|
+
state === undefined ||
|
|
768
|
+
(JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
await local.submit(locator, identifier, submission, lost);
|
|
772
|
+
pendings.set(identifier, { ...state, submission, lost });
|
|
773
|
+
const replica = replicas.get(locator);
|
|
774
|
+
if (replica !== undefined)
|
|
775
|
+
publishChangeSet(changeSet(locator, replica, replica, { queue: true }));
|
|
776
|
+
moveTo({ ...status, ...attention() });
|
|
777
|
+
}
|
|
778
|
+
async function keepUpload(local, locator, identifier, upload) {
|
|
779
|
+
if (!continuing())
|
|
780
|
+
throw new Error('connection refused');
|
|
781
|
+
await local.keepUpload(locator, identifier, upload);
|
|
782
|
+
pendings.set(identifier, { ...pendings.get(identifier), upload });
|
|
783
|
+
if (!continuing())
|
|
784
|
+
throw new Error('connection refused');
|
|
582
785
|
}
|
|
583
786
|
async function readHistory(locator, path, query = {}) {
|
|
584
|
-
if (closed || refusal !== null)
|
|
585
|
-
kind: 'transport-uncertainty'
|
|
586
|
-
};
|
|
787
|
+
if (closed || refusal !== null)
|
|
788
|
+
return { kind: 'transport-uncertainty' };
|
|
587
789
|
try {
|
|
588
790
|
await ensureAuthorization();
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
};
|
|
791
|
+
}
|
|
792
|
+
catch {
|
|
793
|
+
return { kind: 'transport-uncertainty' };
|
|
593
794
|
}
|
|
594
795
|
if (closed || authorization === undefined || refusal !== null) {
|
|
595
|
-
return {
|
|
596
|
-
kind: 'transport-uncertainty'
|
|
597
|
-
};
|
|
796
|
+
return { kind: 'transport-uncertainty' };
|
|
598
797
|
}
|
|
599
|
-
const transfer = [
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
];
|
|
605
|
-
if (query.revision !== undefined) transfer.push(query.revision);
|
|
606
|
-
if (query.pageToken !== undefined) transfer.push(query.pageToken);
|
|
798
|
+
const transfer = ['transfer', 'history', locator, path];
|
|
799
|
+
if (query.revision !== undefined)
|
|
800
|
+
transfer.push(query.revision);
|
|
801
|
+
if (query.pageToken !== undefined)
|
|
802
|
+
transfer.push(query.pageToken);
|
|
607
803
|
journal.record(transfer.join(' '));
|
|
608
804
|
try {
|
|
609
805
|
const result = await options.adapter.history(authorization, {
|
|
610
806
|
locator,
|
|
611
807
|
path,
|
|
612
|
-
...query.revision === undefined ? {} : {
|
|
613
|
-
|
|
614
|
-
},
|
|
615
|
-
...query.pageToken === undefined ? {} : {
|
|
616
|
-
pageToken: query.pageToken
|
|
617
|
-
}
|
|
808
|
+
...(query.revision === undefined ? {} : { revision: query.revision }),
|
|
809
|
+
...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
|
|
618
810
|
});
|
|
619
811
|
journal.record(`outcome history ${result.kind}`);
|
|
620
812
|
if (result.kind === 'scoped-refusal') {
|
|
621
813
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
622
|
-
if (room !== undefined)
|
|
814
|
+
if (room !== undefined)
|
|
815
|
+
await scoped(result, room, await openLocal());
|
|
623
816
|
}
|
|
624
|
-
if (result.kind === 'revoked-authorization')
|
|
817
|
+
if (result.kind === 'revoked-authorization')
|
|
818
|
+
await refuse('revoked-authorization');
|
|
625
819
|
return result;
|
|
626
|
-
}
|
|
820
|
+
}
|
|
821
|
+
catch {
|
|
627
822
|
journal.record('outcome history transport-uncertainty');
|
|
628
|
-
return {
|
|
629
|
-
|
|
630
|
-
|
|
823
|
+
return { kind: 'transport-uncertainty' };
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
function declaredCards() {
|
|
827
|
+
for (const capability of capabilities) {
|
|
828
|
+
if (capability.conversationCards !== undefined)
|
|
829
|
+
return capability.conversationCards;
|
|
631
830
|
}
|
|
831
|
+
return undefined;
|
|
632
832
|
}
|
|
633
833
|
function declaredAction(name) {
|
|
634
|
-
for (const capability of capabilities){
|
|
635
|
-
for (const action of capability.actions ?? []){
|
|
636
|
-
if (action.name === name)
|
|
834
|
+
for (const capability of capabilities) {
|
|
835
|
+
for (const action of capability.actions ?? []) {
|
|
836
|
+
if (action.name === name)
|
|
837
|
+
return action;
|
|
637
838
|
}
|
|
638
839
|
}
|
|
639
840
|
return undefined;
|
|
640
841
|
}
|
|
641
842
|
async function invokeAction(name, input) {
|
|
642
843
|
await ready;
|
|
643
|
-
if (closed || refusal !== null)
|
|
644
|
-
kind: 'transport-uncertainty'
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
kind: 'undeclared'
|
|
648
|
-
};
|
|
844
|
+
if (closed || refusal !== null)
|
|
845
|
+
return { kind: 'transport-uncertainty' };
|
|
846
|
+
if (declaredAction(name) === undefined)
|
|
847
|
+
return { kind: 'undeclared' };
|
|
649
848
|
try {
|
|
650
849
|
await ensureAuthorization();
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
};
|
|
850
|
+
}
|
|
851
|
+
catch {
|
|
852
|
+
return { kind: 'transport-uncertainty' };
|
|
655
853
|
}
|
|
656
854
|
if (closed || authorization === undefined || refusal !== null) {
|
|
657
|
-
return {
|
|
658
|
-
kind: 'transport-uncertainty'
|
|
659
|
-
};
|
|
855
|
+
return { kind: 'transport-uncertainty' };
|
|
660
856
|
}
|
|
661
857
|
journal.record(`transfer action ${name}`);
|
|
662
858
|
try {
|
|
663
|
-
const result = await options.adapter.action(authorization, {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
});
|
|
859
|
+
const result = await options.adapter.action(authorization, { name, input });
|
|
860
|
+
if (result.kind === 'action' && result.at !== undefined)
|
|
861
|
+
want(result.at);
|
|
667
862
|
if (result.kind === 'scoped-refusal') {
|
|
668
863
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
669
|
-
if (room !== undefined)
|
|
864
|
+
if (room !== undefined)
|
|
865
|
+
await scoped(result, room, await openLocal());
|
|
670
866
|
}
|
|
671
|
-
if (result.kind === 'revoked-authorization')
|
|
867
|
+
if (result.kind === 'revoked-authorization')
|
|
868
|
+
await refuse('revoked-authorization');
|
|
672
869
|
journal.record(`outcome action ${result.kind}`);
|
|
673
|
-
if (result.kind === 'rejected')
|
|
870
|
+
if (result.kind === 'rejected')
|
|
871
|
+
journal.record(`refusal rejected ${result.code}`);
|
|
674
872
|
return result;
|
|
675
|
-
}
|
|
873
|
+
}
|
|
874
|
+
catch {
|
|
676
875
|
journal.record('outcome action transport-uncertainty');
|
|
677
|
-
return {
|
|
678
|
-
|
|
679
|
-
|
|
876
|
+
return { kind: 'transport-uncertainty' };
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
function continuing() {
|
|
880
|
+
return !closed && !discoveryPaused;
|
|
881
|
+
}
|
|
882
|
+
function reachedBy(locator, roomVersion) {
|
|
883
|
+
const held = replicas.get(locator);
|
|
884
|
+
return (held !== undefined &&
|
|
885
|
+
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion));
|
|
886
|
+
}
|
|
887
|
+
function writtenAfter(locator, roomVersion, path) {
|
|
888
|
+
const written = replicas.get(locator)?.confirmed.record.written[path];
|
|
889
|
+
return (written !== undefined &&
|
|
890
|
+
options.adapter.reached(locator, written, roomVersion) &&
|
|
891
|
+
!options.adapter.reached(locator, roomVersion, written));
|
|
892
|
+
}
|
|
893
|
+
function want({ locator, roomVersion }) {
|
|
894
|
+
if (reachedBy(locator, roomVersion))
|
|
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
|
+
}
|
|
680
911
|
}
|
|
912
|
+
return target === undefined || reachedBy(locator, target) ? undefined : target;
|
|
681
913
|
}
|
|
682
|
-
async function pullRefresh(authorized, confirmed) {
|
|
914
|
+
async function pullRefresh(authorized, confirmed, minPosition) {
|
|
915
|
+
if (!continuing())
|
|
916
|
+
return unconfirmed;
|
|
683
917
|
try {
|
|
684
|
-
const result = await refreshRoom(options.adapter, authorized, confirmed,
|
|
918
|
+
const result = await refreshRoom(options.adapter, authorized, confirmed, continuing, minPosition);
|
|
685
919
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
686
920
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
687
921
|
return result;
|
|
688
|
-
}
|
|
922
|
+
}
|
|
923
|
+
catch (error) {
|
|
689
924
|
journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
|
|
690
925
|
throw error;
|
|
691
926
|
}
|
|
692
927
|
}
|
|
693
928
|
async function pullBootstrap(authorized, room) {
|
|
929
|
+
if (!continuing())
|
|
930
|
+
return unconfirmed;
|
|
694
931
|
try {
|
|
695
|
-
const result = await bootstrapRoom(options.adapter, authorized, room,
|
|
932
|
+
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
933
|
+
collection: modelOf(room.locator),
|
|
934
|
+
selection: modelOf(room.locator, 'typed-selection'),
|
|
935
|
+
}, continuing);
|
|
696
936
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
697
937
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
698
938
|
return result;
|
|
699
|
-
}
|
|
939
|
+
}
|
|
940
|
+
catch (error) {
|
|
700
941
|
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
701
942
|
throw error;
|
|
702
943
|
}
|
|
@@ -704,8 +945,12 @@ export function createMdRuntime(options) {
|
|
|
704
945
|
async function advance(room, local, authorized) {
|
|
705
946
|
const held = replicas.get(room.locator);
|
|
706
947
|
if (held !== undefined) {
|
|
707
|
-
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
948
|
+
const refreshed = await pullRefresh(authorized, held.confirmed, reachOf(room.locator));
|
|
949
|
+
if (!continuing())
|
|
950
|
+
return unconfirmed;
|
|
708
951
|
if (refreshed.kind === 'still-current') {
|
|
952
|
+
if (sourceRefusals.has(room.locator))
|
|
953
|
+
return unconfirmed;
|
|
709
954
|
if (unreadable.delete(room.locator)) {
|
|
710
955
|
list(room, true);
|
|
711
956
|
emitRooms();
|
|
@@ -713,386 +958,560 @@ export function createMdRuntime(options) {
|
|
|
713
958
|
return null;
|
|
714
959
|
}
|
|
715
960
|
if (refreshed.kind === 'advanced') {
|
|
716
|
-
if (closed) return unconfirmed;
|
|
717
961
|
await publish(local, room, refreshed.confirmed);
|
|
718
962
|
return null;
|
|
719
963
|
}
|
|
720
|
-
if (refreshed.kind !== 'beyond-horizon')
|
|
964
|
+
if (refreshed.kind !== 'beyond-horizon')
|
|
965
|
+
return refreshed;
|
|
721
966
|
}
|
|
722
967
|
const bootstrapped = await pullBootstrap(authorized, room);
|
|
723
|
-
if (
|
|
724
|
-
|
|
968
|
+
if (!continuing())
|
|
969
|
+
return unconfirmed;
|
|
970
|
+
if (bootstrapped.kind !== 'staged')
|
|
971
|
+
return bootstrapped;
|
|
725
972
|
await publish(local, room, bootstrapped.confirmed);
|
|
726
973
|
return null;
|
|
727
974
|
}
|
|
728
|
-
function
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
975
|
+
async function withhold(withheld, room, local, progress) {
|
|
976
|
+
progress.complete = false;
|
|
977
|
+
if (withheld.kind === 'scoped-refusal') {
|
|
978
|
+
await scoped(withheld, room, local);
|
|
979
|
+
}
|
|
980
|
+
else if (withheld.kind === 'transport-uncertainty') {
|
|
981
|
+
progress.reachable = false;
|
|
982
|
+
}
|
|
983
|
+
else if (withheld.kind === 'retryable-refusal') {
|
|
984
|
+
journal.record(`refusal ${withheld.kind} ${withheld.code}`);
|
|
985
|
+
}
|
|
986
|
+
else if (withheld.kind === 'room-not-readable') {
|
|
987
|
+
unreadable.add(room.locator);
|
|
988
|
+
list(room, false);
|
|
989
|
+
emitRooms();
|
|
990
|
+
}
|
|
991
|
+
else if (withheld.kind === 'refused') {
|
|
992
|
+
refusedSource(room, withheld);
|
|
993
|
+
}
|
|
994
|
+
else if (withheld.kind === 'revoked-authorization') {
|
|
995
|
+
await refuse('revoked-authorization');
|
|
739
996
|
}
|
|
740
|
-
return {
|
|
741
|
-
kind: 'rejected',
|
|
742
|
-
affected: result.affected,
|
|
743
|
-
text: result.text
|
|
744
|
-
};
|
|
745
997
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
998
|
+
function refusedSource(room, { refusal }) {
|
|
999
|
+
sourceRefusals.set(room.locator, {
|
|
1000
|
+
code: refusal.rule,
|
|
1001
|
+
fields: {
|
|
1002
|
+
file: refusal.file,
|
|
1003
|
+
...(refusal.location === undefined ? {} : { location: refusal.location }),
|
|
1004
|
+
...(refusal.referrers === undefined ? {} : { referrers: refusal.referrers }),
|
|
1005
|
+
},
|
|
1006
|
+
});
|
|
1007
|
+
declarationRefusals.delete(room.locator);
|
|
1008
|
+
unreadable.add(room.locator);
|
|
1009
|
+
list(room, false);
|
|
1010
|
+
emitRooms();
|
|
1011
|
+
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
1012
|
+
}
|
|
1013
|
+
function leaving(batch, result) {
|
|
1014
|
+
if (result.kind === 'expired')
|
|
1015
|
+
return { batch, returned: { kind: 'expired' } };
|
|
1016
|
+
return { batch, returned: { kind: result.kind, code: result.code, fields: result.fields } };
|
|
1017
|
+
}
|
|
1018
|
+
function logAccepted(locator, submitted, accepted) {
|
|
1019
|
+
journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
|
|
1020
|
+
(accepted.author === undefined ? '' : ` author ${accepted.author}`) +
|
|
1021
|
+
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1022
|
+
}
|
|
1023
|
+
async function recordAccepted(room, local, submitted, accepted) {
|
|
1024
|
+
await exclusive(async () => {
|
|
1025
|
+
if (!continuing())
|
|
1026
|
+
return;
|
|
749
1027
|
const current = replicas.get(room.locator);
|
|
750
|
-
if (current === undefined || !current.queue.some((batch)=>batch.id === submitted.id)) {
|
|
751
|
-
return
|
|
1028
|
+
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
1029
|
+
return;
|
|
752
1030
|
}
|
|
753
|
-
const
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
});
|
|
758
|
-
|
|
759
|
-
const applied = overlay ? applyReturned(retainedBase, accepted.documents, accepted.revisions, accepted.removals, accepted.roomVersion, options.modelBinding) : retainedBase;
|
|
760
|
-
if (applied === null) return false;
|
|
761
|
-
await local.publish(applied.record);
|
|
762
|
-
await persistQueue(local, room.locator, remaining);
|
|
763
|
-
outcomes.set(submitted.id, {
|
|
764
|
-
kind: 'accepted',
|
|
765
|
-
roomVersion: accepted.roomVersion,
|
|
766
|
-
author: accepted.author,
|
|
767
|
-
time: accepted.time
|
|
768
|
-
});
|
|
769
|
-
unreadable.delete(room.locator);
|
|
770
|
-
replicas.set(room.locator, replicaOf(applied, remaining));
|
|
771
|
-
list(room, true);
|
|
772
|
-
emitRooms();
|
|
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);
|
|
773
1037
|
emitQueue(room.locator);
|
|
774
|
-
emitVisible();
|
|
775
|
-
journal.record(`outcome ${room.locator} ${submitted.id} accepted`);
|
|
776
|
-
return true;
|
|
777
1038
|
});
|
|
778
1039
|
}
|
|
1040
|
+
async function commitAccept(room, local, submitted, accepted, base, overlay) {
|
|
1041
|
+
if (!continuing())
|
|
1042
|
+
return unconfirmed;
|
|
1043
|
+
const current = replicas.get(room.locator);
|
|
1044
|
+
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
1045
|
+
return null;
|
|
1046
|
+
}
|
|
1047
|
+
const remaining = current.queue.filter((batch) => batch.id !== submitted.id);
|
|
1048
|
+
const retainedBase = readConfirmed({
|
|
1049
|
+
...base.record,
|
|
1050
|
+
model: modelOf(room.locator),
|
|
1051
|
+
selectionModel: modelOf(room.locator, 'typed-selection'),
|
|
1052
|
+
}, base);
|
|
1053
|
+
const applied = overlay && retainedBase.kind === 'staged'
|
|
1054
|
+
? applyReturned(retainedBase.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.roomVersion)
|
|
1055
|
+
: retainedBase;
|
|
1056
|
+
if (applied.kind !== 'staged')
|
|
1057
|
+
return applied;
|
|
1058
|
+
const set = await settle(local, room.locator, current, applied.confirmed, remaining, {
|
|
1059
|
+
room: true,
|
|
1060
|
+
});
|
|
1061
|
+
unreadable.delete(room.locator);
|
|
1062
|
+
sourceRefusals.delete(room.locator);
|
|
1063
|
+
declarationRefusals.delete(room.locator);
|
|
1064
|
+
list(room, true);
|
|
1065
|
+
publishChangeSet(set);
|
|
1066
|
+
moveTo({ ...status, ...attention() });
|
|
1067
|
+
return null;
|
|
1068
|
+
}
|
|
1069
|
+
async function accepting(room, local, submitted, accepted, pulled, fresh) {
|
|
1070
|
+
const roomVersion = pulled.record.roomVersion;
|
|
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));
|
|
1080
|
+
if (failed === null)
|
|
1081
|
+
return true;
|
|
1082
|
+
if (failed.kind === 'refused' && continuing())
|
|
1083
|
+
refusedSource(room, failed);
|
|
1084
|
+
return false;
|
|
1085
|
+
}
|
|
779
1086
|
async function submitRoom(room, local, authorized, progress) {
|
|
780
|
-
while(
|
|
1087
|
+
while (continuing() && authorization !== undefined) {
|
|
781
1088
|
const replica = replicas.get(room.locator);
|
|
782
1089
|
const head = replica?.queue[0];
|
|
783
|
-
if (replica === undefined || head === undefined
|
|
784
|
-
return
|
|
1090
|
+
if (replica === undefined || head === undefined)
|
|
1091
|
+
return;
|
|
1092
|
+
if (pendings.get(head.id)?.accepted !== undefined) {
|
|
1093
|
+
progress.complete = false;
|
|
1094
|
+
return;
|
|
785
1095
|
}
|
|
786
|
-
|
|
787
|
-
|
|
1096
|
+
const state = pendings.get(head.id);
|
|
1097
|
+
const { submission, lost: earlier } = state ?? {
|
|
1098
|
+
submission: { state: 'waiting' },
|
|
1099
|
+
lost: false,
|
|
1100
|
+
};
|
|
1101
|
+
if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
|
|
1102
|
+
progress.complete = false;
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
if (writesHeld()) {
|
|
1106
|
+
progress.complete = false;
|
|
1107
|
+
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
const lost = earlier || submission.state === 'submitted';
|
|
1111
|
+
let result;
|
|
1112
|
+
journal.record(`submission ${room.locator} ${head.id}`);
|
|
1113
|
+
await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
|
|
1114
|
+
if (discoveryPaused)
|
|
1115
|
+
return;
|
|
1116
|
+
if (writesHeld()) {
|
|
1117
|
+
await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
|
|
1118
|
+
progress.complete = false;
|
|
1119
|
+
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
const opensBefore = openSignals;
|
|
788
1123
|
try {
|
|
789
1124
|
result = await options.adapter.mutate(authorized, {
|
|
790
1125
|
locator: room.locator,
|
|
791
|
-
changeBatch: head
|
|
792
|
-
|
|
793
|
-
|
|
1126
|
+
changeBatch: head,
|
|
1127
|
+
...(state?.upload === undefined ? {} : { upload: state.upload }),
|
|
1128
|
+
}, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
|
|
1129
|
+
}
|
|
1130
|
+
catch {
|
|
794
1131
|
progress.complete = false;
|
|
795
|
-
|
|
796
|
-
|
|
1132
|
+
if (continuing())
|
|
1133
|
+
progress.reachable = false;
|
|
1134
|
+
return;
|
|
797
1135
|
}
|
|
798
|
-
if (
|
|
1136
|
+
if (!continuing())
|
|
1137
|
+
return;
|
|
799
1138
|
if (result.kind === 'transport-uncertainty') {
|
|
800
1139
|
progress.complete = false;
|
|
801
1140
|
progress.reachable = false;
|
|
802
|
-
return
|
|
1141
|
+
return;
|
|
803
1142
|
}
|
|
804
1143
|
if (result.kind === 'room-not-readable') {
|
|
805
1144
|
progress.complete = false;
|
|
806
1145
|
unreadable.add(room.locator);
|
|
807
1146
|
list(room, false);
|
|
808
1147
|
emitRooms();
|
|
809
|
-
return
|
|
1148
|
+
return;
|
|
810
1149
|
}
|
|
811
1150
|
if (result.kind === 'revoked-authorization') {
|
|
812
|
-
refuse('revoked-authorization');
|
|
813
|
-
return
|
|
1151
|
+
await refuse('revoked-authorization');
|
|
1152
|
+
return;
|
|
814
1153
|
}
|
|
815
1154
|
if (result.kind === 'scoped-refusal') {
|
|
816
1155
|
await scoped(result, room, local);
|
|
817
1156
|
progress.complete = false;
|
|
818
|
-
return
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
if (result.kind === 'write-halted') {
|
|
1160
|
+
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
1161
|
+
await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, lost));
|
|
1162
|
+
if (serviceMode === 'open' && openSignals !== opensBefore)
|
|
1163
|
+
continue;
|
|
1164
|
+
haltedRefusal = true;
|
|
1165
|
+
progress.complete = false;
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
if (result.kind === 'conflicted' ||
|
|
1169
|
+
result.kind === 'rejected' ||
|
|
1170
|
+
result.kind === 'expired' ||
|
|
1171
|
+
result.kind === 'retryable-refusal') {
|
|
1172
|
+
if (result.kind === 'expired') {
|
|
1173
|
+
const withheld = await advance(room, local, authorized);
|
|
1174
|
+
if (withheld !== null)
|
|
1175
|
+
return withhold(withheld, room, local, progress);
|
|
1176
|
+
}
|
|
1177
|
+
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
|
|
1178
|
+
(result.kind === 'conflicted' && result.roomVersion !== undefined
|
|
1179
|
+
? ` ${result.roomVersion}`
|
|
1180
|
+
: ''));
|
|
1181
|
+
if (result.kind !== 'expired')
|
|
1182
|
+
journal.record(`refusal ${result.kind} ${result.code}`);
|
|
1183
|
+
}
|
|
1184
|
+
if (result.kind === 'retryable-refusal') {
|
|
1185
|
+
const reset = result.reset;
|
|
1186
|
+
progress.complete = false;
|
|
1187
|
+
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1188
|
+
return;
|
|
819
1189
|
}
|
|
820
1190
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
moveTo({
|
|
834
|
-
...status,
|
|
835
|
-
attentionRequired: true
|
|
836
|
-
});
|
|
1191
|
+
const left = leaving(head, result);
|
|
1192
|
+
await exclusive(async () => {
|
|
1193
|
+
if (!continuing())
|
|
1194
|
+
return;
|
|
1195
|
+
const current = replicas.get(room.locator);
|
|
1196
|
+
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
1200
|
+
leaving: result.kind === 'expired' && landed(head, current.confirmed)
|
|
1201
|
+
? { ...left, returned: null }
|
|
1202
|
+
: left,
|
|
837
1203
|
});
|
|
1204
|
+
publishChangeSet(set);
|
|
1205
|
+
moveTo({ ...status, ...attention() });
|
|
838
1206
|
});
|
|
839
|
-
|
|
1207
|
+
if (result.kind === 'expired')
|
|
1208
|
+
continue;
|
|
1209
|
+
const withheld = await advance(room, local, authorized);
|
|
1210
|
+
if (withheld !== null)
|
|
1211
|
+
return withhold(withheld, room, local, progress);
|
|
1212
|
+
continue;
|
|
840
1213
|
}
|
|
1214
|
+
await recordAccepted(room, local, head, result);
|
|
841
1215
|
const from = replicas.get(room.locator)?.confirmed;
|
|
842
|
-
if (from === undefined)
|
|
843
|
-
|
|
1216
|
+
if (from === undefined)
|
|
1217
|
+
return;
|
|
1218
|
+
const pulled = await pullRefresh(authorized, from, reachOf(room.locator));
|
|
1219
|
+
if (!continuing())
|
|
1220
|
+
return;
|
|
844
1221
|
if (pulled.kind === 'revoked-authorization') {
|
|
845
|
-
refuse('revoked-authorization');
|
|
846
|
-
return
|
|
1222
|
+
await refuse('revoked-authorization');
|
|
1223
|
+
return;
|
|
847
1224
|
}
|
|
848
1225
|
if (pulled.kind === 'scoped-refusal') {
|
|
849
1226
|
await scoped(pulled, room, local);
|
|
850
1227
|
progress.complete = false;
|
|
851
|
-
return
|
|
852
|
-
}
|
|
853
|
-
if (pulled.kind === 'still-current') {
|
|
854
|
-
const committed = await exclusive(()=>commitAccept(room, local, head, result, from, true));
|
|
855
|
-
if (!committed) {
|
|
856
|
-
progress.complete = false;
|
|
857
|
-
return null;
|
|
858
|
-
}
|
|
859
|
-
continue;
|
|
1228
|
+
return;
|
|
860
1229
|
}
|
|
861
|
-
if (pulled.kind === 'advanced') {
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
if (!committed) {
|
|
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))) {
|
|
865
1233
|
progress.complete = false;
|
|
866
|
-
return
|
|
1234
|
+
return;
|
|
867
1235
|
}
|
|
868
1236
|
continue;
|
|
869
1237
|
}
|
|
870
1238
|
if (pulled.kind === 'beyond-horizon') {
|
|
871
1239
|
const bootstrapped = await pullBootstrap(authorized, room);
|
|
1240
|
+
if (!continuing())
|
|
1241
|
+
return;
|
|
872
1242
|
if (bootstrapped.kind === 'revoked-authorization') {
|
|
873
|
-
refuse('revoked-authorization');
|
|
874
|
-
return
|
|
1243
|
+
await refuse('revoked-authorization');
|
|
1244
|
+
return;
|
|
875
1245
|
}
|
|
876
1246
|
if (bootstrapped.kind === 'scoped-refusal') {
|
|
877
1247
|
await scoped(bootstrapped, room, local);
|
|
878
1248
|
progress.complete = false;
|
|
879
|
-
return
|
|
1249
|
+
return;
|
|
880
1250
|
}
|
|
881
1251
|
if (bootstrapped.kind === 'staged') {
|
|
882
|
-
|
|
883
|
-
const committed = await exclusive(()=>commitAccept(room, local, head, result, bootstrapped.confirmed, overlay));
|
|
884
|
-
if (!committed) {
|
|
1252
|
+
if (!(await accepting(room, local, head, result, bootstrapped.confirmed, true))) {
|
|
885
1253
|
progress.complete = false;
|
|
886
|
-
return
|
|
1254
|
+
return;
|
|
887
1255
|
}
|
|
888
1256
|
continue;
|
|
889
1257
|
}
|
|
890
1258
|
progress.complete = false;
|
|
891
1259
|
if (bootstrapped.kind === 'transport-uncertainty') {
|
|
892
1260
|
progress.reachable = false;
|
|
893
|
-
}
|
|
1261
|
+
}
|
|
1262
|
+
else if (bootstrapped.kind === 'room-not-readable') {
|
|
894
1263
|
unreadable.add(room.locator);
|
|
895
1264
|
list(room, false);
|
|
896
1265
|
emitRooms();
|
|
897
1266
|
}
|
|
898
|
-
|
|
1267
|
+
else if (bootstrapped.kind === 'refused') {
|
|
1268
|
+
refusedSource(room, bootstrapped);
|
|
1269
|
+
}
|
|
1270
|
+
return;
|
|
899
1271
|
}
|
|
900
1272
|
progress.complete = false;
|
|
901
1273
|
if (pulled.kind === 'transport-uncertainty') {
|
|
902
1274
|
progress.reachable = false;
|
|
903
|
-
}
|
|
1275
|
+
}
|
|
1276
|
+
else if (pulled.kind === 'room-not-readable') {
|
|
904
1277
|
unreadable.add(room.locator);
|
|
905
1278
|
list(room, false);
|
|
906
1279
|
emitRooms();
|
|
907
1280
|
}
|
|
908
|
-
|
|
1281
|
+
else if (pulled.kind === 'retryable-refusal') {
|
|
1282
|
+
journal.record(`refusal ${pulled.kind} ${pulled.code}`);
|
|
1283
|
+
}
|
|
1284
|
+
else if (pulled.kind === 'refused') {
|
|
1285
|
+
refusedSource(room, pulled);
|
|
1286
|
+
}
|
|
1287
|
+
return;
|
|
909
1288
|
}
|
|
910
|
-
return
|
|
1289
|
+
return;
|
|
911
1290
|
}
|
|
912
|
-
async function retainModel(local, room, next) {
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1291
|
+
async function retainModel(local, room, attached, next) {
|
|
1292
|
+
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
|
+
const previous = modelOf(room.locator, attached);
|
|
916
1297
|
const heldBefore = replicas.get(room.locator)?.confirmed.record;
|
|
917
|
-
if (JSON.stringify(previous) === JSON.stringify(next) &&
|
|
918
|
-
|
|
1298
|
+
if (JSON.stringify(previous) === JSON.stringify(next) &&
|
|
1299
|
+
(heldBefore === undefined ||
|
|
1300
|
+
(JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
|
|
1301
|
+
JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
|
|
1302
|
+
JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) &&
|
|
1303
|
+
JSON.stringify(heldBefore.typedSelection) ===
|
|
1304
|
+
JSON.stringify(room.typedSelection ?? null))))
|
|
1305
|
+
return;
|
|
919
1306
|
if (next.status === 'unavailable') {
|
|
920
|
-
invalidModels.add(
|
|
921
|
-
liveModels.delete(
|
|
922
|
-
}
|
|
923
|
-
await exclusive(async ()=>
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1307
|
+
invalidModels.add(key);
|
|
1308
|
+
liveModels.delete(key);
|
|
1309
|
+
}
|
|
1310
|
+
await exclusive(async () => {
|
|
1311
|
+
if (!continuing())
|
|
1312
|
+
return;
|
|
1313
|
+
if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0))
|
|
1314
|
+
return;
|
|
1315
|
+
const held = replicas.get(room.locator);
|
|
1316
|
+
if (held !== undefined) {
|
|
1317
|
+
const declared = {
|
|
1318
|
+
...held.confirmed.record,
|
|
1319
|
+
operations: room.operations,
|
|
1320
|
+
replica: room.replica,
|
|
1321
|
+
attachedModel: room.attachedModel,
|
|
1322
|
+
typedSelection: room.typedSelection ?? null,
|
|
1323
|
+
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
1324
|
+
};
|
|
1325
|
+
const reclassified = sameMembership(declared, held.confirmed.record)
|
|
1326
|
+
? readConfirmed(declared, held.confirmed)
|
|
1327
|
+
: readReclassified(declared, held.confirmed);
|
|
1328
|
+
const record = reclassified.kind === 'staged'
|
|
1329
|
+
? declared
|
|
1330
|
+
: {
|
|
1331
|
+
...declared,
|
|
1332
|
+
typedSelection: held.confirmed.record.typedSelection,
|
|
934
1333
|
};
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1334
|
+
const read = reclassified.kind === 'deferred' ? readConfirmed(record, held.confirmed) : reclassified;
|
|
1335
|
+
await local.write({
|
|
1336
|
+
next: record,
|
|
1337
|
+
previous: held.confirmed.record,
|
|
1338
|
+
queue: held.queue,
|
|
1339
|
+
});
|
|
1340
|
+
if (read.kind === 'staged') {
|
|
1341
|
+
replicas.set(room.locator, replicaOf(read.confirmed, held.queue, rewriting(), held));
|
|
1342
|
+
if (declarationRefusals.delete(room.locator)) {
|
|
1343
|
+
sourceRefusals.delete(room.locator);
|
|
1344
|
+
unreadable.delete(room.locator);
|
|
1345
|
+
}
|
|
938
1346
|
}
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1347
|
+
else if (read.kind === 'refused') {
|
|
1348
|
+
replicas.set(room.locator, { ...held, confirmed: { ...held.confirmed, record } });
|
|
1349
|
+
refusedSource(room, read);
|
|
1350
|
+
declarationRefusals.add(room.locator);
|
|
943
1351
|
}
|
|
944
|
-
|
|
1352
|
+
}
|
|
1353
|
+
models.set(key, next);
|
|
1354
|
+
if (next.status === 'current') {
|
|
1355
|
+
liveModels.set(key, JSON.stringify(next.snapshot));
|
|
1356
|
+
invalidModels.delete(key);
|
|
1357
|
+
}
|
|
1358
|
+
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
1359
|
+
if (held === undefined)
|
|
945
1360
|
emitRooms();
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1361
|
+
else
|
|
1362
|
+
publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
|
|
1363
|
+
});
|
|
949
1364
|
}
|
|
950
|
-
async function acquireModel(local, room, authorized) {
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1365
|
+
async function acquireModel(local, room, authorized, selection) {
|
|
1366
|
+
const attached = selection === null ? 'collection' : 'typed-selection';
|
|
1367
|
+
const key = modelKey(room.locator, attached);
|
|
1368
|
+
const declared = selection === null ? room.attachedModel : selection.attachedModel;
|
|
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;
|
|
958
1376
|
}
|
|
959
|
-
const epoch = modelEpoch.get(
|
|
1377
|
+
const epoch = modelEpoch.get(key) ?? 0;
|
|
960
1378
|
let answer;
|
|
961
1379
|
try {
|
|
962
|
-
answer = await options.adapter.model(authorized,
|
|
963
|
-
locator: room.locator,
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
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' };
|
|
971
1391
|
}
|
|
972
|
-
if (epoch !== (modelEpoch.get(
|
|
1392
|
+
if (discoveryPaused || epoch !== (modelEpoch.get(key) ?? 0))
|
|
1393
|
+
return;
|
|
973
1394
|
if (answer.kind === 'revoked-authorization') {
|
|
974
|
-
await
|
|
975
|
-
status: 'unavailable',
|
|
976
|
-
reason: 'revoked'
|
|
977
|
-
});
|
|
978
|
-
refuse('revoked-authorization');
|
|
1395
|
+
await refuse('revoked-authorization');
|
|
979
1396
|
return;
|
|
980
1397
|
}
|
|
981
1398
|
if (answer.kind === 'transport-uncertainty') {
|
|
982
|
-
await retainModel(local, room, cached(previous));
|
|
1399
|
+
await retainModel(local, room, attached, cached(previous));
|
|
983
1400
|
return;
|
|
984
1401
|
}
|
|
985
1402
|
if (answer.kind === 'model-unavailable') {
|
|
986
|
-
await retainModel(local, room, {
|
|
1403
|
+
await retainModel(local, room, attached, {
|
|
987
1404
|
status: 'unavailable',
|
|
988
|
-
reason: answer.reason
|
|
1405
|
+
reason: answer.reason,
|
|
989
1406
|
});
|
|
990
1407
|
return;
|
|
991
1408
|
}
|
|
992
|
-
if (
|
|
993
|
-
await retainModel(local, room, {
|
|
1409
|
+
if (declared === null || !sameModel(answer.snapshot, declared)) {
|
|
1410
|
+
await retainModel(local, room, attached, {
|
|
994
1411
|
status: 'unavailable',
|
|
995
|
-
reason: 'version-changed'
|
|
1412
|
+
reason: 'version-changed',
|
|
996
1413
|
});
|
|
997
1414
|
return;
|
|
998
1415
|
}
|
|
999
|
-
if (previous.status !== 'unavailable' &&
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
});
|
|
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' });
|
|
1004
1420
|
return;
|
|
1005
1421
|
}
|
|
1006
|
-
const verified =
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1422
|
+
const verified = answer.snapshot.schema === null ? EMPTY_SCHEMA : verify(answer.snapshot.schema);
|
|
1423
|
+
if (verified.verdict === 'refuse') {
|
|
1424
|
+
const where = verified.location === undefined ? '' : ` ${verified.location}`;
|
|
1425
|
+
journal.record(`model invalid ${room.locator} ${attached} ${answer.snapshot.identity} ${verified.rule}${where}`);
|
|
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' });
|
|
1016
1435
|
}
|
|
1017
1436
|
async function scoped(result, room, local) {
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1437
|
+
scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
|
|
1438
|
+
let scopedAway = room;
|
|
1439
|
+
if (result.scope === 'model')
|
|
1440
|
+
await retainModel(local, room, 'collection', {
|
|
1441
|
+
status: 'unavailable',
|
|
1442
|
+
reason: 'not-readable',
|
|
1443
|
+
});
|
|
1022
1444
|
else if (result.scope !== 'operation') {
|
|
1023
1445
|
unreadable.add(room.locator);
|
|
1024
|
-
|
|
1446
|
+
scopedAway = {
|
|
1025
1447
|
...room,
|
|
1026
|
-
replica: {
|
|
1027
|
-
...room.replica,
|
|
1028
|
-
admission: 'unavailable'
|
|
1029
|
-
},
|
|
1448
|
+
replica: { ...room.replica, admission: 'unavailable' },
|
|
1030
1449
|
operations: {
|
|
1031
1450
|
...room.operations,
|
|
1032
|
-
...result.scope === 'content' ? {
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
...result.scope === 'sync' ? {
|
|
1036
|
-
sync: []
|
|
1037
|
-
} : {}
|
|
1038
|
-
}
|
|
1451
|
+
...(result.scope === 'content' ? { read: [] } : {}),
|
|
1452
|
+
...(result.scope === 'sync' ? { sync: [] } : {}),
|
|
1453
|
+
},
|
|
1039
1454
|
};
|
|
1040
|
-
await retainModel(local,
|
|
1041
|
-
list(unavailable, false);
|
|
1042
|
-
emitRooms();
|
|
1455
|
+
await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
|
|
1043
1456
|
}
|
|
1044
|
-
|
|
1457
|
+
if (!continuing())
|
|
1458
|
+
return;
|
|
1459
|
+
list(scopedAway, scopedAway.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
1460
|
+
emitRooms();
|
|
1461
|
+
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
1045
1462
|
}
|
|
1046
1463
|
async function synchronize(discovered, local, progress) {
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1464
|
+
const prepared = await Promise.allSettled(discovered.map((room) => prepareRoom(room, local, progress)));
|
|
1465
|
+
const ready = discovered.filter((_, index) => {
|
|
1466
|
+
const outcome = prepared[index];
|
|
1467
|
+
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1468
|
+
});
|
|
1469
|
+
const rooms = await Promise.allSettled(ready.map((room) => synchronizeRoom(room, local, progress)));
|
|
1470
|
+
const failed = [...prepared, ...rooms].find((room) => room.status === 'rejected');
|
|
1471
|
+
if (failed !== undefined)
|
|
1472
|
+
throw failed.reason;
|
|
1473
|
+
}
|
|
1474
|
+
async function prepareRoom(room, local, progress) {
|
|
1475
|
+
if (closed || authorization === undefined) {
|
|
1476
|
+
progress.complete = false;
|
|
1477
|
+
return false;
|
|
1478
|
+
}
|
|
1479
|
+
const prior = replicas.get(room.locator)?.confirmed.record;
|
|
1480
|
+
if (prior !== undefined && prior.replica.selection !== room.replica.selection) {
|
|
1481
|
+
list(prior, false);
|
|
1482
|
+
emitRooms();
|
|
1483
|
+
journal.record(`selection changed ${room.locator}`);
|
|
1484
|
+
progress.complete = false;
|
|
1485
|
+
return false;
|
|
1486
|
+
}
|
|
1487
|
+
const selection = room.typedSelection;
|
|
1488
|
+
await Promise.all([
|
|
1489
|
+
acquireModel(local, room, authorization, null),
|
|
1490
|
+
...(selection === undefined ? [] : [acquireModel(local, room, authorization, selection)]),
|
|
1491
|
+
]);
|
|
1492
|
+
if (discoveryPaused || authorization === undefined)
|
|
1493
|
+
return false;
|
|
1494
|
+
if (room.replica.admission !== 'admitted') {
|
|
1495
|
+
if (replicas.has(room.locator))
|
|
1073
1496
|
progress.complete = false;
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
return;
|
|
1091
|
-
}
|
|
1092
|
-
continue;
|
|
1093
|
-
}
|
|
1094
|
-
if (await submitRoom(room, local, authorization, progress) === 'stop') return;
|
|
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;
|
|
1505
|
+
}
|
|
1506
|
+
async function synchronizeRoom(room, local, progress) {
|
|
1507
|
+
if (discoveryPaused || authorization === undefined)
|
|
1508
|
+
return;
|
|
1509
|
+
const withheld = await advance(room, local, authorization);
|
|
1510
|
+
if (withheld !== null) {
|
|
1511
|
+
await withhold(withheld, room, local, progress);
|
|
1512
|
+
return;
|
|
1095
1513
|
}
|
|
1514
|
+
await submitRoom(room, local, authorization, progress);
|
|
1096
1515
|
}
|
|
1097
1516
|
async function cycle(local, progress) {
|
|
1098
1517
|
if (closed || authorization === undefined) {
|
|
@@ -1103,10 +1522,9 @@ export function createMdRuntime(options) {
|
|
|
1103
1522
|
let result;
|
|
1104
1523
|
try {
|
|
1105
1524
|
result = await options.adapter.discover(authorization);
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
};
|
|
1525
|
+
}
|
|
1526
|
+
catch {
|
|
1527
|
+
result = { kind: 'transport-uncertainty' };
|
|
1110
1528
|
}
|
|
1111
1529
|
if (closed) {
|
|
1112
1530
|
notifyReady();
|
|
@@ -1114,64 +1532,63 @@ export function createMdRuntime(options) {
|
|
|
1114
1532
|
}
|
|
1115
1533
|
journal.record(`connection discover ${result.kind}`);
|
|
1116
1534
|
if (result.kind === 'transport-uncertainty') {
|
|
1117
|
-
for (const room of rooms.values())
|
|
1535
|
+
for (const room of rooms.values()) {
|
|
1536
|
+
await retainModel(local, room, 'collection', cached(modelOf(room.locator)));
|
|
1537
|
+
if (room.typedSelection !== null) {
|
|
1538
|
+
await retainModel(local, room, 'typed-selection', cached(modelOf(room.locator, 'typed-selection')));
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1118
1541
|
progress.reachable = false;
|
|
1119
1542
|
notifyReady();
|
|
1120
1543
|
return;
|
|
1121
1544
|
}
|
|
1122
1545
|
if (result.kind === 'contract-unavailable') {
|
|
1123
|
-
journal.record(`connection contract unavailable ${result.
|
|
1124
|
-
refuse('missing-required-guarantee');
|
|
1546
|
+
journal.record(`connection contract unavailable ${result.code}`);
|
|
1547
|
+
await refuse('missing-required-guarantee', { code: result.code, fields: result.fields });
|
|
1125
1548
|
notifyReady();
|
|
1126
1549
|
return;
|
|
1127
1550
|
}
|
|
1128
1551
|
if (result.kind === 'revoked-authorization') {
|
|
1129
|
-
refuse('revoked-authorization');
|
|
1552
|
+
await refuse('revoked-authorization');
|
|
1130
1553
|
notifyReady();
|
|
1131
1554
|
return;
|
|
1132
1555
|
}
|
|
1133
1556
|
if (!result.supportedVersions.includes(ROOM_CONTRACT_VERSION)) {
|
|
1134
|
-
refuse('no-matching-version');
|
|
1557
|
+
await refuse('no-matching-version');
|
|
1135
1558
|
notifyReady();
|
|
1136
1559
|
return;
|
|
1137
1560
|
}
|
|
1138
|
-
if (result.mdModel.identity !== MD_MODEL.identity ||
|
|
1139
|
-
|
|
1561
|
+
if (result.mdModel.identity !== MD_MODEL.identity ||
|
|
1562
|
+
result.mdModel.version !== MD_MODEL.version) {
|
|
1563
|
+
await refuse('md-model-version-mismatch');
|
|
1140
1564
|
notifyReady();
|
|
1141
1565
|
return;
|
|
1142
1566
|
}
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
'
|
|
1149
|
-
|
|
1150
|
-
'
|
|
1151
|
-
|
|
1152
|
-
].every((operation)=>Array.isArray(room.operations[operation]) && room.operations[operation].every((scope)=>typeof scope === 'string')) || room.replica === undefined || typeof room.replica.selection !== 'string' || room.replica.selection.length === 0 || ![
|
|
1153
|
-
'admitted',
|
|
1154
|
-
'unavailable'
|
|
1155
|
-
].includes(room.replica.admission) || room.attachedModel === undefined || room.attachedModel !== null && (typeof room.attachedModel.identity !== 'string' || !room.attachedModel.identity || typeof room.attachedModel.version !== 'string' || !room.attachedModel.version)) {
|
|
1567
|
+
for (const room of result.rooms) {
|
|
1568
|
+
if (room.operations === undefined ||
|
|
1569
|
+
!['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
1570
|
+
room.operations[operation].every((scope) => typeof scope === 'string')) ||
|
|
1571
|
+
room.replica === undefined ||
|
|
1572
|
+
typeof room.replica.selection !== 'string' ||
|
|
1573
|
+
room.replica.selection.length === 0 ||
|
|
1574
|
+
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
1575
|
+
!declaredIdentity(room.attachedModel)) {
|
|
1156
1576
|
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
1157
|
-
refuse('missing-required-guarantee');
|
|
1577
|
+
await refuse('missing-required-guarantee');
|
|
1158
1578
|
notifyReady();
|
|
1159
1579
|
return;
|
|
1160
1580
|
}
|
|
1161
|
-
if (
|
|
1162
|
-
|
|
1163
|
-
|
|
1581
|
+
if (!declaredSelection(room)) {
|
|
1582
|
+
journal.record(`invalid typed selection declaration ${room.locator}`);
|
|
1583
|
+
await refuse('missing-required-guarantee');
|
|
1584
|
+
notifyReady();
|
|
1585
|
+
return;
|
|
1164
1586
|
}
|
|
1165
1587
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
notifyReady();
|
|
1169
|
-
return;
|
|
1170
|
-
}
|
|
1171
|
-
const declared = new Set(result.capabilities.map((capability)=>capability.name));
|
|
1172
|
-
for (const required of options.requiredGuarantees){
|
|
1588
|
+
const declared = new Set(result.capabilities.map((capability) => capability.name));
|
|
1589
|
+
for (const required of options.requiredGuarantees) {
|
|
1173
1590
|
if (!knownCapabilities.has(required) || !declared.has(required)) {
|
|
1174
|
-
refuse('missing-required-guarantee');
|
|
1591
|
+
await refuse('missing-required-guarantee');
|
|
1175
1592
|
notifyReady();
|
|
1176
1593
|
return;
|
|
1177
1594
|
}
|
|
@@ -1180,55 +1597,71 @@ export function createMdRuntime(options) {
|
|
|
1180
1597
|
refusal = null;
|
|
1181
1598
|
emitRefusal();
|
|
1182
1599
|
}
|
|
1600
|
+
const previousCapabilities = capabilities;
|
|
1183
1601
|
capabilities = result.capabilities;
|
|
1184
1602
|
notifyReady();
|
|
1185
|
-
if (
|
|
1603
|
+
if (declared.has('room-version-notifications'))
|
|
1604
|
+
observeRoomVersions();
|
|
1605
|
+
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1606
|
+
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
1186
1607
|
limits = result.providerLimits;
|
|
1187
|
-
await local.declare(limits);
|
|
1608
|
+
await local.declare(limits, capabilities);
|
|
1188
1609
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1610
|
+
await applyDiscovery(result.rooms, local);
|
|
1611
|
+
handSelected(result.rooms
|
|
1612
|
+
.filter((room) => room.replica.admission === 'admitted')
|
|
1613
|
+
.map((room) => room.locator)
|
|
1614
|
+
.sort());
|
|
1615
|
+
progress.complete = true;
|
|
1193
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);
|
|
1194
1623
|
}
|
|
1195
1624
|
async function ensureAuthorization() {
|
|
1196
|
-
if (authorization !== undefined)
|
|
1625
|
+
if (authorization !== undefined)
|
|
1626
|
+
return;
|
|
1197
1627
|
try {
|
|
1198
1628
|
authorization = await options.adapter.authorize();
|
|
1199
1629
|
journal.record('connection authorize');
|
|
1200
|
-
}
|
|
1630
|
+
}
|
|
1631
|
+
catch (error) {
|
|
1201
1632
|
journal.record('connection authorize transport-uncertainty');
|
|
1202
1633
|
throw error;
|
|
1203
1634
|
}
|
|
1204
1635
|
}
|
|
1205
1636
|
async function connect() {
|
|
1206
|
-
if (closed || discoveryPaused) {
|
|
1637
|
+
if (closed || stopping || discoveryPaused) {
|
|
1207
1638
|
notifyReady();
|
|
1208
1639
|
return;
|
|
1209
1640
|
}
|
|
1210
1641
|
startTimer();
|
|
1211
|
-
const progress = {
|
|
1212
|
-
complete: false,
|
|
1213
|
-
reachable: true
|
|
1214
|
-
};
|
|
1642
|
+
const progress = { complete: false, reachable: true };
|
|
1215
1643
|
let completedAt = new Date().toISOString();
|
|
1216
1644
|
try {
|
|
1217
1645
|
const local = await openLocal();
|
|
1218
|
-
if (closed)
|
|
1646
|
+
if (closed)
|
|
1647
|
+
return;
|
|
1219
1648
|
await ensureAuthorization();
|
|
1220
|
-
if (closed)
|
|
1649
|
+
if (closed)
|
|
1650
|
+
return;
|
|
1221
1651
|
await cycle(local, progress);
|
|
1222
|
-
if (closed)
|
|
1652
|
+
if (closed)
|
|
1653
|
+
return;
|
|
1223
1654
|
if (progress.complete) {
|
|
1224
1655
|
completedAt = new Date().toISOString();
|
|
1225
1656
|
await local.complete(completedAt);
|
|
1226
1657
|
}
|
|
1227
|
-
}
|
|
1658
|
+
}
|
|
1659
|
+
catch {
|
|
1228
1660
|
progress.complete = false;
|
|
1229
1661
|
notifyReady();
|
|
1230
1662
|
return;
|
|
1231
|
-
}
|
|
1663
|
+
}
|
|
1664
|
+
finally {
|
|
1232
1665
|
notifyReady();
|
|
1233
1666
|
moveTo(settled(status, progress, completedAt, pending(), attention()));
|
|
1234
1667
|
}
|
|
@@ -1236,7 +1669,8 @@ export function createMdRuntime(options) {
|
|
|
1236
1669
|
async function run() {
|
|
1237
1670
|
try {
|
|
1238
1671
|
await connect();
|
|
1239
|
-
}
|
|
1672
|
+
}
|
|
1673
|
+
finally {
|
|
1240
1674
|
inflight = false;
|
|
1241
1675
|
if (queued && !closed) {
|
|
1242
1676
|
queued = false;
|
|
@@ -1244,11 +1678,158 @@ export function createMdRuntime(options) {
|
|
|
1244
1678
|
}
|
|
1245
1679
|
}
|
|
1246
1680
|
}
|
|
1681
|
+
function observeRoomVersions() {
|
|
1682
|
+
const path = options.adapter.roomVersions;
|
|
1683
|
+
if (path === undefined || withdrawNotifications !== undefined)
|
|
1684
|
+
return;
|
|
1685
|
+
withdrawNotifications = path.observe((notification) => {
|
|
1686
|
+
if (closed || stopping || discoveryPaused)
|
|
1687
|
+
return;
|
|
1688
|
+
if (notification.modelChanged === true && handed?.includes(notification.locator) === true) {
|
|
1689
|
+
void enqueue();
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
notices.set(notification.locator, notification.roomVersion);
|
|
1693
|
+
chain = chain.then(runNotified, runNotified);
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
function writesHeld() {
|
|
1697
|
+
return serviceMode === 'write-halted' || haltedRefusal;
|
|
1698
|
+
}
|
|
1699
|
+
function signalled(mode) {
|
|
1700
|
+
if (closed)
|
|
1701
|
+
return;
|
|
1702
|
+
if (mode === 'open')
|
|
1703
|
+
openSignals += 1;
|
|
1704
|
+
const releasing = mode === 'open' && (haltedRefusal || serviceMode === 'write-halted');
|
|
1705
|
+
if (mode === 'open')
|
|
1706
|
+
haltedRefusal = false;
|
|
1707
|
+
if (mode !== serviceMode) {
|
|
1708
|
+
serviceMode = mode;
|
|
1709
|
+
journal.record(`service mode ${mode}`);
|
|
1710
|
+
for (const listener of serviceModeListeners)
|
|
1711
|
+
listener(mode);
|
|
1712
|
+
}
|
|
1713
|
+
if (releasing)
|
|
1714
|
+
void enqueue();
|
|
1715
|
+
}
|
|
1716
|
+
function observeConversations() {
|
|
1717
|
+
const stream = options.adapter.conversations;
|
|
1718
|
+
if (stream === undefined || withdrawConversations !== undefined)
|
|
1719
|
+
return;
|
|
1720
|
+
withdrawConversations = stream.observe((event) => {
|
|
1721
|
+
conversations.receive(event);
|
|
1722
|
+
if (event.kind !== 'gap' || conversations.current())
|
|
1723
|
+
return;
|
|
1724
|
+
show();
|
|
1725
|
+
scheduleCurrency();
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
function scheduleCurrency() {
|
|
1729
|
+
if (currencyQueued || closed || stopping)
|
|
1730
|
+
return;
|
|
1731
|
+
currencyQueued = true;
|
|
1732
|
+
chain = chain.then(runCurrency, runCurrency);
|
|
1733
|
+
}
|
|
1734
|
+
async function runCurrency() {
|
|
1735
|
+
currencyQueued = false;
|
|
1736
|
+
if (closed || stopping || discoveryPaused)
|
|
1737
|
+
return;
|
|
1738
|
+
await conversations.reconcile();
|
|
1739
|
+
if (conversations.current()) {
|
|
1740
|
+
show();
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
const resting = status;
|
|
1744
|
+
const progress = { complete: false, reachable: !resting.providerUnreachable };
|
|
1745
|
+
moveTo(transferring(resting));
|
|
1746
|
+
try {
|
|
1747
|
+
await conversations.catchUp(progress);
|
|
1748
|
+
}
|
|
1749
|
+
catch {
|
|
1750
|
+
progress.reachable = false;
|
|
1751
|
+
}
|
|
1752
|
+
finally {
|
|
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
|
+
}
|
|
1247
1830
|
function enqueue() {
|
|
1248
|
-
if (
|
|
1249
|
-
announce('enqueue');
|
|
1831
|
+
if (stopping)
|
|
1250
1832
|
return chain;
|
|
1251
|
-
}
|
|
1252
1833
|
if (inflight) {
|
|
1253
1834
|
queued = true;
|
|
1254
1835
|
return chain;
|
|
@@ -1257,213 +1838,268 @@ export function createMdRuntime(options) {
|
|
|
1257
1838
|
chain = chain.then(run, run);
|
|
1258
1839
|
return chain;
|
|
1259
1840
|
}
|
|
1260
|
-
function startProcessing() {
|
|
1261
|
-
if (leader) {
|
|
1262
|
-
void enqueue();
|
|
1263
|
-
return;
|
|
1264
|
-
}
|
|
1265
|
-
void window.navigator.locks.request('mdruntime-sync', {
|
|
1266
|
-
signal: options.signal
|
|
1267
|
-
}, async ()=>{
|
|
1268
|
-
if (closed) return;
|
|
1269
|
-
leader = true;
|
|
1270
|
-
announce();
|
|
1271
|
-
void enqueue();
|
|
1272
|
-
await new Promise((resolve)=>{
|
|
1273
|
-
releaseLeadership = ()=>{
|
|
1274
|
-
leader = false;
|
|
1275
|
-
resolve();
|
|
1276
|
-
};
|
|
1277
|
-
});
|
|
1278
|
-
}).catch(()=>{});
|
|
1279
|
-
}
|
|
1280
1841
|
function close() {
|
|
1842
|
+
if (closed)
|
|
1843
|
+
return;
|
|
1281
1844
|
closed = true;
|
|
1845
|
+
withdrawNotifications?.();
|
|
1846
|
+
withdrawNotifications = undefined;
|
|
1847
|
+
withdrawConversations?.();
|
|
1848
|
+
withdrawConversations = undefined;
|
|
1849
|
+
withdrawServiceMode?.();
|
|
1850
|
+
conversations.close();
|
|
1282
1851
|
stopTimer();
|
|
1283
|
-
releaseLeadership();
|
|
1284
|
-
channel?.close();
|
|
1285
1852
|
store?.close();
|
|
1853
|
+
void opening?.then((opened) => {
|
|
1854
|
+
opened.close();
|
|
1855
|
+
}, () => { });
|
|
1286
1856
|
roomListeners.clear();
|
|
1287
1857
|
refusalListeners.clear();
|
|
1288
1858
|
storageLossListeners.clear();
|
|
1289
1859
|
statusListeners.clear();
|
|
1290
|
-
|
|
1860
|
+
serviceModeListeners.clear();
|
|
1861
|
+
readSide.close();
|
|
1291
1862
|
queueSubscriptions.clear();
|
|
1292
|
-
|
|
1863
|
+
roomDraftListeners.clear();
|
|
1864
|
+
presentationListeners.clear();
|
|
1293
1865
|
}
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1866
|
+
async function stop() {
|
|
1867
|
+
stopping = true;
|
|
1868
|
+
stopTimer();
|
|
1869
|
+
await chain.catch(() => undefined);
|
|
1870
|
+
await mutations;
|
|
1871
|
+
close();
|
|
1872
|
+
}
|
|
1873
|
+
const withdrawServiceMode = options.adapter.serviceMode?.observe(signalled);
|
|
1874
|
+
observeConversations();
|
|
1875
|
+
options.signal?.addEventListener('abort', close, { once: true });
|
|
1876
|
+
if (options.signal?.aborted)
|
|
1877
|
+
close();
|
|
1878
|
+
void openLocal().catch(() => {
|
|
1306
1879
|
notifyReady();
|
|
1307
1880
|
});
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1881
|
+
void enqueue();
|
|
1882
|
+
function watch(topic, input, deliver) {
|
|
1883
|
+
if (topic === 'room-drafts') {
|
|
1884
|
+
const listener = { notify: deliver, ready: false };
|
|
1885
|
+
roomDraftListeners.add(listener);
|
|
1886
|
+
void exclusive(async () => {
|
|
1887
|
+
await openLocal();
|
|
1888
|
+
if (closed || !roomDraftListeners.has(listener))
|
|
1889
|
+
return;
|
|
1890
|
+
listener.ready = true;
|
|
1891
|
+
}).then(() => {
|
|
1892
|
+
if (closed || !listener.ready || !roomDraftListeners.has(listener))
|
|
1893
|
+
return;
|
|
1894
|
+
listener.notify(snapshotRoomDrafts());
|
|
1895
|
+
}, () => { });
|
|
1896
|
+
return () => {
|
|
1897
|
+
roomDraftListeners.delete(listener);
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
if (topic === 'conversation') {
|
|
1901
|
+
return conversations.watch(input[0], input[1], deliver);
|
|
1902
|
+
}
|
|
1903
|
+
if (topic === 'presentation') {
|
|
1904
|
+
const listener = {
|
|
1905
|
+
app: input[0],
|
|
1906
|
+
notify: deliver,
|
|
1907
|
+
ready: false,
|
|
1908
|
+
};
|
|
1909
|
+
presentationListeners.add(listener);
|
|
1910
|
+
void exclusive(async () => {
|
|
1911
|
+
await openLocal();
|
|
1912
|
+
}).then(() => {
|
|
1913
|
+
if (closed || !presentationListeners.has(listener))
|
|
1914
|
+
return;
|
|
1915
|
+
listener.ready = true;
|
|
1916
|
+
listener.notify(presentationOf(listener.app));
|
|
1917
|
+
}, () => { });
|
|
1918
|
+
return () => {
|
|
1919
|
+
presentationListeners.delete(listener);
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
if (topic === 'rooms') {
|
|
1923
|
+
roomListeners.add(deliver);
|
|
1924
|
+
deliver(listed());
|
|
1925
|
+
return () => {
|
|
1926
|
+
roomListeners.delete(deliver);
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
if (topic === 'connection-refusal') {
|
|
1930
|
+
refusalListeners.add(deliver);
|
|
1931
|
+
deliver(refusal);
|
|
1932
|
+
return () => {
|
|
1933
|
+
refusalListeners.delete(deliver);
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
if (topic === 'storage-loss') {
|
|
1937
|
+
storageLossListeners.add(deliver);
|
|
1938
|
+
deliver(storageLoss);
|
|
1939
|
+
return () => {
|
|
1940
|
+
storageLossListeners.delete(deliver);
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
if (topic === 'sync-status') {
|
|
1944
|
+
statusListeners.add(deliver);
|
|
1945
|
+
deliver(shown);
|
|
1946
|
+
return () => {
|
|
1947
|
+
statusListeners.delete(deliver);
|
|
1948
|
+
};
|
|
1949
|
+
}
|
|
1950
|
+
if (topic === 'service-mode') {
|
|
1951
|
+
serviceModeListeners.add(deliver);
|
|
1952
|
+
deliver(serviceMode);
|
|
1953
|
+
return () => {
|
|
1954
|
+
serviceModeListeners.delete(deliver);
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
if (topic === 'queue') {
|
|
1958
|
+
const queue = { locator: input[0], notify: deliver };
|
|
1959
|
+
queueSubscriptions.add(queue);
|
|
1960
|
+
queue.notify(entriesOf(queue.locator));
|
|
1961
|
+
return () => {
|
|
1962
|
+
queueSubscriptions.delete(queue);
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
const plan = planOf(input[0], roomFacts());
|
|
1966
|
+
return readSide.watch(plan, identityOf(plan), deliver);
|
|
1967
|
+
}
|
|
1968
|
+
const md = {
|
|
1969
|
+
loaded() {
|
|
1970
|
+
return openLocal().then(() => undefined);
|
|
1312
1971
|
},
|
|
1313
|
-
|
|
1314
|
-
return
|
|
1972
|
+
rooms() {
|
|
1973
|
+
return listed();
|
|
1315
1974
|
},
|
|
1316
|
-
|
|
1317
|
-
|
|
1975
|
+
query: ((input) => readSide.query(planOf(input, roomFacts()))),
|
|
1976
|
+
async search(input) {
|
|
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);
|
|
1318
1983
|
},
|
|
1319
|
-
|
|
1320
|
-
return
|
|
1984
|
+
types(locator) {
|
|
1985
|
+
return {
|
|
1986
|
+
collection: modelTypes(modelOf(locator)),
|
|
1987
|
+
typedSelection: rooms.get(locator)?.typedSelection === undefined ||
|
|
1988
|
+
rooms.get(locator)?.typedSelection === null
|
|
1989
|
+
? null
|
|
1990
|
+
: modelTypes(modelOf(locator, 'typed-selection')),
|
|
1991
|
+
};
|
|
1321
1992
|
},
|
|
1322
|
-
|
|
1323
|
-
list
|
|
1324
|
-
return exclusive(async ()=>{
|
|
1325
|
-
|
|
1326
|
-
return
|
|
1327
|
-
}).then((
|
|
1328
|
-
|
|
1329
|
-
return
|
|
1993
|
+
roomDraft: {
|
|
1994
|
+
list() {
|
|
1995
|
+
return exclusive(async () => {
|
|
1996
|
+
await openLocal();
|
|
1997
|
+
return snapshotRoomDrafts();
|
|
1998
|
+
}).then((listed) => {
|
|
1999
|
+
publishRoomDrafts(true);
|
|
2000
|
+
return listed;
|
|
1330
2001
|
});
|
|
1331
2002
|
},
|
|
1332
|
-
read
|
|
1333
|
-
const
|
|
1334
|
-
return
|
|
2003
|
+
read(room, path) {
|
|
2004
|
+
const record = roomDrafts.get(room)?.get(path);
|
|
2005
|
+
return record === undefined ? null : roomDraftOf(record);
|
|
1335
2006
|
},
|
|
1336
|
-
|
|
1337
|
-
return
|
|
1338
|
-
const
|
|
1339
|
-
|
|
1340
|
-
|
|
2007
|
+
write(room, path, revision, input) {
|
|
2008
|
+
return exclusive(async () => {
|
|
2009
|
+
const parts = readParts(input);
|
|
2010
|
+
if (path === '' || normalize(path) !== path)
|
|
2011
|
+
throw new Error('room-draft-malformed');
|
|
2012
|
+
const local = await openLocal();
|
|
2013
|
+
const current = roomDrafts.get(room)?.get(path);
|
|
2014
|
+
if ((current?.revision ?? null) !== revision) {
|
|
2015
|
+
return {
|
|
2016
|
+
outcome: 'stale',
|
|
2017
|
+
current: current === undefined ? null : roomDraftOf(current),
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
const record = {
|
|
2021
|
+
locator: room,
|
|
1341
2022
|
path,
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
2023
|
+
revision: roomDraftRevision(),
|
|
2024
|
+
parts: held(current?.parts, parts),
|
|
2025
|
+
returned: current?.returned ?? null,
|
|
2026
|
+
};
|
|
2027
|
+
await local.keepRoomDraft(record);
|
|
2028
|
+
rememberRoomDraft(record);
|
|
2029
|
+
publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
|
|
2030
|
+
return { outcome: 'written', roomDraft: roomDraftOf(record) };
|
|
1345
2031
|
});
|
|
1346
2032
|
},
|
|
1347
|
-
discard
|
|
1348
|
-
return
|
|
1349
|
-
await
|
|
1350
|
-
|
|
2033
|
+
discard(room, path, revision) {
|
|
2034
|
+
return exclusive(async () => {
|
|
2035
|
+
const local = await openLocal();
|
|
2036
|
+
const current = roomDrafts.get(room)?.get(path);
|
|
2037
|
+
if (current === undefined || current.revision !== revision) {
|
|
2038
|
+
return {
|
|
2039
|
+
outcome: 'stale',
|
|
2040
|
+
current: current === undefined ? null : roomDraftOf(current),
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
await local.discardRoomDraft(room, path);
|
|
2044
|
+
forgetRoomDraft(room, path);
|
|
2045
|
+
publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
|
|
2046
|
+
moveTo({ ...status, ...attention() });
|
|
2047
|
+
return { outcome: 'written', roomDraft: null };
|
|
1351
2048
|
});
|
|
1352
|
-
}
|
|
1353
|
-
},
|
|
1354
|
-
save (locator, changes, replacing) {
|
|
1355
|
-
return exclusive(()=>write(locator, changes, replacing));
|
|
2049
|
+
},
|
|
1356
2050
|
},
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
2051
|
+
conversation: conversations.surface,
|
|
2052
|
+
chatDraft: conversations.chatDraft,
|
|
2053
|
+
presentation: {
|
|
2054
|
+
read(app) {
|
|
2055
|
+
return exclusive(async () => {
|
|
2056
|
+
await openLocal();
|
|
2057
|
+
return presentationOf(app);
|
|
2058
|
+
});
|
|
2059
|
+
},
|
|
2060
|
+
write(app, value) {
|
|
2061
|
+
return exclusive(async () => {
|
|
2062
|
+
const local = await openLocal();
|
|
2063
|
+
const stored = structuredClone(value);
|
|
2064
|
+
await local.present({ app, value: stored });
|
|
2065
|
+
presentations.set(app, stored);
|
|
2066
|
+
publishChangeSet(changeSet('', null, null, { presentation: app }));
|
|
2067
|
+
});
|
|
2068
|
+
},
|
|
1366
2069
|
},
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
return exclusive(()=>discardHead(locator, identifier));
|
|
1370
|
-
}
|
|
2070
|
+
save(room, named, writtenBy) {
|
|
2071
|
+
return exclusive(() => saveRoomDrafts(room, named, writtenBy));
|
|
1371
2072
|
},
|
|
1372
|
-
history
|
|
2073
|
+
history(locator, path, query) {
|
|
1373
2074
|
return readHistory(locator, path, query);
|
|
1374
2075
|
},
|
|
1375
|
-
call
|
|
2076
|
+
call(name, input) {
|
|
1376
2077
|
return invokeAction(name, input);
|
|
1377
2078
|
},
|
|
1378
|
-
|
|
1379
|
-
return
|
|
2079
|
+
reached(locator, roomVersion, path) {
|
|
2080
|
+
return path === undefined
|
|
2081
|
+
? reachedBy(locator, roomVersion)
|
|
2082
|
+
: writtenAfter(locator, roomVersion, path);
|
|
1380
2083
|
},
|
|
1381
|
-
|
|
1382
|
-
return
|
|
2084
|
+
log() {
|
|
2085
|
+
return journal.export();
|
|
1383
2086
|
},
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
const notify = second;
|
|
1387
|
-
const listener = {
|
|
1388
|
-
notify,
|
|
1389
|
-
ready: false
|
|
1390
|
-
};
|
|
1391
|
-
draftListeners.add(listener);
|
|
1392
|
-
void exclusive(async ()=>{
|
|
1393
|
-
const local = await openLocal();
|
|
1394
|
-
await readDrafts(local);
|
|
1395
|
-
if (closed || !draftListeners.has(listener)) return;
|
|
1396
|
-
listener.ready = true;
|
|
1397
|
-
}).then(()=>{
|
|
1398
|
-
if (closed || !listener.ready || !draftListeners.has(listener)) return;
|
|
1399
|
-
listener.notify(snapshotDrafts());
|
|
1400
|
-
}, ()=>{});
|
|
1401
|
-
return ()=>{
|
|
1402
|
-
draftListeners.delete(listener);
|
|
1403
|
-
};
|
|
1404
|
-
}
|
|
1405
|
-
if (topic === 'rooms') {
|
|
1406
|
-
const notify = second;
|
|
1407
|
-
roomListeners.add(notify);
|
|
1408
|
-
notify(listed());
|
|
1409
|
-
return ()=>{
|
|
1410
|
-
roomListeners.delete(notify);
|
|
1411
|
-
};
|
|
1412
|
-
}
|
|
1413
|
-
if (topic === 'connection-refusal') {
|
|
1414
|
-
const notify = second;
|
|
1415
|
-
refusalListeners.add(notify);
|
|
1416
|
-
notify(refusal);
|
|
1417
|
-
return ()=>{
|
|
1418
|
-
refusalListeners.delete(notify);
|
|
1419
|
-
};
|
|
1420
|
-
}
|
|
1421
|
-
if (topic === 'storage-loss') {
|
|
1422
|
-
const notify = second;
|
|
1423
|
-
storageLossListeners.add(notify);
|
|
1424
|
-
notify(storageLoss);
|
|
1425
|
-
return ()=>{
|
|
1426
|
-
storageLossListeners.delete(notify);
|
|
1427
|
-
};
|
|
1428
|
-
}
|
|
1429
|
-
if (topic === 'sync-status') {
|
|
1430
|
-
const notify = second;
|
|
1431
|
-
statusListeners.add(notify);
|
|
1432
|
-
notify(status);
|
|
1433
|
-
return ()=>{
|
|
1434
|
-
statusListeners.delete(notify);
|
|
1435
|
-
};
|
|
1436
|
-
}
|
|
1437
|
-
if (topic === 'queue') {
|
|
1438
|
-
const queue = {
|
|
1439
|
-
locator: second,
|
|
1440
|
-
notify: third
|
|
1441
|
-
};
|
|
1442
|
-
queueSubscriptions.add(queue);
|
|
1443
|
-
queue.notify(entriesOf(queue.locator));
|
|
1444
|
-
return ()=>{
|
|
1445
|
-
queueSubscriptions.delete(queue);
|
|
1446
|
-
};
|
|
1447
|
-
}
|
|
1448
|
-
const subscription = {
|
|
1449
|
-
query: second,
|
|
1450
|
-
notify: third
|
|
1451
|
-
};
|
|
1452
|
-
const first = visible(subscription.query);
|
|
1453
|
-
querySubscriptions.add(subscription);
|
|
1454
|
-
subscription.notify(first);
|
|
1455
|
-
return ()=>{
|
|
1456
|
-
querySubscriptions.delete(subscription);
|
|
1457
|
-
};
|
|
2087
|
+
persistence() {
|
|
2088
|
+
return options.shell.persistentStorage;
|
|
1458
2089
|
},
|
|
1459
|
-
|
|
1460
|
-
|
|
2090
|
+
observe: ((topic, ...input) => opened((deliver) => watch(topic, input, (message) => {
|
|
2091
|
+
deliver(structuredClone(message));
|
|
2092
|
+
}), topic === 'query')),
|
|
2093
|
+
async reauthorize() {
|
|
2094
|
+
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2095
|
+
return;
|
|
1461
2096
|
journal.record('connection reauthorize');
|
|
2097
|
+
await chain.catch(() => undefined);
|
|
2098
|
+
await mutations;
|
|
1462
2099
|
authorization = undefined;
|
|
1463
2100
|
discoveryPaused = false;
|
|
1464
2101
|
await enqueue();
|
|
1465
|
-
}
|
|
2102
|
+
},
|
|
1466
2103
|
};
|
|
2104
|
+
return { md, stop, watch };
|
|
1467
2105
|
}
|
|
1468
|
-
|
|
1469
|
-
//# sourceMappingURL=connection.js.map
|