@agentshouse/mdruntime 0.1.0-cc294e382554d → 0.1.0-cc56db96f86ae
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 +28 -23
- package/README.md +9 -8
- package/dist/adapter.d.ts +11 -10
- package/dist/adapter.d.ts.map +1 -1
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +60 -29
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +2 -6
- package/dist/change.d.ts +31 -1
- package/dist/change.d.ts.map +1 -1
- package/dist/change.js +10 -1
- package/dist/chat/house.d.ts +25 -0
- package/dist/chat/house.d.ts.map +1 -0
- package/dist/chat/house.js +38 -0
- package/dist/chat/input.d.ts +3 -1
- package/dist/chat/input.d.ts.map +1 -1
- package/dist/chat/input.js +11 -18
- package/dist/chat/message.d.ts.map +1 -1
- package/dist/chat/message.js +45 -10
- package/dist/chat/messages.d.ts +6 -1
- package/dist/chat/messages.d.ts.map +1 -1
- package/dist/chat/messages.js +14 -11
- package/dist/chat/name.d.ts +1 -0
- package/dist/chat/name.d.ts.map +1 -1
- package/dist/chat/name.js +1 -1
- package/dist/chat/opened.d.ts +1 -0
- package/dist/chat/opened.d.ts.map +1 -1
- package/dist/chat/opened.js +2 -0
- package/dist/chat/overlay.d.ts.map +1 -1
- package/dist/chat/overlay.js +1 -5
- package/dist/chat/parts.d.ts +1 -0
- package/dist/chat/parts.d.ts.map +1 -1
- package/dist/chat/parts.js +3 -2
- package/dist/chat/provider.d.ts +5 -1
- package/dist/chat/provider.d.ts.map +1 -1
- package/dist/chat/provider.js +58 -77
- package/dist/chat/ready.d.ts +4 -1
- package/dist/chat/ready.d.ts.map +1 -1
- package/dist/chat/ready.js +7 -5
- package/dist/chat/room-file.d.ts +8 -0
- package/dist/chat/room-file.d.ts.map +1 -0
- package/dist/chat/room-file.js +67 -0
- package/dist/chat/style.d.ts.map +1 -1
- package/dist/chat/style.js +4 -0
- package/dist/chat.d.ts +1 -0
- package/dist/chat.d.ts.map +1 -1
- package/dist/client.d.ts +4 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +2 -0
- package/dist/collection.d.ts +2 -2
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +56 -40
- package/dist/connection.d.ts +5 -1
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +723 -158
- package/dist/conversation.d.ts +2 -2
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +18 -29
- package/dist/digest.d.ts +2 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +100 -0
- package/dist/durability.d.ts +1 -1
- package/dist/file.d.ts +2 -10
- package/dist/file.d.ts.map +1 -1
- package/dist/file.js +3 -120
- package/dist/host.d.ts +1 -1
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +7 -3
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +9 -5
- package/dist/outcome.d.ts +20 -1
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +22 -0
- package/dist/overlay.d.ts +1 -1
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +27 -3
- package/dist/query.d.ts +2 -1
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +1 -0
- package/dist/reach.d.ts +5 -1
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +144 -39
- package/dist/read-side.d.ts +4 -4
- package/dist/read-side.d.ts.map +1 -1
- package/dist/read-side.js +585 -232
- package/dist/readiness.d.ts +1 -1
- package/dist/readiness.d.ts.map +1 -1
- package/dist/recovery.d.ts +2 -2
- package/dist/recovery.d.ts.map +1 -1
- package/dist/recovery.js +8 -7
- package/dist/refresh.js +8 -8
- package/dist/replica.d.ts +1 -2
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +69 -48
- package/dist/room-draft.d.ts +3 -1
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +21 -13
- package/dist/save.d.ts +43 -7
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +329 -77
- package/dist/search.d.ts +1 -1
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +61 -10
- package/dist/shared.d.ts +5 -1
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +72 -9
- package/dist/store.d.ts +8 -13
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +189 -230
- package/dist/tables.d.ts +181 -0
- package/dist/tables.d.ts.map +1 -0
- package/dist/tables.js +309 -0
- package/dist/turn.d.ts +1 -1
- package/dist/turn.d.ts.map +1 -1
- package/dist/turn.js +9 -6
- package/dist/windows.d.ts +0 -5
- package/dist/windows.d.ts.map +1 -1
- package/dist/windows.js +0 -9
- package/dist/wire.d.ts +8 -0
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +2 -0
- package/package.json +6 -2
package/dist/connection.js
CHANGED
|
@@ -1,33 +1,34 @@
|
|
|
1
|
+
import { roomTarget, target } from '@agentshouse/mdmodel';
|
|
1
2
|
import { attachedModel } from './attached-model.js';
|
|
2
3
|
import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
|
|
3
4
|
import { observing, whole } from './observation.js';
|
|
4
5
|
import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
|
|
5
6
|
import { createReadSide } from './read-side.js';
|
|
6
7
|
import { seededResult } from './seed.js';
|
|
7
|
-
import { markdownPath, roomOfKey } from './rows.js';
|
|
8
|
+
import { documentKey, markdownPath, roomOfKey } from './rows.js';
|
|
8
9
|
import { createSearchIndex, searchPlanOf, } from './search.js';
|
|
9
10
|
import { capped, MAXIMUM_WINDOW } from './windows.js';
|
|
10
11
|
import { bootstrapRoom } from './bootstrap.js';
|
|
11
12
|
import { changeSet, changing, entering, mergeChangeSets } from './change-set.js';
|
|
12
13
|
import { createConversationStore, } from './conversation.js';
|
|
13
|
-
import { queuedFile } from './change.js';
|
|
14
|
+
import { leaving as leavingIn, partnerOf, queuedFile, } from './change.js';
|
|
14
15
|
import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
|
|
15
16
|
import { runtimeError } from './failure.js';
|
|
16
17
|
import { createLog } from './log.js';
|
|
17
18
|
import { binaryRecord } from './binary.js';
|
|
18
|
-
import { absentBinary, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
|
|
19
|
+
import { ABSENT_TARGET, absentBinary, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
|
|
19
20
|
import { createRecovery } from './recovery.js';
|
|
20
21
|
import { doorReads } from './reads.js';
|
|
21
22
|
import { refreshRoom } from './refresh.js';
|
|
22
|
-
import { buildBatch, joined,
|
|
23
|
-
import { applyReturned, compositionOf, confirmedOf, landed, readReclassified,
|
|
23
|
+
import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
|
|
24
|
+
import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicating, sameMembership, standing, } from './replica.js';
|
|
24
25
|
import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
|
|
25
26
|
import { same, saving, STARTING, transferring, } from './status.js';
|
|
26
27
|
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
27
28
|
import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
|
|
28
29
|
import { FencedMap } from './fence.js';
|
|
29
|
-
import {
|
|
30
|
-
import { inChunks, ROW_WEIGHT, stepped, yielded } from './turn.js';
|
|
30
|
+
import { keysIn, tallying } from './shared.js';
|
|
31
|
+
import { finished, inChunks, ROW_WEIGHT, stepped, yielded } from './turn.js';
|
|
31
32
|
import { identityTime, roomDraftRevision } from './identity.js';
|
|
32
33
|
import { beneath, normalize } from './paths.js';
|
|
33
34
|
import { openRuntimeStore, } from './store.js';
|
|
@@ -59,6 +60,8 @@ const OPENING_LANE = 'opening';
|
|
|
59
60
|
const CONVERSATION_LANE = 'conversations';
|
|
60
61
|
const ROOM_READS = 256;
|
|
61
62
|
const ROOM_ATTEMPTS = 16;
|
|
63
|
+
// 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.
|
|
64
|
+
const LOCK_GRACE_MS = 120_000;
|
|
62
65
|
function operationOf(input) {
|
|
63
66
|
const operation = input?.operation_id;
|
|
64
67
|
return typeof operation === 'string' ? operation : null;
|
|
@@ -88,6 +91,9 @@ function abandonedOn(signal) {
|
|
|
88
91
|
function roomLane(locator) {
|
|
89
92
|
return `room ${locator}`;
|
|
90
93
|
}
|
|
94
|
+
function pendingKey(locator, id) {
|
|
95
|
+
return `${locator}\u0000${id}`;
|
|
96
|
+
}
|
|
91
97
|
export function composeRuntime(options) {
|
|
92
98
|
const localSaves = options.adapter.localSaves === true;
|
|
93
99
|
let authorization = options.authorization;
|
|
@@ -180,6 +186,9 @@ export function composeRuntime(options) {
|
|
|
180
186
|
let withdrawConversations;
|
|
181
187
|
let currencyQueued = null;
|
|
182
188
|
let socketOpen = false;
|
|
189
|
+
const confirmed = new Set();
|
|
190
|
+
let lockGrace = null;
|
|
191
|
+
const lockListeners = new Set();
|
|
183
192
|
let serviceMode = 'open';
|
|
184
193
|
let haltedRefusal = false;
|
|
185
194
|
let openSignals = 0;
|
|
@@ -331,20 +340,74 @@ export function composeRuntime(options) {
|
|
|
331
340
|
}
|
|
332
341
|
function documentsIn(locator) {
|
|
333
342
|
const replica = replicas.get(locator);
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const weighed = outsideOf(roots);
|
|
338
|
-
return (
|
|
339
|
-
|
|
343
|
+
return replica === undefined ? 0 : finished(counting(replica));
|
|
344
|
+
}
|
|
345
|
+
function* counting(replica) {
|
|
346
|
+
const weighed = outsideOf(replica.confirmed.record.typedSelection?.roots ?? []);
|
|
347
|
+
return ((yield* tallying(replica.visible.documents, weighed.documents)) +
|
|
348
|
+
(yield* tallying(replica.visible.binaries, weighed.binaries)));
|
|
340
349
|
}
|
|
341
350
|
function listed() {
|
|
342
351
|
return structuredClone([...rooms.values()].map((room) => {
|
|
343
352
|
const documents = documentsIn(room.locator);
|
|
344
353
|
counted.set(room.locator, documents);
|
|
345
|
-
return
|
|
354
|
+
return locked(room.locator)
|
|
355
|
+
? {
|
|
356
|
+
...room,
|
|
357
|
+
model: modelTypes(NO_MODEL),
|
|
358
|
+
typedSelection: null,
|
|
359
|
+
documents: 0,
|
|
360
|
+
locked: true,
|
|
361
|
+
}
|
|
362
|
+
: { ...room, documents, locked: false };
|
|
346
363
|
}));
|
|
347
364
|
}
|
|
365
|
+
function locked(locator) {
|
|
366
|
+
return rooms.get(locator)?.replica.offline === 'locked' && !confirmed.has(locator);
|
|
367
|
+
}
|
|
368
|
+
function lockedRooms() {
|
|
369
|
+
return [...rooms.keys()].filter(locked).sort();
|
|
370
|
+
}
|
|
371
|
+
function relocked(before) {
|
|
372
|
+
const after = lockedRooms();
|
|
373
|
+
if (JSON.stringify(after) === JSON.stringify(before))
|
|
374
|
+
return;
|
|
375
|
+
journal.record(`locked ${after.length}`);
|
|
376
|
+
emitRooms();
|
|
377
|
+
for (const notify of [...lockListeners])
|
|
378
|
+
notify();
|
|
379
|
+
}
|
|
380
|
+
function confirmLocks(listed) {
|
|
381
|
+
if (!socketOpen)
|
|
382
|
+
return;
|
|
383
|
+
for (const room of listed) {
|
|
384
|
+
if (room.replica.admission === 'admitted')
|
|
385
|
+
confirmed.add(room.locator);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function connectionLost() {
|
|
389
|
+
if (lockGrace !== null || closed)
|
|
390
|
+
return;
|
|
391
|
+
lockGrace = setTimeout(() => {
|
|
392
|
+
lockGrace = null;
|
|
393
|
+
if (socketOpen || closed)
|
|
394
|
+
return;
|
|
395
|
+
const before = lockedRooms();
|
|
396
|
+
confirmed.clear();
|
|
397
|
+
relocked(before);
|
|
398
|
+
}, LOCK_GRACE_MS);
|
|
399
|
+
}
|
|
400
|
+
function connectionLive() {
|
|
401
|
+
if (lockGrace !== null)
|
|
402
|
+
clearTimeout(lockGrace);
|
|
403
|
+
lockGrace = null;
|
|
404
|
+
}
|
|
405
|
+
function watchLocks(notify) {
|
|
406
|
+
lockListeners.add(notify);
|
|
407
|
+
return () => {
|
|
408
|
+
lockListeners.delete(notify);
|
|
409
|
+
};
|
|
410
|
+
}
|
|
348
411
|
function roomFacts(reach = null) {
|
|
349
412
|
const held = [...rooms.values()]
|
|
350
413
|
.filter((room) => holds(reach, room.locator))
|
|
@@ -402,17 +465,17 @@ export function composeRuntime(options) {
|
|
|
402
465
|
for (const listener of statusListeners)
|
|
403
466
|
listener(shown);
|
|
404
467
|
}
|
|
405
|
-
function entryOf(batch) {
|
|
468
|
+
function entryOf(locator, batch) {
|
|
406
469
|
if (localSaves)
|
|
407
470
|
return Object.freeze({ batch, state: 'local' });
|
|
408
|
-
const state = pendings.get(batch.id);
|
|
471
|
+
const state = pendings.get(pendingKey(locator, batch.id));
|
|
409
472
|
if (state?.accepted !== undefined) {
|
|
410
473
|
return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
|
|
411
474
|
}
|
|
412
475
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
413
476
|
}
|
|
414
477
|
function entriesOf(locator) {
|
|
415
|
-
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
|
|
478
|
+
return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map((batch) => entryOf(locator, batch));
|
|
416
479
|
}
|
|
417
480
|
function emitQueue(locator) {
|
|
418
481
|
const entries = entriesOf(locator);
|
|
@@ -451,13 +514,10 @@ export function composeRuntime(options) {
|
|
|
451
514
|
if (replica === undefined || closed || prepared.has(locator))
|
|
452
515
|
return;
|
|
453
516
|
const visible = replica.visible;
|
|
454
|
-
const paths = [
|
|
455
|
-
...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
|
|
456
|
-
];
|
|
457
517
|
const entered = [];
|
|
458
518
|
projecting.add(locator);
|
|
459
519
|
try {
|
|
460
|
-
await inChunks(
|
|
520
|
+
await inChunks(await stepped(keysIn(visible.documents, visible.binaries)), (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
|
|
461
521
|
const current = replicas.get(locator);
|
|
462
522
|
if (current === undefined || closed)
|
|
463
523
|
return;
|
|
@@ -514,7 +574,9 @@ export function composeRuntime(options) {
|
|
|
514
574
|
(reached === null
|
|
515
575
|
? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
|
|
516
576
|
: [...reached]);
|
|
517
|
-
return [...new Set(named)]
|
|
577
|
+
return [...new Set(named)]
|
|
578
|
+
.filter((locator) => (reached === null || reached.has(locator)) && !plan.locked.includes(locator))
|
|
579
|
+
.sort();
|
|
518
580
|
}
|
|
519
581
|
function awaited(locator) {
|
|
520
582
|
return (!roomsListed ||
|
|
@@ -717,7 +779,7 @@ export function composeRuntime(options) {
|
|
|
717
779
|
}
|
|
718
780
|
async function estimated() {
|
|
719
781
|
const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
|
|
720
|
-
return estimate === null ? null : Math.max(0, estimate.
|
|
782
|
+
return estimate === null ? null : Math.max(0, estimate.total - estimate.usage);
|
|
721
783
|
}
|
|
722
784
|
function pause(locator, cause) {
|
|
723
785
|
paused.add(locator);
|
|
@@ -738,7 +800,8 @@ export function composeRuntime(options) {
|
|
|
738
800
|
const available = await estimated().catch(() => null);
|
|
739
801
|
if (available === null || available < (held.needed ?? 0))
|
|
740
802
|
return;
|
|
741
|
-
if (held.cause === '
|
|
803
|
+
if (held.cause === 'storage-exceeded' &&
|
|
804
|
+
(held.available === null || available <= held.available))
|
|
742
805
|
return;
|
|
743
806
|
resume();
|
|
744
807
|
}
|
|
@@ -780,7 +843,7 @@ export function composeRuntime(options) {
|
|
|
780
843
|
}
|
|
781
844
|
async function exceededOn(locator, bytes) {
|
|
782
845
|
const available = await estimated().catch(() => null);
|
|
783
|
-
pause(locator, { cause: '
|
|
846
|
+
pause(locator, { cause: 'storage-exceeded', needed: bytes, available });
|
|
784
847
|
}
|
|
785
848
|
function seedRoomHeld(room, version) {
|
|
786
849
|
return prepared.has(room) && reachedBy(room, version);
|
|
@@ -917,17 +980,22 @@ export function composeRuntime(options) {
|
|
|
917
980
|
models.set(key, revoked);
|
|
918
981
|
liveModels.delete(key);
|
|
919
982
|
}
|
|
920
|
-
const
|
|
921
|
-
for (const
|
|
983
|
+
const revocations = [];
|
|
984
|
+
for (const locator of [...rooms.keys()]) {
|
|
922
985
|
const before = replicas.get(locator) ?? null;
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
986
|
+
const after = before === null
|
|
987
|
+
? null
|
|
988
|
+
: await stepped(replicating(await confirmedOf({ ...before.confirmed.record, model: revoked, selectionModel: revoked }, before.confirmed), before.queue, before));
|
|
989
|
+
const set = await stepped(changing(locator, before, after, { room: true }));
|
|
990
|
+
revocations.push([locator, before, after, set]);
|
|
991
|
+
}
|
|
992
|
+
const changed = [];
|
|
993
|
+
for (const [locator, before, after, set] of revocations) {
|
|
994
|
+
const room = rooms.get(locator);
|
|
995
|
+
if (room === undefined || (replicas.get(locator) ?? null) !== before)
|
|
996
|
+
continue;
|
|
997
|
+
if (after !== null)
|
|
998
|
+
replicas.set(locator, after);
|
|
931
999
|
rooms.set(locator, {
|
|
932
1000
|
...room,
|
|
933
1001
|
model: modelTypes(revoked),
|
|
@@ -935,7 +1003,7 @@ export function composeRuntime(options) {
|
|
|
935
1003
|
? null
|
|
936
1004
|
: { ...room.typedSelection, model: modelTypes(revoked) },
|
|
937
1005
|
});
|
|
938
|
-
changed.push(
|
|
1006
|
+
changed.push(set);
|
|
939
1007
|
}
|
|
940
1008
|
publishChangeSet(mergeChangeSets(changed));
|
|
941
1009
|
});
|
|
@@ -974,6 +1042,7 @@ export function composeRuntime(options) {
|
|
|
974
1042
|
});
|
|
975
1043
|
}
|
|
976
1044
|
async function applyDiscovery(found, started, covered) {
|
|
1045
|
+
const lockedBefore = lockedRooms();
|
|
977
1046
|
for (const room of found) {
|
|
978
1047
|
const fence = fences.get(room.locator);
|
|
979
1048
|
if (fence !== undefined && fence.erased && fence.at < started)
|
|
@@ -1006,8 +1075,10 @@ export function composeRuntime(options) {
|
|
|
1006
1075
|
}
|
|
1007
1076
|
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
1008
1077
|
}
|
|
1078
|
+
confirmLocks(listed);
|
|
1009
1079
|
roomsListed = true;
|
|
1010
1080
|
emitRooms();
|
|
1081
|
+
relocked(lockedBefore);
|
|
1011
1082
|
return listed;
|
|
1012
1083
|
}
|
|
1013
1084
|
function heldLocally(locator) {
|
|
@@ -1037,16 +1108,17 @@ export function composeRuntime(options) {
|
|
|
1037
1108
|
}
|
|
1038
1109
|
function fence(locators) {
|
|
1039
1110
|
const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
|
|
1040
|
-
const removed = fresh.map((locator) =>
|
|
1041
|
-
|
|
1042
|
-
|
|
1111
|
+
const removed = fresh.map((locator) => ({
|
|
1112
|
+
locator,
|
|
1113
|
+
shown: prepared.has(locator) || projecting.has(locator) ? (replicas.get(locator) ?? null) : null,
|
|
1043
1114
|
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
1044
1115
|
}));
|
|
1045
1116
|
for (const locator of fresh) {
|
|
1046
1117
|
fences.set(locator, { at: discoveries, erased: false });
|
|
1047
1118
|
lastFenced.set(locator, ++fencings);
|
|
1119
|
+
confirmed.delete(locator);
|
|
1048
1120
|
for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
|
|
1049
|
-
pendings.delete(batch.id);
|
|
1121
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
1050
1122
|
replicas.delete(locator);
|
|
1051
1123
|
queuesWithoutReplica.delete(locator);
|
|
1052
1124
|
rooms.delete(locator);
|
|
@@ -1060,6 +1132,7 @@ export function composeRuntime(options) {
|
|
|
1060
1132
|
repairable.delete(locator);
|
|
1061
1133
|
unindexable.delete(locator);
|
|
1062
1134
|
paused.delete(locator);
|
|
1135
|
+
prepared.delete(locator);
|
|
1063
1136
|
scopedRefusals.delete(locator);
|
|
1064
1137
|
sourceRefusals.delete(locator);
|
|
1065
1138
|
declarationRefusals.delete(locator);
|
|
@@ -1073,14 +1146,21 @@ export function composeRuntime(options) {
|
|
|
1073
1146
|
}
|
|
1074
1147
|
if (fresh.length === 0)
|
|
1075
1148
|
return fresh;
|
|
1076
|
-
publishChangeSet(mergeChangeSets(removed));
|
|
1077
|
-
for (const locator of fresh)
|
|
1078
|
-
prepared.delete(locator);
|
|
1079
1149
|
searchIndex.forget(fresh);
|
|
1150
|
+
const withdrawn = readSide.apply(stepped(withdrawing(removed)), (set) => conversations.changed(set));
|
|
1151
|
+
if (withdrawn instanceof Promise)
|
|
1152
|
+
void withdrawn.catch(unpublished);
|
|
1153
|
+
publishChangeSet(mergeChangeSets(removed.map(({ locator, drafts }) => changeSet(locator, null, null, { room: true, queue: true, drafts }))));
|
|
1080
1154
|
readSide.recover();
|
|
1081
1155
|
moveTo({ ...status, ...attention() });
|
|
1082
1156
|
return fresh;
|
|
1083
1157
|
}
|
|
1158
|
+
function* withdrawing(removed) {
|
|
1159
|
+
const sets = [];
|
|
1160
|
+
for (const { locator, shown } of removed)
|
|
1161
|
+
sets.push(yield* changing(locator, shown, null));
|
|
1162
|
+
return mergeChangeSets(sets);
|
|
1163
|
+
}
|
|
1084
1164
|
async function erased(local, locators) {
|
|
1085
1165
|
await local.erase(locators);
|
|
1086
1166
|
for (const locator of locators) {
|
|
@@ -1091,8 +1171,12 @@ export function composeRuntime(options) {
|
|
|
1091
1171
|
}
|
|
1092
1172
|
async function loseRooms(locators) {
|
|
1093
1173
|
const fresh = fence(locators);
|
|
1094
|
-
if (fresh.length
|
|
1095
|
-
|
|
1174
|
+
if (fresh.length === 0)
|
|
1175
|
+
return;
|
|
1176
|
+
await durably(async (local) => {
|
|
1177
|
+
await erased(local, fresh);
|
|
1178
|
+
await unpair(local);
|
|
1179
|
+
});
|
|
1096
1180
|
}
|
|
1097
1181
|
function restored(key, persisted) {
|
|
1098
1182
|
const prior = models.get(key);
|
|
@@ -1127,6 +1211,22 @@ export function composeRuntime(options) {
|
|
|
1127
1211
|
opened.transfers(),
|
|
1128
1212
|
opened.targets(),
|
|
1129
1213
|
]);
|
|
1214
|
+
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1215
|
+
const previousReplicas = new Map(replicas);
|
|
1216
|
+
const read = new Map();
|
|
1217
|
+
for (const record of records) {
|
|
1218
|
+
const model = restored(modelKey(record.locator, 'collection'), record.model);
|
|
1219
|
+
const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
|
|
1220
|
+
const prior = previousReplicas.get(record.locator);
|
|
1221
|
+
const confirmed = await confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
|
|
1222
|
+
const replica = await stepped(replicating(confirmed, queuedBatches.get(record.locator) ?? [], prior));
|
|
1223
|
+
await stepped(counting(replica));
|
|
1224
|
+
read.set(record.locator, replica);
|
|
1225
|
+
}
|
|
1226
|
+
const sets = new Map();
|
|
1227
|
+
for (const locator of new Set([...previousReplicas.keys(), ...read.keys()])) {
|
|
1228
|
+
sets.set(locator, await stepped(changing(locator, previousReplicas.get(locator) ?? null, read.get(locator) ?? null)));
|
|
1229
|
+
}
|
|
1130
1230
|
targets = heldTargets;
|
|
1131
1231
|
transfers.clear();
|
|
1132
1232
|
for (const record of staged)
|
|
@@ -1141,26 +1241,24 @@ export function composeRuntime(options) {
|
|
|
1141
1241
|
for (const record of presented)
|
|
1142
1242
|
presentations.set(record.app, record.value);
|
|
1143
1243
|
conversations.hydrate(held, chatDrafts);
|
|
1144
|
-
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
1145
1244
|
pendings.clear();
|
|
1146
1245
|
for (const queue of queues) {
|
|
1147
|
-
for (const { batch, state } of queue.pending)
|
|
1148
|
-
pendings.set(batch.id, state);
|
|
1246
|
+
for (const { batch, state } of queue.pending) {
|
|
1247
|
+
pendings.set(pendingKey(queue.locator, batch.id), state);
|
|
1248
|
+
}
|
|
1149
1249
|
}
|
|
1150
|
-
const previousReplicas = new Map(replicas);
|
|
1151
1250
|
replicas.clear();
|
|
1152
1251
|
queuesWithoutReplica.clear();
|
|
1153
|
-
const replicated = new Set(records.map((record) => record.locator));
|
|
1154
1252
|
for (const [locator, queue] of queuedBatches) {
|
|
1155
|
-
if (!
|
|
1253
|
+
if (!read.has(locator))
|
|
1156
1254
|
queuesWithoutReplica.set(locator, queue);
|
|
1157
1255
|
}
|
|
1158
1256
|
for (const record of records) {
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
const
|
|
1162
|
-
const
|
|
1163
|
-
replicas.set(record.locator,
|
|
1257
|
+
if (fences.has(record.locator))
|
|
1258
|
+
continue;
|
|
1259
|
+
const replica = read.get(record.locator);
|
|
1260
|
+
const { model, selectionModel } = replica.confirmed.record;
|
|
1261
|
+
replicas.set(record.locator, replica);
|
|
1164
1262
|
const existing = rooms.get(record.locator);
|
|
1165
1263
|
if (existing === undefined)
|
|
1166
1264
|
list(record, false);
|
|
@@ -1183,7 +1281,7 @@ export function composeRuntime(options) {
|
|
|
1183
1281
|
return {
|
|
1184
1282
|
lastCompleted,
|
|
1185
1283
|
locators: new Set([...queuedBatches.keys(), ...replicas.keys()]),
|
|
1186
|
-
read: mergeChangeSets([...
|
|
1284
|
+
read: mergeChangeSets([...sets].flatMap(([locator, set]) => (fences.has(locator) ? [] : [set]))),
|
|
1187
1285
|
};
|
|
1188
1286
|
}
|
|
1189
1287
|
async function readState(opened) {
|
|
@@ -1202,7 +1300,7 @@ export function composeRuntime(options) {
|
|
|
1202
1300
|
async function open() {
|
|
1203
1301
|
if (closed)
|
|
1204
1302
|
throw new Error('runtime closed');
|
|
1205
|
-
const opened = await openRuntimeStore(isFenced);
|
|
1303
|
+
const opened = await openRuntimeStore(isFenced, options.database);
|
|
1206
1304
|
store = opened;
|
|
1207
1305
|
const interrupted = await opened.fences();
|
|
1208
1306
|
for (const locator of interrupted) {
|
|
@@ -1225,6 +1323,9 @@ export function composeRuntime(options) {
|
|
|
1225
1323
|
if (interrupted.length > 0)
|
|
1226
1324
|
await erased(opened, interrupted);
|
|
1227
1325
|
moveTo({ ...status, lastCompleted, ...attention() });
|
|
1326
|
+
void exclusive(() => unpair(opened)).catch(() => {
|
|
1327
|
+
journal.record('unpair failed');
|
|
1328
|
+
});
|
|
1228
1329
|
return opened;
|
|
1229
1330
|
}
|
|
1230
1331
|
function durably(work) {
|
|
@@ -1245,28 +1346,92 @@ export function composeRuntime(options) {
|
|
|
1245
1346
|
mutations = next.then(() => undefined, () => undefined);
|
|
1246
1347
|
return next;
|
|
1247
1348
|
}
|
|
1248
|
-
function withdrawable(
|
|
1249
|
-
|
|
1250
|
-
|
|
1349
|
+
function withdrawable(locator) {
|
|
1350
|
+
return (batch) => {
|
|
1351
|
+
const state = pendings.get(pendingKey(locator, batch.id));
|
|
1352
|
+
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
function landedWhole(locator, batch, confirmed, current) {
|
|
1356
|
+
if (!landed(batch, confirmed, current))
|
|
1357
|
+
return false;
|
|
1358
|
+
const partner = partnerOf(batch, locator);
|
|
1359
|
+
if (partner === null)
|
|
1360
|
+
return true;
|
|
1361
|
+
const other = replicas.get(partner);
|
|
1362
|
+
const half = other?.queue.find((one) => one.id === batch.id);
|
|
1363
|
+
return other !== undefined && half !== undefined && landed(half, other.confirmed);
|
|
1364
|
+
}
|
|
1365
|
+
function carriedBack(locator, unlanded, written) {
|
|
1366
|
+
const back = new Map();
|
|
1367
|
+
const taken = [];
|
|
1368
|
+
for (const batch of unlanded) {
|
|
1369
|
+
const between = batch.between;
|
|
1370
|
+
if (between === undefined)
|
|
1371
|
+
continue;
|
|
1372
|
+
const source = between.source.room;
|
|
1373
|
+
const destination = between.destination.room;
|
|
1374
|
+
for (const { path: to } of pendings.get(pendingKey(destination, batch.id))?.drafts ?? []) {
|
|
1375
|
+
const record = roomDrafts.get(destination)?.get(to);
|
|
1376
|
+
const path = between.moves.find((move) => move.to === to)?.path;
|
|
1377
|
+
if (record === undefined || path === undefined)
|
|
1378
|
+
continue;
|
|
1379
|
+
const [into, key] = source === locator ? [written, path] : [back, documentKey(source, path)];
|
|
1380
|
+
const existing = into.get(key) ?? roomDrafts.get(source)?.get(path);
|
|
1381
|
+
taken.push({ locator: destination, path: to });
|
|
1382
|
+
into.set(key, {
|
|
1383
|
+
locator: source,
|
|
1384
|
+
path,
|
|
1385
|
+
revision: roomDraftRevision(),
|
|
1386
|
+
parts: returnedInto(existing?.parts, record.parts),
|
|
1387
|
+
returned: existing?.returned ?? null,
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
const records = [...back.values()];
|
|
1392
|
+
const elsewhere = taken.filter((one) => one.locator !== locator);
|
|
1393
|
+
const rooms = new Set([...records, ...elsewhere].map((one) => one.locator));
|
|
1394
|
+
return {
|
|
1395
|
+
taken: taken.filter((one) => one.locator === locator).map(({ path }) => path),
|
|
1396
|
+
writes: [...rooms].map((room) => ({
|
|
1397
|
+
locator: room,
|
|
1398
|
+
roomDrafts: records.filter((record) => record.locator === room),
|
|
1399
|
+
consumedRoomDrafts: elsewhere.filter((one) => one.locator === room).map(({ path }) => path),
|
|
1400
|
+
})),
|
|
1401
|
+
changes: [...rooms].map((room) => changeSet(room, null, null, {
|
|
1402
|
+
drafts: [...records, ...elsewhere]
|
|
1403
|
+
.filter((one) => one.locator === room)
|
|
1404
|
+
.map(({ path }) => path),
|
|
1405
|
+
})),
|
|
1406
|
+
remember() {
|
|
1407
|
+
for (const one of taken)
|
|
1408
|
+
forgetRoomDraft(one.locator, one.path);
|
|
1409
|
+
for (const record of records)
|
|
1410
|
+
rememberRoomDraft(record);
|
|
1411
|
+
},
|
|
1412
|
+
};
|
|
1251
1413
|
}
|
|
1252
1414
|
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
1253
1415
|
const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
1254
|
-
const { withdrawn, kept } = await stepped(standing(confirmed, queue, withdrawable, localSaves));
|
|
1416
|
+
const { withdrawn, kept } = await stepped(standing(confirmed, queue, withdrawable(locator), localSaves));
|
|
1255
1417
|
const founded = kept.filter((batch) => !queue.includes(batch));
|
|
1256
1418
|
const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
|
|
1257
1419
|
const unqueued = withdrawn.map((batch) => ({
|
|
1258
1420
|
batch,
|
|
1259
|
-
|
|
1421
|
+
landed: landedWhole(locator, batch, confirmed),
|
|
1260
1422
|
}));
|
|
1261
1423
|
const returning = [
|
|
1262
|
-
...[...unqueued].reverse(),
|
|
1424
|
+
...[...unqueued].reverse().map(({ batch, landed }) => ({
|
|
1425
|
+
batch,
|
|
1426
|
+
returned: localSaves || landed ? null : returnedBy('withdrawn'),
|
|
1427
|
+
})),
|
|
1263
1428
|
...(settling.leaving === undefined ? [] : [settling.leaving]),
|
|
1264
1429
|
];
|
|
1265
1430
|
const written = new Map();
|
|
1266
1431
|
for (const { batch, returned } of returning) {
|
|
1267
|
-
if (returned === null)
|
|
1432
|
+
if (returned === null || batch.between?.destination.room === locator)
|
|
1268
1433
|
continue;
|
|
1269
|
-
for (const saved of pendings.get(batch.id)?.drafts ?? []) {
|
|
1434
|
+
for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
|
|
1270
1435
|
const existing = written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path);
|
|
1271
1436
|
written.set(saved.path, {
|
|
1272
1437
|
locator,
|
|
@@ -1277,14 +1442,28 @@ export function composeRuntime(options) {
|
|
|
1277
1442
|
});
|
|
1278
1443
|
}
|
|
1279
1444
|
}
|
|
1280
|
-
|
|
1445
|
+
const carried = carriedBack(locator, [
|
|
1446
|
+
...[...unqueued].reverse().flatMap(({ batch, landed }) => (landed ? [] : [batch])),
|
|
1447
|
+
...(settling.leaving === undefined || settling.leaving.returned === null
|
|
1448
|
+
? []
|
|
1449
|
+
: [settling.leaving.batch]),
|
|
1450
|
+
], written);
|
|
1451
|
+
await local.keepAuthored({
|
|
1452
|
+
locator,
|
|
1453
|
+
queue: kept,
|
|
1454
|
+
founded,
|
|
1455
|
+
roomDrafts: [...written.values()],
|
|
1456
|
+
consumedRoomDrafts: carried.taken,
|
|
1457
|
+
}, ...carried.writes);
|
|
1281
1458
|
const staying = new Set(kept.map((batch) => batch.id));
|
|
1282
1459
|
for (const batch of previousQueue) {
|
|
1283
1460
|
if (!staying.has(batch.id))
|
|
1284
|
-
pendings.delete(batch.id);
|
|
1461
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
1285
1462
|
}
|
|
1286
1463
|
for (const record of written.values())
|
|
1287
1464
|
rememberRoomDraft(record);
|
|
1465
|
+
carried.remember();
|
|
1466
|
+
const drafts = [...written.keys(), ...carried.taken];
|
|
1288
1467
|
try {
|
|
1289
1468
|
await local.write({
|
|
1290
1469
|
next: confirmed.record,
|
|
@@ -1294,31 +1473,32 @@ export function composeRuntime(options) {
|
|
|
1294
1473
|
}
|
|
1295
1474
|
catch (error) {
|
|
1296
1475
|
const held = before === undefined ? null : await stepped(replicating(before.confirmed, kept, before));
|
|
1297
|
-
const set = await stepped(changing(locator, before ?? null, held, {
|
|
1298
|
-
queue: queueChanged,
|
|
1299
|
-
drafts: [...written.keys()],
|
|
1300
|
-
}));
|
|
1476
|
+
const set = await stepped(changing(locator, before ?? null, held, { queue: queueChanged, drafts }));
|
|
1301
1477
|
if (held !== null)
|
|
1302
1478
|
replicas.set(locator, held);
|
|
1303
1479
|
else if (kept.length > 0)
|
|
1304
1480
|
queuesWithoutReplica.set(locator, kept);
|
|
1305
1481
|
else
|
|
1306
1482
|
queuesWithoutReplica.delete(locator);
|
|
1307
|
-
publishChangeSet(set);
|
|
1483
|
+
publishChangeSet(mergeChangeSets([set, ...carried.changes]));
|
|
1308
1484
|
moveTo({ ...status, ...attention() });
|
|
1309
1485
|
throw error;
|
|
1310
1486
|
}
|
|
1311
|
-
const changed = { room: settling.room, queue: queueChanged, drafts
|
|
1487
|
+
const changed = { room: settling.room, queue: queueChanged, drafts };
|
|
1312
1488
|
const next = await stepped(replicating(confirmed, kept, before));
|
|
1489
|
+
await stepped(counting(next));
|
|
1313
1490
|
const set = await stepped(changing(locator, before ?? null, next, changed));
|
|
1314
1491
|
return () => {
|
|
1315
1492
|
queuesWithoutReplica.delete(locator);
|
|
1316
|
-
for (const { batch,
|
|
1317
|
-
const outcome = localSaves ? 'dropped' :
|
|
1493
|
+
for (const { batch, landed } of unqueued) {
|
|
1494
|
+
const outcome = localSaves ? 'dropped' : landed ? 'landed' : 'withdrawn';
|
|
1318
1495
|
journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
|
|
1319
1496
|
}
|
|
1320
1497
|
replicas.set(locator, next);
|
|
1321
|
-
return
|
|
1498
|
+
return mergeChangeSets([
|
|
1499
|
+
heldChange(locator, before ?? null, next, set, changed),
|
|
1500
|
+
...carried.changes,
|
|
1501
|
+
]);
|
|
1322
1502
|
};
|
|
1323
1503
|
}
|
|
1324
1504
|
function heldChange(locator, before, next, set, changed) {
|
|
@@ -1376,7 +1556,7 @@ export function composeRuntime(options) {
|
|
|
1376
1556
|
const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
|
|
1377
1557
|
const roomVersion = confirmed.record.roomVersion;
|
|
1378
1558
|
const accepted = queue.filter((batch) => {
|
|
1379
|
-
const accepted = pendings.get(batch.id)?.accepted;
|
|
1559
|
+
const accepted = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
|
|
1380
1560
|
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
1381
1561
|
});
|
|
1382
1562
|
let settled;
|
|
@@ -1396,7 +1576,7 @@ export function composeRuntime(options) {
|
|
|
1396
1576
|
catching.delete(room.locator);
|
|
1397
1577
|
}
|
|
1398
1578
|
for (const batch of accepted)
|
|
1399
|
-
pendings.delete(batch.id);
|
|
1579
|
+
pendings.delete(pendingKey(room.locator, batch.id));
|
|
1400
1580
|
stale.delete(room.locator);
|
|
1401
1581
|
unindexable.delete(room.locator);
|
|
1402
1582
|
if (unreadable.delete(room.locator))
|
|
@@ -1407,8 +1587,12 @@ export function composeRuntime(options) {
|
|
|
1407
1587
|
if (prior === undefined)
|
|
1408
1588
|
prepared.add(room.locator);
|
|
1409
1589
|
publishChangeSet(set);
|
|
1590
|
+
await unpair(local);
|
|
1410
1591
|
moveTo({ ...status, ...attention() });
|
|
1411
1592
|
options.adapter.roomStream?.published(room.locator, roomVersion);
|
|
1593
|
+
if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
|
|
1594
|
+
submitLater(room.locator);
|
|
1595
|
+
}
|
|
1412
1596
|
return true;
|
|
1413
1597
|
});
|
|
1414
1598
|
}
|
|
@@ -1444,13 +1628,17 @@ export function composeRuntime(options) {
|
|
|
1444
1628
|
}
|
|
1445
1629
|
const target = await savedRoom(locator);
|
|
1446
1630
|
const saved = [];
|
|
1631
|
+
const files = [];
|
|
1447
1632
|
for (const { path, parts } of read) {
|
|
1448
1633
|
const file = parts.file === undefined
|
|
1449
1634
|
? undefined
|
|
1450
1635
|
: await heldFile(parts.file.blob, binaryBase(target.replica.visible, path));
|
|
1636
|
+
if (parts.file !== undefined && file !== undefined) {
|
|
1637
|
+
files.push({ key: file.localId, blob: parts.file.blob });
|
|
1638
|
+
}
|
|
1451
1639
|
saved.push({ path, parts: held(undefined, parts, file) });
|
|
1452
1640
|
}
|
|
1453
|
-
return committed(locator, target, saved, new Set(), writtenBy);
|
|
1641
|
+
return committed(locator, target, saved, new Set(), writtenBy, files);
|
|
1454
1642
|
}
|
|
1455
1643
|
function carriedDrafts(locator, saved, consumed) {
|
|
1456
1644
|
const holding = roomDrafts.get(locator);
|
|
@@ -1464,13 +1652,145 @@ export function composeRuntime(options) {
|
|
|
1464
1652
|
return [[path, { ...record, path: to, revision: roomDraftRevision() }]];
|
|
1465
1653
|
}));
|
|
1466
1654
|
}
|
|
1467
|
-
|
|
1655
|
+
function sideOf(locator, replica, room) {
|
|
1656
|
+
const record = replica.confirmed.record;
|
|
1657
|
+
return {
|
|
1658
|
+
room: locator,
|
|
1659
|
+
handle: room.handle,
|
|
1660
|
+
baseRoomVersion: record.roomVersion,
|
|
1661
|
+
composition: compositionFor(record),
|
|
1662
|
+
operations: room.operations,
|
|
1663
|
+
providerOwnedPaths: room.providerOwnedPaths,
|
|
1664
|
+
visible: replica.visible,
|
|
1665
|
+
queued: queuedDocuments(replica.queue),
|
|
1666
|
+
clean: {
|
|
1667
|
+
text: record.documents,
|
|
1668
|
+
binaries: record.binaries,
|
|
1669
|
+
composition: compositionOf(record),
|
|
1670
|
+
},
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
function movedInto(locator, saved) {
|
|
1674
|
+
const into = saved.find(({ parts }) => parts.move?.room !== undefined && parts.move.room !== locator);
|
|
1675
|
+
return into?.parts.move?.room ?? null;
|
|
1676
|
+
}
|
|
1677
|
+
async function committedMove(locator, destination, { local, replica, room }, saved, consumed, writtenBy, files) {
|
|
1678
|
+
const mixed = saved.find(({ parts }) => Object.keys(parts).length !== 1 || parts.move?.room !== destination);
|
|
1679
|
+
if (mixed !== undefined) {
|
|
1680
|
+
return { outcome: 'refused', code: 'mixed-room-move', retryable: false, file: mixed.path };
|
|
1681
|
+
}
|
|
1682
|
+
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
|
|
1683
|
+
if (changed.length > 0)
|
|
1684
|
+
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
1685
|
+
await referenced(destination);
|
|
1686
|
+
const target = replicas.get(destination);
|
|
1687
|
+
const listedRoom = rooms.get(destination);
|
|
1688
|
+
if (target === undefined || listedRoom === undefined) {
|
|
1689
|
+
throw runtimeError('room-not-held', `No Local Replica for ${destination}`);
|
|
1690
|
+
}
|
|
1691
|
+
const built = await buildMove({
|
|
1692
|
+
source: sideOf(locator, replica, room),
|
|
1693
|
+
destination: sideOf(destination, target, listedRoom),
|
|
1694
|
+
moves: saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) })),
|
|
1695
|
+
providerLimits: limits,
|
|
1696
|
+
local: localSaves,
|
|
1697
|
+
...(writtenBy === undefined ? {} : { writtenBy }),
|
|
1698
|
+
});
|
|
1699
|
+
if (built.kind === 'refused')
|
|
1700
|
+
return { outcome: 'refused', ...built.refusal };
|
|
1701
|
+
const id = built.leaving.id;
|
|
1702
|
+
const carrying = saved.flatMap(({ path, parts }) => {
|
|
1703
|
+
const record = roomDrafts.get(locator)?.get(path);
|
|
1704
|
+
const to = moveTarget(parts.move);
|
|
1705
|
+
if (record === undefined || consumed.has(path) || roomDrafts.get(destination)?.has(to)) {
|
|
1706
|
+
return [];
|
|
1707
|
+
}
|
|
1708
|
+
const moved = {
|
|
1709
|
+
...record,
|
|
1710
|
+
locator: destination,
|
|
1711
|
+
path: to,
|
|
1712
|
+
revision: roomDraftRevision(),
|
|
1713
|
+
};
|
|
1714
|
+
return [{ from: path, record: moved }];
|
|
1715
|
+
});
|
|
1716
|
+
const carried = carrying.map(({ record }) => record);
|
|
1717
|
+
const marked = carried.map(({ path }) => ({ path, parts: {} }));
|
|
1718
|
+
const left = [...consumed, ...carrying.map(({ from }) => from)];
|
|
1719
|
+
const leavingQueue = [...replica.queue, built.leaving];
|
|
1720
|
+
const arrivingQueue = [...target.queue, built.arriving];
|
|
1721
|
+
let fencedRoom;
|
|
1722
|
+
try {
|
|
1723
|
+
fencedRoom = await local.saveAuthored(files, {
|
|
1724
|
+
locator,
|
|
1725
|
+
queue: leavingQueue,
|
|
1726
|
+
saved: new Map([[id, saved]]),
|
|
1727
|
+
consumedRoomDrafts: left,
|
|
1728
|
+
}, {
|
|
1729
|
+
locator: destination,
|
|
1730
|
+
queue: arrivingQueue,
|
|
1731
|
+
saved: new Map([[id, marked]]),
|
|
1732
|
+
roomDrafts: carried,
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1735
|
+
catch {
|
|
1736
|
+
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
1737
|
+
}
|
|
1738
|
+
if (fencedRoom !== null) {
|
|
1739
|
+
throw runtimeError('room-not-held', `No Local Replica for ${fencedRoom}`);
|
|
1740
|
+
}
|
|
1741
|
+
const nextSource = await stepped(replicating(replica.confirmed, leavingQueue, replica));
|
|
1742
|
+
const nextDestination = await stepped(replicating(target.confirmed, arrivingQueue, target));
|
|
1743
|
+
const sets = [
|
|
1744
|
+
await stepped(changing(locator, replica, nextSource, { queue: true, drafts: left })),
|
|
1745
|
+
await stepped(changing(destination, target, nextDestination, {
|
|
1746
|
+
queue: true,
|
|
1747
|
+
drafts: carried.map(({ path }) => path),
|
|
1748
|
+
})),
|
|
1749
|
+
];
|
|
1750
|
+
if (fences.has(locator) || fences.has(destination)) {
|
|
1751
|
+
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1752
|
+
}
|
|
1753
|
+
pendings.set(pendingKey(locator, id), {
|
|
1754
|
+
drafts: saved,
|
|
1755
|
+
submission: { state: 'waiting' },
|
|
1756
|
+
lost: false,
|
|
1757
|
+
});
|
|
1758
|
+
pendings.set(pendingKey(destination, id), {
|
|
1759
|
+
drafts: marked,
|
|
1760
|
+
submission: { state: 'waiting' },
|
|
1761
|
+
lost: false,
|
|
1762
|
+
});
|
|
1763
|
+
for (const path of left)
|
|
1764
|
+
forgetRoomDraft(locator, path);
|
|
1765
|
+
for (const record of carried)
|
|
1766
|
+
rememberRoomDraft(record);
|
|
1767
|
+
replicas.set(locator, nextSource);
|
|
1768
|
+
replicas.set(destination, nextDestination);
|
|
1769
|
+
publishChangeSet(mergeChangeSets(sets));
|
|
1770
|
+
queuedSave(locator);
|
|
1771
|
+
return { outcome: 'saved', identifier: id };
|
|
1772
|
+
}
|
|
1773
|
+
function queuedSave(locator) {
|
|
1774
|
+
if (!localSaves)
|
|
1775
|
+
recorded = false;
|
|
1776
|
+
moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
|
|
1777
|
+
submitLater(locator);
|
|
1778
|
+
}
|
|
1779
|
+
async function committed(locator, target, saved, consumed, writtenBy, files = []) {
|
|
1780
|
+
const into = movedInto(locator, saved);
|
|
1781
|
+
if (into !== null) {
|
|
1782
|
+
return committedMove(locator, into, target, saved, consumed, writtenBy, files);
|
|
1783
|
+
}
|
|
1784
|
+
const { local, replica, room } = target;
|
|
1468
1785
|
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
|
|
1469
1786
|
if (changed.length > 0)
|
|
1470
1787
|
return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
|
|
1471
1788
|
const parts = saved.map(({ path, parts }) => changesOf(path, parts));
|
|
1472
1789
|
const changes = parts.flatMap(({ changes }) => changes);
|
|
1473
|
-
const
|
|
1790
|
+
const visibleOver = function* (queue) {
|
|
1791
|
+
return (yield* replicating(replica.confirmed, queue, replica)).visible;
|
|
1792
|
+
};
|
|
1793
|
+
const requeue = await stepped(requeuing(replica.queue, changes, replica.visible, withdrawable(locator), visibleOver));
|
|
1474
1794
|
if (requeue.kind === 'refused')
|
|
1475
1795
|
return { outcome: 'refused', ...requeue.refusal };
|
|
1476
1796
|
const foreign = [...requeue.withdrawn.keys()].some((origin) => {
|
|
@@ -1548,7 +1868,7 @@ export function composeRuntime(options) {
|
|
|
1548
1868
|
for (const [origin, replaced] of withdrawn) {
|
|
1549
1869
|
if (replaced === null)
|
|
1550
1870
|
continue;
|
|
1551
|
-
const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
|
|
1871
|
+
const carried = withdrawnDrafts(pendings.get(pendingKey(locator, origin))?.drafts ?? [], changes);
|
|
1552
1872
|
drafted.set(replaced.id, replaced === join?.batch
|
|
1553
1873
|
? [
|
|
1554
1874
|
...carried.map((held) => joinedDraft(held, saved)),
|
|
@@ -1562,15 +1882,16 @@ export function composeRuntime(options) {
|
|
|
1562
1882
|
for (const replaced of withdrawn.values()) {
|
|
1563
1883
|
if (replaced === null || !batches.includes(replaced))
|
|
1564
1884
|
continue;
|
|
1565
|
-
const limited =
|
|
1885
|
+
const limited = await stepped(limiting(batches, replaced, limits, visibleOver));
|
|
1566
1886
|
if (limited !== null)
|
|
1567
1887
|
return { outcome: 'refused', ...limited };
|
|
1568
1888
|
}
|
|
1569
1889
|
const carried = carriedDrafts(locator, saved, consumed);
|
|
1570
1890
|
const left = [...consumed, ...carried.keys()];
|
|
1571
1891
|
const arrived = [...carried.values()];
|
|
1892
|
+
let fencedRoom;
|
|
1572
1893
|
try {
|
|
1573
|
-
await local.
|
|
1894
|
+
fencedRoom = await local.saveAuthored(files, {
|
|
1574
1895
|
locator,
|
|
1575
1896
|
queue: batches,
|
|
1576
1897
|
saved: drafted,
|
|
@@ -1581,6 +1902,9 @@ export function composeRuntime(options) {
|
|
|
1581
1902
|
catch {
|
|
1582
1903
|
return { outcome: 'refused', code: 'durable-write', retryable: true };
|
|
1583
1904
|
}
|
|
1905
|
+
if (fencedRoom !== null) {
|
|
1906
|
+
throw runtimeError('room-not-held', `No Local Replica for ${fencedRoom}`);
|
|
1907
|
+
}
|
|
1584
1908
|
const next = await stepped(replicating(replica.confirmed, batches, replica));
|
|
1585
1909
|
const set = await stepped(changing(locator, replica, next, {
|
|
1586
1910
|
queue: true,
|
|
@@ -1589,39 +1913,43 @@ export function composeRuntime(options) {
|
|
|
1589
1913
|
if (fences.has(locator))
|
|
1590
1914
|
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1591
1915
|
for (const origin of withdrawn.keys())
|
|
1592
|
-
pendings.delete(origin);
|
|
1916
|
+
pendings.delete(pendingKey(locator, origin));
|
|
1593
1917
|
for (const [id, drafts] of drafted) {
|
|
1594
|
-
pendings.set(
|
|
1918
|
+
pendings.set(pendingKey(locator, id), {
|
|
1919
|
+
drafts,
|
|
1920
|
+
submission: { state: 'waiting' },
|
|
1921
|
+
lost: false,
|
|
1922
|
+
});
|
|
1595
1923
|
}
|
|
1596
|
-
if (!localSaves)
|
|
1597
|
-
recorded = false;
|
|
1598
1924
|
for (const path of left)
|
|
1599
1925
|
forgetRoomDraft(locator, path);
|
|
1600
1926
|
for (const record of arrived)
|
|
1601
1927
|
rememberRoomDraft(record);
|
|
1602
1928
|
replicas.set(locator, next);
|
|
1603
1929
|
publishChangeSet(set);
|
|
1604
|
-
|
|
1605
|
-
submitLater(locator);
|
|
1930
|
+
queuedSave(locator);
|
|
1606
1931
|
const identifier = join?.batch.id ??
|
|
1607
1932
|
built?.batch.id ??
|
|
1608
1933
|
[...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
|
|
1609
1934
|
return { outcome: 'saved', identifier };
|
|
1610
1935
|
}
|
|
1611
|
-
async function mark(local, locator,
|
|
1612
|
-
const
|
|
1613
|
-
|
|
1614
|
-
state
|
|
1615
|
-
(
|
|
1616
|
-
|
|
1936
|
+
async function mark(local, locator, batch, submission, lost) {
|
|
1937
|
+
const partner = partnerOf(batch, locator);
|
|
1938
|
+
for (const held of partner === null ? [locator] : [locator, partner]) {
|
|
1939
|
+
const state = pendings.get(pendingKey(held, batch.id));
|
|
1940
|
+
if (!continuing() ||
|
|
1941
|
+
state === undefined ||
|
|
1942
|
+
(JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
|
|
1943
|
+
continue;
|
|
1944
|
+
}
|
|
1945
|
+
await local.submit(held, batch.id, submission, lost);
|
|
1946
|
+
if (fences.has(held))
|
|
1947
|
+
continue;
|
|
1948
|
+
pendings.set(pendingKey(held, batch.id), { ...state, submission, lost });
|
|
1949
|
+
const replica = replicas.get(held);
|
|
1950
|
+
if (replica !== undefined)
|
|
1951
|
+
publishChangeSet(changeSet(held, replica, replica, { queue: true }));
|
|
1617
1952
|
}
|
|
1618
|
-
await local.submit(locator, identifier, submission, lost);
|
|
1619
|
-
if (fences.has(locator))
|
|
1620
|
-
return;
|
|
1621
|
-
pendings.set(identifier, { ...state, submission, lost });
|
|
1622
|
-
const replica = replicas.get(locator);
|
|
1623
|
-
if (replica !== undefined)
|
|
1624
|
-
publishChangeSet(changeSet(locator, replica, replica, { queue: true }));
|
|
1625
1953
|
moveTo({ ...status, ...attention() });
|
|
1626
1954
|
}
|
|
1627
1955
|
async function keepUpload(local, locator, identifier, upload) {
|
|
@@ -1630,7 +1958,8 @@ export function composeRuntime(options) {
|
|
|
1630
1958
|
await local.keepUpload(locator, identifier, upload);
|
|
1631
1959
|
if (fences.has(locator))
|
|
1632
1960
|
throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
|
|
1633
|
-
|
|
1961
|
+
const key = pendingKey(locator, identifier);
|
|
1962
|
+
pendings.set(key, { ...pendings.get(key), upload });
|
|
1634
1963
|
if (!continuing())
|
|
1635
1964
|
throw new Error('connection refused');
|
|
1636
1965
|
}
|
|
@@ -1676,19 +2005,61 @@ export function composeRuntime(options) {
|
|
|
1676
2005
|
return UNCERTAIN;
|
|
1677
2006
|
}
|
|
1678
2007
|
}
|
|
1679
|
-
function
|
|
2008
|
+
function located(room, path, destination) {
|
|
2009
|
+
const crossing = roomTarget(destination);
|
|
2010
|
+
if (crossing === null) {
|
|
2011
|
+
const named = target(path, destination);
|
|
2012
|
+
return named === null || !rooms.has(room) ? null : { room, path: named };
|
|
2013
|
+
}
|
|
2014
|
+
const held = [...rooms.values()].find((listed) => listed.handle === crossing.handle);
|
|
2015
|
+
return held === undefined ? null : { room: held.locator, path: crossing.path };
|
|
2016
|
+
}
|
|
2017
|
+
async function resolveReference(room, path, destination) {
|
|
2018
|
+
if (roomTarget(destination) === null && target(path, destination) === null) {
|
|
2019
|
+
return ABSENT_TARGET;
|
|
2020
|
+
}
|
|
2021
|
+
const at = located(room, path, destination);
|
|
2022
|
+
if (at === null)
|
|
2023
|
+
return UNREADABLE;
|
|
2024
|
+
if (locked(at.room))
|
|
2025
|
+
return LOCKED_UNREADABLE;
|
|
2026
|
+
const visible = replicas.get(at.room)?.visible;
|
|
2027
|
+
if (visible?.documents[at.path] !== undefined || visible?.binaries[at.path] !== undefined) {
|
|
2028
|
+
return { kind: 'document', ...at };
|
|
2029
|
+
}
|
|
2030
|
+
const history = await readHistory(at.room, at.path);
|
|
2031
|
+
if (history.kind === 'no-history' || history.kind === 'revision')
|
|
2032
|
+
return ABSENT_TARGET;
|
|
2033
|
+
if (history.kind !== 'history')
|
|
2034
|
+
return history;
|
|
2035
|
+
const [last] = history.revisions;
|
|
2036
|
+
if (last === undefined)
|
|
2037
|
+
return ABSENT_TARGET;
|
|
2038
|
+
return history.deleted === true
|
|
2039
|
+
? { kind: 'deleted', ...at, revision: last }
|
|
2040
|
+
: { kind: 'document', ...at };
|
|
2041
|
+
}
|
|
2042
|
+
async function queuedBlob(locator, path, localId) {
|
|
1680
2043
|
for (const batch of replicas.get(locator)?.queue ?? []) {
|
|
1681
2044
|
const change = batch.changes[queuedFile(batch, path, localId)];
|
|
1682
2045
|
if (change?.kind === 'binary')
|
|
1683
|
-
return change.
|
|
2046
|
+
return (await openLocal()).file(localId, change.mediaType);
|
|
1684
2047
|
}
|
|
1685
|
-
|
|
2048
|
+
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2049
|
+
return drafted?.localId === localId
|
|
2050
|
+
? (await openLocal()).file(localId, drafted.mediaType)
|
|
2051
|
+
: null;
|
|
2052
|
+
}
|
|
2053
|
+
function batchFile(batch) {
|
|
2054
|
+
return async (localId) => {
|
|
2055
|
+
const change = batch.changes.find((held) => held.kind === 'binary' && held.localId === localId);
|
|
2056
|
+
return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
|
|
2057
|
+
};
|
|
1686
2058
|
}
|
|
1687
2059
|
function binaryRoom(request) {
|
|
1688
2060
|
if (request.kind === 'path')
|
|
1689
2061
|
return request.locator;
|
|
1690
|
-
|
|
1691
|
-
return scope?.kind === 'room' ? scope.locator : null;
|
|
2062
|
+
return options.adapter.referrerScope(request.referrer)?.locator ?? null;
|
|
1692
2063
|
}
|
|
1693
2064
|
async function transferBinary(request, transfer, answer) {
|
|
1694
2065
|
if (closed || refusal !== null)
|
|
@@ -1765,11 +2136,14 @@ export function composeRuntime(options) {
|
|
|
1765
2136
|
}
|
|
1766
2137
|
async function readBinaryPath(locator, path, revision) {
|
|
1767
2138
|
await ready;
|
|
1768
|
-
const
|
|
2139
|
+
const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
|
|
2140
|
+
const asked = drafted !== undefined && drafted.localId === revision
|
|
2141
|
+
? { kind: 'pending', localId: revision }
|
|
2142
|
+
: binaryPath(locator, path, revision);
|
|
1769
2143
|
if (asked === null)
|
|
1770
2144
|
return absentBinary(path);
|
|
1771
2145
|
if (asked.kind === 'pending') {
|
|
1772
|
-
const blob = queuedBlob(locator, path, asked.localId);
|
|
2146
|
+
const blob = await queuedBlob(locator, path, asked.localId);
|
|
1773
2147
|
return blob === null ? absentBinary(path) : { kind: 'binary', blob };
|
|
1774
2148
|
}
|
|
1775
2149
|
return readBinary(asked);
|
|
@@ -1805,6 +2179,13 @@ export function composeRuntime(options) {
|
|
|
1805
2179
|
}
|
|
1806
2180
|
return undefined;
|
|
1807
2181
|
}
|
|
2182
|
+
function calledRooms(name, input) {
|
|
2183
|
+
const member = declaredAction(name)?.room;
|
|
2184
|
+
if (member === undefined || input === null || typeof input !== 'object')
|
|
2185
|
+
return [];
|
|
2186
|
+
const named = input[member];
|
|
2187
|
+
return (Array.isArray(named) ? named : [named]).filter((room) => typeof room === 'string');
|
|
2188
|
+
}
|
|
1808
2189
|
async function invokeAction(name, input, dispatching) {
|
|
1809
2190
|
await ready;
|
|
1810
2191
|
if (closed || refusal !== null)
|
|
@@ -2143,15 +2524,16 @@ export function composeRuntime(options) {
|
|
|
2143
2524
|
? accepted.roomVersion
|
|
2144
2525
|
: held, accepted.roomVersion);
|
|
2145
2526
|
if (applied.kind !== 'staged') {
|
|
2146
|
-
const
|
|
2527
|
+
const key = pendingKey(room.locator, submitted.id);
|
|
2528
|
+
const state = pendings.get(key);
|
|
2147
2529
|
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
2148
2530
|
if (state !== undefined && !fences.has(room.locator))
|
|
2149
|
-
pendings.set(
|
|
2531
|
+
pendings.set(key, { ...state, accepted: accepted.roomVersion });
|
|
2150
2532
|
emitQueue(room.locator);
|
|
2151
2533
|
return applied;
|
|
2152
2534
|
}
|
|
2153
2535
|
const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
2154
|
-
pendings.delete(submitted.id);
|
|
2536
|
+
pendings.delete(pendingKey(room.locator, submitted.id));
|
|
2155
2537
|
unreadable.delete(room.locator);
|
|
2156
2538
|
sourceRefusals.delete(room.locator);
|
|
2157
2539
|
declarationRefusals.delete(room.locator);
|
|
@@ -2162,22 +2544,150 @@ export function composeRuntime(options) {
|
|
|
2162
2544
|
return null;
|
|
2163
2545
|
}
|
|
2164
2546
|
async function adopted(room, local, submitted, accepted) {
|
|
2165
|
-
const failed = await exclusive(() =>
|
|
2547
|
+
const failed = await exclusive(async () => {
|
|
2548
|
+
const adopting = await adoptAccepted(room, local, submitted, accepted);
|
|
2549
|
+
await unpair(local);
|
|
2550
|
+
return adopting;
|
|
2551
|
+
});
|
|
2166
2552
|
if (failed === null)
|
|
2167
2553
|
return true;
|
|
2168
2554
|
if (failed.kind === 'refused' && continuing())
|
|
2169
2555
|
refusedSource(room, failed);
|
|
2170
2556
|
return false;
|
|
2171
2557
|
}
|
|
2558
|
+
async function adoptedMove(room, partner, local, submitted, accepted) {
|
|
2559
|
+
const other = discovered.get(partner);
|
|
2560
|
+
const failed = await exclusive(async () => {
|
|
2561
|
+
const left = await adoptAccepted(room, local, submitted, accepted);
|
|
2562
|
+
const arriving = replicas.get(partner)?.queue.find((batch) => batch.id === submitted.id);
|
|
2563
|
+
const arrived = other === undefined || arriving === undefined || accepted.destination === undefined
|
|
2564
|
+
? null
|
|
2565
|
+
: await adoptAccepted(other, local, arriving, {
|
|
2566
|
+
kind: 'accepted',
|
|
2567
|
+
...accepted.destination,
|
|
2568
|
+
});
|
|
2569
|
+
if (other !== undefined && arrived?.kind === 'refused' && continuing()) {
|
|
2570
|
+
refusedSource(other, arrived);
|
|
2571
|
+
}
|
|
2572
|
+
await unpair(local);
|
|
2573
|
+
return left;
|
|
2574
|
+
});
|
|
2575
|
+
submitLater(partner);
|
|
2576
|
+
if (failed === null)
|
|
2577
|
+
return true;
|
|
2578
|
+
if (failed.kind === 'refused' && continuing())
|
|
2579
|
+
refusedSource(room, failed);
|
|
2580
|
+
return false;
|
|
2581
|
+
}
|
|
2582
|
+
function queueIn(locator) {
|
|
2583
|
+
return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
|
|
2584
|
+
}
|
|
2585
|
+
function behindIn(locator, batch) {
|
|
2586
|
+
const held = replicas.get(locator)?.queue;
|
|
2587
|
+
if (held === undefined)
|
|
2588
|
+
return queuesWithoutReplica.get(locator) !== undefined;
|
|
2589
|
+
return held.findIndex((one) => one.id === batch.id) > 0;
|
|
2590
|
+
}
|
|
2591
|
+
function unpaired(skipped) {
|
|
2592
|
+
for (const locator of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
|
|
2593
|
+
for (const batch of queueIn(locator)) {
|
|
2594
|
+
const partner = partnerOf(batch, locator);
|
|
2595
|
+
if (partner === null || skipped.has(pendingKey(locator, batch.id)))
|
|
2596
|
+
continue;
|
|
2597
|
+
if (queueIn(partner).some((held) => held.id === batch.id))
|
|
2598
|
+
continue;
|
|
2599
|
+
if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
|
|
2600
|
+
continue;
|
|
2601
|
+
if (leavingIn(batch, locator) && !withdrawable(locator)(batch))
|
|
2602
|
+
continue;
|
|
2603
|
+
return { locator, batch };
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
return null;
|
|
2607
|
+
}
|
|
2608
|
+
async function unpair(local) {
|
|
2609
|
+
const handled = new Set();
|
|
2610
|
+
for (let held = unpaired(handled); held !== null && continuing(); held = unpaired(handled)) {
|
|
2611
|
+
const { locator, batch } = held;
|
|
2612
|
+
handled.add(pendingKey(locator, batch.id));
|
|
2613
|
+
journal.record(`outcome ${locator} ${batch.id} ${localSaves ? 'dropped' : 'unpaired'}`);
|
|
2614
|
+
const leaving = {
|
|
2615
|
+
batch,
|
|
2616
|
+
returned: !localSaves && leavingIn(batch, locator) ? returnedBy('withdrawn') : null,
|
|
2617
|
+
};
|
|
2618
|
+
const current = replicas.get(locator);
|
|
2619
|
+
if (current !== undefined) {
|
|
2620
|
+
const settled = await settle(local, locator, current, current.confirmed, current.queue.filter((one) => one.id !== batch.id), { leaving });
|
|
2621
|
+
publishChangeSet(settled());
|
|
2622
|
+
continue;
|
|
2623
|
+
}
|
|
2624
|
+
const queue = queueIn(locator).filter((one) => one.id !== batch.id);
|
|
2625
|
+
const written = new Map((leaving.returned === null
|
|
2626
|
+
? []
|
|
2627
|
+
: (pendings.get(pendingKey(locator, batch.id))?.drafts ?? [])).map((saved) => [
|
|
2628
|
+
saved.path,
|
|
2629
|
+
{
|
|
2630
|
+
locator,
|
|
2631
|
+
path: saved.path,
|
|
2632
|
+
revision: roomDraftRevision(),
|
|
2633
|
+
parts: returnedInto(roomDrafts.get(locator)?.get(saved.path)?.parts, saved.parts),
|
|
2634
|
+
returned: leaving.returned,
|
|
2635
|
+
},
|
|
2636
|
+
]));
|
|
2637
|
+
const carried = carriedBack(locator, leaving.returned === null ? [] : [batch], written);
|
|
2638
|
+
await local.keepAuthored({
|
|
2639
|
+
locator,
|
|
2640
|
+
queue,
|
|
2641
|
+
roomDrafts: [...written.values()],
|
|
2642
|
+
consumedRoomDrafts: carried.taken,
|
|
2643
|
+
}, ...carried.writes);
|
|
2644
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
2645
|
+
if (queue.length > 0)
|
|
2646
|
+
queuesWithoutReplica.set(locator, queue);
|
|
2647
|
+
else
|
|
2648
|
+
queuesWithoutReplica.delete(locator);
|
|
2649
|
+
for (const record of written.values())
|
|
2650
|
+
rememberRoomDraft(record);
|
|
2651
|
+
carried.remember();
|
|
2652
|
+
publishChangeSet(mergeChangeSets([
|
|
2653
|
+
changeSet(locator, null, null, {
|
|
2654
|
+
queue: true,
|
|
2655
|
+
drafts: [...written.keys(), ...carried.taken],
|
|
2656
|
+
}),
|
|
2657
|
+
...carried.changes,
|
|
2658
|
+
]));
|
|
2659
|
+
}
|
|
2660
|
+
if (handled.size === 0)
|
|
2661
|
+
return;
|
|
2662
|
+
moveTo({ ...status, ...attention() });
|
|
2663
|
+
for (const key of handled)
|
|
2664
|
+
submitLater(key.slice(0, key.indexOf('\u0000')));
|
|
2665
|
+
}
|
|
2666
|
+
async function discardHeld(head, room, local, signal) {
|
|
2667
|
+
const partner = partnerOf(head, room.locator);
|
|
2668
|
+
const other = partner === null ? undefined : discovered.get(partner);
|
|
2669
|
+
await discardPending(room, local, signal);
|
|
2670
|
+
if (other !== undefined && !signal.aborted)
|
|
2671
|
+
await discardPending(other, local, signal);
|
|
2672
|
+
}
|
|
2172
2673
|
async function submitRoom(room, local, authorized, progress, signal) {
|
|
2173
2674
|
while (continuing() && authorization !== undefined && !signal.aborted) {
|
|
2174
2675
|
const replica = replicas.get(room.locator);
|
|
2175
2676
|
const head = replica?.queue[0];
|
|
2176
2677
|
if (replica === undefined || head === undefined)
|
|
2177
2678
|
return 'done';
|
|
2178
|
-
|
|
2679
|
+
const state = pendings.get(pendingKey(room.locator, head.id));
|
|
2680
|
+
if (state?.accepted !== undefined)
|
|
2681
|
+
return 'done';
|
|
2682
|
+
const partner = partnerOf(head, room.locator);
|
|
2683
|
+
if (partner !== null && !leavingIn(head, room.locator)) {
|
|
2684
|
+
submitLater(partner);
|
|
2179
2685
|
return 'done';
|
|
2180
|
-
|
|
2686
|
+
}
|
|
2687
|
+
if (partner !== null && behindIn(partner, head)) {
|
|
2688
|
+
progress.complete = false;
|
|
2689
|
+
return 'done';
|
|
2690
|
+
}
|
|
2181
2691
|
const { submission, lost: earlier } = state ?? {
|
|
2182
2692
|
submission: { state: 'waiting' },
|
|
2183
2693
|
lost: false,
|
|
@@ -2193,16 +2703,16 @@ export function composeRuntime(options) {
|
|
|
2193
2703
|
}
|
|
2194
2704
|
const lost = earlier || submission.state === 'submitted';
|
|
2195
2705
|
if (lost && pastHorizon(head.id)) {
|
|
2196
|
-
await
|
|
2706
|
+
await discardHeld(head, room, local, signal);
|
|
2197
2707
|
return 'done';
|
|
2198
2708
|
}
|
|
2199
2709
|
let result;
|
|
2200
2710
|
journal.record(`submission ${room.locator} ${head.id}`);
|
|
2201
|
-
await exclusive(() => mark(local, room.locator, head
|
|
2711
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
|
|
2202
2712
|
if (discoveryPaused || fences.has(room.locator))
|
|
2203
2713
|
return 'done';
|
|
2204
2714
|
if (signal.aborted || writesHeld()) {
|
|
2205
|
-
await exclusive(() => mark(local, room.locator, head
|
|
2715
|
+
await exclusive(() => mark(local, room.locator, head, submission, earlier));
|
|
2206
2716
|
if (signal.aborted)
|
|
2207
2717
|
return 'done';
|
|
2208
2718
|
progress.complete = false;
|
|
@@ -2210,11 +2720,17 @@ export function composeRuntime(options) {
|
|
|
2210
2720
|
return 'done';
|
|
2211
2721
|
}
|
|
2212
2722
|
const opensBefore = openSignals;
|
|
2213
|
-
const
|
|
2723
|
+
const answeredHere = awaitingAnswer(room.locator);
|
|
2724
|
+
const answeredThere = partner === null ? () => { } : awaitingAnswer(partner);
|
|
2725
|
+
const answered = () => {
|
|
2726
|
+
answeredHere();
|
|
2727
|
+
answeredThere();
|
|
2728
|
+
};
|
|
2214
2729
|
try {
|
|
2215
2730
|
result = await options.adapter.mutate(authorized, {
|
|
2216
2731
|
locator: room.locator,
|
|
2217
2732
|
changeBatch: head,
|
|
2733
|
+
file: batchFile(head),
|
|
2218
2734
|
...(state?.upload === undefined ? {} : { upload: state.upload }),
|
|
2219
2735
|
}, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
|
|
2220
2736
|
}
|
|
@@ -2239,9 +2755,10 @@ export function composeRuntime(options) {
|
|
|
2239
2755
|
return 'again';
|
|
2240
2756
|
}
|
|
2241
2757
|
if (result.kind === 'room-not-readable') {
|
|
2758
|
+
const unread = discovered.get(result.locator) ?? room;
|
|
2242
2759
|
progress.complete = false;
|
|
2243
|
-
unreadable.add(
|
|
2244
|
-
list(
|
|
2760
|
+
unreadable.add(unread.locator);
|
|
2761
|
+
list(unread, false);
|
|
2245
2762
|
emitRooms();
|
|
2246
2763
|
return 'done';
|
|
2247
2764
|
}
|
|
@@ -2256,24 +2773,30 @@ export function composeRuntime(options) {
|
|
|
2256
2773
|
}
|
|
2257
2774
|
if (result.kind === 'write-halted') {
|
|
2258
2775
|
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
2259
|
-
await exclusive(() => mark(local, room.locator, head
|
|
2776
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
|
|
2260
2777
|
if (serviceMode === 'open' && openSignals !== opensBefore)
|
|
2261
2778
|
continue;
|
|
2262
2779
|
haltedRefusal = true;
|
|
2263
2780
|
progress.complete = false;
|
|
2264
2781
|
return 'done';
|
|
2265
2782
|
}
|
|
2783
|
+
const other = partner === null ? undefined : discovered.get(partner);
|
|
2266
2784
|
if (result.kind === 'conflicted' ||
|
|
2267
2785
|
result.kind === 'rejected' ||
|
|
2268
2786
|
result.kind === 'expired' ||
|
|
2269
2787
|
result.kind === 'retryable-refusal') {
|
|
2270
2788
|
if (result.kind === 'expired' && lost && result.restored !== true) {
|
|
2271
2789
|
journal.record(`outcome ${room.locator} ${head.id} expired`);
|
|
2272
|
-
await
|
|
2790
|
+
await discardHeld(head, room, local, signal);
|
|
2273
2791
|
return 'done';
|
|
2274
2792
|
}
|
|
2275
2793
|
if (result.kind === 'expired') {
|
|
2276
|
-
await exclusive(() => mark(local, room.locator, head
|
|
2794
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
|
|
2795
|
+
const there = other === undefined ? null : await advance(other, local, authorized, continuing);
|
|
2796
|
+
if (other !== undefined && there !== null) {
|
|
2797
|
+
await withhold(there, other, local, progress);
|
|
2798
|
+
return 'done';
|
|
2799
|
+
}
|
|
2277
2800
|
const withheld = await advance(room, local, authorized, continuing);
|
|
2278
2801
|
if (withheld !== null) {
|
|
2279
2802
|
await withhold(withheld, room, local, progress);
|
|
@@ -2290,11 +2813,25 @@ export function composeRuntime(options) {
|
|
|
2290
2813
|
if (result.kind === 'retryable-refusal') {
|
|
2291
2814
|
const reset = result.reset;
|
|
2292
2815
|
progress.complete = false;
|
|
2293
|
-
await exclusive(() => mark(local, room.locator, head
|
|
2816
|
+
await exclusive(() => mark(local, room.locator, head, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
2294
2817
|
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
2295
2818
|
}
|
|
2296
2819
|
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
2297
2820
|
const left = leaving(head, result);
|
|
2821
|
+
const refreshed = async () => ({
|
|
2822
|
+
there: other === undefined ? null : await advance(other, local, authorized, continuing),
|
|
2823
|
+
here: await advance(room, local, authorized, continuing),
|
|
2824
|
+
});
|
|
2825
|
+
const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
|
|
2826
|
+
if (judged !== null && (judged.there !== null || judged.here !== null)) {
|
|
2827
|
+
await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
|
|
2828
|
+
if (other !== undefined && judged.there !== null) {
|
|
2829
|
+
await withhold(judged.there, other, local, progress);
|
|
2830
|
+
}
|
|
2831
|
+
if (judged.here !== null)
|
|
2832
|
+
await withhold(judged.here, room, local, progress);
|
|
2833
|
+
return 'done';
|
|
2834
|
+
}
|
|
2298
2835
|
await exclusive(async () => {
|
|
2299
2836
|
if (!continuing())
|
|
2300
2837
|
return;
|
|
@@ -2303,24 +2840,30 @@ export function composeRuntime(options) {
|
|
|
2303
2840
|
return;
|
|
2304
2841
|
}
|
|
2305
2842
|
const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
2306
|
-
leaving: (result.kind === 'expired' &&
|
|
2307
|
-
(result.kind === 'conflicted' &&
|
|
2843
|
+
leaving: (result.kind === 'expired' && landedWhole(room.locator, head, current.confirmed)) ||
|
|
2844
|
+
(result.kind === 'conflicted' &&
|
|
2845
|
+
landedWhole(room.locator, head, current.confirmed, result.affected))
|
|
2308
2846
|
? { ...left, returned: null }
|
|
2309
2847
|
: left,
|
|
2310
2848
|
});
|
|
2311
2849
|
publishChangeSet(settled());
|
|
2850
|
+
await unpair(local);
|
|
2312
2851
|
moveTo({ ...status, ...attention() });
|
|
2313
2852
|
});
|
|
2314
2853
|
if (result.kind === 'expired')
|
|
2315
2854
|
continue;
|
|
2316
|
-
const
|
|
2317
|
-
if (
|
|
2318
|
-
await withhold(
|
|
2855
|
+
const { there, here } = judged ?? (await refreshed());
|
|
2856
|
+
if (other !== undefined && there !== null)
|
|
2857
|
+
await withhold(there, other, local, progress);
|
|
2858
|
+
if (here !== null) {
|
|
2859
|
+
await withhold(here, room, local, progress);
|
|
2319
2860
|
return 'done';
|
|
2320
2861
|
}
|
|
2321
2862
|
continue;
|
|
2322
2863
|
}
|
|
2323
|
-
const kept = await
|
|
2864
|
+
const kept = await (partner === null
|
|
2865
|
+
? adopted(room, local, head, result)
|
|
2866
|
+
: adoptedMove(room, partner, local, head, result)).finally(answered);
|
|
2324
2867
|
if (!kept) {
|
|
2325
2868
|
progress.complete = false;
|
|
2326
2869
|
return 'done';
|
|
@@ -2347,9 +2890,9 @@ export function composeRuntime(options) {
|
|
|
2347
2890
|
fences.delete(locator);
|
|
2348
2891
|
}
|
|
2349
2892
|
for (const batch of queue)
|
|
2350
|
-
pendings.delete(batch.id);
|
|
2893
|
+
pendings.delete(pendingKey(locator, batch.id));
|
|
2351
2894
|
queuesWithoutReplica.delete(locator);
|
|
2352
|
-
const after = before === undefined ? null :
|
|
2895
|
+
const after = before === undefined ? null : await stepped(replicating(before.confirmed, [], before));
|
|
2353
2896
|
if (after !== null)
|
|
2354
2897
|
replicas.set(locator, after);
|
|
2355
2898
|
journal.record(`room discarded ${locator}`);
|
|
@@ -2411,7 +2954,7 @@ export function composeRuntime(options) {
|
|
|
2411
2954
|
...(attached === 'collection' ? { model: next } : { selectionModel: next }),
|
|
2412
2955
|
};
|
|
2413
2956
|
const reclassified = sameMembership(declared, held.confirmed.record)
|
|
2414
|
-
? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
|
|
2957
|
+
? { kind: 'staged', confirmed: await confirmedOf(declared, held.confirmed) }
|
|
2415
2958
|
: await readReclassified(declared, held.confirmed);
|
|
2416
2959
|
const record = reclassified.kind === 'staged'
|
|
2417
2960
|
? declared
|
|
@@ -2420,12 +2963,13 @@ export function composeRuntime(options) {
|
|
|
2420
2963
|
typedSelection: held.confirmed.record.typedSelection,
|
|
2421
2964
|
};
|
|
2422
2965
|
const read = reclassified.kind === 'deferred'
|
|
2423
|
-
? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
|
|
2966
|
+
? { kind: 'staged', confirmed: await confirmedOf(record, held.confirmed) }
|
|
2424
2967
|
: reclassified;
|
|
2425
2968
|
await local.write({ next: record, previous: held.confirmed.record });
|
|
2426
2969
|
const replica = read.kind === 'staged'
|
|
2427
2970
|
? await stepped(replicating(read.confirmed, held.queue, held))
|
|
2428
2971
|
: { ...held, confirmed: { ...held.confirmed, record } };
|
|
2972
|
+
await stepped(counting(replica));
|
|
2429
2973
|
const set = await stepped(changing(room.locator, held, replica, { room: true }));
|
|
2430
2974
|
replicas.set(room.locator, replica);
|
|
2431
2975
|
changed = heldChange(room.locator, held, replica, set, { room: true });
|
|
@@ -2453,7 +2997,7 @@ export function composeRuntime(options) {
|
|
|
2453
2997
|
});
|
|
2454
2998
|
}
|
|
2455
2999
|
async function scoped(result, room, local) {
|
|
2456
|
-
if (
|
|
3000
|
+
if (result.scope === 'content' && !result.retryable) {
|
|
2457
3001
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
2458
3002
|
await loseRooms([room.locator]);
|
|
2459
3003
|
return;
|
|
@@ -2471,15 +3015,7 @@ export function composeRuntime(options) {
|
|
|
2471
3015
|
});
|
|
2472
3016
|
else if (result.scope !== 'operation') {
|
|
2473
3017
|
unreadable.add(room.locator);
|
|
2474
|
-
scopedAway = {
|
|
2475
|
-
...room,
|
|
2476
|
-
replica: { ...room.replica, admission: 'unavailable' },
|
|
2477
|
-
operations: {
|
|
2478
|
-
...room.operations,
|
|
2479
|
-
...(result.scope === 'content' ? { read: [] } : {}),
|
|
2480
|
-
...(result.scope === 'sync' ? { sync: [] } : {}),
|
|
2481
|
-
},
|
|
2482
|
-
};
|
|
3018
|
+
scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
|
|
2483
3019
|
await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
|
|
2484
3020
|
}
|
|
2485
3021
|
if (!continuing())
|
|
@@ -2639,12 +3175,13 @@ export function composeRuntime(options) {
|
|
|
2639
3175
|
}
|
|
2640
3176
|
for (const room of result.rooms) {
|
|
2641
3177
|
if (room.operations === undefined ||
|
|
2642
|
-
!['
|
|
3178
|
+
!['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
2643
3179
|
room.operations[operation].every((scope) => typeof scope === 'string')) ||
|
|
2644
3180
|
room.replica === undefined ||
|
|
2645
3181
|
typeof room.replica.selection !== 'string' ||
|
|
2646
3182
|
room.replica.selection.length === 0 ||
|
|
2647
|
-
!['admitted', 'unavailable'].includes(room.replica.admission)
|
|
3183
|
+
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
3184
|
+
!['locked', 'open'].includes(room.replica.offline)) {
|
|
2648
3185
|
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
2649
3186
|
await refuse('missing-required-guarantee');
|
|
2650
3187
|
notifyReady();
|
|
@@ -2806,6 +3343,7 @@ export function composeRuntime(options) {
|
|
|
2806
3343
|
function closedSocket(revoked, lost) {
|
|
2807
3344
|
const opened = socketOpen;
|
|
2808
3345
|
socketOpen = false;
|
|
3346
|
+
connectionLost();
|
|
2809
3347
|
if (lost) {
|
|
2810
3348
|
reads.lost();
|
|
2811
3349
|
conversations.lost();
|
|
@@ -3130,6 +3668,7 @@ export function composeRuntime(options) {
|
|
|
3130
3668
|
return;
|
|
3131
3669
|
if (event.kind === 'opened') {
|
|
3132
3670
|
socketOpen = true;
|
|
3671
|
+
connectionLive();
|
|
3133
3672
|
generation += 1;
|
|
3134
3673
|
reads.opened();
|
|
3135
3674
|
conversations.reopened();
|
|
@@ -3216,6 +3755,8 @@ export function composeRuntime(options) {
|
|
|
3216
3755
|
closed = true;
|
|
3217
3756
|
if (rechecking !== null)
|
|
3218
3757
|
clearInterval(rechecking);
|
|
3758
|
+
connectionLive();
|
|
3759
|
+
lockListeners.clear();
|
|
3219
3760
|
readinessWatches.clear();
|
|
3220
3761
|
withdrawStream?.();
|
|
3221
3762
|
withdrawStream = undefined;
|
|
@@ -3301,14 +3842,23 @@ export function composeRuntime(options) {
|
|
|
3301
3842
|
stop();
|
|
3302
3843
|
};
|
|
3303
3844
|
}
|
|
3304
|
-
async function searched(input, reach) {
|
|
3845
|
+
async function searched(input, reach, locked) {
|
|
3305
3846
|
const asked = searchPlanOf(input);
|
|
3306
3847
|
const plan = { ...asked, limit: capped(asked.limit) };
|
|
3307
|
-
const filter =
|
|
3848
|
+
const filter = {
|
|
3849
|
+
...filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)),
|
|
3850
|
+
locked,
|
|
3851
|
+
};
|
|
3852
|
+
const unlocking = (filter.rooms ?? [])
|
|
3853
|
+
.filter((room) => locked.includes(room))
|
|
3854
|
+
.map((room) => ({ room, awaits: 'unlock' }));
|
|
3308
3855
|
for (;;) {
|
|
3309
3856
|
const cover = covered(filter);
|
|
3310
3857
|
awaitPreparation(cover);
|
|
3311
|
-
const pending =
|
|
3858
|
+
const pending = [
|
|
3859
|
+
...unlocking,
|
|
3860
|
+
...('pending' in cover ? cover.pending : []),
|
|
3861
|
+
];
|
|
3312
3862
|
const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
|
|
3313
3863
|
const held = covered({
|
|
3314
3864
|
...filter,
|
|
@@ -3351,7 +3901,11 @@ export function composeRuntime(options) {
|
|
|
3351
3901
|
if (searchIndex.changes() !== seen ||
|
|
3352
3902
|
JSON.stringify(covered(answering)) !== JSON.stringify(answered))
|
|
3353
3903
|
continue;
|
|
3354
|
-
const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms
|
|
3904
|
+
const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms !== null
|
|
3905
|
+
? new Set(answering.rooms)
|
|
3906
|
+
: locked.length === 0
|
|
3907
|
+
? null
|
|
3908
|
+
: new Set(reachedRooms(answering)), plan.collection
|
|
3355
3909
|
? new Map(answered.coverage.rooms.map(({ room }) => [
|
|
3356
3910
|
room,
|
|
3357
3911
|
replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
|
|
@@ -3475,7 +4029,7 @@ export function composeRuntime(options) {
|
|
|
3475
4029
|
return listed();
|
|
3476
4030
|
},
|
|
3477
4031
|
query: ((input) => rowsQuery(planned(input, null))),
|
|
3478
|
-
search: (input) => searched(input, null),
|
|
4032
|
+
search: (input) => searched(input, null, []),
|
|
3479
4033
|
types(locator) {
|
|
3480
4034
|
return {
|
|
3481
4035
|
collection: modelTypes(modelOf(locator)),
|
|
@@ -3519,7 +4073,9 @@ export function composeRuntime(options) {
|
|
|
3519
4073
|
parts: held(current?.parts, parts, file),
|
|
3520
4074
|
returned: current?.returned ?? null,
|
|
3521
4075
|
};
|
|
3522
|
-
await local.keepRoomDraft(record
|
|
4076
|
+
await local.keepRoomDraft(record, parts.file === undefined || file === undefined
|
|
4077
|
+
? []
|
|
4078
|
+
: [{ key: file.localId, blob: parts.file.blob }]);
|
|
3523
4079
|
rememberRoomDraft(record);
|
|
3524
4080
|
publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
|
|
3525
4081
|
return { outcome: 'written', roomDraft: roomDraftOf(record) };
|
|
@@ -3568,13 +4124,18 @@ export function composeRuntime(options) {
|
|
|
3568
4124
|
history(locator, path, query) {
|
|
3569
4125
|
return readHistory(locator, path, query);
|
|
3570
4126
|
},
|
|
4127
|
+
resolve(room, path, destination) {
|
|
4128
|
+
return resolveReference(room, path, destination);
|
|
4129
|
+
},
|
|
3571
4130
|
binaryFile: {
|
|
3572
4131
|
read: readBinaryPath,
|
|
3573
4132
|
link: linkBinaryPath,
|
|
3574
4133
|
readReference: readBinaryReference,
|
|
3575
4134
|
},
|
|
3576
|
-
call(name, input) {
|
|
3577
|
-
|
|
4135
|
+
async call(name, input) {
|
|
4136
|
+
await ready;
|
|
4137
|
+
const room = calledRooms(name, input).find((one) => locked(one));
|
|
4138
|
+
return room === undefined ? recoveredAction(name, input) : lockedRoom(room);
|
|
3578
4139
|
},
|
|
3579
4140
|
reached(locator, roomVersion, path) {
|
|
3580
4141
|
return path === undefined
|
|
@@ -3619,6 +4180,10 @@ export function composeRuntime(options) {
|
|
|
3619
4180
|
watchPlan,
|
|
3620
4181
|
search: searched,
|
|
3621
4182
|
watchReadiness,
|
|
4183
|
+
locked,
|
|
4184
|
+
lockedRooms,
|
|
4185
|
+
watchLocks,
|
|
4186
|
+
located,
|
|
3622
4187
|
};
|
|
3623
4188
|
return {
|
|
3624
4189
|
md,
|