@agentshouse/mdruntime 0.1.0-alpha.6 → 0.1.0-alpha.8
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 +40 -4
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/bootstrap.d.ts +3 -1
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +12 -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/collection.d.ts +8 -3
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +66 -28
- package/dist/collection.js.map +1 -1
- package/dist/connection.d.ts +17 -3
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +445 -37
- package/dist/connection.js.map +1 -1
- 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 +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/memory.d.ts +2 -0
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +27 -6
- 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/query.d.ts +12 -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 +3 -1
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +10 -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 +31 -9
- package/dist/save.js.map +1 -1
- package/dist/store.d.ts +7 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +1 -1
- package/dist/store.js.map +1 -1
- package/package.json +1 -1
package/dist/connection.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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 { MD_MODEL, PRESENTED } from './collection.js';
|
|
5
|
+
import { MD_MODEL, PRESENTED, validRoot } from './collection.js';
|
|
5
6
|
import { createLog } from './log.js';
|
|
6
7
|
import { queuedOutcome, refused } from './outcome.js';
|
|
7
8
|
import { refreshRoom } from './refresh.js';
|
|
@@ -25,6 +26,14 @@ export function createMdRuntime(options) {
|
|
|
25
26
|
let capabilities = [];
|
|
26
27
|
const rooms = new Map();
|
|
27
28
|
const unreadable = new Set();
|
|
29
|
+
const models = new Map();
|
|
30
|
+
const liveModels = new Map();
|
|
31
|
+
const invalidModels = new Set();
|
|
32
|
+
const modelEpoch = new Map();
|
|
33
|
+
function modelOf(locator) {
|
|
34
|
+
return models.get(locator) ?? NO_MODEL;
|
|
35
|
+
}
|
|
36
|
+
const mismatchedRoots = new Set();
|
|
28
37
|
let refusal = null;
|
|
29
38
|
let storageLoss = null;
|
|
30
39
|
let status = STARTING;
|
|
@@ -41,6 +50,7 @@ export function createMdRuntime(options) {
|
|
|
41
50
|
const statusListeners = new Set();
|
|
42
51
|
const querySubscriptions = new Set();
|
|
43
52
|
const queueSubscriptions = new Set();
|
|
53
|
+
const draftListeners = new Set();
|
|
44
54
|
const journal = createLog();
|
|
45
55
|
let notifyReady = ()=>{};
|
|
46
56
|
const ready = new Promise((resolve)=>{
|
|
@@ -50,8 +60,70 @@ export function createMdRuntime(options) {
|
|
|
50
60
|
let applyingRemote = false;
|
|
51
61
|
let releaseLeadership = ()=>{};
|
|
52
62
|
const channel = typeof window !== 'undefined' ? new BroadcastChannel('mdruntime') : undefined;
|
|
53
|
-
function
|
|
54
|
-
return
|
|
63
|
+
function draftContent(locator, path) {
|
|
64
|
+
return drafts.get(locator)?.get(path);
|
|
65
|
+
}
|
|
66
|
+
function rememberDraft(locator, path, content) {
|
|
67
|
+
const room = drafts.get(locator) ?? new Map();
|
|
68
|
+
room.set(path, content);
|
|
69
|
+
drafts.set(locator, room);
|
|
70
|
+
}
|
|
71
|
+
function forgetDraft(locator, path) {
|
|
72
|
+
const room = drafts.get(locator);
|
|
73
|
+
if (room === undefined) return;
|
|
74
|
+
room.delete(path);
|
|
75
|
+
if (room.size === 0) drafts.delete(locator);
|
|
76
|
+
}
|
|
77
|
+
function snapshotDrafts() {
|
|
78
|
+
const entries = [];
|
|
79
|
+
for (const [locator, room] of drafts){
|
|
80
|
+
for (const [path, content] of room){
|
|
81
|
+
entries.push({
|
|
82
|
+
locator,
|
|
83
|
+
path,
|
|
84
|
+
content: structuredClone(content)
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
entries.sort((left, right)=>{
|
|
89
|
+
if (left.locator !== right.locator) return left.locator < right.locator ? -1 : 1;
|
|
90
|
+
if (left.path !== right.path) return left.path < right.path ? -1 : 1;
|
|
91
|
+
return 0;
|
|
92
|
+
});
|
|
93
|
+
return entries;
|
|
94
|
+
}
|
|
95
|
+
async function readDrafts(opened) {
|
|
96
|
+
await acrossTabs('mdruntime-replica', async ()=>{
|
|
97
|
+
await hydrate(opened);
|
|
98
|
+
});
|
|
99
|
+
return snapshotDrafts();
|
|
100
|
+
}
|
|
101
|
+
function publishDrafts(waitingOnly) {
|
|
102
|
+
if (closed) return;
|
|
103
|
+
const value = snapshotDrafts();
|
|
104
|
+
for (const listener of draftListeners){
|
|
105
|
+
if (waitingOnly && listener.ready) continue;
|
|
106
|
+
listener.ready = true;
|
|
107
|
+
listener.notify(value);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function emitDrafts() {
|
|
111
|
+
publishDrafts(false);
|
|
112
|
+
}
|
|
113
|
+
function releaseWaitingDrafts() {
|
|
114
|
+
publishDrafts(true);
|
|
115
|
+
}
|
|
116
|
+
function commitDraft(work) {
|
|
117
|
+
return exclusive(async ()=>{
|
|
118
|
+
const local = await openLocal();
|
|
119
|
+
await acrossTabs('mdruntime-replica', async ()=>{
|
|
120
|
+
await hydrate(local);
|
|
121
|
+
await work(local);
|
|
122
|
+
});
|
|
123
|
+
}).then(()=>{
|
|
124
|
+
emitDrafts();
|
|
125
|
+
announce();
|
|
126
|
+
});
|
|
55
127
|
}
|
|
56
128
|
function pending() {
|
|
57
129
|
return [
|
|
@@ -83,9 +155,9 @@ export function createMdRuntime(options) {
|
|
|
83
155
|
announce();
|
|
84
156
|
}
|
|
85
157
|
function listed() {
|
|
86
|
-
return [
|
|
158
|
+
return structuredClone([
|
|
87
159
|
...rooms.values()
|
|
88
|
-
];
|
|
160
|
+
]);
|
|
89
161
|
}
|
|
90
162
|
function queryRooms() {
|
|
91
163
|
return [
|
|
@@ -93,7 +165,8 @@ export function createMdRuntime(options) {
|
|
|
93
165
|
].map((replica)=>({
|
|
94
166
|
room: replica.confirmed.record.locator,
|
|
95
167
|
roomVersion: replica.confirmed.record.roomVersion,
|
|
96
|
-
documents: replica.visible.collection.documents
|
|
168
|
+
documents: replica.visible.collection.documents,
|
|
169
|
+
model: modelTypes(modelOf(replica.confirmed.record.locator))
|
|
97
170
|
}));
|
|
98
171
|
}
|
|
99
172
|
function emitRooms() {
|
|
@@ -130,7 +203,11 @@ export function createMdRuntime(options) {
|
|
|
130
203
|
announce();
|
|
131
204
|
}
|
|
132
205
|
function visible(request) {
|
|
133
|
-
|
|
206
|
+
if (options.queryBinding === null) {
|
|
207
|
+
if (Object.keys(request).length > 0) throw new Error('query-binding-unavailable');
|
|
208
|
+
return rawQuery(queryRooms());
|
|
209
|
+
}
|
|
210
|
+
return options.queryBinding(queryRooms(), {
|
|
134
211
|
...request,
|
|
135
212
|
selector: {
|
|
136
213
|
$and: [
|
|
@@ -171,6 +248,38 @@ export function createMdRuntime(options) {
|
|
|
171
248
|
}, period);
|
|
172
249
|
}
|
|
173
250
|
function refuse(reason) {
|
|
251
|
+
if (reason === 'revoked-authorization') {
|
|
252
|
+
const revoked = {
|
|
253
|
+
status: 'unavailable',
|
|
254
|
+
reason: 'revoked'
|
|
255
|
+
};
|
|
256
|
+
for (const locator of new Set([
|
|
257
|
+
...models.keys(),
|
|
258
|
+
...rooms.keys()
|
|
259
|
+
])){
|
|
260
|
+
invalidModels.add(locator);
|
|
261
|
+
modelEpoch.set(locator, (modelEpoch.get(locator) ?? 0) + 1);
|
|
262
|
+
models.set(locator, revoked);
|
|
263
|
+
liveModels.delete(locator);
|
|
264
|
+
}
|
|
265
|
+
for (const [locator, room] of rooms)rooms.set(locator, {
|
|
266
|
+
...room,
|
|
267
|
+
model: modelTypes(revoked)
|
|
268
|
+
});
|
|
269
|
+
emitRooms();
|
|
270
|
+
void exclusive(async ()=>{
|
|
271
|
+
const local = await openLocal();
|
|
272
|
+
await acrossTabs('mdruntime-replica', async ()=>{
|
|
273
|
+
for (const record of (await local.all()))await local.publish({
|
|
274
|
+
...record,
|
|
275
|
+
model: revoked
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
announce();
|
|
279
|
+
}).catch(()=>{
|
|
280
|
+
journal.record('model invalidation durable-write failure');
|
|
281
|
+
});
|
|
282
|
+
}
|
|
174
283
|
stopTimer();
|
|
175
284
|
discoveryPaused = true;
|
|
176
285
|
refusal = {
|
|
@@ -183,8 +292,12 @@ export function createMdRuntime(options) {
|
|
|
183
292
|
function list(room, synchronizationAvailable) {
|
|
184
293
|
rooms.set(room.locator, {
|
|
185
294
|
name: room.name,
|
|
295
|
+
collectionRoot: room.collectionRoot,
|
|
186
296
|
locator: room.locator,
|
|
187
|
-
|
|
297
|
+
operations: room.operations,
|
|
298
|
+
replica: room.replica,
|
|
299
|
+
attachedModel: room.attachedModel,
|
|
300
|
+
model: modelTypes(modelOf(room.locator)),
|
|
188
301
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
189
302
|
roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
|
|
190
303
|
synchronizationAvailable
|
|
@@ -193,7 +306,12 @@ export function createMdRuntime(options) {
|
|
|
193
306
|
function applyDiscovery(discovered) {
|
|
194
307
|
const listedLocators = new Set(discovered.map((room)=>room.locator));
|
|
195
308
|
for (const room of discovered){
|
|
196
|
-
|
|
309
|
+
const held = replicas.get(room.locator)?.confirmed.record;
|
|
310
|
+
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
311
|
+
list(held, false);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
197
315
|
}
|
|
198
316
|
for (const [locator, room] of rooms){
|
|
199
317
|
if (listedLocators.has(locator)) continue;
|
|
@@ -215,7 +333,7 @@ export function createMdRuntime(options) {
|
|
|
215
333
|
]);
|
|
216
334
|
limits = declared;
|
|
217
335
|
drafts.clear();
|
|
218
|
-
for (const draft of kept)
|
|
336
|
+
for (const draft of kept)rememberDraft(draft.locator, draft.path, draft.content);
|
|
219
337
|
const queuedBatches = new Map(queues.map((queue)=>[
|
|
220
338
|
queue.locator,
|
|
221
339
|
queue.batches
|
|
@@ -228,7 +346,22 @@ export function createMdRuntime(options) {
|
|
|
228
346
|
}
|
|
229
347
|
replicas.clear();
|
|
230
348
|
for (const record of records){
|
|
231
|
-
const
|
|
349
|
+
const persisted = record.model;
|
|
350
|
+
const priorModel = models.get(record.locator);
|
|
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
|
+
}
|
|
361
|
+
const confirmed = readConfirmed({
|
|
362
|
+
...record,
|
|
363
|
+
model
|
|
364
|
+
});
|
|
232
365
|
if (confirmed === null) continue;
|
|
233
366
|
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? []));
|
|
234
367
|
const existing = rooms.get(record.locator);
|
|
@@ -236,7 +369,11 @@ export function createMdRuntime(options) {
|
|
|
236
369
|
else {
|
|
237
370
|
rooms.set(record.locator, {
|
|
238
371
|
...existing,
|
|
239
|
-
roomVersion: record.roomVersion
|
|
372
|
+
roomVersion: record.roomVersion,
|
|
373
|
+
operations: record.operations,
|
|
374
|
+
replica: record.replica,
|
|
375
|
+
attachedModel: record.attachedModel,
|
|
376
|
+
model: modelTypes(model)
|
|
240
377
|
});
|
|
241
378
|
}
|
|
242
379
|
}
|
|
@@ -255,6 +392,7 @@ export function createMdRuntime(options) {
|
|
|
255
392
|
emitVisible();
|
|
256
393
|
}
|
|
257
394
|
for (const locator of locators)emitQueue(locator);
|
|
395
|
+
emitDrafts();
|
|
258
396
|
return lastCompleted;
|
|
259
397
|
}
|
|
260
398
|
async function applyRemote(message) {
|
|
@@ -323,9 +461,14 @@ export function createMdRuntime(options) {
|
|
|
323
461
|
return exclusive(async ()=>{
|
|
324
462
|
await acrossTabs('mdruntime-replica', async ()=>{
|
|
325
463
|
await hydrate(local);
|
|
326
|
-
|
|
464
|
+
const retained = readConfirmed({
|
|
465
|
+
...confirmed.record,
|
|
466
|
+
model: modelOf(room.locator)
|
|
467
|
+
});
|
|
468
|
+
if (retained === null) return;
|
|
469
|
+
await local.publish(retained.record);
|
|
327
470
|
unreadable.delete(room.locator);
|
|
328
|
-
replicas.set(room.locator, replicaOf(
|
|
471
|
+
replicas.set(room.locator, replicaOf(retained, replicas.get(room.locator)?.queue ?? []));
|
|
329
472
|
});
|
|
330
473
|
list(room, true);
|
|
331
474
|
emitRooms();
|
|
@@ -342,9 +485,19 @@ export function createMdRuntime(options) {
|
|
|
342
485
|
if (replica === undefined || room === undefined) {
|
|
343
486
|
throw new Error(`No Local Replica for ${locator}`);
|
|
344
487
|
}
|
|
488
|
+
if (mismatchedRoots.has(locator)) return {
|
|
489
|
+
outcome: 'refused',
|
|
490
|
+
rule: 'collection-root-mismatch'
|
|
491
|
+
};
|
|
345
492
|
const built = buildBatch({
|
|
346
493
|
baseRoomVersion: replica.confirmed.record.roomVersion,
|
|
347
|
-
|
|
494
|
+
collectionRoot: replica.confirmed.record.collectionRoot,
|
|
495
|
+
operations: room.operations,
|
|
496
|
+
model: invalidModels.has(locator) ? {
|
|
497
|
+
status: 'unavailable',
|
|
498
|
+
reason: 'revoked'
|
|
499
|
+
} : modelOf(locator),
|
|
500
|
+
binding: options.modelBinding,
|
|
348
501
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
349
502
|
providerLimits: limits,
|
|
350
503
|
visible: replica.visible,
|
|
@@ -386,10 +539,11 @@ export function createMdRuntime(options) {
|
|
|
386
539
|
};
|
|
387
540
|
}
|
|
388
541
|
if (replacing !== undefined) outcomes.delete(replacing);
|
|
389
|
-
for (const path of consumed)
|
|
542
|
+
for (const path of consumed)forgetDraft(locator, path);
|
|
390
543
|
replicas.set(locator, replicaOf(replica.confirmed, batches));
|
|
391
544
|
emitQueue(locator);
|
|
392
545
|
emitVisible();
|
|
546
|
+
emitDrafts();
|
|
393
547
|
moveTo({
|
|
394
548
|
...saving(status),
|
|
395
549
|
attentionRequired: attention()
|
|
@@ -463,6 +617,10 @@ export function createMdRuntime(options) {
|
|
|
463
617
|
}
|
|
464
618
|
});
|
|
465
619
|
journal.record(`outcome history ${result.kind}`);
|
|
620
|
+
if (result.kind === 'scoped-refusal') {
|
|
621
|
+
const room = replicas.get(result.locator)?.confirmed.record;
|
|
622
|
+
if (room !== undefined) await scoped(result, room, await openLocal());
|
|
623
|
+
}
|
|
466
624
|
if (result.kind === 'revoked-authorization') refuse('revoked-authorization');
|
|
467
625
|
return result;
|
|
468
626
|
} catch {
|
|
@@ -506,6 +664,10 @@ export function createMdRuntime(options) {
|
|
|
506
664
|
name,
|
|
507
665
|
input
|
|
508
666
|
});
|
|
667
|
+
if (result.kind === 'scoped-refusal') {
|
|
668
|
+
const room = replicas.get(result.locator)?.confirmed.record;
|
|
669
|
+
if (room !== undefined) await scoped(result, room, await openLocal());
|
|
670
|
+
}
|
|
509
671
|
if (result.kind === 'revoked-authorization') refuse('revoked-authorization');
|
|
510
672
|
journal.record(`outcome action ${result.kind}`);
|
|
511
673
|
if (result.kind === 'rejected') journal.record(`refusal rejected ${result.text}`);
|
|
@@ -519,7 +681,7 @@ export function createMdRuntime(options) {
|
|
|
519
681
|
}
|
|
520
682
|
async function pullRefresh(authorized, confirmed) {
|
|
521
683
|
try {
|
|
522
|
-
const result = await refreshRoom(options.adapter, authorized, confirmed);
|
|
684
|
+
const result = await refreshRoom(options.adapter, authorized, confirmed, options.modelBinding);
|
|
523
685
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
524
686
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
525
687
|
return result;
|
|
@@ -530,7 +692,7 @@ export function createMdRuntime(options) {
|
|
|
530
692
|
}
|
|
531
693
|
async function pullBootstrap(authorized, room) {
|
|
532
694
|
try {
|
|
533
|
-
const result = await bootstrapRoom(options.adapter, authorized, room);
|
|
695
|
+
const result = await bootstrapRoom(options.adapter, authorized, room, modelOf(room.locator), options.modelBinding);
|
|
534
696
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
535
697
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
536
698
|
return result;
|
|
@@ -589,7 +751,12 @@ export function createMdRuntime(options) {
|
|
|
589
751
|
return true;
|
|
590
752
|
}
|
|
591
753
|
const remaining = current.queue.filter((batch)=>batch.id !== submitted.id);
|
|
592
|
-
const
|
|
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;
|
|
593
760
|
if (applied === null) return false;
|
|
594
761
|
await local.publish(applied.record);
|
|
595
762
|
await persistQueue(local, room.locator, remaining);
|
|
@@ -645,6 +812,11 @@ export function createMdRuntime(options) {
|
|
|
645
812
|
refuse('revoked-authorization');
|
|
646
813
|
return 'stop';
|
|
647
814
|
}
|
|
815
|
+
if (result.kind === 'scoped-refusal') {
|
|
816
|
+
await scoped(result, room, local);
|
|
817
|
+
progress.complete = false;
|
|
818
|
+
return null;
|
|
819
|
+
}
|
|
648
820
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
649
821
|
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}`);
|
|
650
822
|
if (result.kind !== 'expired') journal.record(`refusal ${result.kind} ${result.text}`);
|
|
@@ -673,6 +845,11 @@ export function createMdRuntime(options) {
|
|
|
673
845
|
refuse('revoked-authorization');
|
|
674
846
|
return 'stop';
|
|
675
847
|
}
|
|
848
|
+
if (pulled.kind === 'scoped-refusal') {
|
|
849
|
+
await scoped(pulled, room, local);
|
|
850
|
+
progress.complete = false;
|
|
851
|
+
return null;
|
|
852
|
+
}
|
|
676
853
|
if (pulled.kind === 'still-current') {
|
|
677
854
|
const committed = await exclusive(()=>commitAccept(room, local, head, result, from, true));
|
|
678
855
|
if (!committed) {
|
|
@@ -696,6 +873,11 @@ export function createMdRuntime(options) {
|
|
|
696
873
|
refuse('revoked-authorization');
|
|
697
874
|
return 'stop';
|
|
698
875
|
}
|
|
876
|
+
if (bootstrapped.kind === 'scoped-refusal') {
|
|
877
|
+
await scoped(bootstrapped, room, local);
|
|
878
|
+
progress.complete = false;
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
699
881
|
if (bootstrapped.kind === 'staged') {
|
|
700
882
|
const overlay = bootstrapped.confirmed.record.roomVersion === result.roomVersion;
|
|
701
883
|
const committed = await exclusive(()=>commitAccept(room, local, head, result, bootstrapped.confirmed, overlay));
|
|
@@ -727,15 +909,172 @@ export function createMdRuntime(options) {
|
|
|
727
909
|
}
|
|
728
910
|
return null;
|
|
729
911
|
}
|
|
912
|
+
async function retainModel(local, room, next) {
|
|
913
|
+
if (next.status === 'unavailable') modelEpoch.set(room.locator, (modelEpoch.get(room.locator) ?? 0) + 1);
|
|
914
|
+
const epoch = modelEpoch.get(room.locator) ?? 0;
|
|
915
|
+
const previous = modelOf(room.locator);
|
|
916
|
+
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;
|
|
918
|
+
const queryChanged = previous.status === 'unavailable' || next.status === 'unavailable' ? previous.status !== next.status : JSON.stringify(previous.types) !== JSON.stringify(next.types);
|
|
919
|
+
if (next.status === 'unavailable') {
|
|
920
|
+
invalidModels.add(room.locator);
|
|
921
|
+
liveModels.delete(room.locator);
|
|
922
|
+
}
|
|
923
|
+
await exclusive(async ()=>acrossTabs('mdruntime-replica', async ()=>{
|
|
924
|
+
await hydrate(local);
|
|
925
|
+
if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(room.locator) ?? 0)) return;
|
|
926
|
+
const held = replicas.get(room.locator);
|
|
927
|
+
if (held !== undefined) {
|
|
928
|
+
const record = {
|
|
929
|
+
...held.confirmed.record,
|
|
930
|
+
operations: room.operations,
|
|
931
|
+
replica: room.replica,
|
|
932
|
+
attachedModel: room.attachedModel,
|
|
933
|
+
model: next
|
|
934
|
+
};
|
|
935
|
+
await local.publish(record);
|
|
936
|
+
const confirmed = readConfirmed(record);
|
|
937
|
+
if (confirmed !== null) replicas.set(room.locator, replicaOf(confirmed, held.queue));
|
|
938
|
+
}
|
|
939
|
+
models.set(room.locator, next);
|
|
940
|
+
if (next.status === 'current') {
|
|
941
|
+
liveModels.set(room.locator, JSON.stringify(next.snapshot));
|
|
942
|
+
invalidModels.delete(room.locator);
|
|
943
|
+
}
|
|
944
|
+
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
945
|
+
emitRooms();
|
|
946
|
+
if (queryChanged && held !== undefined) emitVisible();
|
|
947
|
+
announce();
|
|
948
|
+
}));
|
|
949
|
+
}
|
|
950
|
+
async function acquireModel(local, room, authorized) {
|
|
951
|
+
let previous = modelOf(room.locator);
|
|
952
|
+
if (previous.status !== 'unavailable' && (room.attachedModel === null || !sameModel(previous.snapshot, room.attachedModel))) {
|
|
953
|
+
previous = {
|
|
954
|
+
status: 'unavailable',
|
|
955
|
+
reason: 'version-changed'
|
|
956
|
+
};
|
|
957
|
+
await retainModel(local, room, previous);
|
|
958
|
+
}
|
|
959
|
+
const epoch = modelEpoch.get(room.locator) ?? 0;
|
|
960
|
+
let answer;
|
|
961
|
+
try {
|
|
962
|
+
answer = await options.adapter.model(authorized, {
|
|
963
|
+
locator: room.locator,
|
|
964
|
+
collectionRoot: room.collectionRoot,
|
|
965
|
+
model: room.attachedModel
|
|
966
|
+
});
|
|
967
|
+
} catch {
|
|
968
|
+
answer = {
|
|
969
|
+
kind: 'transport-uncertainty'
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
if (epoch !== (modelEpoch.get(room.locator) ?? 0)) return;
|
|
973
|
+
if (answer.kind === 'revoked-authorization') {
|
|
974
|
+
await retainModel(local, room, {
|
|
975
|
+
status: 'unavailable',
|
|
976
|
+
reason: 'revoked'
|
|
977
|
+
});
|
|
978
|
+
refuse('revoked-authorization');
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
if (answer.kind === 'transport-uncertainty') {
|
|
982
|
+
await retainModel(local, room, cached(previous));
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
if (answer.kind === 'model-unavailable') {
|
|
986
|
+
await retainModel(local, room, {
|
|
987
|
+
status: 'unavailable',
|
|
988
|
+
reason: answer.reason
|
|
989
|
+
});
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
if (room.attachedModel === null || !sameModel(answer.snapshot, room.attachedModel)) {
|
|
993
|
+
await retainModel(local, room, {
|
|
994
|
+
status: 'unavailable',
|
|
995
|
+
reason: 'version-changed'
|
|
996
|
+
});
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
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, {
|
|
1001
|
+
status: 'unavailable',
|
|
1002
|
+
reason: 'invalid'
|
|
1003
|
+
});
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
const verified = options.modelBinding?.verify(answer.snapshot);
|
|
1007
|
+
await retainModel(local, room, verified?.kind === 'verified' ? {
|
|
1008
|
+
status: 'current',
|
|
1009
|
+
snapshot: structuredClone(answer.snapshot),
|
|
1010
|
+
verifiedWith: MD_MODEL,
|
|
1011
|
+
types: verified.types
|
|
1012
|
+
} : {
|
|
1013
|
+
status: 'unavailable',
|
|
1014
|
+
reason: verified === undefined ? 'binding-unavailable' : 'invalid'
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
async function scoped(result, room, local) {
|
|
1018
|
+
if (result.scope === 'model') await retainModel(local, room, {
|
|
1019
|
+
status: 'unavailable',
|
|
1020
|
+
reason: 'not-readable'
|
|
1021
|
+
});
|
|
1022
|
+
else if (result.scope !== 'operation') {
|
|
1023
|
+
unreadable.add(room.locator);
|
|
1024
|
+
const unavailable = {
|
|
1025
|
+
...room,
|
|
1026
|
+
replica: {
|
|
1027
|
+
...room.replica,
|
|
1028
|
+
admission: 'unavailable'
|
|
1029
|
+
},
|
|
1030
|
+
operations: {
|
|
1031
|
+
...room.operations,
|
|
1032
|
+
...result.scope === 'content' ? {
|
|
1033
|
+
read: []
|
|
1034
|
+
} : {},
|
|
1035
|
+
...result.scope === 'sync' ? {
|
|
1036
|
+
sync: []
|
|
1037
|
+
} : {}
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
await retainModel(local, unavailable, modelOf(room.locator));
|
|
1041
|
+
list(unavailable, false);
|
|
1042
|
+
emitRooms();
|
|
1043
|
+
}
|
|
1044
|
+
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.text}`);
|
|
1045
|
+
}
|
|
730
1046
|
async function synchronize(discovered, local, progress) {
|
|
731
1047
|
for (const room of discovered){
|
|
732
1048
|
if (closed || authorization === undefined) {
|
|
733
1049
|
progress.complete = false;
|
|
734
1050
|
return;
|
|
735
1051
|
}
|
|
1052
|
+
const prior = replicas.get(room.locator)?.confirmed.record;
|
|
1053
|
+
if (prior !== undefined && prior.replica.selection !== room.replica.selection) {
|
|
1054
|
+
list(prior, false);
|
|
1055
|
+
emitRooms();
|
|
1056
|
+
journal.record(`selection changed ${room.locator}`);
|
|
1057
|
+
progress.complete = false;
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
await acquireModel(local, room, authorization);
|
|
1061
|
+
if (discoveryPaused) return;
|
|
1062
|
+
if (room.replica.admission !== 'admitted') {
|
|
1063
|
+
progress.complete = false;
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
const held = replicas.get(room.locator);
|
|
1067
|
+
if (held !== undefined && held.confirmed.record.replica.selection !== room.replica.selection) {
|
|
1068
|
+
progress.complete = false;
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
736
1071
|
const withheld = await advance(room, local, authorization);
|
|
737
1072
|
if (withheld !== null) {
|
|
738
1073
|
progress.complete = false;
|
|
1074
|
+
if (withheld.kind === 'scoped-refusal') {
|
|
1075
|
+
await scoped(withheld, room, local);
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
739
1078
|
if (withheld.kind === 'transport-uncertainty') {
|
|
740
1079
|
progress.reachable = false;
|
|
741
1080
|
continue;
|
|
@@ -764,10 +1103,10 @@ export function createMdRuntime(options) {
|
|
|
764
1103
|
let result;
|
|
765
1104
|
try {
|
|
766
1105
|
result = await options.adapter.discover(authorization);
|
|
767
|
-
} catch
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1106
|
+
} catch {
|
|
1107
|
+
result = {
|
|
1108
|
+
kind: 'transport-uncertainty'
|
|
1109
|
+
};
|
|
771
1110
|
}
|
|
772
1111
|
if (closed) {
|
|
773
1112
|
notifyReady();
|
|
@@ -775,10 +1114,17 @@ export function createMdRuntime(options) {
|
|
|
775
1114
|
}
|
|
776
1115
|
journal.record(`connection discover ${result.kind}`);
|
|
777
1116
|
if (result.kind === 'transport-uncertainty') {
|
|
1117
|
+
for (const room of rooms.values())await retainModel(local, room, cached(modelOf(room.locator)));
|
|
778
1118
|
progress.reachable = false;
|
|
779
1119
|
notifyReady();
|
|
780
1120
|
return;
|
|
781
1121
|
}
|
|
1122
|
+
if (result.kind === 'contract-unavailable') {
|
|
1123
|
+
journal.record(`connection contract unavailable ${result.text}`);
|
|
1124
|
+
refuse('missing-required-guarantee');
|
|
1125
|
+
notifyReady();
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
782
1128
|
if (result.kind === 'revoked-authorization') {
|
|
783
1129
|
refuse('revoked-authorization');
|
|
784
1130
|
notifyReady();
|
|
@@ -794,6 +1140,34 @@ export function createMdRuntime(options) {
|
|
|
794
1140
|
notifyReady();
|
|
795
1141
|
return;
|
|
796
1142
|
}
|
|
1143
|
+
mismatchedRoots.clear();
|
|
1144
|
+
for (const room of result.rooms){
|
|
1145
|
+
const held = replicas.get(room.locator)?.confirmed.record;
|
|
1146
|
+
const malformed = !validRoot(room.collectionRoot);
|
|
1147
|
+
if (room.operations === undefined || ![
|
|
1148
|
+
'read',
|
|
1149
|
+
'sync',
|
|
1150
|
+
'write',
|
|
1151
|
+
'delete'
|
|
1152
|
+
].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
|
+
'admitted',
|
|
1154
|
+
'unavailable'
|
|
1155
|
+
].includes(room.replica.admission) || room.attachedModel === undefined || room.attachedModel !== null && (typeof room.attachedModel.identity !== 'string' || !room.attachedModel.identity || typeof room.attachedModel.version !== 'string' || !room.attachedModel.version)) {
|
|
1156
|
+
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
1157
|
+
refuse('missing-required-guarantee');
|
|
1158
|
+
notifyReady();
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
if (malformed || held !== undefined && held.collectionRoot !== room.collectionRoot) {
|
|
1162
|
+
mismatchedRoots.add(room.locator);
|
|
1163
|
+
journal.record(`collection root ${malformed ? 'invalid' : 'changed'} ${room.locator}`);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
if (mismatchedRoots.size > 0) {
|
|
1167
|
+
refuse('missing-required-guarantee');
|
|
1168
|
+
notifyReady();
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
797
1171
|
const declared = new Set(result.capabilities.map((capability)=>capability.name));
|
|
798
1172
|
for (const required of options.requiredGuarantees){
|
|
799
1173
|
if (!knownCapabilities.has(required) || !declared.has(required)) {
|
|
@@ -915,6 +1289,7 @@ export function createMdRuntime(options) {
|
|
|
915
1289
|
statusListeners.clear();
|
|
916
1290
|
querySubscriptions.clear();
|
|
917
1291
|
queueSubscriptions.clear();
|
|
1292
|
+
draftListeners.clear();
|
|
918
1293
|
}
|
|
919
1294
|
channel?.addEventListener('message', (event)=>{
|
|
920
1295
|
const data = event.data;
|
|
@@ -939,28 +1314,41 @@ export function createMdRuntime(options) {
|
|
|
939
1314
|
return visible(request);
|
|
940
1315
|
},
|
|
941
1316
|
types (locator) {
|
|
942
|
-
return
|
|
1317
|
+
return modelTypes(modelOf(locator));
|
|
943
1318
|
},
|
|
944
1319
|
syncStatus () {
|
|
945
1320
|
return status;
|
|
946
1321
|
},
|
|
947
1322
|
draft: {
|
|
1323
|
+
list () {
|
|
1324
|
+
return exclusive(async ()=>{
|
|
1325
|
+
const local = await openLocal();
|
|
1326
|
+
return readDrafts(local);
|
|
1327
|
+
}).then((entries)=>{
|
|
1328
|
+
releaseWaitingDrafts();
|
|
1329
|
+
return entries;
|
|
1330
|
+
});
|
|
1331
|
+
},
|
|
948
1332
|
read (locator, path) {
|
|
949
|
-
|
|
1333
|
+
const content = draftContent(locator, path);
|
|
1334
|
+
return content === undefined ? null : structuredClone(content);
|
|
950
1335
|
},
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1336
|
+
replace (locator, path, content) {
|
|
1337
|
+
return commitDraft(async (local)=>{
|
|
1338
|
+
const stored = structuredClone(content);
|
|
1339
|
+
await local.keep({
|
|
1340
|
+
locator,
|
|
1341
|
+
path,
|
|
1342
|
+
content: stored
|
|
1343
|
+
});
|
|
1344
|
+
rememberDraft(locator, path, stored);
|
|
957
1345
|
});
|
|
958
|
-
drafts.set(at(locator, path), content);
|
|
959
1346
|
},
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1347
|
+
discard (locator, path) {
|
|
1348
|
+
return commitDraft(async (local)=>{
|
|
1349
|
+
await local.discard(locator, path);
|
|
1350
|
+
forgetDraft(locator, path);
|
|
1351
|
+
});
|
|
964
1352
|
}
|
|
965
1353
|
},
|
|
966
1354
|
save (locator, changes, replacing) {
|
|
@@ -994,6 +1382,26 @@ export function createMdRuntime(options) {
|
|
|
994
1382
|
return storageLoss;
|
|
995
1383
|
},
|
|
996
1384
|
observe (topic, second, third) {
|
|
1385
|
+
if (topic === 'drafts') {
|
|
1386
|
+
const notify = second;
|
|
1387
|
+
const listener = {
|
|
1388
|
+
notify,
|
|
1389
|
+
ready: false
|
|
1390
|
+
};
|
|
1391
|
+
draftListeners.add(listener);
|
|
1392
|
+
void exclusive(async ()=>{
|
|
1393
|
+
const local = await openLocal();
|
|
1394
|
+
await readDrafts(local);
|
|
1395
|
+
if (closed || !draftListeners.has(listener)) return;
|
|
1396
|
+
listener.ready = true;
|
|
1397
|
+
}).then(()=>{
|
|
1398
|
+
if (closed || !listener.ready || !draftListeners.has(listener)) return;
|
|
1399
|
+
listener.notify(snapshotDrafts());
|
|
1400
|
+
}, ()=>{});
|
|
1401
|
+
return ()=>{
|
|
1402
|
+
draftListeners.delete(listener);
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
997
1405
|
if (topic === 'rooms') {
|
|
998
1406
|
const notify = second;
|
|
999
1407
|
roomListeners.add(notify);
|