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