@agentshouse/mdruntime 0.1.0-c5d3fa6fb67bc → 0.1.0-c6636685d0c4b
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 +205 -205
- package/README.md +8 -8
- package/dist/adapter.d.ts +36 -9
- package/dist/adapter.d.ts.map +1 -1
- package/dist/bootstrap.d.ts +2 -1
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +1 -1
- package/dist/change-set.d.ts +5 -2
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +6 -1
- package/dist/chat/earlier.d.ts +10 -0
- package/dist/chat/earlier.d.ts.map +1 -0
- package/dist/chat/earlier.js +54 -0
- package/dist/chat/kit.js +1 -1
- package/dist/chat/list.d.ts +2 -1
- package/dist/chat/list.d.ts.map +1 -1
- package/dist/chat/list.js +45 -5
- package/dist/chat/markdown.d.ts +1 -0
- package/dist/chat/markdown.d.ts.map +1 -1
- package/dist/chat/markdown.js +78 -20
- package/dist/chat/message.d.ts +1 -0
- package/dist/chat/message.d.ts.map +1 -1
- package/dist/chat/message.js +12 -2
- package/dist/chat/messages.d.ts +2 -0
- package/dist/chat/messages.d.ts.map +1 -1
- package/dist/chat/messages.js +52 -20
- package/dist/chat/overlay.d.ts +1 -0
- package/dist/chat/overlay.d.ts.map +1 -1
- package/dist/chat/overlay.js +10 -0
- package/dist/chat/pieces.d.ts +62 -0
- package/dist/chat/pieces.d.ts.map +1 -0
- package/dist/chat/pieces.js +1110 -0
- package/dist/chat/provider.d.ts +1 -0
- package/dist/chat/provider.d.ts.map +1 -1
- package/dist/chat/provider.js +23 -14
- package/dist/chat/question.d.ts +1 -1
- package/dist/chat/question.d.ts.map +1 -1
- package/dist/chat/question.js +7 -18
- package/dist/chat/ready.d.ts +1 -1
- package/dist/chat/ready.d.ts.map +1 -1
- package/dist/chat/ready.js +1 -1
- package/dist/chat/steady.d.ts +3 -0
- package/dist/chat/steady.d.ts.map +1 -0
- package/dist/chat/steady.js +26 -0
- package/dist/chat/style.js +1 -1
- package/dist/chat.d.ts +2 -2
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +2 -2
- package/dist/client.d.ts +6 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +16 -5
- package/dist/collection.d.ts +4 -3
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +128 -63
- package/dist/connection.d.ts +12 -4
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +577 -217
- package/dist/conversation.d.ts +12 -0
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +196 -41
- package/dist/entry.d.ts.map +1 -1
- package/dist/entry.js +2 -1
- package/dist/host.d.ts +5 -4
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +45 -5
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/memory.d.ts +6 -3
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +17 -11
- package/dist/observation.d.ts +2 -0
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +9 -3
- package/dist/outcome.d.ts +14 -2
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +7 -0
- package/dist/overlay.d.ts +4 -4
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +181 -140
- package/dist/query.d.ts +26 -2
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +34 -6
- package/dist/reach.d.ts +4 -1
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +90 -21
- package/dist/react.d.ts +3 -3
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +7 -17
- package/dist/read-side.d.ts +3 -1
- package/dist/read-side.d.ts.map +1 -1
- package/dist/read-side.js +271 -82
- package/dist/reads.d.ts +11 -0
- package/dist/reads.d.ts.map +1 -0
- package/dist/reads.js +98 -0
- package/dist/refresh.d.ts +4 -4
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +35 -16
- package/dist/replica.d.ts +11 -5
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +69 -40
- package/dist/room-draft.d.ts +12 -0
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +11 -12
- package/dist/rows.d.ts +1 -0
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +5 -0
- package/dist/save.d.ts +4 -2
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +195 -111
- package/dist/search.d.ts +17 -2
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +81 -7
- package/dist/seed.d.ts +16 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +75 -0
- package/dist/shared.d.ts +4 -0
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +63 -33
- package/dist/store.d.ts +18 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +398 -316
- package/dist/turn.d.ts +5 -3
- package/dist/turn.d.ts.map +1 -1
- package/dist/turn.js +26 -7
- package/dist/windows.d.ts +26 -0
- package/dist/windows.d.ts.map +1 -0
- package/dist/windows.js +93 -0
- package/dist/wire.d.ts +16 -0
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +4 -0
- package/package.json +2 -2
- package/dist/acquisition.d.ts +0 -14
- package/dist/acquisition.d.ts.map +0 -1
- package/dist/acquisition.js +0 -24
package/dist/connection.js
CHANGED
|
@@ -1,36 +1,47 @@
|
|
|
1
1
|
import { attachedModel } from './attached-model.js';
|
|
2
2
|
import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
|
|
3
|
-
import { observing } from './observation.js';
|
|
4
|
-
import { filterOf, identityOf, planOf, } from './query.js';
|
|
3
|
+
import { observing, whole } from './observation.js';
|
|
4
|
+
import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
|
|
5
5
|
import { createReadSide } from './read-side.js';
|
|
6
|
+
import { seededResult } from './seed.js';
|
|
6
7
|
import { markdownPath, roomOfKey } from './rows.js';
|
|
7
8
|
import { createSearchIndex, searchPlanOf, } from './search.js';
|
|
9
|
+
import { capped, MAXIMUM_WINDOW } from './windows.js';
|
|
8
10
|
import { bootstrapRoom } from './bootstrap.js';
|
|
9
|
-
import { changeSet, entering, mergeChangeSets } from './change-set.js';
|
|
11
|
+
import { changeSet, changing, entering, mergeChangeSets } from './change-set.js';
|
|
10
12
|
import { createConversationStore, } from './conversation.js';
|
|
11
13
|
import { queuedFile } from './change.js';
|
|
12
14
|
import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
|
|
13
15
|
import { runtimeError } from './failure.js';
|
|
14
16
|
import { createLog } from './log.js';
|
|
15
17
|
import { binaryRecord } from './binary.js';
|
|
16
|
-
import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
|
|
18
|
+
import { absentBinary, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, lockedRoom, undeclared, UNCERTAIN, } from './outcome.js';
|
|
17
19
|
import { createRecovery } from './recovery.js';
|
|
20
|
+
import { doorReads } from './reads.js';
|
|
18
21
|
import { refreshRoom } from './refresh.js';
|
|
19
|
-
import { buildBatch, joined, limitedIn, requeued } from './save.js';
|
|
20
|
-
import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership,
|
|
21
|
-
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
22
|
+
import { buildBatch, joined, limitedIn, queuedDocuments, requeued, } from './save.js';
|
|
23
|
+
import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, replicating, sameMembership, standing, } from './replica.js';
|
|
24
|
+
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
22
25
|
import { same, saving, STARTING, transferring, } from './status.js';
|
|
23
26
|
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
24
|
-
import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
|
|
25
27
|
import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
|
|
26
28
|
import { FencedMap } from './fence.js';
|
|
27
29
|
import { tally } from './shared.js';
|
|
28
|
-
import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
|
|
30
|
+
import { inChunks, ROW_WEIGHT, stepped, yielded } from './turn.js';
|
|
29
31
|
import { identityTime, roomDraftRevision } from './identity.js';
|
|
30
32
|
import { beneath, normalize } from './paths.js';
|
|
31
33
|
import { openRuntimeStore, } from './store.js';
|
|
32
34
|
import { unconfirmed } from './transfer.js';
|
|
33
35
|
import { governs, holds, narrowed, planReach, } from './reach.js';
|
|
36
|
+
function roomsAlone(plan) {
|
|
37
|
+
return (plan.reach === null &&
|
|
38
|
+
plan.root.types === null &&
|
|
39
|
+
plan.root.where === null &&
|
|
40
|
+
plan.root.pathPrefix === null &&
|
|
41
|
+
plan.paths === null &&
|
|
42
|
+
plan.hop === null &&
|
|
43
|
+
plan.backlinks === null);
|
|
44
|
+
}
|
|
34
45
|
function within(root, path) {
|
|
35
46
|
return root === '' || beneath(root, path);
|
|
36
47
|
}
|
|
@@ -48,6 +59,12 @@ const OPENING_LANE = 'opening';
|
|
|
48
59
|
const CONVERSATION_LANE = 'conversations';
|
|
49
60
|
const ROOM_READS = 256;
|
|
50
61
|
const ROOM_ATTEMPTS = 16;
|
|
62
|
+
// A Room that locks offline stays readable this long after the connection drops, so a laptop waking or changing network keeps working while a disconnected device locks soon after.
|
|
63
|
+
const LOCK_GRACE_MS = 120_000;
|
|
64
|
+
function operationOf(input) {
|
|
65
|
+
const operation = input?.operation_id;
|
|
66
|
+
return typeof operation === 'string' ? operation : null;
|
|
67
|
+
}
|
|
51
68
|
const outside = new Map();
|
|
52
69
|
function outsideOf(roots) {
|
|
53
70
|
const key = JSON.stringify(roots);
|
|
@@ -87,6 +104,8 @@ export function composeRuntime(options) {
|
|
|
87
104
|
const replicas = new FencedMap(isFenced);
|
|
88
105
|
const queuesWithoutReplica = new FencedMap(isFenced);
|
|
89
106
|
const pendings = new Map();
|
|
107
|
+
const seeds = new Map();
|
|
108
|
+
const seedWatchers = new Map();
|
|
90
109
|
const roomDrafts = new FencedMap(isFenced);
|
|
91
110
|
const presentations = new Map();
|
|
92
111
|
let limits = [];
|
|
@@ -99,6 +118,7 @@ export function composeRuntime(options) {
|
|
|
99
118
|
const liveModels = new FencedMap(isModelFenced);
|
|
100
119
|
const invalidModels = new Set();
|
|
101
120
|
const discovered = new Map();
|
|
121
|
+
const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
|
|
102
122
|
let roomsListed = false;
|
|
103
123
|
const scopedRefusals = new Map();
|
|
104
124
|
const sourceRefusals = new Map();
|
|
@@ -135,6 +155,7 @@ export function composeRuntime(options) {
|
|
|
135
155
|
let handed = false;
|
|
136
156
|
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
137
157
|
const repairing = new Set();
|
|
158
|
+
const calling = new Set();
|
|
138
159
|
const repairable = new Map();
|
|
139
160
|
let outcomeRetention = null;
|
|
140
161
|
const answering = new Map();
|
|
@@ -145,6 +166,7 @@ export function composeRuntime(options) {
|
|
|
145
166
|
let discovering = null;
|
|
146
167
|
let opened = null;
|
|
147
168
|
const owed = new Set();
|
|
169
|
+
const reloadsDue = new Map();
|
|
148
170
|
const lanes = new Map();
|
|
149
171
|
let roomReads = { generation, free: ROOM_READS, waiting: [] };
|
|
150
172
|
const working = new Set();
|
|
@@ -160,6 +182,9 @@ export function composeRuntime(options) {
|
|
|
160
182
|
let withdrawConversations;
|
|
161
183
|
let currencyQueued = null;
|
|
162
184
|
let socketOpen = false;
|
|
185
|
+
const confirmed = new Set();
|
|
186
|
+
let lockGrace = null;
|
|
187
|
+
const lockListeners = new Set();
|
|
163
188
|
let serviceMode = 'open';
|
|
164
189
|
let haltedRefusal = false;
|
|
165
190
|
let openSignals = 0;
|
|
@@ -171,8 +196,6 @@ export function composeRuntime(options) {
|
|
|
171
196
|
const unpreparedDemand = new Set();
|
|
172
197
|
const unindexable = new Set();
|
|
173
198
|
let preparing = false;
|
|
174
|
-
let demand = WHOLE_DEMAND;
|
|
175
|
-
let narrowing = null;
|
|
176
199
|
let targets = {};
|
|
177
200
|
let persistence = options.shell.persistence;
|
|
178
201
|
let capacity = null;
|
|
@@ -189,7 +212,7 @@ export function composeRuntime(options) {
|
|
|
189
212
|
durably,
|
|
190
213
|
stream: () => options.adapter.conversations,
|
|
191
214
|
publish: publishChangeSet,
|
|
192
|
-
online: () =>
|
|
215
|
+
online: () => connectionOf() !== 'unreachable' && refusal === null,
|
|
193
216
|
closed: () => closed,
|
|
194
217
|
unanswered: resendLater,
|
|
195
218
|
expired: (operation) => pastHorizon(operation),
|
|
@@ -255,6 +278,15 @@ export function composeRuntime(options) {
|
|
|
255
278
|
const value = presentations.get(app);
|
|
256
279
|
return value === undefined ? null : structuredClone(value);
|
|
257
280
|
}
|
|
281
|
+
function keepPresentation(app, next) {
|
|
282
|
+
return exclusive(async () => {
|
|
283
|
+
const local = await openLocal();
|
|
284
|
+
const stored = next(presentations.get(app));
|
|
285
|
+
await local.present({ app, value: stored });
|
|
286
|
+
presentations.set(app, stored);
|
|
287
|
+
publishChangeSet(changeSet('', null, null, { presentation: app }));
|
|
288
|
+
});
|
|
289
|
+
}
|
|
258
290
|
function emitPresentation(app) {
|
|
259
291
|
if (closed)
|
|
260
292
|
return;
|
|
@@ -315,9 +347,63 @@ export function composeRuntime(options) {
|
|
|
315
347
|
return structuredClone([...rooms.values()].map((room) => {
|
|
316
348
|
const documents = documentsIn(room.locator);
|
|
317
349
|
counted.set(room.locator, documents);
|
|
318
|
-
return
|
|
350
|
+
return locked(room.locator)
|
|
351
|
+
? {
|
|
352
|
+
...room,
|
|
353
|
+
model: modelTypes(NO_MODEL),
|
|
354
|
+
typedSelection: null,
|
|
355
|
+
documents: 0,
|
|
356
|
+
locked: true,
|
|
357
|
+
}
|
|
358
|
+
: { ...room, documents, locked: false };
|
|
319
359
|
}));
|
|
320
360
|
}
|
|
361
|
+
function locked(locator) {
|
|
362
|
+
return rooms.get(locator)?.replica.offline === 'locked' && !confirmed.has(locator);
|
|
363
|
+
}
|
|
364
|
+
function lockedRooms() {
|
|
365
|
+
return [...rooms.keys()].filter(locked).sort();
|
|
366
|
+
}
|
|
367
|
+
function relocked(before) {
|
|
368
|
+
const after = lockedRooms();
|
|
369
|
+
if (JSON.stringify(after) === JSON.stringify(before))
|
|
370
|
+
return;
|
|
371
|
+
journal.record(`locked ${after.length}`);
|
|
372
|
+
emitRooms();
|
|
373
|
+
for (const notify of [...lockListeners])
|
|
374
|
+
notify();
|
|
375
|
+
}
|
|
376
|
+
function confirmLocks(listed) {
|
|
377
|
+
if (!socketOpen)
|
|
378
|
+
return;
|
|
379
|
+
for (const room of listed) {
|
|
380
|
+
if (room.replica.admission === 'admitted')
|
|
381
|
+
confirmed.add(room.locator);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function connectionLost() {
|
|
385
|
+
if (lockGrace !== null || closed)
|
|
386
|
+
return;
|
|
387
|
+
lockGrace = setTimeout(() => {
|
|
388
|
+
lockGrace = null;
|
|
389
|
+
if (socketOpen || closed)
|
|
390
|
+
return;
|
|
391
|
+
const before = lockedRooms();
|
|
392
|
+
confirmed.clear();
|
|
393
|
+
relocked(before);
|
|
394
|
+
}, LOCK_GRACE_MS);
|
|
395
|
+
}
|
|
396
|
+
function connectionLive() {
|
|
397
|
+
if (lockGrace !== null)
|
|
398
|
+
clearTimeout(lockGrace);
|
|
399
|
+
lockGrace = null;
|
|
400
|
+
}
|
|
401
|
+
function watchLocks(notify) {
|
|
402
|
+
lockListeners.add(notify);
|
|
403
|
+
return () => {
|
|
404
|
+
lockListeners.delete(notify);
|
|
405
|
+
};
|
|
406
|
+
}
|
|
321
407
|
function roomFacts(reach = null) {
|
|
322
408
|
const held = [...rooms.values()]
|
|
323
409
|
.filter((room) => holds(reach, room.locator))
|
|
@@ -366,7 +452,7 @@ export function composeRuntime(options) {
|
|
|
366
452
|
if (closed)
|
|
367
453
|
return;
|
|
368
454
|
queueReadiness();
|
|
369
|
-
const next = status.state === 'synced' &&
|
|
455
|
+
const next = status.state === 'synced' && !conversations.current()
|
|
370
456
|
? { ...status, state: 'syncing' }
|
|
371
457
|
: status;
|
|
372
458
|
if (same(shown, next))
|
|
@@ -399,8 +485,12 @@ export function composeRuntime(options) {
|
|
|
399
485
|
...set,
|
|
400
486
|
documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
|
|
401
487
|
};
|
|
402
|
-
readSide.apply(held);
|
|
403
|
-
|
|
488
|
+
const applied = readSide.apply(held, () => searchIndex.changed(held));
|
|
489
|
+
if (applied instanceof Promise)
|
|
490
|
+
void applied.catch(unpublished);
|
|
491
|
+
}
|
|
492
|
+
function unpublished(error) {
|
|
493
|
+
journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
|
|
404
494
|
}
|
|
405
495
|
async function referenced(locator) {
|
|
406
496
|
for (;;) {
|
|
@@ -431,8 +521,8 @@ export function composeRuntime(options) {
|
|
|
431
521
|
if (current === undefined || closed)
|
|
432
522
|
return;
|
|
433
523
|
const set = entering(locator, current, chunk);
|
|
434
|
-
readSide.apply(set);
|
|
435
524
|
entered.push(set);
|
|
525
|
+
return readSide.apply(set);
|
|
436
526
|
});
|
|
437
527
|
if (!replicas.has(locator) || closed)
|
|
438
528
|
return;
|
|
@@ -474,17 +564,22 @@ export function composeRuntime(options) {
|
|
|
474
564
|
}
|
|
475
565
|
})();
|
|
476
566
|
}
|
|
567
|
+
function roomsOfReach(reach) {
|
|
568
|
+
return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
569
|
+
}
|
|
477
570
|
function reachedRooms(plan) {
|
|
478
|
-
const
|
|
479
|
-
const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
571
|
+
const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
|
|
480
572
|
const named = plan.rooms ??
|
|
481
573
|
(reached === null
|
|
482
574
|
? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
|
|
483
575
|
: [...reached]);
|
|
484
|
-
return [...new Set(named)]
|
|
576
|
+
return [...new Set(named)]
|
|
577
|
+
.filter((locator) => (reached === null || reached.has(locator)) && !plan.locked.includes(locator))
|
|
578
|
+
.sort();
|
|
485
579
|
}
|
|
486
580
|
function awaited(locator) {
|
|
487
|
-
return
|
|
581
|
+
return (!roomsListed ||
|
|
582
|
+
(rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
|
|
488
583
|
}
|
|
489
584
|
function covered(plan) {
|
|
490
585
|
const locators = reachedRooms(plan);
|
|
@@ -524,61 +619,11 @@ export function composeRuntime(options) {
|
|
|
524
619
|
if (waiting.length > 0)
|
|
525
620
|
prepare(waiting.map(({ room }) => room));
|
|
526
621
|
}
|
|
527
|
-
function
|
|
528
|
-
|
|
529
|
-
(replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
|
|
530
|
-
queuesWithoutReplica.has(room.locator));
|
|
531
|
-
}
|
|
532
|
-
function acquiredLocators() {
|
|
533
|
-
return [...discovered.values()]
|
|
534
|
-
.filter(acquires)
|
|
535
|
-
.map((room) => room.locator)
|
|
536
|
-
.sort();
|
|
537
|
-
}
|
|
538
|
-
function heldDemand(acquired) {
|
|
539
|
-
if (demand.whole)
|
|
540
|
-
return null;
|
|
541
|
-
return { rooms: acquired.map((room) => room.locator), conversations: demand.conversations };
|
|
542
|
-
}
|
|
543
|
-
function applyDemand(next) {
|
|
544
|
-
const before = acquiredLocators();
|
|
545
|
-
const conversed = demand.conversations;
|
|
546
|
-
demand = next;
|
|
547
|
-
const after = acquiredLocators();
|
|
548
|
-
queueReadiness();
|
|
549
|
-
if (!conversed && next.conversations)
|
|
550
|
-
scheduleCurrency();
|
|
551
|
-
if (JSON.stringify(before) === JSON.stringify(after) && conversed === next.conversations)
|
|
552
|
-
return;
|
|
553
|
-
journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
|
|
554
|
-
if (options.adapter.roomStream === undefined)
|
|
555
|
-
void sync('unstreamed');
|
|
556
|
-
else if (socketOpen)
|
|
557
|
-
options.adapter.roomStream.reopen();
|
|
558
|
-
}
|
|
559
|
-
function acquire(next) {
|
|
560
|
-
if (closed)
|
|
561
|
-
return;
|
|
562
|
-
if (narrowing !== null)
|
|
563
|
-
clearTimeout(narrowing);
|
|
564
|
-
narrowing = null;
|
|
565
|
-
const widening = wider(next, demand);
|
|
566
|
-
if (!widening && !narrower(next, demand))
|
|
567
|
-
return;
|
|
568
|
-
if (widening || !roomsListed) {
|
|
569
|
-
applyDemand(next);
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
// A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
|
|
573
|
-
narrowing = setTimeout(() => {
|
|
574
|
-
narrowing = null;
|
|
575
|
-
applyDemand(next);
|
|
576
|
-
}, 5_000);
|
|
577
|
-
}
|
|
578
|
-
function requiredRooms(reach) {
|
|
622
|
+
function requiredRooms(given) {
|
|
623
|
+
const reach = planReach(given);
|
|
579
624
|
if (reach === null)
|
|
580
625
|
return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
|
|
581
|
-
const named = [...
|
|
626
|
+
const named = [...roomsOfReach(reach)].sort();
|
|
582
627
|
if (roomsListed)
|
|
583
628
|
return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
|
|
584
629
|
return { listed: named.every((room) => replicas.has(room)), rooms: named };
|
|
@@ -772,10 +817,7 @@ export function composeRuntime(options) {
|
|
|
772
817
|
void sync('unstreamed');
|
|
773
818
|
}
|
|
774
819
|
async function reclaimed(local) {
|
|
775
|
-
const unreferenced = [...transfers.keys()].filter((locator) =>
|
|
776
|
-
const room = discovered.get(locator);
|
|
777
|
-
return room === undefined || !acquires(room);
|
|
778
|
-
});
|
|
820
|
+
const unreferenced = [...transfers.keys()].filter((locator) => !discovered.has(locator));
|
|
779
821
|
for (const locator of unreferenced) {
|
|
780
822
|
await local.abandon(locator);
|
|
781
823
|
transfers.delete(locator);
|
|
@@ -801,18 +843,54 @@ export function composeRuntime(options) {
|
|
|
801
843
|
const available = await estimated().catch(() => null);
|
|
802
844
|
pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
|
|
803
845
|
}
|
|
846
|
+
function seedRoomHeld(room, version) {
|
|
847
|
+
return prepared.has(room) && reachedBy(room, version);
|
|
848
|
+
}
|
|
849
|
+
function seedHeld(at) {
|
|
850
|
+
return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
|
|
851
|
+
}
|
|
852
|
+
function seedApplies(plan) {
|
|
853
|
+
const seed = seeds.get(plan.key);
|
|
854
|
+
if (seed === undefined)
|
|
855
|
+
return null;
|
|
856
|
+
if (!seedHeld(seed.at))
|
|
857
|
+
return seed;
|
|
858
|
+
seeds.delete(plan.key);
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
function seedBehind(plan, cover) {
|
|
862
|
+
const seed = seedApplies(plan);
|
|
863
|
+
if (seed === null || 'pending' in cover)
|
|
864
|
+
return cover;
|
|
865
|
+
return {
|
|
866
|
+
pending: Object.entries(seed.at)
|
|
867
|
+
.filter(([room, version]) => !seedRoomHeld(room, version))
|
|
868
|
+
.map(([room]) => ({ room, awaits: 'acquisition' })),
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
function seed(entry) {
|
|
872
|
+
if (seedHeld(entry.at))
|
|
873
|
+
return;
|
|
874
|
+
for (const { query, rows } of entry.queries) {
|
|
875
|
+
const key = queryKey(query);
|
|
876
|
+
seeds.set(key, { rows, at: entry.at });
|
|
877
|
+
for (const shown of seedWatchers.get(key) ?? [])
|
|
878
|
+
shown();
|
|
879
|
+
}
|
|
880
|
+
}
|
|
804
881
|
async function rowsQuery(plan) {
|
|
805
882
|
const before = covered(plan);
|
|
806
883
|
awaitPreparation(before);
|
|
884
|
+
const seeded = seedApplies(plan);
|
|
885
|
+
if (seeded !== null)
|
|
886
|
+
return seededResult(seeded).result;
|
|
807
887
|
if ('pending' in before)
|
|
808
888
|
return before;
|
|
809
889
|
const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
|
|
810
890
|
awaitPreparation(cover);
|
|
811
891
|
if ('pending' in cover)
|
|
812
892
|
return cover;
|
|
813
|
-
return
|
|
814
|
-
? { groups: rows.groups, coverage: cover.coverage }
|
|
815
|
-
: { documents: rows.documents, coverage: cover.coverage };
|
|
893
|
+
return { ...rows, coverage: cover.coverage };
|
|
816
894
|
}
|
|
817
895
|
async function indexConversation(conversation) {
|
|
818
896
|
searchIndex.indexMessages(conversation, [], false);
|
|
@@ -956,20 +1034,28 @@ export function composeRuntime(options) {
|
|
|
956
1034
|
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
957
1035
|
});
|
|
958
1036
|
}
|
|
959
|
-
async function applyDiscovery(found, started) {
|
|
1037
|
+
async function applyDiscovery(found, started, covered) {
|
|
1038
|
+
const lockedBefore = lockedRooms();
|
|
960
1039
|
for (const room of found) {
|
|
961
1040
|
const fence = fences.get(room.locator);
|
|
962
1041
|
if (fence !== undefined && fence.erased && fence.at < started)
|
|
963
1042
|
fences.delete(room.locator);
|
|
964
1043
|
}
|
|
1044
|
+
await loseRooms(found
|
|
1045
|
+
.filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
|
|
1046
|
+
.map((room) => room.locator));
|
|
965
1047
|
const listed = found.filter((room) => !fences.has(room.locator));
|
|
966
1048
|
const listedLocators = new Set(listed.map((room) => room.locator));
|
|
967
1049
|
for (const locator of listedLocators)
|
|
968
1050
|
scopedRefusals.delete(locator);
|
|
969
|
-
|
|
1051
|
+
if (covered === undefined)
|
|
1052
|
+
discovered.clear();
|
|
1053
|
+
else
|
|
1054
|
+
for (const locator of covered)
|
|
1055
|
+
discovered.delete(locator);
|
|
970
1056
|
for (const room of listed)
|
|
971
1057
|
discovered.set(room.locator, room);
|
|
972
|
-
const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
|
|
1058
|
+
const unlisted = unlistedHeld(new Set(found.map((room) => room.locator))).filter((locator) => covered === undefined || covered.includes(locator));
|
|
973
1059
|
if (options.adapter.exhaustiveDiscovery === true)
|
|
974
1060
|
await loseRooms(unlisted);
|
|
975
1061
|
else
|
|
@@ -982,10 +1068,18 @@ export function composeRuntime(options) {
|
|
|
982
1068
|
}
|
|
983
1069
|
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
984
1070
|
}
|
|
1071
|
+
confirmLocks(listed);
|
|
985
1072
|
roomsListed = true;
|
|
986
1073
|
emitRooms();
|
|
1074
|
+
relocked(lockedBefore);
|
|
987
1075
|
return listed;
|
|
988
1076
|
}
|
|
1077
|
+
function heldLocally(locator) {
|
|
1078
|
+
return (replicas.has(locator) ||
|
|
1079
|
+
roomDrafts.has(locator) ||
|
|
1080
|
+
transfers.has(locator) ||
|
|
1081
|
+
queuesWithoutReplica.has(locator));
|
|
1082
|
+
}
|
|
989
1083
|
function unlistedHeld(listed) {
|
|
990
1084
|
return [
|
|
991
1085
|
...new Set([
|
|
@@ -1015,6 +1109,7 @@ export function composeRuntime(options) {
|
|
|
1015
1109
|
for (const locator of fresh) {
|
|
1016
1110
|
fences.set(locator, { at: discoveries, erased: false });
|
|
1017
1111
|
lastFenced.set(locator, ++fencings);
|
|
1112
|
+
confirmed.delete(locator);
|
|
1018
1113
|
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
1019
1114
|
pendings.delete(batch.id);
|
|
1020
1115
|
replicas.delete(locator);
|
|
@@ -1221,8 +1316,9 @@ export function composeRuntime(options) {
|
|
|
1221
1316
|
}
|
|
1222
1317
|
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
1223
1318
|
const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
1224
|
-
const withdrawn =
|
|
1225
|
-
const
|
|
1319
|
+
const { withdrawn, kept } = await stepped(standing(confirmed, queue, withdrawable, localSaves));
|
|
1320
|
+
const founded = kept.filter((batch) => !queue.includes(batch));
|
|
1321
|
+
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
1226
1322
|
const unqueued = withdrawn.map((batch) => ({
|
|
1227
1323
|
batch,
|
|
1228
1324
|
returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
|
|
@@ -1246,9 +1342,10 @@ export function composeRuntime(options) {
|
|
|
1246
1342
|
});
|
|
1247
1343
|
}
|
|
1248
1344
|
}
|
|
1249
|
-
await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
|
|
1345
|
+
await local.keepAuthored({ locator, queue: kept, founded, roomDrafts: [...written.values()] });
|
|
1346
|
+
const staying = new Set(kept.map((batch) => batch.id));
|
|
1250
1347
|
for (const batch of previousQueue) {
|
|
1251
|
-
if (!
|
|
1348
|
+
if (!staying.has(batch.id))
|
|
1252
1349
|
pendings.delete(batch.id);
|
|
1253
1350
|
}
|
|
1254
1351
|
for (const record of written.values())
|
|
@@ -1261,34 +1358,38 @@ export function composeRuntime(options) {
|
|
|
1261
1358
|
});
|
|
1262
1359
|
}
|
|
1263
1360
|
catch (error) {
|
|
1264
|
-
const held = before === undefined ? null :
|
|
1361
|
+
const held = before === undefined ? null : await stepped(replicating(before.confirmed, kept, before));
|
|
1362
|
+
const set = await stepped(changing(locator, before ?? null, held, {
|
|
1363
|
+
queue: queueChanged,
|
|
1364
|
+
drafts: [...written.keys()],
|
|
1365
|
+
}));
|
|
1265
1366
|
if (held !== null)
|
|
1266
1367
|
replicas.set(locator, held);
|
|
1267
1368
|
else if (kept.length > 0)
|
|
1268
1369
|
queuesWithoutReplica.set(locator, kept);
|
|
1269
1370
|
else
|
|
1270
1371
|
queuesWithoutReplica.delete(locator);
|
|
1271
|
-
publishChangeSet(
|
|
1272
|
-
queue: kept.length !== previousQueue.length,
|
|
1273
|
-
drafts: [...written.keys()],
|
|
1274
|
-
}));
|
|
1372
|
+
publishChangeSet(set);
|
|
1275
1373
|
moveTo({ ...status, ...attention() });
|
|
1276
1374
|
throw error;
|
|
1277
1375
|
}
|
|
1376
|
+
const changed = { room: settling.room, queue: queueChanged, drafts: [...written.keys()] };
|
|
1377
|
+
const next = await stepped(replicating(confirmed, kept, before));
|
|
1378
|
+
const set = await stepped(changing(locator, before ?? null, next, changed));
|
|
1278
1379
|
return () => {
|
|
1279
1380
|
queuesWithoutReplica.delete(locator);
|
|
1280
1381
|
for (const { batch, returned } of unqueued) {
|
|
1281
1382
|
const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
|
|
1282
1383
|
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
1283
1384
|
}
|
|
1284
|
-
replicas.set(locator,
|
|
1285
|
-
return
|
|
1286
|
-
room: settling.room,
|
|
1287
|
-
queue: kept.length !== previousQueue.length,
|
|
1288
|
-
drafts: [...written.keys()],
|
|
1289
|
-
});
|
|
1385
|
+
replicas.set(locator, next);
|
|
1386
|
+
return heldChange(locator, before ?? null, next, set, changed);
|
|
1290
1387
|
};
|
|
1291
1388
|
}
|
|
1389
|
+
function heldChange(locator, before, next, set, changed) {
|
|
1390
|
+
const held = replicas.get(locator) ?? null;
|
|
1391
|
+
return held === next ? set : changeSet(locator, before, held, changed);
|
|
1392
|
+
}
|
|
1292
1393
|
function adoptModels(locator, record) {
|
|
1293
1394
|
for (const [attached, model] of [
|
|
1294
1395
|
['collection', record.model],
|
|
@@ -1376,7 +1477,7 @@ export function composeRuntime(options) {
|
|
|
1376
1477
|
return true;
|
|
1377
1478
|
});
|
|
1378
1479
|
}
|
|
1379
|
-
async function
|
|
1480
|
+
async function savedRoom(locator) {
|
|
1380
1481
|
const local = await openLocal();
|
|
1381
1482
|
await referenced(locator);
|
|
1382
1483
|
const replica = replicas.get(locator);
|
|
@@ -1384,6 +1485,10 @@ export function composeRuntime(options) {
|
|
|
1384
1485
|
if (replica === undefined || room === undefined) {
|
|
1385
1486
|
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1386
1487
|
}
|
|
1488
|
+
return { local, replica, room };
|
|
1489
|
+
}
|
|
1490
|
+
async function saveRoomDrafts(locator, named, writtenBy) {
|
|
1491
|
+
const target = await savedRoom(locator);
|
|
1387
1492
|
const saved = [];
|
|
1388
1493
|
for (const { path, revision } of named) {
|
|
1389
1494
|
const record = roomDrafts.get(locator)?.get(path);
|
|
@@ -1393,6 +1498,38 @@ export function composeRuntime(options) {
|
|
|
1393
1498
|
if (!saved.some((draft) => draft.path === path))
|
|
1394
1499
|
saved.push({ path, parts: record.parts });
|
|
1395
1500
|
}
|
|
1501
|
+
return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
|
|
1502
|
+
}
|
|
1503
|
+
async function saveParts(locator, named, writtenBy) {
|
|
1504
|
+
const read = named.map(({ path, parts }) => ({ path, parts: readParts(parts) }));
|
|
1505
|
+
if (read.some(({ path }, at) => path === '' ||
|
|
1506
|
+
normalize(path) !== path ||
|
|
1507
|
+
read.findIndex((one) => one.path === path) !== at)) {
|
|
1508
|
+
throw runtimeError('room-draft-malformed');
|
|
1509
|
+
}
|
|
1510
|
+
const target = await savedRoom(locator);
|
|
1511
|
+
const saved = [];
|
|
1512
|
+
for (const { path, parts } of read) {
|
|
1513
|
+
const file = parts.file === undefined
|
|
1514
|
+
? undefined
|
|
1515
|
+
: await heldFile(parts.file.blob, binaryBase(target.replica.visible, path));
|
|
1516
|
+
saved.push({ path, parts: held(undefined, parts, file) });
|
|
1517
|
+
}
|
|
1518
|
+
return committed(locator, target, saved, new Set(), writtenBy);
|
|
1519
|
+
}
|
|
1520
|
+
function carriedDrafts(locator, saved, consumed) {
|
|
1521
|
+
const holding = roomDrafts.get(locator);
|
|
1522
|
+
return new Map(saved.flatMap(({ path, parts }) => {
|
|
1523
|
+
const record = holding?.get(path);
|
|
1524
|
+
if (parts.move === undefined || record === undefined || consumed.has(path))
|
|
1525
|
+
return [];
|
|
1526
|
+
const to = moveTarget(parts.move);
|
|
1527
|
+
if (to === path || holding?.has(to) === true)
|
|
1528
|
+
return [];
|
|
1529
|
+
return [[path, { ...record, path: to, revision: roomDraftRevision() }]];
|
|
1530
|
+
}));
|
|
1531
|
+
}
|
|
1532
|
+
async function committed(locator, { local, replica, room }, saved, consumed, writtenBy) {
|
|
1396
1533
|
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
|
|
1397
1534
|
if (changed.length > 0)
|
|
1398
1535
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
@@ -1425,6 +1562,7 @@ export function composeRuntime(options) {
|
|
|
1425
1562
|
visible: replica.visible,
|
|
1426
1563
|
changes,
|
|
1427
1564
|
bases: parts.flatMap(({ bases }) => bases),
|
|
1565
|
+
queued: queuedDocuments(replica.queue),
|
|
1428
1566
|
writtenBy,
|
|
1429
1567
|
clean: {
|
|
1430
1568
|
text: replica.confirmed.record.documents,
|
|
@@ -1432,7 +1570,9 @@ export function composeRuntime(options) {
|
|
|
1432
1570
|
composition: compositionOf(replica.confirmed.record),
|
|
1433
1571
|
},
|
|
1434
1572
|
};
|
|
1435
|
-
const checked = withdrawing
|
|
1573
|
+
const checked = withdrawing
|
|
1574
|
+
? await buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
|
|
1575
|
+
: null;
|
|
1436
1576
|
if (checked?.kind === 'refused')
|
|
1437
1577
|
return { outcome: 'refused', ...checked.refusal };
|
|
1438
1578
|
const owners = [...requeue.owners].flatMap((origin) => {
|
|
@@ -1448,13 +1588,15 @@ export function composeRuntime(options) {
|
|
|
1448
1588
|
};
|
|
1449
1589
|
}
|
|
1450
1590
|
const built = !withdrawing
|
|
1451
|
-
? buildBatch(input)
|
|
1591
|
+
? await buildBatch(input)
|
|
1452
1592
|
: requeue.changes.length === 0
|
|
1453
1593
|
? null
|
|
1454
|
-
: buildBatch({
|
|
1594
|
+
: await buildBatch({
|
|
1455
1595
|
...input,
|
|
1456
|
-
visible:
|
|
1596
|
+
visible: (await stepped(replicating(replica.confirmed, requeue.queue, replica)))
|
|
1597
|
+
.visible,
|
|
1457
1598
|
changes: requeue.changes,
|
|
1599
|
+
queued: queuedDocuments(requeue.queue),
|
|
1458
1600
|
});
|
|
1459
1601
|
if (built?.kind === 'refused')
|
|
1460
1602
|
return { outcome: 'refused', ...built.refusal };
|
|
@@ -1489,18 +1631,26 @@ export function composeRuntime(options) {
|
|
|
1489
1631
|
if (limited !== null)
|
|
1490
1632
|
return { outcome: 'refused', ...limited };
|
|
1491
1633
|
}
|
|
1492
|
-
const
|
|
1634
|
+
const carried = carriedDrafts(locator, saved, consumed);
|
|
1635
|
+
const left = [...consumed, ...carried.keys()];
|
|
1636
|
+
const arrived = [...carried.values()];
|
|
1493
1637
|
try {
|
|
1494
1638
|
await local.keepAuthored({
|
|
1495
1639
|
locator,
|
|
1496
1640
|
queue: batches,
|
|
1497
1641
|
saved: drafted,
|
|
1498
|
-
consumedRoomDrafts:
|
|
1642
|
+
consumedRoomDrafts: left,
|
|
1643
|
+
roomDrafts: arrived,
|
|
1499
1644
|
});
|
|
1500
1645
|
}
|
|
1501
1646
|
catch {
|
|
1502
1647
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
1503
1648
|
}
|
|
1649
|
+
const next = await stepped(replicating(replica.confirmed, batches, replica));
|
|
1650
|
+
const set = await stepped(changing(locator, replica, next, {
|
|
1651
|
+
queue: true,
|
|
1652
|
+
drafts: [...left, ...arrived.map(({ path }) => path)],
|
|
1653
|
+
}));
|
|
1504
1654
|
if (fences.has(locator))
|
|
1505
1655
|
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1506
1656
|
for (const origin of withdrawn.keys())
|
|
@@ -1510,13 +1660,12 @@ export function composeRuntime(options) {
|
|
|
1510
1660
|
}
|
|
1511
1661
|
if (!localSaves)
|
|
1512
1662
|
recorded = false;
|
|
1513
|
-
for (const path of
|
|
1663
|
+
for (const path of left)
|
|
1514
1664
|
forgetRoomDraft(locator, path);
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
}));
|
|
1665
|
+
for (const record of arrived)
|
|
1666
|
+
rememberRoomDraft(record);
|
|
1667
|
+
replicas.set(locator, next);
|
|
1668
|
+
publishChangeSet(set);
|
|
1520
1669
|
moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
|
|
1521
1670
|
submitLater(locator);
|
|
1522
1671
|
const identifier = join?.batch.id ??
|
|
@@ -1606,7 +1755,7 @@ export function composeRuntime(options) {
|
|
|
1606
1755
|
const scope = options.adapter.referrerScope(request.referrer);
|
|
1607
1756
|
return scope?.kind === 'room' ? scope.locator : null;
|
|
1608
1757
|
}
|
|
1609
|
-
async function
|
|
1758
|
+
async function transferBinary(request, transfer, answer) {
|
|
1610
1759
|
if (closed || refusal !== null)
|
|
1611
1760
|
return UNCERTAIN;
|
|
1612
1761
|
try {
|
|
@@ -1622,40 +1771,41 @@ export function composeRuntime(options) {
|
|
|
1622
1771
|
? `transfer binary ${request.locator} ${request.path}`
|
|
1623
1772
|
: `transfer binary ${request.fileRef}`);
|
|
1624
1773
|
try {
|
|
1625
|
-
const result = await
|
|
1626
|
-
|
|
1774
|
+
const result = await transfer(authorization);
|
|
1775
|
+
const settled = result;
|
|
1776
|
+
journal.record(`outcome binary ${settled.kind}`);
|
|
1627
1777
|
const locator = binaryRoom(request);
|
|
1628
1778
|
if (locator !== null && fencedSince(locator, since))
|
|
1629
1779
|
return failure({ kind: 'room-not-readable', locator });
|
|
1630
|
-
if (
|
|
1631
|
-
const room = replicas.get(
|
|
1780
|
+
if (settled.kind === 'scoped-refusal') {
|
|
1781
|
+
const room = replicas.get(settled.locator)?.confirmed.record;
|
|
1632
1782
|
if (room !== undefined)
|
|
1633
|
-
await scoped(
|
|
1783
|
+
await scoped(settled, room, await openLocal());
|
|
1634
1784
|
}
|
|
1635
|
-
if (
|
|
1785
|
+
if (settled.kind === 'revoked-authorization')
|
|
1636
1786
|
await refuse('revoked-authorization');
|
|
1637
|
-
return
|
|
1787
|
+
return answer(result);
|
|
1638
1788
|
}
|
|
1639
1789
|
catch {
|
|
1640
1790
|
journal.record('outcome binary transport-uncertainty');
|
|
1641
1791
|
return UNCERTAIN;
|
|
1642
1792
|
}
|
|
1643
1793
|
}
|
|
1644
|
-
|
|
1645
|
-
|
|
1794
|
+
function readBinary(request) {
|
|
1795
|
+
return transferBinary(request, (held) => options.adapter.binary(held, request), binaryAnswer);
|
|
1796
|
+
}
|
|
1797
|
+
function binaryPath(locator, path, revision) {
|
|
1646
1798
|
if (revision !== undefined)
|
|
1647
|
-
return
|
|
1799
|
+
return { kind: 'path', locator, path, revision };
|
|
1648
1800
|
const visible = replicas.get(locator)?.visible;
|
|
1649
1801
|
const held = visible?.binaries[path];
|
|
1650
1802
|
if (visible === undefined || held === undefined)
|
|
1651
|
-
return
|
|
1652
|
-
if (held.state === 'pending')
|
|
1653
|
-
|
|
1654
|
-
return blob === null ? absentBinary(path) : { kind: 'binary', blob };
|
|
1655
|
-
}
|
|
1803
|
+
return null;
|
|
1804
|
+
if (held.state === 'pending')
|
|
1805
|
+
return { kind: 'pending', localId: held.localId };
|
|
1656
1806
|
const record = binaryRecord(path, held, visible.revisions[path]);
|
|
1657
1807
|
if (record.state !== 'accepted')
|
|
1658
|
-
return
|
|
1808
|
+
return null;
|
|
1659
1809
|
const replica = replicas.get(locator);
|
|
1660
1810
|
const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
|
|
1661
1811
|
const holds = (at) => {
|
|
@@ -1670,13 +1820,31 @@ export function composeRuntime(options) {
|
|
|
1670
1820
|
if (change.kind === 'move' && change.to === confirmed)
|
|
1671
1821
|
confirmed = change.path;
|
|
1672
1822
|
}
|
|
1673
|
-
return
|
|
1823
|
+
return {
|
|
1674
1824
|
kind: 'path',
|
|
1675
1825
|
locator,
|
|
1676
1826
|
path: confirmed,
|
|
1677
1827
|
revision: record.revision,
|
|
1678
1828
|
fileRef: record.fileRef,
|
|
1679
|
-
}
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
async function readBinaryPath(locator, path, revision) {
|
|
1832
|
+
await ready;
|
|
1833
|
+
const asked = binaryPath(locator, path, revision);
|
|
1834
|
+
if (asked === null)
|
|
1835
|
+
return absentBinary(path);
|
|
1836
|
+
if (asked.kind === 'pending') {
|
|
1837
|
+
const blob = queuedBlob(locator, path, asked.localId);
|
|
1838
|
+
return blob === null ? absentBinary(path) : { kind: 'binary', blob };
|
|
1839
|
+
}
|
|
1840
|
+
return readBinary(asked);
|
|
1841
|
+
}
|
|
1842
|
+
async function linkBinaryPath(locator, path, revision) {
|
|
1843
|
+
await ready;
|
|
1844
|
+
const asked = binaryPath(locator, path, revision);
|
|
1845
|
+
if (asked === null || asked.kind === 'pending')
|
|
1846
|
+
return absentBinary(path);
|
|
1847
|
+
return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
|
|
1680
1848
|
}
|
|
1681
1849
|
async function readBinaryReference(reference) {
|
|
1682
1850
|
await ready;
|
|
@@ -1702,7 +1870,14 @@ export function composeRuntime(options) {
|
|
|
1702
1870
|
}
|
|
1703
1871
|
return undefined;
|
|
1704
1872
|
}
|
|
1705
|
-
|
|
1873
|
+
function calledRoom(name, input) {
|
|
1874
|
+
const member = declaredAction(name)?.room;
|
|
1875
|
+
if (member === undefined || input === null || typeof input !== 'object')
|
|
1876
|
+
return null;
|
|
1877
|
+
const room = input[member];
|
|
1878
|
+
return typeof room === 'string' ? room : null;
|
|
1879
|
+
}
|
|
1880
|
+
async function invokeAction(name, input, dispatching) {
|
|
1706
1881
|
await ready;
|
|
1707
1882
|
if (closed || refusal !== null)
|
|
1708
1883
|
return UNCERTAIN;
|
|
@@ -1716,6 +1891,7 @@ export function composeRuntime(options) {
|
|
|
1716
1891
|
}
|
|
1717
1892
|
if (closed || authorization === undefined || refusal !== null)
|
|
1718
1893
|
return UNCERTAIN;
|
|
1894
|
+
await dispatching?.();
|
|
1719
1895
|
journal.record(`transfer action ${name}`);
|
|
1720
1896
|
try {
|
|
1721
1897
|
const result = await options.adapter.action(authorization, { name, input });
|
|
@@ -1736,6 +1912,60 @@ export function composeRuntime(options) {
|
|
|
1736
1912
|
return UNCERTAIN;
|
|
1737
1913
|
}
|
|
1738
1914
|
}
|
|
1915
|
+
function unsettled(result) {
|
|
1916
|
+
return result.kind !== 'action' && result.retryable;
|
|
1917
|
+
}
|
|
1918
|
+
async function recoveredAction(name, input) {
|
|
1919
|
+
const operation = operationOf(input);
|
|
1920
|
+
if (operation === null)
|
|
1921
|
+
return invokeAction(name, input);
|
|
1922
|
+
const kept = async () => (await openLocal()).keepCall({ operation, name, input });
|
|
1923
|
+
calling.add(operation);
|
|
1924
|
+
try {
|
|
1925
|
+
let result = await invokeAction(name, input, kept);
|
|
1926
|
+
if (unsettled(result)) {
|
|
1927
|
+
await recovery.attempt(`action ${operation}`, async () => {
|
|
1928
|
+
if (closed || refusal !== null)
|
|
1929
|
+
return 'done';
|
|
1930
|
+
if (!socketOpen)
|
|
1931
|
+
return 'again';
|
|
1932
|
+
result = await invokeAction(name, input, kept);
|
|
1933
|
+
return unsettled(result) ? 'again' : 'done';
|
|
1934
|
+
}, { again: true });
|
|
1935
|
+
}
|
|
1936
|
+
if (!unsettled(result))
|
|
1937
|
+
await (await openLocal()).dropCall(operation);
|
|
1938
|
+
return result;
|
|
1939
|
+
}
|
|
1940
|
+
finally {
|
|
1941
|
+
calling.delete(operation);
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
async function resendCalls(signal) {
|
|
1945
|
+
if (!socketOpen || closed || stopping)
|
|
1946
|
+
return 'done';
|
|
1947
|
+
const local = await openLocal();
|
|
1948
|
+
for (const held of await local.calls()) {
|
|
1949
|
+
if (calling.has(held.operation))
|
|
1950
|
+
continue;
|
|
1951
|
+
if (closed || signal.aborted)
|
|
1952
|
+
return 'done';
|
|
1953
|
+
if (pastHorizon(held.operation)) {
|
|
1954
|
+
await local.dropCall(held.operation);
|
|
1955
|
+
continue;
|
|
1956
|
+
}
|
|
1957
|
+
calling.add(held.operation);
|
|
1958
|
+
try {
|
|
1959
|
+
if (unsettled(await invokeAction(held.name, held.input)))
|
|
1960
|
+
return 'again';
|
|
1961
|
+
await local.dropCall(held.operation);
|
|
1962
|
+
}
|
|
1963
|
+
finally {
|
|
1964
|
+
calling.delete(held.operation);
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
return 'done';
|
|
1968
|
+
}
|
|
1739
1969
|
function continuing() {
|
|
1740
1970
|
return !closed && !discoveryPaused;
|
|
1741
1971
|
}
|
|
@@ -1753,8 +1983,9 @@ export function composeRuntime(options) {
|
|
|
1753
1983
|
async function pullRefresh(authorized, confirmed, live) {
|
|
1754
1984
|
if (!live())
|
|
1755
1985
|
return unconfirmed;
|
|
1986
|
+
const turn = generation;
|
|
1756
1987
|
try {
|
|
1757
|
-
const result = await refreshRoom(
|
|
1988
|
+
const result = await refreshRoom(reads, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
|
|
1758
1989
|
const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
|
|
1759
1990
|
journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
|
|
1760
1991
|
return result;
|
|
@@ -1763,6 +1994,9 @@ export function composeRuntime(options) {
|
|
|
1763
1994
|
journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
|
|
1764
1995
|
throw error;
|
|
1765
1996
|
}
|
|
1997
|
+
finally {
|
|
1998
|
+
reads.finished(confirmed.record.locator, turn);
|
|
1999
|
+
}
|
|
1766
2000
|
}
|
|
1767
2001
|
function staging(local, live) {
|
|
1768
2002
|
return {
|
|
@@ -1805,12 +2039,13 @@ export function composeRuntime(options) {
|
|
|
1805
2039
|
async function pullBootstrap(authorized, room, local, live) {
|
|
1806
2040
|
if (!live())
|
|
1807
2041
|
return unconfirmed;
|
|
2042
|
+
const turn = generation;
|
|
1808
2043
|
if (replicas.has(room.locator)) {
|
|
1809
2044
|
catching.add(room.locator);
|
|
1810
2045
|
readSide.recover();
|
|
1811
2046
|
}
|
|
1812
2047
|
try {
|
|
1813
|
-
const result = await bootstrapRoom(
|
|
2048
|
+
const result = await bootstrapRoom(reads, authorized, room, {
|
|
1814
2049
|
collection: modelOf(room.locator),
|
|
1815
2050
|
selection: modelOf(room.locator, 'typed-selection'),
|
|
1816
2051
|
}, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
|
|
@@ -1825,12 +2060,28 @@ export function composeRuntime(options) {
|
|
|
1825
2060
|
caughtUpTo(room.locator);
|
|
1826
2061
|
throw error;
|
|
1827
2062
|
}
|
|
2063
|
+
finally {
|
|
2064
|
+
reads.finished(room.locator, turn);
|
|
2065
|
+
}
|
|
1828
2066
|
}
|
|
1829
2067
|
function caughtUpTo(locator) {
|
|
1830
2068
|
if (catching.delete(locator))
|
|
1831
2069
|
readSide.recover();
|
|
1832
2070
|
}
|
|
2071
|
+
function behind(room) {
|
|
2072
|
+
const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
2073
|
+
return (held !== undefined &&
|
|
2074
|
+
[announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
|
|
2075
|
+
}
|
|
1833
2076
|
async function advance(room, local, authorized, live) {
|
|
2077
|
+
const carried = reads.carried(room.locator);
|
|
2078
|
+
const withheld = await advanceOnce(room, local, authorized, live);
|
|
2079
|
+
if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
|
|
2080
|
+
return withheld;
|
|
2081
|
+
}
|
|
2082
|
+
return advanceOnce(room, local, authorized, live);
|
|
2083
|
+
}
|
|
2084
|
+
async function advanceOnce(room, local, authorized, live) {
|
|
1834
2085
|
await referenced(room.locator);
|
|
1835
2086
|
if (!live() || paused.has(room.locator))
|
|
1836
2087
|
return unconfirmed;
|
|
@@ -1867,6 +2118,8 @@ export function composeRuntime(options) {
|
|
|
1867
2118
|
readSide.recover();
|
|
1868
2119
|
return unconfirmed;
|
|
1869
2120
|
}
|
|
2121
|
+
if (reloadsDue.get(room.locator) === generation)
|
|
2122
|
+
return unconfirmed;
|
|
1870
2123
|
}
|
|
1871
2124
|
const bootstrapped = await pullBootstrap(authorized, room, local, live);
|
|
1872
2125
|
if (!live())
|
|
@@ -1958,7 +2211,7 @@ export function composeRuntime(options) {
|
|
|
1958
2211
|
logAccepted(room.locator, submitted, accepted);
|
|
1959
2212
|
const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
1960
2213
|
? { kind: 'staged', confirmed: current.confirmed }
|
|
1961
|
-
: applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
2214
|
+
: await applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
1962
2215
|
? accepted.roomVersion
|
|
1963
2216
|
: held, accepted.roomVersion);
|
|
1964
2217
|
if (applied.kind !== 'staged') {
|
|
@@ -2220,6 +2473,7 @@ export function composeRuntime(options) {
|
|
|
2220
2473
|
if (!continuing())
|
|
2221
2474
|
return;
|
|
2222
2475
|
const held = replicas.get(room.locator);
|
|
2476
|
+
let changed = null;
|
|
2223
2477
|
if (held !== undefined) {
|
|
2224
2478
|
const declared = {
|
|
2225
2479
|
...held.confirmed.record,
|
|
@@ -2230,7 +2484,7 @@ export function composeRuntime(options) {
|
|
|
2230
2484
|
};
|
|
2231
2485
|
const reclassified = sameMembership(declared, held.confirmed.record)
|
|
2232
2486
|
? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
|
|
2233
|
-
: readReclassified(declared, held.confirmed);
|
|
2487
|
+
: await readReclassified(declared, held.confirmed);
|
|
2234
2488
|
const record = reclassified.kind === 'staged'
|
|
2235
2489
|
? declared
|
|
2236
2490
|
: {
|
|
@@ -2241,15 +2495,19 @@ export function composeRuntime(options) {
|
|
|
2241
2495
|
? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
|
|
2242
2496
|
: reclassified;
|
|
2243
2497
|
await local.write({ next: record, previous: held.confirmed.record });
|
|
2498
|
+
const replica = read.kind === 'staged'
|
|
2499
|
+
? await stepped(replicating(read.confirmed, held.queue, held))
|
|
2500
|
+
: { ...held, confirmed: { ...held.confirmed, record } };
|
|
2501
|
+
const set = await stepped(changing(room.locator, held, replica, { room: true }));
|
|
2502
|
+
replicas.set(room.locator, replica);
|
|
2503
|
+
changed = heldChange(room.locator, held, replica, set, { room: true });
|
|
2244
2504
|
if (read.kind === 'staged') {
|
|
2245
|
-
replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
|
|
2246
2505
|
if (declarationRefusals.delete(room.locator)) {
|
|
2247
2506
|
sourceRefusals.delete(room.locator);
|
|
2248
2507
|
unreadable.delete(room.locator);
|
|
2249
2508
|
}
|
|
2250
2509
|
}
|
|
2251
|
-
else
|
|
2252
|
-
replicas.set(room.locator, { ...held, confirmed: { ...held.confirmed, record } });
|
|
2510
|
+
else {
|
|
2253
2511
|
refusedSource(room, read);
|
|
2254
2512
|
declarationRefusals.add(room.locator);
|
|
2255
2513
|
}
|
|
@@ -2260,13 +2518,18 @@ export function composeRuntime(options) {
|
|
|
2260
2518
|
invalidModels.delete(key);
|
|
2261
2519
|
}
|
|
2262
2520
|
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
2263
|
-
if (
|
|
2521
|
+
if (changed === null)
|
|
2264
2522
|
emitRooms();
|
|
2265
2523
|
else
|
|
2266
|
-
publishChangeSet(
|
|
2524
|
+
publishChangeSet(changed);
|
|
2267
2525
|
});
|
|
2268
2526
|
}
|
|
2269
2527
|
async function scoped(result, room, local) {
|
|
2528
|
+
if (result.scope === 'content' && !result.retryable) {
|
|
2529
|
+
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
2530
|
+
await loseRooms([room.locator]);
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2270
2533
|
scopedRefusals.set(room.locator, {
|
|
2271
2534
|
code: result.code,
|
|
2272
2535
|
fields: result.fields,
|
|
@@ -2280,15 +2543,7 @@ export function composeRuntime(options) {
|
|
|
2280
2543
|
});
|
|
2281
2544
|
else if (result.scope !== 'operation') {
|
|
2282
2545
|
unreadable.add(room.locator);
|
|
2283
|
-
scopedAway = {
|
|
2284
|
-
...room,
|
|
2285
|
-
replica: { ...room.replica, admission: 'unavailable' },
|
|
2286
|
-
operations: {
|
|
2287
|
-
...room.operations,
|
|
2288
|
-
...(result.scope === 'content' ? { read: [] } : {}),
|
|
2289
|
-
...(result.scope === 'sync' ? { sync: [] } : {}),
|
|
2290
|
-
},
|
|
2291
|
-
};
|
|
2546
|
+
scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
|
|
2292
2547
|
await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
|
|
2293
2548
|
}
|
|
2294
2549
|
if (!continuing())
|
|
@@ -2313,16 +2568,15 @@ export function composeRuntime(options) {
|
|
|
2313
2568
|
});
|
|
2314
2569
|
}
|
|
2315
2570
|
async function synchronize(listed, local, progress, attempt, live) {
|
|
2316
|
-
const
|
|
2317
|
-
const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
|
|
2571
|
+
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
|
|
2318
2572
|
if (!live())
|
|
2319
2573
|
return;
|
|
2320
|
-
const ready =
|
|
2574
|
+
const ready = listed.filter((_, index) => {
|
|
2321
2575
|
const outcome = prepared[index];
|
|
2322
2576
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
2323
2577
|
});
|
|
2324
2578
|
if (attempt === 'opened') {
|
|
2325
|
-
options.adapter.roomStream?.hold(heldRooms(ready)
|
|
2579
|
+
options.adapter.roomStream?.hold(heldRooms(ready));
|
|
2326
2580
|
handed = true;
|
|
2327
2581
|
}
|
|
2328
2582
|
discovering?.release();
|
|
@@ -2350,19 +2604,10 @@ export function composeRuntime(options) {
|
|
|
2350
2604
|
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
2351
2605
|
if (discoveryPaused || authorization === undefined)
|
|
2352
2606
|
return false;
|
|
2353
|
-
|
|
2354
|
-
if (replicas.has(room.locator))
|
|
2355
|
-
progress.complete = false;
|
|
2356
|
-
return false;
|
|
2357
|
-
}
|
|
2358
|
-
return true;
|
|
2607
|
+
return room.replica.admission === 'admitted';
|
|
2359
2608
|
}
|
|
2360
2609
|
async function caughtUp(room, local, progress, live) {
|
|
2361
|
-
|
|
2362
|
-
if (held === undefined || authorization === undefined || !live())
|
|
2363
|
-
return;
|
|
2364
|
-
const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
|
|
2365
|
-
if (!behind)
|
|
2610
|
+
if (authorization === undefined || !live() || !behind(room))
|
|
2366
2611
|
return;
|
|
2367
2612
|
const withheld = await advance(room, local, authorization, live);
|
|
2368
2613
|
if (withheld !== null)
|
|
@@ -2390,6 +2635,8 @@ export function composeRuntime(options) {
|
|
|
2390
2635
|
}
|
|
2391
2636
|
async function reload(room, local, progress, live) {
|
|
2392
2637
|
try {
|
|
2638
|
+
if (live())
|
|
2639
|
+
reloadsDue.delete(room.locator);
|
|
2393
2640
|
if (!live() || authorization === undefined)
|
|
2394
2641
|
return;
|
|
2395
2642
|
const bootstrapped = await pullBootstrap(authorization, room, local, live);
|
|
@@ -2406,7 +2653,7 @@ export function composeRuntime(options) {
|
|
|
2406
2653
|
caughtUpTo(room.locator);
|
|
2407
2654
|
}
|
|
2408
2655
|
}
|
|
2409
|
-
async function discoverRooms(local, progress, attempt, live) {
|
|
2656
|
+
async function discoverRooms(local, progress, attempt, live, scope) {
|
|
2410
2657
|
if (closed || authorization === undefined) {
|
|
2411
2658
|
notifyReady();
|
|
2412
2659
|
return;
|
|
@@ -2416,7 +2663,7 @@ export function composeRuntime(options) {
|
|
|
2416
2663
|
const started = discoveries;
|
|
2417
2664
|
let result;
|
|
2418
2665
|
try {
|
|
2419
|
-
result = await options.adapter.discover(authorization);
|
|
2666
|
+
result = await options.adapter.discover(authorization, scope);
|
|
2420
2667
|
}
|
|
2421
2668
|
catch {
|
|
2422
2669
|
result = { kind: 'transport-uncertainty' };
|
|
@@ -2456,12 +2703,13 @@ export function composeRuntime(options) {
|
|
|
2456
2703
|
}
|
|
2457
2704
|
for (const room of result.rooms) {
|
|
2458
2705
|
if (room.operations === undefined ||
|
|
2459
|
-
!['
|
|
2706
|
+
!['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
2460
2707
|
room.operations[operation].every((scope) => typeof scope === 'string')) ||
|
|
2461
2708
|
room.replica === undefined ||
|
|
2462
2709
|
typeof room.replica.selection !== 'string' ||
|
|
2463
2710
|
room.replica.selection.length === 0 ||
|
|
2464
|
-
!['admitted', 'unavailable'].includes(room.replica.admission)
|
|
2711
|
+
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
2712
|
+
!['locked', 'open'].includes(room.replica.offline)) {
|
|
2465
2713
|
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
2466
2714
|
await refuse('missing-required-guarantee');
|
|
2467
2715
|
notifyReady();
|
|
@@ -2493,7 +2741,7 @@ export function composeRuntime(options) {
|
|
|
2493
2741
|
member = signedIn;
|
|
2494
2742
|
emitMember();
|
|
2495
2743
|
}
|
|
2496
|
-
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
2744
|
+
await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
|
|
2497
2745
|
}
|
|
2498
2746
|
async function ensureAuthorization() {
|
|
2499
2747
|
if (authorization !== undefined)
|
|
@@ -2590,9 +2838,44 @@ export function composeRuntime(options) {
|
|
|
2590
2838
|
options.adapter.roomStream?.reopen();
|
|
2591
2839
|
});
|
|
2592
2840
|
}
|
|
2593
|
-
function
|
|
2841
|
+
function rediscover(scope) {
|
|
2842
|
+
return task(OPENING_LANE, async (live) => {
|
|
2843
|
+
if (!live() || !socketOpen)
|
|
2844
|
+
return;
|
|
2845
|
+
const progress = { complete: true, reachable: true };
|
|
2846
|
+
const gate = discover();
|
|
2847
|
+
moveTo(transferring(status));
|
|
2848
|
+
try {
|
|
2849
|
+
const local = await openLocal();
|
|
2850
|
+
if (!live())
|
|
2851
|
+
return;
|
|
2852
|
+
await ensureAuthorization();
|
|
2853
|
+
if (!live())
|
|
2854
|
+
return;
|
|
2855
|
+
await discoverRooms(local, progress, 'authorized', live, scope);
|
|
2856
|
+
}
|
|
2857
|
+
catch {
|
|
2858
|
+
progress.complete = false;
|
|
2859
|
+
}
|
|
2860
|
+
finally {
|
|
2861
|
+
gate.release();
|
|
2862
|
+
notifyReady();
|
|
2863
|
+
}
|
|
2864
|
+
if (!live())
|
|
2865
|
+
return;
|
|
2866
|
+
if (!progress.complete)
|
|
2867
|
+
complete = false;
|
|
2868
|
+
await rest(progress, live);
|
|
2869
|
+
});
|
|
2870
|
+
}
|
|
2871
|
+
function closedSocket(revoked, lost) {
|
|
2594
2872
|
const opened = socketOpen;
|
|
2595
2873
|
socketOpen = false;
|
|
2874
|
+
connectionLost();
|
|
2875
|
+
if (lost) {
|
|
2876
|
+
reads.lost();
|
|
2877
|
+
conversations.lost();
|
|
2878
|
+
}
|
|
2596
2879
|
if (revoked) {
|
|
2597
2880
|
void refuse('revoked-authorization');
|
|
2598
2881
|
return;
|
|
@@ -2609,9 +2892,10 @@ export function composeRuntime(options) {
|
|
|
2609
2892
|
function applyOpening(expired) {
|
|
2610
2893
|
const progress = underway?.generation === generation ? underway.progress : null;
|
|
2611
2894
|
const cycle = progress ?? { complete: true, reachable: true };
|
|
2612
|
-
const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
|
|
2613
2895
|
if (opened === generation)
|
|
2614
2896
|
opened = null;
|
|
2897
|
+
const reloading = progress === null ? [] : owedReloads(expired);
|
|
2898
|
+
const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
|
|
2615
2899
|
return task(OPENING_LANE, async (live) => {
|
|
2616
2900
|
underway = null;
|
|
2617
2901
|
if (!live())
|
|
@@ -2621,24 +2905,17 @@ export function composeRuntime(options) {
|
|
|
2621
2905
|
await rest(cycle, live);
|
|
2622
2906
|
return;
|
|
2623
2907
|
}
|
|
2624
|
-
await roomsCarried(expired);
|
|
2908
|
+
await roomsCarried([...expired, ...reloading]);
|
|
2625
2909
|
if (!live())
|
|
2626
2910
|
return;
|
|
2627
|
-
const replaced = new Set(owed);
|
|
2628
|
-
owed.clear();
|
|
2629
2911
|
try {
|
|
2630
2912
|
const local = await openLocal();
|
|
2631
|
-
const
|
|
2632
|
-
const reloading = held.filter((room) => !expired.includes(room.locator) &&
|
|
2633
|
-
(replaced.has(room.locator) || revoked(room.locator)));
|
|
2634
|
-
for (const room of reloading)
|
|
2635
|
-
catching.add(room.locator);
|
|
2636
|
-
readSide.recover();
|
|
2637
|
-
const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
|
|
2638
|
-
const reloaded = [...(await reloads), ...(await replacing)];
|
|
2913
|
+
const reloaded = await replacing;
|
|
2639
2914
|
if (progress.reachable) {
|
|
2640
|
-
for (const room of
|
|
2641
|
-
if (
|
|
2915
|
+
for (const room of discovered.values()) {
|
|
2916
|
+
if (replicas.has(room.locator) &&
|
|
2917
|
+
!expired.includes(room.locator) &&
|
|
2918
|
+
!reloading.includes(room.locator)) {
|
|
2642
2919
|
await confirmModels(room, local);
|
|
2643
2920
|
}
|
|
2644
2921
|
}
|
|
@@ -2655,14 +2932,29 @@ export function composeRuntime(options) {
|
|
|
2655
2932
|
await finishOpening(progress, live);
|
|
2656
2933
|
});
|
|
2657
2934
|
}
|
|
2935
|
+
function owedReloads(expired) {
|
|
2936
|
+
const reloading = [...discovered.values()]
|
|
2937
|
+
.filter((room) => replicas.has(room.locator) &&
|
|
2938
|
+
!expired.includes(room.locator) &&
|
|
2939
|
+
(owed.has(room.locator) || revoked(room.locator)))
|
|
2940
|
+
.map((room) => room.locator);
|
|
2941
|
+
owed.clear();
|
|
2942
|
+
for (const locator of reloading)
|
|
2943
|
+
catching.add(locator);
|
|
2944
|
+
readSide.recover();
|
|
2945
|
+
return reloading;
|
|
2946
|
+
}
|
|
2658
2947
|
async function reloadNow(replaced, progress) {
|
|
2948
|
+
const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
|
|
2949
|
+
for (const room of reloaded)
|
|
2950
|
+
reloadsDue.set(room.locator, generation);
|
|
2659
2951
|
try {
|
|
2660
2952
|
const local = await openLocal();
|
|
2661
|
-
return await Promise.allSettled(
|
|
2662
|
-
.filter((room) => replicas.has(room.locator) && replaced(room))
|
|
2663
|
-
.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
2953
|
+
return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
|
|
2664
2954
|
}
|
|
2665
2955
|
catch {
|
|
2956
|
+
for (const room of reloaded)
|
|
2957
|
+
reloadsDue.delete(room.locator);
|
|
2666
2958
|
progress.reachable = false;
|
|
2667
2959
|
return [];
|
|
2668
2960
|
}
|
|
@@ -2779,6 +3071,7 @@ export function composeRuntime(options) {
|
|
|
2779
3071
|
progress.complete = false;
|
|
2780
3072
|
return;
|
|
2781
3073
|
}
|
|
3074
|
+
reads.applied();
|
|
2782
3075
|
submitAll();
|
|
2783
3076
|
for (const locator of stale)
|
|
2784
3077
|
if (repairable.has(locator))
|
|
@@ -2787,10 +3080,11 @@ export function composeRuntime(options) {
|
|
|
2787
3080
|
if (status.providerUnreachable)
|
|
2788
3081
|
conversations.reconnected();
|
|
2789
3082
|
const reading = { complete: true, reachable: true };
|
|
2790
|
-
|
|
2791
|
-
await conversations.catchUp(reading);
|
|
3083
|
+
await conversations.catchUp(reading);
|
|
2792
3084
|
if (progress.reachable && reading.reachable)
|
|
2793
3085
|
await conversations.resend(reading);
|
|
3086
|
+
if (progress.reachable)
|
|
3087
|
+
void recovery.attempt('calls', resendCalls);
|
|
2794
3088
|
if (!reading.reachable) {
|
|
2795
3089
|
progress.reachable = false;
|
|
2796
3090
|
resendLater();
|
|
@@ -2851,7 +3145,7 @@ export function composeRuntime(options) {
|
|
|
2851
3145
|
return;
|
|
2852
3146
|
const room = discovered.get(locator);
|
|
2853
3147
|
const held = replicas.get(locator);
|
|
2854
|
-
if (room === undefined || room.replica.admission !== 'admitted'
|
|
3148
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2855
3149
|
return;
|
|
2856
3150
|
if (held !== undefined &&
|
|
2857
3151
|
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
@@ -2902,15 +3196,19 @@ export function composeRuntime(options) {
|
|
|
2902
3196
|
return;
|
|
2903
3197
|
if (event.kind === 'opened') {
|
|
2904
3198
|
socketOpen = true;
|
|
3199
|
+
connectionLive();
|
|
2905
3200
|
generation += 1;
|
|
3201
|
+
reads.opened();
|
|
2906
3202
|
conversations.reopened();
|
|
2907
3203
|
recovery.resume();
|
|
2908
3204
|
void sync('opened');
|
|
2909
3205
|
}
|
|
2910
3206
|
else if (event.kind === 'closed')
|
|
2911
|
-
closedSocket(event.revoked);
|
|
3207
|
+
closedSocket(event.revoked, event.lost);
|
|
2912
3208
|
else if (event.kind === 'opening')
|
|
2913
3209
|
void applyOpening(event.expired);
|
|
3210
|
+
else if (event.kind === 'authority-changed')
|
|
3211
|
+
void rediscover(event.scope);
|
|
2914
3212
|
else
|
|
2915
3213
|
void applyRoom(event.locator, event.roomVersion);
|
|
2916
3214
|
}, receipts);
|
|
@@ -2965,7 +3263,7 @@ export function composeRuntime(options) {
|
|
|
2965
3263
|
if (!live())
|
|
2966
3264
|
return;
|
|
2967
3265
|
await conversations.reconcile();
|
|
2968
|
-
if (conversations.current()
|
|
3266
|
+
if (conversations.current()) {
|
|
2969
3267
|
show();
|
|
2970
3268
|
return;
|
|
2971
3269
|
}
|
|
@@ -2983,10 +3281,10 @@ export function composeRuntime(options) {
|
|
|
2983
3281
|
if (closed)
|
|
2984
3282
|
return;
|
|
2985
3283
|
closed = true;
|
|
2986
|
-
if (narrowing !== null)
|
|
2987
|
-
clearTimeout(narrowing);
|
|
2988
3284
|
if (rechecking !== null)
|
|
2989
3285
|
clearInterval(rechecking);
|
|
3286
|
+
connectionLive();
|
|
3287
|
+
lockListeners.clear();
|
|
2990
3288
|
readinessWatches.clear();
|
|
2991
3289
|
withdrawStream?.();
|
|
2992
3290
|
withdrawStream = undefined;
|
|
@@ -3032,22 +3330,63 @@ export function composeRuntime(options) {
|
|
|
3032
3330
|
if (options.adapter.roomStream === undefined)
|
|
3033
3331
|
void sync('unstreamed');
|
|
3034
3332
|
function planned(input, reach) {
|
|
3035
|
-
|
|
3333
|
+
const plan = planOf(input, roomFacts(reach), planReach(reach));
|
|
3334
|
+
const named = plan.paths !== null &&
|
|
3335
|
+
plan.rooms !== null &&
|
|
3336
|
+
plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
|
|
3337
|
+
return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
|
|
3036
3338
|
}
|
|
3037
3339
|
function watchPlan(plan, deliver) {
|
|
3038
|
-
|
|
3340
|
+
let seeded = false;
|
|
3341
|
+
let pending = true;
|
|
3342
|
+
const shown = () => {
|
|
3343
|
+
const seed = seeds.get(plan.key);
|
|
3344
|
+
if (seed === undefined || seeded || !pending)
|
|
3345
|
+
return;
|
|
3346
|
+
seeded = true;
|
|
3347
|
+
deliver(seededResult(seed));
|
|
3348
|
+
};
|
|
3349
|
+
const watchers = seedWatchers.get(plan.key) ?? new Set();
|
|
3350
|
+
seedWatchers.set(plan.key, watchers);
|
|
3351
|
+
watchers.add(shown);
|
|
3352
|
+
const stop = readSide.watch(plan, identityOf(plan), () => {
|
|
3039
3353
|
const cover = covered(plan);
|
|
3040
3354
|
awaitPreparation(cover);
|
|
3041
|
-
return cover;
|
|
3042
|
-
},
|
|
3355
|
+
return seedBehind(plan, cover);
|
|
3356
|
+
}, (message) => {
|
|
3357
|
+
const answered = message;
|
|
3358
|
+
pending = whole(answered) && 'pending' in answered.result;
|
|
3359
|
+
if (pending && seeds.has(plan.key)) {
|
|
3360
|
+
shown();
|
|
3361
|
+
return;
|
|
3362
|
+
}
|
|
3363
|
+
seeded = false;
|
|
3364
|
+
deliver(message);
|
|
3365
|
+
});
|
|
3366
|
+
return () => {
|
|
3367
|
+
watchers.delete(shown);
|
|
3368
|
+
if (watchers.size === 0)
|
|
3369
|
+
seedWatchers.delete(plan.key);
|
|
3370
|
+
stop();
|
|
3371
|
+
};
|
|
3043
3372
|
}
|
|
3044
|
-
async function searched(input, reach) {
|
|
3045
|
-
const
|
|
3046
|
-
const
|
|
3373
|
+
async function searched(input, reach, locked) {
|
|
3374
|
+
const asked = searchPlanOf(input);
|
|
3375
|
+
const plan = { ...asked, limit: capped(asked.limit) };
|
|
3376
|
+
const filter = {
|
|
3377
|
+
...filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)),
|
|
3378
|
+
locked,
|
|
3379
|
+
};
|
|
3380
|
+
const unlocking = (filter.rooms ?? [])
|
|
3381
|
+
.filter((room) => locked.includes(room))
|
|
3382
|
+
.map((room) => ({ room, awaits: 'unlock' }));
|
|
3047
3383
|
for (;;) {
|
|
3048
3384
|
const cover = covered(filter);
|
|
3049
3385
|
awaitPreparation(cover);
|
|
3050
|
-
const pending =
|
|
3386
|
+
const pending = [
|
|
3387
|
+
...unlocking,
|
|
3388
|
+
...('pending' in cover ? cover.pending : []),
|
|
3389
|
+
];
|
|
3051
3390
|
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
3052
3391
|
const held = covered({
|
|
3053
3392
|
...filter,
|
|
@@ -3081,22 +3420,34 @@ export function composeRuntime(options) {
|
|
|
3081
3420
|
if ('pending' in answered)
|
|
3082
3421
|
continue;
|
|
3083
3422
|
const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
|
|
3423
|
+
await readSide.settled();
|
|
3084
3424
|
const seen = searchIndex.changes();
|
|
3085
3425
|
await searchIndex.caughtUp();
|
|
3086
|
-
const selected =
|
|
3426
|
+
const selected = roomsAlone(answering) ? null : await rowsQuery(answering);
|
|
3087
3427
|
if (selected !== null && 'pending' in selected)
|
|
3088
3428
|
continue;
|
|
3089
3429
|
if (searchIndex.changes() !== seen ||
|
|
3090
3430
|
JSON.stringify(covered(answering)) !== JSON.stringify(answered))
|
|
3091
3431
|
continue;
|
|
3092
|
-
const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations,
|
|
3432
|
+
const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms !== null
|
|
3433
|
+
? new Set(answering.rooms)
|
|
3434
|
+
: locked.length === 0
|
|
3435
|
+
? null
|
|
3436
|
+
: new Set(reachedRooms(answering)), plan.collection
|
|
3437
|
+
? new Map(answered.coverage.rooms.map(({ room }) => [
|
|
3438
|
+
room,
|
|
3439
|
+
replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
|
|
3440
|
+
]))
|
|
3441
|
+
: null);
|
|
3093
3442
|
const coverage = {
|
|
3094
3443
|
rooms: answered.coverage.rooms,
|
|
3095
3444
|
conversations: searchedConversations,
|
|
3096
3445
|
index: searchIndex.generation(),
|
|
3097
3446
|
overlay: answered.coverage.overlay,
|
|
3098
3447
|
};
|
|
3099
|
-
return complete
|
|
3448
|
+
return complete
|
|
3449
|
+
? { ...hits, coverage }
|
|
3450
|
+
: { pending, documents: hits.documents, messages: hits.messages, coverage };
|
|
3100
3451
|
}
|
|
3101
3452
|
}
|
|
3102
3453
|
const watch = (topic, input, deliver) => {
|
|
@@ -3206,7 +3557,7 @@ export function composeRuntime(options) {
|
|
|
3206
3557
|
return listed();
|
|
3207
3558
|
},
|
|
3208
3559
|
query: ((input) => rowsQuery(planned(input, null))),
|
|
3209
|
-
search: (input) => searched(input, null),
|
|
3560
|
+
search: (input) => searched(input, null, []),
|
|
3210
3561
|
types(locator) {
|
|
3211
3562
|
return {
|
|
3212
3563
|
collection: modelTypes(modelOf(locator)),
|
|
@@ -3281,27 +3632,33 @@ export function composeRuntime(options) {
|
|
|
3281
3632
|
});
|
|
3282
3633
|
},
|
|
3283
3634
|
write(app, value) {
|
|
3284
|
-
return
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3635
|
+
return keepPresentation(app, () => structuredClone(value));
|
|
3636
|
+
},
|
|
3637
|
+
edit(app, members) {
|
|
3638
|
+
return keepPresentation(app, (held) => Object.fromEntries([
|
|
3639
|
+
...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(members, member)),
|
|
3640
|
+
...Object.entries(structuredClone(members)).filter(([, value]) => value !== null),
|
|
3641
|
+
]));
|
|
3291
3642
|
},
|
|
3292
3643
|
},
|
|
3293
3644
|
save(room, named, writtenBy) {
|
|
3294
3645
|
return exclusive(() => saveRoomDrafts(room, named, writtenBy));
|
|
3295
3646
|
},
|
|
3647
|
+
saveParts(room, parts, writtenBy) {
|
|
3648
|
+
return exclusive(() => saveParts(room, parts, writtenBy));
|
|
3649
|
+
},
|
|
3296
3650
|
history(locator, path, query) {
|
|
3297
3651
|
return readHistory(locator, path, query);
|
|
3298
3652
|
},
|
|
3299
3653
|
binaryFile: {
|
|
3300
3654
|
read: readBinaryPath,
|
|
3655
|
+
link: linkBinaryPath,
|
|
3301
3656
|
readReference: readBinaryReference,
|
|
3302
3657
|
},
|
|
3303
|
-
call(name, input) {
|
|
3304
|
-
|
|
3658
|
+
async call(name, input) {
|
|
3659
|
+
await ready;
|
|
3660
|
+
const room = calledRoom(name, input);
|
|
3661
|
+
return room !== null && locked(room) ? lockedRoom(room) : recoveredAction(name, input);
|
|
3305
3662
|
},
|
|
3306
3663
|
reached(locator, roomVersion, path) {
|
|
3307
3664
|
return path === undefined
|
|
@@ -3346,13 +3703,16 @@ export function composeRuntime(options) {
|
|
|
3346
3703
|
watchPlan,
|
|
3347
3704
|
search: searched,
|
|
3348
3705
|
watchReadiness,
|
|
3706
|
+
locked,
|
|
3707
|
+
lockedRooms,
|
|
3708
|
+
watchLocks,
|
|
3349
3709
|
};
|
|
3350
3710
|
return {
|
|
3351
3711
|
md,
|
|
3712
|
+
seed,
|
|
3352
3713
|
stop,
|
|
3353
3714
|
lose: loseRooms,
|
|
3354
3715
|
watch,
|
|
3355
|
-
acquire,
|
|
3356
3716
|
persisted(answer) {
|
|
3357
3717
|
if (answer === persistence)
|
|
3358
3718
|
return;
|