@agentshouse/mdruntime 0.1.0-cf28918c2bf91 → 0.1.0-cfbf80d8d4644
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 +33 -8
- package/README.md +10 -10
- package/dist/adapter.d.ts +51 -25
- 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 +34 -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 +501 -512
- package/dist/conversation.d.ts +18 -6
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +179 -78
- 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 +78 -26
- package/dist/model.d.ts +4 -11
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +4 -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 +7 -2
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +29 -20
- 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 +31 -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 +16 -10
- 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 +4 -4
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +12 -13
- 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.map +1 -1
- package/dist/rows.js +1 -4
- package/dist/save.d.ts +1 -2
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +90 -46
- 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,19 @@ 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
|
+
const submitting = new Map();
|
|
86
|
+
const answering = new Map();
|
|
87
|
+
let complete = false;
|
|
88
|
+
let recorded = false;
|
|
89
|
+
const stale = new Set();
|
|
109
90
|
let chain = Promise.resolve();
|
|
110
91
|
let mutations = Promise.resolve();
|
|
111
|
-
let inflight = false;
|
|
112
|
-
let queued = false;
|
|
113
92
|
const roomListeners = new Set();
|
|
114
93
|
const refusalListeners = new Set();
|
|
115
94
|
const storageLossListeners = new Set();
|
|
@@ -120,12 +99,13 @@ export function composeRuntime(options) {
|
|
|
120
99
|
const readSide = createReadSide();
|
|
121
100
|
let withdrawConversations;
|
|
122
101
|
let currencyQueued = false;
|
|
102
|
+
let socketOpen = false;
|
|
123
103
|
let serviceMode = 'open';
|
|
124
104
|
let haltedRefusal = false;
|
|
125
105
|
let openSignals = 0;
|
|
126
106
|
const serviceModeListeners = new Set();
|
|
127
107
|
const conversations = createConversationStore({
|
|
128
|
-
|
|
108
|
+
agents: declaredAgents,
|
|
129
109
|
documents: (locator) => readSide.query(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
|
|
130
110
|
call: (name, input) => invokeAction(name, input),
|
|
131
111
|
durably,
|
|
@@ -234,11 +214,14 @@ export function composeRuntime(options) {
|
|
|
234
214
|
function listed() {
|
|
235
215
|
return structuredClone([...rooms.values()]);
|
|
236
216
|
}
|
|
237
|
-
function roomFacts() {
|
|
238
|
-
const held = [...rooms.values()]
|
|
217
|
+
function roomFacts(reach = null) {
|
|
218
|
+
const held = [...rooms.values()]
|
|
219
|
+
.filter((room) => holds(reach, room.locator))
|
|
220
|
+
.map((room) => {
|
|
221
|
+
const governed = governs(reach, room);
|
|
239
222
|
const attached = [
|
|
240
|
-
modelOf(room.locator),
|
|
241
|
-
...(
|
|
223
|
+
...(governed.collection ? [modelOf(room.locator)] : []),
|
|
224
|
+
...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
|
|
242
225
|
];
|
|
243
226
|
return {
|
|
244
227
|
locator: room.locator,
|
|
@@ -316,23 +299,32 @@ export function composeRuntime(options) {
|
|
|
316
299
|
for (const app of set.presentations)
|
|
317
300
|
emitPresentation(app);
|
|
318
301
|
conversations.changed(set);
|
|
319
|
-
const cards = declaredCards();
|
|
320
|
-
if (cards !== undefined && set.documents.some((change) => change.locator === cards.locator)) {
|
|
321
|
-
scheduleCurrency();
|
|
322
|
-
}
|
|
323
302
|
}
|
|
324
303
|
function stopTimer() {
|
|
325
|
-
if (
|
|
304
|
+
if (resetTimer === undefined)
|
|
326
305
|
return;
|
|
327
|
-
|
|
328
|
-
|
|
306
|
+
clearTimeout(resetTimer);
|
|
307
|
+
resetTimer = undefined;
|
|
329
308
|
}
|
|
330
|
-
function
|
|
331
|
-
|
|
309
|
+
function armTimer() {
|
|
310
|
+
stopTimer();
|
|
311
|
+
if (closed || stopping)
|
|
312
|
+
return;
|
|
313
|
+
let earliest = null;
|
|
314
|
+
for (const { submission } of pendings.values()) {
|
|
315
|
+
if (submission.state !== 'reset')
|
|
316
|
+
continue;
|
|
317
|
+
const at = Date.parse(submission.reset);
|
|
318
|
+
if (earliest === null || at < earliest)
|
|
319
|
+
earliest = at;
|
|
320
|
+
}
|
|
321
|
+
if (earliest === null)
|
|
332
322
|
return;
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
323
|
+
// A Save House held back goes again only once the reset its answer named has passed.
|
|
324
|
+
resetTimer = setTimeout(() => {
|
|
325
|
+
resetTimer = undefined;
|
|
326
|
+
submitAll();
|
|
327
|
+
}, Math.max(0, earliest - Date.now()));
|
|
336
328
|
}
|
|
337
329
|
async function refuse(reason, provider = null) {
|
|
338
330
|
stopTimer();
|
|
@@ -358,7 +350,6 @@ export function composeRuntime(options) {
|
|
|
358
350
|
}
|
|
359
351
|
for (const key of keys) {
|
|
360
352
|
invalidModels.add(key);
|
|
361
|
-
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
362
353
|
models.set(key, revoked);
|
|
363
354
|
liveModels.delete(key);
|
|
364
355
|
}
|
|
@@ -373,7 +364,7 @@ export function composeRuntime(options) {
|
|
|
373
364
|
};
|
|
374
365
|
const read = readConfirmed(record, before.confirmed);
|
|
375
366
|
replicas.set(locator, read.kind === 'staged'
|
|
376
|
-
? replicaOf(read.confirmed, before.queue,
|
|
367
|
+
? replicaOf(read.confirmed, before.queue, before)
|
|
377
368
|
: { ...before, confirmed: { ...before.confirmed, record } });
|
|
378
369
|
}
|
|
379
370
|
rooms.set(locator, {
|
|
@@ -404,15 +395,14 @@ export function composeRuntime(options) {
|
|
|
404
395
|
rooms.set(room.locator, {
|
|
405
396
|
name: room.name,
|
|
406
397
|
locator: room.locator,
|
|
398
|
+
handle: room.handle,
|
|
407
399
|
operations: room.operations,
|
|
408
400
|
replica: room.replica,
|
|
409
|
-
attachedModel: room.attachedModel,
|
|
410
401
|
model: modelTypes(modelOf(room.locator)),
|
|
411
402
|
typedSelection: selection === null
|
|
412
403
|
? null
|
|
413
404
|
: {
|
|
414
405
|
roots: selection.roots,
|
|
415
|
-
attachedModel: selection.attachedModel,
|
|
416
406
|
model: modelTypes(modelOf(room.locator, 'typed-selection')),
|
|
417
407
|
},
|
|
418
408
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
@@ -429,9 +419,6 @@ export function composeRuntime(options) {
|
|
|
429
419
|
discovered.clear();
|
|
430
420
|
for (const room of listed)
|
|
431
421
|
discovered.set(room.locator, room);
|
|
432
|
-
for (const locator of wanted.keys())
|
|
433
|
-
if (!listedLocators.has(locator))
|
|
434
|
-
wanted.delete(locator);
|
|
435
422
|
await dropUnlisted(listedLocators, local);
|
|
436
423
|
for (const room of listed) {
|
|
437
424
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
@@ -465,6 +452,7 @@ export function composeRuntime(options) {
|
|
|
465
452
|
rooms.delete(locator);
|
|
466
453
|
roomDrafts.delete(locator);
|
|
467
454
|
unreadable.delete(locator);
|
|
455
|
+
stale.delete(locator);
|
|
468
456
|
scopedRefusals.delete(locator);
|
|
469
457
|
sourceRefusals.delete(locator);
|
|
470
458
|
declarationRefusals.delete(locator);
|
|
@@ -473,7 +461,6 @@ export function composeRuntime(options) {
|
|
|
473
461
|
models.delete(key);
|
|
474
462
|
liveModels.delete(key);
|
|
475
463
|
invalidModels.delete(key);
|
|
476
|
-
modelEpoch.delete(key);
|
|
477
464
|
}
|
|
478
465
|
journal.record(`room unlisted ${locator}`);
|
|
479
466
|
}
|
|
@@ -486,7 +473,6 @@ export function composeRuntime(options) {
|
|
|
486
473
|
if (prior !== undefined &&
|
|
487
474
|
prior.status !== 'unavailable' &&
|
|
488
475
|
(persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
|
|
489
|
-
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
490
476
|
invalidModels.add(key);
|
|
491
477
|
}
|
|
492
478
|
const model = persisted.status === 'unavailable' ||
|
|
@@ -541,7 +527,7 @@ export function composeRuntime(options) {
|
|
|
541
527
|
record: retyped,
|
|
542
528
|
collection: parseCollection(retyped.documents, compositionOf(retyped)),
|
|
543
529
|
};
|
|
544
|
-
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [],
|
|
530
|
+
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
|
|
545
531
|
const existing = rooms.get(record.locator);
|
|
546
532
|
if (existing === undefined)
|
|
547
533
|
list(record, false);
|
|
@@ -552,13 +538,11 @@ export function composeRuntime(options) {
|
|
|
552
538
|
attachments: record.attachments,
|
|
553
539
|
operations: record.operations,
|
|
554
540
|
replica: record.replica,
|
|
555
|
-
attachedModel: record.attachedModel,
|
|
556
541
|
model: modelTypes(model),
|
|
557
542
|
typedSelection: record.typedSelection === null
|
|
558
543
|
? null
|
|
559
544
|
: {
|
|
560
545
|
roots: record.typedSelection.roots,
|
|
561
|
-
attachedModel: record.typedSelection.attachedModel,
|
|
562
546
|
model: modelTypes(selectionModel),
|
|
563
547
|
},
|
|
564
548
|
});
|
|
@@ -626,7 +610,7 @@ export function composeRuntime(options) {
|
|
|
626
610
|
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
627
611
|
}
|
|
628
612
|
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
629
|
-
const withdrawn = withdrawals(confirmed, queue, withdrawable
|
|
613
|
+
const withdrawn = withdrawals(confirmed, queue, withdrawable);
|
|
630
614
|
const kept = queue.filter((batch) => !withdrawn.includes(batch));
|
|
631
615
|
const returning = [
|
|
632
616
|
...withdrawn.map((batch) => ({ batch, returned: { kind: 'withdrawn' } })).reverse(),
|
|
@@ -662,42 +646,73 @@ export function composeRuntime(options) {
|
|
|
662
646
|
rememberRoomDraft(record);
|
|
663
647
|
for (const batch of withdrawn)
|
|
664
648
|
journal.record(`outcome ${locator} ${batch.id} withdrawn`);
|
|
665
|
-
replicas.set(locator, replicaOf(confirmed, kept,
|
|
649
|
+
replicas.set(locator, replicaOf(confirmed, kept, before));
|
|
666
650
|
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
667
651
|
room: settling.room,
|
|
668
652
|
queue: kept.length !== (before?.queue.length ?? 0),
|
|
669
653
|
drafts: [...written.keys()],
|
|
670
654
|
});
|
|
671
655
|
}
|
|
672
|
-
function
|
|
656
|
+
function adoptModels(locator, record) {
|
|
657
|
+
for (const [attached, model] of [
|
|
658
|
+
['collection', record.model],
|
|
659
|
+
['typed-selection', record.selectionModel],
|
|
660
|
+
]) {
|
|
661
|
+
const key = modelKey(locator, attached);
|
|
662
|
+
models.set(key, model);
|
|
663
|
+
invalidModels.delete(key);
|
|
664
|
+
if (model.status === 'current')
|
|
665
|
+
liveModels.set(key, JSON.stringify(model.snapshot));
|
|
666
|
+
else if (model.status === 'unavailable')
|
|
667
|
+
liveModels.delete(key);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
function remodel(locator, answers, held) {
|
|
671
|
+
const taken = (answer, previous, attached) => {
|
|
672
|
+
if (answer === undefined)
|
|
673
|
+
return previous;
|
|
674
|
+
const { state, refused } = attachedModel(answer, previous);
|
|
675
|
+
if (refused !== null) {
|
|
676
|
+
const where = refused.location === undefined ? '' : ` ${refused.location}`;
|
|
677
|
+
journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
|
|
678
|
+
}
|
|
679
|
+
models.set(modelKey(locator, attached), state);
|
|
680
|
+
return state;
|
|
681
|
+
};
|
|
682
|
+
return {
|
|
683
|
+
collection: taken(answers?.collection, held.collection, 'collection'),
|
|
684
|
+
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function publish(local, room, confirmed, base) {
|
|
673
688
|
return exclusive(async () => {
|
|
674
689
|
if (!continuing())
|
|
675
|
-
return;
|
|
690
|
+
return true;
|
|
676
691
|
const prior = replicas.get(room.locator);
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
throw new Error(`Cannot retype staged Room ${room.locator}`);
|
|
692
|
+
if (base !== undefined &&
|
|
693
|
+
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
694
|
+
prior.confirmed.record.revisions !== base.record.revisions)) {
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
adoptModels(room.locator, confirmed.record);
|
|
684
698
|
const queue = prior?.queue ?? [];
|
|
685
|
-
const roomVersion =
|
|
699
|
+
const roomVersion = confirmed.record.roomVersion;
|
|
686
700
|
const accepted = queue.filter((batch) => {
|
|
687
701
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
688
702
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
689
703
|
});
|
|
690
|
-
const set = await settle(local, room.locator, prior,
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
704
|
+
const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
|
|
705
|
+
for (const batch of accepted)
|
|
706
|
+
pendings.delete(batch.id);
|
|
707
|
+
stale.delete(room.locator);
|
|
708
|
+
if (unreadable.delete(room.locator))
|
|
709
|
+
scopedRefusals.delete(room.locator);
|
|
696
710
|
sourceRefusals.delete(room.locator);
|
|
697
711
|
declarationRefusals.delete(room.locator);
|
|
698
712
|
list(room, true);
|
|
699
713
|
publishChangeSet(set);
|
|
700
714
|
moveTo({ ...status, ...attention() });
|
|
715
|
+
return true;
|
|
701
716
|
});
|
|
702
717
|
}
|
|
703
718
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
@@ -726,7 +741,7 @@ export function composeRuntime(options) {
|
|
|
726
741
|
operations: room.operations,
|
|
727
742
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
728
743
|
providerLimits: limits,
|
|
729
|
-
|
|
744
|
+
handle: room.handle,
|
|
730
745
|
visible: replica.visible,
|
|
731
746
|
changes: parts.flatMap(({ changes }) => changes),
|
|
732
747
|
bases: parts.flatMap(({ bases }) => bases),
|
|
@@ -750,15 +765,16 @@ export function composeRuntime(options) {
|
|
|
750
765
|
return { outcome: 'refused', rule: 'durable-write' };
|
|
751
766
|
}
|
|
752
767
|
pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
|
|
768
|
+
recorded = false;
|
|
753
769
|
for (const path of consumed)
|
|
754
770
|
forgetRoomDraft(locator, path);
|
|
755
|
-
replicas.set(locator, replicaOf(replica.confirmed, batches,
|
|
771
|
+
replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
|
|
756
772
|
publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
|
|
757
773
|
queue: true,
|
|
758
774
|
drafts: consumed,
|
|
759
775
|
}));
|
|
760
776
|
moveTo({ ...saving(status), ...attention() });
|
|
761
|
-
|
|
777
|
+
submitLater(locator);
|
|
762
778
|
return { outcome: 'saved', identifier: built.batch.id };
|
|
763
779
|
}
|
|
764
780
|
async function mark(local, locator, identifier, submission, lost) {
|
|
@@ -823,10 +839,10 @@ export function composeRuntime(options) {
|
|
|
823
839
|
return { kind: 'transport-uncertainty' };
|
|
824
840
|
}
|
|
825
841
|
}
|
|
826
|
-
function
|
|
842
|
+
function declaredAgents() {
|
|
827
843
|
for (const capability of capabilities) {
|
|
828
|
-
if (capability.
|
|
829
|
-
return capability.
|
|
844
|
+
if (capability.conversationAgents !== undefined)
|
|
845
|
+
return capability.conversationAgents;
|
|
830
846
|
}
|
|
831
847
|
return undefined;
|
|
832
848
|
}
|
|
@@ -857,8 +873,6 @@ export function composeRuntime(options) {
|
|
|
857
873
|
journal.record(`transfer action ${name}`);
|
|
858
874
|
try {
|
|
859
875
|
const result = await options.adapter.action(authorization, { name, input });
|
|
860
|
-
if (result.kind === 'action' && result.at !== undefined)
|
|
861
|
-
want(result.at);
|
|
862
876
|
if (result.kind === 'scoped-refusal') {
|
|
863
877
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
864
878
|
if (room !== undefined)
|
|
@@ -890,32 +904,11 @@ export function composeRuntime(options) {
|
|
|
890
904
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
891
905
|
!options.adapter.reached(locator, roomVersion, written));
|
|
892
906
|
}
|
|
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) {
|
|
907
|
+
async function pullRefresh(authorized, confirmed) {
|
|
915
908
|
if (!continuing())
|
|
916
909
|
return unconfirmed;
|
|
917
910
|
try {
|
|
918
|
-
const result = await refreshRoom(options.adapter, authorized, confirmed,
|
|
911
|
+
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
|
|
919
912
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
920
913
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
921
914
|
return result;
|
|
@@ -932,7 +925,7 @@ export function composeRuntime(options) {
|
|
|
932
925
|
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
933
926
|
collection: modelOf(room.locator),
|
|
934
927
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
935
|
-
}, continuing);
|
|
928
|
+
}, (models, held) => remodel(room.locator, models, held), continuing);
|
|
936
929
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
937
930
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
938
931
|
return result;
|
|
@@ -945,20 +938,28 @@ export function composeRuntime(options) {
|
|
|
945
938
|
async function advance(room, local, authorized) {
|
|
946
939
|
const held = replicas.get(room.locator);
|
|
947
940
|
if (held !== undefined) {
|
|
948
|
-
const refreshed = await pullRefresh(authorized, held.confirmed
|
|
941
|
+
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
949
942
|
if (!continuing())
|
|
950
943
|
return unconfirmed;
|
|
951
944
|
if (refreshed.kind === 'still-current') {
|
|
945
|
+
stale.delete(room.locator);
|
|
952
946
|
if (sourceRefusals.has(room.locator))
|
|
953
|
-
return
|
|
947
|
+
return null;
|
|
954
948
|
if (unreadable.delete(room.locator)) {
|
|
949
|
+
scopedRefusals.delete(room.locator);
|
|
955
950
|
list(room, true);
|
|
956
951
|
emitRooms();
|
|
957
952
|
}
|
|
958
953
|
return null;
|
|
959
954
|
}
|
|
960
955
|
if (refreshed.kind === 'advanced') {
|
|
961
|
-
await publish(local, room, refreshed.confirmed)
|
|
956
|
+
if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
|
|
957
|
+
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
958
|
+
const reached = now !== undefined &&
|
|
959
|
+
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
960
|
+
return reached ? null : advance(room, local, authorized);
|
|
961
|
+
}
|
|
962
|
+
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
962
963
|
return null;
|
|
963
964
|
}
|
|
964
965
|
if (refreshed.kind !== 'beyond-horizon')
|
|
@@ -973,7 +974,10 @@ export function composeRuntime(options) {
|
|
|
973
974
|
return null;
|
|
974
975
|
}
|
|
975
976
|
async function withhold(withheld, room, local, progress) {
|
|
976
|
-
|
|
977
|
+
if (withheld.kind === 'refused')
|
|
978
|
+
stale.delete(room.locator);
|
|
979
|
+
else
|
|
980
|
+
stale.add(room.locator);
|
|
977
981
|
if (withheld.kind === 'scoped-refusal') {
|
|
978
982
|
await scoped(withheld, room, local);
|
|
979
983
|
}
|
|
@@ -1020,44 +1024,30 @@ export function composeRuntime(options) {
|
|
|
1020
1024
|
(accepted.author === undefined ? '' : ` author ${accepted.author}`) +
|
|
1021
1025
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1022
1026
|
}
|
|
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) {
|
|
1027
|
+
async function adoptAccepted(room, local, submitted, accepted) {
|
|
1041
1028
|
if (!continuing())
|
|
1042
1029
|
return unconfirmed;
|
|
1043
1030
|
const current = replicas.get(room.locator);
|
|
1044
1031
|
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
1045
1032
|
return null;
|
|
1046
1033
|
}
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1034
|
+
const held = current.confirmed.record.roomVersion;
|
|
1035
|
+
logAccepted(room.locator, submitted, accepted);
|
|
1036
|
+
const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
1037
|
+
? { kind: 'staged', confirmed: current.confirmed }
|
|
1038
|
+
: applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
1039
|
+
? accepted.roomVersion
|
|
1040
|
+
: held, accepted.roomVersion);
|
|
1041
|
+
if (applied.kind !== 'staged') {
|
|
1042
|
+
const state = pendings.get(submitted.id);
|
|
1043
|
+
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1044
|
+
if (state !== undefined)
|
|
1045
|
+
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1046
|
+
emitQueue(room.locator);
|
|
1057
1047
|
return applied;
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1048
|
+
}
|
|
1049
|
+
const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
1050
|
+
pendings.delete(submitted.id);
|
|
1061
1051
|
unreadable.delete(room.locator);
|
|
1062
1052
|
sourceRefusals.delete(room.locator);
|
|
1063
1053
|
declarationRefusals.delete(room.locator);
|
|
@@ -1066,17 +1056,8 @@ export function composeRuntime(options) {
|
|
|
1066
1056
|
moveTo({ ...status, ...attention() });
|
|
1067
1057
|
return null;
|
|
1068
1058
|
}
|
|
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));
|
|
1059
|
+
async function adopted(room, local, submitted, accepted) {
|
|
1060
|
+
const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
|
|
1080
1061
|
if (failed === null)
|
|
1081
1062
|
return true;
|
|
1082
1063
|
if (failed.kind === 'refused' && continuing())
|
|
@@ -1089,10 +1070,8 @@ export function composeRuntime(options) {
|
|
|
1089
1070
|
const head = replica?.queue[0];
|
|
1090
1071
|
if (replica === undefined || head === undefined)
|
|
1091
1072
|
return;
|
|
1092
|
-
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1093
|
-
progress.complete = false;
|
|
1073
|
+
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1094
1074
|
return;
|
|
1095
|
-
}
|
|
1096
1075
|
const state = pendings.get(head.id);
|
|
1097
1076
|
const { submission, lost: earlier } = state ?? {
|
|
1098
1077
|
submission: { state: 'waiting' },
|
|
@@ -1120,6 +1099,7 @@ export function composeRuntime(options) {
|
|
|
1120
1099
|
return;
|
|
1121
1100
|
}
|
|
1122
1101
|
const opensBefore = openSignals;
|
|
1102
|
+
const answered = awaitingAnswer(room.locator);
|
|
1123
1103
|
try {
|
|
1124
1104
|
result = await options.adapter.mutate(authorized, {
|
|
1125
1105
|
locator: room.locator,
|
|
@@ -1128,11 +1108,14 @@ export function composeRuntime(options) {
|
|
|
1128
1108
|
}, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
|
|
1129
1109
|
}
|
|
1130
1110
|
catch {
|
|
1111
|
+
answered();
|
|
1131
1112
|
progress.complete = false;
|
|
1132
1113
|
if (continuing())
|
|
1133
1114
|
progress.reachable = false;
|
|
1134
1115
|
return;
|
|
1135
1116
|
}
|
|
1117
|
+
if (result.kind !== 'accepted' || !continuing())
|
|
1118
|
+
answered();
|
|
1136
1119
|
if (!continuing())
|
|
1137
1120
|
return;
|
|
1138
1121
|
if (result.kind === 'transport-uncertainty') {
|
|
@@ -1185,6 +1168,7 @@ export function composeRuntime(options) {
|
|
|
1185
1168
|
const reset = result.reset;
|
|
1186
1169
|
progress.complete = false;
|
|
1187
1170
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1171
|
+
armTimer();
|
|
1188
1172
|
return;
|
|
1189
1173
|
}
|
|
1190
1174
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
@@ -1211,95 +1195,33 @@ export function composeRuntime(options) {
|
|
|
1211
1195
|
return withhold(withheld, room, local, progress);
|
|
1212
1196
|
continue;
|
|
1213
1197
|
}
|
|
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
|
-
}
|
|
1198
|
+
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1199
|
+
if (!kept) {
|
|
1258
1200
|
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
1201
|
return;
|
|
1271
1202
|
}
|
|
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
1203
|
}
|
|
1289
|
-
|
|
1204
|
+
}
|
|
1205
|
+
function awaitingAnswer(locator) {
|
|
1206
|
+
let release = () => { };
|
|
1207
|
+
const awaited = new Promise((resolve) => {
|
|
1208
|
+
release = resolve;
|
|
1209
|
+
});
|
|
1210
|
+
answering.set(locator, awaited);
|
|
1211
|
+
return () => {
|
|
1212
|
+
if (answering.get(locator) === awaited)
|
|
1213
|
+
answering.delete(locator);
|
|
1214
|
+
release();
|
|
1215
|
+
};
|
|
1290
1216
|
}
|
|
1291
1217
|
async function retainModel(local, room, attached, next) {
|
|
1292
1218
|
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
1219
|
const previous = modelOf(room.locator, attached);
|
|
1297
1220
|
const heldBefore = replicas.get(room.locator)?.confirmed.record;
|
|
1298
1221
|
if (JSON.stringify(previous) === JSON.stringify(next) &&
|
|
1299
1222
|
(heldBefore === undefined ||
|
|
1300
1223
|
(JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
|
|
1301
1224
|
JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
|
|
1302
|
-
JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) &&
|
|
1303
1225
|
JSON.stringify(heldBefore.typedSelection) ===
|
|
1304
1226
|
JSON.stringify(room.typedSelection ?? null))))
|
|
1305
1227
|
return;
|
|
@@ -1310,15 +1232,12 @@ export function composeRuntime(options) {
|
|
|
1310
1232
|
await exclusive(async () => {
|
|
1311
1233
|
if (!continuing())
|
|
1312
1234
|
return;
|
|
1313
|
-
if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0))
|
|
1314
|
-
return;
|
|
1315
1235
|
const held = replicas.get(room.locator);
|
|
1316
1236
|
if (held !== undefined) {
|
|
1317
1237
|
const declared = {
|
|
1318
1238
|
...held.confirmed.record,
|
|
1319
1239
|
operations: room.operations,
|
|
1320
1240
|
replica: room.replica,
|
|
1321
|
-
attachedModel: room.attachedModel,
|
|
1322
1241
|
typedSelection: room.typedSelection ?? null,
|
|
1323
1242
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
1324
1243
|
};
|
|
@@ -1338,7 +1257,7 @@ export function composeRuntime(options) {
|
|
|
1338
1257
|
queue: held.queue,
|
|
1339
1258
|
});
|
|
1340
1259
|
if (read.kind === 'staged') {
|
|
1341
|
-
replicas.set(room.locator, replicaOf(read.confirmed, held.queue,
|
|
1260
|
+
replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
|
|
1342
1261
|
if (declarationRefusals.delete(room.locator)) {
|
|
1343
1262
|
sourceRefusals.delete(room.locator);
|
|
1344
1263
|
unreadable.delete(room.locator);
|
|
@@ -1362,77 +1281,6 @@ export function composeRuntime(options) {
|
|
|
1362
1281
|
publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
|
|
1363
1282
|
});
|
|
1364
1283
|
}
|
|
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
1284
|
async function scoped(result, room, local) {
|
|
1437
1285
|
scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
|
|
1438
1286
|
let scopedAway = room;
|
|
@@ -1460,14 +1308,34 @@ export function composeRuntime(options) {
|
|
|
1460
1308
|
emitRooms();
|
|
1461
1309
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
1462
1310
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1311
|
+
function heldRooms(listed) {
|
|
1312
|
+
return listed.flatMap((room) => {
|
|
1313
|
+
const record = replicas.get(room.locator)?.confirmed.record;
|
|
1314
|
+
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1315
|
+
return [];
|
|
1316
|
+
return [
|
|
1317
|
+
{
|
|
1318
|
+
locator: room.locator,
|
|
1319
|
+
roomVersion: record.roomVersion,
|
|
1320
|
+
documents: record.documents,
|
|
1321
|
+
revisions: record.revisions,
|
|
1322
|
+
attachments: record.attachments,
|
|
1323
|
+
},
|
|
1324
|
+
];
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
async function synchronize(listed, local, progress, attempt) {
|
|
1328
|
+
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
|
|
1329
|
+
const ready = listed.filter((_, index) => {
|
|
1466
1330
|
const outcome = prepared[index];
|
|
1467
1331
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1468
1332
|
});
|
|
1469
|
-
|
|
1470
|
-
|
|
1333
|
+
if (attempt === 'opened')
|
|
1334
|
+
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
1335
|
+
const loaded = await Promise.allSettled(ready
|
|
1336
|
+
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1337
|
+
.map((room) => loadRoom(room, local, progress)));
|
|
1338
|
+
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1471
1339
|
if (failed !== undefined)
|
|
1472
1340
|
throw failed.reason;
|
|
1473
1341
|
}
|
|
@@ -1484,11 +1352,8 @@ export function composeRuntime(options) {
|
|
|
1484
1352
|
progress.complete = false;
|
|
1485
1353
|
return false;
|
|
1486
1354
|
}
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
acquireModel(local, room, authorization, null),
|
|
1490
|
-
...(selection === undefined ? [] : [acquireModel(local, room, authorization, selection)]),
|
|
1491
|
-
]);
|
|
1355
|
+
if (prior !== undefined)
|
|
1356
|
+
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1492
1357
|
if (discoveryPaused || authorization === undefined)
|
|
1493
1358
|
return false;
|
|
1494
1359
|
if (room.replica.admission !== 'admitted') {
|
|
@@ -1496,24 +1361,53 @@ export function composeRuntime(options) {
|
|
|
1496
1361
|
progress.complete = false;
|
|
1497
1362
|
return false;
|
|
1498
1363
|
}
|
|
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
1364
|
return true;
|
|
1505
1365
|
}
|
|
1506
|
-
async function
|
|
1366
|
+
async function caughtUp(room, local, progress) {
|
|
1367
|
+
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1368
|
+
if (held === undefined || authorization === undefined)
|
|
1369
|
+
return;
|
|
1370
|
+
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
1371
|
+
if (!behind)
|
|
1372
|
+
return;
|
|
1373
|
+
const withheld = await advance(room, local, authorization);
|
|
1374
|
+
if (withheld !== null)
|
|
1375
|
+
await withhold(withheld, room, local, progress);
|
|
1376
|
+
}
|
|
1377
|
+
function revoked(locator) {
|
|
1378
|
+
return ['collection', 'typed-selection'].some((attached) => {
|
|
1379
|
+
const model = modelOf(locator, attached);
|
|
1380
|
+
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
async function loadRoom(room, local, progress) {
|
|
1507
1384
|
if (discoveryPaused || authorization === undefined)
|
|
1508
1385
|
return;
|
|
1386
|
+
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1387
|
+
await reload(room, local, progress);
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1509
1390
|
const withheld = await advance(room, local, authorization);
|
|
1510
1391
|
if (withheld !== null) {
|
|
1511
1392
|
await withhold(withheld, room, local, progress);
|
|
1512
1393
|
return;
|
|
1513
1394
|
}
|
|
1514
|
-
await
|
|
1395
|
+
await caughtUp(room, local, progress);
|
|
1515
1396
|
}
|
|
1516
|
-
async function
|
|
1397
|
+
async function reload(room, local, progress) {
|
|
1398
|
+
if (discoveryPaused || authorization === undefined)
|
|
1399
|
+
return;
|
|
1400
|
+
const bootstrapped = await pullBootstrap(authorization, room);
|
|
1401
|
+
if (!continuing())
|
|
1402
|
+
return;
|
|
1403
|
+
if (bootstrapped.kind !== 'staged') {
|
|
1404
|
+
await withhold(bootstrapped, room, local, progress);
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
await publish(local, room, bootstrapped.confirmed);
|
|
1408
|
+
await caughtUp(room, local, progress);
|
|
1409
|
+
}
|
|
1410
|
+
async function discoverRooms(local, progress, attempt) {
|
|
1517
1411
|
if (closed || authorization === undefined) {
|
|
1518
1412
|
notifyReady();
|
|
1519
1413
|
return;
|
|
@@ -1539,6 +1433,10 @@ export function composeRuntime(options) {
|
|
|
1539
1433
|
}
|
|
1540
1434
|
}
|
|
1541
1435
|
progress.reachable = false;
|
|
1436
|
+
progress.complete = false;
|
|
1437
|
+
if (attempt === 'opened') {
|
|
1438
|
+
options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
|
|
1439
|
+
}
|
|
1542
1440
|
notifyReady();
|
|
1543
1441
|
return;
|
|
1544
1442
|
}
|
|
@@ -1553,17 +1451,6 @@ export function composeRuntime(options) {
|
|
|
1553
1451
|
notifyReady();
|
|
1554
1452
|
return;
|
|
1555
1453
|
}
|
|
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
1454
|
for (const room of result.rooms) {
|
|
1568
1455
|
if (room.operations === undefined ||
|
|
1569
1456
|
!['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
@@ -1571,8 +1458,7 @@ export function composeRuntime(options) {
|
|
|
1571
1458
|
room.replica === undefined ||
|
|
1572
1459
|
typeof room.replica.selection !== 'string' ||
|
|
1573
1460
|
room.replica.selection.length === 0 ||
|
|
1574
|
-
!['admitted', 'unavailable'].includes(room.replica.admission)
|
|
1575
|
-
!declaredIdentity(room.attachedModel)) {
|
|
1461
|
+
!['admitted', 'unavailable'].includes(room.replica.admission)) {
|
|
1576
1462
|
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
1577
1463
|
await refuse('missing-required-guarantee');
|
|
1578
1464
|
notifyReady();
|
|
@@ -1585,14 +1471,6 @@ export function composeRuntime(options) {
|
|
|
1585
1471
|
return;
|
|
1586
1472
|
}
|
|
1587
1473
|
}
|
|
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
1474
|
if (refusal !== null) {
|
|
1597
1475
|
refusal = null;
|
|
1598
1476
|
emitRefusal();
|
|
@@ -1600,26 +1478,13 @@ export function composeRuntime(options) {
|
|
|
1600
1478
|
const previousCapabilities = capabilities;
|
|
1601
1479
|
capabilities = result.capabilities;
|
|
1602
1480
|
notifyReady();
|
|
1603
|
-
if (declared.has('room-version-notifications'))
|
|
1604
|
-
observeRoomVersions();
|
|
1605
1481
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1606
1482
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
1607
1483
|
limits = result.providerLimits;
|
|
1608
1484
|
await local.declare(limits, capabilities);
|
|
1609
1485
|
}
|
|
1610
1486
|
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);
|
|
1487
|
+
await synchronize(result.rooms, local, progress, attempt);
|
|
1623
1488
|
}
|
|
1624
1489
|
async function ensureAuthorization() {
|
|
1625
1490
|
if (authorization !== undefined)
|
|
@@ -1633,64 +1498,246 @@ export function composeRuntime(options) {
|
|
|
1633
1498
|
throw error;
|
|
1634
1499
|
}
|
|
1635
1500
|
}
|
|
1636
|
-
|
|
1637
|
-
if (
|
|
1638
|
-
|
|
1501
|
+
function task(work) {
|
|
1502
|
+
if (stopping)
|
|
1503
|
+
return chain;
|
|
1504
|
+
chain = chain.then(work, work).catch(() => undefined);
|
|
1505
|
+
return chain;
|
|
1506
|
+
}
|
|
1507
|
+
function sync(attempt) {
|
|
1508
|
+
return task(async () => {
|
|
1509
|
+
if (closed || stopping || discoveryPaused) {
|
|
1510
|
+
notifyReady();
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
const progress = { complete: true, reachable: true };
|
|
1514
|
+
underway = attempt === 'opened' ? progress : null;
|
|
1515
|
+
complete = false;
|
|
1516
|
+
recorded = false;
|
|
1517
|
+
moveTo(transferring(status));
|
|
1518
|
+
try {
|
|
1519
|
+
const local = await openLocal();
|
|
1520
|
+
if (closed)
|
|
1521
|
+
return;
|
|
1522
|
+
await ensureAuthorization();
|
|
1523
|
+
if (closed)
|
|
1524
|
+
return;
|
|
1525
|
+
await discoverRooms(local, progress, attempt);
|
|
1526
|
+
}
|
|
1527
|
+
catch {
|
|
1528
|
+
progress.complete = false;
|
|
1529
|
+
if (attempt === 'opened') {
|
|
1530
|
+
options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
finally {
|
|
1534
|
+
notifyReady();
|
|
1535
|
+
}
|
|
1536
|
+
if (attempt === 'opened')
|
|
1537
|
+
return;
|
|
1538
|
+
await finishOpening(progress);
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
function closedSocket(revoked) {
|
|
1542
|
+
const opened = socketOpen;
|
|
1543
|
+
socketOpen = false;
|
|
1544
|
+
if (revoked) {
|
|
1545
|
+
void refuse('revoked-authorization');
|
|
1639
1546
|
return;
|
|
1640
1547
|
}
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
if (closed)
|
|
1548
|
+
if (opened || status.providerUnreachable)
|
|
1549
|
+
return;
|
|
1550
|
+
moveTo({ ...status, providerUnreachable: true });
|
|
1551
|
+
}
|
|
1552
|
+
function applyOpening(expired) {
|
|
1553
|
+
return task(async () => {
|
|
1554
|
+
const progress = underway;
|
|
1555
|
+
underway = null;
|
|
1556
|
+
if (closed || stopping)
|
|
1650
1557
|
return;
|
|
1651
|
-
|
|
1652
|
-
|
|
1558
|
+
if (progress === null) {
|
|
1559
|
+
await reloadExpired(expired);
|
|
1653
1560
|
return;
|
|
1654
|
-
if (progress.complete) {
|
|
1655
|
-
completedAt = new Date().toISOString();
|
|
1656
|
-
await local.complete(completedAt);
|
|
1657
1561
|
}
|
|
1562
|
+
try {
|
|
1563
|
+
const local = await openLocal();
|
|
1564
|
+
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
1565
|
+
const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
|
|
1566
|
+
const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
|
|
1567
|
+
if (progress.reachable) {
|
|
1568
|
+
for (const room of held)
|
|
1569
|
+
if (!reloading.includes(room))
|
|
1570
|
+
await confirmModels(room, local);
|
|
1571
|
+
}
|
|
1572
|
+
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1573
|
+
if (failed !== undefined)
|
|
1574
|
+
throw failed.reason;
|
|
1575
|
+
}
|
|
1576
|
+
catch {
|
|
1577
|
+
progress.complete = false;
|
|
1578
|
+
progress.reachable = false;
|
|
1579
|
+
}
|
|
1580
|
+
await finishOpening(progress);
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
async function reloadExpired(expired) {
|
|
1584
|
+
const progress = { complete: true, reachable: true };
|
|
1585
|
+
try {
|
|
1586
|
+
const local = await openLocal();
|
|
1587
|
+
await Promise.allSettled([...discovered.values()]
|
|
1588
|
+
.filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
|
|
1589
|
+
.map((room) => reload(room, local, progress)));
|
|
1658
1590
|
}
|
|
1659
1591
|
catch {
|
|
1660
|
-
progress.
|
|
1661
|
-
|
|
1592
|
+
progress.reachable = false;
|
|
1593
|
+
}
|
|
1594
|
+
await rest(progress);
|
|
1595
|
+
}
|
|
1596
|
+
async function confirmModels(room, local) {
|
|
1597
|
+
for (const attached of ['collection', 'typed-selection']) {
|
|
1598
|
+
const model = modelOf(room.locator, attached);
|
|
1599
|
+
if (model.status === 'cached') {
|
|
1600
|
+
await retainModel(local, room, attached, { ...model, status: 'current' });
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
function submitLater(locator) {
|
|
1605
|
+
const running = submitting.get(locator);
|
|
1606
|
+
if (running !== undefined) {
|
|
1607
|
+
running.again = true;
|
|
1662
1608
|
return;
|
|
1663
1609
|
}
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1610
|
+
const run = { again: false, done: Promise.resolve() };
|
|
1611
|
+
submitting.set(locator, run);
|
|
1612
|
+
run.done = (async () => {
|
|
1613
|
+
try {
|
|
1614
|
+
do {
|
|
1615
|
+
run.again = false;
|
|
1616
|
+
const room = discovered.get(locator);
|
|
1617
|
+
if (closed || stopping || discoveryPaused || room === undefined)
|
|
1618
|
+
return;
|
|
1619
|
+
const progress = { complete: true, reachable: true };
|
|
1620
|
+
try {
|
|
1621
|
+
await ensureAuthorization();
|
|
1622
|
+
if (authorization === undefined)
|
|
1623
|
+
return;
|
|
1624
|
+
await submitRoom(room, await openLocal(), authorization, progress);
|
|
1625
|
+
}
|
|
1626
|
+
catch {
|
|
1627
|
+
progress.reachable = false;
|
|
1628
|
+
}
|
|
1629
|
+
armTimer();
|
|
1630
|
+
await rest(progress);
|
|
1631
|
+
} while (run.again);
|
|
1632
|
+
}
|
|
1633
|
+
finally {
|
|
1634
|
+
submitting.delete(locator);
|
|
1635
|
+
}
|
|
1636
|
+
})();
|
|
1637
|
+
}
|
|
1638
|
+
function submitAll() {
|
|
1639
|
+
for (const [locator, replica] of replicas) {
|
|
1640
|
+
if (replica.queue.length > 0)
|
|
1641
|
+
submitLater(locator);
|
|
1667
1642
|
}
|
|
1668
1643
|
}
|
|
1669
|
-
async function
|
|
1644
|
+
async function finishOpening(progress) {
|
|
1670
1645
|
try {
|
|
1671
|
-
|
|
1646
|
+
if (closed || discoveryPaused) {
|
|
1647
|
+
progress.complete = false;
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
submitAll();
|
|
1651
|
+
await conversations.reconcile();
|
|
1652
|
+
if (status.providerUnreachable)
|
|
1653
|
+
conversations.reconnected();
|
|
1654
|
+
const reading = { complete: true, reachable: true };
|
|
1655
|
+
await conversations.catchUp(reading);
|
|
1656
|
+
if (!reading.reachable)
|
|
1657
|
+
progress.reachable = false;
|
|
1658
|
+
}
|
|
1659
|
+
catch {
|
|
1660
|
+
progress.complete = false;
|
|
1672
1661
|
}
|
|
1673
1662
|
finally {
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
queued = false;
|
|
1677
|
-
void enqueue();
|
|
1678
|
-
}
|
|
1663
|
+
complete = progress.complete;
|
|
1664
|
+
await rest(progress);
|
|
1679
1665
|
}
|
|
1680
1666
|
}
|
|
1681
|
-
function
|
|
1682
|
-
|
|
1683
|
-
if (path === undefined || withdrawNotifications !== undefined)
|
|
1667
|
+
async function rest(progress) {
|
|
1668
|
+
if (closed)
|
|
1684
1669
|
return;
|
|
1685
|
-
|
|
1686
|
-
|
|
1670
|
+
let lastCompleted = status.lastCompleted;
|
|
1671
|
+
let synced = complete && stale.size === 0 && !pending();
|
|
1672
|
+
if (synced && !recorded) {
|
|
1673
|
+
const completedAt = new Date().toISOString();
|
|
1674
|
+
try {
|
|
1675
|
+
await (await openLocal()).complete(completedAt);
|
|
1676
|
+
recorded = true;
|
|
1677
|
+
lastCompleted = completedAt;
|
|
1678
|
+
}
|
|
1679
|
+
catch {
|
|
1680
|
+
synced = false;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
moveTo({
|
|
1684
|
+
state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
|
|
1685
|
+
lastCompleted,
|
|
1686
|
+
providerUnreachable: !progress.reachable,
|
|
1687
|
+
...attention(),
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
function applyRoom(locator, roomVersion) {
|
|
1691
|
+
announced.set(locator, roomVersion);
|
|
1692
|
+
return task(async () => {
|
|
1693
|
+
await answering.get(locator);
|
|
1694
|
+
if (closed || stopping || discoveryPaused || authorization === undefined)
|
|
1695
|
+
return;
|
|
1696
|
+
const room = discovered.get(locator);
|
|
1697
|
+
const held = replicas.get(locator);
|
|
1698
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
1687
1699
|
return;
|
|
1688
|
-
if (
|
|
1689
|
-
|
|
1700
|
+
if (held !== undefined &&
|
|
1701
|
+
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
1702
|
+
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
const during = underway;
|
|
1706
|
+
const progress = during ?? { complete: true, reachable: true };
|
|
1707
|
+
if (during === null)
|
|
1708
|
+
moveTo(transferring(status));
|
|
1709
|
+
try {
|
|
1710
|
+
const local = await openLocal();
|
|
1711
|
+
const withheld = await advance(room, local, authorization);
|
|
1712
|
+
if (withheld !== null)
|
|
1713
|
+
await withhold(withheld, room, local, progress);
|
|
1714
|
+
}
|
|
1715
|
+
catch {
|
|
1716
|
+
stale.add(locator);
|
|
1717
|
+
}
|
|
1718
|
+
finally {
|
|
1719
|
+
if (during === null)
|
|
1720
|
+
await rest(progress);
|
|
1721
|
+
}
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
function observeStream() {
|
|
1725
|
+
const stream = options.adapter.roomStream;
|
|
1726
|
+
if (stream === undefined || withdrawStream !== undefined)
|
|
1727
|
+
return;
|
|
1728
|
+
withdrawStream = stream.observe((event) => {
|
|
1729
|
+
if (closed || stopping)
|
|
1690
1730
|
return;
|
|
1731
|
+
if (event.kind === 'opened') {
|
|
1732
|
+
socketOpen = true;
|
|
1733
|
+
void sync('opened');
|
|
1691
1734
|
}
|
|
1692
|
-
|
|
1693
|
-
|
|
1735
|
+
else if (event.kind === 'closed')
|
|
1736
|
+
closedSocket(event.revoked);
|
|
1737
|
+
else if (event.kind === 'opening')
|
|
1738
|
+
void applyOpening(event.expired);
|
|
1739
|
+
else
|
|
1740
|
+
void applyRoom(event.locator, event.roomVersion);
|
|
1694
1741
|
});
|
|
1695
1742
|
}
|
|
1696
1743
|
function writesHeld() {
|
|
@@ -1711,14 +1758,17 @@ export function composeRuntime(options) {
|
|
|
1711
1758
|
listener(mode);
|
|
1712
1759
|
}
|
|
1713
1760
|
if (releasing)
|
|
1714
|
-
|
|
1761
|
+
submitAll();
|
|
1715
1762
|
}
|
|
1716
1763
|
function observeConversations() {
|
|
1717
1764
|
const stream = options.adapter.conversations;
|
|
1718
1765
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
1719
1766
|
return;
|
|
1720
1767
|
withdrawConversations = stream.observe((event) => {
|
|
1721
|
-
conversations.receive(event)
|
|
1768
|
+
if (conversations.receive(event)) {
|
|
1769
|
+
void ready.then(scheduleCurrency);
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1722
1772
|
if (event.kind !== 'gap' || conversations.current())
|
|
1723
1773
|
return;
|
|
1724
1774
|
show();
|
|
@@ -1729,7 +1779,7 @@ export function composeRuntime(options) {
|
|
|
1729
1779
|
if (currencyQueued || closed || stopping)
|
|
1730
1780
|
return;
|
|
1731
1781
|
currencyQueued = true;
|
|
1732
|
-
|
|
1782
|
+
void task(runCurrency);
|
|
1733
1783
|
}
|
|
1734
1784
|
async function runCurrency() {
|
|
1735
1785
|
currencyQueued = false;
|
|
@@ -1755,95 +1805,12 @@ export function composeRuntime(options) {
|
|
|
1755
1805
|
}
|
|
1756
1806
|
}
|
|
1757
1807
|
}
|
|
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
1808
|
function close() {
|
|
1842
1809
|
if (closed)
|
|
1843
1810
|
return;
|
|
1844
1811
|
closed = true;
|
|
1845
|
-
|
|
1846
|
-
|
|
1812
|
+
withdrawStream?.();
|
|
1813
|
+
withdrawStream = undefined;
|
|
1847
1814
|
withdrawConversations?.();
|
|
1848
1815
|
withdrawConversations = undefined;
|
|
1849
1816
|
withdrawServiceMode?.();
|
|
@@ -1867,6 +1834,7 @@ export function composeRuntime(options) {
|
|
|
1867
1834
|
stopping = true;
|
|
1868
1835
|
stopTimer();
|
|
1869
1836
|
await chain.catch(() => undefined);
|
|
1837
|
+
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
1870
1838
|
await mutations;
|
|
1871
1839
|
close();
|
|
1872
1840
|
}
|
|
@@ -1875,11 +1843,29 @@ export function composeRuntime(options) {
|
|
|
1875
1843
|
options.signal?.addEventListener('abort', close, { once: true });
|
|
1876
1844
|
if (options.signal?.aborted)
|
|
1877
1845
|
close();
|
|
1878
|
-
|
|
1846
|
+
observeStream();
|
|
1847
|
+
void openLocal().then(() => {
|
|
1848
|
+
if (capabilities.length > 0)
|
|
1849
|
+
notifyReady();
|
|
1850
|
+
}, () => {
|
|
1879
1851
|
notifyReady();
|
|
1880
1852
|
});
|
|
1881
|
-
|
|
1882
|
-
|
|
1853
|
+
if (options.adapter.roomStream === undefined)
|
|
1854
|
+
void sync('unstreamed');
|
|
1855
|
+
function planned(input, reach) {
|
|
1856
|
+
return planOf(input, roomFacts(reach), planReach(reach));
|
|
1857
|
+
}
|
|
1858
|
+
function watchPlan(plan, deliver) {
|
|
1859
|
+
return readSide.watch(plan, identityOf(plan), deliver);
|
|
1860
|
+
}
|
|
1861
|
+
async function searched(input, reach) {
|
|
1862
|
+
const plan = searchPlanOf(input);
|
|
1863
|
+
const selected = plan.documents === undefined && reach === null
|
|
1864
|
+
? null
|
|
1865
|
+
: (await readSide.query(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
|
|
1866
|
+
return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
|
|
1867
|
+
}
|
|
1868
|
+
const watch = (topic, input, deliver) => {
|
|
1883
1869
|
if (topic === 'room-drafts') {
|
|
1884
1870
|
const listener = { notify: deliver, ready: false };
|
|
1885
1871
|
roomDraftListeners.add(listener);
|
|
@@ -1900,6 +1886,8 @@ export function composeRuntime(options) {
|
|
|
1900
1886
|
if (topic === 'conversation') {
|
|
1901
1887
|
return conversations.watch(input[0], input[1], deliver);
|
|
1902
1888
|
}
|
|
1889
|
+
if (topic === 'conversations')
|
|
1890
|
+
return conversations.watchList(deliver);
|
|
1903
1891
|
if (topic === 'presentation') {
|
|
1904
1892
|
const listener = {
|
|
1905
1893
|
app: input[0],
|
|
@@ -1962,9 +1950,8 @@ export function composeRuntime(options) {
|
|
|
1962
1950
|
queueSubscriptions.delete(queue);
|
|
1963
1951
|
};
|
|
1964
1952
|
}
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
}
|
|
1953
|
+
return watchPlan(planned(input[0], null), deliver);
|
|
1954
|
+
};
|
|
1968
1955
|
const md = {
|
|
1969
1956
|
loaded() {
|
|
1970
1957
|
return openLocal().then(() => undefined);
|
|
@@ -1972,15 +1959,8 @@ export function composeRuntime(options) {
|
|
|
1972
1959
|
rooms() {
|
|
1973
1960
|
return listed();
|
|
1974
1961
|
},
|
|
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
|
-
},
|
|
1962
|
+
query: ((input) => readSide.query(planned(input, null))),
|
|
1963
|
+
search: (input) => searched(input, null),
|
|
1984
1964
|
types(locator) {
|
|
1985
1965
|
return {
|
|
1986
1966
|
collection: modelTypes(modelOf(locator)),
|
|
@@ -2087,9 +2067,7 @@ export function composeRuntime(options) {
|
|
|
2087
2067
|
persistence() {
|
|
2088
2068
|
return options.shell.persistentStorage;
|
|
2089
2069
|
},
|
|
2090
|
-
observe: (
|
|
2091
|
-
deliver(structuredClone(message));
|
|
2092
|
-
}), topic === 'query')),
|
|
2070
|
+
observe: observing(watch),
|
|
2093
2071
|
async reauthorize() {
|
|
2094
2072
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2095
2073
|
return;
|
|
@@ -2098,8 +2076,19 @@ export function composeRuntime(options) {
|
|
|
2098
2076
|
await mutations;
|
|
2099
2077
|
authorization = undefined;
|
|
2100
2078
|
discoveryPaused = false;
|
|
2101
|
-
|
|
2079
|
+
if (options.adapter.roomStream === undefined)
|
|
2080
|
+
await sync('unstreamed');
|
|
2081
|
+
else
|
|
2082
|
+
await task(ensureAuthorization);
|
|
2102
2083
|
},
|
|
2103
2084
|
};
|
|
2104
|
-
|
|
2085
|
+
const reachable = {
|
|
2086
|
+
md,
|
|
2087
|
+
watch,
|
|
2088
|
+
plan: planned,
|
|
2089
|
+
query: (plan) => readSide.query(plan),
|
|
2090
|
+
watchPlan,
|
|
2091
|
+
search: searched,
|
|
2092
|
+
};
|
|
2093
|
+
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2105
2094
|
}
|