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