@agentshouse/mdruntime 0.1.0-c99e5271060cd → 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 +588 -205
- 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/status.d.ts +0 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +0 -8
- 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';
|
|
19
|
-
import {
|
|
20
|
+
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
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;
|
|
@@ -87,13 +104,20 @@ export function composeRuntime(options) {
|
|
|
87
104
|
let withdrawStream;
|
|
88
105
|
const announced = new Map();
|
|
89
106
|
let underway = null;
|
|
107
|
+
let streamed = null;
|
|
90
108
|
let handed = false;
|
|
91
109
|
const submitting = new Map();
|
|
92
110
|
const answering = new Map();
|
|
93
111
|
let complete = false;
|
|
94
112
|
let recorded = false;
|
|
95
113
|
const stale = new Set();
|
|
96
|
-
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();
|
|
97
121
|
let mutations = Promise.resolve();
|
|
98
122
|
const roomListeners = new Set();
|
|
99
123
|
const refusalListeners = new Set();
|
|
@@ -104,7 +128,7 @@ export function composeRuntime(options) {
|
|
|
104
128
|
const presentationListeners = new Set();
|
|
105
129
|
const readSide = createReadSide();
|
|
106
130
|
let withdrawConversations;
|
|
107
|
-
let currencyQueued =
|
|
131
|
+
let currencyQueued = null;
|
|
108
132
|
let socketOpen = false;
|
|
109
133
|
let serviceMode = 'open';
|
|
110
134
|
let haltedRefusal = false;
|
|
@@ -114,7 +138,7 @@ export function composeRuntime(options) {
|
|
|
114
138
|
const memberListeners = new Set();
|
|
115
139
|
const materialized = new Set();
|
|
116
140
|
let everyRoom = false;
|
|
117
|
-
const transfers = new
|
|
141
|
+
const transfers = new FencedMap(isFenced);
|
|
118
142
|
const conversations = createConversationStore({
|
|
119
143
|
agents: declaredAgents,
|
|
120
144
|
documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
|
|
@@ -122,6 +146,7 @@ export function composeRuntime(options) {
|
|
|
122
146
|
durably,
|
|
123
147
|
stream: () => options.adapter.conversations,
|
|
124
148
|
publish: publishChangeSet,
|
|
149
|
+
online: () => !status.providerUnreachable && refusal === null,
|
|
125
150
|
closed: () => closed,
|
|
126
151
|
});
|
|
127
152
|
const searchIndex = createSearchIndex({
|
|
@@ -193,7 +218,9 @@ export function composeRuntime(options) {
|
|
|
193
218
|
}
|
|
194
219
|
}
|
|
195
220
|
function pending() {
|
|
196
|
-
return !localSaves &&
|
|
221
|
+
return (!localSaves &&
|
|
222
|
+
(queuesWithoutReplica.size > 0 ||
|
|
223
|
+
[...replicas.values()].some((replica) => replica.queue.length > 0)));
|
|
197
224
|
}
|
|
198
225
|
function attention() {
|
|
199
226
|
const attentionRequired = [];
|
|
@@ -227,12 +254,11 @@ export function composeRuntime(options) {
|
|
|
227
254
|
if (replica === undefined)
|
|
228
255
|
return 0;
|
|
229
256
|
const roots = replica.confirmed.record.typedSelection?.roots ?? [];
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
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;
|
|
236
262
|
}
|
|
237
263
|
function listed() {
|
|
238
264
|
return structuredClone([...rooms.values()].map((room) => {
|
|
@@ -303,10 +329,7 @@ export function composeRuntime(options) {
|
|
|
303
329
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
304
330
|
}
|
|
305
331
|
function entriesOf(locator) {
|
|
306
|
-
|
|
307
|
-
if (replica === undefined)
|
|
308
|
-
return [];
|
|
309
|
-
return replica.queue.map(entryOf);
|
|
332
|
+
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
|
|
310
333
|
}
|
|
311
334
|
function emitQueue(locator) {
|
|
312
335
|
const entries = entriesOf(locator);
|
|
@@ -403,10 +426,7 @@ export function composeRuntime(options) {
|
|
|
403
426
|
const revoked = { status: 'unavailable', reason: 'revoked' };
|
|
404
427
|
try {
|
|
405
428
|
const local = await openLocal();
|
|
406
|
-
await local.revokeModels([...replicas.values()].map((replica) =>
|
|
407
|
-
record: replica.confirmed.record,
|
|
408
|
-
queue: replica.queue,
|
|
409
|
-
})));
|
|
429
|
+
await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
|
|
410
430
|
}
|
|
411
431
|
catch {
|
|
412
432
|
journal.record('model revocation persistence failed');
|
|
@@ -473,19 +493,28 @@ export function composeRuntime(options) {
|
|
|
473
493
|
},
|
|
474
494
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
475
495
|
roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
|
|
476
|
-
attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
|
|
477
496
|
synchronizationAvailable,
|
|
478
497
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
479
498
|
});
|
|
480
499
|
}
|
|
481
|
-
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));
|
|
482
507
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
483
508
|
for (const locator of listedLocators)
|
|
484
509
|
scopedRefusals.delete(locator);
|
|
485
510
|
discovered.clear();
|
|
486
511
|
for (const room of listed)
|
|
487
512
|
discovered.set(room.locator, room);
|
|
488
|
-
|
|
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);
|
|
489
518
|
for (const room of listed) {
|
|
490
519
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
491
520
|
if (held !== undefined && held.replica.selection !== room.replica.selection) {
|
|
@@ -496,44 +525,79 @@ export function composeRuntime(options) {
|
|
|
496
525
|
}
|
|
497
526
|
roomsListed = true;
|
|
498
527
|
emitRooms();
|
|
528
|
+
return listed;
|
|
499
529
|
}
|
|
500
530
|
function unlistedHeld(listed) {
|
|
501
|
-
return [
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
pendings.delete(batch.id);
|
|
517
|
-
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
|
|
518
546
|
rooms.delete(locator);
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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);
|
|
533
578
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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));
|
|
537
601
|
}
|
|
538
602
|
function restored(key, persisted) {
|
|
539
603
|
const prior = models.get(key);
|
|
@@ -588,6 +652,12 @@ export function composeRuntime(options) {
|
|
|
588
652
|
}
|
|
589
653
|
const previousReplicas = new Map(replicas);
|
|
590
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
|
+
}
|
|
591
661
|
for (const record of records) {
|
|
592
662
|
const model = restored(modelKey(record.locator, 'collection'), record.model);
|
|
593
663
|
const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
|
|
@@ -601,7 +671,6 @@ export function composeRuntime(options) {
|
|
|
601
671
|
rooms.set(record.locator, {
|
|
602
672
|
...existing,
|
|
603
673
|
roomVersion: record.roomVersion,
|
|
604
|
-
attachments: record.attachments,
|
|
605
674
|
operations: record.operations,
|
|
606
675
|
replica: record.replica,
|
|
607
676
|
model: modelTypes(model),
|
|
@@ -633,8 +702,15 @@ export function composeRuntime(options) {
|
|
|
633
702
|
return lastCompleted;
|
|
634
703
|
}
|
|
635
704
|
async function open() {
|
|
636
|
-
|
|
705
|
+
if (closed)
|
|
706
|
+
throw new Error('runtime closed');
|
|
707
|
+
const opened = await openRuntimeStore(isFenced);
|
|
637
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
|
+
}
|
|
638
714
|
const kept = options.shell.generation;
|
|
639
715
|
const storedGeneration = await opened.generation();
|
|
640
716
|
const loss = detectStorageLoss(storedGeneration, kept);
|
|
@@ -648,6 +724,8 @@ export function composeRuntime(options) {
|
|
|
648
724
|
emitStorageLoss();
|
|
649
725
|
}
|
|
650
726
|
const lastCompleted = await readState(opened);
|
|
727
|
+
if (interrupted.length > 0)
|
|
728
|
+
await erased(opened, interrupted);
|
|
651
729
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
652
730
|
return opened;
|
|
653
731
|
}
|
|
@@ -674,6 +752,7 @@ export function composeRuntime(options) {
|
|
|
674
752
|
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
675
753
|
}
|
|
676
754
|
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
755
|
+
const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
677
756
|
const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
|
|
678
757
|
const kept = queue.filter((batch) => !withdrawn.includes(batch));
|
|
679
758
|
const unqueued = withdrawn.map((batch) => ({
|
|
@@ -699,20 +778,36 @@ export function composeRuntime(options) {
|
|
|
699
778
|
});
|
|
700
779
|
}
|
|
701
780
|
}
|
|
702
|
-
await local.
|
|
703
|
-
|
|
704
|
-
previous: before?.confirmed.record ?? null,
|
|
705
|
-
...(settling.stored === undefined ? {} : { stored: settling.stored }),
|
|
706
|
-
queue: kept,
|
|
707
|
-
previousQueue: before?.queue ?? [],
|
|
708
|
-
roomDrafts: [...written.values()],
|
|
709
|
-
});
|
|
710
|
-
for (const batch of before?.queue ?? []) {
|
|
781
|
+
await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
|
|
782
|
+
for (const batch of previousQueue) {
|
|
711
783
|
if (!kept.includes(batch))
|
|
712
784
|
pendings.delete(batch.id);
|
|
713
785
|
}
|
|
714
786
|
for (const record of written.values())
|
|
715
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);
|
|
716
811
|
for (const { batch, returned } of unqueued) {
|
|
717
812
|
const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
|
|
718
813
|
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
@@ -720,7 +815,7 @@ export function composeRuntime(options) {
|
|
|
720
815
|
replicas.set(locator, replicaOf(confirmed, kept, before));
|
|
721
816
|
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
722
817
|
room: settling.room,
|
|
723
|
-
queue: kept.length !==
|
|
818
|
+
queue: kept.length !== previousQueue.length,
|
|
724
819
|
drafts: [...written.keys()],
|
|
725
820
|
});
|
|
726
821
|
}
|
|
@@ -755,10 +850,10 @@ export function composeRuntime(options) {
|
|
|
755
850
|
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
756
851
|
};
|
|
757
852
|
}
|
|
758
|
-
function publish(local, room, confirmed, landing
|
|
853
|
+
function publish(local, room, confirmed, landing, live) {
|
|
759
854
|
const { base, stored } = landing;
|
|
760
855
|
return exclusive(async () => {
|
|
761
|
-
if (!
|
|
856
|
+
if (!live())
|
|
762
857
|
return true;
|
|
763
858
|
const prior = replicas.get(room.locator);
|
|
764
859
|
if (base !== undefined &&
|
|
@@ -767,7 +862,7 @@ export function composeRuntime(options) {
|
|
|
767
862
|
return false;
|
|
768
863
|
}
|
|
769
864
|
adoptModels(room.locator, confirmed.record);
|
|
770
|
-
const queue = prior?.queue ?? [];
|
|
865
|
+
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
771
866
|
const roomVersion = confirmed.record.roomVersion;
|
|
772
867
|
const accepted = queue.filter((batch) => {
|
|
773
868
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
@@ -786,6 +881,7 @@ export function composeRuntime(options) {
|
|
|
786
881
|
list(room, true);
|
|
787
882
|
publishChangeSet(set);
|
|
788
883
|
moveTo({ ...status, ...attention() });
|
|
884
|
+
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
789
885
|
return true;
|
|
790
886
|
});
|
|
791
887
|
}
|
|
@@ -809,7 +905,24 @@ export function composeRuntime(options) {
|
|
|
809
905
|
if (changed.length > 0)
|
|
810
906
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
811
907
|
const parts = saved.map(({ path, parts }) => changesOf(path, parts));
|
|
812
|
-
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 = {
|
|
813
926
|
local: localSaves,
|
|
814
927
|
baseRoomVersion: replica.confirmed.record.roomVersion,
|
|
815
928
|
composition: compositionFor(replica.confirmed.record),
|
|
@@ -818,28 +931,86 @@ export function composeRuntime(options) {
|
|
|
818
931
|
providerLimits: limits,
|
|
819
932
|
handle: room.handle,
|
|
820
933
|
visible: replica.visible,
|
|
821
|
-
changes
|
|
934
|
+
changes,
|
|
822
935
|
bases: parts.flatMap(({ bases }) => bases),
|
|
823
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 }];
|
|
824
944
|
});
|
|
825
|
-
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')
|
|
826
963
|
return { outcome: 'refused', ...built.refusal };
|
|
827
|
-
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
|
+
}
|
|
828
995
|
const consumed = saved.map(({ path }) => path);
|
|
829
996
|
try {
|
|
830
|
-
await local.
|
|
831
|
-
|
|
832
|
-
previous: replica.confirmed.record,
|
|
997
|
+
await local.keepAuthored({
|
|
998
|
+
locator,
|
|
833
999
|
queue: batches,
|
|
834
|
-
|
|
835
|
-
saved,
|
|
1000
|
+
saved: drafted,
|
|
836
1001
|
consumedRoomDrafts: consumed,
|
|
837
1002
|
});
|
|
838
1003
|
}
|
|
839
1004
|
catch {
|
|
840
1005
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
841
1006
|
}
|
|
842
|
-
|
|
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
|
+
}
|
|
843
1014
|
if (!localSaves)
|
|
844
1015
|
recorded = false;
|
|
845
1016
|
for (const path of consumed)
|
|
@@ -851,7 +1022,10 @@ export function composeRuntime(options) {
|
|
|
851
1022
|
}));
|
|
852
1023
|
moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
|
|
853
1024
|
submitLater(locator);
|
|
854
|
-
|
|
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 };
|
|
855
1029
|
}
|
|
856
1030
|
async function mark(local, locator, identifier, submission, lost) {
|
|
857
1031
|
const state = pendings.get(identifier);
|
|
@@ -861,6 +1035,8 @@ export function composeRuntime(options) {
|
|
|
861
1035
|
return;
|
|
862
1036
|
}
|
|
863
1037
|
await local.submit(locator, identifier, submission, lost);
|
|
1038
|
+
if (fences.has(locator))
|
|
1039
|
+
return;
|
|
864
1040
|
pendings.set(identifier, { ...state, submission, lost });
|
|
865
1041
|
const replica = replicas.get(locator);
|
|
866
1042
|
if (replica !== undefined)
|
|
@@ -871,6 +1047,8 @@ export function composeRuntime(options) {
|
|
|
871
1047
|
if (!continuing())
|
|
872
1048
|
throw new Error('connection refused');
|
|
873
1049
|
await local.keepUpload(locator, identifier, upload);
|
|
1050
|
+
if (fences.has(locator))
|
|
1051
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
874
1052
|
pendings.set(identifier, { ...pendings.get(identifier), upload });
|
|
875
1053
|
if (!continuing())
|
|
876
1054
|
throw new Error('connection refused');
|
|
@@ -886,6 +1064,7 @@ export function composeRuntime(options) {
|
|
|
886
1064
|
}
|
|
887
1065
|
if (closed || authorization === undefined || refusal !== null)
|
|
888
1066
|
return UNCERTAIN;
|
|
1067
|
+
const since = fencings;
|
|
889
1068
|
const transfer = ['transfer', 'history', locator, path];
|
|
890
1069
|
if (query.revision !== undefined)
|
|
891
1070
|
transfer.push(query.revision);
|
|
@@ -900,6 +1079,8 @@ export function composeRuntime(options) {
|
|
|
900
1079
|
...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
|
|
901
1080
|
});
|
|
902
1081
|
journal.record(`outcome history ${result.kind}`);
|
|
1082
|
+
if (fencedSince(locator, since))
|
|
1083
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
903
1084
|
if (result.kind === 'scoped-refusal') {
|
|
904
1085
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
905
1086
|
if (room !== undefined)
|
|
@@ -914,6 +1095,100 @@ export function composeRuntime(options) {
|
|
|
914
1095
|
return UNCERTAIN;
|
|
915
1096
|
}
|
|
916
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
|
+
}
|
|
917
1192
|
function declaredAgents() {
|
|
918
1193
|
for (const capability of capabilities) {
|
|
919
1194
|
if (capability.conversationAgents !== undefined)
|
|
@@ -978,11 +1253,11 @@ export function composeRuntime(options) {
|
|
|
978
1253
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
979
1254
|
!options.adapter.reached(locator, roomVersion, written));
|
|
980
1255
|
}
|
|
981
|
-
async function pullRefresh(authorized, confirmed) {
|
|
982
|
-
if (!
|
|
1256
|
+
async function pullRefresh(authorized, confirmed, live) {
|
|
1257
|
+
if (!live())
|
|
983
1258
|
return unconfirmed;
|
|
984
1259
|
try {
|
|
985
|
-
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);
|
|
986
1261
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
987
1262
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
988
1263
|
return result;
|
|
@@ -992,33 +1267,39 @@ export function composeRuntime(options) {
|
|
|
992
1267
|
throw error;
|
|
993
1268
|
}
|
|
994
1269
|
}
|
|
995
|
-
function staging(local) {
|
|
1270
|
+
function staging(local, live) {
|
|
996
1271
|
return {
|
|
997
1272
|
held: (locator) => transfers.get(locator) ?? null,
|
|
998
1273
|
staged: (record) => local.staged(record),
|
|
999
1274
|
stage: (record, page) => exclusive(async () => {
|
|
1000
|
-
if (!
|
|
1275
|
+
if (!live())
|
|
1001
1276
|
return false;
|
|
1002
1277
|
await local.stage(record, page);
|
|
1003
1278
|
transfers.set(record.locator, record);
|
|
1004
1279
|
return true;
|
|
1005
1280
|
}),
|
|
1006
1281
|
abandon: (locator) => exclusive(async () => {
|
|
1007
|
-
if (!
|
|
1282
|
+
if (!live())
|
|
1008
1283
|
return;
|
|
1009
1284
|
await local.abandon(locator);
|
|
1010
1285
|
transfers.delete(locator);
|
|
1011
1286
|
}),
|
|
1012
1287
|
};
|
|
1013
1288
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
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())
|
|
1016
1297
|
return unconfirmed;
|
|
1017
1298
|
try {
|
|
1018
1299
|
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
1019
1300
|
collection: modelOf(room.locator),
|
|
1020
1301
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
1021
|
-
}, (models, held) => remodel(room.locator, models, held),
|
|
1302
|
+
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
1022
1303
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
1023
1304
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
1024
1305
|
return result;
|
|
@@ -1028,11 +1309,11 @@ export function composeRuntime(options) {
|
|
|
1028
1309
|
throw error;
|
|
1029
1310
|
}
|
|
1030
1311
|
}
|
|
1031
|
-
async function advance(room, local, authorized) {
|
|
1312
|
+
async function advance(room, local, authorized, live) {
|
|
1032
1313
|
const held = replicas.get(room.locator);
|
|
1033
1314
|
if (held !== undefined) {
|
|
1034
|
-
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
1035
|
-
if (!
|
|
1315
|
+
const refreshed = await pullRefresh(authorized, held.confirmed, live);
|
|
1316
|
+
if (!live())
|
|
1036
1317
|
return unconfirmed;
|
|
1037
1318
|
if (refreshed.kind === 'still-current') {
|
|
1038
1319
|
stale.delete(room.locator);
|
|
@@ -1046,24 +1327,28 @@ export function composeRuntime(options) {
|
|
|
1046
1327
|
return null;
|
|
1047
1328
|
}
|
|
1048
1329
|
if (refreshed.kind === 'advanced') {
|
|
1049
|
-
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
|
|
1330
|
+
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
|
|
1050
1331
|
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1051
1332
|
const reached = now !== undefined &&
|
|
1052
1333
|
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
1053
|
-
return reached ? null : advance(room, local, authorized);
|
|
1334
|
+
return reached ? null : advance(room, local, authorized, live);
|
|
1054
1335
|
}
|
|
1055
1336
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1056
1337
|
return null;
|
|
1057
1338
|
}
|
|
1058
1339
|
if (refreshed.kind !== 'beyond-horizon')
|
|
1059
1340
|
return refreshed;
|
|
1341
|
+
if (opened !== null) {
|
|
1342
|
+
owed.add(room.locator);
|
|
1343
|
+
return unconfirmed;
|
|
1344
|
+
}
|
|
1060
1345
|
}
|
|
1061
|
-
const bootstrapped = await pullBootstrap(authorized, room, local);
|
|
1062
|
-
if (!
|
|
1346
|
+
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
1347
|
+
if (!live())
|
|
1063
1348
|
return unconfirmed;
|
|
1064
1349
|
if (bootstrapped.kind !== 'staged')
|
|
1065
1350
|
return bootstrapped;
|
|
1066
|
-
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
1351
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
1067
1352
|
return null;
|
|
1068
1353
|
}
|
|
1069
1354
|
async function withhold(withheld, room, local, progress) {
|
|
@@ -1137,13 +1422,13 @@ export function composeRuntime(options) {
|
|
|
1137
1422
|
logAccepted(room.locator, submitted, accepted);
|
|
1138
1423
|
const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
1139
1424
|
? { kind: 'staged', confirmed: current.confirmed }
|
|
1140
|
-
: applyReturned(current.confirmed,
|
|
1425
|
+
: applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
1141
1426
|
? accepted.roomVersion
|
|
1142
1427
|
: held, accepted.roomVersion);
|
|
1143
1428
|
if (applied.kind !== 'staged') {
|
|
1144
1429
|
const state = pendings.get(submitted.id);
|
|
1145
1430
|
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1146
|
-
if (state !== undefined)
|
|
1431
|
+
if (state !== undefined && !fences.has(room.locator))
|
|
1147
1432
|
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1148
1433
|
emitQueue(room.locator);
|
|
1149
1434
|
return applied;
|
|
@@ -1192,7 +1477,7 @@ export function composeRuntime(options) {
|
|
|
1192
1477
|
let result;
|
|
1193
1478
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
1194
1479
|
await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
|
|
1195
|
-
if (discoveryPaused)
|
|
1480
|
+
if (discoveryPaused || fences.has(room.locator))
|
|
1196
1481
|
return;
|
|
1197
1482
|
if (writesHeld()) {
|
|
1198
1483
|
await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
|
|
@@ -1255,7 +1540,7 @@ export function composeRuntime(options) {
|
|
|
1255
1540
|
result.kind === 'expired' ||
|
|
1256
1541
|
result.kind === 'retryable-refusal') {
|
|
1257
1542
|
if (result.kind === 'expired') {
|
|
1258
|
-
const withheld = await advance(room, local, authorized);
|
|
1543
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
1259
1544
|
if (withheld !== null)
|
|
1260
1545
|
return withhold(withheld, room, local, progress);
|
|
1261
1546
|
}
|
|
@@ -1293,7 +1578,7 @@ export function composeRuntime(options) {
|
|
|
1293
1578
|
});
|
|
1294
1579
|
if (result.kind === 'expired')
|
|
1295
1580
|
continue;
|
|
1296
|
-
const withheld = await advance(room, local, authorized);
|
|
1581
|
+
const withheld = await advance(room, local, authorized, continuing);
|
|
1297
1582
|
if (withheld !== null)
|
|
1298
1583
|
return withhold(withheld, room, local, progress);
|
|
1299
1584
|
continue;
|
|
@@ -1310,9 +1595,9 @@ export function composeRuntime(options) {
|
|
|
1310
1595
|
const awaited = new Promise((resolve) => {
|
|
1311
1596
|
release = resolve;
|
|
1312
1597
|
});
|
|
1313
|
-
answering.set(locator, awaited);
|
|
1598
|
+
answering.set(locator, { generation, answered: awaited });
|
|
1314
1599
|
return () => {
|
|
1315
|
-
if (answering.get(locator) === awaited)
|
|
1600
|
+
if (answering.get(locator)?.answered === awaited)
|
|
1316
1601
|
answering.delete(locator);
|
|
1317
1602
|
release();
|
|
1318
1603
|
};
|
|
@@ -1356,11 +1641,7 @@ export function composeRuntime(options) {
|
|
|
1356
1641
|
const read = reclassified.kind === 'deferred'
|
|
1357
1642
|
? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
|
|
1358
1643
|
: reclassified;
|
|
1359
|
-
await local.write({
|
|
1360
|
-
next: record,
|
|
1361
|
-
previous: held.confirmed.record,
|
|
1362
|
-
queue: held.queue,
|
|
1363
|
-
});
|
|
1644
|
+
await local.write({ next: record, previous: held.confirmed.record });
|
|
1364
1645
|
if (read.kind === 'staged') {
|
|
1365
1646
|
replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
|
|
1366
1647
|
if (declarationRefusals.delete(room.locator)) {
|
|
@@ -1428,13 +1709,14 @@ export function composeRuntime(options) {
|
|
|
1428
1709
|
roomVersion: record.roomVersion,
|
|
1429
1710
|
documents: record.documents,
|
|
1430
1711
|
revisions: record.revisions,
|
|
1431
|
-
attachments: record.attachments,
|
|
1432
1712
|
},
|
|
1433
1713
|
];
|
|
1434
1714
|
});
|
|
1435
1715
|
}
|
|
1436
|
-
async function synchronize(listed, local, progress, attempt) {
|
|
1716
|
+
async function synchronize(listed, local, progress, attempt, live) {
|
|
1437
1717
|
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
|
|
1718
|
+
if (!live())
|
|
1719
|
+
return;
|
|
1438
1720
|
const ready = listed.filter((_, index) => {
|
|
1439
1721
|
const outcome = prepared[index];
|
|
1440
1722
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
@@ -1443,9 +1725,10 @@ export function composeRuntime(options) {
|
|
|
1443
1725
|
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
1444
1726
|
handed = true;
|
|
1445
1727
|
}
|
|
1728
|
+
discovering?.release();
|
|
1446
1729
|
const loaded = await Promise.allSettled(ready
|
|
1447
1730
|
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1448
|
-
.map((room) => loadRoom(room, local, progress)));
|
|
1731
|
+
.map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
|
|
1449
1732
|
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1450
1733
|
if (failed !== undefined)
|
|
1451
1734
|
throw failed.reason;
|
|
@@ -1474,14 +1757,14 @@ export function composeRuntime(options) {
|
|
|
1474
1757
|
}
|
|
1475
1758
|
return true;
|
|
1476
1759
|
}
|
|
1477
|
-
async function caughtUp(room, local, progress) {
|
|
1760
|
+
async function caughtUp(room, local, progress, live) {
|
|
1478
1761
|
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1479
|
-
if (held === undefined || authorization === undefined)
|
|
1762
|
+
if (held === undefined || authorization === undefined || !live())
|
|
1480
1763
|
return;
|
|
1481
1764
|
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
1482
1765
|
if (!behind)
|
|
1483
1766
|
return;
|
|
1484
|
-
const withheld = await advance(room, local, authorization);
|
|
1767
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1485
1768
|
if (withheld !== null)
|
|
1486
1769
|
await withhold(withheld, room, local, progress);
|
|
1487
1770
|
}
|
|
@@ -1491,39 +1774,41 @@ export function composeRuntime(options) {
|
|
|
1491
1774
|
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1492
1775
|
});
|
|
1493
1776
|
}
|
|
1494
|
-
async function loadRoom(room, local, progress) {
|
|
1495
|
-
if (
|
|
1777
|
+
async function loadRoom(room, local, progress, live) {
|
|
1778
|
+
if (!live() || authorization === undefined)
|
|
1496
1779
|
return;
|
|
1497
1780
|
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1498
|
-
await reload(room, local, progress);
|
|
1781
|
+
await reload(room, local, progress, live);
|
|
1499
1782
|
return;
|
|
1500
1783
|
}
|
|
1501
|
-
const withheld = await advance(room, local, authorization);
|
|
1784
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1502
1785
|
if (withheld !== null) {
|
|
1503
1786
|
await withhold(withheld, room, local, progress);
|
|
1504
1787
|
return;
|
|
1505
1788
|
}
|
|
1506
|
-
await caughtUp(room, local, progress);
|
|
1789
|
+
await caughtUp(room, local, progress, live);
|
|
1507
1790
|
}
|
|
1508
|
-
async function reload(room, local, progress) {
|
|
1509
|
-
if (
|
|
1791
|
+
async function reload(room, local, progress, live) {
|
|
1792
|
+
if (!live() || authorization === undefined)
|
|
1510
1793
|
return;
|
|
1511
|
-
const bootstrapped = await pullBootstrap(authorization, room, local);
|
|
1512
|
-
if (!
|
|
1794
|
+
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
1795
|
+
if (!live())
|
|
1513
1796
|
return;
|
|
1514
1797
|
if (bootstrapped.kind !== 'staged') {
|
|
1515
1798
|
await withhold(bootstrapped, room, local, progress);
|
|
1516
1799
|
return;
|
|
1517
1800
|
}
|
|
1518
|
-
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
1519
|
-
await caughtUp(room, local, progress);
|
|
1801
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
|
|
1802
|
+
await caughtUp(room, local, progress, live);
|
|
1520
1803
|
}
|
|
1521
|
-
async function discoverRooms(local, progress, attempt) {
|
|
1804
|
+
async function discoverRooms(local, progress, attempt, live) {
|
|
1522
1805
|
if (closed || authorization === undefined) {
|
|
1523
1806
|
notifyReady();
|
|
1524
1807
|
return;
|
|
1525
1808
|
}
|
|
1526
1809
|
moveTo(transferring(status));
|
|
1810
|
+
discoveries += 1;
|
|
1811
|
+
const started = discoveries;
|
|
1527
1812
|
let result;
|
|
1528
1813
|
try {
|
|
1529
1814
|
result = await options.adapter.discover(authorization);
|
|
@@ -1531,7 +1816,8 @@ export function composeRuntime(options) {
|
|
|
1531
1816
|
catch {
|
|
1532
1817
|
result = { kind: 'transport-uncertainty' };
|
|
1533
1818
|
}
|
|
1534
|
-
if (
|
|
1819
|
+
if (!live()) {
|
|
1820
|
+
progress.complete = false;
|
|
1535
1821
|
notifyReady();
|
|
1536
1822
|
return;
|
|
1537
1823
|
}
|
|
@@ -1601,8 +1887,7 @@ export function composeRuntime(options) {
|
|
|
1601
1887
|
member = signedIn;
|
|
1602
1888
|
emitMember();
|
|
1603
1889
|
}
|
|
1604
|
-
await applyDiscovery(result.rooms, local);
|
|
1605
|
-
await synchronize(result.rooms, local, progress, attempt);
|
|
1890
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
1606
1891
|
}
|
|
1607
1892
|
async function ensureAuthorization() {
|
|
1608
1893
|
if (authorization !== undefined)
|
|
@@ -1616,32 +1901,64 @@ export function composeRuntime(options) {
|
|
|
1616
1901
|
throw error;
|
|
1617
1902
|
}
|
|
1618
1903
|
}
|
|
1619
|
-
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) {
|
|
1620
1924
|
if (stopping)
|
|
1621
|
-
return
|
|
1622
|
-
|
|
1623
|
-
|
|
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;
|
|
1624
1936
|
}
|
|
1625
1937
|
function sync(attempt) {
|
|
1626
|
-
return task(async () => {
|
|
1627
|
-
if (
|
|
1938
|
+
return task(OPENING_LANE, async (live) => {
|
|
1939
|
+
if (!live()) {
|
|
1628
1940
|
notifyReady();
|
|
1629
1941
|
return;
|
|
1630
1942
|
}
|
|
1631
1943
|
const progress = { complete: true, reachable: true };
|
|
1632
|
-
|
|
1944
|
+
const gate = discover();
|
|
1945
|
+
if (attempt === 'opened')
|
|
1946
|
+
opened = generation;
|
|
1947
|
+
underway = attempt === 'opened' ? { generation, progress } : null;
|
|
1948
|
+
if (attempt === 'opened')
|
|
1949
|
+
streamed = progress;
|
|
1633
1950
|
complete = false;
|
|
1634
1951
|
recorded = false;
|
|
1635
1952
|
moveTo(transferring(status));
|
|
1636
1953
|
handed = false;
|
|
1637
1954
|
try {
|
|
1638
1955
|
const local = await openLocal();
|
|
1639
|
-
if (
|
|
1956
|
+
if (!live())
|
|
1640
1957
|
return;
|
|
1641
1958
|
await ensureAuthorization();
|
|
1642
|
-
if (
|
|
1959
|
+
if (!live())
|
|
1643
1960
|
return;
|
|
1644
|
-
await discoverRooms(local, progress, attempt);
|
|
1961
|
+
await discoverRooms(local, progress, attempt, live);
|
|
1645
1962
|
}
|
|
1646
1963
|
catch {
|
|
1647
1964
|
progress.complete = false;
|
|
@@ -1649,17 +1966,21 @@ export function composeRuntime(options) {
|
|
|
1649
1966
|
progress.reachable = false;
|
|
1650
1967
|
}
|
|
1651
1968
|
finally {
|
|
1969
|
+
gate.release();
|
|
1652
1970
|
notifyReady();
|
|
1653
1971
|
}
|
|
1972
|
+
if (!live())
|
|
1973
|
+
return;
|
|
1654
1974
|
if (attempt !== 'opened') {
|
|
1655
|
-
await finishOpening(progress);
|
|
1975
|
+
await finishOpening(progress, live);
|
|
1656
1976
|
return;
|
|
1657
1977
|
}
|
|
1658
1978
|
if (handed || closed || progress.reachable)
|
|
1659
1979
|
return;
|
|
1660
1980
|
underway = null;
|
|
1981
|
+
opened = null;
|
|
1661
1982
|
journal.record('connection reopen');
|
|
1662
|
-
await rest(progress);
|
|
1983
|
+
await rest(progress, live);
|
|
1663
1984
|
options.adapter.roomStream?.reopen();
|
|
1664
1985
|
});
|
|
1665
1986
|
}
|
|
@@ -1670,29 +1991,48 @@ export function composeRuntime(options) {
|
|
|
1670
1991
|
void refuse('revoked-authorization');
|
|
1671
1992
|
return;
|
|
1672
1993
|
}
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1994
|
+
complete = false;
|
|
1995
|
+
if (streamed !== null)
|
|
1996
|
+
streamed.complete = false;
|
|
1997
|
+
moveTo({
|
|
1998
|
+
...status,
|
|
1999
|
+
state: status.state === 'synced' ? 'syncing' : status.state,
|
|
2000
|
+
providerUnreachable: status.providerUnreachable || !opened,
|
|
2001
|
+
});
|
|
1676
2002
|
}
|
|
1677
2003
|
function applyOpening(expired) {
|
|
1678
|
-
|
|
1679
|
-
|
|
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) => {
|
|
1680
2008
|
underway = null;
|
|
1681
|
-
if (
|
|
2009
|
+
if (!live())
|
|
1682
2010
|
return;
|
|
1683
2011
|
if (progress === null) {
|
|
1684
|
-
await
|
|
2012
|
+
await replacing;
|
|
2013
|
+
await rest(cycle, live);
|
|
1685
2014
|
return;
|
|
1686
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();
|
|
1687
2023
|
try {
|
|
1688
2024
|
const local = await openLocal();
|
|
1689
2025
|
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
1690
|
-
const reloading = held.filter((room) => expired.includes(room.locator)
|
|
1691
|
-
|
|
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)];
|
|
1692
2030
|
if (progress.reachable) {
|
|
1693
|
-
for (const room of held)
|
|
1694
|
-
if (!reloading.includes(room))
|
|
2031
|
+
for (const room of held) {
|
|
2032
|
+
if (!expired.includes(room.locator) && !reloading.includes(room)) {
|
|
1695
2033
|
await confirmModels(room, local);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
1696
2036
|
}
|
|
1697
2037
|
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1698
2038
|
if (failed !== undefined)
|
|
@@ -1702,21 +2042,21 @@ export function composeRuntime(options) {
|
|
|
1702
2042
|
progress.complete = false;
|
|
1703
2043
|
progress.reachable = false;
|
|
1704
2044
|
}
|
|
1705
|
-
|
|
2045
|
+
if (live())
|
|
2046
|
+
await finishOpening(progress, live);
|
|
1706
2047
|
});
|
|
1707
2048
|
}
|
|
1708
|
-
async function
|
|
1709
|
-
const progress = { complete: true, reachable: true };
|
|
2049
|
+
async function reloadNow(replaced, progress) {
|
|
1710
2050
|
try {
|
|
1711
2051
|
const local = await openLocal();
|
|
1712
|
-
await Promise.allSettled([...discovered.values()]
|
|
1713
|
-
.filter((room) => replicas.has(room.locator) &&
|
|
1714
|
-
.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))));
|
|
1715
2055
|
}
|
|
1716
2056
|
catch {
|
|
1717
2057
|
progress.reachable = false;
|
|
2058
|
+
return [];
|
|
1718
2059
|
}
|
|
1719
|
-
await rest(progress);
|
|
1720
2060
|
}
|
|
1721
2061
|
async function confirmModels(room, local) {
|
|
1722
2062
|
for (const attached of ['collection', 'typed-selection']) {
|
|
@@ -1768,9 +2108,9 @@ export function composeRuntime(options) {
|
|
|
1768
2108
|
submitLater(locator);
|
|
1769
2109
|
}
|
|
1770
2110
|
}
|
|
1771
|
-
async function finishOpening(progress) {
|
|
2111
|
+
async function finishOpening(progress, live) {
|
|
1772
2112
|
try {
|
|
1773
|
-
if (
|
|
2113
|
+
if (!live()) {
|
|
1774
2114
|
progress.complete = false;
|
|
1775
2115
|
return;
|
|
1776
2116
|
}
|
|
@@ -1780,6 +2120,8 @@ export function composeRuntime(options) {
|
|
|
1780
2120
|
conversations.reconnected();
|
|
1781
2121
|
const reading = { complete: true, reachable: true };
|
|
1782
2122
|
await conversations.catchUp(reading);
|
|
2123
|
+
if (progress.reachable && reading.reachable)
|
|
2124
|
+
await conversations.resend(reading);
|
|
1783
2125
|
if (!reading.reachable)
|
|
1784
2126
|
progress.reachable = false;
|
|
1785
2127
|
}
|
|
@@ -1787,12 +2129,14 @@ export function composeRuntime(options) {
|
|
|
1787
2129
|
progress.complete = false;
|
|
1788
2130
|
}
|
|
1789
2131
|
finally {
|
|
1790
|
-
|
|
1791
|
-
|
|
2132
|
+
if (live()) {
|
|
2133
|
+
complete = progress.complete;
|
|
2134
|
+
await rest(progress, live);
|
|
2135
|
+
}
|
|
1792
2136
|
}
|
|
1793
2137
|
}
|
|
1794
|
-
async function rest(progress) {
|
|
1795
|
-
if (
|
|
2138
|
+
async function rest(progress, live = () => !closed) {
|
|
2139
|
+
if (!live())
|
|
1796
2140
|
return;
|
|
1797
2141
|
let lastCompleted = status.lastCompleted;
|
|
1798
2142
|
let synced = complete && stale.size === 0 && !pending();
|
|
@@ -1800,6 +2144,8 @@ export function composeRuntime(options) {
|
|
|
1800
2144
|
const completedAt = new Date().toISOString();
|
|
1801
2145
|
try {
|
|
1802
2146
|
await (await openLocal()).complete(completedAt);
|
|
2147
|
+
if (!live())
|
|
2148
|
+
return;
|
|
1803
2149
|
recorded = true;
|
|
1804
2150
|
lastCompleted = completedAt;
|
|
1805
2151
|
}
|
|
@@ -1808,17 +2154,29 @@ export function composeRuntime(options) {
|
|
|
1808
2154
|
}
|
|
1809
2155
|
}
|
|
1810
2156
|
moveTo({
|
|
1811
|
-
state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
|
|
2157
|
+
state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
|
|
1812
2158
|
lastCompleted,
|
|
1813
2159
|
providerUnreachable: !progress.reachable,
|
|
1814
2160
|
...attention(),
|
|
1815
2161
|
});
|
|
1816
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
|
+
}
|
|
1817
2171
|
function applyRoom(locator, roomVersion) {
|
|
1818
2172
|
announced.set(locator, roomVersion);
|
|
1819
|
-
return task(async () => {
|
|
1820
|
-
|
|
1821
|
-
|
|
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)
|
|
1822
2180
|
return;
|
|
1823
2181
|
const room = discovered.get(locator);
|
|
1824
2182
|
const held = replicas.get(locator);
|
|
@@ -1829,25 +2187,41 @@ export function composeRuntime(options) {
|
|
|
1829
2187
|
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
1830
2188
|
return;
|
|
1831
2189
|
}
|
|
1832
|
-
const during = underway;
|
|
2190
|
+
const during = underway?.generation === generation ? underway.progress : null;
|
|
1833
2191
|
const progress = during ?? { complete: true, reachable: true };
|
|
1834
2192
|
if (during === null)
|
|
1835
2193
|
moveTo(transferring(status));
|
|
1836
2194
|
try {
|
|
1837
2195
|
const local = await openLocal();
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
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
|
+
});
|
|
1841
2203
|
}
|
|
1842
2204
|
catch {
|
|
1843
2205
|
stale.add(locator);
|
|
1844
2206
|
}
|
|
1845
2207
|
finally {
|
|
1846
2208
|
if (during === null)
|
|
1847
|
-
await rest(progress);
|
|
2209
|
+
await rest(progress, live);
|
|
1848
2210
|
}
|
|
1849
2211
|
});
|
|
1850
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
|
+
};
|
|
1851
2225
|
function observeStream() {
|
|
1852
2226
|
const stream = options.adapter.roomStream;
|
|
1853
2227
|
if (stream === undefined || withdrawStream !== undefined)
|
|
@@ -1857,6 +2231,7 @@ export function composeRuntime(options) {
|
|
|
1857
2231
|
return;
|
|
1858
2232
|
if (event.kind === 'opened') {
|
|
1859
2233
|
socketOpen = true;
|
|
2234
|
+
generation += 1;
|
|
1860
2235
|
void sync('opened');
|
|
1861
2236
|
}
|
|
1862
2237
|
else if (event.kind === 'closed')
|
|
@@ -1865,7 +2240,7 @@ export function composeRuntime(options) {
|
|
|
1865
2240
|
void applyOpening(event.expired);
|
|
1866
2241
|
else
|
|
1867
2242
|
void applyRoom(event.locator, event.roomVersion);
|
|
1868
|
-
});
|
|
2243
|
+
}, receipts);
|
|
1869
2244
|
}
|
|
1870
2245
|
function writesHeld() {
|
|
1871
2246
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -1903,34 +2278,30 @@ export function composeRuntime(options) {
|
|
|
1903
2278
|
});
|
|
1904
2279
|
}
|
|
1905
2280
|
function scheduleCurrency() {
|
|
1906
|
-
if (currencyQueued || closed || stopping)
|
|
2281
|
+
if (currencyQueued === generation || closed || stopping)
|
|
1907
2282
|
return;
|
|
1908
|
-
currencyQueued =
|
|
1909
|
-
void task(runCurrency);
|
|
2283
|
+
currencyQueued = generation;
|
|
2284
|
+
void task(CONVERSATION_LANE, runCurrency);
|
|
1910
2285
|
}
|
|
1911
|
-
async function runCurrency() {
|
|
1912
|
-
currencyQueued
|
|
1913
|
-
|
|
2286
|
+
async function runCurrency(live) {
|
|
2287
|
+
if (currencyQueued === generation)
|
|
2288
|
+
currencyQueued = null;
|
|
2289
|
+
if (!live())
|
|
1914
2290
|
return;
|
|
1915
2291
|
await conversations.reconcile();
|
|
1916
2292
|
if (conversations.current()) {
|
|
1917
2293
|
show();
|
|
1918
2294
|
return;
|
|
1919
2295
|
}
|
|
1920
|
-
const
|
|
1921
|
-
|
|
1922
|
-
moveTo(transferring(resting));
|
|
2296
|
+
const progress = { complete: false, reachable: !status.providerUnreachable };
|
|
2297
|
+
moveTo(transferring(status));
|
|
1923
2298
|
try {
|
|
1924
2299
|
await conversations.catchUp(progress);
|
|
1925
2300
|
}
|
|
1926
2301
|
catch {
|
|
1927
2302
|
progress.reachable = false;
|
|
1928
2303
|
}
|
|
1929
|
-
|
|
1930
|
-
if (!closed) {
|
|
1931
|
-
moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
|
|
1932
|
-
}
|
|
1933
|
-
}
|
|
2304
|
+
await rest(progress, live);
|
|
1934
2305
|
}
|
|
1935
2306
|
function close() {
|
|
1936
2307
|
if (closed)
|
|
@@ -1961,7 +2332,7 @@ export function composeRuntime(options) {
|
|
|
1961
2332
|
async function stop() {
|
|
1962
2333
|
stopping = true;
|
|
1963
2334
|
stopTimer();
|
|
1964
|
-
await
|
|
2335
|
+
await Promise.all(working);
|
|
1965
2336
|
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
1966
2337
|
await mutations;
|
|
1967
2338
|
close();
|
|
@@ -2019,6 +2390,8 @@ export function composeRuntime(options) {
|
|
|
2019
2390
|
}
|
|
2020
2391
|
if (topic === 'conversations')
|
|
2021
2392
|
return conversations.watchList(deliver);
|
|
2393
|
+
if (topic === 'agents')
|
|
2394
|
+
return conversations.watchAgents(deliver);
|
|
2022
2395
|
if (topic === 'presentation') {
|
|
2023
2396
|
const listener = {
|
|
2024
2397
|
app: input[0],
|
|
@@ -2132,11 +2505,14 @@ export function composeRuntime(options) {
|
|
|
2132
2505
|
if ((current?.revision ?? null) !== revision) {
|
|
2133
2506
|
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2134
2507
|
}
|
|
2508
|
+
const file = parts.file === undefined
|
|
2509
|
+
? undefined
|
|
2510
|
+
: await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
|
|
2135
2511
|
const record = {
|
|
2136
2512
|
locator: room,
|
|
2137
2513
|
path,
|
|
2138
2514
|
revision: roomDraftRevision(),
|
|
2139
|
-
parts: held(current?.parts, parts),
|
|
2515
|
+
parts: held(current?.parts, parts, file),
|
|
2140
2516
|
returned: current?.returned ?? null,
|
|
2141
2517
|
};
|
|
2142
2518
|
await local.keepRoomDraft(record);
|
|
@@ -2185,6 +2561,10 @@ export function composeRuntime(options) {
|
|
|
2185
2561
|
history(locator, path, query) {
|
|
2186
2562
|
return readHistory(locator, path, query);
|
|
2187
2563
|
},
|
|
2564
|
+
binaryFile: {
|
|
2565
|
+
read: readBinaryPath,
|
|
2566
|
+
readReference: readBinaryReference,
|
|
2567
|
+
},
|
|
2188
2568
|
call(name, input) {
|
|
2189
2569
|
return invokeAction(name, input);
|
|
2190
2570
|
},
|
|
@@ -2204,23 +2584,26 @@ export function composeRuntime(options) {
|
|
|
2204
2584
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2205
2585
|
return;
|
|
2206
2586
|
journal.record('connection reauthorize');
|
|
2207
|
-
await
|
|
2587
|
+
await Promise.all([...lanes.values()]
|
|
2588
|
+
.filter((held) => held.generation === generation)
|
|
2589
|
+
.map((held) => held.chain));
|
|
2208
2590
|
await mutations;
|
|
2209
2591
|
authorization = undefined;
|
|
2210
2592
|
discoveryPaused = false;
|
|
2211
2593
|
if (options.adapter.roomStream === undefined)
|
|
2212
2594
|
await sync('unstreamed');
|
|
2213
2595
|
else
|
|
2214
|
-
await task(ensureAuthorization);
|
|
2596
|
+
await task(OPENING_LANE, ensureAuthorization);
|
|
2215
2597
|
},
|
|
2216
2598
|
};
|
|
2217
2599
|
const reachable = {
|
|
2218
2600
|
md,
|
|
2219
2601
|
watch,
|
|
2602
|
+
referrerScope: (referrer) => options.adapter.referrerScope(referrer),
|
|
2220
2603
|
plan: planned,
|
|
2221
2604
|
query: (plan) => rowsQuery(plan),
|
|
2222
2605
|
watchPlan,
|
|
2223
2606
|
search: searched,
|
|
2224
2607
|
};
|
|
2225
|
-
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2608
|
+
return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2226
2609
|
}
|