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