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