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