@agentshouse/mdruntime 0.1.0-c967d1ca66fee → 0.1.0-ca14c49c06d90
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 +142 -112
- package/README.md +11 -1
- package/dist/adapter.d.ts +59 -13
- package/dist/adapter.d.ts.map +1 -1
- package/dist/attempt.d.ts +16 -0
- package/dist/attempt.d.ts.map +1 -0
- package/dist/attempt.js +70 -0
- package/dist/binary.d.ts +23 -0
- package/dist/binary.d.ts.map +1 -0
- package/dist/binary.js +8 -0
- package/dist/bootstrap.d.ts +2 -2
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +33 -14
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +8 -7
- package/dist/change.d.ts +11 -0
- package/dist/change.d.ts.map +1 -1
- package/dist/change.js +26 -1
- package/dist/chat/acts.d.ts +33 -0
- package/dist/chat/acts.d.ts.map +1 -0
- package/dist/chat/acts.js +36 -0
- package/dist/chat/context.d.ts +3 -0
- package/dist/chat/context.d.ts.map +1 -0
- package/dist/chat/context.js +10 -0
- package/dist/chat/input.d.ts +9 -0
- package/dist/chat/input.d.ts.map +1 -0
- package/dist/chat/input.js +99 -0
- package/dist/chat/kit.d.ts +60 -0
- package/dist/chat/kit.d.ts.map +1 -0
- package/dist/chat/kit.js +123 -0
- package/dist/chat/lasting.d.ts +6 -0
- package/dist/chat/lasting.d.ts.map +1 -0
- package/dist/chat/lasting.js +19 -0
- package/dist/chat/list.d.ts +26 -0
- package/dist/chat/list.d.ts.map +1 -0
- package/dist/chat/list.js +62 -0
- package/dist/chat/markdown.d.ts +20 -0
- package/dist/chat/markdown.d.ts.map +1 -0
- package/dist/chat/markdown.js +188 -0
- package/dist/chat/message.d.ts +6 -0
- package/dist/chat/message.d.ts.map +1 -0
- package/dist/chat/message.js +192 -0
- package/dist/chat/messages.d.ts +21 -0
- package/dist/chat/messages.d.ts.map +1 -0
- package/dist/chat/messages.js +151 -0
- package/dist/chat/name.d.ts +6 -0
- package/dist/chat/name.d.ts.map +1 -0
- package/dist/chat/name.js +37 -0
- package/dist/chat/opened.d.ts +11 -0
- package/dist/chat/opened.d.ts.map +1 -0
- package/dist/chat/opened.js +17 -0
- package/dist/chat/overlay.d.ts +31 -0
- package/dist/chat/overlay.d.ts.map +1 -0
- package/dist/chat/overlay.js +97 -0
- package/dist/chat/parts.d.ts +57 -0
- package/dist/chat/parts.d.ts.map +1 -0
- package/dist/chat/parts.js +119 -0
- package/dist/chat/process.d.ts +20 -0
- package/dist/chat/process.d.ts.map +1 -0
- package/dist/chat/process.js +32 -0
- package/dist/chat/provider.d.ts +63 -0
- package/dist/chat/provider.d.ts.map +1 -0
- package/dist/chat/provider.js +620 -0
- package/dist/chat/question.d.ts +9 -0
- package/dist/chat/question.d.ts.map +1 -0
- package/dist/chat/question.js +80 -0
- package/dist/chat/questions.d.ts +31 -0
- package/dist/chat/questions.d.ts.map +1 -0
- package/dist/chat/questions.js +111 -0
- package/dist/chat/read.d.ts +5 -0
- package/dist/chat/read.d.ts.map +1 -0
- package/dist/chat/read.js +13 -0
- package/dist/chat/ready.d.ts +33 -0
- package/dist/chat/ready.d.ts.map +1 -0
- package/dist/chat/ready.js +91 -0
- package/dist/chat/style.d.ts +4 -0
- package/dist/chat/style.d.ts.map +1 -0
- package/dist/chat/style.js +302 -0
- package/dist/chat.d.ts +14 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +8 -0
- package/dist/client.d.ts +11 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +12 -2
- package/dist/connection.d.ts +13 -6
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +573 -193
- package/dist/conversation.d.ts +14 -4
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +188 -63
- package/dist/entry.d.ts +17 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +156 -0
- package/dist/expectation.d.ts +21 -0
- package/dist/expectation.d.ts.map +1 -0
- package/dist/expectation.js +70 -0
- package/dist/fence.d.ts +6 -0
- package/dist/fence.d.ts.map +1 -0
- package/dist/fence.js +10 -0
- package/dist/host.d.ts +3 -2
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +13 -6
- package/dist/identity.d.ts +2 -0
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +6 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/memory.d.ts +4 -3
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +40 -11
- package/dist/outcome.d.ts +11 -1
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +7 -0
- package/dist/overlay.d.ts +13 -2
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +64 -22
- package/dist/query.d.ts +2 -0
- package/dist/query.d.ts.map +1 -1
- package/dist/reach.d.ts +2 -0
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +26 -4
- package/dist/react.d.ts +2 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/read-side.d.ts.map +1 -1
- package/dist/read-side.js +9 -2
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +12 -17
- package/dist/replica.d.ts +5 -2
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +33 -28
- package/dist/room-draft.d.ts +19 -2
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +119 -1
- package/dist/rows.d.ts +3 -1
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +3 -1
- package/dist/save.d.ts +23 -0
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +309 -18
- package/dist/store.d.ts +36 -18
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +291 -172
- package/dist/wire.d.ts +3 -0
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +3 -0
- package/package.json +26 -14
- package/dist/attachment.d.ts +0 -3
- package/dist/attachment.d.ts.map +0 -1
- package/dist/attachment.js +0 -8
package/dist/connection.js
CHANGED
|
@@ -8,16 +8,19 @@ import { createSearchIndex, searchPlanOf, } from './search.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 { queuedFile } from './change.js';
|
|
11
12
|
import { validRoot } from './collection.js';
|
|
12
13
|
import { runtimeError } from './failure.js';
|
|
13
14
|
import { createLog } from './log.js';
|
|
14
|
-
import {
|
|
15
|
+
import { binaryRecord } from './binary.js';
|
|
16
|
+
import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
|
|
15
17
|
import { refreshRoom } from './refresh.js';
|
|
16
|
-
import { buildBatch } from './save.js';
|
|
18
|
+
import { buildBatch, joined, limitedIn, requeued } from './save.js';
|
|
17
19
|
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';
|
|
20
|
+
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
19
21
|
import { same, saving, STARTING, transferring, } from './status.js';
|
|
20
22
|
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
23
|
+
import { FencedMap } from './fence.js';
|
|
21
24
|
import { roomDraftRevision } from './identity.js';
|
|
22
25
|
import { beneath, normalize } from './paths.js';
|
|
23
26
|
import { openRuntimeStore, } from './store.js';
|
|
@@ -36,22 +39,36 @@ function declaredSelection(room) {
|
|
|
36
39
|
root !== '' &&
|
|
37
40
|
selection.roots.every((other, at) => at === index || !within(other, root)));
|
|
38
41
|
}
|
|
42
|
+
const OPENING_LANE = 'opening';
|
|
43
|
+
const CONVERSATION_LANE = 'conversations';
|
|
44
|
+
const ROOM_READS = 256;
|
|
45
|
+
function roomLane(locator) {
|
|
46
|
+
return `room ${locator}`;
|
|
47
|
+
}
|
|
39
48
|
export function composeRuntime(options) {
|
|
40
49
|
const localSaves = options.adapter.localSaves === true;
|
|
41
50
|
let authorization = options.authorization;
|
|
42
51
|
let opening;
|
|
43
52
|
let store;
|
|
44
|
-
const
|
|
53
|
+
const fences = new Map();
|
|
54
|
+
const isFenced = (locator) => fences.has(locator);
|
|
55
|
+
let fencings = 0;
|
|
56
|
+
const lastFenced = new Map();
|
|
57
|
+
const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
|
|
58
|
+
let discoveries = 0;
|
|
59
|
+
const replicas = new FencedMap(isFenced);
|
|
60
|
+
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
45
61
|
const pendings = new Map();
|
|
46
|
-
const roomDrafts = new
|
|
62
|
+
const roomDrafts = new FencedMap(isFenced);
|
|
47
63
|
const presentations = new Map();
|
|
48
64
|
let limits = [];
|
|
49
65
|
let capabilities = [];
|
|
50
|
-
const rooms = new
|
|
66
|
+
const rooms = new FencedMap(isFenced);
|
|
51
67
|
const counted = new Map();
|
|
52
68
|
const unreadable = new Set();
|
|
53
|
-
const
|
|
54
|
-
const
|
|
69
|
+
const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
|
|
70
|
+
const models = new FencedMap(isModelFenced);
|
|
71
|
+
const liveModels = new FencedMap(isModelFenced);
|
|
55
72
|
const invalidModels = new Set();
|
|
56
73
|
const discovered = new Map();
|
|
57
74
|
let roomsListed = false;
|
|
@@ -94,7 +111,13 @@ export function composeRuntime(options) {
|
|
|
94
111
|
let complete = false;
|
|
95
112
|
let recorded = false;
|
|
96
113
|
const stale = new Set();
|
|
97
|
-
let
|
|
114
|
+
let generation = 0;
|
|
115
|
+
let discovering = null;
|
|
116
|
+
let opened = null;
|
|
117
|
+
const owed = new Set();
|
|
118
|
+
const lanes = new Map();
|
|
119
|
+
let roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
120
|
+
const working = new Set();
|
|
98
121
|
let mutations = Promise.resolve();
|
|
99
122
|
const roomListeners = new Set();
|
|
100
123
|
const refusalListeners = new Set();
|
|
@@ -105,7 +128,7 @@ export function composeRuntime(options) {
|
|
|
105
128
|
const presentationListeners = new Set();
|
|
106
129
|
const readSide = createReadSide();
|
|
107
130
|
let withdrawConversations;
|
|
108
|
-
let currencyQueued =
|
|
131
|
+
let currencyQueued = null;
|
|
109
132
|
let socketOpen = false;
|
|
110
133
|
let serviceMode = 'open';
|
|
111
134
|
let haltedRefusal = false;
|
|
@@ -115,7 +138,7 @@ export function composeRuntime(options) {
|
|
|
115
138
|
const memberListeners = new Set();
|
|
116
139
|
const materialized = new Set();
|
|
117
140
|
let everyRoom = false;
|
|
118
|
-
const transfers = new
|
|
141
|
+
const transfers = new FencedMap(isFenced);
|
|
119
142
|
const conversations = createConversationStore({
|
|
120
143
|
agents: declaredAgents,
|
|
121
144
|
documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
|
|
@@ -123,6 +146,7 @@ export function composeRuntime(options) {
|
|
|
123
146
|
durably,
|
|
124
147
|
stream: () => options.adapter.conversations,
|
|
125
148
|
publish: publishChangeSet,
|
|
149
|
+
online: () => !status.providerUnreachable && refusal === null,
|
|
126
150
|
closed: () => closed,
|
|
127
151
|
});
|
|
128
152
|
const searchIndex = createSearchIndex({
|
|
@@ -194,7 +218,9 @@ export function composeRuntime(options) {
|
|
|
194
218
|
}
|
|
195
219
|
}
|
|
196
220
|
function pending() {
|
|
197
|
-
return !localSaves &&
|
|
221
|
+
return (!localSaves &&
|
|
222
|
+
(queuesWithoutReplica.size > 0 ||
|
|
223
|
+
[...replicas.values()].some((replica) => replica.queue.length > 0)));
|
|
198
224
|
}
|
|
199
225
|
function attention() {
|
|
200
226
|
const attentionRequired = [];
|
|
@@ -228,12 +254,11 @@ export function composeRuntime(options) {
|
|
|
228
254
|
if (replica === undefined)
|
|
229
255
|
return 0;
|
|
230
256
|
const roots = replica.confirmed.record.typedSelection?.roots ?? [];
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
return count;
|
|
257
|
+
const paths = [
|
|
258
|
+
...Object.keys(replica.visible.documents).filter(markdownPath),
|
|
259
|
+
...Object.keys(replica.visible.binaries),
|
|
260
|
+
];
|
|
261
|
+
return paths.filter((path) => !roots.some((root) => beneath(root, path))).length;
|
|
237
262
|
}
|
|
238
263
|
function listed() {
|
|
239
264
|
return structuredClone([...rooms.values()].map((room) => {
|
|
@@ -304,10 +329,7 @@ export function composeRuntime(options) {
|
|
|
304
329
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
305
330
|
}
|
|
306
331
|
function entriesOf(locator) {
|
|
307
|
-
|
|
308
|
-
if (replica === undefined)
|
|
309
|
-
return [];
|
|
310
|
-
return replica.queue.map(entryOf);
|
|
332
|
+
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
|
|
311
333
|
}
|
|
312
334
|
function emitQueue(locator) {
|
|
313
335
|
const entries = entriesOf(locator);
|
|
@@ -404,10 +426,7 @@ export function composeRuntime(options) {
|
|
|
404
426
|
const revoked = { status: 'unavailable', reason: 'revoked' };
|
|
405
427
|
try {
|
|
406
428
|
const local = await openLocal();
|
|
407
|
-
await local.revokeModels([...replicas.values()].map((replica) =>
|
|
408
|
-
record: replica.confirmed.record,
|
|
409
|
-
queue: replica.queue,
|
|
410
|
-
})));
|
|
429
|
+
await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
|
|
411
430
|
}
|
|
412
431
|
catch {
|
|
413
432
|
journal.record('model revocation persistence failed');
|
|
@@ -474,19 +493,28 @@ export function composeRuntime(options) {
|
|
|
474
493
|
},
|
|
475
494
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
476
495
|
roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
|
|
477
|
-
attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
|
|
478
496
|
synchronizationAvailable,
|
|
479
497
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
480
498
|
});
|
|
481
499
|
}
|
|
482
|
-
async function applyDiscovery(
|
|
500
|
+
async function applyDiscovery(found, started) {
|
|
501
|
+
for (const room of found) {
|
|
502
|
+
const fence = fences.get(room.locator);
|
|
503
|
+
if (fence !== undefined && fence.erased && fence.at < started)
|
|
504
|
+
fences.delete(room.locator);
|
|
505
|
+
}
|
|
506
|
+
const listed = found.filter((room) => !fences.has(room.locator));
|
|
483
507
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
484
508
|
for (const locator of listedLocators)
|
|
485
509
|
scopedRefusals.delete(locator);
|
|
486
510
|
discovered.clear();
|
|
487
511
|
for (const room of listed)
|
|
488
512
|
discovered.set(room.locator, room);
|
|
489
|
-
|
|
513
|
+
const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
|
|
514
|
+
if (options.adapter.exhaustiveDiscovery === true)
|
|
515
|
+
await loseRooms(unlisted);
|
|
516
|
+
else
|
|
517
|
+
unlistedKept(unlisted);
|
|
490
518
|
for (const room of listed) {
|
|
491
519
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
492
520
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -497,44 +525,79 @@ export function composeRuntime(options) {
|
|
|
497
525
|
}
|
|
498
526
|
roomsListed = true;
|
|
499
527
|
emitRooms();
|
|
528
|
+
return listed;
|
|
500
529
|
}
|
|
501
530
|
function unlistedHeld(listed) {
|
|
502
|
-
return [
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
pendings.delete(batch.id);
|
|
518
|
-
replicas.delete(locator);
|
|
531
|
+
return [
|
|
532
|
+
...new Set([
|
|
533
|
+
...rooms.keys(),
|
|
534
|
+
...roomDrafts.keys(),
|
|
535
|
+
...transfers.keys(),
|
|
536
|
+
...queuesWithoutReplica.keys(),
|
|
537
|
+
]),
|
|
538
|
+
].filter((locator) => !listed.has(locator));
|
|
539
|
+
}
|
|
540
|
+
function unlistedKept(unlisted) {
|
|
541
|
+
for (const locator of unlisted) {
|
|
542
|
+
const held = replicas.get(locator)?.confirmed.record;
|
|
543
|
+
if (held !== undefined)
|
|
544
|
+
list(held, false);
|
|
545
|
+
else
|
|
519
546
|
rooms.delete(locator);
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
function fence(locators) {
|
|
550
|
+
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
551
|
+
const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
|
|
552
|
+
room: true,
|
|
553
|
+
queue: true,
|
|
554
|
+
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
555
|
+
}));
|
|
556
|
+
for (const locator of fresh) {
|
|
557
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
558
|
+
lastFenced.set(locator, ++fencings);
|
|
559
|
+
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
560
|
+
pendings.delete(batch.id);
|
|
561
|
+
replicas.delete(locator);
|
|
562
|
+
queuesWithoutReplica.delete(locator);
|
|
563
|
+
rooms.delete(locator);
|
|
564
|
+
roomDrafts.delete(locator);
|
|
565
|
+
transfers.delete(locator);
|
|
566
|
+
discovered.delete(locator);
|
|
567
|
+
announced.delete(locator);
|
|
568
|
+
unreadable.delete(locator);
|
|
569
|
+
stale.delete(locator);
|
|
570
|
+
scopedRefusals.delete(locator);
|
|
571
|
+
sourceRefusals.delete(locator);
|
|
572
|
+
declarationRefusals.delete(locator);
|
|
573
|
+
for (const attached of ['collection', 'typed-selection']) {
|
|
574
|
+
const key = modelKey(locator, attached);
|
|
575
|
+
models.delete(key);
|
|
576
|
+
liveModels.delete(key);
|
|
577
|
+
invalidModels.delete(key);
|
|
534
578
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
579
|
+
journal.record(`room lost ${locator}`);
|
|
580
|
+
}
|
|
581
|
+
if (fresh.length === 0)
|
|
582
|
+
return fresh;
|
|
583
|
+
publishChangeSet(mergeChangeSets(removed));
|
|
584
|
+
for (const locator of fresh)
|
|
585
|
+
materialized.delete(locator);
|
|
586
|
+
moveTo({ ...status, ...attention() });
|
|
587
|
+
return fresh;
|
|
588
|
+
}
|
|
589
|
+
async function erased(local, locators) {
|
|
590
|
+
await local.erase(locators);
|
|
591
|
+
for (const locator of locators) {
|
|
592
|
+
const held = fences.get(locator);
|
|
593
|
+
if (held !== undefined)
|
|
594
|
+
held.erased = true;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
async function loseRooms(locators) {
|
|
598
|
+
const fresh = fence(locators);
|
|
599
|
+
if (fresh.length > 0)
|
|
600
|
+
await durably((local) => erased(local, fresh));
|
|
538
601
|
}
|
|
539
602
|
function restored(key, persisted) {
|
|
540
603
|
const prior = models.get(key);
|
|
@@ -589,6 +652,12 @@ export function composeRuntime(options) {
|
|
|
589
652
|
}
|
|
590
653
|
const previousReplicas = new Map(replicas);
|
|
591
654
|
replicas.clear();
|
|
655
|
+
queuesWithoutReplica.clear();
|
|
656
|
+
const replicated = new Set(records.map((record) => record.locator));
|
|
657
|
+
for (const [locator, queue] of queuedBatches) {
|
|
658
|
+
if (!replicated.has(locator))
|
|
659
|
+
queuesWithoutReplica.set(locator, queue);
|
|
660
|
+
}
|
|
592
661
|
for (const record of records) {
|
|
593
662
|
const model = restored(modelKey(record.locator, 'collection'), record.model);
|
|
594
663
|
const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
|
|
@@ -602,7 +671,6 @@ export function composeRuntime(options) {
|
|
|
602
671
|
rooms.set(record.locator, {
|
|
603
672
|
...existing,
|
|
604
673
|
roomVersion: record.roomVersion,
|
|
605
|
-
attachments: record.attachments,
|
|
606
674
|
operations: record.operations,
|
|
607
675
|
replica: record.replica,
|
|
608
676
|
model: modelTypes(model),
|
|
@@ -634,8 +702,15 @@ export function composeRuntime(options) {
|
|
|
634
702
|
return lastCompleted;
|
|
635
703
|
}
|
|
636
704
|
async function open() {
|
|
637
|
-
|
|
705
|
+
if (closed)
|
|
706
|
+
throw new Error('runtime closed');
|
|
707
|
+
const opened = await openRuntimeStore(isFenced);
|
|
638
708
|
store = opened;
|
|
709
|
+
const interrupted = await opened.fences();
|
|
710
|
+
for (const locator of interrupted) {
|
|
711
|
+
fences.set(locator, { at: discoveries, erased: false });
|
|
712
|
+
lastFenced.set(locator, ++fencings);
|
|
713
|
+
}
|
|
639
714
|
const kept = options.shell.generation;
|
|
640
715
|
const storedGeneration = await opened.generation();
|
|
641
716
|
const loss = detectStorageLoss(storedGeneration, kept);
|
|
@@ -649,6 +724,8 @@ export function composeRuntime(options) {
|
|
|
649
724
|
emitStorageLoss();
|
|
650
725
|
}
|
|
651
726
|
const lastCompleted = await readState(opened);
|
|
727
|
+
if (interrupted.length > 0)
|
|
728
|
+
await erased(opened, interrupted);
|
|
652
729
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
653
730
|
return opened;
|
|
654
731
|
}
|
|
@@ -675,6 +752,7 @@ export function composeRuntime(options) {
|
|
|
675
752
|
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
676
753
|
}
|
|
677
754
|
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
755
|
+
const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
678
756
|
const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
|
|
679
757
|
const kept = queue.filter((batch) => !withdrawn.includes(batch));
|
|
680
758
|
const unqueued = withdrawn.map((batch) => ({
|
|
@@ -700,20 +778,36 @@ export function composeRuntime(options) {
|
|
|
700
778
|
});
|
|
701
779
|
}
|
|
702
780
|
}
|
|
703
|
-
await local.
|
|
704
|
-
|
|
705
|
-
previous: before?.confirmed.record ?? null,
|
|
706
|
-
...(settling.stored === undefined ? {} : { stored: settling.stored }),
|
|
707
|
-
queue: kept,
|
|
708
|
-
previousQueue: before?.queue ?? [],
|
|
709
|
-
roomDrafts: [...written.values()],
|
|
710
|
-
});
|
|
711
|
-
for (const batch of before?.queue ?? []) {
|
|
781
|
+
await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
|
|
782
|
+
for (const batch of previousQueue) {
|
|
712
783
|
if (!kept.includes(batch))
|
|
713
784
|
pendings.delete(batch.id);
|
|
714
785
|
}
|
|
715
786
|
for (const record of written.values())
|
|
716
787
|
rememberRoomDraft(record);
|
|
788
|
+
try {
|
|
789
|
+
await local.write({
|
|
790
|
+
next: confirmed.record,
|
|
791
|
+
previous: before?.confirmed.record ?? null,
|
|
792
|
+
...(settling.stored === undefined ? {} : { stored: settling.stored }),
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
catch (error) {
|
|
796
|
+
const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
|
|
797
|
+
if (held !== null)
|
|
798
|
+
replicas.set(locator, held);
|
|
799
|
+
else if (kept.length > 0)
|
|
800
|
+
queuesWithoutReplica.set(locator, kept);
|
|
801
|
+
else
|
|
802
|
+
queuesWithoutReplica.delete(locator);
|
|
803
|
+
publishChangeSet(changeSet(locator, before ?? null, held, {
|
|
804
|
+
queue: kept.length !== previousQueue.length,
|
|
805
|
+
drafts: [...written.keys()],
|
|
806
|
+
}));
|
|
807
|
+
moveTo({ ...status, ...attention() });
|
|
808
|
+
throw error;
|
|
809
|
+
}
|
|
810
|
+
queuesWithoutReplica.delete(locator);
|
|
717
811
|
for (const { batch, returned } of unqueued) {
|
|
718
812
|
const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
|
|
719
813
|
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
@@ -721,7 +815,7 @@ export function composeRuntime(options) {
|
|
|
721
815
|
replicas.set(locator, replicaOf(confirmed, kept, before));
|
|
722
816
|
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
723
817
|
room: settling.room,
|
|
724
|
-
queue: kept.length !==
|
|
818
|
+
queue: kept.length !== previousQueue.length,
|
|
725
819
|
drafts: [...written.keys()],
|
|
726
820
|
});
|
|
727
821
|
}
|
|
@@ -756,10 +850,10 @@ export function composeRuntime(options) {
|
|
|
756
850
|
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
757
851
|
};
|
|
758
852
|
}
|
|
759
|
-
function publish(local, room, confirmed, landing
|
|
853
|
+
function publish(local, room, confirmed, landing, live) {
|
|
760
854
|
const { base, stored } = landing;
|
|
761
855
|
return exclusive(async () => {
|
|
762
|
-
if (!
|
|
856
|
+
if (!live())
|
|
763
857
|
return true;
|
|
764
858
|
const prior = replicas.get(room.locator);
|
|
765
859
|
if (base !== undefined &&
|
|
@@ -768,7 +862,7 @@ export function composeRuntime(options) {
|
|
|
768
862
|
return false;
|
|
769
863
|
}
|
|
770
864
|
adoptModels(room.locator, confirmed.record);
|
|
771
|
-
const queue = prior?.queue ?? [];
|
|
865
|
+
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
772
866
|
const roomVersion = confirmed.record.roomVersion;
|
|
773
867
|
const accepted = queue.filter((batch) => {
|
|
774
868
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
@@ -787,6 +881,7 @@ export function composeRuntime(options) {
|
|
|
787
881
|
list(room, true);
|
|
788
882
|
publishChangeSet(set);
|
|
789
883
|
moveTo({ ...status, ...attention() });
|
|
884
|
+
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
790
885
|
return true;
|
|
791
886
|
});
|
|
792
887
|
}
|
|
@@ -810,7 +905,24 @@ export function composeRuntime(options) {
|
|
|
810
905
|
if (changed.length > 0)
|
|
811
906
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
812
907
|
const parts = saved.map(({ path, parts }) => changesOf(path, parts));
|
|
813
|
-
const
|
|
908
|
+
const changes = parts.flatMap(({ changes }) => changes);
|
|
909
|
+
const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
|
|
910
|
+
if (requeue.kind === 'refused')
|
|
911
|
+
return { outcome: 'refused', ...requeue.refusal };
|
|
912
|
+
const foreign = [...requeue.withdrawn.keys()].some((origin) => {
|
|
913
|
+
const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
|
|
914
|
+
return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
|
|
915
|
+
});
|
|
916
|
+
if (foreign) {
|
|
917
|
+
return {
|
|
918
|
+
outcome: 'refused',
|
|
919
|
+
code: 'unknown-revision',
|
|
920
|
+
retryable: false,
|
|
921
|
+
file: [...requeue.queued].at(-1),
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
const withdrawing = requeue.withdrawn.size > 0;
|
|
925
|
+
const input = {
|
|
814
926
|
local: localSaves,
|
|
815
927
|
baseRoomVersion: replica.confirmed.record.roomVersion,
|
|
816
928
|
composition: compositionFor(replica.confirmed.record),
|
|
@@ -819,28 +931,86 @@ export function composeRuntime(options) {
|
|
|
819
931
|
providerLimits: limits,
|
|
820
932
|
handle: room.handle,
|
|
821
933
|
visible: replica.visible,
|
|
822
|
-
changes
|
|
934
|
+
changes,
|
|
823
935
|
bases: parts.flatMap(({ bases }) => bases),
|
|
824
936
|
writtenBy,
|
|
937
|
+
};
|
|
938
|
+
const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
|
|
939
|
+
if (checked?.kind === 'refused')
|
|
940
|
+
return { outcome: 'refused', ...checked.refusal };
|
|
941
|
+
const owners = [...requeue.owners].flatMap((origin) => {
|
|
942
|
+
const batch = requeue.withdrawn.get(origin);
|
|
943
|
+
return batch === undefined || batch === null ? [] : [{ origin, batch }];
|
|
825
944
|
});
|
|
826
|
-
if (
|
|
945
|
+
if (owners.length > 1) {
|
|
946
|
+
return {
|
|
947
|
+
outcome: 'refused',
|
|
948
|
+
code: 'unknown-revision',
|
|
949
|
+
retryable: false,
|
|
950
|
+
file: [...requeue.queued].at(-1),
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
const built = !withdrawing
|
|
954
|
+
? buildBatch(input)
|
|
955
|
+
: requeue.changes.length === 0
|
|
956
|
+
? null
|
|
957
|
+
: buildBatch({
|
|
958
|
+
...input,
|
|
959
|
+
visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
|
|
960
|
+
changes: requeue.changes,
|
|
961
|
+
});
|
|
962
|
+
if (built?.kind === 'refused')
|
|
827
963
|
return { outcome: 'refused', ...built.refusal };
|
|
828
|
-
const
|
|
964
|
+
const [owner] = owners;
|
|
965
|
+
const join = built === null || owner === undefined
|
|
966
|
+
? null
|
|
967
|
+
: joined(requeue.queue, owner.batch, built.batch);
|
|
968
|
+
if (join?.kind === 'refused')
|
|
969
|
+
return { outcome: 'refused', ...join.refusal };
|
|
970
|
+
const withdrawn = new Map(requeue.withdrawn);
|
|
971
|
+
if (join !== null && owner !== undefined)
|
|
972
|
+
withdrawn.set(owner.origin, join.batch);
|
|
973
|
+
const drafted = new Map();
|
|
974
|
+
for (const [origin, replaced] of withdrawn) {
|
|
975
|
+
if (replaced === null)
|
|
976
|
+
continue;
|
|
977
|
+
const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
|
|
978
|
+
drafted.set(replaced.id, replaced === join?.batch
|
|
979
|
+
? [
|
|
980
|
+
...carried.map((held) => joinedDraft(held, saved)),
|
|
981
|
+
...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
|
|
982
|
+
]
|
|
983
|
+
: carried);
|
|
984
|
+
}
|
|
985
|
+
if (built !== null && join === null)
|
|
986
|
+
drafted.set(built.batch.id, saved);
|
|
987
|
+
const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
|
|
988
|
+
for (const replaced of withdrawn.values()) {
|
|
989
|
+
if (replaced === null || !batches.includes(replaced))
|
|
990
|
+
continue;
|
|
991
|
+
const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
|
|
992
|
+
if (limited !== null)
|
|
993
|
+
return { outcome: 'refused', ...limited };
|
|
994
|
+
}
|
|
829
995
|
const consumed = saved.map(({ path }) => path);
|
|
830
996
|
try {
|
|
831
|
-
await local.
|
|
832
|
-
|
|
833
|
-
previous: replica.confirmed.record,
|
|
997
|
+
await local.keepAuthored({
|
|
998
|
+
locator,
|
|
834
999
|
queue: batches,
|
|
835
|
-
|
|
836
|
-
saved,
|
|
1000
|
+
saved: drafted,
|
|
837
1001
|
consumedRoomDrafts: consumed,
|
|
838
1002
|
});
|
|
839
1003
|
}
|
|
840
1004
|
catch {
|
|
841
1005
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
842
1006
|
}
|
|
843
|
-
|
|
1007
|
+
if (fences.has(locator))
|
|
1008
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1009
|
+
for (const origin of withdrawn.keys())
|
|
1010
|
+
pendings.delete(origin);
|
|
1011
|
+
for (const [id, drafts] of drafted) {
|
|
1012
|
+
pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
|
|
1013
|
+
}
|
|
844
1014
|
if (!localSaves)
|
|
845
1015
|
recorded = false;
|
|
846
1016
|
for (const path of consumed)
|
|
@@ -852,7 +1022,10 @@ export function composeRuntime(options) {
|
|
|
852
1022
|
}));
|
|
853
1023
|
moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
|
|
854
1024
|
submitLater(locator);
|
|
855
|
-
|
|
1025
|
+
const identifier = join?.batch.id ??
|
|
1026
|
+
built?.batch.id ??
|
|
1027
|
+
[...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
|
|
1028
|
+
return { outcome: 'saved', identifier };
|
|
856
1029
|
}
|
|
857
1030
|
async function mark(local, locator, identifier, submission, lost) {
|
|
858
1031
|
const state = pendings.get(identifier);
|
|
@@ -862,6 +1035,8 @@ export function composeRuntime(options) {
|
|
|
862
1035
|
return;
|
|
863
1036
|
}
|
|
864
1037
|
await local.submit(locator, identifier, submission, lost);
|
|
1038
|
+
if (fences.has(locator))
|
|
1039
|
+
return;
|
|
865
1040
|
pendings.set(identifier, { ...state, submission, lost });
|
|
866
1041
|
const replica = replicas.get(locator);
|
|
867
1042
|
if (replica !== undefined)
|
|
@@ -872,6 +1047,8 @@ export function composeRuntime(options) {
|
|
|
872
1047
|
if (!continuing())
|
|
873
1048
|
throw new Error('connection refused');
|
|
874
1049
|
await local.keepUpload(locator, identifier, upload);
|
|
1050
|
+
if (fences.has(locator))
|
|
1051
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
875
1052
|
pendings.set(identifier, { ...pendings.get(identifier), upload });
|
|
876
1053
|
if (!continuing())
|
|
877
1054
|
throw new Error('connection refused');
|
|
@@ -887,6 +1064,7 @@ export function composeRuntime(options) {
|
|
|
887
1064
|
}
|
|
888
1065
|
if (closed || authorization === undefined || refusal !== null)
|
|
889
1066
|
return UNCERTAIN;
|
|
1067
|
+
const since = fencings;
|
|
890
1068
|
const transfer = ['transfer', 'history', locator, path];
|
|
891
1069
|
if (query.revision !== undefined)
|
|
892
1070
|
transfer.push(query.revision);
|
|
@@ -901,6 +1079,8 @@ export function composeRuntime(options) {
|
|
|
901
1079
|
...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
|
|
902
1080
|
});
|
|
903
1081
|
journal.record(`outcome history ${result.kind}`);
|
|
1082
|
+
if (fencedSince(locator, since))
|
|
1083
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
904
1084
|
if (result.kind === 'scoped-refusal') {
|
|
905
1085
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
906
1086
|
if (room !== undefined)
|
|
@@ -915,6 +1095,100 @@ export function composeRuntime(options) {
|
|
|
915
1095
|
return UNCERTAIN;
|
|
916
1096
|
}
|
|
917
1097
|
}
|
|
1098
|
+
function queuedBlob(locator, path, localId) {
|
|
1099
|
+
for (const batch of replicas.get(locator)?.queue ?? []) {
|
|
1100
|
+
const change = batch.changes[queuedFile(batch, path, localId)];
|
|
1101
|
+
if (change?.kind === 'binary')
|
|
1102
|
+
return change.blob;
|
|
1103
|
+
}
|
|
1104
|
+
return null;
|
|
1105
|
+
}
|
|
1106
|
+
function binaryRoom(request) {
|
|
1107
|
+
if (request.kind === 'path')
|
|
1108
|
+
return request.locator;
|
|
1109
|
+
const scope = options.adapter.referrerScope(request.referrer);
|
|
1110
|
+
return scope?.kind === 'room' ? scope.locator : null;
|
|
1111
|
+
}
|
|
1112
|
+
async function readBinary(request) {
|
|
1113
|
+
if (closed || refusal !== null)
|
|
1114
|
+
return UNCERTAIN;
|
|
1115
|
+
try {
|
|
1116
|
+
await ensureAuthorization();
|
|
1117
|
+
}
|
|
1118
|
+
catch {
|
|
1119
|
+
return UNCERTAIN;
|
|
1120
|
+
}
|
|
1121
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1122
|
+
return UNCERTAIN;
|
|
1123
|
+
const since = fencings;
|
|
1124
|
+
journal.record(request.kind === 'path'
|
|
1125
|
+
? `transfer binary ${request.locator} ${request.path}`
|
|
1126
|
+
: `transfer binary ${request.fileRef}`);
|
|
1127
|
+
try {
|
|
1128
|
+
const result = await options.adapter.binary(authorization, request);
|
|
1129
|
+
journal.record(`outcome binary ${result.kind}`);
|
|
1130
|
+
const locator = binaryRoom(request);
|
|
1131
|
+
if (locator !== null && fencedSince(locator, since))
|
|
1132
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
1133
|
+
if (result.kind === 'scoped-refusal') {
|
|
1134
|
+
const room = replicas.get(result.locator)?.confirmed.record;
|
|
1135
|
+
if (room !== undefined)
|
|
1136
|
+
await scoped(result, room, await openLocal());
|
|
1137
|
+
}
|
|
1138
|
+
if (result.kind === 'revoked-authorization')
|
|
1139
|
+
await refuse('revoked-authorization');
|
|
1140
|
+
return binaryAnswer(result);
|
|
1141
|
+
}
|
|
1142
|
+
catch {
|
|
1143
|
+
journal.record('outcome binary transport-uncertainty');
|
|
1144
|
+
return UNCERTAIN;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
async function readBinaryPath(locator, path, revision) {
|
|
1148
|
+
await ready;
|
|
1149
|
+
if (revision !== undefined)
|
|
1150
|
+
return readBinary({ kind: 'path', locator, path, revision });
|
|
1151
|
+
const visible = replicas.get(locator)?.visible;
|
|
1152
|
+
const held = visible?.binaries[path];
|
|
1153
|
+
if (visible === undefined || held === undefined)
|
|
1154
|
+
return absentBinary(path);
|
|
1155
|
+
if (held.state === 'pending') {
|
|
1156
|
+
const blob = queuedBlob(locator, path, held.localId);
|
|
1157
|
+
return blob === null ? absentBinary(path) : { kind: 'binary', blob };
|
|
1158
|
+
}
|
|
1159
|
+
const record = binaryRecord(path, held, visible.revisions[path]);
|
|
1160
|
+
if (record.state !== 'accepted')
|
|
1161
|
+
return absentBinary(path);
|
|
1162
|
+
const replica = replicas.get(locator);
|
|
1163
|
+
const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
|
|
1164
|
+
const holds = (at) => {
|
|
1165
|
+
const kept = replica?.confirmed.record.binaries[at];
|
|
1166
|
+
return (kept?.state === 'accepted' &&
|
|
1167
|
+
kept.fileRef === record.fileRef &&
|
|
1168
|
+
replica?.confirmed.record.revisions[at] === record.revision);
|
|
1169
|
+
};
|
|
1170
|
+
let confirmed = path;
|
|
1171
|
+
for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
|
|
1172
|
+
const change = queued[index];
|
|
1173
|
+
if (change.kind === 'move' && change.to === confirmed)
|
|
1174
|
+
confirmed = change.path;
|
|
1175
|
+
}
|
|
1176
|
+
return readBinary({
|
|
1177
|
+
kind: 'path',
|
|
1178
|
+
locator,
|
|
1179
|
+
path: confirmed,
|
|
1180
|
+
revision: record.revision,
|
|
1181
|
+
fileRef: record.fileRef,
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
async function readBinaryReference(reference) {
|
|
1185
|
+
await ready;
|
|
1186
|
+
return readBinary({
|
|
1187
|
+
kind: 'reference',
|
|
1188
|
+
fileRef: reference.fileRef,
|
|
1189
|
+
referrer: reference.referrer,
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
918
1192
|
function declaredAgents() {
|
|
919
1193
|
for (const capability of capabilities) {
|
|
920
1194
|
if (capability.conversationAgents !== undefined)
|
|
@@ -979,11 +1253,11 @@ export function composeRuntime(options) {
|
|
|
979
1253
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
980
1254
|
!options.adapter.reached(locator, roomVersion, written));
|
|
981
1255
|
}
|
|
982
|
-
async function pullRefresh(authorized, confirmed) {
|
|
983
|
-
if (!
|
|
1256
|
+
async function pullRefresh(authorized, confirmed, live) {
|
|
1257
|
+
if (!live())
|
|
984
1258
|
return unconfirmed;
|
|
985
1259
|
try {
|
|
986
|
-
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held),
|
|
1260
|
+
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
|
|
987
1261
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
988
1262
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
989
1263
|
return result;
|
|
@@ -993,33 +1267,39 @@ export function composeRuntime(options) {
|
|
|
993
1267
|
throw error;
|
|
994
1268
|
}
|
|
995
1269
|
}
|
|
996
|
-
function staging(local) {
|
|
1270
|
+
function staging(local, live) {
|
|
997
1271
|
return {
|
|
998
1272
|
held: (locator) => transfers.get(locator) ?? null,
|
|
999
1273
|
staged: (record) => local.staged(record),
|
|
1000
1274
|
stage: (record, page) => exclusive(async () => {
|
|
1001
|
-
if (!
|
|
1275
|
+
if (!live())
|
|
1002
1276
|
return false;
|
|
1003
1277
|
await local.stage(record, page);
|
|
1004
1278
|
transfers.set(record.locator, record);
|
|
1005
1279
|
return true;
|
|
1006
1280
|
}),
|
|
1007
1281
|
abandon: (locator) => exclusive(async () => {
|
|
1008
|
-
if (!
|
|
1282
|
+
if (!live())
|
|
1009
1283
|
return;
|
|
1010
1284
|
await local.abandon(locator);
|
|
1011
1285
|
transfers.delete(locator);
|
|
1012
1286
|
}),
|
|
1013
1287
|
};
|
|
1014
1288
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1289
|
+
function reusable(room) {
|
|
1290
|
+
const record = replicas.get(room.locator)?.confirmed.record;
|
|
1291
|
+
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1292
|
+
return null;
|
|
1293
|
+
return { documents: record.documents, revisions: record.revisions };
|
|
1294
|
+
}
|
|
1295
|
+
async function pullBootstrap(authorized, room, local, live) {
|
|
1296
|
+
if (!live())
|
|
1017
1297
|
return unconfirmed;
|
|
1018
1298
|
try {
|
|
1019
1299
|
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
1020
1300
|
collection: modelOf(room.locator),
|
|
1021
1301
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
1022
|
-
}, (models, held) => remodel(room.locator, models, held),
|
|
1302
|
+
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
1023
1303
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
1024
1304
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1025
1305
|
return result;
|
|
@@ -1029,11 +1309,11 @@ export function composeRuntime(options) {
|
|
|
1029
1309
|
throw error;
|
|
1030
1310
|
}
|
|
1031
1311
|
}
|
|
1032
|
-
async function advance(room, local, authorized) {
|
|
1312
|
+
async function advance(room, local, authorized, live) {
|
|
1033
1313
|
const held = replicas.get(room.locator);
|
|
1034
1314
|
if (held !== undefined) {
|
|
1035
|
-
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
1036
|
-
if (!
|
|
1315
|
+
const refreshed = await pullRefresh(authorized, held.confirmed, live);
|
|
1316
|
+
if (!live())
|
|
1037
1317
|
return unconfirmed;
|
|
1038
1318
|
if (refreshed.kind === 'still-current') {
|
|
1039
1319
|
stale.delete(room.locator);
|
|
@@ -1047,24 +1327,28 @@ export function composeRuntime(options) {
|
|
|
1047
1327
|
return null;
|
|
1048
1328
|
}
|
|
1049
1329
|
if (refreshed.kind === 'advanced') {
|
|
1050
|
-
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
|
|
1330
|
+
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
|
|
1051
1331
|
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1052
1332
|
const reached = now !== undefined &&
|
|
1053
1333
|
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
1054
|
-
return reached ? null : advance(room, local, authorized);
|
|
1334
|
+
return reached ? null : advance(room, local, authorized, live);
|
|
1055
1335
|
}
|
|
1056
1336
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1057
1337
|
return null;
|
|
1058
1338
|
}
|
|
1059
1339
|
if (refreshed.kind !== 'beyond-horizon')
|
|
1060
1340
|
return refreshed;
|
|
1341
|
+
if (opened !== null) {
|
|
1342
|
+
owed.add(room.locator);
|
|
1343
|
+
return unconfirmed;
|
|
1344
|
+
}
|
|
1061
1345
|
}
|
|
1062
|
-
const bootstrapped = await pullBootstrap(authorized, room, local);
|
|
1063
|
-
if (!
|
|
1346
|
+
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
1347
|
+
if (!live())
|
|
1064
1348
|
return unconfirmed;
|
|
1065
1349
|
if (bootstrapped.kind !== 'staged')
|
|
1066
1350
|
return bootstrapped;
|
|
1067
|
-
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
1351
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
1068
1352
|
return null;
|
|
1069
1353
|
}
|
|
1070
1354
|
async function withhold(withheld, room, local, progress) {
|
|
@@ -1138,13 +1422,13 @@ export function composeRuntime(options) {
|
|
|
1138
1422
|
logAccepted(room.locator, submitted, accepted);
|
|
1139
1423
|
const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
1140
1424
|
? { kind: 'staged', confirmed: current.confirmed }
|
|
1141
|
-
: applyReturned(current.confirmed,
|
|
1425
|
+
: applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
1142
1426
|
? accepted.roomVersion
|
|
1143
1427
|
: held, accepted.roomVersion);
|
|
1144
1428
|
if (applied.kind !== 'staged') {
|
|
1145
1429
|
const state = pendings.get(submitted.id);
|
|
1146
1430
|
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1147
|
-
if (state !== undefined)
|
|
1431
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
1148
1432
|
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1149
1433
|
emitQueue(room.locator);
|
|
1150
1434
|
return applied;
|
|
@@ -1193,7 +1477,7 @@ export function composeRuntime(options) {
|
|
|
1193
1477
|
let result;
|
|
1194
1478
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
1195
1479
|
await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
|
|
1196
|
-
if (discoveryPaused)
|
|
1480
|
+
if (discoveryPaused || fences.has(room.locator))
|
|
1197
1481
|
return;
|
|
1198
1482
|
if (writesHeld()) {
|
|
1199
1483
|
await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
|
|
@@ -1256,7 +1540,7 @@ export function composeRuntime(options) {
|
|
|
1256
1540
|
result.kind === 'expired' ||
|
|
1257
1541
|
result.kind === 'retryable-refusal') {
|
|
1258
1542
|
if (result.kind === 'expired') {
|
|
1259
|
-
const withheld = await advance(room, local, authorized);
|
|
1543
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
1260
1544
|
if (withheld !== null)
|
|
1261
1545
|
return withhold(withheld, room, local, progress);
|
|
1262
1546
|
}
|
|
@@ -1294,7 +1578,7 @@ export function composeRuntime(options) {
|
|
|
1294
1578
|
});
|
|
1295
1579
|
if (result.kind === 'expired')
|
|
1296
1580
|
continue;
|
|
1297
|
-
const withheld = await advance(room, local, authorized);
|
|
1581
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
1298
1582
|
if (withheld !== null)
|
|
1299
1583
|
return withhold(withheld, room, local, progress);
|
|
1300
1584
|
continue;
|
|
@@ -1311,9 +1595,9 @@ export function composeRuntime(options) {
|
|
|
1311
1595
|
const awaited = new Promise((resolve) => {
|
|
1312
1596
|
release = resolve;
|
|
1313
1597
|
});
|
|
1314
|
-
answering.set(locator, awaited);
|
|
1598
|
+
answering.set(locator, { generation, answered: awaited });
|
|
1315
1599
|
return () => {
|
|
1316
|
-
if (answering.get(locator) === awaited)
|
|
1600
|
+
if (answering.get(locator)?.answered === awaited)
|
|
1317
1601
|
answering.delete(locator);
|
|
1318
1602
|
release();
|
|
1319
1603
|
};
|
|
@@ -1357,11 +1641,7 @@ export function composeRuntime(options) {
|
|
|
1357
1641
|
const read = reclassified.kind === 'deferred'
|
|
1358
1642
|
? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
|
|
1359
1643
|
: reclassified;
|
|
1360
|
-
await local.write({
|
|
1361
|
-
next: record,
|
|
1362
|
-
previous: held.confirmed.record,
|
|
1363
|
-
queue: held.queue,
|
|
1364
|
-
});
|
|
1644
|
+
await local.write({ next: record, previous: held.confirmed.record });
|
|
1365
1645
|
if (read.kind === 'staged') {
|
|
1366
1646
|
replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
|
|
1367
1647
|
if (declarationRefusals.delete(room.locator)) {
|
|
@@ -1429,13 +1709,14 @@ export function composeRuntime(options) {
|
|
|
1429
1709
|
roomVersion: record.roomVersion,
|
|
1430
1710
|
documents: record.documents,
|
|
1431
1711
|
revisions: record.revisions,
|
|
1432
|
-
attachments: record.attachments,
|
|
1433
1712
|
},
|
|
1434
1713
|
];
|
|
1435
1714
|
});
|
|
1436
1715
|
}
|
|
1437
|
-
async function synchronize(listed, local, progress, attempt) {
|
|
1716
|
+
async function synchronize(listed, local, progress, attempt, live) {
|
|
1438
1717
|
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
|
|
1718
|
+
if (!live())
|
|
1719
|
+
return;
|
|
1439
1720
|
const ready = listed.filter((_, index) => {
|
|
1440
1721
|
const outcome = prepared[index];
|
|
1441
1722
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
@@ -1444,9 +1725,10 @@ export function composeRuntime(options) {
|
|
|
1444
1725
|
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
1445
1726
|
handed = true;
|
|
1446
1727
|
}
|
|
1728
|
+
discovering?.release();
|
|
1447
1729
|
const loaded = await Promise.allSettled(ready
|
|
1448
1730
|
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1449
|
-
.map((room) => loadRoom(room, local, progress)));
|
|
1731
|
+
.map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
|
|
1450
1732
|
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1451
1733
|
if (failed !== undefined)
|
|
1452
1734
|
throw failed.reason;
|
|
@@ -1475,14 +1757,14 @@ export function composeRuntime(options) {
|
|
|
1475
1757
|
}
|
|
1476
1758
|
return true;
|
|
1477
1759
|
}
|
|
1478
|
-
async function caughtUp(room, local, progress) {
|
|
1760
|
+
async function caughtUp(room, local, progress, live) {
|
|
1479
1761
|
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1480
|
-
if (held === undefined || authorization === undefined)
|
|
1762
|
+
if (held === undefined || authorization === undefined || !live())
|
|
1481
1763
|
return;
|
|
1482
1764
|
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
1483
1765
|
if (!behind)
|
|
1484
1766
|
return;
|
|
1485
|
-
const withheld = await advance(room, local, authorization);
|
|
1767
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1486
1768
|
if (withheld !== null)
|
|
1487
1769
|
await withhold(withheld, room, local, progress);
|
|
1488
1770
|
}
|
|
@@ -1492,39 +1774,41 @@ export function composeRuntime(options) {
|
|
|
1492
1774
|
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1493
1775
|
});
|
|
1494
1776
|
}
|
|
1495
|
-
async function loadRoom(room, local, progress) {
|
|
1496
|
-
if (
|
|
1777
|
+
async function loadRoom(room, local, progress, live) {
|
|
1778
|
+
if (!live() || authorization === undefined)
|
|
1497
1779
|
return;
|
|
1498
1780
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1499
|
-
await reload(room, local, progress);
|
|
1781
|
+
await reload(room, local, progress, live);
|
|
1500
1782
|
return;
|
|
1501
1783
|
}
|
|
1502
|
-
const withheld = await advance(room, local, authorization);
|
|
1784
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1503
1785
|
if (withheld !== null) {
|
|
1504
1786
|
await withhold(withheld, room, local, progress);
|
|
1505
1787
|
return;
|
|
1506
1788
|
}
|
|
1507
|
-
await caughtUp(room, local, progress);
|
|
1789
|
+
await caughtUp(room, local, progress, live);
|
|
1508
1790
|
}
|
|
1509
|
-
async function reload(room, local, progress) {
|
|
1510
|
-
if (
|
|
1791
|
+
async function reload(room, local, progress, live) {
|
|
1792
|
+
if (!live() || authorization === undefined)
|
|
1511
1793
|
return;
|
|
1512
|
-
const bootstrapped = await pullBootstrap(authorization, room, local);
|
|
1513
|
-
if (!
|
|
1794
|
+
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
1795
|
+
if (!live())
|
|
1514
1796
|
return;
|
|
1515
1797
|
if (bootstrapped.kind !== 'staged') {
|
|
1516
1798
|
await withhold(bootstrapped, room, local, progress);
|
|
1517
1799
|
return;
|
|
1518
1800
|
}
|
|
1519
|
-
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
1520
|
-
await caughtUp(room, local, progress);
|
|
1801
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
1802
|
+
await caughtUp(room, local, progress, live);
|
|
1521
1803
|
}
|
|
1522
|
-
async function discoverRooms(local, progress, attempt) {
|
|
1804
|
+
async function discoverRooms(local, progress, attempt, live) {
|
|
1523
1805
|
if (closed || authorization === undefined) {
|
|
1524
1806
|
notifyReady();
|
|
1525
1807
|
return;
|
|
1526
1808
|
}
|
|
1527
1809
|
moveTo(transferring(status));
|
|
1810
|
+
discoveries += 1;
|
|
1811
|
+
const started = discoveries;
|
|
1528
1812
|
let result;
|
|
1529
1813
|
try {
|
|
1530
1814
|
result = await options.adapter.discover(authorization);
|
|
@@ -1532,7 +1816,8 @@ export function composeRuntime(options) {
|
|
|
1532
1816
|
catch {
|
|
1533
1817
|
result = { kind: 'transport-uncertainty' };
|
|
1534
1818
|
}
|
|
1535
|
-
if (
|
|
1819
|
+
if (!live()) {
|
|
1820
|
+
progress.complete = false;
|
|
1536
1821
|
notifyReady();
|
|
1537
1822
|
return;
|
|
1538
1823
|
}
|
|
@@ -1602,8 +1887,7 @@ export function composeRuntime(options) {
|
|
|
1602
1887
|
member = signedIn;
|
|
1603
1888
|
emitMember();
|
|
1604
1889
|
}
|
|
1605
|
-
await applyDiscovery(result.rooms, local);
|
|
1606
|
-
await synchronize(result.rooms, local, progress, attempt);
|
|
1890
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
1607
1891
|
}
|
|
1608
1892
|
async function ensureAuthorization() {
|
|
1609
1893
|
if (authorization !== undefined)
|
|
@@ -1617,20 +1901,50 @@ export function composeRuntime(options) {
|
|
|
1617
1901
|
throw error;
|
|
1618
1902
|
}
|
|
1619
1903
|
}
|
|
1620
|
-
function
|
|
1904
|
+
async function admittedRead(work) {
|
|
1905
|
+
if (roomReads.generation !== generation) {
|
|
1906
|
+
roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
1907
|
+
}
|
|
1908
|
+
const reads = roomReads;
|
|
1909
|
+
while (reads.free === 0)
|
|
1910
|
+
await new Promise((admit) => reads.waiting.push(admit));
|
|
1911
|
+
reads.free -= 1;
|
|
1912
|
+
try {
|
|
1913
|
+
await work();
|
|
1914
|
+
}
|
|
1915
|
+
finally {
|
|
1916
|
+
reads.free += 1;
|
|
1917
|
+
reads.waiting.shift()?.();
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
function roomTask(locator, work) {
|
|
1921
|
+
return task(roomLane(locator), (live) => admittedRead(() => work(live)));
|
|
1922
|
+
}
|
|
1923
|
+
function task(lane, work) {
|
|
1621
1924
|
if (stopping)
|
|
1622
|
-
return
|
|
1623
|
-
|
|
1624
|
-
|
|
1925
|
+
return Promise.resolve();
|
|
1926
|
+
const held = lanes.get(lane);
|
|
1927
|
+
const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
|
|
1928
|
+
const turn = current.generation;
|
|
1929
|
+
const live = () => turn === generation && continuing();
|
|
1930
|
+
const run = current.chain.then(() => work(live)).catch(() => undefined);
|
|
1931
|
+
current.chain = run;
|
|
1932
|
+
lanes.set(lane, current);
|
|
1933
|
+
working.add(run);
|
|
1934
|
+
void run.finally(() => working.delete(run));
|
|
1935
|
+
return run;
|
|
1625
1936
|
}
|
|
1626
1937
|
function sync(attempt) {
|
|
1627
|
-
return task(async () => {
|
|
1628
|
-
if (
|
|
1938
|
+
return task(OPENING_LANE, async (live) => {
|
|
1939
|
+
if (!live()) {
|
|
1629
1940
|
notifyReady();
|
|
1630
1941
|
return;
|
|
1631
1942
|
}
|
|
1632
1943
|
const progress = { complete: true, reachable: true };
|
|
1633
|
-
|
|
1944
|
+
const gate = discover();
|
|
1945
|
+
if (attempt === 'opened')
|
|
1946
|
+
opened = generation;
|
|
1947
|
+
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
1634
1948
|
if (attempt === 'opened')
|
|
1635
1949
|
streamed = progress;
|
|
1636
1950
|
complete = false;
|
|
@@ -1639,12 +1953,12 @@ export function composeRuntime(options) {
|
|
|
1639
1953
|
handed = false;
|
|
1640
1954
|
try {
|
|
1641
1955
|
const local = await openLocal();
|
|
1642
|
-
if (
|
|
1956
|
+
if (!live())
|
|
1643
1957
|
return;
|
|
1644
1958
|
await ensureAuthorization();
|
|
1645
|
-
if (
|
|
1959
|
+
if (!live())
|
|
1646
1960
|
return;
|
|
1647
|
-
await discoverRooms(local, progress, attempt);
|
|
1961
|
+
await discoverRooms(local, progress, attempt, live);
|
|
1648
1962
|
}
|
|
1649
1963
|
catch {
|
|
1650
1964
|
progress.complete = false;
|
|
@@ -1652,17 +1966,21 @@ export function composeRuntime(options) {
|
|
|
1652
1966
|
progress.reachable = false;
|
|
1653
1967
|
}
|
|
1654
1968
|
finally {
|
|
1969
|
+
gate.release();
|
|
1655
1970
|
notifyReady();
|
|
1656
1971
|
}
|
|
1972
|
+
if (!live())
|
|
1973
|
+
return;
|
|
1657
1974
|
if (attempt !== 'opened') {
|
|
1658
|
-
await finishOpening(progress);
|
|
1975
|
+
await finishOpening(progress, live);
|
|
1659
1976
|
return;
|
|
1660
1977
|
}
|
|
1661
1978
|
if (handed || closed || progress.reachable)
|
|
1662
1979
|
return;
|
|
1663
1980
|
underway = null;
|
|
1981
|
+
opened = null;
|
|
1664
1982
|
journal.record('connection reopen');
|
|
1665
|
-
await rest(progress);
|
|
1983
|
+
await rest(progress, live);
|
|
1666
1984
|
options.adapter.roomStream?.reopen();
|
|
1667
1985
|
});
|
|
1668
1986
|
}
|
|
@@ -1683,24 +2001,38 @@ export function composeRuntime(options) {
|
|
|
1683
2001
|
});
|
|
1684
2002
|
}
|
|
1685
2003
|
function applyOpening(expired) {
|
|
1686
|
-
|
|
1687
|
-
|
|
2004
|
+
const progress = underway?.generation === generation ? underway.progress : null;
|
|
2005
|
+
const cycle = progress ?? { complete: true, reachable: true };
|
|
2006
|
+
const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
|
|
2007
|
+
return task(OPENING_LANE, async (live) => {
|
|
1688
2008
|
underway = null;
|
|
1689
|
-
if (
|
|
2009
|
+
if (!live())
|
|
1690
2010
|
return;
|
|
1691
2011
|
if (progress === null) {
|
|
1692
|
-
await
|
|
2012
|
+
await replacing;
|
|
2013
|
+
await rest(cycle, live);
|
|
1693
2014
|
return;
|
|
1694
2015
|
}
|
|
2016
|
+
await roomsCarried(expired);
|
|
2017
|
+
if (!live())
|
|
2018
|
+
return;
|
|
2019
|
+
if (opened === generation)
|
|
2020
|
+
opened = null;
|
|
2021
|
+
const replaced = new Set(owed);
|
|
2022
|
+
owed.clear();
|
|
1695
2023
|
try {
|
|
1696
2024
|
const local = await openLocal();
|
|
1697
2025
|
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
1698
|
-
const reloading = held.filter((room) => expired.includes(room.locator)
|
|
1699
|
-
|
|
2026
|
+
const reloading = held.filter((room) => !expired.includes(room.locator) &&
|
|
2027
|
+
(replaced.has(room.locator) || revoked(room.locator)));
|
|
2028
|
+
const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
|
|
2029
|
+
const reloaded = [...(await reloads), ...(await replacing)];
|
|
1700
2030
|
if (progress.reachable) {
|
|
1701
|
-
for (const room of held)
|
|
1702
|
-
if (!reloading.includes(room))
|
|
2031
|
+
for (const room of held) {
|
|
2032
|
+
if (!expired.includes(room.locator) && !reloading.includes(room)) {
|
|
1703
2033
|
await confirmModels(room, local);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
1704
2036
|
}
|
|
1705
2037
|
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1706
2038
|
if (failed !== undefined)
|
|
@@ -1710,21 +2042,21 @@ export function composeRuntime(options) {
|
|
|
1710
2042
|
progress.complete = false;
|
|
1711
2043
|
progress.reachable = false;
|
|
1712
2044
|
}
|
|
1713
|
-
|
|
2045
|
+
if (live())
|
|
2046
|
+
await finishOpening(progress, live);
|
|
1714
2047
|
});
|
|
1715
2048
|
}
|
|
1716
|
-
async function
|
|
1717
|
-
const progress = { complete: true, reachable: true };
|
|
2049
|
+
async function reloadNow(replaced, progress) {
|
|
1718
2050
|
try {
|
|
1719
2051
|
const local = await openLocal();
|
|
1720
|
-
await Promise.allSettled([...discovered.values()]
|
|
1721
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
1722
|
-
.map((room) => reload(room, local, progress)));
|
|
2052
|
+
return await Promise.allSettled([...discovered.values()]
|
|
2053
|
+
.filter((room) => replicas.has(room.locator) && replaced(room))
|
|
2054
|
+
.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
1723
2055
|
}
|
|
1724
2056
|
catch {
|
|
1725
2057
|
progress.reachable = false;
|
|
2058
|
+
return [];
|
|
1726
2059
|
}
|
|
1727
|
-
await rest(progress);
|
|
1728
2060
|
}
|
|
1729
2061
|
async function confirmModels(room, local) {
|
|
1730
2062
|
for (const attached of ['collection', 'typed-selection']) {
|
|
@@ -1776,9 +2108,9 @@ export function composeRuntime(options) {
|
|
|
1776
2108
|
submitLater(locator);
|
|
1777
2109
|
}
|
|
1778
2110
|
}
|
|
1779
|
-
async function finishOpening(progress) {
|
|
2111
|
+
async function finishOpening(progress, live) {
|
|
1780
2112
|
try {
|
|
1781
|
-
if (
|
|
2113
|
+
if (!live()) {
|
|
1782
2114
|
progress.complete = false;
|
|
1783
2115
|
return;
|
|
1784
2116
|
}
|
|
@@ -1788,6 +2120,8 @@ export function composeRuntime(options) {
|
|
|
1788
2120
|
conversations.reconnected();
|
|
1789
2121
|
const reading = { complete: true, reachable: true };
|
|
1790
2122
|
await conversations.catchUp(reading);
|
|
2123
|
+
if (progress.reachable && reading.reachable)
|
|
2124
|
+
await conversations.resend(reading);
|
|
1791
2125
|
if (!reading.reachable)
|
|
1792
2126
|
progress.reachable = false;
|
|
1793
2127
|
}
|
|
@@ -1795,12 +2129,14 @@ export function composeRuntime(options) {
|
|
|
1795
2129
|
progress.complete = false;
|
|
1796
2130
|
}
|
|
1797
2131
|
finally {
|
|
1798
|
-
|
|
1799
|
-
|
|
2132
|
+
if (live()) {
|
|
2133
|
+
complete = progress.complete;
|
|
2134
|
+
await rest(progress, live);
|
|
2135
|
+
}
|
|
1800
2136
|
}
|
|
1801
2137
|
}
|
|
1802
|
-
async function rest(progress) {
|
|
1803
|
-
if (
|
|
2138
|
+
async function rest(progress, live = () => !closed) {
|
|
2139
|
+
if (!live())
|
|
1804
2140
|
return;
|
|
1805
2141
|
let lastCompleted = status.lastCompleted;
|
|
1806
2142
|
let synced = complete && stale.size === 0 && !pending();
|
|
@@ -1808,6 +2144,8 @@ export function composeRuntime(options) {
|
|
|
1808
2144
|
const completedAt = new Date().toISOString();
|
|
1809
2145
|
try {
|
|
1810
2146
|
await (await openLocal()).complete(completedAt);
|
|
2147
|
+
if (!live())
|
|
2148
|
+
return;
|
|
1811
2149
|
recorded = true;
|
|
1812
2150
|
lastCompleted = completedAt;
|
|
1813
2151
|
}
|
|
@@ -1822,11 +2160,23 @@ export function composeRuntime(options) {
|
|
|
1822
2160
|
...attention(),
|
|
1823
2161
|
});
|
|
1824
2162
|
}
|
|
2163
|
+
function discover() {
|
|
2164
|
+
let release = () => { };
|
|
2165
|
+
const done = new Promise((resolve) => {
|
|
2166
|
+
release = resolve;
|
|
2167
|
+
});
|
|
2168
|
+
discovering = { generation, done, release };
|
|
2169
|
+
return discovering;
|
|
2170
|
+
}
|
|
1825
2171
|
function applyRoom(locator, roomVersion) {
|
|
1826
2172
|
announced.set(locator, roomVersion);
|
|
1827
|
-
return task(async () => {
|
|
1828
|
-
|
|
1829
|
-
|
|
2173
|
+
return task(roomLane(locator), async (live) => {
|
|
2174
|
+
if (discovering?.generation === generation)
|
|
2175
|
+
await discovering.done;
|
|
2176
|
+
const saving = answering.get(locator);
|
|
2177
|
+
if (saving?.generation === generation)
|
|
2178
|
+
await saving.answered;
|
|
2179
|
+
if (!live() || authorization === undefined)
|
|
1830
2180
|
return;
|
|
1831
2181
|
const room = discovered.get(locator);
|
|
1832
2182
|
const held = replicas.get(locator);
|
|
@@ -1837,25 +2187,41 @@ export function composeRuntime(options) {
|
|
|
1837
2187
|
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
1838
2188
|
return;
|
|
1839
2189
|
}
|
|
1840
|
-
const during = underway;
|
|
2190
|
+
const during = underway?.generation === generation ? underway.progress : null;
|
|
1841
2191
|
const progress = during ?? { complete: true, reachable: true };
|
|
1842
2192
|
if (during === null)
|
|
1843
2193
|
moveTo(transferring(status));
|
|
1844
2194
|
try {
|
|
1845
2195
|
const local = await openLocal();
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
2196
|
+
await admittedRead(async () => {
|
|
2197
|
+
if (!live() || authorization === undefined)
|
|
2198
|
+
return;
|
|
2199
|
+
const withheld = await advance(room, local, authorization, live);
|
|
2200
|
+
if (withheld !== null)
|
|
2201
|
+
await withhold(withheld, room, local, progress);
|
|
2202
|
+
});
|
|
1849
2203
|
}
|
|
1850
2204
|
catch {
|
|
1851
2205
|
stale.add(locator);
|
|
1852
2206
|
}
|
|
1853
2207
|
finally {
|
|
1854
2208
|
if (during === null)
|
|
1855
|
-
await rest(progress);
|
|
2209
|
+
await rest(progress, live);
|
|
1856
2210
|
}
|
|
1857
2211
|
});
|
|
1858
2212
|
}
|
|
2213
|
+
function roomsCarried(replacing) {
|
|
2214
|
+
const replaced = new Set(replacing.map(roomLane));
|
|
2215
|
+
return Promise.all([...lanes]
|
|
2216
|
+
.filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
|
|
2217
|
+
.map(([, held]) => held.chain));
|
|
2218
|
+
}
|
|
2219
|
+
const receipts = {
|
|
2220
|
+
heads: async () => (await openLocal()).receivedHeads(),
|
|
2221
|
+
parts: async (key, count) => (await openLocal()).receivedParts(key, count),
|
|
2222
|
+
keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
|
|
2223
|
+
drop: async (keys) => (await openLocal()).dropReceived(keys),
|
|
2224
|
+
};
|
|
1859
2225
|
function observeStream() {
|
|
1860
2226
|
const stream = options.adapter.roomStream;
|
|
1861
2227
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -1865,6 +2231,7 @@ export function composeRuntime(options) {
|
|
|
1865
2231
|
return;
|
|
1866
2232
|
if (event.kind === 'opened') {
|
|
1867
2233
|
socketOpen = true;
|
|
2234
|
+
generation += 1;
|
|
1868
2235
|
void sync('opened');
|
|
1869
2236
|
}
|
|
1870
2237
|
else if (event.kind === 'closed')
|
|
@@ -1873,7 +2240,7 @@ export function composeRuntime(options) {
|
|
|
1873
2240
|
void applyOpening(event.expired);
|
|
1874
2241
|
else
|
|
1875
2242
|
void applyRoom(event.locator, event.roomVersion);
|
|
1876
|
-
});
|
|
2243
|
+
}, receipts);
|
|
1877
2244
|
}
|
|
1878
2245
|
function writesHeld() {
|
|
1879
2246
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -1911,14 +2278,15 @@ export function composeRuntime(options) {
|
|
|
1911
2278
|
});
|
|
1912
2279
|
}
|
|
1913
2280
|
function scheduleCurrency() {
|
|
1914
|
-
if (currencyQueued || closed || stopping)
|
|
2281
|
+
if (currencyQueued === generation || closed || stopping)
|
|
1915
2282
|
return;
|
|
1916
|
-
currencyQueued =
|
|
1917
|
-
void task(runCurrency);
|
|
2283
|
+
currencyQueued = generation;
|
|
2284
|
+
void task(CONVERSATION_LANE, runCurrency);
|
|
1918
2285
|
}
|
|
1919
|
-
async function runCurrency() {
|
|
1920
|
-
currencyQueued
|
|
1921
|
-
|
|
2286
|
+
async function runCurrency(live) {
|
|
2287
|
+
if (currencyQueued === generation)
|
|
2288
|
+
currencyQueued = null;
|
|
2289
|
+
if (!live())
|
|
1922
2290
|
return;
|
|
1923
2291
|
await conversations.reconcile();
|
|
1924
2292
|
if (conversations.current()) {
|
|
@@ -1933,7 +2301,7 @@ export function composeRuntime(options) {
|
|
|
1933
2301
|
catch {
|
|
1934
2302
|
progress.reachable = false;
|
|
1935
2303
|
}
|
|
1936
|
-
await rest(progress);
|
|
2304
|
+
await rest(progress, live);
|
|
1937
2305
|
}
|
|
1938
2306
|
function close() {
|
|
1939
2307
|
if (closed)
|
|
@@ -1964,7 +2332,7 @@ export function composeRuntime(options) {
|
|
|
1964
2332
|
async function stop() {
|
|
1965
2333
|
stopping = true;
|
|
1966
2334
|
stopTimer();
|
|
1967
|
-
await
|
|
2335
|
+
await Promise.all(working);
|
|
1968
2336
|
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
1969
2337
|
await mutations;
|
|
1970
2338
|
close();
|
|
@@ -2022,6 +2390,8 @@ export function composeRuntime(options) {
|
|
|
2022
2390
|
}
|
|
2023
2391
|
if (topic === 'conversations')
|
|
2024
2392
|
return conversations.watchList(deliver);
|
|
2393
|
+
if (topic === 'agents')
|
|
2394
|
+
return conversations.watchAgents(deliver);
|
|
2025
2395
|
if (topic === 'presentation') {
|
|
2026
2396
|
const listener = {
|
|
2027
2397
|
app: input[0],
|
|
@@ -2135,11 +2505,14 @@ export function composeRuntime(options) {
|
|
|
2135
2505
|
if ((current?.revision ?? null) !== revision) {
|
|
2136
2506
|
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2137
2507
|
}
|
|
2508
|
+
const file = parts.file === undefined
|
|
2509
|
+
? undefined
|
|
2510
|
+
: await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
|
|
2138
2511
|
const record = {
|
|
2139
2512
|
locator: room,
|
|
2140
2513
|
path,
|
|
2141
2514
|
revision: roomDraftRevision(),
|
|
2142
|
-
parts: held(current?.parts, parts),
|
|
2515
|
+
parts: held(current?.parts, parts, file),
|
|
2143
2516
|
returned: current?.returned ?? null,
|
|
2144
2517
|
};
|
|
2145
2518
|
await local.keepRoomDraft(record);
|
|
@@ -2188,6 +2561,10 @@ export function composeRuntime(options) {
|
|
|
2188
2561
|
history(locator, path, query) {
|
|
2189
2562
|
return readHistory(locator, path, query);
|
|
2190
2563
|
},
|
|
2564
|
+
binaryFile: {
|
|
2565
|
+
read: readBinaryPath,
|
|
2566
|
+
readReference: readBinaryReference,
|
|
2567
|
+
},
|
|
2191
2568
|
call(name, input) {
|
|
2192
2569
|
return invokeAction(name, input);
|
|
2193
2570
|
},
|
|
@@ -2207,23 +2584,26 @@ export function composeRuntime(options) {
|
|
|
2207
2584
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2208
2585
|
return;
|
|
2209
2586
|
journal.record('connection reauthorize');
|
|
2210
|
-
await
|
|
2587
|
+
await Promise.all([...lanes.values()]
|
|
2588
|
+
.filter((held) => held.generation === generation)
|
|
2589
|
+
.map((held) => held.chain));
|
|
2211
2590
|
await mutations;
|
|
2212
2591
|
authorization = undefined;
|
|
2213
2592
|
discoveryPaused = false;
|
|
2214
2593
|
if (options.adapter.roomStream === undefined)
|
|
2215
2594
|
await sync('unstreamed');
|
|
2216
2595
|
else
|
|
2217
|
-
await task(ensureAuthorization);
|
|
2596
|
+
await task(OPENING_LANE, ensureAuthorization);
|
|
2218
2597
|
},
|
|
2219
2598
|
};
|
|
2220
2599
|
const reachable = {
|
|
2221
2600
|
md,
|
|
2222
2601
|
watch,
|
|
2602
|
+
referrerScope: (referrer) => options.adapter.referrerScope(referrer),
|
|
2223
2603
|
plan: planned,
|
|
2224
2604
|
query: (plan) => rowsQuery(plan),
|
|
2225
2605
|
watchPlan,
|
|
2226
2606
|
search: searched,
|
|
2227
2607
|
};
|
|
2228
|
-
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2608
|
+
return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2229
2609
|
}
|