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