@agentshouse/mdruntime 0.1.0-c8c4b020a3ec7 → 0.1.0-ca1eff92bbe73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +62 -26
- package/README.md +10 -10
- package/dist/adapter.d.ts +81 -43
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +1 -9
- package/dist/attached-model.d.ts +11 -0
- package/dist/attached-model.d.ts.map +1 -0
- package/dist/attached-model.js +47 -0
- package/dist/bootstrap.d.ts +5 -3
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +3 -2
- package/dist/client.d.ts +3 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +3 -10
- package/dist/connection.d.ts +10 -12
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +518 -516
- package/dist/conversation.d.ts +22 -11
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +250 -105
- package/dist/host.d.ts +7 -1
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +34 -31
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/memory.d.ts +11 -5
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +84 -26
- package/dist/model.d.ts +11 -11
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +6 -3
- package/dist/observation.d.ts +2 -0
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +24 -14
- package/dist/paths.d.ts +0 -1
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +0 -16
- package/dist/query.d.ts +8 -3
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +104 -36
- package/dist/reach.d.ts +40 -0
- package/dist/reach.d.ts.map +1 -0
- package/dist/reach.js +270 -0
- package/dist/react.d.ts +32 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +62 -0
- package/dist/read-side.d.ts +4 -0
- package/dist/read-side.d.ts.map +1 -1
- package/dist/read-side.js +116 -54
- package/dist/refresh.d.ts +2 -1
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +7 -3
- package/dist/replica.d.ts +5 -5
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +14 -15
- package/dist/room-draft.d.ts +3 -0
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +6 -4
- package/dist/rows.d.ts +2 -1
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +75 -38
- package/dist/save.d.ts +1 -2
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +90 -48
- package/dist/store.d.ts +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/wire.d.ts +51 -16
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +50 -59
- package/package.json +14 -2
package/dist/connection.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { attachedModel } from './attached-model.js';
|
|
2
2
|
import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
|
|
3
|
-
import {
|
|
3
|
+
import { observing } from './observation.js';
|
|
4
4
|
import { filterOf, identityOf, planOf, } from './query.js';
|
|
5
5
|
import { createReadSide } from './read-side.js';
|
|
6
6
|
import { createSearchIndex, searchPlanOf, } from './search.js';
|
|
7
|
-
import { ROOM_CONTRACT_VERSION, VERSION_1_CAPABILITIES, } from './adapter.js';
|
|
8
7
|
import { bootstrapRoom } from './bootstrap.js';
|
|
9
8
|
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
10
9
|
import { createConversationStore, } from './conversation.js';
|
|
11
|
-
import {
|
|
10
|
+
import { parseCollection, validRoot } from './collection.js';
|
|
12
11
|
import { createLog } from './log.js';
|
|
13
12
|
import { refreshRoom } from './refresh.js';
|
|
14
13
|
import { buildBatch } from './save.js';
|
|
@@ -20,37 +19,21 @@ import { roomDraftRevision } from './identity.js';
|
|
|
20
19
|
import { beneath, normalize } from './paths.js';
|
|
21
20
|
import { openRuntimeStore, } from './store.js';
|
|
22
21
|
import { unconfirmed } from './transfer.js';
|
|
23
|
-
|
|
24
|
-
const DEFAULT_DISCOVERY_INTERVAL_MS = 60_000;
|
|
25
|
-
const EMPTY_SCHEMA = { verdict: 'accept', model: { types: {} } };
|
|
26
|
-
const knownCapabilities = new Set(VERSION_1_CAPABILITIES);
|
|
22
|
+
import { governs, holds, narrowed, planReach, } from './reach.js';
|
|
27
23
|
function within(root, path) {
|
|
28
24
|
return root === '' || beneath(root, path);
|
|
29
25
|
}
|
|
30
|
-
function declaredIdentity(model) {
|
|
31
|
-
if (model === undefined)
|
|
32
|
-
return false;
|
|
33
|
-
if (model === null)
|
|
34
|
-
return true;
|
|
35
|
-
return (typeof model.identity === 'string' &&
|
|
36
|
-
model.identity.length > 0 &&
|
|
37
|
-
typeof model.version === 'string' &&
|
|
38
|
-
model.version.length > 0);
|
|
39
|
-
}
|
|
40
26
|
function declaredSelection(room) {
|
|
41
27
|
const selection = room.typedSelection;
|
|
42
28
|
if (selection === undefined)
|
|
43
29
|
return true;
|
|
44
30
|
if (!Array.isArray(selection.roots) || selection.roots.length === 0)
|
|
45
31
|
return false;
|
|
46
|
-
if (!declaredIdentity(selection.attachedModel))
|
|
47
|
-
return false;
|
|
48
32
|
return selection.roots.every((root, index) => validRoot(root) &&
|
|
49
33
|
root !== '' &&
|
|
50
34
|
selection.roots.every((other, at) => at === index || !within(other, root)));
|
|
51
35
|
}
|
|
52
36
|
export function composeRuntime(options) {
|
|
53
|
-
const period = options.discoveryIntervalMs ?? DEFAULT_DISCOVERY_INTERVAL_MS;
|
|
54
37
|
let authorization = options.authorization;
|
|
55
38
|
let opening;
|
|
56
39
|
let store;
|
|
@@ -58,20 +41,13 @@ export function composeRuntime(options) {
|
|
|
58
41
|
const pendings = new Map();
|
|
59
42
|
const roomDrafts = new Map();
|
|
60
43
|
const presentations = new Map();
|
|
61
|
-
const wanted = new Map();
|
|
62
44
|
let limits = [];
|
|
63
45
|
let capabilities = [];
|
|
64
|
-
function rewriting() {
|
|
65
|
-
return capabilities.some((capability) => capability.name === 'move-reference-rewrites')
|
|
66
|
-
? 'provider'
|
|
67
|
-
: 'runtime';
|
|
68
|
-
}
|
|
69
46
|
const rooms = new Map();
|
|
70
47
|
const unreadable = new Set();
|
|
71
48
|
const models = new Map();
|
|
72
49
|
const liveModels = new Map();
|
|
73
50
|
const invalidModels = new Set();
|
|
74
|
-
const modelEpoch = new Map();
|
|
75
51
|
const discovered = new Map();
|
|
76
52
|
let roomsListed = false;
|
|
77
53
|
const scopedRefusals = new Map();
|
|
@@ -100,16 +76,20 @@ export function composeRuntime(options) {
|
|
|
100
76
|
let status = STARTING;
|
|
101
77
|
let shown = STARTING;
|
|
102
78
|
let discoveryPaused = false;
|
|
103
|
-
let
|
|
79
|
+
let resetTimer;
|
|
104
80
|
let closed = false;
|
|
105
81
|
let stopping = false;
|
|
106
|
-
let
|
|
107
|
-
|
|
108
|
-
|
|
82
|
+
let withdrawStream;
|
|
83
|
+
const announced = new Map();
|
|
84
|
+
let underway = null;
|
|
85
|
+
let handed = false;
|
|
86
|
+
const submitting = new Map();
|
|
87
|
+
const answering = new Map();
|
|
88
|
+
let complete = false;
|
|
89
|
+
let recorded = false;
|
|
90
|
+
const stale = new Set();
|
|
109
91
|
let chain = Promise.resolve();
|
|
110
92
|
let mutations = Promise.resolve();
|
|
111
|
-
let inflight = false;
|
|
112
|
-
let queued = false;
|
|
113
93
|
const roomListeners = new Set();
|
|
114
94
|
const refusalListeners = new Set();
|
|
115
95
|
const storageLossListeners = new Set();
|
|
@@ -120,12 +100,13 @@ export function composeRuntime(options) {
|
|
|
120
100
|
const readSide = createReadSide();
|
|
121
101
|
let withdrawConversations;
|
|
122
102
|
let currencyQueued = false;
|
|
103
|
+
let socketOpen = false;
|
|
123
104
|
let serviceMode = 'open';
|
|
124
105
|
let haltedRefusal = false;
|
|
125
106
|
let openSignals = 0;
|
|
126
107
|
const serviceModeListeners = new Set();
|
|
127
108
|
const conversations = createConversationStore({
|
|
128
|
-
|
|
109
|
+
agents: declaredAgents,
|
|
129
110
|
documents: (locator) => readSide.query(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
|
|
130
111
|
call: (name, input) => invokeAction(name, input),
|
|
131
112
|
durably,
|
|
@@ -234,11 +215,14 @@ export function composeRuntime(options) {
|
|
|
234
215
|
function listed() {
|
|
235
216
|
return structuredClone([...rooms.values()]);
|
|
236
217
|
}
|
|
237
|
-
function roomFacts() {
|
|
238
|
-
const held = [...rooms.values()]
|
|
218
|
+
function roomFacts(reach = null) {
|
|
219
|
+
const held = [...rooms.values()]
|
|
220
|
+
.filter((room) => holds(reach, room.locator))
|
|
221
|
+
.map((room) => {
|
|
222
|
+
const governed = governs(reach, room);
|
|
239
223
|
const attached = [
|
|
240
|
-
modelOf(room.locator),
|
|
241
|
-
...(
|
|
224
|
+
...(governed.collection ? [modelOf(room.locator)] : []),
|
|
225
|
+
...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
|
|
242
226
|
];
|
|
243
227
|
return {
|
|
244
228
|
locator: room.locator,
|
|
@@ -316,23 +300,32 @@ export function composeRuntime(options) {
|
|
|
316
300
|
for (const app of set.presentations)
|
|
317
301
|
emitPresentation(app);
|
|
318
302
|
conversations.changed(set);
|
|
319
|
-
const cards = declaredCards();
|
|
320
|
-
if (cards !== undefined && set.documents.some((change) => change.locator === cards.locator)) {
|
|
321
|
-
scheduleCurrency();
|
|
322
|
-
}
|
|
323
303
|
}
|
|
324
304
|
function stopTimer() {
|
|
325
|
-
if (
|
|
305
|
+
if (resetTimer === undefined)
|
|
326
306
|
return;
|
|
327
|
-
|
|
328
|
-
|
|
307
|
+
clearTimeout(resetTimer);
|
|
308
|
+
resetTimer = undefined;
|
|
329
309
|
}
|
|
330
|
-
function
|
|
331
|
-
|
|
310
|
+
function armTimer() {
|
|
311
|
+
stopTimer();
|
|
312
|
+
if (closed || stopping)
|
|
313
|
+
return;
|
|
314
|
+
let earliest = null;
|
|
315
|
+
for (const { submission } of pendings.values()) {
|
|
316
|
+
if (submission.state !== 'reset')
|
|
317
|
+
continue;
|
|
318
|
+
const at = Date.parse(submission.reset);
|
|
319
|
+
if (earliest === null || at < earliest)
|
|
320
|
+
earliest = at;
|
|
321
|
+
}
|
|
322
|
+
if (earliest === null)
|
|
332
323
|
return;
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
324
|
+
// A Save House held back goes again only once the reset its answer named has passed.
|
|
325
|
+
resetTimer = setTimeout(() => {
|
|
326
|
+
resetTimer = undefined;
|
|
327
|
+
submitAll();
|
|
328
|
+
}, Math.max(0, earliest - Date.now()));
|
|
336
329
|
}
|
|
337
330
|
async function refuse(reason, provider = null) {
|
|
338
331
|
stopTimer();
|
|
@@ -358,7 +351,6 @@ export function composeRuntime(options) {
|
|
|
358
351
|
}
|
|
359
352
|
for (const key of keys) {
|
|
360
353
|
invalidModels.add(key);
|
|
361
|
-
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
362
354
|
models.set(key, revoked);
|
|
363
355
|
liveModels.delete(key);
|
|
364
356
|
}
|
|
@@ -373,7 +365,7 @@ export function composeRuntime(options) {
|
|
|
373
365
|
};
|
|
374
366
|
const read = readConfirmed(record, before.confirmed);
|
|
375
367
|
replicas.set(locator, read.kind === 'staged'
|
|
376
|
-
? replicaOf(read.confirmed, before.queue,
|
|
368
|
+
? replicaOf(read.confirmed, before.queue, before)
|
|
377
369
|
: { ...before, confirmed: { ...before.confirmed, record } });
|
|
378
370
|
}
|
|
379
371
|
rooms.set(locator, {
|
|
@@ -404,15 +396,14 @@ export function composeRuntime(options) {
|
|
|
404
396
|
rooms.set(room.locator, {
|
|
405
397
|
name: room.name,
|
|
406
398
|
locator: room.locator,
|
|
399
|
+
handle: room.handle,
|
|
407
400
|
operations: room.operations,
|
|
408
401
|
replica: room.replica,
|
|
409
|
-
attachedModel: room.attachedModel,
|
|
410
402
|
model: modelTypes(modelOf(room.locator)),
|
|
411
403
|
typedSelection: selection === null
|
|
412
404
|
? null
|
|
413
405
|
: {
|
|
414
406
|
roots: selection.roots,
|
|
415
|
-
attachedModel: selection.attachedModel,
|
|
416
407
|
model: modelTypes(modelOf(room.locator, 'typed-selection')),
|
|
417
408
|
},
|
|
418
409
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
@@ -429,9 +420,6 @@ export function composeRuntime(options) {
|
|
|
429
420
|
discovered.clear();
|
|
430
421
|
for (const room of listed)
|
|
431
422
|
discovered.set(room.locator, room);
|
|
432
|
-
for (const locator of wanted.keys())
|
|
433
|
-
if (!listedLocators.has(locator))
|
|
434
|
-
wanted.delete(locator);
|
|
435
423
|
await dropUnlisted(listedLocators, local);
|
|
436
424
|
for (const room of listed) {
|
|
437
425
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
@@ -465,6 +453,7 @@ export function composeRuntime(options) {
|
|
|
465
453
|
rooms.delete(locator);
|
|
466
454
|
roomDrafts.delete(locator);
|
|
467
455
|
unreadable.delete(locator);
|
|
456
|
+
stale.delete(locator);
|
|
468
457
|
scopedRefusals.delete(locator);
|
|
469
458
|
sourceRefusals.delete(locator);
|
|
470
459
|
declarationRefusals.delete(locator);
|
|
@@ -473,7 +462,6 @@ export function composeRuntime(options) {
|
|
|
473
462
|
models.delete(key);
|
|
474
463
|
liveModels.delete(key);
|
|
475
464
|
invalidModels.delete(key);
|
|
476
|
-
modelEpoch.delete(key);
|
|
477
465
|
}
|
|
478
466
|
journal.record(`room unlisted ${locator}`);
|
|
479
467
|
}
|
|
@@ -486,7 +474,6 @@ export function composeRuntime(options) {
|
|
|
486
474
|
if (prior !== undefined &&
|
|
487
475
|
prior.status !== 'unavailable' &&
|
|
488
476
|
(persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
|
|
489
|
-
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
490
477
|
invalidModels.add(key);
|
|
491
478
|
}
|
|
492
479
|
const model = persisted.status === 'unavailable' ||
|
|
@@ -541,7 +528,7 @@ export function composeRuntime(options) {
|
|
|
541
528
|
record: retyped,
|
|
542
529
|
collection: parseCollection(retyped.documents, compositionOf(retyped)),
|
|
543
530
|
};
|
|
544
|
-
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [],
|
|
531
|
+
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
|
|
545
532
|
const existing = rooms.get(record.locator);
|
|
546
533
|
if (existing === undefined)
|
|
547
534
|
list(record, false);
|
|
@@ -552,13 +539,11 @@ export function composeRuntime(options) {
|
|
|
552
539
|
attachments: record.attachments,
|
|
553
540
|
operations: record.operations,
|
|
554
541
|
replica: record.replica,
|
|
555
|
-
attachedModel: record.attachedModel,
|
|
556
542
|
model: modelTypes(model),
|
|
557
543
|
typedSelection: record.typedSelection === null
|
|
558
544
|
? null
|
|
559
545
|
: {
|
|
560
546
|
roots: record.typedSelection.roots,
|
|
561
|
-
attachedModel: record.typedSelection.attachedModel,
|
|
562
547
|
model: modelTypes(selectionModel),
|
|
563
548
|
},
|
|
564
549
|
});
|
|
@@ -626,10 +611,14 @@ export function composeRuntime(options) {
|
|
|
626
611
|
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
627
612
|
}
|
|
628
613
|
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
629
|
-
const withdrawn = withdrawals(confirmed, queue, withdrawable
|
|
614
|
+
const withdrawn = withdrawals(confirmed, queue, withdrawable);
|
|
630
615
|
const kept = queue.filter((batch) => !withdrawn.includes(batch));
|
|
616
|
+
const unqueued = withdrawn.map((batch) => ({
|
|
617
|
+
batch,
|
|
618
|
+
returned: landed(batch, confirmed) ? null : { kind: 'withdrawn' },
|
|
619
|
+
}));
|
|
631
620
|
const returning = [
|
|
632
|
-
...
|
|
621
|
+
...[...unqueued].reverse(),
|
|
633
622
|
...(settling.leaving === undefined ? [] : [settling.leaving]),
|
|
634
623
|
];
|
|
635
624
|
const written = new Map();
|
|
@@ -660,44 +649,76 @@ export function composeRuntime(options) {
|
|
|
660
649
|
}
|
|
661
650
|
for (const record of written.values())
|
|
662
651
|
rememberRoomDraft(record);
|
|
663
|
-
for (const batch of
|
|
664
|
-
journal.record(`outcome ${locator} ${batch.id} withdrawn`);
|
|
665
|
-
|
|
652
|
+
for (const { batch, returned } of unqueued) {
|
|
653
|
+
journal.record(`outcome ${locator} ${batch.id} ${returned === null ? 'landed' : 'withdrawn'}`);
|
|
654
|
+
}
|
|
655
|
+
replicas.set(locator, replicaOf(confirmed, kept, before));
|
|
666
656
|
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
667
657
|
room: settling.room,
|
|
668
658
|
queue: kept.length !== (before?.queue.length ?? 0),
|
|
669
659
|
drafts: [...written.keys()],
|
|
670
660
|
});
|
|
671
661
|
}
|
|
672
|
-
function
|
|
662
|
+
function adoptModels(locator, record) {
|
|
663
|
+
for (const [attached, model] of [
|
|
664
|
+
['collection', record.model],
|
|
665
|
+
['typed-selection', record.selectionModel],
|
|
666
|
+
]) {
|
|
667
|
+
const key = modelKey(locator, attached);
|
|
668
|
+
models.set(key, model);
|
|
669
|
+
invalidModels.delete(key);
|
|
670
|
+
if (model.status === 'current')
|
|
671
|
+
liveModels.set(key, JSON.stringify(model.snapshot));
|
|
672
|
+
else if (model.status === 'unavailable')
|
|
673
|
+
liveModels.delete(key);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
function remodel(locator, answers, held) {
|
|
677
|
+
const taken = (answer, previous, attached) => {
|
|
678
|
+
if (answer === undefined)
|
|
679
|
+
return previous;
|
|
680
|
+
const { state, refused } = attachedModel(answer, previous);
|
|
681
|
+
if (refused !== null) {
|
|
682
|
+
const where = refused.location === undefined ? '' : ` ${refused.location}`;
|
|
683
|
+
journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
|
|
684
|
+
}
|
|
685
|
+
models.set(modelKey(locator, attached), state);
|
|
686
|
+
return state;
|
|
687
|
+
};
|
|
688
|
+
return {
|
|
689
|
+
collection: taken(answers?.collection, held.collection, 'collection'),
|
|
690
|
+
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
function publish(local, room, confirmed, base) {
|
|
673
694
|
return exclusive(async () => {
|
|
674
695
|
if (!continuing())
|
|
675
|
-
return;
|
|
696
|
+
return true;
|
|
676
697
|
const prior = replicas.get(room.locator);
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
throw new Error(`Cannot retype staged Room ${room.locator}`);
|
|
698
|
+
if (base !== undefined &&
|
|
699
|
+
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
700
|
+
prior.confirmed.record.revisions !== base.record.revisions)) {
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
703
|
+
adoptModels(room.locator, confirmed.record);
|
|
684
704
|
const queue = prior?.queue ?? [];
|
|
685
|
-
const roomVersion =
|
|
705
|
+
const roomVersion = confirmed.record.roomVersion;
|
|
686
706
|
const accepted = queue.filter((batch) => {
|
|
687
707
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
688
708
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
689
709
|
});
|
|
690
|
-
const set = await settle(local, room.locator, prior,
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
710
|
+
const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
|
|
711
|
+
for (const batch of accepted)
|
|
712
|
+
pendings.delete(batch.id);
|
|
713
|
+
stale.delete(room.locator);
|
|
714
|
+
if (unreadable.delete(room.locator))
|
|
715
|
+
scopedRefusals.delete(room.locator);
|
|
696
716
|
sourceRefusals.delete(room.locator);
|
|
697
717
|
declarationRefusals.delete(room.locator);
|
|
698
718
|
list(room, true);
|
|
699
719
|
publishChangeSet(set);
|
|
700
720
|
moveTo({ ...status, ...attention() });
|
|
721
|
+
return true;
|
|
701
722
|
});
|
|
702
723
|
}
|
|
703
724
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
@@ -726,7 +747,7 @@ export function composeRuntime(options) {
|
|
|
726
747
|
operations: room.operations,
|
|
727
748
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
728
749
|
providerLimits: limits,
|
|
729
|
-
|
|
750
|
+
handle: room.handle,
|
|
730
751
|
visible: replica.visible,
|
|
731
752
|
changes: parts.flatMap(({ changes }) => changes),
|
|
732
753
|
bases: parts.flatMap(({ bases }) => bases),
|
|
@@ -750,15 +771,16 @@ export function composeRuntime(options) {
|
|
|
750
771
|
return { outcome: 'refused', rule: 'durable-write' };
|
|
751
772
|
}
|
|
752
773
|
pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
|
|
774
|
+
recorded = false;
|
|
753
775
|
for (const path of consumed)
|
|
754
776
|
forgetRoomDraft(locator, path);
|
|
755
|
-
replicas.set(locator, replicaOf(replica.confirmed, batches,
|
|
777
|
+
replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
|
|
756
778
|
publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
|
|
757
779
|
queue: true,
|
|
758
780
|
drafts: consumed,
|
|
759
781
|
}));
|
|
760
782
|
moveTo({ ...saving(status), ...attention() });
|
|
761
|
-
|
|
783
|
+
submitLater(locator);
|
|
762
784
|
return { outcome: 'saved', identifier: built.batch.id };
|
|
763
785
|
}
|
|
764
786
|
async function mark(local, locator, identifier, submission, lost) {
|
|
@@ -823,10 +845,10 @@ export function composeRuntime(options) {
|
|
|
823
845
|
return { kind: 'transport-uncertainty' };
|
|
824
846
|
}
|
|
825
847
|
}
|
|
826
|
-
function
|
|
848
|
+
function declaredAgents() {
|
|
827
849
|
for (const capability of capabilities) {
|
|
828
|
-
if (capability.
|
|
829
|
-
return capability.
|
|
850
|
+
if (capability.conversationAgents !== undefined)
|
|
851
|
+
return capability.conversationAgents;
|
|
830
852
|
}
|
|
831
853
|
return undefined;
|
|
832
854
|
}
|
|
@@ -857,8 +879,6 @@ export function composeRuntime(options) {
|
|
|
857
879
|
journal.record(`transfer action ${name}`);
|
|
858
880
|
try {
|
|
859
881
|
const result = await options.adapter.action(authorization, { name, input });
|
|
860
|
-
if (result.kind === 'action' && result.at !== undefined)
|
|
861
|
-
want(result.at);
|
|
862
882
|
if (result.kind === 'scoped-refusal') {
|
|
863
883
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
864
884
|
if (room !== undefined)
|
|
@@ -890,32 +910,11 @@ export function composeRuntime(options) {
|
|
|
890
910
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
891
911
|
!options.adapter.reached(locator, roomVersion, written));
|
|
892
912
|
}
|
|
893
|
-
function
|
|
894
|
-
if (reachedBy(locator, roomVersion))
|
|
895
|
-
return;
|
|
896
|
-
const held = wanted.get(locator);
|
|
897
|
-
if (held === undefined || options.adapter.reached(locator, roomVersion, held)) {
|
|
898
|
-
wanted.set(locator, roomVersion);
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
function reachOf(locator) {
|
|
902
|
-
let target = wanted.get(locator);
|
|
903
|
-
const versions = (replicas.get(locator)?.queue ?? []).flatMap((batch) => {
|
|
904
|
-
const accepted = pendings.get(batch.id)?.accepted;
|
|
905
|
-
return accepted === undefined ? [] : [accepted];
|
|
906
|
-
});
|
|
907
|
-
for (const version of versions) {
|
|
908
|
-
if (target === undefined || options.adapter.reached(locator, version, target)) {
|
|
909
|
-
target = version;
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
return target === undefined || reachedBy(locator, target) ? undefined : target;
|
|
913
|
-
}
|
|
914
|
-
async function pullRefresh(authorized, confirmed, minPosition) {
|
|
913
|
+
async function pullRefresh(authorized, confirmed) {
|
|
915
914
|
if (!continuing())
|
|
916
915
|
return unconfirmed;
|
|
917
916
|
try {
|
|
918
|
-
const result = await refreshRoom(options.adapter, authorized, confirmed,
|
|
917
|
+
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
|
|
919
918
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
920
919
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
921
920
|
return result;
|
|
@@ -932,7 +931,7 @@ export function composeRuntime(options) {
|
|
|
932
931
|
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
933
932
|
collection: modelOf(room.locator),
|
|
934
933
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
935
|
-
}, continuing);
|
|
934
|
+
}, (models, held) => remodel(room.locator, models, held), continuing);
|
|
936
935
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
937
936
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
938
937
|
return result;
|
|
@@ -945,20 +944,28 @@ export function composeRuntime(options) {
|
|
|
945
944
|
async function advance(room, local, authorized) {
|
|
946
945
|
const held = replicas.get(room.locator);
|
|
947
946
|
if (held !== undefined) {
|
|
948
|
-
const refreshed = await pullRefresh(authorized, held.confirmed
|
|
947
|
+
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
949
948
|
if (!continuing())
|
|
950
949
|
return unconfirmed;
|
|
951
950
|
if (refreshed.kind === 'still-current') {
|
|
951
|
+
stale.delete(room.locator);
|
|
952
952
|
if (sourceRefusals.has(room.locator))
|
|
953
|
-
return
|
|
953
|
+
return null;
|
|
954
954
|
if (unreadable.delete(room.locator)) {
|
|
955
|
+
scopedRefusals.delete(room.locator);
|
|
955
956
|
list(room, true);
|
|
956
957
|
emitRooms();
|
|
957
958
|
}
|
|
958
959
|
return null;
|
|
959
960
|
}
|
|
960
961
|
if (refreshed.kind === 'advanced') {
|
|
961
|
-
await publish(local, room, refreshed.confirmed)
|
|
962
|
+
if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
|
|
963
|
+
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
964
|
+
const reached = now !== undefined &&
|
|
965
|
+
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
966
|
+
return reached ? null : advance(room, local, authorized);
|
|
967
|
+
}
|
|
968
|
+
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
962
969
|
return null;
|
|
963
970
|
}
|
|
964
971
|
if (refreshed.kind !== 'beyond-horizon')
|
|
@@ -973,7 +980,10 @@ export function composeRuntime(options) {
|
|
|
973
980
|
return null;
|
|
974
981
|
}
|
|
975
982
|
async function withhold(withheld, room, local, progress) {
|
|
976
|
-
|
|
983
|
+
if (withheld.kind === 'refused')
|
|
984
|
+
stale.delete(room.locator);
|
|
985
|
+
else
|
|
986
|
+
stale.add(room.locator);
|
|
977
987
|
if (withheld.kind === 'scoped-refusal') {
|
|
978
988
|
await scoped(withheld, room, local);
|
|
979
989
|
}
|
|
@@ -1020,44 +1030,30 @@ export function composeRuntime(options) {
|
|
|
1020
1030
|
(accepted.author === undefined ? '' : ` author ${accepted.author}`) +
|
|
1021
1031
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1022
1032
|
}
|
|
1023
|
-
async function
|
|
1024
|
-
await exclusive(async () => {
|
|
1025
|
-
if (!continuing())
|
|
1026
|
-
return;
|
|
1027
|
-
const current = replicas.get(room.locator);
|
|
1028
|
-
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
1029
|
-
return;
|
|
1030
|
-
}
|
|
1031
|
-
const state = pendings.get(submitted.id);
|
|
1032
|
-
if (state === undefined)
|
|
1033
|
-
return;
|
|
1034
|
-
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1035
|
-
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1036
|
-
logAccepted(room.locator, submitted, accepted);
|
|
1037
|
-
emitQueue(room.locator);
|
|
1038
|
-
});
|
|
1039
|
-
}
|
|
1040
|
-
async function commitAccept(room, local, submitted, accepted, base, overlay) {
|
|
1033
|
+
async function adoptAccepted(room, local, submitted, accepted) {
|
|
1041
1034
|
if (!continuing())
|
|
1042
1035
|
return unconfirmed;
|
|
1043
1036
|
const current = replicas.get(room.locator);
|
|
1044
1037
|
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
1045
1038
|
return null;
|
|
1046
1039
|
}
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1040
|
+
const held = current.confirmed.record.roomVersion;
|
|
1041
|
+
logAccepted(room.locator, submitted, accepted);
|
|
1042
|
+
const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
1043
|
+
? { kind: 'staged', confirmed: current.confirmed }
|
|
1044
|
+
: applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
1045
|
+
? accepted.roomVersion
|
|
1046
|
+
: held, accepted.roomVersion);
|
|
1047
|
+
if (applied.kind !== 'staged') {
|
|
1048
|
+
const state = pendings.get(submitted.id);
|
|
1049
|
+
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1050
|
+
if (state !== undefined)
|
|
1051
|
+
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1052
|
+
emitQueue(room.locator);
|
|
1057
1053
|
return applied;
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1054
|
+
}
|
|
1055
|
+
const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
1056
|
+
pendings.delete(submitted.id);
|
|
1061
1057
|
unreadable.delete(room.locator);
|
|
1062
1058
|
sourceRefusals.delete(room.locator);
|
|
1063
1059
|
declarationRefusals.delete(room.locator);
|
|
@@ -1066,17 +1062,8 @@ export function composeRuntime(options) {
|
|
|
1066
1062
|
moveTo({ ...status, ...attention() });
|
|
1067
1063
|
return null;
|
|
1068
1064
|
}
|
|
1069
|
-
async function
|
|
1070
|
-
const
|
|
1071
|
-
if (options.adapter.reached(room.locator, roomVersion, accepted.roomVersion)) {
|
|
1072
|
-
return committed(room, local, submitted, accepted, pulled, roomVersion === accepted.roomVersion);
|
|
1073
|
-
}
|
|
1074
|
-
if (fresh && continuing())
|
|
1075
|
-
await publish(local, room, pulled);
|
|
1076
|
-
return false;
|
|
1077
|
-
}
|
|
1078
|
-
async function committed(room, local, submitted, accepted, base, overlay) {
|
|
1079
|
-
const failed = await exclusive(() => commitAccept(room, local, submitted, accepted, base, overlay));
|
|
1065
|
+
async function adopted(room, local, submitted, accepted) {
|
|
1066
|
+
const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
|
|
1080
1067
|
if (failed === null)
|
|
1081
1068
|
return true;
|
|
1082
1069
|
if (failed.kind === 'refused' && continuing())
|
|
@@ -1089,10 +1076,8 @@ export function composeRuntime(options) {
|
|
|
1089
1076
|
const head = replica?.queue[0];
|
|
1090
1077
|
if (replica === undefined || head === undefined)
|
|
1091
1078
|
return;
|
|
1092
|
-
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1093
|
-
progress.complete = false;
|
|
1079
|
+
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1094
1080
|
return;
|
|
1095
|
-
}
|
|
1096
1081
|
const state = pendings.get(head.id);
|
|
1097
1082
|
const { submission, lost: earlier } = state ?? {
|
|
1098
1083
|
submission: { state: 'waiting' },
|
|
@@ -1120,6 +1105,7 @@ export function composeRuntime(options) {
|
|
|
1120
1105
|
return;
|
|
1121
1106
|
}
|
|
1122
1107
|
const opensBefore = openSignals;
|
|
1108
|
+
const answered = awaitingAnswer(room.locator);
|
|
1123
1109
|
try {
|
|
1124
1110
|
result = await options.adapter.mutate(authorized, {
|
|
1125
1111
|
locator: room.locator,
|
|
@@ -1128,11 +1114,14 @@ export function composeRuntime(options) {
|
|
|
1128
1114
|
}, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
|
|
1129
1115
|
}
|
|
1130
1116
|
catch {
|
|
1117
|
+
answered();
|
|
1131
1118
|
progress.complete = false;
|
|
1132
1119
|
if (continuing())
|
|
1133
1120
|
progress.reachable = false;
|
|
1134
1121
|
return;
|
|
1135
1122
|
}
|
|
1123
|
+
if (result.kind !== 'accepted' || !continuing())
|
|
1124
|
+
answered();
|
|
1136
1125
|
if (!continuing())
|
|
1137
1126
|
return;
|
|
1138
1127
|
if (result.kind === 'transport-uncertainty') {
|
|
@@ -1185,6 +1174,7 @@ export function composeRuntime(options) {
|
|
|
1185
1174
|
const reset = result.reset;
|
|
1186
1175
|
progress.complete = false;
|
|
1187
1176
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1177
|
+
armTimer();
|
|
1188
1178
|
return;
|
|
1189
1179
|
}
|
|
1190
1180
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
@@ -1197,7 +1187,8 @@ export function composeRuntime(options) {
|
|
|
1197
1187
|
return;
|
|
1198
1188
|
}
|
|
1199
1189
|
const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
1200
|
-
leaving: result.kind === 'expired' && landed(head, current.confirmed)
|
|
1190
|
+
leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
|
|
1191
|
+
(result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
|
|
1201
1192
|
? { ...left, returned: null }
|
|
1202
1193
|
: left,
|
|
1203
1194
|
});
|
|
@@ -1211,95 +1202,33 @@ export function composeRuntime(options) {
|
|
|
1211
1202
|
return withhold(withheld, room, local, progress);
|
|
1212
1203
|
continue;
|
|
1213
1204
|
}
|
|
1214
|
-
await
|
|
1215
|
-
|
|
1216
|
-
if (from === undefined)
|
|
1217
|
-
return;
|
|
1218
|
-
const pulled = await pullRefresh(authorized, from, reachOf(room.locator));
|
|
1219
|
-
if (!continuing())
|
|
1220
|
-
return;
|
|
1221
|
-
if (pulled.kind === 'revoked-authorization') {
|
|
1222
|
-
await refuse('revoked-authorization');
|
|
1223
|
-
return;
|
|
1224
|
-
}
|
|
1225
|
-
if (pulled.kind === 'scoped-refusal') {
|
|
1226
|
-
await scoped(pulled, room, local);
|
|
1227
|
-
progress.complete = false;
|
|
1228
|
-
return;
|
|
1229
|
-
}
|
|
1230
|
-
if (pulled.kind === 'still-current' || pulled.kind === 'advanced') {
|
|
1231
|
-
const fresh = pulled.kind === 'advanced';
|
|
1232
|
-
if (!(await accepting(room, local, head, result, fresh ? pulled.confirmed : from, fresh))) {
|
|
1233
|
-
progress.complete = false;
|
|
1234
|
-
return;
|
|
1235
|
-
}
|
|
1236
|
-
continue;
|
|
1237
|
-
}
|
|
1238
|
-
if (pulled.kind === 'beyond-horizon') {
|
|
1239
|
-
const bootstrapped = await pullBootstrap(authorized, room);
|
|
1240
|
-
if (!continuing())
|
|
1241
|
-
return;
|
|
1242
|
-
if (bootstrapped.kind === 'revoked-authorization') {
|
|
1243
|
-
await refuse('revoked-authorization');
|
|
1244
|
-
return;
|
|
1245
|
-
}
|
|
1246
|
-
if (bootstrapped.kind === 'scoped-refusal') {
|
|
1247
|
-
await scoped(bootstrapped, room, local);
|
|
1248
|
-
progress.complete = false;
|
|
1249
|
-
return;
|
|
1250
|
-
}
|
|
1251
|
-
if (bootstrapped.kind === 'staged') {
|
|
1252
|
-
if (!(await accepting(room, local, head, result, bootstrapped.confirmed, true))) {
|
|
1253
|
-
progress.complete = false;
|
|
1254
|
-
return;
|
|
1255
|
-
}
|
|
1256
|
-
continue;
|
|
1257
|
-
}
|
|
1205
|
+
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1206
|
+
if (!kept) {
|
|
1258
1207
|
progress.complete = false;
|
|
1259
|
-
if (bootstrapped.kind === 'transport-uncertainty') {
|
|
1260
|
-
progress.reachable = false;
|
|
1261
|
-
}
|
|
1262
|
-
else if (bootstrapped.kind === 'room-not-readable') {
|
|
1263
|
-
unreadable.add(room.locator);
|
|
1264
|
-
list(room, false);
|
|
1265
|
-
emitRooms();
|
|
1266
|
-
}
|
|
1267
|
-
else if (bootstrapped.kind === 'refused') {
|
|
1268
|
-
refusedSource(room, bootstrapped);
|
|
1269
|
-
}
|
|
1270
1208
|
return;
|
|
1271
1209
|
}
|
|
1272
|
-
progress.complete = false;
|
|
1273
|
-
if (pulled.kind === 'transport-uncertainty') {
|
|
1274
|
-
progress.reachable = false;
|
|
1275
|
-
}
|
|
1276
|
-
else if (pulled.kind === 'room-not-readable') {
|
|
1277
|
-
unreadable.add(room.locator);
|
|
1278
|
-
list(room, false);
|
|
1279
|
-
emitRooms();
|
|
1280
|
-
}
|
|
1281
|
-
else if (pulled.kind === 'retryable-refusal') {
|
|
1282
|
-
journal.record(`refusal ${pulled.kind} ${pulled.code}`);
|
|
1283
|
-
}
|
|
1284
|
-
else if (pulled.kind === 'refused') {
|
|
1285
|
-
refusedSource(room, pulled);
|
|
1286
|
-
}
|
|
1287
|
-
return;
|
|
1288
1210
|
}
|
|
1289
|
-
|
|
1211
|
+
}
|
|
1212
|
+
function awaitingAnswer(locator) {
|
|
1213
|
+
let release = () => { };
|
|
1214
|
+
const awaited = new Promise((resolve) => {
|
|
1215
|
+
release = resolve;
|
|
1216
|
+
});
|
|
1217
|
+
answering.set(locator, awaited);
|
|
1218
|
+
return () => {
|
|
1219
|
+
if (answering.get(locator) === awaited)
|
|
1220
|
+
answering.delete(locator);
|
|
1221
|
+
release();
|
|
1222
|
+
};
|
|
1290
1223
|
}
|
|
1291
1224
|
async function retainModel(local, room, attached, next) {
|
|
1292
1225
|
const key = modelKey(room.locator, attached);
|
|
1293
|
-
if (next.status === 'unavailable')
|
|
1294
|
-
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
1295
|
-
const epoch = modelEpoch.get(key) ?? 0;
|
|
1296
1226
|
const previous = modelOf(room.locator, attached);
|
|
1297
1227
|
const heldBefore = replicas.get(room.locator)?.confirmed.record;
|
|
1298
1228
|
if (JSON.stringify(previous) === JSON.stringify(next) &&
|
|
1299
1229
|
(heldBefore === undefined ||
|
|
1300
1230
|
(JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
|
|
1301
1231
|
JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
|
|
1302
|
-
JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) &&
|
|
1303
1232
|
JSON.stringify(heldBefore.typedSelection) ===
|
|
1304
1233
|
JSON.stringify(room.typedSelection ?? null))))
|
|
1305
1234
|
return;
|
|
@@ -1310,15 +1239,12 @@ export function composeRuntime(options) {
|
|
|
1310
1239
|
await exclusive(async () => {
|
|
1311
1240
|
if (!continuing())
|
|
1312
1241
|
return;
|
|
1313
|
-
if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0))
|
|
1314
|
-
return;
|
|
1315
1242
|
const held = replicas.get(room.locator);
|
|
1316
1243
|
if (held !== undefined) {
|
|
1317
1244
|
const declared = {
|
|
1318
1245
|
...held.confirmed.record,
|
|
1319
1246
|
operations: room.operations,
|
|
1320
1247
|
replica: room.replica,
|
|
1321
|
-
attachedModel: room.attachedModel,
|
|
1322
1248
|
typedSelection: room.typedSelection ?? null,
|
|
1323
1249
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
1324
1250
|
};
|
|
@@ -1338,7 +1264,7 @@ export function composeRuntime(options) {
|
|
|
1338
1264
|
queue: held.queue,
|
|
1339
1265
|
});
|
|
1340
1266
|
if (read.kind === 'staged') {
|
|
1341
|
-
replicas.set(room.locator, replicaOf(read.confirmed, held.queue,
|
|
1267
|
+
replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
|
|
1342
1268
|
if (declarationRefusals.delete(room.locator)) {
|
|
1343
1269
|
sourceRefusals.delete(room.locator);
|
|
1344
1270
|
unreadable.delete(room.locator);
|
|
@@ -1362,77 +1288,6 @@ export function composeRuntime(options) {
|
|
|
1362
1288
|
publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
|
|
1363
1289
|
});
|
|
1364
1290
|
}
|
|
1365
|
-
async function acquireModel(local, room, authorized, selection) {
|
|
1366
|
-
const attached = selection === null ? 'collection' : 'typed-selection';
|
|
1367
|
-
const key = modelKey(room.locator, attached);
|
|
1368
|
-
const declared = selection === null ? room.attachedModel : selection.attachedModel;
|
|
1369
|
-
let previous = modelOf(room.locator, attached);
|
|
1370
|
-
if (previous.status !== 'unavailable' &&
|
|
1371
|
-
(declared === null || !sameModel(previous.snapshot, declared))) {
|
|
1372
|
-
previous = { status: 'unavailable', reason: 'version-changed' };
|
|
1373
|
-
await retainModel(local, room, attached, previous);
|
|
1374
|
-
if (discoveryPaused)
|
|
1375
|
-
return;
|
|
1376
|
-
}
|
|
1377
|
-
const epoch = modelEpoch.get(key) ?? 0;
|
|
1378
|
-
let answer;
|
|
1379
|
-
try {
|
|
1380
|
-
answer = await options.adapter.model(authorized, selection === null
|
|
1381
|
-
? { locator: room.locator, attached: 'collection', model: declared }
|
|
1382
|
-
: {
|
|
1383
|
-
locator: room.locator,
|
|
1384
|
-
attached: 'typed-selection',
|
|
1385
|
-
roots: selection.roots,
|
|
1386
|
-
model: declared,
|
|
1387
|
-
});
|
|
1388
|
-
}
|
|
1389
|
-
catch {
|
|
1390
|
-
answer = { kind: 'transport-uncertainty' };
|
|
1391
|
-
}
|
|
1392
|
-
if (discoveryPaused || epoch !== (modelEpoch.get(key) ?? 0))
|
|
1393
|
-
return;
|
|
1394
|
-
if (answer.kind === 'revoked-authorization') {
|
|
1395
|
-
await refuse('revoked-authorization');
|
|
1396
|
-
return;
|
|
1397
|
-
}
|
|
1398
|
-
if (answer.kind === 'transport-uncertainty') {
|
|
1399
|
-
await retainModel(local, room, attached, cached(previous));
|
|
1400
|
-
return;
|
|
1401
|
-
}
|
|
1402
|
-
if (answer.kind === 'model-unavailable') {
|
|
1403
|
-
await retainModel(local, room, attached, {
|
|
1404
|
-
status: 'unavailable',
|
|
1405
|
-
reason: answer.reason,
|
|
1406
|
-
});
|
|
1407
|
-
return;
|
|
1408
|
-
}
|
|
1409
|
-
if (declared === null || !sameModel(answer.snapshot, declared)) {
|
|
1410
|
-
await retainModel(local, room, attached, {
|
|
1411
|
-
status: 'unavailable',
|
|
1412
|
-
reason: 'version-changed',
|
|
1413
|
-
});
|
|
1414
|
-
return;
|
|
1415
|
-
}
|
|
1416
|
-
if (previous.status !== 'unavailable' &&
|
|
1417
|
-
sameModel(previous.snapshot, answer.snapshot) &&
|
|
1418
|
-
previous.snapshot.schema !== answer.snapshot.schema) {
|
|
1419
|
-
await retainModel(local, room, attached, { status: 'unavailable', reason: 'invalid' });
|
|
1420
|
-
return;
|
|
1421
|
-
}
|
|
1422
|
-
const verified = answer.snapshot.schema === null ? EMPTY_SCHEMA : verify(answer.snapshot.schema);
|
|
1423
|
-
if (verified.verdict === 'refuse') {
|
|
1424
|
-
const where = verified.location === undefined ? '' : ` ${verified.location}`;
|
|
1425
|
-
journal.record(`model invalid ${room.locator} ${attached} ${answer.snapshot.identity} ${verified.rule}${where}`);
|
|
1426
|
-
}
|
|
1427
|
-
await retainModel(local, room, attached, verified.verdict === 'accept'
|
|
1428
|
-
? {
|
|
1429
|
-
status: 'current',
|
|
1430
|
-
snapshot: structuredClone(answer.snapshot),
|
|
1431
|
-
verifiedWith: MD_MODEL,
|
|
1432
|
-
types: verified.model.types,
|
|
1433
|
-
}
|
|
1434
|
-
: { status: 'unavailable', reason: 'invalid' });
|
|
1435
|
-
}
|
|
1436
1291
|
async function scoped(result, room, local) {
|
|
1437
1292
|
scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
|
|
1438
1293
|
let scopedAway = room;
|
|
@@ -1460,14 +1315,36 @@ export function composeRuntime(options) {
|
|
|
1460
1315
|
emitRooms();
|
|
1461
1316
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
1462
1317
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1318
|
+
function heldRooms(listed) {
|
|
1319
|
+
return listed.flatMap((room) => {
|
|
1320
|
+
const record = replicas.get(room.locator)?.confirmed.record;
|
|
1321
|
+
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1322
|
+
return [];
|
|
1323
|
+
return [
|
|
1324
|
+
{
|
|
1325
|
+
locator: room.locator,
|
|
1326
|
+
roomVersion: record.roomVersion,
|
|
1327
|
+
documents: record.documents,
|
|
1328
|
+
revisions: record.revisions,
|
|
1329
|
+
attachments: record.attachments,
|
|
1330
|
+
},
|
|
1331
|
+
];
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
async function synchronize(listed, local, progress, attempt) {
|
|
1335
|
+
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
|
|
1336
|
+
const ready = listed.filter((_, index) => {
|
|
1466
1337
|
const outcome = prepared[index];
|
|
1467
1338
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1468
1339
|
});
|
|
1469
|
-
|
|
1470
|
-
|
|
1340
|
+
if (attempt === 'opened') {
|
|
1341
|
+
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
1342
|
+
handed = true;
|
|
1343
|
+
}
|
|
1344
|
+
const loaded = await Promise.allSettled(ready
|
|
1345
|
+
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1346
|
+
.map((room) => loadRoom(room, local, progress)));
|
|
1347
|
+
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1471
1348
|
if (failed !== undefined)
|
|
1472
1349
|
throw failed.reason;
|
|
1473
1350
|
}
|
|
@@ -1484,11 +1361,8 @@ export function composeRuntime(options) {
|
|
|
1484
1361
|
progress.complete = false;
|
|
1485
1362
|
return false;
|
|
1486
1363
|
}
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
acquireModel(local, room, authorization, null),
|
|
1490
|
-
...(selection === undefined ? [] : [acquireModel(local, room, authorization, selection)]),
|
|
1491
|
-
]);
|
|
1364
|
+
if (prior !== undefined)
|
|
1365
|
+
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1492
1366
|
if (discoveryPaused || authorization === undefined)
|
|
1493
1367
|
return false;
|
|
1494
1368
|
if (room.replica.admission !== 'admitted') {
|
|
@@ -1496,24 +1370,53 @@ export function composeRuntime(options) {
|
|
|
1496
1370
|
progress.complete = false;
|
|
1497
1371
|
return false;
|
|
1498
1372
|
}
|
|
1499
|
-
const held = replicas.get(room.locator);
|
|
1500
|
-
if (held !== undefined && held.confirmed.record.replica.selection !== room.replica.selection) {
|
|
1501
|
-
progress.complete = false;
|
|
1502
|
-
return false;
|
|
1503
|
-
}
|
|
1504
1373
|
return true;
|
|
1505
1374
|
}
|
|
1506
|
-
async function
|
|
1375
|
+
async function caughtUp(room, local, progress) {
|
|
1376
|
+
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1377
|
+
if (held === undefined || authorization === undefined)
|
|
1378
|
+
return;
|
|
1379
|
+
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
1380
|
+
if (!behind)
|
|
1381
|
+
return;
|
|
1382
|
+
const withheld = await advance(room, local, authorization);
|
|
1383
|
+
if (withheld !== null)
|
|
1384
|
+
await withhold(withheld, room, local, progress);
|
|
1385
|
+
}
|
|
1386
|
+
function revoked(locator) {
|
|
1387
|
+
return ['collection', 'typed-selection'].some((attached) => {
|
|
1388
|
+
const model = modelOf(locator, attached);
|
|
1389
|
+
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
async function loadRoom(room, local, progress) {
|
|
1507
1393
|
if (discoveryPaused || authorization === undefined)
|
|
1508
1394
|
return;
|
|
1395
|
+
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1396
|
+
await reload(room, local, progress);
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1509
1399
|
const withheld = await advance(room, local, authorization);
|
|
1510
1400
|
if (withheld !== null) {
|
|
1511
1401
|
await withhold(withheld, room, local, progress);
|
|
1512
1402
|
return;
|
|
1513
1403
|
}
|
|
1514
|
-
await
|
|
1404
|
+
await caughtUp(room, local, progress);
|
|
1405
|
+
}
|
|
1406
|
+
async function reload(room, local, progress) {
|
|
1407
|
+
if (discoveryPaused || authorization === undefined)
|
|
1408
|
+
return;
|
|
1409
|
+
const bootstrapped = await pullBootstrap(authorization, room);
|
|
1410
|
+
if (!continuing())
|
|
1411
|
+
return;
|
|
1412
|
+
if (bootstrapped.kind !== 'staged') {
|
|
1413
|
+
await withhold(bootstrapped, room, local, progress);
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
await publish(local, room, bootstrapped.confirmed);
|
|
1417
|
+
await caughtUp(room, local, progress);
|
|
1515
1418
|
}
|
|
1516
|
-
async function
|
|
1419
|
+
async function discoverRooms(local, progress, attempt) {
|
|
1517
1420
|
if (closed || authorization === undefined) {
|
|
1518
1421
|
notifyReady();
|
|
1519
1422
|
return;
|
|
@@ -1539,6 +1442,7 @@ export function composeRuntime(options) {
|
|
|
1539
1442
|
}
|
|
1540
1443
|
}
|
|
1541
1444
|
progress.reachable = false;
|
|
1445
|
+
progress.complete = false;
|
|
1542
1446
|
notifyReady();
|
|
1543
1447
|
return;
|
|
1544
1448
|
}
|
|
@@ -1553,17 +1457,6 @@ export function composeRuntime(options) {
|
|
|
1553
1457
|
notifyReady();
|
|
1554
1458
|
return;
|
|
1555
1459
|
}
|
|
1556
|
-
if (!result.supportedVersions.includes(ROOM_CONTRACT_VERSION)) {
|
|
1557
|
-
await refuse('no-matching-version');
|
|
1558
|
-
notifyReady();
|
|
1559
|
-
return;
|
|
1560
|
-
}
|
|
1561
|
-
if (result.mdModel.identity !== MD_MODEL.identity ||
|
|
1562
|
-
result.mdModel.version !== MD_MODEL.version) {
|
|
1563
|
-
await refuse('md-model-version-mismatch');
|
|
1564
|
-
notifyReady();
|
|
1565
|
-
return;
|
|
1566
|
-
}
|
|
1567
1460
|
for (const room of result.rooms) {
|
|
1568
1461
|
if (room.operations === undefined ||
|
|
1569
1462
|
!['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
@@ -1571,8 +1464,7 @@ export function composeRuntime(options) {
|
|
|
1571
1464
|
room.replica === undefined ||
|
|
1572
1465
|
typeof room.replica.selection !== 'string' ||
|
|
1573
1466
|
room.replica.selection.length === 0 ||
|
|
1574
|
-
!['admitted', 'unavailable'].includes(room.replica.admission)
|
|
1575
|
-
!declaredIdentity(room.attachedModel)) {
|
|
1467
|
+
!['admitted', 'unavailable'].includes(room.replica.admission)) {
|
|
1576
1468
|
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
1577
1469
|
await refuse('missing-required-guarantee');
|
|
1578
1470
|
notifyReady();
|
|
@@ -1585,14 +1477,6 @@ export function composeRuntime(options) {
|
|
|
1585
1477
|
return;
|
|
1586
1478
|
}
|
|
1587
1479
|
}
|
|
1588
|
-
const declared = new Set(result.capabilities.map((capability) => capability.name));
|
|
1589
|
-
for (const required of options.requiredGuarantees) {
|
|
1590
|
-
if (!knownCapabilities.has(required) || !declared.has(required)) {
|
|
1591
|
-
await refuse('missing-required-guarantee');
|
|
1592
|
-
notifyReady();
|
|
1593
|
-
return;
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
1480
|
if (refusal !== null) {
|
|
1597
1481
|
refusal = null;
|
|
1598
1482
|
emitRefusal();
|
|
@@ -1600,26 +1484,13 @@ export function composeRuntime(options) {
|
|
|
1600
1484
|
const previousCapabilities = capabilities;
|
|
1601
1485
|
capabilities = result.capabilities;
|
|
1602
1486
|
notifyReady();
|
|
1603
|
-
if (declared.has('room-version-notifications'))
|
|
1604
|
-
observeRoomVersions();
|
|
1605
1487
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1606
1488
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
1607
1489
|
limits = result.providerLimits;
|
|
1608
1490
|
await local.declare(limits, capabilities);
|
|
1609
1491
|
}
|
|
1610
1492
|
await applyDiscovery(result.rooms, local);
|
|
1611
|
-
|
|
1612
|
-
.filter((room) => room.replica.admission === 'admitted')
|
|
1613
|
-
.map((room) => room.locator)
|
|
1614
|
-
.sort());
|
|
1615
|
-
progress.complete = true;
|
|
1616
|
-
await synchronize(result.rooms, local, progress);
|
|
1617
|
-
if (closed || discoveryPaused)
|
|
1618
|
-
return;
|
|
1619
|
-
await conversations.reconcile();
|
|
1620
|
-
if (status.providerUnreachable)
|
|
1621
|
-
conversations.reconnected();
|
|
1622
|
-
await conversations.catchUp(progress);
|
|
1493
|
+
await synchronize(result.rooms, local, progress, attempt);
|
|
1623
1494
|
}
|
|
1624
1495
|
async function ensureAuthorization() {
|
|
1625
1496
|
if (authorization !== undefined)
|
|
@@ -1633,64 +1504,253 @@ export function composeRuntime(options) {
|
|
|
1633
1504
|
throw error;
|
|
1634
1505
|
}
|
|
1635
1506
|
}
|
|
1636
|
-
|
|
1637
|
-
if (
|
|
1638
|
-
|
|
1507
|
+
function task(work) {
|
|
1508
|
+
if (stopping)
|
|
1509
|
+
return chain;
|
|
1510
|
+
chain = chain.then(work, work).catch(() => undefined);
|
|
1511
|
+
return chain;
|
|
1512
|
+
}
|
|
1513
|
+
function sync(attempt) {
|
|
1514
|
+
return task(async () => {
|
|
1515
|
+
if (closed || stopping || discoveryPaused) {
|
|
1516
|
+
notifyReady();
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
const progress = { complete: true, reachable: true };
|
|
1520
|
+
underway = attempt === 'opened' ? progress : null;
|
|
1521
|
+
complete = false;
|
|
1522
|
+
recorded = false;
|
|
1523
|
+
moveTo(transferring(status));
|
|
1524
|
+
handed = false;
|
|
1525
|
+
try {
|
|
1526
|
+
const local = await openLocal();
|
|
1527
|
+
if (closed)
|
|
1528
|
+
return;
|
|
1529
|
+
await ensureAuthorization();
|
|
1530
|
+
if (closed)
|
|
1531
|
+
return;
|
|
1532
|
+
await discoverRooms(local, progress, attempt);
|
|
1533
|
+
}
|
|
1534
|
+
catch {
|
|
1535
|
+
progress.complete = false;
|
|
1536
|
+
if (attempt === 'opened' && !handed)
|
|
1537
|
+
progress.reachable = false;
|
|
1538
|
+
}
|
|
1539
|
+
finally {
|
|
1540
|
+
notifyReady();
|
|
1541
|
+
}
|
|
1542
|
+
if (attempt !== 'opened') {
|
|
1543
|
+
await finishOpening(progress);
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
if (handed || closed || progress.reachable)
|
|
1547
|
+
return;
|
|
1548
|
+
underway = null;
|
|
1549
|
+
journal.record('connection reopen');
|
|
1550
|
+
await rest(progress);
|
|
1551
|
+
options.adapter.roomStream?.reopen();
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
function closedSocket(revoked) {
|
|
1555
|
+
const opened = socketOpen;
|
|
1556
|
+
socketOpen = false;
|
|
1557
|
+
if (revoked) {
|
|
1558
|
+
void refuse('revoked-authorization');
|
|
1639
1559
|
return;
|
|
1640
1560
|
}
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
if (closed)
|
|
1561
|
+
if (opened || status.providerUnreachable)
|
|
1562
|
+
return;
|
|
1563
|
+
moveTo({ ...status, providerUnreachable: true });
|
|
1564
|
+
}
|
|
1565
|
+
function applyOpening(expired) {
|
|
1566
|
+
return task(async () => {
|
|
1567
|
+
const progress = underway;
|
|
1568
|
+
underway = null;
|
|
1569
|
+
if (closed || stopping)
|
|
1650
1570
|
return;
|
|
1651
|
-
|
|
1652
|
-
|
|
1571
|
+
if (progress === null) {
|
|
1572
|
+
await reloadExpired(expired);
|
|
1653
1573
|
return;
|
|
1654
|
-
if (progress.complete) {
|
|
1655
|
-
completedAt = new Date().toISOString();
|
|
1656
|
-
await local.complete(completedAt);
|
|
1657
1574
|
}
|
|
1575
|
+
try {
|
|
1576
|
+
const local = await openLocal();
|
|
1577
|
+
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
1578
|
+
const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
|
|
1579
|
+
const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
|
|
1580
|
+
if (progress.reachable) {
|
|
1581
|
+
for (const room of held)
|
|
1582
|
+
if (!reloading.includes(room))
|
|
1583
|
+
await confirmModels(room, local);
|
|
1584
|
+
}
|
|
1585
|
+
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1586
|
+
if (failed !== undefined)
|
|
1587
|
+
throw failed.reason;
|
|
1588
|
+
}
|
|
1589
|
+
catch {
|
|
1590
|
+
progress.complete = false;
|
|
1591
|
+
progress.reachable = false;
|
|
1592
|
+
}
|
|
1593
|
+
await finishOpening(progress);
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
async function reloadExpired(expired) {
|
|
1597
|
+
const progress = { complete: true, reachable: true };
|
|
1598
|
+
try {
|
|
1599
|
+
const local = await openLocal();
|
|
1600
|
+
await Promise.allSettled([...discovered.values()]
|
|
1601
|
+
.filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
|
|
1602
|
+
.map((room) => reload(room, local, progress)));
|
|
1658
1603
|
}
|
|
1659
1604
|
catch {
|
|
1660
|
-
progress.
|
|
1661
|
-
|
|
1605
|
+
progress.reachable = false;
|
|
1606
|
+
}
|
|
1607
|
+
await rest(progress);
|
|
1608
|
+
}
|
|
1609
|
+
async function confirmModels(room, local) {
|
|
1610
|
+
for (const attached of ['collection', 'typed-selection']) {
|
|
1611
|
+
const model = modelOf(room.locator, attached);
|
|
1612
|
+
if (model.status === 'cached') {
|
|
1613
|
+
await retainModel(local, room, attached, { ...model, status: 'current' });
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
function submitLater(locator) {
|
|
1618
|
+
const running = submitting.get(locator);
|
|
1619
|
+
if (running !== undefined) {
|
|
1620
|
+
running.again = true;
|
|
1662
1621
|
return;
|
|
1663
1622
|
}
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1623
|
+
const run = { again: false, done: Promise.resolve() };
|
|
1624
|
+
submitting.set(locator, run);
|
|
1625
|
+
run.done = (async () => {
|
|
1626
|
+
try {
|
|
1627
|
+
do {
|
|
1628
|
+
run.again = false;
|
|
1629
|
+
const room = discovered.get(locator);
|
|
1630
|
+
if (closed || stopping || discoveryPaused || room === undefined)
|
|
1631
|
+
return;
|
|
1632
|
+
const progress = { complete: true, reachable: true };
|
|
1633
|
+
try {
|
|
1634
|
+
await ensureAuthorization();
|
|
1635
|
+
if (authorization === undefined)
|
|
1636
|
+
return;
|
|
1637
|
+
await submitRoom(room, await openLocal(), authorization, progress);
|
|
1638
|
+
}
|
|
1639
|
+
catch {
|
|
1640
|
+
progress.reachable = false;
|
|
1641
|
+
}
|
|
1642
|
+
armTimer();
|
|
1643
|
+
await rest(progress);
|
|
1644
|
+
} while (run.again);
|
|
1645
|
+
}
|
|
1646
|
+
finally {
|
|
1647
|
+
submitting.delete(locator);
|
|
1648
|
+
}
|
|
1649
|
+
})();
|
|
1650
|
+
}
|
|
1651
|
+
function submitAll() {
|
|
1652
|
+
for (const [locator, replica] of replicas) {
|
|
1653
|
+
if (replica.queue.length > 0)
|
|
1654
|
+
submitLater(locator);
|
|
1667
1655
|
}
|
|
1668
1656
|
}
|
|
1669
|
-
async function
|
|
1657
|
+
async function finishOpening(progress) {
|
|
1670
1658
|
try {
|
|
1671
|
-
|
|
1659
|
+
if (closed || discoveryPaused) {
|
|
1660
|
+
progress.complete = false;
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
submitAll();
|
|
1664
|
+
await conversations.reconcile();
|
|
1665
|
+
if (status.providerUnreachable)
|
|
1666
|
+
conversations.reconnected();
|
|
1667
|
+
const reading = { complete: true, reachable: true };
|
|
1668
|
+
await conversations.catchUp(reading);
|
|
1669
|
+
if (!reading.reachable)
|
|
1670
|
+
progress.reachable = false;
|
|
1671
|
+
}
|
|
1672
|
+
catch {
|
|
1673
|
+
progress.complete = false;
|
|
1672
1674
|
}
|
|
1673
1675
|
finally {
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
queued = false;
|
|
1677
|
-
void enqueue();
|
|
1678
|
-
}
|
|
1676
|
+
complete = progress.complete;
|
|
1677
|
+
await rest(progress);
|
|
1679
1678
|
}
|
|
1680
1679
|
}
|
|
1681
|
-
function
|
|
1682
|
-
|
|
1683
|
-
if (path === undefined || withdrawNotifications !== undefined)
|
|
1680
|
+
async function rest(progress) {
|
|
1681
|
+
if (closed)
|
|
1684
1682
|
return;
|
|
1685
|
-
|
|
1686
|
-
|
|
1683
|
+
let lastCompleted = status.lastCompleted;
|
|
1684
|
+
let synced = complete && stale.size === 0 && !pending();
|
|
1685
|
+
if (synced && !recorded) {
|
|
1686
|
+
const completedAt = new Date().toISOString();
|
|
1687
|
+
try {
|
|
1688
|
+
await (await openLocal()).complete(completedAt);
|
|
1689
|
+
recorded = true;
|
|
1690
|
+
lastCompleted = completedAt;
|
|
1691
|
+
}
|
|
1692
|
+
catch {
|
|
1693
|
+
synced = false;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
moveTo({
|
|
1697
|
+
state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
|
|
1698
|
+
lastCompleted,
|
|
1699
|
+
providerUnreachable: !progress.reachable,
|
|
1700
|
+
...attention(),
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
function applyRoom(locator, roomVersion) {
|
|
1704
|
+
announced.set(locator, roomVersion);
|
|
1705
|
+
return task(async () => {
|
|
1706
|
+
await answering.get(locator);
|
|
1707
|
+
if (closed || stopping || discoveryPaused || authorization === undefined)
|
|
1687
1708
|
return;
|
|
1688
|
-
|
|
1689
|
-
|
|
1709
|
+
const room = discovered.get(locator);
|
|
1710
|
+
const held = replicas.get(locator);
|
|
1711
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
1712
|
+
return;
|
|
1713
|
+
if (held !== undefined &&
|
|
1714
|
+
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
1715
|
+
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
const during = underway;
|
|
1719
|
+
const progress = during ?? { complete: true, reachable: true };
|
|
1720
|
+
if (during === null)
|
|
1721
|
+
moveTo(transferring(status));
|
|
1722
|
+
try {
|
|
1723
|
+
const local = await openLocal();
|
|
1724
|
+
const withheld = await advance(room, local, authorization);
|
|
1725
|
+
if (withheld !== null)
|
|
1726
|
+
await withhold(withheld, room, local, progress);
|
|
1727
|
+
}
|
|
1728
|
+
catch {
|
|
1729
|
+
stale.add(locator);
|
|
1730
|
+
}
|
|
1731
|
+
finally {
|
|
1732
|
+
if (during === null)
|
|
1733
|
+
await rest(progress);
|
|
1734
|
+
}
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
function observeStream() {
|
|
1738
|
+
const stream = options.adapter.roomStream;
|
|
1739
|
+
if (stream === undefined || withdrawStream !== undefined)
|
|
1740
|
+
return;
|
|
1741
|
+
withdrawStream = stream.observe((event) => {
|
|
1742
|
+
if (closed || stopping)
|
|
1690
1743
|
return;
|
|
1744
|
+
if (event.kind === 'opened') {
|
|
1745
|
+
socketOpen = true;
|
|
1746
|
+
void sync('opened');
|
|
1691
1747
|
}
|
|
1692
|
-
|
|
1693
|
-
|
|
1748
|
+
else if (event.kind === 'closed')
|
|
1749
|
+
closedSocket(event.revoked);
|
|
1750
|
+
else if (event.kind === 'opening')
|
|
1751
|
+
void applyOpening(event.expired);
|
|
1752
|
+
else
|
|
1753
|
+
void applyRoom(event.locator, event.roomVersion);
|
|
1694
1754
|
});
|
|
1695
1755
|
}
|
|
1696
1756
|
function writesHeld() {
|
|
@@ -1711,14 +1771,17 @@ export function composeRuntime(options) {
|
|
|
1711
1771
|
listener(mode);
|
|
1712
1772
|
}
|
|
1713
1773
|
if (releasing)
|
|
1714
|
-
|
|
1774
|
+
submitAll();
|
|
1715
1775
|
}
|
|
1716
1776
|
function observeConversations() {
|
|
1717
1777
|
const stream = options.adapter.conversations;
|
|
1718
1778
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
1719
1779
|
return;
|
|
1720
1780
|
withdrawConversations = stream.observe((event) => {
|
|
1721
|
-
conversations.receive(event)
|
|
1781
|
+
if (conversations.receive(event)) {
|
|
1782
|
+
void ready.then(scheduleCurrency);
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1722
1785
|
if (event.kind !== 'gap' || conversations.current())
|
|
1723
1786
|
return;
|
|
1724
1787
|
show();
|
|
@@ -1729,7 +1792,7 @@ export function composeRuntime(options) {
|
|
|
1729
1792
|
if (currencyQueued || closed || stopping)
|
|
1730
1793
|
return;
|
|
1731
1794
|
currencyQueued = true;
|
|
1732
|
-
|
|
1795
|
+
void task(runCurrency);
|
|
1733
1796
|
}
|
|
1734
1797
|
async function runCurrency() {
|
|
1735
1798
|
currencyQueued = false;
|
|
@@ -1755,95 +1818,12 @@ export function composeRuntime(options) {
|
|
|
1755
1818
|
}
|
|
1756
1819
|
}
|
|
1757
1820
|
}
|
|
1758
|
-
function handSelected(locators) {
|
|
1759
|
-
const path = options.adapter.roomVersions;
|
|
1760
|
-
if (path === undefined || withdrawNotifications === undefined)
|
|
1761
|
-
return;
|
|
1762
|
-
if (handed !== undefined && JSON.stringify(locators) === JSON.stringify(handed))
|
|
1763
|
-
return;
|
|
1764
|
-
handed = locators;
|
|
1765
|
-
path.select(locators);
|
|
1766
|
-
}
|
|
1767
|
-
async function runNotified() {
|
|
1768
|
-
const announced = [...notices];
|
|
1769
|
-
notices.clear();
|
|
1770
|
-
if (announced.length === 0 || closed || stopping || discoveryPaused)
|
|
1771
|
-
return;
|
|
1772
|
-
const resting = status;
|
|
1773
|
-
const progress = { complete: false, reachable: !resting.providerUnreachable };
|
|
1774
|
-
moveTo(transferring(resting));
|
|
1775
|
-
try {
|
|
1776
|
-
const local = await openLocal();
|
|
1777
|
-
for (const [locator, roomVersion] of announced) {
|
|
1778
|
-
if (closed || discoveryPaused || authorization === undefined)
|
|
1779
|
-
return;
|
|
1780
|
-
if (handed?.includes(locator) !== true)
|
|
1781
|
-
continue;
|
|
1782
|
-
const room = discovered.get(locator);
|
|
1783
|
-
const held = replicas.get(locator);
|
|
1784
|
-
if (room === undefined)
|
|
1785
|
-
continue;
|
|
1786
|
-
if (sourceRefusals.has(locator)) {
|
|
1787
|
-
void enqueue();
|
|
1788
|
-
continue;
|
|
1789
|
-
}
|
|
1790
|
-
if (held === undefined)
|
|
1791
|
-
continue;
|
|
1792
|
-
if (held.confirmed.record.roomVersion === roomVersion)
|
|
1793
|
-
continue;
|
|
1794
|
-
const withheld = await advance(room, local, authorization);
|
|
1795
|
-
if (withheld === null)
|
|
1796
|
-
continue;
|
|
1797
|
-
if (withheld.kind === 'refused') {
|
|
1798
|
-
void enqueue();
|
|
1799
|
-
continue;
|
|
1800
|
-
}
|
|
1801
|
-
if (withheld.kind === 'scoped-refusal') {
|
|
1802
|
-
await scoped(withheld, room, local);
|
|
1803
|
-
continue;
|
|
1804
|
-
}
|
|
1805
|
-
if (withheld.kind === 'transport-uncertainty') {
|
|
1806
|
-
progress.reachable = false;
|
|
1807
|
-
continue;
|
|
1808
|
-
}
|
|
1809
|
-
if (withheld.kind === 'room-not-readable') {
|
|
1810
|
-
unreadable.add(locator);
|
|
1811
|
-
list(room, false);
|
|
1812
|
-
emitRooms();
|
|
1813
|
-
continue;
|
|
1814
|
-
}
|
|
1815
|
-
if (withheld.kind === 'revoked-authorization') {
|
|
1816
|
-
await refuse('revoked-authorization');
|
|
1817
|
-
return;
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
catch {
|
|
1822
|
-
progress.reachable = false;
|
|
1823
|
-
}
|
|
1824
|
-
finally {
|
|
1825
|
-
if (!closed) {
|
|
1826
|
-
moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
|
|
1827
|
-
}
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
function enqueue() {
|
|
1831
|
-
if (stopping)
|
|
1832
|
-
return chain;
|
|
1833
|
-
if (inflight) {
|
|
1834
|
-
queued = true;
|
|
1835
|
-
return chain;
|
|
1836
|
-
}
|
|
1837
|
-
inflight = true;
|
|
1838
|
-
chain = chain.then(run, run);
|
|
1839
|
-
return chain;
|
|
1840
|
-
}
|
|
1841
1821
|
function close() {
|
|
1842
1822
|
if (closed)
|
|
1843
1823
|
return;
|
|
1844
1824
|
closed = true;
|
|
1845
|
-
|
|
1846
|
-
|
|
1825
|
+
withdrawStream?.();
|
|
1826
|
+
withdrawStream = undefined;
|
|
1847
1827
|
withdrawConversations?.();
|
|
1848
1828
|
withdrawConversations = undefined;
|
|
1849
1829
|
withdrawServiceMode?.();
|
|
@@ -1867,6 +1847,7 @@ export function composeRuntime(options) {
|
|
|
1867
1847
|
stopping = true;
|
|
1868
1848
|
stopTimer();
|
|
1869
1849
|
await chain.catch(() => undefined);
|
|
1850
|
+
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
1870
1851
|
await mutations;
|
|
1871
1852
|
close();
|
|
1872
1853
|
}
|
|
@@ -1875,11 +1856,29 @@ export function composeRuntime(options) {
|
|
|
1875
1856
|
options.signal?.addEventListener('abort', close, { once: true });
|
|
1876
1857
|
if (options.signal?.aborted)
|
|
1877
1858
|
close();
|
|
1878
|
-
|
|
1859
|
+
observeStream();
|
|
1860
|
+
void openLocal().then(() => {
|
|
1861
|
+
if (capabilities.length > 0)
|
|
1862
|
+
notifyReady();
|
|
1863
|
+
}, () => {
|
|
1879
1864
|
notifyReady();
|
|
1880
1865
|
});
|
|
1881
|
-
|
|
1882
|
-
|
|
1866
|
+
if (options.adapter.roomStream === undefined)
|
|
1867
|
+
void sync('unstreamed');
|
|
1868
|
+
function planned(input, reach) {
|
|
1869
|
+
return planOf(input, roomFacts(reach), planReach(reach));
|
|
1870
|
+
}
|
|
1871
|
+
function watchPlan(plan, deliver) {
|
|
1872
|
+
return readSide.watch(plan, identityOf(plan), deliver);
|
|
1873
|
+
}
|
|
1874
|
+
async function searched(input, reach) {
|
|
1875
|
+
const plan = searchPlanOf(input);
|
|
1876
|
+
const selected = plan.documents === undefined && reach === null
|
|
1877
|
+
? null
|
|
1878
|
+
: (await readSide.query(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
|
|
1879
|
+
return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
|
|
1880
|
+
}
|
|
1881
|
+
const watch = (topic, input, deliver) => {
|
|
1883
1882
|
if (topic === 'room-drafts') {
|
|
1884
1883
|
const listener = { notify: deliver, ready: false };
|
|
1885
1884
|
roomDraftListeners.add(listener);
|
|
@@ -1900,6 +1899,8 @@ export function composeRuntime(options) {
|
|
|
1900
1899
|
if (topic === 'conversation') {
|
|
1901
1900
|
return conversations.watch(input[0], input[1], deliver);
|
|
1902
1901
|
}
|
|
1902
|
+
if (topic === 'conversations')
|
|
1903
|
+
return conversations.watchList(deliver);
|
|
1903
1904
|
if (topic === 'presentation') {
|
|
1904
1905
|
const listener = {
|
|
1905
1906
|
app: input[0],
|
|
@@ -1962,9 +1963,8 @@ export function composeRuntime(options) {
|
|
|
1962
1963
|
queueSubscriptions.delete(queue);
|
|
1963
1964
|
};
|
|
1964
1965
|
}
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
}
|
|
1966
|
+
return watchPlan(planned(input[0], null), deliver);
|
|
1967
|
+
};
|
|
1968
1968
|
const md = {
|
|
1969
1969
|
loaded() {
|
|
1970
1970
|
return openLocal().then(() => undefined);
|
|
@@ -1972,15 +1972,8 @@ export function composeRuntime(options) {
|
|
|
1972
1972
|
rooms() {
|
|
1973
1973
|
return listed();
|
|
1974
1974
|
},
|
|
1975
|
-
query: ((input) => readSide.query(
|
|
1976
|
-
|
|
1977
|
-
const plan = searchPlanOf(input);
|
|
1978
|
-
const selected = plan.documents === undefined
|
|
1979
|
-
? null
|
|
1980
|
-
: (await readSide.query(filterOf(plan.documents, roomFacts())))
|
|
1981
|
-
.documents;
|
|
1982
|
-
return searchIndex.search(plan, selected);
|
|
1983
|
-
},
|
|
1975
|
+
query: ((input) => readSide.query(planned(input, null))),
|
|
1976
|
+
search: (input) => searched(input, null),
|
|
1984
1977
|
types(locator) {
|
|
1985
1978
|
return {
|
|
1986
1979
|
collection: modelTypes(modelOf(locator)),
|
|
@@ -2087,9 +2080,7 @@ export function composeRuntime(options) {
|
|
|
2087
2080
|
persistence() {
|
|
2088
2081
|
return options.shell.persistentStorage;
|
|
2089
2082
|
},
|
|
2090
|
-
observe: (
|
|
2091
|
-
deliver(structuredClone(message));
|
|
2092
|
-
}), topic === 'query')),
|
|
2083
|
+
observe: observing(watch),
|
|
2093
2084
|
async reauthorize() {
|
|
2094
2085
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2095
2086
|
return;
|
|
@@ -2098,8 +2089,19 @@ export function composeRuntime(options) {
|
|
|
2098
2089
|
await mutations;
|
|
2099
2090
|
authorization = undefined;
|
|
2100
2091
|
discoveryPaused = false;
|
|
2101
|
-
|
|
2092
|
+
if (options.adapter.roomStream === undefined)
|
|
2093
|
+
await sync('unstreamed');
|
|
2094
|
+
else
|
|
2095
|
+
await task(ensureAuthorization);
|
|
2102
2096
|
},
|
|
2103
2097
|
};
|
|
2104
|
-
|
|
2098
|
+
const reachable = {
|
|
2099
|
+
md,
|
|
2100
|
+
watch,
|
|
2101
|
+
plan: planned,
|
|
2102
|
+
query: (plan) => readSide.query(plan),
|
|
2103
|
+
watchPlan,
|
|
2104
|
+
search: searched,
|
|
2105
|
+
};
|
|
2106
|
+
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2105
2107
|
}
|