@agentshouse/mdruntime 0.1.0-c8c4b020a3ec7 → 0.1.0-c8ca66ac9e99d
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 +175 -84
- package/README.md +21 -11
- package/dist/adapter.d.ts +125 -55
- 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 +89 -38
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +19 -12
- 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 -7
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +68 -25
- package/dist/collection.d.ts +6 -11
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +56 -19
- package/dist/connection.d.ts +28 -18
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +942 -626
- package/dist/conversation.d.ts +32 -13
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +293 -108
- 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 -2
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +46 -42
- 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 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/memory.d.ts +14 -7
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +137 -45
- 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 +28 -3
- 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 +81 -62
- 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 +28 -3
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +429 -80
- 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 +37 -11
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +310 -164
- package/dist/wire.d.ts +58 -19
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +65 -61
- package/package.json +27 -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,77 +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
41
|
export function composeRuntime(options) {
|
|
53
|
-
const
|
|
42
|
+
const localSaves = options.adapter.localSaves === true;
|
|
54
43
|
let authorization = options.authorization;
|
|
55
44
|
let opening;
|
|
56
45
|
let store;
|
|
57
46
|
const replicas = new Map();
|
|
47
|
+
const queuesWithoutReplica = new Map();
|
|
58
48
|
const pendings = new Map();
|
|
59
49
|
const roomDrafts = new Map();
|
|
60
50
|
const presentations = new Map();
|
|
61
|
-
const wanted = new Map();
|
|
62
51
|
let limits = [];
|
|
63
52
|
let capabilities = [];
|
|
64
|
-
function rewriting() {
|
|
65
|
-
return capabilities.some((capability) => capability.name === 'move-reference-rewrites')
|
|
66
|
-
? 'provider'
|
|
67
|
-
: 'runtime';
|
|
68
|
-
}
|
|
69
53
|
const rooms = new Map();
|
|
54
|
+
const counted = new Map();
|
|
70
55
|
const unreadable = new Set();
|
|
71
56
|
const models = new Map();
|
|
72
57
|
const liveModels = new Map();
|
|
73
58
|
const invalidModels = new Set();
|
|
74
|
-
const modelEpoch = new Map();
|
|
75
59
|
const discovered = new Map();
|
|
76
60
|
let roomsListed = false;
|
|
77
61
|
const scopedRefusals = new Map();
|
|
@@ -100,16 +84,21 @@ export function composeRuntime(options) {
|
|
|
100
84
|
let status = STARTING;
|
|
101
85
|
let shown = STARTING;
|
|
102
86
|
let discoveryPaused = false;
|
|
103
|
-
let
|
|
87
|
+
let resetTimer;
|
|
104
88
|
let closed = false;
|
|
105
89
|
let stopping = false;
|
|
106
|
-
let
|
|
107
|
-
|
|
108
|
-
|
|
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();
|
|
109
100
|
let chain = Promise.resolve();
|
|
110
101
|
let mutations = Promise.resolve();
|
|
111
|
-
let inflight = false;
|
|
112
|
-
let queued = false;
|
|
113
102
|
const roomListeners = new Set();
|
|
114
103
|
const refusalListeners = new Set();
|
|
115
104
|
const storageLossListeners = new Set();
|
|
@@ -120,13 +109,19 @@ export function composeRuntime(options) {
|
|
|
120
109
|
const readSide = createReadSide();
|
|
121
110
|
let withdrawConversations;
|
|
122
111
|
let currencyQueued = false;
|
|
112
|
+
let socketOpen = false;
|
|
123
113
|
let serviceMode = 'open';
|
|
124
114
|
let haltedRefusal = false;
|
|
125
115
|
let openSignals = 0;
|
|
126
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();
|
|
127
122
|
const conversations = createConversationStore({
|
|
128
|
-
|
|
129
|
-
documents: (locator) =>
|
|
123
|
+
agents: declaredAgents,
|
|
124
|
+
documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
|
|
130
125
|
call: (name, input) => invokeAction(name, input),
|
|
131
126
|
durably,
|
|
132
127
|
stream: () => options.adapter.conversations,
|
|
@@ -202,7 +197,9 @@ export function composeRuntime(options) {
|
|
|
202
197
|
}
|
|
203
198
|
}
|
|
204
199
|
function pending() {
|
|
205
|
-
return
|
|
200
|
+
return (!localSaves &&
|
|
201
|
+
(queuesWithoutReplica.size > 0 ||
|
|
202
|
+
[...replicas.values()].some((replica) => replica.queue.length > 0)));
|
|
206
203
|
}
|
|
207
204
|
function attention() {
|
|
208
205
|
const attentionRequired = [];
|
|
@@ -231,14 +228,32 @@ export function composeRuntime(options) {
|
|
|
231
228
|
}
|
|
232
229
|
return { attentionRequired, reset };
|
|
233
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
|
+
}
|
|
234
242
|
function listed() {
|
|
235
|
-
return structuredClone([...rooms.values()])
|
|
243
|
+
return structuredClone([...rooms.values()].map((room) => {
|
|
244
|
+
const documents = documentsIn(room.locator);
|
|
245
|
+
counted.set(room.locator, documents);
|
|
246
|
+
return { ...room, documents };
|
|
247
|
+
}));
|
|
236
248
|
}
|
|
237
|
-
function roomFacts() {
|
|
238
|
-
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);
|
|
239
254
|
const attached = [
|
|
240
|
-
modelOf(room.locator),
|
|
241
|
-
...(
|
|
255
|
+
...(governed.collection ? [modelOf(room.locator)] : []),
|
|
256
|
+
...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
|
|
242
257
|
];
|
|
243
258
|
return {
|
|
244
259
|
locator: room.locator,
|
|
@@ -261,6 +276,10 @@ export function composeRuntime(options) {
|
|
|
261
276
|
for (const listener of storageLossListeners)
|
|
262
277
|
listener(storageLoss);
|
|
263
278
|
}
|
|
279
|
+
function emitMember() {
|
|
280
|
+
for (const listener of memberListeners)
|
|
281
|
+
listener(member);
|
|
282
|
+
}
|
|
264
283
|
function moveTo(next) {
|
|
265
284
|
if (closed)
|
|
266
285
|
return;
|
|
@@ -280,6 +299,8 @@ export function composeRuntime(options) {
|
|
|
280
299
|
listener(shown);
|
|
281
300
|
}
|
|
282
301
|
function entryOf(batch) {
|
|
302
|
+
if (localSaves)
|
|
303
|
+
return Object.freeze({ batch, state: 'local' });
|
|
283
304
|
const state = pendings.get(batch.id);
|
|
284
305
|
if (state?.accepted !== undefined) {
|
|
285
306
|
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
@@ -287,10 +308,7 @@ export function composeRuntime(options) {
|
|
|
287
308
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
288
309
|
}
|
|
289
310
|
function entriesOf(locator) {
|
|
290
|
-
|
|
291
|
-
if (replica === undefined)
|
|
292
|
-
return [];
|
|
293
|
-
return replica.queue.map(entryOf);
|
|
311
|
+
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
|
|
294
312
|
}
|
|
295
313
|
function emitQueue(locator) {
|
|
296
314
|
const entries = entriesOf(locator);
|
|
@@ -300,12 +318,47 @@ export function composeRuntime(options) {
|
|
|
300
318
|
}
|
|
301
319
|
}
|
|
302
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);
|
|
303
344
|
readSide.apply(set);
|
|
304
345
|
searchIndex.changed(set);
|
|
305
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
|
+
}
|
|
306
358
|
function publishChangeSet(set) {
|
|
307
359
|
applyChangeSet(set);
|
|
308
|
-
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)))
|
|
309
362
|
emitRooms();
|
|
310
363
|
for (const locator of set.queues)
|
|
311
364
|
emitQueue(locator);
|
|
@@ -316,23 +369,32 @@ export function composeRuntime(options) {
|
|
|
316
369
|
for (const app of set.presentations)
|
|
317
370
|
emitPresentation(app);
|
|
318
371
|
conversations.changed(set);
|
|
319
|
-
const cards = declaredCards();
|
|
320
|
-
if (cards !== undefined && set.documents.some((change) => change.locator === cards.locator)) {
|
|
321
|
-
scheduleCurrency();
|
|
322
|
-
}
|
|
323
372
|
}
|
|
324
373
|
function stopTimer() {
|
|
325
|
-
if (
|
|
374
|
+
if (resetTimer === undefined)
|
|
326
375
|
return;
|
|
327
|
-
|
|
328
|
-
|
|
376
|
+
clearTimeout(resetTimer);
|
|
377
|
+
resetTimer = undefined;
|
|
329
378
|
}
|
|
330
|
-
function
|
|
331
|
-
|
|
379
|
+
function armTimer() {
|
|
380
|
+
stopTimer();
|
|
381
|
+
if (closed || stopping)
|
|
332
382
|
return;
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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)
|
|
392
|
+
return;
|
|
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()));
|
|
336
398
|
}
|
|
337
399
|
async function refuse(reason, provider = null) {
|
|
338
400
|
stopTimer();
|
|
@@ -343,10 +405,7 @@ export function composeRuntime(options) {
|
|
|
343
405
|
const revoked = { status: 'unavailable', reason: 'revoked' };
|
|
344
406
|
try {
|
|
345
407
|
const local = await openLocal();
|
|
346
|
-
await local.revokeModels([...replicas.values()].map((replica) =>
|
|
347
|
-
record: replica.confirmed.record,
|
|
348
|
-
queue: replica.queue,
|
|
349
|
-
})));
|
|
408
|
+
await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
|
|
350
409
|
}
|
|
351
410
|
catch {
|
|
352
411
|
journal.record('model revocation persistence failed');
|
|
@@ -358,7 +417,6 @@ export function composeRuntime(options) {
|
|
|
358
417
|
}
|
|
359
418
|
for (const key of keys) {
|
|
360
419
|
invalidModels.add(key);
|
|
361
|
-
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
362
420
|
models.set(key, revoked);
|
|
363
421
|
liveModels.delete(key);
|
|
364
422
|
}
|
|
@@ -371,10 +429,7 @@ export function composeRuntime(options) {
|
|
|
371
429
|
model: revoked,
|
|
372
430
|
selectionModel: revoked,
|
|
373
431
|
};
|
|
374
|
-
|
|
375
|
-
replicas.set(locator, read.kind === 'staged'
|
|
376
|
-
? replicaOf(read.confirmed, before.queue, rewriting(), before)
|
|
377
|
-
: { ...before, confirmed: { ...before.confirmed, record } });
|
|
432
|
+
replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
|
|
378
433
|
}
|
|
379
434
|
rooms.set(locator, {
|
|
380
435
|
...room,
|
|
@@ -391,7 +446,8 @@ export function composeRuntime(options) {
|
|
|
391
446
|
}
|
|
392
447
|
finally {
|
|
393
448
|
refusal = {
|
|
394
|
-
reason,
|
|
449
|
+
code: reason,
|
|
450
|
+
retryable: false,
|
|
395
451
|
reauthorizationOffered: reason === 'revoked-authorization',
|
|
396
452
|
refusal: provider,
|
|
397
453
|
};
|
|
@@ -404,20 +460,18 @@ export function composeRuntime(options) {
|
|
|
404
460
|
rooms.set(room.locator, {
|
|
405
461
|
name: room.name,
|
|
406
462
|
locator: room.locator,
|
|
463
|
+
handle: room.handle,
|
|
407
464
|
operations: room.operations,
|
|
408
465
|
replica: room.replica,
|
|
409
|
-
attachedModel: room.attachedModel,
|
|
410
466
|
model: modelTypes(modelOf(room.locator)),
|
|
411
467
|
typedSelection: selection === null
|
|
412
468
|
? null
|
|
413
469
|
: {
|
|
414
470
|
roots: selection.roots,
|
|
415
|
-
attachedModel: selection.attachedModel,
|
|
416
471
|
model: modelTypes(modelOf(room.locator, 'typed-selection')),
|
|
417
472
|
},
|
|
418
473
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
419
474
|
roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
|
|
420
|
-
attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
|
|
421
475
|
synchronizationAvailable,
|
|
422
476
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
423
477
|
});
|
|
@@ -429,9 +483,6 @@ export function composeRuntime(options) {
|
|
|
429
483
|
discovered.clear();
|
|
430
484
|
for (const room of listed)
|
|
431
485
|
discovered.set(room.locator, room);
|
|
432
|
-
for (const locator of wanted.keys())
|
|
433
|
-
if (!listedLocators.has(locator))
|
|
434
|
-
wanted.delete(locator);
|
|
435
486
|
await dropUnlisted(listedLocators, local);
|
|
436
487
|
for (const room of listed) {
|
|
437
488
|
const held = replicas.get(room.locator)?.confirmed.record;
|
|
@@ -445,7 +496,14 @@ export function composeRuntime(options) {
|
|
|
445
496
|
emitRooms();
|
|
446
497
|
}
|
|
447
498
|
function unlistedHeld(listed) {
|
|
448
|
-
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));
|
|
449
507
|
}
|
|
450
508
|
async function dropUnlisted(listed, local) {
|
|
451
509
|
if (unlistedHeld(listed).length === 0)
|
|
@@ -459,12 +517,15 @@ export function composeRuntime(options) {
|
|
|
459
517
|
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
460
518
|
}));
|
|
461
519
|
for (const locator of unlisted) {
|
|
462
|
-
for (const batch of replicas.get(locator)?.queue ?? [])
|
|
520
|
+
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
463
521
|
pendings.delete(batch.id);
|
|
464
522
|
replicas.delete(locator);
|
|
523
|
+
queuesWithoutReplica.delete(locator);
|
|
465
524
|
rooms.delete(locator);
|
|
466
525
|
roomDrafts.delete(locator);
|
|
526
|
+
transfers.delete(locator);
|
|
467
527
|
unreadable.delete(locator);
|
|
528
|
+
stale.delete(locator);
|
|
468
529
|
scopedRefusals.delete(locator);
|
|
469
530
|
sourceRefusals.delete(locator);
|
|
470
531
|
declarationRefusals.delete(locator);
|
|
@@ -473,7 +534,6 @@ export function composeRuntime(options) {
|
|
|
473
534
|
models.delete(key);
|
|
474
535
|
liveModels.delete(key);
|
|
475
536
|
invalidModels.delete(key);
|
|
476
|
-
modelEpoch.delete(key);
|
|
477
537
|
}
|
|
478
538
|
journal.record(`room unlisted ${locator}`);
|
|
479
539
|
}
|
|
@@ -486,7 +546,6 @@ export function composeRuntime(options) {
|
|
|
486
546
|
if (prior !== undefined &&
|
|
487
547
|
prior.status !== 'unavailable' &&
|
|
488
548
|
(persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
|
|
489
|
-
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
490
549
|
invalidModels.add(key);
|
|
491
550
|
}
|
|
492
551
|
const model = persisted.status === 'unavailable' ||
|
|
@@ -501,7 +560,7 @@ export function composeRuntime(options) {
|
|
|
501
560
|
return model;
|
|
502
561
|
}
|
|
503
562
|
async function hydrate(opened) {
|
|
504
|
-
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
|
|
563
|
+
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
|
|
505
564
|
opened.all(),
|
|
506
565
|
opened.queues(),
|
|
507
566
|
opened.roomDrafts(),
|
|
@@ -511,9 +570,15 @@ export function composeRuntime(options) {
|
|
|
511
570
|
opened.lastCompleted(),
|
|
512
571
|
opened.providerLimits(),
|
|
513
572
|
opened.capabilities(),
|
|
573
|
+
opened.member(),
|
|
574
|
+
opened.transfers(),
|
|
514
575
|
]);
|
|
576
|
+
transfers.clear();
|
|
577
|
+
for (const record of staged)
|
|
578
|
+
transfers.set(record.locator, record);
|
|
515
579
|
limits = declared;
|
|
516
580
|
capabilities = named;
|
|
581
|
+
member = known;
|
|
517
582
|
roomDrafts.clear();
|
|
518
583
|
for (const record of kept)
|
|
519
584
|
rememberRoomDraft(record);
|
|
@@ -529,19 +594,18 @@ export function composeRuntime(options) {
|
|
|
529
594
|
}
|
|
530
595
|
const previousReplicas = new Map(replicas);
|
|
531
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
|
+
}
|
|
532
603
|
for (const record of records) {
|
|
533
604
|
const model = restored(modelKey(record.locator, 'collection'), record.model);
|
|
534
605
|
const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
|
|
535
606
|
const prior = previousReplicas.get(record.locator);
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
const confirmed = read.kind === 'staged'
|
|
539
|
-
? read.confirmed
|
|
540
|
-
: {
|
|
541
|
-
record: retyped,
|
|
542
|
-
collection: parseCollection(retyped.documents, compositionOf(retyped)),
|
|
543
|
-
};
|
|
544
|
-
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], rewriting(), prior));
|
|
607
|
+
const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
|
|
608
|
+
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
|
|
545
609
|
const existing = rooms.get(record.locator);
|
|
546
610
|
if (existing === undefined)
|
|
547
611
|
list(record, false);
|
|
@@ -549,24 +613,17 @@ export function composeRuntime(options) {
|
|
|
549
613
|
rooms.set(record.locator, {
|
|
550
614
|
...existing,
|
|
551
615
|
roomVersion: record.roomVersion,
|
|
552
|
-
attachments: record.attachments,
|
|
553
616
|
operations: record.operations,
|
|
554
617
|
replica: record.replica,
|
|
555
|
-
attachedModel: record.attachedModel,
|
|
556
618
|
model: modelTypes(model),
|
|
557
619
|
typedSelection: record.typedSelection === null
|
|
558
620
|
? null
|
|
559
621
|
: {
|
|
560
622
|
roots: record.typedSelection.roots,
|
|
561
|
-
attachedModel: record.typedSelection.attachedModel,
|
|
562
623
|
model: modelTypes(selectionModel),
|
|
563
624
|
},
|
|
564
625
|
});
|
|
565
626
|
}
|
|
566
|
-
if (read.kind === 'refused') {
|
|
567
|
-
refusedSource(record, read);
|
|
568
|
-
declarationRefusals.add(record.locator);
|
|
569
|
-
}
|
|
570
627
|
}
|
|
571
628
|
return {
|
|
572
629
|
lastCompleted,
|
|
@@ -579,6 +636,8 @@ export function composeRuntime(options) {
|
|
|
579
636
|
applyChangeSet(read);
|
|
580
637
|
if (replicas.size > 0)
|
|
581
638
|
emitRooms();
|
|
639
|
+
if (member !== null)
|
|
640
|
+
emitMember();
|
|
582
641
|
for (const locator of locators)
|
|
583
642
|
emitQueue(locator);
|
|
584
643
|
publishRoomDrafts(false);
|
|
@@ -626,10 +685,15 @@ export function composeRuntime(options) {
|
|
|
626
685
|
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
627
686
|
}
|
|
628
687
|
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
629
|
-
const
|
|
688
|
+
const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
689
|
+
const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
|
|
630
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
|
+
}));
|
|
631
695
|
const returning = [
|
|
632
|
-
...
|
|
696
|
+
...[...unqueued].reverse(),
|
|
633
697
|
...(settling.leaving === undefined ? [] : [settling.leaving]),
|
|
634
698
|
];
|
|
635
699
|
const written = new Map();
|
|
@@ -647,57 +711,110 @@ export function composeRuntime(options) {
|
|
|
647
711
|
});
|
|
648
712
|
}
|
|
649
713
|
}
|
|
650
|
-
await local.
|
|
651
|
-
|
|
652
|
-
previous: before?.confirmed.record ?? null,
|
|
653
|
-
queue: kept,
|
|
654
|
-
previousQueue: before?.queue ?? [],
|
|
655
|
-
roomDrafts: [...written.values()],
|
|
656
|
-
});
|
|
657
|
-
for (const batch of before?.queue ?? []) {
|
|
714
|
+
await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
|
|
715
|
+
for (const batch of previousQueue) {
|
|
658
716
|
if (!kept.includes(batch))
|
|
659
717
|
pendings.delete(batch.id);
|
|
660
718
|
}
|
|
661
719
|
for (const record of written.values())
|
|
662
720
|
rememberRoomDraft(record);
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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));
|
|
666
749
|
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
667
750
|
room: settling.room,
|
|
668
|
-
queue: kept.length !==
|
|
751
|
+
queue: kept.length !== previousQueue.length,
|
|
669
752
|
drafts: [...written.keys()],
|
|
670
753
|
});
|
|
671
754
|
}
|
|
672
|
-
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;
|
|
673
788
|
return exclusive(async () => {
|
|
674
789
|
if (!continuing())
|
|
675
|
-
return;
|
|
790
|
+
return true;
|
|
676
791
|
const prior = replicas.get(room.locator);
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
const
|
|
685
|
-
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;
|
|
686
800
|
const accepted = queue.filter((batch) => {
|
|
687
801
|
const accepted = pendings.get(batch.id)?.accepted;
|
|
688
802
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
689
803
|
});
|
|
690
|
-
const set = await settle(local, room.locator, prior,
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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);
|
|
807
|
+
for (const batch of accepted)
|
|
808
|
+
pendings.delete(batch.id);
|
|
809
|
+
stale.delete(room.locator);
|
|
810
|
+
if (unreadable.delete(room.locator))
|
|
811
|
+
scopedRefusals.delete(room.locator);
|
|
696
812
|
sourceRefusals.delete(room.locator);
|
|
697
813
|
declarationRefusals.delete(room.locator);
|
|
698
814
|
list(room, true);
|
|
699
815
|
publishChangeSet(set);
|
|
700
816
|
moveTo({ ...status, ...attention() });
|
|
817
|
+
return true;
|
|
701
818
|
});
|
|
702
819
|
}
|
|
703
820
|
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
@@ -705,61 +822,140 @@ export function composeRuntime(options) {
|
|
|
705
822
|
const replica = replicas.get(locator);
|
|
706
823
|
const room = rooms.get(locator);
|
|
707
824
|
if (replica === undefined || room === undefined) {
|
|
708
|
-
throw
|
|
825
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
709
826
|
}
|
|
710
827
|
const saved = [];
|
|
711
828
|
for (const { path, revision } of named) {
|
|
712
829
|
const record = roomDrafts.get(locator)?.get(path);
|
|
713
830
|
if (record === undefined || record.revision !== revision) {
|
|
714
|
-
return { outcome: 'refused',
|
|
831
|
+
return { outcome: 'refused', code: 'stale', retryable: false, file: path };
|
|
715
832
|
}
|
|
716
833
|
if (!saved.some((draft) => draft.path === path))
|
|
717
834
|
saved.push({ path, parts: record.parts });
|
|
718
835
|
}
|
|
719
836
|
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
|
|
720
837
|
if (changed.length > 0)
|
|
721
|
-
return { outcome: 'refused',
|
|
838
|
+
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
722
839
|
const parts = saved.map(({ path, parts }) => changesOf(path, parts));
|
|
723
|
-
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,
|
|
724
859
|
baseRoomVersion: replica.confirmed.record.roomVersion,
|
|
725
860
|
composition: compositionFor(replica.confirmed.record),
|
|
726
861
|
operations: room.operations,
|
|
727
862
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
728
863
|
providerLimits: limits,
|
|
729
|
-
|
|
864
|
+
handle: room.handle,
|
|
730
865
|
visible: replica.visible,
|
|
731
|
-
changes
|
|
866
|
+
changes,
|
|
732
867
|
bases: parts.flatMap(({ bases }) => bases),
|
|
733
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 }];
|
|
734
876
|
});
|
|
735
|
-
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')
|
|
736
895
|
return { outcome: 'refused', ...built.refusal };
|
|
737
|
-
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
|
+
}
|
|
738
927
|
const consumed = saved.map(({ path }) => path);
|
|
739
928
|
try {
|
|
740
|
-
await local.
|
|
741
|
-
|
|
742
|
-
previous: replica.confirmed.record,
|
|
929
|
+
await local.keepAuthored({
|
|
930
|
+
locator,
|
|
743
931
|
queue: batches,
|
|
744
|
-
|
|
745
|
-
saved,
|
|
932
|
+
saved: drafted,
|
|
746
933
|
consumedRoomDrafts: consumed,
|
|
747
934
|
});
|
|
748
935
|
}
|
|
749
936
|
catch {
|
|
750
|
-
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 });
|
|
751
943
|
}
|
|
752
|
-
|
|
944
|
+
if (!localSaves)
|
|
945
|
+
recorded = false;
|
|
753
946
|
for (const path of consumed)
|
|
754
947
|
forgetRoomDraft(locator, path);
|
|
755
|
-
replicas.set(locator, replicaOf(replica.confirmed, batches,
|
|
948
|
+
replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
|
|
756
949
|
publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
|
|
757
950
|
queue: true,
|
|
758
951
|
drafts: consumed,
|
|
759
952
|
}));
|
|
760
|
-
moveTo({ ...saving(status), ...attention() });
|
|
761
|
-
|
|
762
|
-
|
|
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 };
|
|
763
959
|
}
|
|
764
960
|
async function mark(local, locator, identifier, submission, lost) {
|
|
765
961
|
const state = pendings.get(identifier);
|
|
@@ -785,16 +981,15 @@ export function composeRuntime(options) {
|
|
|
785
981
|
}
|
|
786
982
|
async function readHistory(locator, path, query = {}) {
|
|
787
983
|
if (closed || refusal !== null)
|
|
788
|
-
return
|
|
984
|
+
return UNCERTAIN;
|
|
789
985
|
try {
|
|
790
986
|
await ensureAuthorization();
|
|
791
987
|
}
|
|
792
988
|
catch {
|
|
793
|
-
return
|
|
794
|
-
}
|
|
795
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
796
|
-
return { kind: 'transport-uncertainty' };
|
|
989
|
+
return UNCERTAIN;
|
|
797
990
|
}
|
|
991
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
992
|
+
return UNCERTAIN;
|
|
798
993
|
const transfer = ['transfer', 'history', locator, path];
|
|
799
994
|
if (query.revision !== undefined)
|
|
800
995
|
transfer.push(query.revision);
|
|
@@ -816,17 +1011,101 @@ export function composeRuntime(options) {
|
|
|
816
1011
|
}
|
|
817
1012
|
if (result.kind === 'revoked-authorization')
|
|
818
1013
|
await refuse('revoked-authorization');
|
|
819
|
-
return result;
|
|
1014
|
+
return historyAnswer(result);
|
|
820
1015
|
}
|
|
821
1016
|
catch {
|
|
822
1017
|
journal.record('outcome history transport-uncertainty');
|
|
823
|
-
return
|
|
1018
|
+
return UNCERTAIN;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
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;
|
|
824
1058
|
}
|
|
825
1059
|
}
|
|
826
|
-
function
|
|
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() {
|
|
827
1106
|
for (const capability of capabilities) {
|
|
828
|
-
if (capability.
|
|
829
|
-
return capability.
|
|
1107
|
+
if (capability.conversationAgents !== undefined)
|
|
1108
|
+
return capability.conversationAgents;
|
|
830
1109
|
}
|
|
831
1110
|
return undefined;
|
|
832
1111
|
}
|
|
@@ -842,23 +1121,20 @@ export function composeRuntime(options) {
|
|
|
842
1121
|
async function invokeAction(name, input) {
|
|
843
1122
|
await ready;
|
|
844
1123
|
if (closed || refusal !== null)
|
|
845
|
-
return
|
|
1124
|
+
return UNCERTAIN;
|
|
846
1125
|
if (declaredAction(name) === undefined)
|
|
847
|
-
return
|
|
1126
|
+
return undeclared(name);
|
|
848
1127
|
try {
|
|
849
1128
|
await ensureAuthorization();
|
|
850
1129
|
}
|
|
851
1130
|
catch {
|
|
852
|
-
return
|
|
853
|
-
}
|
|
854
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
855
|
-
return { kind: 'transport-uncertainty' };
|
|
1131
|
+
return UNCERTAIN;
|
|
856
1132
|
}
|
|
1133
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1134
|
+
return UNCERTAIN;
|
|
857
1135
|
journal.record(`transfer action ${name}`);
|
|
858
1136
|
try {
|
|
859
1137
|
const result = await options.adapter.action(authorization, { name, input });
|
|
860
|
-
if (result.kind === 'action' && result.at !== undefined)
|
|
861
|
-
want(result.at);
|
|
862
1138
|
if (result.kind === 'scoped-refusal') {
|
|
863
1139
|
const room = replicas.get(result.locator)?.confirmed.record;
|
|
864
1140
|
if (room !== undefined)
|
|
@@ -869,11 +1145,11 @@ export function composeRuntime(options) {
|
|
|
869
1145
|
journal.record(`outcome action ${result.kind}`);
|
|
870
1146
|
if (result.kind === 'rejected')
|
|
871
1147
|
journal.record(`refusal rejected ${result.code}`);
|
|
872
|
-
return result;
|
|
1148
|
+
return calledResult(result);
|
|
873
1149
|
}
|
|
874
1150
|
catch {
|
|
875
1151
|
journal.record('outcome action transport-uncertainty');
|
|
876
|
-
return
|
|
1152
|
+
return UNCERTAIN;
|
|
877
1153
|
}
|
|
878
1154
|
}
|
|
879
1155
|
function continuing() {
|
|
@@ -890,32 +1166,11 @@ export function composeRuntime(options) {
|
|
|
890
1166
|
options.adapter.reached(locator, written, roomVersion) &&
|
|
891
1167
|
!options.adapter.reached(locator, roomVersion, written));
|
|
892
1168
|
}
|
|
893
|
-
function
|
|
894
|
-
if (reachedBy(locator, roomVersion))
|
|
895
|
-
return;
|
|
896
|
-
const held = wanted.get(locator);
|
|
897
|
-
if (held === undefined || options.adapter.reached(locator, roomVersion, held)) {
|
|
898
|
-
wanted.set(locator, roomVersion);
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
function reachOf(locator) {
|
|
902
|
-
let target = wanted.get(locator);
|
|
903
|
-
const versions = (replicas.get(locator)?.queue ?? []).flatMap((batch) => {
|
|
904
|
-
const accepted = pendings.get(batch.id)?.accepted;
|
|
905
|
-
return accepted === undefined ? [] : [accepted];
|
|
906
|
-
});
|
|
907
|
-
for (const version of versions) {
|
|
908
|
-
if (target === undefined || options.adapter.reached(locator, version, target)) {
|
|
909
|
-
target = version;
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
return target === undefined || reachedBy(locator, target) ? undefined : target;
|
|
913
|
-
}
|
|
914
|
-
async function pullRefresh(authorized, confirmed, minPosition) {
|
|
1169
|
+
async function pullRefresh(authorized, confirmed) {
|
|
915
1170
|
if (!continuing())
|
|
916
1171
|
return unconfirmed;
|
|
917
1172
|
try {
|
|
918
|
-
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);
|
|
919
1174
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
920
1175
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
921
1176
|
return result;
|
|
@@ -925,14 +1180,33 @@ export function composeRuntime(options) {
|
|
|
925
1180
|
throw error;
|
|
926
1181
|
}
|
|
927
1182
|
}
|
|
928
|
-
|
|
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) {
|
|
929
1203
|
if (!continuing())
|
|
930
1204
|
return unconfirmed;
|
|
931
1205
|
try {
|
|
932
1206
|
const result = await bootstrapRoom(options.adapter, authorized, room, {
|
|
933
1207
|
collection: modelOf(room.locator),
|
|
934
1208
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
935
|
-
}, continuing);
|
|
1209
|
+
}, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
|
|
936
1210
|
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
937
1211
|
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
938
1212
|
return result;
|
|
@@ -945,35 +1219,46 @@ export function composeRuntime(options) {
|
|
|
945
1219
|
async function advance(room, local, authorized) {
|
|
946
1220
|
const held = replicas.get(room.locator);
|
|
947
1221
|
if (held !== undefined) {
|
|
948
|
-
const refreshed = await pullRefresh(authorized, held.confirmed
|
|
1222
|
+
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
949
1223
|
if (!continuing())
|
|
950
1224
|
return unconfirmed;
|
|
951
1225
|
if (refreshed.kind === 'still-current') {
|
|
1226
|
+
stale.delete(room.locator);
|
|
952
1227
|
if (sourceRefusals.has(room.locator))
|
|
953
|
-
return
|
|
1228
|
+
return null;
|
|
954
1229
|
if (unreadable.delete(room.locator)) {
|
|
1230
|
+
scopedRefusals.delete(room.locator);
|
|
955
1231
|
list(room, true);
|
|
956
1232
|
emitRooms();
|
|
957
1233
|
}
|
|
958
1234
|
return null;
|
|
959
1235
|
}
|
|
960
1236
|
if (refreshed.kind === 'advanced') {
|
|
961
|
-
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));
|
|
962
1244
|
return null;
|
|
963
1245
|
}
|
|
964
1246
|
if (refreshed.kind !== 'beyond-horizon')
|
|
965
1247
|
return refreshed;
|
|
966
1248
|
}
|
|
967
|
-
const bootstrapped = await pullBootstrap(authorized, room);
|
|
1249
|
+
const bootstrapped = await pullBootstrap(authorized, room, local);
|
|
968
1250
|
if (!continuing())
|
|
969
1251
|
return unconfirmed;
|
|
970
1252
|
if (bootstrapped.kind !== 'staged')
|
|
971
1253
|
return bootstrapped;
|
|
972
|
-
await publish(local, room, bootstrapped.confirmed);
|
|
1254
|
+
await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
|
|
973
1255
|
return null;
|
|
974
1256
|
}
|
|
975
1257
|
async function withhold(withheld, room, local, progress) {
|
|
976
|
-
|
|
1258
|
+
if (withheld.kind === 'refused')
|
|
1259
|
+
stale.delete(room.locator);
|
|
1260
|
+
else
|
|
1261
|
+
stale.add(room.locator);
|
|
977
1262
|
if (withheld.kind === 'scoped-refusal') {
|
|
978
1263
|
await scoped(withheld, room, local);
|
|
979
1264
|
}
|
|
@@ -998,6 +1283,7 @@ export function composeRuntime(options) {
|
|
|
998
1283
|
function refusedSource(room, { refusal }) {
|
|
999
1284
|
sourceRefusals.set(room.locator, {
|
|
1000
1285
|
code: refusal.rule,
|
|
1286
|
+
retryable: false,
|
|
1001
1287
|
fields: {
|
|
1002
1288
|
file: refusal.file,
|
|
1003
1289
|
...(refusal.location === undefined ? {} : { location: refusal.location }),
|
|
@@ -1012,52 +1298,46 @@ export function composeRuntime(options) {
|
|
|
1012
1298
|
}
|
|
1013
1299
|
function leaving(batch, result) {
|
|
1014
1300
|
if (result.kind === 'expired')
|
|
1015
|
-
return { batch, returned:
|
|
1016
|
-
return {
|
|
1301
|
+
return { batch, returned: returnedBy('expired') };
|
|
1302
|
+
return {
|
|
1303
|
+
batch,
|
|
1304
|
+
returned: {
|
|
1305
|
+
kind: result.kind,
|
|
1306
|
+
code: result.code,
|
|
1307
|
+
fields: result.fields,
|
|
1308
|
+
retryable: result.retryable,
|
|
1309
|
+
},
|
|
1310
|
+
};
|
|
1017
1311
|
}
|
|
1018
1312
|
function logAccepted(locator, submitted, accepted) {
|
|
1019
1313
|
journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
|
|
1020
1314
|
(accepted.author === undefined ? '' : ` author ${accepted.author}`) +
|
|
1021
1315
|
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1022
1316
|
}
|
|
1023
|
-
async function
|
|
1024
|
-
await exclusive(async () => {
|
|
1025
|
-
if (!continuing())
|
|
1026
|
-
return;
|
|
1027
|
-
const current = replicas.get(room.locator);
|
|
1028
|
-
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
1029
|
-
return;
|
|
1030
|
-
}
|
|
1031
|
-
const state = pendings.get(submitted.id);
|
|
1032
|
-
if (state === undefined)
|
|
1033
|
-
return;
|
|
1034
|
-
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1035
|
-
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1036
|
-
logAccepted(room.locator, submitted, accepted);
|
|
1037
|
-
emitQueue(room.locator);
|
|
1038
|
-
});
|
|
1039
|
-
}
|
|
1040
|
-
async function commitAccept(room, local, submitted, accepted, base, overlay) {
|
|
1317
|
+
async function adoptAccepted(room, local, submitted, accepted) {
|
|
1041
1318
|
if (!continuing())
|
|
1042
1319
|
return unconfirmed;
|
|
1043
1320
|
const current = replicas.get(room.locator);
|
|
1044
1321
|
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
1045
1322
|
return null;
|
|
1046
1323
|
}
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
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);
|
|
1057
1337
|
return applied;
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
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);
|
|
1061
1341
|
unreadable.delete(room.locator);
|
|
1062
1342
|
sourceRefusals.delete(room.locator);
|
|
1063
1343
|
declarationRefusals.delete(room.locator);
|
|
@@ -1066,17 +1346,8 @@ export function composeRuntime(options) {
|
|
|
1066
1346
|
moveTo({ ...status, ...attention() });
|
|
1067
1347
|
return null;
|
|
1068
1348
|
}
|
|
1069
|
-
async function
|
|
1070
|
-
const
|
|
1071
|
-
if (options.adapter.reached(room.locator, roomVersion, accepted.roomVersion)) {
|
|
1072
|
-
return committed(room, local, submitted, accepted, pulled, roomVersion === accepted.roomVersion);
|
|
1073
|
-
}
|
|
1074
|
-
if (fresh && continuing())
|
|
1075
|
-
await publish(local, room, pulled);
|
|
1076
|
-
return false;
|
|
1077
|
-
}
|
|
1078
|
-
async function committed(room, local, submitted, accepted, base, overlay) {
|
|
1079
|
-
const failed = await exclusive(() => commitAccept(room, local, submitted, accepted, base, overlay));
|
|
1349
|
+
async function adopted(room, local, submitted, accepted) {
|
|
1350
|
+
const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
|
|
1080
1351
|
if (failed === null)
|
|
1081
1352
|
return true;
|
|
1082
1353
|
if (failed.kind === 'refused' && continuing())
|
|
@@ -1089,10 +1360,8 @@ export function composeRuntime(options) {
|
|
|
1089
1360
|
const head = replica?.queue[0];
|
|
1090
1361
|
if (replica === undefined || head === undefined)
|
|
1091
1362
|
return;
|
|
1092
|
-
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1093
|
-
progress.complete = false;
|
|
1363
|
+
if (pendings.get(head.id)?.accepted !== undefined)
|
|
1094
1364
|
return;
|
|
1095
|
-
}
|
|
1096
1365
|
const state = pendings.get(head.id);
|
|
1097
1366
|
const { submission, lost: earlier } = state ?? {
|
|
1098
1367
|
submission: { state: 'waiting' },
|
|
@@ -1120,6 +1389,7 @@ export function composeRuntime(options) {
|
|
|
1120
1389
|
return;
|
|
1121
1390
|
}
|
|
1122
1391
|
const opensBefore = openSignals;
|
|
1392
|
+
const answered = awaitingAnswer(room.locator);
|
|
1123
1393
|
try {
|
|
1124
1394
|
result = await options.adapter.mutate(authorized, {
|
|
1125
1395
|
locator: room.locator,
|
|
@@ -1128,11 +1398,14 @@ export function composeRuntime(options) {
|
|
|
1128
1398
|
}, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
|
|
1129
1399
|
}
|
|
1130
1400
|
catch {
|
|
1401
|
+
answered();
|
|
1131
1402
|
progress.complete = false;
|
|
1132
1403
|
if (continuing())
|
|
1133
1404
|
progress.reachable = false;
|
|
1134
1405
|
return;
|
|
1135
1406
|
}
|
|
1407
|
+
if (result.kind !== 'accepted' || !continuing())
|
|
1408
|
+
answered();
|
|
1136
1409
|
if (!continuing())
|
|
1137
1410
|
return;
|
|
1138
1411
|
if (result.kind === 'transport-uncertainty') {
|
|
@@ -1185,6 +1458,7 @@ export function composeRuntime(options) {
|
|
|
1185
1458
|
const reset = result.reset;
|
|
1186
1459
|
progress.complete = false;
|
|
1187
1460
|
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1461
|
+
armTimer();
|
|
1188
1462
|
return;
|
|
1189
1463
|
}
|
|
1190
1464
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
@@ -1197,7 +1471,8 @@ export function composeRuntime(options) {
|
|
|
1197
1471
|
return;
|
|
1198
1472
|
}
|
|
1199
1473
|
const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
1200
|
-
leaving: result.kind === 'expired' && landed(head, current.confirmed)
|
|
1474
|
+
leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
|
|
1475
|
+
(result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
|
|
1201
1476
|
? { ...left, returned: null }
|
|
1202
1477
|
: left,
|
|
1203
1478
|
});
|
|
@@ -1211,95 +1486,33 @@ export function composeRuntime(options) {
|
|
|
1211
1486
|
return withhold(withheld, room, local, progress);
|
|
1212
1487
|
continue;
|
|
1213
1488
|
}
|
|
1214
|
-
await
|
|
1215
|
-
|
|
1216
|
-
if (from === undefined)
|
|
1217
|
-
return;
|
|
1218
|
-
const pulled = await pullRefresh(authorized, from, reachOf(room.locator));
|
|
1219
|
-
if (!continuing())
|
|
1220
|
-
return;
|
|
1221
|
-
if (pulled.kind === 'revoked-authorization') {
|
|
1222
|
-
await refuse('revoked-authorization');
|
|
1223
|
-
return;
|
|
1224
|
-
}
|
|
1225
|
-
if (pulled.kind === 'scoped-refusal') {
|
|
1226
|
-
await scoped(pulled, room, local);
|
|
1227
|
-
progress.complete = false;
|
|
1228
|
-
return;
|
|
1229
|
-
}
|
|
1230
|
-
if (pulled.kind === 'still-current' || pulled.kind === 'advanced') {
|
|
1231
|
-
const fresh = pulled.kind === 'advanced';
|
|
1232
|
-
if (!(await accepting(room, local, head, result, fresh ? pulled.confirmed : from, fresh))) {
|
|
1233
|
-
progress.complete = false;
|
|
1234
|
-
return;
|
|
1235
|
-
}
|
|
1236
|
-
continue;
|
|
1237
|
-
}
|
|
1238
|
-
if (pulled.kind === 'beyond-horizon') {
|
|
1239
|
-
const bootstrapped = await pullBootstrap(authorized, room);
|
|
1240
|
-
if (!continuing())
|
|
1241
|
-
return;
|
|
1242
|
-
if (bootstrapped.kind === 'revoked-authorization') {
|
|
1243
|
-
await refuse('revoked-authorization');
|
|
1244
|
-
return;
|
|
1245
|
-
}
|
|
1246
|
-
if (bootstrapped.kind === 'scoped-refusal') {
|
|
1247
|
-
await scoped(bootstrapped, room, local);
|
|
1248
|
-
progress.complete = false;
|
|
1249
|
-
return;
|
|
1250
|
-
}
|
|
1251
|
-
if (bootstrapped.kind === 'staged') {
|
|
1252
|
-
if (!(await accepting(room, local, head, result, bootstrapped.confirmed, true))) {
|
|
1253
|
-
progress.complete = false;
|
|
1254
|
-
return;
|
|
1255
|
-
}
|
|
1256
|
-
continue;
|
|
1257
|
-
}
|
|
1489
|
+
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1490
|
+
if (!kept) {
|
|
1258
1491
|
progress.complete = false;
|
|
1259
|
-
if (bootstrapped.kind === 'transport-uncertainty') {
|
|
1260
|
-
progress.reachable = false;
|
|
1261
|
-
}
|
|
1262
|
-
else if (bootstrapped.kind === 'room-not-readable') {
|
|
1263
|
-
unreadable.add(room.locator);
|
|
1264
|
-
list(room, false);
|
|
1265
|
-
emitRooms();
|
|
1266
|
-
}
|
|
1267
|
-
else if (bootstrapped.kind === 'refused') {
|
|
1268
|
-
refusedSource(room, bootstrapped);
|
|
1269
|
-
}
|
|
1270
1492
|
return;
|
|
1271
1493
|
}
|
|
1272
|
-
progress.complete = false;
|
|
1273
|
-
if (pulled.kind === 'transport-uncertainty') {
|
|
1274
|
-
progress.reachable = false;
|
|
1275
|
-
}
|
|
1276
|
-
else if (pulled.kind === 'room-not-readable') {
|
|
1277
|
-
unreadable.add(room.locator);
|
|
1278
|
-
list(room, false);
|
|
1279
|
-
emitRooms();
|
|
1280
|
-
}
|
|
1281
|
-
else if (pulled.kind === 'retryable-refusal') {
|
|
1282
|
-
journal.record(`refusal ${pulled.kind} ${pulled.code}`);
|
|
1283
|
-
}
|
|
1284
|
-
else if (pulled.kind === 'refused') {
|
|
1285
|
-
refusedSource(room, pulled);
|
|
1286
|
-
}
|
|
1287
|
-
return;
|
|
1288
1494
|
}
|
|
1289
|
-
|
|
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
|
+
};
|
|
1290
1507
|
}
|
|
1291
1508
|
async function retainModel(local, room, attached, next) {
|
|
1292
1509
|
const key = modelKey(room.locator, attached);
|
|
1293
|
-
if (next.status === 'unavailable')
|
|
1294
|
-
modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
|
|
1295
|
-
const epoch = modelEpoch.get(key) ?? 0;
|
|
1296
1510
|
const previous = modelOf(room.locator, attached);
|
|
1297
1511
|
const heldBefore = replicas.get(room.locator)?.confirmed.record;
|
|
1298
1512
|
if (JSON.stringify(previous) === JSON.stringify(next) &&
|
|
1299
1513
|
(heldBefore === undefined ||
|
|
1300
1514
|
(JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
|
|
1301
1515
|
JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
|
|
1302
|
-
JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) &&
|
|
1303
1516
|
JSON.stringify(heldBefore.typedSelection) ===
|
|
1304
1517
|
JSON.stringify(room.typedSelection ?? null))))
|
|
1305
1518
|
return;
|
|
@@ -1310,20 +1523,17 @@ export function composeRuntime(options) {
|
|
|
1310
1523
|
await exclusive(async () => {
|
|
1311
1524
|
if (!continuing())
|
|
1312
1525
|
return;
|
|
1313
|
-
if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0))
|
|
1314
|
-
return;
|
|
1315
1526
|
const held = replicas.get(room.locator);
|
|
1316
1527
|
if (held !== undefined) {
|
|
1317
1528
|
const declared = {
|
|
1318
1529
|
...held.confirmed.record,
|
|
1319
1530
|
operations: room.operations,
|
|
1320
1531
|
replica: room.replica,
|
|
1321
|
-
attachedModel: room.attachedModel,
|
|
1322
1532
|
typedSelection: room.typedSelection ?? null,
|
|
1323
1533
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
1324
1534
|
};
|
|
1325
1535
|
const reclassified = sameMembership(declared, held.confirmed.record)
|
|
1326
|
-
?
|
|
1536
|
+
? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
|
|
1327
1537
|
: readReclassified(declared, held.confirmed);
|
|
1328
1538
|
const record = reclassified.kind === 'staged'
|
|
1329
1539
|
? declared
|
|
@@ -1331,14 +1541,12 @@ export function composeRuntime(options) {
|
|
|
1331
1541
|
...declared,
|
|
1332
1542
|
typedSelection: held.confirmed.record.typedSelection,
|
|
1333
1543
|
};
|
|
1334
|
-
const read = reclassified.kind === 'deferred'
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
queue: held.queue,
|
|
1339
|
-
});
|
|
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 });
|
|
1340
1548
|
if (read.kind === 'staged') {
|
|
1341
|
-
replicas.set(room.locator, replicaOf(read.confirmed, held.queue,
|
|
1549
|
+
replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
|
|
1342
1550
|
if (declarationRefusals.delete(room.locator)) {
|
|
1343
1551
|
sourceRefusals.delete(room.locator);
|
|
1344
1552
|
unreadable.delete(room.locator);
|
|
@@ -1362,79 +1570,12 @@ export function composeRuntime(options) {
|
|
|
1362
1570
|
publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
|
|
1363
1571
|
});
|
|
1364
1572
|
}
|
|
1365
|
-
async function acquireModel(local, room, authorized, selection) {
|
|
1366
|
-
const attached = selection === null ? 'collection' : 'typed-selection';
|
|
1367
|
-
const key = modelKey(room.locator, attached);
|
|
1368
|
-
const declared = selection === null ? room.attachedModel : selection.attachedModel;
|
|
1369
|
-
let previous = modelOf(room.locator, attached);
|
|
1370
|
-
if (previous.status !== 'unavailable' &&
|
|
1371
|
-
(declared === null || !sameModel(previous.snapshot, declared))) {
|
|
1372
|
-
previous = { status: 'unavailable', reason: 'version-changed' };
|
|
1373
|
-
await retainModel(local, room, attached, previous);
|
|
1374
|
-
if (discoveryPaused)
|
|
1375
|
-
return;
|
|
1376
|
-
}
|
|
1377
|
-
const epoch = modelEpoch.get(key) ?? 0;
|
|
1378
|
-
let answer;
|
|
1379
|
-
try {
|
|
1380
|
-
answer = await options.adapter.model(authorized, selection === null
|
|
1381
|
-
? { locator: room.locator, attached: 'collection', model: declared }
|
|
1382
|
-
: {
|
|
1383
|
-
locator: room.locator,
|
|
1384
|
-
attached: 'typed-selection',
|
|
1385
|
-
roots: selection.roots,
|
|
1386
|
-
model: declared,
|
|
1387
|
-
});
|
|
1388
|
-
}
|
|
1389
|
-
catch {
|
|
1390
|
-
answer = { kind: 'transport-uncertainty' };
|
|
1391
|
-
}
|
|
1392
|
-
if (discoveryPaused || epoch !== (modelEpoch.get(key) ?? 0))
|
|
1393
|
-
return;
|
|
1394
|
-
if (answer.kind === 'revoked-authorization') {
|
|
1395
|
-
await refuse('revoked-authorization');
|
|
1396
|
-
return;
|
|
1397
|
-
}
|
|
1398
|
-
if (answer.kind === 'transport-uncertainty') {
|
|
1399
|
-
await retainModel(local, room, attached, cached(previous));
|
|
1400
|
-
return;
|
|
1401
|
-
}
|
|
1402
|
-
if (answer.kind === 'model-unavailable') {
|
|
1403
|
-
await retainModel(local, room, attached, {
|
|
1404
|
-
status: 'unavailable',
|
|
1405
|
-
reason: answer.reason,
|
|
1406
|
-
});
|
|
1407
|
-
return;
|
|
1408
|
-
}
|
|
1409
|
-
if (declared === null || !sameModel(answer.snapshot, declared)) {
|
|
1410
|
-
await retainModel(local, room, attached, {
|
|
1411
|
-
status: 'unavailable',
|
|
1412
|
-
reason: 'version-changed',
|
|
1413
|
-
});
|
|
1414
|
-
return;
|
|
1415
|
-
}
|
|
1416
|
-
if (previous.status !== 'unavailable' &&
|
|
1417
|
-
sameModel(previous.snapshot, answer.snapshot) &&
|
|
1418
|
-
previous.snapshot.schema !== answer.snapshot.schema) {
|
|
1419
|
-
await retainModel(local, room, attached, { status: 'unavailable', reason: 'invalid' });
|
|
1420
|
-
return;
|
|
1421
|
-
}
|
|
1422
|
-
const verified = answer.snapshot.schema === null ? EMPTY_SCHEMA : verify(answer.snapshot.schema);
|
|
1423
|
-
if (verified.verdict === 'refuse') {
|
|
1424
|
-
const where = verified.location === undefined ? '' : ` ${verified.location}`;
|
|
1425
|
-
journal.record(`model invalid ${room.locator} ${attached} ${answer.snapshot.identity} ${verified.rule}${where}`);
|
|
1426
|
-
}
|
|
1427
|
-
await retainModel(local, room, attached, verified.verdict === 'accept'
|
|
1428
|
-
? {
|
|
1429
|
-
status: 'current',
|
|
1430
|
-
snapshot: structuredClone(answer.snapshot),
|
|
1431
|
-
verifiedWith: MD_MODEL,
|
|
1432
|
-
types: verified.model.types,
|
|
1433
|
-
}
|
|
1434
|
-
: { status: 'unavailable', reason: 'invalid' });
|
|
1435
|
-
}
|
|
1436
1573
|
async function scoped(result, room, local) {
|
|
1437
|
-
scopedRefusals.set(room.locator, {
|
|
1574
|
+
scopedRefusals.set(room.locator, {
|
|
1575
|
+
code: result.code,
|
|
1576
|
+
fields: result.fields,
|
|
1577
|
+
retryable: result.retryable,
|
|
1578
|
+
});
|
|
1438
1579
|
let scopedAway = room;
|
|
1439
1580
|
if (result.scope === 'model')
|
|
1440
1581
|
await retainModel(local, room, 'collection', {
|
|
@@ -1460,14 +1601,35 @@ export function composeRuntime(options) {
|
|
|
1460
1601
|
emitRooms();
|
|
1461
1602
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
1462
1603
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
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) => {
|
|
1466
1622
|
const outcome = prepared[index];
|
|
1467
1623
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1468
1624
|
});
|
|
1469
|
-
|
|
1470
|
-
|
|
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');
|
|
1471
1633
|
if (failed !== undefined)
|
|
1472
1634
|
throw failed.reason;
|
|
1473
1635
|
}
|
|
@@ -1484,11 +1646,8 @@ export function composeRuntime(options) {
|
|
|
1484
1646
|
progress.complete = false;
|
|
1485
1647
|
return false;
|
|
1486
1648
|
}
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
acquireModel(local, room, authorization, null),
|
|
1490
|
-
...(selection === undefined ? [] : [acquireModel(local, room, authorization, selection)]),
|
|
1491
|
-
]);
|
|
1649
|
+
if (prior !== undefined)
|
|
1650
|
+
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
1492
1651
|
if (discoveryPaused || authorization === undefined)
|
|
1493
1652
|
return false;
|
|
1494
1653
|
if (room.replica.admission !== 'admitted') {
|
|
@@ -1496,24 +1655,53 @@ export function composeRuntime(options) {
|
|
|
1496
1655
|
progress.complete = false;
|
|
1497
1656
|
return false;
|
|
1498
1657
|
}
|
|
1499
|
-
const held = replicas.get(room.locator);
|
|
1500
|
-
if (held !== undefined && held.confirmed.record.replica.selection !== room.replica.selection) {
|
|
1501
|
-
progress.complete = false;
|
|
1502
|
-
return false;
|
|
1503
|
-
}
|
|
1504
1658
|
return true;
|
|
1505
1659
|
}
|
|
1506
|
-
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) {
|
|
1507
1678
|
if (discoveryPaused || authorization === undefined)
|
|
1508
1679
|
return;
|
|
1680
|
+
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1681
|
+
await reload(room, local, progress);
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1509
1684
|
const withheld = await advance(room, local, authorization);
|
|
1510
1685
|
if (withheld !== null) {
|
|
1511
1686
|
await withhold(withheld, room, local, progress);
|
|
1512
1687
|
return;
|
|
1513
1688
|
}
|
|
1514
|
-
await
|
|
1689
|
+
await caughtUp(room, local, progress);
|
|
1515
1690
|
}
|
|
1516
|
-
async function
|
|
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);
|
|
1703
|
+
}
|
|
1704
|
+
async function discoverRooms(local, progress, attempt) {
|
|
1517
1705
|
if (closed || authorization === undefined) {
|
|
1518
1706
|
notifyReady();
|
|
1519
1707
|
return;
|
|
@@ -1539,12 +1727,17 @@ export function composeRuntime(options) {
|
|
|
1539
1727
|
}
|
|
1540
1728
|
}
|
|
1541
1729
|
progress.reachable = false;
|
|
1730
|
+
progress.complete = false;
|
|
1542
1731
|
notifyReady();
|
|
1543
1732
|
return;
|
|
1544
1733
|
}
|
|
1545
1734
|
if (result.kind === 'contract-unavailable') {
|
|
1546
1735
|
journal.record(`connection contract unavailable ${result.code}`);
|
|
1547
|
-
await refuse('missing-required-guarantee', {
|
|
1736
|
+
await refuse('missing-required-guarantee', {
|
|
1737
|
+
code: result.code,
|
|
1738
|
+
fields: result.fields,
|
|
1739
|
+
retryable: result.retryable,
|
|
1740
|
+
});
|
|
1548
1741
|
notifyReady();
|
|
1549
1742
|
return;
|
|
1550
1743
|
}
|
|
@@ -1553,17 +1746,6 @@ export function composeRuntime(options) {
|
|
|
1553
1746
|
notifyReady();
|
|
1554
1747
|
return;
|
|
1555
1748
|
}
|
|
1556
|
-
if (!result.supportedVersions.includes(ROOM_CONTRACT_VERSION)) {
|
|
1557
|
-
await refuse('no-matching-version');
|
|
1558
|
-
notifyReady();
|
|
1559
|
-
return;
|
|
1560
|
-
}
|
|
1561
|
-
if (result.mdModel.identity !== MD_MODEL.identity ||
|
|
1562
|
-
result.mdModel.version !== MD_MODEL.version) {
|
|
1563
|
-
await refuse('md-model-version-mismatch');
|
|
1564
|
-
notifyReady();
|
|
1565
|
-
return;
|
|
1566
|
-
}
|
|
1567
1749
|
for (const room of result.rooms) {
|
|
1568
1750
|
if (room.operations === undefined ||
|
|
1569
1751
|
!['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
@@ -1571,8 +1753,7 @@ export function composeRuntime(options) {
|
|
|
1571
1753
|
room.replica === undefined ||
|
|
1572
1754
|
typeof room.replica.selection !== 'string' ||
|
|
1573
1755
|
room.replica.selection.length === 0 ||
|
|
1574
|
-
!['admitted', 'unavailable'].includes(room.replica.admission)
|
|
1575
|
-
!declaredIdentity(room.attachedModel)) {
|
|
1756
|
+
!['admitted', 'unavailable'].includes(room.replica.admission)) {
|
|
1576
1757
|
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
1577
1758
|
await refuse('missing-required-guarantee');
|
|
1578
1759
|
notifyReady();
|
|
@@ -1585,14 +1766,6 @@ export function composeRuntime(options) {
|
|
|
1585
1766
|
return;
|
|
1586
1767
|
}
|
|
1587
1768
|
}
|
|
1588
|
-
const declared = new Set(result.capabilities.map((capability) => capability.name));
|
|
1589
|
-
for (const required of options.requiredGuarantees) {
|
|
1590
|
-
if (!knownCapabilities.has(required) || !declared.has(required)) {
|
|
1591
|
-
await refuse('missing-required-guarantee');
|
|
1592
|
-
notifyReady();
|
|
1593
|
-
return;
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
1769
|
if (refusal !== null) {
|
|
1597
1770
|
refusal = null;
|
|
1598
1771
|
emitRefusal();
|
|
@@ -1600,26 +1773,19 @@ export function composeRuntime(options) {
|
|
|
1600
1773
|
const previousCapabilities = capabilities;
|
|
1601
1774
|
capabilities = result.capabilities;
|
|
1602
1775
|
notifyReady();
|
|
1603
|
-
if (declared.has('room-version-notifications'))
|
|
1604
|
-
observeRoomVersions();
|
|
1605
1776
|
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
|
|
1606
1777
|
JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
|
|
1607
1778
|
limits = result.providerLimits;
|
|
1608
1779
|
await local.declare(limits, capabilities);
|
|
1609
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();
|
|
1786
|
+
}
|
|
1610
1787
|
await applyDiscovery(result.rooms, local);
|
|
1611
|
-
|
|
1612
|
-
.filter((room) => room.replica.admission === 'admitted')
|
|
1613
|
-
.map((room) => room.locator)
|
|
1614
|
-
.sort());
|
|
1615
|
-
progress.complete = true;
|
|
1616
|
-
await synchronize(result.rooms, local, progress);
|
|
1617
|
-
if (closed || discoveryPaused)
|
|
1618
|
-
return;
|
|
1619
|
-
await conversations.reconcile();
|
|
1620
|
-
if (status.providerUnreachable)
|
|
1621
|
-
conversations.reconnected();
|
|
1622
|
-
await conversations.catchUp(progress);
|
|
1788
|
+
await synchronize(result.rooms, local, progress, attempt);
|
|
1623
1789
|
}
|
|
1624
1790
|
async function ensureAuthorization() {
|
|
1625
1791
|
if (authorization !== undefined)
|
|
@@ -1633,64 +1799,262 @@ export function composeRuntime(options) {
|
|
|
1633
1799
|
throw error;
|
|
1634
1800
|
}
|
|
1635
1801
|
}
|
|
1636
|
-
|
|
1637
|
-
if (
|
|
1638
|
-
|
|
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');
|
|
1639
1856
|
return;
|
|
1640
1857
|
}
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
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)
|
|
1650
1872
|
return;
|
|
1651
|
-
|
|
1652
|
-
|
|
1873
|
+
if (progress === null) {
|
|
1874
|
+
await reloadExpired(expired);
|
|
1653
1875
|
return;
|
|
1654
|
-
if (progress.complete) {
|
|
1655
|
-
completedAt = new Date().toISOString();
|
|
1656
|
-
await local.complete(completedAt);
|
|
1657
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)));
|
|
1658
1905
|
}
|
|
1659
1906
|
catch {
|
|
1660
|
-
progress.
|
|
1661
|
-
|
|
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;
|
|
1662
1925
|
return;
|
|
1663
1926
|
}
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
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);
|
|
1667
1959
|
}
|
|
1668
1960
|
}
|
|
1669
|
-
async function
|
|
1961
|
+
async function finishOpening(progress) {
|
|
1670
1962
|
try {
|
|
1671
|
-
|
|
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;
|
|
1672
1978
|
}
|
|
1673
1979
|
finally {
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
queued = false;
|
|
1677
|
-
void enqueue();
|
|
1678
|
-
}
|
|
1980
|
+
complete = progress.complete;
|
|
1981
|
+
await rest(progress);
|
|
1679
1982
|
}
|
|
1680
1983
|
}
|
|
1681
|
-
function
|
|
1682
|
-
|
|
1683
|
-
if (path === undefined || withdrawNotifications !== undefined)
|
|
1984
|
+
async function rest(progress) {
|
|
1985
|
+
if (closed)
|
|
1684
1986
|
return;
|
|
1685
|
-
|
|
1686
|
-
|
|
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)
|
|
1687
2012
|
return;
|
|
1688
|
-
|
|
1689
|
-
|
|
2013
|
+
const room = discovered.get(locator);
|
|
2014
|
+
const held = replicas.get(locator);
|
|
2015
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
1690
2016
|
return;
|
|
2017
|
+
if (held !== undefined &&
|
|
2018
|
+
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
2019
|
+
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
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');
|
|
1691
2051
|
}
|
|
1692
|
-
|
|
1693
|
-
|
|
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);
|
|
1694
2058
|
});
|
|
1695
2059
|
}
|
|
1696
2060
|
function writesHeld() {
|
|
@@ -1711,14 +2075,17 @@ export function composeRuntime(options) {
|
|
|
1711
2075
|
listener(mode);
|
|
1712
2076
|
}
|
|
1713
2077
|
if (releasing)
|
|
1714
|
-
|
|
2078
|
+
submitAll();
|
|
1715
2079
|
}
|
|
1716
2080
|
function observeConversations() {
|
|
1717
2081
|
const stream = options.adapter.conversations;
|
|
1718
2082
|
if (stream === undefined || withdrawConversations !== undefined)
|
|
1719
2083
|
return;
|
|
1720
2084
|
withdrawConversations = stream.observe((event) => {
|
|
1721
|
-
conversations.receive(event)
|
|
2085
|
+
if (conversations.receive(event)) {
|
|
2086
|
+
void ready.then(scheduleCurrency);
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
1722
2089
|
if (event.kind !== 'gap' || conversations.current())
|
|
1723
2090
|
return;
|
|
1724
2091
|
show();
|
|
@@ -1729,7 +2096,7 @@ export function composeRuntime(options) {
|
|
|
1729
2096
|
if (currencyQueued || closed || stopping)
|
|
1730
2097
|
return;
|
|
1731
2098
|
currencyQueued = true;
|
|
1732
|
-
|
|
2099
|
+
void task(runCurrency);
|
|
1733
2100
|
}
|
|
1734
2101
|
async function runCurrency() {
|
|
1735
2102
|
currencyQueued = false;
|
|
@@ -1740,110 +2107,22 @@ export function composeRuntime(options) {
|
|
|
1740
2107
|
show();
|
|
1741
2108
|
return;
|
|
1742
2109
|
}
|
|
1743
|
-
const
|
|
1744
|
-
|
|
1745
|
-
moveTo(transferring(resting));
|
|
2110
|
+
const progress = { complete: false, reachable: !status.providerUnreachable };
|
|
2111
|
+
moveTo(transferring(status));
|
|
1746
2112
|
try {
|
|
1747
2113
|
await conversations.catchUp(progress);
|
|
1748
2114
|
}
|
|
1749
2115
|
catch {
|
|
1750
2116
|
progress.reachable = false;
|
|
1751
2117
|
}
|
|
1752
|
-
|
|
1753
|
-
if (!closed) {
|
|
1754
|
-
moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
function handSelected(locators) {
|
|
1759
|
-
const path = options.adapter.roomVersions;
|
|
1760
|
-
if (path === undefined || withdrawNotifications === undefined)
|
|
1761
|
-
return;
|
|
1762
|
-
if (handed !== undefined && JSON.stringify(locators) === JSON.stringify(handed))
|
|
1763
|
-
return;
|
|
1764
|
-
handed = locators;
|
|
1765
|
-
path.select(locators);
|
|
1766
|
-
}
|
|
1767
|
-
async function runNotified() {
|
|
1768
|
-
const announced = [...notices];
|
|
1769
|
-
notices.clear();
|
|
1770
|
-
if (announced.length === 0 || closed || stopping || discoveryPaused)
|
|
1771
|
-
return;
|
|
1772
|
-
const resting = status;
|
|
1773
|
-
const progress = { complete: false, reachable: !resting.providerUnreachable };
|
|
1774
|
-
moveTo(transferring(resting));
|
|
1775
|
-
try {
|
|
1776
|
-
const local = await openLocal();
|
|
1777
|
-
for (const [locator, roomVersion] of announced) {
|
|
1778
|
-
if (closed || discoveryPaused || authorization === undefined)
|
|
1779
|
-
return;
|
|
1780
|
-
if (handed?.includes(locator) !== true)
|
|
1781
|
-
continue;
|
|
1782
|
-
const room = discovered.get(locator);
|
|
1783
|
-
const held = replicas.get(locator);
|
|
1784
|
-
if (room === undefined)
|
|
1785
|
-
continue;
|
|
1786
|
-
if (sourceRefusals.has(locator)) {
|
|
1787
|
-
void enqueue();
|
|
1788
|
-
continue;
|
|
1789
|
-
}
|
|
1790
|
-
if (held === undefined)
|
|
1791
|
-
continue;
|
|
1792
|
-
if (held.confirmed.record.roomVersion === roomVersion)
|
|
1793
|
-
continue;
|
|
1794
|
-
const withheld = await advance(room, local, authorization);
|
|
1795
|
-
if (withheld === null)
|
|
1796
|
-
continue;
|
|
1797
|
-
if (withheld.kind === 'refused') {
|
|
1798
|
-
void enqueue();
|
|
1799
|
-
continue;
|
|
1800
|
-
}
|
|
1801
|
-
if (withheld.kind === 'scoped-refusal') {
|
|
1802
|
-
await scoped(withheld, room, local);
|
|
1803
|
-
continue;
|
|
1804
|
-
}
|
|
1805
|
-
if (withheld.kind === 'transport-uncertainty') {
|
|
1806
|
-
progress.reachable = false;
|
|
1807
|
-
continue;
|
|
1808
|
-
}
|
|
1809
|
-
if (withheld.kind === 'room-not-readable') {
|
|
1810
|
-
unreadable.add(locator);
|
|
1811
|
-
list(room, false);
|
|
1812
|
-
emitRooms();
|
|
1813
|
-
continue;
|
|
1814
|
-
}
|
|
1815
|
-
if (withheld.kind === 'revoked-authorization') {
|
|
1816
|
-
await refuse('revoked-authorization');
|
|
1817
|
-
return;
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
catch {
|
|
1822
|
-
progress.reachable = false;
|
|
1823
|
-
}
|
|
1824
|
-
finally {
|
|
1825
|
-
if (!closed) {
|
|
1826
|
-
moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
|
|
1827
|
-
}
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
function enqueue() {
|
|
1831
|
-
if (stopping)
|
|
1832
|
-
return chain;
|
|
1833
|
-
if (inflight) {
|
|
1834
|
-
queued = true;
|
|
1835
|
-
return chain;
|
|
1836
|
-
}
|
|
1837
|
-
inflight = true;
|
|
1838
|
-
chain = chain.then(run, run);
|
|
1839
|
-
return chain;
|
|
2118
|
+
await rest(progress);
|
|
1840
2119
|
}
|
|
1841
2120
|
function close() {
|
|
1842
2121
|
if (closed)
|
|
1843
2122
|
return;
|
|
1844
2123
|
closed = true;
|
|
1845
|
-
|
|
1846
|
-
|
|
2124
|
+
withdrawStream?.();
|
|
2125
|
+
withdrawStream = undefined;
|
|
1847
2126
|
withdrawConversations?.();
|
|
1848
2127
|
withdrawConversations = undefined;
|
|
1849
2128
|
withdrawServiceMode?.();
|
|
@@ -1858,6 +2137,7 @@ export function composeRuntime(options) {
|
|
|
1858
2137
|
storageLossListeners.clear();
|
|
1859
2138
|
statusListeners.clear();
|
|
1860
2139
|
serviceModeListeners.clear();
|
|
2140
|
+
memberListeners.clear();
|
|
1861
2141
|
readSide.close();
|
|
1862
2142
|
queueSubscriptions.clear();
|
|
1863
2143
|
roomDraftListeners.clear();
|
|
@@ -1867,6 +2147,7 @@ export function composeRuntime(options) {
|
|
|
1867
2147
|
stopping = true;
|
|
1868
2148
|
stopTimer();
|
|
1869
2149
|
await chain.catch(() => undefined);
|
|
2150
|
+
await Promise.all([...submitting.values()].map((run) => run.done));
|
|
1870
2151
|
await mutations;
|
|
1871
2152
|
close();
|
|
1872
2153
|
}
|
|
@@ -1875,11 +2156,32 @@ export function composeRuntime(options) {
|
|
|
1875
2156
|
options.signal?.addEventListener('abort', close, { once: true });
|
|
1876
2157
|
if (options.signal?.aborted)
|
|
1877
2158
|
close();
|
|
1878
|
-
|
|
2159
|
+
observeStream();
|
|
2160
|
+
void openLocal().then(() => {
|
|
2161
|
+
if (capabilities.length > 0)
|
|
2162
|
+
notifyReady();
|
|
2163
|
+
}, () => {
|
|
1879
2164
|
notifyReady();
|
|
1880
2165
|
});
|
|
1881
|
-
|
|
1882
|
-
|
|
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) => {
|
|
1883
2185
|
if (topic === 'room-drafts') {
|
|
1884
2186
|
const listener = { notify: deliver, ready: false };
|
|
1885
2187
|
roomDraftListeners.add(listener);
|
|
@@ -1900,6 +2202,10 @@ export function composeRuntime(options) {
|
|
|
1900
2202
|
if (topic === 'conversation') {
|
|
1901
2203
|
return conversations.watch(input[0], input[1], deliver);
|
|
1902
2204
|
}
|
|
2205
|
+
if (topic === 'conversations')
|
|
2206
|
+
return conversations.watchList(deliver);
|
|
2207
|
+
if (topic === 'agents')
|
|
2208
|
+
return conversations.watchAgents(deliver);
|
|
1903
2209
|
if (topic === 'presentation') {
|
|
1904
2210
|
const listener = {
|
|
1905
2211
|
app: input[0],
|
|
@@ -1954,6 +2260,13 @@ export function composeRuntime(options) {
|
|
|
1954
2260
|
serviceModeListeners.delete(deliver);
|
|
1955
2261
|
};
|
|
1956
2262
|
}
|
|
2263
|
+
if (topic === 'member') {
|
|
2264
|
+
memberListeners.add(deliver);
|
|
2265
|
+
deliver(member);
|
|
2266
|
+
return () => {
|
|
2267
|
+
memberListeners.delete(deliver);
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
1957
2270
|
if (topic === 'queue') {
|
|
1958
2271
|
const queue = { locator: input[0], notify: deliver };
|
|
1959
2272
|
queueSubscriptions.add(queue);
|
|
@@ -1962,9 +2275,8 @@ export function composeRuntime(options) {
|
|
|
1962
2275
|
queueSubscriptions.delete(queue);
|
|
1963
2276
|
};
|
|
1964
2277
|
}
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
}
|
|
2278
|
+
return watchPlan(planned(input[0], null), deliver);
|
|
2279
|
+
};
|
|
1968
2280
|
const md = {
|
|
1969
2281
|
loaded() {
|
|
1970
2282
|
return openLocal().then(() => undefined);
|
|
@@ -1972,15 +2284,8 @@ export function composeRuntime(options) {
|
|
|
1972
2284
|
rooms() {
|
|
1973
2285
|
return listed();
|
|
1974
2286
|
},
|
|
1975
|
-
query: ((input) =>
|
|
1976
|
-
|
|
1977
|
-
const plan = searchPlanOf(input);
|
|
1978
|
-
const selected = plan.documents === undefined
|
|
1979
|
-
? null
|
|
1980
|
-
: (await readSide.query(filterOf(plan.documents, roomFacts())))
|
|
1981
|
-
.documents;
|
|
1982
|
-
return searchIndex.search(plan, selected);
|
|
1983
|
-
},
|
|
2287
|
+
query: ((input) => rowsQuery(planned(input, null))),
|
|
2288
|
+
search: (input) => searched(input, null),
|
|
1984
2289
|
types(locator) {
|
|
1985
2290
|
return {
|
|
1986
2291
|
collection: modelTypes(modelOf(locator)),
|
|
@@ -2008,20 +2313,20 @@ export function composeRuntime(options) {
|
|
|
2008
2313
|
return exclusive(async () => {
|
|
2009
2314
|
const parts = readParts(input);
|
|
2010
2315
|
if (path === '' || normalize(path) !== path)
|
|
2011
|
-
throw
|
|
2316
|
+
throw runtimeError('room-draft-malformed');
|
|
2012
2317
|
const local = await openLocal();
|
|
2013
2318
|
const current = roomDrafts.get(room)?.get(path);
|
|
2014
2319
|
if ((current?.revision ?? null) !== revision) {
|
|
2015
|
-
return
|
|
2016
|
-
outcome: 'stale',
|
|
2017
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
2018
|
-
};
|
|
2320
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2019
2321
|
}
|
|
2322
|
+
const file = parts.file === undefined
|
|
2323
|
+
? undefined
|
|
2324
|
+
: await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
|
|
2020
2325
|
const record = {
|
|
2021
2326
|
locator: room,
|
|
2022
2327
|
path,
|
|
2023
2328
|
revision: roomDraftRevision(),
|
|
2024
|
-
parts: held(current?.parts, parts),
|
|
2329
|
+
parts: held(current?.parts, parts, file),
|
|
2025
2330
|
returned: current?.returned ?? null,
|
|
2026
2331
|
};
|
|
2027
2332
|
await local.keepRoomDraft(record);
|
|
@@ -2035,10 +2340,7 @@ export function composeRuntime(options) {
|
|
|
2035
2340
|
const local = await openLocal();
|
|
2036
2341
|
const current = roomDrafts.get(room)?.get(path);
|
|
2037
2342
|
if (current === undefined || current.revision !== revision) {
|
|
2038
|
-
return
|
|
2039
|
-
outcome: 'stale',
|
|
2040
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
2041
|
-
};
|
|
2343
|
+
return staleWrite(current === undefined ? null : roomDraftOf(current));
|
|
2042
2344
|
}
|
|
2043
2345
|
await local.discardRoomDraft(room, path);
|
|
2044
2346
|
forgetRoomDraft(room, path);
|
|
@@ -2073,6 +2375,10 @@ export function composeRuntime(options) {
|
|
|
2073
2375
|
history(locator, path, query) {
|
|
2074
2376
|
return readHistory(locator, path, query);
|
|
2075
2377
|
},
|
|
2378
|
+
binaryFile: {
|
|
2379
|
+
read: readBinaryPath,
|
|
2380
|
+
readReference: readBinaryReference,
|
|
2381
|
+
},
|
|
2076
2382
|
call(name, input) {
|
|
2077
2383
|
return invokeAction(name, input);
|
|
2078
2384
|
},
|
|
@@ -2087,9 +2393,7 @@ export function composeRuntime(options) {
|
|
|
2087
2393
|
persistence() {
|
|
2088
2394
|
return options.shell.persistentStorage;
|
|
2089
2395
|
},
|
|
2090
|
-
observe: (
|
|
2091
|
-
deliver(structuredClone(message));
|
|
2092
|
-
}), topic === 'query')),
|
|
2396
|
+
observe: observing(watch),
|
|
2093
2397
|
async reauthorize() {
|
|
2094
2398
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2095
2399
|
return;
|
|
@@ -2098,8 +2402,20 @@ export function composeRuntime(options) {
|
|
|
2098
2402
|
await mutations;
|
|
2099
2403
|
authorization = undefined;
|
|
2100
2404
|
discoveryPaused = false;
|
|
2101
|
-
|
|
2405
|
+
if (options.adapter.roomStream === undefined)
|
|
2406
|
+
await sync('unstreamed');
|
|
2407
|
+
else
|
|
2408
|
+
await task(ensureAuthorization);
|
|
2102
2409
|
},
|
|
2103
2410
|
};
|
|
2104
|
-
|
|
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) };
|
|
2105
2421
|
}
|