@agentshouse/mdruntime 0.1.0-cc7bd7a53214e → 0.1.0-cd3486bd400ed
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 +106 -31
- package/README.md +11 -10
- package/dist/adapter.d.ts +65 -28
- 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/attempt.d.ts +16 -0
- package/dist/attempt.d.ts.map +1 -0
- package/dist/attempt.js +57 -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/change.d.ts +4 -0
- package/dist/change.d.ts.map +1 -1
- package/dist/chat/acts.d.ts +34 -0
- package/dist/chat/acts.d.ts.map +1 -0
- package/dist/chat/acts.js +36 -0
- package/dist/chat/messages.d.ts +21 -0
- package/dist/chat/messages.d.ts.map +1 -0
- package/dist/chat/messages.js +151 -0
- package/dist/chat/overlay.d.ts +31 -0
- package/dist/chat/overlay.d.ts.map +1 -0
- package/dist/chat/overlay.js +97 -0
- package/dist/chat/parts.d.ts +57 -0
- package/dist/chat/parts.d.ts.map +1 -0
- package/dist/chat/parts.js +119 -0
- package/dist/chat/process.d.ts +20 -0
- package/dist/chat/process.d.ts.map +1 -0
- package/dist/chat/process.js +32 -0
- package/dist/chat/provider.d.ts +57 -0
- package/dist/chat/provider.d.ts.map +1 -0
- package/dist/chat/provider.js +637 -0
- package/dist/chat/questions.d.ts +31 -0
- package/dist/chat/questions.d.ts.map +1 -0
- package/dist/chat/questions.js +111 -0
- package/dist/chat/read.d.ts +5 -0
- package/dist/chat/read.d.ts.map +1 -0
- package/dist/chat/read.js +13 -0
- package/dist/chat.d.ts +7 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +2 -0
- package/dist/client.d.ts +11 -7
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +58 -25
- 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 +18 -13
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +679 -568
- package/dist/conversation.d.ts +27 -8
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +210 -81
- package/dist/expectation.d.ts +21 -0
- package/dist/expectation.d.ts.map +1 -0
- package/dist/expectation.js +70 -0
- package/dist/failure.d.ts +13 -0
- package/dist/failure.d.ts.map +1 -0
- package/dist/failure.js +19 -0
- package/dist/host.d.ts +1 -0
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +8 -19
- package/dist/identity.d.ts +2 -0
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +10 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/memory.d.ts +12 -5
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +108 -36
- package/dist/model.d.ts +13 -12
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +6 -3
- package/dist/open.d.ts +2 -0
- package/dist/open.d.ts.map +1 -0
- package/dist/open.js +1 -0
- package/dist/outcome.d.ts +22 -3
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +35 -1
- package/dist/overlay.d.ts +1 -1
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +19 -15
- 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 +3 -1
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +74 -33
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +29 -35
- package/dist/react.d.ts +41 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +64 -0
- package/dist/read-side.d.ts.map +1 -1
- package/dist/read-side.js +102 -46
- 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 +53 -39
- package/dist/room-draft.d.ts +8 -8
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +13 -22
- 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 +4 -3
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +127 -68
- package/dist/search.d.ts +3 -2
- package/dist/search.d.ts.map +1 -1
- package/dist/status.d.ts +2 -2
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +0 -8
- package/dist/store.d.ts +26 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +206 -90
- package/dist/wire.d.ts +6 -15
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +14 -17
- package/package.json +14 -2
package/dist/connection.js
CHANGED
|
@@ -1,57 +1,43 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { attachedModel } from './attached-model.js';
|
|
2
2
|
import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
|
|
3
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
|
+
import { runtimeError } from './failure.js';
|
|
12
13
|
import { createLog } from './log.js';
|
|
14
|
+
import { calledResult, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
|
|
13
15
|
import { refreshRoom } from './refresh.js';
|
|
14
16
|
import { buildBatch } from './save.js';
|
|
15
|
-
import { applyReturned, compositionOf,
|
|
16
|
-
import { changedParts, changesOf, held, readParts, returnedInto, underneath, } from './room-draft.js';
|
|
17
|
-
import {
|
|
17
|
+
import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
|
|
18
|
+
import { changedParts, changesOf, held, readParts, returnedBy, returnedInto, staleWrite, underneath, } from './room-draft.js';
|
|
19
|
+
import { same, saving, STARTING, transferring, } from './status.js';
|
|
18
20
|
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
19
21
|
import { roomDraftRevision } from './identity.js';
|
|
20
22
|
import { beneath, normalize } from './paths.js';
|
|
21
23
|
import { openRuntimeStore, } from './store.js';
|
|
22
24
|
import { unconfirmed } from './transfer.js';
|
|
23
25
|
import { governs, holds, narrowed, planReach, } from './reach.js';
|
|
24
|
-
// Discovery repeats each minute so a Room the stream stopped serving stays unrefreshed no longer.
|
|
25
|
-
const DEFAULT_DISCOVERY_INTERVAL_MS = 60_000;
|
|
26
|
-
const EMPTY_SCHEMA = { verdict: 'accept', model: { types: {} } };
|
|
27
|
-
const knownCapabilities = new Set(VERSION_1_CAPABILITIES);
|
|
28
26
|
function within(root, path) {
|
|
29
27
|
return root === '' || beneath(root, path);
|
|
30
28
|
}
|
|
31
|
-
function declaredIdentity(model) {
|
|
32
|
-
if (model === undefined)
|
|
33
|
-
return false;
|
|
34
|
-
if (model === null)
|
|
35
|
-
return true;
|
|
36
|
-
return (typeof model.identity === 'string' &&
|
|
37
|
-
model.identity.length > 0 &&
|
|
38
|
-
typeof model.version === 'string' &&
|
|
39
|
-
model.version.length > 0);
|
|
40
|
-
}
|
|
41
29
|
function declaredSelection(room) {
|
|
42
30
|
const selection = room.typedSelection;
|
|
43
31
|
if (selection === undefined)
|
|
44
32
|
return true;
|
|
45
33
|
if (!Array.isArray(selection.roots) || selection.roots.length === 0)
|
|
46
34
|
return false;
|
|
47
|
-
if (!declaredIdentity(selection.attachedModel))
|
|
48
|
-
return false;
|
|
49
35
|
return selection.roots.every((root, index) => validRoot(root) &&
|
|
50
36
|
root !== '' &&
|
|
51
37
|
selection.roots.every((other, at) => at === index || !within(other, root)));
|
|
52
38
|
}
|
|
53
39
|
export function composeRuntime(options) {
|
|
54
|
-
const
|
|
40
|
+
const localSaves = options.adapter.localSaves === true;
|
|
55
41
|
let authorization = options.authorization;
|
|
56
42
|
let opening;
|
|
57
43
|
let store;
|
|
@@ -59,20 +45,14 @@ export function composeRuntime(options) {
|
|
|
59
45
|
const pendings = new Map();
|
|
60
46
|
const roomDrafts = new Map();
|
|
61
47
|
const presentations = new Map();
|
|
62
|
-
const wanted = new Map();
|
|
63
48
|
let limits = [];
|
|
64
49
|
let capabilities = [];
|
|
65
|
-
function rewriting() {
|
|
66
|
-
return capabilities.some((capability) => capability.name === 'move-reference-rewrites')
|
|
67
|
-
? 'provider'
|
|
68
|
-
: 'runtime';
|
|
69
|
-
}
|
|
70
50
|
const rooms = new Map();
|
|
51
|
+
const counted = new Map();
|
|
71
52
|
const unreadable = new Set();
|
|
72
53
|
const models = new Map();
|
|
73
54
|
const liveModels = new Map();
|
|
74
55
|
const invalidModels = new Set();
|
|
75
|
-
const modelEpoch = new Map();
|
|
76
56
|
const discovered = new Map();
|
|
77
57
|
let roomsListed = false;
|
|
78
58
|
const scopedRefusals = new Map();
|
|
@@ -101,16 +81,21 @@ export function composeRuntime(options) {
|
|
|
101
81
|
let status = STARTING;
|
|
102
82
|
let shown = STARTING;
|
|
103
83
|
let discoveryPaused = false;
|
|
104
|
-
let
|
|
84
|
+
let resetTimer;
|
|
105
85
|
let closed = false;
|
|
106
86
|
let stopping = false;
|
|
107
|
-
let
|
|
108
|
-
|
|
109
|
-
|
|
87
|
+
let withdrawStream;
|
|
88
|
+
const announced = new Map();
|
|
89
|
+
let underway = null;
|
|
90
|
+
let streamed = null;
|
|
91
|
+
let handed = false;
|
|
92
|
+
const submitting = new Map();
|
|
93
|
+
const answering = new Map();
|
|
94
|
+
let complete = false;
|
|
95
|
+
let recorded = false;
|
|
96
|
+
const stale = new Set();
|
|
110
97
|
let chain = Promise.resolve();
|
|
111
98
|
let mutations = Promise.resolve();
|
|
112
|
-
let inflight = false;
|
|
113
|
-
let queued = false;
|
|
114
99
|
const roomListeners = new Set();
|
|
115
100
|
const refusalListeners = new Set();
|
|
116
101
|
const storageLossListeners = new Set();
|
|
@@ -121,13 +106,19 @@ export function composeRuntime(options) {
|
|
|
121
106
|
const readSide = createReadSide();
|
|
122
107
|
let withdrawConversations;
|
|
123
108
|
let currencyQueued = false;
|
|
109
|
+
let socketOpen = false;
|
|
124
110
|
let serviceMode = 'open';
|
|
125
111
|
let haltedRefusal = false;
|
|
126
112
|
let openSignals = 0;
|
|
127
113
|
const serviceModeListeners = new Set();
|
|
114
|
+
let member = null;
|
|
115
|
+
const memberListeners = new Set();
|
|
116
|
+
const materialized = new Set();
|
|
117
|
+
let everyRoom = false;
|
|
118
|
+
const transfers = new Map();
|
|
128
119
|
const conversations = createConversationStore({
|
|
129
|
-
|
|
130
|
-
documents: (locator) =>
|
|
120
|
+
agents: declaredAgents,
|
|
121
|
+
documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
|
|
131
122
|
call: (name, input) => invokeAction(name, input),
|
|
132
123
|
durably,
|
|
133
124
|
stream: () => options.adapter.conversations,
|
|
@@ -203,7 +194,7 @@ export function composeRuntime(options) {
|
|
|
203
194
|
}
|
|
204
195
|
}
|
|
205
196
|
function pending() {
|
|
206
|
-
return [...replicas.values()].some((replica) => replica.queue.length > 0);
|
|
197
|
+
return !localSaves && [...replicas.values()].some((replica) => replica.queue.length > 0);
|
|
207
198
|
}
|
|
208
199
|
function attention() {
|
|
209
200
|
const attentionRequired = [];
|
|
@@ -232,8 +223,24 @@ export function composeRuntime(options) {
|
|
|
232
223
|
}
|
|
233
224
|
return { attentionRequired, reset };
|
|
234
225
|
}
|
|
226
|
+
function documentsIn(locator) {
|
|
227
|
+
const replica = replicas.get(locator);
|
|
228
|
+
if (replica === undefined)
|
|
229
|
+
return 0;
|
|
230
|
+
const roots = replica.confirmed.record.typedSelection?.roots ?? [];
|
|
231
|
+
let count = 0;
|
|
232
|
+
for (const path of Object.keys(replica.visible.documents)) {
|
|
233
|
+
if (markdownPath(path) && !roots.some((root) => beneath(root, path)))
|
|
234
|
+
count += 1;
|
|
235
|
+
}
|
|
236
|
+
return count;
|
|
237
|
+
}
|
|
235
238
|
function listed() {
|
|
236
|
-
return structuredClone([...rooms.values()])
|
|
239
|
+
return structuredClone([...rooms.values()].map((room) => {
|
|
240
|
+
const documents = documentsIn(room.locator);
|
|
241
|
+
counted.set(room.locator, documents);
|
|
242
|
+
return { ...room, documents };
|
|
243
|
+
}));
|
|
237
244
|
}
|
|
238
245
|
function roomFacts(reach = null) {
|
|
239
246
|
const held = [...rooms.values()]
|
|
@@ -265,6 +272,10 @@ export function composeRuntime(options) {
|
|
|
265
272
|
for (const listener of storageLossListeners)
|
|
266
273
|
listener(storageLoss);
|
|
267
274
|
}
|
|
275
|
+
function emitMember() {
|
|
276
|
+
for (const listener of memberListeners)
|
|
277
|
+
listener(member);
|
|
278
|
+
}
|
|
268
279
|
function moveTo(next) {
|
|
269
280
|
if (closed)
|
|
270
281
|
return;
|
|
@@ -284,6 +295,8 @@ export function composeRuntime(options) {
|
|
|
284
295
|
listener(shown);
|
|
285
296
|
}
|
|
286
297
|
function entryOf(batch) {
|
|
298
|
+
if (localSaves)
|
|
299
|
+
return Object.freeze({ batch, state: 'local' });
|
|
287
300
|
const state = pendings.get(batch.id);
|
|
288
301
|
if (state?.accepted !== undefined) {
|
|
289
302
|
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
@@ -304,12 +317,47 @@ export function composeRuntime(options) {
|
|
|
304
317
|
}
|
|
305
318
|
}
|
|
306
319
|
function applyChangeSet(set) {
|
|
320
|
+
const held = everyRoom
|
|
321
|
+
? set
|
|
322
|
+
: { ...set, documents: set.documents.filter(({ locator }) => materialized.has(locator)) };
|
|
323
|
+
readSide.apply(held);
|
|
324
|
+
searchIndex.changed(held);
|
|
325
|
+
}
|
|
326
|
+
function materialize(locators) {
|
|
327
|
+
if (everyRoom)
|
|
328
|
+
return;
|
|
329
|
+
if (locators === null)
|
|
330
|
+
everyRoom = true;
|
|
331
|
+
const sets = [];
|
|
332
|
+
for (const locator of locators ?? [...replicas.keys()]) {
|
|
333
|
+
if (materialized.has(locator))
|
|
334
|
+
continue;
|
|
335
|
+
materialized.add(locator);
|
|
336
|
+
const replica = replicas.get(locator);
|
|
337
|
+
if (replica !== undefined)
|
|
338
|
+
sets.push(changeSet(locator, null, replica));
|
|
339
|
+
}
|
|
340
|
+
if (sets.length === 0)
|
|
341
|
+
return;
|
|
342
|
+
const set = mergeChangeSets(sets);
|
|
307
343
|
readSide.apply(set);
|
|
308
344
|
searchIndex.changed(set);
|
|
309
345
|
}
|
|
346
|
+
function reachedRooms(plan) {
|
|
347
|
+
if (plan.rooms !== null)
|
|
348
|
+
return plan.rooms;
|
|
349
|
+
if (plan.reach === null)
|
|
350
|
+
return null;
|
|
351
|
+
return [...plan.reach.rooms, ...plan.reach.documents.map(roomOfKey)];
|
|
352
|
+
}
|
|
353
|
+
function rowsQuery(plan) {
|
|
354
|
+
materialize(reachedRooms(plan));
|
|
355
|
+
return readSide.query(plan);
|
|
356
|
+
}
|
|
310
357
|
function publishChangeSet(set) {
|
|
311
358
|
applyChangeSet(set);
|
|
312
|
-
if (set.rooms.length > 0
|
|
359
|
+
if (set.rooms.length > 0 ||
|
|
360
|
+
[...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
|
|
313
361
|
emitRooms();
|
|
314
362
|
for (const locator of set.queues)
|
|
315
363
|
emitQueue(locator);
|
|
@@ -320,23 +368,32 @@ export function composeRuntime(options) {
|
|
|
320
368
|
for (const app of set.presentations)
|
|
321
369
|
emitPresentation(app);
|
|
322
370
|
conversations.changed(set);
|
|
323
|
-
const cards = declaredCards();
|
|
324
|
-
if (cards !== undefined && set.documents.some((change) => change.locator === cards.locator)) {
|
|
325
|
-
scheduleCurrency();
|
|
326
|
-
}
|
|
327
371
|
}
|
|
328
372
|
function stopTimer() {
|
|
329
|
-
if (
|
|
373
|
+
if (resetTimer === undefined)
|
|
330
374
|
return;
|
|
331
|
-
|
|
332
|
-
|
|
375
|
+
clearTimeout(resetTimer);
|
|
376
|
+
resetTimer = undefined;
|
|
333
377
|
}
|
|
334
|
-
function
|
|
335
|
-
|
|
378
|
+
function armTimer() {
|
|
379
|
+
stopTimer();
|
|
380
|
+
if (closed || stopping)
|
|
336
381
|
return;
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
382
|
+
let earliest = null;
|
|
383
|
+
for (const { submission } of pendings.values()) {
|
|
384
|
+
if (submission.state !== 'reset')
|
|
385
|
+
continue;
|
|
386
|
+
const at = Date.parse(submission.reset);
|
|
387
|
+
if (earliest === null || at < earliest)
|
|
388
|
+
earliest = at;
|
|
389
|
+
}
|
|
390
|
+
if (earliest === null)
|
|
391
|
+
return;
|
|
392
|
+
// A Save House held back goes again only once the reset its answer named has passed.
|
|
393
|
+
resetTimer = setTimeout(() => {
|
|
394
|
+
resetTimer = undefined;
|
|
395
|
+
submitAll();
|
|
396
|
+
}, Math.max(0, earliest - Date.now()));
|
|
340
397
|
}
|
|
341
398
|
async function refuse(reason, provider = null) {
|
|
342
399
|
stopTimer();
|
|
@@ -362,7 +419,6 @@ export function composeRuntime(options) {
|
|
|
362
419
|
}
|
|
363
420
|
for (const key of keys) {
|
|
364
421
|
invalidModels.add(key);
|
|
365
|
-
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
366
422
|
models.set(key, revoked);
|
|
367
423
|
liveModels.delete(key);
|
|
368
424
|
}
|
|
@@ -375,10 +431,7 @@ export function composeRuntime(options) {
|
|
|
375
431
|
model: revoked,
|
|
376
432
|
selectionModel: revoked,
|
|
377
433
|
};
|
|
378
|
-
|
|
379
|
-
replicas.set(locator, read.kind === 'staged'
|
|
380
|
-
? replicaOf(read.confirmed, before.queue, rewriting(), before)
|
|
381
|
-
: { ...before, confirmed: { ...before.confirmed, record } });
|
|
434
|
+
replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
|
|
382
435
|
}
|
|
383
436
|
rooms.set(locator, {
|
|
384
437
|
...room,
|
|
@@ -395,7 +448,8 @@ export function composeRuntime(options) {
|
|
|
395
448
|
}
|
|
396
449
|
finally {
|
|
397
450
|
refusal = {
|
|
398
|
-
reason,
|
|
451
|
+
code: reason,
|
|
452
|
+
retryable: false,
|
|
399
453
|
reauthorizationOffered: reason === 'revoked-authorization',
|
|
400
454
|
refusal: provider,
|
|
401
455
|
};
|
|
@@ -408,15 +462,14 @@ export function composeRuntime(options) {
|
|
|
408
462
|
rooms.set(room.locator, {
|
|
409
463
|
name: room.name,
|
|
410
464
|
locator: room.locator,
|
|
465
|
+
handle: room.handle,
|
|
411
466
|
operations: room.operations,
|
|
412
467
|
replica: room.replica,
|
|
413
|
-
attachedModel: room.attachedModel,
|
|
414
468
|
model: modelTypes(modelOf(room.locator)),
|
|
415
469
|
typedSelection: selection === null
|
|
416
470
|
? null
|
|
417
471
|
: {
|
|
418
472
|
roots: selection.roots,
|
|
419
|
-
attachedModel: selection.attachedModel,
|
|
420
473
|
model: modelTypes(modelOf(room.locator, 'typed-selection')),
|
|
421
474
|
},
|
|
422
475
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
@@ -433,9 +486,6 @@ export function composeRuntime(options) {
|
|
|
433
486
|
discovered.clear();
|
|
434
487
|
for (const room of listed)
|
|
435
488
|
discovered.set(room.locator, room);
|
|
436
|
-
for (const locator of wanted.keys())
|
|
437
|
-
if (!listedLocators.has(locator))
|
|
438
|
-
wanted.delete(locator);
|
|
439
489
|
await dropUnlisted(listedLocators, local);
|
|
440
490
|
for (const room of listed) {
|
|
441
491
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
@@ -449,7 +499,7 @@ export function composeRuntime(options) {
|
|
|
449
499
|
emitRooms();
|
|
450
500
|
}
|
|
451
501
|
function unlistedHeld(listed) {
|
|
452
|
-
return [...new Set([...rooms.keys(), ...roomDrafts.keys()])].filter((locator) => !listed.has(locator));
|
|
502
|
+
return [...new Set([...rooms.keys(), ...roomDrafts.keys(), ...transfers.keys()])].filter((locator) => !listed.has(locator));
|
|
453
503
|
}
|
|
454
504
|
async function dropUnlisted(listed, local) {
|
|
455
505
|
if (unlistedHeld(listed).length === 0)
|
|
@@ -468,7 +518,9 @@ export function composeRuntime(options) {
|
|
|
468
518
|
replicas.delete(locator);
|
|
469
519
|
rooms.delete(locator);
|
|
470
520
|
roomDrafts.delete(locator);
|
|
521
|
+
transfers.delete(locator);
|
|
471
522
|
unreadable.delete(locator);
|
|
523
|
+
stale.delete(locator);
|
|
472
524
|
scopedRefusals.delete(locator);
|
|
473
525
|
sourceRefusals.delete(locator);
|
|
474
526
|
declarationRefusals.delete(locator);
|
|
@@ -477,7 +529,6 @@ export function composeRuntime(options) {
|
|
|
477
529
|
models.delete(key);
|
|
478
530
|
liveModels.delete(key);
|
|
479
531
|
invalidModels.delete(key);
|
|
480
|
-
modelEpoch.delete(key);
|
|
481
532
|
}
|
|
482
533
|
journal.record(`room unlisted ${locator}`);
|
|
483
534
|
}
|
|
@@ -490,7 +541,6 @@ export function composeRuntime(options) {
|
|
|
490
541
|
if (prior !== undefined &&
|
|
491
542
|
prior.status !== 'unavailable' &&
|
|
492
543
|
(persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
|
|
493
|
-
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
494
544
|
invalidModels.add(key);
|
|
495
545
|
}
|
|
496
546
|
const model = persisted.status === 'unavailable' ||
|
|
@@ -505,7 +555,7 @@ export function composeRuntime(options) {
|
|
|
505
555
|
return model;
|
|
506
556
|
}
|
|
507
557
|
async function hydrate(opened) {
|
|
508
|
-
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
|
|
558
|
+
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
|
|
509
559
|
opened.all(),
|
|
510
560
|
opened.queues(),
|
|
511
561
|
opened.roomDrafts(),
|
|
@@ -515,9 +565,15 @@ export function composeRuntime(options) {
|
|
|
515
565
|
opened.lastCompleted(),
|
|
516
566
|
opened.providerLimits(),
|
|
517
567
|
opened.capabilities(),
|
|
568
|
+
opened.member(),
|
|
569
|
+
opened.transfers(),
|
|
518
570
|
]);
|
|
571
|
+
transfers.clear();
|
|
572
|
+
for (const record of staged)
|
|
573
|
+
transfers.set(record.locator, record);
|
|
519
574
|
limits = declared;
|
|
520
575
|
capabilities = named;
|
|
576
|
+
member = known;
|
|
521
577
|
roomDrafts.clear();
|
|
522
578
|
for (const record of kept)
|
|
523
579
|
rememberRoomDraft(record);
|
|
@@ -537,15 +593,8 @@ export function composeRuntime(options) {
|
|
|
537
593
|
const model = restored(modelKey(record.locator, 'collection'), record.model);
|
|
538
594
|
const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
|
|
539
595
|
const prior = previousReplicas.get(record.locator);
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
const confirmed = read.kind === 'staged'
|
|
543
|
-
? read.confirmed
|
|
544
|
-
: {
|
|
545
|
-
record: retyped,
|
|
546
|
-
collection: parseCollection(retyped.documents, compositionOf(retyped)),
|
|
547
|
-
};
|
|
548
|
-
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], rewriting(), prior));
|
|
596
|
+
const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
|
|
597
|
+
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
|
|
549
598
|
const existing = rooms.get(record.locator);
|
|
550
599
|
if (existing === undefined)
|
|
551
600
|
list(record, false);
|
|
@@ -556,21 +605,15 @@ export function composeRuntime(options) {
|
|
|
556
605
|
attachments: record.attachments,
|
|
557
606
|
operations: record.operations,
|
|
558
607
|
replica: record.replica,
|
|
559
|
-
attachedModel: record.attachedModel,
|
|
560
608
|
model: modelTypes(model),
|
|
561
609
|
typedSelection: record.typedSelection === null
|
|
562
610
|
? null
|
|
563
611
|
: {
|
|
564
612
|
roots: record.typedSelection.roots,
|
|
565
|
-
attachedModel: record.typedSelection.attachedModel,
|
|
566
613
|
model: modelTypes(selectionModel),
|
|
567
614
|
},
|
|
568
615
|
});
|
|
569
616
|
}
|
|
570
|
-
if (read.kind === 'refused') {
|
|
571
|
-
refusedSource(record, read);
|
|
572
|
-
declarationRefusals.add(record.locator);
|
|
573
|
-
}
|
|
574
617
|
}
|
|
575
618
|
return {
|
|
576
619
|
lastCompleted,
|
|
@@ -583,6 +626,8 @@ export function composeRuntime(options) {
|
|
|
583
626
|
applyChangeSet(read);
|
|
584
627
|
if (replicas.size > 0)
|
|
585
628
|
emitRooms();
|
|
629
|
+
if (member !== null)
|
|
630
|
+
emitMember();
|
|
586
631
|
for (const locator of locators)
|
|
587
632
|
emitQueue(locator);
|
|
588
633
|
publishRoomDrafts(false);
|
|
@@ -630,10 +675,14 @@ export function composeRuntime(options) {
|
|
|
630
675
|
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
631
676
|
}
|
|
632
677
|
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
633
|
-
const withdrawn = withdrawals(confirmed, queue, withdrawable,
|
|
678
|
+
const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
|
|
634
679
|
const kept = queue.filter((batch) => !withdrawn.includes(batch));
|
|
680
|
+
const unqueued = withdrawn.map((batch) => ({
|
|
681
|
+
batch,
|
|
682
|
+
returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
|
|
683
|
+
}));
|
|
635
684
|
const returning = [
|
|
636
|
-
...
|
|
685
|
+
...[...unqueued].reverse(),
|
|
637
686
|
...(settling.leaving === undefined ? [] : [settling.leaving]),
|
|
638
687
|
];
|
|
639
688
|
const written = new Map();
|
|
@@ -654,6 +703,7 @@ export function composeRuntime(options) {
|
|
|
654
703
|
await local.write({
|
|
655
704
|
next: confirmed.record,
|
|
656
705
|
previous: before?.confirmed.record ?? null,
|
|
706
|
+
...(settling.stored === undefined ? {} : { stored: settling.stored }),
|
|
657
707
|
queue: kept,
|
|
658
708
|
previousQueue: before?.queue ?? [],
|
|
659
709
|
roomDrafts: [...written.values()],
|
|
@@ -664,44 +714,80 @@ export function composeRuntime(options) {
|
|
|
664
714
|
}
|
|
665
715
|
for (const record of written.values())
|
|
666
716
|
rememberRoomDraft(record);
|
|
667
|
-
for (const batch of
|
|
668
|
-
|
|
669
|
-
|
|
717
|
+
for (const { batch, returned } of unqueued) {
|
|
718
|
+
const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
|
|
719
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
720
|
+
}
|
|
721
|
+
replicas.set(locator, replicaOf(confirmed, kept, before));
|
|
670
722
|
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
671
723
|
room: settling.room,
|
|
672
724
|
queue: kept.length !== (before?.queue.length ?? 0),
|
|
673
725
|
drafts: [...written.keys()],
|
|
674
726
|
});
|
|
675
727
|
}
|
|
676
|
-
function
|
|
728
|
+
function adoptModels(locator, record) {
|
|
729
|
+
for (const [attached, model] of [
|
|
730
|
+
['collection', record.model],
|
|
731
|
+
['typed-selection', record.selectionModel],
|
|
732
|
+
]) {
|
|
733
|
+
const key = modelKey(locator, attached);
|
|
734
|
+
models.set(key, model);
|
|
735
|
+
invalidModels.delete(key);
|
|
736
|
+
if (model.status === 'current')
|
|
737
|
+
liveModels.set(key, JSON.stringify(model.snapshot));
|
|
738
|
+
else if (model.status === 'unavailable')
|
|
739
|
+
liveModels.delete(key);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
function remodel(locator, answers, held) {
|
|
743
|
+
const taken = (answer, previous, attached) => {
|
|
744
|
+
if (answer === undefined)
|
|
745
|
+
return previous;
|
|
746
|
+
const { state, refused } = attachedModel(answer, previous);
|
|
747
|
+
if (refused !== null) {
|
|
748
|
+
const where = refused.location === undefined ? '' : ` ${refused.location}`;
|
|
749
|
+
journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
|
|
750
|
+
}
|
|
751
|
+
models.set(modelKey(locator, attached), state);
|
|
752
|
+
return state;
|
|
753
|
+
};
|
|
754
|
+
return {
|
|
755
|
+
collection: taken(answers?.collection, held.collection, 'collection'),
|
|
756
|
+
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
function publish(local, room, confirmed, landing = {}) {
|
|
760
|
+
const { base, stored } = landing;
|
|
677
761
|
return exclusive(async () => {
|
|
678
762
|
if (!continuing())
|
|
679
|
-
return;
|
|
763
|
+
return true;
|
|
680
764
|
const prior = replicas.get(room.locator);
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
throw new Error(`Cannot retype staged Room ${room.locator}`);
|
|
765
|
+
if (base !== undefined &&
|
|
766
|
+
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
767
|
+
prior.confirmed.record.revisions !== base.record.revisions)) {
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
adoptModels(room.locator, confirmed.record);
|
|
688
771
|
const queue = prior?.queue ?? [];
|
|
689
|
-
const roomVersion =
|
|
772
|
+
const roomVersion = confirmed.record.roomVersion;
|
|
690
773
|
const accepted = queue.filter((batch) => {
|
|
691
774
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
692
775
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
693
776
|
});
|
|
694
|
-
const set = await settle(local, room.locator, prior,
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
777
|
+
const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
|
|
778
|
+
if (stored !== undefined)
|
|
779
|
+
transfers.delete(room.locator);
|
|
780
|
+
for (const batch of accepted)
|
|
781
|
+
pendings.delete(batch.id);
|
|
782
|
+
stale.delete(room.locator);
|
|
783
|
+
if (unreadable.delete(room.locator))
|
|
784
|
+
scopedRefusals.delete(room.locator);
|
|
700
785
|
sourceRefusals.delete(room.locator);
|
|
701
786
|
declarationRefusals.delete(room.locator);
|
|
702
787
|
list(room, true);
|
|
703
788
|
publishChangeSet(set);
|
|
704
789
|
moveTo({ ...status, ...attention() });
|
|
790
|
+
return true;
|
|
705
791
|
});
|
|
706
792
|
}
|
|
707
793
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
@@ -709,28 +795,29 @@ export function composeRuntime(options) {
|
|
|
709
795
|
const replica = replicas.get(locator);
|
|
710
796
|
const room = rooms.get(locator);
|
|
711
797
|
if (replica === undefined || room === undefined) {
|
|
712
|
-
throw
|
|
798
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
713
799
|
}
|
|
714
800
|
const saved = [];
|
|
715
801
|
for (const { path, revision } of named) {
|
|
716
802
|
const record = roomDrafts.get(locator)?.get(path);
|
|
717
803
|
if (record === undefined || record.revision !== revision) {
|
|
718
|
-
return { outcome: 'refused',
|
|
804
|
+
return { outcome: 'refused', code: 'stale', retryable: false, file: path };
|
|
719
805
|
}
|
|
720
806
|
if (!saved.some((draft) => draft.path === path))
|
|
721
807
|
saved.push({ path, parts: record.parts });
|
|
722
808
|
}
|
|
723
809
|
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
|
|
724
810
|
if (changed.length > 0)
|
|
725
|
-
return { outcome: 'refused',
|
|
811
|
+
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
726
812
|
const parts = saved.map(({ path, parts }) => changesOf(path, parts));
|
|
727
813
|
const built = buildBatch({
|
|
814
|
+
local: localSaves,
|
|
728
815
|
baseRoomVersion: replica.confirmed.record.roomVersion,
|
|
729
816
|
composition: compositionFor(replica.confirmed.record),
|
|
730
817
|
operations: room.operations,
|
|
731
818
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
732
819
|
providerLimits: limits,
|
|
733
|
-
|
|
820
|
+
handle: room.handle,
|
|
734
821
|
visible: replica.visible,
|
|
735
822
|
changes: parts.flatMap(({ changes }) => changes),
|
|
736
823
|
bases: parts.flatMap(({ bases }) => bases),
|
|
@@ -751,18 +838,20 @@ export function composeRuntime(options) {
|
|
|
751
838
|
});
|
|
752
839
|
}
|
|
753
840
|
catch {
|
|
754
|
-
return { outcome: 'refused',
|
|
841
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
755
842
|
}
|
|
756
843
|
pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
|
|
844
|
+
if (!localSaves)
|
|
845
|
+
recorded = false;
|
|
757
846
|
for (const path of consumed)
|
|
758
847
|
forgetRoomDraft(locator, path);
|
|
759
|
-
replicas.set(locator, replicaOf(replica.confirmed, batches,
|
|
848
|
+
replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
|
|
760
849
|
publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
|
|
761
850
|
queue: true,
|
|
762
851
|
drafts: consumed,
|
|
763
852
|
}));
|
|
764
|
-
moveTo({ ...saving(status), ...attention() });
|
|
765
|
-
|
|
853
|
+
moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
|
|
854
|
+
submitLater(locator);
|
|
766
855
|
return { outcome: 'saved', identifier: built.batch.id };
|
|
767
856
|
}
|
|
768
857
|
async function mark(local, locator, identifier, submission, lost) {
|
|
@@ -789,16 +878,15 @@ export function composeRuntime(options) {
|
|
|
789
878
|
}
|
|
790
879
|
async function readHistory(locator, path, query = {}) {
|
|
791
880
|
if (closed || refusal !== null)
|
|
792
|
-
return
|
|
881
|
+
return UNCERTAIN;
|
|
793
882
|
try {
|
|
794
883
|
await ensureAuthorization();
|
|
795
884
|
}
|
|
796
885
|
catch {
|
|
797
|
-
return
|
|
798
|
-
}
|
|
799
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
800
|
-
return { kind: 'transport-uncertainty' };
|
|
886
|
+
return UNCERTAIN;
|
|
801
887
|
}
|
|
888
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
889
|
+
return UNCERTAIN;
|
|
802
890
|
const transfer = ['transfer', 'history', locator, path];
|
|
803
891
|
if (query.revision !== undefined)
|
|
804
892
|
transfer.push(query.revision);
|
|
@@ -820,17 +908,17 @@ export function composeRuntime(options) {
|
|
|
820
908
|
}
|
|
821
909
|
if (result.kind === 'revoked-authorization')
|
|
822
910
|
await refuse('revoked-authorization');
|
|
823
|
-
return result;
|
|
911
|
+
return historyAnswer(result);
|
|
824
912
|
}
|
|
825
913
|
catch {
|
|
826
914
|
journal.record('outcome history transport-uncertainty');
|
|
827
|
-
return
|
|
915
|
+
return UNCERTAIN;
|
|
828
916
|
}
|
|
829
917
|
}
|
|
830
|
-
function
|
|
918
|
+
function declaredAgents() {
|
|
831
919
|
for (const capability of capabilities) {
|
|
832
|
-
if (capability.
|
|
833
|
-
return capability.
|
|
920
|
+
if (capability.conversationAgents !== undefined)
|
|
921
|
+
return capability.conversationAgents;
|
|
834
922
|
}
|
|
835
923
|
return undefined;
|
|
836
924
|
}
|
|
@@ -846,23 +934,20 @@ export function composeRuntime(options) {
|
|
|
846
934
|
async function invokeAction(name, input) {
|
|
847
935
|
await ready;
|
|
848
936
|
if (closed || refusal !== null)
|
|
849
|
-
return
|
|
937
|
+
return UNCERTAIN;
|
|
850
938
|
if (declaredAction(name) === undefined)
|
|
851
|
-
return
|
|
939
|
+
return undeclared(name);
|
|
852
940
|
try {
|
|
853
941
|
await ensureAuthorization();
|
|
854
942
|
}
|
|
855
943
|
catch {
|
|
856
|
-
return
|
|
857
|
-
}
|
|
858
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
859
|
-
return { kind: 'transport-uncertainty' };
|
|
944
|
+
return UNCERTAIN;
|
|
860
945
|
}
|
|
946
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
947
|
+
return UNCERTAIN;
|
|
861
948
|
journal.record(`transfer action ${name}`);
|
|
862
949
|
try {
|
|
863
950
|
const result = await options.adapter.action(authorization, { name, input });
|
|
864
|
-
if (result.kind === 'action' && result.at !== undefined)
|
|
865
|
-
want(result.at);
|
|
866
951
|
if (result.kind === 'scoped-refusal') {
|
|
867
952
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
868
953
|
if (room !== undefined)
|
|
@@ -873,11 +958,11 @@ export function composeRuntime(options) {
|
|
|
873
958
|
journal.record(`outcome action ${result.kind}`);
|
|
874
959
|
if (result.kind === 'rejected')
|
|
875
960
|
journal.record(`refusal rejected ${result.code}`);
|
|
876
|
-
return result;
|
|
961
|
+
return calledResult(result);
|
|
877
962
|
}
|
|
878
963
|
catch {
|
|
879
964
|
journal.record('outcome action transport-uncertainty');
|
|
880
|
-
return
|
|
965
|
+
return UNCERTAIN;
|
|
881
966
|
}
|
|
882
967
|
}
|
|
883
968
|
function continuing() {
|
|
@@ -894,32 +979,11 @@ export function composeRuntime(options) {
|
|
|
894
979
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
895
980
|
!options.adapter.reached(locator, roomVersion, written));
|
|
896
981
|
}
|
|
897
|
-
function
|
|
898
|
-
if (reachedBy(locator, roomVersion))
|
|
899
|
-
return;
|
|
900
|
-
const held = wanted.get(locator);
|
|
901
|
-
if (held === undefined || options.adapter.reached(locator, roomVersion, held)) {
|
|
902
|
-
wanted.set(locator, roomVersion);
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
function reachOf(locator) {
|
|
906
|
-
let target = wanted.get(locator);
|
|
907
|
-
const versions = (replicas.get(locator)?.queue ?? []).flatMap((batch) => {
|
|
908
|
-
const accepted = pendings.get(batch.id)?.accepted;
|
|
909
|
-
return accepted === undefined ? [] : [accepted];
|
|
910
|
-
});
|
|
911
|
-
for (const version of versions) {
|
|
912
|
-
if (target === undefined || options.adapter.reached(locator, version, target)) {
|
|
913
|
-
target = version;
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
return target === undefined || reachedBy(locator, target) ? undefined : target;
|
|
917
|
-
}
|
|
918
|
-
async function pullRefresh(authorized, confirmed, minPosition) {
|
|
982
|
+
async function pullRefresh(authorized, confirmed) {
|
|
919
983
|
if (!continuing())
|
|
920
984
|
return unconfirmed;
|
|
921
985
|
try {
|
|
922
|
-
const result = await refreshRoom(options.adapter, authorized, confirmed,
|
|
986
|
+
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
|
|
923
987
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
924
988
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
925
989
|
return result;
|
|
@@ -929,14 +993,33 @@ export function composeRuntime(options) {
|
|
|
929
993
|
throw error;
|
|
930
994
|
}
|
|
931
995
|
}
|
|
932
|
-
|
|
996
|
+
function staging(local) {
|
|
997
|
+
return {
|
|
998
|
+
held: (locator) => transfers.get(locator) ?? null,
|
|
999
|
+
staged: (record) => local.staged(record),
|
|
1000
|
+
stage: (record, page) => exclusive(async () => {
|
|
1001
|
+
if (!continuing())
|
|
1002
|
+
return false;
|
|
1003
|
+
await local.stage(record, page);
|
|
1004
|
+
transfers.set(record.locator, record);
|
|
1005
|
+
return true;
|
|
1006
|
+
}),
|
|
1007
|
+
abandon: (locator) => exclusive(async () => {
|
|
1008
|
+
if (!continuing())
|
|
1009
|
+
return;
|
|
1010
|
+
await local.abandon(locator);
|
|
1011
|
+
transfers.delete(locator);
|
|
1012
|
+
}),
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
async function pullBootstrap(authorized, room, local) {
|
|
933
1016
|
if (!continuing())
|
|
934
1017
|
return unconfirmed;
|
|
935
1018
|
try {
|
|
936
1019
|
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
937
1020
|
collection: modelOf(room.locator),
|
|
938
1021
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
939
|
-
}, continuing);
|
|
1022
|
+
}, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
|
|
940
1023
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
941
1024
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
942
1025
|
return result;
|
|
@@ -949,35 +1032,46 @@ export function composeRuntime(options) {
|
|
|
949
1032
|
async function advance(room, local, authorized) {
|
|
950
1033
|
const held = replicas.get(room.locator);
|
|
951
1034
|
if (held !== undefined) {
|
|
952
|
-
const refreshed = await pullRefresh(authorized, held.confirmed
|
|
1035
|
+
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
953
1036
|
if (!continuing())
|
|
954
1037
|
return unconfirmed;
|
|
955
1038
|
if (refreshed.kind === 'still-current') {
|
|
1039
|
+
stale.delete(room.locator);
|
|
956
1040
|
if (sourceRefusals.has(room.locator))
|
|
957
|
-
return
|
|
1041
|
+
return null;
|
|
958
1042
|
if (unreadable.delete(room.locator)) {
|
|
1043
|
+
scopedRefusals.delete(room.locator);
|
|
959
1044
|
list(room, true);
|
|
960
1045
|
emitRooms();
|
|
961
1046
|
}
|
|
962
1047
|
return null;
|
|
963
1048
|
}
|
|
964
1049
|
if (refreshed.kind === 'advanced') {
|
|
965
|
-
await publish(local, room, refreshed.confirmed)
|
|
1050
|
+
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
|
|
1051
|
+
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1052
|
+
const reached = now !== undefined &&
|
|
1053
|
+
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
1054
|
+
return reached ? null : advance(room, local, authorized);
|
|
1055
|
+
}
|
|
1056
|
+
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
966
1057
|
return null;
|
|
967
1058
|
}
|
|
968
1059
|
if (refreshed.kind !== 'beyond-horizon')
|
|
969
1060
|
return refreshed;
|
|
970
1061
|
}
|
|
971
|
-
const bootstrapped = await pullBootstrap(authorized, room);
|
|
1062
|
+
const bootstrapped = await pullBootstrap(authorized, room, local);
|
|
972
1063
|
if (!continuing())
|
|
973
1064
|
return unconfirmed;
|
|
974
1065
|
if (bootstrapped.kind !== 'staged')
|
|
975
1066
|
return bootstrapped;
|
|
976
|
-
await publish(local, room, bootstrapped.confirmed);
|
|
1067
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
977
1068
|
return null;
|
|
978
1069
|
}
|
|
979
1070
|
async function withhold(withheld, room, local, progress) {
|
|
980
|
-
|
|
1071
|
+
if (withheld.kind === 'refused')
|
|
1072
|
+
stale.delete(room.locator);
|
|
1073
|
+
else
|
|
1074
|
+
stale.add(room.locator);
|
|
981
1075
|
if (withheld.kind === 'scoped-refusal') {
|
|
982
1076
|
await scoped(withheld, room, local);
|
|
983
1077
|
}
|
|
@@ -1002,6 +1096,7 @@ export function composeRuntime(options) {
|
|
|
1002
1096
|
function refusedSource(room, { refusal }) {
|
|
1003
1097
|
sourceRefusals.set(room.locator, {
|
|
1004
1098
|
code: refusal.rule,
|
|
1099
|
+
retryable: false,
|
|
1005
1100
|
fields: {
|
|
1006
1101
|
file: refusal.file,
|
|
1007
1102
|
...(refusal.location === undefined ? {} : { location: refusal.location }),
|
|
@@ -1016,52 +1111,46 @@ export function composeRuntime(options) {
|
|
|
1016
1111
|
}
|
|
1017
1112
|
function leaving(batch, result) {
|
|
1018
1113
|
if (result.kind === 'expired')
|
|
1019
|
-
return { batch, returned:
|
|
1020
|
-
return {
|
|
1114
|
+
return { batch, returned: returnedBy('expired') };
|
|
1115
|
+
return {
|
|
1116
|
+
batch,
|
|
1117
|
+
returned: {
|
|
1118
|
+
kind: result.kind,
|
|
1119
|
+
code: result.code,
|
|
1120
|
+
fields: result.fields,
|
|
1121
|
+
retryable: result.retryable,
|
|
1122
|
+
},
|
|
1123
|
+
};
|
|
1021
1124
|
}
|
|
1022
1125
|
function logAccepted(locator, submitted, accepted) {
|
|
1023
1126
|
journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
|
|
1024
1127
|
(accepted.author === undefined ? '' : ` author ${accepted.author}`) +
|
|
1025
1128
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1026
1129
|
}
|
|
1027
|
-
async function
|
|
1028
|
-
await exclusive(async () => {
|
|
1029
|
-
if (!continuing())
|
|
1030
|
-
return;
|
|
1031
|
-
const current = replicas.get(room.locator);
|
|
1032
|
-
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
1033
|
-
return;
|
|
1034
|
-
}
|
|
1035
|
-
const state = pendings.get(submitted.id);
|
|
1036
|
-
if (state === undefined)
|
|
1037
|
-
return;
|
|
1038
|
-
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1039
|
-
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1040
|
-
logAccepted(room.locator, submitted, accepted);
|
|
1041
|
-
emitQueue(room.locator);
|
|
1042
|
-
});
|
|
1043
|
-
}
|
|
1044
|
-
async function commitAccept(room, local, submitted, accepted, base, overlay) {
|
|
1130
|
+
async function adoptAccepted(room, local, submitted, accepted) {
|
|
1045
1131
|
if (!continuing())
|
|
1046
1132
|
return unconfirmed;
|
|
1047
1133
|
const current = replicas.get(room.locator);
|
|
1048
1134
|
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
1049
1135
|
return null;
|
|
1050
1136
|
}
|
|
1051
|
-
const
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1137
|
+
const held = current.confirmed.record.roomVersion;
|
|
1138
|
+
logAccepted(room.locator, submitted, accepted);
|
|
1139
|
+
const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
1140
|
+
? { kind: 'staged', confirmed: current.confirmed }
|
|
1141
|
+
: applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.changes, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
1142
|
+
? accepted.roomVersion
|
|
1143
|
+
: held, accepted.roomVersion);
|
|
1144
|
+
if (applied.kind !== 'staged') {
|
|
1145
|
+
const state = pendings.get(submitted.id);
|
|
1146
|
+
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1147
|
+
if (state !== undefined)
|
|
1148
|
+
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1149
|
+
emitQueue(room.locator);
|
|
1061
1150
|
return applied;
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1151
|
+
}
|
|
1152
|
+
const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
1153
|
+
pendings.delete(submitted.id);
|
|
1065
1154
|
unreadable.delete(room.locator);
|
|
1066
1155
|
sourceRefusals.delete(room.locator);
|
|
1067
1156
|
declarationRefusals.delete(room.locator);
|
|
@@ -1070,17 +1159,8 @@ export function composeRuntime(options) {
|
|
|
1070
1159
|
moveTo({ ...status, ...attention() });
|
|
1071
1160
|
return null;
|
|
1072
1161
|
}
|
|
1073
|
-
async function
|
|
1074
|
-
const
|
|
1075
|
-
if (options.adapter.reached(room.locator, roomVersion, accepted.roomVersion)) {
|
|
1076
|
-
return committed(room, local, submitted, accepted, pulled, roomVersion === accepted.roomVersion);
|
|
1077
|
-
}
|
|
1078
|
-
if (fresh && continuing())
|
|
1079
|
-
await publish(local, room, pulled);
|
|
1080
|
-
return false;
|
|
1081
|
-
}
|
|
1082
|
-
async function committed(room, local, submitted, accepted, base, overlay) {
|
|
1083
|
-
const failed = await exclusive(() => commitAccept(room, local, submitted, accepted, base, overlay));
|
|
1162
|
+
async function adopted(room, local, submitted, accepted) {
|
|
1163
|
+
const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
|
|
1084
1164
|
if (failed === null)
|
|
1085
1165
|
return true;
|
|
1086
1166
|
if (failed.kind === 'refused' && continuing())
|
|
@@ -1093,10 +1173,8 @@ export function composeRuntime(options) {
|
|
|
1093
1173
|
const head = replica?.queue[0];
|
|
1094
1174
|
if (replica === undefined || head === undefined)
|
|
1095
1175
|
return;
|
|
1096
|
-
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1097
|
-
progress.complete = false;
|
|
1176
|
+
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1098
1177
|
return;
|
|
1099
|
-
}
|
|
1100
1178
|
const state = pendings.get(head.id);
|
|
1101
1179
|
const { submission, lost: earlier } = state ?? {
|
|
1102
1180
|
submission: { state: 'waiting' },
|
|
@@ -1124,6 +1202,7 @@ export function composeRuntime(options) {
|
|
|
1124
1202
|
return;
|
|
1125
1203
|
}
|
|
1126
1204
|
const opensBefore = openSignals;
|
|
1205
|
+
const answered = awaitingAnswer(room.locator);
|
|
1127
1206
|
try {
|
|
1128
1207
|
result = await options.adapter.mutate(authorized, {
|
|
1129
1208
|
locator: room.locator,
|
|
@@ -1132,11 +1211,14 @@ export function composeRuntime(options) {
|
|
|
1132
1211
|
}, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
|
|
1133
1212
|
}
|
|
1134
1213
|
catch {
|
|
1214
|
+
answered();
|
|
1135
1215
|
progress.complete = false;
|
|
1136
1216
|
if (continuing())
|
|
1137
1217
|
progress.reachable = false;
|
|
1138
1218
|
return;
|
|
1139
1219
|
}
|
|
1220
|
+
if (result.kind !== 'accepted' || !continuing())
|
|
1221
|
+
answered();
|
|
1140
1222
|
if (!continuing())
|
|
1141
1223
|
return;
|
|
1142
1224
|
if (result.kind === 'transport-uncertainty') {
|
|
@@ -1189,6 +1271,7 @@ export function composeRuntime(options) {
|
|
|
1189
1271
|
const reset = result.reset;
|
|
1190
1272
|
progress.complete = false;
|
|
1191
1273
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1274
|
+
armTimer();
|
|
1192
1275
|
return;
|
|
1193
1276
|
}
|
|
1194
1277
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
@@ -1201,7 +1284,8 @@ export function composeRuntime(options) {
|
|
|
1201
1284
|
return;
|
|
1202
1285
|
}
|
|
1203
1286
|
const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
1204
|
-
leaving: result.kind === 'expired' && landed(head, current.confirmed)
|
|
1287
|
+
leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
|
|
1288
|
+
(result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
|
|
1205
1289
|
? { ...left, returned: null }
|
|
1206
1290
|
: left,
|
|
1207
1291
|
});
|
|
@@ -1215,95 +1299,33 @@ export function composeRuntime(options) {
|
|
|
1215
1299
|
return withhold(withheld, room, local, progress);
|
|
1216
1300
|
continue;
|
|
1217
1301
|
}
|
|
1218
|
-
await
|
|
1219
|
-
|
|
1220
|
-
if (from === undefined)
|
|
1221
|
-
return;
|
|
1222
|
-
const pulled = await pullRefresh(authorized, from, reachOf(room.locator));
|
|
1223
|
-
if (!continuing())
|
|
1224
|
-
return;
|
|
1225
|
-
if (pulled.kind === 'revoked-authorization') {
|
|
1226
|
-
await refuse('revoked-authorization');
|
|
1227
|
-
return;
|
|
1228
|
-
}
|
|
1229
|
-
if (pulled.kind === 'scoped-refusal') {
|
|
1230
|
-
await scoped(pulled, room, local);
|
|
1302
|
+
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1303
|
+
if (!kept) {
|
|
1231
1304
|
progress.complete = false;
|
|
1232
1305
|
return;
|
|
1233
1306
|
}
|
|
1234
|
-
if (pulled.kind === 'still-current' || pulled.kind === 'advanced') {
|
|
1235
|
-
const fresh = pulled.kind === 'advanced';
|
|
1236
|
-
if (!(await accepting(room, local, head, result, fresh ? pulled.confirmed : from, fresh))) {
|
|
1237
|
-
progress.complete = false;
|
|
1238
|
-
return;
|
|
1239
|
-
}
|
|
1240
|
-
continue;
|
|
1241
|
-
}
|
|
1242
|
-
if (pulled.kind === 'beyond-horizon') {
|
|
1243
|
-
const bootstrapped = await pullBootstrap(authorized, room);
|
|
1244
|
-
if (!continuing())
|
|
1245
|
-
return;
|
|
1246
|
-
if (bootstrapped.kind === 'revoked-authorization') {
|
|
1247
|
-
await refuse('revoked-authorization');
|
|
1248
|
-
return;
|
|
1249
|
-
}
|
|
1250
|
-
if (bootstrapped.kind === 'scoped-refusal') {
|
|
1251
|
-
await scoped(bootstrapped, room, local);
|
|
1252
|
-
progress.complete = false;
|
|
1253
|
-
return;
|
|
1254
|
-
}
|
|
1255
|
-
if (bootstrapped.kind === 'staged') {
|
|
1256
|
-
if (!(await accepting(room, local, head, result, bootstrapped.confirmed, true))) {
|
|
1257
|
-
progress.complete = false;
|
|
1258
|
-
return;
|
|
1259
|
-
}
|
|
1260
|
-
continue;
|
|
1261
|
-
}
|
|
1262
|
-
progress.complete = false;
|
|
1263
|
-
if (bootstrapped.kind === 'transport-uncertainty') {
|
|
1264
|
-
progress.reachable = false;
|
|
1265
|
-
}
|
|
1266
|
-
else if (bootstrapped.kind === 'room-not-readable') {
|
|
1267
|
-
unreadable.add(room.locator);
|
|
1268
|
-
list(room, false);
|
|
1269
|
-
emitRooms();
|
|
1270
|
-
}
|
|
1271
|
-
else if (bootstrapped.kind === 'refused') {
|
|
1272
|
-
refusedSource(room, bootstrapped);
|
|
1273
|
-
}
|
|
1274
|
-
return;
|
|
1275
|
-
}
|
|
1276
|
-
progress.complete = false;
|
|
1277
|
-
if (pulled.kind === 'transport-uncertainty') {
|
|
1278
|
-
progress.reachable = false;
|
|
1279
|
-
}
|
|
1280
|
-
else if (pulled.kind === 'room-not-readable') {
|
|
1281
|
-
unreadable.add(room.locator);
|
|
1282
|
-
list(room, false);
|
|
1283
|
-
emitRooms();
|
|
1284
|
-
}
|
|
1285
|
-
else if (pulled.kind === 'retryable-refusal') {
|
|
1286
|
-
journal.record(`refusal ${pulled.kind} ${pulled.code}`);
|
|
1287
|
-
}
|
|
1288
|
-
else if (pulled.kind === 'refused') {
|
|
1289
|
-
refusedSource(room, pulled);
|
|
1290
|
-
}
|
|
1291
|
-
return;
|
|
1292
1307
|
}
|
|
1293
|
-
|
|
1308
|
+
}
|
|
1309
|
+
function awaitingAnswer(locator) {
|
|
1310
|
+
let release = () => { };
|
|
1311
|
+
const awaited = new Promise((resolve) => {
|
|
1312
|
+
release = resolve;
|
|
1313
|
+
});
|
|
1314
|
+
answering.set(locator, awaited);
|
|
1315
|
+
return () => {
|
|
1316
|
+
if (answering.get(locator) === awaited)
|
|
1317
|
+
answering.delete(locator);
|
|
1318
|
+
release();
|
|
1319
|
+
};
|
|
1294
1320
|
}
|
|
1295
1321
|
async function retainModel(local, room, attached, next) {
|
|
1296
1322
|
const key = modelKey(room.locator, attached);
|
|
1297
|
-
if (next.status === 'unavailable')
|
|
1298
|
-
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
1299
|
-
const epoch = modelEpoch.get(key) ?? 0;
|
|
1300
1323
|
const previous = modelOf(room.locator, attached);
|
|
1301
1324
|
const heldBefore = replicas.get(room.locator)?.confirmed.record;
|
|
1302
1325
|
if (JSON.stringify(previous) === JSON.stringify(next) &&
|
|
1303
1326
|
(heldBefore === undefined ||
|
|
1304
1327
|
(JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
|
|
1305
1328
|
JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
|
|
1306
|
-
JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) &&
|
|
1307
1329
|
JSON.stringify(heldBefore.typedSelection) ===
|
|
1308
1330
|
JSON.stringify(room.typedSelection ?? null))))
|
|
1309
1331
|
return;
|
|
@@ -1314,20 +1336,17 @@ export function composeRuntime(options) {
|
|
|
1314
1336
|
await exclusive(async () => {
|
|
1315
1337
|
if (!continuing())
|
|
1316
1338
|
return;
|
|
1317
|
-
if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0))
|
|
1318
|
-
return;
|
|
1319
1339
|
const held = replicas.get(room.locator);
|
|
1320
1340
|
if (held !== undefined) {
|
|
1321
1341
|
const declared = {
|
|
1322
1342
|
...held.confirmed.record,
|
|
1323
1343
|
operations: room.operations,
|
|
1324
1344
|
replica: room.replica,
|
|
1325
|
-
attachedModel: room.attachedModel,
|
|
1326
1345
|
typedSelection: room.typedSelection ?? null,
|
|
1327
1346
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
1328
1347
|
};
|
|
1329
1348
|
const reclassified = sameMembership(declared, held.confirmed.record)
|
|
1330
|
-
?
|
|
1349
|
+
? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
|
|
1331
1350
|
: readReclassified(declared, held.confirmed);
|
|
1332
1351
|
const record = reclassified.kind === 'staged'
|
|
1333
1352
|
? declared
|
|
@@ -1335,14 +1354,16 @@ export function composeRuntime(options) {
|
|
|
1335
1354
|
...declared,
|
|
1336
1355
|
typedSelection: held.confirmed.record.typedSelection,
|
|
1337
1356
|
};
|
|
1338
|
-
const read = reclassified.kind === 'deferred'
|
|
1357
|
+
const read = reclassified.kind === 'deferred'
|
|
1358
|
+
? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
|
|
1359
|
+
: reclassified;
|
|
1339
1360
|
await local.write({
|
|
1340
1361
|
next: record,
|
|
1341
1362
|
previous: held.confirmed.record,
|
|
1342
1363
|
queue: held.queue,
|
|
1343
1364
|
});
|
|
1344
1365
|
if (read.kind === 'staged') {
|
|
1345
|
-
replicas.set(room.locator, replicaOf(read.confirmed, held.queue,
|
|
1366
|
+
replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
|
|
1346
1367
|
if (declarationRefusals.delete(room.locator)) {
|
|
1347
1368
|
sourceRefusals.delete(room.locator);
|
|
1348
1369
|
unreadable.delete(room.locator);
|
|
@@ -1366,79 +1387,12 @@ export function composeRuntime(options) {
|
|
|
1366
1387
|
publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
|
|
1367
1388
|
});
|
|
1368
1389
|
}
|
|
1369
|
-
async function acquireModel(local, room, authorized, selection) {
|
|
1370
|
-
const attached = selection === null ? 'collection' : 'typed-selection';
|
|
1371
|
-
const key = modelKey(room.locator, attached);
|
|
1372
|
-
const declared = selection === null ? room.attachedModel : selection.attachedModel;
|
|
1373
|
-
let previous = modelOf(room.locator, attached);
|
|
1374
|
-
if (previous.status !== 'unavailable' &&
|
|
1375
|
-
(declared === null || !sameModel(previous.snapshot, declared))) {
|
|
1376
|
-
previous = { status: 'unavailable', reason: 'version-changed' };
|
|
1377
|
-
await retainModel(local, room, attached, previous);
|
|
1378
|
-
if (discoveryPaused)
|
|
1379
|
-
return;
|
|
1380
|
-
}
|
|
1381
|
-
const epoch = modelEpoch.get(key) ?? 0;
|
|
1382
|
-
let answer;
|
|
1383
|
-
try {
|
|
1384
|
-
answer = await options.adapter.model(authorized, selection === null
|
|
1385
|
-
? { locator: room.locator, attached: 'collection', model: declared }
|
|
1386
|
-
: {
|
|
1387
|
-
locator: room.locator,
|
|
1388
|
-
attached: 'typed-selection',
|
|
1389
|
-
roots: selection.roots,
|
|
1390
|
-
model: declared,
|
|
1391
|
-
});
|
|
1392
|
-
}
|
|
1393
|
-
catch {
|
|
1394
|
-
answer = { kind: 'transport-uncertainty' };
|
|
1395
|
-
}
|
|
1396
|
-
if (discoveryPaused || epoch !== (modelEpoch.get(key) ?? 0))
|
|
1397
|
-
return;
|
|
1398
|
-
if (answer.kind === 'revoked-authorization') {
|
|
1399
|
-
await refuse('revoked-authorization');
|
|
1400
|
-
return;
|
|
1401
|
-
}
|
|
1402
|
-
if (answer.kind === 'transport-uncertainty') {
|
|
1403
|
-
await retainModel(local, room, attached, cached(previous));
|
|
1404
|
-
return;
|
|
1405
|
-
}
|
|
1406
|
-
if (answer.kind === 'model-unavailable') {
|
|
1407
|
-
await retainModel(local, room, attached, {
|
|
1408
|
-
status: 'unavailable',
|
|
1409
|
-
reason: answer.reason,
|
|
1410
|
-
});
|
|
1411
|
-
return;
|
|
1412
|
-
}
|
|
1413
|
-
if (declared === null || !sameModel(answer.snapshot, declared)) {
|
|
1414
|
-
await retainModel(local, room, attached, {
|
|
1415
|
-
status: 'unavailable',
|
|
1416
|
-
reason: 'version-changed',
|
|
1417
|
-
});
|
|
1418
|
-
return;
|
|
1419
|
-
}
|
|
1420
|
-
if (previous.status !== 'unavailable' &&
|
|
1421
|
-
sameModel(previous.snapshot, answer.snapshot) &&
|
|
1422
|
-
previous.snapshot.schema !== answer.snapshot.schema) {
|
|
1423
|
-
await retainModel(local, room, attached, { status: 'unavailable', reason: 'invalid' });
|
|
1424
|
-
return;
|
|
1425
|
-
}
|
|
1426
|
-
const verified = answer.snapshot.schema === null ? EMPTY_SCHEMA : verify(answer.snapshot.schema);
|
|
1427
|
-
if (verified.verdict === 'refuse') {
|
|
1428
|
-
const where = verified.location === undefined ? '' : ` ${verified.location}`;
|
|
1429
|
-
journal.record(`model invalid ${room.locator} ${attached} ${answer.snapshot.identity} ${verified.rule}${where}`);
|
|
1430
|
-
}
|
|
1431
|
-
await retainModel(local, room, attached, verified.verdict === 'accept'
|
|
1432
|
-
? {
|
|
1433
|
-
status: 'current',
|
|
1434
|
-
snapshot: structuredClone(answer.snapshot),
|
|
1435
|
-
verifiedWith: MD_MODEL,
|
|
1436
|
-
types: verified.model.types,
|
|
1437
|
-
}
|
|
1438
|
-
: { status: 'unavailable', reason: 'invalid' });
|
|
1439
|
-
}
|
|
1440
1390
|
async function scoped(result, room, local) {
|
|
1441
|
-
scopedRefusals.set(room.locator, {
|
|
1391
|
+
scopedRefusals.set(room.locator, {
|
|
1392
|
+
code: result.code,
|
|
1393
|
+
fields: result.fields,
|
|
1394
|
+
retryable: result.retryable,
|
|
1395
|
+
});
|
|
1442
1396
|
let scopedAway = room;
|
|
1443
1397
|
if (result.scope === 'model')
|
|
1444
1398
|
await retainModel(local, room, 'collection', {
|
|
@@ -1464,14 +1418,36 @@ export function composeRuntime(options) {
|
|
|
1464
1418
|
emitRooms();
|
|
1465
1419
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
1466
1420
|
}
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1421
|
+
function heldRooms(listed) {
|
|
1422
|
+
return listed.flatMap((room) => {
|
|
1423
|
+
const record = replicas.get(room.locator)?.confirmed.record;
|
|
1424
|
+
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1425
|
+
return [];
|
|
1426
|
+
return [
|
|
1427
|
+
{
|
|
1428
|
+
locator: room.locator,
|
|
1429
|
+
roomVersion: record.roomVersion,
|
|
1430
|
+
documents: record.documents,
|
|
1431
|
+
revisions: record.revisions,
|
|
1432
|
+
attachments: record.attachments,
|
|
1433
|
+
},
|
|
1434
|
+
];
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
async function synchronize(listed, local, progress, attempt) {
|
|
1438
|
+
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
|
|
1439
|
+
const ready = listed.filter((_, index) => {
|
|
1470
1440
|
const outcome = prepared[index];
|
|
1471
1441
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1472
1442
|
});
|
|
1473
|
-
|
|
1474
|
-
|
|
1443
|
+
if (attempt === 'opened') {
|
|
1444
|
+
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
1445
|
+
handed = true;
|
|
1446
|
+
}
|
|
1447
|
+
const loaded = await Promise.allSettled(ready
|
|
1448
|
+
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1449
|
+
.map((room) => loadRoom(room, local, progress)));
|
|
1450
|
+
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1475
1451
|
if (failed !== undefined)
|
|
1476
1452
|
throw failed.reason;
|
|
1477
1453
|
}
|
|
@@ -1488,11 +1464,8 @@ export function composeRuntime(options) {
|
|
|
1488
1464
|
progress.complete = false;
|
|
1489
1465
|
return false;
|
|
1490
1466
|
}
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
acquireModel(local, room, authorization, null),
|
|
1494
|
-
...(selection === undefined ? [] : [acquireModel(local, room, authorization, selection)]),
|
|
1495
|
-
]);
|
|
1467
|
+
if (prior !== undefined)
|
|
1468
|
+
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1496
1469
|
if (discoveryPaused || authorization === undefined)
|
|
1497
1470
|
return false;
|
|
1498
1471
|
if (room.replica.admission !== 'admitted') {
|
|
@@ -1500,24 +1473,53 @@ export function composeRuntime(options) {
|
|
|
1500
1473
|
progress.complete = false;
|
|
1501
1474
|
return false;
|
|
1502
1475
|
}
|
|
1503
|
-
const held = replicas.get(room.locator);
|
|
1504
|
-
if (held !== undefined && held.confirmed.record.replica.selection !== room.replica.selection) {
|
|
1505
|
-
progress.complete = false;
|
|
1506
|
-
return false;
|
|
1507
|
-
}
|
|
1508
1476
|
return true;
|
|
1509
1477
|
}
|
|
1510
|
-
async function
|
|
1478
|
+
async function caughtUp(room, local, progress) {
|
|
1479
|
+
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1480
|
+
if (held === undefined || authorization === undefined)
|
|
1481
|
+
return;
|
|
1482
|
+
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
1483
|
+
if (!behind)
|
|
1484
|
+
return;
|
|
1485
|
+
const withheld = await advance(room, local, authorization);
|
|
1486
|
+
if (withheld !== null)
|
|
1487
|
+
await withhold(withheld, room, local, progress);
|
|
1488
|
+
}
|
|
1489
|
+
function revoked(locator) {
|
|
1490
|
+
return ['collection', 'typed-selection'].some((attached) => {
|
|
1491
|
+
const model = modelOf(locator, attached);
|
|
1492
|
+
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
async function loadRoom(room, local, progress) {
|
|
1511
1496
|
if (discoveryPaused || authorization === undefined)
|
|
1512
1497
|
return;
|
|
1498
|
+
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1499
|
+
await reload(room, local, progress);
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1513
1502
|
const withheld = await advance(room, local, authorization);
|
|
1514
1503
|
if (withheld !== null) {
|
|
1515
1504
|
await withhold(withheld, room, local, progress);
|
|
1516
1505
|
return;
|
|
1517
1506
|
}
|
|
1518
|
-
await
|
|
1507
|
+
await caughtUp(room, local, progress);
|
|
1508
|
+
}
|
|
1509
|
+
async function reload(room, local, progress) {
|
|
1510
|
+
if (discoveryPaused || authorization === undefined)
|
|
1511
|
+
return;
|
|
1512
|
+
const bootstrapped = await pullBootstrap(authorization, room, local);
|
|
1513
|
+
if (!continuing())
|
|
1514
|
+
return;
|
|
1515
|
+
if (bootstrapped.kind !== 'staged') {
|
|
1516
|
+
await withhold(bootstrapped, room, local, progress);
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
1520
|
+
await caughtUp(room, local, progress);
|
|
1519
1521
|
}
|
|
1520
|
-
async function
|
|
1522
|
+
async function discoverRooms(local, progress, attempt) {
|
|
1521
1523
|
if (closed || authorization === undefined) {
|
|
1522
1524
|
notifyReady();
|
|
1523
1525
|
return;
|
|
@@ -1543,12 +1545,17 @@ export function composeRuntime(options) {
|
|
|
1543
1545
|
}
|
|
1544
1546
|
}
|
|
1545
1547
|
progress.reachable = false;
|
|
1548
|
+
progress.complete = false;
|
|
1546
1549
|
notifyReady();
|
|
1547
1550
|
return;
|
|
1548
1551
|
}
|
|
1549
1552
|
if (result.kind === 'contract-unavailable') {
|
|
1550
1553
|
journal.record(`connection contract unavailable ${result.code}`);
|
|
1551
|
-
await refuse('missing-required-guarantee', {
|
|
1554
|
+
await refuse('missing-required-guarantee', {
|
|
1555
|
+
code: result.code,
|
|
1556
|
+
fields: result.fields,
|
|
1557
|
+
retryable: result.retryable,
|
|
1558
|
+
});
|
|
1552
1559
|
notifyReady();
|
|
1553
1560
|
return;
|
|
1554
1561
|
}
|
|
@@ -1557,17 +1564,6 @@ export function composeRuntime(options) {
|
|
|
1557
1564
|
notifyReady();
|
|
1558
1565
|
return;
|
|
1559
1566
|
}
|
|
1560
|
-
if (!result.supportedVersions.includes(ROOM_CONTRACT_VERSION)) {
|
|
1561
|
-
await refuse('no-matching-version');
|
|
1562
|
-
notifyReady();
|
|
1563
|
-
return;
|
|
1564
|
-
}
|
|
1565
|
-
if (result.mdModel.identity !== MD_MODEL.identity ||
|
|
1566
|
-
result.mdModel.version !== MD_MODEL.version) {
|
|
1567
|
-
await refuse('md-model-version-mismatch');
|
|
1568
|
-
notifyReady();
|
|
1569
|
-
return;
|
|
1570
|
-
}
|
|
1571
1567
|
for (const room of result.rooms) {
|
|
1572
1568
|
if (room.operations === undefined ||
|
|
1573
1569
|
!['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
@@ -1575,8 +1571,7 @@ export function composeRuntime(options) {
|
|
|
1575
1571
|
room.replica === undefined ||
|
|
1576
1572
|
typeof room.replica.selection !== 'string' ||
|
|
1577
1573
|
room.replica.selection.length === 0 ||
|
|
1578
|
-
!['admitted', 'unavailable'].includes(room.replica.admission)
|
|
1579
|
-
!declaredIdentity(room.attachedModel)) {
|
|
1574
|
+
!['admitted', 'unavailable'].includes(room.replica.admission)) {
|
|
1580
1575
|
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
1581
1576
|
await refuse('missing-required-guarantee');
|
|
1582
1577
|
notifyReady();
|
|
@@ -1589,14 +1584,6 @@ export function composeRuntime(options) {
|
|
|
1589
1584
|
return;
|
|
1590
1585
|
}
|
|
1591
1586
|
}
|
|
1592
|
-
const declared = new Set(result.capabilities.map((capability) => capability.name));
|
|
1593
|
-
for (const required of options.requiredGuarantees) {
|
|
1594
|
-
if (!knownCapabilities.has(required) || !declared.has(required)) {
|
|
1595
|
-
await refuse('missing-required-guarantee');
|
|
1596
|
-
notifyReady();
|
|
1597
|
-
return;
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
1587
|
if (refusal !== null) {
|
|
1601
1588
|
refusal = null;
|
|
1602
1589
|
emitRefusal();
|
|
@@ -1604,26 +1591,19 @@ export function composeRuntime(options) {
|
|
|
1604
1591
|
const previousCapabilities = capabilities;
|
|
1605
1592
|
capabilities = result.capabilities;
|
|
1606
1593
|
notifyReady();
|
|
1607
|
-
if (declared.has('room-version-notifications'))
|
|
1608
|
-
observeRoomVersions();
|
|
1609
1594
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1610
1595
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
1611
1596
|
limits = result.providerLimits;
|
|
1612
1597
|
await local.declare(limits, capabilities);
|
|
1613
1598
|
}
|
|
1599
|
+
const signedIn = result.member ?? null;
|
|
1600
|
+
if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
|
|
1601
|
+
await local.keepMember(signedIn);
|
|
1602
|
+
member = signedIn;
|
|
1603
|
+
emitMember();
|
|
1604
|
+
}
|
|
1614
1605
|
await applyDiscovery(result.rooms, local);
|
|
1615
|
-
|
|
1616
|
-
.filter((room) => room.replica.admission === 'admitted')
|
|
1617
|
-
.map((room) => room.locator)
|
|
1618
|
-
.sort());
|
|
1619
|
-
progress.complete = true;
|
|
1620
|
-
await synchronize(result.rooms, local, progress);
|
|
1621
|
-
if (closed || discoveryPaused)
|
|
1622
|
-
return;
|
|
1623
|
-
await conversations.reconcile();
|
|
1624
|
-
if (status.providerUnreachable)
|
|
1625
|
-
conversations.reconnected();
|
|
1626
|
-
await conversations.catchUp(progress);
|
|
1606
|
+
await synchronize(result.rooms, local, progress, attempt);
|
|
1627
1607
|
}
|
|
1628
1608
|
async function ensureAuthorization() {
|
|
1629
1609
|
if (authorization !== undefined)
|
|
@@ -1637,64 +1617,262 @@ export function composeRuntime(options) {
|
|
|
1637
1617
|
throw error;
|
|
1638
1618
|
}
|
|
1639
1619
|
}
|
|
1640
|
-
|
|
1641
|
-
if (
|
|
1642
|
-
|
|
1620
|
+
function task(work) {
|
|
1621
|
+
if (stopping)
|
|
1622
|
+
return chain;
|
|
1623
|
+
chain = chain.then(work, work).catch(() => undefined);
|
|
1624
|
+
return chain;
|
|
1625
|
+
}
|
|
1626
|
+
function sync(attempt) {
|
|
1627
|
+
return task(async () => {
|
|
1628
|
+
if (closed || stopping || discoveryPaused) {
|
|
1629
|
+
notifyReady();
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
const progress = { complete: true, reachable: true };
|
|
1633
|
+
underway = attempt === 'opened' ? progress : null;
|
|
1634
|
+
if (attempt === 'opened')
|
|
1635
|
+
streamed = progress;
|
|
1636
|
+
complete = false;
|
|
1637
|
+
recorded = false;
|
|
1638
|
+
moveTo(transferring(status));
|
|
1639
|
+
handed = false;
|
|
1640
|
+
try {
|
|
1641
|
+
const local = await openLocal();
|
|
1642
|
+
if (closed)
|
|
1643
|
+
return;
|
|
1644
|
+
await ensureAuthorization();
|
|
1645
|
+
if (closed)
|
|
1646
|
+
return;
|
|
1647
|
+
await discoverRooms(local, progress, attempt);
|
|
1648
|
+
}
|
|
1649
|
+
catch {
|
|
1650
|
+
progress.complete = false;
|
|
1651
|
+
if (attempt === 'opened' && !handed)
|
|
1652
|
+
progress.reachable = false;
|
|
1653
|
+
}
|
|
1654
|
+
finally {
|
|
1655
|
+
notifyReady();
|
|
1656
|
+
}
|
|
1657
|
+
if (attempt !== 'opened') {
|
|
1658
|
+
await finishOpening(progress);
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
if (handed || closed || progress.reachable)
|
|
1662
|
+
return;
|
|
1663
|
+
underway = null;
|
|
1664
|
+
journal.record('connection reopen');
|
|
1665
|
+
await rest(progress);
|
|
1666
|
+
options.adapter.roomStream?.reopen();
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
function closedSocket(revoked) {
|
|
1670
|
+
const opened = socketOpen;
|
|
1671
|
+
socketOpen = false;
|
|
1672
|
+
if (revoked) {
|
|
1673
|
+
void refuse('revoked-authorization');
|
|
1643
1674
|
return;
|
|
1644
1675
|
}
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1676
|
+
complete = false;
|
|
1677
|
+
if (streamed !== null)
|
|
1678
|
+
streamed.complete = false;
|
|
1679
|
+
moveTo({
|
|
1680
|
+
...status,
|
|
1681
|
+
state: status.state === 'synced' ? 'syncing' : status.state,
|
|
1682
|
+
providerUnreachable: status.providerUnreachable || !opened,
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
function applyOpening(expired) {
|
|
1686
|
+
return task(async () => {
|
|
1687
|
+
const progress = underway;
|
|
1688
|
+
underway = null;
|
|
1689
|
+
if (closed || stopping)
|
|
1654
1690
|
return;
|
|
1655
|
-
|
|
1656
|
-
|
|
1691
|
+
if (progress === null) {
|
|
1692
|
+
await reloadExpired(expired);
|
|
1657
1693
|
return;
|
|
1658
|
-
if (progress.complete) {
|
|
1659
|
-
completedAt = new Date().toISOString();
|
|
1660
|
-
await local.complete(completedAt);
|
|
1661
1694
|
}
|
|
1695
|
+
try {
|
|
1696
|
+
const local = await openLocal();
|
|
1697
|
+
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
1698
|
+
const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
|
|
1699
|
+
const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
|
|
1700
|
+
if (progress.reachable) {
|
|
1701
|
+
for (const room of held)
|
|
1702
|
+
if (!reloading.includes(room))
|
|
1703
|
+
await confirmModels(room, local);
|
|
1704
|
+
}
|
|
1705
|
+
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1706
|
+
if (failed !== undefined)
|
|
1707
|
+
throw failed.reason;
|
|
1708
|
+
}
|
|
1709
|
+
catch {
|
|
1710
|
+
progress.complete = false;
|
|
1711
|
+
progress.reachable = false;
|
|
1712
|
+
}
|
|
1713
|
+
await finishOpening(progress);
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
async function reloadExpired(expired) {
|
|
1717
|
+
const progress = { complete: true, reachable: true };
|
|
1718
|
+
try {
|
|
1719
|
+
const local = await openLocal();
|
|
1720
|
+
await Promise.allSettled([...discovered.values()]
|
|
1721
|
+
.filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
|
|
1722
|
+
.map((room) => reload(room, local, progress)));
|
|
1662
1723
|
}
|
|
1663
1724
|
catch {
|
|
1664
|
-
progress.
|
|
1665
|
-
|
|
1725
|
+
progress.reachable = false;
|
|
1726
|
+
}
|
|
1727
|
+
await rest(progress);
|
|
1728
|
+
}
|
|
1729
|
+
async function confirmModels(room, local) {
|
|
1730
|
+
for (const attached of ['collection', 'typed-selection']) {
|
|
1731
|
+
const model = modelOf(room.locator, attached);
|
|
1732
|
+
if (model.status === 'cached') {
|
|
1733
|
+
await retainModel(local, room, attached, { ...model, status: 'current' });
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
function submitLater(locator) {
|
|
1738
|
+
if (localSaves)
|
|
1739
|
+
return;
|
|
1740
|
+
const running = submitting.get(locator);
|
|
1741
|
+
if (running !== undefined) {
|
|
1742
|
+
running.again = true;
|
|
1666
1743
|
return;
|
|
1667
1744
|
}
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1745
|
+
const run = { again: false, done: Promise.resolve() };
|
|
1746
|
+
submitting.set(locator, run);
|
|
1747
|
+
run.done = (async () => {
|
|
1748
|
+
try {
|
|
1749
|
+
do {
|
|
1750
|
+
run.again = false;
|
|
1751
|
+
const room = discovered.get(locator);
|
|
1752
|
+
if (closed || stopping || discoveryPaused || room === undefined)
|
|
1753
|
+
return;
|
|
1754
|
+
const progress = { complete: true, reachable: true };
|
|
1755
|
+
try {
|
|
1756
|
+
await ensureAuthorization();
|
|
1757
|
+
if (authorization === undefined)
|
|
1758
|
+
return;
|
|
1759
|
+
await submitRoom(room, await openLocal(), authorization, progress);
|
|
1760
|
+
}
|
|
1761
|
+
catch {
|
|
1762
|
+
progress.reachable = false;
|
|
1763
|
+
}
|
|
1764
|
+
armTimer();
|
|
1765
|
+
await rest(progress);
|
|
1766
|
+
} while (run.again);
|
|
1767
|
+
}
|
|
1768
|
+
finally {
|
|
1769
|
+
submitting.delete(locator);
|
|
1770
|
+
}
|
|
1771
|
+
})();
|
|
1772
|
+
}
|
|
1773
|
+
function submitAll() {
|
|
1774
|
+
for (const [locator, replica] of replicas) {
|
|
1775
|
+
if (replica.queue.length > 0)
|
|
1776
|
+
submitLater(locator);
|
|
1671
1777
|
}
|
|
1672
1778
|
}
|
|
1673
|
-
async function
|
|
1779
|
+
async function finishOpening(progress) {
|
|
1674
1780
|
try {
|
|
1675
|
-
|
|
1781
|
+
if (closed || discoveryPaused) {
|
|
1782
|
+
progress.complete = false;
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
submitAll();
|
|
1786
|
+
await conversations.reconcile();
|
|
1787
|
+
if (status.providerUnreachable)
|
|
1788
|
+
conversations.reconnected();
|
|
1789
|
+
const reading = { complete: true, reachable: true };
|
|
1790
|
+
await conversations.catchUp(reading);
|
|
1791
|
+
if (!reading.reachable)
|
|
1792
|
+
progress.reachable = false;
|
|
1793
|
+
}
|
|
1794
|
+
catch {
|
|
1795
|
+
progress.complete = false;
|
|
1676
1796
|
}
|
|
1677
1797
|
finally {
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
queued = false;
|
|
1681
|
-
void enqueue();
|
|
1682
|
-
}
|
|
1798
|
+
complete = progress.complete;
|
|
1799
|
+
await rest(progress);
|
|
1683
1800
|
}
|
|
1684
1801
|
}
|
|
1685
|
-
function
|
|
1686
|
-
|
|
1687
|
-
if (path === undefined || withdrawNotifications !== undefined)
|
|
1802
|
+
async function rest(progress) {
|
|
1803
|
+
if (closed)
|
|
1688
1804
|
return;
|
|
1689
|
-
|
|
1690
|
-
|
|
1805
|
+
let lastCompleted = status.lastCompleted;
|
|
1806
|
+
let synced = complete && stale.size === 0 && !pending();
|
|
1807
|
+
if (synced && !recorded) {
|
|
1808
|
+
const completedAt = new Date().toISOString();
|
|
1809
|
+
try {
|
|
1810
|
+
await (await openLocal()).complete(completedAt);
|
|
1811
|
+
recorded = true;
|
|
1812
|
+
lastCompleted = completedAt;
|
|
1813
|
+
}
|
|
1814
|
+
catch {
|
|
1815
|
+
synced = false;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
moveTo({
|
|
1819
|
+
state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
|
|
1820
|
+
lastCompleted,
|
|
1821
|
+
providerUnreachable: !progress.reachable,
|
|
1822
|
+
...attention(),
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
function applyRoom(locator, roomVersion) {
|
|
1826
|
+
announced.set(locator, roomVersion);
|
|
1827
|
+
return task(async () => {
|
|
1828
|
+
await answering.get(locator);
|
|
1829
|
+
if (closed || stopping || discoveryPaused || authorization === undefined)
|
|
1830
|
+
return;
|
|
1831
|
+
const room = discovered.get(locator);
|
|
1832
|
+
const held = replicas.get(locator);
|
|
1833
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
1691
1834
|
return;
|
|
1692
|
-
if (
|
|
1693
|
-
|
|
1835
|
+
if (held !== undefined &&
|
|
1836
|
+
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
1837
|
+
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
1694
1838
|
return;
|
|
1695
1839
|
}
|
|
1696
|
-
|
|
1697
|
-
|
|
1840
|
+
const during = underway;
|
|
1841
|
+
const progress = during ?? { complete: true, reachable: true };
|
|
1842
|
+
if (during === null)
|
|
1843
|
+
moveTo(transferring(status));
|
|
1844
|
+
try {
|
|
1845
|
+
const local = await openLocal();
|
|
1846
|
+
const withheld = await advance(room, local, authorization);
|
|
1847
|
+
if (withheld !== null)
|
|
1848
|
+
await withhold(withheld, room, local, progress);
|
|
1849
|
+
}
|
|
1850
|
+
catch {
|
|
1851
|
+
stale.add(locator);
|
|
1852
|
+
}
|
|
1853
|
+
finally {
|
|
1854
|
+
if (during === null)
|
|
1855
|
+
await rest(progress);
|
|
1856
|
+
}
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
function observeStream() {
|
|
1860
|
+
const stream = options.adapter.roomStream;
|
|
1861
|
+
if (stream === undefined || withdrawStream !== undefined)
|
|
1862
|
+
return;
|
|
1863
|
+
withdrawStream = stream.observe((event) => {
|
|
1864
|
+
if (closed || stopping)
|
|
1865
|
+
return;
|
|
1866
|
+
if (event.kind === 'opened') {
|
|
1867
|
+
socketOpen = true;
|
|
1868
|
+
void sync('opened');
|
|
1869
|
+
}
|
|
1870
|
+
else if (event.kind === 'closed')
|
|
1871
|
+
closedSocket(event.revoked);
|
|
1872
|
+
else if (event.kind === 'opening')
|
|
1873
|
+
void applyOpening(event.expired);
|
|
1874
|
+
else
|
|
1875
|
+
void applyRoom(event.locator, event.roomVersion);
|
|
1698
1876
|
});
|
|
1699
1877
|
}
|
|
1700
1878
|
function writesHeld() {
|
|
@@ -1715,14 +1893,17 @@ export function composeRuntime(options) {
|
|
|
1715
1893
|
listener(mode);
|
|
1716
1894
|
}
|
|
1717
1895
|
if (releasing)
|
|
1718
|
-
|
|
1896
|
+
submitAll();
|
|
1719
1897
|
}
|
|
1720
1898
|
function observeConversations() {
|
|
1721
1899
|
const stream = options.adapter.conversations;
|
|
1722
1900
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
1723
1901
|
return;
|
|
1724
1902
|
withdrawConversations = stream.observe((event) => {
|
|
1725
|
-
conversations.receive(event)
|
|
1903
|
+
if (conversations.receive(event)) {
|
|
1904
|
+
void ready.then(scheduleCurrency);
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1726
1907
|
if (event.kind !== 'gap' || conversations.current())
|
|
1727
1908
|
return;
|
|
1728
1909
|
show();
|
|
@@ -1733,7 +1914,7 @@ export function composeRuntime(options) {
|
|
|
1733
1914
|
if (currencyQueued || closed || stopping)
|
|
1734
1915
|
return;
|
|
1735
1916
|
currencyQueued = true;
|
|
1736
|
-
|
|
1917
|
+
void task(runCurrency);
|
|
1737
1918
|
}
|
|
1738
1919
|
async function runCurrency() {
|
|
1739
1920
|
currencyQueued = false;
|
|
@@ -1744,110 +1925,22 @@ export function composeRuntime(options) {
|
|
|
1744
1925
|
show();
|
|
1745
1926
|
return;
|
|
1746
1927
|
}
|
|
1747
|
-
const
|
|
1748
|
-
|
|
1749
|
-
moveTo(transferring(resting));
|
|
1928
|
+
const progress = { complete: false, reachable: !status.providerUnreachable };
|
|
1929
|
+
moveTo(transferring(status));
|
|
1750
1930
|
try {
|
|
1751
1931
|
await conversations.catchUp(progress);
|
|
1752
1932
|
}
|
|
1753
1933
|
catch {
|
|
1754
1934
|
progress.reachable = false;
|
|
1755
1935
|
}
|
|
1756
|
-
|
|
1757
|
-
if (!closed) {
|
|
1758
|
-
moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
}
|
|
1762
|
-
function handSelected(locators) {
|
|
1763
|
-
const path = options.adapter.roomVersions;
|
|
1764
|
-
if (path === undefined || withdrawNotifications === undefined)
|
|
1765
|
-
return;
|
|
1766
|
-
if (handed !== undefined && JSON.stringify(locators) === JSON.stringify(handed))
|
|
1767
|
-
return;
|
|
1768
|
-
handed = locators;
|
|
1769
|
-
path.select(locators);
|
|
1770
|
-
}
|
|
1771
|
-
async function runNotified() {
|
|
1772
|
-
const announced = [...notices];
|
|
1773
|
-
notices.clear();
|
|
1774
|
-
if (announced.length === 0 || closed || stopping || discoveryPaused)
|
|
1775
|
-
return;
|
|
1776
|
-
const resting = status;
|
|
1777
|
-
const progress = { complete: false, reachable: !resting.providerUnreachable };
|
|
1778
|
-
moveTo(transferring(resting));
|
|
1779
|
-
try {
|
|
1780
|
-
const local = await openLocal();
|
|
1781
|
-
for (const [locator, roomVersion] of announced) {
|
|
1782
|
-
if (closed || discoveryPaused || authorization === undefined)
|
|
1783
|
-
return;
|
|
1784
|
-
if (handed?.includes(locator) !== true)
|
|
1785
|
-
continue;
|
|
1786
|
-
const room = discovered.get(locator);
|
|
1787
|
-
const held = replicas.get(locator);
|
|
1788
|
-
if (room === undefined)
|
|
1789
|
-
continue;
|
|
1790
|
-
if (sourceRefusals.has(locator)) {
|
|
1791
|
-
void enqueue();
|
|
1792
|
-
continue;
|
|
1793
|
-
}
|
|
1794
|
-
if (held === undefined)
|
|
1795
|
-
continue;
|
|
1796
|
-
if (held.confirmed.record.roomVersion === roomVersion)
|
|
1797
|
-
continue;
|
|
1798
|
-
const withheld = await advance(room, local, authorization);
|
|
1799
|
-
if (withheld === null)
|
|
1800
|
-
continue;
|
|
1801
|
-
if (withheld.kind === 'refused') {
|
|
1802
|
-
void enqueue();
|
|
1803
|
-
continue;
|
|
1804
|
-
}
|
|
1805
|
-
if (withheld.kind === 'scoped-refusal') {
|
|
1806
|
-
await scoped(withheld, room, local);
|
|
1807
|
-
continue;
|
|
1808
|
-
}
|
|
1809
|
-
if (withheld.kind === 'transport-uncertainty') {
|
|
1810
|
-
progress.reachable = false;
|
|
1811
|
-
continue;
|
|
1812
|
-
}
|
|
1813
|
-
if (withheld.kind === 'room-not-readable') {
|
|
1814
|
-
unreadable.add(locator);
|
|
1815
|
-
list(room, false);
|
|
1816
|
-
emitRooms();
|
|
1817
|
-
continue;
|
|
1818
|
-
}
|
|
1819
|
-
if (withheld.kind === 'revoked-authorization') {
|
|
1820
|
-
await refuse('revoked-authorization');
|
|
1821
|
-
return;
|
|
1822
|
-
}
|
|
1823
|
-
}
|
|
1824
|
-
}
|
|
1825
|
-
catch {
|
|
1826
|
-
progress.reachable = false;
|
|
1827
|
-
}
|
|
1828
|
-
finally {
|
|
1829
|
-
if (!closed) {
|
|
1830
|
-
moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
|
|
1831
|
-
}
|
|
1832
|
-
}
|
|
1833
|
-
}
|
|
1834
|
-
function enqueue() {
|
|
1835
|
-
if (stopping)
|
|
1836
|
-
return chain;
|
|
1837
|
-
if (inflight) {
|
|
1838
|
-
queued = true;
|
|
1839
|
-
return chain;
|
|
1840
|
-
}
|
|
1841
|
-
inflight = true;
|
|
1842
|
-
chain = chain.then(run, run);
|
|
1843
|
-
return chain;
|
|
1936
|
+
await rest(progress);
|
|
1844
1937
|
}
|
|
1845
1938
|
function close() {
|
|
1846
1939
|
if (closed)
|
|
1847
1940
|
return;
|
|
1848
1941
|
closed = true;
|
|
1849
|
-
|
|
1850
|
-
|
|
1942
|
+
withdrawStream?.();
|
|
1943
|
+
withdrawStream = undefined;
|
|
1851
1944
|
withdrawConversations?.();
|
|
1852
1945
|
withdrawConversations = undefined;
|
|
1853
1946
|
withdrawServiceMode?.();
|
|
@@ -1862,6 +1955,7 @@ export function composeRuntime(options) {
|
|
|
1862
1955
|
storageLossListeners.clear();
|
|
1863
1956
|
statusListeners.clear();
|
|
1864
1957
|
serviceModeListeners.clear();
|
|
1958
|
+
memberListeners.clear();
|
|
1865
1959
|
readSide.close();
|
|
1866
1960
|
queueSubscriptions.clear();
|
|
1867
1961
|
roomDraftListeners.clear();
|
|
@@ -1871,6 +1965,7 @@ export function composeRuntime(options) {
|
|
|
1871
1965
|
stopping = true;
|
|
1872
1966
|
stopTimer();
|
|
1873
1967
|
await chain.catch(() => undefined);
|
|
1968
|
+
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
1874
1969
|
await mutations;
|
|
1875
1970
|
close();
|
|
1876
1971
|
}
|
|
@@ -1879,21 +1974,29 @@ export function composeRuntime(options) {
|
|
|
1879
1974
|
options.signal?.addEventListener('abort', close, { once: true });
|
|
1880
1975
|
if (options.signal?.aborted)
|
|
1881
1976
|
close();
|
|
1882
|
-
|
|
1977
|
+
observeStream();
|
|
1978
|
+
void openLocal().then(() => {
|
|
1979
|
+
if (capabilities.length > 0)
|
|
1980
|
+
notifyReady();
|
|
1981
|
+
}, () => {
|
|
1883
1982
|
notifyReady();
|
|
1884
1983
|
});
|
|
1885
|
-
|
|
1984
|
+
if (options.adapter.roomStream === undefined)
|
|
1985
|
+
void sync('unstreamed');
|
|
1886
1986
|
function planned(input, reach) {
|
|
1887
1987
|
return planOf(input, roomFacts(reach), planReach(reach));
|
|
1888
1988
|
}
|
|
1889
1989
|
function watchPlan(plan, deliver) {
|
|
1990
|
+
materialize(reachedRooms(plan));
|
|
1890
1991
|
return readSide.watch(plan, identityOf(plan), deliver);
|
|
1891
1992
|
}
|
|
1892
1993
|
async function searched(input, reach) {
|
|
1893
1994
|
const plan = searchPlanOf(input);
|
|
1894
1995
|
const selected = plan.documents === undefined && reach === null
|
|
1895
1996
|
? null
|
|
1896
|
-
: (await
|
|
1997
|
+
: (await rowsQuery(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
|
|
1998
|
+
if (selected === null)
|
|
1999
|
+
materialize(null);
|
|
1897
2000
|
return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
|
|
1898
2001
|
}
|
|
1899
2002
|
const watch = (topic, input, deliver) => {
|
|
@@ -1917,6 +2020,10 @@ export function composeRuntime(options) {
|
|
|
1917
2020
|
if (topic === 'conversation') {
|
|
1918
2021
|
return conversations.watch(input[0], input[1], deliver);
|
|
1919
2022
|
}
|
|
2023
|
+
if (topic === 'conversations')
|
|
2024
|
+
return conversations.watchList(deliver);
|
|
2025
|
+
if (topic === 'agents')
|
|
2026
|
+
return conversations.watchAgents(deliver);
|
|
1920
2027
|
if (topic === 'presentation') {
|
|
1921
2028
|
const listener = {
|
|
1922
2029
|
app: input[0],
|
|
@@ -1971,6 +2078,13 @@ export function composeRuntime(options) {
|
|
|
1971
2078
|
serviceModeListeners.delete(deliver);
|
|
1972
2079
|
};
|
|
1973
2080
|
}
|
|
2081
|
+
if (topic === 'member') {
|
|
2082
|
+
memberListeners.add(deliver);
|
|
2083
|
+
deliver(member);
|
|
2084
|
+
return () => {
|
|
2085
|
+
memberListeners.delete(deliver);
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
1974
2088
|
if (topic === 'queue') {
|
|
1975
2089
|
const queue = { locator: input[0], notify: deliver };
|
|
1976
2090
|
queueSubscriptions.add(queue);
|
|
@@ -1988,7 +2102,7 @@ export function composeRuntime(options) {
|
|
|
1988
2102
|
rooms() {
|
|
1989
2103
|
return listed();
|
|
1990
2104
|
},
|
|
1991
|
-
query: ((input) =>
|
|
2105
|
+
query: ((input) => rowsQuery(planned(input, null))),
|
|
1992
2106
|
search: (input) => searched(input, null),
|
|
1993
2107
|
types(locator) {
|
|
1994
2108
|
return {
|
|
@@ -2017,14 +2131,11 @@ export function composeRuntime(options) {
|
|
|
2017
2131
|
return exclusive(async () => {
|
|
2018
2132
|
const parts = readParts(input);
|
|
2019
2133
|
if (path === '' || normalize(path) !== path)
|
|
2020
|
-
throw
|
|
2134
|
+
throw runtimeError('room-draft-malformed');
|
|
2021
2135
|
const local = await openLocal();
|
|
2022
2136
|
const current = roomDrafts.get(room)?.get(path);
|
|
2023
2137
|
if ((current?.revision ?? null) !== revision) {
|
|
2024
|
-
return
|
|
2025
|
-
outcome: 'stale',
|
|
2026
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
2027
|
-
};
|
|
2138
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2028
2139
|
}
|
|
2029
2140
|
const record = {
|
|
2030
2141
|
locator: room,
|
|
@@ -2044,10 +2155,7 @@ export function composeRuntime(options) {
|
|
|
2044
2155
|
const local = await openLocal();
|
|
2045
2156
|
const current = roomDrafts.get(room)?.get(path);
|
|
2046
2157
|
if (current === undefined || current.revision !== revision) {
|
|
2047
|
-
return
|
|
2048
|
-
outcome: 'stale',
|
|
2049
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
2050
|
-
};
|
|
2158
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2051
2159
|
}
|
|
2052
2160
|
await local.discardRoomDraft(room, path);
|
|
2053
2161
|
forgetRoomDraft(room, path);
|
|
@@ -2105,14 +2213,17 @@ export function composeRuntime(options) {
|
|
|
2105
2213
|
await mutations;
|
|
2106
2214
|
authorization = undefined;
|
|
2107
2215
|
discoveryPaused = false;
|
|
2108
|
-
|
|
2216
|
+
if (options.adapter.roomStream === undefined)
|
|
2217
|
+
await sync('unstreamed');
|
|
2218
|
+
else
|
|
2219
|
+
await task(ensureAuthorization);
|
|
2109
2220
|
},
|
|
2110
2221
|
};
|
|
2111
2222
|
const reachable = {
|
|
2112
2223
|
md,
|
|
2113
2224
|
watch,
|
|
2114
2225
|
plan: planned,
|
|
2115
|
-
query: (plan) =>
|
|
2226
|
+
query: (plan) => rowsQuery(plan),
|
|
2116
2227
|
watchPlan,
|
|
2117
2228
|
search: searched,
|
|
2118
2229
|
};
|