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