@agentshouse/mdruntime 0.1.0-cf28918c2bf91 → 0.1.0-cf6041a06849c
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 +499 -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 +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') {
|
|
943
|
+
stale.delete(room.locator);
|
|
952
944
|
if (sourceRefusals.has(room.locator))
|
|
953
|
-
return
|
|
945
|
+
return null;
|
|
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,10 @@ export function composeRuntime(options) {
|
|
|
973
972
|
return null;
|
|
974
973
|
}
|
|
975
974
|
async function withhold(withheld, room, local, progress) {
|
|
976
|
-
|
|
975
|
+
if (withheld.kind === 'refused')
|
|
976
|
+
stale.delete(room.locator);
|
|
977
|
+
else
|
|
978
|
+
stale.add(room.locator);
|
|
977
979
|
if (withheld.kind === 'scoped-refusal') {
|
|
978
980
|
await scoped(withheld, room, local);
|
|
979
981
|
}
|
|
@@ -1020,44 +1022,30 @@ export function composeRuntime(options) {
|
|
|
1020
1022
|
(accepted.author === undefined ? '' : ` author ${accepted.author}`) +
|
|
1021
1023
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1022
1024
|
}
|
|
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) {
|
|
1025
|
+
async function adoptAccepted(room, local, submitted, accepted) {
|
|
1041
1026
|
if (!continuing())
|
|
1042
1027
|
return unconfirmed;
|
|
1043
1028
|
const current = replicas.get(room.locator);
|
|
1044
1029
|
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
1045
1030
|
return null;
|
|
1046
1031
|
}
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1032
|
+
const held = current.confirmed.record.roomVersion;
|
|
1033
|
+
logAccepted(room.locator, submitted, accepted);
|
|
1034
|
+
const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
1035
|
+
? { kind: 'staged', confirmed: current.confirmed }
|
|
1036
|
+
: applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
1037
|
+
? accepted.roomVersion
|
|
1038
|
+
: held, accepted.roomVersion);
|
|
1039
|
+
if (applied.kind !== 'staged') {
|
|
1040
|
+
const state = pendings.get(submitted.id);
|
|
1041
|
+
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1042
|
+
if (state !== undefined)
|
|
1043
|
+
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1044
|
+
emitQueue(room.locator);
|
|
1057
1045
|
return applied;
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1046
|
+
}
|
|
1047
|
+
const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
1048
|
+
pendings.delete(submitted.id);
|
|
1061
1049
|
unreadable.delete(room.locator);
|
|
1062
1050
|
sourceRefusals.delete(room.locator);
|
|
1063
1051
|
declarationRefusals.delete(room.locator);
|
|
@@ -1066,17 +1054,8 @@ export function composeRuntime(options) {
|
|
|
1066
1054
|
moveTo({ ...status, ...attention() });
|
|
1067
1055
|
return null;
|
|
1068
1056
|
}
|
|
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));
|
|
1057
|
+
async function adopted(room, local, submitted, accepted) {
|
|
1058
|
+
const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
|
|
1080
1059
|
if (failed === null)
|
|
1081
1060
|
return true;
|
|
1082
1061
|
if (failed.kind === 'refused' && continuing())
|
|
@@ -1089,10 +1068,8 @@ export function composeRuntime(options) {
|
|
|
1089
1068
|
const head = replica?.queue[0];
|
|
1090
1069
|
if (replica === undefined || head === undefined)
|
|
1091
1070
|
return;
|
|
1092
|
-
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1093
|
-
progress.complete = false;
|
|
1071
|
+
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1094
1072
|
return;
|
|
1095
|
-
}
|
|
1096
1073
|
const state = pendings.get(head.id);
|
|
1097
1074
|
const { submission, lost: earlier } = state ?? {
|
|
1098
1075
|
submission: { state: 'waiting' },
|
|
@@ -1120,6 +1097,7 @@ export function composeRuntime(options) {
|
|
|
1120
1097
|
return;
|
|
1121
1098
|
}
|
|
1122
1099
|
const opensBefore = openSignals;
|
|
1100
|
+
const answered = awaitingAnswer(room.locator);
|
|
1123
1101
|
try {
|
|
1124
1102
|
result = await options.adapter.mutate(authorized, {
|
|
1125
1103
|
locator: room.locator,
|
|
@@ -1128,11 +1106,14 @@ export function composeRuntime(options) {
|
|
|
1128
1106
|
}, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
|
|
1129
1107
|
}
|
|
1130
1108
|
catch {
|
|
1109
|
+
answered();
|
|
1131
1110
|
progress.complete = false;
|
|
1132
1111
|
if (continuing())
|
|
1133
1112
|
progress.reachable = false;
|
|
1134
1113
|
return;
|
|
1135
1114
|
}
|
|
1115
|
+
if (result.kind !== 'accepted' || !continuing())
|
|
1116
|
+
answered();
|
|
1136
1117
|
if (!continuing())
|
|
1137
1118
|
return;
|
|
1138
1119
|
if (result.kind === 'transport-uncertainty') {
|
|
@@ -1185,6 +1166,7 @@ export function composeRuntime(options) {
|
|
|
1185
1166
|
const reset = result.reset;
|
|
1186
1167
|
progress.complete = false;
|
|
1187
1168
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1169
|
+
armTimer();
|
|
1188
1170
|
return;
|
|
1189
1171
|
}
|
|
1190
1172
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
@@ -1211,95 +1193,33 @@ export function composeRuntime(options) {
|
|
|
1211
1193
|
return withhold(withheld, room, local, progress);
|
|
1212
1194
|
continue;
|
|
1213
1195
|
}
|
|
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
|
-
}
|
|
1196
|
+
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1197
|
+
if (!kept) {
|
|
1258
1198
|
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
1199
|
return;
|
|
1271
1200
|
}
|
|
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
1201
|
}
|
|
1289
|
-
|
|
1202
|
+
}
|
|
1203
|
+
function awaitingAnswer(locator) {
|
|
1204
|
+
let release = () => { };
|
|
1205
|
+
const awaited = new Promise((resolve) => {
|
|
1206
|
+
release = resolve;
|
|
1207
|
+
});
|
|
1208
|
+
answering.set(locator, awaited);
|
|
1209
|
+
return () => {
|
|
1210
|
+
if (answering.get(locator) === awaited)
|
|
1211
|
+
answering.delete(locator);
|
|
1212
|
+
release();
|
|
1213
|
+
};
|
|
1290
1214
|
}
|
|
1291
1215
|
async function retainModel(local, room, attached, next) {
|
|
1292
1216
|
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
1217
|
const previous = modelOf(room.locator, attached);
|
|
1297
1218
|
const heldBefore = replicas.get(room.locator)?.confirmed.record;
|
|
1298
1219
|
if (JSON.stringify(previous) === JSON.stringify(next) &&
|
|
1299
1220
|
(heldBefore === undefined ||
|
|
1300
1221
|
(JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
|
|
1301
1222
|
JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
|
|
1302
|
-
JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) &&
|
|
1303
1223
|
JSON.stringify(heldBefore.typedSelection) ===
|
|
1304
1224
|
JSON.stringify(room.typedSelection ?? null))))
|
|
1305
1225
|
return;
|
|
@@ -1310,15 +1230,12 @@ export function composeRuntime(options) {
|
|
|
1310
1230
|
await exclusive(async () => {
|
|
1311
1231
|
if (!continuing())
|
|
1312
1232
|
return;
|
|
1313
|
-
if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0))
|
|
1314
|
-
return;
|
|
1315
1233
|
const held = replicas.get(room.locator);
|
|
1316
1234
|
if (held !== undefined) {
|
|
1317
1235
|
const declared = {
|
|
1318
1236
|
...held.confirmed.record,
|
|
1319
1237
|
operations: room.operations,
|
|
1320
1238
|
replica: room.replica,
|
|
1321
|
-
attachedModel: room.attachedModel,
|
|
1322
1239
|
typedSelection: room.typedSelection ?? null,
|
|
1323
1240
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
1324
1241
|
};
|
|
@@ -1338,7 +1255,7 @@ export function composeRuntime(options) {
|
|
|
1338
1255
|
queue: held.queue,
|
|
1339
1256
|
});
|
|
1340
1257
|
if (read.kind === 'staged') {
|
|
1341
|
-
replicas.set(room.locator, replicaOf(read.confirmed, held.queue,
|
|
1258
|
+
replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
|
|
1342
1259
|
if (declarationRefusals.delete(room.locator)) {
|
|
1343
1260
|
sourceRefusals.delete(room.locator);
|
|
1344
1261
|
unreadable.delete(room.locator);
|
|
@@ -1362,77 +1279,6 @@ export function composeRuntime(options) {
|
|
|
1362
1279
|
publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
|
|
1363
1280
|
});
|
|
1364
1281
|
}
|
|
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
1282
|
async function scoped(result, room, local) {
|
|
1437
1283
|
scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
|
|
1438
1284
|
let scopedAway = room;
|
|
@@ -1460,14 +1306,34 @@ export function composeRuntime(options) {
|
|
|
1460
1306
|
emitRooms();
|
|
1461
1307
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
1462
1308
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1309
|
+
function heldRooms(listed) {
|
|
1310
|
+
return listed.flatMap((room) => {
|
|
1311
|
+
const record = replicas.get(room.locator)?.confirmed.record;
|
|
1312
|
+
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1313
|
+
return [];
|
|
1314
|
+
return [
|
|
1315
|
+
{
|
|
1316
|
+
locator: room.locator,
|
|
1317
|
+
roomVersion: record.roomVersion,
|
|
1318
|
+
documents: record.documents,
|
|
1319
|
+
revisions: record.revisions,
|
|
1320
|
+
attachments: record.attachments,
|
|
1321
|
+
},
|
|
1322
|
+
];
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
async function synchronize(listed, local, progress, attempt) {
|
|
1326
|
+
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
|
|
1327
|
+
const ready = listed.filter((_, index) => {
|
|
1466
1328
|
const outcome = prepared[index];
|
|
1467
1329
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1468
1330
|
});
|
|
1469
|
-
|
|
1470
|
-
|
|
1331
|
+
if (attempt === 'opened')
|
|
1332
|
+
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
1333
|
+
const loaded = await Promise.allSettled(ready
|
|
1334
|
+
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1335
|
+
.map((room) => loadRoom(room, local, progress)));
|
|
1336
|
+
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1471
1337
|
if (failed !== undefined)
|
|
1472
1338
|
throw failed.reason;
|
|
1473
1339
|
}
|
|
@@ -1484,11 +1350,8 @@ export function composeRuntime(options) {
|
|
|
1484
1350
|
progress.complete = false;
|
|
1485
1351
|
return false;
|
|
1486
1352
|
}
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
acquireModel(local, room, authorization, null),
|
|
1490
|
-
...(selection === undefined ? [] : [acquireModel(local, room, authorization, selection)]),
|
|
1491
|
-
]);
|
|
1353
|
+
if (prior !== undefined)
|
|
1354
|
+
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1492
1355
|
if (discoveryPaused || authorization === undefined)
|
|
1493
1356
|
return false;
|
|
1494
1357
|
if (room.replica.admission !== 'admitted') {
|
|
@@ -1496,24 +1359,53 @@ export function composeRuntime(options) {
|
|
|
1496
1359
|
progress.complete = false;
|
|
1497
1360
|
return false;
|
|
1498
1361
|
}
|
|
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
1362
|
return true;
|
|
1505
1363
|
}
|
|
1506
|
-
async function
|
|
1364
|
+
async function caughtUp(room, local, progress) {
|
|
1365
|
+
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1366
|
+
if (held === undefined || authorization === undefined)
|
|
1367
|
+
return;
|
|
1368
|
+
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
1369
|
+
if (!behind)
|
|
1370
|
+
return;
|
|
1371
|
+
const withheld = await advance(room, local, authorization);
|
|
1372
|
+
if (withheld !== null)
|
|
1373
|
+
await withhold(withheld, room, local, progress);
|
|
1374
|
+
}
|
|
1375
|
+
function revoked(locator) {
|
|
1376
|
+
return ['collection', 'typed-selection'].some((attached) => {
|
|
1377
|
+
const model = modelOf(locator, attached);
|
|
1378
|
+
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
async function loadRoom(room, local, progress) {
|
|
1507
1382
|
if (discoveryPaused || authorization === undefined)
|
|
1508
1383
|
return;
|
|
1384
|
+
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1385
|
+
await reload(room, local, progress);
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1509
1388
|
const withheld = await advance(room, local, authorization);
|
|
1510
1389
|
if (withheld !== null) {
|
|
1511
1390
|
await withhold(withheld, room, local, progress);
|
|
1512
1391
|
return;
|
|
1513
1392
|
}
|
|
1514
|
-
await
|
|
1393
|
+
await caughtUp(room, local, progress);
|
|
1515
1394
|
}
|
|
1516
|
-
async function
|
|
1395
|
+
async function reload(room, local, progress) {
|
|
1396
|
+
if (discoveryPaused || authorization === undefined)
|
|
1397
|
+
return;
|
|
1398
|
+
const bootstrapped = await pullBootstrap(authorization, room);
|
|
1399
|
+
if (!continuing())
|
|
1400
|
+
return;
|
|
1401
|
+
if (bootstrapped.kind !== 'staged') {
|
|
1402
|
+
await withhold(bootstrapped, room, local, progress);
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
await publish(local, room, bootstrapped.confirmed);
|
|
1406
|
+
await caughtUp(room, local, progress);
|
|
1407
|
+
}
|
|
1408
|
+
async function discoverRooms(local, progress, attempt) {
|
|
1517
1409
|
if (closed || authorization === undefined) {
|
|
1518
1410
|
notifyReady();
|
|
1519
1411
|
return;
|
|
@@ -1539,6 +1431,10 @@ export function composeRuntime(options) {
|
|
|
1539
1431
|
}
|
|
1540
1432
|
}
|
|
1541
1433
|
progress.reachable = false;
|
|
1434
|
+
progress.complete = false;
|
|
1435
|
+
if (attempt === 'opened') {
|
|
1436
|
+
options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
|
|
1437
|
+
}
|
|
1542
1438
|
notifyReady();
|
|
1543
1439
|
return;
|
|
1544
1440
|
}
|
|
@@ -1553,17 +1449,6 @@ export function composeRuntime(options) {
|
|
|
1553
1449
|
notifyReady();
|
|
1554
1450
|
return;
|
|
1555
1451
|
}
|
|
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
1452
|
for (const room of result.rooms) {
|
|
1568
1453
|
if (room.operations === undefined ||
|
|
1569
1454
|
!['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
@@ -1571,8 +1456,7 @@ export function composeRuntime(options) {
|
|
|
1571
1456
|
room.replica === undefined ||
|
|
1572
1457
|
typeof room.replica.selection !== 'string' ||
|
|
1573
1458
|
room.replica.selection.length === 0 ||
|
|
1574
|
-
!['admitted', 'unavailable'].includes(room.replica.admission)
|
|
1575
|
-
!declaredIdentity(room.attachedModel)) {
|
|
1459
|
+
!['admitted', 'unavailable'].includes(room.replica.admission)) {
|
|
1576
1460
|
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
1577
1461
|
await refuse('missing-required-guarantee');
|
|
1578
1462
|
notifyReady();
|
|
@@ -1585,14 +1469,6 @@ export function composeRuntime(options) {
|
|
|
1585
1469
|
return;
|
|
1586
1470
|
}
|
|
1587
1471
|
}
|
|
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
1472
|
if (refusal !== null) {
|
|
1597
1473
|
refusal = null;
|
|
1598
1474
|
emitRefusal();
|
|
@@ -1600,26 +1476,13 @@ export function composeRuntime(options) {
|
|
|
1600
1476
|
const previousCapabilities = capabilities;
|
|
1601
1477
|
capabilities = result.capabilities;
|
|
1602
1478
|
notifyReady();
|
|
1603
|
-
if (declared.has('room-version-notifications'))
|
|
1604
|
-
observeRoomVersions();
|
|
1605
1479
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1606
1480
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
1607
1481
|
limits = result.providerLimits;
|
|
1608
1482
|
await local.declare(limits, capabilities);
|
|
1609
1483
|
}
|
|
1610
1484
|
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);
|
|
1485
|
+
await synchronize(result.rooms, local, progress, attempt);
|
|
1623
1486
|
}
|
|
1624
1487
|
async function ensureAuthorization() {
|
|
1625
1488
|
if (authorization !== undefined)
|
|
@@ -1633,64 +1496,246 @@ export function composeRuntime(options) {
|
|
|
1633
1496
|
throw error;
|
|
1634
1497
|
}
|
|
1635
1498
|
}
|
|
1636
|
-
|
|
1637
|
-
if (
|
|
1638
|
-
|
|
1499
|
+
function task(work) {
|
|
1500
|
+
if (stopping)
|
|
1501
|
+
return chain;
|
|
1502
|
+
chain = chain.then(work, work).catch(() => undefined);
|
|
1503
|
+
return chain;
|
|
1504
|
+
}
|
|
1505
|
+
function sync(attempt) {
|
|
1506
|
+
return task(async () => {
|
|
1507
|
+
if (closed || stopping || discoveryPaused) {
|
|
1508
|
+
notifyReady();
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
const progress = { complete: true, reachable: true };
|
|
1512
|
+
underway = attempt === 'opened' ? progress : null;
|
|
1513
|
+
complete = false;
|
|
1514
|
+
recorded = false;
|
|
1515
|
+
moveTo(transferring(status));
|
|
1516
|
+
try {
|
|
1517
|
+
const local = await openLocal();
|
|
1518
|
+
if (closed)
|
|
1519
|
+
return;
|
|
1520
|
+
await ensureAuthorization();
|
|
1521
|
+
if (closed)
|
|
1522
|
+
return;
|
|
1523
|
+
await discoverRooms(local, progress, attempt);
|
|
1524
|
+
}
|
|
1525
|
+
catch {
|
|
1526
|
+
progress.complete = false;
|
|
1527
|
+
if (attempt === 'opened') {
|
|
1528
|
+
options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
finally {
|
|
1532
|
+
notifyReady();
|
|
1533
|
+
}
|
|
1534
|
+
if (attempt === 'opened')
|
|
1535
|
+
return;
|
|
1536
|
+
await finishOpening(progress);
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
function closedSocket(revoked) {
|
|
1540
|
+
const opened = socketOpen;
|
|
1541
|
+
socketOpen = false;
|
|
1542
|
+
if (revoked) {
|
|
1543
|
+
void refuse('revoked-authorization');
|
|
1639
1544
|
return;
|
|
1640
1545
|
}
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
if (closed)
|
|
1546
|
+
if (opened || status.providerUnreachable)
|
|
1547
|
+
return;
|
|
1548
|
+
moveTo({ ...status, providerUnreachable: true });
|
|
1549
|
+
}
|
|
1550
|
+
function applyOpening(expired) {
|
|
1551
|
+
return task(async () => {
|
|
1552
|
+
const progress = underway;
|
|
1553
|
+
underway = null;
|
|
1554
|
+
if (closed || stopping)
|
|
1650
1555
|
return;
|
|
1651
|
-
|
|
1652
|
-
|
|
1556
|
+
if (progress === null) {
|
|
1557
|
+
await reloadExpired(expired);
|
|
1653
1558
|
return;
|
|
1654
|
-
if (progress.complete) {
|
|
1655
|
-
completedAt = new Date().toISOString();
|
|
1656
|
-
await local.complete(completedAt);
|
|
1657
1559
|
}
|
|
1560
|
+
try {
|
|
1561
|
+
const local = await openLocal();
|
|
1562
|
+
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
1563
|
+
const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
|
|
1564
|
+
const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
|
|
1565
|
+
if (progress.reachable) {
|
|
1566
|
+
for (const room of held)
|
|
1567
|
+
if (!reloading.includes(room))
|
|
1568
|
+
await confirmModels(room, local);
|
|
1569
|
+
}
|
|
1570
|
+
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1571
|
+
if (failed !== undefined)
|
|
1572
|
+
throw failed.reason;
|
|
1573
|
+
}
|
|
1574
|
+
catch {
|
|
1575
|
+
progress.complete = false;
|
|
1576
|
+
progress.reachable = false;
|
|
1577
|
+
}
|
|
1578
|
+
await finishOpening(progress);
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
async function reloadExpired(expired) {
|
|
1582
|
+
const progress = { complete: true, reachable: true };
|
|
1583
|
+
try {
|
|
1584
|
+
const local = await openLocal();
|
|
1585
|
+
await Promise.allSettled([...discovered.values()]
|
|
1586
|
+
.filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
|
|
1587
|
+
.map((room) => reload(room, local, progress)));
|
|
1658
1588
|
}
|
|
1659
1589
|
catch {
|
|
1660
|
-
progress.
|
|
1661
|
-
|
|
1590
|
+
progress.reachable = false;
|
|
1591
|
+
}
|
|
1592
|
+
await rest(progress);
|
|
1593
|
+
}
|
|
1594
|
+
async function confirmModels(room, local) {
|
|
1595
|
+
for (const attached of ['collection', 'typed-selection']) {
|
|
1596
|
+
const model = modelOf(room.locator, attached);
|
|
1597
|
+
if (model.status === 'cached') {
|
|
1598
|
+
await retainModel(local, room, attached, { ...model, status: 'current' });
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
function submitLater(locator) {
|
|
1603
|
+
const running = submitting.get(locator);
|
|
1604
|
+
if (running !== undefined) {
|
|
1605
|
+
running.again = true;
|
|
1662
1606
|
return;
|
|
1663
1607
|
}
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1608
|
+
const run = { again: false, done: Promise.resolve() };
|
|
1609
|
+
submitting.set(locator, run);
|
|
1610
|
+
run.done = (async () => {
|
|
1611
|
+
try {
|
|
1612
|
+
do {
|
|
1613
|
+
run.again = false;
|
|
1614
|
+
const room = discovered.get(locator);
|
|
1615
|
+
if (closed || stopping || discoveryPaused || room === undefined)
|
|
1616
|
+
return;
|
|
1617
|
+
const progress = { complete: true, reachable: true };
|
|
1618
|
+
try {
|
|
1619
|
+
await ensureAuthorization();
|
|
1620
|
+
if (authorization === undefined)
|
|
1621
|
+
return;
|
|
1622
|
+
await submitRoom(room, await openLocal(), authorization, progress);
|
|
1623
|
+
}
|
|
1624
|
+
catch {
|
|
1625
|
+
progress.reachable = false;
|
|
1626
|
+
}
|
|
1627
|
+
armTimer();
|
|
1628
|
+
await rest(progress);
|
|
1629
|
+
} while (run.again);
|
|
1630
|
+
}
|
|
1631
|
+
finally {
|
|
1632
|
+
submitting.delete(locator);
|
|
1633
|
+
}
|
|
1634
|
+
})();
|
|
1635
|
+
}
|
|
1636
|
+
function submitAll() {
|
|
1637
|
+
for (const [locator, replica] of replicas) {
|
|
1638
|
+
if (replica.queue.length > 0)
|
|
1639
|
+
submitLater(locator);
|
|
1667
1640
|
}
|
|
1668
1641
|
}
|
|
1669
|
-
async function
|
|
1642
|
+
async function finishOpening(progress) {
|
|
1670
1643
|
try {
|
|
1671
|
-
|
|
1644
|
+
if (closed || discoveryPaused) {
|
|
1645
|
+
progress.complete = false;
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
submitAll();
|
|
1649
|
+
await conversations.reconcile();
|
|
1650
|
+
if (status.providerUnreachable)
|
|
1651
|
+
conversations.reconnected();
|
|
1652
|
+
const reading = { complete: true, reachable: true };
|
|
1653
|
+
await conversations.catchUp(reading);
|
|
1654
|
+
if (!reading.reachable)
|
|
1655
|
+
progress.reachable = false;
|
|
1656
|
+
}
|
|
1657
|
+
catch {
|
|
1658
|
+
progress.complete = false;
|
|
1672
1659
|
}
|
|
1673
1660
|
finally {
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
queued = false;
|
|
1677
|
-
void enqueue();
|
|
1678
|
-
}
|
|
1661
|
+
complete = progress.complete;
|
|
1662
|
+
await rest(progress);
|
|
1679
1663
|
}
|
|
1680
1664
|
}
|
|
1681
|
-
function
|
|
1682
|
-
|
|
1683
|
-
if (path === undefined || withdrawNotifications !== undefined)
|
|
1665
|
+
async function rest(progress) {
|
|
1666
|
+
if (closed)
|
|
1684
1667
|
return;
|
|
1685
|
-
|
|
1686
|
-
|
|
1668
|
+
let lastCompleted = status.lastCompleted;
|
|
1669
|
+
let synced = complete && stale.size === 0 && !pending();
|
|
1670
|
+
if (synced && !recorded) {
|
|
1671
|
+
const completedAt = new Date().toISOString();
|
|
1672
|
+
try {
|
|
1673
|
+
await (await openLocal()).complete(completedAt);
|
|
1674
|
+
recorded = true;
|
|
1675
|
+
lastCompleted = completedAt;
|
|
1676
|
+
}
|
|
1677
|
+
catch {
|
|
1678
|
+
synced = false;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
moveTo({
|
|
1682
|
+
state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
|
|
1683
|
+
lastCompleted,
|
|
1684
|
+
providerUnreachable: !progress.reachable,
|
|
1685
|
+
...attention(),
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
function applyRoom(locator, roomVersion) {
|
|
1689
|
+
announced.set(locator, roomVersion);
|
|
1690
|
+
return task(async () => {
|
|
1691
|
+
await answering.get(locator);
|
|
1692
|
+
if (closed || stopping || discoveryPaused || authorization === undefined)
|
|
1693
|
+
return;
|
|
1694
|
+
const room = discovered.get(locator);
|
|
1695
|
+
const held = replicas.get(locator);
|
|
1696
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
1687
1697
|
return;
|
|
1688
|
-
if (
|
|
1689
|
-
|
|
1698
|
+
if (held !== undefined &&
|
|
1699
|
+
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
1700
|
+
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
const during = underway;
|
|
1704
|
+
const progress = during ?? { complete: true, reachable: true };
|
|
1705
|
+
if (during === null)
|
|
1706
|
+
moveTo(transferring(status));
|
|
1707
|
+
try {
|
|
1708
|
+
const local = await openLocal();
|
|
1709
|
+
const withheld = await advance(room, local, authorization);
|
|
1710
|
+
if (withheld !== null)
|
|
1711
|
+
await withhold(withheld, room, local, progress);
|
|
1712
|
+
}
|
|
1713
|
+
catch {
|
|
1714
|
+
stale.add(locator);
|
|
1715
|
+
}
|
|
1716
|
+
finally {
|
|
1717
|
+
if (during === null)
|
|
1718
|
+
await rest(progress);
|
|
1719
|
+
}
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
function observeStream() {
|
|
1723
|
+
const stream = options.adapter.roomStream;
|
|
1724
|
+
if (stream === undefined || withdrawStream !== undefined)
|
|
1725
|
+
return;
|
|
1726
|
+
withdrawStream = stream.observe((event) => {
|
|
1727
|
+
if (closed || stopping)
|
|
1690
1728
|
return;
|
|
1729
|
+
if (event.kind === 'opened') {
|
|
1730
|
+
socketOpen = true;
|
|
1731
|
+
void sync('opened');
|
|
1691
1732
|
}
|
|
1692
|
-
|
|
1693
|
-
|
|
1733
|
+
else if (event.kind === 'closed')
|
|
1734
|
+
closedSocket(event.revoked);
|
|
1735
|
+
else if (event.kind === 'opening')
|
|
1736
|
+
void applyOpening(event.expired);
|
|
1737
|
+
else
|
|
1738
|
+
void applyRoom(event.locator, event.roomVersion);
|
|
1694
1739
|
});
|
|
1695
1740
|
}
|
|
1696
1741
|
function writesHeld() {
|
|
@@ -1711,14 +1756,17 @@ export function composeRuntime(options) {
|
|
|
1711
1756
|
listener(mode);
|
|
1712
1757
|
}
|
|
1713
1758
|
if (releasing)
|
|
1714
|
-
|
|
1759
|
+
submitAll();
|
|
1715
1760
|
}
|
|
1716
1761
|
function observeConversations() {
|
|
1717
1762
|
const stream = options.adapter.conversations;
|
|
1718
1763
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
1719
1764
|
return;
|
|
1720
1765
|
withdrawConversations = stream.observe((event) => {
|
|
1721
|
-
conversations.receive(event)
|
|
1766
|
+
if (conversations.receive(event)) {
|
|
1767
|
+
void ready.then(scheduleCurrency);
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1722
1770
|
if (event.kind !== 'gap' || conversations.current())
|
|
1723
1771
|
return;
|
|
1724
1772
|
show();
|
|
@@ -1729,7 +1777,7 @@ export function composeRuntime(options) {
|
|
|
1729
1777
|
if (currencyQueued || closed || stopping)
|
|
1730
1778
|
return;
|
|
1731
1779
|
currencyQueued = true;
|
|
1732
|
-
|
|
1780
|
+
void task(runCurrency);
|
|
1733
1781
|
}
|
|
1734
1782
|
async function runCurrency() {
|
|
1735
1783
|
currencyQueued = false;
|
|
@@ -1755,95 +1803,12 @@ export function composeRuntime(options) {
|
|
|
1755
1803
|
}
|
|
1756
1804
|
}
|
|
1757
1805
|
}
|
|
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
1806
|
function close() {
|
|
1842
1807
|
if (closed)
|
|
1843
1808
|
return;
|
|
1844
1809
|
closed = true;
|
|
1845
|
-
|
|
1846
|
-
|
|
1810
|
+
withdrawStream?.();
|
|
1811
|
+
withdrawStream = undefined;
|
|
1847
1812
|
withdrawConversations?.();
|
|
1848
1813
|
withdrawConversations = undefined;
|
|
1849
1814
|
withdrawServiceMode?.();
|
|
@@ -1867,6 +1832,7 @@ export function composeRuntime(options) {
|
|
|
1867
1832
|
stopping = true;
|
|
1868
1833
|
stopTimer();
|
|
1869
1834
|
await chain.catch(() => undefined);
|
|
1835
|
+
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
1870
1836
|
await mutations;
|
|
1871
1837
|
close();
|
|
1872
1838
|
}
|
|
@@ -1875,11 +1841,29 @@ export function composeRuntime(options) {
|
|
|
1875
1841
|
options.signal?.addEventListener('abort', close, { once: true });
|
|
1876
1842
|
if (options.signal?.aborted)
|
|
1877
1843
|
close();
|
|
1878
|
-
|
|
1844
|
+
observeStream();
|
|
1845
|
+
void openLocal().then(() => {
|
|
1846
|
+
if (capabilities.length > 0)
|
|
1847
|
+
notifyReady();
|
|
1848
|
+
}, () => {
|
|
1879
1849
|
notifyReady();
|
|
1880
1850
|
});
|
|
1881
|
-
|
|
1882
|
-
|
|
1851
|
+
if (options.adapter.roomStream === undefined)
|
|
1852
|
+
void sync('unstreamed');
|
|
1853
|
+
function planned(input, reach) {
|
|
1854
|
+
return planOf(input, roomFacts(reach), planReach(reach));
|
|
1855
|
+
}
|
|
1856
|
+
function watchPlan(plan, deliver) {
|
|
1857
|
+
return readSide.watch(plan, identityOf(plan), deliver);
|
|
1858
|
+
}
|
|
1859
|
+
async function searched(input, reach) {
|
|
1860
|
+
const plan = searchPlanOf(input);
|
|
1861
|
+
const selected = plan.documents === undefined && reach === null
|
|
1862
|
+
? null
|
|
1863
|
+
: (await readSide.query(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
|
|
1864
|
+
return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
|
|
1865
|
+
}
|
|
1866
|
+
const watch = (topic, input, deliver) => {
|
|
1883
1867
|
if (topic === 'room-drafts') {
|
|
1884
1868
|
const listener = { notify: deliver, ready: false };
|
|
1885
1869
|
roomDraftListeners.add(listener);
|
|
@@ -1900,6 +1884,8 @@ export function composeRuntime(options) {
|
|
|
1900
1884
|
if (topic === 'conversation') {
|
|
1901
1885
|
return conversations.watch(input[0], input[1], deliver);
|
|
1902
1886
|
}
|
|
1887
|
+
if (topic === 'conversations')
|
|
1888
|
+
return conversations.watchList(deliver);
|
|
1903
1889
|
if (topic === 'presentation') {
|
|
1904
1890
|
const listener = {
|
|
1905
1891
|
app: input[0],
|
|
@@ -1962,9 +1948,8 @@ export function composeRuntime(options) {
|
|
|
1962
1948
|
queueSubscriptions.delete(queue);
|
|
1963
1949
|
};
|
|
1964
1950
|
}
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
}
|
|
1951
|
+
return watchPlan(planned(input[0], null), deliver);
|
|
1952
|
+
};
|
|
1968
1953
|
const md = {
|
|
1969
1954
|
loaded() {
|
|
1970
1955
|
return openLocal().then(() => undefined);
|
|
@@ -1972,15 +1957,8 @@ export function composeRuntime(options) {
|
|
|
1972
1957
|
rooms() {
|
|
1973
1958
|
return listed();
|
|
1974
1959
|
},
|
|
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
|
-
},
|
|
1960
|
+
query: ((input) => readSide.query(planned(input, null))),
|
|
1961
|
+
search: (input) => searched(input, null),
|
|
1984
1962
|
types(locator) {
|
|
1985
1963
|
return {
|
|
1986
1964
|
collection: modelTypes(modelOf(locator)),
|
|
@@ -2087,9 +2065,7 @@ export function composeRuntime(options) {
|
|
|
2087
2065
|
persistence() {
|
|
2088
2066
|
return options.shell.persistentStorage;
|
|
2089
2067
|
},
|
|
2090
|
-
observe: (
|
|
2091
|
-
deliver(structuredClone(message));
|
|
2092
|
-
}), topic === 'query')),
|
|
2068
|
+
observe: observing(watch),
|
|
2093
2069
|
async reauthorize() {
|
|
2094
2070
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2095
2071
|
return;
|
|
@@ -2098,8 +2074,19 @@ export function composeRuntime(options) {
|
|
|
2098
2074
|
await mutations;
|
|
2099
2075
|
authorization = undefined;
|
|
2100
2076
|
discoveryPaused = false;
|
|
2101
|
-
|
|
2077
|
+
if (options.adapter.roomStream === undefined)
|
|
2078
|
+
await sync('unstreamed');
|
|
2079
|
+
else
|
|
2080
|
+
await task(ensureAuthorization);
|
|
2102
2081
|
},
|
|
2103
2082
|
};
|
|
2104
|
-
|
|
2083
|
+
const reachable = {
|
|
2084
|
+
md,
|
|
2085
|
+
watch,
|
|
2086
|
+
plan: planned,
|
|
2087
|
+
query: (plan) => readSide.query(plan),
|
|
2088
|
+
watchPlan,
|
|
2089
|
+
search: searched,
|
|
2090
|
+
};
|
|
2091
|
+
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2105
2092
|
}
|