@agentshouse/mdruntime 0.1.0-alpha.8 → 0.1.0-alpha.9
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/dist/adapter.d.ts +76 -21
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/bootstrap.d.ts +4 -1
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +4 -2
- package/dist/bootstrap.js.map +1 -1
- package/dist/client.d.ts +63 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +227 -0
- package/dist/client.js.map +1 -0
- package/dist/collection.d.ts +14 -4
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +60 -35
- package/dist/collection.js.map +1 -1
- package/dist/connection.d.ts +28 -7
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +603 -368
- 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 +401 -0
- package/dist/conversation.js.map +1 -0
- package/dist/durability.d.ts +8 -3
- package/dist/durability.d.ts.map +1 -1
- package/dist/durability.js +5 -59
- package/dist/durability.js.map +1 -1
- 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 +9 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +200 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/memory.d.ts +14 -2
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +171 -11
- package/dist/memory.js.map +1 -1
- package/dist/outcome.d.ts +16 -9
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +10 -0
- package/dist/outcome.js.map +1 -1
- package/dist/query.d.ts +1 -0
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js.map +1 -1
- package/dist/replica.d.ts +2 -1
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +16 -3
- package/dist/replica.js.map +1 -1
- package/dist/save.d.ts +3 -3
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +33 -14
- package/dist/save.js.map +1 -1
- package/dist/status.d.ts +11 -2
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +5 -4
- package/dist/status.js.map +1 -1
- package/dist/store.d.ts +32 -3
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +64 -7
- package/dist/store.js.map +1 -1
- package/dist/wire.d.ts +63 -0
- package/dist/wire.d.ts.map +1 -0
- package/dist/wire.js +139 -0
- package/dist/wire.js.map +1 -0
- package/package.json +10 -2
package/dist/connection.js
CHANGED
|
@@ -2,18 +2,35 @@ import { rawQuery } from './query.js';
|
|
|
2
2
|
import { cached, modelTypes, NO_MODEL, sameModel } from './model.js';
|
|
3
3
|
import { ROOM_CONTRACT_VERSION, VERSION_1_CAPABILITIES } from './adapter.js';
|
|
4
4
|
import { bootstrapRoom } from './bootstrap.js';
|
|
5
|
+
import { createConversationStore } from './conversation.js';
|
|
5
6
|
import { MD_MODEL, PRESENTED, validRoot } from './collection.js';
|
|
6
7
|
import { createLog } from './log.js';
|
|
7
|
-
import { queuedOutcome, refused } from './outcome.js';
|
|
8
|
+
import { queuedOutcome, refusalOf, refused, withheldUntilReset } from './outcome.js';
|
|
8
9
|
import { refreshRoom } from './refresh.js';
|
|
9
10
|
import { buildBatch } from './save.js';
|
|
10
|
-
import { applyReturned, readConfirmed, replicaOf } from './replica.js';
|
|
11
|
+
import { applyReturned, compositionOf, readConfirmed, replicaOf } from './replica.js';
|
|
11
12
|
import { settled, same, saving, STARTING, transferring } from './status.js';
|
|
12
13
|
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
13
|
-
import {
|
|
14
|
+
import { beneath } from './paths.js';
|
|
15
|
+
import { openRuntimeStore } from './store.js';
|
|
14
16
|
import { unconfirmed } from './transfer.js';
|
|
15
17
|
const DEFAULT_DISCOVERY_INTERVAL_MS = 60_000;
|
|
16
18
|
const knownCapabilities = new Set(VERSION_1_CAPABILITIES);
|
|
19
|
+
function within(root, path) {
|
|
20
|
+
return root === '' || beneath(root, path);
|
|
21
|
+
}
|
|
22
|
+
function declaredIdentity(model) {
|
|
23
|
+
if (model === undefined) return false;
|
|
24
|
+
if (model === null) return true;
|
|
25
|
+
return typeof model.identity === 'string' && model.identity.length > 0 && typeof model.version === 'string' && model.version.length > 0;
|
|
26
|
+
}
|
|
27
|
+
function declaredSelection(room) {
|
|
28
|
+
const selection = room.typedSelection;
|
|
29
|
+
if (selection === undefined) return true;
|
|
30
|
+
if (!Array.isArray(selection.roots) || selection.roots.length === 0) return false;
|
|
31
|
+
if (!declaredIdentity(selection.attachedModel)) return false;
|
|
32
|
+
return selection.roots.every((root, index)=>validRoot(root) && root !== '' && !within(room.collectionRoot, root) && selection.roots.every((other, at)=>at === index || !within(other, root)));
|
|
33
|
+
}
|
|
17
34
|
export function createMdRuntime(options) {
|
|
18
35
|
const period = options.discoveryIntervalMs ?? DEFAULT_DISCOVERY_INTERVAL_MS;
|
|
19
36
|
let authorization = options.authorization;
|
|
@@ -21,6 +38,7 @@ export function createMdRuntime(options) {
|
|
|
21
38
|
let store;
|
|
22
39
|
const replicas = new Map();
|
|
23
40
|
const drafts = new Map();
|
|
41
|
+
const presentations = new Map();
|
|
24
42
|
const outcomes = new Map();
|
|
25
43
|
let limits = [];
|
|
26
44
|
let capabilities = [];
|
|
@@ -30,8 +48,26 @@ export function createMdRuntime(options) {
|
|
|
30
48
|
const liveModels = new Map();
|
|
31
49
|
const invalidModels = new Set();
|
|
32
50
|
const modelEpoch = new Map();
|
|
33
|
-
|
|
34
|
-
|
|
51
|
+
const discovered = new Map();
|
|
52
|
+
const scopedRefusals = new Map();
|
|
53
|
+
function modelKey(locator, attached) {
|
|
54
|
+
return `${attached} ${locator}`;
|
|
55
|
+
}
|
|
56
|
+
function modelOf(locator, attached = 'collection') {
|
|
57
|
+
return models.get(modelKey(locator, attached)) ?? NO_MODEL;
|
|
58
|
+
}
|
|
59
|
+
function savedModel(locator, attached) {
|
|
60
|
+
return invalidModels.has(modelKey(locator, attached)) ? {
|
|
61
|
+
status: 'unavailable',
|
|
62
|
+
reason: 'revoked'
|
|
63
|
+
} : modelOf(locator, attached);
|
|
64
|
+
}
|
|
65
|
+
function compositionFor(record) {
|
|
66
|
+
return compositionOf({
|
|
67
|
+
...record,
|
|
68
|
+
model: savedModel(record.locator, 'collection'),
|
|
69
|
+
selectionModel: savedModel(record.locator, 'typed-selection')
|
|
70
|
+
});
|
|
35
71
|
}
|
|
36
72
|
const mismatchedRoots = new Set();
|
|
37
73
|
let refusal = null;
|
|
@@ -40,6 +76,9 @@ export function createMdRuntime(options) {
|
|
|
40
76
|
let discoveryPaused = false;
|
|
41
77
|
let timer;
|
|
42
78
|
let closed = false;
|
|
79
|
+
let withdrawNotifications;
|
|
80
|
+
let handed;
|
|
81
|
+
const notices = new Map();
|
|
43
82
|
let chain = Promise.resolve();
|
|
44
83
|
let mutations = Promise.resolve();
|
|
45
84
|
let inflight = false;
|
|
@@ -51,15 +90,21 @@ export function createMdRuntime(options) {
|
|
|
51
90
|
const querySubscriptions = new Set();
|
|
52
91
|
const queueSubscriptions = new Set();
|
|
53
92
|
const draftListeners = new Set();
|
|
93
|
+
const presentationListeners = new Set();
|
|
94
|
+
let withdrawConversations;
|
|
95
|
+
const conversations = createConversationStore({
|
|
96
|
+
cards: declaredCards,
|
|
97
|
+
documents: ()=>visible({}),
|
|
98
|
+
call: (name, input)=>invokeAction(name, input),
|
|
99
|
+
durably,
|
|
100
|
+
path: ()=>options.adapter.conversations,
|
|
101
|
+
closed: ()=>closed
|
|
102
|
+
});
|
|
54
103
|
const journal = createLog();
|
|
55
104
|
let notifyReady = ()=>{};
|
|
56
105
|
const ready = new Promise((resolve)=>{
|
|
57
106
|
notifyReady = resolve;
|
|
58
107
|
});
|
|
59
|
-
let leader = typeof window === 'undefined';
|
|
60
|
-
let applyingRemote = false;
|
|
61
|
-
let releaseLeadership = ()=>{};
|
|
62
|
-
const channel = typeof window !== 'undefined' ? new BroadcastChannel('mdruntime') : undefined;
|
|
63
108
|
function draftContent(locator, path) {
|
|
64
109
|
return drafts.get(locator)?.get(path);
|
|
65
110
|
}
|
|
@@ -93,9 +138,7 @@ export function createMdRuntime(options) {
|
|
|
93
138
|
return entries;
|
|
94
139
|
}
|
|
95
140
|
async function readDrafts(opened) {
|
|
96
|
-
await
|
|
97
|
-
await hydrate(opened);
|
|
98
|
-
});
|
|
141
|
+
await hydrate(opened);
|
|
99
142
|
return snapshotDrafts();
|
|
100
143
|
}
|
|
101
144
|
function publishDrafts(waitingOnly) {
|
|
@@ -113,16 +156,23 @@ export function createMdRuntime(options) {
|
|
|
113
156
|
function releaseWaitingDrafts() {
|
|
114
157
|
publishDrafts(true);
|
|
115
158
|
}
|
|
159
|
+
function presentationOf(app) {
|
|
160
|
+
const value = presentations.get(app);
|
|
161
|
+
return value === undefined ? null : structuredClone(value);
|
|
162
|
+
}
|
|
163
|
+
function emitPresentation(app) {
|
|
164
|
+
if (closed) return;
|
|
165
|
+
for (const listener of presentationListeners){
|
|
166
|
+
if (listener.app === app && listener.ready) listener.notify(presentationOf(app));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
116
169
|
function commitDraft(work) {
|
|
117
170
|
return exclusive(async ()=>{
|
|
118
171
|
const local = await openLocal();
|
|
119
|
-
await
|
|
120
|
-
|
|
121
|
-
await work(local);
|
|
122
|
-
});
|
|
172
|
+
await hydrate(local);
|
|
173
|
+
await work(local);
|
|
123
174
|
}).then(()=>{
|
|
124
175
|
emitDrafts();
|
|
125
|
-
announce();
|
|
126
176
|
});
|
|
127
177
|
}
|
|
128
178
|
function pending() {
|
|
@@ -131,12 +181,33 @@ export function createMdRuntime(options) {
|
|
|
131
181
|
].some((replica)=>replica.queue.length > 0);
|
|
132
182
|
}
|
|
133
183
|
function attention() {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
184
|
+
const attentionRequired = [];
|
|
185
|
+
let reset = null;
|
|
186
|
+
for (const [locator, replica] of replicas){
|
|
137
187
|
const head = replica.queue[0];
|
|
138
|
-
|
|
139
|
-
|
|
188
|
+
if (head === undefined) continue;
|
|
189
|
+
const outcome = outcomes.get(head.id);
|
|
190
|
+
if (outcome?.kind === 'retryable-refusal') {
|
|
191
|
+
if (outcome.reset !== undefined && (reset === null || outcome.reset < reset)) {
|
|
192
|
+
reset = outcome.reset;
|
|
193
|
+
}
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (!refused(outcome)) continue;
|
|
197
|
+
attentionRequired.push({
|
|
198
|
+
locator,
|
|
199
|
+
batch: head.id,
|
|
200
|
+
outcome: outcome.kind,
|
|
201
|
+
refusal: refusalOf(outcome),
|
|
202
|
+
...outcome.kind === 'conflicted' && outcome.roomVersion !== undefined ? {
|
|
203
|
+
roomVersion: outcome.roomVersion
|
|
204
|
+
} : {}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
attentionRequired,
|
|
209
|
+
reset
|
|
210
|
+
};
|
|
140
211
|
}
|
|
141
212
|
function storedOutcomes(batches) {
|
|
142
213
|
const stored = {};
|
|
@@ -152,7 +223,6 @@ export function createMdRuntime(options) {
|
|
|
152
223
|
batches,
|
|
153
224
|
outcomes: storedOutcomes(batches)
|
|
154
225
|
});
|
|
155
|
-
announce();
|
|
156
226
|
}
|
|
157
227
|
function listed() {
|
|
158
228
|
return structuredClone([
|
|
@@ -166,7 +236,8 @@ export function createMdRuntime(options) {
|
|
|
166
236
|
room: replica.confirmed.record.locator,
|
|
167
237
|
roomVersion: replica.confirmed.record.roomVersion,
|
|
168
238
|
documents: replica.visible.collection.documents,
|
|
169
|
-
model: modelTypes(modelOf(replica.confirmed.record.locator))
|
|
239
|
+
model: modelTypes(modelOf(replica.confirmed.record.locator)),
|
|
240
|
+
typedSelection: replica.confirmed.record.typedSelection === null ? null : modelTypes(modelOf(replica.confirmed.record.locator, 'typed-selection'))
|
|
170
241
|
}));
|
|
171
242
|
}
|
|
172
243
|
function emitRooms() {
|
|
@@ -179,29 +250,11 @@ export function createMdRuntime(options) {
|
|
|
179
250
|
function emitStorageLoss() {
|
|
180
251
|
for (const listener of storageLossListeners)listener(storageLoss);
|
|
181
252
|
}
|
|
182
|
-
function
|
|
183
|
-
if (applyingRemote || channel === undefined) return;
|
|
184
|
-
if (type === 'hello') {
|
|
185
|
-
channel.postMessage({
|
|
186
|
-
type: 'hello'
|
|
187
|
-
});
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
channel.postMessage({
|
|
191
|
-
type,
|
|
192
|
-
status
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
function adopt(next) {
|
|
253
|
+
function moveTo(next) {
|
|
196
254
|
if (closed || same(status, next)) return;
|
|
197
255
|
status = next;
|
|
198
256
|
for (const listener of statusListeners)listener(status);
|
|
199
257
|
}
|
|
200
|
-
function moveTo(next) {
|
|
201
|
-
if (closed || same(status, next)) return;
|
|
202
|
-
adopt(next);
|
|
203
|
-
announce();
|
|
204
|
-
}
|
|
205
258
|
function visible(request) {
|
|
206
259
|
if (options.queryBinding === null) {
|
|
207
260
|
if (Object.keys(request).length > 0) throw new Error('query-binding-unavailable');
|
|
@@ -247,35 +300,42 @@ export function createMdRuntime(options) {
|
|
|
247
300
|
void enqueue();
|
|
248
301
|
}, period);
|
|
249
302
|
}
|
|
250
|
-
function refuse(reason) {
|
|
303
|
+
function refuse(reason, provider = null) {
|
|
251
304
|
if (reason === 'revoked-authorization') {
|
|
252
305
|
const revoked = {
|
|
253
306
|
status: 'unavailable',
|
|
254
307
|
reason: 'revoked'
|
|
255
308
|
};
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
]
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
309
|
+
const attachments = [
|
|
310
|
+
'collection',
|
|
311
|
+
'typed-selection'
|
|
312
|
+
];
|
|
313
|
+
const keys = new Set(models.keys());
|
|
314
|
+
for (const locator of rooms.keys()){
|
|
315
|
+
for (const attached of attachments)keys.add(modelKey(locator, attached));
|
|
316
|
+
}
|
|
317
|
+
for (const key of keys){
|
|
318
|
+
invalidModels.add(key);
|
|
319
|
+
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
320
|
+
models.set(key, revoked);
|
|
321
|
+
liveModels.delete(key);
|
|
264
322
|
}
|
|
265
323
|
for (const [locator, room] of rooms)rooms.set(locator, {
|
|
266
324
|
...room,
|
|
267
|
-
model: modelTypes(revoked)
|
|
325
|
+
model: modelTypes(revoked),
|
|
326
|
+
typedSelection: room.typedSelection === null ? null : {
|
|
327
|
+
...room.typedSelection,
|
|
328
|
+
model: modelTypes(revoked)
|
|
329
|
+
}
|
|
268
330
|
});
|
|
269
331
|
emitRooms();
|
|
270
332
|
void exclusive(async ()=>{
|
|
271
333
|
const local = await openLocal();
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
});
|
|
334
|
+
for (const record of (await local.all()))await local.publish({
|
|
335
|
+
...record,
|
|
336
|
+
model: revoked,
|
|
337
|
+
selectionModel: revoked
|
|
277
338
|
});
|
|
278
|
-
announce();
|
|
279
339
|
}).catch(()=>{
|
|
280
340
|
journal.record('model invalidation durable-write failure');
|
|
281
341
|
});
|
|
@@ -284,12 +344,14 @@ export function createMdRuntime(options) {
|
|
|
284
344
|
discoveryPaused = true;
|
|
285
345
|
refusal = {
|
|
286
346
|
reason,
|
|
287
|
-
reauthorizationOffered: reason === 'revoked-authorization'
|
|
347
|
+
reauthorizationOffered: reason === 'revoked-authorization',
|
|
348
|
+
refusal: provider
|
|
288
349
|
};
|
|
289
350
|
journal.record(`connection refused ${reason}`);
|
|
290
351
|
emitRefusal();
|
|
291
352
|
}
|
|
292
353
|
function list(room, synchronizationAvailable) {
|
|
354
|
+
const selection = room.typedSelection ?? null;
|
|
293
355
|
rooms.set(room.locator, {
|
|
294
356
|
name: room.name,
|
|
295
357
|
collectionRoot: room.collectionRoot,
|
|
@@ -298,14 +360,23 @@ export function createMdRuntime(options) {
|
|
|
298
360
|
replica: room.replica,
|
|
299
361
|
attachedModel: room.attachedModel,
|
|
300
362
|
model: modelTypes(modelOf(room.locator)),
|
|
363
|
+
typedSelection: selection === null ? null : {
|
|
364
|
+
roots: selection.roots,
|
|
365
|
+
attachedModel: selection.attachedModel,
|
|
366
|
+
model: modelTypes(modelOf(room.locator, 'typed-selection'))
|
|
367
|
+
},
|
|
301
368
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
302
369
|
roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
|
|
303
|
-
synchronizationAvailable
|
|
370
|
+
synchronizationAvailable,
|
|
371
|
+
refusal: scopedRefusals.get(room.locator) ?? null
|
|
304
372
|
});
|
|
305
373
|
}
|
|
306
|
-
function applyDiscovery(
|
|
307
|
-
const listedLocators = new Set(
|
|
308
|
-
for (const
|
|
374
|
+
function applyDiscovery(listed) {
|
|
375
|
+
const listedLocators = new Set(listed.map((room)=>room.locator));
|
|
376
|
+
for (const locator of listedLocators)scopedRefusals.delete(locator);
|
|
377
|
+
discovered.clear();
|
|
378
|
+
for (const room of listed)discovered.set(room.locator, room);
|
|
379
|
+
for (const room of listed){
|
|
309
380
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
310
381
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
311
382
|
list(held, false);
|
|
@@ -323,22 +394,41 @@ export function createMdRuntime(options) {
|
|
|
323
394
|
emitRooms();
|
|
324
395
|
return listedLocators;
|
|
325
396
|
}
|
|
397
|
+
function restored(key, persisted) {
|
|
398
|
+
const prior = models.get(key);
|
|
399
|
+
if (prior !== undefined && prior.status !== 'unavailable' && (persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
|
|
400
|
+
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
401
|
+
invalidModels.add(key);
|
|
402
|
+
}
|
|
403
|
+
const model = persisted.status === 'unavailable' || liveModels.get(key) === JSON.stringify(persisted.snapshot) ? persisted : cached(persisted);
|
|
404
|
+
models.set(key, model);
|
|
405
|
+
if (model.status === 'unavailable') {
|
|
406
|
+
liveModels.delete(key);
|
|
407
|
+
invalidModels.delete(key);
|
|
408
|
+
}
|
|
409
|
+
return model;
|
|
410
|
+
}
|
|
326
411
|
async function hydrate(opened) {
|
|
327
|
-
const [records, queues, kept, lastCompleted, declared] = await Promise.all([
|
|
412
|
+
const [records, queues, kept, presented, cached, unsent, lastCompleted, declared] = await Promise.all([
|
|
328
413
|
opened.all(),
|
|
329
414
|
opened.queues(),
|
|
330
415
|
opened.drafts(),
|
|
416
|
+
opened.presentation(),
|
|
417
|
+
opened.conversations(),
|
|
418
|
+
opened.instructions(),
|
|
331
419
|
opened.lastCompleted(),
|
|
332
420
|
opened.providerLimits()
|
|
333
421
|
]);
|
|
334
422
|
limits = declared;
|
|
335
423
|
drafts.clear();
|
|
336
424
|
for (const draft of kept)rememberDraft(draft.locator, draft.path, draft.content);
|
|
425
|
+
presentations.clear();
|
|
426
|
+
for (const record of presented)presentations.set(record.app, record.value);
|
|
427
|
+
conversations.hydrate(cached, unsent);
|
|
337
428
|
const queuedBatches = new Map(queues.map((queue)=>[
|
|
338
429
|
queue.locator,
|
|
339
430
|
queue.batches
|
|
340
431
|
]));
|
|
341
|
-
outcomes.clear();
|
|
342
432
|
for (const queue of queues){
|
|
343
433
|
for (const [id, outcome] of Object.entries(queue.outcomes)){
|
|
344
434
|
outcomes.set(id, outcome);
|
|
@@ -346,21 +436,12 @@ export function createMdRuntime(options) {
|
|
|
346
436
|
}
|
|
347
437
|
replicas.clear();
|
|
348
438
|
for (const record of records){
|
|
349
|
-
const
|
|
350
|
-
const
|
|
351
|
-
if (priorModel !== undefined && priorModel.status !== 'unavailable' && (persisted.status === 'unavailable' || !sameModel(priorModel.snapshot, persisted.snapshot))) {
|
|
352
|
-
modelEpoch.set(record.locator, (modelEpoch.get(record.locator) ?? 0) + 1);
|
|
353
|
-
invalidModels.add(record.locator);
|
|
354
|
-
}
|
|
355
|
-
const model = persisted.status === 'unavailable' || liveModels.get(record.locator) === JSON.stringify(persisted.snapshot) ? persisted : cached(persisted);
|
|
356
|
-
models.set(record.locator, model);
|
|
357
|
-
if (model.status === 'unavailable') {
|
|
358
|
-
liveModels.delete(record.locator);
|
|
359
|
-
invalidModels.delete(record.locator);
|
|
360
|
-
}
|
|
439
|
+
const model = restored(modelKey(record.locator, 'collection'), record.model);
|
|
440
|
+
const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
|
|
361
441
|
const confirmed = readConfirmed({
|
|
362
442
|
...record,
|
|
363
|
-
model
|
|
443
|
+
model,
|
|
444
|
+
selectionModel
|
|
364
445
|
});
|
|
365
446
|
if (confirmed === null) continue;
|
|
366
447
|
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? []));
|
|
@@ -373,7 +454,12 @@ export function createMdRuntime(options) {
|
|
|
373
454
|
operations: record.operations,
|
|
374
455
|
replica: record.replica,
|
|
375
456
|
attachedModel: record.attachedModel,
|
|
376
|
-
model: modelTypes(model)
|
|
457
|
+
model: modelTypes(model),
|
|
458
|
+
typedSelection: record.typedSelection === null ? null : {
|
|
459
|
+
roots: record.typedSelection.roots,
|
|
460
|
+
attachedModel: record.typedSelection.attachedModel,
|
|
461
|
+
model: modelTypes(selectionModel)
|
|
462
|
+
}
|
|
377
463
|
});
|
|
378
464
|
}
|
|
379
465
|
}
|
|
@@ -395,52 +481,34 @@ export function createMdRuntime(options) {
|
|
|
395
481
|
emitDrafts();
|
|
396
482
|
return lastCompleted;
|
|
397
483
|
}
|
|
398
|
-
async function applyRemote(message) {
|
|
399
|
-
if (closed) return;
|
|
400
|
-
if (message.type === 'hello') {
|
|
401
|
-
if (leader) announce();
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
applyingRemote = true;
|
|
405
|
-
try {
|
|
406
|
-
const local = await openLocal();
|
|
407
|
-
const lastCompleted = await readState(local);
|
|
408
|
-
if (message.status !== undefined) adopt(message.status);
|
|
409
|
-
else {
|
|
410
|
-
adopt({
|
|
411
|
-
...status,
|
|
412
|
-
lastCompleted,
|
|
413
|
-
attentionRequired: attention(),
|
|
414
|
-
state: pending() ? 'pending' : status.state
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
} finally{
|
|
418
|
-
applyingRemote = false;
|
|
419
|
-
}
|
|
420
|
-
if (message.type === 'enqueue' && leader) void enqueue();
|
|
421
|
-
}
|
|
422
484
|
async function open() {
|
|
423
|
-
const opened = await
|
|
485
|
+
const opened = await openRuntimeStore();
|
|
424
486
|
store = opened;
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
}
|
|
487
|
+
const kept = options.shell.generation;
|
|
488
|
+
const storedGeneration = await opened.generation();
|
|
489
|
+
const loss = detectStorageLoss(storedGeneration, kept);
|
|
490
|
+
const generation = nextGeneration(storedGeneration, kept);
|
|
491
|
+
if (generation !== storedGeneration) await opened.rememberGeneration(generation);
|
|
492
|
+
if (generation !== kept) await options.shell.keepGeneration(generation);
|
|
493
|
+
if (loss !== null) {
|
|
494
|
+
storageLoss = loss;
|
|
495
|
+
emitStorageLoss();
|
|
496
|
+
}
|
|
435
497
|
const lastCompleted = await readState(opened);
|
|
436
|
-
|
|
498
|
+
moveTo({
|
|
437
499
|
...status,
|
|
438
500
|
lastCompleted,
|
|
439
|
-
|
|
501
|
+
...attention()
|
|
440
502
|
});
|
|
441
|
-
if (!leader) announce('hello');
|
|
442
503
|
return opened;
|
|
443
504
|
}
|
|
505
|
+
function durably(work) {
|
|
506
|
+
return exclusive(async ()=>{
|
|
507
|
+
const local = await openLocal();
|
|
508
|
+
await hydrate(local);
|
|
509
|
+
return work(local);
|
|
510
|
+
});
|
|
511
|
+
}
|
|
444
512
|
function openLocal() {
|
|
445
513
|
opening ??= open().catch((error)=>{
|
|
446
514
|
opening = undefined;
|
|
@@ -453,132 +521,117 @@ export function createMdRuntime(options) {
|
|
|
453
521
|
mutations = next.then(()=>undefined, ()=>undefined);
|
|
454
522
|
return next;
|
|
455
523
|
}
|
|
456
|
-
function acrossTabs(name, work) {
|
|
457
|
-
if (typeof window === 'undefined') return work();
|
|
458
|
-
return window.navigator.locks.request(name, work);
|
|
459
|
-
}
|
|
460
524
|
function publish(local, room, confirmed) {
|
|
461
525
|
return exclusive(async ()=>{
|
|
462
|
-
await
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
526
|
+
await hydrate(local);
|
|
527
|
+
const retained = readConfirmed({
|
|
528
|
+
...confirmed.record,
|
|
529
|
+
model: modelOf(room.locator),
|
|
530
|
+
selectionModel: modelOf(room.locator, 'typed-selection')
|
|
531
|
+
});
|
|
532
|
+
if (retained !== null) {
|
|
469
533
|
await local.publish(retained.record);
|
|
470
534
|
unreadable.delete(room.locator);
|
|
471
535
|
replicas.set(room.locator, replicaOf(retained, replicas.get(room.locator)?.queue ?? []));
|
|
472
|
-
}
|
|
536
|
+
}
|
|
473
537
|
list(room, true);
|
|
474
538
|
emitRooms();
|
|
475
539
|
emitVisible();
|
|
476
|
-
announce();
|
|
477
540
|
});
|
|
478
541
|
}
|
|
479
542
|
async function write(locator, changes, replacing) {
|
|
480
543
|
const local = await openLocal();
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
...built.refusal
|
|
509
|
-
};
|
|
510
|
-
const head = replica.queue[0];
|
|
511
|
-
if (replacing !== undefined) {
|
|
512
|
-
if (head === undefined || head.id !== replacing || !refused(outcomes.get(head.id))) {
|
|
513
|
-
return {
|
|
514
|
-
outcome: 'refused',
|
|
515
|
-
rule: 'no-refused-head'
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
const batches = replacing === undefined ? [
|
|
520
|
-
...replica.queue,
|
|
521
|
-
built.batch
|
|
522
|
-
] : [
|
|
523
|
-
built.batch,
|
|
524
|
-
...replica.queue.slice(1)
|
|
525
|
-
];
|
|
526
|
-
const consumed = [
|
|
527
|
-
...new Set(changes.map((change)=>change.path))
|
|
528
|
-
];
|
|
529
|
-
try {
|
|
530
|
-
await local.append({
|
|
531
|
-
locator,
|
|
532
|
-
batches,
|
|
533
|
-
outcomes: storedOutcomes(batches)
|
|
534
|
-
}, consumed);
|
|
535
|
-
} catch {
|
|
544
|
+
await hydrate(local);
|
|
545
|
+
const replica = replicas.get(locator);
|
|
546
|
+
const room = rooms.get(locator);
|
|
547
|
+
if (replica === undefined || room === undefined) {
|
|
548
|
+
throw new Error(`No Local Replica for ${locator}`);
|
|
549
|
+
}
|
|
550
|
+
if (mismatchedRoots.has(locator)) return {
|
|
551
|
+
outcome: 'refused',
|
|
552
|
+
rule: 'collection-root-mismatch'
|
|
553
|
+
};
|
|
554
|
+
const built = buildBatch({
|
|
555
|
+
baseRoomVersion: replica.confirmed.record.roomVersion,
|
|
556
|
+
composition: compositionFor(replica.confirmed.record),
|
|
557
|
+
operations: room.operations,
|
|
558
|
+
binding: options.modelBinding,
|
|
559
|
+
providerOwnedPaths: room.providerOwnedPaths,
|
|
560
|
+
providerLimits: limits,
|
|
561
|
+
visible: replica.visible,
|
|
562
|
+
changes
|
|
563
|
+
});
|
|
564
|
+
if (built.kind === 'refused') return {
|
|
565
|
+
outcome: 'refused',
|
|
566
|
+
...built.refusal
|
|
567
|
+
};
|
|
568
|
+
const head = replica.queue[0];
|
|
569
|
+
if (replacing !== undefined) {
|
|
570
|
+
if (head === undefined || head.id !== replacing || !refused(outcomes.get(head.id))) {
|
|
536
571
|
return {
|
|
537
572
|
outcome: 'refused',
|
|
538
|
-
rule: '
|
|
573
|
+
rule: 'no-refused-head'
|
|
539
574
|
};
|
|
540
575
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
576
|
+
}
|
|
577
|
+
const batches = replacing === undefined ? [
|
|
578
|
+
...replica.queue,
|
|
579
|
+
built.batch
|
|
580
|
+
] : [
|
|
581
|
+
built.batch,
|
|
582
|
+
...replica.queue.slice(1)
|
|
583
|
+
];
|
|
584
|
+
const consumed = [
|
|
585
|
+
...new Set(changes.map((change)=>change.path))
|
|
586
|
+
];
|
|
587
|
+
try {
|
|
588
|
+
await local.append({
|
|
589
|
+
locator,
|
|
590
|
+
batches,
|
|
591
|
+
outcomes: storedOutcomes(batches)
|
|
592
|
+
}, consumed);
|
|
593
|
+
} catch {
|
|
551
594
|
return {
|
|
552
|
-
outcome: '
|
|
553
|
-
|
|
595
|
+
outcome: 'refused',
|
|
596
|
+
rule: 'durable-write'
|
|
554
597
|
};
|
|
598
|
+
}
|
|
599
|
+
if (replacing !== undefined) outcomes.delete(replacing);
|
|
600
|
+
for (const path of consumed)forgetDraft(locator, path);
|
|
601
|
+
replicas.set(locator, replicaOf(replica.confirmed, batches));
|
|
602
|
+
emitQueue(locator);
|
|
603
|
+
emitVisible();
|
|
604
|
+
emitDrafts();
|
|
605
|
+
moveTo({
|
|
606
|
+
...saving(status),
|
|
607
|
+
...attention()
|
|
555
608
|
});
|
|
556
|
-
|
|
557
|
-
return
|
|
609
|
+
void enqueue();
|
|
610
|
+
return {
|
|
611
|
+
outcome: 'saved',
|
|
612
|
+
id: built.batch.id
|
|
613
|
+
};
|
|
558
614
|
}
|
|
559
615
|
async function discardHead(locator, identifier) {
|
|
560
616
|
const local = await openLocal();
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
attentionRequired: attention()
|
|
578
|
-
});
|
|
579
|
-
return true;
|
|
617
|
+
await hydrate(local);
|
|
618
|
+
const replica = replicas.get(locator);
|
|
619
|
+
const head = replica?.queue[0];
|
|
620
|
+
if (replica === undefined || head === undefined || head.id !== identifier || !refused(outcomes.get(head.id))) {
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
const batches = replica.queue.slice(1);
|
|
624
|
+
outcomes.delete(identifier);
|
|
625
|
+
await persistQueue(local, locator, batches);
|
|
626
|
+
replicas.set(locator, replicaOf(replica.confirmed, batches));
|
|
627
|
+
emitQueue(locator);
|
|
628
|
+
emitVisible();
|
|
629
|
+
moveTo({
|
|
630
|
+
...status,
|
|
631
|
+
state: pending() ? 'pending' : status.state,
|
|
632
|
+
...attention()
|
|
580
633
|
});
|
|
581
|
-
|
|
634
|
+
void enqueue();
|
|
582
635
|
}
|
|
583
636
|
async function readHistory(locator, path, query = {}) {
|
|
584
637
|
if (closed || refusal !== null) return {
|
|
@@ -630,6 +683,12 @@ export function createMdRuntime(options) {
|
|
|
630
683
|
};
|
|
631
684
|
}
|
|
632
685
|
}
|
|
686
|
+
function declaredCards() {
|
|
687
|
+
for (const capability of capabilities){
|
|
688
|
+
if (capability.conversationCards !== undefined) return capability.conversationCards;
|
|
689
|
+
}
|
|
690
|
+
return undefined;
|
|
691
|
+
}
|
|
633
692
|
function declaredAction(name) {
|
|
634
693
|
for (const capability of capabilities){
|
|
635
694
|
for (const action of capability.actions ?? []){
|
|
@@ -670,7 +729,7 @@ export function createMdRuntime(options) {
|
|
|
670
729
|
}
|
|
671
730
|
if (result.kind === 'revoked-authorization') refuse('revoked-authorization');
|
|
672
731
|
journal.record(`outcome action ${result.kind}`);
|
|
673
|
-
if (result.kind === 'rejected') journal.record(`refusal rejected ${result.
|
|
732
|
+
if (result.kind === 'rejected') journal.record(`refusal rejected ${result.code}`);
|
|
674
733
|
return result;
|
|
675
734
|
} catch {
|
|
676
735
|
journal.record('outcome action transport-uncertainty');
|
|
@@ -692,7 +751,10 @@ export function createMdRuntime(options) {
|
|
|
692
751
|
}
|
|
693
752
|
async function pullBootstrap(authorized, room) {
|
|
694
753
|
try {
|
|
695
|
-
const result = await bootstrapRoom(options.adapter, authorized, room,
|
|
754
|
+
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
755
|
+
collection: modelOf(room.locator),
|
|
756
|
+
selection: modelOf(room.locator, 'typed-selection')
|
|
757
|
+
}, options.modelBinding);
|
|
696
758
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
697
759
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
698
760
|
return result;
|
|
@@ -729,59 +791,94 @@ export function createMdRuntime(options) {
|
|
|
729
791
|
if (result.kind === 'expired') return {
|
|
730
792
|
kind: 'expired'
|
|
731
793
|
};
|
|
794
|
+
if (result.kind === 'retryable-refusal') {
|
|
795
|
+
return {
|
|
796
|
+
kind: 'retryable-refusal',
|
|
797
|
+
code: result.code,
|
|
798
|
+
fields: result.fields,
|
|
799
|
+
...result.reset === undefined ? {} : {
|
|
800
|
+
reset: result.reset
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
}
|
|
732
804
|
if (result.kind === 'conflicted') {
|
|
733
805
|
return {
|
|
734
806
|
kind: 'conflicted',
|
|
735
|
-
roomVersion:
|
|
807
|
+
...result.roomVersion === undefined ? {} : {
|
|
808
|
+
roomVersion: result.roomVersion
|
|
809
|
+
},
|
|
736
810
|
affected: result.affected,
|
|
737
|
-
|
|
811
|
+
code: result.code,
|
|
812
|
+
fields: result.fields
|
|
738
813
|
};
|
|
739
814
|
}
|
|
740
815
|
return {
|
|
741
816
|
kind: 'rejected',
|
|
742
817
|
affected: result.affected,
|
|
743
|
-
|
|
818
|
+
code: result.code,
|
|
819
|
+
fields: result.fields
|
|
744
820
|
};
|
|
745
821
|
}
|
|
746
822
|
async function commitAccept(room, local, submitted, accepted, base, overlay) {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
if (current === undefined || !current.queue.some((batch)=>batch.id === submitted.id)) {
|
|
751
|
-
return true;
|
|
752
|
-
}
|
|
753
|
-
const remaining = current.queue.filter((batch)=>batch.id !== submitted.id);
|
|
754
|
-
const retainedBase = readConfirmed({
|
|
755
|
-
...base.record,
|
|
756
|
-
model: modelOf(room.locator)
|
|
757
|
-
});
|
|
758
|
-
if (retainedBase === null) return false;
|
|
759
|
-
const applied = overlay ? applyReturned(retainedBase, accepted.documents, accepted.revisions, accepted.removals, accepted.roomVersion, options.modelBinding) : retainedBase;
|
|
760
|
-
if (applied === null) return false;
|
|
761
|
-
await local.publish(applied.record);
|
|
762
|
-
await persistQueue(local, room.locator, remaining);
|
|
763
|
-
outcomes.set(submitted.id, {
|
|
764
|
-
kind: 'accepted',
|
|
765
|
-
roomVersion: accepted.roomVersion,
|
|
766
|
-
author: accepted.author,
|
|
767
|
-
time: accepted.time
|
|
768
|
-
});
|
|
769
|
-
unreadable.delete(room.locator);
|
|
770
|
-
replicas.set(room.locator, replicaOf(applied, remaining));
|
|
771
|
-
list(room, true);
|
|
772
|
-
emitRooms();
|
|
773
|
-
emitQueue(room.locator);
|
|
774
|
-
emitVisible();
|
|
775
|
-
journal.record(`outcome ${room.locator} ${submitted.id} accepted`);
|
|
823
|
+
await hydrate(local);
|
|
824
|
+
const current = replicas.get(room.locator);
|
|
825
|
+
if (current === undefined || !current.queue.some((batch)=>batch.id === submitted.id)) {
|
|
776
826
|
return true;
|
|
827
|
+
}
|
|
828
|
+
const remaining = current.queue.filter((batch)=>batch.id !== submitted.id);
|
|
829
|
+
const retainedBase = readConfirmed({
|
|
830
|
+
...base.record,
|
|
831
|
+
model: modelOf(room.locator),
|
|
832
|
+
selectionModel: modelOf(room.locator, 'typed-selection')
|
|
777
833
|
});
|
|
834
|
+
if (retainedBase === null) return false;
|
|
835
|
+
const applied = overlay ? applyReturned(retainedBase, accepted.documents, accepted.revisions, accepted.removals, accepted.roomVersion, options.modelBinding) : retainedBase;
|
|
836
|
+
if (applied === null) return false;
|
|
837
|
+
await local.publish(applied.record);
|
|
838
|
+
await persistQueue(local, room.locator, remaining);
|
|
839
|
+
outcomes.set(submitted.id, {
|
|
840
|
+
kind: 'accepted',
|
|
841
|
+
roomVersion: accepted.roomVersion,
|
|
842
|
+
...accepted.author === undefined ? {} : {
|
|
843
|
+
author: accepted.author
|
|
844
|
+
},
|
|
845
|
+
...accepted.time === undefined ? {} : {
|
|
846
|
+
time: accepted.time
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
unreadable.delete(room.locator);
|
|
850
|
+
replicas.set(room.locator, replicaOf(applied, remaining));
|
|
851
|
+
list(room, true);
|
|
852
|
+
emitRooms();
|
|
853
|
+
emitQueue(room.locator);
|
|
854
|
+
emitVisible();
|
|
855
|
+
journal.record(`outcome ${room.locator} ${submitted.id} accepted ${accepted.roomVersion}` + (accepted.author === undefined ? '' : ` author ${accepted.author}`) + (accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
856
|
+
return true;
|
|
778
857
|
}
|
|
779
858
|
async function submitRoom(room, local, authorized, progress) {
|
|
780
859
|
while(!closed && authorization !== undefined){
|
|
781
860
|
const replica = replicas.get(room.locator);
|
|
782
861
|
const head = replica?.queue[0];
|
|
783
|
-
if (replica === undefined || head === undefined
|
|
784
|
-
|
|
862
|
+
if (replica === undefined || head === undefined) return null;
|
|
863
|
+
const held = outcomes.get(head.id);
|
|
864
|
+
if (refused(held)) return null;
|
|
865
|
+
if (held?.kind === 'retryable-refusal') {
|
|
866
|
+
if (withheldUntilReset(held, Date.now())) {
|
|
867
|
+
progress.complete = false;
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
await exclusive(async ()=>{
|
|
871
|
+
await hydrate(local);
|
|
872
|
+
const current = replicas.get(room.locator);
|
|
873
|
+
if (current === undefined || current.queue[0]?.id !== head.id) return;
|
|
874
|
+
outcomes.delete(head.id);
|
|
875
|
+
await persistQueue(local, room.locator, current.queue);
|
|
876
|
+
emitQueue(room.locator);
|
|
877
|
+
moveTo({
|
|
878
|
+
...status,
|
|
879
|
+
...attention()
|
|
880
|
+
});
|
|
881
|
+
});
|
|
785
882
|
}
|
|
786
883
|
let result;
|
|
787
884
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
@@ -817,23 +914,22 @@ export function createMdRuntime(options) {
|
|
|
817
914
|
progress.complete = false;
|
|
818
915
|
return null;
|
|
819
916
|
}
|
|
820
|
-
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
821
|
-
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}`);
|
|
822
|
-
if (result.kind !== 'expired') journal.record(`refusal ${result.kind} ${result.
|
|
917
|
+
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired' || result.kind === 'retryable-refusal') {
|
|
918
|
+
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` + (result.kind === 'conflicted' && result.roomVersion !== undefined ? ` ${result.roomVersion}` : ''));
|
|
919
|
+
if (result.kind !== 'expired') journal.record(`refusal ${result.kind} ${result.code}`);
|
|
920
|
+
if (result.kind === 'retryable-refusal') progress.complete = false;
|
|
823
921
|
await exclusive(async ()=>{
|
|
824
|
-
await
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
attentionRequired: true
|
|
836
|
-
});
|
|
922
|
+
await hydrate(local);
|
|
923
|
+
const current = replicas.get(room.locator);
|
|
924
|
+
if (current === undefined || !current.queue.some((batch)=>batch.id === head.id)) {
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
outcomes.set(head.id, recordRefusal(result));
|
|
928
|
+
await persistQueue(local, room.locator, current.queue);
|
|
929
|
+
emitQueue(room.locator);
|
|
930
|
+
moveTo({
|
|
931
|
+
...status,
|
|
932
|
+
...attention()
|
|
837
933
|
});
|
|
838
934
|
});
|
|
839
935
|
return null;
|
|
@@ -909,69 +1005,83 @@ export function createMdRuntime(options) {
|
|
|
909
1005
|
}
|
|
910
1006
|
return null;
|
|
911
1007
|
}
|
|
912
|
-
async function retainModel(local, room, next) {
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
const
|
|
1008
|
+
async function retainModel(local, room, attached, next) {
|
|
1009
|
+
const key = modelKey(room.locator, attached);
|
|
1010
|
+
if (next.status === 'unavailable') modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
1011
|
+
const epoch = modelEpoch.get(key) ?? 0;
|
|
1012
|
+
const previous = modelOf(room.locator, attached);
|
|
916
1013
|
const heldBefore = replicas.get(room.locator)?.confirmed.record;
|
|
917
|
-
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))) return;
|
|
1014
|
+
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;
|
|
918
1015
|
const queryChanged = previous.status === 'unavailable' || next.status === 'unavailable' ? previous.status !== next.status : JSON.stringify(previous.types) !== JSON.stringify(next.types);
|
|
919
1016
|
if (next.status === 'unavailable') {
|
|
920
|
-
invalidModels.add(
|
|
921
|
-
liveModels.delete(
|
|
922
|
-
}
|
|
923
|
-
await exclusive(async ()=>
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1017
|
+
invalidModels.add(key);
|
|
1018
|
+
liveModels.delete(key);
|
|
1019
|
+
}
|
|
1020
|
+
await exclusive(async ()=>{
|
|
1021
|
+
await hydrate(local);
|
|
1022
|
+
if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0)) return;
|
|
1023
|
+
const held = replicas.get(room.locator);
|
|
1024
|
+
if (held !== undefined) {
|
|
1025
|
+
const record = {
|
|
1026
|
+
...held.confirmed.record,
|
|
1027
|
+
operations: room.operations,
|
|
1028
|
+
replica: room.replica,
|
|
1029
|
+
attachedModel: room.attachedModel,
|
|
1030
|
+
typedSelection: room.typedSelection ?? null,
|
|
1031
|
+
...attached === 'collection' ? {
|
|
933
1032
|
model: next
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
if (
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1033
|
+
} : {
|
|
1034
|
+
selectionModel: next
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
await local.publish(record);
|
|
1038
|
+
const confirmed = readConfirmed(record);
|
|
1039
|
+
if (confirmed !== null) replicas.set(room.locator, replicaOf(confirmed, held.queue));
|
|
1040
|
+
}
|
|
1041
|
+
models.set(key, next);
|
|
1042
|
+
if (next.status === 'current') {
|
|
1043
|
+
liveModels.set(key, JSON.stringify(next.snapshot));
|
|
1044
|
+
invalidModels.delete(key);
|
|
1045
|
+
}
|
|
1046
|
+
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
1047
|
+
emitRooms();
|
|
1048
|
+
if (queryChanged && held !== undefined) emitVisible();
|
|
1049
|
+
});
|
|
949
1050
|
}
|
|
950
|
-
async function acquireModel(local, room, authorized) {
|
|
951
|
-
|
|
952
|
-
|
|
1051
|
+
async function acquireModel(local, room, authorized, selection) {
|
|
1052
|
+
const attached = selection === null ? 'collection' : 'typed-selection';
|
|
1053
|
+
const key = modelKey(room.locator, attached);
|
|
1054
|
+
const declared = selection === null ? room.attachedModel : selection.attachedModel;
|
|
1055
|
+
let previous = modelOf(room.locator, attached);
|
|
1056
|
+
if (previous.status !== 'unavailable' && (declared === null || !sameModel(previous.snapshot, declared))) {
|
|
953
1057
|
previous = {
|
|
954
1058
|
status: 'unavailable',
|
|
955
1059
|
reason: 'version-changed'
|
|
956
1060
|
};
|
|
957
|
-
await retainModel(local, room, previous);
|
|
1061
|
+
await retainModel(local, room, attached, previous);
|
|
958
1062
|
}
|
|
959
|
-
const epoch = modelEpoch.get(
|
|
1063
|
+
const epoch = modelEpoch.get(key) ?? 0;
|
|
960
1064
|
let answer;
|
|
961
1065
|
try {
|
|
962
|
-
answer = await options.adapter.model(authorized, {
|
|
1066
|
+
answer = await options.adapter.model(authorized, selection === null ? {
|
|
963
1067
|
locator: room.locator,
|
|
1068
|
+
attached: 'collection',
|
|
964
1069
|
collectionRoot: room.collectionRoot,
|
|
965
|
-
model:
|
|
1070
|
+
model: declared
|
|
1071
|
+
} : {
|
|
1072
|
+
locator: room.locator,
|
|
1073
|
+
attached: 'typed-selection',
|
|
1074
|
+
roots: selection.roots,
|
|
1075
|
+
model: declared
|
|
966
1076
|
});
|
|
967
1077
|
} catch {
|
|
968
1078
|
answer = {
|
|
969
1079
|
kind: 'transport-uncertainty'
|
|
970
1080
|
};
|
|
971
1081
|
}
|
|
972
|
-
if (epoch !== (modelEpoch.get(
|
|
1082
|
+
if (epoch !== (modelEpoch.get(key) ?? 0)) return;
|
|
973
1083
|
if (answer.kind === 'revoked-authorization') {
|
|
974
|
-
await retainModel(local, room, {
|
|
1084
|
+
await retainModel(local, room, attached, {
|
|
975
1085
|
status: 'unavailable',
|
|
976
1086
|
reason: 'revoked'
|
|
977
1087
|
});
|
|
@@ -979,32 +1089,32 @@ export function createMdRuntime(options) {
|
|
|
979
1089
|
return;
|
|
980
1090
|
}
|
|
981
1091
|
if (answer.kind === 'transport-uncertainty') {
|
|
982
|
-
await retainModel(local, room, cached(previous));
|
|
1092
|
+
await retainModel(local, room, attached, cached(previous));
|
|
983
1093
|
return;
|
|
984
1094
|
}
|
|
985
1095
|
if (answer.kind === 'model-unavailable') {
|
|
986
|
-
await retainModel(local, room, {
|
|
1096
|
+
await retainModel(local, room, attached, {
|
|
987
1097
|
status: 'unavailable',
|
|
988
1098
|
reason: answer.reason
|
|
989
1099
|
});
|
|
990
1100
|
return;
|
|
991
1101
|
}
|
|
992
|
-
if (
|
|
993
|
-
await retainModel(local, room, {
|
|
1102
|
+
if (declared === null || !sameModel(answer.snapshot, declared)) {
|
|
1103
|
+
await retainModel(local, room, attached, {
|
|
994
1104
|
status: 'unavailable',
|
|
995
1105
|
reason: 'version-changed'
|
|
996
1106
|
});
|
|
997
1107
|
return;
|
|
998
1108
|
}
|
|
999
1109
|
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())) {
|
|
1000
|
-
await retainModel(local, room, {
|
|
1110
|
+
await retainModel(local, room, attached, {
|
|
1001
1111
|
status: 'unavailable',
|
|
1002
1112
|
reason: 'invalid'
|
|
1003
1113
|
});
|
|
1004
1114
|
return;
|
|
1005
1115
|
}
|
|
1006
1116
|
const verified = options.modelBinding?.verify(answer.snapshot);
|
|
1007
|
-
await retainModel(local, room, verified?.kind === 'verified' ? {
|
|
1117
|
+
await retainModel(local, room, attached, verified?.kind === 'verified' ? {
|
|
1008
1118
|
status: 'current',
|
|
1009
1119
|
snapshot: structuredClone(answer.snapshot),
|
|
1010
1120
|
verifiedWith: MD_MODEL,
|
|
@@ -1015,13 +1125,18 @@ export function createMdRuntime(options) {
|
|
|
1015
1125
|
});
|
|
1016
1126
|
}
|
|
1017
1127
|
async function scoped(result, room, local) {
|
|
1018
|
-
|
|
1128
|
+
scopedRefusals.set(room.locator, {
|
|
1129
|
+
code: result.code,
|
|
1130
|
+
fields: result.fields
|
|
1131
|
+
});
|
|
1132
|
+
let scopedAway = room;
|
|
1133
|
+
if (result.scope === 'model') await retainModel(local, room, 'collection', {
|
|
1019
1134
|
status: 'unavailable',
|
|
1020
1135
|
reason: 'not-readable'
|
|
1021
1136
|
});
|
|
1022
1137
|
else if (result.scope !== 'operation') {
|
|
1023
1138
|
unreadable.add(room.locator);
|
|
1024
|
-
|
|
1139
|
+
scopedAway = {
|
|
1025
1140
|
...room,
|
|
1026
1141
|
replica: {
|
|
1027
1142
|
...room.replica,
|
|
@@ -1037,11 +1152,11 @@ export function createMdRuntime(options) {
|
|
|
1037
1152
|
} : {}
|
|
1038
1153
|
}
|
|
1039
1154
|
};
|
|
1040
|
-
await retainModel(local,
|
|
1041
|
-
list(unavailable, false);
|
|
1042
|
-
emitRooms();
|
|
1155
|
+
await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
|
|
1043
1156
|
}
|
|
1044
|
-
|
|
1157
|
+
list(scopedAway, scopedAway.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
1158
|
+
emitRooms();
|
|
1159
|
+
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
1045
1160
|
}
|
|
1046
1161
|
async function synchronize(discovered, local, progress) {
|
|
1047
1162
|
for (const room of discovered){
|
|
@@ -1057,8 +1172,12 @@ export function createMdRuntime(options) {
|
|
|
1057
1172
|
progress.complete = false;
|
|
1058
1173
|
continue;
|
|
1059
1174
|
}
|
|
1060
|
-
await acquireModel(local, room, authorization);
|
|
1175
|
+
await acquireModel(local, room, authorization, null);
|
|
1061
1176
|
if (discoveryPaused) return;
|
|
1177
|
+
if (room.typedSelection !== undefined) {
|
|
1178
|
+
await acquireModel(local, room, authorization, room.typedSelection);
|
|
1179
|
+
if (discoveryPaused) return;
|
|
1180
|
+
}
|
|
1062
1181
|
if (room.replica.admission !== 'admitted') {
|
|
1063
1182
|
progress.complete = false;
|
|
1064
1183
|
continue;
|
|
@@ -1114,14 +1233,22 @@ export function createMdRuntime(options) {
|
|
|
1114
1233
|
}
|
|
1115
1234
|
journal.record(`connection discover ${result.kind}`);
|
|
1116
1235
|
if (result.kind === 'transport-uncertainty') {
|
|
1117
|
-
for (const room of rooms.values())
|
|
1236
|
+
for (const room of rooms.values()){
|
|
1237
|
+
await retainModel(local, room, 'collection', cached(modelOf(room.locator)));
|
|
1238
|
+
if (room.typedSelection !== null) {
|
|
1239
|
+
await retainModel(local, room, 'typed-selection', cached(modelOf(room.locator, 'typed-selection')));
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1118
1242
|
progress.reachable = false;
|
|
1119
1243
|
notifyReady();
|
|
1120
1244
|
return;
|
|
1121
1245
|
}
|
|
1122
1246
|
if (result.kind === 'contract-unavailable') {
|
|
1123
|
-
journal.record(`connection contract unavailable ${result.
|
|
1124
|
-
refuse('missing-required-guarantee'
|
|
1247
|
+
journal.record(`connection contract unavailable ${result.code}`);
|
|
1248
|
+
refuse('missing-required-guarantee', {
|
|
1249
|
+
code: result.code,
|
|
1250
|
+
fields: result.fields
|
|
1251
|
+
});
|
|
1125
1252
|
notifyReady();
|
|
1126
1253
|
return;
|
|
1127
1254
|
}
|
|
@@ -1152,12 +1279,18 @@ export function createMdRuntime(options) {
|
|
|
1152
1279
|
].every((operation)=>Array.isArray(room.operations[operation]) && room.operations[operation].every((scope)=>typeof scope === 'string')) || room.replica === undefined || typeof room.replica.selection !== 'string' || room.replica.selection.length === 0 || ![
|
|
1153
1280
|
'admitted',
|
|
1154
1281
|
'unavailable'
|
|
1155
|
-
].includes(room.replica.admission) ||
|
|
1282
|
+
].includes(room.replica.admission) || !declaredIdentity(room.attachedModel)) {
|
|
1156
1283
|
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
1157
1284
|
refuse('missing-required-guarantee');
|
|
1158
1285
|
notifyReady();
|
|
1159
1286
|
return;
|
|
1160
1287
|
}
|
|
1288
|
+
if (!declaredSelection(room)) {
|
|
1289
|
+
journal.record(`invalid typed selection declaration ${room.locator}`);
|
|
1290
|
+
refuse('missing-required-guarantee');
|
|
1291
|
+
notifyReady();
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1161
1294
|
if (malformed || held !== undefined && held.collectionRoot !== room.collectionRoot) {
|
|
1162
1295
|
mismatchedRoots.add(room.locator);
|
|
1163
1296
|
journal.record(`collection root ${malformed ? 'invalid' : 'changed'} ${room.locator}`);
|
|
@@ -1182,11 +1315,14 @@ export function createMdRuntime(options) {
|
|
|
1182
1315
|
}
|
|
1183
1316
|
capabilities = result.capabilities;
|
|
1184
1317
|
notifyReady();
|
|
1318
|
+
if (declared.has('room-version-notifications')) observeRoomVersions();
|
|
1319
|
+
if (declared.has('provider-actions')) observeConversations();
|
|
1185
1320
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits)) {
|
|
1186
1321
|
limits = result.providerLimits;
|
|
1187
1322
|
await local.declare(limits);
|
|
1188
1323
|
}
|
|
1189
1324
|
const listedLocators = applyDiscovery(result.rooms);
|
|
1325
|
+
handSelected(result.rooms.filter((room)=>room.replica.admission === 'admitted').map((room)=>room.locator).sort());
|
|
1190
1326
|
progress.complete = [
|
|
1191
1327
|
...replicas.keys()
|
|
1192
1328
|
].every((locator)=>listedLocators.has(locator));
|
|
@@ -1244,11 +1380,80 @@ export function createMdRuntime(options) {
|
|
|
1244
1380
|
}
|
|
1245
1381
|
}
|
|
1246
1382
|
}
|
|
1247
|
-
function
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1383
|
+
function observeRoomVersions() {
|
|
1384
|
+
const path = options.adapter.roomVersions;
|
|
1385
|
+
if (path === undefined || withdrawNotifications !== undefined) return;
|
|
1386
|
+
withdrawNotifications = path.observe((notification)=>{
|
|
1387
|
+
if (closed || discoveryPaused) return;
|
|
1388
|
+
notices.set(notification.locator, notification.roomVersion);
|
|
1389
|
+
chain = chain.then(runNotified, runNotified);
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
function observeConversations() {
|
|
1393
|
+
const path = options.adapter.conversations;
|
|
1394
|
+
if (path === undefined || withdrawConversations !== undefined) return;
|
|
1395
|
+
withdrawConversations = path.observe((event)=>{
|
|
1396
|
+
conversations.receive(event);
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
function handSelected(locators) {
|
|
1400
|
+
const path = options.adapter.roomVersions;
|
|
1401
|
+
if (path === undefined || withdrawNotifications === undefined) return;
|
|
1402
|
+
if (handed !== undefined && JSON.stringify(locators) === JSON.stringify(handed)) return;
|
|
1403
|
+
handed = locators;
|
|
1404
|
+
path.select(locators);
|
|
1405
|
+
}
|
|
1406
|
+
async function runNotified() {
|
|
1407
|
+
const announced = [
|
|
1408
|
+
...notices
|
|
1409
|
+
];
|
|
1410
|
+
notices.clear();
|
|
1411
|
+
if (announced.length === 0 || closed || discoveryPaused) return;
|
|
1412
|
+
const resting = status;
|
|
1413
|
+
const progress = {
|
|
1414
|
+
complete: false,
|
|
1415
|
+
reachable: !resting.providerUnreachable
|
|
1416
|
+
};
|
|
1417
|
+
moveTo(transferring(resting));
|
|
1418
|
+
try {
|
|
1419
|
+
const local = await openLocal();
|
|
1420
|
+
for (const [locator, roomVersion] of announced){
|
|
1421
|
+
if (closed || discoveryPaused || authorization === undefined) return;
|
|
1422
|
+
if (handed?.includes(locator) !== true) continue;
|
|
1423
|
+
const room = discovered.get(locator);
|
|
1424
|
+
const held = replicas.get(locator);
|
|
1425
|
+
if (room === undefined || held === undefined) continue;
|
|
1426
|
+
if (held.confirmed.record.roomVersion === roomVersion) continue;
|
|
1427
|
+
const withheld = await advance(room, local, authorization);
|
|
1428
|
+
if (withheld === null) continue;
|
|
1429
|
+
if (withheld.kind === 'scoped-refusal') {
|
|
1430
|
+
await scoped(withheld, room, local);
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1433
|
+
if (withheld.kind === 'transport-uncertainty') {
|
|
1434
|
+
progress.reachable = false;
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
if (withheld.kind === 'room-not-readable') {
|
|
1438
|
+
unreadable.add(locator);
|
|
1439
|
+
list(room, false);
|
|
1440
|
+
emitRooms();
|
|
1441
|
+
continue;
|
|
1442
|
+
}
|
|
1443
|
+
if (withheld.kind === 'revoked-authorization') {
|
|
1444
|
+
refuse('revoked-authorization');
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
} catch {
|
|
1449
|
+
progress.reachable = false;
|
|
1450
|
+
} finally{
|
|
1451
|
+
if (!closed) {
|
|
1452
|
+
moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
|
|
1453
|
+
}
|
|
1251
1454
|
}
|
|
1455
|
+
}
|
|
1456
|
+
function enqueue() {
|
|
1252
1457
|
if (inflight) {
|
|
1253
1458
|
queued = true;
|
|
1254
1459
|
return chain;
|
|
@@ -1257,32 +1462,18 @@ export function createMdRuntime(options) {
|
|
|
1257
1462
|
chain = chain.then(run, run);
|
|
1258
1463
|
return chain;
|
|
1259
1464
|
}
|
|
1260
|
-
function startProcessing() {
|
|
1261
|
-
if (leader) {
|
|
1262
|
-
void enqueue();
|
|
1263
|
-
return;
|
|
1264
|
-
}
|
|
1265
|
-
void window.navigator.locks.request('mdruntime-sync', {
|
|
1266
|
-
signal: options.signal
|
|
1267
|
-
}, async ()=>{
|
|
1268
|
-
if (closed) return;
|
|
1269
|
-
leader = true;
|
|
1270
|
-
announce();
|
|
1271
|
-
void enqueue();
|
|
1272
|
-
await new Promise((resolve)=>{
|
|
1273
|
-
releaseLeadership = ()=>{
|
|
1274
|
-
leader = false;
|
|
1275
|
-
resolve();
|
|
1276
|
-
};
|
|
1277
|
-
});
|
|
1278
|
-
}).catch(()=>{});
|
|
1279
|
-
}
|
|
1280
1465
|
function close() {
|
|
1281
1466
|
closed = true;
|
|
1467
|
+
withdrawNotifications?.();
|
|
1468
|
+
withdrawNotifications = undefined;
|
|
1469
|
+
withdrawConversations?.();
|
|
1470
|
+
withdrawConversations = undefined;
|
|
1471
|
+
conversations.close();
|
|
1282
1472
|
stopTimer();
|
|
1283
|
-
releaseLeadership();
|
|
1284
|
-
channel?.close();
|
|
1285
1473
|
store?.close();
|
|
1474
|
+
void opening?.then((opened)=>{
|
|
1475
|
+
opened.close();
|
|
1476
|
+
}, ()=>{});
|
|
1286
1477
|
roomListeners.clear();
|
|
1287
1478
|
refusalListeners.clear();
|
|
1288
1479
|
storageLossListeners.clear();
|
|
@@ -1290,22 +1481,16 @@ export function createMdRuntime(options) {
|
|
|
1290
1481
|
querySubscriptions.clear();
|
|
1291
1482
|
queueSubscriptions.clear();
|
|
1292
1483
|
draftListeners.clear();
|
|
1484
|
+
presentationListeners.clear();
|
|
1293
1485
|
}
|
|
1294
|
-
channel?.addEventListener('message', (event)=>{
|
|
1295
|
-
const data = event.data;
|
|
1296
|
-
if (data === null || typeof data !== 'object') return;
|
|
1297
|
-
void exclusive(()=>applyRemote(data));
|
|
1298
|
-
});
|
|
1299
1486
|
options.signal?.addEventListener('abort', close, {
|
|
1300
1487
|
once: true
|
|
1301
1488
|
});
|
|
1302
1489
|
if (options.signal?.aborted) close();
|
|
1303
|
-
void openLocal().
|
|
1304
|
-
if (!leader) notifyReady();
|
|
1305
|
-
}, ()=>{
|
|
1490
|
+
void openLocal().catch(()=>{
|
|
1306
1491
|
notifyReady();
|
|
1307
1492
|
});
|
|
1308
|
-
|
|
1493
|
+
void enqueue();
|
|
1309
1494
|
return {
|
|
1310
1495
|
rooms () {
|
|
1311
1496
|
return listed();
|
|
@@ -1314,7 +1499,10 @@ export function createMdRuntime(options) {
|
|
|
1314
1499
|
return visible(request);
|
|
1315
1500
|
},
|
|
1316
1501
|
types (locator) {
|
|
1317
|
-
return
|
|
1502
|
+
return {
|
|
1503
|
+
collection: modelTypes(modelOf(locator)),
|
|
1504
|
+
typedSelection: rooms.get(locator)?.typedSelection === undefined || rooms.get(locator)?.typedSelection === null ? null : modelTypes(modelOf(locator, 'typed-selection'))
|
|
1505
|
+
};
|
|
1318
1506
|
},
|
|
1319
1507
|
syncStatus () {
|
|
1320
1508
|
return status;
|
|
@@ -1351,6 +1539,29 @@ export function createMdRuntime(options) {
|
|
|
1351
1539
|
});
|
|
1352
1540
|
}
|
|
1353
1541
|
},
|
|
1542
|
+
conversation: conversations.surface,
|
|
1543
|
+
presentation: {
|
|
1544
|
+
read (app) {
|
|
1545
|
+
return exclusive(async ()=>{
|
|
1546
|
+
await hydrate(await openLocal());
|
|
1547
|
+
return presentationOf(app);
|
|
1548
|
+
});
|
|
1549
|
+
},
|
|
1550
|
+
write (app, value) {
|
|
1551
|
+
return exclusive(async ()=>{
|
|
1552
|
+
const local = await openLocal();
|
|
1553
|
+
await hydrate(local);
|
|
1554
|
+
const stored = structuredClone(value);
|
|
1555
|
+
await local.present({
|
|
1556
|
+
app,
|
|
1557
|
+
value: stored
|
|
1558
|
+
});
|
|
1559
|
+
presentations.set(app, stored);
|
|
1560
|
+
}).then(()=>{
|
|
1561
|
+
emitPresentation(app);
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
},
|
|
1354
1565
|
save (locator, changes, replacing) {
|
|
1355
1566
|
return exclusive(()=>write(locator, changes, replacing));
|
|
1356
1567
|
},
|
|
@@ -1381,6 +1592,9 @@ export function createMdRuntime(options) {
|
|
|
1381
1592
|
storageLoss () {
|
|
1382
1593
|
return storageLoss;
|
|
1383
1594
|
},
|
|
1595
|
+
persistence () {
|
|
1596
|
+
return options.shell.persistentStorage;
|
|
1597
|
+
},
|
|
1384
1598
|
observe (topic, second, third) {
|
|
1385
1599
|
if (topic === 'drafts') {
|
|
1386
1600
|
const notify = second;
|
|
@@ -1402,6 +1616,27 @@ export function createMdRuntime(options) {
|
|
|
1402
1616
|
draftListeners.delete(listener);
|
|
1403
1617
|
};
|
|
1404
1618
|
}
|
|
1619
|
+
if (topic === 'conversation') {
|
|
1620
|
+
return conversations.observe(second, third);
|
|
1621
|
+
}
|
|
1622
|
+
if (topic === 'presentation') {
|
|
1623
|
+
const listener = {
|
|
1624
|
+
app: second,
|
|
1625
|
+
notify: third,
|
|
1626
|
+
ready: false
|
|
1627
|
+
};
|
|
1628
|
+
presentationListeners.add(listener);
|
|
1629
|
+
void exclusive(async ()=>{
|
|
1630
|
+
await hydrate(await openLocal());
|
|
1631
|
+
}).then(()=>{
|
|
1632
|
+
if (closed || !presentationListeners.has(listener)) return;
|
|
1633
|
+
listener.ready = true;
|
|
1634
|
+
listener.notify(presentationOf(listener.app));
|
|
1635
|
+
}, ()=>{});
|
|
1636
|
+
return ()=>{
|
|
1637
|
+
presentationListeners.delete(listener);
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1405
1640
|
if (topic === 'rooms') {
|
|
1406
1641
|
const notify = second;
|
|
1407
1642
|
roomListeners.add(notify);
|