@agentshouse/mdruntime 0.1.0-cf28918c2bf91 → 0.1.0-cff8ba8fa2542
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 +32 -8
- package/README.md +10 -10
- package/dist/adapter.d.ts +50 -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 +2 -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 +9 -12
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +495 -511
- 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 +77 -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/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 +285 -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 +0 -2
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +3 -8
- package/dist/store.d.ts +0 -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, {
|
|
@@ -406,13 +397,11 @@ export function composeRuntime(options) {
|
|
|
406
397
|
locator: room.locator,
|
|
407
398
|
operations: room.operations,
|
|
408
399
|
replica: room.replica,
|
|
409
|
-
attachedModel: room.attachedModel,
|
|
410
400
|
model: modelTypes(modelOf(room.locator)),
|
|
411
401
|
typedSelection: selection === null
|
|
412
402
|
? null
|
|
413
403
|
: {
|
|
414
404
|
roots: selection.roots,
|
|
415
|
-
attachedModel: selection.attachedModel,
|
|
416
405
|
model: modelTypes(modelOf(room.locator, 'typed-selection')),
|
|
417
406
|
},
|
|
418
407
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
@@ -429,9 +418,6 @@ export function composeRuntime(options) {
|
|
|
429
418
|
discovered.clear();
|
|
430
419
|
for (const room of listed)
|
|
431
420
|
discovered.set(room.locator, room);
|
|
432
|
-
for (const locator of wanted.keys())
|
|
433
|
-
if (!listedLocators.has(locator))
|
|
434
|
-
wanted.delete(locator);
|
|
435
421
|
await dropUnlisted(listedLocators, local);
|
|
436
422
|
for (const room of listed) {
|
|
437
423
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
@@ -465,6 +451,7 @@ export function composeRuntime(options) {
|
|
|
465
451
|
rooms.delete(locator);
|
|
466
452
|
roomDrafts.delete(locator);
|
|
467
453
|
unreadable.delete(locator);
|
|
454
|
+
stale.delete(locator);
|
|
468
455
|
scopedRefusals.delete(locator);
|
|
469
456
|
sourceRefusals.delete(locator);
|
|
470
457
|
declarationRefusals.delete(locator);
|
|
@@ -473,7 +460,6 @@ export function composeRuntime(options) {
|
|
|
473
460
|
models.delete(key);
|
|
474
461
|
liveModels.delete(key);
|
|
475
462
|
invalidModels.delete(key);
|
|
476
|
-
modelEpoch.delete(key);
|
|
477
463
|
}
|
|
478
464
|
journal.record(`room unlisted ${locator}`);
|
|
479
465
|
}
|
|
@@ -486,7 +472,6 @@ export function composeRuntime(options) {
|
|
|
486
472
|
if (prior !== undefined &&
|
|
487
473
|
prior.status !== 'unavailable' &&
|
|
488
474
|
(persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
|
|
489
|
-
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
490
475
|
invalidModels.add(key);
|
|
491
476
|
}
|
|
492
477
|
const model = persisted.status === 'unavailable' ||
|
|
@@ -541,7 +526,7 @@ export function composeRuntime(options) {
|
|
|
541
526
|
record: retyped,
|
|
542
527
|
collection: parseCollection(retyped.documents, compositionOf(retyped)),
|
|
543
528
|
};
|
|
544
|
-
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [],
|
|
529
|
+
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
|
|
545
530
|
const existing = rooms.get(record.locator);
|
|
546
531
|
if (existing === undefined)
|
|
547
532
|
list(record, false);
|
|
@@ -552,13 +537,11 @@ export function composeRuntime(options) {
|
|
|
552
537
|
attachments: record.attachments,
|
|
553
538
|
operations: record.operations,
|
|
554
539
|
replica: record.replica,
|
|
555
|
-
attachedModel: record.attachedModel,
|
|
556
540
|
model: modelTypes(model),
|
|
557
541
|
typedSelection: record.typedSelection === null
|
|
558
542
|
? null
|
|
559
543
|
: {
|
|
560
544
|
roots: record.typedSelection.roots,
|
|
561
|
-
attachedModel: record.typedSelection.attachedModel,
|
|
562
545
|
model: modelTypes(selectionModel),
|
|
563
546
|
},
|
|
564
547
|
});
|
|
@@ -626,7 +609,7 @@ export function composeRuntime(options) {
|
|
|
626
609
|
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
627
610
|
}
|
|
628
611
|
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
629
|
-
const withdrawn = withdrawals(confirmed, queue, withdrawable
|
|
612
|
+
const withdrawn = withdrawals(confirmed, queue, withdrawable);
|
|
630
613
|
const kept = queue.filter((batch) => !withdrawn.includes(batch));
|
|
631
614
|
const returning = [
|
|
632
615
|
...withdrawn.map((batch) => ({ batch, returned: { kind: 'withdrawn' } })).reverse(),
|
|
@@ -662,42 +645,73 @@ export function composeRuntime(options) {
|
|
|
662
645
|
rememberRoomDraft(record);
|
|
663
646
|
for (const batch of withdrawn)
|
|
664
647
|
journal.record(`outcome ${locator} ${batch.id} withdrawn`);
|
|
665
|
-
replicas.set(locator, replicaOf(confirmed, kept,
|
|
648
|
+
replicas.set(locator, replicaOf(confirmed, kept, before));
|
|
666
649
|
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
667
650
|
room: settling.room,
|
|
668
651
|
queue: kept.length !== (before?.queue.length ?? 0),
|
|
669
652
|
drafts: [...written.keys()],
|
|
670
653
|
});
|
|
671
654
|
}
|
|
672
|
-
function
|
|
655
|
+
function adoptModels(locator, record) {
|
|
656
|
+
for (const [attached, model] of [
|
|
657
|
+
['collection', record.model],
|
|
658
|
+
['typed-selection', record.selectionModel],
|
|
659
|
+
]) {
|
|
660
|
+
const key = modelKey(locator, attached);
|
|
661
|
+
models.set(key, model);
|
|
662
|
+
invalidModels.delete(key);
|
|
663
|
+
if (model.status === 'current')
|
|
664
|
+
liveModels.set(key, JSON.stringify(model.snapshot));
|
|
665
|
+
else if (model.status === 'unavailable')
|
|
666
|
+
liveModels.delete(key);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function remodel(locator, answers, held) {
|
|
670
|
+
const taken = (answer, previous, attached) => {
|
|
671
|
+
if (answer === undefined)
|
|
672
|
+
return previous;
|
|
673
|
+
const { state, refused } = attachedModel(answer, previous);
|
|
674
|
+
if (refused !== null) {
|
|
675
|
+
const where = refused.location === undefined ? '' : ` ${refused.location}`;
|
|
676
|
+
journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
|
|
677
|
+
}
|
|
678
|
+
models.set(modelKey(locator, attached), state);
|
|
679
|
+
return state;
|
|
680
|
+
};
|
|
681
|
+
return {
|
|
682
|
+
collection: taken(answers?.collection, held.collection, 'collection'),
|
|
683
|
+
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
function publish(local, room, confirmed, base) {
|
|
673
687
|
return exclusive(async () => {
|
|
674
688
|
if (!continuing())
|
|
675
|
-
return;
|
|
689
|
+
return true;
|
|
676
690
|
const prior = replicas.get(room.locator);
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
throw new Error(`Cannot retype staged Room ${room.locator}`);
|
|
691
|
+
if (base !== undefined &&
|
|
692
|
+
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
693
|
+
prior.confirmed.record.revisions !== base.record.revisions)) {
|
|
694
|
+
return false;
|
|
695
|
+
}
|
|
696
|
+
adoptModels(room.locator, confirmed.record);
|
|
684
697
|
const queue = prior?.queue ?? [];
|
|
685
|
-
const roomVersion =
|
|
698
|
+
const roomVersion = confirmed.record.roomVersion;
|
|
686
699
|
const accepted = queue.filter((batch) => {
|
|
687
700
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
688
701
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
689
702
|
});
|
|
690
|
-
const set = await settle(local, room.locator, prior,
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
703
|
+
const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
|
|
704
|
+
for (const batch of accepted)
|
|
705
|
+
pendings.delete(batch.id);
|
|
706
|
+
stale.delete(room.locator);
|
|
707
|
+
if (unreadable.delete(room.locator))
|
|
708
|
+
scopedRefusals.delete(room.locator);
|
|
696
709
|
sourceRefusals.delete(room.locator);
|
|
697
710
|
declarationRefusals.delete(room.locator);
|
|
698
711
|
list(room, true);
|
|
699
712
|
publishChangeSet(set);
|
|
700
713
|
moveTo({ ...status, ...attention() });
|
|
714
|
+
return true;
|
|
701
715
|
});
|
|
702
716
|
}
|
|
703
717
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
@@ -726,7 +740,6 @@ export function composeRuntime(options) {
|
|
|
726
740
|
operations: room.operations,
|
|
727
741
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
728
742
|
providerLimits: limits,
|
|
729
|
-
rewriting: rewriting(),
|
|
730
743
|
visible: replica.visible,
|
|
731
744
|
changes: parts.flatMap(({ changes }) => changes),
|
|
732
745
|
bases: parts.flatMap(({ bases }) => bases),
|
|
@@ -750,15 +763,16 @@ export function composeRuntime(options) {
|
|
|
750
763
|
return { outcome: 'refused', rule: 'durable-write' };
|
|
751
764
|
}
|
|
752
765
|
pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
|
|
766
|
+
recorded = false;
|
|
753
767
|
for (const path of consumed)
|
|
754
768
|
forgetRoomDraft(locator, path);
|
|
755
|
-
replicas.set(locator, replicaOf(replica.confirmed, batches,
|
|
769
|
+
replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
|
|
756
770
|
publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
|
|
757
771
|
queue: true,
|
|
758
772
|
drafts: consumed,
|
|
759
773
|
}));
|
|
760
774
|
moveTo({ ...saving(status), ...attention() });
|
|
761
|
-
|
|
775
|
+
submitLater(locator);
|
|
762
776
|
return { outcome: 'saved', identifier: built.batch.id };
|
|
763
777
|
}
|
|
764
778
|
async function mark(local, locator, identifier, submission, lost) {
|
|
@@ -823,10 +837,10 @@ export function composeRuntime(options) {
|
|
|
823
837
|
return { kind: 'transport-uncertainty' };
|
|
824
838
|
}
|
|
825
839
|
}
|
|
826
|
-
function
|
|
840
|
+
function declaredAgents() {
|
|
827
841
|
for (const capability of capabilities) {
|
|
828
|
-
if (capability.
|
|
829
|
-
return capability.
|
|
842
|
+
if (capability.conversationAgents !== undefined)
|
|
843
|
+
return capability.conversationAgents;
|
|
830
844
|
}
|
|
831
845
|
return undefined;
|
|
832
846
|
}
|
|
@@ -857,8 +871,6 @@ export function composeRuntime(options) {
|
|
|
857
871
|
journal.record(`transfer action ${name}`);
|
|
858
872
|
try {
|
|
859
873
|
const result = await options.adapter.action(authorization, { name, input });
|
|
860
|
-
if (result.kind === 'action' && result.at !== undefined)
|
|
861
|
-
want(result.at);
|
|
862
874
|
if (result.kind === 'scoped-refusal') {
|
|
863
875
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
864
876
|
if (room !== undefined)
|
|
@@ -890,32 +902,11 @@ export function composeRuntime(options) {
|
|
|
890
902
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
891
903
|
!options.adapter.reached(locator, roomVersion, written));
|
|
892
904
|
}
|
|
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) {
|
|
905
|
+
async function pullRefresh(authorized, confirmed) {
|
|
915
906
|
if (!continuing())
|
|
916
907
|
return unconfirmed;
|
|
917
908
|
try {
|
|
918
|
-
const result = await refreshRoom(options.adapter, authorized, confirmed,
|
|
909
|
+
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
|
|
919
910
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
920
911
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
921
912
|
return result;
|
|
@@ -932,7 +923,7 @@ export function composeRuntime(options) {
|
|
|
932
923
|
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
933
924
|
collection: modelOf(room.locator),
|
|
934
925
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
935
|
-
}, continuing);
|
|
926
|
+
}, (models, held) => remodel(room.locator, models, held), continuing);
|
|
936
927
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
937
928
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
938
929
|
return result;
|
|
@@ -945,20 +936,28 @@ export function composeRuntime(options) {
|
|
|
945
936
|
async function advance(room, local, authorized) {
|
|
946
937
|
const held = replicas.get(room.locator);
|
|
947
938
|
if (held !== undefined) {
|
|
948
|
-
const refreshed = await pullRefresh(authorized, held.confirmed
|
|
939
|
+
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
949
940
|
if (!continuing())
|
|
950
941
|
return unconfirmed;
|
|
951
942
|
if (refreshed.kind === 'still-current') {
|
|
952
943
|
if (sourceRefusals.has(room.locator))
|
|
953
944
|
return unconfirmed;
|
|
945
|
+
stale.delete(room.locator);
|
|
954
946
|
if (unreadable.delete(room.locator)) {
|
|
947
|
+
scopedRefusals.delete(room.locator);
|
|
955
948
|
list(room, true);
|
|
956
949
|
emitRooms();
|
|
957
950
|
}
|
|
958
951
|
return null;
|
|
959
952
|
}
|
|
960
953
|
if (refreshed.kind === 'advanced') {
|
|
961
|
-
await publish(local, room, refreshed.confirmed)
|
|
954
|
+
if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
|
|
955
|
+
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
956
|
+
const reached = now !== undefined &&
|
|
957
|
+
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
958
|
+
return reached ? null : advance(room, local, authorized);
|
|
959
|
+
}
|
|
960
|
+
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
962
961
|
return null;
|
|
963
962
|
}
|
|
964
963
|
if (refreshed.kind !== 'beyond-horizon')
|
|
@@ -973,7 +972,7 @@ export function composeRuntime(options) {
|
|
|
973
972
|
return null;
|
|
974
973
|
}
|
|
975
974
|
async function withhold(withheld, room, local, progress) {
|
|
976
|
-
|
|
975
|
+
stale.add(room.locator);
|
|
977
976
|
if (withheld.kind === 'scoped-refusal') {
|
|
978
977
|
await scoped(withheld, room, local);
|
|
979
978
|
}
|
|
@@ -1020,44 +1019,30 @@ export function composeRuntime(options) {
|
|
|
1020
1019
|
(accepted.author === undefined ? '' : ` author ${accepted.author}`) +
|
|
1021
1020
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1022
1021
|
}
|
|
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) {
|
|
1022
|
+
async function adoptAccepted(room, local, submitted, accepted) {
|
|
1041
1023
|
if (!continuing())
|
|
1042
1024
|
return unconfirmed;
|
|
1043
1025
|
const current = replicas.get(room.locator);
|
|
1044
1026
|
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
1045
1027
|
return null;
|
|
1046
1028
|
}
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1029
|
+
const held = current.confirmed.record.roomVersion;
|
|
1030
|
+
logAccepted(room.locator, submitted, accepted);
|
|
1031
|
+
const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
1032
|
+
? { kind: 'staged', confirmed: current.confirmed }
|
|
1033
|
+
: applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
1034
|
+
? accepted.roomVersion
|
|
1035
|
+
: held, accepted.roomVersion);
|
|
1036
|
+
if (applied.kind !== 'staged') {
|
|
1037
|
+
const state = pendings.get(submitted.id);
|
|
1038
|
+
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1039
|
+
if (state !== undefined)
|
|
1040
|
+
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1041
|
+
emitQueue(room.locator);
|
|
1057
1042
|
return applied;
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1043
|
+
}
|
|
1044
|
+
const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
1045
|
+
pendings.delete(submitted.id);
|
|
1061
1046
|
unreadable.delete(room.locator);
|
|
1062
1047
|
sourceRefusals.delete(room.locator);
|
|
1063
1048
|
declarationRefusals.delete(room.locator);
|
|
@@ -1066,17 +1051,8 @@ export function composeRuntime(options) {
|
|
|
1066
1051
|
moveTo({ ...status, ...attention() });
|
|
1067
1052
|
return null;
|
|
1068
1053
|
}
|
|
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));
|
|
1054
|
+
async function adopted(room, local, submitted, accepted) {
|
|
1055
|
+
const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
|
|
1080
1056
|
if (failed === null)
|
|
1081
1057
|
return true;
|
|
1082
1058
|
if (failed.kind === 'refused' && continuing())
|
|
@@ -1089,10 +1065,8 @@ export function composeRuntime(options) {
|
|
|
1089
1065
|
const head = replica?.queue[0];
|
|
1090
1066
|
if (replica === undefined || head === undefined)
|
|
1091
1067
|
return;
|
|
1092
|
-
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1093
|
-
progress.complete = false;
|
|
1068
|
+
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1094
1069
|
return;
|
|
1095
|
-
}
|
|
1096
1070
|
const state = pendings.get(head.id);
|
|
1097
1071
|
const { submission, lost: earlier } = state ?? {
|
|
1098
1072
|
submission: { state: 'waiting' },
|
|
@@ -1120,6 +1094,7 @@ export function composeRuntime(options) {
|
|
|
1120
1094
|
return;
|
|
1121
1095
|
}
|
|
1122
1096
|
const opensBefore = openSignals;
|
|
1097
|
+
const answered = awaitingAnswer(room.locator);
|
|
1123
1098
|
try {
|
|
1124
1099
|
result = await options.adapter.mutate(authorized, {
|
|
1125
1100
|
locator: room.locator,
|
|
@@ -1128,11 +1103,14 @@ export function composeRuntime(options) {
|
|
|
1128
1103
|
}, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
|
|
1129
1104
|
}
|
|
1130
1105
|
catch {
|
|
1106
|
+
answered();
|
|
1131
1107
|
progress.complete = false;
|
|
1132
1108
|
if (continuing())
|
|
1133
1109
|
progress.reachable = false;
|
|
1134
1110
|
return;
|
|
1135
1111
|
}
|
|
1112
|
+
if (result.kind !== 'accepted' || !continuing())
|
|
1113
|
+
answered();
|
|
1136
1114
|
if (!continuing())
|
|
1137
1115
|
return;
|
|
1138
1116
|
if (result.kind === 'transport-uncertainty') {
|
|
@@ -1185,6 +1163,7 @@ export function composeRuntime(options) {
|
|
|
1185
1163
|
const reset = result.reset;
|
|
1186
1164
|
progress.complete = false;
|
|
1187
1165
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1166
|
+
armTimer();
|
|
1188
1167
|
return;
|
|
1189
1168
|
}
|
|
1190
1169
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
@@ -1211,95 +1190,33 @@ export function composeRuntime(options) {
|
|
|
1211
1190
|
return withhold(withheld, room, local, progress);
|
|
1212
1191
|
continue;
|
|
1213
1192
|
}
|
|
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
|
-
}
|
|
1193
|
+
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1194
|
+
if (!kept) {
|
|
1258
1195
|
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
1196
|
return;
|
|
1271
1197
|
}
|
|
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
1198
|
}
|
|
1289
|
-
|
|
1199
|
+
}
|
|
1200
|
+
function awaitingAnswer(locator) {
|
|
1201
|
+
let release = () => { };
|
|
1202
|
+
const awaited = new Promise((resolve) => {
|
|
1203
|
+
release = resolve;
|
|
1204
|
+
});
|
|
1205
|
+
answering.set(locator, awaited);
|
|
1206
|
+
return () => {
|
|
1207
|
+
if (answering.get(locator) === awaited)
|
|
1208
|
+
answering.delete(locator);
|
|
1209
|
+
release();
|
|
1210
|
+
};
|
|
1290
1211
|
}
|
|
1291
1212
|
async function retainModel(local, room, attached, next) {
|
|
1292
1213
|
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
1214
|
const previous = modelOf(room.locator, attached);
|
|
1297
1215
|
const heldBefore = replicas.get(room.locator)?.confirmed.record;
|
|
1298
1216
|
if (JSON.stringify(previous) === JSON.stringify(next) &&
|
|
1299
1217
|
(heldBefore === undefined ||
|
|
1300
1218
|
(JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
|
|
1301
1219
|
JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
|
|
1302
|
-
JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) &&
|
|
1303
1220
|
JSON.stringify(heldBefore.typedSelection) ===
|
|
1304
1221
|
JSON.stringify(room.typedSelection ?? null))))
|
|
1305
1222
|
return;
|
|
@@ -1310,15 +1227,12 @@ export function composeRuntime(options) {
|
|
|
1310
1227
|
await exclusive(async () => {
|
|
1311
1228
|
if (!continuing())
|
|
1312
1229
|
return;
|
|
1313
|
-
if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0))
|
|
1314
|
-
return;
|
|
1315
1230
|
const held = replicas.get(room.locator);
|
|
1316
1231
|
if (held !== undefined) {
|
|
1317
1232
|
const declared = {
|
|
1318
1233
|
...held.confirmed.record,
|
|
1319
1234
|
operations: room.operations,
|
|
1320
1235
|
replica: room.replica,
|
|
1321
|
-
attachedModel: room.attachedModel,
|
|
1322
1236
|
typedSelection: room.typedSelection ?? null,
|
|
1323
1237
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
1324
1238
|
};
|
|
@@ -1338,7 +1252,7 @@ export function composeRuntime(options) {
|
|
|
1338
1252
|
queue: held.queue,
|
|
1339
1253
|
});
|
|
1340
1254
|
if (read.kind === 'staged') {
|
|
1341
|
-
replicas.set(room.locator, replicaOf(read.confirmed, held.queue,
|
|
1255
|
+
replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
|
|
1342
1256
|
if (declarationRefusals.delete(room.locator)) {
|
|
1343
1257
|
sourceRefusals.delete(room.locator);
|
|
1344
1258
|
unreadable.delete(room.locator);
|
|
@@ -1362,77 +1276,6 @@ export function composeRuntime(options) {
|
|
|
1362
1276
|
publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
|
|
1363
1277
|
});
|
|
1364
1278
|
}
|
|
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
1279
|
async function scoped(result, room, local) {
|
|
1437
1280
|
scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
|
|
1438
1281
|
let scopedAway = room;
|
|
@@ -1460,14 +1303,34 @@ export function composeRuntime(options) {
|
|
|
1460
1303
|
emitRooms();
|
|
1461
1304
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
1462
1305
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1306
|
+
function heldRooms(listed) {
|
|
1307
|
+
return listed.flatMap((room) => {
|
|
1308
|
+
const record = replicas.get(room.locator)?.confirmed.record;
|
|
1309
|
+
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1310
|
+
return [];
|
|
1311
|
+
return [
|
|
1312
|
+
{
|
|
1313
|
+
locator: room.locator,
|
|
1314
|
+
roomVersion: record.roomVersion,
|
|
1315
|
+
documents: record.documents,
|
|
1316
|
+
revisions: record.revisions,
|
|
1317
|
+
attachments: record.attachments,
|
|
1318
|
+
},
|
|
1319
|
+
];
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
async function synchronize(listed, local, progress, attempt) {
|
|
1323
|
+
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
|
|
1324
|
+
const ready = listed.filter((_, index) => {
|
|
1466
1325
|
const outcome = prepared[index];
|
|
1467
1326
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1468
1327
|
});
|
|
1469
|
-
|
|
1470
|
-
|
|
1328
|
+
if (attempt === 'opened')
|
|
1329
|
+
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
1330
|
+
const loaded = await Promise.allSettled(ready
|
|
1331
|
+
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1332
|
+
.map((room) => loadRoom(room, local, progress)));
|
|
1333
|
+
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1471
1334
|
if (failed !== undefined)
|
|
1472
1335
|
throw failed.reason;
|
|
1473
1336
|
}
|
|
@@ -1484,11 +1347,8 @@ export function composeRuntime(options) {
|
|
|
1484
1347
|
progress.complete = false;
|
|
1485
1348
|
return false;
|
|
1486
1349
|
}
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
acquireModel(local, room, authorization, null),
|
|
1490
|
-
...(selection === undefined ? [] : [acquireModel(local, room, authorization, selection)]),
|
|
1491
|
-
]);
|
|
1350
|
+
if (prior !== undefined)
|
|
1351
|
+
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1492
1352
|
if (discoveryPaused || authorization === undefined)
|
|
1493
1353
|
return false;
|
|
1494
1354
|
if (room.replica.admission !== 'admitted') {
|
|
@@ -1496,24 +1356,53 @@ export function composeRuntime(options) {
|
|
|
1496
1356
|
progress.complete = false;
|
|
1497
1357
|
return false;
|
|
1498
1358
|
}
|
|
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
1359
|
return true;
|
|
1505
1360
|
}
|
|
1506
|
-
async function
|
|
1361
|
+
async function caughtUp(room, local, progress) {
|
|
1362
|
+
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1363
|
+
if (held === undefined || authorization === undefined)
|
|
1364
|
+
return;
|
|
1365
|
+
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
1366
|
+
if (!behind)
|
|
1367
|
+
return;
|
|
1368
|
+
const withheld = await advance(room, local, authorization);
|
|
1369
|
+
if (withheld !== null)
|
|
1370
|
+
await withhold(withheld, room, local, progress);
|
|
1371
|
+
}
|
|
1372
|
+
function revoked(locator) {
|
|
1373
|
+
return ['collection', 'typed-selection'].some((attached) => {
|
|
1374
|
+
const model = modelOf(locator, attached);
|
|
1375
|
+
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
async function loadRoom(room, local, progress) {
|
|
1507
1379
|
if (discoveryPaused || authorization === undefined)
|
|
1508
1380
|
return;
|
|
1381
|
+
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1382
|
+
await reload(room, local, progress);
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1509
1385
|
const withheld = await advance(room, local, authorization);
|
|
1510
1386
|
if (withheld !== null) {
|
|
1511
1387
|
await withhold(withheld, room, local, progress);
|
|
1512
1388
|
return;
|
|
1513
1389
|
}
|
|
1514
|
-
await
|
|
1390
|
+
await caughtUp(room, local, progress);
|
|
1515
1391
|
}
|
|
1516
|
-
async function
|
|
1392
|
+
async function reload(room, local, progress) {
|
|
1393
|
+
if (discoveryPaused || authorization === undefined)
|
|
1394
|
+
return;
|
|
1395
|
+
const bootstrapped = await pullBootstrap(authorization, room);
|
|
1396
|
+
if (!continuing())
|
|
1397
|
+
return;
|
|
1398
|
+
if (bootstrapped.kind !== 'staged') {
|
|
1399
|
+
await withhold(bootstrapped, room, local, progress);
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
await publish(local, room, bootstrapped.confirmed);
|
|
1403
|
+
await caughtUp(room, local, progress);
|
|
1404
|
+
}
|
|
1405
|
+
async function discoverRooms(local, progress, attempt) {
|
|
1517
1406
|
if (closed || authorization === undefined) {
|
|
1518
1407
|
notifyReady();
|
|
1519
1408
|
return;
|
|
@@ -1539,6 +1428,10 @@ export function composeRuntime(options) {
|
|
|
1539
1428
|
}
|
|
1540
1429
|
}
|
|
1541
1430
|
progress.reachable = false;
|
|
1431
|
+
progress.complete = false;
|
|
1432
|
+
if (attempt === 'opened') {
|
|
1433
|
+
options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
|
|
1434
|
+
}
|
|
1542
1435
|
notifyReady();
|
|
1543
1436
|
return;
|
|
1544
1437
|
}
|
|
@@ -1553,17 +1446,6 @@ export function composeRuntime(options) {
|
|
|
1553
1446
|
notifyReady();
|
|
1554
1447
|
return;
|
|
1555
1448
|
}
|
|
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
1449
|
for (const room of result.rooms) {
|
|
1568
1450
|
if (room.operations === undefined ||
|
|
1569
1451
|
!['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
@@ -1571,8 +1453,7 @@ export function composeRuntime(options) {
|
|
|
1571
1453
|
room.replica === undefined ||
|
|
1572
1454
|
typeof room.replica.selection !== 'string' ||
|
|
1573
1455
|
room.replica.selection.length === 0 ||
|
|
1574
|
-
!['admitted', 'unavailable'].includes(room.replica.admission)
|
|
1575
|
-
!declaredIdentity(room.attachedModel)) {
|
|
1456
|
+
!['admitted', 'unavailable'].includes(room.replica.admission)) {
|
|
1576
1457
|
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
1577
1458
|
await refuse('missing-required-guarantee');
|
|
1578
1459
|
notifyReady();
|
|
@@ -1585,14 +1466,6 @@ export function composeRuntime(options) {
|
|
|
1585
1466
|
return;
|
|
1586
1467
|
}
|
|
1587
1468
|
}
|
|
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
1469
|
if (refusal !== null) {
|
|
1597
1470
|
refusal = null;
|
|
1598
1471
|
emitRefusal();
|
|
@@ -1600,26 +1473,13 @@ export function composeRuntime(options) {
|
|
|
1600
1473
|
const previousCapabilities = capabilities;
|
|
1601
1474
|
capabilities = result.capabilities;
|
|
1602
1475
|
notifyReady();
|
|
1603
|
-
if (declared.has('room-version-notifications'))
|
|
1604
|
-
observeRoomVersions();
|
|
1605
1476
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1606
1477
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
1607
1478
|
limits = result.providerLimits;
|
|
1608
1479
|
await local.declare(limits, capabilities);
|
|
1609
1480
|
}
|
|
1610
1481
|
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);
|
|
1482
|
+
await synchronize(result.rooms, local, progress, attempt);
|
|
1623
1483
|
}
|
|
1624
1484
|
async function ensureAuthorization() {
|
|
1625
1485
|
if (authorization !== undefined)
|
|
@@ -1633,64 +1493,246 @@ export function composeRuntime(options) {
|
|
|
1633
1493
|
throw error;
|
|
1634
1494
|
}
|
|
1635
1495
|
}
|
|
1636
|
-
|
|
1637
|
-
if (
|
|
1638
|
-
|
|
1496
|
+
function task(work) {
|
|
1497
|
+
if (stopping)
|
|
1498
|
+
return chain;
|
|
1499
|
+
chain = chain.then(work, work).catch(() => undefined);
|
|
1500
|
+
return chain;
|
|
1501
|
+
}
|
|
1502
|
+
function sync(attempt) {
|
|
1503
|
+
return task(async () => {
|
|
1504
|
+
if (closed || stopping || discoveryPaused) {
|
|
1505
|
+
notifyReady();
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
const progress = { complete: true, reachable: true };
|
|
1509
|
+
underway = attempt === 'opened' ? progress : null;
|
|
1510
|
+
complete = false;
|
|
1511
|
+
recorded = false;
|
|
1512
|
+
moveTo(transferring(status));
|
|
1513
|
+
try {
|
|
1514
|
+
const local = await openLocal();
|
|
1515
|
+
if (closed)
|
|
1516
|
+
return;
|
|
1517
|
+
await ensureAuthorization();
|
|
1518
|
+
if (closed)
|
|
1519
|
+
return;
|
|
1520
|
+
await discoverRooms(local, progress, attempt);
|
|
1521
|
+
}
|
|
1522
|
+
catch {
|
|
1523
|
+
progress.complete = false;
|
|
1524
|
+
if (attempt === 'opened') {
|
|
1525
|
+
options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
finally {
|
|
1529
|
+
notifyReady();
|
|
1530
|
+
}
|
|
1531
|
+
if (attempt === 'opened')
|
|
1532
|
+
return;
|
|
1533
|
+
await finishOpening(progress);
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
function closedSocket(revoked) {
|
|
1537
|
+
const opened = socketOpen;
|
|
1538
|
+
socketOpen = false;
|
|
1539
|
+
if (revoked) {
|
|
1540
|
+
void refuse('revoked-authorization');
|
|
1639
1541
|
return;
|
|
1640
1542
|
}
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
if (closed)
|
|
1543
|
+
if (opened || status.providerUnreachable)
|
|
1544
|
+
return;
|
|
1545
|
+
moveTo({ ...status, providerUnreachable: true });
|
|
1546
|
+
}
|
|
1547
|
+
function applyOpening(expired) {
|
|
1548
|
+
return task(async () => {
|
|
1549
|
+
const progress = underway;
|
|
1550
|
+
underway = null;
|
|
1551
|
+
if (closed || stopping)
|
|
1650
1552
|
return;
|
|
1651
|
-
|
|
1652
|
-
|
|
1553
|
+
if (progress === null) {
|
|
1554
|
+
await reloadExpired(expired);
|
|
1653
1555
|
return;
|
|
1654
|
-
if (progress.complete) {
|
|
1655
|
-
completedAt = new Date().toISOString();
|
|
1656
|
-
await local.complete(completedAt);
|
|
1657
1556
|
}
|
|
1557
|
+
try {
|
|
1558
|
+
const local = await openLocal();
|
|
1559
|
+
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
1560
|
+
const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
|
|
1561
|
+
const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
|
|
1562
|
+
if (progress.reachable) {
|
|
1563
|
+
for (const room of held)
|
|
1564
|
+
if (!reloading.includes(room))
|
|
1565
|
+
await confirmModels(room, local);
|
|
1566
|
+
}
|
|
1567
|
+
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1568
|
+
if (failed !== undefined)
|
|
1569
|
+
throw failed.reason;
|
|
1570
|
+
}
|
|
1571
|
+
catch {
|
|
1572
|
+
progress.complete = false;
|
|
1573
|
+
progress.reachable = false;
|
|
1574
|
+
}
|
|
1575
|
+
await finishOpening(progress);
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
async function reloadExpired(expired) {
|
|
1579
|
+
const progress = { complete: true, reachable: true };
|
|
1580
|
+
try {
|
|
1581
|
+
const local = await openLocal();
|
|
1582
|
+
await Promise.allSettled([...discovered.values()]
|
|
1583
|
+
.filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
|
|
1584
|
+
.map((room) => reload(room, local, progress)));
|
|
1658
1585
|
}
|
|
1659
1586
|
catch {
|
|
1660
|
-
progress.
|
|
1661
|
-
|
|
1587
|
+
progress.reachable = false;
|
|
1588
|
+
}
|
|
1589
|
+
await rest(progress);
|
|
1590
|
+
}
|
|
1591
|
+
async function confirmModels(room, local) {
|
|
1592
|
+
for (const attached of ['collection', 'typed-selection']) {
|
|
1593
|
+
const model = modelOf(room.locator, attached);
|
|
1594
|
+
if (model.status === 'cached') {
|
|
1595
|
+
await retainModel(local, room, attached, { ...model, status: 'current' });
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
function submitLater(locator) {
|
|
1600
|
+
const running = submitting.get(locator);
|
|
1601
|
+
if (running !== undefined) {
|
|
1602
|
+
running.again = true;
|
|
1662
1603
|
return;
|
|
1663
1604
|
}
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1605
|
+
const run = { again: false, done: Promise.resolve() };
|
|
1606
|
+
submitting.set(locator, run);
|
|
1607
|
+
run.done = (async () => {
|
|
1608
|
+
try {
|
|
1609
|
+
do {
|
|
1610
|
+
run.again = false;
|
|
1611
|
+
const room = discovered.get(locator);
|
|
1612
|
+
if (closed || stopping || discoveryPaused || room === undefined)
|
|
1613
|
+
return;
|
|
1614
|
+
const progress = { complete: true, reachable: true };
|
|
1615
|
+
try {
|
|
1616
|
+
await ensureAuthorization();
|
|
1617
|
+
if (authorization === undefined)
|
|
1618
|
+
return;
|
|
1619
|
+
await submitRoom(room, await openLocal(), authorization, progress);
|
|
1620
|
+
}
|
|
1621
|
+
catch {
|
|
1622
|
+
progress.reachable = false;
|
|
1623
|
+
}
|
|
1624
|
+
armTimer();
|
|
1625
|
+
await rest(progress);
|
|
1626
|
+
} while (run.again);
|
|
1627
|
+
}
|
|
1628
|
+
finally {
|
|
1629
|
+
submitting.delete(locator);
|
|
1630
|
+
}
|
|
1631
|
+
})();
|
|
1632
|
+
}
|
|
1633
|
+
function submitAll() {
|
|
1634
|
+
for (const [locator, replica] of replicas) {
|
|
1635
|
+
if (replica.queue.length > 0)
|
|
1636
|
+
submitLater(locator);
|
|
1667
1637
|
}
|
|
1668
1638
|
}
|
|
1669
|
-
async function
|
|
1639
|
+
async function finishOpening(progress) {
|
|
1670
1640
|
try {
|
|
1671
|
-
|
|
1641
|
+
if (closed || discoveryPaused) {
|
|
1642
|
+
progress.complete = false;
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
submitAll();
|
|
1646
|
+
await conversations.reconcile();
|
|
1647
|
+
if (status.providerUnreachable)
|
|
1648
|
+
conversations.reconnected();
|
|
1649
|
+
const reading = { complete: true, reachable: true };
|
|
1650
|
+
await conversations.catchUp(reading);
|
|
1651
|
+
if (!reading.reachable)
|
|
1652
|
+
progress.reachable = false;
|
|
1653
|
+
}
|
|
1654
|
+
catch {
|
|
1655
|
+
progress.complete = false;
|
|
1672
1656
|
}
|
|
1673
1657
|
finally {
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
queued = false;
|
|
1677
|
-
void enqueue();
|
|
1678
|
-
}
|
|
1658
|
+
complete = progress.complete;
|
|
1659
|
+
await rest(progress);
|
|
1679
1660
|
}
|
|
1680
1661
|
}
|
|
1681
|
-
function
|
|
1682
|
-
|
|
1683
|
-
if (path === undefined || withdrawNotifications !== undefined)
|
|
1662
|
+
async function rest(progress) {
|
|
1663
|
+
if (closed)
|
|
1684
1664
|
return;
|
|
1685
|
-
|
|
1686
|
-
|
|
1665
|
+
let lastCompleted = status.lastCompleted;
|
|
1666
|
+
let synced = complete && stale.size === 0 && !pending();
|
|
1667
|
+
if (synced && !recorded) {
|
|
1668
|
+
const completedAt = new Date().toISOString();
|
|
1669
|
+
try {
|
|
1670
|
+
await (await openLocal()).complete(completedAt);
|
|
1671
|
+
recorded = true;
|
|
1672
|
+
lastCompleted = completedAt;
|
|
1673
|
+
}
|
|
1674
|
+
catch {
|
|
1675
|
+
synced = false;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
moveTo({
|
|
1679
|
+
state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
|
|
1680
|
+
lastCompleted,
|
|
1681
|
+
providerUnreachable: !progress.reachable,
|
|
1682
|
+
...attention(),
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
function applyRoom(locator, roomVersion) {
|
|
1686
|
+
announced.set(locator, roomVersion);
|
|
1687
|
+
return task(async () => {
|
|
1688
|
+
await answering.get(locator);
|
|
1689
|
+
if (closed || stopping || discoveryPaused || authorization === undefined)
|
|
1690
|
+
return;
|
|
1691
|
+
const room = discovered.get(locator);
|
|
1692
|
+
const held = replicas.get(locator);
|
|
1693
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
1687
1694
|
return;
|
|
1688
|
-
if (
|
|
1689
|
-
|
|
1695
|
+
if (held !== undefined &&
|
|
1696
|
+
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
1697
|
+
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
const during = underway;
|
|
1701
|
+
const progress = during ?? { complete: true, reachable: true };
|
|
1702
|
+
if (during === null)
|
|
1703
|
+
moveTo(transferring(status));
|
|
1704
|
+
try {
|
|
1705
|
+
const local = await openLocal();
|
|
1706
|
+
const withheld = await advance(room, local, authorization);
|
|
1707
|
+
if (withheld !== null)
|
|
1708
|
+
await withhold(withheld, room, local, progress);
|
|
1709
|
+
}
|
|
1710
|
+
catch {
|
|
1711
|
+
stale.add(locator);
|
|
1712
|
+
}
|
|
1713
|
+
finally {
|
|
1714
|
+
if (during === null)
|
|
1715
|
+
await rest(progress);
|
|
1716
|
+
}
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
function observeStream() {
|
|
1720
|
+
const stream = options.adapter.roomStream;
|
|
1721
|
+
if (stream === undefined || withdrawStream !== undefined)
|
|
1722
|
+
return;
|
|
1723
|
+
withdrawStream = stream.observe((event) => {
|
|
1724
|
+
if (closed || stopping)
|
|
1690
1725
|
return;
|
|
1726
|
+
if (event.kind === 'opened') {
|
|
1727
|
+
socketOpen = true;
|
|
1728
|
+
void sync('opened');
|
|
1691
1729
|
}
|
|
1692
|
-
|
|
1693
|
-
|
|
1730
|
+
else if (event.kind === 'closed')
|
|
1731
|
+
closedSocket(event.revoked);
|
|
1732
|
+
else if (event.kind === 'opening')
|
|
1733
|
+
void applyOpening(event.expired);
|
|
1734
|
+
else
|
|
1735
|
+
void applyRoom(event.locator, event.roomVersion);
|
|
1694
1736
|
});
|
|
1695
1737
|
}
|
|
1696
1738
|
function writesHeld() {
|
|
@@ -1711,14 +1753,17 @@ export function composeRuntime(options) {
|
|
|
1711
1753
|
listener(mode);
|
|
1712
1754
|
}
|
|
1713
1755
|
if (releasing)
|
|
1714
|
-
|
|
1756
|
+
submitAll();
|
|
1715
1757
|
}
|
|
1716
1758
|
function observeConversations() {
|
|
1717
1759
|
const stream = options.adapter.conversations;
|
|
1718
1760
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
1719
1761
|
return;
|
|
1720
1762
|
withdrawConversations = stream.observe((event) => {
|
|
1721
|
-
conversations.receive(event)
|
|
1763
|
+
if (conversations.receive(event)) {
|
|
1764
|
+
void ready.then(scheduleCurrency);
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1722
1767
|
if (event.kind !== 'gap' || conversations.current())
|
|
1723
1768
|
return;
|
|
1724
1769
|
show();
|
|
@@ -1729,7 +1774,7 @@ export function composeRuntime(options) {
|
|
|
1729
1774
|
if (currencyQueued || closed || stopping)
|
|
1730
1775
|
return;
|
|
1731
1776
|
currencyQueued = true;
|
|
1732
|
-
|
|
1777
|
+
void task(runCurrency);
|
|
1733
1778
|
}
|
|
1734
1779
|
async function runCurrency() {
|
|
1735
1780
|
currencyQueued = false;
|
|
@@ -1755,95 +1800,12 @@ export function composeRuntime(options) {
|
|
|
1755
1800
|
}
|
|
1756
1801
|
}
|
|
1757
1802
|
}
|
|
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
1803
|
function close() {
|
|
1842
1804
|
if (closed)
|
|
1843
1805
|
return;
|
|
1844
1806
|
closed = true;
|
|
1845
|
-
|
|
1846
|
-
|
|
1807
|
+
withdrawStream?.();
|
|
1808
|
+
withdrawStream = undefined;
|
|
1847
1809
|
withdrawConversations?.();
|
|
1848
1810
|
withdrawConversations = undefined;
|
|
1849
1811
|
withdrawServiceMode?.();
|
|
@@ -1867,6 +1829,7 @@ export function composeRuntime(options) {
|
|
|
1867
1829
|
stopping = true;
|
|
1868
1830
|
stopTimer();
|
|
1869
1831
|
await chain.catch(() => undefined);
|
|
1832
|
+
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
1870
1833
|
await mutations;
|
|
1871
1834
|
close();
|
|
1872
1835
|
}
|
|
@@ -1875,11 +1838,29 @@ export function composeRuntime(options) {
|
|
|
1875
1838
|
options.signal?.addEventListener('abort', close, { once: true });
|
|
1876
1839
|
if (options.signal?.aborted)
|
|
1877
1840
|
close();
|
|
1878
|
-
|
|
1841
|
+
observeStream();
|
|
1842
|
+
void openLocal().then(() => {
|
|
1843
|
+
if (capabilities.length > 0)
|
|
1844
|
+
notifyReady();
|
|
1845
|
+
}, () => {
|
|
1879
1846
|
notifyReady();
|
|
1880
1847
|
});
|
|
1881
|
-
|
|
1882
|
-
|
|
1848
|
+
if (options.adapter.roomStream === undefined)
|
|
1849
|
+
void sync('unstreamed');
|
|
1850
|
+
function planned(input, reach) {
|
|
1851
|
+
return planOf(input, roomFacts(reach), planReach(reach));
|
|
1852
|
+
}
|
|
1853
|
+
function watchPlan(plan, deliver) {
|
|
1854
|
+
return readSide.watch(plan, identityOf(plan), deliver);
|
|
1855
|
+
}
|
|
1856
|
+
async function searched(input, reach) {
|
|
1857
|
+
const plan = searchPlanOf(input);
|
|
1858
|
+
const selected = plan.documents === undefined && reach === null
|
|
1859
|
+
? null
|
|
1860
|
+
: (await readSide.query(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
|
|
1861
|
+
return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
|
|
1862
|
+
}
|
|
1863
|
+
const watch = (topic, input, deliver) => {
|
|
1883
1864
|
if (topic === 'room-drafts') {
|
|
1884
1865
|
const listener = { notify: deliver, ready: false };
|
|
1885
1866
|
roomDraftListeners.add(listener);
|
|
@@ -1900,6 +1881,8 @@ export function composeRuntime(options) {
|
|
|
1900
1881
|
if (topic === 'conversation') {
|
|
1901
1882
|
return conversations.watch(input[0], input[1], deliver);
|
|
1902
1883
|
}
|
|
1884
|
+
if (topic === 'conversations')
|
|
1885
|
+
return conversations.watchList(deliver);
|
|
1903
1886
|
if (topic === 'presentation') {
|
|
1904
1887
|
const listener = {
|
|
1905
1888
|
app: input[0],
|
|
@@ -1962,9 +1945,8 @@ export function composeRuntime(options) {
|
|
|
1962
1945
|
queueSubscriptions.delete(queue);
|
|
1963
1946
|
};
|
|
1964
1947
|
}
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
}
|
|
1948
|
+
return watchPlan(planned(input[0], null), deliver);
|
|
1949
|
+
};
|
|
1968
1950
|
const md = {
|
|
1969
1951
|
loaded() {
|
|
1970
1952
|
return openLocal().then(() => undefined);
|
|
@@ -1972,15 +1954,8 @@ export function composeRuntime(options) {
|
|
|
1972
1954
|
rooms() {
|
|
1973
1955
|
return listed();
|
|
1974
1956
|
},
|
|
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
|
-
},
|
|
1957
|
+
query: ((input) => readSide.query(planned(input, null))),
|
|
1958
|
+
search: (input) => searched(input, null),
|
|
1984
1959
|
types(locator) {
|
|
1985
1960
|
return {
|
|
1986
1961
|
collection: modelTypes(modelOf(locator)),
|
|
@@ -2087,9 +2062,7 @@ export function composeRuntime(options) {
|
|
|
2087
2062
|
persistence() {
|
|
2088
2063
|
return options.shell.persistentStorage;
|
|
2089
2064
|
},
|
|
2090
|
-
observe: (
|
|
2091
|
-
deliver(structuredClone(message));
|
|
2092
|
-
}), topic === 'query')),
|
|
2065
|
+
observe: observing(watch),
|
|
2093
2066
|
async reauthorize() {
|
|
2094
2067
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2095
2068
|
return;
|
|
@@ -2098,8 +2071,19 @@ export function composeRuntime(options) {
|
|
|
2098
2071
|
await mutations;
|
|
2099
2072
|
authorization = undefined;
|
|
2100
2073
|
discoveryPaused = false;
|
|
2101
|
-
|
|
2074
|
+
if (options.adapter.roomStream === undefined)
|
|
2075
|
+
await sync('unstreamed');
|
|
2076
|
+
else
|
|
2077
|
+
await task(ensureAuthorization);
|
|
2102
2078
|
},
|
|
2103
2079
|
};
|
|
2104
|
-
|
|
2080
|
+
const reachable = {
|
|
2081
|
+
md,
|
|
2082
|
+
watch,
|
|
2083
|
+
plan: planned,
|
|
2084
|
+
query: (plan) => readSide.query(plan),
|
|
2085
|
+
watchPlan,
|
|
2086
|
+
search: searched,
|
|
2087
|
+
};
|
|
2088
|
+
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2105
2089
|
}
|