@agentshouse/mdruntime 0.1.0-ca7c26c6bc27e → 0.1.0-ca8707258fdde
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/README.md +25 -12
- package/dist/acceptance.d.ts +26 -0
- package/dist/acceptance.d.ts.map +1 -0
- package/dist/acceptance.js +384 -0
- package/dist/adapter.d.ts +200 -54
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +1 -9
- package/dist/attached-model.d.ts +7 -10
- package/dist/attached-model.d.ts.map +1 -1
- package/dist/attached-model.js +40 -28
- package/dist/attempt.d.ts +16 -0
- package/dist/attempt.d.ts.map +1 -0
- package/dist/attempt.js +70 -0
- package/dist/binary.d.ts +18 -0
- package/dist/binary.d.ts.map +1 -0
- package/dist/binary.js +8 -0
- package/dist/bootstrap.d.ts +37 -5
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +336 -51
- package/dist/chain.d.ts +12 -0
- package/dist/chain.d.ts.map +1 -0
- package/dist/chain.js +64 -0
- package/dist/change-set.d.ts +9 -14
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +11 -56
- package/dist/change.d.ts +46 -0
- package/dist/change.d.ts.map +1 -1
- package/dist/change.js +34 -0
- package/dist/chat-types.d.ts +3 -0
- package/dist/chat-types.d.ts.map +1 -0
- package/dist/chat-types.js +2 -0
- package/dist/client.d.ts +50 -18
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +162 -30
- package/dist/collection.d.ts +13 -14
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +52 -56
- package/dist/compile.d.ts +16 -0
- package/dist/compile.d.ts.map +1 -0
- package/dist/compile.js +717 -0
- package/dist/connection.d.ts +61 -43
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +2377 -1476
- package/dist/context.d.ts +2 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +7 -0
- package/dist/conversation-row.d.ts +33 -0
- package/dist/conversation-row.d.ts.map +1 -0
- package/dist/conversation-row.js +47 -0
- package/dist/conversation.d.ts +71 -22
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +996 -273
- package/dist/digest.d.ts +7 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +113 -0
- package/dist/durability.d.ts +10 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/durability.js +5 -0
- package/dist/entry.d.ts +20 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +161 -0
- package/dist/expectation.d.ts +21 -0
- package/dist/expectation.d.ts.map +1 -0
- package/dist/expectation.js +70 -0
- package/dist/failure.d.ts +13 -0
- package/dist/failure.d.ts.map +1 -0
- package/dist/failure.js +19 -0
- package/dist/figures.d.ts +48 -0
- package/dist/figures.d.ts.map +1 -0
- package/dist/figures.js +197 -0
- package/dist/file.d.ts +2 -19
- package/dist/file.d.ts.map +1 -1
- package/dist/file.js +7 -122
- package/dist/host.d.ts +11 -2
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +134 -34
- package/dist/identity.d.ts +3 -0
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +13 -1
- package/dist/index.d.ts +31 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -0
- package/dist/install.d.ts +86 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +537 -0
- package/dist/job.d.ts +33 -0
- package/dist/job.d.ts.map +1 -0
- package/dist/job.js +54 -0
- package/dist/live.d.ts +23 -0
- package/dist/live.d.ts.map +1 -0
- package/dist/live.js +165 -0
- package/dist/log.d.ts +1 -0
- package/dist/log.d.ts.map +1 -1
- package/dist/media.d.ts +8 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +218 -0
- package/dist/memory.d.ts +22 -8
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +299 -94
- package/dist/messages.d.ts +52 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +168 -0
- package/dist/model.d.ts +17 -12
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +9 -9
- package/dist/name.d.ts +4 -0
- package/dist/name.d.ts.map +1 -0
- package/dist/name.js +122 -0
- package/dist/observation.d.ts +22 -4
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +120 -14
- package/dist/open.d.ts +2 -0
- package/dist/open.d.ts.map +1 -0
- package/dist/open.js +1 -0
- package/dist/opened.d.ts +22 -0
- package/dist/opened.d.ts.map +1 -0
- package/dist/opened.js +107 -0
- package/dist/outcome.d.ts +73 -5
- package/dist/outcome.d.ts.map +1 -1
- package/dist/outcome.js +84 -1
- package/dist/overlay.d.ts +16 -5
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +147 -180
- package/dist/parts.d.ts +59 -0
- package/dist/parts.d.ts.map +1 -0
- package/dist/parts.js +118 -0
- package/dist/paths.d.ts +1 -1
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +4 -16
- package/dist/query.d.ts +52 -181
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +3 -423
- package/dist/questions.d.ts +32 -0
- package/dist/questions.d.ts.map +1 -0
- package/dist/questions.js +118 -0
- package/dist/reach.d.ts +19 -5
- package/dist/reach.d.ts.map +1 -1
- package/dist/reach.js +309 -92
- package/dist/react.d.ts +46 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +54 -0
- package/dist/read.d.ts +7 -0
- package/dist/read.d.ts.map +1 -0
- package/dist/read.js +17 -0
- package/dist/readiness.d.ts +50 -0
- package/dist/readiness.d.ts.map +1 -0
- package/dist/readiness.js +27 -0
- package/dist/reads.d.ts +35 -0
- package/dist/reads.d.ts.map +1 -0
- package/dist/reads.js +128 -0
- package/dist/recovery.d.ts +26 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +232 -0
- package/dist/refresh.d.ts +20 -7
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +155 -59
- package/dist/replica.d.ts +58 -34
- package/dist/replica.d.ts.map +1 -1
- package/dist/replica.js +355 -162
- package/dist/room-draft.d.ts +59 -11
- package/dist/room-draft.d.ts.map +1 -1
- package/dist/room-draft.js +276 -80
- package/dist/room-drafts.d.ts +29 -0
- package/dist/room-drafts.d.ts.map +1 -0
- package/dist/room-drafts.js +216 -0
- package/dist/rows.d.ts +32 -34
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +65 -87
- package/dist/save.d.ts +74 -6
- package/dist/save.d.ts.map +1 -1
- package/dist/save.js +902 -182
- package/dist/saves.d.ts +96 -0
- package/dist/saves.d.ts.map +1 -0
- package/dist/saves.js +999 -0
- package/dist/search.d.ts +15 -54
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +10 -121
- package/dist/seed.d.ts +30 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +141 -0
- package/dist/shared.d.ts +15 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +421 -0
- package/dist/socket/admission.d.ts +7 -0
- package/dist/socket/admission.d.ts.map +1 -0
- package/dist/socket/admission.js +39 -0
- package/dist/socket/answers.d.ts +6 -0
- package/dist/socket/answers.d.ts.map +1 -0
- package/dist/socket/answers.js +20 -0
- package/dist/socket/reads.d.ts +10 -0
- package/dist/socket/reads.d.ts.map +1 -0
- package/dist/socket/reads.js +31 -0
- package/dist/socket.d.ts +43 -0
- package/dist/socket.d.ts.map +1 -0
- package/dist/socket.js +96 -0
- package/dist/source.d.ts +61 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +81 -0
- package/dist/status.d.ts +7 -4
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +3 -15
- package/dist/store/fences.d.ts +16 -0
- package/dist/store/fences.d.ts.map +1 -0
- package/dist/store/fences.js +26 -0
- package/dist/store/installs.d.ts +30 -0
- package/dist/store/installs.d.ts.map +1 -0
- package/dist/store/installs.js +34 -0
- package/dist/store/records.d.ts +212 -0
- package/dist/store/records.d.ts.map +1 -0
- package/dist/store/records.js +408 -0
- package/dist/store/room-held.d.ts +8 -0
- package/dist/store/room-held.d.ts.map +1 -0
- package/dist/store/room-held.js +17 -0
- package/dist/store/watchers.d.ts +15 -0
- package/dist/store/watchers.d.ts.map +1 -0
- package/dist/store/watchers.js +36 -0
- package/dist/store/writes.d.ts +24 -0
- package/dist/store/writes.d.ts.map +1 -0
- package/dist/store/writes.js +125 -0
- package/dist/store.d.ts +77 -93
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +118 -389
- package/dist/stored.d.ts +156 -0
- package/dist/stored.d.ts.map +1 -0
- package/dist/stored.js +637 -0
- package/dist/tables.d.ts +407 -0
- package/dist/tables.d.ts.map +1 -0
- package/dist/tables.js +724 -0
- package/dist/tallies.d.ts +18 -0
- package/dist/tallies.d.ts.map +1 -0
- package/dist/tallies.js +129 -0
- package/dist/transfer.d.ts +0 -5
- package/dist/transfer.d.ts.map +1 -1
- package/dist/turn.d.ts +9 -0
- package/dist/turn.d.ts.map +1 -0
- package/dist/turn.js +75 -0
- package/dist/validation.d.ts +29 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +104 -0
- package/dist/windows.d.ts +11 -0
- package/dist/windows.d.ts.map +1 -0
- package/dist/windows.js +16 -0
- package/dist/wire.d.ts +185 -45
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +75 -46
- package/package.json +33 -10
- package/AGENTS.md +0 -215
- package/dist/attachment.d.ts +0 -3
- package/dist/attachment.d.ts.map +0 -1
- package/dist/attachment.js +0 -8
- package/dist/read-side.d.ts +0 -18
- package/dist/read-side.d.ts.map +0 -1
- package/dist/read-side.js +0 -377
package/dist/connection.js
CHANGED
|
@@ -1,27 +1,35 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { identityOf, roomTarget, target, } from '@agentshouse/mdmodel';
|
|
2
|
+
import { builtInModel, roomTypesOf } from './attached-model.js';
|
|
3
|
+
import { modelTypes, NO_MODEL } from './model.js';
|
|
3
4
|
import { observing } from './observation.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
5
|
+
import { planWithin, } from './query.js';
|
|
6
|
+
import { createFigures, ROOM_TOTALS } from './figures.js';
|
|
7
|
+
import { createLiveQueries } from './live.js';
|
|
8
|
+
import { keepEntrySeed, seedApplies, seededResult, watchSeeded, } from './seed.js';
|
|
9
|
+
import { documentKey, roomOfKey } from './rows.js';
|
|
10
|
+
import { searchPlanOf } from './search.js';
|
|
11
|
+
import { capped, MAXIMUM_WINDOW } from './windows.js';
|
|
12
|
+
import { behind, catching, caughtUpTo, forgetInstall, installationOf, publish, pullBootstrap, pullRefresh, reachedBy, replaced, roomInFlight, standingAt, writtenAfter, } from './install.js';
|
|
9
13
|
import { changeSet, mergeChangeSets } from './change-set.js';
|
|
10
14
|
import { createConversationStore, } from './conversation.js';
|
|
11
|
-
import {
|
|
15
|
+
import { composedOf, validRoot } from './collection.js';
|
|
12
16
|
import { createLog } from './log.js';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { detectStorageLoss, nextGeneration } from './durability.js';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
17
|
+
import { ABSENT_TARGET, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, OFFLINE, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
|
|
18
|
+
import { readAnswers } from './reads.js';
|
|
19
|
+
import { createRecovery } from './recovery.js';
|
|
20
|
+
import { storedRecord } from './replica.js';
|
|
21
|
+
import { same, STARTING, } from './status.js';
|
|
22
|
+
import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
|
|
23
|
+
import { readinessOf, } from './readiness.js';
|
|
24
|
+
import { yielded } from './turn.js';
|
|
25
|
+
import { beneath } from './paths.js';
|
|
26
|
+
import { createStore, } from './store.js';
|
|
27
|
+
import { emitPresentation, presentationSurface, publishRoomDrafts, roomDraftSurface, watchPresentation, watchRoomDrafts, } from './room-drafts.js';
|
|
28
|
+
import { createSocket } from './socket.js';
|
|
22
29
|
import { unconfirmed } from './transfer.js';
|
|
23
30
|
import { governs, holds, narrowed, planReach, } from './reach.js';
|
|
24
|
-
|
|
31
|
+
import { binaryFileSurface, overlayDiff, preparedAfter, replayed, saveParts, saveRoomDrafts, settle, sliceAt, } from './saves.js';
|
|
32
|
+
import { submitAll, submitLater, unpair } from './acceptance.js';
|
|
25
33
|
function within(root, path) {
|
|
26
34
|
return root === '' || beneath(root, path);
|
|
27
35
|
}
|
|
@@ -35,200 +43,448 @@ function declaredSelection(room) {
|
|
|
35
43
|
root !== '' &&
|
|
36
44
|
selection.roots.every((other, at) => at === index || !within(other, root)));
|
|
37
45
|
}
|
|
46
|
+
const OPENING_LANE = 'opening';
|
|
47
|
+
const CONVERSATION_LANE = 'conversations';
|
|
48
|
+
const ROOM_READS = 256;
|
|
49
|
+
const ROOM_ATTEMPTS = 16;
|
|
50
|
+
const ERASE_BATCH = 512;
|
|
51
|
+
const QUERY_BYTES = 1 << 20;
|
|
52
|
+
// A Room the Provider answers not ready is asked again after these waits, the last repeating, so a chunk build that catches up within seconds installs at once and a long one is asked every 15 seconds.
|
|
53
|
+
const NOT_READY_MS = [1_000, 2_000, 3_000, 5_000, 8_000, 11_000, 15_000];
|
|
54
|
+
// 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.
|
|
55
|
+
const LOCK_GRACE_MS = 120_000;
|
|
56
|
+
function operationOf(input) {
|
|
57
|
+
const operation = input?.operation_id;
|
|
58
|
+
return typeof operation === 'string' ? operation : null;
|
|
59
|
+
}
|
|
38
60
|
export function composeRuntime(options) {
|
|
61
|
+
const localSaves = options.adapter.localSaves === true;
|
|
62
|
+
const roomTypes = roomTypesOf(options.adapter.roomTypes, options.adapter.nameableRoomTypes);
|
|
39
63
|
let authorization = options.authorization;
|
|
40
|
-
let
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const presentations = new Map();
|
|
46
|
-
let limits = [];
|
|
47
|
-
let capabilities = [];
|
|
48
|
-
function rewriting() {
|
|
49
|
-
return capabilities.some((capability) => capability.name === 'move-reference-rewrites')
|
|
50
|
-
? 'provider'
|
|
51
|
-
: 'runtime';
|
|
52
|
-
}
|
|
53
|
-
const rooms = new Map();
|
|
64
|
+
let discoveries = 0;
|
|
65
|
+
const usableWhenHeld = options.usableWhenHeld ?? [];
|
|
66
|
+
const build = options.build ?? '';
|
|
67
|
+
const declared = new Map();
|
|
68
|
+
const unheldModels = new Map();
|
|
54
69
|
const unreadable = new Set();
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
const invalidModels = new Set();
|
|
70
|
+
const builtIn = builtInModel(roomTypes);
|
|
71
|
+
const typedPaths = options.adapter.typedPaths ?? [];
|
|
58
72
|
const discovered = new Map();
|
|
73
|
+
const journal = createLog();
|
|
74
|
+
const database = {
|
|
75
|
+
async ask(request) {
|
|
76
|
+
try {
|
|
77
|
+
return await options.database.ask(request);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (refusedForSpace(error))
|
|
81
|
+
filled();
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
const socket = createSocket(options.adapter);
|
|
87
|
+
const store = createStore({
|
|
88
|
+
database,
|
|
89
|
+
roomTypes,
|
|
90
|
+
builtIn,
|
|
91
|
+
typedPaths,
|
|
92
|
+
opening: open,
|
|
93
|
+
unpublished,
|
|
94
|
+
});
|
|
95
|
+
const { covering, durably, exclusive, composed, publication } = store;
|
|
96
|
+
const openLocal = store.open;
|
|
97
|
+
const isFenced = store.fences.has;
|
|
98
|
+
const storedRooms = store.rooms;
|
|
99
|
+
const recordAt = store.record;
|
|
100
|
+
const workOf = store.work;
|
|
101
|
+
const replicaOf = store.replica;
|
|
102
|
+
const draftsOf = store.drafts;
|
|
103
|
+
const draftOf = store.draft;
|
|
104
|
+
const reads = socket.reads;
|
|
59
105
|
let roomsListed = false;
|
|
60
106
|
const scopedRefusals = new Map();
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return
|
|
65
|
-
}
|
|
66
|
-
function modelOf(locator, attached = 'collection') {
|
|
67
|
-
return models.get(modelKey(locator, attached)) ?? NO_MODEL;
|
|
68
|
-
}
|
|
69
|
-
function savedModel(locator, attached) {
|
|
70
|
-
return invalidModels.has(modelKey(locator, attached))
|
|
71
|
-
? { status: 'unavailable', reason: 'revoked' }
|
|
72
|
-
: modelOf(locator, attached);
|
|
107
|
+
function modelIn(locator, stored) {
|
|
108
|
+
if (refusal?.code === 'revoked-authorization')
|
|
109
|
+
return { status: 'unavailable', reason: 'revoked' };
|
|
110
|
+
return stored?.record.model ?? unheldModels.get(locator) ?? NO_MODEL;
|
|
73
111
|
}
|
|
74
112
|
function compositionFor(record) {
|
|
75
|
-
return
|
|
113
|
+
return composed({
|
|
76
114
|
...record,
|
|
77
|
-
model:
|
|
78
|
-
selectionModel: savedModel(record.locator, 'typed-selection'),
|
|
115
|
+
model: modelIn(record.locator, { record, indexed: true, documents: 0 }),
|
|
79
116
|
});
|
|
80
117
|
}
|
|
81
118
|
let refusal = null;
|
|
82
119
|
let storageLoss = null;
|
|
83
|
-
let status =
|
|
120
|
+
let status = { state: 'syncing', providerUnreachable: false, resting: false };
|
|
84
121
|
let shown = STARTING;
|
|
85
122
|
let discoveryPaused = false;
|
|
86
|
-
let resetTimer;
|
|
87
123
|
let closed = false;
|
|
88
124
|
let stopping = false;
|
|
89
|
-
let withdrawStream;
|
|
90
|
-
const announced = new Map();
|
|
91
125
|
let underway = null;
|
|
92
|
-
|
|
93
|
-
const
|
|
126
|
+
let streamed = null;
|
|
127
|
+
const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
|
|
128
|
+
const repairing = new Set();
|
|
129
|
+
const calling = new Set();
|
|
130
|
+
const repairable = new Map();
|
|
131
|
+
const unready = new Map();
|
|
94
132
|
let complete = false;
|
|
95
133
|
let recorded = false;
|
|
96
134
|
const stale = new Set();
|
|
97
|
-
|
|
98
|
-
|
|
135
|
+
const reloadsDue = new Set();
|
|
136
|
+
const lanes = new Map();
|
|
137
|
+
const roomLanes = new Map();
|
|
138
|
+
let roomWork = 0;
|
|
139
|
+
const roomReads = { free: ROOM_READS, waiting: [] };
|
|
140
|
+
const working = new Set();
|
|
99
141
|
const roomListeners = new Set();
|
|
142
|
+
const unlistedRooms = new Set();
|
|
100
143
|
const refusalListeners = new Set();
|
|
101
144
|
const storageLossListeners = new Set();
|
|
102
145
|
const statusListeners = new Set();
|
|
103
146
|
const queueSubscriptions = new Set();
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
147
|
+
const liveQueries = createLiveQueries((plan, cover) => answeredPlan(plan, cover), () => covering());
|
|
148
|
+
const figures = createFigures({
|
|
149
|
+
cover: figuresCover,
|
|
150
|
+
rooms: async (rooms, since) => database.ask({
|
|
151
|
+
kind: 'replica.tallies',
|
|
152
|
+
totals: { ...(await planned(ROOM_TOTALS, null)), limit: null },
|
|
153
|
+
rooms,
|
|
154
|
+
since,
|
|
155
|
+
}),
|
|
156
|
+
conversations: (conversations, since) => database.ask({ kind: 'conversation.tallies', conversations, since }),
|
|
157
|
+
});
|
|
158
|
+
let currencyQueued = null;
|
|
159
|
+
const confirmed = new Set();
|
|
160
|
+
let lockGrace = null;
|
|
161
|
+
const lockListeners = new Set();
|
|
110
162
|
let serviceMode = 'open';
|
|
111
163
|
let haltedRefusal = false;
|
|
112
164
|
let openSignals = 0;
|
|
113
165
|
const serviceModeListeners = new Set();
|
|
166
|
+
const memberListeners = new Set();
|
|
167
|
+
let authorityChanges = 0;
|
|
168
|
+
const authorityChangeListeners = new Set();
|
|
169
|
+
const unpreparedDemand = new Set();
|
|
170
|
+
const unpreparable = new Set();
|
|
171
|
+
let preparing = false;
|
|
172
|
+
let persistence = options.shell.persistence;
|
|
173
|
+
let heldWhenOpened = false;
|
|
174
|
+
let full = false;
|
|
175
|
+
let probing = null;
|
|
176
|
+
let testing = null;
|
|
177
|
+
const readinessWatches = new Set();
|
|
178
|
+
let readinessQueued = false;
|
|
114
179
|
const conversations = createConversationStore({
|
|
115
|
-
cards: declaredCards,
|
|
116
|
-
documents: (locator) => readSide.query(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
|
|
117
180
|
call: (name, input) => invokeAction(name, input),
|
|
181
|
+
history: (request) => readConversationHistory(request),
|
|
118
182
|
durably,
|
|
183
|
+
local: () => openLocal(),
|
|
184
|
+
again: () => {
|
|
185
|
+
void ready.then(scheduleCurrency);
|
|
186
|
+
},
|
|
187
|
+
settled: () => {
|
|
188
|
+
show();
|
|
189
|
+
},
|
|
119
190
|
stream: () => options.adapter.conversations,
|
|
120
191
|
publish: publishChangeSet,
|
|
192
|
+
online: providerReached,
|
|
193
|
+
full: () => full,
|
|
121
194
|
closed: () => closed,
|
|
195
|
+
unanswered: resendLater,
|
|
122
196
|
});
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
197
|
+
const lastAnswers = readAnswers({
|
|
198
|
+
database,
|
|
199
|
+
build,
|
|
200
|
+
unkept: options.adapter.unkeptReads ?? [],
|
|
201
|
+
recovery: recovery.scope(),
|
|
202
|
+
call: (name, input) => md.call(name, input),
|
|
203
|
+
online: providerReached,
|
|
204
|
+
closed: () => closed,
|
|
205
|
+
full: () => full,
|
|
206
|
+
filled,
|
|
128
207
|
});
|
|
129
|
-
|
|
130
|
-
let notifyReady = () => { };
|
|
208
|
+
let readyFromStore = () => { };
|
|
131
209
|
const ready = new Promise((resolve) => {
|
|
132
|
-
|
|
210
|
+
readyFromStore = resolve;
|
|
133
211
|
});
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
room.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
212
|
+
let notifyDiscovered = () => { };
|
|
213
|
+
const firstDiscovery = new Promise((resolve) => {
|
|
214
|
+
notifyDiscovered = resolve;
|
|
215
|
+
});
|
|
216
|
+
let roomsNamed = false;
|
|
217
|
+
const seeding = {
|
|
218
|
+
store,
|
|
219
|
+
reached: reads.reached,
|
|
220
|
+
listed: () => roomsListed,
|
|
221
|
+
declares: (room) => declared.has(room),
|
|
222
|
+
};
|
|
223
|
+
const drafting = {
|
|
224
|
+
store,
|
|
225
|
+
closed: () => closed,
|
|
226
|
+
publishChangeSet,
|
|
227
|
+
statusShown: () => moveTo(status),
|
|
228
|
+
};
|
|
229
|
+
const saver = {
|
|
230
|
+
store,
|
|
231
|
+
socket,
|
|
232
|
+
journal,
|
|
233
|
+
localSaves,
|
|
234
|
+
ready,
|
|
235
|
+
firstDiscovery,
|
|
236
|
+
full: () => full,
|
|
237
|
+
closed: () => closed,
|
|
238
|
+
refused: () => refusal !== null,
|
|
239
|
+
roomsNamed: () => roomsNamed,
|
|
240
|
+
authorized: () => authorization,
|
|
241
|
+
ensureAuthorization,
|
|
242
|
+
connectionNow,
|
|
243
|
+
scoped,
|
|
244
|
+
refuse,
|
|
245
|
+
compositionFor,
|
|
246
|
+
prepare,
|
|
247
|
+
publishChangeSet,
|
|
248
|
+
pendingShown,
|
|
249
|
+
submitLater: (locator) => submitLater(accepting, locator),
|
|
250
|
+
};
|
|
251
|
+
const accepting = {
|
|
252
|
+
...saver,
|
|
253
|
+
recovery,
|
|
254
|
+
stopping: () => stopping,
|
|
255
|
+
paused: () => discoveryPaused,
|
|
256
|
+
continuing,
|
|
257
|
+
discovered: (locator) => discovered.get(locator),
|
|
258
|
+
readable(room) {
|
|
259
|
+
unreadable.delete(room.locator);
|
|
260
|
+
list(room, true);
|
|
261
|
+
},
|
|
262
|
+
unreadable: roomUnreadable,
|
|
263
|
+
advanced,
|
|
264
|
+
withhold,
|
|
265
|
+
writesHeld,
|
|
266
|
+
openSignals: () => openSignals,
|
|
267
|
+
haltedSince,
|
|
268
|
+
rest,
|
|
269
|
+
statusShown: () => moveTo(status),
|
|
270
|
+
};
|
|
271
|
+
const installer = {
|
|
272
|
+
store,
|
|
273
|
+
socket,
|
|
274
|
+
journal,
|
|
275
|
+
roomTypes,
|
|
276
|
+
builtIn,
|
|
277
|
+
typedPaths,
|
|
278
|
+
unheldModel: (locator) => unheldModels.get(locator),
|
|
279
|
+
recover: recoverReads,
|
|
280
|
+
queueReadiness,
|
|
281
|
+
sweep,
|
|
282
|
+
settle: (local, locator, work, record, queue, settling) => settle(saver, local, locator, work, record, queue, settling),
|
|
283
|
+
readied,
|
|
284
|
+
modelKept: (locator) => {
|
|
285
|
+
unheldModels.delete(locator);
|
|
286
|
+
},
|
|
287
|
+
name,
|
|
288
|
+
usable(room) {
|
|
289
|
+
stale.delete(room.locator);
|
|
290
|
+
unpreparable.delete(room.locator);
|
|
291
|
+
if (unreadable.delete(room.locator))
|
|
292
|
+
scopedRefusals.delete(room.locator);
|
|
293
|
+
list(room, true);
|
|
294
|
+
},
|
|
295
|
+
publishChangeSet,
|
|
296
|
+
unpair: (local) => unpair(accepting, local),
|
|
297
|
+
statusShown: () => moveTo(status),
|
|
298
|
+
submitLater: (locator) => submitLater(accepting, locator),
|
|
299
|
+
};
|
|
300
|
+
function notifyReady() {
|
|
301
|
+
notifyDiscovered();
|
|
302
|
+
readyFromStore();
|
|
303
|
+
}
|
|
304
|
+
async function attended() {
|
|
305
|
+
const held = await (await openLocal()).status();
|
|
306
|
+
const attentionRequired = held.returned.flatMap((record) => record.returned === null || isFenced(record.locator)
|
|
307
|
+
? []
|
|
308
|
+
: [{ room: record.locator, path: record.path, returned: structuredClone(record.returned) }]);
|
|
309
|
+
attentionRequired.sort((left, right) => {
|
|
310
|
+
if (left.room !== right.room)
|
|
311
|
+
return left.room < right.room ? -1 : 1;
|
|
162
312
|
if (left.path !== right.path)
|
|
163
313
|
return left.path < right.path ? -1 : 1;
|
|
164
314
|
return 0;
|
|
165
315
|
});
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
for (const listener of roomDraftListeners) {
|
|
173
|
-
if (waitingOnly && listener.ready)
|
|
174
|
-
continue;
|
|
175
|
-
listener.ready = true;
|
|
176
|
-
listener.notify(value);
|
|
316
|
+
const changed = new Map();
|
|
317
|
+
if (!localSaves) {
|
|
318
|
+
for (const change of held.unsent) {
|
|
319
|
+
if (!isFenced(change.room))
|
|
320
|
+
changed.set(`${change.room}\u0000${change.path}`, change);
|
|
321
|
+
}
|
|
177
322
|
}
|
|
323
|
+
const unsent = [...changed]
|
|
324
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
325
|
+
.map(([, change]) => change);
|
|
326
|
+
return {
|
|
327
|
+
queued: !localSaves && held.queued.some((locator) => !isFenced(locator)),
|
|
328
|
+
lastCompleted: held.lastCompleted,
|
|
329
|
+
attentionRequired,
|
|
330
|
+
unsent,
|
|
331
|
+
reset: held.resets.reduce((earliest, { locator, reset }) => isFenced(locator) || (earliest !== null && earliest <= reset) ? earliest : reset, null),
|
|
332
|
+
};
|
|
178
333
|
}
|
|
179
|
-
function
|
|
180
|
-
const
|
|
181
|
-
return
|
|
334
|
+
async function queuedWork() {
|
|
335
|
+
const held = await (await openLocal()).queued();
|
|
336
|
+
return !localSaves && held.some((locator) => !isFenced(locator));
|
|
182
337
|
}
|
|
183
|
-
function
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
338
|
+
function declarationOf(locator, stored) {
|
|
339
|
+
const held = declared.get(locator);
|
|
340
|
+
const room = stored?.record ?? held?.room ?? discovered.get(locator);
|
|
341
|
+
return room === undefined ? undefined : { room, available: held?.available ?? true };
|
|
342
|
+
}
|
|
343
|
+
function listedLocators(held) {
|
|
344
|
+
return [
|
|
345
|
+
...declared.keys(),
|
|
346
|
+
...[...held.keys()].filter((locator) => !declared.has(locator)),
|
|
347
|
+
].filter((locator) => !isFenced(locator));
|
|
348
|
+
}
|
|
349
|
+
function selectionModel() {
|
|
350
|
+
return refusal?.code === 'revoked-authorization'
|
|
351
|
+
? { status: 'unavailable', reason: 'revoked' }
|
|
352
|
+
: builtIn;
|
|
353
|
+
}
|
|
354
|
+
function listedRoom(locator, stored) {
|
|
355
|
+
const declaration = declarationOf(locator, stored);
|
|
356
|
+
if (declaration === undefined)
|
|
357
|
+
return undefined;
|
|
358
|
+
const { room, available } = declaration;
|
|
359
|
+
const selection = room.typedSelection ?? null;
|
|
360
|
+
return {
|
|
361
|
+
name: room.name,
|
|
362
|
+
locator,
|
|
363
|
+
handle: room.handle,
|
|
364
|
+
operations: room.operations,
|
|
365
|
+
replica: room.replica,
|
|
366
|
+
model: modelTypes(modelIn(locator, stored)),
|
|
367
|
+
typedSelection: selection === null ? null : { roots: selection.roots, model: modelTypes(selectionModel()) },
|
|
368
|
+
providerOwnedPaths: room.providerOwnedPaths,
|
|
369
|
+
roomVersion: stored?.record.roomVersion ?? null,
|
|
370
|
+
synchronizationAvailable: available,
|
|
371
|
+
refusal: scopedRefusals.get(locator) ?? null,
|
|
372
|
+
documents: stored?.documents ?? 0,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
function listingOf(held) {
|
|
376
|
+
const listing = new Map();
|
|
377
|
+
for (const locator of listedLocators(held)) {
|
|
378
|
+
const room = listedRoom(locator, held.get(locator));
|
|
379
|
+
if (room !== undefined)
|
|
380
|
+
listing.set(locator, room);
|
|
189
381
|
}
|
|
382
|
+
return listing;
|
|
190
383
|
}
|
|
191
|
-
function
|
|
192
|
-
|
|
384
|
+
async function patchedListing(changed) {
|
|
385
|
+
if (roomsShown === null)
|
|
386
|
+
return null;
|
|
387
|
+
const held = await storedRooms([...changed]);
|
|
388
|
+
const listing = new Map(roomsShown);
|
|
389
|
+
for (const locator of changed) {
|
|
390
|
+
const room = isFenced(locator) ? undefined : listedRoom(locator, held.get(locator));
|
|
391
|
+
if (room === undefined || !listing.has(locator))
|
|
392
|
+
return null;
|
|
393
|
+
listing.set(locator, room);
|
|
394
|
+
}
|
|
395
|
+
return listing;
|
|
193
396
|
}
|
|
194
|
-
function
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
397
|
+
async function listingNow() {
|
|
398
|
+
return listingOf(await storedRooms());
|
|
399
|
+
}
|
|
400
|
+
function lockedIn(listing, locator) {
|
|
401
|
+
return listing.get(locator)?.replica.offline === 'locked' && !confirmed.has(locator);
|
|
402
|
+
}
|
|
403
|
+
function shownRooms(listing) {
|
|
404
|
+
return structuredClone([...listing.values()].map(({ documents, ...room }) => lockedIn(listing, room.locator)
|
|
405
|
+
? {
|
|
406
|
+
...room,
|
|
407
|
+
model: modelTypes(NO_MODEL),
|
|
408
|
+
typedSelection: null,
|
|
409
|
+
documents: 0,
|
|
410
|
+
locked: true,
|
|
411
|
+
}
|
|
412
|
+
: { ...room, documents, locked: false }));
|
|
413
|
+
}
|
|
414
|
+
async function offlineModes() {
|
|
415
|
+
const modes = new Map();
|
|
416
|
+
for (const [locator, { room }] of declared) {
|
|
417
|
+
const declaration = room ?? discovered.get(locator);
|
|
418
|
+
if (declaration !== undefined)
|
|
419
|
+
modes.set(locator, declaration.replica.offline);
|
|
204
420
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
421
|
+
for (const [locator, { offline }] of (await covering()).rooms)
|
|
422
|
+
modes.set(locator, offline);
|
|
423
|
+
return modes;
|
|
424
|
+
}
|
|
425
|
+
async function locked(locator) {
|
|
426
|
+
if (isFenced(locator) || confirmed.has(locator))
|
|
427
|
+
return false;
|
|
428
|
+
const held = declared.get(locator)?.room ?? discovered.get(locator);
|
|
429
|
+
const mode = (await (await openLocal()).offline(locator)) ?? held?.replica.offline;
|
|
430
|
+
return mode === 'locked';
|
|
431
|
+
}
|
|
432
|
+
async function lockedRooms() {
|
|
433
|
+
return [...(await offlineModes())]
|
|
434
|
+
.filter(([locator, mode]) => mode === 'locked' && !confirmed.has(locator) && !isFenced(locator))
|
|
435
|
+
.map(([locator]) => locator)
|
|
436
|
+
.sort();
|
|
437
|
+
}
|
|
438
|
+
async function relocked(before) {
|
|
439
|
+
const after = await lockedRooms();
|
|
440
|
+
if (JSON.stringify(after) === JSON.stringify(before))
|
|
441
|
+
return;
|
|
442
|
+
journal.record(`locked ${after.length}`);
|
|
443
|
+
emitRooms();
|
|
444
|
+
for (const notify of [...lockListeners])
|
|
445
|
+
notify();
|
|
446
|
+
}
|
|
447
|
+
function confirmLocks(listed) {
|
|
448
|
+
if (!socket.isOpen())
|
|
449
|
+
return;
|
|
450
|
+
for (const room of listed) {
|
|
451
|
+
if (room.replica.admission === 'admitted')
|
|
452
|
+
confirmed.add(room.locator);
|
|
218
453
|
}
|
|
219
|
-
return { attentionRequired, reset };
|
|
220
454
|
}
|
|
221
|
-
function
|
|
222
|
-
|
|
455
|
+
function connectionLost() {
|
|
456
|
+
if (lockGrace !== null || closed)
|
|
457
|
+
return;
|
|
458
|
+
lockGrace = setTimeout(() => {
|
|
459
|
+
lockGrace = null;
|
|
460
|
+
if (socket.isOpen() || closed)
|
|
461
|
+
return;
|
|
462
|
+
void lockedRooms().then((before) => {
|
|
463
|
+
confirmed.clear();
|
|
464
|
+
return relocked(before);
|
|
465
|
+
}, () => { });
|
|
466
|
+
}, LOCK_GRACE_MS);
|
|
223
467
|
}
|
|
224
|
-
function
|
|
225
|
-
|
|
468
|
+
function connectionLive() {
|
|
469
|
+
if (lockGrace !== null)
|
|
470
|
+
clearTimeout(lockGrace);
|
|
471
|
+
lockGrace = null;
|
|
472
|
+
}
|
|
473
|
+
function watchLocks(notify) {
|
|
474
|
+
lockListeners.add(notify);
|
|
475
|
+
return () => {
|
|
476
|
+
lockListeners.delete(notify);
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
async function roomFacts(reach = null) {
|
|
480
|
+
const held = await storedRooms();
|
|
481
|
+
const rooms = [...listingOf(held).values()]
|
|
226
482
|
.filter((room) => holds(reach, room.locator))
|
|
227
483
|
.map((room) => {
|
|
228
484
|
const governed = governs(reach, room);
|
|
229
485
|
const attached = [
|
|
230
|
-
...(governed.collection ? [
|
|
231
|
-
...(governed.selection ? [
|
|
486
|
+
...(governed.collection ? [modelIn(room.locator, held.get(room.locator))] : []),
|
|
487
|
+
...(governed.selection ? [builtIn] : []),
|
|
232
488
|
];
|
|
233
489
|
return {
|
|
234
490
|
locator: room.locator,
|
|
@@ -236,164 +492,665 @@ export function composeRuntime(options) {
|
|
|
236
492
|
types: attached.flatMap((model) => model.status === 'unavailable' ? [] : Object.values(model.types)),
|
|
237
493
|
};
|
|
238
494
|
});
|
|
239
|
-
return { listed: roomsListed, rooms
|
|
495
|
+
return { listed: roomsListed, rooms };
|
|
496
|
+
}
|
|
497
|
+
function emitRooms(changed) {
|
|
498
|
+
recoverReads();
|
|
499
|
+
queueReadiness();
|
|
500
|
+
return changed === undefined ? publishRooms('all') : publishRooms('changed', changed);
|
|
501
|
+
}
|
|
502
|
+
function emitKnownRooms() {
|
|
503
|
+
conversations.learned();
|
|
504
|
+
if (unlistedRooms.size === 0)
|
|
505
|
+
return;
|
|
506
|
+
void publishRooms('known');
|
|
240
507
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
508
|
+
let roomsOwed = null;
|
|
509
|
+
let roomsShown = null;
|
|
510
|
+
function publishRooms(mode, touched = []) {
|
|
511
|
+
if (closed)
|
|
512
|
+
return Promise.resolve();
|
|
513
|
+
if (roomsOwed !== null) {
|
|
514
|
+
roomsOwed.all ||= mode === 'all';
|
|
515
|
+
for (const locator of touched) {
|
|
516
|
+
(mode === 'changed' ? roomsOwed.changed : roomsOwed.counted).add(locator);
|
|
517
|
+
}
|
|
518
|
+
return roomsOwed.shown;
|
|
519
|
+
}
|
|
520
|
+
const owing = {
|
|
521
|
+
all: mode === 'all',
|
|
522
|
+
changed: new Set(mode === 'changed' ? touched : []),
|
|
523
|
+
counted: new Set(mode === 'changed' ? [] : touched),
|
|
524
|
+
shown: Promise.resolve(),
|
|
525
|
+
};
|
|
526
|
+
roomsOwed = owing;
|
|
527
|
+
owing.shown = publication(async () => {
|
|
528
|
+
const owed = owing;
|
|
529
|
+
if (roomsOwed === owing)
|
|
530
|
+
roomsOwed = null;
|
|
531
|
+
if (closed || (roomListeners.size === 0 && unlistedRooms.size === 0))
|
|
532
|
+
return;
|
|
533
|
+
const patched = owed.all || owed.changed.size === 0 || unlistedRooms.size > 0
|
|
534
|
+
? null
|
|
535
|
+
: await patchedListing(new Set([...owed.changed, ...owed.counted]));
|
|
536
|
+
if (patched === null && owed.changed.size > 0)
|
|
537
|
+
owed.all = true;
|
|
538
|
+
if (patched === null && !owed.all && owed.counted.size > 0) {
|
|
539
|
+
const held = await covering().catch(() => null);
|
|
540
|
+
if (held === null)
|
|
541
|
+
return;
|
|
542
|
+
owed.all = [...owed.counted].some((locator) => held.rooms.has(locator) &&
|
|
543
|
+
roomsShown?.get(locator)?.documents !== held.rooms.get(locator).documents);
|
|
544
|
+
if (owed.all) {
|
|
545
|
+
recoverReads();
|
|
546
|
+
queueReadiness();
|
|
547
|
+
}
|
|
548
|
+
else if (unlistedRooms.size === 0)
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const listing = patched ?? (await listingNow().catch(() => null));
|
|
552
|
+
if (listing === null || closed)
|
|
553
|
+
return;
|
|
554
|
+
const value = shownRooms(listing);
|
|
555
|
+
const joining = unlistedRooms.size > 0 &&
|
|
556
|
+
(roomsListed || listing.size > 0 || refusal !== null || status.providerUnreachable);
|
|
557
|
+
if (!owed.all && patched === null && !joining)
|
|
558
|
+
return;
|
|
559
|
+
const moved = owed.all ||
|
|
560
|
+
patched !== null ||
|
|
561
|
+
(roomsShown !== null && JSON.stringify(shownRooms(roomsShown)) !== JSON.stringify(value));
|
|
562
|
+
if (moved)
|
|
563
|
+
for (const listener of roomListeners)
|
|
564
|
+
listener(value);
|
|
565
|
+
if (joining) {
|
|
566
|
+
for (const listener of [...unlistedRooms]) {
|
|
567
|
+
unlistedRooms.delete(listener);
|
|
568
|
+
roomListeners.add(listener);
|
|
569
|
+
listener(value);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
roomsShown = roomListeners.size === 0 ? null : listing;
|
|
573
|
+
});
|
|
574
|
+
return owing.shown;
|
|
245
575
|
}
|
|
246
576
|
function emitRefusal() {
|
|
577
|
+
queueReadiness();
|
|
247
578
|
for (const listener of refusalListeners)
|
|
248
579
|
listener(refusal);
|
|
580
|
+
emitKnownRooms();
|
|
249
581
|
}
|
|
250
582
|
function emitStorageLoss() {
|
|
583
|
+
queueReadiness();
|
|
251
584
|
for (const listener of storageLossListeners)
|
|
252
585
|
listener(storageLoss);
|
|
253
586
|
}
|
|
587
|
+
function emitMember(member) {
|
|
588
|
+
for (const listener of memberListeners)
|
|
589
|
+
listener(member);
|
|
590
|
+
}
|
|
254
591
|
function moveTo(next) {
|
|
255
592
|
if (closed)
|
|
256
|
-
return;
|
|
593
|
+
return Promise.resolve();
|
|
257
594
|
status = next;
|
|
258
|
-
show();
|
|
595
|
+
return show();
|
|
259
596
|
}
|
|
597
|
+
function transferring() {
|
|
598
|
+
return { ...status, state: 'syncing', resting: false };
|
|
599
|
+
}
|
|
600
|
+
let statusOwed = null;
|
|
601
|
+
let statusShown = Promise.resolve();
|
|
260
602
|
function show() {
|
|
261
603
|
if (closed)
|
|
262
|
-
return;
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
604
|
+
return Promise.resolve();
|
|
605
|
+
queueReadiness();
|
|
606
|
+
emitKnownRooms();
|
|
607
|
+
const moved = { moving: status, current: conversations.current() };
|
|
608
|
+
if (statusOwed !== null) {
|
|
609
|
+
statusOwed.push(moved);
|
|
610
|
+
return statusShown;
|
|
611
|
+
}
|
|
612
|
+
const owed = [moved];
|
|
613
|
+
statusOwed = owed;
|
|
614
|
+
statusShown = publication(async () => {
|
|
615
|
+
if (statusOwed === owed)
|
|
616
|
+
statusOwed = null;
|
|
617
|
+
const held = await attended();
|
|
618
|
+
if (closed)
|
|
619
|
+
return;
|
|
620
|
+
for (const { moving, current } of owed) {
|
|
621
|
+
const state = moving.resting && held.queued ? 'pending' : moving.state;
|
|
622
|
+
const next = {
|
|
623
|
+
state: state === 'synced' && !current ? 'syncing' : state,
|
|
624
|
+
lastCompleted: held.lastCompleted,
|
|
625
|
+
providerUnreachable: moving.providerUnreachable,
|
|
626
|
+
attentionRequired: held.attentionRequired,
|
|
627
|
+
unsent: held.unsent,
|
|
628
|
+
reset: held.reset,
|
|
629
|
+
};
|
|
630
|
+
if (same(shown, next))
|
|
631
|
+
continue;
|
|
632
|
+
shown = next;
|
|
633
|
+
for (const listener of statusListeners)
|
|
634
|
+
listener(shown);
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
return statusShown;
|
|
271
638
|
}
|
|
272
|
-
function entryOf(batch) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
639
|
+
function entryOf(states, batch) {
|
|
640
|
+
if (localSaves)
|
|
641
|
+
return Object.freeze({ batch, state: 'local' });
|
|
642
|
+
const state = states.get(batch.id);
|
|
277
643
|
return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
|
|
278
644
|
}
|
|
279
|
-
function entriesOf(locator) {
|
|
280
|
-
|
|
281
|
-
if (replica === undefined)
|
|
645
|
+
async function entriesOf(locator) {
|
|
646
|
+
if (isFenced(locator))
|
|
282
647
|
return [];
|
|
283
|
-
|
|
648
|
+
const { queue, states } = await workOf(locator);
|
|
649
|
+
return queue.map((batch) => entryOf(states, batch));
|
|
284
650
|
}
|
|
285
651
|
function emitQueue(locator) {
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
652
|
+
const listening = () => [...queueSubscriptions].filter((subscription) => subscription.locator === locator);
|
|
653
|
+
if (closed || listening().length === 0)
|
|
654
|
+
return Promise.resolve();
|
|
655
|
+
return publication(async () => {
|
|
656
|
+
if (closed || listening().length === 0)
|
|
657
|
+
return;
|
|
658
|
+
const entries = await entriesOf(locator);
|
|
659
|
+
if (closed)
|
|
660
|
+
return;
|
|
661
|
+
for (const subscription of listening())
|
|
289
662
|
subscription.notify(entries);
|
|
290
|
-
}
|
|
663
|
+
});
|
|
291
664
|
}
|
|
292
665
|
function applyChangeSet(set) {
|
|
293
|
-
|
|
294
|
-
|
|
666
|
+
liveQueries.touched(set);
|
|
667
|
+
figures.touched(set);
|
|
295
668
|
}
|
|
296
|
-
function
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
669
|
+
function recoverReads() {
|
|
670
|
+
liveQueries.recover();
|
|
671
|
+
figures.recover();
|
|
672
|
+
}
|
|
673
|
+
function unpublished(error) {
|
|
674
|
+
journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
|
|
675
|
+
}
|
|
676
|
+
async function prepareReplica(locator) {
|
|
677
|
+
for (;;) {
|
|
678
|
+
const held = (await storedRooms([locator])).get(locator);
|
|
679
|
+
if (held === undefined || closed || held.indexed)
|
|
680
|
+
return;
|
|
681
|
+
const key = held.record.key;
|
|
682
|
+
const done = await exclusive(async () => {
|
|
683
|
+
const local = await openLocal();
|
|
684
|
+
return store.rederive(local, key, composed(held.record));
|
|
685
|
+
});
|
|
686
|
+
if (!done.done)
|
|
687
|
+
continue;
|
|
688
|
+
if ((await storedRooms([locator])).get(locator)?.record.key !== key)
|
|
689
|
+
continue;
|
|
690
|
+
publishChangeSet(changeSet(locator, { documents: null }));
|
|
691
|
+
recoverReads();
|
|
692
|
+
queueReadiness();
|
|
693
|
+
return;
|
|
305
694
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
695
|
+
}
|
|
696
|
+
async function unprepared() {
|
|
697
|
+
const held = (await covering()).rooms;
|
|
698
|
+
return [...unpreparedDemand, ...held.keys()].find((locator) => !(held.get(locator)?.indexed ?? false) && !unpreparable.has(locator));
|
|
699
|
+
}
|
|
700
|
+
function unpreparedBy(locator, error) {
|
|
701
|
+
unpreparable.add(locator);
|
|
702
|
+
journal.record(`preparation failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
|
|
703
|
+
queueReadiness();
|
|
704
|
+
}
|
|
705
|
+
function prepare(locators = []) {
|
|
706
|
+
for (const locator of locators)
|
|
707
|
+
unpreparedDemand.add(locator);
|
|
708
|
+
if (preparing)
|
|
709
|
+
return;
|
|
710
|
+
preparing = true;
|
|
711
|
+
void (async () => {
|
|
712
|
+
try {
|
|
713
|
+
for (let waiting = await unprepared(); waiting !== undefined; waiting = await unprepared()) {
|
|
714
|
+
await yielded();
|
|
715
|
+
if (closed)
|
|
716
|
+
return;
|
|
717
|
+
unpreparedDemand.delete(waiting);
|
|
718
|
+
await prepareReplica(waiting).catch((error) => unpreparedBy(waiting, error));
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
catch {
|
|
722
|
+
journal.record('preparation failed');
|
|
723
|
+
}
|
|
724
|
+
finally {
|
|
725
|
+
preparing = false;
|
|
726
|
+
}
|
|
727
|
+
})();
|
|
728
|
+
}
|
|
729
|
+
function roomsOfReach(reach) {
|
|
730
|
+
return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
|
|
731
|
+
}
|
|
732
|
+
function everyRoom(held) {
|
|
733
|
+
return [...new Set([...declared.keys(), ...held.rooms.keys(), ...held.queues.keys()])];
|
|
734
|
+
}
|
|
735
|
+
function reachedRooms(plan, held) {
|
|
736
|
+
const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
|
|
737
|
+
const named = plan.rooms ?? (reached === null ? everyRoom(held) : [...reached]);
|
|
738
|
+
return [...new Set(named)]
|
|
739
|
+
.filter((locator) => (reached === null || reached.has(locator)) && !plan.locked.includes(locator))
|
|
740
|
+
.sort();
|
|
741
|
+
}
|
|
742
|
+
function awaited(locator) {
|
|
743
|
+
return (!roomsListed ||
|
|
744
|
+
(declared.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
|
|
745
|
+
}
|
|
746
|
+
function covered(plan, held) {
|
|
747
|
+
return coveredRooms(reachedRooms(plan, held), plan.rooms === null && plan.reach === null, held);
|
|
748
|
+
}
|
|
749
|
+
async function figuresCover() {
|
|
750
|
+
const held = await covering();
|
|
751
|
+
const locked = await lockedRooms();
|
|
752
|
+
const cover = coveredRooms(everyRoom(held)
|
|
753
|
+
.filter((locator) => !locked.includes(locator))
|
|
754
|
+
.sort(), true, held);
|
|
755
|
+
awaitPreparation(cover);
|
|
756
|
+
return 'pending' in cover ? cover : { rooms: cover.rooms };
|
|
757
|
+
}
|
|
758
|
+
function coveredRooms(locators, whole, held) {
|
|
759
|
+
if (locators.length === 0 && whole && !roomsListed)
|
|
760
|
+
return { pending: [] };
|
|
761
|
+
const pending = [];
|
|
762
|
+
const coverage = [];
|
|
763
|
+
const overlay = [];
|
|
764
|
+
const read = [];
|
|
765
|
+
for (const locator of locators) {
|
|
766
|
+
const replica = held.rooms.get(locator);
|
|
767
|
+
if (replica === undefined) {
|
|
768
|
+
if (awaited(locator))
|
|
769
|
+
pending.push({ room: locator, awaits: 'acquisition' });
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
if (!replica.indexed) {
|
|
773
|
+
pending.push({ room: locator, awaits: 'preparation' });
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
coverage.push({
|
|
777
|
+
room: locator,
|
|
778
|
+
generation: replica.transfer,
|
|
779
|
+
current: !roomInFlight(store, locator),
|
|
780
|
+
});
|
|
781
|
+
read.push({ locator, key: replica.key });
|
|
782
|
+
for (const id of held.queues.get(locator) ?? [])
|
|
783
|
+
overlay.push(id);
|
|
312
784
|
}
|
|
785
|
+
if (pending.length > 0)
|
|
786
|
+
return { pending };
|
|
787
|
+
return { coverage: { rooms: coverage, indexes: [], overlay }, rooms: read };
|
|
313
788
|
}
|
|
314
|
-
function
|
|
315
|
-
if (
|
|
789
|
+
function awaitPreparation(cover) {
|
|
790
|
+
if (!('pending' in cover))
|
|
316
791
|
return;
|
|
317
|
-
|
|
318
|
-
|
|
792
|
+
const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
|
|
793
|
+
if (waiting.length > 0)
|
|
794
|
+
prepare(waiting.map(({ room }) => room));
|
|
795
|
+
}
|
|
796
|
+
function requiredRooms(given, held) {
|
|
797
|
+
const reach = planReach(given);
|
|
798
|
+
const listed = new Set(listedLocators(held));
|
|
799
|
+
if (reach === null)
|
|
800
|
+
return { listed: roomsListed, rooms: [...listed].sort() };
|
|
801
|
+
const named = [...roomsOfReach(reach)].sort();
|
|
802
|
+
if (roomsListed)
|
|
803
|
+
return { listed: true, rooms: named.filter((room) => listed.has(room)) };
|
|
804
|
+
return { listed: named.every((room) => held.has(room)), rooms: named };
|
|
805
|
+
}
|
|
806
|
+
function roomState(locator, held) {
|
|
807
|
+
const listedRoom = discovered.get(locator);
|
|
808
|
+
if (roomsListed &&
|
|
809
|
+
(listedRoom === undefined ||
|
|
810
|
+
listedRoom.replica.admission !== 'admitted' ||
|
|
811
|
+
unreadable.has(locator)))
|
|
812
|
+
return 'unadmitted';
|
|
813
|
+
const stored = held.get(locator);
|
|
814
|
+
if (stored === undefined)
|
|
815
|
+
return 'acquiring';
|
|
816
|
+
if (!stored.indexed)
|
|
817
|
+
return 'preparing';
|
|
818
|
+
return 'complete';
|
|
819
|
+
}
|
|
820
|
+
function conversationCoverage(reach) {
|
|
821
|
+
const declaredConversations = reach === null || reach.conversations;
|
|
822
|
+
if (!declaredConversations || options.adapter.conversations === undefined)
|
|
823
|
+
return 'undeclared';
|
|
824
|
+
if (!conversations.known() || !conversations.current())
|
|
825
|
+
return 'acquiring';
|
|
826
|
+
return 'complete';
|
|
827
|
+
}
|
|
828
|
+
function connectionOf(lastCompleted) {
|
|
829
|
+
if (socket.isOpen())
|
|
830
|
+
return 'connected';
|
|
831
|
+
if (status.providerUnreachable)
|
|
832
|
+
return 'unreachable';
|
|
833
|
+
return options.adapter.roomStream === undefined && lastCompleted !== null
|
|
834
|
+
? 'connected'
|
|
835
|
+
: 'connecting';
|
|
836
|
+
}
|
|
837
|
+
async function connectionNow() {
|
|
838
|
+
if (socket.isOpen() || status.providerUnreachable || options.adapter.roomStream !== undefined) {
|
|
839
|
+
return connectionOf(null);
|
|
840
|
+
}
|
|
841
|
+
return connectionOf((await (await openLocal()).facts()).lastCompleted);
|
|
842
|
+
}
|
|
843
|
+
async function readinessHeld() {
|
|
844
|
+
const local = await openLocal();
|
|
845
|
+
const [held, facts] = await Promise.all([covering(), local.facts()]);
|
|
846
|
+
return { held: held.rooms, facts };
|
|
847
|
+
}
|
|
848
|
+
function readinessFor(port, reach, { held, facts }) {
|
|
849
|
+
const required = requiredRooms(reach, held);
|
|
850
|
+
const coverage = required.rooms.map((room) => ({ room, state: roomState(room, held) }));
|
|
851
|
+
const preparing = coverage
|
|
852
|
+
.filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
|
|
853
|
+
.map(({ room }) => room);
|
|
854
|
+
if (preparing.length > 0)
|
|
855
|
+
prepare(preparing);
|
|
856
|
+
return readinessOf({
|
|
857
|
+
code: port.code,
|
|
858
|
+
coverage: {
|
|
859
|
+
listed: required.listed,
|
|
860
|
+
rooms: coverage,
|
|
861
|
+
conversations: conversationCoverage(reach),
|
|
862
|
+
},
|
|
863
|
+
persistence,
|
|
864
|
+
connection: connectionOf(facts.lastCompleted),
|
|
865
|
+
house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
|
|
866
|
+
space: full ? 'full' : null,
|
|
867
|
+
installation: installationOf(store, coverage, held),
|
|
868
|
+
lost: storageLoss !== null,
|
|
869
|
+
established: facts.targets[port.target]?.established === true ||
|
|
870
|
+
(heldWhenOpened && usableWhenHeld.includes(port.target)),
|
|
871
|
+
});
|
|
319
872
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (
|
|
873
|
+
const establishing = new Set();
|
|
874
|
+
function establish(name) {
|
|
875
|
+
if (establishing.has(name))
|
|
323
876
|
return;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
877
|
+
establishing.add(name);
|
|
878
|
+
void durably(async (local) => {
|
|
879
|
+
const { targets } = await local.facts();
|
|
880
|
+
await local.fact('targets', { ...targets, [name]: { established: true } });
|
|
881
|
+
})
|
|
882
|
+
.then(() => {
|
|
883
|
+
queueReadiness();
|
|
884
|
+
void probeEachMinute();
|
|
885
|
+
}, () => {
|
|
886
|
+
journal.record(`target record failed ${name}`);
|
|
887
|
+
})
|
|
888
|
+
.finally(() => establishing.delete(name));
|
|
889
|
+
}
|
|
890
|
+
function deliverReadiness(watch, held) {
|
|
891
|
+
if (!readinessWatches.has(watch))
|
|
892
|
+
return;
|
|
893
|
+
const readiness = readinessFor(watch.port, watch.reach, held);
|
|
894
|
+
if (readiness.ready && held.facts.targets[watch.port.target]?.established !== true) {
|
|
895
|
+
establish(watch.port.target);
|
|
331
896
|
}
|
|
332
|
-
|
|
897
|
+
const shownReadiness = JSON.stringify(readiness);
|
|
898
|
+
if (shownReadiness === watch.last)
|
|
899
|
+
return;
|
|
900
|
+
watch.last = shownReadiness;
|
|
901
|
+
watch.deliver(structuredClone(readiness));
|
|
902
|
+
}
|
|
903
|
+
let readinessShown = Promise.resolve();
|
|
904
|
+
const readinessOwed = new Set();
|
|
905
|
+
function deliverAll(watches) {
|
|
906
|
+
const queued = readinessOwed.size > 0;
|
|
907
|
+
for (const watch of watches)
|
|
908
|
+
readinessOwed.add(watch);
|
|
909
|
+
if (queued)
|
|
910
|
+
return;
|
|
911
|
+
readinessShown = readinessShown
|
|
912
|
+
.then(async () => {
|
|
913
|
+
const owed = [...readinessOwed];
|
|
914
|
+
readinessOwed.clear();
|
|
915
|
+
const held = await readinessHeld();
|
|
916
|
+
if (closed)
|
|
917
|
+
return;
|
|
918
|
+
for (const watch of owed)
|
|
919
|
+
deliverReadiness(watch, held);
|
|
920
|
+
})
|
|
921
|
+
.catch(unpublished);
|
|
922
|
+
}
|
|
923
|
+
function queueReadiness() {
|
|
924
|
+
if (readinessQueued || closed || readinessWatches.size === 0)
|
|
925
|
+
return;
|
|
926
|
+
readinessQueued = true;
|
|
927
|
+
queueMicrotask(() => {
|
|
928
|
+
readinessQueued = false;
|
|
929
|
+
if (closed)
|
|
930
|
+
return;
|
|
931
|
+
deliverAll([...readinessWatches]);
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
function watchReadiness(port, reach, deliver) {
|
|
935
|
+
const watch = { port, reach, deliver, last: null };
|
|
936
|
+
readinessWatches.add(watch);
|
|
937
|
+
void openLocal().then(() => deliverAll([watch]), () => deliverAll([watch]));
|
|
938
|
+
return () => {
|
|
939
|
+
readinessWatches.delete(watch);
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
function providerReached() {
|
|
943
|
+
return (socket.isOpen() || !status.providerUnreachable) && refusal === null;
|
|
944
|
+
}
|
|
945
|
+
function filled() {
|
|
946
|
+
if (full || closed)
|
|
333
947
|
return;
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
948
|
+
full = true;
|
|
949
|
+
journal.record('space full');
|
|
950
|
+
queueReadiness();
|
|
951
|
+
void probeEachMinute();
|
|
952
|
+
}
|
|
953
|
+
async function installedTarget() {
|
|
954
|
+
if (heldWhenOpened && usableWhenHeld.length > 0)
|
|
955
|
+
return true;
|
|
956
|
+
const { targets } = await (await openLocal()).facts();
|
|
957
|
+
return Object.values(targets).some((target) => target.established);
|
|
958
|
+
}
|
|
959
|
+
async function probeEachMinute() {
|
|
960
|
+
if (!full || probing !== null || closed)
|
|
961
|
+
return;
|
|
962
|
+
if (!(await installedTarget().catch(() => false)))
|
|
963
|
+
return;
|
|
964
|
+
if (!full || probing !== null || closed)
|
|
965
|
+
return;
|
|
966
|
+
// A device whose storage filled after its install tries a write each minute, so space freed elsewhere ends its read-only mode without a Retry.
|
|
967
|
+
probing = setInterval(() => {
|
|
968
|
+
void probed();
|
|
969
|
+
}, 60_000);
|
|
970
|
+
}
|
|
971
|
+
function probed() {
|
|
972
|
+
testing ??= database.ask({ kind: 'space.probe' }).then(() => {
|
|
973
|
+
testing = null;
|
|
974
|
+
spaceFreed();
|
|
975
|
+
return true;
|
|
976
|
+
}, () => {
|
|
977
|
+
testing = null;
|
|
978
|
+
return false;
|
|
979
|
+
});
|
|
980
|
+
return testing;
|
|
981
|
+
}
|
|
982
|
+
function spaceFreed() {
|
|
983
|
+
if (!full)
|
|
984
|
+
return;
|
|
985
|
+
full = false;
|
|
986
|
+
if (probing !== null)
|
|
987
|
+
clearInterval(probing);
|
|
988
|
+
probing = null;
|
|
989
|
+
unpreparable.clear();
|
|
990
|
+
journal.record('space freed');
|
|
991
|
+
queueReadiness();
|
|
992
|
+
if (closed || authorization === undefined)
|
|
993
|
+
return;
|
|
994
|
+
if (options.adapter.roomStream === undefined)
|
|
995
|
+
void sync('unstreamed', null);
|
|
996
|
+
else
|
|
997
|
+
void resynchronize();
|
|
998
|
+
}
|
|
999
|
+
function sameCover(left, right) {
|
|
1000
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
1001
|
+
}
|
|
1002
|
+
async function coveredRows(plan, cover) {
|
|
1003
|
+
for (;;) {
|
|
1004
|
+
const before = await cover();
|
|
1005
|
+
if ('pending' in before)
|
|
1006
|
+
return { pending: before };
|
|
1007
|
+
const local = await openLocal();
|
|
1008
|
+
const keys = before.rooms.map(({ key }) => key);
|
|
1009
|
+
const changes = local.changes(keys);
|
|
1010
|
+
const current = () => local.changes(keys) === changes;
|
|
1011
|
+
const rows = await windowRows(local, plan, before.rooms, current);
|
|
1012
|
+
const after = await cover();
|
|
1013
|
+
if (rows !== null && sameCover(before, after))
|
|
1014
|
+
return { rows, cover: before, current };
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
async function windowRows(local, plan, rooms, unchanged) {
|
|
1018
|
+
const first = await local.query(plan, rooms, QUERY_BYTES);
|
|
1019
|
+
if (first.more === undefined || !('documents' in first)) {
|
|
1020
|
+
const { more: _more, ...rows } = first;
|
|
1021
|
+
return rows;
|
|
1022
|
+
}
|
|
1023
|
+
const documents = [...first.documents];
|
|
1024
|
+
for (let more = first.more; more !== undefined;) {
|
|
1025
|
+
if (!unchanged())
|
|
1026
|
+
return null;
|
|
1027
|
+
const page = await local.query({
|
|
1028
|
+
...plan,
|
|
1029
|
+
after: { text: more, values: JSON.parse(more) },
|
|
1030
|
+
limit: plan.limit === null ? null : plan.limit - documents.length,
|
|
1031
|
+
total: false,
|
|
1032
|
+
}, rooms, QUERY_BYTES);
|
|
1033
|
+
if (!('documents' in page))
|
|
1034
|
+
return null;
|
|
1035
|
+
documents.push(...page.documents);
|
|
1036
|
+
more = page.more;
|
|
1037
|
+
if (more === undefined) {
|
|
1038
|
+
return {
|
|
1039
|
+
documents,
|
|
1040
|
+
...(page.next === undefined ? {} : { next: page.next }),
|
|
1041
|
+
...(first.total === undefined ? {} : { total: first.total }),
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
return null;
|
|
1046
|
+
}
|
|
1047
|
+
function keyedOf(rows, coverage) {
|
|
1048
|
+
if ('groups' in rows) {
|
|
1049
|
+
return {
|
|
1050
|
+
result: { ...rows, coverage },
|
|
1051
|
+
keys: rows.groups.map((group) => JSON.stringify(group.key)),
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
return {
|
|
1055
|
+
result: { ...rows, coverage },
|
|
1056
|
+
keys: rows.documents.map((document) => documentKey(document.room, document.path)),
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
async function answeredPlan(plan, cover) {
|
|
1060
|
+
const answered = await coveredRows(plan, cover);
|
|
1061
|
+
if ('pending' in answered) {
|
|
1062
|
+
const pending = answered.pending;
|
|
1063
|
+
return { result: pending, keys: [], current: () => true };
|
|
1064
|
+
}
|
|
1065
|
+
const held = answered.cover;
|
|
1066
|
+
return { ...keyedOf(answered.rows, held.coverage), current: answered.current };
|
|
1067
|
+
}
|
|
1068
|
+
async function coverNow(plan) {
|
|
1069
|
+
const cover = covered(plan, await covering());
|
|
1070
|
+
awaitPreparation(cover);
|
|
1071
|
+
return cover;
|
|
1072
|
+
}
|
|
1073
|
+
async function rowsQuery(plan) {
|
|
1074
|
+
const held = await covering();
|
|
1075
|
+
const before = covered(plan, held);
|
|
1076
|
+
awaitPreparation(before);
|
|
1077
|
+
const seeded = seedApplies(seeding, plan, held);
|
|
1078
|
+
if (seeded !== null)
|
|
1079
|
+
return seededResult(seeded).result;
|
|
1080
|
+
if ('pending' in before)
|
|
1081
|
+
return before;
|
|
1082
|
+
const answered = await coveredRows(plan, () => coverNow(plan));
|
|
1083
|
+
if ('pending' in answered)
|
|
1084
|
+
return answered.pending;
|
|
1085
|
+
const shownCover = answered.cover;
|
|
1086
|
+
return { ...answered.rows, coverage: shownCover.coverage };
|
|
1087
|
+
}
|
|
1088
|
+
function publishChangeSet(set) {
|
|
1089
|
+
applyChangeSet(set);
|
|
1090
|
+
for (const { locator, path } of set.documents) {
|
|
1091
|
+
if (path !== null)
|
|
1092
|
+
store.attached.drop(locator, path);
|
|
1093
|
+
}
|
|
1094
|
+
const shownNow = [];
|
|
1095
|
+
if (set.queues.length > 0 || set.drafts.length > 0)
|
|
1096
|
+
shownNow.push(show());
|
|
1097
|
+
if (set.rooms.length > 0)
|
|
1098
|
+
shownNow.push(emitRooms(set.rooms));
|
|
1099
|
+
else if (set.documents.length > 0) {
|
|
1100
|
+
shownNow.push(publishRooms('counted', [...new Set(set.documents.map(({ locator }) => locator))]));
|
|
1101
|
+
}
|
|
1102
|
+
for (const locator of set.queues)
|
|
1103
|
+
shownNow.push(emitQueue(locator));
|
|
1104
|
+
if (set.drafts.length > 0)
|
|
1105
|
+
shownNow.push(publishRoomDrafts(drafting, false));
|
|
1106
|
+
else if (set.documents.length > 0) {
|
|
1107
|
+
shownNow.push(publishRoomDrafts(drafting, false, set.documents));
|
|
1108
|
+
}
|
|
1109
|
+
for (const app of set.presentations)
|
|
1110
|
+
shownNow.push(emitPresentation(drafting, app));
|
|
1111
|
+
conversations.changed(set);
|
|
1112
|
+
return Promise.all(shownNow).then(() => undefined);
|
|
339
1113
|
}
|
|
340
1114
|
async function refuse(reason, provider = null) {
|
|
341
|
-
stopTimer();
|
|
342
1115
|
discoveryPaused = true;
|
|
1116
|
+
socket.abortReads();
|
|
343
1117
|
try {
|
|
344
1118
|
if (reason === 'revoked-authorization') {
|
|
345
1119
|
await exclusive(async () => {
|
|
346
1120
|
const revoked = { status: 'unavailable', reason: 'revoked' };
|
|
1121
|
+
let held = new Map();
|
|
347
1122
|
try {
|
|
348
1123
|
const local = await openLocal();
|
|
349
|
-
await
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
1124
|
+
held = await storedRooms();
|
|
1125
|
+
await local.commit({
|
|
1126
|
+
rooms: [...held.values()].map(({ record: kept }) => {
|
|
1127
|
+
const record = { ...kept, model: revoked };
|
|
1128
|
+
return {
|
|
1129
|
+
locator: record.locator,
|
|
1130
|
+
key: record.key,
|
|
1131
|
+
record: storedRecord(record),
|
|
1132
|
+
composed: composedOf(composed(record)),
|
|
1133
|
+
};
|
|
1134
|
+
}),
|
|
1135
|
+
});
|
|
353
1136
|
}
|
|
354
1137
|
catch {
|
|
355
1138
|
journal.record('model revocation persistence failed');
|
|
356
1139
|
}
|
|
357
|
-
const
|
|
358
|
-
for (const locator of
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
for (const key of keys) {
|
|
363
|
-
invalidModels.add(key);
|
|
364
|
-
models.set(key, revoked);
|
|
365
|
-
liveModels.delete(key);
|
|
1140
|
+
const listedNow = listedLocators(held);
|
|
1141
|
+
for (const locator of listedNow) {
|
|
1142
|
+
if (!held.has(locator))
|
|
1143
|
+
unheldModels.set(locator, revoked);
|
|
366
1144
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const before = replicas.get(locator) ?? null;
|
|
370
|
-
if (before !== null) {
|
|
371
|
-
const record = {
|
|
372
|
-
...before.confirmed.record,
|
|
373
|
-
model: revoked,
|
|
374
|
-
selectionModel: revoked,
|
|
375
|
-
};
|
|
376
|
-
const read = readConfirmed(record, before.confirmed);
|
|
377
|
-
replicas.set(locator, read.kind === 'staged'
|
|
378
|
-
? replicaOf(read.confirmed, before.queue, rewriting(), before)
|
|
379
|
-
: { ...before, confirmed: { ...before.confirmed, record } });
|
|
380
|
-
}
|
|
381
|
-
rooms.set(locator, {
|
|
382
|
-
...room,
|
|
383
|
-
model: modelTypes(revoked),
|
|
384
|
-
typedSelection: room.typedSelection === null
|
|
385
|
-
? null
|
|
386
|
-
: { ...room.typedSelection, model: modelTypes(revoked) },
|
|
387
|
-
});
|
|
388
|
-
changed.push(changeSet(locator, before, replicas.get(locator) ?? null, { room: true }));
|
|
389
|
-
}
|
|
390
|
-
publishChangeSet(mergeChangeSets(changed));
|
|
1145
|
+
publishChangeSet(mergeChangeSets(listedNow.map((locator) => changeSet(locator, { documents: null, room: true }))));
|
|
1146
|
+
prepare();
|
|
391
1147
|
});
|
|
392
1148
|
}
|
|
393
1149
|
}
|
|
394
1150
|
finally {
|
|
395
1151
|
refusal = {
|
|
396
|
-
reason,
|
|
1152
|
+
code: reason,
|
|
1153
|
+
retryable: false,
|
|
397
1154
|
reauthorizationOffered: reason === 'revoked-authorization',
|
|
398
1155
|
refusal: provider,
|
|
399
1156
|
};
|
|
@@ -402,425 +1159,219 @@ export function composeRuntime(options) {
|
|
|
402
1159
|
}
|
|
403
1160
|
}
|
|
404
1161
|
function list(room, synchronizationAvailable) {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
:
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
providerOwnedPaths: room.providerOwnedPaths,
|
|
419
|
-
roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
|
|
420
|
-
attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
|
|
421
|
-
synchronizationAvailable,
|
|
422
|
-
refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
|
|
1162
|
+
if (isFenced(room.locator))
|
|
1163
|
+
return;
|
|
1164
|
+
declared.set(room.locator, {
|
|
1165
|
+
room: {
|
|
1166
|
+
name: room.name,
|
|
1167
|
+
locator: room.locator,
|
|
1168
|
+
handle: room.handle,
|
|
1169
|
+
operations: room.operations,
|
|
1170
|
+
replica: room.replica,
|
|
1171
|
+
typedSelection: room.typedSelection ?? null,
|
|
1172
|
+
providerOwnedPaths: room.providerOwnedPaths,
|
|
1173
|
+
},
|
|
1174
|
+
available: synchronizationAvailable,
|
|
423
1175
|
});
|
|
424
1176
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
1177
|
+
function listHeld(locator) {
|
|
1178
|
+
if (isFenced(locator))
|
|
1179
|
+
return;
|
|
1180
|
+
declared.set(locator, { room: null, available: false });
|
|
1181
|
+
}
|
|
1182
|
+
async function applyDiscovery(found, started, left) {
|
|
1183
|
+
const lockedBefore = await lockedRooms();
|
|
1184
|
+
const local = await openLocal();
|
|
1185
|
+
const stillFenced = new Set(await local.fences());
|
|
1186
|
+
const relisting = found
|
|
1187
|
+
.map(({ locator }) => locator)
|
|
1188
|
+
.filter((locator) => {
|
|
1189
|
+
const fence = store.fences.of(locator);
|
|
1190
|
+
return stillFenced.has(locator) && (fence === undefined || fence.at < started);
|
|
1191
|
+
});
|
|
1192
|
+
const relisted = relisting.length === 0 ? new Set() : await durably((held) => held.unfence(relisting));
|
|
1193
|
+
for (const { locator } of found) {
|
|
1194
|
+
const fence = store.fences.of(locator);
|
|
1195
|
+
if (fence === undefined || fence.at >= started)
|
|
1196
|
+
continue;
|
|
1197
|
+
if (relisted.has(locator) || (!stillFenced.has(locator) && !fence.written)) {
|
|
1198
|
+
store.fences.lift(locator);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
const kept = await local.held();
|
|
1202
|
+
await loseRooms(found
|
|
1203
|
+
.filter((room) => room.replica.admission !== 'admitted' &&
|
|
1204
|
+
(kept.held.has(room.locator) || store.installs.of(room.locator).transfer !== null))
|
|
1205
|
+
.map((room) => room.locator));
|
|
1206
|
+
const listed = found.filter(({ locator }) => !isFenced(locator) && (!stillFenced.has(locator) || relisted.has(locator)));
|
|
1207
|
+
const listedLocatorsNow = new Set(listed.map((room) => room.locator));
|
|
1208
|
+
for (const locator of listedLocatorsNow)
|
|
428
1209
|
scopedRefusals.delete(locator);
|
|
429
|
-
|
|
1210
|
+
if (left === undefined)
|
|
1211
|
+
discovered.clear();
|
|
430
1212
|
for (const room of listed)
|
|
431
1213
|
discovered.set(room.locator, room);
|
|
432
|
-
|
|
1214
|
+
const named = new Set(found.map((room) => room.locator));
|
|
1215
|
+
await loseRooms(left ??
|
|
1216
|
+
[...new Set([...declared.keys(), ...kept.held, ...store.installs.transferring()])].filter((locator) => !named.has(locator)));
|
|
1217
|
+
const held = await storedRooms(listed.map((room) => room.locator));
|
|
433
1218
|
for (const room of listed) {
|
|
434
|
-
const
|
|
435
|
-
if (
|
|
436
|
-
|
|
1219
|
+
const record = held.get(room.locator)?.record;
|
|
1220
|
+
if (record !== undefined && record.replica.selection !== room.replica.selection) {
|
|
1221
|
+
listHeld(room.locator);
|
|
437
1222
|
continue;
|
|
438
1223
|
}
|
|
439
1224
|
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
440
1225
|
}
|
|
1226
|
+
confirmLocks(listed);
|
|
441
1227
|
roomsListed = true;
|
|
442
1228
|
emitRooms();
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
1229
|
+
await relocked(lockedBefore);
|
|
1230
|
+
return listed;
|
|
1231
|
+
}
|
|
1232
|
+
function forget(locator) {
|
|
1233
|
+
confirmed.delete(locator);
|
|
1234
|
+
forgetInstall(store, locator);
|
|
1235
|
+
declared.delete(locator);
|
|
1236
|
+
discovered.delete(locator);
|
|
1237
|
+
unreadable.delete(locator);
|
|
1238
|
+
stale.delete(locator);
|
|
1239
|
+
repairable.delete(locator);
|
|
1240
|
+
unpreparable.delete(locator);
|
|
1241
|
+
scopedRefusals.delete(locator);
|
|
1242
|
+
unheldModels.delete(locator);
|
|
1243
|
+
store.forget(locator);
|
|
1244
|
+
}
|
|
1245
|
+
async function eraseFenced(locators) {
|
|
1246
|
+
for (const locator of locators) {
|
|
1247
|
+
while (await durably((local) => local.erase(locator, ERASE_BATCH)))
|
|
1248
|
+
await yielded();
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
let sweeping = false;
|
|
1252
|
+
function sweep() {
|
|
1253
|
+
if (sweeping || closed)
|
|
449
1254
|
return;
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
queue: true,
|
|
456
|
-
drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
|
|
457
|
-
}));
|
|
458
|
-
for (const locator of unlisted) {
|
|
459
|
-
for (const batch of replicas.get(locator)?.queue ?? [])
|
|
460
|
-
pendings.delete(batch.id);
|
|
461
|
-
replicas.delete(locator);
|
|
462
|
-
rooms.delete(locator);
|
|
463
|
-
roomDrafts.delete(locator);
|
|
464
|
-
unreadable.delete(locator);
|
|
465
|
-
stale.delete(locator);
|
|
466
|
-
scopedRefusals.delete(locator);
|
|
467
|
-
sourceRefusals.delete(locator);
|
|
468
|
-
declarationRefusals.delete(locator);
|
|
469
|
-
for (const attached of ['collection', 'typed-selection']) {
|
|
470
|
-
const key = modelKey(locator, attached);
|
|
471
|
-
models.delete(key);
|
|
472
|
-
liveModels.delete(key);
|
|
473
|
-
invalidModels.delete(key);
|
|
1255
|
+
sweeping = true;
|
|
1256
|
+
void (async () => {
|
|
1257
|
+
try {
|
|
1258
|
+
while (!closed && (await durably((local) => local.erase(null, ERASE_BATCH)))) {
|
|
1259
|
+
await yielded();
|
|
474
1260
|
}
|
|
475
|
-
journal.record(`room unlisted ${locator}`);
|
|
476
1261
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
function restored(key, persisted) {
|
|
482
|
-
const prior = models.get(key);
|
|
483
|
-
if (prior !== undefined &&
|
|
484
|
-
prior.status !== 'unavailable' &&
|
|
485
|
-
(persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
|
|
486
|
-
invalidModels.add(key);
|
|
487
|
-
}
|
|
488
|
-
const model = persisted.status === 'unavailable' ||
|
|
489
|
-
liveModels.get(key) === JSON.stringify(persisted.snapshot)
|
|
490
|
-
? persisted
|
|
491
|
-
: cached(persisted);
|
|
492
|
-
models.set(key, model);
|
|
493
|
-
if (model.status === 'unavailable') {
|
|
494
|
-
liveModels.delete(key);
|
|
495
|
-
invalidModels.delete(key);
|
|
496
|
-
}
|
|
497
|
-
return model;
|
|
498
|
-
}
|
|
499
|
-
async function hydrate(opened) {
|
|
500
|
-
const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
|
|
501
|
-
opened.all(),
|
|
502
|
-
opened.queues(),
|
|
503
|
-
opened.roomDrafts(),
|
|
504
|
-
opened.presentation(),
|
|
505
|
-
opened.conversations(),
|
|
506
|
-
opened.chatDrafts(),
|
|
507
|
-
opened.lastCompleted(),
|
|
508
|
-
opened.providerLimits(),
|
|
509
|
-
opened.capabilities(),
|
|
510
|
-
]);
|
|
511
|
-
limits = declared;
|
|
512
|
-
capabilities = named;
|
|
513
|
-
roomDrafts.clear();
|
|
514
|
-
for (const record of kept)
|
|
515
|
-
rememberRoomDraft(record);
|
|
516
|
-
presentations.clear();
|
|
517
|
-
for (const record of presented)
|
|
518
|
-
presentations.set(record.app, record.value);
|
|
519
|
-
conversations.hydrate(held, chatDrafts);
|
|
520
|
-
const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
|
|
521
|
-
pendings.clear();
|
|
522
|
-
for (const queue of queues) {
|
|
523
|
-
for (const { batch, state } of queue.pending)
|
|
524
|
-
pendings.set(batch.id, state);
|
|
525
|
-
}
|
|
526
|
-
const previousReplicas = new Map(replicas);
|
|
527
|
-
replicas.clear();
|
|
528
|
-
for (const record of records) {
|
|
529
|
-
const model = restored(modelKey(record.locator, 'collection'), record.model);
|
|
530
|
-
const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
|
|
531
|
-
const prior = previousReplicas.get(record.locator);
|
|
532
|
-
const retyped = { ...record, model, selectionModel };
|
|
533
|
-
const read = readConfirmed(retyped, prior?.confirmed);
|
|
534
|
-
const confirmed = read.kind === 'staged'
|
|
535
|
-
? read.confirmed
|
|
536
|
-
: {
|
|
537
|
-
record: retyped,
|
|
538
|
-
collection: parseCollection(retyped.documents, compositionOf(retyped)),
|
|
539
|
-
};
|
|
540
|
-
replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], rewriting(), prior));
|
|
541
|
-
const existing = rooms.get(record.locator);
|
|
542
|
-
if (existing === undefined)
|
|
543
|
-
list(record, false);
|
|
544
|
-
else {
|
|
545
|
-
rooms.set(record.locator, {
|
|
546
|
-
...existing,
|
|
547
|
-
roomVersion: record.roomVersion,
|
|
548
|
-
attachments: record.attachments,
|
|
549
|
-
operations: record.operations,
|
|
550
|
-
replica: record.replica,
|
|
551
|
-
model: modelTypes(model),
|
|
552
|
-
typedSelection: record.typedSelection === null
|
|
553
|
-
? null
|
|
554
|
-
: {
|
|
555
|
-
roots: record.typedSelection.roots,
|
|
556
|
-
model: modelTypes(selectionModel),
|
|
557
|
-
},
|
|
558
|
-
});
|
|
1262
|
+
catch {
|
|
1263
|
+
journal.record('erasure failed');
|
|
559
1264
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
declarationRefusals.add(record.locator);
|
|
1265
|
+
finally {
|
|
1266
|
+
sweeping = false;
|
|
563
1267
|
}
|
|
564
|
-
}
|
|
565
|
-
return {
|
|
566
|
-
lastCompleted,
|
|
567
|
-
locators: new Set([...queuedBatches.keys(), ...replicas.keys()]),
|
|
568
|
-
read: mergeChangeSets([...new Set([...previousReplicas.keys(), ...replicas.keys()])].map((locator) => changeSet(locator, previousReplicas.get(locator) ?? null, replicas.get(locator) ?? null))),
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
async function readState(opened) {
|
|
572
|
-
const { lastCompleted, locators, read } = await hydrate(opened);
|
|
573
|
-
applyChangeSet(read);
|
|
574
|
-
if (replicas.size > 0)
|
|
575
|
-
emitRooms();
|
|
576
|
-
for (const locator of locators)
|
|
577
|
-
emitQueue(locator);
|
|
578
|
-
publishRoomDrafts(false);
|
|
579
|
-
return lastCompleted;
|
|
580
|
-
}
|
|
581
|
-
async function open() {
|
|
582
|
-
const opened = await openRuntimeStore();
|
|
583
|
-
store = opened;
|
|
584
|
-
const kept = options.shell.generation;
|
|
585
|
-
const storedGeneration = await opened.generation();
|
|
586
|
-
const loss = detectStorageLoss(storedGeneration, kept);
|
|
587
|
-
const generation = nextGeneration(storedGeneration, kept);
|
|
588
|
-
if (generation !== storedGeneration)
|
|
589
|
-
await opened.rememberGeneration(generation);
|
|
590
|
-
if (generation !== kept)
|
|
591
|
-
await options.shell.keepGeneration(generation);
|
|
592
|
-
if (loss !== null) {
|
|
593
|
-
storageLoss = loss;
|
|
594
|
-
emitStorageLoss();
|
|
595
|
-
}
|
|
596
|
-
const lastCompleted = await readState(opened);
|
|
597
|
-
moveTo({ ...status, lastCompleted, ...attention() });
|
|
598
|
-
return opened;
|
|
599
|
-
}
|
|
600
|
-
function durably(work) {
|
|
601
|
-
return exclusive(async () => {
|
|
602
|
-
const local = await openLocal();
|
|
603
|
-
return work(local);
|
|
604
|
-
});
|
|
1268
|
+
})();
|
|
605
1269
|
}
|
|
606
|
-
function
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
throw error;
|
|
610
|
-
});
|
|
611
|
-
return opening;
|
|
612
|
-
}
|
|
613
|
-
function exclusive(work) {
|
|
614
|
-
const next = mutations.then(work, work);
|
|
615
|
-
mutations = next.then(() => undefined, () => undefined);
|
|
616
|
-
return next;
|
|
617
|
-
}
|
|
618
|
-
function withdrawable(batch) {
|
|
619
|
-
const state = pendings.get(batch.id);
|
|
620
|
-
return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
|
|
621
|
-
}
|
|
622
|
-
async function settle(local, locator, before, confirmed, queue, settling = {}) {
|
|
623
|
-
const withdrawn = withdrawals(confirmed, queue, withdrawable, rewriting());
|
|
624
|
-
const kept = queue.filter((batch) => !withdrawn.includes(batch));
|
|
625
|
-
const returning = [
|
|
626
|
-
...withdrawn.map((batch) => ({ batch, returned: { kind: 'withdrawn' } })).reverse(),
|
|
627
|
-
...(settling.leaving === undefined ? [] : [settling.leaving]),
|
|
628
|
-
];
|
|
629
|
-
const written = new Map();
|
|
630
|
-
for (const { batch, returned } of returning) {
|
|
631
|
-
if (returned === null)
|
|
1270
|
+
function fenceLive(locators) {
|
|
1271
|
+
for (const locator of new Set(locators)) {
|
|
1272
|
+
if (!store.fences.raise(locator, discoveries))
|
|
632
1273
|
continue;
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
written.set(saved.path, {
|
|
636
|
-
locator,
|
|
637
|
-
path: saved.path,
|
|
638
|
-
revision: roomDraftRevision(),
|
|
639
|
-
parts: returnedInto(existing?.parts, saved.parts),
|
|
640
|
-
returned,
|
|
641
|
-
});
|
|
642
|
-
}
|
|
1274
|
+
forget(locator);
|
|
1275
|
+
journal.record(`room lost ${locator}`);
|
|
643
1276
|
}
|
|
644
|
-
await local.write({
|
|
645
|
-
next: confirmed.record,
|
|
646
|
-
previous: before?.confirmed.record ?? null,
|
|
647
|
-
queue: kept,
|
|
648
|
-
previousQueue: before?.queue ?? [],
|
|
649
|
-
roomDrafts: [...written.values()],
|
|
650
|
-
});
|
|
651
|
-
for (const batch of before?.queue ?? []) {
|
|
652
|
-
if (!kept.includes(batch))
|
|
653
|
-
pendings.delete(batch.id);
|
|
654
|
-
}
|
|
655
|
-
for (const record of written.values())
|
|
656
|
-
rememberRoomDraft(record);
|
|
657
|
-
for (const batch of withdrawn)
|
|
658
|
-
journal.record(`outcome ${locator} ${batch.id} withdrawn`);
|
|
659
|
-
replicas.set(locator, replicaOf(confirmed, kept, rewriting(), before));
|
|
660
|
-
return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
|
|
661
|
-
room: settling.room,
|
|
662
|
-
queue: kept.length !== (before?.queue.length ?? 0),
|
|
663
|
-
drafts: [...written.keys()],
|
|
664
|
-
});
|
|
665
1277
|
}
|
|
666
|
-
function
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
const key = modelKey(locator, attached);
|
|
672
|
-
models.set(key, model);
|
|
673
|
-
invalidModels.delete(key);
|
|
674
|
-
if (model.status === 'current')
|
|
675
|
-
liveModels.set(key, JSON.stringify(model.snapshot));
|
|
676
|
-
else if (model.status === 'unavailable')
|
|
677
|
-
liveModels.delete(key);
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
function remodel(locator, answers, held) {
|
|
681
|
-
const taken = (answer, previous, attached) => {
|
|
682
|
-
if (answer === undefined)
|
|
683
|
-
return previous;
|
|
684
|
-
const { state, refused } = attachedModel(answer, previous);
|
|
685
|
-
if (refused !== null) {
|
|
686
|
-
const where = refused.location === undefined ? '' : ` ${refused.location}`;
|
|
687
|
-
journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
|
|
688
|
-
}
|
|
689
|
-
models.set(modelKey(locator, attached), state);
|
|
690
|
-
return state;
|
|
691
|
-
};
|
|
692
|
-
return {
|
|
693
|
-
collection: taken(answers?.collection, held.collection, 'collection'),
|
|
694
|
-
selection: taken(answers?.selection, held.selection, 'typed-selection'),
|
|
695
|
-
};
|
|
696
|
-
}
|
|
697
|
-
function publish(local, room, confirmed, base) {
|
|
698
|
-
return exclusive(async () => {
|
|
699
|
-
if (!continuing())
|
|
700
|
-
return true;
|
|
701
|
-
const prior = replicas.get(room.locator);
|
|
702
|
-
if (base !== undefined &&
|
|
703
|
-
(prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
|
|
704
|
-
prior.confirmed.record.revisions !== base.record.revisions)) {
|
|
1278
|
+
async function loseRooms(locators) {
|
|
1279
|
+
fenceLive(locators);
|
|
1280
|
+
const fresh = [...new Set(locators)].filter((locator) => {
|
|
1281
|
+
const fence = store.fences.of(locator);
|
|
1282
|
+
if (fence === undefined || fence.written)
|
|
705
1283
|
return false;
|
|
706
|
-
|
|
707
|
-
adoptModels(room.locator, confirmed.record);
|
|
708
|
-
const queue = prior?.queue ?? [];
|
|
709
|
-
const roomVersion = confirmed.record.roomVersion;
|
|
710
|
-
const accepted = queue.filter((batch) => {
|
|
711
|
-
const accepted = pendings.get(batch.id)?.accepted;
|
|
712
|
-
return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
|
|
713
|
-
});
|
|
714
|
-
const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
|
|
715
|
-
for (const batch of accepted)
|
|
716
|
-
pendings.delete(batch.id);
|
|
717
|
-
stale.delete(room.locator);
|
|
718
|
-
if (unreadable.delete(room.locator))
|
|
719
|
-
scopedRefusals.delete(room.locator);
|
|
720
|
-
sourceRefusals.delete(room.locator);
|
|
721
|
-
declarationRefusals.delete(room.locator);
|
|
722
|
-
list(room, true);
|
|
723
|
-
publishChangeSet(set);
|
|
724
|
-
moveTo({ ...status, ...attention() });
|
|
1284
|
+
store.fences.mark(locator, true);
|
|
725
1285
|
return true;
|
|
726
1286
|
});
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
const replica = replicas.get(locator);
|
|
731
|
-
const room = rooms.get(locator);
|
|
732
|
-
if (replica === undefined || room === undefined) {
|
|
733
|
-
throw new Error(`No Local Replica for ${locator}`);
|
|
734
|
-
}
|
|
735
|
-
const saved = [];
|
|
736
|
-
for (const { path, revision } of named) {
|
|
737
|
-
const record = roomDrafts.get(locator)?.get(path);
|
|
738
|
-
if (record === undefined || record.revision !== revision) {
|
|
739
|
-
return { outcome: 'refused', rule: 'stale', file: path };
|
|
740
|
-
}
|
|
741
|
-
if (!saved.some((draft) => draft.path === path))
|
|
742
|
-
saved.push({ path, parts: record.parts });
|
|
743
|
-
}
|
|
744
|
-
const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
|
|
745
|
-
if (changed.length > 0)
|
|
746
|
-
return { outcome: 'refused', rule: 'changed-underneath', parts: changed };
|
|
747
|
-
const parts = saved.map(({ path, parts }) => changesOf(path, parts));
|
|
748
|
-
const built = buildBatch({
|
|
749
|
-
baseRoomVersion: replica.confirmed.record.roomVersion,
|
|
750
|
-
composition: compositionFor(replica.confirmed.record),
|
|
751
|
-
operations: room.operations,
|
|
752
|
-
providerOwnedPaths: room.providerOwnedPaths,
|
|
753
|
-
providerLimits: limits,
|
|
754
|
-
rewriting: rewriting(),
|
|
755
|
-
visible: replica.visible,
|
|
756
|
-
changes: parts.flatMap(({ changes }) => changes),
|
|
757
|
-
bases: parts.flatMap(({ bases }) => bases),
|
|
758
|
-
writtenBy,
|
|
759
|
-
});
|
|
760
|
-
if (built.kind === 'refused')
|
|
761
|
-
return { outcome: 'refused', ...built.refusal };
|
|
762
|
-
const batches = [...replica.queue, built.batch];
|
|
763
|
-
const consumed = saved.map(({ path }) => path);
|
|
1287
|
+
if (fresh.length === 0)
|
|
1288
|
+
return;
|
|
1289
|
+
let held = [];
|
|
764
1290
|
try {
|
|
765
|
-
await local
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
queue: batches,
|
|
769
|
-
previousQueue: replica.queue,
|
|
770
|
-
saved,
|
|
771
|
-
consumedRoomDrafts: consumed,
|
|
1291
|
+
await durably(async (local) => {
|
|
1292
|
+
held = await local.roomDrafts();
|
|
1293
|
+
await local.fence(fresh);
|
|
772
1294
|
});
|
|
773
1295
|
}
|
|
774
|
-
catch {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
return { outcome: 'saved', identifier: built.batch.id };
|
|
789
|
-
}
|
|
790
|
-
async function mark(local, locator, identifier, submission, lost) {
|
|
791
|
-
const state = pendings.get(identifier);
|
|
792
|
-
if (!continuing() ||
|
|
793
|
-
state === undefined ||
|
|
794
|
-
(JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
|
|
795
|
-
return;
|
|
1296
|
+
catch (error) {
|
|
1297
|
+
for (const locator of fresh)
|
|
1298
|
+
store.fences.mark(locator, false);
|
|
1299
|
+
throw error;
|
|
1300
|
+
}
|
|
1301
|
+
finally {
|
|
1302
|
+
publishChangeSet(mergeChangeSets(fresh.map((locator) => changeSet(locator, {
|
|
1303
|
+
documents: null,
|
|
1304
|
+
room: true,
|
|
1305
|
+
queue: true,
|
|
1306
|
+
drafts: held.filter((draft) => draft.locator === locator).map(({ path }) => path),
|
|
1307
|
+
}))));
|
|
1308
|
+
recoverReads();
|
|
1309
|
+
moveTo(status);
|
|
796
1310
|
}
|
|
797
|
-
await local
|
|
798
|
-
|
|
799
|
-
const replica = replicas.get(locator);
|
|
800
|
-
if (replica !== undefined)
|
|
801
|
-
publishChangeSet(changeSet(locator, replica, replica, { queue: true }));
|
|
802
|
-
moveTo({ ...status, ...attention() });
|
|
1311
|
+
await durably((local) => unpair(accepting, local));
|
|
1312
|
+
await eraseFenced(fresh);
|
|
803
1313
|
}
|
|
804
|
-
async function
|
|
805
|
-
if (
|
|
806
|
-
throw new Error('
|
|
807
|
-
await
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
1314
|
+
async function open(opened) {
|
|
1315
|
+
if (closed)
|
|
1316
|
+
throw new Error('runtime closed');
|
|
1317
|
+
const interrupted = await opened.fences();
|
|
1318
|
+
const kept = options.shell.generation;
|
|
1319
|
+
const storedGeneration = await opened.generation();
|
|
1320
|
+
const loss = detectStorageLoss(storedGeneration, kept);
|
|
1321
|
+
const generation = nextGeneration(storedGeneration, kept);
|
|
1322
|
+
if (generation !== storedGeneration)
|
|
1323
|
+
await opened.rememberGeneration(generation);
|
|
1324
|
+
if (generation !== kept)
|
|
1325
|
+
await options.shell.keepGeneration(generation);
|
|
1326
|
+
if (loss !== null) {
|
|
1327
|
+
storageLoss = loss;
|
|
1328
|
+
emitStorageLoss();
|
|
1329
|
+
}
|
|
1330
|
+
const held = await opened.opened();
|
|
1331
|
+
heldWhenOpened = usableWhenHeld.length > 0 && (await opened.covering()).rooms.size > 0;
|
|
1332
|
+
try {
|
|
1333
|
+
await opened.demote();
|
|
1334
|
+
await database.ask({ kind: 'read.builds', build });
|
|
1335
|
+
for (const key of held.orphans)
|
|
1336
|
+
await opened.release(key);
|
|
1337
|
+
}
|
|
1338
|
+
catch (error) {
|
|
1339
|
+
if (!refusedForSpace(error))
|
|
1340
|
+
throw error;
|
|
1341
|
+
}
|
|
1342
|
+
if (held.erasing || held.orphans.length > 0)
|
|
1343
|
+
queueMicrotask(sweep);
|
|
1344
|
+
await conversations.opened(opened);
|
|
1345
|
+
if (interrupted.length > 0) {
|
|
1346
|
+
queueMicrotask(() => {
|
|
1347
|
+
void eraseFenced(interrupted).catch(() => {
|
|
1348
|
+
journal.record('erasure failed');
|
|
1349
|
+
});
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
moveTo(status);
|
|
1353
|
+
prepare();
|
|
1354
|
+
emitRooms();
|
|
1355
|
+
void exclusive(() => unpair(accepting, opened)).catch(() => {
|
|
1356
|
+
journal.record('unpair failed');
|
|
1357
|
+
});
|
|
1358
|
+
return opened;
|
|
811
1359
|
}
|
|
812
1360
|
async function readHistory(locator, path, query = {}) {
|
|
813
1361
|
if (closed || refusal !== null)
|
|
814
|
-
return
|
|
1362
|
+
return UNCERTAIN;
|
|
1363
|
+
await firstDiscovery;
|
|
1364
|
+
if (!roomsNamed)
|
|
1365
|
+
return UNCERTAIN;
|
|
815
1366
|
try {
|
|
816
1367
|
await ensureAuthorization();
|
|
817
1368
|
}
|
|
818
1369
|
catch {
|
|
819
|
-
return
|
|
820
|
-
}
|
|
821
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
822
|
-
return { kind: 'transport-uncertainty' };
|
|
1370
|
+
return UNCERTAIN;
|
|
823
1371
|
}
|
|
1372
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1373
|
+
return UNCERTAIN;
|
|
1374
|
+
const since = store.fences.count();
|
|
824
1375
|
const transfer = ['transfer', 'history', locator, path];
|
|
825
1376
|
if (query.revision !== undefined)
|
|
826
1377
|
transfer.push(query.revision);
|
|
@@ -835,29 +1386,64 @@ export function composeRuntime(options) {
|
|
|
835
1386
|
...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
|
|
836
1387
|
});
|
|
837
1388
|
journal.record(`outcome history ${result.kind}`);
|
|
1389
|
+
if (store.fences.since(locator, since))
|
|
1390
|
+
return failure({ kind: 'room-not-readable', locator });
|
|
838
1391
|
if (result.kind === 'scoped-refusal') {
|
|
839
|
-
const room =
|
|
1392
|
+
const room = await recordAt(result.locator);
|
|
840
1393
|
if (room !== undefined)
|
|
841
1394
|
await scoped(result, room, await openLocal());
|
|
842
1395
|
}
|
|
843
1396
|
if (result.kind === 'revoked-authorization')
|
|
844
1397
|
await refuse('revoked-authorization');
|
|
845
|
-
return result;
|
|
1398
|
+
return historyAnswer(result);
|
|
846
1399
|
}
|
|
847
1400
|
catch {
|
|
848
1401
|
journal.record('outcome history transport-uncertainty');
|
|
849
|
-
return
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
function
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1402
|
+
return UNCERTAIN;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
async function located(room, path, destination) {
|
|
1406
|
+
const crossing = roomTarget(destination);
|
|
1407
|
+
const listing = await listingNow();
|
|
1408
|
+
if (crossing === null) {
|
|
1409
|
+
const named = target(path, destination);
|
|
1410
|
+
return named === null || !listing.has(room) ? null : { room, path: named };
|
|
1411
|
+
}
|
|
1412
|
+
const held = [...listing.values()].find((listed) => listed.handle === crossing.handle);
|
|
1413
|
+
return held === undefined ? null : { room: held.locator, path: crossing.path };
|
|
1414
|
+
}
|
|
1415
|
+
async function resolveReference(room, path, destination) {
|
|
1416
|
+
if (roomTarget(destination) === null && target(path, destination) === null) {
|
|
1417
|
+
return ABSENT_TARGET;
|
|
1418
|
+
}
|
|
1419
|
+
const at = await located(room, path, destination);
|
|
1420
|
+
if (at === null)
|
|
1421
|
+
return UNREADABLE;
|
|
1422
|
+
if (await locked(at.room))
|
|
1423
|
+
return LOCKED_UNREADABLE;
|
|
1424
|
+
const held = await recordAt(at.room);
|
|
1425
|
+
if (held !== undefined &&
|
|
1426
|
+
(await (await openLocal()).entries(held.key, 'visible', [at.path])).length > 0) {
|
|
1427
|
+
return { kind: 'document', ...at };
|
|
1428
|
+
}
|
|
1429
|
+
const history = await readHistory(at.room, at.path);
|
|
1430
|
+
if (history.kind === 'no-history' || history.kind === 'revision')
|
|
1431
|
+
return ABSENT_TARGET;
|
|
1432
|
+
if (history.kind !== 'history')
|
|
1433
|
+
return history;
|
|
1434
|
+
const [last] = history.revisions;
|
|
1435
|
+
if (last === undefined)
|
|
1436
|
+
return ABSENT_TARGET;
|
|
1437
|
+
return history.deleted === true
|
|
1438
|
+
? { kind: 'deleted', ...at, revision: last }
|
|
1439
|
+
: { kind: 'document', ...at };
|
|
1440
|
+
}
|
|
1441
|
+
async function cleanUp() {
|
|
1442
|
+
const freed = await database.ask({ kind: 'opened.clean' });
|
|
1443
|
+
return freed + ((await options.shell.cleanUp?.()) ?? 0);
|
|
1444
|
+
}
|
|
1445
|
+
async function declaredAction(name) {
|
|
1446
|
+
for (const capability of (await (await openLocal()).facts()).capabilities) {
|
|
861
1447
|
for (const action of capability.actions ?? []) {
|
|
862
1448
|
if (action.name === name)
|
|
863
1449
|
return action;
|
|
@@ -865,26 +1451,33 @@ export function composeRuntime(options) {
|
|
|
865
1451
|
}
|
|
866
1452
|
return undefined;
|
|
867
1453
|
}
|
|
868
|
-
async function
|
|
1454
|
+
async function calledRooms(name, input) {
|
|
1455
|
+
const member = (await declaredAction(name))?.room;
|
|
1456
|
+
if (member === undefined || input === null || typeof input !== 'object')
|
|
1457
|
+
return [];
|
|
1458
|
+
const named = input[member];
|
|
1459
|
+
return (Array.isArray(named) ? named : [named]).filter((room) => typeof room === 'string');
|
|
1460
|
+
}
|
|
1461
|
+
async function invokeAction(name, input, dispatching) {
|
|
869
1462
|
await ready;
|
|
870
1463
|
if (closed || refusal !== null)
|
|
871
|
-
return
|
|
872
|
-
if (declaredAction(name) === undefined)
|
|
873
|
-
return
|
|
1464
|
+
return UNCERTAIN;
|
|
1465
|
+
if ((await declaredAction(name)) === undefined)
|
|
1466
|
+
return undeclared(name);
|
|
874
1467
|
try {
|
|
875
1468
|
await ensureAuthorization();
|
|
876
1469
|
}
|
|
877
1470
|
catch {
|
|
878
|
-
return
|
|
879
|
-
}
|
|
880
|
-
if (closed || authorization === undefined || refusal !== null) {
|
|
881
|
-
return { kind: 'transport-uncertainty' };
|
|
1471
|
+
return UNCERTAIN;
|
|
882
1472
|
}
|
|
1473
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
1474
|
+
return UNCERTAIN;
|
|
1475
|
+
await dispatching?.();
|
|
883
1476
|
journal.record(`transfer action ${name}`);
|
|
884
1477
|
try {
|
|
885
1478
|
const result = await options.adapter.action(authorization, { name, input });
|
|
886
1479
|
if (result.kind === 'scoped-refusal') {
|
|
887
|
-
const room =
|
|
1480
|
+
const room = await recordAt(result.locator);
|
|
888
1481
|
if (room !== undefined)
|
|
889
1482
|
await scoped(result, room, await openLocal());
|
|
890
1483
|
}
|
|
@@ -893,98 +1486,165 @@ export function composeRuntime(options) {
|
|
|
893
1486
|
journal.record(`outcome action ${result.kind}`);
|
|
894
1487
|
if (result.kind === 'rejected')
|
|
895
1488
|
journal.record(`refusal rejected ${result.code}`);
|
|
896
|
-
return result;
|
|
1489
|
+
return calledResult(result);
|
|
897
1490
|
}
|
|
898
1491
|
catch {
|
|
899
1492
|
journal.record('outcome action transport-uncertainty');
|
|
900
|
-
return
|
|
1493
|
+
return UNCERTAIN;
|
|
901
1494
|
}
|
|
902
1495
|
}
|
|
903
|
-
function
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion));
|
|
910
|
-
}
|
|
911
|
-
function writtenAfter(locator, roomVersion, path) {
|
|
912
|
-
const written = replicas.get(locator)?.confirmed.record.written[path];
|
|
913
|
-
return (written !== undefined &&
|
|
914
|
-
options.adapter.reached(locator, written, roomVersion) &&
|
|
915
|
-
!options.adapter.reached(locator, roomVersion, written));
|
|
916
|
-
}
|
|
917
|
-
async function pullRefresh(authorized, confirmed) {
|
|
918
|
-
if (!continuing())
|
|
919
|
-
return unconfirmed;
|
|
1496
|
+
async function readConversationHistory(request) {
|
|
1497
|
+
await ready;
|
|
1498
|
+
const stream = options.adapter.conversations;
|
|
1499
|
+
if (closed || refusal !== null || stream?.history === undefined)
|
|
1500
|
+
return UNCERTAIN;
|
|
1501
|
+
journal.record('transfer conversation history');
|
|
920
1502
|
try {
|
|
921
|
-
const result = await
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1503
|
+
const result = await stream.history(request);
|
|
1504
|
+
if (result.kind === 'revoked-authorization')
|
|
1505
|
+
await refuse('revoked-authorization');
|
|
1506
|
+
journal.record(`outcome conversation history ${result.kind}`);
|
|
1507
|
+
return calledResult(result);
|
|
925
1508
|
}
|
|
926
|
-
catch
|
|
927
|
-
journal.record(
|
|
928
|
-
|
|
1509
|
+
catch {
|
|
1510
|
+
journal.record('outcome conversation history transport-uncertainty');
|
|
1511
|
+
return UNCERTAIN;
|
|
929
1512
|
}
|
|
930
1513
|
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1514
|
+
function unsettled(result) {
|
|
1515
|
+
return result.kind !== 'action' && result.retryable;
|
|
1516
|
+
}
|
|
1517
|
+
async function recoveredAction(name, input) {
|
|
1518
|
+
const operation = operationOf(input);
|
|
1519
|
+
if (operation === null)
|
|
1520
|
+
return invokeAction(name, input);
|
|
1521
|
+
const kept = async () => (await openLocal()).keepCall({ operation, name, input });
|
|
1522
|
+
calling.add(operation);
|
|
934
1523
|
try {
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1524
|
+
let result = await invokeAction(name, input, kept);
|
|
1525
|
+
if (unsettled(result)) {
|
|
1526
|
+
await recovery.attempt(`action ${operation}`, async () => {
|
|
1527
|
+
if (closed || refusal !== null)
|
|
1528
|
+
return 'done';
|
|
1529
|
+
if (!socket.isOpen())
|
|
1530
|
+
return 'again';
|
|
1531
|
+
result = await invokeAction(name, input, kept);
|
|
1532
|
+
return unsettled(result) ? 'again' : 'done';
|
|
1533
|
+
}, { again: true });
|
|
1534
|
+
}
|
|
1535
|
+
if (!unsettled(result))
|
|
1536
|
+
await (await openLocal()).dropCall(operation);
|
|
941
1537
|
return result;
|
|
942
1538
|
}
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
throw error;
|
|
1539
|
+
finally {
|
|
1540
|
+
calling.delete(operation);
|
|
946
1541
|
}
|
|
947
1542
|
}
|
|
948
|
-
async function
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
}
|
|
963
|
-
return null;
|
|
1543
|
+
async function resendCalls(signal) {
|
|
1544
|
+
if (!socket.isOpen() || closed || stopping)
|
|
1545
|
+
return 'done';
|
|
1546
|
+
const local = await openLocal();
|
|
1547
|
+
for (const held of await local.calls()) {
|
|
1548
|
+
if (calling.has(held.operation))
|
|
1549
|
+
continue;
|
|
1550
|
+
if (closed || signal.aborted)
|
|
1551
|
+
return 'done';
|
|
1552
|
+
calling.add(held.operation);
|
|
1553
|
+
try {
|
|
1554
|
+
if (unsettled(await invokeAction(held.name, held.input)))
|
|
1555
|
+
return 'again';
|
|
1556
|
+
await local.dropCall(held.operation);
|
|
964
1557
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
|
|
968
|
-
const reached = now !== undefined &&
|
|
969
|
-
options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
|
|
970
|
-
return reached ? null : advance(room, local, authorized);
|
|
971
|
-
}
|
|
972
|
-
await retainModel(local, room, 'collection', modelOf(room.locator));
|
|
973
|
-
return null;
|
|
1558
|
+
finally {
|
|
1559
|
+
calling.delete(held.operation);
|
|
974
1560
|
}
|
|
975
|
-
if (refreshed.kind !== 'beyond-horizon')
|
|
976
|
-
return refreshed;
|
|
977
1561
|
}
|
|
978
|
-
|
|
979
|
-
|
|
1562
|
+
return 'done';
|
|
1563
|
+
}
|
|
1564
|
+
function continuing() {
|
|
1565
|
+
return !closed && !discoveryPaused;
|
|
1566
|
+
}
|
|
1567
|
+
async function advance(room, local, authorized, running, unheld) {
|
|
1568
|
+
const live = () => running() && !full;
|
|
1569
|
+
if (!live())
|
|
980
1570
|
return unconfirmed;
|
|
1571
|
+
const held = unheld === undefined ? await recordAt(room.locator) : undefined;
|
|
1572
|
+
if (options.adapter.roomStream !== undefined &&
|
|
1573
|
+
(held !== undefined || !store.installs.of(room.locator).beyondHorizon)) {
|
|
1574
|
+
stale.delete(room.locator);
|
|
1575
|
+
await name(room.locator);
|
|
1576
|
+
return null;
|
|
1577
|
+
}
|
|
1578
|
+
const refresh = reads.refresh;
|
|
1579
|
+
if (held !== undefined && refresh !== undefined) {
|
|
1580
|
+
const refreshed = await store.ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
|
|
1581
|
+
if (refreshed === 'behind')
|
|
1582
|
+
return advance(room, local, authorized, live);
|
|
1583
|
+
if (refreshed !== 'beyond-horizon')
|
|
1584
|
+
return refreshed;
|
|
1585
|
+
if (reloadsDue.has(room.locator))
|
|
1586
|
+
return unconfirmed;
|
|
1587
|
+
replaced(store, [room.locator]);
|
|
1588
|
+
}
|
|
1589
|
+
const prior = held === undefined ? undefined : await recordAt(room.locator);
|
|
1590
|
+
const from = standingAt(store, room.locator, prior);
|
|
1591
|
+
const bootstrapped = await pullBootstrap(installer, authorized, room, local, live, prior, reads, unheld);
|
|
981
1592
|
if (bootstrapped.kind !== 'staged')
|
|
982
|
-
return bootstrapped;
|
|
983
|
-
|
|
984
|
-
|
|
1593
|
+
return live() ? bootstrapped : unconfirmed;
|
|
1594
|
+
const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
|
|
1595
|
+
if (await publish(installer, local, room, bootstrapped.record, landing, live)) {
|
|
1596
|
+
return live() ? null : unconfirmed;
|
|
1597
|
+
}
|
|
1598
|
+
return (await reachedBy(installer, room.locator, bootstrapped.record.roomVersion))
|
|
1599
|
+
? null
|
|
1600
|
+
: advance(room, local, authorized, live);
|
|
1601
|
+
}
|
|
1602
|
+
async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
|
|
1603
|
+
const refreshed = await pullRefresh(installer, authorized, held, live, hold, refresh);
|
|
1604
|
+
if (!live())
|
|
1605
|
+
return unconfirmed;
|
|
1606
|
+
if (refreshed.kind === 'still-current') {
|
|
1607
|
+
stale.delete(room.locator);
|
|
1608
|
+
if (unreadable.delete(room.locator)) {
|
|
1609
|
+
scopedRefusals.delete(room.locator);
|
|
1610
|
+
list(room, true);
|
|
1611
|
+
emitRooms();
|
|
1612
|
+
}
|
|
1613
|
+
return null;
|
|
1614
|
+
}
|
|
1615
|
+
if (refreshed.kind === 'advanced') {
|
|
1616
|
+
const landing = {
|
|
1617
|
+
kind: 'refreshed',
|
|
1618
|
+
from: refreshed.from,
|
|
1619
|
+
remodelled: refreshed.remodelled,
|
|
1620
|
+
};
|
|
1621
|
+
if (!(await publish(installer, local, room, refreshed.record, landing, live))) {
|
|
1622
|
+
const now = (await recordAt(room.locator))?.roomVersion;
|
|
1623
|
+
const reached = now !== undefined &&
|
|
1624
|
+
options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
|
|
1625
|
+
return reached ? null : 'behind';
|
|
1626
|
+
}
|
|
1627
|
+
await retainModel(local, room, null);
|
|
1628
|
+
return null;
|
|
1629
|
+
}
|
|
1630
|
+
return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
|
|
985
1631
|
}
|
|
986
1632
|
async function withhold(withheld, room, local, progress) {
|
|
1633
|
+
if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
|
|
1634
|
+
return;
|
|
1635
|
+
if (withheld.kind === 'not-ready') {
|
|
1636
|
+
progress.complete = false;
|
|
1637
|
+
askAgain(room.locator);
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
987
1640
|
stale.add(room.locator);
|
|
1641
|
+
if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
|
|
1642
|
+
const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
|
|
1643
|
+
repairable.set(room.locator, reset);
|
|
1644
|
+
repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
|
|
1645
|
+
}
|
|
1646
|
+
else
|
|
1647
|
+
repairable.delete(room.locator);
|
|
988
1648
|
if (withheld.kind === 'scoped-refusal') {
|
|
989
1649
|
await scoped(withheld, room, local);
|
|
990
1650
|
}
|
|
@@ -995,319 +1655,156 @@ export function composeRuntime(options) {
|
|
|
995
1655
|
journal.record(`refusal ${withheld.kind} ${withheld.code}`);
|
|
996
1656
|
}
|
|
997
1657
|
else if (withheld.kind === 'room-not-readable') {
|
|
998
|
-
|
|
999
|
-
list(room, false);
|
|
1000
|
-
emitRooms();
|
|
1001
|
-
}
|
|
1002
|
-
else if (withheld.kind === 'refused') {
|
|
1003
|
-
refusedSource(room, withheld);
|
|
1658
|
+
roomUnreadable(room);
|
|
1004
1659
|
}
|
|
1005
1660
|
else if (withheld.kind === 'revoked-authorization') {
|
|
1006
1661
|
await refuse('revoked-authorization');
|
|
1007
1662
|
}
|
|
1008
1663
|
}
|
|
1009
|
-
function
|
|
1010
|
-
sourceRefusals.set(room.locator, {
|
|
1011
|
-
code: refusal.rule,
|
|
1012
|
-
fields: {
|
|
1013
|
-
file: refusal.file,
|
|
1014
|
-
...(refusal.location === undefined ? {} : { location: refusal.location }),
|
|
1015
|
-
...(refusal.referrers === undefined ? {} : { referrers: refusal.referrers }),
|
|
1016
|
-
},
|
|
1017
|
-
});
|
|
1018
|
-
declarationRefusals.delete(room.locator);
|
|
1664
|
+
function roomUnreadable(room) {
|
|
1019
1665
|
unreadable.add(room.locator);
|
|
1020
1666
|
list(room, false);
|
|
1021
1667
|
emitRooms();
|
|
1022
|
-
journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
|
|
1023
|
-
}
|
|
1024
|
-
function leaving(batch, result) {
|
|
1025
|
-
if (result.kind === 'expired')
|
|
1026
|
-
return { batch, returned: { kind: 'expired' } };
|
|
1027
|
-
return { batch, returned: { kind: result.kind, code: result.code, fields: result.fields } };
|
|
1028
|
-
}
|
|
1029
|
-
function logAccepted(locator, submitted, accepted) {
|
|
1030
|
-
journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
|
|
1031
|
-
(accepted.author === undefined ? '' : ` author ${accepted.author}`) +
|
|
1032
|
-
(accepted.time === undefined ? '' : ` time ${accepted.time}`));
|
|
1033
|
-
}
|
|
1034
|
-
async function adoptAccepted(room, local, submitted, accepted) {
|
|
1035
|
-
if (!continuing())
|
|
1036
|
-
return unconfirmed;
|
|
1037
|
-
const current = replicas.get(room.locator);
|
|
1038
|
-
if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
|
|
1039
|
-
return null;
|
|
1040
|
-
}
|
|
1041
|
-
const held = current.confirmed.record.roomVersion;
|
|
1042
|
-
logAccepted(room.locator, submitted, accepted);
|
|
1043
|
-
const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
|
|
1044
|
-
? { kind: 'staged', confirmed: current.confirmed }
|
|
1045
|
-
: applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
|
|
1046
|
-
? accepted.roomVersion
|
|
1047
|
-
: held, accepted.roomVersion);
|
|
1048
|
-
if (applied.kind !== 'staged') {
|
|
1049
|
-
const state = pendings.get(submitted.id);
|
|
1050
|
-
await local.accept(room.locator, submitted.id, accepted.roomVersion);
|
|
1051
|
-
if (state !== undefined)
|
|
1052
|
-
pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
|
|
1053
|
-
emitQueue(room.locator);
|
|
1054
|
-
return applied;
|
|
1055
|
-
}
|
|
1056
|
-
const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
|
|
1057
|
-
pendings.delete(submitted.id);
|
|
1058
|
-
unreadable.delete(room.locator);
|
|
1059
|
-
sourceRefusals.delete(room.locator);
|
|
1060
|
-
declarationRefusals.delete(room.locator);
|
|
1061
|
-
list(room, true);
|
|
1062
|
-
publishChangeSet(set);
|
|
1063
|
-
moveTo({ ...status, ...attention() });
|
|
1064
|
-
return null;
|
|
1065
|
-
}
|
|
1066
|
-
async function adopted(room, local, submitted, accepted) {
|
|
1067
|
-
const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
|
|
1068
|
-
if (failed === null)
|
|
1069
|
-
return true;
|
|
1070
|
-
if (failed.kind === 'refused' && continuing())
|
|
1071
|
-
refusedSource(room, failed);
|
|
1072
|
-
return false;
|
|
1073
1668
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
};
|
|
1087
|
-
if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
|
|
1088
|
-
progress.complete = false;
|
|
1089
|
-
return;
|
|
1090
|
-
}
|
|
1091
|
-
if (writesHeld()) {
|
|
1092
|
-
progress.complete = false;
|
|
1093
|
-
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1094
|
-
return;
|
|
1095
|
-
}
|
|
1096
|
-
const lost = earlier || submission.state === 'submitted';
|
|
1097
|
-
let result;
|
|
1098
|
-
journal.record(`submission ${room.locator} ${head.id}`);
|
|
1099
|
-
await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
|
|
1100
|
-
if (discoveryPaused)
|
|
1101
|
-
return;
|
|
1102
|
-
if (writesHeld()) {
|
|
1103
|
-
await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
|
|
1104
|
-
progress.complete = false;
|
|
1105
|
-
journal.record(`submission ${room.locator} ${head.id} held`);
|
|
1106
|
-
return;
|
|
1107
|
-
}
|
|
1108
|
-
const opensBefore = openSignals;
|
|
1109
|
-
const answered = awaitingAnswer(room.locator);
|
|
1110
|
-
try {
|
|
1111
|
-
result = await options.adapter.mutate(authorized, {
|
|
1112
|
-
locator: room.locator,
|
|
1113
|
-
changeBatch: head,
|
|
1114
|
-
...(state?.upload === undefined ? {} : { upload: state.upload }),
|
|
1115
|
-
}, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
|
|
1116
|
-
}
|
|
1117
|
-
catch {
|
|
1118
|
-
answered();
|
|
1119
|
-
progress.complete = false;
|
|
1120
|
-
if (continuing())
|
|
1121
|
-
progress.reachable = false;
|
|
1669
|
+
function askAgain(locator) {
|
|
1670
|
+
const held = unready.get(locator) ?? { tries: 0, timer: null };
|
|
1671
|
+
unready.set(locator, held);
|
|
1672
|
+
if (held.timer !== null)
|
|
1673
|
+
return;
|
|
1674
|
+
const wait = NOT_READY_MS[Math.min(held.tries, NOT_READY_MS.length - 1)];
|
|
1675
|
+
held.tries += 1;
|
|
1676
|
+
held.timer = setTimeout(() => {
|
|
1677
|
+
held.timer = null;
|
|
1678
|
+
const room = discovered.get(locator);
|
|
1679
|
+
const unstreamed = options.adapter.roomStream === undefined;
|
|
1680
|
+
if (closed || stopping || (!unstreamed && !socket.isOpen()) || room === undefined)
|
|
1122
1681
|
return;
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1682
|
+
const progress = { complete: true, reachable: true };
|
|
1683
|
+
void openLocal()
|
|
1684
|
+
.then((local) => roomTask(locator, (live) => loadRoom(room, local, progress, live)))
|
|
1685
|
+
.then(() => rest(progress), () => undefined);
|
|
1686
|
+
}, wait);
|
|
1687
|
+
}
|
|
1688
|
+
function readied(locator) {
|
|
1689
|
+
const held = unready.get(locator);
|
|
1690
|
+
if (held === undefined)
|
|
1691
|
+
return;
|
|
1692
|
+
if (held.timer !== null)
|
|
1693
|
+
clearTimeout(held.timer);
|
|
1694
|
+
unready.delete(locator);
|
|
1695
|
+
}
|
|
1696
|
+
function unreadied() {
|
|
1697
|
+
for (const locator of [...unready.keys()])
|
|
1698
|
+
readied(locator);
|
|
1699
|
+
}
|
|
1700
|
+
function declaredAsHeld(held, room) {
|
|
1701
|
+
return (held === undefined ||
|
|
1702
|
+
(JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
|
|
1703
|
+
JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
|
|
1704
|
+
JSON.stringify(held.providerOwnedPaths) === JSON.stringify(room.providerOwnedPaths) &&
|
|
1705
|
+
JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
|
|
1706
|
+
}
|
|
1707
|
+
async function keepNaming(local, room) {
|
|
1708
|
+
const held = await recordAt(room.locator);
|
|
1709
|
+
if (held === undefined || (held.name === room.name && held.handle === room.handle))
|
|
1710
|
+
return;
|
|
1711
|
+
await exclusive(async () => {
|
|
1126
1712
|
if (!continuing())
|
|
1127
1713
|
return;
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
progress.reachable = false;
|
|
1131
|
-
return;
|
|
1132
|
-
}
|
|
1133
|
-
if (result.kind === 'room-not-readable') {
|
|
1134
|
-
progress.complete = false;
|
|
1135
|
-
unreadable.add(room.locator);
|
|
1136
|
-
list(room, false);
|
|
1137
|
-
emitRooms();
|
|
1138
|
-
return;
|
|
1139
|
-
}
|
|
1140
|
-
if (result.kind === 'revoked-authorization') {
|
|
1141
|
-
await refuse('revoked-authorization');
|
|
1142
|
-
return;
|
|
1143
|
-
}
|
|
1144
|
-
if (result.kind === 'scoped-refusal') {
|
|
1145
|
-
await scoped(result, room, local);
|
|
1146
|
-
progress.complete = false;
|
|
1147
|
-
return;
|
|
1148
|
-
}
|
|
1149
|
-
if (result.kind === 'write-halted') {
|
|
1150
|
-
journal.record(`outcome ${room.locator} ${head.id} write-halted`);
|
|
1151
|
-
await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, lost));
|
|
1152
|
-
if (serviceMode === 'open' && openSignals !== opensBefore)
|
|
1153
|
-
continue;
|
|
1154
|
-
haltedRefusal = true;
|
|
1155
|
-
progress.complete = false;
|
|
1156
|
-
return;
|
|
1157
|
-
}
|
|
1158
|
-
if (result.kind === 'conflicted' ||
|
|
1159
|
-
result.kind === 'rejected' ||
|
|
1160
|
-
result.kind === 'expired' ||
|
|
1161
|
-
result.kind === 'retryable-refusal') {
|
|
1162
|
-
if (result.kind === 'expired') {
|
|
1163
|
-
const withheld = await advance(room, local, authorized);
|
|
1164
|
-
if (withheld !== null)
|
|
1165
|
-
return withhold(withheld, room, local, progress);
|
|
1166
|
-
}
|
|
1167
|
-
journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
|
|
1168
|
-
(result.kind === 'conflicted' && result.roomVersion !== undefined
|
|
1169
|
-
? ` ${result.roomVersion}`
|
|
1170
|
-
: ''));
|
|
1171
|
-
if (result.kind !== 'expired')
|
|
1172
|
-
journal.record(`refusal ${result.kind} ${result.code}`);
|
|
1173
|
-
}
|
|
1174
|
-
if (result.kind === 'retryable-refusal') {
|
|
1175
|
-
const reset = result.reset;
|
|
1176
|
-
progress.complete = false;
|
|
1177
|
-
await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
|
|
1178
|
-
armTimer();
|
|
1179
|
-
return;
|
|
1180
|
-
}
|
|
1181
|
-
if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
|
|
1182
|
-
const left = leaving(head, result);
|
|
1183
|
-
await exclusive(async () => {
|
|
1184
|
-
if (!continuing())
|
|
1185
|
-
return;
|
|
1186
|
-
const current = replicas.get(room.locator);
|
|
1187
|
-
if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
|
|
1188
|
-
return;
|
|
1189
|
-
}
|
|
1190
|
-
const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
|
|
1191
|
-
leaving: result.kind === 'expired' && landed(head, current.confirmed)
|
|
1192
|
-
? { ...left, returned: null }
|
|
1193
|
-
: left,
|
|
1194
|
-
});
|
|
1195
|
-
publishChangeSet(set);
|
|
1196
|
-
moveTo({ ...status, ...attention() });
|
|
1197
|
-
});
|
|
1198
|
-
if (result.kind === 'expired')
|
|
1199
|
-
continue;
|
|
1200
|
-
const withheld = await advance(room, local, authorized);
|
|
1201
|
-
if (withheld !== null)
|
|
1202
|
-
return withhold(withheld, room, local, progress);
|
|
1203
|
-
continue;
|
|
1204
|
-
}
|
|
1205
|
-
const kept = await adopted(room, local, head, result).finally(answered);
|
|
1206
|
-
if (!kept) {
|
|
1207
|
-
progress.complete = false;
|
|
1714
|
+
const current = await recordAt(room.locator);
|
|
1715
|
+
if (current === undefined)
|
|
1208
1716
|
return;
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1717
|
+
const record = { ...current, name: room.name, handle: room.handle };
|
|
1718
|
+
await local.commit({
|
|
1719
|
+
rooms: [
|
|
1720
|
+
{
|
|
1721
|
+
locator: room.locator,
|
|
1722
|
+
key: record.key,
|
|
1723
|
+
record: storedRecord(record),
|
|
1724
|
+
composed: composedOf(composed(record)),
|
|
1725
|
+
},
|
|
1726
|
+
],
|
|
1727
|
+
});
|
|
1728
|
+
emitRooms();
|
|
1216
1729
|
});
|
|
1217
|
-
answering.set(locator, awaited);
|
|
1218
|
-
return () => {
|
|
1219
|
-
if (answering.get(locator) === awaited)
|
|
1220
|
-
answering.delete(locator);
|
|
1221
|
-
release();
|
|
1222
|
-
};
|
|
1223
1730
|
}
|
|
1224
|
-
async function
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
JSON.stringify(heldBefore.typedSelection) ===
|
|
1233
|
-
JSON.stringify(room.typedSelection ?? null))))
|
|
1731
|
+
async function modelNow(locator) {
|
|
1732
|
+
return (await recordAt(locator))?.model ?? unheldModels.get(locator) ?? NO_MODEL;
|
|
1733
|
+
}
|
|
1734
|
+
async function retainModel(local, room, given) {
|
|
1735
|
+
const held = await recordAt(room.locator);
|
|
1736
|
+
const previous = held?.model ?? unheldModels.get(room.locator) ?? NO_MODEL;
|
|
1737
|
+
const next = given ?? previous;
|
|
1738
|
+
if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(held, room))
|
|
1234
1739
|
return;
|
|
1235
|
-
if (next.status === 'unavailable') {
|
|
1236
|
-
invalidModels.add(key);
|
|
1237
|
-
liveModels.delete(key);
|
|
1238
|
-
}
|
|
1239
1740
|
await exclusive(async () => {
|
|
1240
1741
|
if (!continuing())
|
|
1241
1742
|
return;
|
|
1242
|
-
const
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1743
|
+
const work = await workOf(room.locator);
|
|
1744
|
+
const current = work.replica;
|
|
1745
|
+
let changed = null;
|
|
1746
|
+
if (current !== undefined) {
|
|
1747
|
+
const record = {
|
|
1748
|
+
...current.record,
|
|
1749
|
+
name: room.name,
|
|
1750
|
+
handle: room.handle,
|
|
1246
1751
|
operations: room.operations,
|
|
1247
1752
|
replica: room.replica,
|
|
1753
|
+
providerOwnedPaths: room.providerOwnedPaths,
|
|
1248
1754
|
typedSelection: room.typedSelection ?? null,
|
|
1249
|
-
|
|
1755
|
+
model: next,
|
|
1250
1756
|
};
|
|
1251
|
-
const
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
const
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1757
|
+
const confirmedLayer = await sliceAt(store, record, 'confirmed', current.queue, current.overlaid);
|
|
1758
|
+
const replayedLayer = await replayed(store, record, confirmedLayer, current.queue);
|
|
1759
|
+
const diff = await overlayDiff(record, current, replayedLayer.visible, replayedLayer.touched);
|
|
1760
|
+
const answer = await local.commit({
|
|
1761
|
+
rooms: [
|
|
1762
|
+
{
|
|
1763
|
+
locator: room.locator,
|
|
1764
|
+
key: record.key,
|
|
1765
|
+
record: storedRecord(record),
|
|
1766
|
+
roots: record.typedSelection?.roots ?? [],
|
|
1767
|
+
composed: composedOf(composed(record)),
|
|
1768
|
+
overlay: diff.overlay,
|
|
1769
|
+
},
|
|
1770
|
+
],
|
|
1771
|
+
});
|
|
1772
|
+
preparedAfter(saver, room.locator, answer);
|
|
1773
|
+
changed = changeSet(room.locator, {
|
|
1774
|
+
documents: answer.changed[room.locator] ?? [],
|
|
1775
|
+
room: true,
|
|
1265
1776
|
});
|
|
1266
|
-
if (read.kind === 'staged') {
|
|
1267
|
-
replicas.set(room.locator, replicaOf(read.confirmed, held.queue, rewriting(), held));
|
|
1268
|
-
if (declarationRefusals.delete(room.locator)) {
|
|
1269
|
-
sourceRefusals.delete(room.locator);
|
|
1270
|
-
unreadable.delete(room.locator);
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
else if (read.kind === 'refused') {
|
|
1274
|
-
replicas.set(room.locator, { ...held, confirmed: { ...held.confirmed, record } });
|
|
1275
|
-
refusedSource(room, read);
|
|
1276
|
-
declarationRefusals.add(room.locator);
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
models.set(key, next);
|
|
1280
|
-
if (next.status === 'current') {
|
|
1281
|
-
liveModels.set(key, JSON.stringify(next.snapshot));
|
|
1282
|
-
invalidModels.delete(key);
|
|
1283
1777
|
}
|
|
1778
|
+
else
|
|
1779
|
+
unheldModels.set(room.locator, next);
|
|
1284
1780
|
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
1285
|
-
if (
|
|
1781
|
+
if (changed === null)
|
|
1286
1782
|
emitRooms();
|
|
1287
1783
|
else
|
|
1288
|
-
publishChangeSet(
|
|
1784
|
+
publishChangeSet(changed);
|
|
1289
1785
|
});
|
|
1290
1786
|
}
|
|
1291
1787
|
async function scoped(result, room, local) {
|
|
1292
|
-
|
|
1788
|
+
if (result.scope === 'content' && !result.retryable) {
|
|
1789
|
+
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
1790
|
+
await loseRooms([room.locator]);
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
scopedRefusals.set(room.locator, {
|
|
1794
|
+
code: result.code,
|
|
1795
|
+
fields: result.fields,
|
|
1796
|
+
retryable: result.retryable,
|
|
1797
|
+
});
|
|
1293
1798
|
let scopedAway = room;
|
|
1294
1799
|
if (result.scope === 'model')
|
|
1295
|
-
await retainModel(local, room,
|
|
1800
|
+
await retainModel(local, room, {
|
|
1296
1801
|
status: 'unavailable',
|
|
1297
1802
|
reason: 'not-readable',
|
|
1298
1803
|
});
|
|
1299
1804
|
else if (result.scope !== 'operation') {
|
|
1300
1805
|
unreadable.add(room.locator);
|
|
1301
|
-
scopedAway = {
|
|
1302
|
-
|
|
1303
|
-
replica: { ...room.replica, admission: 'unavailable' },
|
|
1304
|
-
operations: {
|
|
1305
|
-
...room.operations,
|
|
1306
|
-
...(result.scope === 'content' ? { read: [] } : {}),
|
|
1307
|
-
...(result.scope === 'sync' ? { sync: [] } : {}),
|
|
1308
|
-
},
|
|
1309
|
-
};
|
|
1310
|
-
await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
|
|
1806
|
+
scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
|
|
1807
|
+
await retainModel(local, scopedAway, null);
|
|
1311
1808
|
}
|
|
1312
1809
|
if (!continuing())
|
|
1313
1810
|
return;
|
|
@@ -1315,141 +1812,194 @@ export function composeRuntime(options) {
|
|
|
1315
1812
|
emitRooms();
|
|
1316
1813
|
journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
|
|
1317
1814
|
}
|
|
1318
|
-
function
|
|
1815
|
+
async function name(locator) {
|
|
1816
|
+
const room = discovered.get(locator);
|
|
1817
|
+
if (!socket.streamed || room === undefined || !socket.isOpen() || !socket.isHanded())
|
|
1818
|
+
return;
|
|
1819
|
+
const [held] = await heldRooms([room]);
|
|
1820
|
+
if (held === undefined || !socket.isOpen() || !socket.isHanded())
|
|
1821
|
+
return;
|
|
1822
|
+
socket.name(held);
|
|
1823
|
+
}
|
|
1824
|
+
async function heldRooms(listed) {
|
|
1825
|
+
const stored = await storedRooms(listed.map((room) => room.locator));
|
|
1319
1826
|
return listed.flatMap((room) => {
|
|
1320
|
-
const record =
|
|
1321
|
-
if (record === undefined
|
|
1827
|
+
const record = stored.get(room.locator)?.record;
|
|
1828
|
+
if (record === undefined) {
|
|
1829
|
+
return room.replica.admission === 'admitted'
|
|
1830
|
+
? [{ locator: room.locator, roomVersion: null }]
|
|
1831
|
+
: [];
|
|
1832
|
+
}
|
|
1833
|
+
if (record.replica.selection !== room.replica.selection)
|
|
1322
1834
|
return [];
|
|
1323
|
-
return [
|
|
1324
|
-
{
|
|
1325
|
-
locator: room.locator,
|
|
1326
|
-
roomVersion: record.roomVersion,
|
|
1327
|
-
documents: record.documents,
|
|
1328
|
-
revisions: record.revisions,
|
|
1329
|
-
attachments: record.attachments,
|
|
1330
|
-
},
|
|
1331
|
-
];
|
|
1835
|
+
return [{ locator: room.locator, roomVersion: record.roomVersion }];
|
|
1332
1836
|
});
|
|
1333
1837
|
}
|
|
1334
|
-
async function synchronize(listed, local, progress, attempt) {
|
|
1335
|
-
const
|
|
1838
|
+
async function synchronize(listed, local, progress, attempt, live) {
|
|
1839
|
+
const heldNow = await storedRooms(listed.map(({ locator }) => locator));
|
|
1840
|
+
const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress, heldNow.get(room.locator)?.record)));
|
|
1841
|
+
if (!live())
|
|
1842
|
+
return;
|
|
1336
1843
|
const ready = listed.filter((_, index) => {
|
|
1337
1844
|
const outcome = prepared[index];
|
|
1338
1845
|
return outcome?.status === 'fulfilled' && outcome.value;
|
|
1339
1846
|
});
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
.
|
|
1345
|
-
|
|
1847
|
+
const held = await heldRooms(ready);
|
|
1848
|
+
if (!live())
|
|
1849
|
+
return;
|
|
1850
|
+
if (attempt === 'opened') {
|
|
1851
|
+
socket.hand(held);
|
|
1852
|
+
}
|
|
1853
|
+
const kept = attempt === 'unstreamed'
|
|
1854
|
+
? new Set()
|
|
1855
|
+
: new Set((await storedRooms(ready.map(({ locator }) => locator))).keys());
|
|
1856
|
+
for (const room of ready) {
|
|
1857
|
+
if (attempt === 'opened' || kept.has(room.locator)) {
|
|
1858
|
+
if (attempt === 'authorized')
|
|
1859
|
+
await name(room.locator);
|
|
1860
|
+
continue;
|
|
1861
|
+
}
|
|
1862
|
+
void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
|
|
1863
|
+
}
|
|
1864
|
+
const failed = prepared.find((room) => room.status === 'rejected');
|
|
1346
1865
|
if (failed !== undefined)
|
|
1347
1866
|
throw failed.reason;
|
|
1348
1867
|
}
|
|
1349
|
-
async function prepareRoom(room, local, progress) {
|
|
1868
|
+
async function prepareRoom(room, local, progress, prior) {
|
|
1350
1869
|
if (closed || authorization === undefined) {
|
|
1351
1870
|
progress.complete = false;
|
|
1352
1871
|
return false;
|
|
1353
1872
|
}
|
|
1354
|
-
const prior = replicas.get(room.locator)?.confirmed.record;
|
|
1355
1873
|
if (prior !== undefined && prior.replica.selection !== room.replica.selection) {
|
|
1356
|
-
|
|
1874
|
+
listHeld(room.locator);
|
|
1357
1875
|
emitRooms();
|
|
1358
1876
|
journal.record(`selection changed ${room.locator}`);
|
|
1359
1877
|
progress.complete = false;
|
|
1360
1878
|
return false;
|
|
1361
1879
|
}
|
|
1362
|
-
if (prior !== undefined)
|
|
1363
|
-
await retainModel(local, room,
|
|
1880
|
+
if (prior !== undefined && !declaredAsHeld(prior, room))
|
|
1881
|
+
await retainModel(local, room, null);
|
|
1882
|
+
if (prior !== undefined && (prior.name !== room.name || prior.handle !== room.handle)) {
|
|
1883
|
+
await keepNaming(local, room);
|
|
1884
|
+
}
|
|
1364
1885
|
if (discoveryPaused || authorization === undefined)
|
|
1365
1886
|
return false;
|
|
1366
|
-
|
|
1367
|
-
if (replicas.has(room.locator))
|
|
1368
|
-
progress.complete = false;
|
|
1369
|
-
return false;
|
|
1370
|
-
}
|
|
1371
|
-
return true;
|
|
1887
|
+
return room.replica.admission === 'admitted';
|
|
1372
1888
|
}
|
|
1373
|
-
async function caughtUp(room, local, progress) {
|
|
1374
|
-
|
|
1375
|
-
if (held === undefined || authorization === undefined)
|
|
1889
|
+
async function caughtUp(room, local, progress, live) {
|
|
1890
|
+
if (options.adapter.roomStream !== undefined || authorization === undefined)
|
|
1376
1891
|
return;
|
|
1377
|
-
|
|
1378
|
-
if (!behind)
|
|
1892
|
+
if (!live() || !(await behind(installer, room)))
|
|
1379
1893
|
return;
|
|
1380
|
-
const withheld = await advance(room, local, authorization);
|
|
1894
|
+
const withheld = await advance(room, local, authorization, live);
|
|
1381
1895
|
if (withheld !== null)
|
|
1382
1896
|
await withhold(withheld, room, local, progress);
|
|
1383
1897
|
}
|
|
1384
|
-
function revoked(
|
|
1385
|
-
return
|
|
1386
|
-
const model = modelOf(locator, attached);
|
|
1387
|
-
return model.status === 'unavailable' && model.reason === 'revoked';
|
|
1388
|
-
});
|
|
1898
|
+
function revoked(model) {
|
|
1899
|
+
return model?.status === 'unavailable' && model.reason === 'revoked';
|
|
1389
1900
|
}
|
|
1390
|
-
async function loadRoom(room, local, progress) {
|
|
1391
|
-
if (
|
|
1392
|
-
return;
|
|
1393
|
-
if (replicas.has(room.locator) && revoked(room.locator)) {
|
|
1394
|
-
await reload(room, local, progress);
|
|
1901
|
+
async function loadRoom(room, local, progress, live, unheld) {
|
|
1902
|
+
if (!live() || authorization === undefined)
|
|
1395
1903
|
return;
|
|
1904
|
+
const prior = unheld === undefined ? await recordAt(room.locator) : undefined;
|
|
1905
|
+
if (prior !== undefined) {
|
|
1906
|
+
if (!(await prepareRoom(room, local, progress, prior)))
|
|
1907
|
+
return;
|
|
1908
|
+
if (revoked(prior.model)) {
|
|
1909
|
+
await reload(room, local, progress, live);
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1396
1912
|
}
|
|
1397
|
-
const withheld = await advance(room, local, authorization);
|
|
1913
|
+
const withheld = await advance(room, local, authorization, live, unheld);
|
|
1398
1914
|
if (withheld !== null) {
|
|
1399
1915
|
await withhold(withheld, room, local, progress);
|
|
1400
1916
|
return;
|
|
1401
1917
|
}
|
|
1402
|
-
await caughtUp(room, local, progress);
|
|
1918
|
+
await caughtUp(room, local, progress, live);
|
|
1403
1919
|
}
|
|
1404
|
-
async function reload(room, local, progress) {
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1920
|
+
async function reload(room, local, progress, live) {
|
|
1921
|
+
try {
|
|
1922
|
+
if (live())
|
|
1923
|
+
reloadsDue.delete(room.locator);
|
|
1924
|
+
if (!live() || authorization === undefined)
|
|
1925
|
+
return;
|
|
1926
|
+
const prior = await recordAt(room.locator);
|
|
1927
|
+
const from = standingAt(store, room.locator, prior);
|
|
1928
|
+
const bootstrapped = await pullBootstrap(installer, authorization, room, local, live, prior);
|
|
1929
|
+
if (bootstrapped.kind !== 'staged') {
|
|
1930
|
+
if (live())
|
|
1931
|
+
await withhold(bootstrapped, room, local, progress);
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
await publish(installer, local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
|
|
1935
|
+
await caughtUp(room, local, progress, live);
|
|
1936
|
+
}
|
|
1937
|
+
finally {
|
|
1938
|
+
caughtUpTo(installer, room.locator);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
async function asked(authorized) {
|
|
1942
|
+
try {
|
|
1943
|
+
return (await options.adapter.discover?.(authorized)) ?? { kind: 'transport-uncertainty' };
|
|
1944
|
+
}
|
|
1945
|
+
catch {
|
|
1946
|
+
return { kind: 'transport-uncertainty' };
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
function malformed(listed) {
|
|
1950
|
+
for (const room of listed) {
|
|
1951
|
+
if (room.operations === undefined ||
|
|
1952
|
+
!['write', 'erase'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
1953
|
+
room.operations[operation].every((scope) => typeof scope === 'string')) ||
|
|
1954
|
+
room.replica === undefined ||
|
|
1955
|
+
typeof room.replica.selection !== 'string' ||
|
|
1956
|
+
room.replica.selection.length === 0 ||
|
|
1957
|
+
!['admitted', 'unavailable'].includes(room.replica.admission) ||
|
|
1958
|
+
!['locked', 'open'].includes(room.replica.offline)) {
|
|
1959
|
+
return `invalid model or replica declaration ${room.locator}`;
|
|
1960
|
+
}
|
|
1961
|
+
if (!declaredSelection(room))
|
|
1962
|
+
return `invalid typed selection declaration ${room.locator}`;
|
|
1413
1963
|
}
|
|
1414
|
-
|
|
1415
|
-
|
|
1964
|
+
return null;
|
|
1965
|
+
}
|
|
1966
|
+
async function signIn(local, signedIn) {
|
|
1967
|
+
if (JSON.stringify(signedIn) === JSON.stringify((await local.facts()).member))
|
|
1968
|
+
return;
|
|
1969
|
+
await local.fact('member', signedIn);
|
|
1970
|
+
emitMember(signedIn);
|
|
1416
1971
|
}
|
|
1417
|
-
async function discoverRooms(local, progress, attempt) {
|
|
1972
|
+
async function discoverRooms(local, progress, attempt, live, found) {
|
|
1418
1973
|
if (closed || authorization === undefined) {
|
|
1419
1974
|
notifyReady();
|
|
1420
1975
|
return;
|
|
1421
1976
|
}
|
|
1422
|
-
moveTo(transferring(
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
if (closed) {
|
|
1977
|
+
moveTo(transferring());
|
|
1978
|
+
discoveries += 1;
|
|
1979
|
+
const started = discoveries;
|
|
1980
|
+
const result = found ?? (await asked(authorization));
|
|
1981
|
+
if (result.kind === 'discovery')
|
|
1982
|
+
roomsNamed = true;
|
|
1983
|
+
if (!live()) {
|
|
1984
|
+
progress.complete = false;
|
|
1431
1985
|
notifyReady();
|
|
1432
1986
|
return;
|
|
1433
1987
|
}
|
|
1434
1988
|
journal.record(`connection discover ${result.kind}`);
|
|
1435
1989
|
if (result.kind === 'transport-uncertainty') {
|
|
1436
|
-
|
|
1437
|
-
await retainModel(local, room, 'collection', cached(modelOf(room.locator)));
|
|
1438
|
-
if (room.typedSelection !== null) {
|
|
1439
|
-
await retainModel(local, room, 'typed-selection', cached(modelOf(room.locator, 'typed-selection')));
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1990
|
+
await cachedModels(local);
|
|
1442
1991
|
progress.reachable = false;
|
|
1443
1992
|
progress.complete = false;
|
|
1444
|
-
if (attempt === 'opened') {
|
|
1445
|
-
options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
|
|
1446
|
-
}
|
|
1447
1993
|
notifyReady();
|
|
1448
1994
|
return;
|
|
1449
1995
|
}
|
|
1450
1996
|
if (result.kind === 'contract-unavailable') {
|
|
1451
1997
|
journal.record(`connection contract unavailable ${result.code}`);
|
|
1452
|
-
await refuse('missing-required-guarantee', {
|
|
1998
|
+
await refuse('missing-required-guarantee', {
|
|
1999
|
+
code: result.code,
|
|
2000
|
+
fields: result.fields,
|
|
2001
|
+
retryable: result.retryable,
|
|
2002
|
+
});
|
|
1453
2003
|
notifyReady();
|
|
1454
2004
|
return;
|
|
1455
2005
|
}
|
|
@@ -1458,59 +2008,26 @@ export function composeRuntime(options) {
|
|
|
1458
2008
|
notifyReady();
|
|
1459
2009
|
return;
|
|
1460
2010
|
}
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
}
|
|
1466
|
-
if (result.mdModel.identity !== MD_MODEL.identity ||
|
|
1467
|
-
result.mdModel.version !== MD_MODEL.version) {
|
|
1468
|
-
await refuse('md-model-version-mismatch');
|
|
2011
|
+
const invalid = malformed(result.rooms);
|
|
2012
|
+
if (invalid !== null) {
|
|
2013
|
+
journal.record(invalid);
|
|
2014
|
+
await refuse('missing-required-guarantee');
|
|
1469
2015
|
notifyReady();
|
|
1470
2016
|
return;
|
|
1471
2017
|
}
|
|
1472
|
-
for (const room of result.rooms) {
|
|
1473
|
-
if (room.operations === undefined ||
|
|
1474
|
-
!['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
|
|
1475
|
-
room.operations[operation].every((scope) => typeof scope === 'string')) ||
|
|
1476
|
-
room.replica === undefined ||
|
|
1477
|
-
typeof room.replica.selection !== 'string' ||
|
|
1478
|
-
room.replica.selection.length === 0 ||
|
|
1479
|
-
!['admitted', 'unavailable'].includes(room.replica.admission)) {
|
|
1480
|
-
journal.record(`invalid model or replica declaration ${room.locator}`);
|
|
1481
|
-
await refuse('missing-required-guarantee');
|
|
1482
|
-
notifyReady();
|
|
1483
|
-
return;
|
|
1484
|
-
}
|
|
1485
|
-
if (!declaredSelection(room)) {
|
|
1486
|
-
journal.record(`invalid typed selection declaration ${room.locator}`);
|
|
1487
|
-
await refuse('missing-required-guarantee');
|
|
1488
|
-
notifyReady();
|
|
1489
|
-
return;
|
|
1490
|
-
}
|
|
1491
|
-
}
|
|
1492
|
-
const declared = new Set(result.capabilities.map((capability) => capability.name));
|
|
1493
|
-
for (const required of options.requiredGuarantees) {
|
|
1494
|
-
if (!knownCapabilities.has(required) || !declared.has(required)) {
|
|
1495
|
-
await refuse('missing-required-guarantee');
|
|
1496
|
-
notifyReady();
|
|
1497
|
-
return;
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
2018
|
if (refusal !== null) {
|
|
1501
2019
|
refusal = null;
|
|
1502
2020
|
emitRefusal();
|
|
1503
2021
|
}
|
|
1504
|
-
const
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
limits = result.providerLimits;
|
|
1510
|
-
await local.declare(limits, capabilities);
|
|
2022
|
+
const held = await local.facts();
|
|
2023
|
+
if (JSON.stringify(result.providerLimits) !== JSON.stringify(held.providerLimits) ||
|
|
2024
|
+
JSON.stringify(result.capabilities) !== JSON.stringify(held.capabilities)) {
|
|
2025
|
+
await local.fact('providerLimits', result.providerLimits);
|
|
2026
|
+
await local.fact('capabilities', result.capabilities);
|
|
1511
2027
|
}
|
|
1512
|
-
|
|
1513
|
-
await
|
|
2028
|
+
notifyReady();
|
|
2029
|
+
await signIn(local, result.member ?? null);
|
|
2030
|
+
await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
|
|
1514
2031
|
}
|
|
1515
2032
|
async function ensureAuthorization() {
|
|
1516
2033
|
if (authorization !== undefined)
|
|
@@ -1524,247 +2041,538 @@ export function composeRuntime(options) {
|
|
|
1524
2041
|
throw error;
|
|
1525
2042
|
}
|
|
1526
2043
|
}
|
|
1527
|
-
function
|
|
2044
|
+
async function admittedRead(work) {
|
|
2045
|
+
while (roomReads.free === 0) {
|
|
2046
|
+
await new Promise((admit) => roomReads.waiting.push(admit));
|
|
2047
|
+
}
|
|
2048
|
+
roomReads.free -= 1;
|
|
2049
|
+
try {
|
|
2050
|
+
await work();
|
|
2051
|
+
}
|
|
2052
|
+
finally {
|
|
2053
|
+
roomReads.free += 1;
|
|
2054
|
+
roomReads.waiting.shift()?.();
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
function inRoom(locator, work) {
|
|
2058
|
+
roomWork += 1;
|
|
2059
|
+
const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
|
|
2060
|
+
try {
|
|
2061
|
+
return await work(continuing);
|
|
2062
|
+
}
|
|
2063
|
+
finally {
|
|
2064
|
+
roomWork -= 1;
|
|
2065
|
+
}
|
|
2066
|
+
});
|
|
2067
|
+
const chain = ran.then(() => undefined, () => undefined);
|
|
2068
|
+
roomLanes.set(locator, chain);
|
|
2069
|
+
working.add(chain);
|
|
2070
|
+
void chain.finally(() => {
|
|
2071
|
+
working.delete(chain);
|
|
2072
|
+
if (roomLanes.get(locator) === chain)
|
|
2073
|
+
roomLanes.delete(locator);
|
|
2074
|
+
});
|
|
2075
|
+
return ran;
|
|
2076
|
+
}
|
|
2077
|
+
function roomTask(locator, work) {
|
|
1528
2078
|
if (stopping)
|
|
1529
|
-
return
|
|
1530
|
-
|
|
1531
|
-
return chain;
|
|
2079
|
+
return Promise.resolve();
|
|
2080
|
+
return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
|
|
1532
2081
|
}
|
|
1533
|
-
function
|
|
1534
|
-
return
|
|
1535
|
-
|
|
1536
|
-
|
|
2082
|
+
function advanced(room, local, authorized) {
|
|
2083
|
+
return inRoom(room.locator, (live) => advance(room, local, authorized, live));
|
|
2084
|
+
}
|
|
2085
|
+
function task(lane, work) {
|
|
2086
|
+
if (stopping)
|
|
2087
|
+
return Promise.resolve();
|
|
2088
|
+
const held = lanes.get(lane);
|
|
2089
|
+
const opened = socket.generation();
|
|
2090
|
+
const current = held?.generation === opened ? held : { generation: opened, chain: Promise.resolve() };
|
|
2091
|
+
const turn = current.generation;
|
|
2092
|
+
const live = () => turn === socket.generation() && continuing();
|
|
2093
|
+
const run = current.chain.then(() => work(live)).catch(() => undefined);
|
|
2094
|
+
current.chain = run;
|
|
2095
|
+
lanes.set(lane, current);
|
|
2096
|
+
working.add(run);
|
|
2097
|
+
void run.finally(() => working.delete(run));
|
|
2098
|
+
return run;
|
|
2099
|
+
}
|
|
2100
|
+
function sync(attempt, found) {
|
|
2101
|
+
return task(OPENING_LANE, async (live) => {
|
|
2102
|
+
if (!live()) {
|
|
2103
|
+
readyFromStore();
|
|
1537
2104
|
return;
|
|
1538
2105
|
}
|
|
1539
2106
|
const progress = { complete: true, reachable: true };
|
|
1540
2107
|
underway = attempt === 'opened' ? progress : null;
|
|
2108
|
+
if (attempt === 'opened')
|
|
2109
|
+
streamed = progress;
|
|
1541
2110
|
complete = false;
|
|
1542
2111
|
recorded = false;
|
|
1543
|
-
moveTo(transferring(
|
|
2112
|
+
moveTo(transferring());
|
|
1544
2113
|
try {
|
|
1545
2114
|
const local = await openLocal();
|
|
1546
|
-
if (
|
|
2115
|
+
if (!live())
|
|
1547
2116
|
return;
|
|
1548
2117
|
await ensureAuthorization();
|
|
1549
|
-
if (
|
|
2118
|
+
if (!live())
|
|
1550
2119
|
return;
|
|
1551
|
-
await discoverRooms(local, progress, attempt);
|
|
2120
|
+
await discoverRooms(local, progress, attempt, live, found);
|
|
1552
2121
|
}
|
|
1553
2122
|
catch {
|
|
1554
2123
|
progress.complete = false;
|
|
1555
|
-
if (attempt === 'opened')
|
|
1556
|
-
|
|
1557
|
-
}
|
|
2124
|
+
if (attempt === 'opened' && !socket.isHanded())
|
|
2125
|
+
progress.reachable = false;
|
|
1558
2126
|
}
|
|
1559
2127
|
finally {
|
|
1560
2128
|
notifyReady();
|
|
1561
2129
|
}
|
|
1562
|
-
if (
|
|
2130
|
+
if (!live())
|
|
2131
|
+
return;
|
|
2132
|
+
if (attempt !== 'opened') {
|
|
2133
|
+
await finishOpening(progress, live);
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
if (socket.isHanded() || closed || progress.reachable)
|
|
2137
|
+
return;
|
|
2138
|
+
underway = null;
|
|
2139
|
+
await rest(progress, live);
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
function resynchronize() {
|
|
2143
|
+
return task(OPENING_LANE, async (live) => {
|
|
2144
|
+
if (!live() || !socket.isAnnounced())
|
|
2145
|
+
return;
|
|
2146
|
+
const progress = { complete: true, reachable: true };
|
|
2147
|
+
moveTo(transferring());
|
|
2148
|
+
try {
|
|
2149
|
+
const local = await openLocal();
|
|
2150
|
+
if (!live())
|
|
2151
|
+
return;
|
|
2152
|
+
await synchronize([...discovered.values()], local, progress, 'unstreamed', live);
|
|
2153
|
+
}
|
|
2154
|
+
catch {
|
|
2155
|
+
progress.complete = false;
|
|
2156
|
+
}
|
|
2157
|
+
if (live())
|
|
2158
|
+
await finishOpening(progress, live);
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
function countAuthorityChange() {
|
|
2162
|
+
authorityChanges += 1;
|
|
2163
|
+
for (const listener of authorityChangeListeners)
|
|
2164
|
+
listener(authorityChanges);
|
|
2165
|
+
lastAnswers.again();
|
|
2166
|
+
}
|
|
2167
|
+
function changeAuthority(change) {
|
|
2168
|
+
return task(OPENING_LANE, async (live) => {
|
|
2169
|
+
if (!live() || !socket.isAnnounced())
|
|
2170
|
+
return;
|
|
2171
|
+
countAuthorityChange();
|
|
2172
|
+
const progress = { complete: true, reachable: true };
|
|
2173
|
+
moveTo(transferring());
|
|
2174
|
+
try {
|
|
2175
|
+
const local = await openLocal();
|
|
2176
|
+
if (!live())
|
|
2177
|
+
return;
|
|
2178
|
+
const invalid = malformed(change.rooms);
|
|
2179
|
+
if (invalid !== null) {
|
|
2180
|
+
journal.record(invalid);
|
|
2181
|
+
await refuse('missing-required-guarantee');
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
if (change.member !== undefined)
|
|
2185
|
+
await signIn(local, change.member);
|
|
2186
|
+
discoveries += 1;
|
|
2187
|
+
const listed = await applyDiscovery(change.rooms, discoveries, change.left);
|
|
2188
|
+
await synchronize(listed, local, progress, 'authorized', live);
|
|
2189
|
+
}
|
|
2190
|
+
catch {
|
|
2191
|
+
progress.complete = false;
|
|
2192
|
+
}
|
|
2193
|
+
if (!live())
|
|
1563
2194
|
return;
|
|
1564
|
-
|
|
2195
|
+
if (!progress.complete)
|
|
2196
|
+
complete = false;
|
|
2197
|
+
await rest(progress, live);
|
|
1565
2198
|
});
|
|
1566
2199
|
}
|
|
1567
|
-
function closedSocket(revoked) {
|
|
1568
|
-
|
|
1569
|
-
|
|
2200
|
+
function closedSocket(revoked, lost, opened) {
|
|
2201
|
+
notifyReady();
|
|
2202
|
+
unreadied();
|
|
2203
|
+
connectionLost();
|
|
2204
|
+
if (lost)
|
|
2205
|
+
conversations.lost();
|
|
1570
2206
|
if (revoked) {
|
|
1571
2207
|
void refuse('revoked-authorization');
|
|
1572
2208
|
return;
|
|
1573
2209
|
}
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
2210
|
+
complete = false;
|
|
2211
|
+
if (streamed !== null)
|
|
2212
|
+
streamed.complete = false;
|
|
2213
|
+
moveTo({
|
|
2214
|
+
...status,
|
|
2215
|
+
state: status.state === 'synced' ? 'syncing' : status.state,
|
|
2216
|
+
providerUnreachable: status.providerUnreachable || (!opened && lost),
|
|
2217
|
+
});
|
|
2218
|
+
void closing(opened);
|
|
1577
2219
|
}
|
|
1578
|
-
function
|
|
1579
|
-
return task(async () => {
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
2220
|
+
function closing(opened) {
|
|
2221
|
+
return task(OPENING_LANE, async (live) => {
|
|
2222
|
+
if (!live())
|
|
2223
|
+
return;
|
|
2224
|
+
const local = await openLocal();
|
|
2225
|
+
if (!opened && !socket.isOpen())
|
|
2226
|
+
await cachedModels(local);
|
|
2227
|
+
await rest(null, live);
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
async function cachedModels(local) {
|
|
2231
|
+
const held = await storedRooms();
|
|
2232
|
+
await settleModels(listedLocators(held).flatMap((locator) => {
|
|
2233
|
+
const room = discovered.get(locator) ?? declarationOf(locator, held.get(locator))?.room;
|
|
2234
|
+
return room === undefined ? [] : [room];
|
|
2235
|
+
}), 'cached', local);
|
|
2236
|
+
}
|
|
2237
|
+
function caughtUpRooms(expired) {
|
|
2238
|
+
const progress = underway;
|
|
2239
|
+
underway = null;
|
|
2240
|
+
socket.named();
|
|
2241
|
+
const cycle = progress ?? { complete: true, reachable: true };
|
|
2242
|
+
replaced(store, expired);
|
|
2243
|
+
const installing = store.installs.underway();
|
|
2244
|
+
const held = progress === null ? storedRooms(expired) : storedRooms();
|
|
2245
|
+
const reloading = progress === null
|
|
2246
|
+
? Promise.resolve([])
|
|
2247
|
+
: held.then((stored) => owedReloads(expired, stored));
|
|
2248
|
+
const replacing = Promise.all([held, reloading]).then(([stored, owed]) => reloadNow((room) => expired.includes(room.locator) || owed.includes(room.locator), cycle, stored, installing));
|
|
2249
|
+
return task(OPENING_LANE, async (live) => {
|
|
2250
|
+
if (!live())
|
|
1583
2251
|
return;
|
|
1584
2252
|
if (progress === null) {
|
|
1585
|
-
await
|
|
2253
|
+
await replacing;
|
|
2254
|
+
await rest(cycle, live);
|
|
1586
2255
|
return;
|
|
1587
2256
|
}
|
|
1588
2257
|
try {
|
|
1589
2258
|
const local = await openLocal();
|
|
1590
|
-
|
|
1591
|
-
const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
|
|
1592
|
-
const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
|
|
2259
|
+
await replacing;
|
|
1593
2260
|
if (progress.reachable) {
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
2261
|
+
const stored = await held;
|
|
2262
|
+
const owed = await reloading;
|
|
2263
|
+
await settleModels([...discovered.values()].filter((room) => stored.has(room.locator) &&
|
|
2264
|
+
!expired.includes(room.locator) &&
|
|
2265
|
+
!owed.includes(room.locator)), 'current', local);
|
|
1597
2266
|
}
|
|
1598
|
-
const failed = reloaded.find((outcome) => outcome.status === 'rejected');
|
|
1599
|
-
if (failed !== undefined)
|
|
1600
|
-
throw failed.reason;
|
|
1601
2267
|
}
|
|
1602
2268
|
catch {
|
|
1603
2269
|
progress.complete = false;
|
|
1604
2270
|
progress.reachable = false;
|
|
1605
2271
|
}
|
|
1606
|
-
|
|
2272
|
+
if (live())
|
|
2273
|
+
await finishOpening(progress, live);
|
|
1607
2274
|
});
|
|
1608
2275
|
}
|
|
1609
|
-
|
|
1610
|
-
const
|
|
2276
|
+
function owedReloads(expired, held) {
|
|
2277
|
+
const reloading = [...discovered.values()]
|
|
2278
|
+
.filter((room) => !expired.includes(room.locator) && revoked(held.get(room.locator)?.record.model))
|
|
2279
|
+
.map((room) => room.locator);
|
|
2280
|
+
for (const locator of reloading)
|
|
2281
|
+
catching(store, locator);
|
|
2282
|
+
recoverReads();
|
|
2283
|
+
return reloading;
|
|
2284
|
+
}
|
|
2285
|
+
async function reloadNow(replaced, progress, held, installing) {
|
|
2286
|
+
const reloaded = [...discovered.values()].filter((room) => held.has(room.locator) && !installing.has(room.locator) && replaced(room));
|
|
2287
|
+
const acquired = [...discovered.values()].filter((room) => (!held.has(room.locator) || installing.has(room.locator)) &&
|
|
2288
|
+
room.replica.admission === 'admitted' &&
|
|
2289
|
+
replaced(room));
|
|
2290
|
+
for (const room of reloaded)
|
|
2291
|
+
reloadsDue.add(room.locator);
|
|
1611
2292
|
try {
|
|
1612
2293
|
const local = await openLocal();
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
.
|
|
2294
|
+
const kept = acquired.length === 0 ? new Set() : (await local.held()).staged;
|
|
2295
|
+
for (const room of reloaded) {
|
|
2296
|
+
void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
|
|
2297
|
+
}
|
|
2298
|
+
for (const room of acquired) {
|
|
2299
|
+
const unheld = installing.has(room.locator)
|
|
2300
|
+
? undefined
|
|
2301
|
+
: { staged: kept.has(room.locator) };
|
|
2302
|
+
void roomTask(room.locator, (live) => loadRoom(room, local, progress, live, unheld)).then(() => rest(progress));
|
|
2303
|
+
}
|
|
1616
2304
|
}
|
|
1617
2305
|
catch {
|
|
2306
|
+
for (const room of reloaded)
|
|
2307
|
+
reloadsDue.delete(room.locator);
|
|
1618
2308
|
progress.reachable = false;
|
|
1619
2309
|
}
|
|
1620
|
-
await rest(progress);
|
|
1621
2310
|
}
|
|
1622
|
-
async function
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
if (
|
|
1626
|
-
|
|
2311
|
+
async function settleModels(held, status, local) {
|
|
2312
|
+
const owed = (model) => model.status !== 'unavailable' && model.status !== status;
|
|
2313
|
+
const declaredAnew = await exclusive(async () => {
|
|
2314
|
+
if (!continuing())
|
|
2315
|
+
return [];
|
|
2316
|
+
const stored = await storedRooms(held.map(({ locator }) => locator));
|
|
2317
|
+
const unchanged = held.filter((room) => {
|
|
2318
|
+
const record = stored.get(room.locator)?.record;
|
|
2319
|
+
return (owed(record?.model ?? unheldModels.get(room.locator) ?? NO_MODEL) &&
|
|
2320
|
+
declaredAsHeld(record, room));
|
|
2321
|
+
});
|
|
2322
|
+
const records = unchanged.flatMap((room) => {
|
|
2323
|
+
const record = stored.get(room.locator)?.record;
|
|
2324
|
+
return record === undefined
|
|
2325
|
+
? []
|
|
2326
|
+
: [{ ...record, model: { ...record.model, status } }];
|
|
2327
|
+
});
|
|
2328
|
+
if (records.length > 0) {
|
|
2329
|
+
try {
|
|
2330
|
+
await local.commit({
|
|
2331
|
+
rooms: records.map((record) => ({
|
|
2332
|
+
locator: record.locator,
|
|
2333
|
+
key: record.key,
|
|
2334
|
+
record: storedRecord(record),
|
|
2335
|
+
composed: composedOf(composed(record)),
|
|
2336
|
+
})),
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2339
|
+
catch {
|
|
2340
|
+
journal.record(`model ${status} persistence failed`);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
for (const room of unchanged) {
|
|
2344
|
+
const unheld = unheldModels.get(room.locator);
|
|
2345
|
+
if (!stored.has(room.locator) && unheld !== undefined) {
|
|
2346
|
+
unheldModels.set(room.locator, { ...unheld, status });
|
|
2347
|
+
}
|
|
2348
|
+
list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
|
|
1627
2349
|
}
|
|
2350
|
+
if (unchanged.length > 0)
|
|
2351
|
+
emitRooms();
|
|
2352
|
+
return held.filter((room) => {
|
|
2353
|
+
const record = stored.get(room.locator)?.record;
|
|
2354
|
+
return (!unchanged.includes(room) &&
|
|
2355
|
+
owed(record?.model ?? unheldModels.get(room.locator) ?? NO_MODEL));
|
|
2356
|
+
});
|
|
2357
|
+
});
|
|
2358
|
+
for (const room of declaredAnew) {
|
|
2359
|
+
const model = await modelNow(room.locator);
|
|
2360
|
+
if (owed(model))
|
|
2361
|
+
await retainModel(local, room, { ...model, status });
|
|
1628
2362
|
}
|
|
1629
2363
|
}
|
|
1630
|
-
function
|
|
1631
|
-
|
|
1632
|
-
if (running !== undefined) {
|
|
1633
|
-
running.again = true;
|
|
2364
|
+
function repairLater(locator, when) {
|
|
2365
|
+
if (!socket.isOpen() || repairing.has(locator))
|
|
1634
2366
|
return;
|
|
2367
|
+
void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
|
|
2368
|
+
}
|
|
2369
|
+
async function repairPass(locator, signal) {
|
|
2370
|
+
const room = discovered.get(locator);
|
|
2371
|
+
if (!socket.isOpen() || closed || stopping || discoveryPaused || !stale.has(locator))
|
|
2372
|
+
return 'done';
|
|
2373
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2374
|
+
return 'done';
|
|
2375
|
+
const progress = { complete: true, reachable: true };
|
|
2376
|
+
repairing.add(locator);
|
|
2377
|
+
try {
|
|
2378
|
+
const local = await openLocal();
|
|
2379
|
+
await roomTask(locator, async (live) => {
|
|
2380
|
+
if (!live() || authorization === undefined)
|
|
2381
|
+
return;
|
|
2382
|
+
const withheld = await advance(room, local, authorization, live);
|
|
2383
|
+
if (withheld !== null && live())
|
|
2384
|
+
await withhold(withheld, room, local, progress);
|
|
2385
|
+
});
|
|
1635
2386
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
submitting.delete(locator);
|
|
1661
|
-
}
|
|
1662
|
-
})();
|
|
2387
|
+
catch {
|
|
2388
|
+
stale.add(locator);
|
|
2389
|
+
progress.reachable = false;
|
|
2390
|
+
}
|
|
2391
|
+
finally {
|
|
2392
|
+
repairing.delete(locator);
|
|
2393
|
+
}
|
|
2394
|
+
if (signal.aborted)
|
|
2395
|
+
return 'done';
|
|
2396
|
+
await rest(progress);
|
|
2397
|
+
if (!stale.has(locator)) {
|
|
2398
|
+
if ((await (await openLocal()).order(locator)).length > 0)
|
|
2399
|
+
submitLater(accepting, locator);
|
|
2400
|
+
return 'done';
|
|
2401
|
+
}
|
|
2402
|
+
if (!repairable.has(locator))
|
|
2403
|
+
return 'done';
|
|
2404
|
+
const reset = repairable.get(locator);
|
|
2405
|
+
return reset === undefined ? 'again' : { at: Date.parse(reset) };
|
|
2406
|
+
}
|
|
2407
|
+
function resendLater() {
|
|
2408
|
+
if (!socket.isOpen())
|
|
2409
|
+
return;
|
|
2410
|
+
void recovery.attempt('resend', resendPass, { again: true });
|
|
1663
2411
|
}
|
|
1664
|
-
function
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
2412
|
+
async function resendPass(signal) {
|
|
2413
|
+
if (!socket.isOpen() || closed || stopping || discoveryPaused)
|
|
2414
|
+
return 'done';
|
|
2415
|
+
const reading = { complete: true, reachable: true };
|
|
2416
|
+
try {
|
|
2417
|
+
await conversations.resend(reading);
|
|
2418
|
+
}
|
|
2419
|
+
catch {
|
|
2420
|
+
reading.reachable = false;
|
|
1668
2421
|
}
|
|
2422
|
+
if (signal.aborted)
|
|
2423
|
+
return 'done';
|
|
2424
|
+
return reading.reachable && reading.complete ? 'done' : 'again';
|
|
1669
2425
|
}
|
|
1670
|
-
async function finishOpening(progress) {
|
|
2426
|
+
async function finishOpening(progress, live) {
|
|
1671
2427
|
try {
|
|
1672
|
-
if (
|
|
2428
|
+
if (!live()) {
|
|
1673
2429
|
progress.complete = false;
|
|
1674
2430
|
return;
|
|
1675
2431
|
}
|
|
1676
|
-
submitAll();
|
|
2432
|
+
await submitAll(accepting);
|
|
2433
|
+
for (const locator of stale)
|
|
2434
|
+
if (repairable.has(locator))
|
|
2435
|
+
repairLater(locator, {});
|
|
1677
2436
|
await conversations.reconcile();
|
|
1678
|
-
if (status.providerUnreachable)
|
|
1679
|
-
conversations.reconnected();
|
|
1680
2437
|
const reading = { complete: true, reachable: true };
|
|
1681
2438
|
await conversations.catchUp(reading);
|
|
1682
|
-
if (
|
|
2439
|
+
if (progress.reachable && reading.reachable)
|
|
2440
|
+
await conversations.resend(reading);
|
|
2441
|
+
if (progress.reachable)
|
|
2442
|
+
void recovery.attempt('calls', resendCalls);
|
|
2443
|
+
if (!reading.reachable) {
|
|
1683
2444
|
progress.reachable = false;
|
|
2445
|
+
resendLater();
|
|
2446
|
+
}
|
|
1684
2447
|
}
|
|
1685
2448
|
catch {
|
|
1686
2449
|
progress.complete = false;
|
|
1687
2450
|
}
|
|
1688
2451
|
finally {
|
|
1689
|
-
|
|
1690
|
-
|
|
2452
|
+
if (live()) {
|
|
2453
|
+
complete = progress.complete;
|
|
2454
|
+
await rest(progress, live);
|
|
2455
|
+
}
|
|
1691
2456
|
}
|
|
1692
2457
|
}
|
|
1693
|
-
async function rest(progress) {
|
|
1694
|
-
if (
|
|
2458
|
+
async function rest(progress, live = () => !closed) {
|
|
2459
|
+
if (!live())
|
|
1695
2460
|
return;
|
|
1696
|
-
|
|
1697
|
-
let synced =
|
|
2461
|
+
const settled = () => complete && roomWork === 0 && !socket.naming() && stale.size === 0;
|
|
2462
|
+
let synced = settled();
|
|
2463
|
+
if (synced && !recorded) {
|
|
2464
|
+
const queued = await queuedWork().catch(() => true);
|
|
2465
|
+
if (!live())
|
|
2466
|
+
return;
|
|
2467
|
+
synced = !queued && settled();
|
|
2468
|
+
}
|
|
1698
2469
|
if (synced && !recorded) {
|
|
1699
2470
|
const completedAt = new Date().toISOString();
|
|
1700
2471
|
try {
|
|
1701
|
-
await (await openLocal()).
|
|
2472
|
+
await (await openLocal()).fact('lastCompleted', completedAt);
|
|
2473
|
+
if (!live())
|
|
2474
|
+
return;
|
|
1702
2475
|
recorded = true;
|
|
1703
|
-
lastCompleted = completedAt;
|
|
1704
2476
|
}
|
|
1705
2477
|
catch {
|
|
1706
2478
|
synced = false;
|
|
1707
2479
|
}
|
|
1708
2480
|
}
|
|
1709
2481
|
moveTo({
|
|
1710
|
-
state:
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
...attention(),
|
|
2482
|
+
state: synced && complete && roomWork === 0 && !socket.naming() ? 'synced' : 'syncing',
|
|
2483
|
+
providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
|
|
2484
|
+
resting: true,
|
|
1714
2485
|
});
|
|
1715
2486
|
}
|
|
1716
|
-
function applyRoom(
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
const
|
|
1723
|
-
const held = replicas.get(locator);
|
|
1724
|
-
if (room === undefined || room.replica.admission !== 'admitted')
|
|
1725
|
-
return;
|
|
1726
|
-
if (held !== undefined &&
|
|
1727
|
-
(held.confirmed.record.replica.selection !== room.replica.selection ||
|
|
1728
|
-
options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
|
|
1729
|
-
return;
|
|
1730
|
-
}
|
|
1731
|
-
const during = underway;
|
|
1732
|
-
const progress = during ?? { complete: true, reachable: true };
|
|
1733
|
-
if (during === null)
|
|
1734
|
-
moveTo(transferring(status));
|
|
2487
|
+
function applyRoom(change) {
|
|
2488
|
+
const { locator } = change;
|
|
2489
|
+
if (stopping)
|
|
2490
|
+
return Promise.resolve();
|
|
2491
|
+
return inRoom(locator, async (live) => {
|
|
2492
|
+
await socket.answered(locator);
|
|
2493
|
+
const progress = underway ?? { complete: true, reachable: true };
|
|
1735
2494
|
try {
|
|
1736
2495
|
const local = await openLocal();
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
2496
|
+
for (;;) {
|
|
2497
|
+
if (!live())
|
|
2498
|
+
return null;
|
|
2499
|
+
const room = discovered.get(locator);
|
|
2500
|
+
if (room === undefined || room.replica.admission !== 'admitted')
|
|
2501
|
+
return null;
|
|
2502
|
+
const held = await recordAt(locator);
|
|
2503
|
+
if (held === undefined) {
|
|
2504
|
+
if (change.from !== null)
|
|
2505
|
+
return null;
|
|
2506
|
+
await installCarried(room, local, change, live, progress);
|
|
2507
|
+
return progress;
|
|
2508
|
+
}
|
|
2509
|
+
if (held.replica.selection !== room.replica.selection)
|
|
2510
|
+
return null;
|
|
2511
|
+
const at = held.roomVersion;
|
|
2512
|
+
if (options.adapter.reached(locator, at, change.roomVersion))
|
|
2513
|
+
return progress;
|
|
2514
|
+
if (change.from === null || !options.adapter.reached(locator, at, change.from)) {
|
|
2515
|
+
await name(locator);
|
|
2516
|
+
return progress;
|
|
2517
|
+
}
|
|
2518
|
+
if (underway === null)
|
|
2519
|
+
moveTo(transferring());
|
|
2520
|
+
const writing = () => live() && !full;
|
|
2521
|
+
const refreshed = await store.ahead((hold) => pullRefresh(installer, authorization ?? {}, held, writing, hold, () => change.read()));
|
|
2522
|
+
if (refreshed.kind !== 'advanced')
|
|
2523
|
+
return progress;
|
|
2524
|
+
const landing = {
|
|
2525
|
+
kind: 'refreshed',
|
|
2526
|
+
from: refreshed.from,
|
|
2527
|
+
remodelled: refreshed.remodelled,
|
|
2528
|
+
};
|
|
2529
|
+
if (await publish(installer, local, room, refreshed.record, landing, writing)) {
|
|
2530
|
+
stale.delete(locator);
|
|
2531
|
+
if (!declaredAsHeld(held, room))
|
|
2532
|
+
await retainModel(local, room, null);
|
|
2533
|
+
return progress;
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
1740
2536
|
}
|
|
1741
2537
|
catch {
|
|
2538
|
+
if (!stale.has(locator))
|
|
2539
|
+
void name(locator);
|
|
1742
2540
|
stale.add(locator);
|
|
2541
|
+
return progress;
|
|
1743
2542
|
}
|
|
1744
|
-
|
|
1745
|
-
if (during === null)
|
|
1746
|
-
await rest(progress);
|
|
1747
|
-
}
|
|
1748
|
-
});
|
|
2543
|
+
}).then((progress) => rest(progress), () => undefined);
|
|
1749
2544
|
}
|
|
1750
|
-
function
|
|
1751
|
-
|
|
1752
|
-
if (stream === undefined || withdrawStream !== undefined)
|
|
2545
|
+
async function installCarried(room, local, change, live, progress) {
|
|
2546
|
+
if (authorization === undefined)
|
|
1753
2547
|
return;
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
2548
|
+
if (underway === null)
|
|
2549
|
+
moveTo(transferring());
|
|
2550
|
+
const prior = await recordAt(room.locator);
|
|
2551
|
+
const from = standingAt(store, room.locator, prior);
|
|
2552
|
+
const page = await change.read();
|
|
2553
|
+
const carried = {
|
|
2554
|
+
bootstrap: () => Promise.resolve({
|
|
2555
|
+
kind: 'bootstrap',
|
|
2556
|
+
roomVersion: page.roomVersion,
|
|
2557
|
+
...(page.models === undefined ? {} : { models: page.models }),
|
|
2558
|
+
documents: page.documents,
|
|
2559
|
+
facts: page.facts,
|
|
2560
|
+
revisions: page.revisions,
|
|
2561
|
+
...(page.fileIds === undefined ? {} : { fileIds: page.fileIds }),
|
|
2562
|
+
...(page.created === undefined ? {} : { created: page.created }),
|
|
2563
|
+
changes: page.changes,
|
|
2564
|
+
binaryFiles: page.binaryFiles,
|
|
2565
|
+
...(page.folders === undefined ? {} : { folders: page.folders }),
|
|
2566
|
+
}),
|
|
2567
|
+
reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
|
|
2568
|
+
};
|
|
2569
|
+
const bootstrapped = await pullBootstrap(installer, authorization, room, local, live, prior, carried);
|
|
2570
|
+
if (bootstrapped.kind !== 'staged') {
|
|
2571
|
+
if (live())
|
|
2572
|
+
await withhold(bootstrapped, room, local, progress);
|
|
2573
|
+
return;
|
|
2574
|
+
}
|
|
2575
|
+
await publish(installer, local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
|
|
1768
2576
|
}
|
|
1769
2577
|
function writesHeld() {
|
|
1770
2578
|
return serviceMode === 'write-halted' || haltedRefusal;
|
|
@@ -1779,160 +2587,244 @@ export function composeRuntime(options) {
|
|
|
1779
2587
|
haltedRefusal = false;
|
|
1780
2588
|
if (mode !== serviceMode) {
|
|
1781
2589
|
serviceMode = mode;
|
|
2590
|
+
queueReadiness();
|
|
1782
2591
|
journal.record(`service mode ${mode}`);
|
|
1783
2592
|
for (const listener of serviceModeListeners)
|
|
1784
2593
|
listener(mode);
|
|
1785
2594
|
}
|
|
1786
2595
|
if (releasing)
|
|
1787
|
-
submitAll();
|
|
2596
|
+
submitAll(accepting);
|
|
1788
2597
|
}
|
|
1789
|
-
function
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
});
|
|
2598
|
+
function haltedSince(opens) {
|
|
2599
|
+
if (serviceMode === 'open' && openSignals !== opens)
|
|
2600
|
+
return false;
|
|
2601
|
+
haltedRefusal = true;
|
|
2602
|
+
return true;
|
|
2603
|
+
}
|
|
2604
|
+
function pendingShown() {
|
|
2605
|
+
if (!localSaves)
|
|
2606
|
+
recorded = false;
|
|
2607
|
+
return moveTo(localSaves ? status : { ...status, state: 'pending', resting: false });
|
|
1800
2608
|
}
|
|
1801
2609
|
function scheduleCurrency() {
|
|
1802
|
-
if (currencyQueued || closed || stopping)
|
|
2610
|
+
if (currencyQueued === socket.generation() || closed || stopping)
|
|
1803
2611
|
return;
|
|
1804
|
-
currencyQueued =
|
|
1805
|
-
void task(runCurrency);
|
|
2612
|
+
currencyQueued = socket.generation();
|
|
2613
|
+
void task(CONVERSATION_LANE, runCurrency);
|
|
1806
2614
|
}
|
|
1807
|
-
async function runCurrency() {
|
|
1808
|
-
currencyQueued
|
|
1809
|
-
|
|
2615
|
+
async function runCurrency(live) {
|
|
2616
|
+
if (currencyQueued === socket.generation())
|
|
2617
|
+
currencyQueued = null;
|
|
2618
|
+
if (!live())
|
|
1810
2619
|
return;
|
|
1811
2620
|
await conversations.reconcile();
|
|
1812
2621
|
if (conversations.current()) {
|
|
1813
2622
|
show();
|
|
1814
2623
|
return;
|
|
1815
2624
|
}
|
|
1816
|
-
const
|
|
1817
|
-
|
|
1818
|
-
moveTo(transferring(resting));
|
|
2625
|
+
const progress = { complete: false, reachable: !status.providerUnreachable };
|
|
2626
|
+
moveTo(transferring());
|
|
1819
2627
|
try {
|
|
1820
2628
|
await conversations.catchUp(progress);
|
|
1821
2629
|
}
|
|
1822
2630
|
catch {
|
|
1823
2631
|
progress.reachable = false;
|
|
1824
2632
|
}
|
|
1825
|
-
|
|
1826
|
-
if (!closed) {
|
|
1827
|
-
moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
2633
|
+
await rest(progress, live);
|
|
1830
2634
|
}
|
|
1831
2635
|
function close() {
|
|
1832
2636
|
if (closed)
|
|
1833
2637
|
return;
|
|
1834
2638
|
closed = true;
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
2639
|
+
notifyDiscovered();
|
|
2640
|
+
unreadied();
|
|
2641
|
+
if (probing !== null)
|
|
2642
|
+
clearInterval(probing);
|
|
2643
|
+
connectionLive();
|
|
2644
|
+
lockListeners.clear();
|
|
2645
|
+
readinessWatches.clear();
|
|
2646
|
+
socket.withdraw();
|
|
1839
2647
|
withdrawServiceMode?.();
|
|
1840
2648
|
conversations.close();
|
|
1841
|
-
|
|
1842
|
-
store?.close();
|
|
1843
|
-
void opening?.then((opened) => {
|
|
1844
|
-
opened.close();
|
|
1845
|
-
}, () => { });
|
|
2649
|
+
void recovery.retire();
|
|
1846
2650
|
roomListeners.clear();
|
|
2651
|
+
unlistedRooms.clear();
|
|
1847
2652
|
refusalListeners.clear();
|
|
1848
2653
|
storageLossListeners.clear();
|
|
1849
2654
|
statusListeners.clear();
|
|
1850
2655
|
serviceModeListeners.clear();
|
|
1851
|
-
|
|
2656
|
+
memberListeners.clear();
|
|
2657
|
+
authorityChangeListeners.clear();
|
|
2658
|
+
liveQueries.close();
|
|
2659
|
+
figures.close();
|
|
1852
2660
|
queueSubscriptions.clear();
|
|
1853
|
-
|
|
1854
|
-
presentationListeners.clear();
|
|
2661
|
+
store.close();
|
|
1855
2662
|
}
|
|
1856
2663
|
async function stop() {
|
|
1857
2664
|
stopping = true;
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
await
|
|
1861
|
-
await
|
|
2665
|
+
unreadied();
|
|
2666
|
+
socket.stopReads();
|
|
2667
|
+
await recovery.retire();
|
|
2668
|
+
await Promise.all(working);
|
|
2669
|
+
await store.written();
|
|
1862
2670
|
close();
|
|
1863
2671
|
}
|
|
1864
2672
|
const withdrawServiceMode = options.adapter.serviceMode?.observe(signalled);
|
|
1865
|
-
observeConversations()
|
|
2673
|
+
socket.observeConversations((event) => {
|
|
2674
|
+
if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
|
|
2675
|
+
queueReadiness();
|
|
2676
|
+
conversations.receive(event);
|
|
2677
|
+
if (event.kind !== 'gap' || conversations.current())
|
|
2678
|
+
return;
|
|
2679
|
+
show();
|
|
2680
|
+
scheduleCurrency();
|
|
2681
|
+
});
|
|
1866
2682
|
options.signal?.addEventListener('abort', close, { once: true });
|
|
1867
2683
|
if (options.signal?.aborted)
|
|
1868
2684
|
close();
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
2685
|
+
socket.observe(() => !closed && !stopping, (event) => {
|
|
2686
|
+
if (event.kind === 'opened') {
|
|
2687
|
+
connectionLive();
|
|
2688
|
+
conversations.reopened();
|
|
2689
|
+
lastAnswers.reopened();
|
|
2690
|
+
recovery.resume();
|
|
2691
|
+
}
|
|
2692
|
+
else if (event.kind === 'discovered') {
|
|
2693
|
+
if (event.missed)
|
|
2694
|
+
countAuthorityChange();
|
|
2695
|
+
void sync('opened', event.discovery);
|
|
2696
|
+
}
|
|
2697
|
+
else if (event.kind === 'closed')
|
|
2698
|
+
closedSocket(event.revoked, event.lost, event.opened);
|
|
2699
|
+
else if (event.kind === 'caught-up')
|
|
2700
|
+
void caughtUpRooms(event.expired);
|
|
2701
|
+
else if (event.kind === 'authority-changed') {
|
|
2702
|
+
if (socket.isAnnounced() && socket.isOpen())
|
|
2703
|
+
fenceLive(event.left);
|
|
2704
|
+
void changeAuthority(event);
|
|
2705
|
+
}
|
|
2706
|
+
else
|
|
2707
|
+
void applyRoom(event);
|
|
2708
|
+
});
|
|
2709
|
+
void openLocal()
|
|
2710
|
+
.then(async (local) => {
|
|
2711
|
+
if ((await local.facts()).capabilities.length > 0)
|
|
2712
|
+
readyFromStore();
|
|
2713
|
+
})
|
|
2714
|
+
.catch(() => {
|
|
2715
|
+
readyFromStore();
|
|
1875
2716
|
});
|
|
1876
2717
|
if (options.adapter.roomStream === undefined)
|
|
1877
|
-
void sync('unstreamed');
|
|
1878
|
-
function planned(input, reach) {
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
2718
|
+
void sync('unstreamed', null);
|
|
2719
|
+
async function planned(input, reach) {
|
|
2720
|
+
const plan = planWithin(input, await roomFacts(reach), planReach(reach));
|
|
2721
|
+
const named = plan.paths !== null &&
|
|
2722
|
+
plan.rooms !== null &&
|
|
2723
|
+
plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
|
|
2724
|
+
return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
|
|
2725
|
+
}
|
|
2726
|
+
function watchPlan(plan, deliver, fail) {
|
|
2727
|
+
return watchSeeded(seeding, plan, deliver, (behind, answered) => liveQueries.watch(plan, identityOf(plan), (held) => {
|
|
2728
|
+
const cover = covered(plan, held);
|
|
2729
|
+
awaitPreparation(cover);
|
|
2730
|
+
return behind(cover, held);
|
|
2731
|
+
}, answered, fail));
|
|
2732
|
+
}
|
|
2733
|
+
function watchPlanned(planning, deliver, fail) {
|
|
2734
|
+
let stop = null;
|
|
2735
|
+
let stopped = false;
|
|
2736
|
+
planning.then((plan) => {
|
|
2737
|
+
if (!stopped)
|
|
2738
|
+
stop = watchPlan(plan, deliver, fail);
|
|
2739
|
+
}, (error) => {
|
|
2740
|
+
if (!stopped)
|
|
2741
|
+
fail(error);
|
|
2742
|
+
});
|
|
2743
|
+
return () => {
|
|
2744
|
+
stopped = true;
|
|
2745
|
+
stop?.();
|
|
2746
|
+
};
|
|
1883
2747
|
}
|
|
1884
2748
|
async function searched(input, reach) {
|
|
1885
|
-
const
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
2749
|
+
const asked = searchPlanOf(input);
|
|
2750
|
+
await ready;
|
|
2751
|
+
const search = options.adapter.search;
|
|
2752
|
+
if (search === undefined) {
|
|
2753
|
+
return {
|
|
2754
|
+
kind: 'undeclared',
|
|
2755
|
+
code: 'action-undeclared',
|
|
2756
|
+
fields: { name: 'search' },
|
|
2757
|
+
retryable: false,
|
|
2758
|
+
};
|
|
2759
|
+
}
|
|
2760
|
+
if (closed || refusal !== null || (await connectionNow()) !== 'connected')
|
|
2761
|
+
return OFFLINE;
|
|
2762
|
+
const reached = reach === null ? null : new Set(reach.rooms);
|
|
2763
|
+
const named = asked.rooms === null
|
|
2764
|
+
? reached === null
|
|
2765
|
+
? null
|
|
2766
|
+
: [...reached]
|
|
2767
|
+
: asked.rooms.filter((room) => reached === null || reached.has(room));
|
|
2768
|
+
const held = named === null ? null : await storedRooms(named);
|
|
2769
|
+
const rooms = named === null ? null : named.filter((room) => held.has(room)).sort();
|
|
2770
|
+
if (rooms !== null && rooms.length === 0)
|
|
2771
|
+
return { kind: 'found', documents: [] };
|
|
2772
|
+
try {
|
|
2773
|
+
await ensureAuthorization();
|
|
2774
|
+
}
|
|
2775
|
+
catch {
|
|
2776
|
+
return UNCERTAIN;
|
|
2777
|
+
}
|
|
2778
|
+
if (closed || authorization === undefined || refusal !== null)
|
|
2779
|
+
return UNCERTAIN;
|
|
2780
|
+
journal.record('transfer search');
|
|
2781
|
+
try {
|
|
2782
|
+
const result = await search(authorization, { text: asked.text, rooms });
|
|
2783
|
+
if (result.kind === 'revoked-authorization')
|
|
2784
|
+
await refuse('revoked-authorization');
|
|
2785
|
+
journal.record(`outcome search ${result.kind}`);
|
|
2786
|
+
if (result.kind === 'rejected')
|
|
2787
|
+
return result;
|
|
2788
|
+
if (result.kind !== 'found')
|
|
2789
|
+
return failure(result);
|
|
2790
|
+
const kept = rooms === null ? null : new Set(rooms);
|
|
2791
|
+
return {
|
|
2792
|
+
kind: 'found',
|
|
2793
|
+
documents: result.documents.filter((hit) => kept === null || kept.has(hit.room)),
|
|
1907
2794
|
};
|
|
1908
2795
|
}
|
|
2796
|
+
catch {
|
|
2797
|
+
journal.record('outcome search transport-uncertainty');
|
|
2798
|
+
return UNCERTAIN;
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
const watch = (topic, input, deliver, fail) => {
|
|
2802
|
+
if (topic === 'room-drafts')
|
|
2803
|
+
return watchRoomDrafts(drafting, deliver);
|
|
1909
2804
|
if (topic === 'conversation') {
|
|
1910
2805
|
return conversations.watch(input[0], input[1], deliver);
|
|
1911
2806
|
}
|
|
1912
|
-
if (topic === '
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
listener.ready = true;
|
|
1925
|
-
listener.notify(presentationOf(listener.app));
|
|
1926
|
-
}, () => { });
|
|
1927
|
-
return () => {
|
|
1928
|
-
presentationListeners.delete(listener);
|
|
1929
|
-
};
|
|
2807
|
+
if (topic === 'conversations')
|
|
2808
|
+
return conversations.watchList(deliver);
|
|
2809
|
+
if (topic === 'agents')
|
|
2810
|
+
return conversations.watchAgents(deliver);
|
|
2811
|
+
if (topic === 'figures')
|
|
2812
|
+
return figures.watch(deliver, fail);
|
|
2813
|
+
if (topic === 'read')
|
|
2814
|
+
return lastAnswers.watch(input[0], input[1], deliver);
|
|
2815
|
+
if (topic === 'presentation')
|
|
2816
|
+
return watchPresentation(drafting, input[0], deliver);
|
|
2817
|
+
if (topic === 'readiness') {
|
|
2818
|
+
return watchReadiness({ target: '', code: 'stored' }, null, deliver);
|
|
1930
2819
|
}
|
|
1931
2820
|
if (topic === 'rooms') {
|
|
1932
|
-
|
|
1933
|
-
|
|
2821
|
+
unlistedRooms.add(deliver);
|
|
2822
|
+
emitKnownRooms();
|
|
1934
2823
|
return () => {
|
|
2824
|
+
unlistedRooms.delete(deliver);
|
|
1935
2825
|
roomListeners.delete(deliver);
|
|
2826
|
+
if (roomListeners.size === 0)
|
|
2827
|
+
roomsShown = null;
|
|
1936
2828
|
};
|
|
1937
2829
|
}
|
|
1938
2830
|
if (topic === 'connection-refusal') {
|
|
@@ -1963,153 +2855,162 @@ export function composeRuntime(options) {
|
|
|
1963
2855
|
serviceModeListeners.delete(deliver);
|
|
1964
2856
|
};
|
|
1965
2857
|
}
|
|
2858
|
+
if (topic === 'member') {
|
|
2859
|
+
memberListeners.add(deliver);
|
|
2860
|
+
void openLocal()
|
|
2861
|
+
.then((local) => local.facts())
|
|
2862
|
+
.then(({ member }) => {
|
|
2863
|
+
if (memberListeners.has(deliver))
|
|
2864
|
+
deliver(member);
|
|
2865
|
+
}, () => { });
|
|
2866
|
+
return () => {
|
|
2867
|
+
memberListeners.delete(deliver);
|
|
2868
|
+
};
|
|
2869
|
+
}
|
|
2870
|
+
if (topic === 'authority-changes') {
|
|
2871
|
+
authorityChangeListeners.add(deliver);
|
|
2872
|
+
deliver(authorityChanges);
|
|
2873
|
+
return () => {
|
|
2874
|
+
authorityChangeListeners.delete(deliver);
|
|
2875
|
+
};
|
|
2876
|
+
}
|
|
1966
2877
|
if (topic === 'queue') {
|
|
1967
2878
|
const queue = { locator: input[0], notify: deliver };
|
|
1968
2879
|
queueSubscriptions.add(queue);
|
|
1969
|
-
|
|
2880
|
+
void publication(async () => {
|
|
2881
|
+
if (!queueSubscriptions.has(queue))
|
|
2882
|
+
return;
|
|
2883
|
+
const entries = await entriesOf(queue.locator);
|
|
2884
|
+
if (queueSubscriptions.has(queue))
|
|
2885
|
+
queue.notify(entries);
|
|
2886
|
+
});
|
|
1970
2887
|
return () => {
|
|
1971
2888
|
queueSubscriptions.delete(queue);
|
|
1972
2889
|
};
|
|
1973
2890
|
}
|
|
1974
|
-
return
|
|
2891
|
+
return watchPlanned(planned(input[0], null), deliver, fail);
|
|
1975
2892
|
};
|
|
1976
2893
|
const md = {
|
|
1977
2894
|
loaded() {
|
|
1978
2895
|
return openLocal().then(() => undefined);
|
|
1979
2896
|
},
|
|
1980
2897
|
rooms() {
|
|
1981
|
-
|
|
2898
|
+
if (closed)
|
|
2899
|
+
return Promise.resolve([]);
|
|
2900
|
+
return new Promise((settle) => {
|
|
2901
|
+
const answer = (value) => {
|
|
2902
|
+
unlistedRooms.delete(answer);
|
|
2903
|
+
roomListeners.delete(answer);
|
|
2904
|
+
settle(value);
|
|
2905
|
+
};
|
|
2906
|
+
unlistedRooms.add(answer);
|
|
2907
|
+
emitKnownRooms();
|
|
2908
|
+
});
|
|
1982
2909
|
},
|
|
1983
|
-
query: ((input) =>
|
|
2910
|
+
query: ((input) => planned(input, null).then(rowsQuery)),
|
|
1984
2911
|
search: (input) => searched(input, null),
|
|
1985
|
-
types(locator) {
|
|
2912
|
+
async types(locator) {
|
|
2913
|
+
const held = (await storedRooms([locator])).get(locator);
|
|
2914
|
+
const room = isFenced(locator) ? undefined : declarationOf(locator, held)?.room;
|
|
1986
2915
|
return {
|
|
1987
|
-
collection: modelTypes(
|
|
1988
|
-
typedSelection:
|
|
1989
|
-
rooms.get(locator)?.typedSelection === null
|
|
1990
|
-
? null
|
|
1991
|
-
: modelTypes(modelOf(locator, 'typed-selection')),
|
|
2916
|
+
collection: modelTypes(isFenced(locator) ? NO_MODEL : modelIn(locator, held)),
|
|
2917
|
+
typedSelection: (room?.typedSelection ?? null) === null ? null : modelTypes(builtIn),
|
|
1992
2918
|
};
|
|
1993
2919
|
},
|
|
1994
|
-
roomDraft:
|
|
1995
|
-
list() {
|
|
1996
|
-
return exclusive(async () => {
|
|
1997
|
-
await openLocal();
|
|
1998
|
-
return snapshotRoomDrafts();
|
|
1999
|
-
}).then((listed) => {
|
|
2000
|
-
publishRoomDrafts(true);
|
|
2001
|
-
return listed;
|
|
2002
|
-
});
|
|
2003
|
-
},
|
|
2004
|
-
read(room, path) {
|
|
2005
|
-
const record = roomDrafts.get(room)?.get(path);
|
|
2006
|
-
return record === undefined ? null : roomDraftOf(record);
|
|
2007
|
-
},
|
|
2008
|
-
write(room, path, revision, input) {
|
|
2009
|
-
return exclusive(async () => {
|
|
2010
|
-
const parts = readParts(input);
|
|
2011
|
-
if (path === '' || normalize(path) !== path)
|
|
2012
|
-
throw new Error('room-draft-malformed');
|
|
2013
|
-
const local = await openLocal();
|
|
2014
|
-
const current = roomDrafts.get(room)?.get(path);
|
|
2015
|
-
if ((current?.revision ?? null) !== revision) {
|
|
2016
|
-
return {
|
|
2017
|
-
outcome: 'stale',
|
|
2018
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
2019
|
-
};
|
|
2020
|
-
}
|
|
2021
|
-
const record = {
|
|
2022
|
-
locator: room,
|
|
2023
|
-
path,
|
|
2024
|
-
revision: roomDraftRevision(),
|
|
2025
|
-
parts: held(current?.parts, parts),
|
|
2026
|
-
returned: current?.returned ?? null,
|
|
2027
|
-
};
|
|
2028
|
-
await local.keepRoomDraft(record);
|
|
2029
|
-
rememberRoomDraft(record);
|
|
2030
|
-
publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
|
|
2031
|
-
return { outcome: 'written', roomDraft: roomDraftOf(record) };
|
|
2032
|
-
});
|
|
2033
|
-
},
|
|
2034
|
-
discard(room, path, revision) {
|
|
2035
|
-
return exclusive(async () => {
|
|
2036
|
-
const local = await openLocal();
|
|
2037
|
-
const current = roomDrafts.get(room)?.get(path);
|
|
2038
|
-
if (current === undefined || current.revision !== revision) {
|
|
2039
|
-
return {
|
|
2040
|
-
outcome: 'stale',
|
|
2041
|
-
current: current === undefined ? null : roomDraftOf(current),
|
|
2042
|
-
};
|
|
2043
|
-
}
|
|
2044
|
-
await local.discardRoomDraft(room, path);
|
|
2045
|
-
forgetRoomDraft(room, path);
|
|
2046
|
-
publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
|
|
2047
|
-
moveTo({ ...status, ...attention() });
|
|
2048
|
-
return { outcome: 'written', roomDraft: null };
|
|
2049
|
-
});
|
|
2050
|
-
},
|
|
2051
|
-
},
|
|
2920
|
+
roomDraft: roomDraftSurface(drafting),
|
|
2052
2921
|
conversation: conversations.surface,
|
|
2053
2922
|
chatDraft: conversations.chatDraft,
|
|
2054
|
-
presentation:
|
|
2055
|
-
read(app) {
|
|
2056
|
-
return exclusive(async () => {
|
|
2057
|
-
await openLocal();
|
|
2058
|
-
return presentationOf(app);
|
|
2059
|
-
});
|
|
2060
|
-
},
|
|
2061
|
-
write(app, value) {
|
|
2062
|
-
return exclusive(async () => {
|
|
2063
|
-
const local = await openLocal();
|
|
2064
|
-
const stored = structuredClone(value);
|
|
2065
|
-
await local.present({ app, value: stored });
|
|
2066
|
-
presentations.set(app, stored);
|
|
2067
|
-
publishChangeSet(changeSet('', null, null, { presentation: app }));
|
|
2068
|
-
});
|
|
2069
|
-
},
|
|
2070
|
-
},
|
|
2923
|
+
presentation: presentationSurface(drafting),
|
|
2071
2924
|
save(room, named, writtenBy) {
|
|
2072
|
-
return exclusive(() => saveRoomDrafts(room, named, writtenBy));
|
|
2925
|
+
return exclusive(() => saveRoomDrafts(saver, room, named, writtenBy));
|
|
2926
|
+
},
|
|
2927
|
+
saveParts(room, parts, writtenBy) {
|
|
2928
|
+
return exclusive(() => saveParts(saver, room, parts, writtenBy));
|
|
2073
2929
|
},
|
|
2074
2930
|
history(locator, path, query) {
|
|
2075
2931
|
return readHistory(locator, path, query);
|
|
2076
2932
|
},
|
|
2077
|
-
|
|
2078
|
-
return
|
|
2933
|
+
resolve(room, path, destination) {
|
|
2934
|
+
return resolveReference(room, path, destination);
|
|
2935
|
+
},
|
|
2936
|
+
binaryFile: binaryFileSurface(saver),
|
|
2937
|
+
async call(name, input) {
|
|
2938
|
+
await ready;
|
|
2939
|
+
const lockedNow = await lockedRooms();
|
|
2940
|
+
const room = (await calledRooms(name, input)).find((one) => lockedNow.includes(one));
|
|
2941
|
+
return room === undefined ? recoveredAction(name, input) : lockedRoom(room);
|
|
2079
2942
|
},
|
|
2080
|
-
reached(locator, roomVersion, path) {
|
|
2943
|
+
async reached(locator, roomVersion, path) {
|
|
2081
2944
|
return path === undefined
|
|
2082
|
-
? reachedBy(locator, roomVersion)
|
|
2083
|
-
: writtenAfter(locator, roomVersion, path);
|
|
2945
|
+
? reachedBy(installer, locator, roomVersion)
|
|
2946
|
+
: writtenAfter(installer, locator, roomVersion, path);
|
|
2084
2947
|
},
|
|
2085
2948
|
log() {
|
|
2086
2949
|
return journal.export();
|
|
2087
2950
|
},
|
|
2951
|
+
cleanUp,
|
|
2952
|
+
unsent: () => database.ask({ kind: 'authored.unsent' }),
|
|
2953
|
+
reset: () => options.shell.reset(),
|
|
2954
|
+
async readAgain() {
|
|
2955
|
+
lastAnswers.again();
|
|
2956
|
+
},
|
|
2088
2957
|
persistence() {
|
|
2089
|
-
return
|
|
2958
|
+
return persistence === 'granted';
|
|
2959
|
+
},
|
|
2960
|
+
async retry() {
|
|
2961
|
+
journal.record('retry');
|
|
2962
|
+
unpreparable.clear();
|
|
2963
|
+
queueReadiness();
|
|
2964
|
+
if (full)
|
|
2965
|
+
await probed();
|
|
2090
2966
|
},
|
|
2091
2967
|
observe: observing(watch),
|
|
2092
2968
|
async reauthorize() {
|
|
2093
2969
|
if (refusal === null || !refusal.reauthorizationOffered)
|
|
2094
2970
|
return;
|
|
2095
2971
|
journal.record('connection reauthorize');
|
|
2096
|
-
await
|
|
2097
|
-
|
|
2972
|
+
await Promise.all([...lanes.values()]
|
|
2973
|
+
.filter((held) => held.generation === socket.generation())
|
|
2974
|
+
.map((held) => held.chain));
|
|
2975
|
+
await store.written();
|
|
2098
2976
|
authorization = undefined;
|
|
2099
2977
|
discoveryPaused = false;
|
|
2100
2978
|
if (options.adapter.roomStream === undefined)
|
|
2101
|
-
await sync('unstreamed');
|
|
2979
|
+
await sync('unstreamed', null);
|
|
2102
2980
|
else
|
|
2103
|
-
await task(ensureAuthorization);
|
|
2981
|
+
await task(OPENING_LANE, ensureAuthorization);
|
|
2104
2982
|
},
|
|
2105
2983
|
};
|
|
2106
2984
|
const reachable = {
|
|
2107
2985
|
md,
|
|
2108
2986
|
watch,
|
|
2109
2987
|
plan: planned,
|
|
2110
|
-
query: (plan) =>
|
|
2988
|
+
query: (plan) => rowsQuery(plan),
|
|
2111
2989
|
watchPlan,
|
|
2112
2990
|
search: searched,
|
|
2991
|
+
watchReadiness,
|
|
2992
|
+
locked,
|
|
2993
|
+
lockedRooms,
|
|
2994
|
+
watchLocks,
|
|
2995
|
+
located,
|
|
2996
|
+
room: async (locator) => shownRooms(await listingNow()).find((one) => one.locator === locator),
|
|
2997
|
+
};
|
|
2998
|
+
return {
|
|
2999
|
+
md,
|
|
3000
|
+
seed: (entry) => keepEntrySeed(store, entry),
|
|
3001
|
+
stop,
|
|
3002
|
+
watch,
|
|
3003
|
+
persisted(answer) {
|
|
3004
|
+
if (answer === persistence)
|
|
3005
|
+
return;
|
|
3006
|
+
persistence = answer;
|
|
3007
|
+
journal.record(`persistence ${answer}`);
|
|
3008
|
+
queueReadiness();
|
|
3009
|
+
},
|
|
3010
|
+
within: (reach, port) => {
|
|
3011
|
+
if (full)
|
|
3012
|
+
void probed();
|
|
3013
|
+
return narrowed(reachable, reach, port);
|
|
3014
|
+
},
|
|
2113
3015
|
};
|
|
2114
|
-
return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
|
|
2115
3016
|
}
|