@agentshouse/mdruntime 0.1.0-alpha.7 → 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 +107 -16
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/bootstrap.d.ts +6 -1
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +14 -6
- package/dist/bootstrap.js.map +1 -1
- package/dist/change.d.ts +1 -0
- package/dist/change.d.ts.map +1 -1
- package/dist/change.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 +19 -4
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +88 -25
- package/dist/collection.js.map +1 -1
- package/dist/connection.d.ts +37 -9
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +818 -274
- 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/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 +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/memory.d.ts +16 -2
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +196 -15
- 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 +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 +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 +2 -1
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +6 -4
- package/dist/refresh.js.map +1 -1
- package/dist/replica.d.ts +5 -2
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +23 -6
- package/dist/replica.js.map +1 -1
- package/dist/save.d.ts +6 -1
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +52 -11
- 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 +38 -4
- 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
|
@@ -1,18 +1,36 @@
|
|
|
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
4
|
import { bootstrapRoom } from './bootstrap.js';
|
|
4
|
-
import {
|
|
5
|
+
import { createConversationStore } from './conversation.js';
|
|
6
|
+
import { MD_MODEL, PRESENTED, validRoot } from './collection.js';
|
|
5
7
|
import { createLog } from './log.js';
|
|
6
|
-
import { queuedOutcome, refused } from './outcome.js';
|
|
8
|
+
import { queuedOutcome, refusalOf, refused, withheldUntilReset } from './outcome.js';
|
|
7
9
|
import { refreshRoom } from './refresh.js';
|
|
8
10
|
import { buildBatch } from './save.js';
|
|
9
|
-
import { applyReturned, readConfirmed, replicaOf } from './replica.js';
|
|
11
|
+
import { applyReturned, compositionOf, readConfirmed, replicaOf } from './replica.js';
|
|
10
12
|
import { settled, same, saving, STARTING, transferring } from './status.js';
|
|
11
13
|
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
12
|
-
import {
|
|
14
|
+
import { beneath } from './paths.js';
|
|
15
|
+
import { openRuntimeStore } from './store.js';
|
|
13
16
|
import { unconfirmed } from './transfer.js';
|
|
14
17
|
const DEFAULT_DISCOVERY_INTERVAL_MS = 60_000;
|
|
15
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
|
+
}
|
|
16
34
|
export function createMdRuntime(options) {
|
|
17
35
|
const period = options.discoveryIntervalMs ?? DEFAULT_DISCOVERY_INTERVAL_MS;
|
|
18
36
|
let authorization = options.authorization;
|
|
@@ -20,17 +38,47 @@ export function createMdRuntime(options) {
|
|
|
20
38
|
let store;
|
|
21
39
|
const replicas = new Map();
|
|
22
40
|
const drafts = new Map();
|
|
41
|
+
const presentations = new Map();
|
|
23
42
|
const outcomes = new Map();
|
|
24
43
|
let limits = [];
|
|
25
44
|
let capabilities = [];
|
|
26
45
|
const rooms = new Map();
|
|
27
46
|
const unreadable = new Set();
|
|
47
|
+
const models = new Map();
|
|
48
|
+
const liveModels = new Map();
|
|
49
|
+
const invalidModels = new Set();
|
|
50
|
+
const modelEpoch = new Map();
|
|
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
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const mismatchedRoots = new Set();
|
|
28
73
|
let refusal = null;
|
|
29
74
|
let storageLoss = null;
|
|
30
75
|
let status = STARTING;
|
|
31
76
|
let discoveryPaused = false;
|
|
32
77
|
let timer;
|
|
33
78
|
let closed = false;
|
|
79
|
+
let withdrawNotifications;
|
|
80
|
+
let handed;
|
|
81
|
+
const notices = new Map();
|
|
34
82
|
let chain = Promise.resolve();
|
|
35
83
|
let mutations = Promise.resolve();
|
|
36
84
|
let inflight = false;
|
|
@@ -42,15 +90,21 @@ export function createMdRuntime(options) {
|
|
|
42
90
|
const querySubscriptions = new Set();
|
|
43
91
|
const queueSubscriptions = new Set();
|
|
44
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
|
+
});
|
|
45
103
|
const journal = createLog();
|
|
46
104
|
let notifyReady = ()=>{};
|
|
47
105
|
const ready = new Promise((resolve)=>{
|
|
48
106
|
notifyReady = resolve;
|
|
49
107
|
});
|
|
50
|
-
let leader = typeof window === 'undefined';
|
|
51
|
-
let applyingRemote = false;
|
|
52
|
-
let releaseLeadership = ()=>{};
|
|
53
|
-
const channel = typeof window !== 'undefined' ? new BroadcastChannel('mdruntime') : undefined;
|
|
54
108
|
function draftContent(locator, path) {
|
|
55
109
|
return drafts.get(locator)?.get(path);
|
|
56
110
|
}
|
|
@@ -84,9 +138,7 @@ export function createMdRuntime(options) {
|
|
|
84
138
|
return entries;
|
|
85
139
|
}
|
|
86
140
|
async function readDrafts(opened) {
|
|
87
|
-
await
|
|
88
|
-
await hydrate(opened);
|
|
89
|
-
});
|
|
141
|
+
await hydrate(opened);
|
|
90
142
|
return snapshotDrafts();
|
|
91
143
|
}
|
|
92
144
|
function publishDrafts(waitingOnly) {
|
|
@@ -104,16 +156,23 @@ export function createMdRuntime(options) {
|
|
|
104
156
|
function releaseWaitingDrafts() {
|
|
105
157
|
publishDrafts(true);
|
|
106
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
|
+
}
|
|
107
169
|
function commitDraft(work) {
|
|
108
170
|
return exclusive(async ()=>{
|
|
109
171
|
const local = await openLocal();
|
|
110
|
-
await
|
|
111
|
-
|
|
112
|
-
await work(local);
|
|
113
|
-
});
|
|
172
|
+
await hydrate(local);
|
|
173
|
+
await work(local);
|
|
114
174
|
}).then(()=>{
|
|
115
175
|
emitDrafts();
|
|
116
|
-
announce();
|
|
117
176
|
});
|
|
118
177
|
}
|
|
119
178
|
function pending() {
|
|
@@ -122,12 +181,33 @@ export function createMdRuntime(options) {
|
|
|
122
181
|
].some((replica)=>replica.queue.length > 0);
|
|
123
182
|
}
|
|
124
183
|
function attention() {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
184
|
+
const attentionRequired = [];
|
|
185
|
+
let reset = null;
|
|
186
|
+
for (const [locator, replica] of replicas){
|
|
128
187
|
const head = replica.queue[0];
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
};
|
|
131
211
|
}
|
|
132
212
|
function storedOutcomes(batches) {
|
|
133
213
|
const stored = {};
|
|
@@ -143,12 +223,11 @@ export function createMdRuntime(options) {
|
|
|
143
223
|
batches,
|
|
144
224
|
outcomes: storedOutcomes(batches)
|
|
145
225
|
});
|
|
146
|
-
announce();
|
|
147
226
|
}
|
|
148
227
|
function listed() {
|
|
149
|
-
return [
|
|
228
|
+
return structuredClone([
|
|
150
229
|
...rooms.values()
|
|
151
|
-
];
|
|
230
|
+
]);
|
|
152
231
|
}
|
|
153
232
|
function queryRooms() {
|
|
154
233
|
return [
|
|
@@ -156,7 +235,9 @@ export function createMdRuntime(options) {
|
|
|
156
235
|
].map((replica)=>({
|
|
157
236
|
room: replica.confirmed.record.locator,
|
|
158
237
|
roomVersion: replica.confirmed.record.roomVersion,
|
|
159
|
-
documents: replica.visible.collection.documents
|
|
238
|
+
documents: replica.visible.collection.documents,
|
|
239
|
+
model: modelTypes(modelOf(replica.confirmed.record.locator)),
|
|
240
|
+
typedSelection: replica.confirmed.record.typedSelection === null ? null : modelTypes(modelOf(replica.confirmed.record.locator, 'typed-selection'))
|
|
160
241
|
}));
|
|
161
242
|
}
|
|
162
243
|
function emitRooms() {
|
|
@@ -169,31 +250,17 @@ export function createMdRuntime(options) {
|
|
|
169
250
|
function emitStorageLoss() {
|
|
170
251
|
for (const listener of storageLossListeners)listener(storageLoss);
|
|
171
252
|
}
|
|
172
|
-
function
|
|
173
|
-
if (applyingRemote || channel === undefined) return;
|
|
174
|
-
if (type === 'hello') {
|
|
175
|
-
channel.postMessage({
|
|
176
|
-
type: 'hello'
|
|
177
|
-
});
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
channel.postMessage({
|
|
181
|
-
type,
|
|
182
|
-
status
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
function adopt(next) {
|
|
253
|
+
function moveTo(next) {
|
|
186
254
|
if (closed || same(status, next)) return;
|
|
187
255
|
status = next;
|
|
188
256
|
for (const listener of statusListeners)listener(status);
|
|
189
257
|
}
|
|
190
|
-
function moveTo(next) {
|
|
191
|
-
if (closed || same(status, next)) return;
|
|
192
|
-
adopt(next);
|
|
193
|
-
announce();
|
|
194
|
-
}
|
|
195
258
|
function visible(request) {
|
|
196
|
-
|
|
259
|
+
if (options.queryBinding === null) {
|
|
260
|
+
if (Object.keys(request).length > 0) throw new Error('query-binding-unavailable');
|
|
261
|
+
return rawQuery(queryRooms());
|
|
262
|
+
}
|
|
263
|
+
return options.queryBinding(queryRooms(), {
|
|
197
264
|
...request,
|
|
198
265
|
selector: {
|
|
199
266
|
$and: [
|
|
@@ -233,30 +300,89 @@ export function createMdRuntime(options) {
|
|
|
233
300
|
void enqueue();
|
|
234
301
|
}, period);
|
|
235
302
|
}
|
|
236
|
-
function refuse(reason) {
|
|
303
|
+
function refuse(reason, provider = null) {
|
|
304
|
+
if (reason === 'revoked-authorization') {
|
|
305
|
+
const revoked = {
|
|
306
|
+
status: 'unavailable',
|
|
307
|
+
reason: 'revoked'
|
|
308
|
+
};
|
|
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);
|
|
322
|
+
}
|
|
323
|
+
for (const [locator, room] of rooms)rooms.set(locator, {
|
|
324
|
+
...room,
|
|
325
|
+
model: modelTypes(revoked),
|
|
326
|
+
typedSelection: room.typedSelection === null ? null : {
|
|
327
|
+
...room.typedSelection,
|
|
328
|
+
model: modelTypes(revoked)
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
emitRooms();
|
|
332
|
+
void exclusive(async ()=>{
|
|
333
|
+
const local = await openLocal();
|
|
334
|
+
for (const record of (await local.all()))await local.publish({
|
|
335
|
+
...record,
|
|
336
|
+
model: revoked,
|
|
337
|
+
selectionModel: revoked
|
|
338
|
+
});
|
|
339
|
+
}).catch(()=>{
|
|
340
|
+
journal.record('model invalidation durable-write failure');
|
|
341
|
+
});
|
|
342
|
+
}
|
|
237
343
|
stopTimer();
|
|
238
344
|
discoveryPaused = true;
|
|
239
345
|
refusal = {
|
|
240
346
|
reason,
|
|
241
|
-
reauthorizationOffered: reason === 'revoked-authorization'
|
|
347
|
+
reauthorizationOffered: reason === 'revoked-authorization',
|
|
348
|
+
refusal: provider
|
|
242
349
|
};
|
|
243
350
|
journal.record(`connection refused ${reason}`);
|
|
244
351
|
emitRefusal();
|
|
245
352
|
}
|
|
246
353
|
function list(room, synchronizationAvailable) {
|
|
354
|
+
const selection = room.typedSelection ?? null;
|
|
247
355
|
rooms.set(room.locator, {
|
|
248
356
|
name: room.name,
|
|
357
|
+
collectionRoot: room.collectionRoot,
|
|
249
358
|
locator: room.locator,
|
|
250
|
-
|
|
359
|
+
operations: room.operations,
|
|
360
|
+
replica: room.replica,
|
|
361
|
+
attachedModel: room.attachedModel,
|
|
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
|
+
},
|
|
251
368
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
252
369
|
roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
|
|
253
|
-
synchronizationAvailable
|
|
370
|
+
synchronizationAvailable,
|
|
371
|
+
refusal: scopedRefusals.get(room.locator) ?? null
|
|
254
372
|
});
|
|
255
373
|
}
|
|
256
|
-
function applyDiscovery(
|
|
257
|
-
const listedLocators = new Set(
|
|
258
|
-
for (const
|
|
259
|
-
|
|
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){
|
|
380
|
+
const held = replicas.get(room.locator)?.confirmed.record;
|
|
381
|
+
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
382
|
+
list(held, false);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
260
386
|
}
|
|
261
387
|
for (const [locator, room] of rooms){
|
|
262
388
|
if (listedLocators.has(locator)) continue;
|
|
@@ -268,22 +394,41 @@ export function createMdRuntime(options) {
|
|
|
268
394
|
emitRooms();
|
|
269
395
|
return listedLocators;
|
|
270
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
|
+
}
|
|
271
411
|
async function hydrate(opened) {
|
|
272
|
-
const [records, queues, kept, lastCompleted, declared] = await Promise.all([
|
|
412
|
+
const [records, queues, kept, presented, cached, unsent, lastCompleted, declared] = await Promise.all([
|
|
273
413
|
opened.all(),
|
|
274
414
|
opened.queues(),
|
|
275
415
|
opened.drafts(),
|
|
416
|
+
opened.presentation(),
|
|
417
|
+
opened.conversations(),
|
|
418
|
+
opened.instructions(),
|
|
276
419
|
opened.lastCompleted(),
|
|
277
420
|
opened.providerLimits()
|
|
278
421
|
]);
|
|
279
422
|
limits = declared;
|
|
280
423
|
drafts.clear();
|
|
281
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);
|
|
282
428
|
const queuedBatches = new Map(queues.map((queue)=>[
|
|
283
429
|
queue.locator,
|
|
284
430
|
queue.batches
|
|
285
431
|
]));
|
|
286
|
-
outcomes.clear();
|
|
287
432
|
for (const queue of queues){
|
|
288
433
|
for (const [id, outcome] of Object.entries(queue.outcomes)){
|
|
289
434
|
outcomes.set(id, outcome);
|
|
@@ -291,7 +436,13 @@ export function createMdRuntime(options) {
|
|
|
291
436
|
}
|
|
292
437
|
replicas.clear();
|
|
293
438
|
for (const record of records){
|
|
294
|
-
const
|
|
439
|
+
const model = restored(modelKey(record.locator, 'collection'), record.model);
|
|
440
|
+
const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
|
|
441
|
+
const confirmed = readConfirmed({
|
|
442
|
+
...record,
|
|
443
|
+
model,
|
|
444
|
+
selectionModel
|
|
445
|
+
});
|
|
295
446
|
if (confirmed === null) continue;
|
|
296
447
|
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? []));
|
|
297
448
|
const existing = rooms.get(record.locator);
|
|
@@ -299,7 +450,16 @@ export function createMdRuntime(options) {
|
|
|
299
450
|
else {
|
|
300
451
|
rooms.set(record.locator, {
|
|
301
452
|
...existing,
|
|
302
|
-
roomVersion: record.roomVersion
|
|
453
|
+
roomVersion: record.roomVersion,
|
|
454
|
+
operations: record.operations,
|
|
455
|
+
replica: record.replica,
|
|
456
|
+
attachedModel: record.attachedModel,
|
|
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
|
+
}
|
|
303
463
|
});
|
|
304
464
|
}
|
|
305
465
|
}
|
|
@@ -321,52 +481,34 @@ export function createMdRuntime(options) {
|
|
|
321
481
|
emitDrafts();
|
|
322
482
|
return lastCompleted;
|
|
323
483
|
}
|
|
324
|
-
async function applyRemote(message) {
|
|
325
|
-
if (closed) return;
|
|
326
|
-
if (message.type === 'hello') {
|
|
327
|
-
if (leader) announce();
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
applyingRemote = true;
|
|
331
|
-
try {
|
|
332
|
-
const local = await openLocal();
|
|
333
|
-
const lastCompleted = await readState(local);
|
|
334
|
-
if (message.status !== undefined) adopt(message.status);
|
|
335
|
-
else {
|
|
336
|
-
adopt({
|
|
337
|
-
...status,
|
|
338
|
-
lastCompleted,
|
|
339
|
-
attentionRequired: attention(),
|
|
340
|
-
state: pending() ? 'pending' : status.state
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
} finally{
|
|
344
|
-
applyingRemote = false;
|
|
345
|
-
}
|
|
346
|
-
if (message.type === 'enqueue' && leader) void enqueue();
|
|
347
|
-
}
|
|
348
484
|
async function open() {
|
|
349
|
-
const opened = await
|
|
485
|
+
const opened = await openRuntimeStore();
|
|
350
486
|
store = opened;
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
}
|
|
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
|
+
}
|
|
361
497
|
const lastCompleted = await readState(opened);
|
|
362
|
-
|
|
498
|
+
moveTo({
|
|
363
499
|
...status,
|
|
364
500
|
lastCompleted,
|
|
365
|
-
|
|
501
|
+
...attention()
|
|
366
502
|
});
|
|
367
|
-
if (!leader) announce('hello');
|
|
368
503
|
return opened;
|
|
369
504
|
}
|
|
505
|
+
function durably(work) {
|
|
506
|
+
return exclusive(async ()=>{
|
|
507
|
+
const local = await openLocal();
|
|
508
|
+
await hydrate(local);
|
|
509
|
+
return work(local);
|
|
510
|
+
});
|
|
511
|
+
}
|
|
370
512
|
function openLocal() {
|
|
371
513
|
opening ??= open().catch((error)=>{
|
|
372
514
|
opening = undefined;
|
|
@@ -379,117 +521,117 @@ export function createMdRuntime(options) {
|
|
|
379
521
|
mutations = next.then(()=>undefined, ()=>undefined);
|
|
380
522
|
return next;
|
|
381
523
|
}
|
|
382
|
-
function acrossTabs(name, work) {
|
|
383
|
-
if (typeof window === 'undefined') return work();
|
|
384
|
-
return window.navigator.locks.request(name, work);
|
|
385
|
-
}
|
|
386
524
|
function publish(local, room, confirmed) {
|
|
387
525
|
return exclusive(async ()=>{
|
|
388
|
-
await
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
526
|
+
await hydrate(local);
|
|
527
|
+
const retained = readConfirmed({
|
|
528
|
+
...confirmed.record,
|
|
529
|
+
model: modelOf(room.locator),
|
|
530
|
+
selectionModel: modelOf(room.locator, 'typed-selection')
|
|
393
531
|
});
|
|
532
|
+
if (retained !== null) {
|
|
533
|
+
await local.publish(retained.record);
|
|
534
|
+
unreadable.delete(room.locator);
|
|
535
|
+
replicas.set(room.locator, replicaOf(retained, replicas.get(room.locator)?.queue ?? []));
|
|
536
|
+
}
|
|
394
537
|
list(room, true);
|
|
395
538
|
emitRooms();
|
|
396
539
|
emitVisible();
|
|
397
|
-
announce();
|
|
398
540
|
});
|
|
399
541
|
}
|
|
400
542
|
async function write(locator, changes, replacing) {
|
|
401
543
|
const local = await openLocal();
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
const batches = replacing === undefined ? [
|
|
431
|
-
...replica.queue,
|
|
432
|
-
built.batch
|
|
433
|
-
] : [
|
|
434
|
-
built.batch,
|
|
435
|
-
...replica.queue.slice(1)
|
|
436
|
-
];
|
|
437
|
-
const consumed = [
|
|
438
|
-
...new Set(changes.map((change)=>change.path))
|
|
439
|
-
];
|
|
440
|
-
try {
|
|
441
|
-
await local.append({
|
|
442
|
-
locator,
|
|
443
|
-
batches,
|
|
444
|
-
outcomes: storedOutcomes(batches)
|
|
445
|
-
}, consumed);
|
|
446
|
-
} 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))) {
|
|
447
571
|
return {
|
|
448
572
|
outcome: 'refused',
|
|
449
|
-
rule: '
|
|
573
|
+
rule: 'no-refused-head'
|
|
450
574
|
};
|
|
451
575
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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 {
|
|
462
594
|
return {
|
|
463
|
-
outcome: '
|
|
464
|
-
|
|
595
|
+
outcome: 'refused',
|
|
596
|
+
rule: 'durable-write'
|
|
465
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()
|
|
466
608
|
});
|
|
467
|
-
|
|
468
|
-
return
|
|
609
|
+
void enqueue();
|
|
610
|
+
return {
|
|
611
|
+
outcome: 'saved',
|
|
612
|
+
id: built.batch.id
|
|
613
|
+
};
|
|
469
614
|
}
|
|
470
615
|
async function discardHead(locator, identifier) {
|
|
471
616
|
const local = await openLocal();
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
attentionRequired: attention()
|
|
489
|
-
});
|
|
490
|
-
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()
|
|
491
633
|
});
|
|
492
|
-
|
|
634
|
+
void enqueue();
|
|
493
635
|
}
|
|
494
636
|
async function readHistory(locator, path, query = {}) {
|
|
495
637
|
if (closed || refusal !== null) return {
|
|
@@ -528,6 +670,10 @@ export function createMdRuntime(options) {
|
|
|
528
670
|
}
|
|
529
671
|
});
|
|
530
672
|
journal.record(`outcome history ${result.kind}`);
|
|
673
|
+
if (result.kind === 'scoped-refusal') {
|
|
674
|
+
const room = replicas.get(result.locator)?.confirmed.record;
|
|
675
|
+
if (room !== undefined) await scoped(result, room, await openLocal());
|
|
676
|
+
}
|
|
531
677
|
if (result.kind === 'revoked-authorization') refuse('revoked-authorization');
|
|
532
678
|
return result;
|
|
533
679
|
} catch {
|
|
@@ -537,6 +683,12 @@ export function createMdRuntime(options) {
|
|
|
537
683
|
};
|
|
538
684
|
}
|
|
539
685
|
}
|
|
686
|
+
function declaredCards() {
|
|
687
|
+
for (const capability of capabilities){
|
|
688
|
+
if (capability.conversationCards !== undefined) return capability.conversationCards;
|
|
689
|
+
}
|
|
690
|
+
return undefined;
|
|
691
|
+
}
|
|
540
692
|
function declaredAction(name) {
|
|
541
693
|
for (const capability of capabilities){
|
|
542
694
|
for (const action of capability.actions ?? []){
|
|
@@ -571,9 +723,13 @@ export function createMdRuntime(options) {
|
|
|
571
723
|
name,
|
|
572
724
|
input
|
|
573
725
|
});
|
|
726
|
+
if (result.kind === 'scoped-refusal') {
|
|
727
|
+
const room = replicas.get(result.locator)?.confirmed.record;
|
|
728
|
+
if (room !== undefined) await scoped(result, room, await openLocal());
|
|
729
|
+
}
|
|
574
730
|
if (result.kind === 'revoked-authorization') refuse('revoked-authorization');
|
|
575
731
|
journal.record(`outcome action ${result.kind}`);
|
|
576
|
-
if (result.kind === 'rejected') journal.record(`refusal rejected ${result.
|
|
732
|
+
if (result.kind === 'rejected') journal.record(`refusal rejected ${result.code}`);
|
|
577
733
|
return result;
|
|
578
734
|
} catch {
|
|
579
735
|
journal.record('outcome action transport-uncertainty');
|
|
@@ -584,7 +740,7 @@ export function createMdRuntime(options) {
|
|
|
584
740
|
}
|
|
585
741
|
async function pullRefresh(authorized, confirmed) {
|
|
586
742
|
try {
|
|
587
|
-
const result = await refreshRoom(options.adapter, authorized, confirmed);
|
|
743
|
+
const result = await refreshRoom(options.adapter, authorized, confirmed, options.modelBinding);
|
|
588
744
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
589
745
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
590
746
|
return result;
|
|
@@ -595,7 +751,10 @@ export function createMdRuntime(options) {
|
|
|
595
751
|
}
|
|
596
752
|
async function pullBootstrap(authorized, room) {
|
|
597
753
|
try {
|
|
598
|
-
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);
|
|
599
758
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
600
759
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
601
760
|
return result;
|
|
@@ -632,54 +791,94 @@ export function createMdRuntime(options) {
|
|
|
632
791
|
if (result.kind === 'expired') return {
|
|
633
792
|
kind: 'expired'
|
|
634
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
|
+
}
|
|
635
804
|
if (result.kind === 'conflicted') {
|
|
636
805
|
return {
|
|
637
806
|
kind: 'conflicted',
|
|
638
|
-
roomVersion:
|
|
807
|
+
...result.roomVersion === undefined ? {} : {
|
|
808
|
+
roomVersion: result.roomVersion
|
|
809
|
+
},
|
|
639
810
|
affected: result.affected,
|
|
640
|
-
|
|
811
|
+
code: result.code,
|
|
812
|
+
fields: result.fields
|
|
641
813
|
};
|
|
642
814
|
}
|
|
643
815
|
return {
|
|
644
816
|
kind: 'rejected',
|
|
645
817
|
affected: result.affected,
|
|
646
|
-
|
|
818
|
+
code: result.code,
|
|
819
|
+
fields: result.fields
|
|
647
820
|
};
|
|
648
821
|
}
|
|
649
822
|
async function commitAccept(room, local, submitted, accepted, base, overlay) {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
if (current === undefined || !current.queue.some((batch)=>batch.id === submitted.id)) {
|
|
654
|
-
return true;
|
|
655
|
-
}
|
|
656
|
-
const remaining = current.queue.filter((batch)=>batch.id !== submitted.id);
|
|
657
|
-
const applied = overlay ? applyReturned(base, accepted.documents, accepted.revisions, accepted.removals, accepted.roomVersion) : base;
|
|
658
|
-
if (applied === null) return false;
|
|
659
|
-
await local.publish(applied.record);
|
|
660
|
-
await persistQueue(local, room.locator, remaining);
|
|
661
|
-
outcomes.set(submitted.id, {
|
|
662
|
-
kind: 'accepted',
|
|
663
|
-
roomVersion: accepted.roomVersion,
|
|
664
|
-
author: accepted.author,
|
|
665
|
-
time: accepted.time
|
|
666
|
-
});
|
|
667
|
-
unreadable.delete(room.locator);
|
|
668
|
-
replicas.set(room.locator, replicaOf(applied, remaining));
|
|
669
|
-
list(room, true);
|
|
670
|
-
emitRooms();
|
|
671
|
-
emitQueue(room.locator);
|
|
672
|
-
emitVisible();
|
|
673
|
-
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)) {
|
|
674
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')
|
|
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
|
+
}
|
|
675
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;
|
|
676
857
|
}
|
|
677
858
|
async function submitRoom(room, local, authorized, progress) {
|
|
678
859
|
while(!closed && authorization !== undefined){
|
|
679
860
|
const replica = replicas.get(room.locator);
|
|
680
861
|
const head = replica?.queue[0];
|
|
681
|
-
if (replica === undefined || head === undefined
|
|
682
|
-
|
|
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
|
+
});
|
|
683
882
|
}
|
|
684
883
|
let result;
|
|
685
884
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
@@ -710,23 +909,27 @@ export function createMdRuntime(options) {
|
|
|
710
909
|
refuse('revoked-authorization');
|
|
711
910
|
return 'stop';
|
|
712
911
|
}
|
|
713
|
-
if (result.kind === '
|
|
714
|
-
|
|
715
|
-
|
|
912
|
+
if (result.kind === 'scoped-refusal') {
|
|
913
|
+
await scoped(result, room, local);
|
|
914
|
+
progress.complete = false;
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
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;
|
|
716
921
|
await exclusive(async ()=>{
|
|
717
|
-
await
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
attentionRequired: true
|
|
729
|
-
});
|
|
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()
|
|
730
933
|
});
|
|
731
934
|
});
|
|
732
935
|
return null;
|
|
@@ -738,6 +941,11 @@ export function createMdRuntime(options) {
|
|
|
738
941
|
refuse('revoked-authorization');
|
|
739
942
|
return 'stop';
|
|
740
943
|
}
|
|
944
|
+
if (pulled.kind === 'scoped-refusal') {
|
|
945
|
+
await scoped(pulled, room, local);
|
|
946
|
+
progress.complete = false;
|
|
947
|
+
return null;
|
|
948
|
+
}
|
|
741
949
|
if (pulled.kind === 'still-current') {
|
|
742
950
|
const committed = await exclusive(()=>commitAccept(room, local, head, result, from, true));
|
|
743
951
|
if (!committed) {
|
|
@@ -761,6 +969,11 @@ export function createMdRuntime(options) {
|
|
|
761
969
|
refuse('revoked-authorization');
|
|
762
970
|
return 'stop';
|
|
763
971
|
}
|
|
972
|
+
if (bootstrapped.kind === 'scoped-refusal') {
|
|
973
|
+
await scoped(bootstrapped, room, local);
|
|
974
|
+
progress.complete = false;
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
764
977
|
if (bootstrapped.kind === 'staged') {
|
|
765
978
|
const overlay = bootstrapped.confirmed.record.roomVersion === result.roomVersion;
|
|
766
979
|
const committed = await exclusive(()=>commitAccept(room, local, head, result, bootstrapped.confirmed, overlay));
|
|
@@ -792,15 +1005,195 @@ export function createMdRuntime(options) {
|
|
|
792
1005
|
}
|
|
793
1006
|
return null;
|
|
794
1007
|
}
|
|
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);
|
|
1013
|
+
const heldBefore = replicas.get(room.locator)?.confirmed.record;
|
|
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;
|
|
1015
|
+
const queryChanged = previous.status === 'unavailable' || next.status === 'unavailable' ? previous.status !== next.status : JSON.stringify(previous.types) !== JSON.stringify(next.types);
|
|
1016
|
+
if (next.status === 'unavailable') {
|
|
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' ? {
|
|
1032
|
+
model: next
|
|
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
|
+
});
|
|
1050
|
+
}
|
|
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))) {
|
|
1057
|
+
previous = {
|
|
1058
|
+
status: 'unavailable',
|
|
1059
|
+
reason: 'version-changed'
|
|
1060
|
+
};
|
|
1061
|
+
await retainModel(local, room, attached, previous);
|
|
1062
|
+
}
|
|
1063
|
+
const epoch = modelEpoch.get(key) ?? 0;
|
|
1064
|
+
let answer;
|
|
1065
|
+
try {
|
|
1066
|
+
answer = await options.adapter.model(authorized, selection === null ? {
|
|
1067
|
+
locator: room.locator,
|
|
1068
|
+
attached: 'collection',
|
|
1069
|
+
collectionRoot: room.collectionRoot,
|
|
1070
|
+
model: declared
|
|
1071
|
+
} : {
|
|
1072
|
+
locator: room.locator,
|
|
1073
|
+
attached: 'typed-selection',
|
|
1074
|
+
roots: selection.roots,
|
|
1075
|
+
model: declared
|
|
1076
|
+
});
|
|
1077
|
+
} catch {
|
|
1078
|
+
answer = {
|
|
1079
|
+
kind: 'transport-uncertainty'
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
if (epoch !== (modelEpoch.get(key) ?? 0)) return;
|
|
1083
|
+
if (answer.kind === 'revoked-authorization') {
|
|
1084
|
+
await retainModel(local, room, attached, {
|
|
1085
|
+
status: 'unavailable',
|
|
1086
|
+
reason: 'revoked'
|
|
1087
|
+
});
|
|
1088
|
+
refuse('revoked-authorization');
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
if (answer.kind === 'transport-uncertainty') {
|
|
1092
|
+
await retainModel(local, room, attached, cached(previous));
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
if (answer.kind === 'model-unavailable') {
|
|
1096
|
+
await retainModel(local, room, attached, {
|
|
1097
|
+
status: 'unavailable',
|
|
1098
|
+
reason: answer.reason
|
|
1099
|
+
});
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
if (declared === null || !sameModel(answer.snapshot, declared)) {
|
|
1103
|
+
await retainModel(local, room, attached, {
|
|
1104
|
+
status: 'unavailable',
|
|
1105
|
+
reason: 'version-changed'
|
|
1106
|
+
});
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
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())) {
|
|
1110
|
+
await retainModel(local, room, attached, {
|
|
1111
|
+
status: 'unavailable',
|
|
1112
|
+
reason: 'invalid'
|
|
1113
|
+
});
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
const verified = options.modelBinding?.verify(answer.snapshot);
|
|
1117
|
+
await retainModel(local, room, attached, verified?.kind === 'verified' ? {
|
|
1118
|
+
status: 'current',
|
|
1119
|
+
snapshot: structuredClone(answer.snapshot),
|
|
1120
|
+
verifiedWith: MD_MODEL,
|
|
1121
|
+
types: verified.types
|
|
1122
|
+
} : {
|
|
1123
|
+
status: 'unavailable',
|
|
1124
|
+
reason: verified === undefined ? 'binding-unavailable' : 'invalid'
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
async function scoped(result, room, local) {
|
|
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', {
|
|
1134
|
+
status: 'unavailable',
|
|
1135
|
+
reason: 'not-readable'
|
|
1136
|
+
});
|
|
1137
|
+
else if (result.scope !== 'operation') {
|
|
1138
|
+
unreadable.add(room.locator);
|
|
1139
|
+
scopedAway = {
|
|
1140
|
+
...room,
|
|
1141
|
+
replica: {
|
|
1142
|
+
...room.replica,
|
|
1143
|
+
admission: 'unavailable'
|
|
1144
|
+
},
|
|
1145
|
+
operations: {
|
|
1146
|
+
...room.operations,
|
|
1147
|
+
...result.scope === 'content' ? {
|
|
1148
|
+
read: []
|
|
1149
|
+
} : {},
|
|
1150
|
+
...result.scope === 'sync' ? {
|
|
1151
|
+
sync: []
|
|
1152
|
+
} : {}
|
|
1153
|
+
}
|
|
1154
|
+
};
|
|
1155
|
+
await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
|
|
1156
|
+
}
|
|
1157
|
+
list(scopedAway, scopedAway.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
1158
|
+
emitRooms();
|
|
1159
|
+
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
1160
|
+
}
|
|
795
1161
|
async function synchronize(discovered, local, progress) {
|
|
796
1162
|
for (const room of discovered){
|
|
797
1163
|
if (closed || authorization === undefined) {
|
|
798
1164
|
progress.complete = false;
|
|
799
1165
|
return;
|
|
800
1166
|
}
|
|
1167
|
+
const prior = replicas.get(room.locator)?.confirmed.record;
|
|
1168
|
+
if (prior !== undefined && prior.replica.selection !== room.replica.selection) {
|
|
1169
|
+
list(prior, false);
|
|
1170
|
+
emitRooms();
|
|
1171
|
+
journal.record(`selection changed ${room.locator}`);
|
|
1172
|
+
progress.complete = false;
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
1175
|
+
await acquireModel(local, room, authorization, null);
|
|
1176
|
+
if (discoveryPaused) return;
|
|
1177
|
+
if (room.typedSelection !== undefined) {
|
|
1178
|
+
await acquireModel(local, room, authorization, room.typedSelection);
|
|
1179
|
+
if (discoveryPaused) return;
|
|
1180
|
+
}
|
|
1181
|
+
if (room.replica.admission !== 'admitted') {
|
|
1182
|
+
progress.complete = false;
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1185
|
+
const held = replicas.get(room.locator);
|
|
1186
|
+
if (held !== undefined && held.confirmed.record.replica.selection !== room.replica.selection) {
|
|
1187
|
+
progress.complete = false;
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
801
1190
|
const withheld = await advance(room, local, authorization);
|
|
802
1191
|
if (withheld !== null) {
|
|
803
1192
|
progress.complete = false;
|
|
1193
|
+
if (withheld.kind === 'scoped-refusal') {
|
|
1194
|
+
await scoped(withheld, room, local);
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
804
1197
|
if (withheld.kind === 'transport-uncertainty') {
|
|
805
1198
|
progress.reachable = false;
|
|
806
1199
|
continue;
|
|
@@ -829,10 +1222,10 @@ export function createMdRuntime(options) {
|
|
|
829
1222
|
let result;
|
|
830
1223
|
try {
|
|
831
1224
|
result = await options.adapter.discover(authorization);
|
|
832
|
-
} catch
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
1225
|
+
} catch {
|
|
1226
|
+
result = {
|
|
1227
|
+
kind: 'transport-uncertainty'
|
|
1228
|
+
};
|
|
836
1229
|
}
|
|
837
1230
|
if (closed) {
|
|
838
1231
|
notifyReady();
|
|
@@ -840,10 +1233,25 @@ export function createMdRuntime(options) {
|
|
|
840
1233
|
}
|
|
841
1234
|
journal.record(`connection discover ${result.kind}`);
|
|
842
1235
|
if (result.kind === 'transport-uncertainty') {
|
|
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
|
+
}
|
|
843
1242
|
progress.reachable = false;
|
|
844
1243
|
notifyReady();
|
|
845
1244
|
return;
|
|
846
1245
|
}
|
|
1246
|
+
if (result.kind === 'contract-unavailable') {
|
|
1247
|
+
journal.record(`connection contract unavailable ${result.code}`);
|
|
1248
|
+
refuse('missing-required-guarantee', {
|
|
1249
|
+
code: result.code,
|
|
1250
|
+
fields: result.fields
|
|
1251
|
+
});
|
|
1252
|
+
notifyReady();
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
847
1255
|
if (result.kind === 'revoked-authorization') {
|
|
848
1256
|
refuse('revoked-authorization');
|
|
849
1257
|
notifyReady();
|
|
@@ -859,6 +1267,40 @@ export function createMdRuntime(options) {
|
|
|
859
1267
|
notifyReady();
|
|
860
1268
|
return;
|
|
861
1269
|
}
|
|
1270
|
+
mismatchedRoots.clear();
|
|
1271
|
+
for (const room of result.rooms){
|
|
1272
|
+
const held = replicas.get(room.locator)?.confirmed.record;
|
|
1273
|
+
const malformed = !validRoot(room.collectionRoot);
|
|
1274
|
+
if (room.operations === undefined || ![
|
|
1275
|
+
'read',
|
|
1276
|
+
'sync',
|
|
1277
|
+
'write',
|
|
1278
|
+
'delete'
|
|
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 || ![
|
|
1280
|
+
'admitted',
|
|
1281
|
+
'unavailable'
|
|
1282
|
+
].includes(room.replica.admission) || !declaredIdentity(room.attachedModel)) {
|
|
1283
|
+
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
1284
|
+
refuse('missing-required-guarantee');
|
|
1285
|
+
notifyReady();
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
if (!declaredSelection(room)) {
|
|
1289
|
+
journal.record(`invalid typed selection declaration ${room.locator}`);
|
|
1290
|
+
refuse('missing-required-guarantee');
|
|
1291
|
+
notifyReady();
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
if (malformed || held !== undefined && held.collectionRoot !== room.collectionRoot) {
|
|
1295
|
+
mismatchedRoots.add(room.locator);
|
|
1296
|
+
journal.record(`collection root ${malformed ? 'invalid' : 'changed'} ${room.locator}`);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
if (mismatchedRoots.size > 0) {
|
|
1300
|
+
refuse('missing-required-guarantee');
|
|
1301
|
+
notifyReady();
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
862
1304
|
const declared = new Set(result.capabilities.map((capability)=>capability.name));
|
|
863
1305
|
for (const required of options.requiredGuarantees){
|
|
864
1306
|
if (!knownCapabilities.has(required) || !declared.has(required)) {
|
|
@@ -873,11 +1315,14 @@ export function createMdRuntime(options) {
|
|
|
873
1315
|
}
|
|
874
1316
|
capabilities = result.capabilities;
|
|
875
1317
|
notifyReady();
|
|
1318
|
+
if (declared.has('room-version-notifications')) observeRoomVersions();
|
|
1319
|
+
if (declared.has('provider-actions')) observeConversations();
|
|
876
1320
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits)) {
|
|
877
1321
|
limits = result.providerLimits;
|
|
878
1322
|
await local.declare(limits);
|
|
879
1323
|
}
|
|
880
1324
|
const listedLocators = applyDiscovery(result.rooms);
|
|
1325
|
+
handSelected(result.rooms.filter((room)=>room.replica.admission === 'admitted').map((room)=>room.locator).sort());
|
|
881
1326
|
progress.complete = [
|
|
882
1327
|
...replicas.keys()
|
|
883
1328
|
].every((locator)=>listedLocators.has(locator));
|
|
@@ -935,11 +1380,80 @@ export function createMdRuntime(options) {
|
|
|
935
1380
|
}
|
|
936
1381
|
}
|
|
937
1382
|
}
|
|
938
|
-
function
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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
|
+
}
|
|
942
1454
|
}
|
|
1455
|
+
}
|
|
1456
|
+
function enqueue() {
|
|
943
1457
|
if (inflight) {
|
|
944
1458
|
queued = true;
|
|
945
1459
|
return chain;
|
|
@@ -948,32 +1462,18 @@ export function createMdRuntime(options) {
|
|
|
948
1462
|
chain = chain.then(run, run);
|
|
949
1463
|
return chain;
|
|
950
1464
|
}
|
|
951
|
-
function startProcessing() {
|
|
952
|
-
if (leader) {
|
|
953
|
-
void enqueue();
|
|
954
|
-
return;
|
|
955
|
-
}
|
|
956
|
-
void window.navigator.locks.request('mdruntime-sync', {
|
|
957
|
-
signal: options.signal
|
|
958
|
-
}, async ()=>{
|
|
959
|
-
if (closed) return;
|
|
960
|
-
leader = true;
|
|
961
|
-
announce();
|
|
962
|
-
void enqueue();
|
|
963
|
-
await new Promise((resolve)=>{
|
|
964
|
-
releaseLeadership = ()=>{
|
|
965
|
-
leader = false;
|
|
966
|
-
resolve();
|
|
967
|
-
};
|
|
968
|
-
});
|
|
969
|
-
}).catch(()=>{});
|
|
970
|
-
}
|
|
971
1465
|
function close() {
|
|
972
1466
|
closed = true;
|
|
1467
|
+
withdrawNotifications?.();
|
|
1468
|
+
withdrawNotifications = undefined;
|
|
1469
|
+
withdrawConversations?.();
|
|
1470
|
+
withdrawConversations = undefined;
|
|
1471
|
+
conversations.close();
|
|
973
1472
|
stopTimer();
|
|
974
|
-
releaseLeadership();
|
|
975
|
-
channel?.close();
|
|
976
1473
|
store?.close();
|
|
1474
|
+
void opening?.then((opened)=>{
|
|
1475
|
+
opened.close();
|
|
1476
|
+
}, ()=>{});
|
|
977
1477
|
roomListeners.clear();
|
|
978
1478
|
refusalListeners.clear();
|
|
979
1479
|
storageLossListeners.clear();
|
|
@@ -981,22 +1481,16 @@ export function createMdRuntime(options) {
|
|
|
981
1481
|
querySubscriptions.clear();
|
|
982
1482
|
queueSubscriptions.clear();
|
|
983
1483
|
draftListeners.clear();
|
|
1484
|
+
presentationListeners.clear();
|
|
984
1485
|
}
|
|
985
|
-
channel?.addEventListener('message', (event)=>{
|
|
986
|
-
const data = event.data;
|
|
987
|
-
if (data === null || typeof data !== 'object') return;
|
|
988
|
-
void exclusive(()=>applyRemote(data));
|
|
989
|
-
});
|
|
990
1486
|
options.signal?.addEventListener('abort', close, {
|
|
991
1487
|
once: true
|
|
992
1488
|
});
|
|
993
1489
|
if (options.signal?.aborted) close();
|
|
994
|
-
void openLocal().
|
|
995
|
-
if (!leader) notifyReady();
|
|
996
|
-
}, ()=>{
|
|
1490
|
+
void openLocal().catch(()=>{
|
|
997
1491
|
notifyReady();
|
|
998
1492
|
});
|
|
999
|
-
|
|
1493
|
+
void enqueue();
|
|
1000
1494
|
return {
|
|
1001
1495
|
rooms () {
|
|
1002
1496
|
return listed();
|
|
@@ -1005,7 +1499,10 @@ export function createMdRuntime(options) {
|
|
|
1005
1499
|
return visible(request);
|
|
1006
1500
|
},
|
|
1007
1501
|
types (locator) {
|
|
1008
|
-
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
|
+
};
|
|
1009
1506
|
},
|
|
1010
1507
|
syncStatus () {
|
|
1011
1508
|
return status;
|
|
@@ -1042,6 +1539,29 @@ export function createMdRuntime(options) {
|
|
|
1042
1539
|
});
|
|
1043
1540
|
}
|
|
1044
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
|
+
},
|
|
1045
1565
|
save (locator, changes, replacing) {
|
|
1046
1566
|
return exclusive(()=>write(locator, changes, replacing));
|
|
1047
1567
|
},
|
|
@@ -1072,6 +1592,9 @@ export function createMdRuntime(options) {
|
|
|
1072
1592
|
storageLoss () {
|
|
1073
1593
|
return storageLoss;
|
|
1074
1594
|
},
|
|
1595
|
+
persistence () {
|
|
1596
|
+
return options.shell.persistentStorage;
|
|
1597
|
+
},
|
|
1075
1598
|
observe (topic, second, third) {
|
|
1076
1599
|
if (topic === 'drafts') {
|
|
1077
1600
|
const notify = second;
|
|
@@ -1093,6 +1616,27 @@ export function createMdRuntime(options) {
|
|
|
1093
1616
|
draftListeners.delete(listener);
|
|
1094
1617
|
};
|
|
1095
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
|
+
}
|
|
1096
1640
|
if (topic === 'rooms') {
|
|
1097
1641
|
const notify = second;
|
|
1098
1642
|
roomListeners.add(notify);
|