@agentshouse/mdruntime 0.1.0-alpha.2 → 0.1.0-alpha.4
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/dist/adapter.d.ts +33 -14
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/bootstrap.d.ts +5 -12
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +13 -20
- package/dist/bootstrap.js.map +1 -1
- package/dist/change.d.ts +64 -0
- package/dist/change.d.ts.map +1 -0
- package/dist/change.js +12 -0
- package/dist/change.js.map +1 -0
- package/dist/collection.d.ts +1 -0
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +10 -6
- package/dist/collection.js.map +1 -1
- package/dist/connection.d.ts +36 -1
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +657 -38
- package/dist/connection.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/log.d.ts +5 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +15 -0
- package/dist/log.js.map +1 -0
- package/dist/memory.d.ts +1 -0
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +69 -7
- package/dist/memory.js.map +1 -1
- package/dist/outcome.d.ts +30 -0
- package/dist/outcome.d.ts.map +1 -0
- package/dist/outcome.js +13 -0
- package/dist/outcome.js.map +1 -0
- package/dist/overlay.d.ts +8 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +139 -0
- package/dist/overlay.js.map +1 -0
- package/dist/paths.d.ts +6 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +47 -0
- package/dist/paths.js.map +1 -0
- package/dist/refresh.d.ts +13 -0
- package/dist/refresh.d.ts.map +1 -0
- package/dist/refresh.js +63 -0
- package/dist/refresh.js.map +1 -0
- package/dist/replica.d.ts +20 -0
- package/dist/replica.d.ts.map +1 -0
- package/dist/replica.js +61 -0
- package/dist/replica.js.map +1 -0
- package/dist/save.d.ts +26 -0
- package/dist/save.d.ts.map +1 -0
- package/dist/save.js +496 -0
- package/dist/save.js.map +1 -0
- package/dist/status.d.ts +16 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +31 -0
- package/dist/status.js.map +1 -0
- package/dist/store.d.ts +24 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +82 -2
- package/dist/store.js.map +1 -1
- package/dist/transfer.d.ts +5 -0
- package/dist/transfer.d.ts.map +1 -0
- package/dist/transfer.js +5 -0
- package/dist/transfer.js.map +1 -0
- package/package.json +3 -3
package/dist/connection.js
CHANGED
|
@@ -1,27 +1,79 @@
|
|
|
1
1
|
import { query as evaluate } from '@agentshouse/query';
|
|
2
2
|
import { ROOM_CONTRACT_VERSION, VERSION_1_CAPABILITIES } from './adapter.js';
|
|
3
|
-
import {
|
|
3
|
+
import { bootstrapRoom } from './bootstrap.js';
|
|
4
4
|
import { MD_MODEL, PRESENTED } from './collection.js';
|
|
5
|
+
import { createLog } from './log.js';
|
|
6
|
+
import { queuedOutcome, refused } from './outcome.js';
|
|
7
|
+
import { refreshRoom } from './refresh.js';
|
|
8
|
+
import { buildBatch } from './save.js';
|
|
9
|
+
import { applyReturned, readConfirmed, replicaOf } from './replica.js';
|
|
10
|
+
import { settled, same, saving, STARTING, transferring } from './status.js';
|
|
5
11
|
import { openReplicaStore } from './store.js';
|
|
12
|
+
import { unconfirmed } from './transfer.js';
|
|
6
13
|
const DEFAULT_DISCOVERY_INTERVAL_MS = 60_000;
|
|
7
14
|
const knownCapabilities = new Set(VERSION_1_CAPABILITIES);
|
|
8
15
|
export function createMdRuntime(options) {
|
|
9
16
|
const period = options.discoveryIntervalMs ?? DEFAULT_DISCOVERY_INTERVAL_MS;
|
|
10
17
|
let authorization = options.authorization;
|
|
18
|
+
let opening;
|
|
11
19
|
let store;
|
|
12
20
|
const replicas = new Map();
|
|
21
|
+
const drafts = new Map();
|
|
22
|
+
const outcomes = new Map();
|
|
23
|
+
let limits = [];
|
|
24
|
+
let capabilities = [];
|
|
13
25
|
const rooms = new Map();
|
|
14
26
|
const unreadable = new Set();
|
|
15
27
|
let refusal = null;
|
|
28
|
+
let status = STARTING;
|
|
16
29
|
let discoveryPaused = false;
|
|
17
30
|
let timer;
|
|
18
31
|
let closed = false;
|
|
19
32
|
let chain = Promise.resolve();
|
|
33
|
+
let mutations = Promise.resolve();
|
|
20
34
|
let inflight = false;
|
|
21
35
|
let queued = false;
|
|
22
36
|
const roomListeners = new Set();
|
|
23
37
|
const refusalListeners = new Set();
|
|
38
|
+
const statusListeners = new Set();
|
|
24
39
|
const querySubscriptions = new Set();
|
|
40
|
+
const queueSubscriptions = new Set();
|
|
41
|
+
const journal = createLog();
|
|
42
|
+
let notifyReady = ()=>{};
|
|
43
|
+
const ready = new Promise((resolve)=>{
|
|
44
|
+
notifyReady = resolve;
|
|
45
|
+
});
|
|
46
|
+
function at(locator, path) {
|
|
47
|
+
return `${locator} ${path}`;
|
|
48
|
+
}
|
|
49
|
+
function pending() {
|
|
50
|
+
return [
|
|
51
|
+
...replicas.values()
|
|
52
|
+
].some((replica)=>replica.queue.length > 0);
|
|
53
|
+
}
|
|
54
|
+
function attention() {
|
|
55
|
+
return [
|
|
56
|
+
...replicas.values()
|
|
57
|
+
].some((replica)=>{
|
|
58
|
+
const head = replica.queue[0];
|
|
59
|
+
return head !== undefined && refused(outcomes.get(head.id));
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function storedOutcomes(batches) {
|
|
63
|
+
const stored = {};
|
|
64
|
+
for (const batch of batches){
|
|
65
|
+
const outcome = queuedOutcome(outcomes.get(batch.id));
|
|
66
|
+
if (outcome.kind !== 'pending') stored[batch.id] = outcome;
|
|
67
|
+
}
|
|
68
|
+
return stored;
|
|
69
|
+
}
|
|
70
|
+
async function persistQueue(local, locator, batches) {
|
|
71
|
+
await local.writeQueue({
|
|
72
|
+
locator,
|
|
73
|
+
batches,
|
|
74
|
+
outcomes: storedOutcomes(batches)
|
|
75
|
+
});
|
|
76
|
+
}
|
|
25
77
|
function listed() {
|
|
26
78
|
return [
|
|
27
79
|
...rooms.values()
|
|
@@ -31,9 +83,9 @@ export function createMdRuntime(options) {
|
|
|
31
83
|
return [
|
|
32
84
|
...replicas.values()
|
|
33
85
|
].map((replica)=>({
|
|
34
|
-
room: replica.record.locator,
|
|
35
|
-
roomVersion: replica.record.roomVersion,
|
|
36
|
-
documents: replica.collection.documents
|
|
86
|
+
room: replica.confirmed.record.locator,
|
|
87
|
+
roomVersion: replica.confirmed.record.roomVersion,
|
|
88
|
+
documents: replica.visible.collection.documents
|
|
37
89
|
}));
|
|
38
90
|
}
|
|
39
91
|
function emitRooms() {
|
|
@@ -43,6 +95,11 @@ export function createMdRuntime(options) {
|
|
|
43
95
|
function emitRefusal() {
|
|
44
96
|
for (const listener of refusalListeners)listener(refusal);
|
|
45
97
|
}
|
|
98
|
+
function moveTo(next) {
|
|
99
|
+
if (closed || same(status, next)) return;
|
|
100
|
+
status = next;
|
|
101
|
+
for (const listener of statusListeners)listener(status);
|
|
102
|
+
}
|
|
46
103
|
function visible(request) {
|
|
47
104
|
return evaluate(queryRooms(), {
|
|
48
105
|
...request,
|
|
@@ -59,6 +116,20 @@ export function createMdRuntime(options) {
|
|
|
59
116
|
subscription.notify(visible(subscription.query));
|
|
60
117
|
}
|
|
61
118
|
}
|
|
119
|
+
function entriesOf(locator) {
|
|
120
|
+
const replica = replicas.get(locator);
|
|
121
|
+
if (replica === undefined) return [];
|
|
122
|
+
return replica.queue.map((batch)=>Object.freeze({
|
|
123
|
+
batch,
|
|
124
|
+
outcome: queuedOutcome(outcomes.get(batch.id))
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
function emitQueue(locator) {
|
|
128
|
+
const entries = entriesOf(locator);
|
|
129
|
+
for (const subscription of queueSubscriptions){
|
|
130
|
+
if (subscription.locator === locator) subscription.notify(entries);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
62
133
|
function stopTimer() {
|
|
63
134
|
if (timer === undefined) return;
|
|
64
135
|
clearInterval(timer);
|
|
@@ -77,6 +148,7 @@ export function createMdRuntime(options) {
|
|
|
77
148
|
reason,
|
|
78
149
|
reauthorizationOffered: reason === 'revoked-authorization'
|
|
79
150
|
};
|
|
151
|
+
journal.record(`connection refused ${reason}`);
|
|
80
152
|
emitRefusal();
|
|
81
153
|
}
|
|
82
154
|
function list(room, synchronizationAvailable) {
|
|
@@ -85,7 +157,7 @@ export function createMdRuntime(options) {
|
|
|
85
157
|
locator: room.locator,
|
|
86
158
|
writeGrants: room.writeGrants,
|
|
87
159
|
providerOwnedPaths: room.providerOwnedPaths,
|
|
88
|
-
roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
|
|
160
|
+
roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
|
|
89
161
|
synchronizationAvailable
|
|
90
162
|
});
|
|
91
163
|
}
|
|
@@ -102,70 +174,513 @@ export function createMdRuntime(options) {
|
|
|
102
174
|
});
|
|
103
175
|
}
|
|
104
176
|
emitRooms();
|
|
177
|
+
return listedLocators;
|
|
105
178
|
}
|
|
106
|
-
async function
|
|
107
|
-
if (store !== undefined) return store;
|
|
179
|
+
async function open() {
|
|
108
180
|
const opened = await openReplicaStore();
|
|
109
|
-
const records = await
|
|
181
|
+
const [records, queues, kept, lastCompleted, declared] = await Promise.all([
|
|
182
|
+
opened.all(),
|
|
183
|
+
opened.queues(),
|
|
184
|
+
opened.drafts(),
|
|
185
|
+
opened.lastCompleted(),
|
|
186
|
+
opened.providerLimits()
|
|
187
|
+
]);
|
|
110
188
|
store = opened;
|
|
189
|
+
limits = declared;
|
|
190
|
+
for (const draft of kept)drafts.set(at(draft.locator, draft.path), draft.content);
|
|
191
|
+
const queued = new Map(queues.map((queue)=>[
|
|
192
|
+
queue.locator,
|
|
193
|
+
queue.batches
|
|
194
|
+
]));
|
|
195
|
+
for (const queue of queues){
|
|
196
|
+
for (const [id, outcome] of Object.entries(queue.outcomes)){
|
|
197
|
+
outcomes.set(id, outcome);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
111
200
|
for (const record of records){
|
|
112
|
-
const
|
|
113
|
-
if (
|
|
114
|
-
replicas.set(record.locator,
|
|
201
|
+
const confirmed = readConfirmed(record);
|
|
202
|
+
if (confirmed === null) continue;
|
|
203
|
+
replicas.set(record.locator, replicaOf(confirmed, queued.get(record.locator) ?? []));
|
|
115
204
|
list(record, false);
|
|
116
205
|
}
|
|
206
|
+
moveTo({
|
|
207
|
+
...status,
|
|
208
|
+
lastCompleted,
|
|
209
|
+
attentionRequired: attention()
|
|
210
|
+
});
|
|
117
211
|
if (replicas.size > 0) {
|
|
118
212
|
emitRooms();
|
|
119
213
|
emitVisible();
|
|
120
214
|
}
|
|
121
215
|
return opened;
|
|
122
216
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
217
|
+
function openLocal() {
|
|
218
|
+
opening ??= open().catch((error)=>{
|
|
219
|
+
opening = undefined;
|
|
220
|
+
throw error;
|
|
221
|
+
});
|
|
222
|
+
return opening;
|
|
223
|
+
}
|
|
224
|
+
function exclusive(work) {
|
|
225
|
+
const next = mutations.then(work, work);
|
|
226
|
+
mutations = next.then(()=>undefined, ()=>undefined);
|
|
227
|
+
return next;
|
|
228
|
+
}
|
|
229
|
+
function publish(local, room, confirmed) {
|
|
230
|
+
return exclusive(async ()=>{
|
|
231
|
+
await local.publish(confirmed.record);
|
|
232
|
+
unreadable.delete(room.locator);
|
|
233
|
+
replicas.set(room.locator, replicaOf(confirmed, replicas.get(room.locator)?.queue ?? []));
|
|
234
|
+
list(room, true);
|
|
235
|
+
emitRooms();
|
|
236
|
+
emitVisible();
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
async function write(locator, changes, replacing) {
|
|
240
|
+
const replica = replicas.get(locator);
|
|
241
|
+
const room = rooms.get(locator);
|
|
242
|
+
if (replica === undefined || room === undefined) {
|
|
243
|
+
throw new Error(`No Local Replica for ${locator}`);
|
|
244
|
+
}
|
|
245
|
+
const built = buildBatch({
|
|
246
|
+
baseRoomVersion: replica.confirmed.record.roomVersion,
|
|
247
|
+
writeGrants: room.writeGrants,
|
|
248
|
+
providerOwnedPaths: room.providerOwnedPaths,
|
|
249
|
+
providerLimits: limits,
|
|
250
|
+
visible: replica.visible,
|
|
251
|
+
changes
|
|
252
|
+
});
|
|
253
|
+
if (built.kind === 'refused') return {
|
|
254
|
+
outcome: 'refused',
|
|
255
|
+
...built.refusal
|
|
256
|
+
};
|
|
257
|
+
const head = replica.queue[0];
|
|
258
|
+
if (replacing !== undefined) {
|
|
259
|
+
if (head === undefined || head.id !== replacing || !refused(outcomes.get(head.id))) {
|
|
260
|
+
return {
|
|
261
|
+
outcome: 'refused',
|
|
262
|
+
rule: 'no-refused-head'
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const local = await openLocal();
|
|
267
|
+
const batches = replacing === undefined ? [
|
|
268
|
+
...replica.queue,
|
|
269
|
+
built.batch
|
|
270
|
+
] : [
|
|
271
|
+
built.batch,
|
|
272
|
+
...replica.queue.slice(1)
|
|
273
|
+
];
|
|
274
|
+
if (replacing !== undefined) outcomes.delete(replacing);
|
|
275
|
+
const consumed = [
|
|
276
|
+
...new Set(changes.map((change)=>change.path))
|
|
277
|
+
];
|
|
278
|
+
await local.append({
|
|
279
|
+
locator,
|
|
280
|
+
batches,
|
|
281
|
+
outcomes: storedOutcomes(batches)
|
|
282
|
+
}, consumed);
|
|
283
|
+
for (const path of consumed)drafts.delete(at(locator, path));
|
|
284
|
+
replicas.set(locator, replicaOf(replica.confirmed, batches));
|
|
285
|
+
emitQueue(locator);
|
|
286
|
+
emitVisible();
|
|
287
|
+
moveTo({
|
|
288
|
+
...saving(status),
|
|
289
|
+
attentionRequired: attention()
|
|
290
|
+
});
|
|
291
|
+
void enqueue();
|
|
292
|
+
return {
|
|
293
|
+
outcome: 'saved',
|
|
294
|
+
id: built.batch.id
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
async function discardHead(locator, identifier) {
|
|
298
|
+
const replica = replicas.get(locator);
|
|
299
|
+
const head = replica?.queue[0];
|
|
300
|
+
if (replica === undefined || head === undefined || head.id !== identifier || !refused(outcomes.get(head.id))) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const local = await openLocal();
|
|
304
|
+
const batches = replica.queue.slice(1);
|
|
305
|
+
outcomes.delete(identifier);
|
|
306
|
+
await persistQueue(local, locator, batches);
|
|
307
|
+
replicas.set(locator, replicaOf(replica.confirmed, batches));
|
|
308
|
+
emitQueue(locator);
|
|
309
|
+
emitVisible();
|
|
310
|
+
moveTo({
|
|
311
|
+
...status,
|
|
312
|
+
state: pending() ? 'pending' : status.state,
|
|
313
|
+
attentionRequired: attention()
|
|
314
|
+
});
|
|
315
|
+
void enqueue();
|
|
316
|
+
}
|
|
317
|
+
async function readHistory(locator, path, query = {}) {
|
|
318
|
+
if (closed || refusal !== null) return {
|
|
319
|
+
kind: 'transport-uncertainty'
|
|
320
|
+
};
|
|
321
|
+
try {
|
|
322
|
+
await ensureAuthorization();
|
|
323
|
+
} catch {
|
|
324
|
+
return {
|
|
325
|
+
kind: 'transport-uncertainty'
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
if (closed || authorization === undefined || refusal !== null) {
|
|
329
|
+
return {
|
|
330
|
+
kind: 'transport-uncertainty'
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const transfer = [
|
|
334
|
+
'transfer',
|
|
335
|
+
'history',
|
|
336
|
+
locator,
|
|
337
|
+
path
|
|
338
|
+
];
|
|
339
|
+
if (query.revision !== undefined) transfer.push(query.revision);
|
|
340
|
+
if (query.pageToken !== undefined) transfer.push(query.pageToken);
|
|
341
|
+
journal.record(transfer.join(' '));
|
|
342
|
+
try {
|
|
343
|
+
const result = await options.adapter.history(authorization, {
|
|
344
|
+
locator,
|
|
345
|
+
path,
|
|
346
|
+
...query.revision === undefined ? {} : {
|
|
347
|
+
revision: query.revision
|
|
348
|
+
},
|
|
349
|
+
...query.pageToken === undefined ? {} : {
|
|
350
|
+
pageToken: query.pageToken
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
journal.record(`outcome history ${result.kind}`);
|
|
354
|
+
if (result.kind === 'revoked-authorization') refuse('revoked-authorization');
|
|
355
|
+
return result;
|
|
356
|
+
} catch {
|
|
357
|
+
journal.record('outcome history transport-uncertainty');
|
|
358
|
+
return {
|
|
359
|
+
kind: 'transport-uncertainty'
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function declaredAction(name) {
|
|
364
|
+
for (const capability of capabilities){
|
|
365
|
+
for (const action of capability.actions ?? []){
|
|
366
|
+
if (action.name === name) return action;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
async function invokeAction(name, input) {
|
|
372
|
+
await ready;
|
|
373
|
+
if (closed || refusal !== null) return {
|
|
374
|
+
kind: 'transport-uncertainty'
|
|
375
|
+
};
|
|
376
|
+
if (declaredAction(name) === undefined) return {
|
|
377
|
+
kind: 'undeclared'
|
|
378
|
+
};
|
|
379
|
+
try {
|
|
380
|
+
await ensureAuthorization();
|
|
381
|
+
} catch {
|
|
382
|
+
return {
|
|
383
|
+
kind: 'transport-uncertainty'
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
if (closed || authorization === undefined || refusal !== null) {
|
|
387
|
+
return {
|
|
388
|
+
kind: 'transport-uncertainty'
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
journal.record(`transfer action ${name}`);
|
|
392
|
+
try {
|
|
393
|
+
const result = await options.adapter.action(authorization, {
|
|
394
|
+
name,
|
|
395
|
+
input
|
|
396
|
+
});
|
|
397
|
+
if (result.kind === 'revoked-authorization') refuse('revoked-authorization');
|
|
398
|
+
journal.record(`outcome action ${result.kind}`);
|
|
399
|
+
if (result.kind === 'rejected') journal.record(`refusal rejected ${result.text}`);
|
|
400
|
+
return result;
|
|
401
|
+
} catch {
|
|
402
|
+
journal.record('outcome action transport-uncertainty');
|
|
403
|
+
return {
|
|
404
|
+
kind: 'transport-uncertainty'
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
async function pullRefresh(authorized, confirmed) {
|
|
409
|
+
try {
|
|
410
|
+
const result = await refreshRoom(options.adapter, authorized, confirmed);
|
|
411
|
+
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
412
|
+
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
413
|
+
return result;
|
|
414
|
+
} catch (error) {
|
|
415
|
+
journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
|
|
416
|
+
throw error;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
async function pullBootstrap(authorized, room) {
|
|
420
|
+
try {
|
|
421
|
+
const result = await bootstrapRoom(options.adapter, authorized, room);
|
|
422
|
+
const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
|
|
423
|
+
journal.record(`transfer bootstrap ${room.locator} ${detail}`);
|
|
424
|
+
return result;
|
|
425
|
+
} catch (error) {
|
|
426
|
+
journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
|
|
427
|
+
throw error;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async function advance(room, local, authorized) {
|
|
431
|
+
const held = replicas.get(room.locator);
|
|
432
|
+
if (held !== undefined) {
|
|
433
|
+
const refreshed = await pullRefresh(authorized, held.confirmed);
|
|
434
|
+
if (refreshed.kind === 'still-current') {
|
|
435
|
+
if (unreadable.delete(room.locator)) {
|
|
436
|
+
list(room, true);
|
|
437
|
+
emitRooms();
|
|
438
|
+
}
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
if (refreshed.kind === 'advanced') {
|
|
442
|
+
if (closed) return unconfirmed;
|
|
443
|
+
await publish(local, room, refreshed.confirmed);
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
if (refreshed.kind !== 'beyond-horizon') return refreshed;
|
|
447
|
+
}
|
|
448
|
+
const bootstrapped = await pullBootstrap(authorized, room);
|
|
449
|
+
if (bootstrapped.kind !== 'staged') return bootstrapped;
|
|
450
|
+
if (closed) return unconfirmed;
|
|
451
|
+
await publish(local, room, bootstrapped.confirmed);
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
function recordRefusal(result) {
|
|
455
|
+
if (result.kind === 'expired') return {
|
|
456
|
+
kind: 'expired'
|
|
457
|
+
};
|
|
458
|
+
if (result.kind === 'conflicted') {
|
|
459
|
+
return {
|
|
460
|
+
kind: 'conflicted',
|
|
461
|
+
roomVersion: result.roomVersion,
|
|
462
|
+
affected: result.affected,
|
|
463
|
+
text: result.text
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
kind: 'rejected',
|
|
468
|
+
affected: result.affected,
|
|
469
|
+
text: result.text
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
async function commitAccept(room, local, submitted, accepted, base, overlay) {
|
|
473
|
+
const current = replicas.get(room.locator);
|
|
474
|
+
if (current === undefined || !current.queue.some((batch)=>batch.id === submitted.id)) {
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
const remaining = current.queue.filter((batch)=>batch.id !== submitted.id);
|
|
478
|
+
const applied = overlay ? applyReturned(base, accepted.documents, accepted.revisions, accepted.removals, accepted.roomVersion) : base;
|
|
479
|
+
if (applied === null) return false;
|
|
480
|
+
await local.publish(applied.record);
|
|
481
|
+
await persistQueue(local, room.locator, remaining);
|
|
482
|
+
outcomes.set(submitted.id, {
|
|
483
|
+
kind: 'accepted',
|
|
484
|
+
roomVersion: accepted.roomVersion,
|
|
485
|
+
author: accepted.author,
|
|
486
|
+
time: accepted.time
|
|
487
|
+
});
|
|
488
|
+
unreadable.delete(room.locator);
|
|
489
|
+
replicas.set(room.locator, replicaOf(applied, remaining));
|
|
490
|
+
list(room, true);
|
|
491
|
+
emitRooms();
|
|
492
|
+
emitQueue(room.locator);
|
|
493
|
+
emitVisible();
|
|
494
|
+
journal.record(`outcome ${room.locator} ${submitted.id} accepted`);
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
async function submitRoom(room, local, authorized, progress) {
|
|
498
|
+
while(!closed && authorization !== undefined){
|
|
499
|
+
const replica = replicas.get(room.locator);
|
|
500
|
+
const head = replica?.queue[0];
|
|
501
|
+
if (replica === undefined || head === undefined || refused(outcomes.get(head.id))) {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
let result;
|
|
505
|
+
journal.record(`submission ${room.locator} ${head.id}`);
|
|
506
|
+
try {
|
|
507
|
+
result = await options.adapter.mutate(authorized, {
|
|
508
|
+
locator: room.locator,
|
|
509
|
+
changeBatch: head
|
|
510
|
+
});
|
|
511
|
+
} catch {
|
|
512
|
+
progress.complete = false;
|
|
513
|
+
progress.reachable = false;
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
if (closed) return null;
|
|
517
|
+
if (result.kind === 'transport-uncertainty') {
|
|
518
|
+
progress.complete = false;
|
|
519
|
+
progress.reachable = false;
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
if (result.kind === 'room-not-readable') {
|
|
523
|
+
progress.complete = false;
|
|
524
|
+
unreadable.add(room.locator);
|
|
525
|
+
list(room, false);
|
|
526
|
+
emitRooms();
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
if (result.kind === 'revoked-authorization') {
|
|
129
530
|
refuse('revoked-authorization');
|
|
130
|
-
return;
|
|
531
|
+
return 'stop';
|
|
532
|
+
}
|
|
533
|
+
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
534
|
+
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}`);
|
|
535
|
+
if (result.kind !== 'expired') journal.record(`refusal ${result.kind} ${result.text}`);
|
|
536
|
+
await exclusive(async ()=>{
|
|
537
|
+
const current = replicas.get(room.locator);
|
|
538
|
+
if (current === undefined || !current.queue.some((batch)=>batch.id === head.id)) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
outcomes.set(head.id, recordRefusal(result));
|
|
542
|
+
await persistQueue(local, room.locator, current.queue);
|
|
543
|
+
emitQueue(room.locator);
|
|
544
|
+
moveTo({
|
|
545
|
+
...status,
|
|
546
|
+
attentionRequired: true
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
const from = replicas.get(room.locator)?.confirmed;
|
|
552
|
+
if (from === undefined) return null;
|
|
553
|
+
const pulled = await pullRefresh(authorized, from);
|
|
554
|
+
if (pulled.kind === 'revoked-authorization') {
|
|
555
|
+
refuse('revoked-authorization');
|
|
556
|
+
return 'stop';
|
|
557
|
+
}
|
|
558
|
+
if (pulled.kind === 'still-current') {
|
|
559
|
+
const committed = await exclusive(()=>commitAccept(room, local, head, result, from, true));
|
|
560
|
+
if (!committed) {
|
|
561
|
+
progress.complete = false;
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
continue;
|
|
131
565
|
}
|
|
132
|
-
if (
|
|
566
|
+
if (pulled.kind === 'advanced') {
|
|
567
|
+
const overlay = pulled.confirmed.record.roomVersion === result.roomVersion;
|
|
568
|
+
const committed = await exclusive(()=>commitAccept(room, local, head, result, pulled.confirmed, overlay));
|
|
569
|
+
if (!committed) {
|
|
570
|
+
progress.complete = false;
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
if (pulled.kind === 'beyond-horizon') {
|
|
576
|
+
const bootstrapped = await pullBootstrap(authorized, room);
|
|
577
|
+
if (bootstrapped.kind === 'revoked-authorization') {
|
|
578
|
+
refuse('revoked-authorization');
|
|
579
|
+
return 'stop';
|
|
580
|
+
}
|
|
581
|
+
if (bootstrapped.kind === 'staged') {
|
|
582
|
+
const overlay = bootstrapped.confirmed.record.roomVersion === result.roomVersion;
|
|
583
|
+
const committed = await exclusive(()=>commitAccept(room, local, head, result, bootstrapped.confirmed, overlay));
|
|
584
|
+
if (!committed) {
|
|
585
|
+
progress.complete = false;
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
progress.complete = false;
|
|
591
|
+
if (bootstrapped.kind === 'transport-uncertainty') {
|
|
592
|
+
progress.reachable = false;
|
|
593
|
+
} else if (bootstrapped.kind === 'room-not-readable') {
|
|
594
|
+
unreadable.add(room.locator);
|
|
595
|
+
list(room, false);
|
|
596
|
+
emitRooms();
|
|
597
|
+
}
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
progress.complete = false;
|
|
601
|
+
if (pulled.kind === 'transport-uncertainty') {
|
|
602
|
+
progress.reachable = false;
|
|
603
|
+
} else if (pulled.kind === 'room-not-readable') {
|
|
133
604
|
unreadable.add(room.locator);
|
|
134
605
|
list(room, false);
|
|
135
606
|
emitRooms();
|
|
607
|
+
}
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
async function synchronize(discovered, local, progress) {
|
|
613
|
+
for (const room of discovered){
|
|
614
|
+
if (closed || authorization === undefined) {
|
|
615
|
+
progress.complete = false;
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const withheld = await advance(room, local, authorization);
|
|
619
|
+
if (withheld !== null) {
|
|
620
|
+
progress.complete = false;
|
|
621
|
+
if (withheld.kind === 'transport-uncertainty') {
|
|
622
|
+
progress.reachable = false;
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
if (withheld.kind === 'room-not-readable') {
|
|
626
|
+
unreadable.add(room.locator);
|
|
627
|
+
list(room, false);
|
|
628
|
+
emitRooms();
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
if (withheld.kind === 'revoked-authorization') {
|
|
632
|
+
refuse('revoked-authorization');
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
136
635
|
continue;
|
|
137
636
|
}
|
|
138
|
-
if (
|
|
139
|
-
if (closed) return;
|
|
140
|
-
await local.publish(outcome.replica.record);
|
|
141
|
-
unreadable.delete(room.locator);
|
|
142
|
-
replicas.set(room.locator, outcome.replica);
|
|
143
|
-
list(room, true);
|
|
144
|
-
emitRooms();
|
|
145
|
-
emitVisible();
|
|
637
|
+
if (await submitRoom(room, local, authorization, progress) === 'stop') return;
|
|
146
638
|
}
|
|
147
639
|
}
|
|
148
|
-
async function cycle(local) {
|
|
149
|
-
if (closed || authorization === undefined)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
640
|
+
async function cycle(local, progress) {
|
|
641
|
+
if (closed || authorization === undefined) {
|
|
642
|
+
notifyReady();
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
moveTo(transferring(status));
|
|
646
|
+
let result;
|
|
647
|
+
try {
|
|
648
|
+
result = await options.adapter.discover(authorization);
|
|
649
|
+
} catch (error) {
|
|
650
|
+
journal.record('connection discover transport-uncertainty');
|
|
651
|
+
notifyReady();
|
|
652
|
+
throw error;
|
|
653
|
+
}
|
|
654
|
+
if (closed) {
|
|
655
|
+
notifyReady();
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
journal.record(`connection discover ${result.kind}`);
|
|
659
|
+
if (result.kind === 'transport-uncertainty') {
|
|
660
|
+
progress.reachable = false;
|
|
661
|
+
notifyReady();
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
153
664
|
if (result.kind === 'revoked-authorization') {
|
|
154
665
|
refuse('revoked-authorization');
|
|
666
|
+
notifyReady();
|
|
155
667
|
return;
|
|
156
668
|
}
|
|
157
669
|
if (!result.supportedVersions.includes(ROOM_CONTRACT_VERSION)) {
|
|
158
670
|
refuse('no-matching-version');
|
|
671
|
+
notifyReady();
|
|
159
672
|
return;
|
|
160
673
|
}
|
|
161
674
|
if (result.mdModel.identity !== MD_MODEL.identity || result.mdModel.version !== MD_MODEL.version) {
|
|
162
675
|
refuse('md-model-version-mismatch');
|
|
676
|
+
notifyReady();
|
|
163
677
|
return;
|
|
164
678
|
}
|
|
165
679
|
const declared = new Set(result.capabilities.map((capability)=>capability.name));
|
|
166
680
|
for (const required of options.requiredGuarantees){
|
|
167
681
|
if (!knownCapabilities.has(required) || !declared.has(required)) {
|
|
168
682
|
refuse('missing-required-guarantee');
|
|
683
|
+
notifyReady();
|
|
169
684
|
return;
|
|
170
685
|
}
|
|
171
686
|
}
|
|
@@ -173,24 +688,57 @@ export function createMdRuntime(options) {
|
|
|
173
688
|
refusal = null;
|
|
174
689
|
emitRefusal();
|
|
175
690
|
}
|
|
176
|
-
|
|
177
|
-
|
|
691
|
+
capabilities = result.capabilities;
|
|
692
|
+
notifyReady();
|
|
693
|
+
if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits)) {
|
|
694
|
+
limits = result.providerLimits;
|
|
695
|
+
await local.declare(limits);
|
|
696
|
+
}
|
|
697
|
+
const listedLocators = applyDiscovery(result.rooms);
|
|
698
|
+
progress.complete = [
|
|
699
|
+
...replicas.keys()
|
|
700
|
+
].every((locator)=>listedLocators.has(locator));
|
|
701
|
+
await synchronize(result.rooms, local, progress);
|
|
178
702
|
}
|
|
179
703
|
async function ensureAuthorization() {
|
|
180
704
|
if (authorization !== undefined) return;
|
|
181
|
-
|
|
705
|
+
try {
|
|
706
|
+
authorization = await options.adapter.authorize();
|
|
707
|
+
journal.record('connection authorize');
|
|
708
|
+
} catch (error) {
|
|
709
|
+
journal.record('connection authorize transport-uncertainty');
|
|
710
|
+
throw error;
|
|
711
|
+
}
|
|
182
712
|
}
|
|
183
713
|
async function connect() {
|
|
184
|
-
if (closed || discoveryPaused)
|
|
714
|
+
if (closed || discoveryPaused) {
|
|
715
|
+
notifyReady();
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
185
718
|
startTimer();
|
|
719
|
+
const progress = {
|
|
720
|
+
complete: false,
|
|
721
|
+
reachable: true
|
|
722
|
+
};
|
|
723
|
+
let completedAt = new Date().toISOString();
|
|
186
724
|
try {
|
|
187
725
|
const local = await openLocal();
|
|
188
726
|
if (closed) return;
|
|
189
727
|
await ensureAuthorization();
|
|
190
728
|
if (closed) return;
|
|
191
|
-
await cycle(local);
|
|
729
|
+
await cycle(local, progress);
|
|
730
|
+
if (closed) return;
|
|
731
|
+
if (progress.complete) {
|
|
732
|
+
completedAt = new Date().toISOString();
|
|
733
|
+
await local.complete(completedAt);
|
|
734
|
+
}
|
|
192
735
|
} catch {
|
|
736
|
+
progress.complete = false;
|
|
737
|
+
notifyReady();
|
|
193
738
|
return;
|
|
739
|
+
} finally{
|
|
740
|
+
notifyReady();
|
|
741
|
+
moveTo(settled(status, progress, completedAt, pending(), attention()));
|
|
194
742
|
}
|
|
195
743
|
}
|
|
196
744
|
async function run() {
|
|
@@ -219,7 +767,9 @@ export function createMdRuntime(options) {
|
|
|
219
767
|
store?.close();
|
|
220
768
|
roomListeners.clear();
|
|
221
769
|
refusalListeners.clear();
|
|
770
|
+
statusListeners.clear();
|
|
222
771
|
querySubscriptions.clear();
|
|
772
|
+
queueSubscriptions.clear();
|
|
223
773
|
}
|
|
224
774
|
options.signal?.addEventListener('abort', close, {
|
|
225
775
|
once: true
|
|
@@ -234,7 +784,56 @@ export function createMdRuntime(options) {
|
|
|
234
784
|
return visible(request);
|
|
235
785
|
},
|
|
236
786
|
types (locator) {
|
|
237
|
-
return replicas.get(locator)?.collection.types ?? [];
|
|
787
|
+
return replicas.get(locator)?.visible.collection.types ?? [];
|
|
788
|
+
},
|
|
789
|
+
syncStatus () {
|
|
790
|
+
return status;
|
|
791
|
+
},
|
|
792
|
+
draft: {
|
|
793
|
+
read (locator, path) {
|
|
794
|
+
return drafts.get(at(locator, path)) ?? null;
|
|
795
|
+
},
|
|
796
|
+
async replace (locator, path, content) {
|
|
797
|
+
const local = await openLocal();
|
|
798
|
+
await local.keep({
|
|
799
|
+
locator,
|
|
800
|
+
path,
|
|
801
|
+
content
|
|
802
|
+
});
|
|
803
|
+
drafts.set(at(locator, path), content);
|
|
804
|
+
},
|
|
805
|
+
async discard (locator, path) {
|
|
806
|
+
const local = await openLocal();
|
|
807
|
+
await local.discard(locator, path);
|
|
808
|
+
drafts.delete(at(locator, path));
|
|
809
|
+
}
|
|
810
|
+
},
|
|
811
|
+
save (locator, changes, replacing) {
|
|
812
|
+
return exclusive(()=>write(locator, changes, replacing));
|
|
813
|
+
},
|
|
814
|
+
outcome (identifier) {
|
|
815
|
+
const recorded = outcomes.get(identifier);
|
|
816
|
+
if (recorded !== undefined) return recorded;
|
|
817
|
+
for (const replica of replicas.values()){
|
|
818
|
+
if (replica.queue.some((batch)=>batch.id === identifier)) return {
|
|
819
|
+
kind: 'pending'
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
return null;
|
|
823
|
+
},
|
|
824
|
+
queue: {
|
|
825
|
+
discard (locator, identifier) {
|
|
826
|
+
return exclusive(()=>discardHead(locator, identifier));
|
|
827
|
+
}
|
|
828
|
+
},
|
|
829
|
+
history (locator, path, query) {
|
|
830
|
+
return readHistory(locator, path, query);
|
|
831
|
+
},
|
|
832
|
+
call (name, input) {
|
|
833
|
+
return invokeAction(name, input);
|
|
834
|
+
},
|
|
835
|
+
log () {
|
|
836
|
+
return journal.export();
|
|
238
837
|
},
|
|
239
838
|
observe (topic, second, third) {
|
|
240
839
|
if (topic === 'rooms') {
|
|
@@ -253,6 +852,25 @@ export function createMdRuntime(options) {
|
|
|
253
852
|
refusalListeners.delete(notify);
|
|
254
853
|
};
|
|
255
854
|
}
|
|
855
|
+
if (topic === 'sync-status') {
|
|
856
|
+
const notify = second;
|
|
857
|
+
statusListeners.add(notify);
|
|
858
|
+
notify(status);
|
|
859
|
+
return ()=>{
|
|
860
|
+
statusListeners.delete(notify);
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
if (topic === 'queue') {
|
|
864
|
+
const queue = {
|
|
865
|
+
locator: second,
|
|
866
|
+
notify: third
|
|
867
|
+
};
|
|
868
|
+
queueSubscriptions.add(queue);
|
|
869
|
+
queue.notify(entriesOf(queue.locator));
|
|
870
|
+
return ()=>{
|
|
871
|
+
queueSubscriptions.delete(queue);
|
|
872
|
+
};
|
|
873
|
+
}
|
|
256
874
|
const subscription = {
|
|
257
875
|
query: second,
|
|
258
876
|
notify: third
|
|
@@ -266,6 +884,7 @@ export function createMdRuntime(options) {
|
|
|
266
884
|
},
|
|
267
885
|
async reauthorize () {
|
|
268
886
|
if (refusal === null || !refusal.reauthorizationOffered) return;
|
|
887
|
+
journal.record('connection reauthorize');
|
|
269
888
|
authorization = undefined;
|
|
270
889
|
discoveryPaused = false;
|
|
271
890
|
await enqueue();
|