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