@agentshouse/mdruntime 0.1.0-c3903a68dcbbf → 0.1.0-c56daf494ecb4
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 +184 -92
- package/README.md +22 -12
- package/dist/adapter.d.ts +125 -63
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +1 -9
- package/dist/attached-model.d.ts +11 -0
- package/dist/attached-model.d.ts.map +1 -0
- package/dist/attached-model.js +47 -0
- package/dist/attempt.d.ts +16 -0
- package/dist/attempt.d.ts.map +1 -0
- package/dist/attempt.js +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 +20 -6
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +93 -39
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +20 -17
- package/dist/change.d.ts +15 -0
- package/dist/change.d.ts.map +1 -1
- package/dist/change.js +26 -1
- package/dist/chat/acts.d.ts +34 -0
- package/dist/chat/acts.d.ts.map +1 -0
- package/dist/chat/acts.js +36 -0
- package/dist/chat/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 +645 -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 +17 -13
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +68 -31
- package/dist/collection.d.ts +6 -13
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +56 -29
- package/dist/connection.d.ts +28 -33
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +951 -745
- package/dist/conversation.d.ts +32 -13
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +293 -108
- 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/failure.d.ts +13 -0
- package/dist/failure.d.ts.map +1 -0
- package/dist/failure.js +19 -0
- package/dist/host.d.ts +9 -3
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +49 -68
- package/dist/identity.d.ts +3 -0
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +13 -1
- package/dist/index.d.ts +9 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/memory.d.ts +14 -7
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +137 -46
- package/dist/model.d.ts +13 -12
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +6 -3
- package/dist/observation.d.ts +2 -0
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +24 -14
- package/dist/open.d.ts +2 -0
- package/dist/open.d.ts.map +1 -0
- package/dist/open.js +1 -0
- package/dist/outcome.d.ts +32 -23
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +41 -1
- package/dist/overlay.d.ts +14 -3
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +75 -37
- package/dist/paths.d.ts +0 -1
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +0 -16
- package/dist/query.d.ts +12 -3
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +78 -36
- package/dist/reach.d.ts +42 -0
- package/dist/reach.d.ts.map +1 -0
- package/dist/reach.js +294 -0
- package/dist/react.d.ts +41 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +64 -0
- package/dist/read-side.d.ts +4 -0
- package/dist/read-side.d.ts.map +1 -1
- package/dist/read-side.js +125 -56
- package/dist/refresh.d.ts +12 -1
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +44 -36
- package/dist/replica.d.ts +9 -6
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +83 -68
- package/dist/room-draft.d.ts +29 -9
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +137 -26
- package/dist/rows.d.ts +7 -2
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +85 -40
- package/dist/save.d.ts +27 -3
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +435 -86
- package/dist/search.d.ts +3 -2
- package/dist/search.d.ts.map +1 -1
- package/dist/status.d.ts +2 -2
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +0 -8
- package/dist/store.d.ts +41 -25
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +315 -191
- package/dist/wire.d.ts +58 -19
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +65 -67
- package/package.json +31 -7
- 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
|
@@ -1,95 +1,61 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { attachedModel } from './attached-model.js';
|
|
2
2
|
import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
|
|
3
|
-
import {
|
|
3
|
+
import { observing } from './observation.js';
|
|
4
4
|
import { filterOf, identityOf, planOf, } from './query.js';
|
|
5
5
|
import { createReadSide } from './read-side.js';
|
|
6
|
+
import { markdownPath, roomOfKey } from './rows.js';
|
|
6
7
|
import { createSearchIndex, searchPlanOf, } from './search.js';
|
|
7
|
-
import { ROOM_CONTRACT_VERSION, VERSION_1_CAPABILITIES, } from './adapter.js';
|
|
8
8
|
import { bootstrapRoom } from './bootstrap.js';
|
|
9
9
|
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
10
10
|
import { createConversationStore, } from './conversation.js';
|
|
11
|
-
import {
|
|
11
|
+
import { queuedFile } from './change.js';
|
|
12
|
+
import { validRoot } from './collection.js';
|
|
13
|
+
import { runtimeError } from './failure.js';
|
|
12
14
|
import { createLog } from './log.js';
|
|
15
|
+
import { binaryRecord } from './binary.js';
|
|
16
|
+
import { absentBinary, binaryAnswer, calledResult, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
|
|
13
17
|
import { refreshRoom } from './refresh.js';
|
|
14
|
-
import { buildBatch } from './save.js';
|
|
15
|
-
import { applyReturned, compositionOf,
|
|
16
|
-
import { changedParts, changesOf, held, readParts, returnedInto, underneath, } from './room-draft.js';
|
|
17
|
-
import {
|
|
18
|
+
import { buildBatch, joined, limitedIn, requeued } from './save.js';
|
|
19
|
+
import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
|
|
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';
|
|
18
22
|
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
19
23
|
import { roomDraftRevision } from './identity.js';
|
|
20
24
|
import { beneath, normalize } from './paths.js';
|
|
21
25
|
import { openRuntimeStore, } from './store.js';
|
|
22
26
|
import { unconfirmed } from './transfer.js';
|
|
23
|
-
|
|
24
|
-
const DEFAULT_DISCOVERY_INTERVAL_MS = 60_000;
|
|
25
|
-
const EMPTY_SCHEMA = { verdict: 'accept', model: { types: {} } };
|
|
26
|
-
const knownCapabilities = new Set(VERSION_1_CAPABILITIES);
|
|
27
|
+
import { governs, holds, narrowed, planReach, } from './reach.js';
|
|
27
28
|
function within(root, path) {
|
|
28
29
|
return root === '' || beneath(root, path);
|
|
29
30
|
}
|
|
30
|
-
function declaredIdentity(model) {
|
|
31
|
-
if (model === undefined)
|
|
32
|
-
return false;
|
|
33
|
-
if (model === null)
|
|
34
|
-
return true;
|
|
35
|
-
return (typeof model.identity === 'string' &&
|
|
36
|
-
model.identity.length > 0 &&
|
|
37
|
-
typeof model.version === 'string' &&
|
|
38
|
-
model.version.length > 0);
|
|
39
|
-
}
|
|
40
31
|
function declaredSelection(room) {
|
|
41
32
|
const selection = room.typedSelection;
|
|
42
33
|
if (selection === undefined)
|
|
43
34
|
return true;
|
|
44
35
|
if (!Array.isArray(selection.roots) || selection.roots.length === 0)
|
|
45
36
|
return false;
|
|
46
|
-
if (!declaredIdentity(selection.attachedModel))
|
|
47
|
-
return false;
|
|
48
37
|
return selection.roots.every((root, index) => validRoot(root) &&
|
|
49
38
|
root !== '' &&
|
|
50
39
|
selection.roots.every((other, at) => at === index || !within(other, root)));
|
|
51
40
|
}
|
|
52
|
-
function declaredUntypedFolders(room) {
|
|
53
|
-
const roots = room.untypedFolders;
|
|
54
|
-
if (roots === undefined)
|
|
55
|
-
return true;
|
|
56
|
-
if (!Array.isArray(roots) || roots.length === 0)
|
|
57
|
-
return false;
|
|
58
|
-
const selected = room.typedSelection?.roots ?? [];
|
|
59
|
-
return roots.every((root, index) => validRoot(root) &&
|
|
60
|
-
root !== '' &&
|
|
61
|
-
roots.every((other, at) => at === index || !within(other, root)) &&
|
|
62
|
-
selected.every((other) => !within(other, root) && !within(root, other)));
|
|
63
|
-
}
|
|
64
|
-
export function createMdRuntime(options) {
|
|
65
|
-
return composeRuntime(options).md;
|
|
66
|
-
}
|
|
67
41
|
export function composeRuntime(options) {
|
|
68
|
-
const
|
|
42
|
+
const localSaves = options.adapter.localSaves === true;
|
|
69
43
|
let authorization = options.authorization;
|
|
70
44
|
let opening;
|
|
71
45
|
let store;
|
|
72
46
|
const replicas = new Map();
|
|
47
|
+
const queuesWithoutReplica = new Map();
|
|
73
48
|
const pendings = new Map();
|
|
74
49
|
const roomDrafts = new Map();
|
|
75
50
|
const presentations = new Map();
|
|
76
|
-
const outcomes = new Map();
|
|
77
|
-
const outcomeRooms = new Map();
|
|
78
|
-
const wanted = new Map();
|
|
79
51
|
let limits = [];
|
|
80
|
-
let slots = [];
|
|
81
52
|
let capabilities = [];
|
|
82
|
-
function rewriting() {
|
|
83
|
-
return capabilities.some((capability) => capability.name === 'move-reference-rewrites')
|
|
84
|
-
? 'provider'
|
|
85
|
-
: 'runtime';
|
|
86
|
-
}
|
|
87
53
|
const rooms = new Map();
|
|
54
|
+
const counted = new Map();
|
|
88
55
|
const unreadable = new Set();
|
|
89
56
|
const models = new Map();
|
|
90
57
|
const liveModels = new Map();
|
|
91
58
|
const invalidModels = new Set();
|
|
92
|
-
const modelEpoch = new Map();
|
|
93
59
|
const discovered = new Map();
|
|
94
60
|
let roomsListed = false;
|
|
95
61
|
const scopedRefusals = new Map();
|
|
@@ -118,16 +84,21 @@ export function composeRuntime(options) {
|
|
|
118
84
|
let status = STARTING;
|
|
119
85
|
let shown = STARTING;
|
|
120
86
|
let discoveryPaused = false;
|
|
121
|
-
let
|
|
87
|
+
let resetTimer;
|
|
122
88
|
let closed = false;
|
|
123
89
|
let stopping = false;
|
|
124
|
-
let
|
|
125
|
-
|
|
126
|
-
|
|
90
|
+
let withdrawStream;
|
|
91
|
+
const announced = new Map();
|
|
92
|
+
let underway = null;
|
|
93
|
+
let streamed = null;
|
|
94
|
+
let handed = false;
|
|
95
|
+
const submitting = new Map();
|
|
96
|
+
const answering = new Map();
|
|
97
|
+
let complete = false;
|
|
98
|
+
let recorded = false;
|
|
99
|
+
const stale = new Set();
|
|
127
100
|
let chain = Promise.resolve();
|
|
128
101
|
let mutations = Promise.resolve();
|
|
129
|
-
let inflight = false;
|
|
130
|
-
let queued = false;
|
|
131
102
|
const roomListeners = new Set();
|
|
132
103
|
const refusalListeners = new Set();
|
|
133
104
|
const storageLossListeners = new Set();
|
|
@@ -138,13 +109,19 @@ export function composeRuntime(options) {
|
|
|
138
109
|
const readSide = createReadSide();
|
|
139
110
|
let withdrawConversations;
|
|
140
111
|
let currencyQueued = false;
|
|
112
|
+
let socketOpen = false;
|
|
141
113
|
let serviceMode = 'open';
|
|
142
114
|
let haltedRefusal = false;
|
|
143
115
|
let openSignals = 0;
|
|
144
116
|
const serviceModeListeners = new Set();
|
|
117
|
+
let member = null;
|
|
118
|
+
const memberListeners = new Set();
|
|
119
|
+
const materialized = new Set();
|
|
120
|
+
let everyRoom = false;
|
|
121
|
+
const transfers = new Map();
|
|
145
122
|
const conversations = createConversationStore({
|
|
146
|
-
|
|
147
|
-
documents: (locator) =>
|
|
123
|
+
agents: declaredAgents,
|
|
124
|
+
documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
|
|
148
125
|
call: (name, input) => invokeAction(name, input),
|
|
149
126
|
durably,
|
|
150
127
|
stream: () => options.adapter.conversations,
|
|
@@ -220,7 +197,9 @@ export function composeRuntime(options) {
|
|
|
220
197
|
}
|
|
221
198
|
}
|
|
222
199
|
function pending() {
|
|
223
|
-
return
|
|
200
|
+
return (!localSaves &&
|
|
201
|
+
(queuesWithoutReplica.size > 0 ||
|
|
202
|
+
[...replicas.values()].some((replica) => replica.queue.length > 0)));
|
|
224
203
|
}
|
|
225
204
|
function attention() {
|
|
226
205
|
const attentionRequired = [];
|
|
@@ -249,23 +228,32 @@ export function composeRuntime(options) {
|
|
|
249
228
|
}
|
|
250
229
|
return { attentionRequired, reset };
|
|
251
230
|
}
|
|
231
|
+
function documentsIn(locator) {
|
|
232
|
+
const replica = replicas.get(locator);
|
|
233
|
+
if (replica === undefined)
|
|
234
|
+
return 0;
|
|
235
|
+
const roots = replica.confirmed.record.typedSelection?.roots ?? [];
|
|
236
|
+
const paths = [
|
|
237
|
+
...Object.keys(replica.visible.documents).filter(markdownPath),
|
|
238
|
+
...Object.keys(replica.visible.binaries),
|
|
239
|
+
];
|
|
240
|
+
return paths.filter((path) => !roots.some((root) => beneath(root, path))).length;
|
|
241
|
+
}
|
|
252
242
|
function listed() {
|
|
253
|
-
return structuredClone([...rooms.values()])
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
entries: entries.flatMap(({ locator, segment }) => {
|
|
259
|
-
const room = rooms.get(locator);
|
|
260
|
-
return room === undefined ? [] : [{ room, segment }];
|
|
261
|
-
}),
|
|
262
|
-
})));
|
|
243
|
+
return structuredClone([...rooms.values()].map((room) => {
|
|
244
|
+
const documents = documentsIn(room.locator);
|
|
245
|
+
counted.set(room.locator, documents);
|
|
246
|
+
return { ...room, documents };
|
|
247
|
+
}));
|
|
263
248
|
}
|
|
264
|
-
function roomFacts() {
|
|
265
|
-
const held = [...rooms.values()]
|
|
249
|
+
function roomFacts(reach = null) {
|
|
250
|
+
const held = [...rooms.values()]
|
|
251
|
+
.filter((room) => holds(reach, room.locator))
|
|
252
|
+
.map((room) => {
|
|
253
|
+
const governed = governs(reach, room);
|
|
266
254
|
const attached = [
|
|
267
|
-
modelOf(room.locator),
|
|
268
|
-
...(
|
|
255
|
+
...(governed.collection ? [modelOf(room.locator)] : []),
|
|
256
|
+
...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
|
|
269
257
|
];
|
|
270
258
|
return {
|
|
271
259
|
locator: room.locator,
|
|
@@ -288,6 +276,10 @@ export function composeRuntime(options) {
|
|
|
288
276
|
for (const listener of storageLossListeners)
|
|
289
277
|
listener(storageLoss);
|
|
290
278
|
}
|
|
279
|
+
function emitMember() {
|
|
280
|
+
for (const listener of memberListeners)
|
|
281
|
+
listener(member);
|
|
282
|
+
}
|
|
291
283
|
function moveTo(next) {
|
|
292
284
|
if (closed)
|
|
293
285
|
return;
|
|
@@ -306,14 +298,17 @@ export function composeRuntime(options) {
|
|
|
306
298
|
for (const listener of statusListeners)
|
|
307
299
|
listener(shown);
|
|
308
300
|
}
|
|
309
|
-
function
|
|
310
|
-
|
|
301
|
+
function entryOf(batch) {
|
|
302
|
+
if (localSaves)
|
|
303
|
+
return Object.freeze({ batch, state: 'local' });
|
|
304
|
+
const state = pendings.get(batch.id);
|
|
305
|
+
if (state?.accepted !== undefined) {
|
|
306
|
+
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
307
|
+
}
|
|
308
|
+
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
311
309
|
}
|
|
312
310
|
function entriesOf(locator) {
|
|
313
|
-
|
|
314
|
-
if (replica === undefined)
|
|
315
|
-
return [];
|
|
316
|
-
return replica.queue.map((batch) => Object.freeze({ batch, ...submissionOf(batch) }));
|
|
311
|
+
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
|
|
317
312
|
}
|
|
318
313
|
function emitQueue(locator) {
|
|
319
314
|
const entries = entriesOf(locator);
|
|
@@ -323,12 +318,47 @@ export function composeRuntime(options) {
|
|
|
323
318
|
}
|
|
324
319
|
}
|
|
325
320
|
function applyChangeSet(set) {
|
|
321
|
+
const held = everyRoom
|
|
322
|
+
? set
|
|
323
|
+
: { ...set, documents: set.documents.filter(({ locator }) => materialized.has(locator)) };
|
|
324
|
+
readSide.apply(held);
|
|
325
|
+
searchIndex.changed(held);
|
|
326
|
+
}
|
|
327
|
+
function materialize(locators) {
|
|
328
|
+
if (everyRoom)
|
|
329
|
+
return;
|
|
330
|
+
if (locators === null)
|
|
331
|
+
everyRoom = true;
|
|
332
|
+
const sets = [];
|
|
333
|
+
for (const locator of locators ?? [...replicas.keys()]) {
|
|
334
|
+
if (materialized.has(locator))
|
|
335
|
+
continue;
|
|
336
|
+
materialized.add(locator);
|
|
337
|
+
const replica = replicas.get(locator);
|
|
338
|
+
if (replica !== undefined)
|
|
339
|
+
sets.push(changeSet(locator, null, replica));
|
|
340
|
+
}
|
|
341
|
+
if (sets.length === 0)
|
|
342
|
+
return;
|
|
343
|
+
const set = mergeChangeSets(sets);
|
|
326
344
|
readSide.apply(set);
|
|
327
345
|
searchIndex.changed(set);
|
|
328
346
|
}
|
|
347
|
+
function reachedRooms(plan) {
|
|
348
|
+
if (plan.rooms !== null)
|
|
349
|
+
return plan.rooms;
|
|
350
|
+
if (plan.reach === null)
|
|
351
|
+
return null;
|
|
352
|
+
return [...plan.reach.rooms, ...plan.reach.documents.map(roomOfKey)];
|
|
353
|
+
}
|
|
354
|
+
function rowsQuery(plan) {
|
|
355
|
+
materialize(reachedRooms(plan));
|
|
356
|
+
return readSide.query(plan);
|
|
357
|
+
}
|
|
329
358
|
function publishChangeSet(set) {
|
|
330
359
|
applyChangeSet(set);
|
|
331
|
-
if (set.rooms.length > 0
|
|
360
|
+
if (set.rooms.length > 0 ||
|
|
361
|
+
[...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
|
|
332
362
|
emitRooms();
|
|
333
363
|
for (const locator of set.queues)
|
|
334
364
|
emitQueue(locator);
|
|
@@ -339,23 +369,32 @@ export function composeRuntime(options) {
|
|
|
339
369
|
for (const app of set.presentations)
|
|
340
370
|
emitPresentation(app);
|
|
341
371
|
conversations.changed(set);
|
|
342
|
-
const cards = declaredCards();
|
|
343
|
-
if (cards !== undefined && set.documents.some((change) => change.locator === cards.locator)) {
|
|
344
|
-
scheduleCurrency();
|
|
345
|
-
}
|
|
346
372
|
}
|
|
347
373
|
function stopTimer() {
|
|
348
|
-
if (
|
|
374
|
+
if (resetTimer === undefined)
|
|
349
375
|
return;
|
|
350
|
-
|
|
351
|
-
|
|
376
|
+
clearTimeout(resetTimer);
|
|
377
|
+
resetTimer = undefined;
|
|
352
378
|
}
|
|
353
|
-
function
|
|
354
|
-
|
|
379
|
+
function armTimer() {
|
|
380
|
+
stopTimer();
|
|
381
|
+
if (closed || stopping)
|
|
382
|
+
return;
|
|
383
|
+
let earliest = null;
|
|
384
|
+
for (const { submission } of pendings.values()) {
|
|
385
|
+
if (submission.state !== 'reset')
|
|
386
|
+
continue;
|
|
387
|
+
const at = Date.parse(submission.reset);
|
|
388
|
+
if (earliest === null || at < earliest)
|
|
389
|
+
earliest = at;
|
|
390
|
+
}
|
|
391
|
+
if (earliest === null)
|
|
355
392
|
return;
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
393
|
+
// A Save House held back goes again only once the reset its answer named has passed.
|
|
394
|
+
resetTimer = setTimeout(() => {
|
|
395
|
+
resetTimer = undefined;
|
|
396
|
+
submitAll();
|
|
397
|
+
}, Math.max(0, earliest - Date.now()));
|
|
359
398
|
}
|
|
360
399
|
async function refuse(reason, provider = null) {
|
|
361
400
|
stopTimer();
|
|
@@ -366,10 +405,7 @@ export function composeRuntime(options) {
|
|
|
366
405
|
const revoked = { status: 'unavailable', reason: 'revoked' };
|
|
367
406
|
try {
|
|
368
407
|
const local = await openLocal();
|
|
369
|
-
await local.revokeModels([...replicas.values()].map((replica) =>
|
|
370
|
-
record: replica.confirmed.record,
|
|
371
|
-
queue: replica.queue,
|
|
372
|
-
})));
|
|
408
|
+
await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
|
|
373
409
|
}
|
|
374
410
|
catch {
|
|
375
411
|
journal.record('model revocation persistence failed');
|
|
@@ -381,7 +417,6 @@ export function composeRuntime(options) {
|
|
|
381
417
|
}
|
|
382
418
|
for (const key of keys) {
|
|
383
419
|
invalidModels.add(key);
|
|
384
|
-
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
385
420
|
models.set(key, revoked);
|
|
386
421
|
liveModels.delete(key);
|
|
387
422
|
}
|
|
@@ -394,10 +429,7 @@ export function composeRuntime(options) {
|
|
|
394
429
|
model: revoked,
|
|
395
430
|
selectionModel: revoked,
|
|
396
431
|
};
|
|
397
|
-
|
|
398
|
-
replicas.set(locator, read.kind === 'staged'
|
|
399
|
-
? replicaOf(read.confirmed, before.queue, rewriting(), before)
|
|
400
|
-
: { ...before, confirmed: { ...before.confirmed, record } });
|
|
432
|
+
replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
|
|
401
433
|
}
|
|
402
434
|
rooms.set(locator, {
|
|
403
435
|
...room,
|
|
@@ -414,7 +446,8 @@ export function composeRuntime(options) {
|
|
|
414
446
|
}
|
|
415
447
|
finally {
|
|
416
448
|
refusal = {
|
|
417
|
-
reason,
|
|
449
|
+
code: reason,
|
|
450
|
+
retryable: false,
|
|
418
451
|
reauthorizationOffered: reason === 'revoked-authorization',
|
|
419
452
|
refusal: provider,
|
|
420
453
|
};
|
|
@@ -427,21 +460,18 @@ export function composeRuntime(options) {
|
|
|
427
460
|
rooms.set(room.locator, {
|
|
428
461
|
name: room.name,
|
|
429
462
|
locator: room.locator,
|
|
463
|
+
handle: room.handle,
|
|
430
464
|
operations: room.operations,
|
|
431
465
|
replica: room.replica,
|
|
432
|
-
attachedModel: room.attachedModel,
|
|
433
466
|
model: modelTypes(modelOf(room.locator)),
|
|
434
467
|
typedSelection: selection === null
|
|
435
468
|
? null
|
|
436
469
|
: {
|
|
437
470
|
roots: selection.roots,
|
|
438
|
-
attachedModel: selection.attachedModel,
|
|
439
471
|
model: modelTypes(modelOf(room.locator, 'typed-selection')),
|
|
440
472
|
},
|
|
441
|
-
untypedFolders: room.untypedFolders ?? [],
|
|
442
473
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
443
474
|
roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
|
|
444
|
-
attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
|
|
445
475
|
synchronizationAvailable,
|
|
446
476
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
447
477
|
});
|
|
@@ -453,9 +483,6 @@ export function composeRuntime(options) {
|
|
|
453
483
|
discovered.clear();
|
|
454
484
|
for (const room of listed)
|
|
455
485
|
discovered.set(room.locator, room);
|
|
456
|
-
for (const locator of wanted.keys())
|
|
457
|
-
if (!listedLocators.has(locator))
|
|
458
|
-
wanted.delete(locator);
|
|
459
486
|
await dropUnlisted(listedLocators, local);
|
|
460
487
|
for (const room of listed) {
|
|
461
488
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
@@ -469,7 +496,14 @@ export function composeRuntime(options) {
|
|
|
469
496
|
emitRooms();
|
|
470
497
|
}
|
|
471
498
|
function unlistedHeld(listed) {
|
|
472
|
-
return [
|
|
499
|
+
return [
|
|
500
|
+
...new Set([
|
|
501
|
+
...rooms.keys(),
|
|
502
|
+
...roomDrafts.keys(),
|
|
503
|
+
...transfers.keys(),
|
|
504
|
+
...queuesWithoutReplica.keys(),
|
|
505
|
+
]),
|
|
506
|
+
].filter((locator) => !listed.has(locator));
|
|
473
507
|
}
|
|
474
508
|
async function dropUnlisted(listed, local) {
|
|
475
509
|
if (unlistedHeld(listed).length === 0)
|
|
@@ -477,24 +511,21 @@ export function composeRuntime(options) {
|
|
|
477
511
|
await exclusive(async () => {
|
|
478
512
|
const unlisted = unlistedHeld(listed);
|
|
479
513
|
await local.dropRooms(unlisted);
|
|
480
|
-
for (const [batch, locator] of outcomeRooms) {
|
|
481
|
-
if (listed.has(locator))
|
|
482
|
-
continue;
|
|
483
|
-
outcomeRooms.delete(batch);
|
|
484
|
-
outcomes.delete(batch);
|
|
485
|
-
}
|
|
486
514
|
const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
|
|
487
515
|
room: true,
|
|
488
516
|
queue: true,
|
|
489
517
|
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
490
518
|
}));
|
|
491
519
|
for (const locator of unlisted) {
|
|
492
|
-
for (const batch of replicas.get(locator)?.queue ?? [])
|
|
520
|
+
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
493
521
|
pendings.delete(batch.id);
|
|
494
522
|
replicas.delete(locator);
|
|
523
|
+
queuesWithoutReplica.delete(locator);
|
|
495
524
|
rooms.delete(locator);
|
|
496
525
|
roomDrafts.delete(locator);
|
|
526
|
+
transfers.delete(locator);
|
|
497
527
|
unreadable.delete(locator);
|
|
528
|
+
stale.delete(locator);
|
|
498
529
|
scopedRefusals.delete(locator);
|
|
499
530
|
sourceRefusals.delete(locator);
|
|
500
531
|
declarationRefusals.delete(locator);
|
|
@@ -503,7 +534,6 @@ export function composeRuntime(options) {
|
|
|
503
534
|
models.delete(key);
|
|
504
535
|
liveModels.delete(key);
|
|
505
536
|
invalidModels.delete(key);
|
|
506
|
-
modelEpoch.delete(key);
|
|
507
537
|
}
|
|
508
538
|
journal.record(`room unlisted ${locator}`);
|
|
509
539
|
}
|
|
@@ -516,7 +546,6 @@ export function composeRuntime(options) {
|
|
|
516
546
|
if (prior !== undefined &&
|
|
517
547
|
prior.status !== 'unavailable' &&
|
|
518
548
|
(persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
|
|
519
|
-
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
520
549
|
invalidModels.add(key);
|
|
521
550
|
}
|
|
522
551
|
const model = persisted.status === 'unavailable' ||
|
|
@@ -531,10 +560,9 @@ export function composeRuntime(options) {
|
|
|
531
560
|
return model;
|
|
532
561
|
}
|
|
533
562
|
async function hydrate(opened) {
|
|
534
|
-
const [records, queues,
|
|
563
|
+
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
|
|
535
564
|
opened.all(),
|
|
536
565
|
opened.queues(),
|
|
537
|
-
opened.outcomes(),
|
|
538
566
|
opened.roomDrafts(),
|
|
539
567
|
opened.presentation(),
|
|
540
568
|
opened.conversations(),
|
|
@@ -542,11 +570,15 @@ export function composeRuntime(options) {
|
|
|
542
570
|
opened.lastCompleted(),
|
|
543
571
|
opened.providerLimits(),
|
|
544
572
|
opened.capabilities(),
|
|
545
|
-
opened.
|
|
573
|
+
opened.member(),
|
|
574
|
+
opened.transfers(),
|
|
546
575
|
]);
|
|
576
|
+
transfers.clear();
|
|
577
|
+
for (const record of staged)
|
|
578
|
+
transfers.set(record.locator, record);
|
|
547
579
|
limits = declared;
|
|
548
580
|
capabilities = named;
|
|
549
|
-
|
|
581
|
+
member = known;
|
|
550
582
|
roomDrafts.clear();
|
|
551
583
|
for (const record of kept)
|
|
552
584
|
rememberRoomDraft(record);
|
|
@@ -560,27 +592,20 @@ export function composeRuntime(options) {
|
|
|
560
592
|
for (const { batch, state } of queue.pending)
|
|
561
593
|
pendings.set(batch.id, state);
|
|
562
594
|
}
|
|
563
|
-
outcomes.clear();
|
|
564
|
-
outcomeRooms.clear();
|
|
565
|
-
for (const record of recordedOutcomes) {
|
|
566
|
-
outcomes.set(record.id, record.outcome);
|
|
567
|
-
outcomeRooms.set(record.id, record.locator);
|
|
568
|
-
}
|
|
569
595
|
const previousReplicas = new Map(replicas);
|
|
570
596
|
replicas.clear();
|
|
597
|
+
queuesWithoutReplica.clear();
|
|
598
|
+
const replicated = new Set(records.map((record) => record.locator));
|
|
599
|
+
for (const [locator, queue] of queuedBatches) {
|
|
600
|
+
if (!replicated.has(locator))
|
|
601
|
+
queuesWithoutReplica.set(locator, queue);
|
|
602
|
+
}
|
|
571
603
|
for (const record of records) {
|
|
572
604
|
const model = restored(modelKey(record.locator, 'collection'), record.model);
|
|
573
605
|
const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
|
|
574
606
|
const prior = previousReplicas.get(record.locator);
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
const confirmed = read.kind === 'staged'
|
|
578
|
-
? read.confirmed
|
|
579
|
-
: {
|
|
580
|
-
record: retyped,
|
|
581
|
-
collection: parseCollection(retyped.documents, compositionOf(retyped)),
|
|
582
|
-
};
|
|
583
|
-
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], rewriting(), prior));
|
|
607
|
+
const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
|
|
608
|
+
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
|
|
584
609
|
const existing = rooms.get(record.locator);
|
|
585
610
|
if (existing === undefined)
|
|
586
611
|
list(record, false);
|
|
@@ -588,25 +613,17 @@ export function composeRuntime(options) {
|
|
|
588
613
|
rooms.set(record.locator, {
|
|
589
614
|
...existing,
|
|
590
615
|
roomVersion: record.roomVersion,
|
|
591
|
-
attachments: record.attachments,
|
|
592
616
|
operations: record.operations,
|
|
593
617
|
replica: record.replica,
|
|
594
|
-
attachedModel: record.attachedModel,
|
|
595
618
|
model: modelTypes(model),
|
|
596
619
|
typedSelection: record.typedSelection === null
|
|
597
620
|
? null
|
|
598
621
|
: {
|
|
599
622
|
roots: record.typedSelection.roots,
|
|
600
|
-
attachedModel: record.typedSelection.attachedModel,
|
|
601
623
|
model: modelTypes(selectionModel),
|
|
602
624
|
},
|
|
603
|
-
untypedFolders: record.untypedFolders,
|
|
604
625
|
});
|
|
605
626
|
}
|
|
606
|
-
if (read.kind === 'refused') {
|
|
607
|
-
refusedSource(record, read);
|
|
608
|
-
declarationRefusals.add(record.locator);
|
|
609
|
-
}
|
|
610
627
|
}
|
|
611
628
|
return {
|
|
612
629
|
lastCompleted,
|
|
@@ -619,6 +636,8 @@ export function composeRuntime(options) {
|
|
|
619
636
|
applyChangeSet(read);
|
|
620
637
|
if (replicas.size > 0)
|
|
621
638
|
emitRooms();
|
|
639
|
+
if (member !== null)
|
|
640
|
+
emitMember();
|
|
622
641
|
for (const locator of locators)
|
|
623
642
|
emitQueue(locator);
|
|
624
643
|
publishRoomDrafts(false);
|
|
@@ -666,10 +685,15 @@ export function composeRuntime(options) {
|
|
|
666
685
|
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
667
686
|
}
|
|
668
687
|
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
669
|
-
const
|
|
688
|
+
const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
689
|
+
const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
|
|
670
690
|
const kept = queue.filter((batch) => !withdrawn.includes(batch));
|
|
691
|
+
const unqueued = withdrawn.map((batch) => ({
|
|
692
|
+
batch,
|
|
693
|
+
returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
|
|
694
|
+
}));
|
|
671
695
|
const returning = [
|
|
672
|
-
...
|
|
696
|
+
...[...unqueued].reverse(),
|
|
673
697
|
...(settling.leaving === undefined ? [] : [settling.leaving]),
|
|
674
698
|
];
|
|
675
699
|
const written = new Map();
|
|
@@ -687,69 +711,110 @@ export function composeRuntime(options) {
|
|
|
687
711
|
});
|
|
688
712
|
}
|
|
689
713
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
: [{ id: settling.leaving.batch.id, outcome: settling.leaving.outcome }]),
|
|
695
|
-
...withdrawn.map((batch) => ({ id: batch.id, outcome: { kind: 'withdrawn' } })),
|
|
696
|
-
];
|
|
697
|
-
await local.write({
|
|
698
|
-
next: confirmed.record,
|
|
699
|
-
previous: before?.confirmed.record ?? null,
|
|
700
|
-
queue: kept,
|
|
701
|
-
previousQueue: before?.queue ?? [],
|
|
702
|
-
outcomes: settledOutcomes,
|
|
703
|
-
roomDrafts: [...written.values()],
|
|
704
|
-
});
|
|
705
|
-
for (const { id, outcome } of settledOutcomes) {
|
|
706
|
-
outcomes.set(id, outcome);
|
|
707
|
-
outcomeRooms.set(id, locator);
|
|
708
|
-
pendings.delete(id);
|
|
714
|
+
await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
|
|
715
|
+
for (const batch of previousQueue) {
|
|
716
|
+
if (!kept.includes(batch))
|
|
717
|
+
pendings.delete(batch.id);
|
|
709
718
|
}
|
|
710
719
|
for (const record of written.values())
|
|
711
720
|
rememberRoomDraft(record);
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
721
|
+
try {
|
|
722
|
+
await local.write({
|
|
723
|
+
next: confirmed.record,
|
|
724
|
+
previous: before?.confirmed.record ?? null,
|
|
725
|
+
...(settling.stored === undefined ? {} : { stored: settling.stored }),
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
catch (error) {
|
|
729
|
+
const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
|
|
730
|
+
if (held !== null)
|
|
731
|
+
replicas.set(locator, held);
|
|
732
|
+
else if (kept.length > 0)
|
|
733
|
+
queuesWithoutReplica.set(locator, kept);
|
|
734
|
+
else
|
|
735
|
+
queuesWithoutReplica.delete(locator);
|
|
736
|
+
publishChangeSet(changeSet(locator, before ?? null, held, {
|
|
737
|
+
queue: kept.length !== previousQueue.length,
|
|
738
|
+
drafts: [...written.keys()],
|
|
739
|
+
}));
|
|
740
|
+
moveTo({ ...status, ...attention() });
|
|
741
|
+
throw error;
|
|
742
|
+
}
|
|
743
|
+
queuesWithoutReplica.delete(locator);
|
|
744
|
+
for (const { batch, returned } of unqueued) {
|
|
745
|
+
const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
|
|
746
|
+
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
747
|
+
}
|
|
748
|
+
replicas.set(locator, replicaOf(confirmed, kept, before));
|
|
715
749
|
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
716
750
|
room: settling.room,
|
|
717
|
-
queue: kept.length !==
|
|
751
|
+
queue: kept.length !== previousQueue.length,
|
|
718
752
|
drafts: [...written.keys()],
|
|
719
753
|
});
|
|
720
754
|
}
|
|
721
|
-
function
|
|
755
|
+
function adoptModels(locator, record) {
|
|
756
|
+
for (const [attached, model] of [
|
|
757
|
+
['collection', record.model],
|
|
758
|
+
['typed-selection', record.selectionModel],
|
|
759
|
+
]) {
|
|
760
|
+
const key = modelKey(locator, attached);
|
|
761
|
+
models.set(key, model);
|
|
762
|
+
invalidModels.delete(key);
|
|
763
|
+
if (model.status === 'current')
|
|
764
|
+
liveModels.set(key, JSON.stringify(model.snapshot));
|
|
765
|
+
else if (model.status === 'unavailable')
|
|
766
|
+
liveModels.delete(key);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
function remodel(locator, answers, held) {
|
|
770
|
+
const taken = (answer, previous, attached) => {
|
|
771
|
+
if (answer === undefined)
|
|
772
|
+
return previous;
|
|
773
|
+
const { state, refused } = attachedModel(answer, previous);
|
|
774
|
+
if (refused !== null) {
|
|
775
|
+
const where = refused.location === undefined ? '' : ` ${refused.location}`;
|
|
776
|
+
journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
|
|
777
|
+
}
|
|
778
|
+
models.set(modelKey(locator, attached), state);
|
|
779
|
+
return state;
|
|
780
|
+
};
|
|
781
|
+
return {
|
|
782
|
+
collection: taken(answers?.collection, held.collection, 'collection'),
|
|
783
|
+
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
function publish(local, room, confirmed, landing = {}) {
|
|
787
|
+
const { base, stored } = landing;
|
|
722
788
|
return exclusive(async () => {
|
|
723
789
|
if (!continuing())
|
|
724
|
-
return;
|
|
790
|
+
return true;
|
|
725
791
|
const prior = replicas.get(room.locator);
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
const
|
|
734
|
-
const roomVersion = retained.confirmed.record.roomVersion;
|
|
792
|
+
if (base !== undefined &&
|
|
793
|
+
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
794
|
+
prior.confirmed.record.revisions !== base.record.revisions)) {
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
797
|
+
adoptModels(room.locator, confirmed.record);
|
|
798
|
+
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
799
|
+
const roomVersion = confirmed.record.roomVersion;
|
|
735
800
|
const accepted = queue.filter((batch) => {
|
|
736
|
-
const
|
|
737
|
-
return (
|
|
738
|
-
options.adapter.reached(room.locator, roomVersion, outcome.roomVersion));
|
|
801
|
+
const accepted = pendings.get(batch.id)?.accepted;
|
|
802
|
+
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
739
803
|
});
|
|
740
|
-
const set = await settle(local, room.locator, prior,
|
|
804
|
+
const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
|
|
805
|
+
if (stored !== undefined)
|
|
806
|
+
transfers.delete(room.locator);
|
|
741
807
|
for (const batch of accepted)
|
|
742
808
|
pendings.delete(batch.id);
|
|
743
|
-
|
|
744
|
-
if (
|
|
745
|
-
|
|
746
|
-
}
|
|
747
|
-
unreadable.delete(room.locator);
|
|
809
|
+
stale.delete(room.locator);
|
|
810
|
+
if (unreadable.delete(room.locator))
|
|
811
|
+
scopedRefusals.delete(room.locator);
|
|
748
812
|
sourceRefusals.delete(room.locator);
|
|
749
813
|
declarationRefusals.delete(room.locator);
|
|
750
814
|
list(room, true);
|
|
751
815
|
publishChangeSet(set);
|
|
752
816
|
moveTo({ ...status, ...attention() });
|
|
817
|
+
return true;
|
|
753
818
|
});
|
|
754
819
|
}
|
|
755
820
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
@@ -757,61 +822,140 @@ export function composeRuntime(options) {
|
|
|
757
822
|
const replica = replicas.get(locator);
|
|
758
823
|
const room = rooms.get(locator);
|
|
759
824
|
if (replica === undefined || room === undefined) {
|
|
760
|
-
throw
|
|
825
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
761
826
|
}
|
|
762
827
|
const saved = [];
|
|
763
828
|
for (const { path, revision } of named) {
|
|
764
829
|
const record = roomDrafts.get(locator)?.get(path);
|
|
765
830
|
if (record === undefined || record.revision !== revision) {
|
|
766
|
-
return { outcome: 'refused',
|
|
831
|
+
return { outcome: 'refused', code: 'stale', retryable: false, file: path };
|
|
767
832
|
}
|
|
768
833
|
if (!saved.some((draft) => draft.path === path))
|
|
769
834
|
saved.push({ path, parts: record.parts });
|
|
770
835
|
}
|
|
771
836
|
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
|
|
772
837
|
if (changed.length > 0)
|
|
773
|
-
return { outcome: 'refused',
|
|
838
|
+
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
774
839
|
const parts = saved.map(({ path, parts }) => changesOf(path, parts));
|
|
775
|
-
const
|
|
840
|
+
const changes = parts.flatMap(({ changes }) => changes);
|
|
841
|
+
const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
|
|
842
|
+
if (requeue.kind === 'refused')
|
|
843
|
+
return { outcome: 'refused', ...requeue.refusal };
|
|
844
|
+
const foreign = [...requeue.withdrawn.keys()].some((origin) => {
|
|
845
|
+
const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
|
|
846
|
+
return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
|
|
847
|
+
});
|
|
848
|
+
if (foreign) {
|
|
849
|
+
return {
|
|
850
|
+
outcome: 'refused',
|
|
851
|
+
code: 'unknown-revision',
|
|
852
|
+
retryable: false,
|
|
853
|
+
file: [...requeue.queued].at(-1),
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
const withdrawing = requeue.withdrawn.size > 0;
|
|
857
|
+
const input = {
|
|
858
|
+
local: localSaves,
|
|
776
859
|
baseRoomVersion: replica.confirmed.record.roomVersion,
|
|
777
860
|
composition: compositionFor(replica.confirmed.record),
|
|
778
861
|
operations: room.operations,
|
|
779
862
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
780
863
|
providerLimits: limits,
|
|
781
|
-
|
|
864
|
+
handle: room.handle,
|
|
782
865
|
visible: replica.visible,
|
|
783
|
-
changes
|
|
866
|
+
changes,
|
|
784
867
|
bases: parts.flatMap(({ bases }) => bases),
|
|
785
868
|
writtenBy,
|
|
869
|
+
};
|
|
870
|
+
const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
|
|
871
|
+
if (checked?.kind === 'refused')
|
|
872
|
+
return { outcome: 'refused', ...checked.refusal };
|
|
873
|
+
const owners = [...requeue.owners].flatMap((origin) => {
|
|
874
|
+
const batch = requeue.withdrawn.get(origin);
|
|
875
|
+
return batch === undefined || batch === null ? [] : [{ origin, batch }];
|
|
786
876
|
});
|
|
787
|
-
if (
|
|
877
|
+
if (owners.length > 1) {
|
|
878
|
+
return {
|
|
879
|
+
outcome: 'refused',
|
|
880
|
+
code: 'unknown-revision',
|
|
881
|
+
retryable: false,
|
|
882
|
+
file: [...requeue.queued].at(-1),
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
const built = !withdrawing
|
|
886
|
+
? buildBatch(input)
|
|
887
|
+
: requeue.changes.length === 0
|
|
888
|
+
? null
|
|
889
|
+
: buildBatch({
|
|
890
|
+
...input,
|
|
891
|
+
visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
|
|
892
|
+
changes: requeue.changes,
|
|
893
|
+
});
|
|
894
|
+
if (built?.kind === 'refused')
|
|
788
895
|
return { outcome: 'refused', ...built.refusal };
|
|
789
|
-
const
|
|
896
|
+
const [owner] = owners;
|
|
897
|
+
const join = built === null || owner === undefined
|
|
898
|
+
? null
|
|
899
|
+
: joined(requeue.queue, owner.batch, built.batch);
|
|
900
|
+
if (join?.kind === 'refused')
|
|
901
|
+
return { outcome: 'refused', ...join.refusal };
|
|
902
|
+
const withdrawn = new Map(requeue.withdrawn);
|
|
903
|
+
if (join !== null && owner !== undefined)
|
|
904
|
+
withdrawn.set(owner.origin, join.batch);
|
|
905
|
+
const drafted = new Map();
|
|
906
|
+
for (const [origin, replaced] of withdrawn) {
|
|
907
|
+
if (replaced === null)
|
|
908
|
+
continue;
|
|
909
|
+
const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
|
|
910
|
+
drafted.set(replaced.id, replaced === join?.batch
|
|
911
|
+
? [
|
|
912
|
+
...carried.map((held) => joinedDraft(held, saved)),
|
|
913
|
+
...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
|
|
914
|
+
]
|
|
915
|
+
: carried);
|
|
916
|
+
}
|
|
917
|
+
if (built !== null && join === null)
|
|
918
|
+
drafted.set(built.batch.id, saved);
|
|
919
|
+
const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
|
|
920
|
+
for (const replaced of withdrawn.values()) {
|
|
921
|
+
if (replaced === null || !batches.includes(replaced))
|
|
922
|
+
continue;
|
|
923
|
+
const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
|
|
924
|
+
if (limited !== null)
|
|
925
|
+
return { outcome: 'refused', ...limited };
|
|
926
|
+
}
|
|
790
927
|
const consumed = saved.map(({ path }) => path);
|
|
791
928
|
try {
|
|
792
|
-
await local.
|
|
793
|
-
|
|
794
|
-
previous: replica.confirmed.record,
|
|
929
|
+
await local.keepAuthored({
|
|
930
|
+
locator,
|
|
795
931
|
queue: batches,
|
|
796
|
-
|
|
797
|
-
saved,
|
|
932
|
+
saved: drafted,
|
|
798
933
|
consumedRoomDrafts: consumed,
|
|
799
934
|
});
|
|
800
935
|
}
|
|
801
936
|
catch {
|
|
802
|
-
return { outcome: 'refused',
|
|
937
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
938
|
+
}
|
|
939
|
+
for (const origin of withdrawn.keys())
|
|
940
|
+
pendings.delete(origin);
|
|
941
|
+
for (const [id, drafts] of drafted) {
|
|
942
|
+
pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
|
|
803
943
|
}
|
|
804
|
-
|
|
944
|
+
if (!localSaves)
|
|
945
|
+
recorded = false;
|
|
805
946
|
for (const path of consumed)
|
|
806
947
|
forgetRoomDraft(locator, path);
|
|
807
|
-
replicas.set(locator, replicaOf(replica.confirmed, batches,
|
|
948
|
+
replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
|
|
808
949
|
publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
|
|
809
950
|
queue: true,
|
|
810
951
|
drafts: consumed,
|
|
811
952
|
}));
|
|
812
|
-
moveTo({ ...saving(status), ...attention() });
|
|
813
|
-
|
|
814
|
-
|
|
953
|
+
moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
|
|
954
|
+
submitLater(locator);
|
|
955
|
+
const identifier = join?.batch.id ??
|
|
956
|
+
built?.batch.id ??
|
|
957
|
+
[...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
|
|
958
|
+
return { outcome: 'saved', identifier };
|
|
815
959
|
}
|
|
816
960
|
async function mark(local, locator, identifier, submission, lost) {
|
|
817
961
|
const state = pendings.get(identifier);
|
|
@@ -837,16 +981,15 @@ export function composeRuntime(options) {
|
|
|
837
981
|
}
|
|
838
982
|
async function readHistory(locator, path, query = {}) {
|
|
839
983
|
if (closed || refusal !== null)
|
|
840
|
-
return
|
|
984
|
+
return UNCERTAIN;
|
|
841
985
|
try {
|
|
842
986
|
await ensureAuthorization();
|
|
843
987
|
}
|
|
844
988
|
catch {
|
|
845
|
-
return
|
|
846
|
-
}
|
|
847
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
848
|
-
return { kind: 'transport-uncertainty' };
|
|
989
|
+
return UNCERTAIN;
|
|
849
990
|
}
|
|
991
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
992
|
+
return UNCERTAIN;
|
|
850
993
|
const transfer = ['transfer', 'history', locator, path];
|
|
851
994
|
if (query.revision !== undefined)
|
|
852
995
|
transfer.push(query.revision);
|
|
@@ -868,17 +1011,101 @@ export function composeRuntime(options) {
|
|
|
868
1011
|
}
|
|
869
1012
|
if (result.kind === 'revoked-authorization')
|
|
870
1013
|
await refuse('revoked-authorization');
|
|
871
|
-
return result;
|
|
1014
|
+
return historyAnswer(result);
|
|
872
1015
|
}
|
|
873
1016
|
catch {
|
|
874
1017
|
journal.record('outcome history transport-uncertainty');
|
|
875
|
-
return
|
|
1018
|
+
return UNCERTAIN;
|
|
876
1019
|
}
|
|
877
1020
|
}
|
|
878
|
-
function
|
|
1021
|
+
function queuedBlob(locator, path, localId) {
|
|
1022
|
+
for (const batch of replicas.get(locator)?.queue ?? []) {
|
|
1023
|
+
const change = batch.changes[queuedFile(batch, path, localId)];
|
|
1024
|
+
if (change?.kind === 'binary')
|
|
1025
|
+
return change.blob;
|
|
1026
|
+
}
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
1029
|
+
async function readBinary(request) {
|
|
1030
|
+
if (closed || refusal !== null)
|
|
1031
|
+
return UNCERTAIN;
|
|
1032
|
+
try {
|
|
1033
|
+
await ensureAuthorization();
|
|
1034
|
+
}
|
|
1035
|
+
catch {
|
|
1036
|
+
return UNCERTAIN;
|
|
1037
|
+
}
|
|
1038
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1039
|
+
return UNCERTAIN;
|
|
1040
|
+
journal.record(request.kind === 'path'
|
|
1041
|
+
? `transfer binary ${request.locator} ${request.path}`
|
|
1042
|
+
: `transfer binary ${request.fileRef}`);
|
|
1043
|
+
try {
|
|
1044
|
+
const result = await options.adapter.binary(authorization, request);
|
|
1045
|
+
journal.record(`outcome binary ${result.kind}`);
|
|
1046
|
+
if (result.kind === 'scoped-refusal') {
|
|
1047
|
+
const room = replicas.get(result.locator)?.confirmed.record;
|
|
1048
|
+
if (room !== undefined)
|
|
1049
|
+
await scoped(result, room, await openLocal());
|
|
1050
|
+
}
|
|
1051
|
+
if (result.kind === 'revoked-authorization')
|
|
1052
|
+
await refuse('revoked-authorization');
|
|
1053
|
+
return binaryAnswer(result);
|
|
1054
|
+
}
|
|
1055
|
+
catch {
|
|
1056
|
+
journal.record('outcome binary transport-uncertainty');
|
|
1057
|
+
return UNCERTAIN;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
async function readBinaryPath(locator, path, revision) {
|
|
1061
|
+
await ready;
|
|
1062
|
+
if (revision !== undefined)
|
|
1063
|
+
return readBinary({ kind: 'path', locator, path, revision });
|
|
1064
|
+
const visible = replicas.get(locator)?.visible;
|
|
1065
|
+
const held = visible?.binaries[path];
|
|
1066
|
+
if (visible === undefined || held === undefined)
|
|
1067
|
+
return absentBinary(path);
|
|
1068
|
+
if (held.state === 'pending') {
|
|
1069
|
+
const blob = queuedBlob(locator, path, held.localId);
|
|
1070
|
+
return blob === null ? absentBinary(path) : { kind: 'binary', blob };
|
|
1071
|
+
}
|
|
1072
|
+
const record = binaryRecord(path, held, visible.revisions[path]);
|
|
1073
|
+
if (record.state !== 'accepted')
|
|
1074
|
+
return absentBinary(path);
|
|
1075
|
+
const replica = replicas.get(locator);
|
|
1076
|
+
const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
|
|
1077
|
+
const holds = (at) => {
|
|
1078
|
+
const kept = replica?.confirmed.record.binaries[at];
|
|
1079
|
+
return (kept?.state === 'accepted' &&
|
|
1080
|
+
kept.fileRef === record.fileRef &&
|
|
1081
|
+
replica?.confirmed.record.revisions[at] === record.revision);
|
|
1082
|
+
};
|
|
1083
|
+
let confirmed = path;
|
|
1084
|
+
for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
|
|
1085
|
+
const change = queued[index];
|
|
1086
|
+
if (change.kind === 'move' && change.to === confirmed)
|
|
1087
|
+
confirmed = change.path;
|
|
1088
|
+
}
|
|
1089
|
+
return readBinary({
|
|
1090
|
+
kind: 'path',
|
|
1091
|
+
locator,
|
|
1092
|
+
path: confirmed,
|
|
1093
|
+
revision: record.revision,
|
|
1094
|
+
fileRef: record.fileRef,
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
async function readBinaryReference(reference) {
|
|
1098
|
+
await ready;
|
|
1099
|
+
return readBinary({
|
|
1100
|
+
kind: 'reference',
|
|
1101
|
+
fileRef: reference.fileRef,
|
|
1102
|
+
referrer: reference.referrer,
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
function declaredAgents() {
|
|
879
1106
|
for (const capability of capabilities) {
|
|
880
|
-
if (capability.
|
|
881
|
-
return capability.
|
|
1107
|
+
if (capability.conversationAgents !== undefined)
|
|
1108
|
+
return capability.conversationAgents;
|
|
882
1109
|
}
|
|
883
1110
|
return undefined;
|
|
884
1111
|
}
|
|
@@ -894,23 +1121,20 @@ export function composeRuntime(options) {
|
|
|
894
1121
|
async function invokeAction(name, input) {
|
|
895
1122
|
await ready;
|
|
896
1123
|
if (closed || refusal !== null)
|
|
897
|
-
return
|
|
1124
|
+
return UNCERTAIN;
|
|
898
1125
|
if (declaredAction(name) === undefined)
|
|
899
|
-
return
|
|
1126
|
+
return undeclared(name);
|
|
900
1127
|
try {
|
|
901
1128
|
await ensureAuthorization();
|
|
902
1129
|
}
|
|
903
1130
|
catch {
|
|
904
|
-
return
|
|
905
|
-
}
|
|
906
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
907
|
-
return { kind: 'transport-uncertainty' };
|
|
1131
|
+
return UNCERTAIN;
|
|
908
1132
|
}
|
|
1133
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1134
|
+
return UNCERTAIN;
|
|
909
1135
|
journal.record(`transfer action ${name}`);
|
|
910
1136
|
try {
|
|
911
1137
|
const result = await options.adapter.action(authorization, { name, input });
|
|
912
|
-
if (result.kind === 'action' && result.at !== undefined)
|
|
913
|
-
want(result.at);
|
|
914
1138
|
if (result.kind === 'scoped-refusal') {
|
|
915
1139
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
916
1140
|
if (room !== undefined)
|
|
@@ -921,11 +1145,11 @@ export function composeRuntime(options) {
|
|
|
921
1145
|
journal.record(`outcome action ${result.kind}`);
|
|
922
1146
|
if (result.kind === 'rejected')
|
|
923
1147
|
journal.record(`refusal rejected ${result.code}`);
|
|
924
|
-
return result;
|
|
1148
|
+
return calledResult(result);
|
|
925
1149
|
}
|
|
926
1150
|
catch {
|
|
927
1151
|
journal.record('outcome action transport-uncertainty');
|
|
928
|
-
return
|
|
1152
|
+
return UNCERTAIN;
|
|
929
1153
|
}
|
|
930
1154
|
}
|
|
931
1155
|
function continuing() {
|
|
@@ -942,32 +1166,11 @@ export function composeRuntime(options) {
|
|
|
942
1166
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
943
1167
|
!options.adapter.reached(locator, roomVersion, written));
|
|
944
1168
|
}
|
|
945
|
-
function
|
|
946
|
-
if (reachedBy(locator, roomVersion))
|
|
947
|
-
return;
|
|
948
|
-
const held = wanted.get(locator);
|
|
949
|
-
if (held === undefined || options.adapter.reached(locator, roomVersion, held)) {
|
|
950
|
-
wanted.set(locator, roomVersion);
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
function reachOf(locator) {
|
|
954
|
-
let target = wanted.get(locator);
|
|
955
|
-
const versions = (replicas.get(locator)?.queue ?? []).flatMap((batch) => {
|
|
956
|
-
const outcome = outcomes.get(batch.id);
|
|
957
|
-
return outcome?.kind === 'accepted' ? [outcome.roomVersion] : [];
|
|
958
|
-
});
|
|
959
|
-
for (const version of versions) {
|
|
960
|
-
if (target === undefined || options.adapter.reached(locator, version, target)) {
|
|
961
|
-
target = version;
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
return target === undefined || reachedBy(locator, target) ? undefined : target;
|
|
965
|
-
}
|
|
966
|
-
async function pullRefresh(authorized, confirmed, minPosition) {
|
|
1169
|
+
async function pullRefresh(authorized, confirmed) {
|
|
967
1170
|
if (!continuing())
|
|
968
1171
|
return unconfirmed;
|
|
969
1172
|
try {
|
|
970
|
-
const result = await refreshRoom(options.adapter, authorized, confirmed,
|
|
1173
|
+
const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
|
|
971
1174
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
972
1175
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
973
1176
|
return result;
|
|
@@ -977,14 +1180,33 @@ export function composeRuntime(options) {
|
|
|
977
1180
|
throw error;
|
|
978
1181
|
}
|
|
979
1182
|
}
|
|
980
|
-
|
|
1183
|
+
function staging(local) {
|
|
1184
|
+
return {
|
|
1185
|
+
held: (locator) => transfers.get(locator) ?? null,
|
|
1186
|
+
staged: (record) => local.staged(record),
|
|
1187
|
+
stage: (record, page) => exclusive(async () => {
|
|
1188
|
+
if (!continuing())
|
|
1189
|
+
return false;
|
|
1190
|
+
await local.stage(record, page);
|
|
1191
|
+
transfers.set(record.locator, record);
|
|
1192
|
+
return true;
|
|
1193
|
+
}),
|
|
1194
|
+
abandon: (locator) => exclusive(async () => {
|
|
1195
|
+
if (!continuing())
|
|
1196
|
+
return;
|
|
1197
|
+
await local.abandon(locator);
|
|
1198
|
+
transfers.delete(locator);
|
|
1199
|
+
}),
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
async function pullBootstrap(authorized, room, local) {
|
|
981
1203
|
if (!continuing())
|
|
982
1204
|
return unconfirmed;
|
|
983
1205
|
try {
|
|
984
1206
|
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
985
1207
|
collection: modelOf(room.locator),
|
|
986
1208
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
987
|
-
}, continuing);
|
|
1209
|
+
}, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
|
|
988
1210
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
989
1211
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
990
1212
|
return result;
|
|
@@ -997,35 +1219,46 @@ export function composeRuntime(options) {
|
|
|
997
1219
|
async function advance(room, local, authorized) {
|
|
998
1220
|
const held = replicas.get(room.locator);
|
|
999
1221
|
if (held !== undefined) {
|
|
1000
|
-
const refreshed = await pullRefresh(authorized, held.confirmed
|
|
1222
|
+
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
1001
1223
|
if (!continuing())
|
|
1002
1224
|
return unconfirmed;
|
|
1003
1225
|
if (refreshed.kind === 'still-current') {
|
|
1226
|
+
stale.delete(room.locator);
|
|
1004
1227
|
if (sourceRefusals.has(room.locator))
|
|
1005
|
-
return
|
|
1228
|
+
return null;
|
|
1006
1229
|
if (unreadable.delete(room.locator)) {
|
|
1230
|
+
scopedRefusals.delete(room.locator);
|
|
1007
1231
|
list(room, true);
|
|
1008
1232
|
emitRooms();
|
|
1009
1233
|
}
|
|
1010
1234
|
return null;
|
|
1011
1235
|
}
|
|
1012
1236
|
if (refreshed.kind === 'advanced') {
|
|
1013
|
-
await publish(local, room, refreshed.confirmed)
|
|
1237
|
+
if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
|
|
1238
|
+
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1239
|
+
const reached = now !== undefined &&
|
|
1240
|
+
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
1241
|
+
return reached ? null : advance(room, local, authorized);
|
|
1242
|
+
}
|
|
1243
|
+
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1014
1244
|
return null;
|
|
1015
1245
|
}
|
|
1016
1246
|
if (refreshed.kind !== 'beyond-horizon')
|
|
1017
1247
|
return refreshed;
|
|
1018
1248
|
}
|
|
1019
|
-
const bootstrapped = await pullBootstrap(authorized, room);
|
|
1249
|
+
const bootstrapped = await pullBootstrap(authorized, room, local);
|
|
1020
1250
|
if (!continuing())
|
|
1021
1251
|
return unconfirmed;
|
|
1022
1252
|
if (bootstrapped.kind !== 'staged')
|
|
1023
1253
|
return bootstrapped;
|
|
1024
|
-
await publish(local, room, bootstrapped.confirmed);
|
|
1254
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
1025
1255
|
return null;
|
|
1026
1256
|
}
|
|
1027
1257
|
async function withhold(withheld, room, local, progress) {
|
|
1028
|
-
|
|
1258
|
+
if (withheld.kind === 'refused')
|
|
1259
|
+
stale.delete(room.locator);
|
|
1260
|
+
else
|
|
1261
|
+
stale.add(room.locator);
|
|
1029
1262
|
if (withheld.kind === 'scoped-refusal') {
|
|
1030
1263
|
await scoped(withheld, room, local);
|
|
1031
1264
|
}
|
|
@@ -1050,6 +1283,7 @@ export function composeRuntime(options) {
|
|
|
1050
1283
|
function refusedSource(room, { refusal }) {
|
|
1051
1284
|
sourceRefusals.set(room.locator, {
|
|
1052
1285
|
code: refusal.rule,
|
|
1286
|
+
retryable: false,
|
|
1053
1287
|
fields: {
|
|
1054
1288
|
file: refusal.file,
|
|
1055
1289
|
...(refusal.location === undefined ? {} : { location: refusal.location }),
|
|
@@ -1063,34 +1297,16 @@ export function composeRuntime(options) {
|
|
|
1063
1297
|
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
1064
1298
|
}
|
|
1065
1299
|
function leaving(batch, result) {
|
|
1066
|
-
if (result.kind === 'expired')
|
|
1067
|
-
return { batch,
|
|
1068
|
-
}
|
|
1069
|
-
const refusal = { code: result.code, fields: result.fields };
|
|
1070
|
-
if (result.kind === 'conflicted') {
|
|
1071
|
-
return {
|
|
1072
|
-
batch,
|
|
1073
|
-
outcome: {
|
|
1074
|
-
kind: 'conflicted',
|
|
1075
|
-
...(result.roomVersion === undefined ? {} : { roomVersion: result.roomVersion }),
|
|
1076
|
-
affected: result.affected,
|
|
1077
|
-
...refusal,
|
|
1078
|
-
},
|
|
1079
|
-
returned: { kind: 'conflicted', ...refusal },
|
|
1080
|
-
};
|
|
1081
|
-
}
|
|
1300
|
+
if (result.kind === 'expired')
|
|
1301
|
+
return { batch, returned: returnedBy('expired') };
|
|
1082
1302
|
return {
|
|
1083
1303
|
batch,
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
kind: 'accepted',
|
|
1091
|
-
roomVersion: accepted.roomVersion,
|
|
1092
|
-
...(accepted.author === undefined ? {} : { author: accepted.author }),
|
|
1093
|
-
...(accepted.time === undefined ? {} : { time: accepted.time }),
|
|
1304
|
+
returned: {
|
|
1305
|
+
kind: result.kind,
|
|
1306
|
+
code: result.code,
|
|
1307
|
+
fields: result.fields,
|
|
1308
|
+
retryable: result.retryable,
|
|
1309
|
+
},
|
|
1094
1310
|
};
|
|
1095
1311
|
}
|
|
1096
1312
|
function logAccepted(locator, submitted, accepted) {
|
|
@@ -1098,49 +1314,30 @@ export function composeRuntime(options) {
|
|
|
1098
1314
|
(accepted.author === undefined ? '' : ` author ${accepted.author}`) +
|
|
1099
1315
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1100
1316
|
}
|
|
1101
|
-
async function
|
|
1102
|
-
await exclusive(async () => {
|
|
1103
|
-
if (!continuing())
|
|
1104
|
-
return;
|
|
1105
|
-
const current = replicas.get(room.locator);
|
|
1106
|
-
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
1107
|
-
return;
|
|
1108
|
-
}
|
|
1109
|
-
const outcome = acceptedOf(accepted);
|
|
1110
|
-
await local.write({
|
|
1111
|
-
next: current.confirmed.record,
|
|
1112
|
-
previous: current.confirmed.record,
|
|
1113
|
-
queue: current.queue,
|
|
1114
|
-
previousQueue: current.queue,
|
|
1115
|
-
outcomes: [{ id: submitted.id, outcome }],
|
|
1116
|
-
});
|
|
1117
|
-
outcomes.set(submitted.id, outcome);
|
|
1118
|
-
outcomeRooms.set(submitted.id, room.locator);
|
|
1119
|
-
logAccepted(room.locator, submitted, accepted);
|
|
1120
|
-
});
|
|
1121
|
-
}
|
|
1122
|
-
async function commitAccept(room, local, submitted, accepted, base, overlay) {
|
|
1317
|
+
async function adoptAccepted(room, local, submitted, accepted) {
|
|
1123
1318
|
if (!continuing())
|
|
1124
1319
|
return unconfirmed;
|
|
1125
1320
|
const current = replicas.get(room.locator);
|
|
1126
1321
|
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
1127
1322
|
return null;
|
|
1128
1323
|
}
|
|
1129
|
-
const
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1324
|
+
const held = current.confirmed.record.roomVersion;
|
|
1325
|
+
logAccepted(room.locator, submitted, accepted);
|
|
1326
|
+
const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
1327
|
+
? { kind: 'staged', confirmed: current.confirmed }
|
|
1328
|
+
: applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
1329
|
+
? accepted.roomVersion
|
|
1330
|
+
: held, accepted.roomVersion);
|
|
1331
|
+
if (applied.kind !== 'staged') {
|
|
1332
|
+
const state = pendings.get(submitted.id);
|
|
1333
|
+
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1334
|
+
if (state !== undefined)
|
|
1335
|
+
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1336
|
+
emitQueue(room.locator);
|
|
1139
1337
|
return applied;
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
});
|
|
1338
|
+
}
|
|
1339
|
+
const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
1340
|
+
pendings.delete(submitted.id);
|
|
1144
1341
|
unreadable.delete(room.locator);
|
|
1145
1342
|
sourceRefusals.delete(room.locator);
|
|
1146
1343
|
declarationRefusals.delete(room.locator);
|
|
@@ -1149,17 +1346,8 @@ export function composeRuntime(options) {
|
|
|
1149
1346
|
moveTo({ ...status, ...attention() });
|
|
1150
1347
|
return null;
|
|
1151
1348
|
}
|
|
1152
|
-
async function
|
|
1153
|
-
const
|
|
1154
|
-
if (options.adapter.reached(room.locator, roomVersion, accepted.roomVersion)) {
|
|
1155
|
-
return committed(room, local, submitted, accepted, pulled, roomVersion === accepted.roomVersion);
|
|
1156
|
-
}
|
|
1157
|
-
if (fresh && continuing())
|
|
1158
|
-
await publish(local, room, pulled);
|
|
1159
|
-
return false;
|
|
1160
|
-
}
|
|
1161
|
-
async function committed(room, local, submitted, accepted, base, overlay) {
|
|
1162
|
-
const failed = await exclusive(() => commitAccept(room, local, submitted, accepted, base, overlay));
|
|
1349
|
+
async function adopted(room, local, submitted, accepted) {
|
|
1350
|
+
const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
|
|
1163
1351
|
if (failed === null)
|
|
1164
1352
|
return true;
|
|
1165
1353
|
if (failed.kind === 'refused' && continuing())
|
|
@@ -1172,10 +1360,8 @@ export function composeRuntime(options) {
|
|
|
1172
1360
|
const head = replica?.queue[0];
|
|
1173
1361
|
if (replica === undefined || head === undefined)
|
|
1174
1362
|
return;
|
|
1175
|
-
if (
|
|
1176
|
-
progress.complete = false;
|
|
1363
|
+
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1177
1364
|
return;
|
|
1178
|
-
}
|
|
1179
1365
|
const state = pendings.get(head.id);
|
|
1180
1366
|
const { submission, lost: earlier } = state ?? {
|
|
1181
1367
|
submission: { state: 'waiting' },
|
|
@@ -1203,6 +1389,7 @@ export function composeRuntime(options) {
|
|
|
1203
1389
|
return;
|
|
1204
1390
|
}
|
|
1205
1391
|
const opensBefore = openSignals;
|
|
1392
|
+
const answered = awaitingAnswer(room.locator);
|
|
1206
1393
|
try {
|
|
1207
1394
|
result = await options.adapter.mutate(authorized, {
|
|
1208
1395
|
locator: room.locator,
|
|
@@ -1211,11 +1398,14 @@ export function composeRuntime(options) {
|
|
|
1211
1398
|
}, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
|
|
1212
1399
|
}
|
|
1213
1400
|
catch {
|
|
1401
|
+
answered();
|
|
1214
1402
|
progress.complete = false;
|
|
1215
1403
|
if (continuing())
|
|
1216
1404
|
progress.reachable = false;
|
|
1217
1405
|
return;
|
|
1218
1406
|
}
|
|
1407
|
+
if (result.kind !== 'accepted' || !continuing())
|
|
1408
|
+
answered();
|
|
1219
1409
|
if (!continuing())
|
|
1220
1410
|
return;
|
|
1221
1411
|
if (result.kind === 'transport-uncertainty') {
|
|
@@ -1268,6 +1458,7 @@ export function composeRuntime(options) {
|
|
|
1268
1458
|
const reset = result.reset;
|
|
1269
1459
|
progress.complete = false;
|
|
1270
1460
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1461
|
+
armTimer();
|
|
1271
1462
|
return;
|
|
1272
1463
|
}
|
|
1273
1464
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
@@ -1280,7 +1471,8 @@ export function composeRuntime(options) {
|
|
|
1280
1471
|
return;
|
|
1281
1472
|
}
|
|
1282
1473
|
const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
1283
|
-
leaving: result.kind === 'expired' && landed(head, current.confirmed)
|
|
1474
|
+
leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
|
|
1475
|
+
(result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
|
|
1284
1476
|
? { ...left, returned: null }
|
|
1285
1477
|
: left,
|
|
1286
1478
|
});
|
|
@@ -1294,98 +1486,35 @@ export function composeRuntime(options) {
|
|
|
1294
1486
|
return withhold(withheld, room, local, progress);
|
|
1295
1487
|
continue;
|
|
1296
1488
|
}
|
|
1297
|
-
await
|
|
1298
|
-
|
|
1299
|
-
if (from === undefined)
|
|
1300
|
-
return;
|
|
1301
|
-
const pulled = await pullRefresh(authorized, from, reachOf(room.locator));
|
|
1302
|
-
if (!continuing())
|
|
1303
|
-
return;
|
|
1304
|
-
if (pulled.kind === 'revoked-authorization') {
|
|
1305
|
-
await refuse('revoked-authorization');
|
|
1306
|
-
return;
|
|
1307
|
-
}
|
|
1308
|
-
if (pulled.kind === 'scoped-refusal') {
|
|
1309
|
-
await scoped(pulled, room, local);
|
|
1310
|
-
progress.complete = false;
|
|
1311
|
-
return;
|
|
1312
|
-
}
|
|
1313
|
-
if (pulled.kind === 'still-current' || pulled.kind === 'advanced') {
|
|
1314
|
-
const fresh = pulled.kind === 'advanced';
|
|
1315
|
-
if (!(await accepting(room, local, head, result, fresh ? pulled.confirmed : from, fresh))) {
|
|
1316
|
-
progress.complete = false;
|
|
1317
|
-
return;
|
|
1318
|
-
}
|
|
1319
|
-
continue;
|
|
1320
|
-
}
|
|
1321
|
-
if (pulled.kind === 'beyond-horizon') {
|
|
1322
|
-
const bootstrapped = await pullBootstrap(authorized, room);
|
|
1323
|
-
if (!continuing())
|
|
1324
|
-
return;
|
|
1325
|
-
if (bootstrapped.kind === 'revoked-authorization') {
|
|
1326
|
-
await refuse('revoked-authorization');
|
|
1327
|
-
return;
|
|
1328
|
-
}
|
|
1329
|
-
if (bootstrapped.kind === 'scoped-refusal') {
|
|
1330
|
-
await scoped(bootstrapped, room, local);
|
|
1331
|
-
progress.complete = false;
|
|
1332
|
-
return;
|
|
1333
|
-
}
|
|
1334
|
-
if (bootstrapped.kind === 'staged') {
|
|
1335
|
-
if (!(await accepting(room, local, head, result, bootstrapped.confirmed, true))) {
|
|
1336
|
-
progress.complete = false;
|
|
1337
|
-
return;
|
|
1338
|
-
}
|
|
1339
|
-
continue;
|
|
1340
|
-
}
|
|
1489
|
+
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1490
|
+
if (!kept) {
|
|
1341
1491
|
progress.complete = false;
|
|
1342
|
-
if (bootstrapped.kind === 'transport-uncertainty') {
|
|
1343
|
-
progress.reachable = false;
|
|
1344
|
-
}
|
|
1345
|
-
else if (bootstrapped.kind === 'room-not-readable') {
|
|
1346
|
-
unreadable.add(room.locator);
|
|
1347
|
-
list(room, false);
|
|
1348
|
-
emitRooms();
|
|
1349
|
-
}
|
|
1350
|
-
else if (bootstrapped.kind === 'refused') {
|
|
1351
|
-
refusedSource(room, bootstrapped);
|
|
1352
|
-
}
|
|
1353
1492
|
return;
|
|
1354
1493
|
}
|
|
1355
|
-
progress.complete = false;
|
|
1356
|
-
if (pulled.kind === 'transport-uncertainty') {
|
|
1357
|
-
progress.reachable = false;
|
|
1358
|
-
}
|
|
1359
|
-
else if (pulled.kind === 'room-not-readable') {
|
|
1360
|
-
unreadable.add(room.locator);
|
|
1361
|
-
list(room, false);
|
|
1362
|
-
emitRooms();
|
|
1363
|
-
}
|
|
1364
|
-
else if (pulled.kind === 'retryable-refusal') {
|
|
1365
|
-
journal.record(`refusal ${pulled.kind} ${pulled.code}`);
|
|
1366
|
-
}
|
|
1367
|
-
else if (pulled.kind === 'refused') {
|
|
1368
|
-
refusedSource(room, pulled);
|
|
1369
|
-
}
|
|
1370
|
-
return;
|
|
1371
1494
|
}
|
|
1372
|
-
|
|
1495
|
+
}
|
|
1496
|
+
function awaitingAnswer(locator) {
|
|
1497
|
+
let release = () => { };
|
|
1498
|
+
const awaited = new Promise((resolve) => {
|
|
1499
|
+
release = resolve;
|
|
1500
|
+
});
|
|
1501
|
+
answering.set(locator, awaited);
|
|
1502
|
+
return () => {
|
|
1503
|
+
if (answering.get(locator) === awaited)
|
|
1504
|
+
answering.delete(locator);
|
|
1505
|
+
release();
|
|
1506
|
+
};
|
|
1373
1507
|
}
|
|
1374
1508
|
async function retainModel(local, room, attached, next) {
|
|
1375
1509
|
const key = modelKey(room.locator, attached);
|
|
1376
|
-
if (next.status === 'unavailable')
|
|
1377
|
-
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
1378
|
-
const epoch = modelEpoch.get(key) ?? 0;
|
|
1379
1510
|
const previous = modelOf(room.locator, attached);
|
|
1380
1511
|
const heldBefore = replicas.get(room.locator)?.confirmed.record;
|
|
1381
1512
|
if (JSON.stringify(previous) === JSON.stringify(next) &&
|
|
1382
1513
|
(heldBefore === undefined ||
|
|
1383
1514
|
(JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
|
|
1384
1515
|
JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
|
|
1385
|
-
JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) &&
|
|
1386
1516
|
JSON.stringify(heldBefore.typedSelection) ===
|
|
1387
|
-
JSON.stringify(room.typedSelection ?? null)
|
|
1388
|
-
JSON.stringify(heldBefore.untypedFolders) === JSON.stringify(room.untypedFolders ?? []))))
|
|
1517
|
+
JSON.stringify(room.typedSelection ?? null))))
|
|
1389
1518
|
return;
|
|
1390
1519
|
if (next.status === 'unavailable') {
|
|
1391
1520
|
invalidModels.add(key);
|
|
@@ -1394,37 +1523,30 @@ export function composeRuntime(options) {
|
|
|
1394
1523
|
await exclusive(async () => {
|
|
1395
1524
|
if (!continuing())
|
|
1396
1525
|
return;
|
|
1397
|
-
if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0))
|
|
1398
|
-
return;
|
|
1399
1526
|
const held = replicas.get(room.locator);
|
|
1400
1527
|
if (held !== undefined) {
|
|
1401
1528
|
const declared = {
|
|
1402
1529
|
...held.confirmed.record,
|
|
1403
1530
|
operations: room.operations,
|
|
1404
1531
|
replica: room.replica,
|
|
1405
|
-
attachedModel: room.attachedModel,
|
|
1406
1532
|
typedSelection: room.typedSelection ?? null,
|
|
1407
|
-
untypedFolders: room.untypedFolders ?? [],
|
|
1408
1533
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
1409
1534
|
};
|
|
1410
1535
|
const reclassified = sameMembership(declared, held.confirmed.record)
|
|
1411
|
-
?
|
|
1536
|
+
? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
|
|
1412
1537
|
: readReclassified(declared, held.confirmed);
|
|
1413
1538
|
const record = reclassified.kind === 'staged'
|
|
1414
1539
|
? declared
|
|
1415
1540
|
: {
|
|
1416
1541
|
...declared,
|
|
1417
1542
|
typedSelection: held.confirmed.record.typedSelection,
|
|
1418
|
-
untypedFolders: held.confirmed.record.untypedFolders,
|
|
1419
1543
|
};
|
|
1420
|
-
const read = reclassified.kind === 'deferred'
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
queue: held.queue,
|
|
1425
|
-
});
|
|
1544
|
+
const read = reclassified.kind === 'deferred'
|
|
1545
|
+
? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
|
|
1546
|
+
: reclassified;
|
|
1547
|
+
await local.write({ next: record, previous: held.confirmed.record });
|
|
1426
1548
|
if (read.kind === 'staged') {
|
|
1427
|
-
replicas.set(room.locator, replicaOf(read.confirmed, held.queue,
|
|
1549
|
+
replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
|
|
1428
1550
|
if (declarationRefusals.delete(room.locator)) {
|
|
1429
1551
|
sourceRefusals.delete(room.locator);
|
|
1430
1552
|
unreadable.delete(room.locator);
|
|
@@ -1448,79 +1570,12 @@ export function composeRuntime(options) {
|
|
|
1448
1570
|
publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
|
|
1449
1571
|
});
|
|
1450
1572
|
}
|
|
1451
|
-
async function acquireModel(local, room, authorized, selection) {
|
|
1452
|
-
const attached = selection === null ? 'collection' : 'typed-selection';
|
|
1453
|
-
const key = modelKey(room.locator, attached);
|
|
1454
|
-
const declared = selection === null ? room.attachedModel : selection.attachedModel;
|
|
1455
|
-
let previous = modelOf(room.locator, attached);
|
|
1456
|
-
if (previous.status !== 'unavailable' &&
|
|
1457
|
-
(declared === null || !sameModel(previous.snapshot, declared))) {
|
|
1458
|
-
previous = { status: 'unavailable', reason: 'version-changed' };
|
|
1459
|
-
await retainModel(local, room, attached, previous);
|
|
1460
|
-
if (discoveryPaused)
|
|
1461
|
-
return;
|
|
1462
|
-
}
|
|
1463
|
-
const epoch = modelEpoch.get(key) ?? 0;
|
|
1464
|
-
let answer;
|
|
1465
|
-
try {
|
|
1466
|
-
answer = await options.adapter.model(authorized, selection === null
|
|
1467
|
-
? { locator: room.locator, attached: 'collection', model: declared }
|
|
1468
|
-
: {
|
|
1469
|
-
locator: room.locator,
|
|
1470
|
-
attached: 'typed-selection',
|
|
1471
|
-
roots: selection.roots,
|
|
1472
|
-
model: declared,
|
|
1473
|
-
});
|
|
1474
|
-
}
|
|
1475
|
-
catch {
|
|
1476
|
-
answer = { kind: 'transport-uncertainty' };
|
|
1477
|
-
}
|
|
1478
|
-
if (discoveryPaused || epoch !== (modelEpoch.get(key) ?? 0))
|
|
1479
|
-
return;
|
|
1480
|
-
if (answer.kind === 'revoked-authorization') {
|
|
1481
|
-
await refuse('revoked-authorization');
|
|
1482
|
-
return;
|
|
1483
|
-
}
|
|
1484
|
-
if (answer.kind === 'transport-uncertainty') {
|
|
1485
|
-
await retainModel(local, room, attached, cached(previous));
|
|
1486
|
-
return;
|
|
1487
|
-
}
|
|
1488
|
-
if (answer.kind === 'model-unavailable') {
|
|
1489
|
-
await retainModel(local, room, attached, {
|
|
1490
|
-
status: 'unavailable',
|
|
1491
|
-
reason: answer.reason,
|
|
1492
|
-
});
|
|
1493
|
-
return;
|
|
1494
|
-
}
|
|
1495
|
-
if (declared === null || !sameModel(answer.snapshot, declared)) {
|
|
1496
|
-
await retainModel(local, room, attached, {
|
|
1497
|
-
status: 'unavailable',
|
|
1498
|
-
reason: 'version-changed',
|
|
1499
|
-
});
|
|
1500
|
-
return;
|
|
1501
|
-
}
|
|
1502
|
-
if (previous.status !== 'unavailable' &&
|
|
1503
|
-
sameModel(previous.snapshot, answer.snapshot) &&
|
|
1504
|
-
previous.snapshot.schema !== answer.snapshot.schema) {
|
|
1505
|
-
await retainModel(local, room, attached, { status: 'unavailable', reason: 'invalid' });
|
|
1506
|
-
return;
|
|
1507
|
-
}
|
|
1508
|
-
const verified = answer.snapshot.schema === null ? EMPTY_SCHEMA : verify(answer.snapshot.schema);
|
|
1509
|
-
if (verified.verdict === 'refuse') {
|
|
1510
|
-
const where = verified.location === undefined ? '' : ` ${verified.location}`;
|
|
1511
|
-
journal.record(`model invalid ${room.locator} ${attached} ${answer.snapshot.identity} ${verified.rule}${where}`);
|
|
1512
|
-
}
|
|
1513
|
-
await retainModel(local, room, attached, verified.verdict === 'accept'
|
|
1514
|
-
? {
|
|
1515
|
-
status: 'current',
|
|
1516
|
-
snapshot: structuredClone(answer.snapshot),
|
|
1517
|
-
verifiedWith: MD_MODEL,
|
|
1518
|
-
types: verified.model.types,
|
|
1519
|
-
}
|
|
1520
|
-
: { status: 'unavailable', reason: 'invalid' });
|
|
1521
|
-
}
|
|
1522
1573
|
async function scoped(result, room, local) {
|
|
1523
|
-
scopedRefusals.set(room.locator, {
|
|
1574
|
+
scopedRefusals.set(room.locator, {
|
|
1575
|
+
code: result.code,
|
|
1576
|
+
fields: result.fields,
|
|
1577
|
+
retryable: result.retryable,
|
|
1578
|
+
});
|
|
1524
1579
|
let scopedAway = room;
|
|
1525
1580
|
if (result.scope === 'model')
|
|
1526
1581
|
await retainModel(local, room, 'collection', {
|
|
@@ -1546,14 +1601,35 @@ export function composeRuntime(options) {
|
|
|
1546
1601
|
emitRooms();
|
|
1547
1602
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
1548
1603
|
}
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1604
|
+
function heldRooms(listed) {
|
|
1605
|
+
return listed.flatMap((room) => {
|
|
1606
|
+
const record = replicas.get(room.locator)?.confirmed.record;
|
|
1607
|
+
if (record === undefined || record.replica.selection !== room.replica.selection)
|
|
1608
|
+
return [];
|
|
1609
|
+
return [
|
|
1610
|
+
{
|
|
1611
|
+
locator: room.locator,
|
|
1612
|
+
roomVersion: record.roomVersion,
|
|
1613
|
+
documents: record.documents,
|
|
1614
|
+
revisions: record.revisions,
|
|
1615
|
+
},
|
|
1616
|
+
];
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
async function synchronize(listed, local, progress, attempt) {
|
|
1620
|
+
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
|
|
1621
|
+
const ready = listed.filter((_, index) => {
|
|
1552
1622
|
const outcome = prepared[index];
|
|
1553
1623
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1554
1624
|
});
|
|
1555
|
-
|
|
1556
|
-
|
|
1625
|
+
if (attempt === 'opened') {
|
|
1626
|
+
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
1627
|
+
handed = true;
|
|
1628
|
+
}
|
|
1629
|
+
const loaded = await Promise.allSettled(ready
|
|
1630
|
+
.filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
|
|
1631
|
+
.map((room) => loadRoom(room, local, progress)));
|
|
1632
|
+
const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
|
|
1557
1633
|
if (failed !== undefined)
|
|
1558
1634
|
throw failed.reason;
|
|
1559
1635
|
}
|
|
@@ -1570,11 +1646,8 @@ export function composeRuntime(options) {
|
|
|
1570
1646
|
progress.complete = false;
|
|
1571
1647
|
return false;
|
|
1572
1648
|
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
acquireModel(local, room, authorization, null),
|
|
1576
|
-
...(selection === undefined ? [] : [acquireModel(local, room, authorization, selection)]),
|
|
1577
|
-
]);
|
|
1649
|
+
if (prior !== undefined)
|
|
1650
|
+
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1578
1651
|
if (discoveryPaused || authorization === undefined)
|
|
1579
1652
|
return false;
|
|
1580
1653
|
if (room.replica.admission !== 'admitted') {
|
|
@@ -1582,24 +1655,53 @@ export function composeRuntime(options) {
|
|
|
1582
1655
|
progress.complete = false;
|
|
1583
1656
|
return false;
|
|
1584
1657
|
}
|
|
1585
|
-
const held = replicas.get(room.locator);
|
|
1586
|
-
if (held !== undefined && held.confirmed.record.replica.selection !== room.replica.selection) {
|
|
1587
|
-
progress.complete = false;
|
|
1588
|
-
return false;
|
|
1589
|
-
}
|
|
1590
1658
|
return true;
|
|
1591
1659
|
}
|
|
1592
|
-
async function
|
|
1660
|
+
async function caughtUp(room, local, progress) {
|
|
1661
|
+
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
1662
|
+
if (held === undefined || authorization === undefined)
|
|
1663
|
+
return;
|
|
1664
|
+
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
1665
|
+
if (!behind)
|
|
1666
|
+
return;
|
|
1667
|
+
const withheld = await advance(room, local, authorization);
|
|
1668
|
+
if (withheld !== null)
|
|
1669
|
+
await withhold(withheld, room, local, progress);
|
|
1670
|
+
}
|
|
1671
|
+
function revoked(locator) {
|
|
1672
|
+
return ['collection', 'typed-selection'].some((attached) => {
|
|
1673
|
+
const model = modelOf(locator, attached);
|
|
1674
|
+
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
async function loadRoom(room, local, progress) {
|
|
1593
1678
|
if (discoveryPaused || authorization === undefined)
|
|
1594
1679
|
return;
|
|
1680
|
+
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1681
|
+
await reload(room, local, progress);
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1595
1684
|
const withheld = await advance(room, local, authorization);
|
|
1596
1685
|
if (withheld !== null) {
|
|
1597
1686
|
await withhold(withheld, room, local, progress);
|
|
1598
1687
|
return;
|
|
1599
1688
|
}
|
|
1600
|
-
await
|
|
1689
|
+
await caughtUp(room, local, progress);
|
|
1690
|
+
}
|
|
1691
|
+
async function reload(room, local, progress) {
|
|
1692
|
+
if (discoveryPaused || authorization === undefined)
|
|
1693
|
+
return;
|
|
1694
|
+
const bootstrapped = await pullBootstrap(authorization, room, local);
|
|
1695
|
+
if (!continuing())
|
|
1696
|
+
return;
|
|
1697
|
+
if (bootstrapped.kind !== 'staged') {
|
|
1698
|
+
await withhold(bootstrapped, room, local, progress);
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
1702
|
+
await caughtUp(room, local, progress);
|
|
1601
1703
|
}
|
|
1602
|
-
async function
|
|
1704
|
+
async function discoverRooms(local, progress, attempt) {
|
|
1603
1705
|
if (closed || authorization === undefined) {
|
|
1604
1706
|
notifyReady();
|
|
1605
1707
|
return;
|
|
@@ -1625,12 +1727,17 @@ export function composeRuntime(options) {
|
|
|
1625
1727
|
}
|
|
1626
1728
|
}
|
|
1627
1729
|
progress.reachable = false;
|
|
1730
|
+
progress.complete = false;
|
|
1628
1731
|
notifyReady();
|
|
1629
1732
|
return;
|
|
1630
1733
|
}
|
|
1631
1734
|
if (result.kind === 'contract-unavailable') {
|
|
1632
1735
|
journal.record(`connection contract unavailable ${result.code}`);
|
|
1633
|
-
await refuse('missing-required-guarantee', {
|
|
1736
|
+
await refuse('missing-required-guarantee', {
|
|
1737
|
+
code: result.code,
|
|
1738
|
+
fields: result.fields,
|
|
1739
|
+
retryable: result.retryable,
|
|
1740
|
+
});
|
|
1634
1741
|
notifyReady();
|
|
1635
1742
|
return;
|
|
1636
1743
|
}
|
|
@@ -1639,17 +1746,6 @@ export function composeRuntime(options) {
|
|
|
1639
1746
|
notifyReady();
|
|
1640
1747
|
return;
|
|
1641
1748
|
}
|
|
1642
|
-
if (!result.supportedVersions.includes(ROOM_CONTRACT_VERSION)) {
|
|
1643
|
-
await refuse('no-matching-version');
|
|
1644
|
-
notifyReady();
|
|
1645
|
-
return;
|
|
1646
|
-
}
|
|
1647
|
-
if (result.mdModel.identity !== MD_MODEL.identity ||
|
|
1648
|
-
result.mdModel.version !== MD_MODEL.version) {
|
|
1649
|
-
await refuse('md-model-version-mismatch');
|
|
1650
|
-
notifyReady();
|
|
1651
|
-
return;
|
|
1652
|
-
}
|
|
1653
1749
|
for (const room of result.rooms) {
|
|
1654
1750
|
if (room.operations === undefined ||
|
|
1655
1751
|
!['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
@@ -1657,8 +1753,7 @@ export function composeRuntime(options) {
|
|
|
1657
1753
|
room.replica === undefined ||
|
|
1658
1754
|
typeof room.replica.selection !== 'string' ||
|
|
1659
1755
|
room.replica.selection.length === 0 ||
|
|
1660
|
-
!['admitted', 'unavailable'].includes(room.replica.admission)
|
|
1661
|
-
!declaredIdentity(room.attachedModel)) {
|
|
1756
|
+
!['admitted', 'unavailable'].includes(room.replica.admission)) {
|
|
1662
1757
|
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
1663
1758
|
await refuse('missing-required-guarantee');
|
|
1664
1759
|
notifyReady();
|
|
@@ -1670,20 +1765,6 @@ export function composeRuntime(options) {
|
|
|
1670
1765
|
notifyReady();
|
|
1671
1766
|
return;
|
|
1672
1767
|
}
|
|
1673
|
-
if (!declaredUntypedFolders(room)) {
|
|
1674
|
-
journal.record(`invalid untyped folder declaration ${room.locator}`);
|
|
1675
|
-
await refuse('missing-required-guarantee');
|
|
1676
|
-
notifyReady();
|
|
1677
|
-
return;
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
const declared = new Set(result.capabilities.map((capability) => capability.name));
|
|
1681
|
-
for (const required of options.requiredGuarantees) {
|
|
1682
|
-
if (!knownCapabilities.has(required) || !declared.has(required)) {
|
|
1683
|
-
await refuse('missing-required-guarantee');
|
|
1684
|
-
notifyReady();
|
|
1685
|
-
return;
|
|
1686
|
-
}
|
|
1687
1768
|
}
|
|
1688
1769
|
if (refusal !== null) {
|
|
1689
1770
|
refusal = null;
|
|
@@ -1692,29 +1773,19 @@ export function composeRuntime(options) {
|
|
|
1692
1773
|
const previousCapabilities = capabilities;
|
|
1693
1774
|
capabilities = result.capabilities;
|
|
1694
1775
|
notifyReady();
|
|
1695
|
-
if (declared.has('room-version-notifications'))
|
|
1696
|
-
observeRoomVersions();
|
|
1697
|
-
const discoveredSlots = result.slots ?? [];
|
|
1698
1776
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1699
|
-
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)
|
|
1700
|
-
JSON.stringify(discoveredSlots) !== JSON.stringify(slots)) {
|
|
1777
|
+
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
1701
1778
|
limits = result.providerLimits;
|
|
1702
|
-
|
|
1703
|
-
|
|
1779
|
+
await local.declare(limits, capabilities);
|
|
1780
|
+
}
|
|
1781
|
+
const signedIn = result.member ?? null;
|
|
1782
|
+
if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
|
|
1783
|
+
await local.keepMember(signedIn);
|
|
1784
|
+
member = signedIn;
|
|
1785
|
+
emitMember();
|
|
1704
1786
|
}
|
|
1705
1787
|
await applyDiscovery(result.rooms, local);
|
|
1706
|
-
|
|
1707
|
-
.filter((room) => room.replica.admission === 'admitted')
|
|
1708
|
-
.map((room) => room.locator)
|
|
1709
|
-
.sort());
|
|
1710
|
-
progress.complete = true;
|
|
1711
|
-
await synchronize(result.rooms, local, progress);
|
|
1712
|
-
if (closed || discoveryPaused)
|
|
1713
|
-
return;
|
|
1714
|
-
await conversations.reconcile();
|
|
1715
|
-
if (status.providerUnreachable)
|
|
1716
|
-
conversations.reconnected();
|
|
1717
|
-
await conversations.catchUp(progress);
|
|
1788
|
+
await synchronize(result.rooms, local, progress, attempt);
|
|
1718
1789
|
}
|
|
1719
1790
|
async function ensureAuthorization() {
|
|
1720
1791
|
if (authorization !== undefined)
|
|
@@ -1728,64 +1799,262 @@ export function composeRuntime(options) {
|
|
|
1728
1799
|
throw error;
|
|
1729
1800
|
}
|
|
1730
1801
|
}
|
|
1731
|
-
|
|
1732
|
-
if (
|
|
1733
|
-
|
|
1802
|
+
function task(work) {
|
|
1803
|
+
if (stopping)
|
|
1804
|
+
return chain;
|
|
1805
|
+
chain = chain.then(work, work).catch(() => undefined);
|
|
1806
|
+
return chain;
|
|
1807
|
+
}
|
|
1808
|
+
function sync(attempt) {
|
|
1809
|
+
return task(async () => {
|
|
1810
|
+
if (closed || stopping || discoveryPaused) {
|
|
1811
|
+
notifyReady();
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
const progress = { complete: true, reachable: true };
|
|
1815
|
+
underway = attempt === 'opened' ? progress : null;
|
|
1816
|
+
if (attempt === 'opened')
|
|
1817
|
+
streamed = progress;
|
|
1818
|
+
complete = false;
|
|
1819
|
+
recorded = false;
|
|
1820
|
+
moveTo(transferring(status));
|
|
1821
|
+
handed = false;
|
|
1822
|
+
try {
|
|
1823
|
+
const local = await openLocal();
|
|
1824
|
+
if (closed)
|
|
1825
|
+
return;
|
|
1826
|
+
await ensureAuthorization();
|
|
1827
|
+
if (closed)
|
|
1828
|
+
return;
|
|
1829
|
+
await discoverRooms(local, progress, attempt);
|
|
1830
|
+
}
|
|
1831
|
+
catch {
|
|
1832
|
+
progress.complete = false;
|
|
1833
|
+
if (attempt === 'opened' && !handed)
|
|
1834
|
+
progress.reachable = false;
|
|
1835
|
+
}
|
|
1836
|
+
finally {
|
|
1837
|
+
notifyReady();
|
|
1838
|
+
}
|
|
1839
|
+
if (attempt !== 'opened') {
|
|
1840
|
+
await finishOpening(progress);
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
if (handed || closed || progress.reachable)
|
|
1844
|
+
return;
|
|
1845
|
+
underway = null;
|
|
1846
|
+
journal.record('connection reopen');
|
|
1847
|
+
await rest(progress);
|
|
1848
|
+
options.adapter.roomStream?.reopen();
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
function closedSocket(revoked) {
|
|
1852
|
+
const opened = socketOpen;
|
|
1853
|
+
socketOpen = false;
|
|
1854
|
+
if (revoked) {
|
|
1855
|
+
void refuse('revoked-authorization');
|
|
1734
1856
|
return;
|
|
1735
1857
|
}
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1858
|
+
complete = false;
|
|
1859
|
+
if (streamed !== null)
|
|
1860
|
+
streamed.complete = false;
|
|
1861
|
+
moveTo({
|
|
1862
|
+
...status,
|
|
1863
|
+
state: status.state === 'synced' ? 'syncing' : status.state,
|
|
1864
|
+
providerUnreachable: status.providerUnreachable || !opened,
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
function applyOpening(expired) {
|
|
1868
|
+
return task(async () => {
|
|
1869
|
+
const progress = underway;
|
|
1870
|
+
underway = null;
|
|
1871
|
+
if (closed || stopping)
|
|
1745
1872
|
return;
|
|
1746
|
-
|
|
1747
|
-
|
|
1873
|
+
if (progress === null) {
|
|
1874
|
+
await reloadExpired(expired);
|
|
1748
1875
|
return;
|
|
1749
|
-
if (progress.complete) {
|
|
1750
|
-
completedAt = new Date().toISOString();
|
|
1751
|
-
await local.complete(completedAt);
|
|
1752
1876
|
}
|
|
1877
|
+
try {
|
|
1878
|
+
const local = await openLocal();
|
|
1879
|
+
const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
|
|
1880
|
+
const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
|
|
1881
|
+
const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
|
|
1882
|
+
if (progress.reachable) {
|
|
1883
|
+
for (const room of held)
|
|
1884
|
+
if (!reloading.includes(room))
|
|
1885
|
+
await confirmModels(room, local);
|
|
1886
|
+
}
|
|
1887
|
+
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1888
|
+
if (failed !== undefined)
|
|
1889
|
+
throw failed.reason;
|
|
1890
|
+
}
|
|
1891
|
+
catch {
|
|
1892
|
+
progress.complete = false;
|
|
1893
|
+
progress.reachable = false;
|
|
1894
|
+
}
|
|
1895
|
+
await finishOpening(progress);
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
async function reloadExpired(expired) {
|
|
1899
|
+
const progress = { complete: true, reachable: true };
|
|
1900
|
+
try {
|
|
1901
|
+
const local = await openLocal();
|
|
1902
|
+
await Promise.allSettled([...discovered.values()]
|
|
1903
|
+
.filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
|
|
1904
|
+
.map((room) => reload(room, local, progress)));
|
|
1753
1905
|
}
|
|
1754
1906
|
catch {
|
|
1755
|
-
progress.
|
|
1756
|
-
|
|
1907
|
+
progress.reachable = false;
|
|
1908
|
+
}
|
|
1909
|
+
await rest(progress);
|
|
1910
|
+
}
|
|
1911
|
+
async function confirmModels(room, local) {
|
|
1912
|
+
for (const attached of ['collection', 'typed-selection']) {
|
|
1913
|
+
const model = modelOf(room.locator, attached);
|
|
1914
|
+
if (model.status === 'cached') {
|
|
1915
|
+
await retainModel(local, room, attached, { ...model, status: 'current' });
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
function submitLater(locator) {
|
|
1920
|
+
if (localSaves)
|
|
1921
|
+
return;
|
|
1922
|
+
const running = submitting.get(locator);
|
|
1923
|
+
if (running !== undefined) {
|
|
1924
|
+
running.again = true;
|
|
1757
1925
|
return;
|
|
1758
1926
|
}
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1927
|
+
const run = { again: false, done: Promise.resolve() };
|
|
1928
|
+
submitting.set(locator, run);
|
|
1929
|
+
run.done = (async () => {
|
|
1930
|
+
try {
|
|
1931
|
+
do {
|
|
1932
|
+
run.again = false;
|
|
1933
|
+
const room = discovered.get(locator);
|
|
1934
|
+
if (closed || stopping || discoveryPaused || room === undefined)
|
|
1935
|
+
return;
|
|
1936
|
+
const progress = { complete: true, reachable: true };
|
|
1937
|
+
try {
|
|
1938
|
+
await ensureAuthorization();
|
|
1939
|
+
if (authorization === undefined)
|
|
1940
|
+
return;
|
|
1941
|
+
await submitRoom(room, await openLocal(), authorization, progress);
|
|
1942
|
+
}
|
|
1943
|
+
catch {
|
|
1944
|
+
progress.reachable = false;
|
|
1945
|
+
}
|
|
1946
|
+
armTimer();
|
|
1947
|
+
await rest(progress);
|
|
1948
|
+
} while (run.again);
|
|
1949
|
+
}
|
|
1950
|
+
finally {
|
|
1951
|
+
submitting.delete(locator);
|
|
1952
|
+
}
|
|
1953
|
+
})();
|
|
1954
|
+
}
|
|
1955
|
+
function submitAll() {
|
|
1956
|
+
for (const [locator, replica] of replicas) {
|
|
1957
|
+
if (replica.queue.length > 0)
|
|
1958
|
+
submitLater(locator);
|
|
1762
1959
|
}
|
|
1763
1960
|
}
|
|
1764
|
-
async function
|
|
1961
|
+
async function finishOpening(progress) {
|
|
1765
1962
|
try {
|
|
1766
|
-
|
|
1963
|
+
if (closed || discoveryPaused) {
|
|
1964
|
+
progress.complete = false;
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
submitAll();
|
|
1968
|
+
await conversations.reconcile();
|
|
1969
|
+
if (status.providerUnreachable)
|
|
1970
|
+
conversations.reconnected();
|
|
1971
|
+
const reading = { complete: true, reachable: true };
|
|
1972
|
+
await conversations.catchUp(reading);
|
|
1973
|
+
if (!reading.reachable)
|
|
1974
|
+
progress.reachable = false;
|
|
1975
|
+
}
|
|
1976
|
+
catch {
|
|
1977
|
+
progress.complete = false;
|
|
1767
1978
|
}
|
|
1768
1979
|
finally {
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
queued = false;
|
|
1772
|
-
void enqueue();
|
|
1773
|
-
}
|
|
1980
|
+
complete = progress.complete;
|
|
1981
|
+
await rest(progress);
|
|
1774
1982
|
}
|
|
1775
1983
|
}
|
|
1776
|
-
function
|
|
1777
|
-
|
|
1778
|
-
if (path === undefined || withdrawNotifications !== undefined)
|
|
1984
|
+
async function rest(progress) {
|
|
1985
|
+
if (closed)
|
|
1779
1986
|
return;
|
|
1780
|
-
|
|
1781
|
-
|
|
1987
|
+
let lastCompleted = status.lastCompleted;
|
|
1988
|
+
let synced = complete && stale.size === 0 && !pending();
|
|
1989
|
+
if (synced && !recorded) {
|
|
1990
|
+
const completedAt = new Date().toISOString();
|
|
1991
|
+
try {
|
|
1992
|
+
await (await openLocal()).complete(completedAt);
|
|
1993
|
+
recorded = true;
|
|
1994
|
+
lastCompleted = completedAt;
|
|
1995
|
+
}
|
|
1996
|
+
catch {
|
|
1997
|
+
synced = false;
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
moveTo({
|
|
2001
|
+
state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
|
|
2002
|
+
lastCompleted,
|
|
2003
|
+
providerUnreachable: !progress.reachable,
|
|
2004
|
+
...attention(),
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
function applyRoom(locator, roomVersion) {
|
|
2008
|
+
announced.set(locator, roomVersion);
|
|
2009
|
+
return task(async () => {
|
|
2010
|
+
await answering.get(locator);
|
|
2011
|
+
if (closed || stopping || discoveryPaused || authorization === undefined)
|
|
2012
|
+
return;
|
|
2013
|
+
const room = discovered.get(locator);
|
|
2014
|
+
const held = replicas.get(locator);
|
|
2015
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
1782
2016
|
return;
|
|
1783
|
-
if (
|
|
1784
|
-
|
|
2017
|
+
if (held !== undefined &&
|
|
2018
|
+
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
2019
|
+
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
1785
2020
|
return;
|
|
1786
2021
|
}
|
|
1787
|
-
|
|
1788
|
-
|
|
2022
|
+
const during = underway;
|
|
2023
|
+
const progress = during ?? { complete: true, reachable: true };
|
|
2024
|
+
if (during === null)
|
|
2025
|
+
moveTo(transferring(status));
|
|
2026
|
+
try {
|
|
2027
|
+
const local = await openLocal();
|
|
2028
|
+
const withheld = await advance(room, local, authorization);
|
|
2029
|
+
if (withheld !== null)
|
|
2030
|
+
await withhold(withheld, room, local, progress);
|
|
2031
|
+
}
|
|
2032
|
+
catch {
|
|
2033
|
+
stale.add(locator);
|
|
2034
|
+
}
|
|
2035
|
+
finally {
|
|
2036
|
+
if (during === null)
|
|
2037
|
+
await rest(progress);
|
|
2038
|
+
}
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
function observeStream() {
|
|
2042
|
+
const stream = options.adapter.roomStream;
|
|
2043
|
+
if (stream === undefined || withdrawStream !== undefined)
|
|
2044
|
+
return;
|
|
2045
|
+
withdrawStream = stream.observe((event) => {
|
|
2046
|
+
if (closed || stopping)
|
|
2047
|
+
return;
|
|
2048
|
+
if (event.kind === 'opened') {
|
|
2049
|
+
socketOpen = true;
|
|
2050
|
+
void sync('opened');
|
|
2051
|
+
}
|
|
2052
|
+
else if (event.kind === 'closed')
|
|
2053
|
+
closedSocket(event.revoked);
|
|
2054
|
+
else if (event.kind === 'opening')
|
|
2055
|
+
void applyOpening(event.expired);
|
|
2056
|
+
else
|
|
2057
|
+
void applyRoom(event.locator, event.roomVersion);
|
|
1789
2058
|
});
|
|
1790
2059
|
}
|
|
1791
2060
|
function writesHeld() {
|
|
@@ -1806,14 +2075,17 @@ export function composeRuntime(options) {
|
|
|
1806
2075
|
listener(mode);
|
|
1807
2076
|
}
|
|
1808
2077
|
if (releasing)
|
|
1809
|
-
|
|
2078
|
+
submitAll();
|
|
1810
2079
|
}
|
|
1811
2080
|
function observeConversations() {
|
|
1812
2081
|
const stream = options.adapter.conversations;
|
|
1813
2082
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
1814
2083
|
return;
|
|
1815
2084
|
withdrawConversations = stream.observe((event) => {
|
|
1816
|
-
conversations.receive(event)
|
|
2085
|
+
if (conversations.receive(event)) {
|
|
2086
|
+
void ready.then(scheduleCurrency);
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
1817
2089
|
if (event.kind !== 'gap' || conversations.current())
|
|
1818
2090
|
return;
|
|
1819
2091
|
show();
|
|
@@ -1824,7 +2096,7 @@ export function composeRuntime(options) {
|
|
|
1824
2096
|
if (currencyQueued || closed || stopping)
|
|
1825
2097
|
return;
|
|
1826
2098
|
currencyQueued = true;
|
|
1827
|
-
|
|
2099
|
+
void task(runCurrency);
|
|
1828
2100
|
}
|
|
1829
2101
|
async function runCurrency() {
|
|
1830
2102
|
currencyQueued = false;
|
|
@@ -1835,110 +2107,22 @@ export function composeRuntime(options) {
|
|
|
1835
2107
|
show();
|
|
1836
2108
|
return;
|
|
1837
2109
|
}
|
|
1838
|
-
const
|
|
1839
|
-
|
|
1840
|
-
moveTo(transferring(resting));
|
|
2110
|
+
const progress = { complete: false, reachable: !status.providerUnreachable };
|
|
2111
|
+
moveTo(transferring(status));
|
|
1841
2112
|
try {
|
|
1842
2113
|
await conversations.catchUp(progress);
|
|
1843
2114
|
}
|
|
1844
2115
|
catch {
|
|
1845
2116
|
progress.reachable = false;
|
|
1846
2117
|
}
|
|
1847
|
-
|
|
1848
|
-
if (!closed) {
|
|
1849
|
-
moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
|
|
1850
|
-
}
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
1853
|
-
function handSelected(locators) {
|
|
1854
|
-
const path = options.adapter.roomVersions;
|
|
1855
|
-
if (path === undefined || withdrawNotifications === undefined)
|
|
1856
|
-
return;
|
|
1857
|
-
if (handed !== undefined && JSON.stringify(locators) === JSON.stringify(handed))
|
|
1858
|
-
return;
|
|
1859
|
-
handed = locators;
|
|
1860
|
-
path.select(locators);
|
|
1861
|
-
}
|
|
1862
|
-
async function runNotified() {
|
|
1863
|
-
const announced = [...notices];
|
|
1864
|
-
notices.clear();
|
|
1865
|
-
if (announced.length === 0 || closed || stopping || discoveryPaused)
|
|
1866
|
-
return;
|
|
1867
|
-
const resting = status;
|
|
1868
|
-
const progress = { complete: false, reachable: !resting.providerUnreachable };
|
|
1869
|
-
moveTo(transferring(resting));
|
|
1870
|
-
try {
|
|
1871
|
-
const local = await openLocal();
|
|
1872
|
-
for (const [locator, roomVersion] of announced) {
|
|
1873
|
-
if (closed || discoveryPaused || authorization === undefined)
|
|
1874
|
-
return;
|
|
1875
|
-
if (handed?.includes(locator) !== true)
|
|
1876
|
-
continue;
|
|
1877
|
-
const room = discovered.get(locator);
|
|
1878
|
-
const held = replicas.get(locator);
|
|
1879
|
-
if (room === undefined)
|
|
1880
|
-
continue;
|
|
1881
|
-
if (sourceRefusals.has(locator)) {
|
|
1882
|
-
void enqueue();
|
|
1883
|
-
continue;
|
|
1884
|
-
}
|
|
1885
|
-
if (held === undefined)
|
|
1886
|
-
continue;
|
|
1887
|
-
if (held.confirmed.record.roomVersion === roomVersion)
|
|
1888
|
-
continue;
|
|
1889
|
-
const withheld = await advance(room, local, authorization);
|
|
1890
|
-
if (withheld === null)
|
|
1891
|
-
continue;
|
|
1892
|
-
if (withheld.kind === 'refused') {
|
|
1893
|
-
void enqueue();
|
|
1894
|
-
continue;
|
|
1895
|
-
}
|
|
1896
|
-
if (withheld.kind === 'scoped-refusal') {
|
|
1897
|
-
await scoped(withheld, room, local);
|
|
1898
|
-
continue;
|
|
1899
|
-
}
|
|
1900
|
-
if (withheld.kind === 'transport-uncertainty') {
|
|
1901
|
-
progress.reachable = false;
|
|
1902
|
-
continue;
|
|
1903
|
-
}
|
|
1904
|
-
if (withheld.kind === 'room-not-readable') {
|
|
1905
|
-
unreadable.add(locator);
|
|
1906
|
-
list(room, false);
|
|
1907
|
-
emitRooms();
|
|
1908
|
-
continue;
|
|
1909
|
-
}
|
|
1910
|
-
if (withheld.kind === 'revoked-authorization') {
|
|
1911
|
-
await refuse('revoked-authorization');
|
|
1912
|
-
return;
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
1915
|
-
}
|
|
1916
|
-
catch {
|
|
1917
|
-
progress.reachable = false;
|
|
1918
|
-
}
|
|
1919
|
-
finally {
|
|
1920
|
-
if (!closed) {
|
|
1921
|
-
moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
|
|
1922
|
-
}
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
function enqueue() {
|
|
1926
|
-
if (stopping)
|
|
1927
|
-
return chain;
|
|
1928
|
-
if (inflight) {
|
|
1929
|
-
queued = true;
|
|
1930
|
-
return chain;
|
|
1931
|
-
}
|
|
1932
|
-
inflight = true;
|
|
1933
|
-
chain = chain.then(run, run);
|
|
1934
|
-
return chain;
|
|
2118
|
+
await rest(progress);
|
|
1935
2119
|
}
|
|
1936
2120
|
function close() {
|
|
1937
2121
|
if (closed)
|
|
1938
2122
|
return;
|
|
1939
2123
|
closed = true;
|
|
1940
|
-
|
|
1941
|
-
|
|
2124
|
+
withdrawStream?.();
|
|
2125
|
+
withdrawStream = undefined;
|
|
1942
2126
|
withdrawConversations?.();
|
|
1943
2127
|
withdrawConversations = undefined;
|
|
1944
2128
|
withdrawServiceMode?.();
|
|
@@ -1953,6 +2137,7 @@ export function composeRuntime(options) {
|
|
|
1953
2137
|
storageLossListeners.clear();
|
|
1954
2138
|
statusListeners.clear();
|
|
1955
2139
|
serviceModeListeners.clear();
|
|
2140
|
+
memberListeners.clear();
|
|
1956
2141
|
readSide.close();
|
|
1957
2142
|
queueSubscriptions.clear();
|
|
1958
2143
|
roomDraftListeners.clear();
|
|
@@ -1962,6 +2147,7 @@ export function composeRuntime(options) {
|
|
|
1962
2147
|
stopping = true;
|
|
1963
2148
|
stopTimer();
|
|
1964
2149
|
await chain.catch(() => undefined);
|
|
2150
|
+
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
1965
2151
|
await mutations;
|
|
1966
2152
|
close();
|
|
1967
2153
|
}
|
|
@@ -1970,11 +2156,32 @@ export function composeRuntime(options) {
|
|
|
1970
2156
|
options.signal?.addEventListener('abort', close, { once: true });
|
|
1971
2157
|
if (options.signal?.aborted)
|
|
1972
2158
|
close();
|
|
1973
|
-
|
|
2159
|
+
observeStream();
|
|
2160
|
+
void openLocal().then(() => {
|
|
2161
|
+
if (capabilities.length > 0)
|
|
2162
|
+
notifyReady();
|
|
2163
|
+
}, () => {
|
|
1974
2164
|
notifyReady();
|
|
1975
2165
|
});
|
|
1976
|
-
|
|
1977
|
-
|
|
2166
|
+
if (options.adapter.roomStream === undefined)
|
|
2167
|
+
void sync('unstreamed');
|
|
2168
|
+
function planned(input, reach) {
|
|
2169
|
+
return planOf(input, roomFacts(reach), planReach(reach));
|
|
2170
|
+
}
|
|
2171
|
+
function watchPlan(plan, deliver) {
|
|
2172
|
+
materialize(reachedRooms(plan));
|
|
2173
|
+
return readSide.watch(plan, identityOf(plan), deliver);
|
|
2174
|
+
}
|
|
2175
|
+
async function searched(input, reach) {
|
|
2176
|
+
const plan = searchPlanOf(input);
|
|
2177
|
+
const selected = plan.documents === undefined && reach === null
|
|
2178
|
+
? null
|
|
2179
|
+
: (await rowsQuery(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
|
|
2180
|
+
if (selected === null)
|
|
2181
|
+
materialize(null);
|
|
2182
|
+
return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
|
|
2183
|
+
}
|
|
2184
|
+
const watch = (topic, input, deliver) => {
|
|
1978
2185
|
if (topic === 'room-drafts') {
|
|
1979
2186
|
const listener = { notify: deliver, ready: false };
|
|
1980
2187
|
roomDraftListeners.add(listener);
|
|
@@ -1995,6 +2202,10 @@ export function composeRuntime(options) {
|
|
|
1995
2202
|
if (topic === 'conversation') {
|
|
1996
2203
|
return conversations.watch(input[0], input[1], deliver);
|
|
1997
2204
|
}
|
|
2205
|
+
if (topic === 'conversations')
|
|
2206
|
+
return conversations.watchList(deliver);
|
|
2207
|
+
if (topic === 'agents')
|
|
2208
|
+
return conversations.watchAgents(deliver);
|
|
1998
2209
|
if (topic === 'presentation') {
|
|
1999
2210
|
const listener = {
|
|
2000
2211
|
app: input[0],
|
|
@@ -2049,6 +2260,13 @@ export function composeRuntime(options) {
|
|
|
2049
2260
|
serviceModeListeners.delete(deliver);
|
|
2050
2261
|
};
|
|
2051
2262
|
}
|
|
2263
|
+
if (topic === 'member') {
|
|
2264
|
+
memberListeners.add(deliver);
|
|
2265
|
+
deliver(member);
|
|
2266
|
+
return () => {
|
|
2267
|
+
memberListeners.delete(deliver);
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2052
2270
|
if (topic === 'queue') {
|
|
2053
2271
|
const queue = { locator: input[0], notify: deliver };
|
|
2054
2272
|
queueSubscriptions.add(queue);
|
|
@@ -2057,9 +2275,8 @@ export function composeRuntime(options) {
|
|
|
2057
2275
|
queueSubscriptions.delete(queue);
|
|
2058
2276
|
};
|
|
2059
2277
|
}
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
}
|
|
2278
|
+
return watchPlan(planned(input[0], null), deliver);
|
|
2279
|
+
};
|
|
2063
2280
|
const md = {
|
|
2064
2281
|
loaded() {
|
|
2065
2282
|
return openLocal().then(() => undefined);
|
|
@@ -2067,18 +2284,8 @@ export function composeRuntime(options) {
|
|
|
2067
2284
|
rooms() {
|
|
2068
2285
|
return listed();
|
|
2069
2286
|
},
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
},
|
|
2073
|
-
query: ((input) => readSide.query(planOf(input, roomFacts()))),
|
|
2074
|
-
async search(input) {
|
|
2075
|
-
const plan = searchPlanOf(input);
|
|
2076
|
-
const selected = plan.documents === undefined
|
|
2077
|
-
? null
|
|
2078
|
-
: (await readSide.query(filterOf(plan.documents, roomFacts())))
|
|
2079
|
-
.documents;
|
|
2080
|
-
return searchIndex.search(plan, selected);
|
|
2081
|
-
},
|
|
2287
|
+
query: ((input) => rowsQuery(planned(input, null))),
|
|
2288
|
+
search: (input) => searched(input, null),
|
|
2082
2289
|
types(locator) {
|
|
2083
2290
|
return {
|
|
2084
2291
|
collection: modelTypes(modelOf(locator)),
|
|
@@ -2088,12 +2295,6 @@ export function composeRuntime(options) {
|
|
|
2088
2295
|
: modelTypes(modelOf(locator, 'typed-selection')),
|
|
2089
2296
|
};
|
|
2090
2297
|
},
|
|
2091
|
-
syncStatus() {
|
|
2092
|
-
return shown;
|
|
2093
|
-
},
|
|
2094
|
-
serviceMode() {
|
|
2095
|
-
return serviceMode;
|
|
2096
|
-
},
|
|
2097
2298
|
roomDraft: {
|
|
2098
2299
|
list() {
|
|
2099
2300
|
return exclusive(async () => {
|
|
@@ -2112,20 +2313,20 @@ export function composeRuntime(options) {
|
|
|
2112
2313
|
return exclusive(async () => {
|
|
2113
2314
|
const parts = readParts(input);
|
|
2114
2315
|
if (path === '' || normalize(path) !== path)
|
|
2115
|
-
throw
|
|
2316
|
+
throw runtimeError('room-draft-malformed');
|
|
2116
2317
|
const local = await openLocal();
|
|
2117
2318
|
const current = roomDrafts.get(room)?.get(path);
|
|
2118
2319
|
if ((current?.revision ?? null) !== revision) {
|
|
2119
|
-
return
|
|
2120
|
-
outcome: 'stale',
|
|
2121
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
2122
|
-
};
|
|
2320
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2123
2321
|
}
|
|
2322
|
+
const file = parts.file === undefined
|
|
2323
|
+
? undefined
|
|
2324
|
+
: await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
|
|
2124
2325
|
const record = {
|
|
2125
2326
|
locator: room,
|
|
2126
2327
|
path,
|
|
2127
2328
|
revision: roomDraftRevision(),
|
|
2128
|
-
parts: held(current?.parts, parts),
|
|
2329
|
+
parts: held(current?.parts, parts, file),
|
|
2129
2330
|
returned: current?.returned ?? null,
|
|
2130
2331
|
};
|
|
2131
2332
|
await local.keepRoomDraft(record);
|
|
@@ -2139,10 +2340,7 @@ export function composeRuntime(options) {
|
|
|
2139
2340
|
const local = await openLocal();
|
|
2140
2341
|
const current = roomDrafts.get(room)?.get(path);
|
|
2141
2342
|
if (current === undefined || current.revision !== revision) {
|
|
2142
|
-
return
|
|
2143
|
-
outcome: 'stale',
|
|
2144
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
2145
|
-
};
|
|
2343
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2146
2344
|
}
|
|
2147
2345
|
await local.discardRoomDraft(room, path);
|
|
2148
2346
|
forgetRoomDraft(room, path);
|
|
@@ -2174,12 +2372,13 @@ export function composeRuntime(options) {
|
|
|
2174
2372
|
save(room, named, writtenBy) {
|
|
2175
2373
|
return exclusive(() => saveRoomDrafts(room, named, writtenBy));
|
|
2176
2374
|
},
|
|
2177
|
-
outcome(identifier) {
|
|
2178
|
-
return outcomes.get(identifier) ?? null;
|
|
2179
|
-
},
|
|
2180
2375
|
history(locator, path, query) {
|
|
2181
2376
|
return readHistory(locator, path, query);
|
|
2182
2377
|
},
|
|
2378
|
+
binaryFile: {
|
|
2379
|
+
read: readBinaryPath,
|
|
2380
|
+
readReference: readBinaryReference,
|
|
2381
|
+
},
|
|
2183
2382
|
call(name, input) {
|
|
2184
2383
|
return invokeAction(name, input);
|
|
2185
2384
|
},
|
|
@@ -2191,15 +2390,10 @@ export function composeRuntime(options) {
|
|
|
2191
2390
|
log() {
|
|
2192
2391
|
return journal.export();
|
|
2193
2392
|
},
|
|
2194
|
-
storageLoss() {
|
|
2195
|
-
return storageLoss;
|
|
2196
|
-
},
|
|
2197
2393
|
persistence() {
|
|
2198
2394
|
return options.shell.persistentStorage;
|
|
2199
2395
|
},
|
|
2200
|
-
observe: (
|
|
2201
|
-
deliver(structuredClone(message));
|
|
2202
|
-
}), topic === 'query')),
|
|
2396
|
+
observe: observing(watch),
|
|
2203
2397
|
async reauthorize() {
|
|
2204
2398
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2205
2399
|
return;
|
|
@@ -2208,8 +2402,20 @@ export function composeRuntime(options) {
|
|
|
2208
2402
|
await mutations;
|
|
2209
2403
|
authorization = undefined;
|
|
2210
2404
|
discoveryPaused = false;
|
|
2211
|
-
|
|
2405
|
+
if (options.adapter.roomStream === undefined)
|
|
2406
|
+
await sync('unstreamed');
|
|
2407
|
+
else
|
|
2408
|
+
await task(ensureAuthorization);
|
|
2212
2409
|
},
|
|
2213
2410
|
};
|
|
2214
|
-
|
|
2411
|
+
const reachable = {
|
|
2412
|
+
md,
|
|
2413
|
+
watch,
|
|
2414
|
+
referrerScope: (referrer) => options.adapter.referrerScope(referrer),
|
|
2415
|
+
plan: planned,
|
|
2416
|
+
query: (plan) => rowsQuery(plan),
|
|
2417
|
+
watchPlan,
|
|
2418
|
+
search: searched,
|
|
2419
|
+
};
|
|
2420
|
+
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2215
2421
|
}
|