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