@agentshouse/mdruntime 0.1.0-c8c4b020a3ec7 → 0.1.0-c967d1ca66fee
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 +711 -587
- 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 -2
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +0 -8
- 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 {
|
|
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
|
-
|
|
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,21 @@ 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 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();
|
|
109
97
|
let chain = Promise.resolve();
|
|
110
98
|
let mutations = Promise.resolve();
|
|
111
|
-
let inflight = false;
|
|
112
|
-
let queued = false;
|
|
113
99
|
const roomListeners = new Set();
|
|
114
100
|
const refusalListeners = new Set();
|
|
115
101
|
const storageLossListeners = new Set();
|
|
@@ -120,13 +106,19 @@ export function composeRuntime(options) {
|
|
|
120
106
|
const readSide = createReadSide();
|
|
121
107
|
let withdrawConversations;
|
|
122
108
|
let currencyQueued = false;
|
|
109
|
+
let socketOpen = false;
|
|
123
110
|
let serviceMode = 'open';
|
|
124
111
|
let haltedRefusal = false;
|
|
125
112
|
let openSignals = 0;
|
|
126
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();
|
|
127
119
|
const conversations = createConversationStore({
|
|
128
|
-
|
|
129
|
-
documents: (locator) =>
|
|
120
|
+
agents: declaredAgents,
|
|
121
|
+
documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
|
|
130
122
|
call: (name, input) => invokeAction(name, input),
|
|
131
123
|
durably,
|
|
132
124
|
stream: () => options.adapter.conversations,
|
|
@@ -202,7 +194,7 @@ export function composeRuntime(options) {
|
|
|
202
194
|
}
|
|
203
195
|
}
|
|
204
196
|
function pending() {
|
|
205
|
-
return [...replicas.values()].some((replica) => replica.queue.length > 0);
|
|
197
|
+
return !localSaves && [...replicas.values()].some((replica) => replica.queue.length > 0);
|
|
206
198
|
}
|
|
207
199
|
function attention() {
|
|
208
200
|
const attentionRequired = [];
|
|
@@ -231,14 +223,33 @@ export function composeRuntime(options) {
|
|
|
231
223
|
}
|
|
232
224
|
return { attentionRequired, reset };
|
|
233
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
|
+
}
|
|
234
238
|
function listed() {
|
|
235
|
-
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
|
+
}));
|
|
236
244
|
}
|
|
237
|
-
function roomFacts() {
|
|
238
|
-
const held = [...rooms.values()]
|
|
245
|
+
function roomFacts(reach = null) {
|
|
246
|
+
const held = [...rooms.values()]
|
|
247
|
+
.filter((room) => holds(reach, room.locator))
|
|
248
|
+
.map((room) => {
|
|
249
|
+
const governed = governs(reach, room);
|
|
239
250
|
const attached = [
|
|
240
|
-
modelOf(room.locator),
|
|
241
|
-
...(
|
|
251
|
+
...(governed.collection ? [modelOf(room.locator)] : []),
|
|
252
|
+
...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
|
|
242
253
|
];
|
|
243
254
|
return {
|
|
244
255
|
locator: room.locator,
|
|
@@ -261,6 +272,10 @@ export function composeRuntime(options) {
|
|
|
261
272
|
for (const listener of storageLossListeners)
|
|
262
273
|
listener(storageLoss);
|
|
263
274
|
}
|
|
275
|
+
function emitMember() {
|
|
276
|
+
for (const listener of memberListeners)
|
|
277
|
+
listener(member);
|
|
278
|
+
}
|
|
264
279
|
function moveTo(next) {
|
|
265
280
|
if (closed)
|
|
266
281
|
return;
|
|
@@ -280,6 +295,8 @@ export function composeRuntime(options) {
|
|
|
280
295
|
listener(shown);
|
|
281
296
|
}
|
|
282
297
|
function entryOf(batch) {
|
|
298
|
+
if (localSaves)
|
|
299
|
+
return Object.freeze({ batch, state: 'local' });
|
|
283
300
|
const state = pendings.get(batch.id);
|
|
284
301
|
if (state?.accepted !== undefined) {
|
|
285
302
|
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
@@ -300,12 +317,47 @@ export function composeRuntime(options) {
|
|
|
300
317
|
}
|
|
301
318
|
}
|
|
302
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);
|
|
303
343
|
readSide.apply(set);
|
|
304
344
|
searchIndex.changed(set);
|
|
305
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
|
+
}
|
|
306
357
|
function publishChangeSet(set) {
|
|
307
358
|
applyChangeSet(set);
|
|
308
|
-
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)))
|
|
309
361
|
emitRooms();
|
|
310
362
|
for (const locator of set.queues)
|
|
311
363
|
emitQueue(locator);
|
|
@@ -316,23 +368,32 @@ export function composeRuntime(options) {
|
|
|
316
368
|
for (const app of set.presentations)
|
|
317
369
|
emitPresentation(app);
|
|
318
370
|
conversations.changed(set);
|
|
319
|
-
const cards = declaredCards();
|
|
320
|
-
if (cards !== undefined && set.documents.some((change) => change.locator === cards.locator)) {
|
|
321
|
-
scheduleCurrency();
|
|
322
|
-
}
|
|
323
371
|
}
|
|
324
372
|
function stopTimer() {
|
|
325
|
-
if (
|
|
373
|
+
if (resetTimer === undefined)
|
|
326
374
|
return;
|
|
327
|
-
|
|
328
|
-
|
|
375
|
+
clearTimeout(resetTimer);
|
|
376
|
+
resetTimer = undefined;
|
|
329
377
|
}
|
|
330
|
-
function
|
|
331
|
-
|
|
378
|
+
function armTimer() {
|
|
379
|
+
stopTimer();
|
|
380
|
+
if (closed || stopping)
|
|
332
381
|
return;
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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()));
|
|
336
397
|
}
|
|
337
398
|
async function refuse(reason, provider = null) {
|
|
338
399
|
stopTimer();
|
|
@@ -358,7 +419,6 @@ export function composeRuntime(options) {
|
|
|
358
419
|
}
|
|
359
420
|
for (const key of keys) {
|
|
360
421
|
invalidModels.add(key);
|
|
361
|
-
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
362
422
|
models.set(key, revoked);
|
|
363
423
|
liveModels.delete(key);
|
|
364
424
|
}
|
|
@@ -371,10 +431,7 @@ export function composeRuntime(options) {
|
|
|
371
431
|
model: revoked,
|
|
372
432
|
selectionModel: revoked,
|
|
373
433
|
};
|
|
374
|
-
|
|
375
|
-
replicas.set(locator, read.kind === 'staged'
|
|
376
|
-
? replicaOf(read.confirmed, before.queue, rewriting(), before)
|
|
377
|
-
: { ...before, confirmed: { ...before.confirmed, record } });
|
|
434
|
+
replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
|
|
378
435
|
}
|
|
379
436
|
rooms.set(locator, {
|
|
380
437
|
...room,
|
|
@@ -391,7 +448,8 @@ export function composeRuntime(options) {
|
|
|
391
448
|
}
|
|
392
449
|
finally {
|
|
393
450
|
refusal = {
|
|
394
|
-
reason,
|
|
451
|
+
code: reason,
|
|
452
|
+
retryable: false,
|
|
395
453
|
reauthorizationOffered: reason === 'revoked-authorization',
|
|
396
454
|
refusal: provider,
|
|
397
455
|
};
|
|
@@ -404,15 +462,14 @@ export function composeRuntime(options) {
|
|
|
404
462
|
rooms.set(room.locator, {
|
|
405
463
|
name: room.name,
|
|
406
464
|
locator: room.locator,
|
|
465
|
+
handle: room.handle,
|
|
407
466
|
operations: room.operations,
|
|
408
467
|
replica: room.replica,
|
|
409
|
-
attachedModel: room.attachedModel,
|
|
410
468
|
model: modelTypes(modelOf(room.locator)),
|
|
411
469
|
typedSelection: selection === null
|
|
412
470
|
? null
|
|
413
471
|
: {
|
|
414
472
|
roots: selection.roots,
|
|
415
|
-
attachedModel: selection.attachedModel,
|
|
416
473
|
model: modelTypes(modelOf(room.locator, 'typed-selection')),
|
|
417
474
|
},
|
|
418
475
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
@@ -429,9 +486,6 @@ export function composeRuntime(options) {
|
|
|
429
486
|
discovered.clear();
|
|
430
487
|
for (const room of listed)
|
|
431
488
|
discovered.set(room.locator, room);
|
|
432
|
-
for (const locator of wanted.keys())
|
|
433
|
-
if (!listedLocators.has(locator))
|
|
434
|
-
wanted.delete(locator);
|
|
435
489
|
await dropUnlisted(listedLocators, local);
|
|
436
490
|
for (const room of listed) {
|
|
437
491
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
@@ -445,7 +499,7 @@ export function composeRuntime(options) {
|
|
|
445
499
|
emitRooms();
|
|
446
500
|
}
|
|
447
501
|
function unlistedHeld(listed) {
|
|
448
|
-
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));
|
|
449
503
|
}
|
|
450
504
|
async function dropUnlisted(listed, local) {
|
|
451
505
|
if (unlistedHeld(listed).length === 0)
|
|
@@ -464,7 +518,9 @@ export function composeRuntime(options) {
|
|
|
464
518
|
replicas.delete(locator);
|
|
465
519
|
rooms.delete(locator);
|
|
466
520
|
roomDrafts.delete(locator);
|
|
521
|
+
transfers.delete(locator);
|
|
467
522
|
unreadable.delete(locator);
|
|
523
|
+
stale.delete(locator);
|
|
468
524
|
scopedRefusals.delete(locator);
|
|
469
525
|
sourceRefusals.delete(locator);
|
|
470
526
|
declarationRefusals.delete(locator);
|
|
@@ -473,7 +529,6 @@ export function composeRuntime(options) {
|
|
|
473
529
|
models.delete(key);
|
|
474
530
|
liveModels.delete(key);
|
|
475
531
|
invalidModels.delete(key);
|
|
476
|
-
modelEpoch.delete(key);
|
|
477
532
|
}
|
|
478
533
|
journal.record(`room unlisted ${locator}`);
|
|
479
534
|
}
|
|
@@ -486,7 +541,6 @@ export function composeRuntime(options) {
|
|
|
486
541
|
if (prior !== undefined &&
|
|
487
542
|
prior.status !== 'unavailable' &&
|
|
488
543
|
(persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
|
|
489
|
-
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
490
544
|
invalidModels.add(key);
|
|
491
545
|
}
|
|
492
546
|
const model = persisted.status === 'unavailable' ||
|
|
@@ -501,7 +555,7 @@ export function composeRuntime(options) {
|
|
|
501
555
|
return model;
|
|
502
556
|
}
|
|
503
557
|
async function hydrate(opened) {
|
|
504
|
-
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([
|
|
505
559
|
opened.all(),
|
|
506
560
|
opened.queues(),
|
|
507
561
|
opened.roomDrafts(),
|
|
@@ -511,9 +565,15 @@ export function composeRuntime(options) {
|
|
|
511
565
|
opened.lastCompleted(),
|
|
512
566
|
opened.providerLimits(),
|
|
513
567
|
opened.capabilities(),
|
|
568
|
+
opened.member(),
|
|
569
|
+
opened.transfers(),
|
|
514
570
|
]);
|
|
571
|
+
transfers.clear();
|
|
572
|
+
for (const record of staged)
|
|
573
|
+
transfers.set(record.locator, record);
|
|
515
574
|
limits = declared;
|
|
516
575
|
capabilities = named;
|
|
576
|
+
member = known;
|
|
517
577
|
roomDrafts.clear();
|
|
518
578
|
for (const record of kept)
|
|
519
579
|
rememberRoomDraft(record);
|
|
@@ -533,15 +593,8 @@ export function composeRuntime(options) {
|
|
|
533
593
|
const model = restored(modelKey(record.locator, 'collection'), record.model);
|
|
534
594
|
const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
|
|
535
595
|
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));
|
|
596
|
+
const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
|
|
597
|
+
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
|
|
545
598
|
const existing = rooms.get(record.locator);
|
|
546
599
|
if (existing === undefined)
|
|
547
600
|
list(record, false);
|
|
@@ -552,21 +605,15 @@ export function composeRuntime(options) {
|
|
|
552
605
|
attachments: record.attachments,
|
|
553
606
|
operations: record.operations,
|
|
554
607
|
replica: record.replica,
|
|
555
|
-
attachedModel: record.attachedModel,
|
|
556
608
|
model: modelTypes(model),
|
|
557
609
|
typedSelection: record.typedSelection === null
|
|
558
610
|
? null
|
|
559
611
|
: {
|
|
560
612
|
roots: record.typedSelection.roots,
|
|
561
|
-
attachedModel: record.typedSelection.attachedModel,
|
|
562
613
|
model: modelTypes(selectionModel),
|
|
563
614
|
},
|
|
564
615
|
});
|
|
565
616
|
}
|
|
566
|
-
if (read.kind === 'refused') {
|
|
567
|
-
refusedSource(record, read);
|
|
568
|
-
declarationRefusals.add(record.locator);
|
|
569
|
-
}
|
|
570
617
|
}
|
|
571
618
|
return {
|
|
572
619
|
lastCompleted,
|
|
@@ -579,6 +626,8 @@ export function composeRuntime(options) {
|
|
|
579
626
|
applyChangeSet(read);
|
|
580
627
|
if (replicas.size > 0)
|
|
581
628
|
emitRooms();
|
|
629
|
+
if (member !== null)
|
|
630
|
+
emitMember();
|
|
582
631
|
for (const locator of locators)
|
|
583
632
|
emitQueue(locator);
|
|
584
633
|
publishRoomDrafts(false);
|
|
@@ -626,10 +675,14 @@ export function composeRuntime(options) {
|
|
|
626
675
|
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
627
676
|
}
|
|
628
677
|
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
629
|
-
const withdrawn = withdrawals(confirmed, queue, withdrawable,
|
|
678
|
+
const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
|
|
630
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
|
+
}));
|
|
631
684
|
const returning = [
|
|
632
|
-
...
|
|
685
|
+
...[...unqueued].reverse(),
|
|
633
686
|
...(settling.leaving === undefined ? [] : [settling.leaving]),
|
|
634
687
|
];
|
|
635
688
|
const written = new Map();
|
|
@@ -650,6 +703,7 @@ export function composeRuntime(options) {
|
|
|
650
703
|
await local.write({
|
|
651
704
|
next: confirmed.record,
|
|
652
705
|
previous: before?.confirmed.record ?? null,
|
|
706
|
+
...(settling.stored === undefined ? {} : { stored: settling.stored }),
|
|
653
707
|
queue: kept,
|
|
654
708
|
previousQueue: before?.queue ?? [],
|
|
655
709
|
roomDrafts: [...written.values()],
|
|
@@ -660,44 +714,80 @@ export function composeRuntime(options) {
|
|
|
660
714
|
}
|
|
661
715
|
for (const record of written.values())
|
|
662
716
|
rememberRoomDraft(record);
|
|
663
|
-
for (const batch of
|
|
664
|
-
|
|
665
|
-
|
|
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));
|
|
666
722
|
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
667
723
|
room: settling.room,
|
|
668
724
|
queue: kept.length !== (before?.queue.length ?? 0),
|
|
669
725
|
drafts: [...written.keys()],
|
|
670
726
|
});
|
|
671
727
|
}
|
|
672
|
-
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;
|
|
673
761
|
return exclusive(async () => {
|
|
674
762
|
if (!continuing())
|
|
675
|
-
return;
|
|
763
|
+
return true;
|
|
676
764
|
const prior = replicas.get(room.locator);
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
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);
|
|
684
771
|
const queue = prior?.queue ?? [];
|
|
685
|
-
const roomVersion =
|
|
772
|
+
const roomVersion = confirmed.record.roomVersion;
|
|
686
773
|
const accepted = queue.filter((batch) => {
|
|
687
774
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
688
775
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
689
776
|
});
|
|
690
|
-
const set = await settle(local, room.locator, prior,
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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);
|
|
696
785
|
sourceRefusals.delete(room.locator);
|
|
697
786
|
declarationRefusals.delete(room.locator);
|
|
698
787
|
list(room, true);
|
|
699
788
|
publishChangeSet(set);
|
|
700
789
|
moveTo({ ...status, ...attention() });
|
|
790
|
+
return true;
|
|
701
791
|
});
|
|
702
792
|
}
|
|
703
793
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
@@ -705,28 +795,29 @@ export function composeRuntime(options) {
|
|
|
705
795
|
const replica = replicas.get(locator);
|
|
706
796
|
const room = rooms.get(locator);
|
|
707
797
|
if (replica === undefined || room === undefined) {
|
|
708
|
-
throw
|
|
798
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
709
799
|
}
|
|
710
800
|
const saved = [];
|
|
711
801
|
for (const { path, revision } of named) {
|
|
712
802
|
const record = roomDrafts.get(locator)?.get(path);
|
|
713
803
|
if (record === undefined || record.revision !== revision) {
|
|
714
|
-
return { outcome: 'refused',
|
|
804
|
+
return { outcome: 'refused', code: 'stale', retryable: false, file: path };
|
|
715
805
|
}
|
|
716
806
|
if (!saved.some((draft) => draft.path === path))
|
|
717
807
|
saved.push({ path, parts: record.parts });
|
|
718
808
|
}
|
|
719
809
|
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
|
|
720
810
|
if (changed.length > 0)
|
|
721
|
-
return { outcome: 'refused',
|
|
811
|
+
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
722
812
|
const parts = saved.map(({ path, parts }) => changesOf(path, parts));
|
|
723
813
|
const built = buildBatch({
|
|
814
|
+
local: localSaves,
|
|
724
815
|
baseRoomVersion: replica.confirmed.record.roomVersion,
|
|
725
816
|
composition: compositionFor(replica.confirmed.record),
|
|
726
817
|
operations: room.operations,
|
|
727
818
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
728
819
|
providerLimits: limits,
|
|
729
|
-
|
|
820
|
+
handle: room.handle,
|
|
730
821
|
visible: replica.visible,
|
|
731
822
|
changes: parts.flatMap(({ changes }) => changes),
|
|
732
823
|
bases: parts.flatMap(({ bases }) => bases),
|
|
@@ -747,18 +838,20 @@ export function composeRuntime(options) {
|
|
|
747
838
|
});
|
|
748
839
|
}
|
|
749
840
|
catch {
|
|
750
|
-
return { outcome: 'refused',
|
|
841
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
751
842
|
}
|
|
752
843
|
pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
|
|
844
|
+
if (!localSaves)
|
|
845
|
+
recorded = false;
|
|
753
846
|
for (const path of consumed)
|
|
754
847
|
forgetRoomDraft(locator, path);
|
|
755
|
-
replicas.set(locator, replicaOf(replica.confirmed, batches,
|
|
848
|
+
replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
|
|
756
849
|
publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
|
|
757
850
|
queue: true,
|
|
758
851
|
drafts: consumed,
|
|
759
852
|
}));
|
|
760
|
-
moveTo({ ...saving(status), ...attention() });
|
|
761
|
-
|
|
853
|
+
moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
|
|
854
|
+
submitLater(locator);
|
|
762
855
|
return { outcome: 'saved', identifier: built.batch.id };
|
|
763
856
|
}
|
|
764
857
|
async function mark(local, locator, identifier, submission, lost) {
|
|
@@ -785,16 +878,15 @@ export function composeRuntime(options) {
|
|
|
785
878
|
}
|
|
786
879
|
async function readHistory(locator, path, query = {}) {
|
|
787
880
|
if (closed || refusal !== null)
|
|
788
|
-
return
|
|
881
|
+
return UNCERTAIN;
|
|
789
882
|
try {
|
|
790
883
|
await ensureAuthorization();
|
|
791
884
|
}
|
|
792
885
|
catch {
|
|
793
|
-
return
|
|
794
|
-
}
|
|
795
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
796
|
-
return { kind: 'transport-uncertainty' };
|
|
886
|
+
return UNCERTAIN;
|
|
797
887
|
}
|
|
888
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
889
|
+
return UNCERTAIN;
|
|
798
890
|
const transfer = ['transfer', 'history', locator, path];
|
|
799
891
|
if (query.revision !== undefined)
|
|
800
892
|
transfer.push(query.revision);
|
|
@@ -816,17 +908,17 @@ export function composeRuntime(options) {
|
|
|
816
908
|
}
|
|
817
909
|
if (result.kind === 'revoked-authorization')
|
|
818
910
|
await refuse('revoked-authorization');
|
|
819
|
-
return result;
|
|
911
|
+
return historyAnswer(result);
|
|
820
912
|
}
|
|
821
913
|
catch {
|
|
822
914
|
journal.record('outcome history transport-uncertainty');
|
|
823
|
-
return
|
|
915
|
+
return UNCERTAIN;
|
|
824
916
|
}
|
|
825
917
|
}
|
|
826
|
-
function
|
|
918
|
+
function declaredAgents() {
|
|
827
919
|
for (const capability of capabilities) {
|
|
828
|
-
if (capability.
|
|
829
|
-
return capability.
|
|
920
|
+
if (capability.conversationAgents !== undefined)
|
|
921
|
+
return capability.conversationAgents;
|
|
830
922
|
}
|
|
831
923
|
return undefined;
|
|
832
924
|
}
|
|
@@ -842,23 +934,20 @@ export function composeRuntime(options) {
|
|
|
842
934
|
async function invokeAction(name, input) {
|
|
843
935
|
await ready;
|
|
844
936
|
if (closed || refusal !== null)
|
|
845
|
-
return
|
|
937
|
+
return UNCERTAIN;
|
|
846
938
|
if (declaredAction(name) === undefined)
|
|
847
|
-
return
|
|
939
|
+
return undeclared(name);
|
|
848
940
|
try {
|
|
849
941
|
await ensureAuthorization();
|
|
850
942
|
}
|
|
851
943
|
catch {
|
|
852
|
-
return
|
|
853
|
-
}
|
|
854
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
855
|
-
return { kind: 'transport-uncertainty' };
|
|
944
|
+
return UNCERTAIN;
|
|
856
945
|
}
|
|
946
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
947
|
+
return UNCERTAIN;
|
|
857
948
|
journal.record(`transfer action ${name}`);
|
|
858
949
|
try {
|
|
859
950
|
const result = await options.adapter.action(authorization, { name, input });
|
|
860
|
-
if (result.kind === 'action' && result.at !== undefined)
|
|
861
|
-
want(result.at);
|
|
862
951
|
if (result.kind === 'scoped-refusal') {
|
|
863
952
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
864
953
|
if (room !== undefined)
|
|
@@ -869,11 +958,11 @@ export function composeRuntime(options) {
|
|
|
869
958
|
journal.record(`outcome action ${result.kind}`);
|
|
870
959
|
if (result.kind === 'rejected')
|
|
871
960
|
journal.record(`refusal rejected ${result.code}`);
|
|
872
|
-
return result;
|
|
961
|
+
return calledResult(result);
|
|
873
962
|
}
|
|
874
963
|
catch {
|
|
875
964
|
journal.record('outcome action transport-uncertainty');
|
|
876
|
-
return
|
|
965
|
+
return UNCERTAIN;
|
|
877
966
|
}
|
|
878
967
|
}
|
|
879
968
|
function continuing() {
|
|
@@ -890,32 +979,11 @@ export function composeRuntime(options) {
|
|
|
890
979
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
891
980
|
!options.adapter.reached(locator, roomVersion, written));
|
|
892
981
|
}
|
|
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) {
|
|
982
|
+
async function pullRefresh(authorized, confirmed) {
|
|
915
983
|
if (!continuing())
|
|
916
984
|
return unconfirmed;
|
|
917
985
|
try {
|
|
918
|
-
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);
|
|
919
987
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
920
988
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
921
989
|
return result;
|
|
@@ -925,14 +993,33 @@ export function composeRuntime(options) {
|
|
|
925
993
|
throw error;
|
|
926
994
|
}
|
|
927
995
|
}
|
|
928
|
-
|
|
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) {
|
|
929
1016
|
if (!continuing())
|
|
930
1017
|
return unconfirmed;
|
|
931
1018
|
try {
|
|
932
1019
|
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
933
1020
|
collection: modelOf(room.locator),
|
|
934
1021
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
935
|
-
}, continuing);
|
|
1022
|
+
}, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
|
|
936
1023
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
937
1024
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
938
1025
|
return result;
|
|
@@ -945,35 +1032,46 @@ export function composeRuntime(options) {
|
|
|
945
1032
|
async function advance(room, local, authorized) {
|
|
946
1033
|
const held = replicas.get(room.locator);
|
|
947
1034
|
if (held !== undefined) {
|
|
948
|
-
const refreshed = await pullRefresh(authorized, held.confirmed
|
|
1035
|
+
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
949
1036
|
if (!continuing())
|
|
950
1037
|
return unconfirmed;
|
|
951
1038
|
if (refreshed.kind === 'still-current') {
|
|
1039
|
+
stale.delete(room.locator);
|
|
952
1040
|
if (sourceRefusals.has(room.locator))
|
|
953
|
-
return
|
|
1041
|
+
return null;
|
|
954
1042
|
if (unreadable.delete(room.locator)) {
|
|
1043
|
+
scopedRefusals.delete(room.locator);
|
|
955
1044
|
list(room, true);
|
|
956
1045
|
emitRooms();
|
|
957
1046
|
}
|
|
958
1047
|
return null;
|
|
959
1048
|
}
|
|
960
1049
|
if (refreshed.kind === 'advanced') {
|
|
961
|
-
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));
|
|
962
1057
|
return null;
|
|
963
1058
|
}
|
|
964
1059
|
if (refreshed.kind !== 'beyond-horizon')
|
|
965
1060
|
return refreshed;
|
|
966
1061
|
}
|
|
967
|
-
const bootstrapped = await pullBootstrap(authorized, room);
|
|
1062
|
+
const bootstrapped = await pullBootstrap(authorized, room, local);
|
|
968
1063
|
if (!continuing())
|
|
969
1064
|
return unconfirmed;
|
|
970
1065
|
if (bootstrapped.kind !== 'staged')
|
|
971
1066
|
return bootstrapped;
|
|
972
|
-
await publish(local, room, bootstrapped.confirmed);
|
|
1067
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
973
1068
|
return null;
|
|
974
1069
|
}
|
|
975
1070
|
async function withhold(withheld, room, local, progress) {
|
|
976
|
-
|
|
1071
|
+
if (withheld.kind === 'refused')
|
|
1072
|
+
stale.delete(room.locator);
|
|
1073
|
+
else
|
|
1074
|
+
stale.add(room.locator);
|
|
977
1075
|
if (withheld.kind === 'scoped-refusal') {
|
|
978
1076
|
await scoped(withheld, room, local);
|
|
979
1077
|
}
|
|
@@ -998,6 +1096,7 @@ export function composeRuntime(options) {
|
|
|
998
1096
|
function refusedSource(room, { refusal }) {
|
|
999
1097
|
sourceRefusals.set(room.locator, {
|
|
1000
1098
|
code: refusal.rule,
|
|
1099
|
+
retryable: false,
|
|
1001
1100
|
fields: {
|
|
1002
1101
|
file: refusal.file,
|
|
1003
1102
|
...(refusal.location === undefined ? {} : { location: refusal.location }),
|
|
@@ -1012,52 +1111,46 @@ export function composeRuntime(options) {
|
|
|
1012
1111
|
}
|
|
1013
1112
|
function leaving(batch, result) {
|
|
1014
1113
|
if (result.kind === 'expired')
|
|
1015
|
-
return { batch, returned:
|
|
1016
|
-
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
|
+
};
|
|
1017
1124
|
}
|
|
1018
1125
|
function logAccepted(locator, submitted, accepted) {
|
|
1019
1126
|
journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
|
|
1020
1127
|
(accepted.author === undefined ? '' : ` author ${accepted.author}`) +
|
|
1021
1128
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1022
1129
|
}
|
|
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) {
|
|
1130
|
+
async function adoptAccepted(room, local, submitted, accepted) {
|
|
1041
1131
|
if (!continuing())
|
|
1042
1132
|
return unconfirmed;
|
|
1043
1133
|
const current = replicas.get(room.locator);
|
|
1044
1134
|
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
1045
1135
|
return null;
|
|
1046
1136
|
}
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
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);
|
|
1057
1150
|
return applied;
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
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);
|
|
1061
1154
|
unreadable.delete(room.locator);
|
|
1062
1155
|
sourceRefusals.delete(room.locator);
|
|
1063
1156
|
declarationRefusals.delete(room.locator);
|
|
@@ -1066,17 +1159,8 @@ export function composeRuntime(options) {
|
|
|
1066
1159
|
moveTo({ ...status, ...attention() });
|
|
1067
1160
|
return null;
|
|
1068
1161
|
}
|
|
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));
|
|
1162
|
+
async function adopted(room, local, submitted, accepted) {
|
|
1163
|
+
const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
|
|
1080
1164
|
if (failed === null)
|
|
1081
1165
|
return true;
|
|
1082
1166
|
if (failed.kind === 'refused' && continuing())
|
|
@@ -1089,10 +1173,8 @@ export function composeRuntime(options) {
|
|
|
1089
1173
|
const head = replica?.queue[0];
|
|
1090
1174
|
if (replica === undefined || head === undefined)
|
|
1091
1175
|
return;
|
|
1092
|
-
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1093
|
-
progress.complete = false;
|
|
1176
|
+
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1094
1177
|
return;
|
|
1095
|
-
}
|
|
1096
1178
|
const state = pendings.get(head.id);
|
|
1097
1179
|
const { submission, lost: earlier } = state ?? {
|
|
1098
1180
|
submission: { state: 'waiting' },
|
|
@@ -1120,6 +1202,7 @@ export function composeRuntime(options) {
|
|
|
1120
1202
|
return;
|
|
1121
1203
|
}
|
|
1122
1204
|
const opensBefore = openSignals;
|
|
1205
|
+
const answered = awaitingAnswer(room.locator);
|
|
1123
1206
|
try {
|
|
1124
1207
|
result = await options.adapter.mutate(authorized, {
|
|
1125
1208
|
locator: room.locator,
|
|
@@ -1128,11 +1211,14 @@ export function composeRuntime(options) {
|
|
|
1128
1211
|
}, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
|
|
1129
1212
|
}
|
|
1130
1213
|
catch {
|
|
1214
|
+
answered();
|
|
1131
1215
|
progress.complete = false;
|
|
1132
1216
|
if (continuing())
|
|
1133
1217
|
progress.reachable = false;
|
|
1134
1218
|
return;
|
|
1135
1219
|
}
|
|
1220
|
+
if (result.kind !== 'accepted' || !continuing())
|
|
1221
|
+
answered();
|
|
1136
1222
|
if (!continuing())
|
|
1137
1223
|
return;
|
|
1138
1224
|
if (result.kind === 'transport-uncertainty') {
|
|
@@ -1185,6 +1271,7 @@ export function composeRuntime(options) {
|
|
|
1185
1271
|
const reset = result.reset;
|
|
1186
1272
|
progress.complete = false;
|
|
1187
1273
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1274
|
+
armTimer();
|
|
1188
1275
|
return;
|
|
1189
1276
|
}
|
|
1190
1277
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
@@ -1197,7 +1284,8 @@ export function composeRuntime(options) {
|
|
|
1197
1284
|
return;
|
|
1198
1285
|
}
|
|
1199
1286
|
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)
|
|
1287
|
+
leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
|
|
1288
|
+
(result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
|
|
1201
1289
|
? { ...left, returned: null }
|
|
1202
1290
|
: left,
|
|
1203
1291
|
});
|
|
@@ -1211,95 +1299,33 @@ export function composeRuntime(options) {
|
|
|
1211
1299
|
return withhold(withheld, room, local, progress);
|
|
1212
1300
|
continue;
|
|
1213
1301
|
}
|
|
1214
|
-
await
|
|
1215
|
-
|
|
1216
|
-
if (from === undefined)
|
|
1217
|
-
return;
|
|
1218
|
-
const pulled = await pullRefresh(authorized, from, reachOf(room.locator));
|
|
1219
|
-
if (!continuing())
|
|
1220
|
-
return;
|
|
1221
|
-
if (pulled.kind === 'revoked-authorization') {
|
|
1222
|
-
await refuse('revoked-authorization');
|
|
1223
|
-
return;
|
|
1224
|
-
}
|
|
1225
|
-
if (pulled.kind === 'scoped-refusal') {
|
|
1226
|
-
await scoped(pulled, room, local);
|
|
1227
|
-
progress.complete = false;
|
|
1228
|
-
return;
|
|
1229
|
-
}
|
|
1230
|
-
if (pulled.kind === 'still-current' || pulled.kind === 'advanced') {
|
|
1231
|
-
const fresh = pulled.kind === 'advanced';
|
|
1232
|
-
if (!(await accepting(room, local, head, result, fresh ? pulled.confirmed : from, fresh))) {
|
|
1233
|
-
progress.complete = false;
|
|
1234
|
-
return;
|
|
1235
|
-
}
|
|
1236
|
-
continue;
|
|
1237
|
-
}
|
|
1238
|
-
if (pulled.kind === 'beyond-horizon') {
|
|
1239
|
-
const bootstrapped = await pullBootstrap(authorized, room);
|
|
1240
|
-
if (!continuing())
|
|
1241
|
-
return;
|
|
1242
|
-
if (bootstrapped.kind === 'revoked-authorization') {
|
|
1243
|
-
await refuse('revoked-authorization');
|
|
1244
|
-
return;
|
|
1245
|
-
}
|
|
1246
|
-
if (bootstrapped.kind === 'scoped-refusal') {
|
|
1247
|
-
await scoped(bootstrapped, room, local);
|
|
1248
|
-
progress.complete = false;
|
|
1249
|
-
return;
|
|
1250
|
-
}
|
|
1251
|
-
if (bootstrapped.kind === 'staged') {
|
|
1252
|
-
if (!(await accepting(room, local, head, result, bootstrapped.confirmed, true))) {
|
|
1253
|
-
progress.complete = false;
|
|
1254
|
-
return;
|
|
1255
|
-
}
|
|
1256
|
-
continue;
|
|
1257
|
-
}
|
|
1302
|
+
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1303
|
+
if (!kept) {
|
|
1258
1304
|
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
1305
|
return;
|
|
1271
1306
|
}
|
|
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
1307
|
}
|
|
1289
|
-
|
|
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
|
+
};
|
|
1290
1320
|
}
|
|
1291
1321
|
async function retainModel(local, room, attached, next) {
|
|
1292
1322
|
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
1323
|
const previous = modelOf(room.locator, attached);
|
|
1297
1324
|
const heldBefore = replicas.get(room.locator)?.confirmed.record;
|
|
1298
1325
|
if (JSON.stringify(previous) === JSON.stringify(next) &&
|
|
1299
1326
|
(heldBefore === undefined ||
|
|
1300
1327
|
(JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
|
|
1301
1328
|
JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
|
|
1302
|
-
JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) &&
|
|
1303
1329
|
JSON.stringify(heldBefore.typedSelection) ===
|
|
1304
1330
|
JSON.stringify(room.typedSelection ?? null))))
|
|
1305
1331
|
return;
|
|
@@ -1310,20 +1336,17 @@ export function composeRuntime(options) {
|
|
|
1310
1336
|
await exclusive(async () => {
|
|
1311
1337
|
if (!continuing())
|
|
1312
1338
|
return;
|
|
1313
|
-
if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0))
|
|
1314
|
-
return;
|
|
1315
1339
|
const held = replicas.get(room.locator);
|
|
1316
1340
|
if (held !== undefined) {
|
|
1317
1341
|
const declared = {
|
|
1318
1342
|
...held.confirmed.record,
|
|
1319
1343
|
operations: room.operations,
|
|
1320
1344
|
replica: room.replica,
|
|
1321
|
-
attachedModel: room.attachedModel,
|
|
1322
1345
|
typedSelection: room.typedSelection ?? null,
|
|
1323
1346
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
1324
1347
|
};
|
|
1325
1348
|
const reclassified = sameMembership(declared, held.confirmed.record)
|
|
1326
|
-
?
|
|
1349
|
+
? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
|
|
1327
1350
|
: readReclassified(declared, held.confirmed);
|
|
1328
1351
|
const record = reclassified.kind === 'staged'
|
|
1329
1352
|
? declared
|
|
@@ -1331,14 +1354,16 @@ export function composeRuntime(options) {
|
|
|
1331
1354
|
...declared,
|
|
1332
1355
|
typedSelection: held.confirmed.record.typedSelection,
|
|
1333
1356
|
};
|
|
1334
|
-
const read = reclassified.kind === 'deferred'
|
|
1357
|
+
const read = reclassified.kind === 'deferred'
|
|
1358
|
+
? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
|
|
1359
|
+
: reclassified;
|
|
1335
1360
|
await local.write({
|
|
1336
1361
|
next: record,
|
|
1337
1362
|
previous: held.confirmed.record,
|
|
1338
1363
|
queue: held.queue,
|
|
1339
1364
|
});
|
|
1340
1365
|
if (read.kind === 'staged') {
|
|
1341
|
-
replicas.set(room.locator, replicaOf(read.confirmed, held.queue,
|
|
1366
|
+
replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
|
|
1342
1367
|
if (declarationRefusals.delete(room.locator)) {
|
|
1343
1368
|
sourceRefusals.delete(room.locator);
|
|
1344
1369
|
unreadable.delete(room.locator);
|
|
@@ -1362,79 +1387,12 @@ export function composeRuntime(options) {
|
|
|
1362
1387
|
publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
|
|
1363
1388
|
});
|
|
1364
1389
|
}
|
|
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
1390
|
async function scoped(result, room, local) {
|
|
1437
|
-
scopedRefusals.set(room.locator, {
|
|
1391
|
+
scopedRefusals.set(room.locator, {
|
|
1392
|
+
code: result.code,
|
|
1393
|
+
fields: result.fields,
|
|
1394
|
+
retryable: result.retryable,
|
|
1395
|
+
});
|
|
1438
1396
|
let scopedAway = room;
|
|
1439
1397
|
if (result.scope === 'model')
|
|
1440
1398
|
await retainModel(local, room, 'collection', {
|
|
@@ -1460,14 +1418,36 @@ export function composeRuntime(options) {
|
|
|
1460
1418
|
emitRooms();
|
|
1461
1419
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
1462
1420
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
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) => {
|
|
1466
1440
|
const outcome = prepared[index];
|
|
1467
1441
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1468
1442
|
});
|
|
1469
|
-
|
|
1470
|
-
|
|
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');
|
|
1471
1451
|
if (failed !== undefined)
|
|
1472
1452
|
throw failed.reason;
|
|
1473
1453
|
}
|
|
@@ -1484,11 +1464,8 @@ export function composeRuntime(options) {
|
|
|
1484
1464
|
progress.complete = false;
|
|
1485
1465
|
return false;
|
|
1486
1466
|
}
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
acquireModel(local, room, authorization, null),
|
|
1490
|
-
...(selection === undefined ? [] : [acquireModel(local, room, authorization, selection)]),
|
|
1491
|
-
]);
|
|
1467
|
+
if (prior !== undefined)
|
|
1468
|
+
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1492
1469
|
if (discoveryPaused || authorization === undefined)
|
|
1493
1470
|
return false;
|
|
1494
1471
|
if (room.replica.admission !== 'admitted') {
|
|
@@ -1496,24 +1473,53 @@ export function composeRuntime(options) {
|
|
|
1496
1473
|
progress.complete = false;
|
|
1497
1474
|
return false;
|
|
1498
1475
|
}
|
|
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
1476
|
return true;
|
|
1505
1477
|
}
|
|
1506
|
-
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) {
|
|
1507
1496
|
if (discoveryPaused || authorization === undefined)
|
|
1508
1497
|
return;
|
|
1498
|
+
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1499
|
+
await reload(room, local, progress);
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1509
1502
|
const withheld = await advance(room, local, authorization);
|
|
1510
1503
|
if (withheld !== null) {
|
|
1511
1504
|
await withhold(withheld, room, local, progress);
|
|
1512
1505
|
return;
|
|
1513
1506
|
}
|
|
1514
|
-
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);
|
|
1515
1521
|
}
|
|
1516
|
-
async function
|
|
1522
|
+
async function discoverRooms(local, progress, attempt) {
|
|
1517
1523
|
if (closed || authorization === undefined) {
|
|
1518
1524
|
notifyReady();
|
|
1519
1525
|
return;
|
|
@@ -1539,12 +1545,17 @@ export function composeRuntime(options) {
|
|
|
1539
1545
|
}
|
|
1540
1546
|
}
|
|
1541
1547
|
progress.reachable = false;
|
|
1548
|
+
progress.complete = false;
|
|
1542
1549
|
notifyReady();
|
|
1543
1550
|
return;
|
|
1544
1551
|
}
|
|
1545
1552
|
if (result.kind === 'contract-unavailable') {
|
|
1546
1553
|
journal.record(`connection contract unavailable ${result.code}`);
|
|
1547
|
-
await refuse('missing-required-guarantee', {
|
|
1554
|
+
await refuse('missing-required-guarantee', {
|
|
1555
|
+
code: result.code,
|
|
1556
|
+
fields: result.fields,
|
|
1557
|
+
retryable: result.retryable,
|
|
1558
|
+
});
|
|
1548
1559
|
notifyReady();
|
|
1549
1560
|
return;
|
|
1550
1561
|
}
|
|
@@ -1553,17 +1564,6 @@ export function composeRuntime(options) {
|
|
|
1553
1564
|
notifyReady();
|
|
1554
1565
|
return;
|
|
1555
1566
|
}
|
|
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
1567
|
for (const room of result.rooms) {
|
|
1568
1568
|
if (room.operations === undefined ||
|
|
1569
1569
|
!['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
@@ -1571,8 +1571,7 @@ export function composeRuntime(options) {
|
|
|
1571
1571
|
room.replica === undefined ||
|
|
1572
1572
|
typeof room.replica.selection !== 'string' ||
|
|
1573
1573
|
room.replica.selection.length === 0 ||
|
|
1574
|
-
!['admitted', 'unavailable'].includes(room.replica.admission)
|
|
1575
|
-
!declaredIdentity(room.attachedModel)) {
|
|
1574
|
+
!['admitted', 'unavailable'].includes(room.replica.admission)) {
|
|
1576
1575
|
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
1577
1576
|
await refuse('missing-required-guarantee');
|
|
1578
1577
|
notifyReady();
|
|
@@ -1585,14 +1584,6 @@ export function composeRuntime(options) {
|
|
|
1585
1584
|
return;
|
|
1586
1585
|
}
|
|
1587
1586
|
}
|
|
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
1587
|
if (refusal !== null) {
|
|
1597
1588
|
refusal = null;
|
|
1598
1589
|
emitRefusal();
|
|
@@ -1600,26 +1591,19 @@ export function composeRuntime(options) {
|
|
|
1600
1591
|
const previousCapabilities = capabilities;
|
|
1601
1592
|
capabilities = result.capabilities;
|
|
1602
1593
|
notifyReady();
|
|
1603
|
-
if (declared.has('room-version-notifications'))
|
|
1604
|
-
observeRoomVersions();
|
|
1605
1594
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1606
1595
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
1607
1596
|
limits = result.providerLimits;
|
|
1608
1597
|
await local.declare(limits, capabilities);
|
|
1609
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
|
+
}
|
|
1610
1605
|
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);
|
|
1606
|
+
await synchronize(result.rooms, local, progress, attempt);
|
|
1623
1607
|
}
|
|
1624
1608
|
async function ensureAuthorization() {
|
|
1625
1609
|
if (authorization !== undefined)
|
|
@@ -1633,64 +1617,262 @@ export function composeRuntime(options) {
|
|
|
1633
1617
|
throw error;
|
|
1634
1618
|
}
|
|
1635
1619
|
}
|
|
1636
|
-
|
|
1637
|
-
if (
|
|
1638
|
-
|
|
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');
|
|
1639
1674
|
return;
|
|
1640
1675
|
}
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
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)
|
|
1650
1690
|
return;
|
|
1651
|
-
|
|
1652
|
-
|
|
1691
|
+
if (progress === null) {
|
|
1692
|
+
await reloadExpired(expired);
|
|
1653
1693
|
return;
|
|
1654
|
-
if (progress.complete) {
|
|
1655
|
-
completedAt = new Date().toISOString();
|
|
1656
|
-
await local.complete(completedAt);
|
|
1657
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)));
|
|
1658
1723
|
}
|
|
1659
1724
|
catch {
|
|
1660
|
-
progress.
|
|
1661
|
-
|
|
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;
|
|
1662
1743
|
return;
|
|
1663
1744
|
}
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
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);
|
|
1667
1777
|
}
|
|
1668
1778
|
}
|
|
1669
|
-
async function
|
|
1779
|
+
async function finishOpening(progress) {
|
|
1670
1780
|
try {
|
|
1671
|
-
|
|
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;
|
|
1672
1796
|
}
|
|
1673
1797
|
finally {
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
queued = false;
|
|
1677
|
-
void enqueue();
|
|
1678
|
-
}
|
|
1798
|
+
complete = progress.complete;
|
|
1799
|
+
await rest(progress);
|
|
1679
1800
|
}
|
|
1680
1801
|
}
|
|
1681
|
-
function
|
|
1682
|
-
|
|
1683
|
-
if (path === undefined || withdrawNotifications !== undefined)
|
|
1802
|
+
async function rest(progress) {
|
|
1803
|
+
if (closed)
|
|
1684
1804
|
return;
|
|
1685
|
-
|
|
1686
|
-
|
|
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)
|
|
1687
1830
|
return;
|
|
1688
|
-
|
|
1689
|
-
|
|
1831
|
+
const room = discovered.get(locator);
|
|
1832
|
+
const held = replicas.get(locator);
|
|
1833
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
1690
1834
|
return;
|
|
1835
|
+
if (held !== undefined &&
|
|
1836
|
+
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
1837
|
+
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
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);
|
|
1691
1856
|
}
|
|
1692
|
-
|
|
1693
|
-
|
|
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);
|
|
1694
1876
|
});
|
|
1695
1877
|
}
|
|
1696
1878
|
function writesHeld() {
|
|
@@ -1711,14 +1893,17 @@ export function composeRuntime(options) {
|
|
|
1711
1893
|
listener(mode);
|
|
1712
1894
|
}
|
|
1713
1895
|
if (releasing)
|
|
1714
|
-
|
|
1896
|
+
submitAll();
|
|
1715
1897
|
}
|
|
1716
1898
|
function observeConversations() {
|
|
1717
1899
|
const stream = options.adapter.conversations;
|
|
1718
1900
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
1719
1901
|
return;
|
|
1720
1902
|
withdrawConversations = stream.observe((event) => {
|
|
1721
|
-
conversations.receive(event)
|
|
1903
|
+
if (conversations.receive(event)) {
|
|
1904
|
+
void ready.then(scheduleCurrency);
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1722
1907
|
if (event.kind !== 'gap' || conversations.current())
|
|
1723
1908
|
return;
|
|
1724
1909
|
show();
|
|
@@ -1729,7 +1914,7 @@ export function composeRuntime(options) {
|
|
|
1729
1914
|
if (currencyQueued || closed || stopping)
|
|
1730
1915
|
return;
|
|
1731
1916
|
currencyQueued = true;
|
|
1732
|
-
|
|
1917
|
+
void task(runCurrency);
|
|
1733
1918
|
}
|
|
1734
1919
|
async function runCurrency() {
|
|
1735
1920
|
currencyQueued = false;
|
|
@@ -1740,110 +1925,22 @@ export function composeRuntime(options) {
|
|
|
1740
1925
|
show();
|
|
1741
1926
|
return;
|
|
1742
1927
|
}
|
|
1743
|
-
const
|
|
1744
|
-
|
|
1745
|
-
moveTo(transferring(resting));
|
|
1928
|
+
const progress = { complete: false, reachable: !status.providerUnreachable };
|
|
1929
|
+
moveTo(transferring(status));
|
|
1746
1930
|
try {
|
|
1747
1931
|
await conversations.catchUp(progress);
|
|
1748
1932
|
}
|
|
1749
1933
|
catch {
|
|
1750
1934
|
progress.reachable = false;
|
|
1751
1935
|
}
|
|
1752
|
-
|
|
1753
|
-
if (!closed) {
|
|
1754
|
-
moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
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;
|
|
1936
|
+
await rest(progress);
|
|
1840
1937
|
}
|
|
1841
1938
|
function close() {
|
|
1842
1939
|
if (closed)
|
|
1843
1940
|
return;
|
|
1844
1941
|
closed = true;
|
|
1845
|
-
|
|
1846
|
-
|
|
1942
|
+
withdrawStream?.();
|
|
1943
|
+
withdrawStream = undefined;
|
|
1847
1944
|
withdrawConversations?.();
|
|
1848
1945
|
withdrawConversations = undefined;
|
|
1849
1946
|
withdrawServiceMode?.();
|
|
@@ -1858,6 +1955,7 @@ export function composeRuntime(options) {
|
|
|
1858
1955
|
storageLossListeners.clear();
|
|
1859
1956
|
statusListeners.clear();
|
|
1860
1957
|
serviceModeListeners.clear();
|
|
1958
|
+
memberListeners.clear();
|
|
1861
1959
|
readSide.close();
|
|
1862
1960
|
queueSubscriptions.clear();
|
|
1863
1961
|
roomDraftListeners.clear();
|
|
@@ -1867,6 +1965,7 @@ export function composeRuntime(options) {
|
|
|
1867
1965
|
stopping = true;
|
|
1868
1966
|
stopTimer();
|
|
1869
1967
|
await chain.catch(() => undefined);
|
|
1968
|
+
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
1870
1969
|
await mutations;
|
|
1871
1970
|
close();
|
|
1872
1971
|
}
|
|
@@ -1875,11 +1974,32 @@ export function composeRuntime(options) {
|
|
|
1875
1974
|
options.signal?.addEventListener('abort', close, { once: true });
|
|
1876
1975
|
if (options.signal?.aborted)
|
|
1877
1976
|
close();
|
|
1878
|
-
|
|
1977
|
+
observeStream();
|
|
1978
|
+
void openLocal().then(() => {
|
|
1979
|
+
if (capabilities.length > 0)
|
|
1980
|
+
notifyReady();
|
|
1981
|
+
}, () => {
|
|
1879
1982
|
notifyReady();
|
|
1880
1983
|
});
|
|
1881
|
-
|
|
1882
|
-
|
|
1984
|
+
if (options.adapter.roomStream === undefined)
|
|
1985
|
+
void sync('unstreamed');
|
|
1986
|
+
function planned(input, reach) {
|
|
1987
|
+
return planOf(input, roomFacts(reach), planReach(reach));
|
|
1988
|
+
}
|
|
1989
|
+
function watchPlan(plan, deliver) {
|
|
1990
|
+
materialize(reachedRooms(plan));
|
|
1991
|
+
return readSide.watch(plan, identityOf(plan), deliver);
|
|
1992
|
+
}
|
|
1993
|
+
async function searched(input, reach) {
|
|
1994
|
+
const plan = searchPlanOf(input);
|
|
1995
|
+
const selected = plan.documents === undefined && reach === null
|
|
1996
|
+
? null
|
|
1997
|
+
: (await rowsQuery(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
|
|
1998
|
+
if (selected === null)
|
|
1999
|
+
materialize(null);
|
|
2000
|
+
return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
|
|
2001
|
+
}
|
|
2002
|
+
const watch = (topic, input, deliver) => {
|
|
1883
2003
|
if (topic === 'room-drafts') {
|
|
1884
2004
|
const listener = { notify: deliver, ready: false };
|
|
1885
2005
|
roomDraftListeners.add(listener);
|
|
@@ -1900,6 +2020,8 @@ export function composeRuntime(options) {
|
|
|
1900
2020
|
if (topic === 'conversation') {
|
|
1901
2021
|
return conversations.watch(input[0], input[1], deliver);
|
|
1902
2022
|
}
|
|
2023
|
+
if (topic === 'conversations')
|
|
2024
|
+
return conversations.watchList(deliver);
|
|
1903
2025
|
if (topic === 'presentation') {
|
|
1904
2026
|
const listener = {
|
|
1905
2027
|
app: input[0],
|
|
@@ -1954,6 +2076,13 @@ export function composeRuntime(options) {
|
|
|
1954
2076
|
serviceModeListeners.delete(deliver);
|
|
1955
2077
|
};
|
|
1956
2078
|
}
|
|
2079
|
+
if (topic === 'member') {
|
|
2080
|
+
memberListeners.add(deliver);
|
|
2081
|
+
deliver(member);
|
|
2082
|
+
return () => {
|
|
2083
|
+
memberListeners.delete(deliver);
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
1957
2086
|
if (topic === 'queue') {
|
|
1958
2087
|
const queue = { locator: input[0], notify: deliver };
|
|
1959
2088
|
queueSubscriptions.add(queue);
|
|
@@ -1962,9 +2091,8 @@ export function composeRuntime(options) {
|
|
|
1962
2091
|
queueSubscriptions.delete(queue);
|
|
1963
2092
|
};
|
|
1964
2093
|
}
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
}
|
|
2094
|
+
return watchPlan(planned(input[0], null), deliver);
|
|
2095
|
+
};
|
|
1968
2096
|
const md = {
|
|
1969
2097
|
loaded() {
|
|
1970
2098
|
return openLocal().then(() => undefined);
|
|
@@ -1972,15 +2100,8 @@ export function composeRuntime(options) {
|
|
|
1972
2100
|
rooms() {
|
|
1973
2101
|
return listed();
|
|
1974
2102
|
},
|
|
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
|
-
},
|
|
2103
|
+
query: ((input) => rowsQuery(planned(input, null))),
|
|
2104
|
+
search: (input) => searched(input, null),
|
|
1984
2105
|
types(locator) {
|
|
1985
2106
|
return {
|
|
1986
2107
|
collection: modelTypes(modelOf(locator)),
|
|
@@ -2008,14 +2129,11 @@ export function composeRuntime(options) {
|
|
|
2008
2129
|
return exclusive(async () => {
|
|
2009
2130
|
const parts = readParts(input);
|
|
2010
2131
|
if (path === '' || normalize(path) !== path)
|
|
2011
|
-
throw
|
|
2132
|
+
throw runtimeError('room-draft-malformed');
|
|
2012
2133
|
const local = await openLocal();
|
|
2013
2134
|
const current = roomDrafts.get(room)?.get(path);
|
|
2014
2135
|
if ((current?.revision ?? null) !== revision) {
|
|
2015
|
-
return
|
|
2016
|
-
outcome: 'stale',
|
|
2017
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
2018
|
-
};
|
|
2136
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2019
2137
|
}
|
|
2020
2138
|
const record = {
|
|
2021
2139
|
locator: room,
|
|
@@ -2035,10 +2153,7 @@ export function composeRuntime(options) {
|
|
|
2035
2153
|
const local = await openLocal();
|
|
2036
2154
|
const current = roomDrafts.get(room)?.get(path);
|
|
2037
2155
|
if (current === undefined || current.revision !== revision) {
|
|
2038
|
-
return
|
|
2039
|
-
outcome: 'stale',
|
|
2040
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
2041
|
-
};
|
|
2156
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2042
2157
|
}
|
|
2043
2158
|
await local.discardRoomDraft(room, path);
|
|
2044
2159
|
forgetRoomDraft(room, path);
|
|
@@ -2087,9 +2202,7 @@ export function composeRuntime(options) {
|
|
|
2087
2202
|
persistence() {
|
|
2088
2203
|
return options.shell.persistentStorage;
|
|
2089
2204
|
},
|
|
2090
|
-
observe: (
|
|
2091
|
-
deliver(structuredClone(message));
|
|
2092
|
-
}), topic === 'query')),
|
|
2205
|
+
observe: observing(watch),
|
|
2093
2206
|
async reauthorize() {
|
|
2094
2207
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2095
2208
|
return;
|
|
@@ -2098,8 +2211,19 @@ export function composeRuntime(options) {
|
|
|
2098
2211
|
await mutations;
|
|
2099
2212
|
authorization = undefined;
|
|
2100
2213
|
discoveryPaused = false;
|
|
2101
|
-
|
|
2214
|
+
if (options.adapter.roomStream === undefined)
|
|
2215
|
+
await sync('unstreamed');
|
|
2216
|
+
else
|
|
2217
|
+
await task(ensureAuthorization);
|
|
2102
2218
|
},
|
|
2103
2219
|
};
|
|
2104
|
-
|
|
2220
|
+
const reachable = {
|
|
2221
|
+
md,
|
|
2222
|
+
watch,
|
|
2223
|
+
plan: planned,
|
|
2224
|
+
query: (plan) => rowsQuery(plan),
|
|
2225
|
+
watchPlan,
|
|
2226
|
+
search: searched,
|
|
2227
|
+
};
|
|
2228
|
+
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2105
2229
|
}
|