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