@agentshouse/mdruntime 0.1.0-cf28918c2bf91 → 0.1.0-cf6d5f0a5c9c8
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 +68 -22
- package/README.md +10 -10
- package/dist/adapter.d.ts +58 -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 +47 -0
- package/dist/bootstrap.d.ts +20 -6
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +89 -35
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +13 -7
- package/dist/client.d.ts +6 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +38 -10
- package/dist/collection.d.ts +6 -11
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +56 -19
- package/dist/connection.d.ts +13 -13
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +643 -542
- 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 +8 -1
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +35 -31
- package/dist/identity.d.ts +1 -0
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +7 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/memory.d.ts +12 -5
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +92 -31
- package/dist/model.d.ts +11 -11
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +6 -3
- package/dist/observation.d.ts +2 -0
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +24 -14
- package/dist/paths.d.ts +0 -1
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +0 -16
- package/dist/query.d.ts +8 -3
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +75 -36
- package/dist/reach.d.ts +40 -0
- package/dist/reach.d.ts.map +1 -0
- package/dist/reach.js +270 -0
- package/dist/react.d.ts +39 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +63 -0
- package/dist/read-side.d.ts +4 -0
- package/dist/read-side.d.ts.map +1 -1
- package/dist/read-side.js +116 -54
- package/dist/refresh.d.ts +12 -1
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +35 -22
- package/dist/replica.d.ts +6 -6
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +41 -36
- package/dist/room-draft.d.ts +3 -0
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +7 -5
- package/dist/rows.d.ts +4 -1
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +82 -39
- package/dist/save.d.ts +1 -2
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +106 -58
- package/dist/store.d.ts +25 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +206 -90
- package/dist/wire.d.ts +52 -16
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +51 -59
- package/package.json +14 -2
package/dist/connection.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
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
|
+
import { markdownPath, roomOfKey } from './rows.js';
|
|
6
7
|
import { createSearchIndex, searchPlanOf, } from './search.js';
|
|
7
|
-
import { ROOM_CONTRACT_VERSION, VERSION_1_CAPABILITIES, } from './adapter.js';
|
|
8
8
|
import { bootstrapRoom } from './bootstrap.js';
|
|
9
9
|
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
10
10
|
import { createConversationStore, } from './conversation.js';
|
|
11
|
-
import {
|
|
11
|
+
import { validRoot } from './collection.js';
|
|
12
12
|
import { createLog } from './log.js';
|
|
13
13
|
import { refreshRoom } from './refresh.js';
|
|
14
14
|
import { buildBatch } from './save.js';
|
|
15
|
-
import { applyReturned, compositionOf,
|
|
15
|
+
import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
|
|
16
16
|
import { changedParts, changesOf, held, readParts, returnedInto, underneath, } from './room-draft.js';
|
|
17
17
|
import { settled, same, saving, STARTING, transferring, } from './status.js';
|
|
18
18
|
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
@@ -20,37 +20,21 @@ import { roomDraftRevision } from './identity.js';
|
|
|
20
20
|
import { beneath, normalize } from './paths.js';
|
|
21
21
|
import { openRuntimeStore, } from './store.js';
|
|
22
22
|
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);
|
|
23
|
+
import { governs, holds, narrowed, planReach, } from './reach.js';
|
|
27
24
|
function within(root, path) {
|
|
28
25
|
return root === '' || beneath(root, path);
|
|
29
26
|
}
|
|
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
27
|
function declaredSelection(room) {
|
|
41
28
|
const selection = room.typedSelection;
|
|
42
29
|
if (selection === undefined)
|
|
43
30
|
return true;
|
|
44
31
|
if (!Array.isArray(selection.roots) || selection.roots.length === 0)
|
|
45
32
|
return false;
|
|
46
|
-
if (!declaredIdentity(selection.attachedModel))
|
|
47
|
-
return false;
|
|
48
33
|
return selection.roots.every((root, index) => validRoot(root) &&
|
|
49
34
|
root !== '' &&
|
|
50
35
|
selection.roots.every((other, at) => at === index || !within(other, root)));
|
|
51
36
|
}
|
|
52
37
|
export function composeRuntime(options) {
|
|
53
|
-
const period = options.discoveryIntervalMs ?? DEFAULT_DISCOVERY_INTERVAL_MS;
|
|
54
38
|
let authorization = options.authorization;
|
|
55
39
|
let opening;
|
|
56
40
|
let store;
|
|
@@ -58,20 +42,14 @@ export function composeRuntime(options) {
|
|
|
58
42
|
const pendings = new Map();
|
|
59
43
|
const roomDrafts = new Map();
|
|
60
44
|
const presentations = new Map();
|
|
61
|
-
const wanted = new Map();
|
|
62
45
|
let limits = [];
|
|
63
46
|
let capabilities = [];
|
|
64
|
-
function rewriting() {
|
|
65
|
-
return capabilities.some((capability) => capability.name === 'move-reference-rewrites')
|
|
66
|
-
? 'provider'
|
|
67
|
-
: 'runtime';
|
|
68
|
-
}
|
|
69
47
|
const rooms = new Map();
|
|
48
|
+
const counted = new Map();
|
|
70
49
|
const unreadable = new Set();
|
|
71
50
|
const models = new Map();
|
|
72
51
|
const liveModels = new Map();
|
|
73
52
|
const invalidModels = new Set();
|
|
74
|
-
const modelEpoch = new Map();
|
|
75
53
|
const discovered = new Map();
|
|
76
54
|
let roomsListed = false;
|
|
77
55
|
const scopedRefusals = new Map();
|
|
@@ -100,16 +78,20 @@ export function composeRuntime(options) {
|
|
|
100
78
|
let status = STARTING;
|
|
101
79
|
let shown = STARTING;
|
|
102
80
|
let discoveryPaused = false;
|
|
103
|
-
let
|
|
81
|
+
let resetTimer;
|
|
104
82
|
let closed = false;
|
|
105
83
|
let stopping = false;
|
|
106
|
-
let
|
|
107
|
-
|
|
108
|
-
|
|
84
|
+
let withdrawStream;
|
|
85
|
+
const announced = new Map();
|
|
86
|
+
let underway = null;
|
|
87
|
+
let handed = false;
|
|
88
|
+
const submitting = new Map();
|
|
89
|
+
const answering = new Map();
|
|
90
|
+
let complete = false;
|
|
91
|
+
let recorded = false;
|
|
92
|
+
const stale = new Set();
|
|
109
93
|
let chain = Promise.resolve();
|
|
110
94
|
let mutations = Promise.resolve();
|
|
111
|
-
let inflight = false;
|
|
112
|
-
let queued = false;
|
|
113
95
|
const roomListeners = new Set();
|
|
114
96
|
const refusalListeners = new Set();
|
|
115
97
|
const storageLossListeners = new Set();
|
|
@@ -120,13 +102,19 @@ export function composeRuntime(options) {
|
|
|
120
102
|
const readSide = createReadSide();
|
|
121
103
|
let withdrawConversations;
|
|
122
104
|
let currencyQueued = false;
|
|
105
|
+
let socketOpen = false;
|
|
123
106
|
let serviceMode = 'open';
|
|
124
107
|
let haltedRefusal = false;
|
|
125
108
|
let openSignals = 0;
|
|
126
109
|
const serviceModeListeners = new Set();
|
|
110
|
+
let member = null;
|
|
111
|
+
const memberListeners = new Set();
|
|
112
|
+
const materialized = new Set();
|
|
113
|
+
let everyRoom = false;
|
|
114
|
+
const transfers = new Map();
|
|
127
115
|
const conversations = createConversationStore({
|
|
128
|
-
|
|
129
|
-
documents: (locator) =>
|
|
116
|
+
agents: declaredAgents,
|
|
117
|
+
documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
|
|
130
118
|
call: (name, input) => invokeAction(name, input),
|
|
131
119
|
durably,
|
|
132
120
|
stream: () => options.adapter.conversations,
|
|
@@ -231,14 +219,33 @@ export function composeRuntime(options) {
|
|
|
231
219
|
}
|
|
232
220
|
return { attentionRequired, reset };
|
|
233
221
|
}
|
|
222
|
+
function documentsIn(locator) {
|
|
223
|
+
const replica = replicas.get(locator);
|
|
224
|
+
if (replica === undefined)
|
|
225
|
+
return 0;
|
|
226
|
+
const roots = replica.confirmed.record.typedSelection?.roots ?? [];
|
|
227
|
+
let count = 0;
|
|
228
|
+
for (const path of Object.keys(replica.visible.documents)) {
|
|
229
|
+
if (markdownPath(path) && !roots.some((root) => beneath(root, path)))
|
|
230
|
+
count += 1;
|
|
231
|
+
}
|
|
232
|
+
return count;
|
|
233
|
+
}
|
|
234
234
|
function listed() {
|
|
235
|
-
return structuredClone([...rooms.values()])
|
|
235
|
+
return structuredClone([...rooms.values()].map((room) => {
|
|
236
|
+
const documents = documentsIn(room.locator);
|
|
237
|
+
counted.set(room.locator, documents);
|
|
238
|
+
return { ...room, documents };
|
|
239
|
+
}));
|
|
236
240
|
}
|
|
237
|
-
function roomFacts() {
|
|
238
|
-
const held = [...rooms.values()]
|
|
241
|
+
function roomFacts(reach = null) {
|
|
242
|
+
const held = [...rooms.values()]
|
|
243
|
+
.filter((room) => holds(reach, room.locator))
|
|
244
|
+
.map((room) => {
|
|
245
|
+
const governed = governs(reach, room);
|
|
239
246
|
const attached = [
|
|
240
|
-
modelOf(room.locator),
|
|
241
|
-
...(
|
|
247
|
+
...(governed.collection ? [modelOf(room.locator)] : []),
|
|
248
|
+
...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
|
|
242
249
|
];
|
|
243
250
|
return {
|
|
244
251
|
locator: room.locator,
|
|
@@ -261,6 +268,10 @@ export function composeRuntime(options) {
|
|
|
261
268
|
for (const listener of storageLossListeners)
|
|
262
269
|
listener(storageLoss);
|
|
263
270
|
}
|
|
271
|
+
function emitMember() {
|
|
272
|
+
for (const listener of memberListeners)
|
|
273
|
+
listener(member);
|
|
274
|
+
}
|
|
264
275
|
function moveTo(next) {
|
|
265
276
|
if (closed)
|
|
266
277
|
return;
|
|
@@ -300,12 +311,47 @@ export function composeRuntime(options) {
|
|
|
300
311
|
}
|
|
301
312
|
}
|
|
302
313
|
function applyChangeSet(set) {
|
|
314
|
+
const held = everyRoom
|
|
315
|
+
? set
|
|
316
|
+
: { ...set, documents: set.documents.filter(({ locator }) => materialized.has(locator)) };
|
|
317
|
+
readSide.apply(held);
|
|
318
|
+
searchIndex.changed(held);
|
|
319
|
+
}
|
|
320
|
+
function materialize(locators) {
|
|
321
|
+
if (everyRoom)
|
|
322
|
+
return;
|
|
323
|
+
if (locators === null)
|
|
324
|
+
everyRoom = true;
|
|
325
|
+
const sets = [];
|
|
326
|
+
for (const locator of locators ?? [...replicas.keys()]) {
|
|
327
|
+
if (materialized.has(locator))
|
|
328
|
+
continue;
|
|
329
|
+
materialized.add(locator);
|
|
330
|
+
const replica = replicas.get(locator);
|
|
331
|
+
if (replica !== undefined)
|
|
332
|
+
sets.push(changeSet(locator, null, replica));
|
|
333
|
+
}
|
|
334
|
+
if (sets.length === 0)
|
|
335
|
+
return;
|
|
336
|
+
const set = mergeChangeSets(sets);
|
|
303
337
|
readSide.apply(set);
|
|
304
338
|
searchIndex.changed(set);
|
|
305
339
|
}
|
|
340
|
+
function reachedRooms(plan) {
|
|
341
|
+
if (plan.rooms !== null)
|
|
342
|
+
return plan.rooms;
|
|
343
|
+
if (plan.reach === null)
|
|
344
|
+
return null;
|
|
345
|
+
return [...plan.reach.rooms, ...plan.reach.documents.map(roomOfKey)];
|
|
346
|
+
}
|
|
347
|
+
function rowsQuery(plan) {
|
|
348
|
+
materialize(reachedRooms(plan));
|
|
349
|
+
return readSide.query(plan);
|
|
350
|
+
}
|
|
306
351
|
function publishChangeSet(set) {
|
|
307
352
|
applyChangeSet(set);
|
|
308
|
-
if (set.rooms.length > 0
|
|
353
|
+
if (set.rooms.length > 0 ||
|
|
354
|
+
[...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
|
|
309
355
|
emitRooms();
|
|
310
356
|
for (const locator of set.queues)
|
|
311
357
|
emitQueue(locator);
|
|
@@ -316,23 +362,32 @@ export function composeRuntime(options) {
|
|
|
316
362
|
for (const app of set.presentations)
|
|
317
363
|
emitPresentation(app);
|
|
318
364
|
conversations.changed(set);
|
|
319
|
-
const cards = declaredCards();
|
|
320
|
-
if (cards !== undefined && set.documents.some((change) => change.locator === cards.locator)) {
|
|
321
|
-
scheduleCurrency();
|
|
322
|
-
}
|
|
323
365
|
}
|
|
324
366
|
function stopTimer() {
|
|
325
|
-
if (
|
|
367
|
+
if (resetTimer === undefined)
|
|
326
368
|
return;
|
|
327
|
-
|
|
328
|
-
|
|
369
|
+
clearTimeout(resetTimer);
|
|
370
|
+
resetTimer = undefined;
|
|
329
371
|
}
|
|
330
|
-
function
|
|
331
|
-
|
|
372
|
+
function armTimer() {
|
|
373
|
+
stopTimer();
|
|
374
|
+
if (closed || stopping)
|
|
332
375
|
return;
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
376
|
+
let earliest = null;
|
|
377
|
+
for (const { submission } of pendings.values()) {
|
|
378
|
+
if (submission.state !== 'reset')
|
|
379
|
+
continue;
|
|
380
|
+
const at = Date.parse(submission.reset);
|
|
381
|
+
if (earliest === null || at < earliest)
|
|
382
|
+
earliest = at;
|
|
383
|
+
}
|
|
384
|
+
if (earliest === null)
|
|
385
|
+
return;
|
|
386
|
+
// A Save House held back goes again only once the reset its answer named has passed.
|
|
387
|
+
resetTimer = setTimeout(() => {
|
|
388
|
+
resetTimer = undefined;
|
|
389
|
+
submitAll();
|
|
390
|
+
}, Math.max(0, earliest - Date.now()));
|
|
336
391
|
}
|
|
337
392
|
async function refuse(reason, provider = null) {
|
|
338
393
|
stopTimer();
|
|
@@ -358,7 +413,6 @@ export function composeRuntime(options) {
|
|
|
358
413
|
}
|
|
359
414
|
for (const key of keys) {
|
|
360
415
|
invalidModels.add(key);
|
|
361
|
-
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
362
416
|
models.set(key, revoked);
|
|
363
417
|
liveModels.delete(key);
|
|
364
418
|
}
|
|
@@ -371,10 +425,7 @@ export function composeRuntime(options) {
|
|
|
371
425
|
model: revoked,
|
|
372
426
|
selectionModel: revoked,
|
|
373
427
|
};
|
|
374
|
-
|
|
375
|
-
replicas.set(locator, read.kind === 'staged'
|
|
376
|
-
? replicaOf(read.confirmed, before.queue, rewriting(), before)
|
|
377
|
-
: { ...before, confirmed: { ...before.confirmed, record } });
|
|
428
|
+
replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
|
|
378
429
|
}
|
|
379
430
|
rooms.set(locator, {
|
|
380
431
|
...room,
|
|
@@ -404,15 +455,14 @@ export function composeRuntime(options) {
|
|
|
404
455
|
rooms.set(room.locator, {
|
|
405
456
|
name: room.name,
|
|
406
457
|
locator: room.locator,
|
|
458
|
+
handle: room.handle,
|
|
407
459
|
operations: room.operations,
|
|
408
460
|
replica: room.replica,
|
|
409
|
-
attachedModel: room.attachedModel,
|
|
410
461
|
model: modelTypes(modelOf(room.locator)),
|
|
411
462
|
typedSelection: selection === null
|
|
412
463
|
? null
|
|
413
464
|
: {
|
|
414
465
|
roots: selection.roots,
|
|
415
|
-
attachedModel: selection.attachedModel,
|
|
416
466
|
model: modelTypes(modelOf(room.locator, 'typed-selection')),
|
|
417
467
|
},
|
|
418
468
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
@@ -429,9 +479,6 @@ export function composeRuntime(options) {
|
|
|
429
479
|
discovered.clear();
|
|
430
480
|
for (const room of listed)
|
|
431
481
|
discovered.set(room.locator, room);
|
|
432
|
-
for (const locator of wanted.keys())
|
|
433
|
-
if (!listedLocators.has(locator))
|
|
434
|
-
wanted.delete(locator);
|
|
435
482
|
await dropUnlisted(listedLocators, local);
|
|
436
483
|
for (const room of listed) {
|
|
437
484
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
@@ -445,7 +492,7 @@ export function composeRuntime(options) {
|
|
|
445
492
|
emitRooms();
|
|
446
493
|
}
|
|
447
494
|
function unlistedHeld(listed) {
|
|
448
|
-
return [...new Set([...rooms.keys(), ...roomDrafts.keys()])].filter((locator) => !listed.has(locator));
|
|
495
|
+
return [...new Set([...rooms.keys(), ...roomDrafts.keys(), ...transfers.keys()])].filter((locator) => !listed.has(locator));
|
|
449
496
|
}
|
|
450
497
|
async function dropUnlisted(listed, local) {
|
|
451
498
|
if (unlistedHeld(listed).length === 0)
|
|
@@ -464,7 +511,9 @@ export function composeRuntime(options) {
|
|
|
464
511
|
replicas.delete(locator);
|
|
465
512
|
rooms.delete(locator);
|
|
466
513
|
roomDrafts.delete(locator);
|
|
514
|
+
transfers.delete(locator);
|
|
467
515
|
unreadable.delete(locator);
|
|
516
|
+
stale.delete(locator);
|
|
468
517
|
scopedRefusals.delete(locator);
|
|
469
518
|
sourceRefusals.delete(locator);
|
|
470
519
|
declarationRefusals.delete(locator);
|
|
@@ -473,7 +522,6 @@ export function composeRuntime(options) {
|
|
|
473
522
|
models.delete(key);
|
|
474
523
|
liveModels.delete(key);
|
|
475
524
|
invalidModels.delete(key);
|
|
476
|
-
modelEpoch.delete(key);
|
|
477
525
|
}
|
|
478
526
|
journal.record(`room unlisted ${locator}`);
|
|
479
527
|
}
|
|
@@ -486,7 +534,6 @@ export function composeRuntime(options) {
|
|
|
486
534
|
if (prior !== undefined &&
|
|
487
535
|
prior.status !== 'unavailable' &&
|
|
488
536
|
(persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
|
|
489
|
-
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
490
537
|
invalidModels.add(key);
|
|
491
538
|
}
|
|
492
539
|
const model = persisted.status === 'unavailable' ||
|
|
@@ -501,7 +548,7 @@ export function composeRuntime(options) {
|
|
|
501
548
|
return model;
|
|
502
549
|
}
|
|
503
550
|
async function hydrate(opened) {
|
|
504
|
-
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
|
|
551
|
+
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
|
|
505
552
|
opened.all(),
|
|
506
553
|
opened.queues(),
|
|
507
554
|
opened.roomDrafts(),
|
|
@@ -511,9 +558,15 @@ export function composeRuntime(options) {
|
|
|
511
558
|
opened.lastCompleted(),
|
|
512
559
|
opened.providerLimits(),
|
|
513
560
|
opened.capabilities(),
|
|
561
|
+
opened.member(),
|
|
562
|
+
opened.transfers(),
|
|
514
563
|
]);
|
|
564
|
+
transfers.clear();
|
|
565
|
+
for (const record of staged)
|
|
566
|
+
transfers.set(record.locator, record);
|
|
515
567
|
limits = declared;
|
|
516
568
|
capabilities = named;
|
|
569
|
+
member = known;
|
|
517
570
|
roomDrafts.clear();
|
|
518
571
|
for (const record of kept)
|
|
519
572
|
rememberRoomDraft(record);
|
|
@@ -533,15 +586,8 @@ export function composeRuntime(options) {
|
|
|
533
586
|
const model = restored(modelKey(record.locator, 'collection'), record.model);
|
|
534
587
|
const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
|
|
535
588
|
const prior = previousReplicas.get(record.locator);
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
const confirmed = read.kind === 'staged'
|
|
539
|
-
? read.confirmed
|
|
540
|
-
: {
|
|
541
|
-
record: retyped,
|
|
542
|
-
collection: parseCollection(retyped.documents, compositionOf(retyped)),
|
|
543
|
-
};
|
|
544
|
-
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], rewriting(), prior));
|
|
589
|
+
const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
|
|
590
|
+
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
|
|
545
591
|
const existing = rooms.get(record.locator);
|
|
546
592
|
if (existing === undefined)
|
|
547
593
|
list(record, false);
|
|
@@ -552,21 +598,15 @@ export function composeRuntime(options) {
|
|
|
552
598
|
attachments: record.attachments,
|
|
553
599
|
operations: record.operations,
|
|
554
600
|
replica: record.replica,
|
|
555
|
-
attachedModel: record.attachedModel,
|
|
556
601
|
model: modelTypes(model),
|
|
557
602
|
typedSelection: record.typedSelection === null
|
|
558
603
|
? null
|
|
559
604
|
: {
|
|
560
605
|
roots: record.typedSelection.roots,
|
|
561
|
-
attachedModel: record.typedSelection.attachedModel,
|
|
562
606
|
model: modelTypes(selectionModel),
|
|
563
607
|
},
|
|
564
608
|
});
|
|
565
609
|
}
|
|
566
|
-
if (read.kind === 'refused') {
|
|
567
|
-
refusedSource(record, read);
|
|
568
|
-
declarationRefusals.add(record.locator);
|
|
569
|
-
}
|
|
570
610
|
}
|
|
571
611
|
return {
|
|
572
612
|
lastCompleted,
|
|
@@ -579,6 +619,8 @@ export function composeRuntime(options) {
|
|
|
579
619
|
applyChangeSet(read);
|
|
580
620
|
if (replicas.size > 0)
|
|
581
621
|
emitRooms();
|
|
622
|
+
if (member !== null)
|
|
623
|
+
emitMember();
|
|
582
624
|
for (const locator of locators)
|
|
583
625
|
emitQueue(locator);
|
|
584
626
|
publishRoomDrafts(false);
|
|
@@ -626,10 +668,14 @@ export function composeRuntime(options) {
|
|
|
626
668
|
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
627
669
|
}
|
|
628
670
|
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
629
|
-
const withdrawn = withdrawals(confirmed, queue, withdrawable
|
|
671
|
+
const withdrawn = withdrawals(confirmed, queue, withdrawable);
|
|
630
672
|
const kept = queue.filter((batch) => !withdrawn.includes(batch));
|
|
673
|
+
const unqueued = withdrawn.map((batch) => ({
|
|
674
|
+
batch,
|
|
675
|
+
returned: landed(batch, confirmed) ? null : { kind: 'withdrawn' },
|
|
676
|
+
}));
|
|
631
677
|
const returning = [
|
|
632
|
-
...
|
|
678
|
+
...[...unqueued].reverse(),
|
|
633
679
|
...(settling.leaving === undefined ? [] : [settling.leaving]),
|
|
634
680
|
];
|
|
635
681
|
const written = new Map();
|
|
@@ -650,6 +696,7 @@ export function composeRuntime(options) {
|
|
|
650
696
|
await local.write({
|
|
651
697
|
next: confirmed.record,
|
|
652
698
|
previous: before?.confirmed.record ?? null,
|
|
699
|
+
...(settling.stored === undefined ? {} : { stored: settling.stored }),
|
|
653
700
|
queue: kept,
|
|
654
701
|
previousQueue: before?.queue ?? [],
|
|
655
702
|
roomDrafts: [...written.values()],
|
|
@@ -660,44 +707,79 @@ export function composeRuntime(options) {
|
|
|
660
707
|
}
|
|
661
708
|
for (const record of written.values())
|
|
662
709
|
rememberRoomDraft(record);
|
|
663
|
-
for (const batch of
|
|
664
|
-
journal.record(`outcome ${locator} ${batch.id} withdrawn`);
|
|
665
|
-
|
|
710
|
+
for (const { batch, returned } of unqueued) {
|
|
711
|
+
journal.record(`outcome ${locator} ${batch.id} ${returned === null ? 'landed' : 'withdrawn'}`);
|
|
712
|
+
}
|
|
713
|
+
replicas.set(locator, replicaOf(confirmed, kept, before));
|
|
666
714
|
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
667
715
|
room: settling.room,
|
|
668
716
|
queue: kept.length !== (before?.queue.length ?? 0),
|
|
669
717
|
drafts: [...written.keys()],
|
|
670
718
|
});
|
|
671
719
|
}
|
|
672
|
-
function
|
|
720
|
+
function adoptModels(locator, record) {
|
|
721
|
+
for (const [attached, model] of [
|
|
722
|
+
['collection', record.model],
|
|
723
|
+
['typed-selection', record.selectionModel],
|
|
724
|
+
]) {
|
|
725
|
+
const key = modelKey(locator, attached);
|
|
726
|
+
models.set(key, model);
|
|
727
|
+
invalidModels.delete(key);
|
|
728
|
+
if (model.status === 'current')
|
|
729
|
+
liveModels.set(key, JSON.stringify(model.snapshot));
|
|
730
|
+
else if (model.status === 'unavailable')
|
|
731
|
+
liveModels.delete(key);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
function remodel(locator, answers, held) {
|
|
735
|
+
const taken = (answer, previous, attached) => {
|
|
736
|
+
if (answer === undefined)
|
|
737
|
+
return previous;
|
|
738
|
+
const { state, refused } = attachedModel(answer, previous);
|
|
739
|
+
if (refused !== null) {
|
|
740
|
+
const where = refused.location === undefined ? '' : ` ${refused.location}`;
|
|
741
|
+
journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
|
|
742
|
+
}
|
|
743
|
+
models.set(modelKey(locator, attached), state);
|
|
744
|
+
return state;
|
|
745
|
+
};
|
|
746
|
+
return {
|
|
747
|
+
collection: taken(answers?.collection, held.collection, 'collection'),
|
|
748
|
+
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
function publish(local, room, confirmed, landing = {}) {
|
|
752
|
+
const { base, stored } = landing;
|
|
673
753
|
return exclusive(async () => {
|
|
674
754
|
if (!continuing())
|
|
675
|
-
return;
|
|
755
|
+
return true;
|
|
676
756
|
const prior = replicas.get(room.locator);
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
throw new Error(`Cannot retype staged Room ${room.locator}`);
|
|
757
|
+
if (base !== undefined &&
|
|
758
|
+
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
759
|
+
prior.confirmed.record.revisions !== base.record.revisions)) {
|
|
760
|
+
return false;
|
|
761
|
+
}
|
|
762
|
+
adoptModels(room.locator, confirmed.record);
|
|
684
763
|
const queue = prior?.queue ?? [];
|
|
685
|
-
const roomVersion =
|
|
764
|
+
const roomVersion = confirmed.record.roomVersion;
|
|
686
765
|
const accepted = queue.filter((batch) => {
|
|
687
766
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
688
767
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
689
768
|
});
|
|
690
|
-
const set = await settle(local, room.locator, prior,
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
769
|
+
const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
|
|
770
|
+
if (stored !== undefined)
|
|
771
|
+
transfers.delete(room.locator);
|
|
772
|
+
for (const batch of accepted)
|
|
773
|
+
pendings.delete(batch.id);
|
|
774
|
+
stale.delete(room.locator);
|
|
775
|
+
if (unreadable.delete(room.locator))
|
|
776
|
+
scopedRefusals.delete(room.locator);
|
|
696
777
|
sourceRefusals.delete(room.locator);
|
|
697
778
|
declarationRefusals.delete(room.locator);
|
|
698
779
|
list(room, true);
|
|
699
780
|
publishChangeSet(set);
|
|
700
781
|
moveTo({ ...status, ...attention() });
|
|
782
|
+
return true;
|
|
701
783
|
});
|
|
702
784
|
}
|
|
703
785
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
@@ -726,7 +808,7 @@ export function composeRuntime(options) {
|
|
|
726
808
|
operations: room.operations,
|
|
727
809
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
728
810
|
providerLimits: limits,
|
|
729
|
-
|
|
811
|
+
handle: room.handle,
|
|
730
812
|
visible: replica.visible,
|
|
731
813
|
changes: parts.flatMap(({ changes }) => changes),
|
|
732
814
|
bases: parts.flatMap(({ bases }) => bases),
|
|
@@ -750,15 +832,16 @@ export function composeRuntime(options) {
|
|
|
750
832
|
return { outcome: 'refused', rule: 'durable-write' };
|
|
751
833
|
}
|
|
752
834
|
pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
|
|
835
|
+
recorded = false;
|
|
753
836
|
for (const path of consumed)
|
|
754
837
|
forgetRoomDraft(locator, path);
|
|
755
|
-
replicas.set(locator, replicaOf(replica.confirmed, batches,
|
|
838
|
+
replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
|
|
756
839
|
publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
|
|
757
840
|
queue: true,
|
|
758
841
|
drafts: consumed,
|
|
759
842
|
}));
|
|
760
843
|
moveTo({ ...saving(status), ...attention() });
|
|
761
|
-
|
|
844
|
+
submitLater(locator);
|
|
762
845
|
return { outcome: 'saved', identifier: built.batch.id };
|
|
763
846
|
}
|
|
764
847
|
async function mark(local, locator, identifier, submission, lost) {
|
|
@@ -823,10 +906,10 @@ export function composeRuntime(options) {
|
|
|
823
906
|
return { kind: 'transport-uncertainty' };
|
|
824
907
|
}
|
|
825
908
|
}
|
|
826
|
-
function
|
|
909
|
+
function declaredAgents() {
|
|
827
910
|
for (const capability of capabilities) {
|
|
828
|
-
if (capability.
|
|
829
|
-
return capability.
|
|
911
|
+
if (capability.conversationAgents !== undefined)
|
|
912
|
+
return capability.conversationAgents;
|
|
830
913
|
}
|
|
831
914
|
return undefined;
|
|
832
915
|
}
|
|
@@ -857,8 +940,6 @@ export function composeRuntime(options) {
|
|
|
857
940
|
journal.record(`transfer action ${name}`);
|
|
858
941
|
try {
|
|
859
942
|
const result = await options.adapter.action(authorization, { name, input });
|
|
860
|
-
if (result.kind === 'action' && result.at !== undefined)
|
|
861
|
-
want(result.at);
|
|
862
943
|
if (result.kind === 'scoped-refusal') {
|
|
863
944
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
864
945
|
if (room !== undefined)
|
|
@@ -890,32 +971,11 @@ export function composeRuntime(options) {
|
|
|
890
971
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
891
972
|
!options.adapter.reached(locator, roomVersion, written));
|
|
892
973
|
}
|
|
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) {
|
|
974
|
+
async function pullRefresh(authorized, confirmed) {
|
|
915
975
|
if (!continuing())
|
|
916
976
|
return unconfirmed;
|
|
917
977
|
try {
|
|
918
|
-
const result = await refreshRoom(options.adapter, authorized, confirmed,
|
|
978
|
+
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
|
|
919
979
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
920
980
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
921
981
|
return result;
|
|
@@ -925,14 +985,33 @@ export function composeRuntime(options) {
|
|
|
925
985
|
throw error;
|
|
926
986
|
}
|
|
927
987
|
}
|
|
928
|
-
|
|
988
|
+
function staging(local) {
|
|
989
|
+
return {
|
|
990
|
+
held: (locator) => transfers.get(locator) ?? null,
|
|
991
|
+
staged: (record) => local.staged(record),
|
|
992
|
+
stage: (record, page) => exclusive(async () => {
|
|
993
|
+
if (!continuing())
|
|
994
|
+
return false;
|
|
995
|
+
await local.stage(record, page);
|
|
996
|
+
transfers.set(record.locator, record);
|
|
997
|
+
return true;
|
|
998
|
+
}),
|
|
999
|
+
abandon: (locator) => exclusive(async () => {
|
|
1000
|
+
if (!continuing())
|
|
1001
|
+
return;
|
|
1002
|
+
await local.abandon(locator);
|
|
1003
|
+
transfers.delete(locator);
|
|
1004
|
+
}),
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
async function pullBootstrap(authorized, room, local) {
|
|
929
1008
|
if (!continuing())
|
|
930
1009
|
return unconfirmed;
|
|
931
1010
|
try {
|
|
932
1011
|
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
933
1012
|
collection: modelOf(room.locator),
|
|
934
1013
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
935
|
-
}, continuing);
|
|
1014
|
+
}, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
|
|
936
1015
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
937
1016
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
938
1017
|
return result;
|
|
@@ -945,35 +1024,46 @@ export function composeRuntime(options) {
|
|
|
945
1024
|
async function advance(room, local, authorized) {
|
|
946
1025
|
const held = replicas.get(room.locator);
|
|
947
1026
|
if (held !== undefined) {
|
|
948
|
-
const refreshed = await pullRefresh(authorized, held.confirmed
|
|
1027
|
+
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
949
1028
|
if (!continuing())
|
|
950
1029
|
return unconfirmed;
|
|
951
1030
|
if (refreshed.kind === 'still-current') {
|
|
1031
|
+
stale.delete(room.locator);
|
|
952
1032
|
if (sourceRefusals.has(room.locator))
|
|
953
|
-
return
|
|
1033
|
+
return null;
|
|
954
1034
|
if (unreadable.delete(room.locator)) {
|
|
1035
|
+
scopedRefusals.delete(room.locator);
|
|
955
1036
|
list(room, true);
|
|
956
1037
|
emitRooms();
|
|
957
1038
|
}
|
|
958
1039
|
return null;
|
|
959
1040
|
}
|
|
960
1041
|
if (refreshed.kind === 'advanced') {
|
|
961
|
-
await publish(local, room, refreshed.confirmed)
|
|
1042
|
+
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
|
|
1043
|
+
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1044
|
+
const reached = now !== undefined &&
|
|
1045
|
+
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
1046
|
+
return reached ? null : advance(room, local, authorized);
|
|
1047
|
+
}
|
|
1048
|
+
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
962
1049
|
return null;
|
|
963
1050
|
}
|
|
964
1051
|
if (refreshed.kind !== 'beyond-horizon')
|
|
965
1052
|
return refreshed;
|
|
966
1053
|
}
|
|
967
|
-
const bootstrapped = await pullBootstrap(authorized, room);
|
|
1054
|
+
const bootstrapped = await pullBootstrap(authorized, room, local);
|
|
968
1055
|
if (!continuing())
|
|
969
1056
|
return unconfirmed;
|
|
970
1057
|
if (bootstrapped.kind !== 'staged')
|
|
971
1058
|
return bootstrapped;
|
|
972
|
-
await publish(local, room, bootstrapped.confirmed);
|
|
1059
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
973
1060
|
return null;
|
|
974
1061
|
}
|
|
975
1062
|
async function withhold(withheld, room, local, progress) {
|
|
976
|
-
|
|
1063
|
+
if (withheld.kind === 'refused')
|
|
1064
|
+
stale.delete(room.locator);
|
|
1065
|
+
else
|
|
1066
|
+
stale.add(room.locator);
|
|
977
1067
|
if (withheld.kind === 'scoped-refusal') {
|
|
978
1068
|
await scoped(withheld, room, local);
|
|
979
1069
|
}
|
|
@@ -1020,44 +1110,30 @@ export function composeRuntime(options) {
|
|
|
1020
1110
|
(accepted.author === undefined ? '' : ` author ${accepted.author}`) +
|
|
1021
1111
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1022
1112
|
}
|
|
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) {
|
|
1113
|
+
async function adoptAccepted(room, local, submitted, accepted) {
|
|
1041
1114
|
if (!continuing())
|
|
1042
1115
|
return unconfirmed;
|
|
1043
1116
|
const current = replicas.get(room.locator);
|
|
1044
1117
|
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
1045
1118
|
return null;
|
|
1046
1119
|
}
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1120
|
+
const held = current.confirmed.record.roomVersion;
|
|
1121
|
+
logAccepted(room.locator, submitted, accepted);
|
|
1122
|
+
const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
1123
|
+
? { kind: 'staged', confirmed: current.confirmed }
|
|
1124
|
+
: applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.changes, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
1125
|
+
? accepted.roomVersion
|
|
1126
|
+
: held, accepted.roomVersion);
|
|
1127
|
+
if (applied.kind !== 'staged') {
|
|
1128
|
+
const state = pendings.get(submitted.id);
|
|
1129
|
+
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1130
|
+
if (state !== undefined)
|
|
1131
|
+
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1132
|
+
emitQueue(room.locator);
|
|
1057
1133
|
return applied;
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1134
|
+
}
|
|
1135
|
+
const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
1136
|
+
pendings.delete(submitted.id);
|
|
1061
1137
|
unreadable.delete(room.locator);
|
|
1062
1138
|
sourceRefusals.delete(room.locator);
|
|
1063
1139
|
declarationRefusals.delete(room.locator);
|
|
@@ -1066,17 +1142,8 @@ export function composeRuntime(options) {
|
|
|
1066
1142
|
moveTo({ ...status, ...attention() });
|
|
1067
1143
|
return null;
|
|
1068
1144
|
}
|
|
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));
|
|
1145
|
+
async function adopted(room, local, submitted, accepted) {
|
|
1146
|
+
const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
|
|
1080
1147
|
if (failed === null)
|
|
1081
1148
|
return true;
|
|
1082
1149
|
if (failed.kind === 'refused' && continuing())
|
|
@@ -1089,10 +1156,8 @@ export function composeRuntime(options) {
|
|
|
1089
1156
|
const head = replica?.queue[0];
|
|
1090
1157
|
if (replica === undefined || head === undefined)
|
|
1091
1158
|
return;
|
|
1092
|
-
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1093
|
-
progress.complete = false;
|
|
1159
|
+
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1094
1160
|
return;
|
|
1095
|
-
}
|
|
1096
1161
|
const state = pendings.get(head.id);
|
|
1097
1162
|
const { submission, lost: earlier } = state ?? {
|
|
1098
1163
|
submission: { state: 'waiting' },
|
|
@@ -1120,6 +1185,7 @@ export function composeRuntime(options) {
|
|
|
1120
1185
|
return;
|
|
1121
1186
|
}
|
|
1122
1187
|
const opensBefore = openSignals;
|
|
1188
|
+
const answered = awaitingAnswer(room.locator);
|
|
1123
1189
|
try {
|
|
1124
1190
|
result = await options.adapter.mutate(authorized, {
|
|
1125
1191
|
locator: room.locator,
|
|
@@ -1128,11 +1194,14 @@ export function composeRuntime(options) {
|
|
|
1128
1194
|
}, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
|
|
1129
1195
|
}
|
|
1130
1196
|
catch {
|
|
1197
|
+
answered();
|
|
1131
1198
|
progress.complete = false;
|
|
1132
1199
|
if (continuing())
|
|
1133
1200
|
progress.reachable = false;
|
|
1134
1201
|
return;
|
|
1135
1202
|
}
|
|
1203
|
+
if (result.kind !== 'accepted' || !continuing())
|
|
1204
|
+
answered();
|
|
1136
1205
|
if (!continuing())
|
|
1137
1206
|
return;
|
|
1138
1207
|
if (result.kind === 'transport-uncertainty') {
|
|
@@ -1185,6 +1254,7 @@ export function composeRuntime(options) {
|
|
|
1185
1254
|
const reset = result.reset;
|
|
1186
1255
|
progress.complete = false;
|
|
1187
1256
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1257
|
+
armTimer();
|
|
1188
1258
|
return;
|
|
1189
1259
|
}
|
|
1190
1260
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
@@ -1197,7 +1267,8 @@ export function composeRuntime(options) {
|
|
|
1197
1267
|
return;
|
|
1198
1268
|
}
|
|
1199
1269
|
const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
1200
|
-
leaving: result.kind === 'expired' && landed(head, current.confirmed)
|
|
1270
|
+
leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
|
|
1271
|
+
(result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
|
|
1201
1272
|
? { ...left, returned: null }
|
|
1202
1273
|
: left,
|
|
1203
1274
|
});
|
|
@@ -1211,95 +1282,33 @@ export function composeRuntime(options) {
|
|
|
1211
1282
|
return withhold(withheld, room, local, progress);
|
|
1212
1283
|
continue;
|
|
1213
1284
|
}
|
|
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
|
-
}
|
|
1285
|
+
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1286
|
+
if (!kept) {
|
|
1258
1287
|
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
1288
|
return;
|
|
1271
1289
|
}
|
|
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
1290
|
}
|
|
1289
|
-
|
|
1291
|
+
}
|
|
1292
|
+
function awaitingAnswer(locator) {
|
|
1293
|
+
let release = () => { };
|
|
1294
|
+
const awaited = new Promise((resolve) => {
|
|
1295
|
+
release = resolve;
|
|
1296
|
+
});
|
|
1297
|
+
answering.set(locator, awaited);
|
|
1298
|
+
return () => {
|
|
1299
|
+
if (answering.get(locator) === awaited)
|
|
1300
|
+
answering.delete(locator);
|
|
1301
|
+
release();
|
|
1302
|
+
};
|
|
1290
1303
|
}
|
|
1291
1304
|
async function retainModel(local, room, attached, next) {
|
|
1292
1305
|
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
1306
|
const previous = modelOf(room.locator, attached);
|
|
1297
1307
|
const heldBefore = replicas.get(room.locator)?.confirmed.record;
|
|
1298
1308
|
if (JSON.stringify(previous) === JSON.stringify(next) &&
|
|
1299
1309
|
(heldBefore === undefined ||
|
|
1300
1310
|
(JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
|
|
1301
1311
|
JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
|
|
1302
|
-
JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) &&
|
|
1303
1312
|
JSON.stringify(heldBefore.typedSelection) ===
|
|
1304
1313
|
JSON.stringify(room.typedSelection ?? null))))
|
|
1305
1314
|
return;
|
|
@@ -1310,20 +1319,17 @@ export function composeRuntime(options) {
|
|
|
1310
1319
|
await exclusive(async () => {
|
|
1311
1320
|
if (!continuing())
|
|
1312
1321
|
return;
|
|
1313
|
-
if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0))
|
|
1314
|
-
return;
|
|
1315
1322
|
const held = replicas.get(room.locator);
|
|
1316
1323
|
if (held !== undefined) {
|
|
1317
1324
|
const declared = {
|
|
1318
1325
|
...held.confirmed.record,
|
|
1319
1326
|
operations: room.operations,
|
|
1320
1327
|
replica: room.replica,
|
|
1321
|
-
attachedModel: room.attachedModel,
|
|
1322
1328
|
typedSelection: room.typedSelection ?? null,
|
|
1323
1329
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
1324
1330
|
};
|
|
1325
1331
|
const reclassified = sameMembership(declared, held.confirmed.record)
|
|
1326
|
-
?
|
|
1332
|
+
? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
|
|
1327
1333
|
: readReclassified(declared, held.confirmed);
|
|
1328
1334
|
const record = reclassified.kind === 'staged'
|
|
1329
1335
|
? declared
|
|
@@ -1331,14 +1337,16 @@ export function composeRuntime(options) {
|
|
|
1331
1337
|
...declared,
|
|
1332
1338
|
typedSelection: held.confirmed.record.typedSelection,
|
|
1333
1339
|
};
|
|
1334
|
-
const read = reclassified.kind === 'deferred'
|
|
1340
|
+
const read = reclassified.kind === 'deferred'
|
|
1341
|
+
? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
|
|
1342
|
+
: reclassified;
|
|
1335
1343
|
await local.write({
|
|
1336
1344
|
next: record,
|
|
1337
1345
|
previous: held.confirmed.record,
|
|
1338
1346
|
queue: held.queue,
|
|
1339
1347
|
});
|
|
1340
1348
|
if (read.kind === 'staged') {
|
|
1341
|
-
replicas.set(room.locator, replicaOf(read.confirmed, held.queue,
|
|
1349
|
+
replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
|
|
1342
1350
|
if (declarationRefusals.delete(room.locator)) {
|
|
1343
1351
|
sourceRefusals.delete(room.locator);
|
|
1344
1352
|
unreadable.delete(room.locator);
|
|
@@ -1362,77 +1370,6 @@ export function composeRuntime(options) {
|
|
|
1362
1370
|
publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
|
|
1363
1371
|
});
|
|
1364
1372
|
}
|
|
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
1373
|
async function scoped(result, room, local) {
|
|
1437
1374
|
scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
|
|
1438
1375
|
let scopedAway = room;
|
|
@@ -1460,14 +1397,36 @@ export function composeRuntime(options) {
|
|
|
1460
1397
|
emitRooms();
|
|
1461
1398
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
1462
1399
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1400
|
+
function heldRooms(listed) {
|
|
1401
|
+
return listed.flatMap((room) => {
|
|
1402
|
+
const record = replicas.get(room.locator)?.confirmed.record;
|
|
1403
|
+
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1404
|
+
return [];
|
|
1405
|
+
return [
|
|
1406
|
+
{
|
|
1407
|
+
locator: room.locator,
|
|
1408
|
+
roomVersion: record.roomVersion,
|
|
1409
|
+
documents: record.documents,
|
|
1410
|
+
revisions: record.revisions,
|
|
1411
|
+
attachments: record.attachments,
|
|
1412
|
+
},
|
|
1413
|
+
];
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
async function synchronize(listed, local, progress, attempt) {
|
|
1417
|
+
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
|
|
1418
|
+
const ready = listed.filter((_, index) => {
|
|
1466
1419
|
const outcome = prepared[index];
|
|
1467
1420
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1468
1421
|
});
|
|
1469
|
-
|
|
1470
|
-
|
|
1422
|
+
if (attempt === 'opened') {
|
|
1423
|
+
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
1424
|
+
handed = true;
|
|
1425
|
+
}
|
|
1426
|
+
const loaded = await Promise.allSettled(ready
|
|
1427
|
+
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1428
|
+
.map((room) => loadRoom(room, local, progress)));
|
|
1429
|
+
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1471
1430
|
if (failed !== undefined)
|
|
1472
1431
|
throw failed.reason;
|
|
1473
1432
|
}
|
|
@@ -1484,11 +1443,8 @@ export function composeRuntime(options) {
|
|
|
1484
1443
|
progress.complete = false;
|
|
1485
1444
|
return false;
|
|
1486
1445
|
}
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
acquireModel(local, room, authorization, null),
|
|
1490
|
-
...(selection === undefined ? [] : [acquireModel(local, room, authorization, selection)]),
|
|
1491
|
-
]);
|
|
1446
|
+
if (prior !== undefined)
|
|
1447
|
+
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1492
1448
|
if (discoveryPaused || authorization === undefined)
|
|
1493
1449
|
return false;
|
|
1494
1450
|
if (room.replica.admission !== 'admitted') {
|
|
@@ -1496,24 +1452,53 @@ export function composeRuntime(options) {
|
|
|
1496
1452
|
progress.complete = false;
|
|
1497
1453
|
return false;
|
|
1498
1454
|
}
|
|
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
1455
|
return true;
|
|
1505
1456
|
}
|
|
1506
|
-
async function
|
|
1457
|
+
async function caughtUp(room, local, progress) {
|
|
1458
|
+
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1459
|
+
if (held === undefined || authorization === undefined)
|
|
1460
|
+
return;
|
|
1461
|
+
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
1462
|
+
if (!behind)
|
|
1463
|
+
return;
|
|
1464
|
+
const withheld = await advance(room, local, authorization);
|
|
1465
|
+
if (withheld !== null)
|
|
1466
|
+
await withhold(withheld, room, local, progress);
|
|
1467
|
+
}
|
|
1468
|
+
function revoked(locator) {
|
|
1469
|
+
return ['collection', 'typed-selection'].some((attached) => {
|
|
1470
|
+
const model = modelOf(locator, attached);
|
|
1471
|
+
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
async function loadRoom(room, local, progress) {
|
|
1507
1475
|
if (discoveryPaused || authorization === undefined)
|
|
1508
1476
|
return;
|
|
1477
|
+
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1478
|
+
await reload(room, local, progress);
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1509
1481
|
const withheld = await advance(room, local, authorization);
|
|
1510
1482
|
if (withheld !== null) {
|
|
1511
1483
|
await withhold(withheld, room, local, progress);
|
|
1512
1484
|
return;
|
|
1513
1485
|
}
|
|
1514
|
-
await
|
|
1486
|
+
await caughtUp(room, local, progress);
|
|
1515
1487
|
}
|
|
1516
|
-
async function
|
|
1488
|
+
async function reload(room, local, progress) {
|
|
1489
|
+
if (discoveryPaused || authorization === undefined)
|
|
1490
|
+
return;
|
|
1491
|
+
const bootstrapped = await pullBootstrap(authorization, room, local);
|
|
1492
|
+
if (!continuing())
|
|
1493
|
+
return;
|
|
1494
|
+
if (bootstrapped.kind !== 'staged') {
|
|
1495
|
+
await withhold(bootstrapped, room, local, progress);
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
1499
|
+
await caughtUp(room, local, progress);
|
|
1500
|
+
}
|
|
1501
|
+
async function discoverRooms(local, progress, attempt) {
|
|
1517
1502
|
if (closed || authorization === undefined) {
|
|
1518
1503
|
notifyReady();
|
|
1519
1504
|
return;
|
|
@@ -1539,6 +1524,7 @@ export function composeRuntime(options) {
|
|
|
1539
1524
|
}
|
|
1540
1525
|
}
|
|
1541
1526
|
progress.reachable = false;
|
|
1527
|
+
progress.complete = false;
|
|
1542
1528
|
notifyReady();
|
|
1543
1529
|
return;
|
|
1544
1530
|
}
|
|
@@ -1553,17 +1539,6 @@ export function composeRuntime(options) {
|
|
|
1553
1539
|
notifyReady();
|
|
1554
1540
|
return;
|
|
1555
1541
|
}
|
|
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
1542
|
for (const room of result.rooms) {
|
|
1568
1543
|
if (room.operations === undefined ||
|
|
1569
1544
|
!['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
@@ -1571,8 +1546,7 @@ export function composeRuntime(options) {
|
|
|
1571
1546
|
room.replica === undefined ||
|
|
1572
1547
|
typeof room.replica.selection !== 'string' ||
|
|
1573
1548
|
room.replica.selection.length === 0 ||
|
|
1574
|
-
!['admitted', 'unavailable'].includes(room.replica.admission)
|
|
1575
|
-
!declaredIdentity(room.attachedModel)) {
|
|
1549
|
+
!['admitted', 'unavailable'].includes(room.replica.admission)) {
|
|
1576
1550
|
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
1577
1551
|
await refuse('missing-required-guarantee');
|
|
1578
1552
|
notifyReady();
|
|
@@ -1585,14 +1559,6 @@ export function composeRuntime(options) {
|
|
|
1585
1559
|
return;
|
|
1586
1560
|
}
|
|
1587
1561
|
}
|
|
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
1562
|
if (refusal !== null) {
|
|
1597
1563
|
refusal = null;
|
|
1598
1564
|
emitRefusal();
|
|
@@ -1600,26 +1566,19 @@ export function composeRuntime(options) {
|
|
|
1600
1566
|
const previousCapabilities = capabilities;
|
|
1601
1567
|
capabilities = result.capabilities;
|
|
1602
1568
|
notifyReady();
|
|
1603
|
-
if (declared.has('room-version-notifications'))
|
|
1604
|
-
observeRoomVersions();
|
|
1605
1569
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1606
1570
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
1607
1571
|
limits = result.providerLimits;
|
|
1608
1572
|
await local.declare(limits, capabilities);
|
|
1609
1573
|
}
|
|
1574
|
+
const signedIn = result.member ?? null;
|
|
1575
|
+
if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
|
|
1576
|
+
await local.keepMember(signedIn);
|
|
1577
|
+
member = signedIn;
|
|
1578
|
+
emitMember();
|
|
1579
|
+
}
|
|
1610
1580
|
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);
|
|
1581
|
+
await synchronize(result.rooms, local, progress, attempt);
|
|
1623
1582
|
}
|
|
1624
1583
|
async function ensureAuthorization() {
|
|
1625
1584
|
if (authorization !== undefined)
|
|
@@ -1633,64 +1592,253 @@ export function composeRuntime(options) {
|
|
|
1633
1592
|
throw error;
|
|
1634
1593
|
}
|
|
1635
1594
|
}
|
|
1636
|
-
|
|
1637
|
-
if (
|
|
1638
|
-
|
|
1595
|
+
function task(work) {
|
|
1596
|
+
if (stopping)
|
|
1597
|
+
return chain;
|
|
1598
|
+
chain = chain.then(work, work).catch(() => undefined);
|
|
1599
|
+
return chain;
|
|
1600
|
+
}
|
|
1601
|
+
function sync(attempt) {
|
|
1602
|
+
return task(async () => {
|
|
1603
|
+
if (closed || stopping || discoveryPaused) {
|
|
1604
|
+
notifyReady();
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
const progress = { complete: true, reachable: true };
|
|
1608
|
+
underway = attempt === 'opened' ? progress : null;
|
|
1609
|
+
complete = false;
|
|
1610
|
+
recorded = false;
|
|
1611
|
+
moveTo(transferring(status));
|
|
1612
|
+
handed = false;
|
|
1613
|
+
try {
|
|
1614
|
+
const local = await openLocal();
|
|
1615
|
+
if (closed)
|
|
1616
|
+
return;
|
|
1617
|
+
await ensureAuthorization();
|
|
1618
|
+
if (closed)
|
|
1619
|
+
return;
|
|
1620
|
+
await discoverRooms(local, progress, attempt);
|
|
1621
|
+
}
|
|
1622
|
+
catch {
|
|
1623
|
+
progress.complete = false;
|
|
1624
|
+
if (attempt === 'opened' && !handed)
|
|
1625
|
+
progress.reachable = false;
|
|
1626
|
+
}
|
|
1627
|
+
finally {
|
|
1628
|
+
notifyReady();
|
|
1629
|
+
}
|
|
1630
|
+
if (attempt !== 'opened') {
|
|
1631
|
+
await finishOpening(progress);
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
if (handed || closed || progress.reachable)
|
|
1635
|
+
return;
|
|
1636
|
+
underway = null;
|
|
1637
|
+
journal.record('connection reopen');
|
|
1638
|
+
await rest(progress);
|
|
1639
|
+
options.adapter.roomStream?.reopen();
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
function closedSocket(revoked) {
|
|
1643
|
+
const opened = socketOpen;
|
|
1644
|
+
socketOpen = false;
|
|
1645
|
+
if (revoked) {
|
|
1646
|
+
void refuse('revoked-authorization');
|
|
1639
1647
|
return;
|
|
1640
1648
|
}
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
if (closed)
|
|
1649
|
+
if (opened || status.providerUnreachable)
|
|
1650
|
+
return;
|
|
1651
|
+
moveTo({ ...status, providerUnreachable: true });
|
|
1652
|
+
}
|
|
1653
|
+
function applyOpening(expired) {
|
|
1654
|
+
return task(async () => {
|
|
1655
|
+
const progress = underway;
|
|
1656
|
+
underway = null;
|
|
1657
|
+
if (closed || stopping)
|
|
1650
1658
|
return;
|
|
1651
|
-
|
|
1652
|
-
|
|
1659
|
+
if (progress === null) {
|
|
1660
|
+
await reloadExpired(expired);
|
|
1653
1661
|
return;
|
|
1654
|
-
if (progress.complete) {
|
|
1655
|
-
completedAt = new Date().toISOString();
|
|
1656
|
-
await local.complete(completedAt);
|
|
1657
1662
|
}
|
|
1663
|
+
try {
|
|
1664
|
+
const local = await openLocal();
|
|
1665
|
+
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
1666
|
+
const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
|
|
1667
|
+
const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
|
|
1668
|
+
if (progress.reachable) {
|
|
1669
|
+
for (const room of held)
|
|
1670
|
+
if (!reloading.includes(room))
|
|
1671
|
+
await confirmModels(room, local);
|
|
1672
|
+
}
|
|
1673
|
+
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1674
|
+
if (failed !== undefined)
|
|
1675
|
+
throw failed.reason;
|
|
1676
|
+
}
|
|
1677
|
+
catch {
|
|
1678
|
+
progress.complete = false;
|
|
1679
|
+
progress.reachable = false;
|
|
1680
|
+
}
|
|
1681
|
+
await finishOpening(progress);
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
async function reloadExpired(expired) {
|
|
1685
|
+
const progress = { complete: true, reachable: true };
|
|
1686
|
+
try {
|
|
1687
|
+
const local = await openLocal();
|
|
1688
|
+
await Promise.allSettled([...discovered.values()]
|
|
1689
|
+
.filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
|
|
1690
|
+
.map((room) => reload(room, local, progress)));
|
|
1658
1691
|
}
|
|
1659
1692
|
catch {
|
|
1660
|
-
progress.
|
|
1661
|
-
|
|
1693
|
+
progress.reachable = false;
|
|
1694
|
+
}
|
|
1695
|
+
await rest(progress);
|
|
1696
|
+
}
|
|
1697
|
+
async function confirmModels(room, local) {
|
|
1698
|
+
for (const attached of ['collection', 'typed-selection']) {
|
|
1699
|
+
const model = modelOf(room.locator, attached);
|
|
1700
|
+
if (model.status === 'cached') {
|
|
1701
|
+
await retainModel(local, room, attached, { ...model, status: 'current' });
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
function submitLater(locator) {
|
|
1706
|
+
const running = submitting.get(locator);
|
|
1707
|
+
if (running !== undefined) {
|
|
1708
|
+
running.again = true;
|
|
1662
1709
|
return;
|
|
1663
1710
|
}
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1711
|
+
const run = { again: false, done: Promise.resolve() };
|
|
1712
|
+
submitting.set(locator, run);
|
|
1713
|
+
run.done = (async () => {
|
|
1714
|
+
try {
|
|
1715
|
+
do {
|
|
1716
|
+
run.again = false;
|
|
1717
|
+
const room = discovered.get(locator);
|
|
1718
|
+
if (closed || stopping || discoveryPaused || room === undefined)
|
|
1719
|
+
return;
|
|
1720
|
+
const progress = { complete: true, reachable: true };
|
|
1721
|
+
try {
|
|
1722
|
+
await ensureAuthorization();
|
|
1723
|
+
if (authorization === undefined)
|
|
1724
|
+
return;
|
|
1725
|
+
await submitRoom(room, await openLocal(), authorization, progress);
|
|
1726
|
+
}
|
|
1727
|
+
catch {
|
|
1728
|
+
progress.reachable = false;
|
|
1729
|
+
}
|
|
1730
|
+
armTimer();
|
|
1731
|
+
await rest(progress);
|
|
1732
|
+
} while (run.again);
|
|
1733
|
+
}
|
|
1734
|
+
finally {
|
|
1735
|
+
submitting.delete(locator);
|
|
1736
|
+
}
|
|
1737
|
+
})();
|
|
1738
|
+
}
|
|
1739
|
+
function submitAll() {
|
|
1740
|
+
for (const [locator, replica] of replicas) {
|
|
1741
|
+
if (replica.queue.length > 0)
|
|
1742
|
+
submitLater(locator);
|
|
1667
1743
|
}
|
|
1668
1744
|
}
|
|
1669
|
-
async function
|
|
1745
|
+
async function finishOpening(progress) {
|
|
1670
1746
|
try {
|
|
1671
|
-
|
|
1747
|
+
if (closed || discoveryPaused) {
|
|
1748
|
+
progress.complete = false;
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
submitAll();
|
|
1752
|
+
await conversations.reconcile();
|
|
1753
|
+
if (status.providerUnreachable)
|
|
1754
|
+
conversations.reconnected();
|
|
1755
|
+
const reading = { complete: true, reachable: true };
|
|
1756
|
+
await conversations.catchUp(reading);
|
|
1757
|
+
if (!reading.reachable)
|
|
1758
|
+
progress.reachable = false;
|
|
1759
|
+
}
|
|
1760
|
+
catch {
|
|
1761
|
+
progress.complete = false;
|
|
1672
1762
|
}
|
|
1673
1763
|
finally {
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
queued = false;
|
|
1677
|
-
void enqueue();
|
|
1678
|
-
}
|
|
1764
|
+
complete = progress.complete;
|
|
1765
|
+
await rest(progress);
|
|
1679
1766
|
}
|
|
1680
1767
|
}
|
|
1681
|
-
function
|
|
1682
|
-
|
|
1683
|
-
if (path === undefined || withdrawNotifications !== undefined)
|
|
1768
|
+
async function rest(progress) {
|
|
1769
|
+
if (closed)
|
|
1684
1770
|
return;
|
|
1685
|
-
|
|
1686
|
-
|
|
1771
|
+
let lastCompleted = status.lastCompleted;
|
|
1772
|
+
let synced = complete && stale.size === 0 && !pending();
|
|
1773
|
+
if (synced && !recorded) {
|
|
1774
|
+
const completedAt = new Date().toISOString();
|
|
1775
|
+
try {
|
|
1776
|
+
await (await openLocal()).complete(completedAt);
|
|
1777
|
+
recorded = true;
|
|
1778
|
+
lastCompleted = completedAt;
|
|
1779
|
+
}
|
|
1780
|
+
catch {
|
|
1781
|
+
synced = false;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
moveTo({
|
|
1785
|
+
state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
|
|
1786
|
+
lastCompleted,
|
|
1787
|
+
providerUnreachable: !progress.reachable,
|
|
1788
|
+
...attention(),
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
function applyRoom(locator, roomVersion) {
|
|
1792
|
+
announced.set(locator, roomVersion);
|
|
1793
|
+
return task(async () => {
|
|
1794
|
+
await answering.get(locator);
|
|
1795
|
+
if (closed || stopping || discoveryPaused || authorization === undefined)
|
|
1796
|
+
return;
|
|
1797
|
+
const room = discovered.get(locator);
|
|
1798
|
+
const held = replicas.get(locator);
|
|
1799
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
1687
1800
|
return;
|
|
1688
|
-
if (
|
|
1689
|
-
|
|
1801
|
+
if (held !== undefined &&
|
|
1802
|
+
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
1803
|
+
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
const during = underway;
|
|
1807
|
+
const progress = during ?? { complete: true, reachable: true };
|
|
1808
|
+
if (during === null)
|
|
1809
|
+
moveTo(transferring(status));
|
|
1810
|
+
try {
|
|
1811
|
+
const local = await openLocal();
|
|
1812
|
+
const withheld = await advance(room, local, authorization);
|
|
1813
|
+
if (withheld !== null)
|
|
1814
|
+
await withhold(withheld, room, local, progress);
|
|
1815
|
+
}
|
|
1816
|
+
catch {
|
|
1817
|
+
stale.add(locator);
|
|
1818
|
+
}
|
|
1819
|
+
finally {
|
|
1820
|
+
if (during === null)
|
|
1821
|
+
await rest(progress);
|
|
1822
|
+
}
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
function observeStream() {
|
|
1826
|
+
const stream = options.adapter.roomStream;
|
|
1827
|
+
if (stream === undefined || withdrawStream !== undefined)
|
|
1828
|
+
return;
|
|
1829
|
+
withdrawStream = stream.observe((event) => {
|
|
1830
|
+
if (closed || stopping)
|
|
1690
1831
|
return;
|
|
1832
|
+
if (event.kind === 'opened') {
|
|
1833
|
+
socketOpen = true;
|
|
1834
|
+
void sync('opened');
|
|
1691
1835
|
}
|
|
1692
|
-
|
|
1693
|
-
|
|
1836
|
+
else if (event.kind === 'closed')
|
|
1837
|
+
closedSocket(event.revoked);
|
|
1838
|
+
else if (event.kind === 'opening')
|
|
1839
|
+
void applyOpening(event.expired);
|
|
1840
|
+
else
|
|
1841
|
+
void applyRoom(event.locator, event.roomVersion);
|
|
1694
1842
|
});
|
|
1695
1843
|
}
|
|
1696
1844
|
function writesHeld() {
|
|
@@ -1711,14 +1859,17 @@ export function composeRuntime(options) {
|
|
|
1711
1859
|
listener(mode);
|
|
1712
1860
|
}
|
|
1713
1861
|
if (releasing)
|
|
1714
|
-
|
|
1862
|
+
submitAll();
|
|
1715
1863
|
}
|
|
1716
1864
|
function observeConversations() {
|
|
1717
1865
|
const stream = options.adapter.conversations;
|
|
1718
1866
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
1719
1867
|
return;
|
|
1720
1868
|
withdrawConversations = stream.observe((event) => {
|
|
1721
|
-
conversations.receive(event)
|
|
1869
|
+
if (conversations.receive(event)) {
|
|
1870
|
+
void ready.then(scheduleCurrency);
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1722
1873
|
if (event.kind !== 'gap' || conversations.current())
|
|
1723
1874
|
return;
|
|
1724
1875
|
show();
|
|
@@ -1729,7 +1880,7 @@ export function composeRuntime(options) {
|
|
|
1729
1880
|
if (currencyQueued || closed || stopping)
|
|
1730
1881
|
return;
|
|
1731
1882
|
currencyQueued = true;
|
|
1732
|
-
|
|
1883
|
+
void task(runCurrency);
|
|
1733
1884
|
}
|
|
1734
1885
|
async function runCurrency() {
|
|
1735
1886
|
currencyQueued = false;
|
|
@@ -1755,95 +1906,12 @@ export function composeRuntime(options) {
|
|
|
1755
1906
|
}
|
|
1756
1907
|
}
|
|
1757
1908
|
}
|
|
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
1909
|
function close() {
|
|
1842
1910
|
if (closed)
|
|
1843
1911
|
return;
|
|
1844
1912
|
closed = true;
|
|
1845
|
-
|
|
1846
|
-
|
|
1913
|
+
withdrawStream?.();
|
|
1914
|
+
withdrawStream = undefined;
|
|
1847
1915
|
withdrawConversations?.();
|
|
1848
1916
|
withdrawConversations = undefined;
|
|
1849
1917
|
withdrawServiceMode?.();
|
|
@@ -1858,6 +1926,7 @@ export function composeRuntime(options) {
|
|
|
1858
1926
|
storageLossListeners.clear();
|
|
1859
1927
|
statusListeners.clear();
|
|
1860
1928
|
serviceModeListeners.clear();
|
|
1929
|
+
memberListeners.clear();
|
|
1861
1930
|
readSide.close();
|
|
1862
1931
|
queueSubscriptions.clear();
|
|
1863
1932
|
roomDraftListeners.clear();
|
|
@@ -1867,6 +1936,7 @@ export function composeRuntime(options) {
|
|
|
1867
1936
|
stopping = true;
|
|
1868
1937
|
stopTimer();
|
|
1869
1938
|
await chain.catch(() => undefined);
|
|
1939
|
+
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
1870
1940
|
await mutations;
|
|
1871
1941
|
close();
|
|
1872
1942
|
}
|
|
@@ -1875,11 +1945,32 @@ export function composeRuntime(options) {
|
|
|
1875
1945
|
options.signal?.addEventListener('abort', close, { once: true });
|
|
1876
1946
|
if (options.signal?.aborted)
|
|
1877
1947
|
close();
|
|
1878
|
-
|
|
1948
|
+
observeStream();
|
|
1949
|
+
void openLocal().then(() => {
|
|
1950
|
+
if (capabilities.length > 0)
|
|
1951
|
+
notifyReady();
|
|
1952
|
+
}, () => {
|
|
1879
1953
|
notifyReady();
|
|
1880
1954
|
});
|
|
1881
|
-
|
|
1882
|
-
|
|
1955
|
+
if (options.adapter.roomStream === undefined)
|
|
1956
|
+
void sync('unstreamed');
|
|
1957
|
+
function planned(input, reach) {
|
|
1958
|
+
return planOf(input, roomFacts(reach), planReach(reach));
|
|
1959
|
+
}
|
|
1960
|
+
function watchPlan(plan, deliver) {
|
|
1961
|
+
materialize(reachedRooms(plan));
|
|
1962
|
+
return readSide.watch(plan, identityOf(plan), deliver);
|
|
1963
|
+
}
|
|
1964
|
+
async function searched(input, reach) {
|
|
1965
|
+
const plan = searchPlanOf(input);
|
|
1966
|
+
const selected = plan.documents === undefined && reach === null
|
|
1967
|
+
? null
|
|
1968
|
+
: (await rowsQuery(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
|
|
1969
|
+
if (selected === null)
|
|
1970
|
+
materialize(null);
|
|
1971
|
+
return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
|
|
1972
|
+
}
|
|
1973
|
+
const watch = (topic, input, deliver) => {
|
|
1883
1974
|
if (topic === 'room-drafts') {
|
|
1884
1975
|
const listener = { notify: deliver, ready: false };
|
|
1885
1976
|
roomDraftListeners.add(listener);
|
|
@@ -1900,6 +1991,8 @@ export function composeRuntime(options) {
|
|
|
1900
1991
|
if (topic === 'conversation') {
|
|
1901
1992
|
return conversations.watch(input[0], input[1], deliver);
|
|
1902
1993
|
}
|
|
1994
|
+
if (topic === 'conversations')
|
|
1995
|
+
return conversations.watchList(deliver);
|
|
1903
1996
|
if (topic === 'presentation') {
|
|
1904
1997
|
const listener = {
|
|
1905
1998
|
app: input[0],
|
|
@@ -1954,6 +2047,13 @@ export function composeRuntime(options) {
|
|
|
1954
2047
|
serviceModeListeners.delete(deliver);
|
|
1955
2048
|
};
|
|
1956
2049
|
}
|
|
2050
|
+
if (topic === 'member') {
|
|
2051
|
+
memberListeners.add(deliver);
|
|
2052
|
+
deliver(member);
|
|
2053
|
+
return () => {
|
|
2054
|
+
memberListeners.delete(deliver);
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
1957
2057
|
if (topic === 'queue') {
|
|
1958
2058
|
const queue = { locator: input[0], notify: deliver };
|
|
1959
2059
|
queueSubscriptions.add(queue);
|
|
@@ -1962,9 +2062,8 @@ export function composeRuntime(options) {
|
|
|
1962
2062
|
queueSubscriptions.delete(queue);
|
|
1963
2063
|
};
|
|
1964
2064
|
}
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
}
|
|
2065
|
+
return watchPlan(planned(input[0], null), deliver);
|
|
2066
|
+
};
|
|
1968
2067
|
const md = {
|
|
1969
2068
|
loaded() {
|
|
1970
2069
|
return openLocal().then(() => undefined);
|
|
@@ -1972,15 +2071,8 @@ export function composeRuntime(options) {
|
|
|
1972
2071
|
rooms() {
|
|
1973
2072
|
return listed();
|
|
1974
2073
|
},
|
|
1975
|
-
query: ((input) =>
|
|
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
|
-
},
|
|
2074
|
+
query: ((input) => rowsQuery(planned(input, null))),
|
|
2075
|
+
search: (input) => searched(input, null),
|
|
1984
2076
|
types(locator) {
|
|
1985
2077
|
return {
|
|
1986
2078
|
collection: modelTypes(modelOf(locator)),
|
|
@@ -2087,9 +2179,7 @@ export function composeRuntime(options) {
|
|
|
2087
2179
|
persistence() {
|
|
2088
2180
|
return options.shell.persistentStorage;
|
|
2089
2181
|
},
|
|
2090
|
-
observe: (
|
|
2091
|
-
deliver(structuredClone(message));
|
|
2092
|
-
}), topic === 'query')),
|
|
2182
|
+
observe: observing(watch),
|
|
2093
2183
|
async reauthorize() {
|
|
2094
2184
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2095
2185
|
return;
|
|
@@ -2098,8 +2188,19 @@ export function composeRuntime(options) {
|
|
|
2098
2188
|
await mutations;
|
|
2099
2189
|
authorization = undefined;
|
|
2100
2190
|
discoveryPaused = false;
|
|
2101
|
-
|
|
2191
|
+
if (options.adapter.roomStream === undefined)
|
|
2192
|
+
await sync('unstreamed');
|
|
2193
|
+
else
|
|
2194
|
+
await task(ensureAuthorization);
|
|
2102
2195
|
},
|
|
2103
2196
|
};
|
|
2104
|
-
|
|
2197
|
+
const reachable = {
|
|
2198
|
+
md,
|
|
2199
|
+
watch,
|
|
2200
|
+
plan: planned,
|
|
2201
|
+
query: (plan) => rowsQuery(plan),
|
|
2202
|
+
watchPlan,
|
|
2203
|
+
search: searched,
|
|
2204
|
+
};
|
|
2205
|
+
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2105
2206
|
}
|